diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md
index 1e0d5c31ef5..e9ff7bd9f60 100644
--- a/docs/customization/custom-fields.md
+++ b/docs/customization/custom-fields.md
@@ -40,14 +40,22 @@ Related custom fields can be grouped together within the UI by assigning each th
This parameter has no effect on the API representation of custom field data.
-### Visibility
+### Visibility & Editing
-When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI.
+!!! info "This feature was improved in NetBox v3.7."
-* **Read/write** (default): The custom field is included when viewing and editing objects.
-* **Read-only**: The custom field is displayed when viewing an object, but it cannot be edited via the UI. (It will appear in the form as a read-only field.)
+When creating a custom field, users can control the conditions under which it may be displayed and edited within the NetBox user interface. The following choices are available for controlling the display of a custom field on an object:
+
+* **Always** (default): The custom field is included when viewing an object.
+* **If Set**: The custom field is included only if a value has been defined for the object.
* **Hidden**: The custom field will never be displayed within the UI. This option is recommended for fields which are not intended for use by human users.
+Additionally, the following options are available for controlling whether custom field values can be altered within the NetBox UI:
+
+* **Yes** (default): The custom field's value may be modified when editing an object.
+* **No**: The custom field is displayed for reference when editing an object, but its value may not be modified.
+* **Hidden**: The custom field is not displayed when editing an object.
+
Note that this setting has no impact on the REST or GraphQL APIs: Custom field data will always be available via either API.
### Validation
diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md
index bf0c4755ac6..e68ddb79d35 100644
--- a/docs/models/extras/customfield.md
+++ b/docs/models/extras/customfield.md
@@ -64,16 +64,25 @@ Defines how filters are evaluated against custom field values.
| Loose | Match any occurrence of the value |
| Exact | Match only the complete field value |
-### UI Visibility
+### UI Visible
-Controls how and whether the custom field is displayed within the NetBox user interface.
+Controls whether the custom field is displayed for objects within the NetBox user interface.
-| Option | Description |
-|-------------------|--------------------------------------------------|
-| Read/write | Display and permit editing (default) |
-| Read-only | Display field but disallow editing |
-| Hidden | Do not display field in the UI |
-| Hidden (if unset) | Display in the UI only when a value has been set |
+| Option | Description |
+|--------|----------------------------------------------------------------|
+| Always | The field is always displayed when viewing an object (default) |
+| If set | The field is displayed only if a value has been defined |
+| Hidden | The field is not displayed when viewing an object |
+
+### UI Editable
+
+Controls whether the custom field is editable on objects within the NetBox user interface.
+
+| Option | Description |
+|--------|------------------------------------------------------------------------------|
+| Yes | The field's value may be changed when editing an object (default) |
+| No | The field's value is displayed when editing an object but may not be altered |
+| Hidden | The field is not displayed when editing an object |
### Default
diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py
index 4864253abd1..4e1b47503d0 100644
--- a/netbox/extras/api/serializers.py
+++ b/netbox/extras/api/serializers.py
@@ -95,15 +95,16 @@ class CustomFieldSerializer(ValidatedModelSerializer):
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
data_type = serializers.SerializerMethodField()
choice_set = NestedCustomFieldChoiceSetSerializer(required=False)
- ui_visibility = ChoiceField(choices=CustomFieldVisibilityChoices, required=False)
+ ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False)
+ ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False)
class Meta:
model = CustomField
fields = [
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
- 'description', 'required', 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'default',
- 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'created',
- 'last_updated',
+ 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
+ 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
+ 'created', 'last_updated',
]
def validate_type(self, value):
diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py
index 0572a33a129..fdb951b7ddf 100644
--- a/netbox/extras/choices.py
+++ b/netbox/extras/choices.py
@@ -53,18 +53,29 @@ class CustomFieldFilterLogicChoices(ChoiceSet):
)
-class CustomFieldVisibilityChoices(ChoiceSet):
+class CustomFieldUIVisibleChoices(ChoiceSet):
- VISIBILITY_READ_WRITE = 'read-write'
- VISIBILITY_READ_ONLY = 'read-only'
- VISIBILITY_HIDDEN = 'hidden'
- VISIBILITY_HIDDEN_IFUNSET = 'hidden-ifunset'
+ ALWAYS = 'always'
+ IF_SET = 'if-set'
+ HIDDEN = 'hidden'
CHOICES = (
- (VISIBILITY_READ_WRITE, _('Read/write')),
- (VISIBILITY_READ_ONLY, _('Read-only')),
- (VISIBILITY_HIDDEN, _('Hidden')),
- (VISIBILITY_HIDDEN_IFUNSET, _('Hidden (if unset)')),
+ (ALWAYS, _('Always'), 'green'),
+ (IF_SET, _('If set'), 'yellow'),
+ (HIDDEN, _('Hidden'), 'gray'),
+ )
+
+
+class CustomFieldUIEditableChoices(ChoiceSet):
+
+ YES = 'yes'
+ NO = 'no'
+ HIDDEN = 'hidden'
+
+ CHOICES = (
+ (YES, _('Yes'), 'green'),
+ (NO, _('No'), 'red'),
+ (HIDDEN, _('Hidden'), 'gray'),
)
diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py
index fec06726357..32850bee2cf 100644
--- a/netbox/extras/filtersets.py
+++ b/netbox/extras/filtersets.py
@@ -87,8 +87,8 @@ class CustomFieldFilterSet(BaseFilterSet):
class Meta:
model = CustomField
fields = [
- 'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visibility',
- 'weight', 'is_cloneable', 'description',
+ 'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
+ 'ui_editable', 'weight', 'is_cloneable', 'description',
]
def search(self, queryset, name, value):
diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py
index 821ce7eb243..5da2a5ddeca 100644
--- a/netbox/extras/forms/bulk_edit.py
+++ b/netbox/extras/forms/bulk_edit.py
@@ -48,11 +48,15 @@ class CustomFieldBulkEditForm(BulkEditForm):
queryset=CustomFieldChoiceSet.objects.all(),
required=False
)
- ui_visibility = forms.ChoiceField(
- label=_("UI visibility"),
- choices=add_blank_choice(CustomFieldVisibilityChoices),
- required=False,
- initial=''
+ ui_visible = forms.ChoiceField(
+ label=_("UI visible"),
+ choices=add_blank_choice(CustomFieldUIVisibleChoices),
+ required=False
+ )
+ ui_editable = forms.ChoiceField(
+ label=_("UI editable"),
+ choices=add_blank_choice(CustomFieldUIEditableChoices),
+ required=False
)
is_cloneable = forms.NullBooleanField(
label=_('Is cloneable'),
diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py
index 9b3f59af055..181b1f8d3eb 100644
--- a/netbox/extras/forms/bulk_import.py
+++ b/netbox/extras/forms/bulk_import.py
@@ -49,10 +49,17 @@ class CustomFieldImportForm(CSVModelForm):
required=False,
help_text=_('Choice set (for selection fields)')
)
- ui_visibility = CSVChoiceField(
- label=_('UI visibility'),
- choices=CustomFieldVisibilityChoices,
- help_text=_('How the custom field is displayed in the user interface')
+ ui_visible = CSVChoiceField(
+ label=_('UI visible'),
+ choices=CustomFieldUIVisibleChoices,
+ required=False,
+ help_text=_('Whether the custom field is displayed in the UI')
+ )
+ ui_editable = CSVChoiceField(
+ label=_('UI editable'),
+ choices=CustomFieldUIEditableChoices,
+ required=False,
+ help_text=_('Whether the custom field is editable in the UI')
)
class Meta:
@@ -60,7 +67,7 @@ class Meta:
fields = (
'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description',
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
- 'validation_maximum', 'validation_regex', 'ui_visibility', 'is_cloneable',
+ 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable',
)
diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py
index 2d438377b09..5da3ba1e66a 100644
--- a/netbox/extras/forms/filtersets.py
+++ b/netbox/extras/forms/filtersets.py
@@ -38,7 +38,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Attributes'), (
- 'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visibility',
+ 'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable',
'is_cloneable',
)),
)
@@ -72,10 +72,15 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
required=False,
label=_('Choice set')
)
- ui_visibility = forms.ChoiceField(
- choices=add_blank_choice(CustomFieldVisibilityChoices),
+ ui_visible = forms.ChoiceField(
+ choices=add_blank_choice(CustomFieldUIVisibleChoices),
required=False,
- label=_('UI visibility')
+ label=_('UI visible')
+ )
+ ui_editable = forms.ChoiceField(
+ choices=add_blank_choice(CustomFieldUIEditableChoices),
+ required=False,
+ label=_('UI editable')
)
is_cloneable = forms.NullBooleanField(
label=_('Is cloneable'),
diff --git a/netbox/extras/forms/mixins.py b/netbox/extras/forms/mixins.py
index 5366dcc285f..e9fb897c0ca 100644
--- a/netbox/extras/forms/mixins.py
+++ b/netbox/extras/forms/mixins.py
@@ -2,7 +2,7 @@
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
-from extras.choices import CustomFieldVisibilityChoices
+from extras.choices import *
from extras.models import *
from utilities.forms.fields import DynamicModelMultipleChoiceField
@@ -40,7 +40,7 @@ def _get_content_type(self):
def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).exclude(
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
+ ui_visible=CustomFieldUIVisibleChoices.HIDDEN
)
def _get_form_field(self, customfield):
@@ -51,9 +51,6 @@ def _append_customfield_fields(self):
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)
diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py
index 755f7e83659..1a4d45f9a68 100644
--- a/netbox/extras/forms/model_forms.py
+++ b/netbox/extras/forms/model_forms.py
@@ -59,7 +59,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
(_('Custom Field'), (
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description',
)),
- (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')),
+ (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')),
(_('Values'), ('default', 'choice_set')),
(_('Validation'), ('validation_minimum', 'validation_maximum', 'validation_regex')),
)
diff --git a/netbox/extras/migrations/0100_customfield_ui_attrs.py b/netbox/extras/migrations/0100_customfield_ui_attrs.py
new file mode 100644
index 00000000000..a4a713a865e
--- /dev/null
+++ b/netbox/extras/migrations/0100_customfield_ui_attrs.py
@@ -0,0 +1,41 @@
+from django.db import migrations, models
+
+
+def update_ui_attrs(apps, schema_editor):
+ """
+ Replicate legacy ui_visibility values to the new ui_visible and ui_editable fields.
+ """
+ CustomField = apps.get_model('extras', 'CustomField')
+
+ CustomField.objects.filter(ui_visibility='read-write').update(ui_visible='always', ui_editable='yes')
+ CustomField.objects.filter(ui_visibility='read-only').update(ui_visible='always', ui_editable='no')
+ CustomField.objects.filter(ui_visibility='hidden').update(ui_visible='hidden', ui_editable='hidden')
+ CustomField.objects.filter(ui_visibility='hidden-ifunset').update(ui_visible='if-set', ui_editable='yes')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0099_cachedvalue_ordering'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='customfield',
+ name='ui_editable',
+ field=models.CharField(default='yes', max_length=50),
+ ),
+ migrations.AddField(
+ model_name='customfield',
+ name='ui_visible',
+ field=models.CharField(default='always', max_length=50),
+ ),
+ migrations.RunPython(
+ code=update_ui_attrs,
+ reverse_code=migrations.RunPython.noop
+ ),
+ migrations.RemoveField(
+ model_name='customfield',
+ name='ui_visibility',
+ ),
+ ]
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index 939e8b73b1b..08190d20fb4 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -177,12 +177,19 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
blank=True,
null=True
)
- ui_visibility = models.CharField(
+ ui_visible = models.CharField(
max_length=50,
- choices=CustomFieldVisibilityChoices,
- default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
- verbose_name=_('UI visibility'),
- help_text=_('Specifies the visibility of custom field in the UI')
+ choices=CustomFieldUIVisibleChoices,
+ default=CustomFieldUIVisibleChoices.ALWAYS,
+ verbose_name=_('UI visible'),
+ help_text=_('Specifies whether the custom field is displayed in the UI')
+ )
+ ui_editable = models.CharField(
+ max_length=50,
+ choices=CustomFieldUIEditableChoices,
+ default=CustomFieldUIEditableChoices.YES,
+ verbose_name=_('UI editable'),
+ help_text=_('Specifies whether the custom field value can be edited in the UI')
)
is_cloneable = models.BooleanField(
default=False,
@@ -195,7 +202,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
clone_fields = (
'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight',
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
- 'choice_set', 'ui_visibility', 'is_cloneable',
+ 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
)
class Meta:
@@ -229,6 +236,12 @@ def choices(self):
return self.choice_set.choices
return []
+ def get_ui_visible_color(self):
+ return CustomFieldUIVisibleChoices.colors.get(self.ui_visible)
+
+ def get_ui_editable_color(self):
+ return CustomFieldUIEditableChoices.colors.get(self.ui_editable)
+
def get_choice_label(self, value):
if not hasattr(self, '_choice_map'):
self._choice_map = dict(self.choices)
@@ -379,7 +392,7 @@ def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibil
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
- enforce_visibility: Honor the value of CustomField.ui_visibility. Set to False for filtering.
+ enforce_visibility: Honor the value of CustomField.ui_visible. Set to False for filtering.
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
"""
initial = self.default if set_initial else None
@@ -504,10 +517,10 @@ def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibil
field.help_text = render_markdown(self.description)
# Annotate read-only fields
- if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
+ if enforce_visibility and self.ui_editable != CustomFieldUIEditableChoices.YES:
field.disabled = True
prepend = '
' if field.help_text else ''
- field.help_text += f'{prepend} ' + _('Field is set to read-only.')
+ field.help_text += f'{prepend} ' + _('Field is not editable.')
return field
diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py
index 9e14a2d2745..54194c00fb8 100644
--- a/netbox/extras/tables/tables.py
+++ b/netbox/extras/tables/tables.py
@@ -71,8 +71,11 @@ class CustomFieldTable(NetBoxTable):
required = columns.BooleanColumn(
verbose_name=_('Required')
)
- ui_visibility = columns.ChoiceFieldColumn(
- verbose_name=_('UI Visibility')
+ ui_visible = columns.ChoiceFieldColumn(
+ verbose_name=_('Visible')
+ )
+ ui_editable = columns.ChoiceFieldColumn(
+ verbose_name=_('Editable')
)
description = columns.MarkdownColumn(
verbose_name=_('Description')
@@ -94,8 +97,8 @@ class Meta(NetBoxTable.Meta):
model = CustomField
fields = (
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description',
- 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choice_set', 'choices',
- 'created', 'last_updated',
+ 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set',
+ 'choices', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py
index 69111e6a781..c5a6706c07f 100644
--- a/netbox/extras/tests/test_filtersets.py
+++ b/netbox/extras/tests/test_filtersets.py
@@ -40,7 +40,8 @@ def setUpTestData(cls):
required=True,
weight=100,
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE,
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE
+ ui_visible=CustomFieldUIVisibleChoices.ALWAYS,
+ ui_editable=CustomFieldUIEditableChoices.YES
),
CustomField(
name='Custom Field 2',
@@ -48,7 +49,8 @@ def setUpTestData(cls):
required=False,
weight=200,
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT,
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY
+ ui_visible=CustomFieldUIVisibleChoices.IF_SET,
+ ui_editable=CustomFieldUIEditableChoices.NO
),
CustomField(
name='Custom Field 3',
@@ -56,7 +58,8 @@ def setUpTestData(cls):
required=False,
weight=300,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
+ ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
+ ui_editable=CustomFieldUIEditableChoices.HIDDEN
),
CustomField(
name='Custom Field 4',
@@ -64,7 +67,8 @@ def setUpTestData(cls):
required=False,
weight=400,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN,
+ ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
+ ui_editable=CustomFieldUIEditableChoices.HIDDEN,
choice_set=choice_sets[0]
),
CustomField(
@@ -73,7 +77,8 @@ def setUpTestData(cls):
required=False,
weight=500,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN,
+ ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
+ ui_editable=CustomFieldUIEditableChoices.HIDDEN,
choice_set=choice_sets[1]
),
)
@@ -106,8 +111,12 @@ def test_filter_logic(self):
params = {'filter_logic': CustomFieldFilterLogicChoices.FILTER_LOOSE}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
- def test_ui_visibility(self):
- params = {'ui_visibility': CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE}
+ def test_ui_visible(self):
+ params = {'ui_visible': CustomFieldUIVisibleChoices.ALWAYS}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_ui_editable(self):
+ params = {'ui_editable': CustomFieldUIEditableChoices.YES}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_choice_set(self):
diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py
index e034abff53b..3d4b3e9a9f2 100644
--- a/netbox/extras/tests/test_views.py
+++ b/netbox/extras/tests/test_views.py
@@ -50,15 +50,16 @@ def setUpTestData(cls):
'default': None,
'weight': 200,
'required': True,
- 'ui_visibility': CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
+ 'ui_visible': CustomFieldUIVisibleChoices.ALWAYS,
+ 'ui_editable': CustomFieldUIEditableChoices.YES,
}
cls.csv_data = (
- 'name,label,type,content_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visibility',
- 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},read-write',
- 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,read-write',
- 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,read-write',
- 'field7,Field 7,object,dcim.site,dcim.region,100,4000,exact,,,,,read-write',
+ 'name,label,type,content_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable',
+ 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes',
+ 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes',
+ 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes',
+ 'field7,Field 7,object,dcim.site,dcim.region,100,4000,exact,,,,,always,yes',
)
cls.csv_update_data = (
diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py
index 43d0850f0eb..b51efe9c01d 100644
--- a/netbox/netbox/forms/base.py
+++ b/netbox/netbox/forms/base.py
@@ -3,7 +3,7 @@
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
-from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
+from extras.choices import *
from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
from extras.models import CustomField, Tag
from utilities.forms import CSVModelForm
@@ -76,11 +76,9 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
)
def _get_custom_fields(self, content_type):
- return CustomField.objects.filter(content_types=content_type).filter(
- ui_visibility__in=[
- CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
- CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET,
- ]
+ return CustomField.objects.filter(
+ content_types=content_type,
+ ui_editable=CustomFieldUIEditableChoices.YES
)
def _get_form_field(self, customfield):
@@ -131,7 +129,8 @@ def _get_form_field(self, customfield):
def _extend_nullable_fields(self):
nullable_custom_fields = [
- name for name, customfield in self.custom_fields.items() if (not customfield.required and customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE)
+ name for name, customfield in self.custom_fields.items()
+ if (not customfield.required and customfield.ui_editable == CustomFieldUIEditableChoices.YES)
]
self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py
index 11307b4f855..f39f3562081 100644
--- a/netbox/netbox/models/features.py
+++ b/netbox/netbox/models/features.py
@@ -13,7 +13,7 @@
from core.choices import JobStatusChoices
from core.models import ContentType
-from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
+from extras.choices import *
from extras.utils import is_taggable, register_features
from netbox.registry import registry
from netbox.signals import post_clean
@@ -205,12 +205,11 @@ def get_custom_fields(self, omit_hidden=False):
for field in CustomField.objects.get_for_model(self):
value = self.custom_field_data.get(field.name)
- # Skip fields that are hidden if 'omit_hidden' is set
- if omit_hidden:
- if field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN:
- continue
- if field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET and not value:
- continue
+ # Skip hidden fields if 'omit_hidden' is True
+ if omit_hidden and field.ui_visible == CustomFieldUIVisibleChoices.HIDDEN:
+ continue
+ elif omit_hidden and field.ui_visible == CustomFieldUIVisibleChoices.IF_SET and not value:
+ continue
data[field] = field.deserialize(value)
@@ -232,12 +231,12 @@ def get_custom_fields_by_group(self):
from extras.models import CustomField
groups = defaultdict(dict)
visible_custom_fields = CustomField.objects.get_for_model(self).exclude(
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
+ ui_visible=CustomFieldUIVisibleChoices.HIDDEN
)
for cf in visible_custom_fields:
value = self.custom_field_data.get(cf.name)
- if value in (None, []) and cf.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET:
+ if value in (None, []) and cf.ui_visible == CustomFieldUIVisibleChoices.IF_SET:
continue
value = cf.deserialize(value)
groups[cf.group_name][cf] = value
diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py
index 83dc3ae3cb2..495e569915c 100644
--- a/netbox/netbox/tables/tables.py
+++ b/netbox/netbox/tables/tables.py
@@ -12,8 +12,8 @@
from django.utils.translation import gettext_lazy as _
from django_tables2.data import TableQuerysetData
+from extras.choices import *
from extras.models import CustomField, CustomLink
-from extras.choices import CustomFieldVisibilityChoices
from netbox.registry import registry
from netbox.tables import columns
from utilities.paginator import EnhancedPaginator, get_paginate_count
@@ -204,7 +204,7 @@ def __init__(self, *args, extra_columns=None, **kwargs):
content_type = ContentType.objects.get_for_model(self._meta.model)
custom_fields = CustomField.objects.filter(
content_types=content_type
- ).exclude(ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN)
+ ).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN)
extra_columns.extend([
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
])
diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html
index dd5cce7bdbd..95919b4147f 100644
--- a/netbox/templates/extras/customfield.html
+++ b/netbox/templates/extras/customfield.html
@@ -79,8 +79,12 @@