pretixPOS API compat

This commit is contained in:
Mira Weller
2026-09-30 21:03:28 +02:00
parent 0d99672197
commit 2f638a51b7
5 changed files with 122 additions and 25 deletions
+1
View File
@@ -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):
+14 -5
View File
@@ -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()
+64 -17
View File
@@ -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
+37 -1
View File
@@ -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.
+6 -2
View File
@@ -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]