Răsfoiți Sursa

Merge pull request #9411 from kkthxbye-code/fix-9166-2

Fixes #9166 - Add the option to make custom fields read-only or hidden in UI
Jeremy Stretch 3 ani în urmă
părinte
comite
aea023357b

+ 2 - 1
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):

+ 13 - 0
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
 #

+ 3 - 1
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():

+ 7 - 0
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',)
 

+ 1 - 1
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',
         )
 
 

+ 7 - 0
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

+ 7 - 1
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):

+ 2 - 1
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(),
         }
 
 

+ 18 - 0
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),
+        ),
+    ]

+ 6 - 0
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:

+ 2 - 1
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')
 

+ 5 - 4
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 = {

+ 7 - 3
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)

+ 5 - 1
netbox/netbox/tables/tables.py

@@ -7,6 +7,7 @@ from django.db.models.fields.related import RelatedField
 from django_tables2.data import TableQuerysetData
 
 from extras.models import CustomField, CustomLink
+from extras.choices import CustomFieldVisibilityChoices
 from netbox.tables import columns
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 
@@ -178,7 +179,10 @@ class NetBoxTable(BaseTable):
 
         # Add custom field & custom link columns
         content_type = ContentType.objects.get_for_model(self._meta.model)
-        custom_fields = CustomField.objects.filter(content_types=content_type)
+        custom_fields = CustomField.objects.filter(
+            content_types=content_type
+        ).exclude(ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN)
+
         extra_columns.extend([
             (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
         ])

+ 4 - 0
netbox/templates/extras/customfield.html

@@ -42,6 +42,10 @@
             <th scope="row">Weight</th>
             <td>{{ object.weight }}</td>
           </tr>
+          <tr>
+            <th scope="row">UI Visibility</th>
+            <td>{{ object.get_ui_visibility_display }}</td>
+          </tr>
         </table>
       </div>
     </div>