forms.py 11 KB

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