model_forms.py 24 KB

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