From 2f638a51b783e13ed6b3cc9ff05bbef0bacc7095 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Wed, 30 Sep 2026 15:26:33 +0200 Subject: [PATCH] pretixPOS API compat --- src/pretix/api/serializers/event.py | 1 + src/pretix/api/serializers/item.py | 19 +++++-- src/pretix/api/serializers/order.py | 81 +++++++++++++++++++++++------ src/pretix/api/views/item.py | 38 +++++++++++++- src/pretix/control/views/item.py | 8 ++- 5 files changed, 122 insertions(+), 25 deletions(-) diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 1b08756403..f23e5b2e7d 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -995,6 +995,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer): else [] ) ) + self.fields['system_question_order'] = serializers.JSONField(read_only=True, default=dict(), source='_invalid') class MultiLineStringField(serializers.Field): diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py index 8efbcd9943..6f37776c30 100644 --- a/src/pretix/api/serializers/item.py +++ b/src/pretix/api/serializers/item.py @@ -551,6 +551,8 @@ class LegacyDependencyValueField(serializers.CharField): +#name_schemes_all_fields = sorted(list(set(k for scheme in PERSON_NAME_SCHEMES.values() for k, v, w, *__ in scheme['fields']))) +name_schemes_all_fields = ['calling_name', 'degree', 'family_name', 'full_name', 'given_name', 'latin_transcription', 'middle_name', 'salutation', 'title'] system_datafield_numbers = [ QuestionnaireChild.SystemQuestion.ATTENDEE_NAME_PARTS.value, QuestionnaireChild.SystemQuestion.ATTENDEE_EMAIL.value, @@ -560,13 +562,19 @@ system_datafield_numbers = [ QuestionnaireChild.SystemQuestion.CITY.value, QuestionnaireChild.SystemQuestion.STATE.value, QuestionnaireChild.SystemQuestion.COUNTRY.value, +] + [ + QuestionnaireChild.SystemQuestion.ATTENDEE_NAME_PARTS.value + ':' + field for field in name_schemes_all_fields ] def cook_id(questionnaire_child): - df_id = questionnaire_child.user_datafield_id + 100 if questionnaire_child.user_datafield_id else system_datafield_numbers.index(questionnaire_child.system_datafield) - assert questionnaire_child.questionnaire_id < 1000000000 - return questionnaire_child.questionnaire_id * 1000000000 + df_id + return cook_id_from_parts(questionnaire_child.questionnaire_id, questionnaire_child.user_datafield_id, questionnaire_child.system_datafield) + + +def cook_id_from_parts(qid, dfid, sys_df): + df_id = dfid + 100 if dfid else system_datafield_numbers.index(sys_df) + assert qid < 1000000000 + return qid * 1000000000 + df_id def uncook_id(id): @@ -584,7 +592,7 @@ def uncook_id(id): class CompatQuestionSerializer(I18nAwareModelSerializer): id = serializers.SerializerMethodField('cook_id', read_only=True) question = I18nField(source='label', allow_null=True) - type = serializers.CharField(source='user_datafield.type', allow_null=True, default=Question.FieldType.TEXT) + type = serializers.CharField(source='user_datafield.type', allow_null=True, default=Question.FieldType.STRING) #identifier = serializers.CharField(source='user_datafield.identifier', allow_null=True) identifier = serializers.SerializerMethodField('get_identifier', read_only=True) show_during_checkin = serializers.BooleanField(source='user_datafield.show_during_checkin', allow_null=True, default=False) @@ -604,6 +612,7 @@ class CompatQuestionSerializer(I18nAwareModelSerializer): dependency_value = serializers.SerializerMethodField('empty', read_only=True) position = serializers.SerializerMethodField('get_position', read_only=True) items = serializers.PrimaryKeyRelatedField(source='questionnaire.items', many=True, queryset=Item.objects.none(), allow_null=True, required=False) + questionnaire_id = serializers.IntegerField(source='questionnaire.id', read_only=True) options = InlineQuestionOptionSerializer(source='user_datafield.options', many=True, required=False, default=[]) @@ -613,7 +622,7 @@ class CompatQuestionSerializer(I18nAwareModelSerializer): 'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values', 'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min', 'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max', - 'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait') + 'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait', 'questionnaire_id') def __init__(self, *args, **kwargs): self.fields['items'].child_relation.queryset = kwargs['context']['event'].items.all() diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 89b7af474b..77fc9a364f 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -48,7 +48,8 @@ from pretix.api.serializers.event import SubEventSerializer from pretix.api.serializers.forms import form_field_to_serializer_field from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.item import ( - InlineItemVariationSerializer, ItemSerializer, DatafieldSerializer, cook_id, system_datafield_numbers, uncook_id, + InlineItemVariationSerializer, ItemSerializer, DatafieldSerializer, cook_id, cook_id_from_parts, + system_datafield_numbers, uncook_id, ) from pretix.api.signals import order_api_details, orderposition_api_details from pretix.base.decimal import round_decimal @@ -75,7 +76,8 @@ from pretix.base.services.pricing import ( ) from pretix.base.services.quotas import QuotaAvailability from pretix.base.settings import ( - COUNTRIES_WITH_STATE_IN_ADDRESS, ROUNDING_MODES, + COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, + ROUNDING_MODES, ) from pretix.base.signals import register_ticket_outputs from pretix.helpers.countries import CachedCountries @@ -240,23 +242,58 @@ class MixedAnswerListSerializer(ListSerializer): def to_representation(self, instance): answers = super().to_representation(instance.answers) - for qc in QuestionnaireChild.objects.filter(questionnaire__items__id=instance.item_id, system_datafield__isnull=False): - answers.append({ - "question": cook_id(qc), - "answer": instance.get_system_answer(qc.system_datafield), - "question_identifier": f'@{qc.system_datafield}', - "options": [], - "option_identifiers": [], - "questionnaire_id": qc.questionnaire_id, - "system_datafield": qc.system_datafield, - }) - return { - "answers": answers, - } + answers_dict = {answer["question"]: answer for answer in answers} + for qc in QuestionnaireChild.objects.filter(questionnaire__items__id=instance.item_id): + if qc.user_datafield_id: + if answer := answers_dict.get(qc.user_datafield_id): + answer = dict(answer) + answer["question"] = cook_id(qc) + answer["questionnaire_id"] = qc.questionnaire_id + answer["user_datafield_id"] = qc.user_datafield_id + answers.append(answer) + elif (sys_answer := instance.get_system_answer(qc.system_datafield)) is not None: + if qc.system_datafield == 'attendee_name_parts': + name_parts = instance.attendee_name_parts + settings = self.context['event'].settings + for k, v, w, *__ in PERSON_NAME_SCHEMES.get(settings.name_scheme)['fields']: + if k in name_parts: + options = [] + options_ident = [] + try: + if k == "salutation": + salutation_keys = [key for key, value in PERSON_NAME_SALUTATIONS] + options = [salutation_keys.index(name_parts[k]) + 1] + options_ident = [name_parts[k]] + elif k == "title" and settings.name_scheme_titles: + options = [PERSON_NAME_TITLE_GROUPS.get(settings.name_scheme_titles)[1].index(name_parts[k]) + 1] + options_ident = [name_parts[k]] + except ValueError: + pass + answers.append({ + "question": cook_id_from_parts(qc.questionnaire_id, None, f'{qc.system_datafield}:{k}'), + "answer": name_parts[k], + "question_identifier": f'@{qc.system_datafield}:{k}', + "options": options, + "option_identifiers": options_ident, + "questionnaire_id": qc.questionnaire_id, + "system_datafield": f'{qc.system_datafield}:{k}', + }) + answers.append({ + "question": cook_id(qc), + "answer": sys_answer, + "question_identifier": f'@{qc.system_datafield}', + "options": [], + "option_identifiers": [], + "questionnaire_id": qc.questionnaire_id, + "system_datafield": qc.system_datafield, + }) + return answers def to_internal_value(self, data): result = {} answers = [] + attendee_name_parts = {} + settings = self.context['event'].settings for answer in data["answers"]: try: qid, dfid, sys_df = uncook_id(answer["question"]) @@ -264,13 +301,23 @@ class MixedAnswerListSerializer(ListSerializer): if type(answer["question"]) is str and answer["question"].lstrip("@") in system_datafield_numbers: qid, dfid, sys_df = None, None, answer["question"].lstrip("@") else: - raise + raise ValidationError('Invalid question ID') if dfid: answer["question"] = dfid answers.append(answer) + elif sys_df == "country": + result[sys_df] = Country(answer["answer"]) + elif sys_df == "attendee_name_parts:salutation": + attendee_name_parts['salutation'] = PERSON_NAME_SALUTATIONS[answer["options"][0] - 1][0] + elif sys_df == "attendee_name_parts:title" and settings.name_scheme_titles: + attendee_name_parts['title'] = PERSON_NAME_TITLE_GROUPS.get(settings.name_scheme_titles)[1][answer["options"][0] - 1] + elif sys_df.startswith("attendee_name_parts:"): + attendee_name_parts[sys_df[len("attendee_name_parts:"):]] = answer["answer"] else: - result[sys_df] = Country(answer["answer"]) if sys_df == "country" else answer["answer"] + result[sys_df] = answer["answer"] result["answers"] = super().to_internal_value(answers) + if attendee_name_parts: + result["attendee_name_parts"] = attendee_name_parts return result diff --git a/src/pretix/api/views/item.py b/src/pretix/api/views/item.py index 3d70a2b786..50fb4f8bf3 100644 --- a/src/pretix/api/views/item.py +++ b/src/pretix/api/views/item.py @@ -39,6 +39,7 @@ from django.shortcuts import get_object_or_404 from django.utils.functional import cached_property from django_filters.rest_framework import DjangoFilterBackend, FilterSet from django_scopes import scopes_disabled +from i18nfield.strings import LazyI18nString from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied, ValidationError @@ -49,15 +50,18 @@ from pretix.api.serializers.item import ( CompatQuestionSerializer, ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer, ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer, QuestionnaireSerializer, QuestionOptionSerializer, DatafieldSerializer, - QuotaSerializer, uncook_id, + QuotaSerializer, cook_id_from_parts, uncook_id, ) from pretix.api.views import ConditionalListView +from pretix.base.forms.questions import REQUIRED_NAME_PARTS from pretix.base.models import ( CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime, ItemVariation, Question, QuestionOption, Quota, ) from pretix.base.models.items import Questionnaire, QuestionnaireChild from pretix.base.services.quotas import QuotaAvailability +from pretix.base.settings import PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS +from pretix.control.views.item import i18n_all_from_gettext from pretix.helpers.dicts import merge_dicts from pretix.helpers.i18n import i18ncomp @@ -497,6 +501,38 @@ class CompatQuestionViewSet(ConditionalListView, viewsets.ModelViewSet): questionnaire__type__startswith='P', ).prefetch_related('user_datafield', 'user_datafield__options', 'questionnaire').all() + def list(self, request, **kwargs): + resp = super().list(request, **kwargs) + mangled_results = [] + pos_offset = 0 + for row in resp.data['results']: + row['position'] += pos_offset + if row['identifier'] == '@attendee_name_parts': + settings = self.request.event.settings + for k, v, w, *__ in PERSON_NAME_SCHEMES.get(settings.name_scheme)['fields']: + part = dict(row) + part["identifier"] += ":" + k + part["question"] = i18n_all_from_gettext(v, settings.locales) + if k == "title" and settings.name_scheme_titles: + part["type"] = Question.FieldType.CHOICE + part["options"] = [{"id": idx + 1, "identifier": k, "answer": i18n_all_from_gettext(k, settings.locales), "position": idx + 1} for idx, k in + enumerate(PERSON_NAME_TITLE_GROUPS.get(settings.name_scheme_titles)[1])] + elif k == "salutation": + part["type"] = Question.FieldType.CHOICE + part["options"] = [{"id": idx + 1, "identifier": k, "answer": i18n_all_from_gettext(v, settings.locales), "position": idx + 1} for idx, (k, v) in + enumerate(PERSON_NAME_SALUTATIONS)] + part["id"] = cook_id_from_parts(row['questionnaire_id'], None, part["identifier"][1:]) + part["required"] = part["required"] and k in REQUIRED_NAME_PARTS + mangled_results.append(part) + pos_offset += 1 + row["position"] += 1 + else: + if row['identifier'] == '@country': + row['type'] = Question.FieldType.COUNTRYCODE + mangled_results.append(row) + resp.data['results'] = mangled_results + return resp + def get_object(self): """ Returns the object the view is displaying. diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index 57c723c85a..0acdda1764 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -708,10 +708,14 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView): def textchoices_to_json(choices, event): - return [(c.name, c.value, i18n_all(event.settings.locales, LazyI18nString.from_gettext(c.label).data)) for c in choices] + return [(c.name, c.value, i18n_all_from_gettext(c.label, event.settings.locales)) for c in choices] -def i18n_all(locales, data): +def i18n_all_from_gettext(gettext_str, locales): + return i18n_all(LazyI18nString.from_gettext(gettext_str).data, locales) + + +def i18n_all(data, locales): out = {} for locale in locales: out[locale] = data[locale]