forms.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. from django import forms
  2. from django.contrib.auth.models import User
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.utils.safestring import mark_safe
  5. from django.utils.translation import gettext as _
  6. from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
  7. from tenancy.models import Tenant, TenantGroup
  8. from utilities.forms import (
  9. add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
  10. CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
  11. BOOLEAN_WITH_BLANK_CHOICES,
  12. )
  13. from virtualization.models import Cluster, ClusterGroup
  14. from .choices import *
  15. from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
  16. #
  17. # Custom fields
  18. #
  19. class CustomFieldForm(forms.Form):
  20. """
  21. Extend Form to include custom field support.
  22. """
  23. model = None
  24. def __init__(self, *args, **kwargs):
  25. if self.model is None:
  26. raise NotImplementedError("CustomFieldForm must specify a model class.")
  27. self.custom_fields = []
  28. super().__init__(*args, **kwargs)
  29. # Append relevant custom fields to the form instance
  30. obj_type = ContentType.objects.get_for_model(self.model)
  31. for cf in CustomField.objects.filter(content_types=obj_type):
  32. field_name = 'cf_{}'.format(cf.name)
  33. self.fields[field_name] = cf.to_form_field()
  34. # Annotate the field in the list of CustomField form fields
  35. self.custom_fields.append(field_name)
  36. class CustomFieldModelForm(forms.ModelForm):
  37. """
  38. Extend ModelForm to include custom field support.
  39. """
  40. def __init__(self, *args, **kwargs):
  41. self.obj_type = ContentType.objects.get_for_model(self._meta.model)
  42. self.custom_fields = []
  43. super().__init__(*args, **kwargs)
  44. self._append_customfield_fields()
  45. def _append_customfield_fields(self):
  46. """
  47. Append form fields for all CustomFields assigned to this model.
  48. """
  49. # Append form fields; assign initial values if modifying and existing object
  50. for cf in CustomField.objects.filter(content_types=self.obj_type):
  51. field_name = 'cf_{}'.format(cf.name)
  52. if self.instance.pk:
  53. self.fields[field_name] = cf.to_form_field(set_initial=False)
  54. self.fields[field_name].initial = self.instance.custom_field_data.get(cf.name)
  55. else:
  56. self.fields[field_name] = cf.to_form_field()
  57. # Annotate the field in the list of CustomField form fields
  58. self.custom_fields.append(field_name)
  59. def clean(self):
  60. # Save custom field data on instance
  61. for cf_name in self.custom_fields:
  62. self.instance.custom_field_data[cf_name[3:]] = self.cleaned_data.get(cf_name)
  63. return super().clean()
  64. class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
  65. def _append_customfield_fields(self):
  66. # Append form fields
  67. for cf in CustomField.objects.filter(content_types=self.obj_type):
  68. field_name = 'cf_{}'.format(cf.name)
  69. self.fields[field_name] = cf.to_form_field(for_csv_import=True)
  70. # Annotate the field in the list of CustomField form fields
  71. self.custom_fields.append(field_name)
  72. class CustomFieldBulkEditForm(BulkEditForm):
  73. def __init__(self, *args, **kwargs):
  74. super().__init__(*args, **kwargs)
  75. self.custom_fields = []
  76. self.obj_type = ContentType.objects.get_for_model(self.model)
  77. # Add all applicable CustomFields to the form
  78. custom_fields = CustomField.objects.filter(content_types=self.obj_type)
  79. for cf in custom_fields:
  80. # Annotate non-required custom fields as nullable
  81. if not cf.required:
  82. self.nullable_fields.append(cf.name)
  83. self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False)
  84. # Annotate this as a custom field
  85. self.custom_fields.append(cf.name)
  86. class CustomFieldFilterForm(forms.Form):
  87. def __init__(self, *args, **kwargs):
  88. self.obj_type = ContentType.objects.get_for_model(self.model)
  89. super().__init__(*args, **kwargs)
  90. # Add all applicable CustomFields to the form
  91. custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude(
  92. filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
  93. )
  94. for cf in custom_fields:
  95. field_name = 'cf_{}'.format(cf.name)
  96. self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False)
  97. #
  98. # Tags
  99. #
  100. class TagForm(BootstrapMixin, forms.ModelForm):
  101. slug = SlugField()
  102. class Meta:
  103. model = Tag
  104. fields = [
  105. 'name', 'slug', 'color', 'description'
  106. ]
  107. fieldsets = (
  108. ('Tag', ('name', 'slug', 'color', 'description')),
  109. )
  110. class TagCSVForm(CSVModelForm):
  111. slug = SlugField()
  112. class Meta:
  113. model = Tag
  114. fields = Tag.csv_headers
  115. help_texts = {
  116. 'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
  117. }
  118. class AddRemoveTagsForm(forms.Form):
  119. def __init__(self, *args, **kwargs):
  120. super().__init__(*args, **kwargs)
  121. # Add add/remove tags fields
  122. self.fields['add_tags'] = DynamicModelMultipleChoiceField(
  123. queryset=Tag.objects.all(),
  124. required=False
  125. )
  126. self.fields['remove_tags'] = DynamicModelMultipleChoiceField(
  127. queryset=Tag.objects.all(),
  128. required=False
  129. )
  130. class TagFilterForm(BootstrapMixin, forms.Form):
  131. model = Tag
  132. q = forms.CharField(
  133. required=False,
  134. label=_('Search')
  135. )
  136. class TagBulkEditForm(BootstrapMixin, BulkEditForm):
  137. pk = forms.ModelMultipleChoiceField(
  138. queryset=Tag.objects.all(),
  139. widget=forms.MultipleHiddenInput
  140. )
  141. color = forms.CharField(
  142. max_length=6,
  143. required=False,
  144. widget=ColorSelect()
  145. )
  146. description = forms.CharField(
  147. max_length=200,
  148. required=False
  149. )
  150. class Meta:
  151. nullable_fields = ['description']
  152. #
  153. # Config contexts
  154. #
  155. class ConfigContextForm(BootstrapMixin, forms.ModelForm):
  156. regions = DynamicModelMultipleChoiceField(
  157. queryset=Region.objects.all(),
  158. required=False
  159. )
  160. site_groups = DynamicModelMultipleChoiceField(
  161. queryset=SiteGroup.objects.all(),
  162. required=False
  163. )
  164. sites = DynamicModelMultipleChoiceField(
  165. queryset=Site.objects.all(),
  166. required=False
  167. )
  168. roles = DynamicModelMultipleChoiceField(
  169. queryset=DeviceRole.objects.all(),
  170. required=False
  171. )
  172. platforms = DynamicModelMultipleChoiceField(
  173. queryset=Platform.objects.all(),
  174. required=False
  175. )
  176. cluster_groups = DynamicModelMultipleChoiceField(
  177. queryset=ClusterGroup.objects.all(),
  178. required=False
  179. )
  180. clusters = DynamicModelMultipleChoiceField(
  181. queryset=Cluster.objects.all(),
  182. required=False
  183. )
  184. tenant_groups = DynamicModelMultipleChoiceField(
  185. queryset=TenantGroup.objects.all(),
  186. required=False
  187. )
  188. tenants = DynamicModelMultipleChoiceField(
  189. queryset=Tenant.objects.all(),
  190. required=False
  191. )
  192. tags = DynamicModelMultipleChoiceField(
  193. queryset=Tag.objects.all(),
  194. required=False
  195. )
  196. data = JSONField(
  197. label=''
  198. )
  199. class Meta:
  200. model = ConfigContext
  201. fields = (
  202. 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'platforms',
  203. 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
  204. )
  205. class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
  206. pk = forms.ModelMultipleChoiceField(
  207. queryset=ConfigContext.objects.all(),
  208. widget=forms.MultipleHiddenInput
  209. )
  210. weight = forms.IntegerField(
  211. required=False,
  212. min_value=0
  213. )
  214. is_active = forms.NullBooleanField(
  215. required=False,
  216. widget=BulkEditNullBooleanSelect()
  217. )
  218. description = forms.CharField(
  219. required=False,
  220. max_length=100
  221. )
  222. class Meta:
  223. nullable_fields = [
  224. 'description',
  225. ]
  226. class ConfigContextFilterForm(BootstrapMixin, forms.Form):
  227. field_order = [
  228. 'q', 'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id', 'cluster_id',
  229. 'tenant_group_id', 'tenant_id',
  230. ]
  231. q = forms.CharField(
  232. required=False,
  233. label=_('Search')
  234. )
  235. region_id = DynamicModelMultipleChoiceField(
  236. queryset=Region.objects.all(),
  237. required=False,
  238. label=_('Regions')
  239. )
  240. site_group_id = DynamicModelMultipleChoiceField(
  241. queryset=SiteGroup.objects.all(),
  242. required=False,
  243. label=_('Site groups')
  244. )
  245. site_id = DynamicModelMultipleChoiceField(
  246. queryset=Site.objects.all(),
  247. required=False,
  248. label=_('Sites')
  249. )
  250. role_id = DynamicModelMultipleChoiceField(
  251. queryset=DeviceRole.objects.all(),
  252. required=False,
  253. label=_('Roles')
  254. )
  255. platform_id = DynamicModelMultipleChoiceField(
  256. queryset=Platform.objects.all(),
  257. required=False,
  258. label=_('Platforms')
  259. )
  260. cluster_group_id = DynamicModelMultipleChoiceField(
  261. queryset=ClusterGroup.objects.all(),
  262. required=False,
  263. label=_('Cluster groups')
  264. )
  265. cluster_id = DynamicModelMultipleChoiceField(
  266. queryset=Cluster.objects.all(),
  267. required=False,
  268. label=_('Clusters')
  269. )
  270. tenant_group_id = DynamicModelMultipleChoiceField(
  271. queryset=TenantGroup.objects.all(),
  272. required=False,
  273. label=_('Tenant groups')
  274. )
  275. tenant_id = DynamicModelMultipleChoiceField(
  276. queryset=Tenant.objects.all(),
  277. required=False,
  278. label=_('Tenant')
  279. )
  280. tag = DynamicModelMultipleChoiceField(
  281. queryset=Tag.objects.all(),
  282. to_field_name='slug',
  283. required=False,
  284. label=_('Tags')
  285. )
  286. #
  287. # Filter form for local config context data
  288. #
  289. class LocalConfigContextFilterForm(forms.Form):
  290. local_context_data = forms.NullBooleanField(
  291. required=False,
  292. label=_('Has local config context data'),
  293. widget=StaticSelect2(
  294. choices=BOOLEAN_WITH_BLANK_CHOICES
  295. )
  296. )
  297. #
  298. # Image attachments
  299. #
  300. class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
  301. class Meta:
  302. model = ImageAttachment
  303. fields = [
  304. 'name', 'image',
  305. ]
  306. #
  307. # Journal entries
  308. #
  309. class JournalEntryForm(BootstrapMixin, forms.ModelForm):
  310. class Meta:
  311. model = JournalEntry
  312. fields = ['assigned_object_type', 'assigned_object_id', 'comments']
  313. widgets = {
  314. 'assigned_object_type': forms.HiddenInput,
  315. 'assigned_object_id': forms.HiddenInput,
  316. }
  317. #
  318. # Change logging
  319. #
  320. class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
  321. model = ObjectChange
  322. q = forms.CharField(
  323. required=False,
  324. label=_('Search')
  325. )
  326. time_after = forms.DateTimeField(
  327. required=False,
  328. label=_('After'),
  329. widget=DateTimePicker()
  330. )
  331. time_before = forms.DateTimeField(
  332. required=False,
  333. label=_('Before'),
  334. widget=DateTimePicker()
  335. )
  336. action = forms.ChoiceField(
  337. choices=add_blank_choice(ObjectChangeActionChoices),
  338. required=False,
  339. widget=StaticSelect2()
  340. )
  341. user_id = DynamicModelMultipleChoiceField(
  342. queryset=User.objects.all(),
  343. required=False,
  344. label=_('User'),
  345. widget=APISelectMultiple(
  346. api_url='/api/users/users/',
  347. )
  348. )
  349. changed_object_type_id = DynamicModelMultipleChoiceField(
  350. queryset=ContentType.objects.all(),
  351. required=False,
  352. label=_('Object Type'),
  353. widget=APISelectMultiple(
  354. api_url='/api/extras/content-types/',
  355. )
  356. )
  357. #
  358. # Scripts
  359. #
  360. class ScriptForm(BootstrapMixin, forms.Form):
  361. _commit = forms.BooleanField(
  362. required=False,
  363. initial=True,
  364. label="Commit changes",
  365. help_text="Commit changes to the database (uncheck for a dry-run)"
  366. )
  367. def __init__(self, *args, **kwargs):
  368. super().__init__(*args, **kwargs)
  369. # Move _commit to the end of the form
  370. commit = self.fields.pop('_commit')
  371. self.fields['_commit'] = commit
  372. @property
  373. def requires_input(self):
  374. """
  375. A boolean indicating whether the form requires user input (ignore the _commit field).
  376. """
  377. return bool(len(self.fields) > 1)