forms.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  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. comments = CommentField()
  113. class Meta:
  114. model = Tag
  115. fields = [
  116. 'name', 'slug', 'color', 'comments'
  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'] = TagField(required=False)
  123. self.fields['remove_tags'] = TagField(required=False)
  124. class TagFilterForm(BootstrapMixin, forms.Form):
  125. model = Tag
  126. q = forms.CharField(
  127. required=False,
  128. label='Search'
  129. )
  130. class TagBulkEditForm(BootstrapMixin, BulkEditForm):
  131. pk = forms.ModelMultipleChoiceField(
  132. queryset=Tag.objects.all(),
  133. widget=forms.MultipleHiddenInput
  134. )
  135. color = forms.CharField(
  136. max_length=6,
  137. required=False,
  138. widget=ColorSelect()
  139. )
  140. class Meta:
  141. nullable_fields = []
  142. #
  143. # Config contexts
  144. #
  145. class ConfigContextForm(BootstrapMixin, forms.ModelForm):
  146. regions = TreeNodeMultipleChoiceField(
  147. queryset=Region.objects.all(),
  148. required=False,
  149. widget=StaticSelect2Multiple()
  150. )
  151. sites = DynamicModelMultipleChoiceField(
  152. queryset=Site.objects.all(),
  153. required=False
  154. )
  155. roles = DynamicModelMultipleChoiceField(
  156. queryset=DeviceRole.objects.all(),
  157. required=False
  158. )
  159. platforms = DynamicModelMultipleChoiceField(
  160. queryset=Platform.objects.all(),
  161. required=False
  162. )
  163. cluster_groups = DynamicModelMultipleChoiceField(
  164. queryset=ClusterGroup.objects.all(),
  165. required=False
  166. )
  167. clusters = DynamicModelMultipleChoiceField(
  168. queryset=Cluster.objects.all(),
  169. required=False
  170. )
  171. tenant_groups = DynamicModelMultipleChoiceField(
  172. queryset=TenantGroup.objects.all(),
  173. required=False
  174. )
  175. tenants = DynamicModelMultipleChoiceField(
  176. queryset=Tenant.objects.all(),
  177. required=False
  178. )
  179. tags = DynamicModelMultipleChoiceField(
  180. queryset=Tag.objects.all(),
  181. to_field_name='slug',
  182. required=False
  183. )
  184. data = JSONField(
  185. label=''
  186. )
  187. class Meta:
  188. model = ConfigContext
  189. fields = (
  190. 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
  191. 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
  192. )
  193. class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
  194. pk = forms.ModelMultipleChoiceField(
  195. queryset=ConfigContext.objects.all(),
  196. widget=forms.MultipleHiddenInput
  197. )
  198. weight = forms.IntegerField(
  199. required=False,
  200. min_value=0
  201. )
  202. is_active = forms.NullBooleanField(
  203. required=False,
  204. widget=BulkEditNullBooleanSelect()
  205. )
  206. description = forms.CharField(
  207. required=False,
  208. max_length=100
  209. )
  210. class Meta:
  211. nullable_fields = [
  212. 'description',
  213. ]
  214. class ConfigContextFilterForm(BootstrapMixin, forms.Form):
  215. q = forms.CharField(
  216. required=False,
  217. label='Search'
  218. )
  219. region = DynamicModelMultipleChoiceField(
  220. queryset=Region.objects.all(),
  221. to_field_name='slug',
  222. required=False,
  223. widget=APISelectMultiple(
  224. value_field="slug",
  225. )
  226. )
  227. site = DynamicModelMultipleChoiceField(
  228. queryset=Site.objects.all(),
  229. to_field_name='slug',
  230. required=False,
  231. widget=APISelectMultiple(
  232. value_field="slug",
  233. )
  234. )
  235. role = DynamicModelMultipleChoiceField(
  236. queryset=DeviceRole.objects.all(),
  237. to_field_name='slug',
  238. required=False,
  239. widget=APISelectMultiple(
  240. value_field="slug",
  241. )
  242. )
  243. platform = DynamicModelMultipleChoiceField(
  244. queryset=Platform.objects.all(),
  245. to_field_name='slug',
  246. required=False,
  247. widget=APISelectMultiple(
  248. value_field="slug",
  249. )
  250. )
  251. cluster_group = DynamicModelMultipleChoiceField(
  252. queryset=ClusterGroup.objects.all(),
  253. to_field_name='slug',
  254. required=False,
  255. widget=APISelectMultiple(
  256. value_field="slug",
  257. )
  258. )
  259. cluster_id = DynamicModelMultipleChoiceField(
  260. queryset=Cluster.objects.all(),
  261. required=False,
  262. label='Cluster'
  263. )
  264. tenant_group = DynamicModelMultipleChoiceField(
  265. queryset=TenantGroup.objects.all(),
  266. to_field_name='slug',
  267. required=False,
  268. widget=APISelectMultiple(
  269. value_field="slug",
  270. )
  271. )
  272. tenant = DynamicModelMultipleChoiceField(
  273. queryset=Tenant.objects.all(),
  274. to_field_name='slug',
  275. required=False,
  276. widget=APISelectMultiple(
  277. value_field="slug",
  278. )
  279. )
  280. tag = DynamicModelMultipleChoiceField(
  281. queryset=Tag.objects.all(),
  282. to_field_name='slug',
  283. required=False,
  284. widget=APISelectMultiple(
  285. value_field="slug",
  286. )
  287. )
  288. #
  289. # Filter form for local config context data
  290. #
  291. class LocalConfigContextFilterForm(forms.Form):
  292. local_context_data = forms.NullBooleanField(
  293. required=False,
  294. label='Has local config context data',
  295. widget=StaticSelect2(
  296. choices=BOOLEAN_WITH_BLANK_CHOICES
  297. )
  298. )
  299. #
  300. # Image attachments
  301. #
  302. class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
  303. class Meta:
  304. model = ImageAttachment
  305. fields = [
  306. 'name', 'image',
  307. ]
  308. #
  309. # Change logging
  310. #
  311. class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
  312. model = ObjectChange
  313. q = forms.CharField(
  314. required=False,
  315. label='Search'
  316. )
  317. time_after = forms.DateTimeField(
  318. label='After',
  319. required=False,
  320. widget=DateTimePicker()
  321. )
  322. time_before = forms.DateTimeField(
  323. label='Before',
  324. required=False,
  325. widget=DateTimePicker()
  326. )
  327. action = forms.ChoiceField(
  328. choices=add_blank_choice(ObjectChangeActionChoices),
  329. required=False,
  330. widget=StaticSelect2()
  331. )
  332. # TODO: Convert to DynamicModelMultipleChoiceField once we have an API endpoint for users
  333. user = forms.ModelChoiceField(
  334. queryset=User.objects.order_by('username'),
  335. required=False,
  336. widget=StaticSelect2()
  337. )
  338. changed_object_type = forms.ModelChoiceField(
  339. queryset=ContentType.objects.order_by('model'),
  340. required=False,
  341. widget=ContentTypeSelect(),
  342. label='Object Type'
  343. )
  344. #
  345. # Scripts
  346. #
  347. class ScriptForm(BootstrapMixin, forms.Form):
  348. _commit = forms.BooleanField(
  349. required=False,
  350. initial=True,
  351. label="Commit changes",
  352. help_text="Commit changes to the database (uncheck for a dry-run)"
  353. )
  354. def __init__(self, vars, *args, commit_default=True, **kwargs):
  355. super().__init__(*args, **kwargs)
  356. # Dynamically populate fields for variables
  357. for name, var in vars.items():
  358. self.fields[name] = var.as_field()
  359. # Toggle default commit behavior based on Meta option
  360. if not commit_default:
  361. self.fields['_commit'].initial = False
  362. # Move _commit to the end of the form
  363. self.fields.move_to_end('_commit', True)
  364. @property
  365. def requires_input(self):
  366. """
  367. A boolean indicating whether the form requires user input (ignore the _commit field).
  368. """
  369. return bool(len(self.fields) > 1)