diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index 7e83daf82f..c7b39c5bc1 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -643,7 +643,40 @@ class OrganizerSettingsSerializer(SettingsSerializer): return 'pub/' + fname +class KeyLabelObjectListField(serializers.Field): + + def to_representation(self, value): + # django added unneccessary keys DELETE, ORDER through formsets, filter them here for backwards compat + def strip_unknown_keys(v): + return {k: v[k] for k in v.keys() if k in ("key", "label")} + return [strip_unknown_keys(v) for v in value] + + def to_internal_value(self, data): + if data is None: + return data + if not isinstance(data, list): + raise ValidationError("Choices need to be a list or null.") + if not data: + # empty list + return None + + if any([not isinstance(choice, dict) for choice in data]): + raise ValidationError("Choices need to contain only objects.") + + required_keys = {"key"} + allowed_keys = {"key", "label"} + if not all([required_keys <= set(choice.keys()) <= allowed_keys for choice in data]): + raise ValidationError("Each choice must contain a key and optionally a label.") + + choice_keys = [choice.get("key") for choice in data] + if len(set(choice_keys)) < len(choice_keys): + raise ValidationError("Each choice must have a unique key.") + return data + + class EventMetaPropertiesSerializer(I18nAwareModelSerializer): + choices = KeyLabelObjectListField(allow_null=True) + class Meta: model = EventMetaProperty fields = ( @@ -657,26 +690,13 @@ class EventMetaPropertiesSerializer(I18nAwareModelSerializer): full_data.update(data) choices = full_data.get("choices") - if "choices" in data and data["choices"] is not None: - if not isinstance(choices, list): - raise ValidationError("Choices need to be a list or null.") - if any([not isinstance(c, dict) for c in choices]): - raise ValidationError("Choices need to contain only objects.") - required_keys = {"key"} - allowed_keys = {"key", "label"} - if not all([required_keys <= set(c.keys()) <= allowed_keys for c in choices]): - raise ValidationError("Each choice must contain a key and optionally a label.") - - if choices: + default = full_data.get("default") + if choices and default: choice_keys = [c.get("key") for c in choices] - if len(set(choice_keys)) < len(choice_keys): - raise ValidationError("Each choice must have a unique key.") - - default = full_data.get("default") - if default and default not in choice_keys: + if default not in choice_keys: raise ValidationError("You cannot set a default value that is not a valid value.") if not choices and "choices" in data: - # normalize empty dict to None + # normalize empty dict to None data["choices"] = None return data diff --git a/src/tests/api/test_event_meta_properties.py b/src/tests/api/test_event_meta_properties.py index 87975a4471..c0d516522d 100644 --- a/src/tests/api/test_event_meta_properties.py +++ b/src/tests/api/test_event_meta_properties.py @@ -29,7 +29,14 @@ def event_meta_property(organizer): name="Color", default="Red", required=False, - choices=None, + choices=[ + { + "key": "Red", + "label": "Rot", + "DELETE": False, + "ORDER": 1, + } + ], ) @@ -37,7 +44,7 @@ TEST_TYPE_RES = { "name": "Color", "default": "Red", "required": False, - "choices": None, + "choices": [{"key": "Red", "label": "Rot"}], 'filter_allowed': True, 'filter_public': False, 'protected': False, @@ -141,6 +148,18 @@ def test_meta_property_patch(token_client, organizer, event_meta_property): } ) assert resp.status_code == 400 + resp = token_client.patch( + '/api/v1/organizers/{}/event_meta_properties/{}/' + .format(organizer.slug, event_meta_property.pk), + format='json', + data={ + "choices": [], + } + ) + assert resp.status_code == 200 + event_meta_property.refresh_from_db() + assert event_meta_property.choices is None + resp = token_client.patch( '/api/v1/organizers/{}/event_meta_properties/{}/' .format(organizer.slug, event_meta_property.pk), @@ -156,8 +175,6 @@ def test_meta_property_patch(token_client, organizer, event_meta_property): assert event_meta_property.choices is None - - @pytest.mark.django_db def test_meta_property_delete(token_client, organizer, event_meta_property): resp = token_client.delete(