diff --git a/src/pretix/api/serializers/checkin.py b/src/pretix/api/serializers/checkin.py index 63759c0160..765e444fee 100644 --- a/src/pretix/api/serializers/checkin.py +++ b/src/pretix/api/serializers/checkin.py @@ -26,7 +26,7 @@ from rest_framework.exceptions import ValidationError from pretix.api.serializers.event import SubEventSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer 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): @@ -88,6 +88,13 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer): nonce = serializers.CharField(required=False, allow_null=True) datetime = serializers.DateTimeField(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): super().__init__(*args, **kwargs) diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index c01a32eede..f1e2baceeb 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -72,6 +72,7 @@ from pretix.base.services.checkin import ( CheckInError, RequiredMediaExchangeError, RequiredQuestionsError, SQLLogic, perform_checkin, ) +from pretix.base.services.media import perform_media_exchange from pretix.base.signals import checkin_annulled 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, 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: 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 with language(locale): try: - 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, - ) + if all(k is not None for k in [media_type, media_identifier, media_policy, media_action]) and not media: + with transaction.atomic(): + media = perform_media_exchange( + organizer=request.organizer, + media_type=media_type, + media_identifier=media_identifier, + media_policy=media_policy, + media_action=media_action, + op=op, + ) + source_type = media.media_type.identifier + + 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, + ) + 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: return Response({ 'status': 'incomplete', @@ -843,6 +878,17 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force, 'media_policy': e.media_policy, 'media_type': e.media_type, '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) except CheckInError as e: if not simulate: @@ -1031,6 +1077,10 @@ class CheckinRPCRedeemView(views.APIView): canceled_supported=True, request=self.request, # this is not clean, but we need it in the serializers for URL generation 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'), ) diff --git a/src/pretix/api/views/media.py b/src/pretix/api/views/media.py index 7b1509128d..5c20079303 100644 --- a/src/pretix/api/views/media.py +++ b/src/pretix/api/views/media.py @@ -196,7 +196,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet): 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): date = serializers.DateTimeField().to_representation(now()) queryset = self.filter_queryset(self.get_queryset()) diff --git a/src/pretix/base/models/media.py b/src/pretix/base/models/media.py index eabefae59f..88698f855e 100644 --- a/src/pretix/base/models/media.py +++ b/src/pretix/base/models/media.py @@ -138,6 +138,9 @@ class ReusableMedium(LoggedModel): ] ordering = "identifier", "type", "organizer" + class DuplicateEntry(Exception): + pass + class MediumKeySet(models.Model): organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE, related_name='medium_key_sets') diff --git a/src/pretix/base/services/media.py b/src/pretix/base/services/media.py index c431e79ae3..6547963e30 100644 --- a/src/pretix/base/services/media.py +++ b/src/pretix/base/services/media.py @@ -25,8 +25,8 @@ from django.db import IntegrityError from django.db.models import Q from django_scopes import scopes_disabled -from pretix.base.models import GiftCardAcceptance -from pretix.base.models.media import MediumKeySet +from pretix.base.models import GiftCardAcceptance, Item +from pretix.base.models.media import MediumKeySet, ReusableMedium def create_nfc_mf0aes_keyset(organizer): @@ -70,3 +70,36 @@ def get_keysets_for_organizer(organizer): if new_set: sets.append(new_set) 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