mirror of
https://github.com/pretix/pretix.git
synced 2026-10-01 19:14:43 +00:00
pretixSCAN compat
This commit is contained in:
@@ -592,6 +592,14 @@ class CompatQuestionSerializer(I18nAwareModelSerializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['items'].child_relation.queryset = self.context['event'].items.all() if 'event' in self.context else Item.objects.none()
|
||||
|
||||
def get_attribute(self, instance):
|
||||
if isinstance(instance, QuestionAnswer):
|
||||
position = instance.cartposition if instance.cartposition_id else instance.orderposition
|
||||
print('searching ',repr(instance), repr(position), repr(instance.question), instance.pk, position.item_id, self.context)
|
||||
qcs = list(QuestionnaireChild.objects.filter(user_datafield_id=instance.question_id, questionnaire__items__in=[position.item_id]))
|
||||
return next((qc for qc in qcs if qc.questionnaire.type == self.context.get('preferred_questionnaire_type', Questionnaire.QuestionnaireType.ORDER_POSITION_SALE)), qcs[0])
|
||||
|
||||
return super().get_attribute(instance)
|
||||
|
||||
def cook_dependency_question_id(self, qc):
|
||||
if qc.dependency_question:
|
||||
|
||||
@@ -48,8 +48,7 @@ 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, cook_id_from_parts,
|
||||
system_datafield_numbers, uncook_id,
|
||||
CompatQuestionSerializer, InlineItemVariationSerializer, ItemSerializer,
|
||||
)
|
||||
from pretix.api.signals import order_api_details, orderposition_api_details
|
||||
from pretix.base.decimal import round_decimal
|
||||
@@ -59,7 +58,7 @@ from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
|
||||
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
||||
QuestionAnswer, QuestionnaireChild, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
|
||||
QuestionAnswer, Questionnaire, QuestionnaireChild, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
|
||||
Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
@@ -247,7 +246,7 @@ class MixedAnswerListSerializer(ListSerializer):
|
||||
if qc.user_datafield_id:
|
||||
if answer := answers_dict.get(qc.user_datafield_id):
|
||||
answer = dict(answer)
|
||||
answer["question"] = cook_id(qc)
|
||||
answer["question"] = qc.cooked_id
|
||||
answer["questionnaire_id"] = qc.questionnaire_id
|
||||
answer["user_datafield_id"] = qc.user_datafield_id
|
||||
answers.append(answer)
|
||||
@@ -270,7 +269,7 @@ class MixedAnswerListSerializer(ListSerializer):
|
||||
except ValueError:
|
||||
pass
|
||||
answers.append({
|
||||
"question": cook_id_from_parts(qc.questionnaire_id, None, f'{qc.system_datafield}:{k}'),
|
||||
"question": QuestionnaireChild.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,
|
||||
@@ -279,7 +278,7 @@ class MixedAnswerListSerializer(ListSerializer):
|
||||
"system_datafield": f'{qc.system_datafield}:{k}',
|
||||
})
|
||||
answers.append({
|
||||
"question": cook_id(qc),
|
||||
"question": qc.cooked_id,
|
||||
"answer": sys_answer,
|
||||
"question_identifier": f'@{qc.system_datafield}',
|
||||
"options": [],
|
||||
@@ -296,9 +295,9 @@ class MixedAnswerListSerializer(ListSerializer):
|
||||
settings = self.context['event'].settings
|
||||
for answer in data["answers"]:
|
||||
try:
|
||||
qid, dfid, sys_df = uncook_id(answer["question"])
|
||||
qid, dfid, sys_df = QuestionnaireChild.uncook_id(answer["question"])
|
||||
except ValueError:
|
||||
if type(answer["question"]) is str and answer["question"].lstrip("@") in system_datafield_numbers:
|
||||
if type(answer["question"]) is str and answer["question"].lstrip("@") in QuestionnaireChild.SYSTEM_DATAFIELD_NUMBERS:
|
||||
qid, dfid, sys_df = None, None, answer["question"].lstrip("@")
|
||||
else:
|
||||
raise ValidationError('Invalid question ID')
|
||||
@@ -784,6 +783,7 @@ class AttendeeNamePartsField(serializers.Field):
|
||||
|
||||
class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
require_attention = RequireAttentionField(source='*')
|
||||
answers = AnswerSerializer(many=True)
|
||||
attendee_name = AttendeeNameField(source='*')
|
||||
attendee_name_parts = AttendeeNamePartsField(source='*')
|
||||
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
|
||||
@@ -813,7 +813,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True, context=self.context)
|
||||
|
||||
if 'answers.question' in self.context['expand']:
|
||||
self.fields['answers'].child.fields['question'] = DatafieldSerializer(read_only=True) # TODO(questionnaires)
|
||||
self.fields['answers'].child.fields['question'] = CompatQuestionSerializer(read_only=True) # TODO(questionnaires)
|
||||
|
||||
if 'addons' in self.context['expand']:
|
||||
# Experimental feature, undocumented on purpose for now in case we need to remove it again
|
||||
|
||||
@@ -55,7 +55,7 @@ from pretix.api.serializers.checkin import (
|
||||
CheckinListSerializer, CheckinRPCAnnulInputSerializer,
|
||||
CheckinRPCRedeemInputSerializer, MiniCheckinListSerializer,
|
||||
)
|
||||
from pretix.api.serializers.item import DatafieldSerializer
|
||||
from pretix.api.serializers.item import CompatQuestionSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
CheckinListOrderPositionSerializer, CheckinSerializer,
|
||||
FailedCheckinSerializer,
|
||||
@@ -65,7 +65,7 @@ from pretix.api.views.order import OrderPositionFilter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
|
||||
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
||||
Question, Questionnaire, QuestionnaireChild, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.orders import PrintLog
|
||||
from pretix.base.permissions import AnyPermissionOf
|
||||
@@ -489,6 +489,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'pdf_data': pdf_data and (
|
||||
user if user and user.is_authenticated else auth
|
||||
).has_event_permission(request.organizer, event, 'event.orders:read', request),
|
||||
'preferred_questionnaire_type': Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN,
|
||||
}
|
||||
|
||||
common_checkin_args = dict(
|
||||
@@ -797,16 +798,19 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
# 5. Pre-validate all incoming answers, handle file upload
|
||||
given_answers = {}
|
||||
if answers_data:
|
||||
for q in op.item.questions.filter(ask_during_checkin=True):
|
||||
if str(q.pk) in answers_data:
|
||||
for qc in QuestionnaireChild.objects.filter(
|
||||
questionnaire__items__in=[op.item], questionnaire__type=Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN
|
||||
):
|
||||
if qc.user_datafield_id and (str(qc.user_datafield_id) in answers_data or str(qc.cooked_id) in answers_data):
|
||||
try:
|
||||
if q.type == Question.TYPE_FILE:
|
||||
if answers_data[str(q.pk)]:
|
||||
given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth)
|
||||
ans = answers_data.get(str(qc.user_datafield_id), answers_data[str(qc.cooked_id)])
|
||||
if qc.user_datafield.type == Question.TYPE_FILE:
|
||||
if ans:
|
||||
given_answers[qc.user_datafield] = _handle_file_upload(ans, user, auth)
|
||||
else:
|
||||
given_answers[q] = None
|
||||
given_answers[qc.user_datafield] = None
|
||||
else:
|
||||
given_answers[q] = q.clean_answer(answers_data[str(q.pk)])
|
||||
given_answers[qc.user_datafield] = qc.user_datafield.clean_answer(ans)
|
||||
except (ValidationError, BaseValidationError):
|
||||
pass
|
||||
|
||||
@@ -867,14 +871,13 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
else:
|
||||
perform_checkin(**checkin_args)
|
||||
except RequiredQuestionsError as e:
|
||||
qs = CompatQuestionSerializer(context={'event': op.order.event})
|
||||
return Response({
|
||||
'status': 'incomplete',
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'questions': [
|
||||
DatafieldSerializer(q).data for q in e.questions # TODO(questionnaires)
|
||||
],
|
||||
'questions': CompatQuestionSerializer.mangle_results(op.order.event, (qs.to_representation(q) for q in e.questions)), # TODO(questionnaires)
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
except RequiredMediaExchangeError as e:
|
||||
@@ -973,6 +976,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ctx['event'] = self.request.event
|
||||
ctx['expand'] = self.request.query_params.getlist('expand')
|
||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
|
||||
ctx['preferred_questionnaire_type'] = Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN
|
||||
return ctx
|
||||
|
||||
def get_filterset_kwargs(self):
|
||||
|
||||
@@ -1807,9 +1807,10 @@ class Question(LoggedModel):
|
||||
return self.sortkey < other.sortkey
|
||||
|
||||
def clean_answer(self, answer):
|
||||
if self.required:
|
||||
if not answer or (self.type == Question.TYPE_BOOLEAN and answer not in ("true", "True", True)):
|
||||
raise ValidationError(_('An answer to this question is required to proceed.'))
|
||||
# TODO(questionnaires) - we don't have Question.required any more - do we actually need this check? if yes, pass in required from caller
|
||||
# if self.required:
|
||||
# if not answer or (self.type == Question.TYPE_BOOLEAN and answer not in ("true", "True", True)):
|
||||
# raise ValidationError(_('An answer to this question is required to proceed.'))
|
||||
if not answer:
|
||||
if self.type == Question.TYPE_BOOLEAN:
|
||||
return False
|
||||
|
||||
@@ -95,7 +95,7 @@ from ._transactions import (
|
||||
)
|
||||
from .base import LockModel, LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
from .items import Item, ItemVariation, Question, QuestionOption, Quota
|
||||
from .items import Item, ItemVariation, Question, QuestionOption, QuestionnaireChild, Quota
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1597,7 +1597,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
||||
def meta_info_data(self, d):
|
||||
self.meta_info = json.dumps(d)
|
||||
|
||||
def cache_answers(self, all=True):
|
||||
def cache_answers(self, questionnaire_type, sales_channel):
|
||||
"""
|
||||
Creates a new property on the object:
|
||||
questions: a list of Question objects, extended by an 'answer' property
|
||||
@@ -1605,13 +1605,16 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
||||
# We need to clone our question objects, otherwise we will override the cached
|
||||
# answers of other items in the same cart if the question objects have been
|
||||
# selected via prefetch_related
|
||||
if not all:
|
||||
if hasattr(self.item, 'relevant_questionnaires'):
|
||||
children = list(copy.copy(qc) for qq in self.item.relevant_questionnaires for qc in qq.childlist)
|
||||
else:
|
||||
children = list(copy.copy(qc) for qq in self.item.questionnaires.filter(type='PS') for qc in qq.children.all())
|
||||
if hasattr(self.item, 'relevant_questionnaires'):
|
||||
children = list(copy.copy(qc) for qq in self.item.relevant_questionnaires for qc in qq.childlist)
|
||||
else:
|
||||
children = list(copy.copy(qc) for qq in self.item.questionnaires.filter(type__startswith='P') for qc in qq.children.all())
|
||||
children = QuestionnaireChild.objects.filter(questionnaire__event_id=self.item.event_id, questionnaire__items__in=[self.item.id])
|
||||
if questionnaire_type is not None:
|
||||
children = children.filter(questionnaire__type=questionnaire_type)
|
||||
if sales_channel is not None:
|
||||
children = children.filter(Q(questionnaire__all_sales_channels=True) | Q(questionnaire__limit_sales_channel=sales_channel))
|
||||
|
||||
children = list(children)
|
||||
|
||||
qc_cache = {
|
||||
q.pk: q for q in children
|
||||
@@ -1624,28 +1627,30 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
||||
if parentqc.dependency_question_id and not qc_is_visible(parentqc.dependency_question_id, parentqc.dependency_values):
|
||||
return False
|
||||
answer_values = self.get_dependency_answer_values(parentqc)
|
||||
return any(qval in answer_values for qval in qvals)
|
||||
return answer_values and any(qval in answer_values for qval in qvals)
|
||||
|
||||
self.questions = []
|
||||
for qc in children:
|
||||
if qc.user_datafield_id:
|
||||
if qc.user_datafield_id in self.answer_cache:
|
||||
qc.answer = self.answer_cache[qc.user_datafield_id]
|
||||
else:
|
||||
qc.answer = ""
|
||||
#qc.answer.question = qc # cache object
|
||||
elif qc.system_datafield:
|
||||
qc.answer = self.get_system_answer(qc.system_datafield)
|
||||
#qc.answer.question = qc # cache object
|
||||
else:
|
||||
try:
|
||||
qc.answer = self.answer_cache[qc.user_datafield_id or qc.system_datafield]
|
||||
except KeyError:
|
||||
continue
|
||||
if not qc.dependency_question_id or qc_is_visible(qc.dependency_question_id, qc.dependency_values):
|
||||
self.questions.append(qc)
|
||||
return self.questions
|
||||
|
||||
@cached_property
|
||||
def answer_cache(self):
|
||||
return {
|
||||
aw.question_id: aw for aw in getattr(self, 'answerlist', self.answers.all())
|
||||
"attendee_name_parts": self.attendee_name,
|
||||
"attendee_email": self.attendee_email,
|
||||
"company": self.company,
|
||||
"street": self.street,
|
||||
"zipcode": self.zipcode,
|
||||
"city": self.city,
|
||||
"state": self.state,
|
||||
"country": str(self.country),
|
||||
**{aw.question_id: aw for aw in getattr(self, 'answerlist', self.answers.all())},
|
||||
}
|
||||
|
||||
def get_dependency_answer_values(self, qc):
|
||||
|
||||
@@ -1410,6 +1410,7 @@ class CartManager:
|
||||
for k, v in self._widget_data.items():
|
||||
if not k.startswith('question-'):
|
||||
continue
|
||||
# TODO(questionnaires) - switch to QuestionnaireChild
|
||||
q = cp.item.questions.filter(ask_during_checkin=False, identifier__iexact=k[9:]).first()
|
||||
if q:
|
||||
try:
|
||||
|
||||
@@ -55,7 +55,7 @@ from django_scopes import scope, scopes_disabled
|
||||
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Device, Event, Gate, Item, ItemVariation, Order,
|
||||
OrderPosition, QuestionOption,
|
||||
OrderPosition, QuestionOption, Questionnaire, QuestionnaireChild,
|
||||
)
|
||||
from pretix.base.signals import checkin_created, periodic_task
|
||||
from pretix.helpers import OF_SELF
|
||||
@@ -897,14 +897,14 @@ def _save_answers(op, answers, given_answers):
|
||||
written = False
|
||||
for q, a in given_answers.items():
|
||||
if not a:
|
||||
if q in answers:
|
||||
if q.id in answers:
|
||||
written = True
|
||||
answers[q].delete()
|
||||
answers[q.id].delete()
|
||||
else:
|
||||
continue
|
||||
if isinstance(a, QuestionOption):
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
if q.id in answers:
|
||||
qa = answers[q.id]
|
||||
qa.answer = str(a.answer)
|
||||
qa.save()
|
||||
written = True
|
||||
@@ -913,8 +913,8 @@ def _save_answers(op, answers, given_answers):
|
||||
qa = _create_answer(question=q, answer=str(a.answer))
|
||||
qa.options.add(a)
|
||||
elif isinstance(a, list):
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
if q.id in answers:
|
||||
qa = answers[q.id]
|
||||
qa.answer = ", ".join([str(o) for o in a])
|
||||
qa.save()
|
||||
written = True
|
||||
@@ -923,8 +923,8 @@ def _save_answers(op, answers, given_answers):
|
||||
qa = _create_answer(question=q, answer=", ".join([str(o) for o in a]))
|
||||
qa.options.add(*a)
|
||||
elif isinstance(a, File):
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
if q.id in answers:
|
||||
qa = answers[q.id]
|
||||
else:
|
||||
qa = _create_answer(question=q, answer=str(a))
|
||||
qa.file.save(os.path.basename(a.name), a, save=False)
|
||||
@@ -932,8 +932,8 @@ def _save_answers(op, answers, given_answers):
|
||||
qa.save()
|
||||
written = True
|
||||
else:
|
||||
if q in answers:
|
||||
qa = answers[q]
|
||||
if q.id in answers:
|
||||
qa = answers[q.id]
|
||||
qa.answer = str(a)
|
||||
qa.save()
|
||||
else:
|
||||
@@ -1033,17 +1033,19 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
|
||||
# Do this outside of transaction so it is saved even if the checkin fails for some other reason
|
||||
checkin_questions = list(
|
||||
clist.event.questions.filter(ask_during_checkin=True, items__in=[op.item_id])
|
||||
QuestionnaireChild.objects.filter(
|
||||
questionnaire__event=clist.event, questionnaire__type=Questionnaire.QuestionnaireType.ORDER_POSITION_CHECKIN,
|
||||
questionnaire__items__in=[op.item_id],
|
||||
).select_related('user_datafield', 'questionnaire')
|
||||
)
|
||||
require_answers = []
|
||||
if type != Checkin.TYPE_EXIT and checkin_questions:
|
||||
answers = {a.question: a for a in op.answers.all()}
|
||||
for q in checkin_questions:
|
||||
if q not in given_answers and q not in answers:
|
||||
if q.user_datafield not in given_answers and q.user_datafield_id not in op.answer_cache:
|
||||
require_answers.append(q)
|
||||
|
||||
if not simulate:
|
||||
_save_answers(op, answers, given_answers)
|
||||
_save_answers(op, op.answer_cache, given_answers)
|
||||
|
||||
with conditional_atomic(not simulate):
|
||||
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
||||
|
||||
@@ -626,7 +626,7 @@ class OrderDetail(OrderView):
|
||||
(p.item.ask_attendee_data and self.request.event.settings.attendee_emails_asked) or
|
||||
p.item.questionnaires.all()
|
||||
)
|
||||
p.cache_answers()
|
||||
p.cache_answers(questionnaire_type=None, sales_channel=None)
|
||||
p.order = self.order
|
||||
|
||||
positions.append(p)
|
||||
|
||||
@@ -1103,7 +1103,7 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
if parentqc.dependency_question_id and not question_is_visible(parentqc.dependency_question_id, parentqc.dependency_values):
|
||||
return False
|
||||
answer_values = cp.get_dependency_answer_values(parentqc)
|
||||
return any(qval in answer_values for qval in qvals)
|
||||
return answer_values and any(qval in answer_values for qval in qvals)
|
||||
|
||||
def question_is_required(q):
|
||||
return (
|
||||
|
||||
@@ -52,7 +52,7 @@ from pretix.base.i18n import get_language_without_region, set_region
|
||||
from pretix.base.middleware import get_supported_language
|
||||
from pretix.base.models import (
|
||||
CartPosition, Customer, InvoiceAddress, ItemAddOn, OrderFee, Question,
|
||||
QuestionAnswer, QuestionOption, TaxRule,
|
||||
QuestionAnswer, QuestionOption, Questionnaire, TaxRule,
|
||||
)
|
||||
from pretix.base.models.items import QuestionnaireChild
|
||||
from pretix.base.models.orders import CheckoutSession
|
||||
@@ -251,7 +251,7 @@ class CartMixin:
|
||||
group.price_for_input_net = group.net_price_before_rounding + sum(a.net_price_before_rounding for a in has_addons[group.pk])
|
||||
|
||||
if answers:
|
||||
group.cache_answers(all=False)
|
||||
group.cache_answers(questionnaire_type=Questionnaire.QuestionnaireType.ORDER_POSITION_SALE, sales_channel=self.request.sales_channel)
|
||||
group.additional_answers = pos_additional_fields.get(group.pk)
|
||||
positions.append(group)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user