forms.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. from django import forms
  2. from django.contrib.auth.models import User
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.utils.safestring import mark_safe
  5. from django.utils.translation import gettext as _
  6. from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
  7. from tenancy.models import Tenant, TenantGroup
  8. from utilities.forms import (
  9. add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
  10. CommentField, ContentTypeMultipleChoiceField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField,
  11. JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
  12. )
  13. from virtualization.models import Cluster, ClusterGroup
  14. from .choices import *
  15. from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
  16. from .utils import FeatureQuery
  17. #
  18. # Custom fields
  19. #
  20. class CustomFieldForm(forms.Form):
  21. """
  22. Extend Form to include custom field support.
  23. """
  24. model = None
  25. def __init__(self, *args, **kwargs):
  26. if self.model is None:
  27. raise NotImplementedError("CustomFieldForm must specify a model class.")
  28. self.custom_fields = []
  29. super().__init__(*args, **kwargs)
  30. # Append relevant custom fields to the form instance
  31. obj_type = ContentType.objects.get_for_model(self.model)
  32. for cf in CustomField.objects.filter(content_types=obj_type):
  33. field_name = 'cf_{}'.format(cf.name)
  34. self.fields[field_name] = cf.to_form_field()
  35. # Annotate the field in the list of CustomField form fields
  36. self.custom_fields.append(field_name)
  37. class CustomFieldModelForm(forms.ModelForm):
  38. """
  39. Extend ModelForm to include custom field support.
  40. """
  41. def __init__(self, *args, **kwargs):
  42. self.obj_type = ContentType.objects.get_for_model(self._meta.model)
  43. self.custom_fields = []
  44. super().__init__(*args, **kwargs)
  45. self._append_customfield_fields()
  46. def _append_customfield_fields(self):
  47. """
  48. Append form fields for all CustomFields assigned to this model.
  49. """
  50. # Append form fields; assign initial values if modifying and existing object
  51. for cf in CustomField.objects.filter(content_types=self.obj_type):
  52. field_name = 'cf_{}'.format(cf.name)
  53. if self.instance.pk:
  54. self.fields[field_name] = cf.to_form_field(set_initial=False)
  55. self.fields[field_name].initial = self.instance.custom_field_data.get(cf.name)
  56. else:
  57. self.fields[field_name] = cf.to_form_field()
  58. # Annotate the field in the list of CustomField form fields
  59. self.custom_fields.append(field_name)
  60. def clean(self):
  61. # Save custom field data on instance
  62. for cf_name in self.custom_fields:
  63. self.instance.custom_field_data[cf_name[3:]] = self.cleaned_data.get(cf_name)
  64. return super().clean()
  65. class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
  66. def _append_customfield_fields(self):
  67. # Append form fields
  68. for cf in CustomField.objects.filter(content_types=self.obj_type):
  69. field_name = 'cf_{}'.format(cf.name)
  70. self.fields[field_name] = cf.to_form_field(for_csv_import=True)
  71. # Annotate the field in the list of CustomField form fields
  72. self.custom_fields.append(field_name)
  73. class CustomFieldBulkEditForm(BulkEditForm):
  74. def __init__(self, *args, **kwargs):
  75. super().__init__(*args, **kwargs)
  76. self.custom_fields = []
  77. self.obj_type = ContentType.objects.get_for_model(self.model)
  78. # Add all applicable CustomFields to the form
  79. custom_fields = CustomField.objects.filter(content_types=self.obj_type)
  80. for cf in custom_fields:
  81. # Annotate non-required custom fields as nullable
  82. if not cf.required:
  83. self.nullable_fields.append(cf.name)
  84. self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False)
  85. # Annotate this as a custom field
  86. self.custom_fields.append(cf.name)
  87. class CustomFieldFilterForm(forms.Form):
  88. def __init__(self, *args, **kwargs):
  89. self.obj_type = ContentType.objects.get_for_model(self.model)
  90. super().__init__(*args, **kwargs)
  91. # Add all applicable CustomFields to the form
  92. custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude(
  93. filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
  94. )
  95. for cf in custom_fields:
  96. field_name = 'cf_{}'.format(cf.name)
  97. self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False)
  98. #
  99. # Tags
  100. #
  101. class TagForm(BootstrapMixin, forms.ModelForm):
  102. slug = SlugField()
  103. class Meta:
  104. model = Tag
  105. fields = [
  106. 'name', 'slug', 'color', 'description'
  107. ]
  108. fieldsets = (
  109. ('Tag', ('name', 'slug', 'color', 'description')),
  110. )
  111. class TagCSVForm(CSVModelForm):
  112. slug = SlugField()
  113. class Meta:
  114. model = Tag
  115. fields = Tag.csv_headers
  116. help_texts = {
  117. 'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
  118. }
  119. class AddRemoveTagsForm(forms.Form):
  120. def __init__(self, *args, **kwargs):
  121. super().__init__(*args, **kwargs)
  122. # Add add/remove tags fields
  123. self.fields['add_tags'] = DynamicModelMultipleChoiceField(
  124. queryset=Tag.objects.all(),
  125. required=False
  126. )
  127. self.fields['remove_tags'] = DynamicModelMultipleChoiceField(
  128. queryset=Tag.objects.all(),
  129. required=False
  130. )
  131. class TagFilterForm(BootstrapMixin, forms.Form):
  132. model = Tag
  133. q = forms.CharField(
  134. required=False,
  135. label=_('Search')
  136. )
  137. content_type_id = ContentTypeMultipleChoiceField(
  138. queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
  139. required=False,
  140. label=_('Tagged object type')
  141. )
  142. class TagBulkEditForm(BootstrapMixin, BulkEditForm):
  143. pk = forms.ModelMultipleChoiceField(
  144. queryset=Tag.objects.all(),
  145. widget=forms.MultipleHiddenInput
  146. )
  147. color = forms.CharField(
  148. max_length=6,
  149. required=False,
  150. widget=ColorSelect()
  151. )
  152. description = forms.CharField(
  153. max_length=200,
  154. required=False
  155. )
  156. class Meta:
  157. nullable_fields = ['description']
  158. #
  159. # Config contexts
  160. #
  161. class ConfigContextForm(BootstrapMixin, forms.ModelForm):
  162. regions = DynamicModelMultipleChoiceField(
  163. queryset=Region.objects.all(),
  164. required=False
  165. )
  166. site_groups = DynamicModelMultipleChoiceField(
  167. queryset=SiteGroup.objects.all(),
  168. required=False
  169. )
  170. sites = DynamicModelMultipleChoiceField(
  171. queryset=Site.objects.all(),
  172. required=False
  173. )
  174. device_types = DynamicModelMultipleChoiceField(
  175. queryset=DeviceType.objects.all(),
  176. required=False
  177. )
  178. roles = DynamicModelMultipleChoiceField(
  179. queryset=DeviceRole.objects.all(),
  180. required=False
  181. )
  182. platforms = DynamicModelMultipleChoiceField(
  183. queryset=Platform.objects.all(),
  184. required=False
  185. )
  186. cluster_groups = DynamicModelMultipleChoiceField(
  187. queryset=ClusterGroup.objects.all(),
  188. required=False
  189. )
  190. clusters = DynamicModelMultipleChoiceField(
  191. queryset=Cluster.objects.all(),
  192. required=False
  193. )
  194. tenant_groups = DynamicModelMultipleChoiceField(
  195. queryset=TenantGroup.objects.all(),
  196. required=False
  197. )
  198. tenants = DynamicModelMultipleChoiceField(
  199. queryset=Tenant.objects.all(),
  200. required=False
  201. )
  202. tags = DynamicModelMultipleChoiceField(
  203. queryset=Tag.objects.all(),
  204. required=False
  205. )
  206. data = JSONField(
  207. label=''
  208. )
  209. class Meta:
  210. model = ConfigContext
  211. fields = (
  212. 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types',
  213. 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
  214. )
  215. class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
  216. pk = forms.ModelMultipleChoiceField(
  217. queryset=ConfigContext.objects.all(),
  218. widget=forms.MultipleHiddenInput
  219. )
  220. weight = forms.IntegerField(
  221. required=False,
  222. min_value=0
  223. )
  224. is_active = forms.NullBooleanField(
  225. required=False,
  226. widget=BulkEditNullBooleanSelect()
  227. )
  228. description = forms.CharField(
  229. required=False,
  230. max_length=100
  231. )
  232. class Meta:
  233. nullable_fields = [
  234. 'description',
  235. ]
  236. class ConfigContextFilterForm(BootstrapMixin, forms.Form):
  237. field_order = [
  238. 'q', 'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id', 'cluster_id',
  239. 'tenant_group_id', 'tenant_id',
  240. ]
  241. q = forms.CharField(
  242. required=False,
  243. label=_('Search')
  244. )
  245. region_id = DynamicModelMultipleChoiceField(
  246. queryset=Region.objects.all(),
  247. required=False,
  248. label=_('Regions')
  249. )
  250. site_group_id = DynamicModelMultipleChoiceField(
  251. queryset=SiteGroup.objects.all(),
  252. required=False,
  253. label=_('Site groups')
  254. )
  255. site_id = DynamicModelMultipleChoiceField(
  256. queryset=Site.objects.all(),
  257. required=False,
  258. label=_('Sites')
  259. )
  260. device_type_id = DynamicModelMultipleChoiceField(
  261. queryset=DeviceType.objects.all(),
  262. required=False,
  263. label=_('Device types')
  264. )
  265. role_id = DynamicModelMultipleChoiceField(
  266. queryset=DeviceRole.objects.all(),
  267. required=False,
  268. label=_('Roles')
  269. )
  270. platform_id = DynamicModelMultipleChoiceField(
  271. queryset=Platform.objects.all(),
  272. required=False,
  273. label=_('Platforms')
  274. )
  275. cluster_group_id = DynamicModelMultipleChoiceField(
  276. queryset=ClusterGroup.objects.all(),
  277. required=False,
  278. label=_('Cluster groups')
  279. )
  280. cluster_id = DynamicModelMultipleChoiceField(
  281. queryset=Cluster.objects.all(),
  282. required=False,
  283. label=_('Clusters')
  284. )
  285. tenant_group_id = DynamicModelMultipleChoiceField(
  286. queryset=TenantGroup.objects.all(),
  287. required=False,
  288. label=_('Tenant groups')
  289. )
  290. tenant_id = DynamicModelMultipleChoiceField(
  291. queryset=Tenant.objects.all(),
  292. required=False,
  293. label=_('Tenant')
  294. )
  295. tag = DynamicModelMultipleChoiceField(
  296. queryset=Tag.objects.all(),
  297. to_field_name='slug',
  298. required=False,
  299. label=_('Tags')
  300. )
  301. #
  302. # Filter form for local config context data
  303. #
  304. class LocalConfigContextFilterForm(forms.Form):
  305. local_context_data = forms.NullBooleanField(
  306. required=False,
  307. label=_('Has local config context data'),
  308. widget=StaticSelect2(
  309. choices=BOOLEAN_WITH_BLANK_CHOICES
  310. )
  311. )
  312. #
  313. # Image attachments
  314. #
  315. class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
  316. class Meta:
  317. model = ImageAttachment
  318. fields = [
  319. 'name', 'image',
  320. ]
  321. #
  322. # Journal entries
  323. #
  324. class JournalEntryForm(BootstrapMixin, forms.ModelForm):
  325. comments = CommentField()
  326. class Meta:
  327. model = JournalEntry
  328. fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments']
  329. widgets = {
  330. 'assigned_object_type': forms.HiddenInput,
  331. 'assigned_object_id': forms.HiddenInput,
  332. }
  333. class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm):
  334. pk = forms.ModelMultipleChoiceField(
  335. queryset=JournalEntry.objects.all(),
  336. widget=forms.MultipleHiddenInput
  337. )
  338. kind = forms.ChoiceField(
  339. choices=JournalEntryKindChoices,
  340. required=False
  341. )
  342. comments = forms.CharField(
  343. required=False,
  344. widget=forms.Textarea()
  345. )
  346. class Meta:
  347. nullable_fields = []
  348. class JournalEntryFilterForm(BootstrapMixin, forms.Form):
  349. model = JournalEntry
  350. q = forms.CharField(
  351. required=False,
  352. label=_('Search')
  353. )
  354. created_after = forms.DateTimeField(
  355. required=False,
  356. label=_('After'),
  357. widget=DateTimePicker()
  358. )
  359. created_before = forms.DateTimeField(
  360. required=False,
  361. label=_('Before'),
  362. widget=DateTimePicker()
  363. )
  364. created_by_id = DynamicModelMultipleChoiceField(
  365. queryset=User.objects.all(),
  366. required=False,
  367. label=_('User'),
  368. widget=APISelectMultiple(
  369. api_url='/api/users/users/',
  370. )
  371. )
  372. assigned_object_type_id = DynamicModelMultipleChoiceField(
  373. queryset=ContentType.objects.all(),
  374. required=False,
  375. label=_('Object Type'),
  376. widget=APISelectMultiple(
  377. api_url='/api/extras/content-types/',
  378. )
  379. )
  380. kind = forms.ChoiceField(
  381. choices=add_blank_choice(JournalEntryKindChoices),
  382. required=False,
  383. widget=StaticSelect2()
  384. )
  385. #
  386. # Change logging
  387. #
  388. class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
  389. model = ObjectChange
  390. q = forms.CharField(
  391. required=False,
  392. label=_('Search')
  393. )
  394. time_after = forms.DateTimeField(
  395. required=False,
  396. label=_('After'),
  397. widget=DateTimePicker()
  398. )
  399. time_before = forms.DateTimeField(
  400. required=False,
  401. label=_('Before'),
  402. widget=DateTimePicker()
  403. )
  404. action = forms.ChoiceField(
  405. choices=add_blank_choice(ObjectChangeActionChoices),
  406. required=False,
  407. widget=StaticSelect2()
  408. )
  409. user_id = DynamicModelMultipleChoiceField(
  410. queryset=User.objects.all(),
  411. required=False,
  412. label=_('User'),
  413. widget=APISelectMultiple(
  414. api_url='/api/users/users/',
  415. )
  416. )
  417. changed_object_type_id = DynamicModelMultipleChoiceField(
  418. queryset=ContentType.objects.all(),
  419. required=False,
  420. label=_('Object Type'),
  421. widget=APISelectMultiple(
  422. api_url='/api/extras/content-types/',
  423. )
  424. )
  425. #
  426. # Scripts
  427. #
  428. class ScriptForm(BootstrapMixin, forms.Form):
  429. _commit = forms.BooleanField(
  430. required=False,
  431. initial=True,
  432. label="Commit changes",
  433. help_text="Commit changes to the database (uncheck for a dry-run)"
  434. )
  435. def __init__(self, *args, **kwargs):
  436. super().__init__(*args, **kwargs)
  437. # Move _commit to the end of the form
  438. commit = self.fields.pop('_commit')
  439. self.fields['_commit'] = commit
  440. @property
  441. def requires_input(self):
  442. """
  443. A boolean indicating whether the form requires user input (ignore the _commit field).
  444. """
  445. return bool(len(self.fields) > 1)