model_forms.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
  1. import json
  2. import re
  3. from django import forms
  4. from django.contrib.postgres.forms import SimpleArrayField
  5. from django.utils.safestring import mark_safe
  6. from django.utils.translation import gettext_lazy as _
  7. from core.forms.mixins import SyncedDataMixin
  8. from core.models import ObjectType
  9. from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
  10. from extras.choices import *
  11. from extras.models import *
  12. from netbox.events import get_event_type_choices
  13. from netbox.forms import NetBoxModelForm
  14. from tenancy.models import Tenant, TenantGroup
  15. from users.models import Group, User
  16. from utilities.forms import get_field_value
  17. from utilities.forms.fields import (
  18. CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
  19. DynamicModelMultipleChoiceField, JSONField, SlugField,
  20. )
  21. from utilities.forms.rendering import FieldSet, ObjectAttribute
  22. from utilities.forms.widgets import ChoicesWidget, HTMXSelect
  23. from utilities.tables import get_table_for_model
  24. from virtualization.models import Cluster, ClusterGroup, ClusterType
  25. __all__ = (
  26. 'BookmarkForm',
  27. 'ConfigContextForm',
  28. 'ConfigTemplateForm',
  29. 'CustomFieldChoiceSetForm',
  30. 'CustomFieldForm',
  31. 'CustomLinkForm',
  32. 'EventRuleForm',
  33. 'ExportTemplateForm',
  34. 'ImageAttachmentForm',
  35. 'JournalEntryForm',
  36. 'NotificationGroupForm',
  37. 'SavedFilterForm',
  38. 'SubscriptionForm',
  39. 'TableConfigForm',
  40. 'TagForm',
  41. 'WebhookForm',
  42. )
  43. class CustomFieldForm(forms.ModelForm):
  44. object_types = ContentTypeMultipleChoiceField(
  45. label=_('Object types'),
  46. queryset=ObjectType.objects.with_feature('custom_fields'),
  47. help_text=_("The type(s) of object that have this custom field")
  48. )
  49. default = JSONField(
  50. label=_('Default value'),
  51. required=False
  52. )
  53. related_object_type = ContentTypeChoiceField(
  54. label=_('Related object type'),
  55. queryset=ObjectType.objects.public(),
  56. help_text=_("Type of the related object (for object/multi-object fields only)")
  57. )
  58. related_object_filter = JSONField(
  59. label=_('Related object filter'),
  60. required=False,
  61. help_text=_('Specify query parameters as a JSON object.')
  62. )
  63. choice_set = DynamicModelChoiceField(
  64. queryset=CustomFieldChoiceSet.objects.all()
  65. )
  66. comments = CommentField()
  67. fieldsets = (
  68. FieldSet(
  69. 'object_types', 'name', 'label', 'group_name', 'description', 'type', 'required', 'unique', 'default',
  70. name=_('Custom Field')
  71. ),
  72. FieldSet(
  73. 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
  74. ),
  75. )
  76. class Meta:
  77. model = CustomField
  78. fields = '__all__'
  79. help_texts = {
  80. 'type': _(
  81. "The type of data stored in this field. For object/multi-object fields, select the related object "
  82. "type below."
  83. ),
  84. 'description': _("This will be displayed as help text for the form field. Markdown is supported.")
  85. }
  86. def __init__(self, *args, **kwargs):
  87. super().__init__(*args, **kwargs)
  88. # Mimic HTMXSelect()
  89. self.fields['type'].widget.attrs.update({
  90. 'hx-get': '.',
  91. 'hx-include': '#form_fields',
  92. 'hx-target': '#form_fields',
  93. })
  94. # Disable changing the type of a CustomField as it almost universally causes errors if custom field data
  95. # is already present.
  96. if self.instance.pk:
  97. self.fields['type'].disabled = True
  98. field_type = get_field_value(self, 'type')
  99. # Adjust for text fields
  100. if field_type in (
  101. CustomFieldTypeChoices.TYPE_TEXT,
  102. CustomFieldTypeChoices.TYPE_LONGTEXT,
  103. CustomFieldTypeChoices.TYPE_URL
  104. ):
  105. self.fieldsets = (
  106. self.fieldsets[0],
  107. FieldSet('validation_regex', name=_('Validation')),
  108. *self.fieldsets[1:]
  109. )
  110. else:
  111. del self.fields['validation_regex']
  112. # Adjust for numeric fields
  113. if field_type in (
  114. CustomFieldTypeChoices.TYPE_INTEGER,
  115. CustomFieldTypeChoices.TYPE_DECIMAL
  116. ):
  117. self.fieldsets = (
  118. self.fieldsets[0],
  119. FieldSet('validation_minimum', 'validation_maximum', name=_('Validation')),
  120. *self.fieldsets[1:]
  121. )
  122. else:
  123. del self.fields['validation_minimum']
  124. del self.fields['validation_maximum']
  125. # Adjust for object & multi-object fields
  126. if field_type in (
  127. CustomFieldTypeChoices.TYPE_OBJECT,
  128. CustomFieldTypeChoices.TYPE_MULTIOBJECT
  129. ):
  130. self.fieldsets = (
  131. self.fieldsets[0],
  132. FieldSet('related_object_type', 'related_object_filter', name=_('Related Object')),
  133. *self.fieldsets[1:]
  134. )
  135. else:
  136. del self.fields['related_object_type']
  137. del self.fields['related_object_filter']
  138. # Adjust for selection & multi-select fields
  139. if field_type in (
  140. CustomFieldTypeChoices.TYPE_SELECT,
  141. CustomFieldTypeChoices.TYPE_MULTISELECT
  142. ):
  143. self.fieldsets = (
  144. self.fieldsets[0],
  145. FieldSet('choice_set', name=_('Choices')),
  146. *self.fieldsets[1:]
  147. )
  148. else:
  149. del self.fields['choice_set']
  150. class CustomFieldChoiceSetForm(forms.ModelForm):
  151. # TODO: The extra_choices field definition diverge from the CustomFieldChoiceSet model
  152. extra_choices = forms.CharField(
  153. widget=ChoicesWidget(),
  154. required=False,
  155. help_text=mark_safe(_(
  156. 'Enter one choice per line. An optional label may be specified for each choice by appending it with a '
  157. 'colon. Example:'
  158. ) + ' <code>choice1:First Choice</code>')
  159. )
  160. class Meta:
  161. model = CustomFieldChoiceSet
  162. fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically')
  163. def __init__(self, *args, initial=None, **kwargs):
  164. super().__init__(*args, initial=initial, **kwargs)
  165. # TODO: The check for str / list below is to handle difference in extra_choices field definition
  166. # In CustomFieldChoiceSetForm, extra_choices is a CharField but in CustomFieldChoiceSet, it is an ArrayField
  167. # if standardize these, we can simplify this code
  168. # Convert extra_choices Array Field from model to CharField for form
  169. if 'extra_choices' in self.initial and self.initial['extra_choices']:
  170. extra_choices = self.initial['extra_choices']
  171. if isinstance(extra_choices, str):
  172. extra_choices = [extra_choices]
  173. choices = ""
  174. for choice in extra_choices:
  175. # Setup choices in Add Another use case
  176. if isinstance(choice, str):
  177. choice_str = ":".join(choice.replace("'", "").replace(" ", "")[1:-1].split(","))
  178. choices += choice_str + "\n"
  179. # Setup choices in Edit use case
  180. elif isinstance(choice, list):
  181. choice_str = ":".join(choice)
  182. choices += choice_str + "\n"
  183. self.initial['extra_choices'] = choices
  184. def clean_extra_choices(self):
  185. data = []
  186. for line in self.cleaned_data['extra_choices'].splitlines():
  187. try:
  188. value, label = re.split(r'(?<!\\):', line, maxsplit=1)
  189. value = value.replace('\\:', ':')
  190. label = label.replace('\\:', ':')
  191. except ValueError:
  192. value, label = line, line
  193. data.append((value.strip(), label.strip()))
  194. return data
  195. class CustomLinkForm(forms.ModelForm):
  196. object_types = ContentTypeMultipleChoiceField(
  197. label=_('Object types'),
  198. queryset=ObjectType.objects.with_feature('custom_links')
  199. )
  200. fieldsets = (
  201. FieldSet(
  202. 'name', 'object_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window',
  203. name=_('Custom Link')
  204. ),
  205. FieldSet('link_text', 'link_url', name=_('Templates')),
  206. )
  207. class Meta:
  208. model = CustomLink
  209. fields = '__all__'
  210. widgets = {
  211. 'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
  212. 'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
  213. }
  214. help_texts = {
  215. 'link_text': _(
  216. "Jinja2 template code for the link text. Reference the object as {example}. Links "
  217. "which render as empty text will not be displayed."
  218. ).format(example="<code>{{ object }}</code>"),
  219. 'link_url': _(
  220. "Jinja2 template code for the link URL. Reference the object as {example}."
  221. ).format(example="<code>{{ object }}</code>"),
  222. }
  223. class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
  224. object_types = ContentTypeMultipleChoiceField(
  225. label=_('Object types'),
  226. queryset=ObjectType.objects.with_feature('export_templates')
  227. )
  228. template_code = forms.CharField(
  229. label=_('Template code'),
  230. required=False,
  231. widget=forms.Textarea(attrs={'class': 'font-monospace'})
  232. )
  233. fieldsets = (
  234. FieldSet('name', 'object_types', 'description', 'template_code', name=_('Export Template')),
  235. FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
  236. FieldSet(
  237. 'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', name=_('Rendering')
  238. ),
  239. )
  240. class Meta:
  241. model = ExportTemplate
  242. fields = '__all__'
  243. def __init__(self, *args, **kwargs):
  244. super().__init__(*args, **kwargs)
  245. # Disable data field when a DataFile has been set
  246. if self.instance.data_file:
  247. self.fields['template_code'].widget.attrs['readonly'] = True
  248. self.fields['template_code'].help_text = _(
  249. 'Template content is populated from the remote source selected below.'
  250. )
  251. def clean(self):
  252. super().clean()
  253. if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
  254. raise forms.ValidationError(_("Must specify either local content or a data file"))
  255. return self.cleaned_data
  256. class SavedFilterForm(forms.ModelForm):
  257. slug = SlugField()
  258. object_types = ContentTypeMultipleChoiceField(
  259. label=_('Object types'),
  260. queryset=ObjectType.objects.all()
  261. )
  262. parameters = JSONField()
  263. fieldsets = (
  264. FieldSet('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', name=_('Saved Filter')),
  265. FieldSet('parameters', name=_('Parameters')),
  266. )
  267. class Meta:
  268. model = SavedFilter
  269. exclude = ('user',)
  270. def __init__(self, *args, initial=None, **kwargs):
  271. # Convert any parameters delivered via initial data to JSON data
  272. if initial and 'parameters' in initial:
  273. if type(initial['parameters']) is str:
  274. initial['parameters'] = json.loads(initial['parameters'])
  275. super().__init__(*args, initial=initial, **kwargs)
  276. class TableConfigForm(forms.ModelForm):
  277. object_type = ContentTypeChoiceField(
  278. label=_('Object type'),
  279. queryset=ObjectType.objects.all()
  280. )
  281. ordering = SimpleArrayField(
  282. base_field=forms.CharField(),
  283. required=False,
  284. label=_('Ordering'),
  285. help_text=_(
  286. "Enter a comma-separated list of column names. Prepend a name with a hyphen to reverse the order."
  287. )
  288. )
  289. available_columns = SimpleArrayField(
  290. base_field=forms.CharField(),
  291. required=False,
  292. widget=forms.SelectMultiple(
  293. attrs={'size': 10, 'class': 'form-select'}
  294. ),
  295. label=_('Available Columns')
  296. )
  297. columns = SimpleArrayField(
  298. base_field=forms.CharField(),
  299. widget=forms.SelectMultiple(
  300. attrs={'size': 10, 'class': 'form-select select-all'}
  301. ),
  302. label=_('Selected Columns')
  303. )
  304. class Meta:
  305. model = TableConfig
  306. exclude = ('user',)
  307. def __init__(self, data=None, *args, **kwargs):
  308. super().__init__(data, *args, **kwargs)
  309. object_type = ObjectType.objects.get(pk=get_field_value(self, 'object_type'))
  310. model = object_type.model_class()
  311. table_name = get_field_value(self, 'table')
  312. table_class = get_table_for_model(model, table_name)
  313. table = table_class([])
  314. if columns := self._get_columns():
  315. table._set_columns(columns)
  316. # Initialize columns field based on table attributes
  317. self.fields['available_columns'].widget.choices = table.available_columns
  318. self.fields['columns'].widget.choices = table.selected_columns
  319. def _get_columns(self):
  320. if self.is_bound and (columns := self.data.getlist('columns')):
  321. return columns
  322. if 'columns' in self.initial:
  323. columns = self.get_initial_for_field(self.fields['columns'], 'columns')
  324. return columns.split(',') if type(columns) is str else columns
  325. if self.instance is not None:
  326. return self.instance.columns
  327. class BookmarkForm(forms.ModelForm):
  328. object_type = ContentTypeChoiceField(
  329. label=_('Object type'),
  330. queryset=ObjectType.objects.with_feature('bookmarks')
  331. )
  332. class Meta:
  333. model = Bookmark
  334. fields = ('object_type', 'object_id')
  335. class NotificationGroupForm(forms.ModelForm):
  336. groups = DynamicModelMultipleChoiceField(
  337. label=_('Groups'),
  338. required=False,
  339. queryset=Group.objects.all()
  340. )
  341. users = DynamicModelMultipleChoiceField(
  342. label=_('Users'),
  343. required=False,
  344. queryset=User.objects.all()
  345. )
  346. class Meta:
  347. model = NotificationGroup
  348. fields = ('name', 'description', 'groups', 'users')
  349. def clean(self):
  350. super().clean()
  351. # At least one User or Group must be assigned
  352. if not self.cleaned_data['groups'] and not self.cleaned_data['users']:
  353. raise forms.ValidationError(_("A notification group specify at least one user or group."))
  354. return self.cleaned_data
  355. class SubscriptionForm(forms.ModelForm):
  356. object_type = ContentTypeChoiceField(
  357. label=_('Object type'),
  358. queryset=ObjectType.objects.with_feature('notifications')
  359. )
  360. class Meta:
  361. model = Subscription
  362. fields = ('object_type', 'object_id')
  363. class WebhookForm(NetBoxModelForm):
  364. fieldsets = (
  365. FieldSet('name', 'description', 'tags', name=_('Webhook')),
  366. FieldSet(
  367. 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
  368. name=_('HTTP Request')
  369. ),
  370. FieldSet('ssl_verification', 'ca_file_path', name=_('SSL')),
  371. )
  372. class Meta:
  373. model = Webhook
  374. fields = '__all__'
  375. widgets = {
  376. 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
  377. 'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
  378. }
  379. class EventRuleForm(NetBoxModelForm):
  380. object_types = ContentTypeMultipleChoiceField(
  381. label=_('Object types'),
  382. queryset=ObjectType.objects.with_feature('event_rules'),
  383. )
  384. event_types = forms.MultipleChoiceField(
  385. choices=get_event_type_choices(),
  386. label=_('Event types')
  387. )
  388. action_choice = forms.ChoiceField(
  389. label=_('Action choice'),
  390. choices=[]
  391. )
  392. conditions = JSONField(
  393. required=False,
  394. help_text=_('Enter conditions in <a href="https://json.org/">JSON</a> format.')
  395. )
  396. action_data = JSONField(
  397. required=False,
  398. help_text=_('Enter parameters to pass to the action in <a href="https://json.org/">JSON</a> format.')
  399. )
  400. comments = CommentField()
  401. fieldsets = (
  402. FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')),
  403. FieldSet('event_types', 'conditions', name=_('Triggers')),
  404. FieldSet('action_type', 'action_choice', 'action_data', name=_('Action')),
  405. )
  406. class Meta:
  407. model = EventRule
  408. fields = (
  409. 'object_types', 'name', 'description', 'enabled', 'event_types', 'conditions', 'action_type',
  410. 'action_object_type', 'action_object_id', 'action_data', 'comments', 'tags'
  411. )
  412. widgets = {
  413. 'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
  414. 'action_type': HTMXSelect(),
  415. 'action_object_type': forms.HiddenInput,
  416. 'action_object_id': forms.HiddenInput,
  417. }
  418. def init_script_choice(self):
  419. initial = None
  420. if self.instance.action_type == EventRuleActionChoices.SCRIPT:
  421. script_id = get_field_value(self, 'action_object_id')
  422. initial = Script.objects.get(pk=script_id) if script_id else None
  423. self.fields['action_choice'] = DynamicModelChoiceField(
  424. label=_('Script'),
  425. queryset=Script.objects.all(),
  426. required=True,
  427. initial=initial
  428. )
  429. def init_webhook_choice(self):
  430. initial = None
  431. if self.instance.action_type == EventRuleActionChoices.WEBHOOK:
  432. webhook_id = get_field_value(self, 'action_object_id')
  433. initial = Webhook.objects.get(pk=webhook_id) if webhook_id else None
  434. self.fields['action_choice'] = DynamicModelChoiceField(
  435. label=_('Webhook'),
  436. queryset=Webhook.objects.all(),
  437. required=True,
  438. initial=initial
  439. )
  440. def init_notificationgroup_choice(self):
  441. initial = None
  442. if self.instance.action_type == EventRuleActionChoices.NOTIFICATION:
  443. notificationgroup_id = get_field_value(self, 'action_object_id')
  444. initial = NotificationGroup.objects.get(pk=notificationgroup_id) if notificationgroup_id else None
  445. self.fields['action_choice'] = DynamicModelChoiceField(
  446. label=_('Notification group'),
  447. queryset=NotificationGroup.objects.all(),
  448. required=True,
  449. initial=initial
  450. )
  451. def __init__(self, *args, **kwargs):
  452. super().__init__(*args, **kwargs)
  453. self.fields['action_object_type'].required = False
  454. self.fields['action_object_id'].required = False
  455. # Determine the action type
  456. action_type = get_field_value(self, 'action_type')
  457. if action_type == EventRuleActionChoices.WEBHOOK:
  458. self.init_webhook_choice()
  459. elif action_type == EventRuleActionChoices.SCRIPT:
  460. self.init_script_choice()
  461. elif action_type == EventRuleActionChoices.NOTIFICATION:
  462. self.init_notificationgroup_choice()
  463. def clean(self):
  464. super().clean()
  465. action_choice = self.cleaned_data.get('action_choice')
  466. # Webhook
  467. if self.cleaned_data.get('action_type') == EventRuleActionChoices.WEBHOOK:
  468. self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(action_choice)
  469. self.cleaned_data['action_object_id'] = action_choice.id
  470. # Script
  471. elif self.cleaned_data.get('action_type') == EventRuleActionChoices.SCRIPT:
  472. self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(
  473. Script,
  474. for_concrete_model=False
  475. )
  476. self.cleaned_data['action_object_id'] = action_choice.id
  477. # Notification
  478. elif self.cleaned_data.get('action_type') == EventRuleActionChoices.NOTIFICATION:
  479. self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(action_choice)
  480. self.cleaned_data['action_object_id'] = action_choice.id
  481. return self.cleaned_data
  482. class TagForm(forms.ModelForm):
  483. slug = SlugField()
  484. object_types = ContentTypeMultipleChoiceField(
  485. label=_('Object types'),
  486. queryset=ObjectType.objects.with_feature('tags'),
  487. required=False
  488. )
  489. weight = forms.IntegerField(
  490. label=_('Weight'),
  491. required=False
  492. )
  493. fieldsets = (
  494. FieldSet('name', 'slug', 'color', 'weight', 'description', 'object_types', name=_('Tag')),
  495. )
  496. class Meta:
  497. model = Tag
  498. fields = [
  499. 'name', 'slug', 'color', 'weight', 'description', 'object_types',
  500. ]
  501. class ConfigContextForm(SyncedDataMixin, forms.ModelForm):
  502. regions = DynamicModelMultipleChoiceField(
  503. label=_('Regions'),
  504. queryset=Region.objects.all(),
  505. required=False
  506. )
  507. site_groups = DynamicModelMultipleChoiceField(
  508. label=_('Site groups'),
  509. queryset=SiteGroup.objects.all(),
  510. required=False
  511. )
  512. sites = DynamicModelMultipleChoiceField(
  513. label=_('Sites'),
  514. queryset=Site.objects.all(),
  515. required=False
  516. )
  517. locations = DynamicModelMultipleChoiceField(
  518. label=_('Locations'),
  519. queryset=Location.objects.all(),
  520. required=False
  521. )
  522. device_types = DynamicModelMultipleChoiceField(
  523. label=_('Device types'),
  524. queryset=DeviceType.objects.all(),
  525. required=False
  526. )
  527. roles = DynamicModelMultipleChoiceField(
  528. label=_('Roles'),
  529. queryset=DeviceRole.objects.all(),
  530. required=False
  531. )
  532. platforms = DynamicModelMultipleChoiceField(
  533. label=_('Platforms'),
  534. queryset=Platform.objects.all(),
  535. required=False
  536. )
  537. cluster_types = DynamicModelMultipleChoiceField(
  538. label=_('Cluster types'),
  539. queryset=ClusterType.objects.all(),
  540. required=False
  541. )
  542. cluster_groups = DynamicModelMultipleChoiceField(
  543. label=_('Cluster groups'),
  544. queryset=ClusterGroup.objects.all(),
  545. required=False
  546. )
  547. clusters = DynamicModelMultipleChoiceField(
  548. label=_('Clusters'),
  549. queryset=Cluster.objects.all(),
  550. required=False
  551. )
  552. tenant_groups = DynamicModelMultipleChoiceField(
  553. label=_('Tenant groups'),
  554. queryset=TenantGroup.objects.all(),
  555. required=False
  556. )
  557. tenants = DynamicModelMultipleChoiceField(
  558. label=_('Tenants'),
  559. queryset=Tenant.objects.all(),
  560. required=False
  561. )
  562. tags = DynamicModelMultipleChoiceField(
  563. label=_('Tags'),
  564. queryset=Tag.objects.all(),
  565. required=False
  566. )
  567. data = JSONField(
  568. label=_('Data'),
  569. required=False
  570. )
  571. fieldsets = (
  572. FieldSet('name', 'weight', 'description', 'data', 'is_active', name=_('Config Context')),
  573. FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
  574. FieldSet(
  575. 'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
  576. 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
  577. name=_('Assignment')
  578. ),
  579. )
  580. class Meta:
  581. model = ConfigContext
  582. fields = (
  583. 'name', 'weight', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites', 'locations',
  584. 'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
  585. 'tenants', 'tags', 'data_source', 'data_file', 'auto_sync_enabled',
  586. )
  587. def __init__(self, *args, initial=None, **kwargs):
  588. # Convert data delivered via initial data to JSON data
  589. if initial and 'data' in initial:
  590. if type(initial['data']) is str:
  591. initial['data'] = json.loads(initial['data'])
  592. super().__init__(*args, initial=initial, **kwargs)
  593. # Disable data field when a DataFile has been set
  594. if self.instance.data_file:
  595. self.fields['data'].widget.attrs['readonly'] = True
  596. self.fields['data'].help_text = _('Data is populated from the remote source selected below.')
  597. def clean(self):
  598. super().clean()
  599. if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_file'):
  600. raise forms.ValidationError(_("Must specify either local data or a data file"))
  601. return self.cleaned_data
  602. class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
  603. tags = DynamicModelMultipleChoiceField(
  604. label=_('Tags'),
  605. queryset=Tag.objects.all(),
  606. required=False
  607. )
  608. template_code = forms.CharField(
  609. label=_('Template code'),
  610. required=False,
  611. widget=forms.Textarea(attrs={'class': 'font-monospace'})
  612. )
  613. fieldsets = (
  614. FieldSet('name', 'description', 'tags', 'template_code', name=_('Config Template')),
  615. FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
  616. FieldSet(
  617. 'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', name=_('Rendering')
  618. ),
  619. )
  620. class Meta:
  621. model = ConfigTemplate
  622. fields = '__all__'
  623. widgets = {
  624. 'environment_params': forms.Textarea(attrs={'rows': 5})
  625. }
  626. def __init__(self, *args, **kwargs):
  627. super().__init__(*args, **kwargs)
  628. # Disable content field when a DataFile has been set
  629. if self.instance.data_file:
  630. self.fields['template_code'].widget.attrs['readonly'] = True
  631. self.fields['template_code'].help_text = _(
  632. 'Template content is populated from the remote source selected below.'
  633. )
  634. def clean(self):
  635. super().clean()
  636. if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
  637. raise forms.ValidationError(_("Must specify either local content or a data file"))
  638. return self.cleaned_data
  639. class ImageAttachmentForm(forms.ModelForm):
  640. fieldsets = (
  641. FieldSet(ObjectAttribute('parent'), 'name', 'image'),
  642. )
  643. class Meta:
  644. model = ImageAttachment
  645. fields = [
  646. 'name', 'image',
  647. ]
  648. class JournalEntryForm(NetBoxModelForm):
  649. kind = forms.ChoiceField(
  650. label=_('Kind'),
  651. choices=JournalEntryKindChoices
  652. )
  653. comments = CommentField()
  654. class Meta:
  655. model = JournalEntry
  656. fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'tags', 'comments']
  657. widgets = {
  658. 'assigned_object_type': forms.HiddenInput,
  659. 'assigned_object_id': forms.HiddenInput,
  660. }