mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 00:36:11 -06:00
Initial work on custom field choice sets
This commit is contained in:
parent
837be4d45f
commit
70e1e1197e
@ -7,6 +7,7 @@ __all__ = [
|
||||
'NestedBookmarkSerializer',
|
||||
'NestedConfigContextSerializer',
|
||||
'NestedConfigTemplateSerializer',
|
||||
'NestedCustomFieldChoiceSetSerializer',
|
||||
'NestedCustomFieldSerializer',
|
||||
'NestedCustomLinkSerializer',
|
||||
'NestedExportTemplateSerializer',
|
||||
@ -34,6 +35,14 @@ class NestedCustomFieldSerializer(WritableNestedSerializer):
|
||||
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):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||
|
||||
|
@ -35,6 +35,7 @@ __all__ = (
|
||||
'ConfigContextSerializer',
|
||||
'ConfigTemplateSerializer',
|
||||
'ContentTypeSerializer',
|
||||
'CustomFieldChoiceSetSerializer',
|
||||
'CustomFieldSerializer',
|
||||
'CustomLinkSerializer',
|
||||
'DashboardSerializer',
|
||||
@ -94,6 +95,7 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
)
|
||||
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
|
||||
data_type = serializers.SerializerMethodField()
|
||||
choice_set = NestedCustomFieldChoiceSetSerializer(required=False)
|
||||
ui_visibility = ChoiceField(choices=CustomFieldVisibilityChoices, required=False)
|
||||
|
||||
class Meta:
|
||||
@ -101,8 +103,8 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
fields = [
|
||||
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
|
||||
'description', 'required', 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'default',
|
||||
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created',
|
||||
'last_updated',
|
||||
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'choices',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
def validate_type(self, value):
|
||||
@ -127,6 +129,16 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
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
|
||||
#
|
||||
|
@ -9,6 +9,7 @@ router.APIRootView = views.ExtrasRootView
|
||||
|
||||
router.register('webhooks', views.WebhookViewSet)
|
||||
router.register('custom-fields', views.CustomFieldViewSet)
|
||||
router.register('custom-field-choices', views.CustomFieldChoiceSetViewSet)
|
||||
router.register('custom-links', views.CustomLinkViewSet)
|
||||
router.register('export-templates', views.ExportTemplateViewSet)
|
||||
router.register('saved-filters', views.SavedFilterViewSet)
|
||||
|
@ -1,6 +1,5 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_rq.queues import get_connection
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
@ -55,11 +54,17 @@ class WebhookViewSet(NetBoxModelViewSet):
|
||||
|
||||
class CustomFieldViewSet(NetBoxModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = CustomField.objects.all()
|
||||
queryset = CustomField.objects.select_related('choice_set')
|
||||
serializer_class = serializers.CustomFieldSerializer
|
||||
filterset_class = filtersets.CustomFieldFilterSet
|
||||
|
||||
|
||||
class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
|
||||
queryset = CustomFieldChoiceSet.objects.all()
|
||||
serializer_class = serializers.CustomFieldChoiceSetSerializer
|
||||
filterset_class = filtersets.CustomFieldChoiceSetFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Custom links
|
||||
#
|
||||
|
@ -20,6 +20,7 @@ __all__ = (
|
||||
'ConfigRevisionFilterSet',
|
||||
'ConfigTemplateFilterSet',
|
||||
'ContentTypeFilterSet',
|
||||
'CustomFieldChoiceSetFilterSet',
|
||||
'CustomFieldFilterSet',
|
||||
'CustomLinkFilterSet',
|
||||
'ExportTemplateFilterSet',
|
||||
@ -74,6 +75,9 @@ class CustomFieldFilterSet(BaseFilterSet):
|
||||
field_name='content_types__id'
|
||||
)
|
||||
content_types = ContentTypeFilter()
|
||||
choice_set_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CustomFieldChoiceSet.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
@ -4,13 +4,14 @@ from django.utils.translation import gettext as _
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
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
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextBulkEditForm',
|
||||
'ConfigTemplateBulkEditForm',
|
||||
'CustomFieldBulkEditForm',
|
||||
'CustomFieldChoiceSetBulkEditForm',
|
||||
'CustomLinkBulkEditForm',
|
||||
'ExportTemplateBulkEditForm',
|
||||
'JournalEntryBulkEditForm',
|
||||
@ -38,6 +39,10 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
||||
weight = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
choice_set = DynamicModelChoiceField(
|
||||
queryset=CustomFieldChoiceSet.objects.all(),
|
||||
required=False
|
||||
)
|
||||
ui_visibility = forms.ChoiceField(
|
||||
label=_("UI visibility"),
|
||||
choices=add_blank_choice(CustomFieldVisibilityChoices),
|
||||
@ -49,7 +54,19 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
||||
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):
|
||||
|
@ -9,10 +9,13 @@ from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
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__ = (
|
||||
'ConfigTemplateImportForm',
|
||||
'CustomFieldChoiceSetImportForm',
|
||||
'CustomFieldImportForm',
|
||||
'CustomLinkImportForm',
|
||||
'ExportTemplateImportForm',
|
||||
@ -39,6 +42,11 @@ class CustomFieldImportForm(CSVModelForm):
|
||||
required=False,
|
||||
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(
|
||||
base_field=forms.CharField(),
|
||||
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):
|
||||
content_types = CSVMultipleContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
|
@ -20,6 +20,7 @@ __all__ = (
|
||||
'ConfigContextFilterForm',
|
||||
'ConfigRevisionFilterForm',
|
||||
'ConfigTemplateFilterForm',
|
||||
'CustomFieldChoiceSetFilterForm',
|
||||
'CustomFieldFilterForm',
|
||||
'CustomLinkFilterForm',
|
||||
'ExportTemplateFilterForm',
|
||||
@ -37,7 +38,8 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
('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(
|
||||
@ -62,6 +64,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
choice_set_id = DynamicModelMultipleChoiceField(
|
||||
queryset=CustomFieldChoiceSet.objects.all(),
|
||||
required=False,
|
||||
label=_('Choice set')
|
||||
)
|
||||
ui_visibility = forms.ChoiceField(
|
||||
choices=add_blank_choice(CustomFieldVisibilityChoices),
|
||||
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):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
|
@ -16,8 +16,8 @@ from netbox.forms import NetBoxModelForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import BootstrapMixin, add_blank_choice
|
||||
from utilities.forms.fields import (
|
||||
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField,
|
||||
SlugField,
|
||||
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, JSONField, SlugField,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
@ -27,6 +27,7 @@ __all__ = (
|
||||
'ConfigContextForm',
|
||||
'ConfigRevisionForm',
|
||||
'ConfigTemplateForm',
|
||||
'CustomFieldChoiceSetForm',
|
||||
'CustomFieldForm',
|
||||
'CustomLinkForm',
|
||||
'ExportTemplateForm',
|
||||
@ -50,13 +51,16 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||
required=False,
|
||||
help_text=_("Type of the related object (for object/multi-object fields only)")
|
||||
)
|
||||
choice_set = DynamicModelChoiceField(
|
||||
queryset=CustomFieldChoiceSet.objects.all()
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Custom Field', (
|
||||
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description',
|
||||
)),
|
||||
('Behavior', ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')),
|
||||
('Values', ('default', 'choices')),
|
||||
('Values', ('default', 'choice_set')),
|
||||
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
||||
)
|
||||
|
||||
@ -78,6 +82,13 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||
self.fields['type'].disabled = True
|
||||
|
||||
|
||||
class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
fields = ('name', 'description', 'choices')
|
||||
|
||||
|
||||
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
|
@ -25,6 +25,12 @@ class ExtrasQuery(graphene.ObjectType):
|
||||
def resolve_custom_field_list(root, info, **kwargs):
|
||||
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_list = ObjectListField(CustomLinkType)
|
||||
|
||||
|
@ -5,6 +5,7 @@ from netbox.graphql.types import BaseObjectType, ObjectType
|
||||
__all__ = (
|
||||
'ConfigContextType',
|
||||
'ConfigTemplateType',
|
||||
'CustomFieldChoiceSetType',
|
||||
'CustomFieldType',
|
||||
'CustomLinkType',
|
||||
'ExportTemplateType',
|
||||
@ -41,6 +42,14 @@ class CustomFieldType(ObjectType):
|
||||
filterset_class = filtersets.CustomFieldFilterSet
|
||||
|
||||
|
||||
class CustomFieldChoiceSetType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CustomFieldChoiceSet
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CustomFieldChoiceSetFilterSet
|
||||
|
||||
|
||||
class CustomLinkType(ObjectType):
|
||||
|
||||
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 .configs import *
|
||||
from .customfields import CustomField
|
||||
from .customfields import *
|
||||
from .dashboard import *
|
||||
from .models import *
|
||||
from .reports import *
|
||||
|
@ -31,6 +31,7 @@ from utilities.validators import validate_regex
|
||||
|
||||
__all__ = (
|
||||
'CustomField',
|
||||
'CustomFieldChoiceSet',
|
||||
'CustomFieldManager',
|
||||
)
|
||||
|
||||
@ -158,6 +159,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
'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(
|
||||
base_field=models.CharField(max_length=100),
|
||||
blank=True,
|
||||
@ -278,13 +286,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
'validation_regex': "Regular expression validation is supported only for text and URL fields"
|
||||
})
|
||||
|
||||
# Choices can be set only on selection fields
|
||||
if self.choices and self.type not in (
|
||||
# Choice set must be set on selection fields
|
||||
if self.type in (
|
||||
CustomFieldTypeChoices.TYPE_SELECT,
|
||||
CustomFieldTypeChoices.TYPE_MULTISELECT
|
||||
):
|
||||
) and not self.choice_set:
|
||||
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
|
||||
@ -627,3 +639,30 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
||||
elif self.required:
|
||||
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',
|
||||
'ConfigRevisionTable',
|
||||
'ConfigTemplateTable',
|
||||
'CustomFieldChoiceSetTable',
|
||||
'CustomFieldTable',
|
||||
'CustomLinkTable',
|
||||
'ExportTemplateTable',
|
||||
@ -76,6 +77,19 @@ class CustomFieldTable(NetBoxTable):
|
||||
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):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
|
@ -15,6 +15,14 @@ urlpatterns = [
|
||||
path('custom-fields/delete/', views.CustomFieldBulkDeleteView.as_view(), name='customfield_bulk_delete'),
|
||||
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
|
||||
path('custom-links/', views.CustomLinkListView.as_view(), name='customlink_list'),
|
||||
path('custom-links/add/', views.CustomLinkEditView.as_view(), name='customlink_add'),
|
||||
|
@ -34,7 +34,7 @@ from .scripts import run_script
|
||||
#
|
||||
|
||||
class CustomFieldListView(generic.ObjectListView):
|
||||
queryset = CustomField.objects.all()
|
||||
queryset = CustomField.objects.select_related('choice_set')
|
||||
filterset = filtersets.CustomFieldFilterSet
|
||||
filterset_form = forms.CustomFieldFilterForm
|
||||
table = tables.CustomFieldTable
|
||||
@ -42,38 +42,83 @@ class CustomFieldListView(generic.ObjectListView):
|
||||
|
||||
@register_model_view(CustomField)
|
||||
class CustomFieldView(generic.ObjectView):
|
||||
queryset = CustomField.objects.all()
|
||||
queryset = CustomField.objects.select_related('choice_set')
|
||||
|
||||
|
||||
@register_model_view(CustomField, 'edit')
|
||||
class CustomFieldEditView(generic.ObjectEditView):
|
||||
queryset = CustomField.objects.all()
|
||||
queryset = CustomField.objects.select_related('choice_set')
|
||||
form = forms.CustomFieldForm
|
||||
|
||||
|
||||
@register_model_view(CustomField, 'delete')
|
||||
class CustomFieldDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CustomField.objects.all()
|
||||
queryset = CustomField.objects.select_related('choice_set')
|
||||
|
||||
|
||||
class CustomFieldBulkImportView(generic.BulkImportView):
|
||||
queryset = CustomField.objects.all()
|
||||
queryset = CustomField.objects.select_related('choice_set')
|
||||
model_form = forms.CustomFieldImportForm
|
||||
|
||||
|
||||
class CustomFieldBulkEditView(generic.BulkEditView):
|
||||
queryset = CustomField.objects.all()
|
||||
queryset = CustomField.objects.select_related('choice_set')
|
||||
filterset = filtersets.CustomFieldFilterSet
|
||||
table = tables.CustomFieldTable
|
||||
form = forms.CustomFieldBulkEditForm
|
||||
|
||||
|
||||
class CustomFieldBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CustomField.objects.all()
|
||||
queryset = CustomField.objects.select_related('choice_set')
|
||||
filterset = filtersets.CustomFieldFilterSet
|
||||
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
|
||||
#
|
||||
|
@ -288,6 +288,7 @@ CUSTOMIZATION_MENU = Menu(
|
||||
label=_('Customization'),
|
||||
items=(
|
||||
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', 'exporttemplate', _('Export Templates')),
|
||||
get_model_item('extras', 'savedfilter', _('Saved Filters')),
|
||||
|
@ -15,14 +15,6 @@
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</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>
|
||||
<th scope="row">Type</th>
|
||||
<td>
|
||||
@ -30,6 +22,14 @@
|
||||
{% if object.object_type %}({{ object.object_type.model|bettertitle }}){% endif %}
|
||||
</td>
|
||||
</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>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|markdown|placeholder }}</td>
|
||||
@ -38,6 +38,27 @@
|
||||
<th scope="row">Required</th>
|
||||
<td>{% checkmark object.required %}</td>
|
||||
</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>
|
||||
<th scope="row">Search Weight</th>
|
||||
<td>
|
||||
@ -60,33 +81,6 @@
|
||||
<th scope="row">UI Visibility</th>
|
||||
<td>{{ object.get_ui_visibility_display }}</td>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,9 +88,7 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Assigned Models
|
||||
</h5>
|
||||
<h5 class="card-header">Object Types</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
{% for ct in object.content_types.all %}
|
||||
@ -108,9 +100,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Validation Rules
|
||||
</h5>
|
||||
<h5 class="card-header">Validation Rules</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
@ -138,8 +128,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% 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