mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
* 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:
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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'),
|
||||||
|
@ -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',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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'),
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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')),
|
||||||
)
|
)
|
||||||
|
41
netbox/extras/migrations/0100_customfield_ui_attrs.py
Normal file
41
netbox/extras/migrations/0100_customfield_ui_attrs.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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 = (
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
])
|
])
|
||||||
|
@ -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>
|
||||||
|
Reference in New Issue
Block a user