forms.py 12 KB

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