mirror of
https://github.com/pretix/pretix.git
synced 2026-08-18 12:06:26 +00:00
API: Allow to simulate check-ins (#6360)
* API: Allow to simulate check-ins * Add missing file
This commit is contained in:
@@ -71,6 +71,8 @@ Checking a ticket in
|
|||||||
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
||||||
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
|
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
|
||||||
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
|
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
|
||||||
|
:>json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
|
||||||
|
object will not reflect the simulated changes.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
|
|||||||
answers = serializers.JSONField(required=False, allow_null=True)
|
answers = serializers.JSONField(required=False, allow_null=True)
|
||||||
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
|
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
|
||||||
exchange_medium_identifier = serializers.CharField(required=False)
|
exchange_medium_identifier = serializers.CharField(required=False)
|
||||||
|
simulate = serializers.BooleanField(default=False, required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -839,6 +839,11 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
)
|
)
|
||||||
|
|
||||||
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
|
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
|
||||||
|
if simulate:
|
||||||
|
raise CheckInError(
|
||||||
|
gettext('You cannot simulate a medium exchange.'),
|
||||||
|
'error'
|
||||||
|
)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Do exchange and check-in atomically, i.e. both succeed or both fail
|
# Do exchange and check-in atomically, i.e. both succeed or both fail
|
||||||
medium = perform_media_exchange(
|
medium = perform_media_exchange(
|
||||||
@@ -1066,6 +1071,7 @@ class CheckinRPCRedeemView(views.APIView):
|
|||||||
legacy_url_support=False,
|
legacy_url_support=False,
|
||||||
exchange_medium_type=s.validated_data.get('exchange_medium_type'),
|
exchange_medium_type=s.validated_data.get('exchange_medium_type'),
|
||||||
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
|
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
|
||||||
|
simulate=s.validated_data.get('simulate'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import dateutil
|
|||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
from dateutil.tz import datetime_exists
|
from dateutil.tz import datetime_exists
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
|
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
|
||||||
OuterRef, Q, Subquery, TextField, Value, When,
|
OuterRef, Q, Subquery, TextField, Value, When,
|
||||||
@@ -59,6 +59,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.signals import checkin_created, periodic_task
|
from pretix.base.signals import checkin_created, periodic_task
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
|
from pretix.helpers.database import conditional_atomic
|
||||||
from pretix.helpers.jsonlogic import Logic
|
from pretix.helpers.jsonlogic import Logic
|
||||||
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
||||||
from pretix.helpers.jsonlogic_query import (
|
from pretix.helpers.jsonlogic_query import (
|
||||||
@@ -1043,10 +1044,10 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
if not simulate:
|
if not simulate:
|
||||||
_save_answers(op, answers, given_answers)
|
_save_answers(op, answers, given_answers)
|
||||||
|
|
||||||
with transaction.atomic():
|
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
|
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
||||||
opqs = OrderPosition.all.select_related("order", "item")
|
opqs = OrderPosition.all.select_related("order", "item")
|
||||||
if type != Checkin.TYPE_EXIT:
|
if type != Checkin.TYPE_EXIT and not simulate:
|
||||||
opqs = opqs.select_for_update(of=OF_SELF)
|
opqs = opqs.select_for_update(of=OF_SELF)
|
||||||
op = opqs.get(pk=op.pk)
|
op = opqs.get(pk=op.pk)
|
||||||
|
|
||||||
|
|||||||
@@ -288,6 +288,15 @@ def get_deterministic_ordering(model, ordering):
|
|||||||
return ordering
|
return ordering
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def conditional_atomic(do_atomic, **kwargs):
|
||||||
|
if do_atomic:
|
||||||
|
with transaction.atomic(**kwargs):
|
||||||
|
yield
|
||||||
|
else:
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
class IgnoreOnSQLiteMixin:
|
class IgnoreOnSQLiteMixin:
|
||||||
# Mixin to allow defining PostgreSQL-specific indexes that will just not be created
|
# Mixin to allow defining PostgreSQL-specific indexes that will just not be created
|
||||||
# on SQLite. SQLite is supported for testing only anyways!
|
# on SQLite. SQLite is supported for testing only anyways!
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
def readonly_db(execute, sql, params, many, context):
|
||||||
|
if not sql.lower().startswith("select"):
|
||||||
|
raise Exception(f"Should not write anything to the database, but detected query: {sql}")
|
||||||
|
return execute(sql, params, many, context)
|
||||||
@@ -25,6 +25,7 @@ from unittest import mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
from django.db import connection
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
@@ -36,6 +37,7 @@ from pretix.api.serializers.item import QuestionSerializer
|
|||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Checkin, InvoiceAddress, Item, Order, OrderPosition, ReusableMedium,
|
Checkin, InvoiceAddress, Item, Order, OrderPosition, ReusableMedium,
|
||||||
)
|
)
|
||||||
|
from pretix.testutils.db import readonly_db
|
||||||
|
|
||||||
# Lots of this code is overlapping with test_checkin.py, and some of it is arguably redundant since it's triggering
|
# Lots of this code is overlapping with test_checkin.py, and some of it is arguably redundant since it's triggering
|
||||||
# the same backend code paths (for now). However, this is SUCH a critical part of pretix that we don't want to take
|
# the same backend code paths (for now). However, this is SUCH a critical part of pretix that we don't want to take
|
||||||
@@ -1739,3 +1741,41 @@ def test_exchange_create_gift_card(token_client, organizer, clist, event, order,
|
|||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
rm = ReusableMedium.objects.get(identifier="0412345")
|
rm = ReusableMedium.objects.get(identifier="0412345")
|
||||||
assert rm.linked_giftcard.currency == "EUR"
|
assert rm.linked_giftcard.currency == "EUR"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_simulate(token_client, organizer, clist, event, order):
|
||||||
|
with scopes_disabled():
|
||||||
|
p = order.positions.first()
|
||||||
|
with connection.execute_wrapper(readonly_db):
|
||||||
|
resp = _redeem(token_client, organizer, clist, p.secret, {"simulate": True})
|
||||||
|
assert resp.status_code == 201
|
||||||
|
assert resp.data['status'] == 'ok'
|
||||||
|
with scopes_disabled():
|
||||||
|
assert not p.checkins.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_simulate_no_exchange(token_client, organizer, clist, event, order, item):
|
||||||
|
organizer.settings.reusable_media_type_nfc_uid = True
|
||||||
|
item.media_type = "nfc_uid"
|
||||||
|
item.media_policy = Item.MEDIA_POLICY_NEW
|
||||||
|
item.save()
|
||||||
|
with scopes_disabled():
|
||||||
|
rm = ReusableMedium.objects.create(
|
||||||
|
type="nfc_uid",
|
||||||
|
identifier="12345678",
|
||||||
|
organizer=organizer,
|
||||||
|
)
|
||||||
|
with connection.execute_wrapper(readonly_db):
|
||||||
|
resp = _redeem(token_client, organizer, clist, "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", {
|
||||||
|
"source_type": "barcode",
|
||||||
|
"exchange_medium_type": "nfc_uid",
|
||||||
|
"exchange_medium_identifier": "12345678",
|
||||||
|
"simulate": True,
|
||||||
|
})
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.data['status'] == 'error'
|
||||||
|
assert resp.data['reason'] == 'error'
|
||||||
|
with scopes_disabled():
|
||||||
|
assert not rm.linked_orderpositions.exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user