CheckinRPC: Also perform media exchange

This commit is contained in:
Martin Gross
2026-05-29 18:58:14 +02:00
committed by Raphael Michel
parent 1f4189d539
commit b2380f794e
5 changed files with 118 additions and 25 deletions

View File

@@ -26,7 +26,7 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.event import SubEventSerializer from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.media import MEDIA_TYPES from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Checkin, CheckinList from pretix.base.models import Checkin, CheckinList, Item
class CheckinListSerializer(I18nAwareModelSerializer): class CheckinListSerializer(I18nAwareModelSerializer):
@@ -88,6 +88,13 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
nonce = serializers.CharField(required=False, allow_null=True) nonce = serializers.CharField(required=False, allow_null=True)
datetime = serializers.DateTimeField(required=False, allow_null=True) datetime = serializers.DateTimeField(required=False, allow_null=True)
answers = serializers.JSONField(required=False, allow_null=True) answers = serializers.JSONField(required=False, allow_null=True)
media_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
media_identifier = serializers.CharField(required=False)
media_policy = serializers.ChoiceField(required=False, choices=Item.MEDIA_POLICIES)
media_action = serializers.ChoiceField(required=False, choices=[
('append', 'append'),
('replace', 'replace'),
])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@@ -72,6 +72,7 @@ from pretix.base.services.checkin import (
CheckInError, RequiredMediaExchangeError, RequiredQuestionsError, SQLLogic, CheckInError, RequiredMediaExchangeError, RequiredQuestionsError, SQLLogic,
perform_checkin, perform_checkin,
) )
from pretix.base.services.media import perform_media_exchange
from pretix.base.signals import checkin_annulled from pretix.base.signals import checkin_annulled
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
@@ -455,7 +456,8 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce, def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, checkin_type, ignore_unpaid, nonce,
untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported, untrusted_input, user, auth, expand, pdf_data, request, questions_supported, canceled_supported,
source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False): source_type='barcode', legacy_url_support=False, simulate=False, gate=None, use_order_locale=False,
media_type=None, media_identifier=None, media_policy=None, media_action=None):
if not checkinlists: if not checkinlists:
raise ValidationError('No check-in list passed.') raise ValidationError('No check-in list passed.')
@@ -803,26 +805,59 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
locale = op.order.event.settings.locale locale = op.order.event.settings.locale
with language(locale): with language(locale):
try: try:
perform_checkin( if all(k is not None for k in [media_type, media_identifier, media_policy, media_action]) and not media:
op=op, with transaction.atomic():
clist=list_by_event[op.order.event_id], media = perform_media_exchange(
given_answers=given_answers, organizer=request.organizer,
force=force, media_type=media_type,
ignore_unpaid=ignore_unpaid, media_identifier=media_identifier,
nonce=nonce, media_policy=media_policy,
datetime=datetime, media_action=media_action,
questions_supported=questions_supported, op=op,
canceled_supported=canceled_supported, )
user=user, source_type = media.media_type.identifier
auth=auth,
type=checkin_type, perform_checkin(
raw_barcode=raw_barcode_for_checkin, op=op,
raw_source_type=source_type, clist=list_by_event[op.order.event_id],
from_revoked_secret=from_revoked_secret, given_answers=given_answers,
simulate=simulate, force=force,
gate=gate, ignore_unpaid=ignore_unpaid,
reusable_media=media, nonce=nonce,
) datetime=datetime,
questions_supported=questions_supported,
canceled_supported=canceled_supported,
user=user,
auth=auth,
type=checkin_type,
raw_barcode=raw_barcode_for_checkin,
raw_source_type=source_type,
from_revoked_secret=from_revoked_secret,
simulate=simulate,
gate=gate,
reusable_media=media,
)
else:
perform_checkin(
op=op,
clist=list_by_event[op.order.event_id],
given_answers=given_answers,
force=force,
ignore_unpaid=ignore_unpaid,
nonce=nonce,
datetime=datetime,
questions_supported=questions_supported,
canceled_supported=canceled_supported,
user=user,
auth=auth,
type=checkin_type,
raw_barcode=raw_barcode_for_checkin,
raw_source_type=source_type,
from_revoked_secret=from_revoked_secret,
simulate=simulate,
gate=gate,
reusable_media=media,
)
except RequiredQuestionsError as e: except RequiredQuestionsError as e:
return Response({ return Response({
'status': 'incomplete', 'status': 'incomplete',
@@ -843,6 +878,17 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'media_policy': e.media_policy, 'media_policy': e.media_policy,
'media_type': e.media_type, 'media_type': e.media_type,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data, 'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
'reason_explanation': e.msg,
}, status=400)
except ReusableMedium.DuplicateEntry:
return Response({
'status': 'error',
'reason': Checkin.REASON_AMBIGUOUS,
'reason_explanation': 'Reusable medium identifier is ambigous',
'require_attention': op.require_checkin_attention,
'checkin_texts': op.checkin_texts,
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
}, status=400) }, status=400)
except CheckInError as e: except CheckInError as e:
if not simulate: if not simulate:
@@ -1031,6 +1077,10 @@ class CheckinRPCRedeemView(views.APIView):
canceled_supported=True, canceled_supported=True,
request=self.request, # this is not clean, but we need it in the serializers for URL generation request=self.request, # this is not clean, but we need it in the serializers for URL generation
legacy_url_support=False, legacy_url_support=False,
media_type=s.validated_data.get('media_type'),
media_identifier=s.validated_data.get('media_identifier'),
media_policy=s.validated_data.get('media_policy'),
media_action=s.validated_data.get('media_action'),
) )

View File

@@ -196,7 +196,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
return Response({"result": None}) return Response({"result": None})
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce @scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some performance
def list(self, request, **kwargs): def list(self, request, **kwargs):
date = serializers.DateTimeField().to_representation(now()) date = serializers.DateTimeField().to_representation(now())
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())

View File

@@ -138,6 +138,9 @@ class ReusableMedium(LoggedModel):
] ]
ordering = "identifier", "type", "organizer" ordering = "identifier", "type", "organizer"
class DuplicateEntry(Exception):
pass
class MediumKeySet(models.Model): class MediumKeySet(models.Model):
organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE, related_name='medium_key_sets') organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE, related_name='medium_key_sets')

View File

@@ -25,8 +25,8 @@ from django.db import IntegrityError
from django.db.models import Q from django.db.models import Q
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from pretix.base.models import GiftCardAcceptance from pretix.base.models import GiftCardAcceptance, Item
from pretix.base.models.media import MediumKeySet from pretix.base.models.media import MediumKeySet, ReusableMedium
def create_nfc_mf0aes_keyset(organizer): def create_nfc_mf0aes_keyset(organizer):
@@ -70,3 +70,36 @@ def get_keysets_for_organizer(organizer):
if new_set: if new_set:
sets.append(new_set) sets.append(new_set)
return sets return sets
def perform_media_exchange(organizer, media_type, media_identifier, media_policy, media_action, op):
medium = None
if media_policy in [Item.MEDIA_POLICY_REUSE, Item.MEDIA_POLICY_REUSE_OR_NEW]:
try:
medium = ReusableMedium.objects.get(
type=media_type,
identifier=media_identifier,
organizer=organizer,
)
except ReusableMedium.DoesNotExist:
pass
if not medium and media_policy in [Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW]:
try:
medium = ReusableMedium.objects.create(
type=media_type,
identifier=media_identifier,
organizer=organizer,
)
except IntegrityError:
raise ReusableMedium.DuplicateEntry()
if medium:
if media_action == 'append':
medium.linked_orderpositions.add(*[op])
elif media_action == 'replace':
medium.linked_orderpositions.set([op])
medium.save()
return medium