model_forms.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  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. extra_choices = forms.CharField(
  149. widget=ChoicesWidget(),
  150. required=False,
  151. help_text=mark_safe(_(
  152. 'Enter one choice per line. An optional label may be specified for each choice by appending it with a '
  153. 'colon. Example:'
  154. ) + ' <code>choice1:First Choice</code>')
  155. )
  156. class Meta:
  157. model = CustomFieldChoiceSet
  158. fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically')
  159. def __init__(self, *args, initial=None, **kwargs):
  160. super().__init__(*args, initial=initial, **kwargs)
  161. # Escape colons in extra_choices
  162. if 'extra_choices' in self.initial and self.initial['extra_choices']:
  163. choices = []
  164. for choice in self.initial['extra_choices']:
  165. choice = (choice[0].replace(':', '\\:'), choice[1].replace(':', '\\:'))
  166. choices.append(choice)
  167. self.initial['extra_choices'] = choices
  168. def clean_extra_choices(self):
  169. data = []
  170. for line in self.cleaned_data['extra_choices'].splitlines():
  171. try:
  172. value, label = re.split(r'(?<!\\):', line, maxsplit=1)
  173. value = value.replace('\\:', ':')
  174. label = label.replace('\\:', ':')
  175. except ValueError:
  176. value, label = line, line
  177. data.append((value.strip(), label.strip()))
  178. return data
  179. class CustomLinkForm(forms.ModelForm):
  180. object_types = ContentTypeMultipleChoiceField(
  181. label=_('Object types'),
  182. queryset=ObjectType.objects.with_feature('custom_links')
  183. )
  184. fieldsets = (
  185. FieldSet(
  186. 'name', 'object_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window',
  187. name=_('Custom Link')
  188. ),
  189. FieldSet('link_text', 'link_url', name=_('Templates')),
  190. )
  191. class Meta:
  192. model = CustomLink
  193. fields = '__all__'
  194. widgets = {
  195. 'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
  196. 'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
  197. }
  198. help_texts = {
  199. 'link_text': _(
  200. "Jinja2 template code for the link text. Reference the object as {example}. Links "
  201. "which render as empty text will not be displayed."
  202. ).format(example="<code>{{ object }}</code>"),
  203. 'link_url': _(
  204. "Jinja2 template code for the link URL. Reference the object as {example}."
  205. ).format(example="<code>{{ object }}</code>"),
  206. }
  207. class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
  208. object_types = ContentTypeMultipleChoiceField(
  209. label=_('Object types'),
  210. queryset=ObjectType.objects.with_feature('export_templates')
  211. )
  212. template_code = forms.CharField(
  213. label=_('Template code'),
  214. required=False,
  215. widget=forms.Textarea(attrs={'class': 'font-monospace'})
  216. )
  217. fieldsets = (
  218. FieldSet('name', 'object_types', 'description', 'template_code', name=_('Export Template')),
  219. FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
  220. FieldSet(
  221. 'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', name=_('Rendering')
  222. ),
  223. )
  224. class Meta:
  225. model = ExportTemplate
  226. fields = '__all__'
  227. def __init__(self, *args, **kwargs):
  228. super().__init__(*args, **kwargs)
  229. # Disable data field when a DataFile has been set
  230. if self.instance.data_file:
  231. self.fields['template_code'].widget.attrs['readonly'] = True
  232. self.fields['template_code'].help_text = _(
  233. 'Template content is populated from the remote source selected below.'
  234. )
  235. def clean(self):
  236. super().clean()
  237. if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
  238. raise forms.ValidationError(_("Must specify either local content or a data file"))
  239. return self.cleaned_data
  240. class SavedFilterForm(forms.ModelForm):
  241. slug = SlugField()
  242. object_types = ContentTypeMultipleChoiceField(
  243. label=_('Object types'),
  244. queryset=ObjectType.objects.all()
  245. )
  246. parameters = JSONField()
  247. fieldsets = (
  248. FieldSet('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', name=_('Saved Filter')),
  249. FieldSet('parameters', name=_('Parameters')),
  250. )
  251. class Meta:
  252. model = SavedFilter
  253. exclude = ('user',)
  254. def __init__(self, *args, initial=None, **kwargs):
  255. # Convert any parameters delivered via initial data to JSON data
  256. if initial and 'parameters' in initial:
  257. if type(initial['parameters']) is str:
  258. initial['parameters'] = json.loads(initial['parameters'])
  259. super().__init__(*args, initial=initial, **kwargs)
  260. class BookmarkForm(forms.ModelForm):
  261. object_type = ContentTypeChoiceField(
  262. label=_('Object type'),
  263. queryset=ObjectType.objects.with_feature('bookmarks')
  264. )
  265. class Meta:
  266. model = Bookmark
  267. fields = ('object_type', 'object_id')
  268. class NotificationGroupForm(forms.ModelForm):
  269. groups = DynamicModelMultipleChoiceField(
  270. label=_('Groups'),
  271. required=False,
  272. queryset=Group.objects.all()
  273. )
  274. users = DynamicModelMultipleChoiceField(
  275. label=_('Users'),
  276. required=False,
  277. queryset=User.objects.all()
  278. )
  279. class Meta:
  280. model = NotificationGroup
  281. fields = ('name', 'description', 'groups', 'users')
  282. def clean(self):
  283. super().clean()
  284. # At least one User or Group must be assigned
  285. if not self.cleaned_data['groups'] and not self.cleaned_data['users']:
  286. raise forms.ValidationError(_("A notification group specify at least one user or group."))
  287. return self.cleaned_data
  288. class SubscriptionForm(forms.ModelForm):
  289. object_type = ContentTypeChoiceField(
  290. label=_('Object type'),
  291. queryset=ObjectType.objects.with_feature('notifications')
  292. )
  293. class Meta:
  294. model = Subscription
  295. fields = ('object_type', 'object_id')
  296. class WebhookForm(NetBoxModelForm):
  297. fieldsets = (
  298. FieldSet('name', 'description', 'tags', name=_('Webhook')),
  299. FieldSet(
  300. 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
  301. name=_('HTTP Request')
  302. ),
  303. FieldSet('ssl_verification', 'ca_file_path', name=_('SSL')),
  304. )
  305. class Meta:
  306. model = Webhook
  307. fields = '__all__'
  308. widgets = {
  309. 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
  310. 'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
  311. }
  312. class EventRuleForm(NetBoxModelForm):
  313. object_types = ContentTypeMultipleChoiceField(
  314. label=_('Object types'),
  315. queryset=ObjectType.objects.with_feature('event_rules'),
  316. )
  317. event_types = forms.MultipleChoiceField(
  318. choices=get_event_type_choices(),
  319. label=_('Event types')
  320. )
  321. action_choice = forms.ChoiceField(
  322. label=_('Action choice'),
  323. choices=[]
  324. )
  325. conditions = JSONField(
  326. required=False,
  327. help_text=_('Enter conditions in <a href="https://json.org/">JSON</a> format.')
  328. )
  329. action_data = JSONField(
  330. required=False,
  331. help_text=_('Enter parameters to pass to the action in <a href="https://json.org/">JSON</a> format.')
  332. )
  333. comments = CommentField()
  334. fieldsets = (
  335. FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')),
  336. FieldSet('event_types', 'conditions', name=_('Triggers')),
  337. FieldSet('action_type', 'action_choice', 'action_data', name=_('Action')),
  338. )
  339. class Meta:
  340. model = EventRule
  341. fields = (
  342. 'object_types', 'name', 'description', 'enabled', 'event_types', 'conditions', 'action_type',
  343. 'action_object_type', 'action_object_id', 'action_data', 'comments', 'tags'
  344. )
  345. widgets = {
  346. 'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
  347. 'action_type': HTMXSelect(),
  348. 'action_object_type': forms.HiddenInput,
  349. 'action_object_id': forms.HiddenInput,
  350. }
  351. def init_script_choice(self):
  352. initial = None
  353. if self.instance.action_type == EventRuleActionChoices.SCRIPT:
  354. script_id = get_field_value(self, 'action_object_id')
  355. initial = Script.objects.get(pk=script_id) if script_id else None
  356. self.fields['action_choice'] = DynamicModelChoiceField(
  357. label=_('Script'),
  358. queryset=Script.objects.all(),
  359. required=True,
  360. initial=initial
  361. )
  362. def init_webhook_choice(self):
  363. initial = None
  364. if self.instance.action_type == EventRuleActionChoices.WEBHOOK:
  365. webhook_id = get_field_value(self, 'action_object_id')
  366. initial = Webhook.objects.get(pk=webhook_id) if webhook_id else None
  367. self.fields['action_choice'] = DynamicModelChoiceField(
  368. label=_('Webhook'),
  369. queryset=Webhook.objects.all(),
  370. required=True,
  371. initial=initial
  372. )
  373. def init_notificationgroup_choice(self):
  374. initial = None
  375. if self.instance.action_type == EventRuleActionChoices.NOTIFICATION:
  376. notificationgroup_id = get_field_value(self, 'action_object_id')
  377. initial = NotificationGroup.objects.get(pk=notificationgroup_id) if notificationgroup_id else None
  378. self.fields['action_choice'] = DynamicModelChoiceField(
  379. label=_('Notification group'),
  380. queryset=NotificationGroup.objects.all(),
  381. required=True,
  382. initial=initial
  383. )
  384. def __init__(self, *args, **kwargs):
  385. super().__init__(*args, **kwargs)
  386. self.fields['action_object_type'].required = False
  387. self.fields['action_object_id'].required = False
  388. # Determine the action type
  389. action_type = get_field_value(self, 'action_type')
  390. if action_type == EventRuleActionChoices.WEBHOOK:
  391. self.init_webhook_choice()
  392. elif action_type == EventRuleActionChoices.SCRIPT:
  393. self.init_script_choice()
  394. elif action_type == EventRuleActionChoices.NOTIFICATION:
  395. self.init_notificationgroup_choice()
  396. def clean(self):
  397. super().clean()
  398. action_choice = self.cleaned_data.get('action_choice')
  399. # Webhook
  400. if self.cleaned_data.get('action_type') == EventRuleActionChoices.WEBHOOK:
  401. self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(action_choice)
  402. self.cleaned_data['action_object_id'] = action_choice.id
  403. # Script
  404. elif self.cleaned_data.get('action_type') == EventRuleActionChoices.SCRIPT:
  405. self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(
  406. Script,
  407. for_concrete_model=False
  408. )
  409. self.cleaned_data['action_object_id'] = action_choice.id
  410. # Notification
  411. elif self.cleaned_data.get('action_type') == EventRuleActionChoices.NOTIFICATION:
  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. return self.cleaned_data
  415. class TagForm(forms.ModelForm):
  416. slug = SlugField()
  417. object_types = ContentTypeMultipleChoiceField(
  418. label=_('Object types'),
  419. queryset=ObjectType.objects.with_feature('tags'),
  420. required=False
  421. )
  422. weight = forms.IntegerField(
  423. label=_('Weight'),
  424. required=False
  425. )
  426. fieldsets = (
  427. FieldSet('name', 'slug', 'color', 'weight', 'description', 'object_types', name=_('Tag')),
  428. )
  429. class Meta:
  430. model = Tag
  431. fields = [
  432. 'name', 'slug', 'color', 'weight', 'description', 'object_types',
  433. ]
  434. class ConfigContextForm(SyncedDataMixin, forms.ModelForm):
  435. regions = DynamicModelMultipleChoiceField(
  436. label=_('Regions'),
  437. queryset=Region.objects.all(),
  438. required=False
  439. )
  440. site_groups = DynamicModelMultipleChoiceField(
  441. label=_('Site groups'),
  442. queryset=SiteGroup.objects.all(),
  443. required=False
  444. )
  445. sites = DynamicModelMultipleChoiceField(
  446. label=_('Sites'),
  447. queryset=Site.objects.all(),
  448. required=False
  449. )
  450. locations = DynamicModelMultipleChoiceField(
  451. label=_('Locations'),
  452. queryset=Location.objects.all(),
  453. required=False
  454. )
  455. device_types = DynamicModelMultipleChoiceField(
  456. label=_('Device types'),
  457. queryset=DeviceType.objects.all(),
  458. required=False
  459. )
  460. roles = DynamicModelMultipleChoiceField(
  461. label=_('Roles'),
  462. queryset=DeviceRole.objects.all(),
  463. required=False
  464. )
  465. platforms = DynamicModelMultipleChoiceField(
  466. label=_('Platforms'),
  467. queryset=Platform.objects.all(),
  468. required=False
  469. )
  470. cluster_types = DynamicModelMultipleChoiceField(
  471. label=_('Cluster types'),
  472. queryset=ClusterType.objects.all(),
  473. required=False
  474. )
  475. cluster_groups = DynamicModelMultipleChoiceField(
  476. label=_('Cluster groups'),
  477. queryset=ClusterGroup.objects.all(),
  478. required=False
  479. )
  480. clusters = DynamicModelMultipleChoiceField(
  481. label=_('Clusters'),
  482. queryset=Cluster.objects.all(),
  483. required=False
  484. )
  485. tenant_groups = DynamicModelMultipleChoiceField(
  486. label=_('Tenant groups'),
  487. queryset=TenantGroup.objects.all(),
  488. required=False
  489. )
  490. tenants = DynamicModelMultipleChoiceField(
  491. label=_('Tenants'),
  492. queryset=Tenant.objects.all(),
  493. required=False
  494. )
  495. tags = DynamicModelMultipleChoiceField(
  496. label=_('Tags'),
  497. queryset=Tag.objects.all(),
  498. required=False
  499. )
  500. data = JSONField(
  501. label=_('Data'),
  502. required=False
  503. )
  504. fieldsets = (
  505. FieldSet('name', 'weight', 'description', 'data', 'is_active', name=_('Config Context')),
  506. FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
  507. FieldSet(
  508. 'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
  509. 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
  510. name=_('Assignment')
  511. ),
  512. )
  513. class Meta:
  514. model = ConfigContext
  515. fields = (
  516. 'name', 'weight', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites', 'locations',
  517. 'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
  518. 'tenants', 'tags', 'data_source', 'data_file', 'auto_sync_enabled',
  519. )
  520. def __init__(self, *args, initial=None, **kwargs):
  521. # Convert data delivered via initial data to JSON data
  522. if initial and 'data' in initial:
  523. if type(initial['data']) is str:
  524. initial['data'] = json.loads(initial['data'])
  525. super().__init__(*args, initial=initial, **kwargs)
  526. # Disable data field when a DataFile has been set
  527. if self.instance.data_file:
  528. self.fields['data'].widget.attrs['readonly'] = True
  529. self.fields['data'].help_text = _('Data is populated from the remote source selected below.')
  530. def clean(self):
  531. super().clean()
  532. if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_file'):
  533. raise forms.ValidationError(_("Must specify either local data or a data file"))
  534. return self.cleaned_data
  535. class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
  536. tags = DynamicModelMultipleChoiceField(
  537. label=_('Tags'),
  538. queryset=Tag.objects.all(),
  539. required=False
  540. )
  541. template_code = forms.CharField(
  542. label=_('Template code'),
  543. required=False,
  544. widget=forms.Textarea(attrs={'class': 'font-monospace'})
  545. )
  546. fieldsets = (
  547. FieldSet('name', 'description', 'tags', 'template_code', name=_('Config Template')),
  548. FieldSet('data_source', 'data_file', 'auto_sync_enabled', name=_('Data Source')),
  549. FieldSet(
  550. 'mime_type', 'file_name', 'file_extension', 'environment_params', 'as_attachment', name=_('Rendering')
  551. ),
  552. )
  553. class Meta:
  554. model = ConfigTemplate
  555. fields = '__all__'
  556. widgets = {
  557. 'environment_params': forms.Textarea(attrs={'rows': 5})
  558. }
  559. def __init__(self, *args, **kwargs):
  560. super().__init__(*args, **kwargs)
  561. # Disable content field when a DataFile has been set
  562. if self.instance.data_file:
  563. self.fields['template_code'].widget.attrs['readonly'] = True
  564. self.fields['template_code'].help_text = _(
  565. 'Template content is populated from the remote source selected below.'
  566. )
  567. def clean(self):
  568. super().clean()
  569. if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
  570. raise forms.ValidationError(_("Must specify either local content or a data file"))
  571. return self.cleaned_data
  572. class ImageAttachmentForm(forms.ModelForm):
  573. fieldsets = (
  574. FieldSet(ObjectAttribute('parent'), 'name', 'image'),
  575. )
  576. class Meta:
  577. model = ImageAttachment
  578. fields = [
  579. 'name', 'image',
  580. ]
  581. class JournalEntryForm(NetBoxModelForm):
  582. kind = forms.ChoiceField(
  583. label=_('Kind'),
  584. choices=add_blank_choice(JournalEntryKindChoices),
  585. required=False
  586. )
  587. comments = CommentField()
  588. class Meta:
  589. model = JournalEntry
  590. fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'tags', 'comments']
  591. widgets = {
  592. 'assigned_object_type': forms.HiddenInput,
  593. 'assigned_object_id': forms.HiddenInput,
  594. }