Add Reusable Media Exchange to Checkin API

This commit is contained in:
Martin Gross
2026-04-27 14:37:51 +02:00
parent 82a14a4f83
commit 0dc95a22df
13 changed files with 103 additions and 6 deletions

View File

@@ -89,7 +89,7 @@ class NfcUidMediaType(BaseMediaType):
icon = 'pretixbase/img/media/nfc_uid.svg'
medium_created_by_server = False
supports_giftcard = True
supports_orderposition = False
supports_orderposition = True
def handle_unknown(self, organizer, identifier, user, auth):
from pretix.base.models import GiftCard, ReusableMedium
@@ -129,7 +129,7 @@ class NfcMf0aesMediaType(BaseMediaType):
icon = 'pretixbase/img/media/nfc_secure.svg'
medium_created_by_server = False
supports_giftcard = True
supports_orderposition = False
supports_orderposition = True
def handle_new(self, organizer, medium, user, auth):
from pretix.base.models import GiftCard

View File

@@ -351,6 +351,7 @@ class Checkin(models.Model):
REASON_UNAPPROVED = 'unapproved'
REASON_INVALID_TIME = 'invalid_time'
REASON_ANNULLED = 'annulled'
REASON_ALREADY_EXCHANGED = 'already_exchanged'
REASONS = (
(REASON_CANCELED, _('Order canceled')),
(REASON_INVALID, _('Unknown ticket')),
@@ -366,6 +367,7 @@ class Checkin(models.Model):
(REASON_UNAPPROVED, _('Order not approved')),
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
(REASON_ANNULLED, _('Check-in annulled')),
(REASON_ALREADY_EXCHANGED, _('Ticket already exchanged')),
)
successful = models.BooleanField(

View File

@@ -867,6 +867,15 @@ class RequiredQuestionsError(Exception):
super().__init__(msg)
class RequiredMediaExchangeError(Exception):
def __init__(self, msg, code, media_policy, media_type):
self.msg = msg
self.code = code
self.media_policy = media_policy
self.media_type = media_type
super().__init__(msg)
def _save_answers(op, answers, given_answers):
def _create_answer(question, answer):
try:
@@ -939,7 +948,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
raw_barcode=None, raw_source_type=None, from_revoked_secret=False, simulate=False,
gate=None):
gate=None, media_exchange_supported=False, reusable_media=None):
"""
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
not valid at this time.
@@ -951,6 +960,8 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
questions are not filled out.
:param ignore_unpaid: When set to True, this will succeed even when the order is unpaid.
:param questions_supported: When set to False, questions are ignored
:param media_exchange_supported: When set to False, media exchanges are ignored and access with un-exchanged media
might be permitted
:param nonce: A random nonce to prevent race conditions.
:param datetime: The datetime of the checkin, defaults to now.
:param simulate: If true, the check-in is not saved.
@@ -1100,6 +1111,34 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'incomplete',
require_answers
)
media_exchange_supported = True
required_media_policy = op.item.media_policy
required_media_type = op.item.media_type
linked_media = op.linked_media
require_media_exchange = required_media_policy and required_media_type and not linked_media.exists()
if require_media_exchange and not force and media_exchange_supported:
raise RequiredMediaExchangeError(
_('You need to exchange your ticket to complete this check-in.'),
'exchange',
required_media_policy,
required_media_type
)
require_reusable_media_usage = required_media_policy and required_media_type and op.organizer.settings.reusable_media_usage_enforced
if require_reusable_media_usage and not force:
if not reusable_media and not linked_media.exists() and media_exchange_supported:
raise RequiredMediaExchangeError(
_('You need to exchange your ticket to complete this check-in.'),
'exchange',
required_media_policy,
required_media_type
)
elif not reusable_media and linked_media.exists():
raise CheckInError(
_('This ticket has already been exchanged - use the reusable media instead.'),
'already_exchanged',
)
device = None
if isinstance(auth, Device):

View File

@@ -217,6 +217,19 @@ DEFAULTS = {
"later.")
)
},
'reusable_media_usage_enforced': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Enforce the usage of issued re-usable media for check-in"),
help_text=_("If enabled, a ticket barcode will not be accepted anymore, if a re-usable media has been "
"created and linked to a ticket. Keeping this option turned off will treat the re-usable "
"medium and ticket as equals."),
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-reusable_media_active'}),
)
},
'reusable_media_type_barcode': {
'default': 'False',
'type': bool,