mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
@@ -59,7 +59,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, RevokedTicketSecret, TeamAPIToken,
|
||||
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.services.checkin import (
|
||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||
@@ -396,7 +396,7 @@ 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,
|
||||
legacy_url_support=False):
|
||||
source_type='barcode', legacy_url_support=False):
|
||||
if not checkinlists:
|
||||
raise ValidationError('No check-in list passed.')
|
||||
|
||||
@@ -422,6 +422,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
|
||||
common_checkin_args = dict(
|
||||
raw_barcode=raw_barcode,
|
||||
raw_source_type=source_type,
|
||||
type=checkin_type,
|
||||
list=checkinlists[0],
|
||||
datetime=datetime,
|
||||
@@ -433,7 +434,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
raw_barcode_for_checkin = None
|
||||
from_revoked_secret = False
|
||||
|
||||
# 1. Gather a list of positions that could be the one we looking fore, either from their ID, secret or
|
||||
# 1. Gather a list of positions that could be the one we looking for, either from their ID, secret or
|
||||
# parent secret
|
||||
queryset = _checkin_list_position_queryset(checkinlists, pdf_data=pdf_data, ignore_status=True, ignore_products=True).order_by(
|
||||
F('addon_to').asc(nulls_first=True)
|
||||
@@ -457,98 +458,111 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
|
||||
# 2. Handle the "nothing found" case: Either it's really a bogus secret that we don't know (-> error), or it
|
||||
# might be a revoked one that we actually know (-> error, but with better error message and logging and
|
||||
# with respecting the force option).
|
||||
# with respecting the force option), or it's a reusable medium (-> proceed with that)
|
||||
if not op_candidates:
|
||||
revoked_matches = list(RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
|
||||
if len(revoked_matches) == 0:
|
||||
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': checkinlists[0].pk,
|
||||
'barcode': raw_barcode,
|
||||
'searched_lists': [cl.pk for cl in checkinlists]
|
||||
}, user=user, auth=auth)
|
||||
try:
|
||||
media = ReusableMedium.objects.select_related('linked_orderposition').active().get(
|
||||
organizer_id=checkinlists[0].event.organizer_id,
|
||||
type=source_type,
|
||||
identifier=raw_barcode,
|
||||
linked_orderposition__isnull=False,
|
||||
)
|
||||
raw_barcode_for_checkin = raw_barcode
|
||||
except ReusableMedium.DoesNotExist:
|
||||
revoked_matches = list(
|
||||
RevokedTicketSecret.objects.filter(event_id__in=list_by_event.keys(), secret=raw_barcode))
|
||||
if len(revoked_matches) == 0:
|
||||
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': checkinlists[0].pk,
|
||||
'barcode': raw_barcode,
|
||||
'searched_lists': [cl.pk for cl in checkinlists]
|
||||
}, user=user, auth=auth)
|
||||
|
||||
for cl in checkinlists:
|
||||
for k, s in cl.event.ticket_secret_generators.items():
|
||||
for cl in checkinlists:
|
||||
for k, s in cl.event.ticket_secret_generators.items():
|
||||
try:
|
||||
parsed = s.parse_secret(raw_barcode)
|
||||
common_checkin_args.update({
|
||||
'raw_item': parsed.item,
|
||||
'raw_variation': parsed.variation,
|
||||
'raw_subevent': parsed.subevent,
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
Checkin.objects.create(
|
||||
position=None,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_INVALID,
|
||||
**common_checkin_args,
|
||||
)
|
||||
|
||||
if force and legacy_url_support and isinstance(auth, Device):
|
||||
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
|
||||
# valid at the time but no longer exists at time of upload, the device would retry to
|
||||
# upload the same scan over and over again. Since we can't update all devices quickly,
|
||||
# here's a dirty workaround to make it stop.
|
||||
try:
|
||||
parsed = s.parse_secret(raw_barcode)
|
||||
common_checkin_args.update({
|
||||
'raw_item': parsed.item,
|
||||
'raw_variation': parsed.variation,
|
||||
'raw_subevent': parsed.subevent,
|
||||
})
|
||||
except:
|
||||
brand = auth.software_brand
|
||||
ver = parse(auth.software_version)
|
||||
legacy_mode = (
|
||||
(brand == 'pretixSCANPROXY' and ver < parse('0.0.3')) or
|
||||
(brand == 'pretixSCAN Android' and ver < parse('1.11.2')) or
|
||||
(brand == 'pretixSCAN' and ver < parse('1.11.2'))
|
||||
)
|
||||
if legacy_mode:
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_ALREADY_REDEEMED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
|
||||
}, status=400)
|
||||
except: # we don't care e.g. about invalid version numbers
|
||||
pass
|
||||
|
||||
Checkin.objects.create(
|
||||
position=None,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_INVALID,
|
||||
**common_checkin_args,
|
||||
)
|
||||
|
||||
if force and legacy_url_support and isinstance(auth, Device):
|
||||
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
|
||||
# valid at the time but no longer exists at time of upload, the device would retry to
|
||||
# upload the same scan over and over again. Since we can't update all devices quickly,
|
||||
# here's a dirty workaround to make it stop.
|
||||
try:
|
||||
brand = auth.software_brand
|
||||
ver = parse(auth.software_version)
|
||||
legacy_mode = (
|
||||
(brand == 'pretixSCANPROXY' and ver < parse('0.0.3')) or
|
||||
(brand == 'pretixSCAN Android' and ver < parse('1.11.2')) or
|
||||
(brand == 'pretixSCAN' and ver < parse('1.11.2'))
|
||||
)
|
||||
if legacy_mode:
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_ALREADY_REDEEMED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
|
||||
}, status=400)
|
||||
except: # we don't care e.g. about invalid version numbers
|
||||
pass
|
||||
|
||||
return Response({
|
||||
'detail': 'Not found.', # for backwards compatibility
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_INVALID,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'list': MiniCheckinListSerializer(checkinlists[0]).data,
|
||||
}, status=404)
|
||||
elif revoked_matches and force:
|
||||
op_candidates = [revoked_matches[0].position]
|
||||
if list_by_event[revoked_matches[0].event_id].addon_match:
|
||||
op_candidates += list(revoked_matches[0].position.addons.all())
|
||||
raw_barcode_for_checkin = raw_barcode
|
||||
from_revoked_secret = True
|
||||
return Response({
|
||||
'detail': 'Not found.', # for backwards compatibility
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_INVALID,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'list': MiniCheckinListSerializer(checkinlists[0]).data,
|
||||
}, status=404)
|
||||
elif revoked_matches and force:
|
||||
op_candidates = [revoked_matches[0].position]
|
||||
if list_by_event[revoked_matches[0].event_id].addon_match:
|
||||
op_candidates += list(revoked_matches[0].position.addons.all())
|
||||
raw_barcode_for_checkin = raw_barcode_for_checkin or raw_barcode
|
||||
from_revoked_secret = True
|
||||
else:
|
||||
op = revoked_matches[0].position
|
||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': list_by_event[revoked_matches[0].event_id].pk,
|
||||
'barcode': raw_barcode
|
||||
}, user=user, auth=auth)
|
||||
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_REVOKED,
|
||||
**common_checkin_args
|
||||
)
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_REVOKED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, revoked_matches[
|
||||
0].event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||
}, status=400)
|
||||
else:
|
||||
op = revoked_matches[0].position
|
||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||
'datetime': datetime,
|
||||
'type': checkin_type,
|
||||
'list': list_by_event[revoked_matches[0].event_id].pk,
|
||||
'barcode': raw_barcode
|
||||
}, user=user, auth=auth)
|
||||
common_checkin_args['list'] = list_by_event[revoked_matches[0].event_id]
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_REVOKED,
|
||||
**common_checkin_args
|
||||
)
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_REVOKED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, revoked_matches[0].event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||
}, status=400)
|
||||
op_candidates = [media.linked_orderposition] + list(media.linked_orderposition.addons.all())
|
||||
|
||||
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
|
||||
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
|
||||
@@ -634,6 +648,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
auth=auth,
|
||||
type=checkin_type,
|
||||
raw_barcode=raw_barcode_for_checkin,
|
||||
raw_source_type=source_type,
|
||||
from_revoked_secret=from_revoked_secret,
|
||||
)
|
||||
except RequiredQuestionsError as e:
|
||||
@@ -812,6 +827,7 @@ class CheckinRPCRedeemView(views.APIView):
|
||||
return _redeem_process(
|
||||
checkinlists=s.validated_data['lists'],
|
||||
raw_barcode=s.validated_data['secret'],
|
||||
source_type=s.validated_data['source_type'],
|
||||
answers_data=s.validated_data.get('answers'),
|
||||
datetime=s.validated_data.get('datetime') or now(),
|
||||
force=s.validated_data['force'],
|
||||
|
||||
@@ -542,7 +542,8 @@ class EventSettingsView(views.APIView):
|
||||
fname: {
|
||||
'value': s.data[fname],
|
||||
'label': getattr(field, '_label', fname),
|
||||
'help_text': getattr(field, '_help_text', None)
|
||||
'help_text': getattr(field, '_help_text', None),
|
||||
'readonly': fname in s.readonly_fields,
|
||||
} for fname, field in s.fields.items()
|
||||
})
|
||||
return Response(s.data)
|
||||
|
||||
160
src/pretix/api/views/media.py
Normal file
160
src/pretix/api/views/media.py
Normal file
@@ -0,0 +1,160 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io 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/>.
|
||||
#
|
||||
from decimal import Decimal
|
||||
|
||||
import django_filters
|
||||
from django.db import transaction
|
||||
from django.db.models import OuterRef, Prefetch, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets, serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import MethodNotAllowed
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.serializers.media import (
|
||||
MediaLookupInputSerializer, ReusableMediaSerializer,
|
||||
)
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import (
|
||||
Checkin, GiftCard, GiftCardTransaction, OrderPosition, ReusableMedium,
|
||||
)
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
with scopes_disabled():
|
||||
class ReusableMediumFilter(FilterSet):
|
||||
identifier = django_filters.CharFilter(field_name='identifier')
|
||||
type = django_filters.CharFilter(field_name='type')
|
||||
customer = django_filters.CharFilter(field_name='customer__identifier')
|
||||
updated_since = django_filters.IsoDateTimeFilter(field_name='updated', lookup_expr='gte')
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
|
||||
|
||||
class Meta:
|
||||
model = ReusableMedium
|
||||
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderposition', 'linked_giftcard']
|
||||
|
||||
|
||||
class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ReusableMediaSerializer
|
||||
queryset = ReusableMedium.objects.none()
|
||||
permission = 'can_manage_reusable_media'
|
||||
write_permission = 'can_manage_reusable_media'
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('-updated', '-id')
|
||||
ordering_fields = ('created', 'updated', 'identifier', 'type', 'id')
|
||||
filterset_class = ReusableMediumFilter
|
||||
|
||||
def get_queryset(self):
|
||||
s = GiftCardTransaction.objects.filter(
|
||||
card=OuterRef('pk')
|
||||
).order_by().values('card').annotate(s=Sum('value')).values('s')
|
||||
return self.request.organizer.reusable_media.prefetch_related(
|
||||
Prefetch(
|
||||
'linked_orderposition',
|
||||
queryset=OrderPosition.objects.select_related(
|
||||
'order', 'order__event', 'order__event__organizer', 'seat',
|
||||
).prefetch_related(
|
||||
Prefetch('checkins', queryset=Checkin.objects.all()),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
)
|
||||
),
|
||||
Prefetch(
|
||||
'linked_giftcard',
|
||||
queryset=GiftCard.objects.annotate(
|
||||
cached_value=Coalesce(Subquery(s), Decimal('0.00'))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
inst.log_action(
|
||||
'pretix.reusable_medium.created',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
ReusableMedium.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
|
||||
inst = serializer.save(identifier=serializer.instance.identifier, type=serializer.instance.type)
|
||||
inst.log_action(
|
||||
'pretix.reusable_medium.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
raise MethodNotAllowed("Media cannot be deleted.")
|
||||
|
||||
@action(methods=["POST"], detail=False)
|
||||
def lookup(self, request, *args, **kwargs):
|
||||
s = MediaLookupInputSerializer(
|
||||
data=request.data,
|
||||
)
|
||||
s.is_valid(raise_exception=True)
|
||||
|
||||
try:
|
||||
m = ReusableMedium.objects.get(
|
||||
type=s.validated_data["type"],
|
||||
identifier=s.validated_data["identifier"],
|
||||
organizer=request.organizer,
|
||||
)
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
except ReusableMedium.DoesNotExist:
|
||||
mt = MEDIA_TYPES.get(s.validated_data["type"])
|
||||
if mt:
|
||||
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
|
||||
if m:
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
|
||||
return Response({"result": None})
|
||||
|
||||
@scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce
|
||||
def list(self, request, **kwargs):
|
||||
date = serializers.DateTimeField().to_representation(now())
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
resp = self.get_paginated_response(serializer.data)
|
||||
resp['X-Page-Generated'] = date
|
||||
return resp
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data, headers={'X-Page-Generated': date})
|
||||
@@ -244,7 +244,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related(
|
||||
Prefetch('meta_values', to_attr='meta_values_cached', queryset=SubEventMetaValue.objects.select_related('property'))
|
||||
)),
|
||||
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
|
||||
Prefetch('addons', opq.select_related('item', 'variation', 'seat')),
|
||||
'linked_media',
|
||||
).select_related('seat', 'addon_to', 'addon_to__seat')
|
||||
)
|
||||
else:
|
||||
@@ -639,13 +640,11 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
raise ValidationError(_('One of the selected products is not available in the selected country.'))
|
||||
send_mail = serializer._send_mail
|
||||
order = serializer.instance
|
||||
|
||||
if not order.pk:
|
||||
# Simulation
|
||||
# Simulation -- exit here
|
||||
serializer = SimulatedOrderSerializer(order, context=serializer.context)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
prefetch_related_objects([order], self._positions_prefetch(request))
|
||||
serializer = OrderSerializer(order, context=serializer.context)
|
||||
|
||||
order.log_action(
|
||||
'pretix.event.order.placed',
|
||||
@@ -679,6 +678,10 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
if gen_invoice:
|
||||
invoice = generate_invoice(order, trigger_pdf=True)
|
||||
|
||||
# Refresh serializer only after running signals
|
||||
prefetch_related_objects([order], self._positions_prefetch(request))
|
||||
serializer = OrderSerializer(order, context=serializer.context)
|
||||
|
||||
if send_mail:
|
||||
free_flow = (
|
||||
payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and
|
||||
@@ -1005,6 +1008,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
Prefetch('meta_values', to_attr='meta_values_cached',
|
||||
queryset=SubEventMetaValue.objects.select_related('property'))
|
||||
)),
|
||||
'linked_media',
|
||||
Prefetch('order', self.request.event.orders.select_related('invoice_address').prefetch_related(
|
||||
Prefetch(
|
||||
'positions',
|
||||
|
||||
@@ -457,7 +457,8 @@ class OrganizerSettingsView(views.APIView):
|
||||
fname: {
|
||||
'value': s.data[fname],
|
||||
'label': getattr(field, '_label', fname),
|
||||
'help_text': getattr(field, '_help_text', None)
|
||||
'help_text': getattr(field, '_help_text', None),
|
||||
'readonly': fname in s.readonly_fields,
|
||||
} for fname, field in s.fields.items()
|
||||
})
|
||||
return Response(s.data)
|
||||
|
||||
Reference in New Issue
Block a user