forms.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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 mptt.forms import TreeNodeMultipleChoiceField
  7. from taggit.forms import TagField
  8. from taggit.models import Tag
  9. from dcim.models import DeviceRole, Platform, Region, Site
  10. from tenancy.models import Tenant, TenantGroup
  11. from utilities.forms import (
  12. add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField,
  13. FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
  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
  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(obj_type=self.obj_type, obj_id=self.instance.pk)\
  95. .select_related('field')
  96. for cfv in existing_values:
  97. self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.serialized_value
  98. def _save_custom_fields(self):
  99. for field_name in self.custom_fields:
  100. try:
  101. cfv = CustomFieldValue.objects.select_related('field').get(field=self.fields[field_name].model,
  102. obj_type=self.obj_type,
  103. obj_id=self.instance.pk)
  104. except CustomFieldValue.DoesNotExist:
  105. # Skip this field if none exists already and its value is empty
  106. if self.cleaned_data[field_name] in [None, '']:
  107. continue
  108. cfv = CustomFieldValue(
  109. field=self.fields[field_name].model,
  110. obj_type=self.obj_type,
  111. obj_id=self.instance.pk
  112. )
  113. cfv.value = self.cleaned_data[field_name]
  114. cfv.save()
  115. def save(self, commit=True):
  116. obj = super().save(commit)
  117. # Handle custom fields the same way we do M2M fields
  118. if commit:
  119. self._save_custom_fields()
  120. else:
  121. self.save_custom_fields = self._save_custom_fields
  122. return obj
  123. class CustomFieldBulkEditForm(BulkEditForm):
  124. def __init__(self, *args, **kwargs):
  125. super().__init__(*args, **kwargs)
  126. self.custom_fields = []
  127. self.obj_type = ContentType.objects.get_for_model(self.model)
  128. # Add all applicable CustomFields to the form
  129. custom_fields = get_custom_fields_for_model(self.obj_type, bulk_edit=True).items()
  130. for name, field in custom_fields:
  131. # Annotate non-required custom fields as nullable
  132. if not field.required:
  133. self.nullable_fields.append(name)
  134. field.required = False
  135. self.fields[name] = field
  136. # Annotate this as a custom field
  137. self.custom_fields.append(name)
  138. class CustomFieldFilterForm(forms.Form):
  139. def __init__(self, *args, **kwargs):
  140. self.obj_type = ContentType.objects.get_for_model(self.model)
  141. super().__init__(*args, **kwargs)
  142. # Add all applicable CustomFields to the form
  143. custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items()
  144. for name, field in custom_fields:
  145. field.required = False
  146. self.fields[name] = field
  147. #
  148. # Tags
  149. #
  150. class TagForm(BootstrapMixin, forms.ModelForm):
  151. slug = SlugField()
  152. class Meta:
  153. model = Tag
  154. fields = [
  155. 'name', 'slug',
  156. ]
  157. class AddRemoveTagsForm(forms.Form):
  158. def __init__(self, *args, **kwargs):
  159. super().__init__(*args, **kwargs)
  160. # Add add/remove tags fields
  161. self.fields['add_tags'] = TagField(required=False)
  162. self.fields['remove_tags'] = TagField(required=False)
  163. class TagFilterForm(BootstrapMixin, forms.Form):
  164. model = Tag
  165. q = forms.CharField(
  166. required=False,
  167. label='Search'
  168. )
  169. #
  170. # Config contexts
  171. #
  172. class ConfigContextForm(BootstrapMixin, forms.ModelForm):
  173. regions = TreeNodeMultipleChoiceField(
  174. queryset=Region.objects.all(),
  175. required=False
  176. )
  177. data = JSONField()
  178. class Meta:
  179. model = ConfigContext
  180. fields = [
  181. 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
  182. 'tenants', 'data',
  183. ]
  184. class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
  185. pk = forms.ModelMultipleChoiceField(
  186. queryset=ConfigContext.objects.all(),
  187. widget=forms.MultipleHiddenInput
  188. )
  189. weight = forms.IntegerField(
  190. required=False,
  191. min_value=0
  192. )
  193. is_active = forms.NullBooleanField(
  194. required=False,
  195. widget=BulkEditNullBooleanSelect()
  196. )
  197. description = forms.CharField(
  198. required=False,
  199. max_length=100
  200. )
  201. class Meta:
  202. nullable_fields = [
  203. 'description',
  204. ]
  205. class ConfigContextFilterForm(BootstrapMixin, forms.Form):
  206. q = forms.CharField(
  207. required=False,
  208. label='Search'
  209. )
  210. region = FilterTreeNodeMultipleChoiceField(
  211. queryset=Region.objects.all(),
  212. to_field_name='slug'
  213. )
  214. site = FilterChoiceField(
  215. queryset=Site.objects.all(),
  216. to_field_name='slug'
  217. )
  218. role = FilterChoiceField(
  219. queryset=DeviceRole.objects.all(),
  220. to_field_name='slug'
  221. )
  222. platform = FilterChoiceField(
  223. queryset=Platform.objects.all(),
  224. to_field_name='slug'
  225. )
  226. tenant_group = FilterChoiceField(
  227. queryset=TenantGroup.objects.all(),
  228. to_field_name='slug'
  229. )
  230. tenant = FilterChoiceField(
  231. queryset=Tenant.objects.all(),
  232. to_field_name='slug'
  233. )
  234. #
  235. # Image attachments
  236. #
  237. class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
  238. class Meta:
  239. model = ImageAttachment
  240. fields = [
  241. 'name', 'image',
  242. ]
  243. #
  244. # Change logging
  245. #
  246. class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
  247. model = ObjectChange
  248. q = forms.CharField(
  249. required=False,
  250. label='Search'
  251. )
  252. time_after = forms.DateTimeField(
  253. label='After',
  254. required=False,
  255. widget=forms.TextInput(
  256. attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
  257. )
  258. )
  259. time_before = forms.DateTimeField(
  260. label='Before',
  261. required=False,
  262. widget=forms.TextInput(
  263. attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
  264. )
  265. )
  266. action = forms.ChoiceField(
  267. choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES),
  268. required=False
  269. )
  270. user = forms.ModelChoiceField(
  271. queryset=User.objects.order_by('username'),
  272. required=False
  273. )
  274. changed_object_type = forms.ModelChoiceField(
  275. queryset=ContentType.objects.order_by('model'),
  276. required=False,
  277. widget=ContentTypeSelect(),
  278. label='Object Type'
  279. )