forms.py 28 KB

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