1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #13299: Improve options for controlling custom field visibility (#14289)

* Add ui_visible and ui_editable fields

* Extend migration to map new visible/editable values

* Remove ui_visibility field

* Update docs
This commit is contained in:
Jeremy Stretch
2023-11-20 13:06:34 -05:00
committed by GitHub
parent 549b0ea107
commit a73ba00aa0
19 changed files with 204 additions and 93 deletions

View File

@ -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. 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. 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:
* **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.)
* **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. * **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. Note that this setting has no impact on the REST or GraphQL APIs: Custom field data will always be available via either API.
### Validation ### Validation

View File

@ -64,16 +64,25 @@ Defines how filters are evaluated against custom field values.
| Loose | Match any occurrence of the value | | Loose | Match any occurrence of the value |
| Exact | Match only the complete field 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 | | Option | Description |
|-------------------|--------------------------------------------------| |--------|----------------------------------------------------------------|
| Read/write | Display and permit editing (default) | | Always | The field is always displayed when viewing an object (default) |
| Read-only | Display field but disallow editing | | If set | The field is displayed only if a value has been defined |
| Hidden | Do not display field in the UI | | Hidden | The field is not displayed when viewing an object |
| Hidden (if unset) | Display in the UI only when a value has been set |
### 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 ### Default

View File

@ -95,15 +95,16 @@ class CustomFieldSerializer(ValidatedModelSerializer):
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
data_type = serializers.SerializerMethodField() data_type = serializers.SerializerMethodField()
choice_set = NestedCustomFieldChoiceSetSerializer(required=False) 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: class Meta:
model = CustomField model = CustomField
fields = [ fields = [
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', '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', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'created', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
'last_updated', 'created', 'last_updated',
] ]
def validate_type(self, value): def validate_type(self, value):

View File

@ -53,18 +53,29 @@ class CustomFieldFilterLogicChoices(ChoiceSet):
) )
class CustomFieldVisibilityChoices(ChoiceSet): class CustomFieldUIVisibleChoices(ChoiceSet):
VISIBILITY_READ_WRITE = 'read-write' ALWAYS = 'always'
VISIBILITY_READ_ONLY = 'read-only' IF_SET = 'if-set'
VISIBILITY_HIDDEN = 'hidden' HIDDEN = 'hidden'
VISIBILITY_HIDDEN_IFUNSET = 'hidden-ifunset'
CHOICES = ( CHOICES = (
(VISIBILITY_READ_WRITE, _('Read/write')), (ALWAYS, _('Always'), 'green'),
(VISIBILITY_READ_ONLY, _('Read-only')), (IF_SET, _('If set'), 'yellow'),
(VISIBILITY_HIDDEN, _('Hidden')), (HIDDEN, _('Hidden'), 'gray'),
(VISIBILITY_HIDDEN_IFUNSET, _('Hidden (if unset)')), )
class CustomFieldUIEditableChoices(ChoiceSet):
YES = 'yes'
NO = 'no'
HIDDEN = 'hidden'
CHOICES = (
(YES, _('Yes'), 'green'),
(NO, _('No'), 'red'),
(HIDDEN, _('Hidden'), 'gray'),
) )

View File

@ -87,8 +87,8 @@ class CustomFieldFilterSet(BaseFilterSet):
class Meta: class Meta:
model = CustomField model = CustomField
fields = [ fields = [
'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visibility', 'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
'weight', 'is_cloneable', 'description', 'ui_editable', 'weight', 'is_cloneable', 'description',
] ]
def search(self, queryset, name, value): def search(self, queryset, name, value):

View File

@ -48,11 +48,15 @@ class CustomFieldBulkEditForm(BulkEditForm):
queryset=CustomFieldChoiceSet.objects.all(), queryset=CustomFieldChoiceSet.objects.all(),
required=False required=False
) )
ui_visibility = forms.ChoiceField( ui_visible = forms.ChoiceField(
label=_("UI visibility"), label=_("UI visible"),
choices=add_blank_choice(CustomFieldVisibilityChoices), choices=add_blank_choice(CustomFieldUIVisibleChoices),
required=False, required=False
initial='' )
ui_editable = forms.ChoiceField(
label=_("UI editable"),
choices=add_blank_choice(CustomFieldUIEditableChoices),
required=False
) )
is_cloneable = forms.NullBooleanField( is_cloneable = forms.NullBooleanField(
label=_('Is cloneable'), label=_('Is cloneable'),

View File

@ -49,10 +49,17 @@ class CustomFieldImportForm(CSVModelForm):
required=False, required=False,
help_text=_('Choice set (for selection fields)') help_text=_('Choice set (for selection fields)')
) )
ui_visibility = CSVChoiceField( ui_visible = CSVChoiceField(
label=_('UI visibility'), label=_('UI visible'),
choices=CustomFieldVisibilityChoices, choices=CustomFieldUIVisibleChoices,
help_text=_('How the custom field is displayed in the user interface') 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: class Meta:
@ -60,7 +67,7 @@ class CustomFieldImportForm(CSVModelForm):
fields = ( fields = (
'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description', 'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description',
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum', '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',
) )

View File

@ -38,7 +38,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
(_('Attributes'), ( (_('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', 'is_cloneable',
)), )),
) )
@ -72,10 +72,15 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
required=False, required=False,
label=_('Choice set') label=_('Choice set')
) )
ui_visibility = forms.ChoiceField( ui_visible = forms.ChoiceField(
choices=add_blank_choice(CustomFieldVisibilityChoices), choices=add_blank_choice(CustomFieldUIVisibleChoices),
required=False, 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( is_cloneable = forms.NullBooleanField(
label=_('Is cloneable'), label=_('Is cloneable'),

View File

@ -2,7 +2,7 @@ from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from extras.choices import CustomFieldVisibilityChoices from extras.choices import *
from extras.models import * from extras.models import *
from utilities.forms.fields import DynamicModelMultipleChoiceField from utilities.forms.fields import DynamicModelMultipleChoiceField
@ -40,7 +40,7 @@ class CustomFieldsMixin:
def _get_custom_fields(self, content_type): def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).exclude( return CustomField.objects.filter(content_types=content_type).exclude(
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN ui_visible=CustomFieldUIVisibleChoices.HIDDEN
) )
def _get_form_field(self, customfield): def _get_form_field(self, customfield):
@ -51,9 +51,6 @@ class CustomFieldsMixin:
Append form fields for all CustomFields assigned to this object type. Append form fields for all CustomFields assigned to this object type.
""" """
for customfield in self._get_custom_fields(self._get_content_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}' field_name = f'cf_{customfield.name}'
self.fields[field_name] = self._get_form_field(customfield) self.fields[field_name] = self._get_form_field(customfield)

View File

@ -59,7 +59,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
(_('Custom Field'), ( (_('Custom Field'), (
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', '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')), (_('Values'), ('default', 'choice_set')),
(_('Validation'), ('validation_minimum', 'validation_maximum', 'validation_regex')), (_('Validation'), ('validation_minimum', 'validation_maximum', 'validation_regex')),
) )

View File

@ -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',
),
]

View File

@ -177,12 +177,19 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
blank=True, blank=True,
null=True null=True
) )
ui_visibility = models.CharField( ui_visible = models.CharField(
max_length=50, max_length=50,
choices=CustomFieldVisibilityChoices, choices=CustomFieldUIVisibleChoices,
default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE, default=CustomFieldUIVisibleChoices.ALWAYS,
verbose_name=_('UI visibility'), verbose_name=_('UI visible'),
help_text=_('Specifies the visibility of custom field in the UI') 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( is_cloneable = models.BooleanField(
default=False, default=False,
@ -195,7 +202,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
clone_fields = ( clone_fields = (
'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight', 'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight',
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', '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: class Meta:
@ -229,6 +236,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
return self.choice_set.choices return self.choice_set.choices
return [] 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): def get_choice_label(self, value):
if not hasattr(self, '_choice_map'): if not hasattr(self, '_choice_map'):
self._choice_map = dict(self.choices) self._choice_map = dict(self.choices)
@ -379,7 +392,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing. 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_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. for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
""" """
initial = self.default if set_initial else None initial = self.default if set_initial else None
@ -504,10 +517,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
field.help_text = render_markdown(self.description) field.help_text = render_markdown(self.description)
# Annotate read-only fields # 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 field.disabled = True
prepend = '<br />' if field.help_text else '' prepend = '<br />' if field.help_text else ''
field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> ' + _('Field is set to read-only.') field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> ' + _('Field is not editable.')
return field return field

View File

@ -71,8 +71,11 @@ class CustomFieldTable(NetBoxTable):
required = columns.BooleanColumn( required = columns.BooleanColumn(
verbose_name=_('Required') verbose_name=_('Required')
) )
ui_visibility = columns.ChoiceFieldColumn( ui_visible = columns.ChoiceFieldColumn(
verbose_name=_('UI Visibility') verbose_name=_('Visible')
)
ui_editable = columns.ChoiceFieldColumn(
verbose_name=_('Editable')
) )
description = columns.MarkdownColumn( description = columns.MarkdownColumn(
verbose_name=_('Description') verbose_name=_('Description')
@ -94,8 +97,8 @@ class CustomFieldTable(NetBoxTable):
model = CustomField model = CustomField
fields = ( fields = (
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description', 'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description',
'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choice_set', 'choices', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set',
'created', 'last_updated', 'choices', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description') default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')

View File

@ -40,7 +40,8 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
required=True, required=True,
weight=100, weight=100,
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE, filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE,
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE ui_visible=CustomFieldUIVisibleChoices.ALWAYS,
ui_editable=CustomFieldUIEditableChoices.YES
), ),
CustomField( CustomField(
name='Custom Field 2', name='Custom Field 2',
@ -48,7 +49,8 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
required=False, required=False,
weight=200, weight=200,
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT, filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT,
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY ui_visible=CustomFieldUIVisibleChoices.IF_SET,
ui_editable=CustomFieldUIEditableChoices.NO
), ),
CustomField( CustomField(
name='Custom Field 3', name='Custom Field 3',
@ -56,7 +58,8 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
required=False, required=False,
weight=300, weight=300,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED, filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
ui_editable=CustomFieldUIEditableChoices.HIDDEN
), ),
CustomField( CustomField(
name='Custom Field 4', name='Custom Field 4',
@ -64,7 +67,8 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
required=False, required=False,
weight=400, weight=400,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED, filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN, ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
ui_editable=CustomFieldUIEditableChoices.HIDDEN,
choice_set=choice_sets[0] choice_set=choice_sets[0]
), ),
CustomField( CustomField(
@ -73,7 +77,8 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
required=False, required=False,
weight=500, weight=500,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED, filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN, ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
ui_editable=CustomFieldUIEditableChoices.HIDDEN,
choice_set=choice_sets[1] choice_set=choice_sets[1]
), ),
) )
@ -106,8 +111,12 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
params = {'filter_logic': CustomFieldFilterLogicChoices.FILTER_LOOSE} params = {'filter_logic': CustomFieldFilterLogicChoices.FILTER_LOOSE}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_ui_visibility(self): def test_ui_visible(self):
params = {'ui_visibility': CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE} 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) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_choice_set(self): def test_choice_set(self):

View File

@ -50,15 +50,16 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'default': None, 'default': None,
'weight': 200, 'weight': 200,
'required': True, 'required': True,
'ui_visibility': CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE, 'ui_visible': CustomFieldUIVisibleChoices.ALWAYS,
'ui_editable': CustomFieldUIEditableChoices.YES,
} }
cls.csv_data = ( 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', '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},read-write', '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,,read-write', '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,,,,read-write', '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,,,,,read-write', 'field7,Field 7,object,dcim.site,dcim.region,100,4000,exact,,,,,always,yes',
) )
cls.csv_update_data = ( cls.csv_update_data = (

View File

@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ 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.forms.mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
from extras.models import CustomField, Tag from extras.models import CustomField, Tag
from utilities.forms import CSVModelForm from utilities.forms import CSVModelForm
@ -76,11 +76,9 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
) )
def _get_custom_fields(self, content_type): def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).filter( return CustomField.objects.filter(
ui_visibility__in=[ content_types=content_type,
CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE, ui_editable=CustomFieldUIEditableChoices.YES
CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET,
]
) )
def _get_form_field(self, customfield): def _get_form_field(self, customfield):
@ -131,7 +129,8 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
def _extend_nullable_fields(self): def _extend_nullable_fields(self):
nullable_custom_fields = [ 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) self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)

View File

@ -13,7 +13,7 @@ from taggit.managers import TaggableManager
from core.choices import JobStatusChoices from core.choices import JobStatusChoices
from core.models import ContentType from core.models import ContentType
from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices from extras.choices import *
from extras.utils import is_taggable, register_features from extras.utils import is_taggable, register_features
from netbox.registry import registry from netbox.registry import registry
from netbox.signals import post_clean from netbox.signals import post_clean
@ -205,12 +205,11 @@ class CustomFieldsMixin(models.Model):
for field in CustomField.objects.get_for_model(self): for field in CustomField.objects.get_for_model(self):
value = self.custom_field_data.get(field.name) value = self.custom_field_data.get(field.name)
# Skip fields that are hidden if 'omit_hidden' is set # Skip hidden fields if 'omit_hidden' is True
if omit_hidden: if omit_hidden and field.ui_visible == CustomFieldUIVisibleChoices.HIDDEN:
if field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN: continue
continue elif omit_hidden and field.ui_visible == CustomFieldUIVisibleChoices.IF_SET and not value:
if field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET and not value: continue
continue
data[field] = field.deserialize(value) data[field] = field.deserialize(value)
@ -232,12 +231,12 @@ class CustomFieldsMixin(models.Model):
from extras.models import CustomField from extras.models import CustomField
groups = defaultdict(dict) groups = defaultdict(dict)
visible_custom_fields = CustomField.objects.get_for_model(self).exclude( 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: for cf in visible_custom_fields:
value = self.custom_field_data.get(cf.name) 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 continue
value = cf.deserialize(value) value = cf.deserialize(value)
groups[cf.group_name][cf] = value groups[cf.group_name][cf] = value

View File

@ -12,8 +12,8 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_tables2.data import TableQuerysetData from django_tables2.data import TableQuerysetData
from extras.choices import *
from extras.models import CustomField, CustomLink from extras.models import CustomField, CustomLink
from extras.choices import CustomFieldVisibilityChoices
from netbox.registry import registry from netbox.registry import registry
from netbox.tables import columns from netbox.tables import columns
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
@ -204,7 +204,7 @@ class NetBoxTable(BaseTable):
content_type = ContentType.objects.get_for_model(self._meta.model) content_type = ContentType.objects.get_for_model(self._meta.model)
custom_fields = CustomField.objects.filter( custom_fields = CustomField.objects.filter(
content_types=content_type content_types=content_type
).exclude(ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN) ).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN)
extra_columns.extend([ extra_columns.extend([
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
]) ])

View File

@ -79,8 +79,12 @@
<td>{{ object.weight }}</td> <td>{{ object.weight }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans "UI Visibility" %}</th> <th scope="row">{% trans "UI Visible" %}</th>
<td>{{ object.get_ui_visibility_display }}</td> <td>{{ object.get_ui_visible_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "UI Editable" %}</th>
<td>{{ object.get_ui_editable_display }}</td>
</tr> </tr>
</table> </table>
</div> </div>