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):
|
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
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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={
|
||||||
|
@ -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)
|
||||||
|
@ -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 }}',
|
||||||
|
@ -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'}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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 = {
|
||||||
|
@ -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)')
|
||||||
|
@ -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>
|
||||||
|
@ -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()])
|
||||||
|
Loading…
Reference in New Issue
Block a user