Initial work on custom field choice sets

This commit is contained in:
Jeremy Stretch 2023-07-17 11:05:10 -04:00
parent 837be4d45f
commit 70e1e1197e
20 changed files with 414 additions and 67 deletions

View File

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

View File

@ -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
#

View File

@ -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)

View File

@ -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
#

View File

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

View File

@ -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):

View File

@ -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(),

View File

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

View File

@ -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(),

View File

@ -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)

View File

@ -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:

View 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
),
]

View File

@ -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 *

View File

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

View File

@ -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

View File

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

View File

@ -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
#

View File

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

View File

@ -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 %}

View 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 %}