mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
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
This commit is contained in:
commit
aea023357b
@ -84,13 +84,14 @@ 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()
|
||||||
|
ui_visibility = ChoiceField(choices=CustomFieldVisibilityChoices, 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', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum',
|
'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):
|
def get_data_type(self, obj):
|
||||||
|
@ -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
|
# CustomLinks
|
||||||
#
|
#
|
||||||
|
@ -62,7 +62,9 @@ class CustomFieldFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
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):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -37,6 +37,13 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
|||||||
weight = forms.IntegerField(
|
weight = forms.IntegerField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
ui_visibility = forms.ChoiceField(
|
||||||
|
label="UI visibility",
|
||||||
|
choices=add_blank_choice(CustomFieldVisibilityChoices),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
|
||||||
nullable_fields = ('group_name', 'description',)
|
nullable_fields = ('group_name', 'description',)
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ class CustomFieldCSVForm(CSVModelForm):
|
|||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'label', 'group_name', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic',
|
'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',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
|
from extras.choices import CustomFieldVisibilityChoices
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CustomFieldsMixin',
|
'CustomFieldsMixin',
|
||||||
@ -42,8 +43,14 @@ 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)
|
||||||
|
|
||||||
|
if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
|
||||||
|
self.fields[field_name].disabled = True
|
||||||
|
|
||||||
# Annotate the field in the list of CustomField form fields
|
# Annotate the field in the list of CustomField form fields
|
||||||
self.custom_fields[field_name] = customfield
|
self.custom_fields[field_name] = customfield
|
||||||
|
@ -32,7 +32,7 @@ __all__ = (
|
|||||||
class CustomFieldFilterForm(FilterForm):
|
class CustomFieldFilterForm(FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q',)),
|
||||||
('Attributes', ('content_types', 'type', 'group_name', 'weight', 'required')),
|
('Attributes', ('content_types', 'type', 'group_name', 'weight', 'required', 'ui_visibility')),
|
||||||
)
|
)
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
@ -56,6 +56,12 @@ class CustomFieldFilterForm(FilterForm):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
ui_visibility = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(CustomFieldVisibilityChoices),
|
||||||
|
required=False,
|
||||||
|
label=_('UI Visibility'),
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkFilterForm(FilterForm):
|
class CustomLinkFilterForm(FilterForm):
|
||||||
|
@ -41,7 +41,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Custom Field', (
|
('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',)),
|
('Behavior', ('filter_logic',)),
|
||||||
('Values', ('default', 'choices')),
|
('Values', ('default', 'choices')),
|
||||||
@ -58,6 +58,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
widgets = {
|
widgets = {
|
||||||
'type': StaticSelect(),
|
'type': StaticSelect(),
|
||||||
'filter_logic': StaticSelect(),
|
'filter_logic': StaticSelect(),
|
||||||
|
'ui_visibility': StaticSelect(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
18
netbox/extras/migrations/0075_customfield_ui_visibility.py
Normal file
18
netbox/extras/migrations/0075_customfield_ui_visibility.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -136,6 +136,12 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text='Comma-separated list of available choices (for selection fields)'
|
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()
|
objects = CustomFieldManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -28,12 +28,13 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
content_types = columns.ContentTypesColumn()
|
content_types = columns.ContentTypesColumn()
|
||||||
required = columns.BooleanColumn()
|
required = columns.BooleanColumn()
|
||||||
|
ui_visibility = columns.ChoiceFieldColumn(verbose_name="UI visibility")
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'weight', 'default',
|
'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')
|
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
||||||
|
|
||||||
|
@ -36,13 +36,14 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'default': None,
|
'default': None,
|
||||||
'weight': 200,
|
'weight': 200,
|
||||||
'required': True,
|
'required': True,
|
||||||
|
'ui_visibility': CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex',
|
'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}',
|
'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3},read-write',
|
||||||
'field5,Field 5,integer,dcim.site,100,exact,,1,100,',
|
'field5,Field 5,integer,dcim.site,100,exact,,1,100,,read-write',
|
||||||
'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,',
|
'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,,read-write',
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
|
@ -9,7 +9,7 @@ from django.core.validators import ValidationError
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from extras.choices import ObjectChangeActionChoices
|
from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
|
||||||
from extras.utils import register_features
|
from extras.utils import register_features
|
||||||
from netbox.signals import post_clean
|
from netbox.signals import post_clean
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
@ -100,7 +100,7 @@ class CustomFieldsMixin(models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.custom_field_data
|
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}`.
|
Return a dictionary of custom fields for a single object in the form `{field: value}`.
|
||||||
|
|
||||||
@ -114,6 +114,10 @@ class CustomFieldsMixin(models.Model):
|
|||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
for field in CustomField.objects.get_for_model(self):
|
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)
|
value = self.custom_field_data.get(field.name)
|
||||||
data[field] = field.deserialize(value)
|
data[field] = field.deserialize(value)
|
||||||
|
|
||||||
@ -124,7 +128,7 @@ class CustomFieldsMixin(models.Model):
|
|||||||
Return a dictionary of custom field/value mappings organized by group.
|
Return a dictionary of custom field/value mappings organized by group.
|
||||||
"""
|
"""
|
||||||
grouped_custom_fields = defaultdict(dict)
|
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
|
grouped_custom_fields[cf.group_name][cf] = value
|
||||||
|
|
||||||
return dict(grouped_custom_fields)
|
return dict(grouped_custom_fields)
|
||||||
|
@ -7,6 +7,7 @@ from django.db.models.fields.related import RelatedField
|
|||||||
from django_tables2.data import TableQuerysetData
|
from django_tables2.data import TableQuerysetData
|
||||||
|
|
||||||
from extras.models import CustomField, CustomLink
|
from extras.models import CustomField, CustomLink
|
||||||
|
from extras.choices import CustomFieldVisibilityChoices
|
||||||
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
|
||||||
|
|
||||||
@ -178,7 +179,10 @@ class NetBoxTable(BaseTable):
|
|||||||
|
|
||||||
# Add custom field & custom link columns
|
# Add custom field & custom link columns
|
||||||
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(content_types=content_type)
|
custom_fields = CustomField.objects.filter(
|
||||||
|
content_types=content_type
|
||||||
|
).exclude(ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_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
|
||||||
])
|
])
|
||||||
|
@ -42,6 +42,10 @@
|
|||||||
<th scope="row">Weight</th>
|
<th scope="row">Weight</th>
|
||||||
<td>{{ object.weight }}</td>
|
<td>{{ object.weight }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">UI Visibility</th>
|
||||||
|
<td>{{ object.get_ui_visibility_display }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user