#13241: Add support for custom field choice labels

This commit is contained in:
Jeremy Stretch 2023-07-26 16:00:03 -04:00
parent 5b570d6922
commit c60aec2725
15 changed files with 102 additions and 101 deletions

View File

@ -131,6 +131,10 @@ class CustomFieldSerializer(ValidatedModelSerializer):
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail') url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
base_choices = ChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False
)
class Meta: class Meta:
model = CustomFieldChoiceSet model = CustomFieldChoiceSet

View File

@ -82,8 +82,8 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
if page is not None: if page is not None:
data = [ data = [
{ {
'id': c[0], 'value': c[0],
'display': c[1], 'label': c[1],
} for c in page } for c in page
] ]
return self.get_paginated_response(data) return self.get_paginated_response(data)

View File

@ -128,7 +128,7 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
def filter_by_choice(self, queryset, name, value): def filter_by_choice(self, queryset, name, value):
# TODO: Support case-insensitive matching # TODO: Support case-insensitive matching
return queryset.filter(extra_choices__overlap=value) return queryset.filter(extra_choices__has_any_keys=value)
class CustomLinkFilterSet(BaseFilterSet): class CustomLinkFilterSet(BaseFilterSet):

View File

@ -68,11 +68,6 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
required=False, required=False,
help_text=_('The classification of entry') help_text=_('The classification of entry')
) )
extra_choices = SimpleArrayField(
base_field=forms.CharField(),
required=False,
help_text=_('Comma-separated list of field choices')
)
class Meta: class Meta:
model = CustomFieldChoiceSet model = CustomFieldChoiceSet

View File

@ -19,7 +19,7 @@ from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField, DynamicModelMultipleChoiceField, JSONField, SlugField,
) )
from utilities.forms.widgets import ArrayWidget from utilities.forms.widgets import ChoicesWidget
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
@ -85,19 +85,14 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm): class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
extra_choices = forms.CharField( extra_choices = forms.JSONField(
widget=ArrayWidget(), required=False
required=False,
help_text=_('Enter one choice per line.')
) )
class Meta: class Meta:
model = CustomFieldChoiceSet model = CustomFieldChoiceSet
fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically') fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically')
def clean_extra_choices(self):
return self.cleaned_data['extra_choices'].splitlines()
class CustomLinkForm(BootstrapMixin, forms.ModelForm): class CustomLinkForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(

View File

@ -19,7 +19,7 @@ def create_choice_sets(apps, schema_editor):
for cf in choice_fields: for cf in choice_fields:
choiceset = CustomFieldChoiceSet.objects.create( choiceset = CustomFieldChoiceSet.objects.create(
name=f'{cf.name} Choices', name=f'{cf.name} Choices',
extra_choices=cf.choices extra_choices=dict(zip(cf.choices, cf.choices)) # Convert list to key:val dict
) )
cf.choice_set = choiceset cf.choice_set = choiceset
@ -43,7 +43,7 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=100, unique=True)), ('name', models.CharField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)), ('description', models.CharField(blank=True, max_length=200)),
('base_choices', models.CharField(blank=True, max_length=50)), ('base_choices', models.CharField(blank=True, max_length=50)),
('extra_choices', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, null=True, size=None)), ('extra_choices', models.JSONField(blank=True, default=dict, null=True)),
('order_alphabetically', models.BooleanField(default=False)), ('order_alphabetically', models.BooleanField(default=False)),
], ],
options={ options={

View File

@ -6,7 +6,6 @@ import django_filters
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField
from django.core.validators import RegexValidator, ValidationError from django.core.validators import RegexValidator, ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
@ -658,9 +657,8 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
blank=True, blank=True,
help_text=_('Base set of predefined choices (optional)') help_text=_('Base set of predefined choices (optional)')
) )
extra_choices = ArrayField( extra_choices = models.JSONField(
base_field=models.CharField(max_length=100), default=dict,
help_text=_('List of field choices'),
blank=True, blank=True,
null=True null=True
) )
@ -686,25 +684,27 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
Returns a concatenation of the base and extra choices. Returns a concatenation of the base and extra choices.
""" """
if not hasattr(self, '_choices'): if not hasattr(self, '_choices'):
self._choices = [] self._choices = {}
if self.base_choices: if self.base_choices:
self._choices.extend(CHOICE_SETS.get(self.base_choices)) self._choices.update(dict(CHOICE_SETS.get(self.base_choices)))
if self.extra_choices: if self.extra_choices:
self._choices.extend([(k, k) for k in self.extra_choices]) self._choices.update(self.extra_choices)
return self._choices if self.order_alphabetically:
self._choices = dict(sorted(self._choices.items()))
def clean(self): return list(self._choices.items())
if not self.base_choices and not self.extra_choices:
raise ValidationError(_("Must define base or extra choices."))
@property @property
def choices_count(self): def choices_count(self):
return len(self.choices) return len(self.choices)
def clean(self):
if not self.base_choices and not self.extra_choices:
raise ValidationError(_("Must define base or extra choices."))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Sort choices if alphabetical ordering is enforced # Sort choices if alphabetical ordering is enforced
if self.order_alphabetically: if self.order_alphabetically:
self.extra_choices = sorted(self.extra_choices) self.extra_choices = dict(sorted(self.extra_choices.items()))
return super().save(*args, **kwargs) return super().save(*args, **kwargs)

View File

@ -68,11 +68,9 @@ class CustomFieldTable(NetBoxTable):
verbose_name="UI visibility" verbose_name="UI visibility"
) )
description = columns.MarkdownColumn() description = columns.MarkdownColumn()
choices = columns.ArrayColumn( choices = columns.ChoiceSetColumn(
max_items=10, max_items=10,
func=lambda x: x[1], orderable=False
orderable=False,
verbose_name=_('Choices')
) )
is_cloneable = columns.BooleanColumn() is_cloneable = columns.BooleanColumn()
@ -91,16 +89,13 @@ class CustomFieldChoiceSetTable(NetBoxTable):
linkify=True linkify=True
) )
base_choices = columns.ChoiceFieldColumn() base_choices = columns.ChoiceFieldColumn()
extra_choices = columns.ArrayColumn( extra_choices = tables.TemplateColumn(
template_code="""{% for k, v in value.items %}{{ v }}{% if not forloop.last %}, {% endif %}{% endfor %}"""
)
choices = columns.ChoiceSetColumn(
max_items=10, max_items=10,
orderable=False orderable=False
) )
choices = columns.ArrayColumn(
max_items=10,
func=lambda x: x[1],
orderable=False,
verbose_name=_('Choices')
)
choice_count = tables.TemplateColumn( choice_count = tables.TemplateColumn(
accessor=tables.A('extra_choices'), accessor=tables.A('extra_choices'),
template_code='{{ value|length }}', template_code='{{ value|length }}',

View File

@ -17,8 +17,8 @@ class ChangeLogViewTest(ModelViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1', name='Choice Set 1',
extra_choices=['Bar', 'Foo'] extra_choices={'bar': 'Bar', 'foo': 'Foo'}
) )
# Create a custom field on the Site model # Create a custom field on the Site model
@ -48,7 +48,7 @@ class ChangeLogViewTest(ModelViewTestCase):
'slug': 'site-1', 'slug': 'site-1',
'status': SiteStatusChoices.STATUS_ACTIVE, 'status': SiteStatusChoices.STATUS_ACTIVE,
'cf_cf1': 'ABC', 'cf_cf1': 'ABC',
'cf_cf2': 'Bar', 'cf_cf2': 'bar',
'tags': [tag.pk for tag in tags], 'tags': [tag.pk for tag in tags],
} }
@ -84,7 +84,7 @@ class ChangeLogViewTest(ModelViewTestCase):
'slug': 'site-x', 'slug': 'site-x',
'status': SiteStatusChoices.STATUS_PLANNED, 'status': SiteStatusChoices.STATUS_PLANNED,
'cf_cf1': 'DEF', 'cf_cf1': 'DEF',
'cf_cf2': 'Foo', 'cf_cf2': 'foo',
'tags': [tags[2].pk], 'tags': [tags[2].pk],
} }
@ -226,7 +226,7 @@ class ChangeLogAPITest(APITestCase):
# Create a select custom field on the Site model # Create a select custom field on the Site model
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Choice Set 1', name='Choice Set 1',
extra_choices=['Bar', 'Foo'] extra_choices={'bar': 'Bar', 'foo': 'Foo'}
) )
cf_select = CustomField( cf_select = CustomField(
type=CustomFieldTypeChoices.TYPE_SELECT, type=CustomFieldTypeChoices.TYPE_SELECT,
@ -251,7 +251,7 @@ class ChangeLogAPITest(APITestCase):
'slug': 'site-1', 'slug': 'site-1',
'custom_fields': { 'custom_fields': {
'cf1': 'ABC', 'cf1': 'ABC',
'cf2': 'Bar', 'cf2': 'bar',
}, },
'tags': [ 'tags': [
{'name': 'Tag 1'}, {'name': 'Tag 1'},
@ -285,7 +285,7 @@ class ChangeLogAPITest(APITestCase):
'slug': 'site-x', 'slug': 'site-x',
'custom_fields': { 'custom_fields': {
'cf1': 'DEF', 'cf1': 'DEF',
'cf2': 'Foo', 'cf2': 'foo',
}, },
'tags': [ 'tags': [
{'name': 'Tag 3'} {'name': 'Tag 3'}

View File

@ -269,8 +269,12 @@ class CustomFieldTest(TestCase):
self.assertIsNone(instance.custom_field_data.get(cf.name)) self.assertIsNone(instance.custom_field_data.get(cf.name))
def test_select_field(self): def test_select_field(self):
CHOICES = ('Option A', 'Option B', 'Option C') CHOICES = {
value = CHOICES[1] 'a': 'Option A',
'b': 'Option B',
'c': 'Option C',
}
value = 'a'
# Create a set of custom field choices # Create a set of custom field choices
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
@ -302,8 +306,12 @@ class CustomFieldTest(TestCase):
self.assertIsNone(instance.custom_field_data.get(cf.name)) self.assertIsNone(instance.custom_field_data.get(cf.name))
def test_multiselect_field(self): def test_multiselect_field(self):
CHOICES = ['Option A', 'Option B', 'Option C'] CHOICES = {
value = [CHOICES[1], CHOICES[2]] 'a': 'Option A',
'b': 'Option B',
'c': 'Option C',
}
value = ['a', 'b']
# Create a set of custom field choices # Create a set of custom field choices
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
@ -453,7 +461,7 @@ class CustomFieldAPITest(APITestCase):
# Create a set of custom field choices # Create a set of custom field choices
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1', name='Custom Field Choice Set 1',
extra_choices=('Foo', 'Bar', 'Baz') extra_choices={'foo': 'Foo', 'bar': 'Bar', 'baz': 'Baz'}
) )
custom_fields = ( custom_fields = (
@ -469,13 +477,13 @@ class CustomFieldAPITest(APITestCase):
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_SELECT, type=CustomFieldTypeChoices.TYPE_SELECT,
name='select_field', name='select_field',
default='Foo', default='foo',
choice_set=choice_set choice_set=choice_set
), ),
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_MULTISELECT, type=CustomFieldTypeChoices.TYPE_MULTISELECT,
name='multiselect_field', name='multiselect_field',
default=['Foo'], default=['foo'],
choice_set=choice_set choice_set=choice_set
), ),
CustomField( CustomField(
@ -514,8 +522,8 @@ class CustomFieldAPITest(APITestCase):
custom_fields[6].name: '2020-01-02 12:00:00', custom_fields[6].name: '2020-01-02 12:00:00',
custom_fields[7].name: 'http://example.com/2', custom_fields[7].name: 'http://example.com/2',
custom_fields[8].name: '{"foo": 1, "bar": 2}', custom_fields[8].name: '{"foo": 1, "bar": 2}',
custom_fields[9].name: 'Bar', custom_fields[9].name: 'bar',
custom_fields[10].name: ['Bar', 'Baz'], custom_fields[10].name: ['bar', 'baz'],
custom_fields[11].name: vlans[1].pk, custom_fields[11].name: vlans[1].pk,
custom_fields[12].name: [vlans[2].pk, vlans[3].pk], custom_fields[12].name: [vlans[2].pk, vlans[3].pk],
} }
@ -671,8 +679,8 @@ class CustomFieldAPITest(APITestCase):
'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0), 'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0),
'url_field': 'http://example.com/2', 'url_field': 'http://example.com/2',
'json_field': '{"foo": 1, "bar": 2}', 'json_field': '{"foo": 1, "bar": 2}',
'select_field': 'Bar', 'select_field': 'bar',
'multiselect_field': ['Bar', 'Baz'], 'multiselect_field': ['bar', 'baz'],
'object_field': VLAN.objects.get(vid=2).pk, 'object_field': VLAN.objects.get(vid=2).pk,
'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)), 'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
}, },
@ -799,8 +807,8 @@ class CustomFieldAPITest(APITestCase):
'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0), 'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0),
'url_field': 'http://example.com/2', 'url_field': 'http://example.com/2',
'json_field': '{"foo": 1, "bar": 2}', 'json_field': '{"foo": 1, "bar": 2}',
'select_field': 'Bar', 'select_field': 'bar',
'multiselect_field': ['Bar', 'Baz'], 'multiselect_field': ['bar', 'baz'],
'object_field': VLAN.objects.get(vid=2).pk, 'object_field': VLAN.objects.get(vid=2).pk,
'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)), 'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
} }
@ -1041,7 +1049,7 @@ class CustomFieldImportTest(TestCase):
# Create a set of custom field choices # Create a set of custom field choices
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1', name='Custom Field Choice Set 1',
extra_choices=('Choice A', 'Choice B', 'Choice C') extra_choices={'a': 'Option A', 'b': 'Option B', 'c': 'Option C'}
) )
custom_fields = ( custom_fields = (
@ -1067,8 +1075,8 @@ class CustomFieldImportTest(TestCase):
""" """
data = ( data = (
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_decimal', 'cf_boolean', 'cf_date', 'cf_datetime', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'), ('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_decimal', 'cf_boolean', 'cf_date', 'cf_datetime', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'),
('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', '123.45', 'True', '2020-01-01', '2020-01-01 12:00:00', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'), ('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', '123.45', 'True', '2020-01-01', '2020-01-01 12:00:00', 'http://example.com/1', '{"foo": 123}', 'a', '"a,b"'),
('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', '456.78', 'False', '2020-01-02', '2020-01-02 12:00:00', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'), ('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', '456.78', 'False', '2020-01-02', '2020-01-02 12:00:00', 'http://example.com/2', '{"bar": 456}', 'b', '"b,c"'),
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', '', ''), ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', '', ''),
) )
csv_data = '\n'.join(','.join(row) for row in data) csv_data = '\n'.join(','.join(row) for row in data)
@ -1089,8 +1097,8 @@ class CustomFieldImportTest(TestCase):
self.assertEqual(site1.cf['datetime'].isoformat(), '2020-01-01T12:00:00+00:00') self.assertEqual(site1.cf['datetime'].isoformat(), '2020-01-01T12:00:00+00:00')
self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1') self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
self.assertEqual(site1.custom_field_data['json'], {"foo": 123}) self.assertEqual(site1.custom_field_data['json'], {"foo": 123})
self.assertEqual(site1.custom_field_data['select'], 'Choice A') self.assertEqual(site1.custom_field_data['select'], 'a')
self.assertEqual(site1.custom_field_data['multiselect'], ['Choice A', 'Choice B']) self.assertEqual(site1.custom_field_data['multiselect'], ['a', 'b'])
# Validate data for site 2 # Validate data for site 2
site2 = Site.objects.get(name='Site 2') site2 = Site.objects.get(name='Site 2')
@ -1104,8 +1112,8 @@ class CustomFieldImportTest(TestCase):
self.assertEqual(site2.cf['datetime'].isoformat(), '2020-01-02T12:00:00+00:00') self.assertEqual(site2.cf['datetime'].isoformat(), '2020-01-02T12:00:00+00:00')
self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2') self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
self.assertEqual(site2.custom_field_data['json'], {"bar": 456}) self.assertEqual(site2.custom_field_data['json'], {"bar": 456})
self.assertEqual(site2.custom_field_data['select'], 'Choice B') self.assertEqual(site2.custom_field_data['select'], 'b')
self.assertEqual(site2.custom_field_data['multiselect'], ['Choice B', 'Choice C']) self.assertEqual(site2.custom_field_data['multiselect'], ['b', 'c'])
# No custom field data should be set for site 3 # No custom field data should be set for site 3
site3 = Site.objects.get(name='Site 3') site3 = Site.objects.get(name='Site 3')
@ -1221,7 +1229,7 @@ class CustomFieldModelFilterTest(TestCase):
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1', name='Custom Field Choice Set 1',
extra_choices=['A', 'B', 'C', 'X'] extra_choices={'A': 'A', 'B': 'B', 'C': 'C', 'X': 'X'}
) )
# Integer filtering # Integer filtering

View File

@ -14,8 +14,8 @@ class CustomFieldModelFormTest(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
obj_type = ContentType.objects.get_for_model(Site) obj_type = ContentType.objects.get_for_model(Site)
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1', name='Choice Set 1',
extra_choices=('A', 'B', 'C') extra_choices={'a': 'A', 'b': 'B', 'c': 'C'}
) )
cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT) cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT)

View File

@ -1,3 +1,4 @@
import json
import urllib.parse import urllib.parse
import uuid import uuid
@ -23,7 +24,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
site_ct = ContentType.objects.get_for_model(Site) site_ct = ContentType.objects.get_for_model(Site)
CustomFieldChoiceSet.objects.create( CustomFieldChoiceSet.objects.create(
name='Choice Set 1', name='Choice Set 1',
extra_choices=('A', 'B', 'C') extra_choices={'A': 'A', 'B': 'B', 'C': 'C'}
) )
custom_fields = ( custom_fields = (
@ -76,29 +77,38 @@ class CustomFieldChoiceSetTestCase(ViewTestCases.PrimaryObjectViewTestCase):
def setUpTestData(cls): def setUpTestData(cls):
choice_sets = ( choice_sets = (
CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['1A', '1B', '1C', '1D', '1E']), CustomFieldChoiceSet(
CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['2A', '2B', '2C', '2D', '2E']), name='Choice Set 1',
CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['3A', '3B', '3C', '3D', '3E']), extra_choices={'A1': 'Choice 1', 'A2': 'Choice 2', 'A3': 'Choice 3'}
),
CustomFieldChoiceSet(
name='Choice Set 2',
extra_choices={'B1': 'Choice 1', 'B2': 'Choice 2', 'B3': 'Choice 3'}
),
CustomFieldChoiceSet(
name='Choice Set 3',
extra_choices={'C1': 'Choice 1', 'C2': 'Choice 2', 'C3': 'Choice 3'}
),
) )
CustomFieldChoiceSet.objects.bulk_create(choice_sets) CustomFieldChoiceSet.objects.bulk_create(choice_sets)
cls.form_data = { cls.form_data = {
'name': 'Choice Set X', 'name': 'Choice Set X',
'extra_choices': 'X1,X2,X3,X4,X5', 'extra_choices': json.dumps({'X1': 'Choice 1', 'X2': 'Choice 2', 'X3': 'Choice 3'})
} }
cls.csv_data = ( cls.csv_data = (
'name,extra_choices', 'name,extra_choices',
'Choice Set 4,"4A,4B,4C,4D,4E"', 'Choice Set 4,"{""D1"": ""Choice 1"", ""D2"": ""Choice 2"", ""D3"": ""Choice 3""}"',
'Choice Set 5,"5A,5B,5C,5D,5E"', 'Choice Set 5,"{""E1"": ""Choice 1"", ""E2"": ""Choice 2"", ""E3"": ""Choice 3""}"',
'Choice Set 6,"6A,6B,6C,6D,6E"', 'Choice Set 6,"{""F1"": ""Choice 1"", ""F2"": ""Choice 2"", ""F3"": ""Choice 3""}"',
) )
cls.csv_update_data = ( cls.csv_update_data = (
'id,extra_choices', 'id,extra_choices',
f'{choice_sets[0].pk},"1X,1Y,1Z"', f'{choice_sets[0].pk},"{{""a"": ""A"", ""b"": ""B"", ""c"": ""C""}}"',
f'{choice_sets[1].pk},"2X,2Y,2Z"', f'{choice_sets[1].pk},"{{""a"": ""A"", ""b"": ""B"", ""c"": ""C""}}"',
f'{choice_sets[2].pk},"3X,3Y,3Z"', f'{choice_sets[2].pk},"{{""a"": ""A"", ""b"": ""B"", ""c"": ""C""}}"',
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {

View File

@ -9,8 +9,8 @@ from django.db.models import DateField, DateTimeField
from django.template import Context, Template from django.template import Context, Template
from django.urls import reverse from django.urls import reverse
from django.utils.dateparse import parse_date from django.utils.dateparse import parse_date
from django.utils.html import escape
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django_tables2.columns import library from django_tables2.columns import library
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
@ -21,9 +21,9 @@ from utilities.utils import content_type_identifier, content_type_name, get_view
__all__ = ( __all__ = (
'ActionsColumn', 'ActionsColumn',
'ArrayColumn',
'BooleanColumn', 'BooleanColumn',
'ChoiceFieldColumn', 'ChoiceFieldColumn',
'ChoiceSetColumn',
'ColorColumn', 'ColorColumn',
'ColoredLabelColumn', 'ColoredLabelColumn',
'ContentTypeColumn', 'ContentTypeColumn',
@ -594,27 +594,21 @@ class MarkdownColumn(tables.TemplateColumn):
return value return value
class ArrayColumn(tables.Column): class ChoiceSetColumn(tables.Column):
"""
List array items as a comma-separated list. def __init__(self, *args, max_items=None, **kwargs):
"""
def __init__(self, *args, max_items=None, func=str, **kwargs):
self.max_items = max_items self.max_items = max_items
self.func = func
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def render(self, value): def render(self, value):
omitted_count = 0 omitted_count = 0
value = [v[1] for v in value]
# Limit the returned items to the specified maximum number (if any) # Limit the returned items to the specified maximum number (if any)
if self.max_items: if self.max_items:
omitted_count = len(value) - self.max_items omitted_count = len(value) - self.max_items
value = value[:self.max_items - 1] value = value[:self.max_items - 1]
# Apply custom processing function (if any) per item
if self.func:
value = [self.func(v) for v in value]
# Annotate omitted items (if applicable) # Annotate omitted items (if applicable)
if omitted_count > 0: if omitted_count > 0:
value.append(f'({omitted_count} more)') value.append(f'({omitted_count} more)')

View File

@ -55,7 +55,7 @@
<th>Label</th> <th>Label</th>
</tr> </tr>
</thead> </thead>
{% for value, label in object.choices|slice:":100" %} {% for value, label in object.choices.items %}
<tr> <tr>
<td>{{ value }}</td> <td>{{ value }}</td>
<td>{{ label }}</td> <td>{{ label }}</td>

View File

@ -1,7 +1,7 @@
from django import forms from django import forms
__all__ = ( __all__ = (
'ArrayWidget', 'ChoicesWidget',
'ClearableFileInput', 'ClearableFileInput',
'MarkdownWidget', 'MarkdownWidget',
'NumberWithOptions', 'NumberWithOptions',
@ -46,11 +46,11 @@ class SlugWidget(forms.TextInput):
template_name = 'widgets/sluginput.html' template_name = 'widgets/sluginput.html'
class ArrayWidget(forms.Textarea): class ChoicesWidget(forms.Textarea):
""" """
Render each item of an array on a new line within a textarea for easy editing/ Render each key-value pair of a dictionary on a new line within a textarea for easy editing.
""" """
def format_value(self, value): def format_value(self, value):
if value is None or not len(value): if not value:
return None return None
return '\n'.join(value) return '\n'.join([f'{k},{v}' for k, v in value.items()])