forms.py 12 KB

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