mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 08:46:10 -06:00
Initial work on custom field choice sets
This commit is contained in:
parent
837be4d45f
commit
70e1e1197e
@ -7,6 +7,7 @@ __all__ = [
|
|||||||
'NestedBookmarkSerializer',
|
'NestedBookmarkSerializer',
|
||||||
'NestedConfigContextSerializer',
|
'NestedConfigContextSerializer',
|
||||||
'NestedConfigTemplateSerializer',
|
'NestedConfigTemplateSerializer',
|
||||||
|
'NestedCustomFieldChoiceSetSerializer',
|
||||||
'NestedCustomFieldSerializer',
|
'NestedCustomFieldSerializer',
|
||||||
'NestedCustomLinkSerializer',
|
'NestedCustomLinkSerializer',
|
||||||
'NestedExportTemplateSerializer',
|
'NestedExportTemplateSerializer',
|
||||||
@ -34,6 +35,14 @@ class NestedCustomFieldSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name']
|
fields = ['id', 'url', 'display', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedCustomFieldChoiceSetSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.CustomFieldChoiceSet
|
||||||
|
fields = ['id', 'url', 'display', 'name']
|
||||||
|
|
||||||
|
|
||||||
class NestedCustomLinkSerializer(WritableNestedSerializer):
|
class NestedCustomLinkSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ __all__ = (
|
|||||||
'ConfigContextSerializer',
|
'ConfigContextSerializer',
|
||||||
'ConfigTemplateSerializer',
|
'ConfigTemplateSerializer',
|
||||||
'ContentTypeSerializer',
|
'ContentTypeSerializer',
|
||||||
|
'CustomFieldChoiceSetSerializer',
|
||||||
'CustomFieldSerializer',
|
'CustomFieldSerializer',
|
||||||
'CustomLinkSerializer',
|
'CustomLinkSerializer',
|
||||||
'DashboardSerializer',
|
'DashboardSerializer',
|
||||||
@ -94,6 +95,7 @@ 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)
|
||||||
ui_visibility = ChoiceField(choices=CustomFieldVisibilityChoices, required=False)
|
ui_visibility = ChoiceField(choices=CustomFieldVisibilityChoices, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -101,8 +103,8 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
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_visibility', 'is_cloneable', 'default',
|
||||||
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created',
|
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'choices',
|
||||||
'last_updated',
|
'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_type(self, value):
|
def validate_type(self, value):
|
||||||
@ -127,6 +129,16 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
return 'string'
|
return 'string'
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomFieldChoiceSet
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'name', 'description', 'choices', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Custom links
|
# Custom links
|
||||||
#
|
#
|
||||||
|
@ -9,6 +9,7 @@ router.APIRootView = views.ExtrasRootView
|
|||||||
|
|
||||||
router.register('webhooks', views.WebhookViewSet)
|
router.register('webhooks', views.WebhookViewSet)
|
||||||
router.register('custom-fields', views.CustomFieldViewSet)
|
router.register('custom-fields', views.CustomFieldViewSet)
|
||||||
|
router.register('custom-field-choices', views.CustomFieldChoiceSetViewSet)
|
||||||
router.register('custom-links', views.CustomLinkViewSet)
|
router.register('custom-links', views.CustomLinkViewSet)
|
||||||
router.register('export-templates', views.ExportTemplateViewSet)
|
router.register('export-templates', views.ExportTemplateViewSet)
|
||||||
router.register('saved-filters', views.SavedFilterViewSet)
|
router.register('saved-filters', views.SavedFilterViewSet)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
@ -55,11 +54,17 @@ class WebhookViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
class CustomFieldViewSet(NetBoxModelViewSet):
|
class CustomFieldViewSet(NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.select_related('choice_set')
|
||||||
serializer_class = serializers.CustomFieldSerializer
|
serializer_class = serializers.CustomFieldSerializer
|
||||||
filterset_class = filtersets.CustomFieldFilterSet
|
filterset_class = filtersets.CustomFieldFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
|
||||||
|
queryset = CustomFieldChoiceSet.objects.all()
|
||||||
|
serializer_class = serializers.CustomFieldChoiceSetSerializer
|
||||||
|
filterset_class = filtersets.CustomFieldChoiceSetFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Custom links
|
# Custom links
|
||||||
#
|
#
|
||||||
|
@ -20,6 +20,7 @@ __all__ = (
|
|||||||
'ConfigRevisionFilterSet',
|
'ConfigRevisionFilterSet',
|
||||||
'ConfigTemplateFilterSet',
|
'ConfigTemplateFilterSet',
|
||||||
'ContentTypeFilterSet',
|
'ContentTypeFilterSet',
|
||||||
|
'CustomFieldChoiceSetFilterSet',
|
||||||
'CustomFieldFilterSet',
|
'CustomFieldFilterSet',
|
||||||
'CustomLinkFilterSet',
|
'CustomLinkFilterSet',
|
||||||
'ExportTemplateFilterSet',
|
'ExportTemplateFilterSet',
|
||||||
@ -74,6 +75,9 @@ class CustomFieldFilterSet(BaseFilterSet):
|
|||||||
field_name='content_types__id'
|
field_name='content_types__id'
|
||||||
)
|
)
|
||||||
content_types = ContentTypeFilter()
|
content_types = ContentTypeFilter()
|
||||||
|
choice_set_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=CustomFieldChoiceSet.objects.all()
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
@ -93,6 +97,34 @@ class CustomFieldFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetFilterSet(BaseFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label=_('Search'),
|
||||||
|
)
|
||||||
|
choice = MultiValueCharFilter(
|
||||||
|
method='filter_by_choice'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomFieldChoiceSet
|
||||||
|
fields = [
|
||||||
|
'id', 'name', 'description',
|
||||||
|
]
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value) |
|
||||||
|
Q(choices__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_by_choice(self, queryset, name, value):
|
||||||
|
return queryset.filter(choices__icontains=value.strip())
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkFilterSet(BaseFilterSet):
|
class CustomLinkFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
@ -4,13 +4,14 @@ from django.utils.translation import gettext as _
|
|||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from utilities.forms import BulkEditForm, add_blank_choice
|
from utilities.forms import BulkEditForm, add_blank_choice
|
||||||
from utilities.forms.fields import ColorField
|
from utilities.forms.fields import ColorField, DynamicModelChoiceField
|
||||||
from utilities.forms.widgets import BulkEditNullBooleanSelect
|
from utilities.forms.widgets import BulkEditNullBooleanSelect
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextBulkEditForm',
|
'ConfigContextBulkEditForm',
|
||||||
'ConfigTemplateBulkEditForm',
|
'ConfigTemplateBulkEditForm',
|
||||||
'CustomFieldBulkEditForm',
|
'CustomFieldBulkEditForm',
|
||||||
|
'CustomFieldChoiceSetBulkEditForm',
|
||||||
'CustomLinkBulkEditForm',
|
'CustomLinkBulkEditForm',
|
||||||
'ExportTemplateBulkEditForm',
|
'ExportTemplateBulkEditForm',
|
||||||
'JournalEntryBulkEditForm',
|
'JournalEntryBulkEditForm',
|
||||||
@ -38,6 +39,10 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
|||||||
weight = forms.IntegerField(
|
weight = forms.IntegerField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
choice_set = DynamicModelChoiceField(
|
||||||
|
queryset=CustomFieldChoiceSet.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
ui_visibility = forms.ChoiceField(
|
ui_visibility = forms.ChoiceField(
|
||||||
label=_("UI visibility"),
|
label=_("UI visibility"),
|
||||||
choices=add_blank_choice(CustomFieldVisibilityChoices),
|
choices=add_blank_choice(CustomFieldVisibilityChoices),
|
||||||
@ -49,7 +54,19 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
|||||||
widget=BulkEditNullBooleanSelect()
|
widget=BulkEditNullBooleanSelect()
|
||||||
)
|
)
|
||||||
|
|
||||||
nullable_fields = ('group_name', 'description',)
|
nullable_fields = ('group_name', 'description', 'choice_set')
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=CustomFieldChoiceSet.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
nullable_fields = ('description',)
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkBulkEditForm(BulkEditForm):
|
class CustomLinkBulkEditForm(BulkEditForm):
|
||||||
|
@ -9,10 +9,13 @@ from extras.models import *
|
|||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
from netbox.forms import NetBoxModelImportForm
|
from netbox.forms import NetBoxModelImportForm
|
||||||
from utilities.forms import CSVModelForm
|
from utilities.forms import CSVModelForm
|
||||||
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVMultipleContentTypeField, SlugField
|
from utilities.forms.fields import (
|
||||||
|
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVMultipleContentTypeField, SlugField,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigTemplateImportForm',
|
'ConfigTemplateImportForm',
|
||||||
|
'CustomFieldChoiceSetImportForm',
|
||||||
'CustomFieldImportForm',
|
'CustomFieldImportForm',
|
||||||
'CustomLinkImportForm',
|
'CustomLinkImportForm',
|
||||||
'ExportTemplateImportForm',
|
'ExportTemplateImportForm',
|
||||||
@ -39,6 +42,11 @@ class CustomFieldImportForm(CSVModelForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_("Object type (for object or multi-object fields)")
|
help_text=_("Object type (for object or multi-object fields)")
|
||||||
)
|
)
|
||||||
|
choice_set = CSVModelChoiceField(
|
||||||
|
queryset=CustomFieldChoiceSet.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text=_('Choice set (for selection fields)')
|
||||||
|
)
|
||||||
choices = SimpleArrayField(
|
choices = SimpleArrayField(
|
||||||
base_field=forms.CharField(),
|
base_field=forms.CharField(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -58,6 +66,20 @@ class CustomFieldImportForm(CSVModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetImportForm(CSVModelForm):
|
||||||
|
choices = SimpleArrayField(
|
||||||
|
base_field=forms.CharField(),
|
||||||
|
required=False,
|
||||||
|
help_text=_('Comma-separated list of field choices')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomFieldChoiceSet
|
||||||
|
fields = (
|
||||||
|
'name', 'description', 'choices',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkImportForm(CSVModelForm):
|
class CustomLinkImportForm(CSVModelForm):
|
||||||
content_types = CSVMultipleContentTypeField(
|
content_types = CSVMultipleContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
|
@ -20,6 +20,7 @@ __all__ = (
|
|||||||
'ConfigContextFilterForm',
|
'ConfigContextFilterForm',
|
||||||
'ConfigRevisionFilterForm',
|
'ConfigRevisionFilterForm',
|
||||||
'ConfigTemplateFilterForm',
|
'ConfigTemplateFilterForm',
|
||||||
|
'CustomFieldChoiceSetFilterForm',
|
||||||
'CustomFieldFilterForm',
|
'CustomFieldFilterForm',
|
||||||
'CustomLinkFilterForm',
|
'CustomLinkFilterForm',
|
||||||
'ExportTemplateFilterForm',
|
'ExportTemplateFilterForm',
|
||||||
@ -37,7 +38,8 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id')),
|
(None, ('q', 'filter_id')),
|
||||||
('Attributes', (
|
('Attributes', (
|
||||||
'type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility', 'is_cloneable',
|
'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visibility',
|
||||||
|
'is_cloneable',
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
content_type_id = ContentTypeMultipleChoiceField(
|
content_type_id = ContentTypeMultipleChoiceField(
|
||||||
@ -62,6 +64,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
choice_set_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=CustomFieldChoiceSet.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Choice set')
|
||||||
|
)
|
||||||
ui_visibility = forms.ChoiceField(
|
ui_visibility = forms.ChoiceField(
|
||||||
choices=add_blank_choice(CustomFieldVisibilityChoices),
|
choices=add_blank_choice(CustomFieldVisibilityChoices),
|
||||||
required=False,
|
required=False,
|
||||||
@ -75,6 +82,15 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
|
fieldsets = (
|
||||||
|
(None, ('q', 'filter_id', 'choice')),
|
||||||
|
)
|
||||||
|
choice = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id')),
|
(None, ('q', 'filter_id')),
|
||||||
|
@ -16,8 +16,8 @@ from netbox.forms import NetBoxModelForm
|
|||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import BootstrapMixin, add_blank_choice
|
from utilities.forms import BootstrapMixin, add_blank_choice
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField,
|
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
|
||||||
SlugField,
|
DynamicModelMultipleChoiceField, JSONField, SlugField,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
@ -27,6 +27,7 @@ __all__ = (
|
|||||||
'ConfigContextForm',
|
'ConfigContextForm',
|
||||||
'ConfigRevisionForm',
|
'ConfigRevisionForm',
|
||||||
'ConfigTemplateForm',
|
'ConfigTemplateForm',
|
||||||
|
'CustomFieldChoiceSetForm',
|
||||||
'CustomFieldForm',
|
'CustomFieldForm',
|
||||||
'CustomLinkForm',
|
'CustomLinkForm',
|
||||||
'ExportTemplateForm',
|
'ExportTemplateForm',
|
||||||
@ -50,13 +51,16 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_("Type of the related object (for object/multi-object fields only)")
|
help_text=_("Type of the related object (for object/multi-object fields only)")
|
||||||
)
|
)
|
||||||
|
choice_set = DynamicModelChoiceField(
|
||||||
|
queryset=CustomFieldChoiceSet.objects.all()
|
||||||
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('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_visibility', 'weight', 'is_cloneable')),
|
||||||
('Values', ('default', 'choices')),
|
('Values', ('default', 'choice_set')),
|
||||||
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,6 +82,13 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
self.fields['type'].disabled = True
|
self.fields['type'].disabled = True
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomFieldChoiceSet
|
||||||
|
fields = ('name', 'description', 'choices')
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
|
@ -25,6 +25,12 @@ class ExtrasQuery(graphene.ObjectType):
|
|||||||
def resolve_custom_field_list(root, info, **kwargs):
|
def resolve_custom_field_list(root, info, **kwargs):
|
||||||
return gql_query_optimizer(models.CustomField.objects.all(), info)
|
return gql_query_optimizer(models.CustomField.objects.all(), info)
|
||||||
|
|
||||||
|
custom_field_choices = ObjectField(CustomFieldChoiceSetType)
|
||||||
|
custom_field_choices_list = ObjectListField(CustomFieldChoiceSetType)
|
||||||
|
|
||||||
|
def resolve_custom_field_choices_list(root, info, **kwargs):
|
||||||
|
return gql_query_optimizer(models.CustomFieldChoiceSet.objects.all(), info)
|
||||||
|
|
||||||
custom_link = ObjectField(CustomLinkType)
|
custom_link = ObjectField(CustomLinkType)
|
||||||
custom_link_list = ObjectListField(CustomLinkType)
|
custom_link_list = ObjectListField(CustomLinkType)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ from netbox.graphql.types import BaseObjectType, ObjectType
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'ConfigContextType',
|
'ConfigContextType',
|
||||||
'ConfigTemplateType',
|
'ConfigTemplateType',
|
||||||
|
'CustomFieldChoiceSetType',
|
||||||
'CustomFieldType',
|
'CustomFieldType',
|
||||||
'CustomLinkType',
|
'CustomLinkType',
|
||||||
'ExportTemplateType',
|
'ExportTemplateType',
|
||||||
@ -41,6 +42,14 @@ class CustomFieldType(ObjectType):
|
|||||||
filterset_class = filtersets.CustomFieldFilterSet
|
filterset_class = filtersets.CustomFieldFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetType(ObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.CustomFieldChoiceSet
|
||||||
|
fields = '__all__'
|
||||||
|
filterset_class = filtersets.CustomFieldChoiceSetFilterSet
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkType(ObjectType):
|
class CustomLinkType(ObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
60
netbox/extras/migrations/0096_customfieldchoiceset.py
Normal file
60
netbox/extras/migrations/0096_customfieldchoiceset.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from extras.choices import CustomFieldTypeChoices
|
||||||
|
|
||||||
|
|
||||||
|
def create_choice_sets(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Create a CustomFieldChoiceSet for each CustomField with choices defined.
|
||||||
|
"""
|
||||||
|
CustomField = apps.get_model('extras', 'CustomField')
|
||||||
|
CustomFieldChoiceSet = apps.get_model('extras', 'CustomFieldChoiceSet')
|
||||||
|
|
||||||
|
# Create custom field choice sets
|
||||||
|
choice_fields = CustomField.objects.filter(
|
||||||
|
type__in=(CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT),
|
||||||
|
choices__len__gt=0
|
||||||
|
)
|
||||||
|
for cf in choice_fields:
|
||||||
|
choiceset = CustomFieldChoiceSet.objects.create(
|
||||||
|
name=f'{cf.name} Choices',
|
||||||
|
choices=cf.choices
|
||||||
|
)
|
||||||
|
cf.choice_set = choiceset
|
||||||
|
|
||||||
|
# Update custom fields to point to new choice sets
|
||||||
|
CustomField.objects.bulk_update(choice_fields, ['choice_set'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0095_bookmarks'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomFieldChoiceSet',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('choices', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), size=None)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('name',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='choice_set',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='custom_fields', to='extras.customfieldchoiceset'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=create_choice_sets,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
@ -1,6 +1,6 @@
|
|||||||
from .change_logging import *
|
from .change_logging import *
|
||||||
from .configs import *
|
from .configs import *
|
||||||
from .customfields import CustomField
|
from .customfields import *
|
||||||
from .dashboard import *
|
from .dashboard import *
|
||||||
from .models import *
|
from .models import *
|
||||||
from .reports import *
|
from .reports import *
|
||||||
|
@ -31,6 +31,7 @@ from utilities.validators import validate_regex
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CustomField',
|
'CustomField',
|
||||||
|
'CustomFieldChoiceSet',
|
||||||
'CustomFieldManager',
|
'CustomFieldManager',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -158,6 +159,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.'
|
'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
choice_set = models.ForeignKey(
|
||||||
|
to='CustomFieldChoiceSet',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='custom_fields',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
choices = ArrayField(
|
choices = ArrayField(
|
||||||
base_field=models.CharField(max_length=100),
|
base_field=models.CharField(max_length=100),
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -278,13 +286,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
'validation_regex': "Regular expression validation is supported only for text and URL fields"
|
'validation_regex': "Regular expression validation is supported only for text and URL fields"
|
||||||
})
|
})
|
||||||
|
|
||||||
# Choices can be set only on selection fields
|
# Choice set must be set on selection fields
|
||||||
if self.choices and self.type not in (
|
if self.type in (
|
||||||
CustomFieldTypeChoices.TYPE_SELECT,
|
CustomFieldTypeChoices.TYPE_SELECT,
|
||||||
CustomFieldTypeChoices.TYPE_MULTISELECT
|
CustomFieldTypeChoices.TYPE_MULTISELECT
|
||||||
):
|
) and not self.choice_set:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'choices': "Choices may be set only for custom selection fields."
|
'choice_set': "Selection fields must define a set of choices."
|
||||||
|
})
|
||||||
|
elif self.choice_set:
|
||||||
|
raise ValidationError({
|
||||||
|
'choice_set': "Choices may be set only for selection fields."
|
||||||
})
|
})
|
||||||
|
|
||||||
# Selection fields must have at least one choice defined
|
# Selection fields must have at least one choice defined
|
||||||
@ -627,3 +639,30 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
elif self.required:
|
elif self.required:
|
||||||
raise ValidationError("Required field cannot be empty.")
|
raise ValidationError("Required field cannot be empty.")
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSet(ChangeLoggedModel):
|
||||||
|
"""
|
||||||
|
Represents a set of choices available for choice and multi-choice custom fields.
|
||||||
|
"""
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
choices = ArrayField(
|
||||||
|
base_field=models.CharField(max_length=100),
|
||||||
|
help_text=_('Comma-separated list of available choices (for selection fields)')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name',)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:customfieldchoiceset', args=[self.pk])
|
||||||
|
@ -12,6 +12,7 @@ __all__ = (
|
|||||||
'ConfigContextTable',
|
'ConfigContextTable',
|
||||||
'ConfigRevisionTable',
|
'ConfigRevisionTable',
|
||||||
'ConfigTemplateTable',
|
'ConfigTemplateTable',
|
||||||
|
'CustomFieldChoiceSetTable',
|
||||||
'CustomFieldTable',
|
'CustomFieldTable',
|
||||||
'CustomLinkTable',
|
'CustomLinkTable',
|
||||||
'ExportTemplateTable',
|
'ExportTemplateTable',
|
||||||
@ -76,6 +77,19 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetTable(NetBoxTable):
|
||||||
|
name = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = CustomFieldChoiceSet
|
||||||
|
fields = (
|
||||||
|
'pk', 'id', 'name', 'description', 'choices', 'created', 'last_updated',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'name', 'description', 'choices')
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkTable(NetBoxTable):
|
class CustomLinkTable(NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
|
@ -15,6 +15,14 @@ urlpatterns = [
|
|||||||
path('custom-fields/delete/', views.CustomFieldBulkDeleteView.as_view(), name='customfield_bulk_delete'),
|
path('custom-fields/delete/', views.CustomFieldBulkDeleteView.as_view(), name='customfield_bulk_delete'),
|
||||||
path('custom-fields/<int:pk>/', include(get_model_urls('extras', 'customfield'))),
|
path('custom-fields/<int:pk>/', include(get_model_urls('extras', 'customfield'))),
|
||||||
|
|
||||||
|
# Custom field choices
|
||||||
|
path('custom-field-choices/', views.CustomFieldChoiceSetListView.as_view(), name='customfieldchoiceset_list'),
|
||||||
|
path('custom-field-choices/add/', views.CustomFieldChoiceSetEditView.as_view(), name='customfieldchoiceset_add'),
|
||||||
|
path('custom-field-choices/import/', views.CustomFieldChoiceSetBulkImportView.as_view(), name='customfieldchoiceset_import'),
|
||||||
|
path('custom-field-choices/edit/', views.CustomFieldChoiceSetBulkEditView.as_view(), name='customfieldchoiceset_bulk_edit'),
|
||||||
|
path('custom-field-choices/delete/', views.CustomFieldChoiceSetBulkDeleteView.as_view(), name='customfieldchoiceset_bulk_delete'),
|
||||||
|
path('custom-field-choices/<int:pk>/', include(get_model_urls('extras', 'customfieldchoiceset'))),
|
||||||
|
|
||||||
# Custom links
|
# Custom links
|
||||||
path('custom-links/', views.CustomLinkListView.as_view(), name='customlink_list'),
|
path('custom-links/', views.CustomLinkListView.as_view(), name='customlink_list'),
|
||||||
path('custom-links/add/', views.CustomLinkEditView.as_view(), name='customlink_add'),
|
path('custom-links/add/', views.CustomLinkEditView.as_view(), name='customlink_add'),
|
||||||
|
@ -34,7 +34,7 @@ from .scripts import run_script
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CustomFieldListView(generic.ObjectListView):
|
class CustomFieldListView(generic.ObjectListView):
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.select_related('choice_set')
|
||||||
filterset = filtersets.CustomFieldFilterSet
|
filterset = filtersets.CustomFieldFilterSet
|
||||||
filterset_form = forms.CustomFieldFilterForm
|
filterset_form = forms.CustomFieldFilterForm
|
||||||
table = tables.CustomFieldTable
|
table = tables.CustomFieldTable
|
||||||
@ -42,38 +42,83 @@ class CustomFieldListView(generic.ObjectListView):
|
|||||||
|
|
||||||
@register_model_view(CustomField)
|
@register_model_view(CustomField)
|
||||||
class CustomFieldView(generic.ObjectView):
|
class CustomFieldView(generic.ObjectView):
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.select_related('choice_set')
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CustomField, 'edit')
|
@register_model_view(CustomField, 'edit')
|
||||||
class CustomFieldEditView(generic.ObjectEditView):
|
class CustomFieldEditView(generic.ObjectEditView):
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.select_related('choice_set')
|
||||||
form = forms.CustomFieldForm
|
form = forms.CustomFieldForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CustomField, 'delete')
|
@register_model_view(CustomField, 'delete')
|
||||||
class CustomFieldDeleteView(generic.ObjectDeleteView):
|
class CustomFieldDeleteView(generic.ObjectDeleteView):
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.select_related('choice_set')
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldBulkImportView(generic.BulkImportView):
|
class CustomFieldBulkImportView(generic.BulkImportView):
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.select_related('choice_set')
|
||||||
model_form = forms.CustomFieldImportForm
|
model_form = forms.CustomFieldImportForm
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldBulkEditView(generic.BulkEditView):
|
class CustomFieldBulkEditView(generic.BulkEditView):
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.select_related('choice_set')
|
||||||
filterset = filtersets.CustomFieldFilterSet
|
filterset = filtersets.CustomFieldFilterSet
|
||||||
table = tables.CustomFieldTable
|
table = tables.CustomFieldTable
|
||||||
form = forms.CustomFieldBulkEditForm
|
form = forms.CustomFieldBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldBulkDeleteView(generic.BulkDeleteView):
|
class CustomFieldBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.select_related('choice_set')
|
||||||
filterset = filtersets.CustomFieldFilterSet
|
filterset = filtersets.CustomFieldFilterSet
|
||||||
table = tables.CustomFieldTable
|
table = tables.CustomFieldTable
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom field choices
|
||||||
|
#
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetListView(generic.ObjectListView):
|
||||||
|
queryset = CustomFieldChoiceSet.objects.all()
|
||||||
|
filterset = filtersets.CustomFieldChoiceSetFilterSet
|
||||||
|
filterset_form = forms.CustomFieldChoiceSetFilterForm
|
||||||
|
table = tables.CustomFieldChoiceSetTable
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(CustomFieldChoiceSet)
|
||||||
|
class CustomFieldChoiceSetView(generic.ObjectView):
|
||||||
|
queryset = CustomFieldChoiceSet.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(CustomFieldChoiceSet, 'edit')
|
||||||
|
class CustomFieldChoiceSetEditView(generic.ObjectEditView):
|
||||||
|
queryset = CustomFieldChoiceSet.objects.all()
|
||||||
|
form = forms.CustomFieldChoiceSetForm
|
||||||
|
|
||||||
|
|
||||||
|
@register_model_view(CustomFieldChoiceSet, 'delete')
|
||||||
|
class CustomFieldChoiceSetDeleteView(generic.ObjectDeleteView):
|
||||||
|
queryset = CustomFieldChoiceSet.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetBulkImportView(generic.BulkImportView):
|
||||||
|
queryset = CustomFieldChoiceSet.objects.all()
|
||||||
|
model_form = forms.CustomFieldChoiceSetImportForm
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetBulkEditView(generic.BulkEditView):
|
||||||
|
queryset = CustomFieldChoiceSet.objects.all()
|
||||||
|
filterset = filtersets.CustomFieldChoiceSetFilterSet
|
||||||
|
table = tables.CustomFieldChoiceSetTable
|
||||||
|
form = forms.CustomFieldChoiceSetBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetBulkDeleteView(generic.BulkDeleteView):
|
||||||
|
queryset = CustomFieldChoiceSet.objects.all()
|
||||||
|
filterset = filtersets.CustomFieldChoiceSetFilterSet
|
||||||
|
table = tables.CustomFieldChoiceSetTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Custom links
|
# Custom links
|
||||||
#
|
#
|
||||||
|
@ -288,6 +288,7 @@ CUSTOMIZATION_MENU = Menu(
|
|||||||
label=_('Customization'),
|
label=_('Customization'),
|
||||||
items=(
|
items=(
|
||||||
get_model_item('extras', 'customfield', _('Custom Fields')),
|
get_model_item('extras', 'customfield', _('Custom Fields')),
|
||||||
|
get_model_item('extras', 'customfieldchoiceset', _('Custom Field Choices')),
|
||||||
get_model_item('extras', 'customlink', _('Custom Links')),
|
get_model_item('extras', 'customlink', _('Custom Links')),
|
||||||
get_model_item('extras', 'exporttemplate', _('Export Templates')),
|
get_model_item('extras', 'exporttemplate', _('Export Templates')),
|
||||||
get_model_item('extras', 'savedfilter', _('Saved Filters')),
|
get_model_item('extras', 'savedfilter', _('Saved Filters')),
|
||||||
|
@ -15,14 +15,6 @@
|
|||||||
<th scope="row">Name</th>
|
<th scope="row">Name</th>
|
||||||
<td>{{ object.name }}</td>
|
<td>{{ object.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">Label</th>
|
|
||||||
<td>{{ object.label|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Group Name</th>
|
|
||||||
<td>{{ object.group_name|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Type</th>
|
<th scope="row">Type</th>
|
||||||
<td>
|
<td>
|
||||||
@ -30,6 +22,14 @@
|
|||||||
{% if object.object_type %}({{ object.object_type.model|bettertitle }}){% endif %}
|
{% if object.object_type %}({{ object.object_type.model|bettertitle }}){% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Label</th>
|
||||||
|
<td>{{ object.label|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Group</th>
|
||||||
|
<td>{{ object.group_name|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Description</th>
|
<th scope="row">Description</th>
|
||||||
<td>{{ object.description|markdown|placeholder }}</td>
|
<td>{{ object.description|markdown|placeholder }}</td>
|
||||||
@ -38,6 +38,27 @@
|
|||||||
<th scope="row">Required</th>
|
<th scope="row">Required</th>
|
||||||
<td>{% checkmark object.required %}</td>
|
<td>{% checkmark object.required %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Cloneable</th>
|
||||||
|
<td>{% checkmark object.is_cloneable %}</td>
|
||||||
|
</tr>
|
||||||
|
{% if object.choice_set %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Choice Set</th>
|
||||||
|
<td>{{ object.choice_set|linkify }} ({{ object.choice_set.choices|length }} choices)</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Default Value</th>
|
||||||
|
<td>{{ object.default }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Behavior</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Search Weight</th>
|
<th scope="row">Search Weight</th>
|
||||||
<td>
|
<td>
|
||||||
@ -60,33 +81,6 @@
|
|||||||
<th scope="row">UI Visibility</th>
|
<th scope="row">UI Visibility</th>
|
||||||
<td>{{ object.get_ui_visibility_display }}</td>
|
<td>{{ object.get_ui_visibility_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">Cloneable</th>
|
|
||||||
<td>{% checkmark object.is_cloneable %}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h5 class="card-header">
|
|
||||||
Values
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
<table class="table table-hover attr-table">
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Default Value</th>
|
|
||||||
<td>{{ object.default }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Choices</th>
|
|
||||||
<td>
|
|
||||||
{% if object.choices %}
|
|
||||||
{{ object.choices|join:", " }}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -94,9 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Object Types</h5>
|
||||||
Assigned Models
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
{% for ct in object.content_types.all %}
|
{% for ct in object.content_types.all %}
|
||||||
@ -108,9 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Validation Rules</h5>
|
||||||
Validation Rules
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
@ -138,8 +128,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% plugin_full_width_page object %}
|
{% plugin_full_width_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
50
netbox/templates/extras/customfieldchoiceset.html
Normal file
50
netbox/templates/extras/customfieldchoiceset.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{% extends 'generic/object.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load plugins %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Custom Field Choice Set</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Name</th>
|
||||||
|
<td>{{ object.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ object.description|markdown|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Used by</th>
|
||||||
|
<td>{# TODO #}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_left_page object %}
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Choices</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
{% for choice in object.choices %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ choice }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user