mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 08:46:10 -06:00
#13241: Add support for custom field choice labels
This commit is contained in:
parent
5b570d6922
commit
c60aec2725
@ -131,6 +131,10 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
|
||||
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
|
||||
base_choices = ChoiceField(
|
||||
choices=CustomFieldChoiceSetBaseChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
|
@ -82,8 +82,8 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
|
||||
if page is not None:
|
||||
data = [
|
||||
{
|
||||
'id': c[0],
|
||||
'display': c[1],
|
||||
'value': c[0],
|
||||
'label': c[1],
|
||||
} for c in page
|
||||
]
|
||||
return self.get_paginated_response(data)
|
||||
|
@ -128,7 +128,7 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
|
||||
|
||||
def filter_by_choice(self, queryset, name, value):
|
||||
# TODO: Support case-insensitive matching
|
||||
return queryset.filter(extra_choices__overlap=value)
|
||||
return queryset.filter(extra_choices__has_any_keys=value)
|
||||
|
||||
|
||||
class CustomLinkFilterSet(BaseFilterSet):
|
||||
|
@ -68,11 +68,6 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
|
||||
required=False,
|
||||
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:
|
||||
model = CustomFieldChoiceSet
|
||||
|
@ -19,7 +19,7 @@ from utilities.forms.fields import (
|
||||
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, JSONField, SlugField,
|
||||
)
|
||||
from utilities.forms.widgets import ArrayWidget
|
||||
from utilities.forms.widgets import ChoicesWidget
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
|
||||
@ -85,19 +85,14 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
|
||||
extra_choices = forms.CharField(
|
||||
widget=ArrayWidget(),
|
||||
required=False,
|
||||
help_text=_('Enter one choice per line.')
|
||||
extra_choices = forms.JSONField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
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):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
|
@ -19,7 +19,7 @@ def create_choice_sets(apps, schema_editor):
|
||||
for cf in choice_fields:
|
||||
choiceset = CustomFieldChoiceSet.objects.create(
|
||||
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
|
||||
|
||||
@ -43,7 +43,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('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)),
|
||||
],
|
||||
options={
|
||||
|
@ -6,7 +6,6 @@ import django_filters
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.validators import RegexValidator, ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
@ -658,9 +657,8 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
|
||||
blank=True,
|
||||
help_text=_('Base set of predefined choices (optional)')
|
||||
)
|
||||
extra_choices = ArrayField(
|
||||
base_field=models.CharField(max_length=100),
|
||||
help_text=_('List of field choices'),
|
||||
extra_choices = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
@ -686,25 +684,27 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
|
||||
Returns a concatenation of the base and extra choices.
|
||||
"""
|
||||
if not hasattr(self, '_choices'):
|
||||
self._choices = []
|
||||
self._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:
|
||||
self._choices.extend([(k, k) for k in self.extra_choices])
|
||||
return self._choices
|
||||
|
||||
def clean(self):
|
||||
if not self.base_choices and not self.extra_choices:
|
||||
raise ValidationError(_("Must define base or extra choices."))
|
||||
self._choices.update(self.extra_choices)
|
||||
if self.order_alphabetically:
|
||||
self._choices = dict(sorted(self._choices.items()))
|
||||
return list(self._choices.items())
|
||||
|
||||
@property
|
||||
def choices_count(self):
|
||||
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):
|
||||
|
||||
# Sort choices if alphabetical ordering is enforced
|
||||
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)
|
||||
|
@ -68,11 +68,9 @@ class CustomFieldTable(NetBoxTable):
|
||||
verbose_name="UI visibility"
|
||||
)
|
||||
description = columns.MarkdownColumn()
|
||||
choices = columns.ArrayColumn(
|
||||
choices = columns.ChoiceSetColumn(
|
||||
max_items=10,
|
||||
func=lambda x: x[1],
|
||||
orderable=False,
|
||||
verbose_name=_('Choices')
|
||||
orderable=False
|
||||
)
|
||||
is_cloneable = columns.BooleanColumn()
|
||||
|
||||
@ -91,16 +89,13 @@ class CustomFieldChoiceSetTable(NetBoxTable):
|
||||
linkify=True
|
||||
)
|
||||
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,
|
||||
orderable=False
|
||||
)
|
||||
choices = columns.ArrayColumn(
|
||||
max_items=10,
|
||||
func=lambda x: x[1],
|
||||
orderable=False,
|
||||
verbose_name=_('Choices')
|
||||
)
|
||||
choice_count = tables.TemplateColumn(
|
||||
accessor=tables.A('extra_choices'),
|
||||
template_code='{{ value|length }}',
|
||||
|
@ -17,8 +17,8 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Custom Field Choice Set 1',
|
||||
extra_choices=['Bar', 'Foo']
|
||||
name='Choice Set 1',
|
||||
extra_choices={'bar': 'Bar', 'foo': 'Foo'}
|
||||
)
|
||||
|
||||
# Create a custom field on the Site model
|
||||
@ -48,7 +48,7 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
'slug': 'site-1',
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
'cf_cf1': 'ABC',
|
||||
'cf_cf2': 'Bar',
|
||||
'cf_cf2': 'bar',
|
||||
'tags': [tag.pk for tag in tags],
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
'slug': 'site-x',
|
||||
'status': SiteStatusChoices.STATUS_PLANNED,
|
||||
'cf_cf1': 'DEF',
|
||||
'cf_cf2': 'Foo',
|
||||
'cf_cf2': 'foo',
|
||||
'tags': [tags[2].pk],
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ class ChangeLogAPITest(APITestCase):
|
||||
# Create a select custom field on the Site model
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Choice Set 1',
|
||||
extra_choices=['Bar', 'Foo']
|
||||
extra_choices={'bar': 'Bar', 'foo': 'Foo'}
|
||||
)
|
||||
cf_select = CustomField(
|
||||
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
@ -251,7 +251,7 @@ class ChangeLogAPITest(APITestCase):
|
||||
'slug': 'site-1',
|
||||
'custom_fields': {
|
||||
'cf1': 'ABC',
|
||||
'cf2': 'Bar',
|
||||
'cf2': 'bar',
|
||||
},
|
||||
'tags': [
|
||||
{'name': 'Tag 1'},
|
||||
@ -285,7 +285,7 @@ class ChangeLogAPITest(APITestCase):
|
||||
'slug': 'site-x',
|
||||
'custom_fields': {
|
||||
'cf1': 'DEF',
|
||||
'cf2': 'Foo',
|
||||
'cf2': 'foo',
|
||||
},
|
||||
'tags': [
|
||||
{'name': 'Tag 3'}
|
||||
|
@ -269,8 +269,12 @@ class CustomFieldTest(TestCase):
|
||||
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||
|
||||
def test_select_field(self):
|
||||
CHOICES = ('Option A', 'Option B', 'Option C')
|
||||
value = CHOICES[1]
|
||||
CHOICES = {
|
||||
'a': 'Option A',
|
||||
'b': 'Option B',
|
||||
'c': 'Option C',
|
||||
}
|
||||
value = 'a'
|
||||
|
||||
# Create a set of custom field choices
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
@ -302,8 +306,12 @@ class CustomFieldTest(TestCase):
|
||||
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||
|
||||
def test_multiselect_field(self):
|
||||
CHOICES = ['Option A', 'Option B', 'Option C']
|
||||
value = [CHOICES[1], CHOICES[2]]
|
||||
CHOICES = {
|
||||
'a': 'Option A',
|
||||
'b': 'Option B',
|
||||
'c': 'Option C',
|
||||
}
|
||||
value = ['a', 'b']
|
||||
|
||||
# Create a set of custom field choices
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
@ -453,7 +461,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
# Create a set of custom field choices
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Custom Field Choice Set 1',
|
||||
extra_choices=('Foo', 'Bar', 'Baz')
|
||||
extra_choices={'foo': 'Foo', 'bar': 'Bar', 'baz': 'Baz'}
|
||||
)
|
||||
|
||||
custom_fields = (
|
||||
@ -469,13 +477,13 @@ class CustomFieldAPITest(APITestCase):
|
||||
CustomField(
|
||||
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
name='select_field',
|
||||
default='Foo',
|
||||
default='foo',
|
||||
choice_set=choice_set
|
||||
),
|
||||
CustomField(
|
||||
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||
name='multiselect_field',
|
||||
default=['Foo'],
|
||||
default=['foo'],
|
||||
choice_set=choice_set
|
||||
),
|
||||
CustomField(
|
||||
@ -514,8 +522,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
custom_fields[6].name: '2020-01-02 12:00:00',
|
||||
custom_fields[7].name: 'http://example.com/2',
|
||||
custom_fields[8].name: '{"foo": 1, "bar": 2}',
|
||||
custom_fields[9].name: 'Bar',
|
||||
custom_fields[10].name: ['Bar', 'Baz'],
|
||||
custom_fields[9].name: 'bar',
|
||||
custom_fields[10].name: ['bar', 'baz'],
|
||||
custom_fields[11].name: vlans[1].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),
|
||||
'url_field': 'http://example.com/2',
|
||||
'json_field': '{"foo": 1, "bar": 2}',
|
||||
'select_field': 'Bar',
|
||||
'multiselect_field': ['Bar', 'Baz'],
|
||||
'select_field': 'bar',
|
||||
'multiselect_field': ['bar', 'baz'],
|
||||
'object_field': VLAN.objects.get(vid=2).pk,
|
||||
'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),
|
||||
'url_field': 'http://example.com/2',
|
||||
'json_field': '{"foo": 1, "bar": 2}',
|
||||
'select_field': 'Bar',
|
||||
'multiselect_field': ['Bar', 'Baz'],
|
||||
'select_field': 'bar',
|
||||
'multiselect_field': ['bar', 'baz'],
|
||||
'object_field': VLAN.objects.get(vid=2).pk,
|
||||
'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
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
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 = (
|
||||
@ -1067,8 +1075,8 @@ class CustomFieldImportTest(TestCase):
|
||||
"""
|
||||
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'),
|
||||
('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 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 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}', 'b', '"b,c"'),
|
||||
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', '', ''),
|
||||
)
|
||||
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.custom_field_data['url'], 'http://example.com/1')
|
||||
self.assertEqual(site1.custom_field_data['json'], {"foo": 123})
|
||||
self.assertEqual(site1.custom_field_data['select'], 'Choice A')
|
||||
self.assertEqual(site1.custom_field_data['multiselect'], ['Choice A', 'Choice B'])
|
||||
self.assertEqual(site1.custom_field_data['select'], 'a')
|
||||
self.assertEqual(site1.custom_field_data['multiselect'], ['a', 'b'])
|
||||
|
||||
# Validate data for 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.custom_field_data['url'], 'http://example.com/2')
|
||||
self.assertEqual(site2.custom_field_data['json'], {"bar": 456})
|
||||
self.assertEqual(site2.custom_field_data['select'], 'Choice B')
|
||||
self.assertEqual(site2.custom_field_data['multiselect'], ['Choice B', 'Choice C'])
|
||||
self.assertEqual(site2.custom_field_data['select'], 'b')
|
||||
self.assertEqual(site2.custom_field_data['multiselect'], ['b', 'c'])
|
||||
|
||||
# No custom field data should be set for site 3
|
||||
site3 = Site.objects.get(name='Site 3')
|
||||
@ -1221,7 +1229,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Custom Field Choice Set 1',
|
||||
extra_choices=['A', 'B', 'C', 'X']
|
||||
extra_choices={'A': 'A', 'B': 'B', 'C': 'C', 'X': 'X'}
|
||||
)
|
||||
|
||||
# Integer filtering
|
||||
|
@ -14,8 +14,8 @@ class CustomFieldModelFormTest(TestCase):
|
||||
def setUpTestData(cls):
|
||||
obj_type = ContentType.objects.get_for_model(Site)
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Custom Field Choice Set 1',
|
||||
extra_choices=('A', 'B', 'C')
|
||||
name='Choice Set 1',
|
||||
extra_choices={'a': 'A', 'b': 'B', 'c': 'C'}
|
||||
)
|
||||
|
||||
cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import json
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
@ -23,7 +24,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
CustomFieldChoiceSet.objects.create(
|
||||
name='Choice Set 1',
|
||||
extra_choices=('A', 'B', 'C')
|
||||
extra_choices={'A': 'A', 'B': 'B', 'C': 'C'}
|
||||
)
|
||||
|
||||
custom_fields = (
|
||||
@ -76,29 +77,38 @@ class CustomFieldChoiceSetTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
def setUpTestData(cls):
|
||||
|
||||
choice_sets = (
|
||||
CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['1A', '1B', '1C', '1D', '1E']),
|
||||
CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['2A', '2B', '2C', '2D', '2E']),
|
||||
CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['3A', '3B', '3C', '3D', '3E']),
|
||||
CustomFieldChoiceSet(
|
||||
name='Choice Set 1',
|
||||
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)
|
||||
|
||||
cls.form_data = {
|
||||
'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 = (
|
||||
'name,extra_choices',
|
||||
'Choice Set 4,"4A,4B,4C,4D,4E"',
|
||||
'Choice Set 5,"5A,5B,5C,5D,5E"',
|
||||
'Choice Set 6,"6A,6B,6C,6D,6E"',
|
||||
'Choice Set 4,"{""D1"": ""Choice 1"", ""D2"": ""Choice 2"", ""D3"": ""Choice 3""}"',
|
||||
'Choice Set 5,"{""E1"": ""Choice 1"", ""E2"": ""Choice 2"", ""E3"": ""Choice 3""}"',
|
||||
'Choice Set 6,"{""F1"": ""Choice 1"", ""F2"": ""Choice 2"", ""F3"": ""Choice 3""}"',
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
'id,extra_choices',
|
||||
f'{choice_sets[0].pk},"1X,1Y,1Z"',
|
||||
f'{choice_sets[1].pk},"2X,2Y,2Z"',
|
||||
f'{choice_sets[2].pk},"3X,3Y,3Z"',
|
||||
f'{choice_sets[0].pk},"{{""a"": ""A"", ""b"": ""B"", ""c"": ""C""}}"',
|
||||
f'{choice_sets[1].pk},"{{""a"": ""A"", ""b"": ""B"", ""c"": ""C""}}"',
|
||||
f'{choice_sets[2].pk},"{{""a"": ""A"", ""b"": ""B"", ""c"": ""C""}}"',
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
|
@ -9,8 +9,8 @@ from django.db.models import DateField, DateTimeField
|
||||
from django.template import Context, Template
|
||||
from django.urls import reverse
|
||||
from django.utils.dateparse import parse_date
|
||||
from django.utils.html import escape
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_tables2.columns import library
|
||||
from django_tables2.utils import Accessor
|
||||
@ -21,9 +21,9 @@ from utilities.utils import content_type_identifier, content_type_name, get_view
|
||||
|
||||
__all__ = (
|
||||
'ActionsColumn',
|
||||
'ArrayColumn',
|
||||
'BooleanColumn',
|
||||
'ChoiceFieldColumn',
|
||||
'ChoiceSetColumn',
|
||||
'ColorColumn',
|
||||
'ColoredLabelColumn',
|
||||
'ContentTypeColumn',
|
||||
@ -594,27 +594,21 @@ class MarkdownColumn(tables.TemplateColumn):
|
||||
return value
|
||||
|
||||
|
||||
class ArrayColumn(tables.Column):
|
||||
"""
|
||||
List array items as a comma-separated list.
|
||||
"""
|
||||
def __init__(self, *args, max_items=None, func=str, **kwargs):
|
||||
class ChoiceSetColumn(tables.Column):
|
||||
|
||||
def __init__(self, *args, max_items=None, **kwargs):
|
||||
self.max_items = max_items
|
||||
self.func = func
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def render(self, value):
|
||||
omitted_count = 0
|
||||
value = [v[1] for v in value]
|
||||
|
||||
# Limit the returned items to the specified maximum number (if any)
|
||||
if self.max_items:
|
||||
omitted_count = len(value) - self.max_items
|
||||
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)
|
||||
if omitted_count > 0:
|
||||
value.append(f'({omitted_count} more)')
|
||||
|
@ -55,7 +55,7 @@
|
||||
<th>Label</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for value, label in object.choices|slice:":100" %}
|
||||
{% for value, label in object.choices.items %}
|
||||
<tr>
|
||||
<td>{{ value }}</td>
|
||||
<td>{{ label }}</td>
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
|
||||
__all__ = (
|
||||
'ArrayWidget',
|
||||
'ChoicesWidget',
|
||||
'ClearableFileInput',
|
||||
'MarkdownWidget',
|
||||
'NumberWithOptions',
|
||||
@ -46,11 +46,11 @@ class SlugWidget(forms.TextInput):
|
||||
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):
|
||||
if value is None or not len(value):
|
||||
if not value:
|
||||
return None
|
||||
return '\n'.join(value)
|
||||
return '\n'.join([f'{k},{v}' for k, v in value.items()])
|
||||
|
Loading…
Reference in New Issue
Block a user