From 20eaa7d069b20bc8b123ef270ffd4ba3ea410105 Mon Sep 17 00:00:00 2001 From: kkthxbye <> Date: Tue, 24 May 2022 10:12:32 +0200 Subject: [PATCH] #9166 - Add UI Visibility setting for custom fields --- netbox/extras/api/serializers.py | 3 ++- netbox/extras/choices.py | 13 +++++++++++++ netbox/extras/filtersets.py | 4 +++- netbox/extras/forms/bulk_edit.py | 7 +++++++ netbox/extras/forms/bulk_import.py | 2 +- netbox/extras/forms/customfields.py | 7 +++++++ netbox/extras/forms/filtersets.py | 8 +++++++- netbox/extras/forms/models.py | 3 ++- .../0075_customfield_ui_visibility.py | 18 ++++++++++++++++++ netbox/extras/models/customfields.py | 6 ++++++ netbox/extras/tables/tables.py | 3 ++- netbox/extras/tests/test_views.py | 9 +++++---- netbox/netbox/models/features.py | 10 +++++++--- netbox/templates/extras/customfield.html | 4 ++++ 14 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 netbox/extras/migrations/0075_customfield_ui_visibility.py diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index eed7f7603..1a26faec1 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -84,13 +84,14 @@ class CustomFieldSerializer(ValidatedModelSerializer): ) filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) data_type = serializers.SerializerMethodField() + ui_visibility = ChoiceField(choices=CustomFieldVisibilityChoices, required=False) class Meta: model = CustomField fields = [ 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', 'description', 'required', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', - 'validation_regex', 'choices', 'created', 'last_updated', + 'validation_regex', 'choices', 'created', 'last_updated', 'ui_visibility', ] def get_data_type(self, obj): diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index f14368d3d..123fd2cd4 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -47,6 +47,19 @@ class CustomFieldFilterLogicChoices(ChoiceSet): ) +class CustomFieldVisibilityChoices(ChoiceSet): + + VISIBILITY_READ_WRITE = 'read-write' + VISIBILITY_READ_ONLY = 'read-only' + VISIBILITY_HIDDEN = 'hidden' + + CHOICES = ( + (VISIBILITY_READ_WRITE, 'Read/Write'), + (VISIBILITY_READ_ONLY, 'Read-only'), + (VISIBILITY_HIDDEN, 'Hidden'), + ) + + # # CustomLinks # diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 467ae23af..ea74dfc82 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -62,7 +62,9 @@ class CustomFieldFilterSet(BaseFilterSet): class Meta: model = CustomField - fields = ['id', 'content_types', 'name', 'group_name', 'required', 'filter_logic', 'weight', 'description'] + fields = [ + 'id', 'content_types', 'name', 'group_name', 'required', 'filter_logic', 'weight', 'description', 'ui_visibility' + ] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index b722bd751..b1d8a6c21 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -37,6 +37,13 @@ class CustomFieldBulkEditForm(BulkEditForm): weight = forms.IntegerField( required=False ) + ui_visibility = forms.ChoiceField( + label="UI visibility", + choices=add_blank_choice(CustomFieldVisibilityChoices), + required=False, + initial='', + widget=StaticSelect() + ) nullable_fields = ('group_name', 'description',) diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index dabf2f811..c0483d36e 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -37,7 +37,7 @@ class CustomFieldCSVForm(CSVModelForm): model = CustomField fields = ( 'name', 'label', 'group_name', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', - 'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', + 'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'ui_visibility', ) diff --git a/netbox/extras/forms/customfields.py b/netbox/extras/forms/customfields.py index bb8028eec..c4496c5f8 100644 --- a/netbox/extras/forms/customfields.py +++ b/netbox/extras/forms/customfields.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.models import ContentType from extras.models import * +from extras.choices import CustomFieldVisibilityChoices __all__ = ( 'CustomFieldsMixin', @@ -42,8 +43,14 @@ class CustomFieldsMixin: Append form fields for all CustomFields assigned to this object type. """ for customfield in self._get_custom_fields(self._get_content_type()): + if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN: + continue + field_name = f'cf_{customfield.name}' self.fields[field_name] = self._get_form_field(customfield) + if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY: + self.fields[field_name].disabled = True + # Annotate the field in the list of CustomField form fields self.custom_fields[field_name] = customfield diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 1710ecb89..cd59a9db1 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -32,7 +32,7 @@ __all__ = ( class CustomFieldFilterForm(FilterForm): fieldsets = ( (None, ('q',)), - ('Attributes', ('content_types', 'type', 'group_name', 'weight', 'required')), + ('Attributes', ('content_types', 'type', 'group_name', 'weight', 'required', 'ui_visibility')), ) content_types = ContentTypeMultipleChoiceField( queryset=ContentType.objects.all(), @@ -56,6 +56,12 @@ class CustomFieldFilterForm(FilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + ui_visibility = forms.ChoiceField( + choices=add_blank_choice(CustomFieldVisibilityChoices), + required=False, + label=_('UI Visibility'), + widget=StaticSelect() + ) class CustomLinkFilterForm(FilterForm): diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py index b07853f86..16874c49e 100644 --- a/netbox/extras/forms/models.py +++ b/netbox/extras/forms/models.py @@ -41,7 +41,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): fieldsets = ( ('Custom Field', ( - 'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'weight', 'required', 'description', + 'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'weight', 'required', 'description', 'ui_visibility', )), ('Behavior', ('filter_logic',)), ('Values', ('default', 'choices')), @@ -58,6 +58,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): widgets = { 'type': StaticSelect(), 'filter_logic': StaticSelect(), + 'ui_visibility': StaticSelect(), } diff --git a/netbox/extras/migrations/0075_customfield_ui_visibility.py b/netbox/extras/migrations/0075_customfield_ui_visibility.py new file mode 100644 index 000000000..29ee65516 --- /dev/null +++ b/netbox/extras/migrations/0075_customfield_ui_visibility.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2022-05-23 20:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0074_customfield_group_name'), + ] + + operations = [ + migrations.AddField( + model_name='customfield', + name='ui_visibility', + field=models.CharField(default='read-write', max_length=50), + ), + ] diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 55caa4a70..c48b6895c 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -136,6 +136,12 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): null=True, help_text='Comma-separated list of available choices (for selection fields)' ) + ui_visibility = models.CharField( + max_length=50, + choices=CustomFieldVisibilityChoices, + default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE, + help_text='Specifies the visibility of custom field in the UI.' + ) objects = CustomFieldManager() class Meta: diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 1a0f5d58a..d294fd231 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -28,12 +28,13 @@ class CustomFieldTable(NetBoxTable): ) content_types = columns.ContentTypesColumn() required = columns.BooleanColumn() + ui_visibility = columns.ChoiceFieldColumn(verbose_name="UI visibility") class Meta(NetBoxTable.Meta): model = CustomField fields = ( 'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'weight', 'default', - 'description', 'filter_logic', 'choices', 'created', 'last_updated', + 'description', 'filter_logic', 'choices', 'created', 'last_updated', 'ui_visibility', ) default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description') diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index ea3a952d6..0a9d85e15 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -36,13 +36,14 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'default': None, 'weight': 200, 'required': True, + 'ui_visibility': CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE, } cls.csv_data = ( - 'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex', - 'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3}', - 'field5,Field 5,integer,dcim.site,100,exact,,1,100,', - 'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,', + 'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex,ui_visibility', + 'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3},read-write', + 'field5,Field 5,integer,dcim.site,100,exact,,1,100,,read-write', + 'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,,read-write', ) cls.bulk_edit_data = { diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index 4bd1b0e9c..76b546192 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -9,7 +9,7 @@ from django.core.validators import ValidationError from django.db import models from taggit.managers import TaggableManager -from extras.choices import ObjectChangeActionChoices +from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices from extras.utils import register_features from netbox.signals import post_clean from utilities.utils import serialize_object @@ -100,7 +100,7 @@ class CustomFieldsMixin(models.Model): """ return self.custom_field_data - def get_custom_fields(self): + def get_custom_fields(self, omit_hidden=False): """ Return a dictionary of custom fields for a single object in the form `{field: value}`. @@ -114,6 +114,10 @@ class CustomFieldsMixin(models.Model): data = {} for field in CustomField.objects.get_for_model(self): + # Skip fields that are hidden if 'omit_hidden' is set + if omit_hidden and field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN: + continue + value = self.custom_field_data.get(field.name) data[field] = field.deserialize(value) @@ -124,7 +128,7 @@ class CustomFieldsMixin(models.Model): Return a dictionary of custom field/value mappings organized by group. """ grouped_custom_fields = defaultdict(dict) - for cf, value in self.get_custom_fields().items(): + for cf, value in self.get_custom_fields(omit_hidden=True).items(): grouped_custom_fields[cf.group_name][cf] = value return dict(grouped_custom_fields) diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html index dc51d3e82..72dc2e4c3 100644 --- a/netbox/templates/extras/customfield.html +++ b/netbox/templates/extras/customfield.html @@ -42,6 +42,10 @@ Weight {{ object.weight }} + + UI Visibility + {{ object.get_ui_visibility_display }} +