forms.py 14 KB

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