| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- import json
- from django import forms
- from django.conf import settings
- from django.db.models import Q
- from django.contrib.contenttypes.models import ContentType
- from django.utils.translation import gettext as _
- from core.forms.mixins import SyncedDataMixin
- from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
- from extras.choices import *
- from extras.models import *
- from extras.utils import FeatureQuery
- from netbox.config import get_config, PARAMS
- from netbox.forms import NetBoxModelForm
- from tenancy.models import Tenant, TenantGroup
- from utilities.forms import BootstrapMixin, add_blank_choice
- from utilities.forms.fields import (
- CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField,
- SlugField,
- )
- from virtualization.models import Cluster, ClusterGroup, ClusterType
- __all__ = (
- 'BookmarkForm',
- 'ConfigContextForm',
- 'ConfigRevisionForm',
- 'ConfigTemplateForm',
- 'CustomFieldForm',
- 'CustomLinkForm',
- 'ExportTemplateForm',
- 'ImageAttachmentForm',
- 'JournalEntryForm',
- 'SavedFilterForm',
- 'TagForm',
- 'WebhookForm',
- )
- class CustomFieldForm(BootstrapMixin, forms.ModelForm):
- content_types = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.all(),
- limit_choices_to=FeatureQuery('custom_fields'),
- )
- object_type = ContentTypeChoiceField(
- queryset=ContentType.objects.all(),
- # TODO: Come up with a canonical way to register suitable models
- limit_choices_to=FeatureQuery('webhooks').get_query() | Q(app_label='auth', model__in=['user', 'group']),
- required=False,
- help_text=_("Type of the related object (for object/multi-object fields only)")
- )
- fieldsets = (
- ('Custom Field', (
- 'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description',
- )),
- ('Behavior', ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')),
- ('Values', ('default', 'choices')),
- ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
- )
- class Meta:
- model = CustomField
- fields = '__all__'
- help_texts = {
- 'type': _(
- "The type of data stored in this field. For object/multi-object fields, select the related object "
- "type below."
- )
- }
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # Disable changing the type of a CustomField as it almost universally causes errors if custom field data is already present.
- if self.instance.pk:
- self.fields['type'].disabled = True
- class CustomLinkForm(BootstrapMixin, forms.ModelForm):
- content_types = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.all(),
- limit_choices_to=FeatureQuery('custom_links')
- )
- fieldsets = (
- ('Custom Link', ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
- ('Templates', ('link_text', 'link_url')),
- )
- class Meta:
- model = CustomLink
- fields = '__all__'
- widgets = {
- 'link_text': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'link_url': forms.Textarea(attrs={'class': 'font-monospace'}),
- }
- help_texts = {
- 'link_text': _(
- "Jinja2 template code for the link text. Reference the object as <code>{{ object }}</code>. Links "
- "which render as empty text will not be displayed."
- ),
- 'link_url': _("Jinja2 template code for the link URL. Reference the object as <code>{{ object }}</code>."),
- }
- class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
- content_types = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.all(),
- limit_choices_to=FeatureQuery('export_templates')
- )
- template_code = forms.CharField(
- required=False,
- widget=forms.Textarea(attrs={'class': 'font-monospace'})
- )
- fieldsets = (
- ('Export Template', ('name', 'content_types', 'description', 'template_code')),
- ('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
- ('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
- )
- class Meta:
- model = ExportTemplate
- fields = '__all__'
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # Disable data field when a DataFile has been set
- if self.instance.data_file:
- self.fields['template_code'].widget.attrs['readonly'] = True
- self.fields['template_code'].help_text = _(
- 'Template content is populated from the remote source selected below.'
- )
- def clean(self):
- super().clean()
- if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
- raise forms.ValidationError("Must specify either local content or a data file")
- return self.cleaned_data
- class SavedFilterForm(BootstrapMixin, forms.ModelForm):
- slug = SlugField()
- content_types = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.all()
- )
- parameters = JSONField()
- fieldsets = (
- ('Saved Filter', ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')),
- ('Parameters', ('parameters',)),
- )
- class Meta:
- model = SavedFilter
- exclude = ('user',)
- def __init__(self, *args, initial=None, **kwargs):
- # Convert any parameters delivered via initial data to JSON data
- if initial and 'parameters' in initial:
- if type(initial['parameters']) is str:
- initial['parameters'] = json.loads(initial['parameters'])
- super().__init__(*args, initial=initial, **kwargs)
- class BookmarkForm(BootstrapMixin, forms.ModelForm):
- object_type = ContentTypeChoiceField(
- queryset=ContentType.objects.all(),
- limit_choices_to=FeatureQuery('bookmarks').get_query()
- )
- class Meta:
- model = Bookmark
- fields = ('object_type', 'object_id')
- class WebhookForm(BootstrapMixin, forms.ModelForm):
- content_types = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.all(),
- limit_choices_to=FeatureQuery('webhooks')
- )
- fieldsets = (
- ('Webhook', ('name', 'content_types', 'enabled')),
- ('Events', ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
- ('HTTP Request', (
- 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
- )),
- ('Conditions', ('conditions',)),
- ('SSL', ('ssl_verification', 'ca_file_path')),
- )
- class Meta:
- model = Webhook
- fields = '__all__'
- labels = {
- 'type_create': 'Creations',
- 'type_update': 'Updates',
- 'type_delete': 'Deletions',
- 'type_job_start': 'Job executions',
- 'type_job_end': 'Job terminations',
- }
- widgets = {
- 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
- }
- class TagForm(BootstrapMixin, forms.ModelForm):
- slug = SlugField()
- object_types = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.all(),
- limit_choices_to=FeatureQuery('tags'),
- required=False
- )
- fieldsets = (
- ('Tag', ('name', 'slug', 'color', 'description', 'object_types')),
- )
- class Meta:
- model = Tag
- fields = [
- 'name', 'slug', 'color', 'description', 'object_types',
- ]
- class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
- regions = DynamicModelMultipleChoiceField(
- queryset=Region.objects.all(),
- required=False
- )
- site_groups = DynamicModelMultipleChoiceField(
- queryset=SiteGroup.objects.all(),
- required=False
- )
- sites = DynamicModelMultipleChoiceField(
- queryset=Site.objects.all(),
- required=False
- )
- locations = DynamicModelMultipleChoiceField(
- queryset=Location.objects.all(),
- required=False
- )
- device_types = DynamicModelMultipleChoiceField(
- queryset=DeviceType.objects.all(),
- required=False
- )
- roles = DynamicModelMultipleChoiceField(
- queryset=DeviceRole.objects.all(),
- required=False
- )
- platforms = DynamicModelMultipleChoiceField(
- queryset=Platform.objects.all(),
- required=False
- )
- cluster_types = DynamicModelMultipleChoiceField(
- queryset=ClusterType.objects.all(),
- required=False
- )
- cluster_groups = DynamicModelMultipleChoiceField(
- queryset=ClusterGroup.objects.all(),
- required=False
- )
- clusters = DynamicModelMultipleChoiceField(
- queryset=Cluster.objects.all(),
- required=False
- )
- tenant_groups = DynamicModelMultipleChoiceField(
- queryset=TenantGroup.objects.all(),
- required=False
- )
- tenants = DynamicModelMultipleChoiceField(
- queryset=Tenant.objects.all(),
- required=False
- )
- tags = DynamicModelMultipleChoiceField(
- queryset=Tag.objects.all(),
- required=False
- )
- data = JSONField(
- required=False
- )
- fieldsets = (
- ('Config Context', ('name', 'weight', 'description', 'data', 'is_active')),
- ('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
- ('Assignment', (
- 'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
- 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
- )),
- )
- class Meta:
- model = ConfigContext
- fields = (
- 'name', 'weight', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites', 'locations',
- 'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
- 'tenants', 'tags', 'data_source', 'data_file', 'auto_sync_enabled',
- )
- def __init__(self, *args, initial=None, **kwargs):
- # Convert data delivered via initial data to JSON data
- if initial and 'data' in initial:
- if type(initial['data']) is str:
- initial['data'] = json.loads(initial['data'])
- super().__init__(*args, initial=initial, **kwargs)
- # Disable data field when a DataFile has been set
- if self.instance.data_file:
- self.fields['data'].widget.attrs['readonly'] = True
- self.fields['data'].help_text = _('Data is populated from the remote source selected below.')
- def clean(self):
- super().clean()
- if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_file'):
- raise forms.ValidationError("Must specify either local data or a data file")
- return self.cleaned_data
- class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
- tags = DynamicModelMultipleChoiceField(
- queryset=Tag.objects.all(),
- required=False
- )
- template_code = forms.CharField(
- required=False,
- widget=forms.Textarea(attrs={'class': 'font-monospace'})
- )
- fieldsets = (
- ('Config Template', ('name', 'description', 'environment_params', 'tags')),
- ('Content', ('template_code',)),
- ('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
- )
- class Meta:
- model = ConfigTemplate
- fields = '__all__'
- widgets = {
- 'environment_params': forms.Textarea(attrs={'rows': 5})
- }
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # Disable content field when a DataFile has been set
- if self.instance.data_file:
- self.fields['template_code'].widget.attrs['readonly'] = True
- self.fields['template_code'].help_text = _(
- 'Template content is populated from the remote source selected below.'
- )
- def clean(self):
- super().clean()
- if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
- raise forms.ValidationError("Must specify either local content or a data file")
- return self.cleaned_data
- class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
- class Meta:
- model = ImageAttachment
- fields = [
- 'name', 'image',
- ]
- class JournalEntryForm(NetBoxModelForm):
- kind = forms.ChoiceField(
- choices=add_blank_choice(JournalEntryKindChoices),
- required=False
- )
- comments = CommentField()
- class Meta:
- model = JournalEntry
- fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'tags', 'comments']
- widgets = {
- 'assigned_object_type': forms.HiddenInput,
- 'assigned_object_id': forms.HiddenInput,
- }
- EMPTY_VALUES = ('', None, [], ())
- class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
- def __new__(mcs, name, bases, attrs):
- # Emulate a declared field for each supported configuration parameter
- param_fields = {}
- for param in PARAMS:
- field_kwargs = {
- 'required': False,
- 'label': param.label,
- 'help_text': param.description,
- }
- field_kwargs.update(**param.field_kwargs)
- param_fields[param.name] = param.field(**field_kwargs)
- attrs.update(param_fields)
- return super().__new__(mcs, name, bases, attrs)
- class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass):
- """
- Form for creating a new ConfigRevision.
- """
- fieldsets = (
- ('Rack Elevations', ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
- ('Power', ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
- ('IPAM', ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
- ('Security', ('ALLOWED_URL_SCHEMES',)),
- ('Banners', ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
- ('Pagination', ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
- ('Validation', ('CUSTOM_VALIDATORS',)),
- ('User Preferences', ('DEFAULT_USER_PREFERENCES',)),
- ('Miscellaneous', ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL')),
- ('Config Revision', ('comment',))
- )
- class Meta:
- model = ConfigRevision
- fields = '__all__'
- widgets = {
- 'BANNER_LOGIN': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'BANNER_MAINTENANCE': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'BANNER_TOP': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'BANNER_BOTTOM': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'CUSTOM_VALIDATORS': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'comment': forms.Textarea(),
- }
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # Append current parameter values to form field help texts and check for static configurations
- config = get_config()
- for param in PARAMS:
- value = getattr(config, param.name)
- is_static = hasattr(settings, param.name)
- if value:
- help_text = self.fields[param.name].help_text
- if help_text:
- help_text += '<br />' # Line break
- help_text += f'Current value: <strong>{value}</strong>'
- if is_static:
- help_text += ' (defined statically)'
- elif value == param.default:
- help_text += ' (default)'
- self.fields[param.name].help_text = help_text
- self.fields[param.name].initial = value
- if is_static:
- self.fields[param.name].disabled = True
- def save(self, commit=True):
- instance = super().save(commit=False)
- # Populate JSON data on the instance
- instance.data = self.render_json()
- if commit:
- instance.save()
- return instance
- def render_json(self):
- json = {}
- # Iterate through each field and populate non-empty values
- for field_name in self.declared_fields:
- if field_name in self.cleaned_data and self.cleaned_data[field_name] not in EMPTY_VALUES:
- json[field_name] = self.cleaned_data[field_name]
- return json
|