forms.py 13 KB

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