forms.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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 dcim.models import DeviceRole, Platform, Region, Site
  6. from tenancy.models import Tenant, TenantGroup
  7. from utilities.forms import (
  8. add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
  9. ContentTypeSelect, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
  10. StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
  11. )
  12. from virtualization.models import Cluster, ClusterGroup
  13. from .choices import *
  14. from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
  15. #
  16. # Custom fields
  17. #
  18. class CustomFieldModelForm(forms.ModelForm):
  19. def __init__(self, *args, **kwargs):
  20. self.obj_type = ContentType.objects.get_for_model(self._meta.model)
  21. self.custom_fields = []
  22. self.custom_field_values = {}
  23. super().__init__(*args, **kwargs)
  24. if self.instance._cf is None:
  25. self.instance._cf = {}
  26. self._append_customfield_fields()
  27. def _append_customfield_fields(self):
  28. """
  29. Append form fields for all CustomFields assigned to this model.
  30. """
  31. # Retrieve initial CustomField values for the instance
  32. if self.instance.pk:
  33. for cfv in CustomFieldValue.objects.filter(
  34. obj_type=self.obj_type,
  35. obj_id=self.instance.pk
  36. ).prefetch_related('field'):
  37. self.custom_field_values[cfv.field.name] = cfv.serialized_value
  38. # Append form fields; assign initial values if modifying and existing object
  39. for cf in CustomField.objects.filter(obj_type=self.obj_type):
  40. field_name = 'cf_{}'.format(cf.name)
  41. if self.instance.pk:
  42. self.fields[field_name] = cf.to_form_field(set_initial=False)
  43. value = self.custom_field_values.get(cf.name)
  44. self.fields[field_name].initial = value
  45. self.instance._cf[cf.name] = value
  46. else:
  47. self.fields[field_name] = cf.to_form_field()
  48. self.instance._cf[cf.name] = self.fields[field_name].initial
  49. # Annotate the field in the list of CustomField form fields
  50. self.custom_fields.append(field_name)
  51. def _save_custom_fields(self):
  52. for field_name in self.custom_fields:
  53. try:
  54. cfv = CustomFieldValue.objects.prefetch_related('field').get(
  55. field=self.fields[field_name].model,
  56. obj_type=self.obj_type,
  57. obj_id=self.instance.pk
  58. )
  59. except CustomFieldValue.DoesNotExist:
  60. # Skip this field if none exists already and its value is empty
  61. if self.cleaned_data[field_name] in [None, '']:
  62. continue
  63. cfv = CustomFieldValue(
  64. field=self.fields[field_name].model,
  65. obj_type=self.obj_type,
  66. obj_id=self.instance.pk
  67. )
  68. cfv.value = self.cleaned_data[field_name]
  69. cfv.save()
  70. def save(self, commit=True):
  71. # Cache custom field values on object prior to save to ensure change logging
  72. for cf_name in self.custom_fields:
  73. self.instance._cf[cf_name[3:]] = self.cleaned_data.get(cf_name)
  74. obj = super().save(commit)
  75. # Handle custom fields the same way we do M2M fields
  76. if commit:
  77. self._save_custom_fields()
  78. else:
  79. obj.save_custom_fields = self._save_custom_fields
  80. return obj
  81. class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
  82. def _append_customfield_fields(self):
  83. # Append form fields
  84. for cf in CustomField.objects.filter(obj_type=self.obj_type):
  85. field_name = 'cf_{}'.format(cf.name)
  86. self.fields[field_name] = cf.to_form_field(for_csv_import=True)
  87. # Annotate the field in the list of CustomField form fields
  88. self.custom_fields.append(field_name)
  89. class CustomFieldBulkEditForm(BulkEditForm):
  90. def __init__(self, *args, **kwargs):
  91. super().__init__(*args, **kwargs)
  92. self.custom_fields = []
  93. self.obj_type = ContentType.objects.get_for_model(self.model)
  94. # Add all applicable CustomFields to the form
  95. custom_fields = CustomField.objects.filter(obj_type=self.obj_type)
  96. for cf in custom_fields:
  97. # Annotate non-required custom fields as nullable
  98. if not cf.required:
  99. self.nullable_fields.append(cf.name)
  100. self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False)
  101. # Annotate this as a custom field
  102. self.custom_fields.append(cf.name)
  103. class CustomFieldFilterForm(forms.Form):
  104. def __init__(self, *args, **kwargs):
  105. self.obj_type = ContentType.objects.get_for_model(self.model)
  106. super().__init__(*args, **kwargs)
  107. # Add all applicable CustomFields to the form
  108. custom_fields = CustomField.objects.filter(obj_type=self.obj_type).exclude(
  109. filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
  110. )
  111. for cf in custom_fields:
  112. field_name = 'cf_{}'.format(cf.name)
  113. self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False)
  114. #
  115. # Tags
  116. #
  117. class TagForm(BootstrapMixin, forms.ModelForm):
  118. slug = SlugField()
  119. class Meta:
  120. model = Tag
  121. fields = [
  122. 'name', 'slug', 'color', 'description'
  123. ]
  124. class TagCSVForm(CSVModelForm):
  125. slug = SlugField()
  126. class Meta:
  127. model = Tag
  128. fields = Tag.csv_headers
  129. help_texts = {
  130. 'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
  131. }
  132. class AddRemoveTagsForm(forms.Form):
  133. def __init__(self, *args, **kwargs):
  134. super().__init__(*args, **kwargs)
  135. # Add add/remove tags fields
  136. self.fields['add_tags'] = DynamicModelMultipleChoiceField(
  137. queryset=Tag.objects.all(),
  138. required=False
  139. )
  140. self.fields['remove_tags'] = DynamicModelMultipleChoiceField(
  141. queryset=Tag.objects.all(),
  142. required=False
  143. )
  144. class TagFilterForm(BootstrapMixin, forms.Form):
  145. model = Tag
  146. q = forms.CharField(
  147. required=False,
  148. label='Search'
  149. )
  150. class TagBulkEditForm(BootstrapMixin, BulkEditForm):
  151. pk = forms.ModelMultipleChoiceField(
  152. queryset=Tag.objects.all(),
  153. widget=forms.MultipleHiddenInput
  154. )
  155. color = forms.CharField(
  156. max_length=6,
  157. required=False,
  158. widget=ColorSelect()
  159. )
  160. description = forms.CharField(
  161. max_length=200,
  162. required=False
  163. )
  164. class Meta:
  165. nullable_fields = ['description']
  166. #
  167. # Config contexts
  168. #
  169. class ConfigContextForm(BootstrapMixin, forms.ModelForm):
  170. regions = DynamicModelMultipleChoiceField(
  171. queryset=Region.objects.all(),
  172. required=False
  173. )
  174. sites = DynamicModelMultipleChoiceField(
  175. queryset=Site.objects.all(),
  176. required=False
  177. )
  178. roles = DynamicModelMultipleChoiceField(
  179. queryset=DeviceRole.objects.all(),
  180. required=False
  181. )
  182. platforms = DynamicModelMultipleChoiceField(
  183. queryset=Platform.objects.all(),
  184. required=False
  185. )
  186. cluster_groups = DynamicModelMultipleChoiceField(
  187. queryset=ClusterGroup.objects.all(),
  188. required=False
  189. )
  190. clusters = DynamicModelMultipleChoiceField(
  191. queryset=Cluster.objects.all(),
  192. required=False
  193. )
  194. tenant_groups = DynamicModelMultipleChoiceField(
  195. queryset=TenantGroup.objects.all(),
  196. required=False
  197. )
  198. tenants = DynamicModelMultipleChoiceField(
  199. queryset=Tenant.objects.all(),
  200. required=False
  201. )
  202. tags = DynamicModelMultipleChoiceField(
  203. queryset=Tag.objects.all(),
  204. required=False
  205. )
  206. data = JSONField(
  207. label=''
  208. )
  209. class Meta:
  210. model = ConfigContext
  211. fields = (
  212. 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
  213. 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
  214. )
  215. class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
  216. pk = forms.ModelMultipleChoiceField(
  217. queryset=ConfigContext.objects.all(),
  218. widget=forms.MultipleHiddenInput
  219. )
  220. weight = forms.IntegerField(
  221. required=False,
  222. min_value=0
  223. )
  224. is_active = forms.NullBooleanField(
  225. required=False,
  226. widget=BulkEditNullBooleanSelect()
  227. )
  228. description = forms.CharField(
  229. required=False,
  230. max_length=100
  231. )
  232. class Meta:
  233. nullable_fields = [
  234. 'description',
  235. ]
  236. class ConfigContextFilterForm(BootstrapMixin, forms.Form):
  237. q = forms.CharField(
  238. required=False,
  239. label='Search'
  240. )
  241. region = DynamicModelMultipleChoiceField(
  242. queryset=Region.objects.all(),
  243. to_field_name='slug',
  244. required=False
  245. )
  246. site = DynamicModelMultipleChoiceField(
  247. queryset=Site.objects.all(),
  248. to_field_name='slug',
  249. required=False
  250. )
  251. role = DynamicModelMultipleChoiceField(
  252. queryset=DeviceRole.objects.all(),
  253. to_field_name='slug',
  254. required=False
  255. )
  256. platform = DynamicModelMultipleChoiceField(
  257. queryset=Platform.objects.all(),
  258. to_field_name='slug',
  259. required=False
  260. )
  261. cluster_group = DynamicModelMultipleChoiceField(
  262. queryset=ClusterGroup.objects.all(),
  263. to_field_name='slug',
  264. required=False
  265. )
  266. cluster_id = DynamicModelMultipleChoiceField(
  267. queryset=Cluster.objects.all(),
  268. required=False,
  269. label='Cluster'
  270. )
  271. tenant_group = DynamicModelMultipleChoiceField(
  272. queryset=TenantGroup.objects.all(),
  273. to_field_name='slug',
  274. required=False
  275. )
  276. tenant = DynamicModelMultipleChoiceField(
  277. queryset=Tenant.objects.all(),
  278. to_field_name='slug',
  279. required=False
  280. )
  281. tag = DynamicModelMultipleChoiceField(
  282. queryset=Tag.objects.all(),
  283. to_field_name='slug',
  284. required=False
  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. # Change logging
  308. #
  309. class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
  310. model = ObjectChange
  311. q = forms.CharField(
  312. required=False,
  313. label='Search'
  314. )
  315. time_after = forms.DateTimeField(
  316. label='After',
  317. required=False,
  318. widget=DateTimePicker()
  319. )
  320. time_before = forms.DateTimeField(
  321. label='Before',
  322. required=False,
  323. widget=DateTimePicker()
  324. )
  325. action = forms.ChoiceField(
  326. choices=add_blank_choice(ObjectChangeActionChoices),
  327. required=False,
  328. widget=StaticSelect2()
  329. )
  330. user_id = DynamicModelMultipleChoiceField(
  331. queryset=User.objects.all(),
  332. required=False,
  333. display_field='username',
  334. label='User',
  335. widget=APISelectMultiple(
  336. api_url='/api/users/users/',
  337. )
  338. )
  339. changed_object_type = forms.ModelChoiceField(
  340. queryset=ContentType.objects.order_by('model'),
  341. required=False,
  342. widget=ContentTypeSelect(),
  343. label='Object Type'
  344. )
  345. #
  346. # Scripts
  347. #
  348. class ScriptForm(BootstrapMixin, forms.Form):
  349. _commit = forms.BooleanField(
  350. required=False,
  351. initial=True,
  352. label="Commit changes",
  353. help_text="Commit changes to the database (uncheck for a dry-run)"
  354. )
  355. def __init__(self, *args, **kwargs):
  356. super().__init__(*args, **kwargs)
  357. # Move _commit to the end of the form
  358. commit = self.fields.pop('_commit')
  359. self.fields['_commit'] = commit
  360. @property
  361. def requires_input(self):
  362. """
  363. A boolean indicating whether the form requires user input (ignore the _commit field).
  364. """
  365. return bool(len(self.fields) > 1)