2
0

model_forms.py 23 KB

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