forms.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964
  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, ColorField,
  10. CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, CSVContentTypeField, CSVModelForm,
  11. CSVMultipleContentTypeField, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
  12. StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES,
  13. )
  14. from virtualization.models import Cluster, ClusterGroup
  15. from .choices import *
  16. from .models import *
  17. from .utils import FeatureQuery
  18. #
  19. # Custom fields
  20. #
  21. class CustomFieldForm(BootstrapMixin, forms.ModelForm):
  22. content_types = ContentTypeMultipleChoiceField(
  23. queryset=ContentType.objects.all(),
  24. limit_choices_to=FeatureQuery('custom_fields')
  25. )
  26. class Meta:
  27. model = CustomField
  28. fields = '__all__'
  29. fieldsets = (
  30. ('Custom Field', ('name', 'label', 'type', 'weight', 'required', 'description')),
  31. ('Assigned Models', ('content_types',)),
  32. ('Behavior', ('filter_logic',)),
  33. ('Values', ('default', 'choices')),
  34. ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
  35. )
  36. class CustomFieldCSVForm(CSVModelForm):
  37. content_types = CSVMultipleContentTypeField(
  38. queryset=ContentType.objects.all(),
  39. limit_choices_to=FeatureQuery('custom_fields'),
  40. help_text="One or more assigned object types"
  41. )
  42. class Meta:
  43. model = CustomField
  44. fields = (
  45. 'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default',
  46. 'weight',
  47. )
  48. class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm):
  49. pk = forms.ModelMultipleChoiceField(
  50. queryset=CustomField.objects.all(),
  51. widget=forms.MultipleHiddenInput
  52. )
  53. description = forms.CharField(
  54. required=False
  55. )
  56. required = forms.NullBooleanField(
  57. required=False,
  58. widget=BulkEditNullBooleanSelect()
  59. )
  60. weight = forms.IntegerField(
  61. required=False
  62. )
  63. class Meta:
  64. nullable_fields = []
  65. class CustomFieldFilterForm(BootstrapMixin, forms.Form):
  66. field_groups = [
  67. ['q'],
  68. ['type', 'content_types'],
  69. ['weight', 'required'],
  70. ]
  71. q = forms.CharField(
  72. required=False,
  73. widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
  74. label=_('Search')
  75. )
  76. content_types = ContentTypeMultipleChoiceField(
  77. queryset=ContentType.objects.all(),
  78. limit_choices_to=FeatureQuery('custom_fields')
  79. )
  80. type = forms.MultipleChoiceField(
  81. choices=CustomFieldTypeChoices,
  82. required=False,
  83. widget=StaticSelectMultiple()
  84. )
  85. weight = forms.IntegerField(
  86. required=False
  87. )
  88. required = forms.NullBooleanField(
  89. required=False,
  90. widget=StaticSelect(
  91. choices=BOOLEAN_WITH_BLANK_CHOICES
  92. )
  93. )
  94. #
  95. # Custom links
  96. #
  97. class CustomLinkForm(BootstrapMixin, forms.ModelForm):
  98. content_type = ContentTypeChoiceField(
  99. queryset=ContentType.objects.all(),
  100. limit_choices_to=FeatureQuery('custom_links')
  101. )
  102. class Meta:
  103. model = CustomLink
  104. fields = '__all__'
  105. fieldsets = (
  106. ('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')),
  107. ('Templates', ('link_text', 'link_url')),
  108. )
  109. help_texts = {
  110. 'link_text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. '
  111. 'Links which render as empty text will not be displayed.',
  112. 'link_url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
  113. }
  114. class CustomLinkCSVForm(CSVModelForm):
  115. content_type = CSVContentTypeField(
  116. queryset=ContentType.objects.all(),
  117. limit_choices_to=FeatureQuery('custom_links'),
  118. help_text="Assigned object type"
  119. )
  120. class Meta:
  121. model = CustomLink
  122. fields = (
  123. 'name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url',
  124. )
  125. class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm):
  126. pk = forms.ModelMultipleChoiceField(
  127. queryset=CustomLink.objects.all(),
  128. widget=forms.MultipleHiddenInput
  129. )
  130. content_type = ContentTypeChoiceField(
  131. queryset=ContentType.objects.all(),
  132. limit_choices_to=FeatureQuery('custom_fields'),
  133. required=False
  134. )
  135. new_window = forms.NullBooleanField(
  136. required=False,
  137. widget=BulkEditNullBooleanSelect()
  138. )
  139. weight = forms.IntegerField(
  140. required=False
  141. )
  142. button_class = forms.ChoiceField(
  143. choices=CustomLinkButtonClassChoices,
  144. required=False,
  145. widget=StaticSelect()
  146. )
  147. class Meta:
  148. nullable_fields = []
  149. class CustomLinkFilterForm(BootstrapMixin, forms.Form):
  150. field_groups = [
  151. ['q'],
  152. ['content_type'],
  153. ['weight', 'new_window'],
  154. ]
  155. q = forms.CharField(
  156. required=False,
  157. widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
  158. label=_('Search')
  159. )
  160. content_type = ContentTypeChoiceField(
  161. queryset=ContentType.objects.all(),
  162. limit_choices_to=FeatureQuery('custom_fields')
  163. )
  164. weight = forms.IntegerField(
  165. required=False
  166. )
  167. new_window = forms.NullBooleanField(
  168. required=False,
  169. widget=StaticSelect(
  170. choices=BOOLEAN_WITH_BLANK_CHOICES
  171. )
  172. )
  173. #
  174. # Export templates
  175. #
  176. class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
  177. content_type = ContentTypeChoiceField(
  178. queryset=ContentType.objects.all(),
  179. limit_choices_to=FeatureQuery('custom_links')
  180. )
  181. class Meta:
  182. model = ExportTemplate
  183. fields = '__all__'
  184. fieldsets = (
  185. ('Custom Link', ('name', 'content_type', 'description')),
  186. ('Template', ('template_code',)),
  187. ('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
  188. )
  189. class ExportTemplateCSVForm(CSVModelForm):
  190. content_type = CSVContentTypeField(
  191. queryset=ContentType.objects.all(),
  192. limit_choices_to=FeatureQuery('export_templates'),
  193. help_text="Assigned object type"
  194. )
  195. class Meta:
  196. model = ExportTemplate
  197. fields = (
  198. 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code',
  199. )
  200. class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
  201. pk = forms.ModelMultipleChoiceField(
  202. queryset=ExportTemplate.objects.all(),
  203. widget=forms.MultipleHiddenInput
  204. )
  205. content_type = ContentTypeChoiceField(
  206. queryset=ContentType.objects.all(),
  207. limit_choices_to=FeatureQuery('custom_fields'),
  208. required=False
  209. )
  210. description = forms.CharField(
  211. max_length=200,
  212. required=False
  213. )
  214. mime_type = forms.CharField(
  215. max_length=50,
  216. required=False
  217. )
  218. file_extension = forms.CharField(
  219. max_length=15,
  220. required=False
  221. )
  222. as_attachment = forms.NullBooleanField(
  223. required=False,
  224. widget=BulkEditNullBooleanSelect()
  225. )
  226. class Meta:
  227. nullable_fields = ['description', 'mime_type', 'file_extension']
  228. class ExportTemplateFilterForm(BootstrapMixin, forms.Form):
  229. field_groups = [
  230. ['q'],
  231. ['content_type', 'mime_type'],
  232. ['file_extension', 'as_attachment'],
  233. ]
  234. q = forms.CharField(
  235. required=False,
  236. widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
  237. label=_('Search')
  238. )
  239. content_type = ContentTypeChoiceField(
  240. queryset=ContentType.objects.all(),
  241. limit_choices_to=FeatureQuery('custom_fields')
  242. )
  243. mime_type = forms.CharField(
  244. required=False
  245. )
  246. file_extension = forms.CharField(
  247. required=False
  248. )
  249. as_attachment = forms.NullBooleanField(
  250. required=False,
  251. widget=StaticSelect(
  252. choices=BOOLEAN_WITH_BLANK_CHOICES
  253. )
  254. )
  255. #
  256. # Webhooks
  257. #
  258. class WebhookForm(BootstrapMixin, forms.ModelForm):
  259. content_types = ContentTypeMultipleChoiceField(
  260. queryset=ContentType.objects.all(),
  261. limit_choices_to=FeatureQuery('webhooks')
  262. )
  263. class Meta:
  264. model = Webhook
  265. fields = '__all__'
  266. fieldsets = (
  267. ('Webhook', ('name', 'enabled')),
  268. ('Assigned Models', ('content_types',)),
  269. ('Events', ('type_create', 'type_update', 'type_delete')),
  270. ('HTTP Request', (
  271. 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
  272. )),
  273. ('SSL', ('ssl_verification', 'ca_file_path')),
  274. )
  275. class WebhookCSVForm(CSVModelForm):
  276. content_types = CSVMultipleContentTypeField(
  277. queryset=ContentType.objects.all(),
  278. limit_choices_to=FeatureQuery('webhooks'),
  279. help_text="One or more assigned object types"
  280. )
  281. class Meta:
  282. model = Webhook
  283. fields = (
  284. 'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'payload_url',
  285. 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification',
  286. 'ca_file_path'
  287. )
  288. class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
  289. pk = forms.ModelMultipleChoiceField(
  290. queryset=Webhook.objects.all(),
  291. widget=forms.MultipleHiddenInput
  292. )
  293. enabled = forms.NullBooleanField(
  294. required=False,
  295. widget=BulkEditNullBooleanSelect()
  296. )
  297. type_create = forms.NullBooleanField(
  298. required=False,
  299. widget=BulkEditNullBooleanSelect()
  300. )
  301. type_update = forms.NullBooleanField(
  302. required=False,
  303. widget=BulkEditNullBooleanSelect()
  304. )
  305. type_delete = forms.NullBooleanField(
  306. required=False,
  307. widget=BulkEditNullBooleanSelect()
  308. )
  309. http_method = forms.ChoiceField(
  310. choices=WebhookHttpMethodChoices,
  311. required=False
  312. )
  313. payload_url = forms.CharField(
  314. required=False
  315. )
  316. ssl_verification = forms.NullBooleanField(
  317. required=False,
  318. widget=BulkEditNullBooleanSelect()
  319. )
  320. secret = forms.CharField(
  321. required=False
  322. )
  323. ca_file_path = forms.CharField(
  324. required=False
  325. )
  326. class Meta:
  327. nullable_fields = ['secret', 'ca_file_path']
  328. class WebhookFilterForm(BootstrapMixin, forms.Form):
  329. field_groups = [
  330. ['q'],
  331. ['content_types', 'http_method'],
  332. ['enabled', 'type_create', 'type_update', 'type_delete'],
  333. ]
  334. q = forms.CharField(
  335. required=False,
  336. widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
  337. label=_('Search')
  338. )
  339. content_types = ContentTypeMultipleChoiceField(
  340. queryset=ContentType.objects.all(),
  341. limit_choices_to=FeatureQuery('custom_fields')
  342. )
  343. http_method = forms.MultipleChoiceField(
  344. choices=WebhookHttpMethodChoices,
  345. required=False,
  346. widget=StaticSelectMultiple()
  347. )
  348. enabled = forms.NullBooleanField(
  349. required=False,
  350. widget=StaticSelect(
  351. choices=BOOLEAN_WITH_BLANK_CHOICES
  352. )
  353. )
  354. type_create = forms.NullBooleanField(
  355. required=False,
  356. widget=StaticSelect(
  357. choices=BOOLEAN_WITH_BLANK_CHOICES
  358. )
  359. )
  360. type_update = forms.NullBooleanField(
  361. required=False,
  362. widget=StaticSelect(
  363. choices=BOOLEAN_WITH_BLANK_CHOICES
  364. )
  365. )
  366. type_delete = forms.NullBooleanField(
  367. required=False,
  368. widget=StaticSelect(
  369. choices=BOOLEAN_WITH_BLANK_CHOICES
  370. )
  371. )
  372. #
  373. # Custom field models
  374. #
  375. class CustomFieldsMixin:
  376. """
  377. Extend a Form to include custom field support.
  378. """
  379. def __init__(self, *args, **kwargs):
  380. self.custom_fields = []
  381. super().__init__(*args, **kwargs)
  382. self._append_customfield_fields()
  383. def _get_content_type(self):
  384. """
  385. Return the ContentType of the form's model.
  386. """
  387. if not hasattr(self, 'model'):
  388. raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.")
  389. return ContentType.objects.get_for_model(self.model)
  390. def _get_form_field(self, customfield):
  391. return customfield.to_form_field()
  392. def _append_customfield_fields(self):
  393. """
  394. Append form fields for all CustomFields assigned to this object type.
  395. """
  396. content_type = self._get_content_type()
  397. # Append form fields; assign initial values if modifying and existing object
  398. for customfield in CustomField.objects.filter(content_types=content_type):
  399. field_name = f'cf_{customfield.name}'
  400. self.fields[field_name] = self._get_form_field(customfield)
  401. # Annotate the field in the list of CustomField form fields
  402. self.custom_fields.append(field_name)
  403. class CustomFieldModelForm(CustomFieldsMixin, forms.ModelForm):
  404. """
  405. Extend ModelForm to include custom field support.
  406. """
  407. def _get_content_type(self):
  408. return ContentType.objects.get_for_model(self._meta.model)
  409. def _get_form_field(self, customfield):
  410. if self.instance.pk:
  411. form_field = customfield.to_form_field(set_initial=False)
  412. form_field.initial = self.instance.custom_field_data.get(customfield.name, None)
  413. return form_field
  414. return customfield.to_form_field()
  415. def clean(self):
  416. # Save custom field data on instance
  417. for cf_name in self.custom_fields:
  418. self.instance.custom_field_data[cf_name[3:]] = self.cleaned_data.get(cf_name)
  419. return super().clean()
  420. class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm):
  421. def _get_form_field(self, customfield):
  422. return customfield.to_form_field(for_csv_import=True)
  423. class CustomFieldModelBulkEditForm(BulkEditForm):
  424. def __init__(self, *args, **kwargs):
  425. super().__init__(*args, **kwargs)
  426. self.custom_fields = []
  427. self.obj_type = ContentType.objects.get_for_model(self.model)
  428. # Add all applicable CustomFields to the form
  429. custom_fields = CustomField.objects.filter(content_types=self.obj_type)
  430. for cf in custom_fields:
  431. # Annotate non-required custom fields as nullable
  432. if not cf.required:
  433. self.nullable_fields.append(cf.name)
  434. self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False)
  435. # Annotate this as a custom field
  436. self.custom_fields.append(cf.name)
  437. class CustomFieldModelFilterForm(forms.Form):
  438. def __init__(self, *args, **kwargs):
  439. self.obj_type = ContentType.objects.get_for_model(self.model)
  440. super().__init__(*args, **kwargs)
  441. # Add all applicable CustomFields to the form
  442. custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude(
  443. filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
  444. )
  445. for cf in custom_fields:
  446. field_name = 'cf_{}'.format(cf.name)
  447. self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False)
  448. #
  449. # Tags
  450. #
  451. class TagForm(BootstrapMixin, forms.ModelForm):
  452. slug = SlugField()
  453. class Meta:
  454. model = Tag
  455. fields = [
  456. 'name', 'slug', 'color', 'description'
  457. ]
  458. fieldsets = (
  459. ('Tag', ('name', 'slug', 'color', 'description')),
  460. )
  461. class TagCSVForm(CSVModelForm):
  462. slug = SlugField()
  463. class Meta:
  464. model = Tag
  465. fields = ('name', 'slug', 'color', 'description')
  466. help_texts = {
  467. 'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
  468. }
  469. class AddRemoveTagsForm(forms.Form):
  470. def __init__(self, *args, **kwargs):
  471. super().__init__(*args, **kwargs)
  472. # Add add/remove tags fields
  473. self.fields['add_tags'] = DynamicModelMultipleChoiceField(
  474. queryset=Tag.objects.all(),
  475. required=False
  476. )
  477. self.fields['remove_tags'] = DynamicModelMultipleChoiceField(
  478. queryset=Tag.objects.all(),
  479. required=False
  480. )
  481. class TagFilterForm(BootstrapMixin, forms.Form):
  482. model = Tag
  483. q = forms.CharField(
  484. required=False,
  485. label=_('Search')
  486. )
  487. content_type_id = ContentTypeMultipleChoiceField(
  488. queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
  489. required=False,
  490. label=_('Tagged object type')
  491. )
  492. class TagBulkEditForm(BootstrapMixin, BulkEditForm):
  493. pk = forms.ModelMultipleChoiceField(
  494. queryset=Tag.objects.all(),
  495. widget=forms.MultipleHiddenInput
  496. )
  497. color = ColorField(
  498. required=False
  499. )
  500. description = forms.CharField(
  501. max_length=200,
  502. required=False
  503. )
  504. class Meta:
  505. nullable_fields = ['description']
  506. #
  507. # Config contexts
  508. #
  509. class ConfigContextForm(BootstrapMixin, forms.ModelForm):
  510. regions = DynamicModelMultipleChoiceField(
  511. queryset=Region.objects.all(),
  512. required=False
  513. )
  514. site_groups = DynamicModelMultipleChoiceField(
  515. queryset=SiteGroup.objects.all(),
  516. required=False
  517. )
  518. sites = DynamicModelMultipleChoiceField(
  519. queryset=Site.objects.all(),
  520. required=False
  521. )
  522. device_types = DynamicModelMultipleChoiceField(
  523. queryset=DeviceType.objects.all(),
  524. required=False
  525. )
  526. roles = DynamicModelMultipleChoiceField(
  527. queryset=DeviceRole.objects.all(),
  528. required=False
  529. )
  530. platforms = DynamicModelMultipleChoiceField(
  531. queryset=Platform.objects.all(),
  532. required=False
  533. )
  534. cluster_groups = DynamicModelMultipleChoiceField(
  535. queryset=ClusterGroup.objects.all(),
  536. required=False
  537. )
  538. clusters = DynamicModelMultipleChoiceField(
  539. queryset=Cluster.objects.all(),
  540. required=False
  541. )
  542. tenant_groups = DynamicModelMultipleChoiceField(
  543. queryset=TenantGroup.objects.all(),
  544. required=False
  545. )
  546. tenants = DynamicModelMultipleChoiceField(
  547. queryset=Tenant.objects.all(),
  548. required=False
  549. )
  550. tags = DynamicModelMultipleChoiceField(
  551. queryset=Tag.objects.all(),
  552. required=False
  553. )
  554. data = JSONField(
  555. label=''
  556. )
  557. class Meta:
  558. model = ConfigContext
  559. fields = (
  560. 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types',
  561. 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
  562. )
  563. class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
  564. pk = forms.ModelMultipleChoiceField(
  565. queryset=ConfigContext.objects.all(),
  566. widget=forms.MultipleHiddenInput
  567. )
  568. weight = forms.IntegerField(
  569. required=False,
  570. min_value=0
  571. )
  572. is_active = forms.NullBooleanField(
  573. required=False,
  574. widget=BulkEditNullBooleanSelect()
  575. )
  576. description = forms.CharField(
  577. required=False,
  578. max_length=100
  579. )
  580. class Meta:
  581. nullable_fields = [
  582. 'description',
  583. ]
  584. class ConfigContextFilterForm(BootstrapMixin, forms.Form):
  585. field_order = [
  586. 'q', 'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id',
  587. 'cluster_id', 'tenant_group_id', 'tenant_id',
  588. ]
  589. field_groups = [
  590. ['q'],
  591. ['region_id', 'site_group_id', 'site_id'],
  592. ['device_type_id', 'role_id', 'platform_id'],
  593. ['cluster_group_id', 'cluster_id'],
  594. ['tenant_group_id', 'tenant_id', 'tag']
  595. ]
  596. q = forms.CharField(
  597. required=False,
  598. widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
  599. label=_('Search')
  600. )
  601. region_id = DynamicModelMultipleChoiceField(
  602. queryset=Region.objects.all(),
  603. required=False,
  604. label=_('Regions'),
  605. fetch_trigger='open'
  606. )
  607. site_group_id = DynamicModelMultipleChoiceField(
  608. queryset=SiteGroup.objects.all(),
  609. required=False,
  610. label=_('Site groups'),
  611. fetch_trigger='open'
  612. )
  613. site_id = DynamicModelMultipleChoiceField(
  614. queryset=Site.objects.all(),
  615. required=False,
  616. label=_('Sites'),
  617. fetch_trigger='open'
  618. )
  619. device_type_id = DynamicModelMultipleChoiceField(
  620. queryset=DeviceType.objects.all(),
  621. required=False,
  622. label=_('Device types'),
  623. fetch_trigger='open'
  624. )
  625. role_id = DynamicModelMultipleChoiceField(
  626. queryset=DeviceRole.objects.all(),
  627. required=False,
  628. label=_('Roles'),
  629. fetch_trigger='open'
  630. )
  631. platform_id = DynamicModelMultipleChoiceField(
  632. queryset=Platform.objects.all(),
  633. required=False,
  634. label=_('Platforms'),
  635. fetch_trigger='open'
  636. )
  637. cluster_group_id = DynamicModelMultipleChoiceField(
  638. queryset=ClusterGroup.objects.all(),
  639. required=False,
  640. label=_('Cluster groups'),
  641. fetch_trigger='open'
  642. )
  643. cluster_id = DynamicModelMultipleChoiceField(
  644. queryset=Cluster.objects.all(),
  645. required=False,
  646. label=_('Clusters'),
  647. fetch_trigger='open'
  648. )
  649. tenant_group_id = DynamicModelMultipleChoiceField(
  650. queryset=TenantGroup.objects.all(),
  651. required=False,
  652. label=_('Tenant groups'),
  653. fetch_trigger='open'
  654. )
  655. tenant_id = DynamicModelMultipleChoiceField(
  656. queryset=Tenant.objects.all(),
  657. required=False,
  658. label=_('Tenant'),
  659. fetch_trigger='open'
  660. )
  661. tag = DynamicModelMultipleChoiceField(
  662. queryset=Tag.objects.all(),
  663. to_field_name='slug',
  664. required=False,
  665. label=_('Tags'),
  666. fetch_trigger='open'
  667. )
  668. #
  669. # Filter form for local config context data
  670. #
  671. class LocalConfigContextFilterForm(forms.Form):
  672. local_context_data = forms.NullBooleanField(
  673. required=False,
  674. label=_('Has local config context data'),
  675. widget=StaticSelect(
  676. choices=BOOLEAN_WITH_BLANK_CHOICES
  677. )
  678. )
  679. #
  680. # Image attachments
  681. #
  682. class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
  683. class Meta:
  684. model = ImageAttachment
  685. fields = [
  686. 'name', 'image',
  687. ]
  688. #
  689. # Journal entries
  690. #
  691. class JournalEntryForm(BootstrapMixin, forms.ModelForm):
  692. comments = CommentField()
  693. kind = forms.ChoiceField(
  694. choices=add_blank_choice(JournalEntryKindChoices),
  695. required=False,
  696. widget=StaticSelect()
  697. )
  698. class Meta:
  699. model = JournalEntry
  700. fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments']
  701. widgets = {
  702. 'assigned_object_type': forms.HiddenInput,
  703. 'assigned_object_id': forms.HiddenInput,
  704. }
  705. class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm):
  706. pk = forms.ModelMultipleChoiceField(
  707. queryset=JournalEntry.objects.all(),
  708. widget=forms.MultipleHiddenInput
  709. )
  710. kind = forms.ChoiceField(
  711. choices=JournalEntryKindChoices,
  712. required=False
  713. )
  714. comments = forms.CharField(
  715. required=False,
  716. widget=forms.Textarea()
  717. )
  718. class Meta:
  719. nullable_fields = []
  720. class JournalEntryFilterForm(BootstrapMixin, forms.Form):
  721. model = JournalEntry
  722. field_groups = [
  723. ['q'],
  724. ['created_before', 'created_after', 'created_by_id'],
  725. ['assigned_object_type_id', 'kind']
  726. ]
  727. q = forms.CharField(
  728. required=False,
  729. widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
  730. label=_('Search')
  731. )
  732. created_after = forms.DateTimeField(
  733. required=False,
  734. label=_('After'),
  735. widget=DateTimePicker()
  736. )
  737. created_before = forms.DateTimeField(
  738. required=False,
  739. label=_('Before'),
  740. widget=DateTimePicker()
  741. )
  742. created_by_id = DynamicModelMultipleChoiceField(
  743. queryset=User.objects.all(),
  744. required=False,
  745. label=_('User'),
  746. widget=APISelectMultiple(
  747. api_url='/api/users/users/',
  748. ),
  749. fetch_trigger='open'
  750. )
  751. assigned_object_type_id = DynamicModelMultipleChoiceField(
  752. queryset=ContentType.objects.all(),
  753. required=False,
  754. label=_('Object Type'),
  755. widget=APISelectMultiple(
  756. api_url='/api/extras/content-types/',
  757. ),
  758. fetch_trigger='open'
  759. )
  760. kind = forms.ChoiceField(
  761. choices=add_blank_choice(JournalEntryKindChoices),
  762. required=False,
  763. widget=StaticSelect()
  764. )
  765. #
  766. # Change logging
  767. #
  768. class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
  769. model = ObjectChange
  770. field_groups = [
  771. ['q'],
  772. ['time_before', 'time_after', 'action'],
  773. ['user_id', 'changed_object_type_id'],
  774. ]
  775. q = forms.CharField(
  776. required=False,
  777. widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
  778. label=_('Search')
  779. )
  780. time_after = forms.DateTimeField(
  781. required=False,
  782. label=_('After'),
  783. widget=DateTimePicker()
  784. )
  785. time_before = forms.DateTimeField(
  786. required=False,
  787. label=_('Before'),
  788. widget=DateTimePicker()
  789. )
  790. action = forms.ChoiceField(
  791. choices=add_blank_choice(ObjectChangeActionChoices),
  792. required=False,
  793. widget=StaticSelect()
  794. )
  795. user_id = DynamicModelMultipleChoiceField(
  796. queryset=User.objects.all(),
  797. required=False,
  798. label=_('User'),
  799. widget=APISelectMultiple(
  800. api_url='/api/users/users/',
  801. ),
  802. fetch_trigger='open'
  803. )
  804. changed_object_type_id = DynamicModelMultipleChoiceField(
  805. queryset=ContentType.objects.all(),
  806. required=False,
  807. label=_('Object Type'),
  808. widget=APISelectMultiple(
  809. api_url='/api/extras/content-types/',
  810. ),
  811. fetch_trigger='open'
  812. )
  813. #
  814. # Scripts
  815. #
  816. class ScriptForm(BootstrapMixin, forms.Form):
  817. _commit = forms.BooleanField(
  818. required=False,
  819. initial=True,
  820. label="Commit changes",
  821. help_text="Commit changes to the database (uncheck for a dry-run)"
  822. )
  823. def __init__(self, *args, **kwargs):
  824. super().__init__(*args, **kwargs)
  825. # Move _commit to the end of the form
  826. commit = self.fields.pop('_commit')
  827. self.fields['_commit'] = commit
  828. @property
  829. def requires_input(self):
  830. """
  831. A boolean indicating whether the form requires user input (ignore the _commit field).
  832. """
  833. return bool(len(self.fields) > 1)