diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index dc73363301..0f1d6b395e 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -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, + Case, Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, + Subquery, Sum, Value, When, ) -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, @@ -1487,83 +1487,96 @@ 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' + ).annotate( + val=Case( + When(value="None", then=None), + default=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 - - if days is None: + for event in events.iterator(chunk_size=10_000): + event_reminder_date = (event.date_from - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0) + if not event.has_subevents and now() < event_reminder_date: continue - if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder: - continue + qs = event.orders.annotate( + first_date=Min('all_positions__subevent__date_from') + ).filter( + Q(first_date__isnull=True) | Q(first_date__gte=today), + download_reminder_sent=False, + datetime__lte=now() - timedelta(hours=2), + ).only( + 'pk', 'event_id', 'sales_channel', 'datetime', + ).order_by() - 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: + for o in qs: + if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder: continue - if not o.ticket_download_available: + reminder_date = ((o.first_date or event.date_from) - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0) + if now() < reminder_date or o.datetime > reminder_date: 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): + 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=[]): diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index ae679d186c..bdf8813598 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -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,