diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py deleted file mode 100644 index fe98d9ca3..000000000 --- a/netbox/extras/forms.py +++ /dev/null @@ -1,988 +0,0 @@ -from django import forms -from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType -from django.contrib.postgres.forms import SimpleArrayField -from django.utils.safestring import mark_safe -from django.utils.translation import gettext as _ - -from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup -from tenancy.models import Tenant, TenantGroup -from utilities.forms import ( - add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, - CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, CSVContentTypeField, CSVModelForm, - CSVMultipleContentTypeField, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect, - StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, -) -from virtualization.models import Cluster, ClusterGroup -from .choices import * -from .models import * -from .utils import FeatureQuery - - -# -# Custom fields -# - -class CustomFieldForm(BootstrapMixin, forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields') - ) - - class Meta: - model = CustomField - fields = '__all__' - fieldsets = ( - ('Custom Field', ('name', 'label', 'type', 'weight', 'required', 'description')), - ('Assigned Models', ('content_types',)), - ('Behavior', ('filter_logic',)), - ('Values', ('default', 'choices')), - ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')), - ) - - -class CustomFieldCSVForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - help_text="One or more assigned object types" - ) - choices = SimpleArrayField( - base_field=forms.CharField(), - required=False, - help_text='Comma-separated list of field choices' - ) - - class Meta: - model = CustomField - fields = ( - 'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default', - 'choices', 'weight', - ) - - -class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=CustomField.objects.all(), - widget=forms.MultipleHiddenInput - ) - description = forms.CharField( - required=False - ) - required = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - weight = forms.IntegerField( - required=False - ) - - class Meta: - nullable_fields = [] - - -class CustomFieldFilterForm(BootstrapMixin, forms.Form): - field_groups = [ - ['q'], - ['type', 'content_types'], - ['weight', 'required'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - type = forms.MultipleChoiceField( - choices=CustomFieldTypeChoices, - required=False, - widget=StaticSelectMultiple(), - label=_('Field type') - ) - weight = forms.IntegerField( - required=False - ) - required = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Custom links -# - -class CustomLinkForm(BootstrapMixin, forms.ModelForm): - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_links') - ) - - class Meta: - model = CustomLink - fields = '__all__' - fieldsets = ( - ('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')), - ('Templates', ('link_text', 'link_url')), - ) - 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 {{ obj }}. ' - 'Links which render as empty text will not be displayed.', - 'link_url': 'Jinja2 template code for the link URL. Reference the object as {{ obj }}.', - } - - -class CustomLinkCSVForm(CSVModelForm): - content_type = CSVContentTypeField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_links'), - help_text="Assigned object type" - ) - - class Meta: - model = CustomLink - fields = ( - 'name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url', - ) - - -class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=CustomLink.objects.all(), - widget=forms.MultipleHiddenInput - ) - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - new_window = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - weight = forms.IntegerField( - required=False - ) - button_class = forms.ChoiceField( - choices=CustomLinkButtonClassChoices, - required=False, - widget=StaticSelect() - ) - - class Meta: - nullable_fields = [] - - -class CustomLinkFilterForm(BootstrapMixin, forms.Form): - field_groups = [ - ['q'], - ['content_type', 'weight', 'new_window'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - weight = forms.IntegerField( - required=False - ) - new_window = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Export templates -# - -class ExportTemplateForm(BootstrapMixin, forms.ModelForm): - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_links') - ) - - class Meta: - model = ExportTemplate - fields = '__all__' - fieldsets = ( - ('Custom Link', ('name', 'content_type', 'description')), - ('Template', ('template_code',)), - ('Rendering', ('mime_type', 'file_extension', 'as_attachment')), - ) - widgets = { - 'template_code': forms.Textarea(attrs={'class': 'font-monospace'}), - } - - -class ExportTemplateCSVForm(CSVModelForm): - content_type = CSVContentTypeField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('export_templates'), - help_text="Assigned object type" - ) - - class Meta: - model = ExportTemplate - fields = ( - 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code', - ) - - -class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=ExportTemplate.objects.all(), - widget=forms.MultipleHiddenInput - ) - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - description = forms.CharField( - max_length=200, - required=False - ) - mime_type = forms.CharField( - max_length=50, - required=False - ) - file_extension = forms.CharField( - max_length=15, - required=False - ) - as_attachment = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - - class Meta: - nullable_fields = ['description', 'mime_type', 'file_extension'] - - -class ExportTemplateFilterForm(BootstrapMixin, forms.Form): - field_groups = [ - ['q'], - ['content_type', 'mime_type', 'file_extension', 'as_attachment'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - mime_type = forms.CharField( - required=False, - label=_('MIME type') - ) - file_extension = forms.CharField( - required=False - ) - as_attachment = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Webhooks -# - -class WebhookForm(BootstrapMixin, forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('webhooks') - ) - - class Meta: - model = Webhook - fields = '__all__' - fieldsets = ( - ('Webhook', ('name', 'enabled')), - ('Assigned Models', ('content_types',)), - ('Events', ('type_create', 'type_update', 'type_delete')), - ('HTTP Request', ( - 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', - )), - ('SSL', ('ssl_verification', 'ca_file_path')), - ) - widgets = { - 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}), - 'body_template': forms.Textarea(attrs={'class': 'font-monospace'}), - } - - -class WebhookCSVForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('webhooks'), - help_text="One or more assigned object types" - ) - - class Meta: - model = Webhook - fields = ( - 'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'payload_url', - 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification', - 'ca_file_path' - ) - - -class WebhookBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Webhook.objects.all(), - widget=forms.MultipleHiddenInput - ) - enabled = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - type_create = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - type_update = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - type_delete = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - http_method = forms.ChoiceField( - choices=WebhookHttpMethodChoices, - required=False - ) - payload_url = forms.CharField( - required=False - ) - ssl_verification = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - secret = forms.CharField( - required=False - ) - ca_file_path = forms.CharField( - required=False - ) - - class Meta: - nullable_fields = ['secret', 'ca_file_path'] - - -class WebhookFilterForm(BootstrapMixin, forms.Form): - field_groups = [ - ['q'], - ['content_types', 'http_method', 'enabled'], - ['type_create', 'type_update', 'type_delete'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - content_types = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), - required=False - ) - http_method = forms.MultipleChoiceField( - choices=WebhookHttpMethodChoices, - required=False, - widget=StaticSelectMultiple(), - label=_('HTTP method') - ) - enabled = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - type_create = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - type_update = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - type_delete = forms.NullBooleanField( - required=False, - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Custom field models -# - -class CustomFieldsMixin: - """ - Extend a Form to include custom field support. - """ - def __init__(self, *args, **kwargs): - self.custom_fields = [] - - super().__init__(*args, **kwargs) - - self._append_customfield_fields() - - def _get_content_type(self): - """ - Return the ContentType of the form's model. - """ - if not hasattr(self, 'model'): - raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.") - return ContentType.objects.get_for_model(self.model) - - def _get_form_field(self, customfield): - return customfield.to_form_field() - - def _append_customfield_fields(self): - """ - Append form fields for all CustomFields assigned to this object type. - """ - content_type = self._get_content_type() - - # Append form fields; assign initial values if modifying and existing object - for customfield in CustomField.objects.filter(content_types=content_type): - field_name = f'cf_{customfield.name}' - self.fields[field_name] = self._get_form_field(customfield) - - # Annotate the field in the list of CustomField form fields - self.custom_fields.append(field_name) - - -class CustomFieldModelForm(CustomFieldsMixin, forms.ModelForm): - """ - Extend ModelForm to include custom field support. - """ - def _get_content_type(self): - return ContentType.objects.get_for_model(self._meta.model) - - def _get_form_field(self, customfield): - if self.instance.pk: - form_field = customfield.to_form_field(set_initial=False) - form_field.initial = self.instance.custom_field_data.get(customfield.name, None) - return form_field - - return customfield.to_form_field() - - def clean(self): - - # Save custom field data on instance - for cf_name in self.custom_fields: - key = cf_name[3:] # Strip "cf_" from field name - value = self.cleaned_data.get(cf_name) - empty_values = self.fields[cf_name].empty_values - # Convert "empty" values to null - self.instance.custom_field_data[key] = value if value not in empty_values else None - - return super().clean() - - -class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm): - - def _get_form_field(self, customfield): - return customfield.to_form_field(for_csv_import=True) - - -class CustomFieldModelBulkEditForm(BulkEditForm): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.custom_fields = [] - self.obj_type = ContentType.objects.get_for_model(self.model) - - # Add all applicable CustomFields to the form - custom_fields = CustomField.objects.filter(content_types=self.obj_type) - for cf in custom_fields: - # Annotate non-required custom fields as nullable - if not cf.required: - self.nullable_fields.append(cf.name) - self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False) - # Annotate this as a custom field - self.custom_fields.append(cf.name) - - -class CustomFieldModelFilterForm(forms.Form): - - def __init__(self, *args, **kwargs): - - self.obj_type = ContentType.objects.get_for_model(self.model) - - super().__init__(*args, **kwargs) - - # Add all applicable CustomFields to the form - self.custom_field_filters = [] - custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude( - filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED - ) - for cf in custom_fields: - field_name = 'cf_{}'.format(cf.name) - self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False) - self.custom_field_filters.append(field_name) - - -# -# Tags -# - -class TagForm(BootstrapMixin, forms.ModelForm): - slug = SlugField() - - class Meta: - model = Tag - fields = [ - 'name', 'slug', 'color', 'description' - ] - fieldsets = ( - ('Tag', ('name', 'slug', 'color', 'description')), - ) - - -class TagCSVForm(CSVModelForm): - slug = SlugField() - - class Meta: - model = Tag - fields = ('name', 'slug', 'color', 'description') - help_texts = { - 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), - } - - -class AddRemoveTagsForm(forms.Form): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Add add/remove tags fields - self.fields['add_tags'] = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - self.fields['remove_tags'] = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - -class TagFilterForm(BootstrapMixin, forms.Form): - model = Tag - q = forms.CharField( - required=False, - label=_('Search') - ) - content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()), - required=False, - label=_('Tagged object type') - ) - - -class TagBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Tag.objects.all(), - widget=forms.MultipleHiddenInput - ) - color = ColorField( - required=False - ) - description = forms.CharField( - max_length=200, - required=False - ) - - class Meta: - nullable_fields = ['description'] - - -# -# Config contexts -# - -class ConfigContextForm(BootstrapMixin, 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 - ) - 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_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( - label='' - ) - - class Meta: - model = ConfigContext - fields = ( - 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types', - 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', - ) - - -class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=ConfigContext.objects.all(), - widget=forms.MultipleHiddenInput - ) - weight = forms.IntegerField( - required=False, - min_value=0 - ) - is_active = forms.NullBooleanField( - required=False, - widget=BulkEditNullBooleanSelect() - ) - description = forms.CharField( - required=False, - max_length=100 - ) - - class Meta: - nullable_fields = [ - 'description', - ] - - -class ConfigContextFilterForm(BootstrapMixin, forms.Form): - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['device_type_id', 'platform_id', 'role_id'], - ['cluster_group_id', 'cluster_id'], - ['tenant_group_id', 'tenant_id'] - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - region_id = DynamicModelMultipleChoiceField( - queryset=Region.objects.all(), - required=False, - label=_('Regions'), - fetch_trigger='open' - ) - site_group_id = DynamicModelMultipleChoiceField( - queryset=SiteGroup.objects.all(), - required=False, - label=_('Site groups'), - fetch_trigger='open' - ) - site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - label=_('Sites'), - fetch_trigger='open' - ) - device_type_id = DynamicModelMultipleChoiceField( - queryset=DeviceType.objects.all(), - required=False, - label=_('Device types'), - fetch_trigger='open' - ) - role_id = DynamicModelMultipleChoiceField( - queryset=DeviceRole.objects.all(), - required=False, - label=_('Roles'), - fetch_trigger='open' - ) - platform_id = DynamicModelMultipleChoiceField( - queryset=Platform.objects.all(), - required=False, - label=_('Platforms'), - fetch_trigger='open' - ) - cluster_group_id = DynamicModelMultipleChoiceField( - queryset=ClusterGroup.objects.all(), - required=False, - label=_('Cluster groups'), - fetch_trigger='open' - ) - cluster_id = DynamicModelMultipleChoiceField( - queryset=Cluster.objects.all(), - required=False, - label=_('Clusters'), - fetch_trigger='open' - ) - tenant_group_id = DynamicModelMultipleChoiceField( - queryset=TenantGroup.objects.all(), - required=False, - label=_('Tenant groups'), - fetch_trigger='open' - ) - tenant_id = DynamicModelMultipleChoiceField( - queryset=Tenant.objects.all(), - required=False, - label=_('Tenant'), - fetch_trigger='open' - ) - tag = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - to_field_name='slug', - required=False, - label=_('Tags'), - fetch_trigger='open' - ) - - -# -# Filter form for local config context data -# - -class LocalConfigContextFilterForm(forms.Form): - local_context_data = forms.NullBooleanField( - required=False, - label=_('Has local config context data'), - widget=StaticSelect( - choices=BOOLEAN_WITH_BLANK_CHOICES - ) - ) - - -# -# Image attachments -# - -class ImageAttachmentForm(BootstrapMixin, forms.ModelForm): - - class Meta: - model = ImageAttachment - fields = [ - 'name', 'image', - ] - - -# -# Journal entries -# - -class JournalEntryForm(BootstrapMixin, forms.ModelForm): - comments = CommentField() - - kind = forms.ChoiceField( - choices=add_blank_choice(JournalEntryKindChoices), - required=False, - widget=StaticSelect() - ) - - class Meta: - model = JournalEntry - fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments'] - widgets = { - 'assigned_object_type': forms.HiddenInput, - 'assigned_object_id': forms.HiddenInput, - } - - -class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=JournalEntry.objects.all(), - widget=forms.MultipleHiddenInput - ) - kind = forms.ChoiceField( - choices=JournalEntryKindChoices, - required=False - ) - comments = forms.CharField( - required=False, - widget=forms.Textarea() - ) - - class Meta: - nullable_fields = [] - - -class JournalEntryFilterForm(BootstrapMixin, forms.Form): - model = JournalEntry - field_groups = [ - ['q'], - ['created_before', 'created_after', 'created_by_id'], - ['assigned_object_type_id', 'kind'] - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - created_after = forms.DateTimeField( - required=False, - label=_('After'), - widget=DateTimePicker() - ) - created_before = forms.DateTimeField( - required=False, - label=_('Before'), - widget=DateTimePicker() - ) - created_by_id = DynamicModelMultipleChoiceField( - queryset=User.objects.all(), - required=False, - label=_('User'), - widget=APISelectMultiple( - api_url='/api/users/users/', - ), - fetch_trigger='open' - ) - assigned_object_type_id = DynamicModelMultipleChoiceField( - queryset=ContentType.objects.all(), - required=False, - label=_('Object Type'), - widget=APISelectMultiple( - api_url='/api/extras/content-types/', - ), - fetch_trigger='open' - ) - kind = forms.ChoiceField( - choices=add_blank_choice(JournalEntryKindChoices), - required=False, - widget=StaticSelect() - ) - - -# -# Change logging -# - -class ObjectChangeFilterForm(BootstrapMixin, forms.Form): - model = ObjectChange - field_groups = [ - ['q'], - ['time_before', 'time_after', 'action'], - ['user_id', 'changed_object_type_id'], - ] - q = forms.CharField( - required=False, - widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), - label=_('Search') - ) - time_after = forms.DateTimeField( - required=False, - label=_('After'), - widget=DateTimePicker() - ) - time_before = forms.DateTimeField( - required=False, - label=_('Before'), - widget=DateTimePicker() - ) - action = forms.ChoiceField( - choices=add_blank_choice(ObjectChangeActionChoices), - required=False, - widget=StaticSelect() - ) - user_id = DynamicModelMultipleChoiceField( - queryset=User.objects.all(), - required=False, - label=_('User'), - widget=APISelectMultiple( - api_url='/api/users/users/', - ), - fetch_trigger='open' - ) - changed_object_type_id = DynamicModelMultipleChoiceField( - queryset=ContentType.objects.all(), - required=False, - label=_('Object Type'), - widget=APISelectMultiple( - api_url='/api/extras/content-types/', - ), - fetch_trigger='open' - ) - - -# -# Scripts -# - -class ScriptForm(BootstrapMixin, forms.Form): - _commit = forms.BooleanField( - required=False, - initial=True, - label="Commit changes", - help_text="Commit changes to the database (uncheck for a dry-run)" - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Move _commit to the end of the form - commit = self.fields.pop('_commit') - self.fields['_commit'] = commit - - @property - def requires_input(self): - """ - A boolean indicating whether the form requires user input (ignore the _commit field). - """ - return bool(len(self.fields) > 1) diff --git a/netbox/extras/forms/__init__.py b/netbox/extras/forms/__init__.py new file mode 100644 index 000000000..1584e2f51 --- /dev/null +++ b/netbox/extras/forms/__init__.py @@ -0,0 +1,6 @@ +from .models import * +from .filtersets import * +from .bulk_edit import * +from .bulk_import import * +from .customfields import * +from .scripts import * diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py new file mode 100644 index 000000000..b85a74a5b --- /dev/null +++ b/netbox/extras/forms/bulk_edit.py @@ -0,0 +1,199 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType + +from extras.choices import * +from extras.models import * +from extras.utils import FeatureQuery +from utilities.forms import ( + BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect, +) + +__all__ = ( + 'ConfigContextBulkEditForm', + 'CustomFieldBulkEditForm', + 'CustomLinkBulkEditForm', + 'ExportTemplateBulkEditForm', + 'JournalEntryBulkEditForm', + 'TagBulkEditForm', + 'WebhookBulkEditForm', +) + + +class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=CustomField.objects.all(), + widget=forms.MultipleHiddenInput + ) + description = forms.CharField( + required=False + ) + required = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + weight = forms.IntegerField( + required=False + ) + + class Meta: + nullable_fields = [] + + +class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=CustomLink.objects.all(), + widget=forms.MultipleHiddenInput + ) + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + new_window = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + weight = forms.IntegerField( + required=False + ) + button_class = forms.ChoiceField( + choices=CustomLinkButtonClassChoices, + required=False, + widget=StaticSelect() + ) + + class Meta: + nullable_fields = [] + + +class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ExportTemplate.objects.all(), + widget=forms.MultipleHiddenInput + ) + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + mime_type = forms.CharField( + max_length=50, + required=False + ) + file_extension = forms.CharField( + max_length=15, + required=False + ) + as_attachment = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + + class Meta: + nullable_fields = ['description', 'mime_type', 'file_extension'] + + +class WebhookBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Webhook.objects.all(), + widget=forms.MultipleHiddenInput + ) + enabled = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_create = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_update = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_delete = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + http_method = forms.ChoiceField( + choices=WebhookHttpMethodChoices, + required=False + ) + payload_url = forms.CharField( + required=False + ) + ssl_verification = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + secret = forms.CharField( + required=False + ) + ca_file_path = forms.CharField( + required=False + ) + + class Meta: + nullable_fields = ['secret', 'ca_file_path'] + + +class TagBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Tag.objects.all(), + widget=forms.MultipleHiddenInput + ) + color = ColorField( + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + class Meta: + nullable_fields = ['description'] + + +class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConfigContext.objects.all(), + widget=forms.MultipleHiddenInput + ) + weight = forms.IntegerField( + required=False, + min_value=0 + ) + is_active = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + description = forms.CharField( + required=False, + max_length=100 + ) + + class Meta: + nullable_fields = [ + 'description', + ] + + +class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=JournalEntry.objects.all(), + widget=forms.MultipleHiddenInput + ) + kind = forms.ChoiceField( + choices=JournalEntryKindChoices, + required=False + ) + comments = forms.CharField( + required=False, + widget=forms.Textarea() + ) + + class Meta: + nullable_fields = [] diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py new file mode 100644 index 000000000..fb8cf53e8 --- /dev/null +++ b/netbox/extras/forms/bulk_import.py @@ -0,0 +1,91 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.forms import SimpleArrayField +from django.utils.safestring import mark_safe + +from extras.models import * +from extras.utils import FeatureQuery +from utilities.forms import CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField + +__all__ = ( + 'CustomFieldCSVForm', + 'CustomLinkCSVForm', + 'ExportTemplateCSVForm', + 'TagCSVForm', + 'WebhookCSVForm', +) + + +class CustomFieldCSVForm(CSVModelForm): + content_types = CSVMultipleContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + help_text="One or more assigned object types" + ) + choices = SimpleArrayField( + base_field=forms.CharField(), + required=False, + help_text='Comma-separated list of field choices' + ) + + class Meta: + model = CustomField + fields = ( + 'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default', + 'choices', 'weight', + ) + + +class CustomLinkCSVForm(CSVModelForm): + content_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_links'), + help_text="Assigned object type" + ) + + class Meta: + model = CustomLink + fields = ( + 'name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url', + ) + + +class ExportTemplateCSVForm(CSVModelForm): + content_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('export_templates'), + help_text="Assigned object type" + ) + + class Meta: + model = ExportTemplate + fields = ( + 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code', + ) + + +class WebhookCSVForm(CSVModelForm): + content_types = CSVMultipleContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('webhooks'), + help_text="One or more assigned object types" + ) + + class Meta: + model = Webhook + fields = ( + 'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'payload_url', + 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification', + 'ca_file_path' + ) + + +class TagCSVForm(CSVModelForm): + slug = SlugField() + + class Meta: + model = Tag + fields = ('name', 'slug', 'color', 'description') + help_texts = { + 'color': mark_safe('RGB color in hexadecimal (e.g. 00ff00)'), + } diff --git a/netbox/extras/forms/customfields.py b/netbox/extras/forms/customfields.py new file mode 100644 index 000000000..9f68467fa --- /dev/null +++ b/netbox/extras/forms/customfields.py @@ -0,0 +1,123 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType + +from extras.choices import * +from extras.models import * +from utilities.forms import BulkEditForm, CSVModelForm + +__all__ = ( + 'CustomFieldModelCSVForm', + 'CustomFieldModelBulkEditForm', + 'CustomFieldModelFilterForm', + 'CustomFieldModelForm', + 'CustomFieldsMixin', +) + + +class CustomFieldsMixin: + """ + Extend a Form to include custom field support. + """ + def __init__(self, *args, **kwargs): + self.custom_fields = [] + + super().__init__(*args, **kwargs) + + self._append_customfield_fields() + + def _get_content_type(self): + """ + Return the ContentType of the form's model. + """ + if not hasattr(self, 'model'): + raise NotImplementedError(f"{self.__class__.__name__} must specify a model class.") + return ContentType.objects.get_for_model(self.model) + + def _get_form_field(self, customfield): + return customfield.to_form_field() + + def _append_customfield_fields(self): + """ + Append form fields for all CustomFields assigned to this object type. + """ + content_type = self._get_content_type() + + # Append form fields; assign initial values if modifying and existing object + for customfield in CustomField.objects.filter(content_types=content_type): + field_name = f'cf_{customfield.name}' + self.fields[field_name] = self._get_form_field(customfield) + + # Annotate the field in the list of CustomField form fields + self.custom_fields.append(field_name) + + +class CustomFieldModelForm(CustomFieldsMixin, forms.ModelForm): + """ + Extend ModelForm to include custom field support. + """ + def _get_content_type(self): + return ContentType.objects.get_for_model(self._meta.model) + + def _get_form_field(self, customfield): + if self.instance.pk: + form_field = customfield.to_form_field(set_initial=False) + form_field.initial = self.instance.custom_field_data.get(customfield.name, None) + return form_field + + return customfield.to_form_field() + + def clean(self): + + # Save custom field data on instance + for cf_name in self.custom_fields: + key = cf_name[3:] # Strip "cf_" from field name + value = self.cleaned_data.get(cf_name) + empty_values = self.fields[cf_name].empty_values + # Convert "empty" values to null + self.instance.custom_field_data[key] = value if value not in empty_values else None + + return super().clean() + + +class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm): + + def _get_form_field(self, customfield): + return customfield.to_form_field(for_csv_import=True) + + +class CustomFieldModelBulkEditForm(BulkEditForm): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.custom_fields = [] + self.obj_type = ContentType.objects.get_for_model(self.model) + + # Add all applicable CustomFields to the form + custom_fields = CustomField.objects.filter(content_types=self.obj_type) + for cf in custom_fields: + # Annotate non-required custom fields as nullable + if not cf.required: + self.nullable_fields.append(cf.name) + self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False) + # Annotate this as a custom field + self.custom_fields.append(cf.name) + + +class CustomFieldModelFilterForm(forms.Form): + + def __init__(self, *args, **kwargs): + + self.obj_type = ContentType.objects.get_for_model(self.model) + + super().__init__(*args, **kwargs) + + # Add all applicable CustomFields to the form + self.custom_field_filters = [] + custom_fields = CustomField.objects.filter(content_types=self.obj_type).exclude( + filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED + ) + for cf in custom_fields: + field_name = 'cf_{}'.format(cf.name) + self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False) + self.custom_field_filters.append(field_name) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py new file mode 100644 index 000000000..6196ba8da --- /dev/null +++ b/netbox/extras/forms/filtersets.py @@ -0,0 +1,364 @@ +from django import forms +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext as _ + +from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup +from extras.choices import * +from extras.models import * +from extras.utils import FeatureQuery +from tenancy.models import Tenant, TenantGroup +from utilities.forms import ( + add_blank_choice, APISelectMultiple, BootstrapMixin, ContentTypeChoiceField, + ContentTypeMultipleChoiceField, DateTimePicker, DynamicModelMultipleChoiceField, StaticSelect, + StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, +) +from virtualization.models import Cluster, ClusterGroup + +__all__ = ( + 'ConfigContextFilterForm', + 'CustomFieldFilterForm', + 'CustomLinkFilterForm', + 'ExportTemplateFilterForm', + 'JournalEntryFilterForm', + 'LocalConfigContextFilterForm', + 'ObjectChangeFilterForm', + 'TagFilterForm', + 'WebhookFilterForm', +) + + +class CustomFieldFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['q'], + ['type', 'content_types'], + ['weight', 'required'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + content_types = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + type = forms.MultipleChoiceField( + choices=CustomFieldTypeChoices, + required=False, + widget=StaticSelectMultiple(), + label=_('Field type') + ) + weight = forms.IntegerField( + required=False + ) + required = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class CustomLinkFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['q'], + ['content_type', 'weight', 'new_window'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + weight = forms.IntegerField( + required=False + ) + new_window = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class ExportTemplateFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['q'], + ['content_type', 'mime_type', 'file_extension', 'as_attachment'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + mime_type = forms.CharField( + required=False, + label=_('MIME type') + ) + file_extension = forms.CharField( + required=False + ) + as_attachment = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class WebhookFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['q'], + ['content_types', 'http_method', 'enabled'], + ['type_create', 'type_update', 'type_delete'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + content_types = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + http_method = forms.MultipleChoiceField( + choices=WebhookHttpMethodChoices, + required=False, + widget=StaticSelectMultiple(), + label=_('HTTP method') + ) + enabled = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + type_create = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + type_update = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + type_delete = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class TagFilterForm(BootstrapMixin, forms.Form): + model = Tag + q = forms.CharField( + required=False, + label=_('Search') + ) + content_type_id = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()), + required=False, + label=_('Tagged object type') + ) + + +class ConfigContextFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['q', 'tag'], + ['region_id', 'site_group_id', 'site_id'], + ['device_type_id', 'platform_id', 'role_id'], + ['cluster_group_id', 'cluster_id'], + ['tenant_group_id', 'tenant_id'] + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + region_id = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_('Regions'), + fetch_trigger='open' + ) + site_group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_('Site groups'), + fetch_trigger='open' + ) + site_id = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + label=_('Sites'), + fetch_trigger='open' + ) + device_type_id = DynamicModelMultipleChoiceField( + queryset=DeviceType.objects.all(), + required=False, + label=_('Device types'), + fetch_trigger='open' + ) + role_id = DynamicModelMultipleChoiceField( + queryset=DeviceRole.objects.all(), + required=False, + label=_('Roles'), + fetch_trigger='open' + ) + platform_id = DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), + required=False, + label=_('Platforms'), + fetch_trigger='open' + ) + cluster_group_id = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + label=_('Cluster groups'), + fetch_trigger='open' + ) + cluster_id = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_('Clusters'), + fetch_trigger='open' + ) + tenant_group_id = DynamicModelMultipleChoiceField( + queryset=TenantGroup.objects.all(), + required=False, + label=_('Tenant groups'), + fetch_trigger='open' + ) + tenant_id = DynamicModelMultipleChoiceField( + queryset=Tenant.objects.all(), + required=False, + label=_('Tenant'), + fetch_trigger='open' + ) + tag = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + to_field_name='slug', + required=False, + label=_('Tags'), + fetch_trigger='open' + ) + + +class LocalConfigContextFilterForm(forms.Form): + local_context_data = forms.NullBooleanField( + required=False, + label=_('Has local config context data'), + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + +class JournalEntryFilterForm(BootstrapMixin, forms.Form): + model = JournalEntry + field_groups = [ + ['q'], + ['created_before', 'created_after', 'created_by_id'], + ['assigned_object_type_id', 'kind'] + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + created_after = forms.DateTimeField( + required=False, + label=_('After'), + widget=DateTimePicker() + ) + created_before = forms.DateTimeField( + required=False, + label=_('Before'), + widget=DateTimePicker() + ) + created_by_id = DynamicModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + label=_('User'), + widget=APISelectMultiple( + api_url='/api/users/users/', + ), + fetch_trigger='open' + ) + assigned_object_type_id = DynamicModelMultipleChoiceField( + queryset=ContentType.objects.all(), + required=False, + label=_('Object Type'), + widget=APISelectMultiple( + api_url='/api/extras/content-types/', + ), + fetch_trigger='open' + ) + kind = forms.ChoiceField( + choices=add_blank_choice(JournalEntryKindChoices), + required=False, + widget=StaticSelect() + ) + + +class ObjectChangeFilterForm(BootstrapMixin, forms.Form): + model = ObjectChange + field_groups = [ + ['q'], + ['time_before', 'time_after', 'action'], + ['user_id', 'changed_object_type_id'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + time_after = forms.DateTimeField( + required=False, + label=_('After'), + widget=DateTimePicker() + ) + time_before = forms.DateTimeField( + required=False, + label=_('Before'), + widget=DateTimePicker() + ) + action = forms.ChoiceField( + choices=add_blank_choice(ObjectChangeActionChoices), + required=False, + widget=StaticSelect() + ) + user_id = DynamicModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + label=_('User'), + widget=APISelectMultiple( + api_url='/api/users/users/', + ), + fetch_trigger='open' + ) + changed_object_type_id = DynamicModelMultipleChoiceField( + queryset=ContentType.objects.all(), + required=False, + label=_('Object Type'), + widget=APISelectMultiple( + api_url='/api/extras/content-types/', + ), + fetch_trigger='open' + ) diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py new file mode 100644 index 000000000..7e462e62b --- /dev/null +++ b/netbox/extras/forms/models.py @@ -0,0 +1,223 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType + +from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup +from extras.choices import * +from extras.models import * +from extras.utils import FeatureQuery +from tenancy.models import Tenant, TenantGroup +from utilities.forms import ( + add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, + ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect, +) +from virtualization.models import Cluster, ClusterGroup + +__all__ = ( + 'AddRemoveTagsForm', + 'ConfigContextForm', + 'CustomFieldForm', + 'CustomLinkForm', + 'ExportTemplateForm', + 'ImageAttachmentForm', + 'JournalEntryForm', + 'TagForm', + 'WebhookForm', +) + + +class CustomFieldForm(BootstrapMixin, forms.ModelForm): + content_types = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields') + ) + + class Meta: + model = CustomField + fields = '__all__' + fieldsets = ( + ('Custom Field', ('name', 'label', 'type', 'weight', 'required', 'description')), + ('Assigned Models', ('content_types',)), + ('Behavior', ('filter_logic',)), + ('Values', ('default', 'choices')), + ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')), + ) + + +class CustomLinkForm(BootstrapMixin, forms.ModelForm): + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_links') + ) + + class Meta: + model = CustomLink + fields = '__all__' + fieldsets = ( + ('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')), + ('Templates', ('link_text', 'link_url')), + ) + 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 {{ obj }}. ' + 'Links which render as empty text will not be displayed.', + 'link_url': 'Jinja2 template code for the link URL. Reference the object as {{ obj }}.', + } + + +class ExportTemplateForm(BootstrapMixin, forms.ModelForm): + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_links') + ) + + class Meta: + model = ExportTemplate + fields = '__all__' + fieldsets = ( + ('Custom Link', ('name', 'content_type', 'description')), + ('Template', ('template_code',)), + ('Rendering', ('mime_type', 'file_extension', 'as_attachment')), + ) + widgets = { + 'template_code': forms.Textarea(attrs={'class': 'font-monospace'}), + } + + +class WebhookForm(BootstrapMixin, forms.ModelForm): + content_types = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('webhooks') + ) + + class Meta: + model = Webhook + fields = '__all__' + fieldsets = ( + ('Webhook', ('name', 'enabled')), + ('Assigned Models', ('content_types',)), + ('Events', ('type_create', 'type_update', 'type_delete')), + ('HTTP Request', ( + 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', + )), + ('SSL', ('ssl_verification', 'ca_file_path')), + ) + widgets = { + 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}), + 'body_template': forms.Textarea(attrs={'class': 'font-monospace'}), + } + + +class TagForm(BootstrapMixin, forms.ModelForm): + slug = SlugField() + + class Meta: + model = Tag + fields = [ + 'name', 'slug', 'color', 'description' + ] + fieldsets = ( + ('Tag', ('name', 'slug', 'color', 'description')), + ) + + +class AddRemoveTagsForm(forms.Form): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Add add/remove tags fields + self.fields['add_tags'] = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + self.fields['remove_tags'] = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + +class ConfigContextForm(BootstrapMixin, 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 + ) + 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_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( + label='' + ) + + class Meta: + model = ConfigContext + fields = ( + 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types', + 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', + ) + + +class ImageAttachmentForm(BootstrapMixin, forms.ModelForm): + + class Meta: + model = ImageAttachment + fields = [ + 'name', 'image', + ] + + +class JournalEntryForm(BootstrapMixin, forms.ModelForm): + comments = CommentField() + + kind = forms.ChoiceField( + choices=add_blank_choice(JournalEntryKindChoices), + required=False, + widget=StaticSelect() + ) + + class Meta: + model = JournalEntry + fields = ['assigned_object_type', 'assigned_object_id', 'kind', 'comments'] + widgets = { + 'assigned_object_type': forms.HiddenInput, + 'assigned_object_id': forms.HiddenInput, + } diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py new file mode 100644 index 000000000..380b4364c --- /dev/null +++ b/netbox/extras/forms/scripts.py @@ -0,0 +1,30 @@ +from django import forms + +from utilities.forms import BootstrapMixin + +__all__ = ( + 'ScriptForm', +) + + +class ScriptForm(BootstrapMixin, forms.Form): + _commit = forms.BooleanField( + required=False, + initial=True, + label="Commit changes", + help_text="Commit changes to the database (uncheck for a dry-run)" + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Move _commit to the end of the form + commit = self.fields.pop('_commit') + self.fields['_commit'] = commit + + @property + def requires_input(self): + """ + A boolean indicating whether the form requires user input (ignore the _commit field). + """ + return bool(len(self.fields) > 1)