forked from CGM_Public/pretix_original
Merge branch 'master' into per-payment-abort_pending_payment_allowed
This commit is contained in:
@@ -115,7 +115,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('POST', 'api-v1:checkinrpc.annull'),
|
||||
('POST', 'api-v1:checkinrpc.annul'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
('GET', 'api-v1:reusablemedium-list'),
|
||||
('POST', 'api-v1:reusablemedium-lookup'),
|
||||
@@ -154,7 +154,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('POST', 'api-v1:checkinrpc.annull'),
|
||||
('POST', 'api-v1:checkinrpc.annul'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
)
|
||||
|
||||
@@ -191,7 +191,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('POST', 'api-v1:checkinrpc.annull'),
|
||||
('POST', 'api-v1:checkinrpc.annul'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
)
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
|
||||
answers = serializers.JSONField(required=False, allow_null=True)
|
||||
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
|
||||
exchange_medium_identifier = serializers.CharField(required=False)
|
||||
simulate = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
def __init__(self, *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 simulate:
|
||||
raise CheckInError(
|
||||
gettext('You cannot simulate a medium exchange.'),
|
||||
'error'
|
||||
)
|
||||
with transaction.atomic():
|
||||
# Do exchange and check-in atomically, i.e. both succeed or both fail
|
||||
medium = perform_media_exchange(
|
||||
@@ -1066,6 +1071,7 @@ class CheckinRPCRedeemView(views.APIView):
|
||||
legacy_url_support=False,
|
||||
exchange_medium_type=s.validated_data.get('exchange_medium_type'),
|
||||
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
|
||||
from dateutil.tz import datetime_exists
|
||||
from django.core.files import File
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import (
|
||||
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
|
||||
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.helpers import OF_SELF
|
||||
from pretix.helpers.database import conditional_atomic
|
||||
from pretix.helpers.jsonlogic import Logic
|
||||
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
||||
from pretix.helpers.jsonlogic_query import (
|
||||
@@ -1043,10 +1044,10 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
if not simulate:
|
||||
_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
|
||||
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)
|
||||
op = opqs.get(pk=op.pk)
|
||||
|
||||
|
||||
@@ -48,12 +48,12 @@ from celery.exceptions import MaxRetriesExceededError
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db import models, transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Sum,
|
||||
Value,
|
||||
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Subquery,
|
||||
Sum, Value,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.db.models.functions import Cast, Greatest
|
||||
from django.db.transaction import get_connection
|
||||
from django.dispatch import receiver
|
||||
from django.utils.functional import cached_property
|
||||
@@ -71,7 +71,7 @@ from pretix.base.models import (
|
||||
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
|
||||
SeatCategoryMapping, User, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.event import Event_SettingsStore, SubEvent
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
|
||||
generate_secret,
|
||||
@@ -1494,83 +1494,104 @@ def send_expiry_warnings(sender, **kwargs):
|
||||
@scopes_disabled()
|
||||
def send_download_reminders(sender, **kwargs):
|
||||
today = now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
qs = Order.objects.annotate(
|
||||
first_date=Coalesce(
|
||||
Min('all_positions__subevent__date_from'),
|
||||
F('event__date_from')
|
||||
|
||||
events = Event.objects.filter(
|
||||
Q(has_subevents=False, date_from__gte=now()) |
|
||||
(Q(has_subevents=True) & Q(Exists(
|
||||
SubEvent.objects.filter(event_id=OuterRef('id'), date_from__gte=now())
|
||||
)))
|
||||
).annotate(
|
||||
reminder_days=Subquery(
|
||||
Event_SettingsStore.objects.filter(
|
||||
object=OuterRef('id'),
|
||||
key='mail_days_download_reminder'
|
||||
).exclude(
|
||||
value="None"
|
||||
).annotate(
|
||||
val=Cast(F("value"), output_field=models.IntegerField()),
|
||||
).values("val")
|
||||
)
|
||||
).filter(
|
||||
download_reminder_sent=False,
|
||||
datetime__lte=now() - timedelta(hours=2),
|
||||
first_date__gte=today,
|
||||
).only(
|
||||
'pk', 'event_id', 'sales_channel', 'datetime',
|
||||
).order_by('event_id')
|
||||
event_id = None
|
||||
days = None
|
||||
event = None
|
||||
reminder_days__isnull=False,
|
||||
).order_by()
|
||||
|
||||
for o in qs:
|
||||
if o.event_id != event_id:
|
||||
days = o.event.settings.get('mail_days_download_reminder', as_type=int)
|
||||
event = o.event
|
||||
event_id = o.event_id
|
||||
for event in events.iterator(chunk_size=10_000):
|
||||
qs = event.orders.filter(
|
||||
download_reminder_sent=False,
|
||||
datetime__lte=now() - timedelta(hours=2),
|
||||
)
|
||||
|
||||
if days is None:
|
||||
continue
|
||||
|
||||
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
|
||||
continue
|
||||
|
||||
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if now() < reminder_date or o.datetime > reminder_date:
|
||||
continue
|
||||
|
||||
with transaction.atomic():
|
||||
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
|
||||
if o.download_reminder_sent:
|
||||
# Race condition
|
||||
continue
|
||||
positions = list(o.positions_with_tickets)
|
||||
if not positions:
|
||||
if event.has_subevents:
|
||||
qs = qs.annotate(
|
||||
first_date=Min('all_positions__subevent__date_from')
|
||||
).filter(
|
||||
Q(first_date__gte=today)
|
||||
)
|
||||
else:
|
||||
event_reminder_date = (event.date_from - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if now() < event_reminder_date:
|
||||
continue
|
||||
|
||||
if not o.ticket_download_available:
|
||||
qs = qs.only(
|
||||
'pk', 'event_id', 'sales_channel', 'datetime',
|
||||
).order_by()
|
||||
|
||||
for o in qs:
|
||||
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
|
||||
continue
|
||||
|
||||
if o.status != Order.STATUS_PAID:
|
||||
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
|
||||
if event.has_subevents:
|
||||
reminder_date = ((o.first_date or event.date_from) - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
else:
|
||||
reminder_date = event_reminder_date
|
||||
if now() < reminder_date or o.datetime > reminder_date:
|
||||
continue
|
||||
|
||||
with transaction.atomic():
|
||||
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
|
||||
if o.download_reminder_sent:
|
||||
# Race condition
|
||||
continue
|
||||
positions = list(o.positions_with_tickets)
|
||||
if not positions:
|
||||
continue
|
||||
|
||||
with language(o.locale, o.event.settings.region):
|
||||
o.download_reminder_sent = True
|
||||
o.save(update_fields=['download_reminder_sent'])
|
||||
email_template = event.settings.mail_text_download_reminder
|
||||
email_subject = event.settings.mail_subject_download_reminder
|
||||
email_context = get_email_context(event=event, order=o)
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True
|
||||
)
|
||||
if not o.ticket_download_available:
|
||||
continue
|
||||
|
||||
if event.settings.mail_send_download_reminder_attendee:
|
||||
for p in positions:
|
||||
if p.subevent_id:
|
||||
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
if now() < reminder_date:
|
||||
continue
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
|
||||
email_template = event.settings.mail_text_download_reminder_attendee
|
||||
email_subject = event.settings.mail_subject_download_reminder_attendee
|
||||
email_context = get_email_context(event=event, order=o, position=p)
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True, position=p
|
||||
)
|
||||
if o.status != Order.STATUS_PAID:
|
||||
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
|
||||
continue
|
||||
|
||||
with language(o.locale, o.event.settings.region):
|
||||
o.download_reminder_sent = True
|
||||
o.save(update_fields=['download_reminder_sent'])
|
||||
email_template = event.settings.mail_text_download_reminder
|
||||
email_subject = event.settings.mail_subject_download_reminder
|
||||
email_context = get_email_context(event=event, order=o)
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True
|
||||
)
|
||||
|
||||
if event.settings.mail_send_download_reminder_attendee:
|
||||
for p in positions:
|
||||
if p.subevent_id:
|
||||
reminder_date = (p.subevent.date_from - timedelta(days=event.reminder_days)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
if now() < reminder_date:
|
||||
continue
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
|
||||
email_template = event.settings.mail_text_download_reminder_attendee
|
||||
email_subject = event.settings.mail_subject_download_reminder_attendee
|
||||
email_context = get_email_context(event=event, order=o, position=p)
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True, position=p
|
||||
)
|
||||
|
||||
|
||||
def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
|
||||
|
||||
@@ -2930,7 +2930,7 @@ Your {event} team""")) # noqa: W291
|
||||
},
|
||||
'mail_days_download_reminder': {
|
||||
'type': int,
|
||||
'default': None
|
||||
'default': None # when this default is changed, send_download_reminders needs to change
|
||||
},
|
||||
'mail_send_download_reminder_attendee': {
|
||||
'type': bool,
|
||||
|
||||
@@ -288,6 +288,15 @@ def get_deterministic_ordering(model, ordering):
|
||||
return ordering
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def conditional_atomic(do_atomic, **kwargs):
|
||||
if do_atomic:
|
||||
with transaction.atomic(**kwargs):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
class IgnoreOnSQLiteMixin:
|
||||
# Mixin to allow defining PostgreSQL-specific indexes that will just not be created
|
||||
# on SQLite. SQLite is supported for testing only anyways!
|
||||
|
||||
@@ -645,7 +645,7 @@ class PaypalMethod(BasePaymentProvider):
|
||||
def _execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
||||
payment = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=payment.pk)
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||
# payment is already confirmed; possible return-view/webhook race-condition
|
||||
logger.warning('payment is already confirmed; possible return-view/webhook race-condition')
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user