diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 63f538e20d..43444369f5 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -496,7 +496,7 @@ class Order(LoggedModel): def send_mail(self, subject: str, template: Union[str, LazyI18nString], context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent', user: User=None, headers: dict=None, sender: str=None, invoices: list=None, - auth=None): + auth=None, attach_tickets=False): """ Sends an email to the user that placed this order. Basically, this method does two things: @@ -512,6 +512,7 @@ class Order(LoggedModel): :param user: Administrative user who triggered this mail to be sent :param headers: Dictionary with additional mail headers :param sender: Custom email sender. + :param attach_tickets: Attach tickets of this order, if they are existing and ready to download """ from pretix.base.services.mail import SendMailException, mail, render_mail @@ -525,7 +526,7 @@ class Order(LoggedModel): mail( recipient, subject, template, context, self.event, self.locale, self, headers, sender, - invoices=invoices + invoices=invoices, attach_tickets=attach_tickets ) except SendMailException: raise @@ -991,7 +992,8 @@ class OrderPayment(models.Model): self.order.send_mail( email_subject, email_template, email_context, 'pretix.event.order.email.order_paid', user, - invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [] + invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [], + attach_tickets=True ) except SendMailException: logger.exception('Order paid email could not be sent') diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 7caf159493..891d4aab6f 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -14,6 +14,7 @@ from pretix.base.email import ClassicMailRenderer from pretix.base.i18n import language from pretix.base.models import Event, Invoice, InvoiceAddress, Order from pretix.base.services.invoices import invoice_pdf_task +from pretix.base.services.tickets import get_tickets_for_order from pretix.base.signals import email_filter from pretix.celery_app import app from pretix.multidomain.urlreverse import build_absolute_uri @@ -35,7 +36,8 @@ class SendMailException(Exception): def mail(email: str, subject: str, template: Union[str, LazyI18nString], context: Dict[str, Any]=None, event: Event=None, locale: str=None, - order: Order=None, headers: dict=None, sender: str=None, invoices: list=None): + order: Order=None, headers: dict=None, sender: str=None, invoices: list=None, + attach_tickets=False): """ Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation. @@ -65,6 +67,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString], :param invoices: A list of invoices to attach to this email. + :param attach_tickets: Whether to attach tickets to this email, if they are available to download. + :raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean that the email has been sent, just that it has been queued by the email backend. """ @@ -153,7 +157,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString], event=event.id if event else None, headers=headers, invoices=[i.pk for i in invoices] if invoices else [], - order=order.pk if order else None + order=order.pk if order else None, + attach_tickets=attach_tickets ) if invoices: @@ -168,7 +173,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString], @app.task def mail_send_task(*args, to: List[str], subject: str, body: str, html: str, sender: str, event: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None, - order: int=None) -> bool: + order: int=None, attach_tickets=False) -> bool: email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers) if html is not None: email.attach_alternative(html, "text/html") @@ -185,6 +190,7 @@ def mail_send_task(*args, to: List[str], subject: str, body: str, html: str, sen except: logger.exception('Could not attach invoice to email') pass + if event: event = Event.objects.get(id=event) backend = event.get_mail_backend() @@ -197,6 +203,15 @@ def mail_send_task(*args, to: List[str], subject: str, body: str, html: str, sen order = event.orders.get(pk=order) except Order.DoesNotExist: order = None + else: + if attach_tickets: + for name, ct in get_tickets_for_order(order): + email.attach( + name, + ct.file.read(), + ct.type + ) + email = email_filter.send_chained(event, 'message', message=email, order=order) try: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index effa207267..0b77d5165c 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -633,7 +633,8 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str], order.send_mail( email_subject, email_template, email_context, log_entry, - invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [] + invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [], + attach_tickets=(payment_provider == 'free') ) except SendMailException: logger.exception('Order received email could not be sent') @@ -735,7 +736,8 @@ def send_download_reminders(sender, **kwargs): try: o.send_mail( email_subject, email_template, email_context, - 'pretix.event.order.email.download_reminder_sent' + 'pretix.event.order.email.download_reminder_sent', + attach_tickets=True ) except SendMailException: logger.exception('Reminder email could not be sent') diff --git a/src/pretix/base/services/tickets.py b/src/pretix/base/services/tickets.py index d1e443de59..ce06b9644c 100644 --- a/src/pretix/base/services/tickets.py +++ b/src/pretix/base/services/tickets.py @@ -1,5 +1,7 @@ +import logging import os from datetime import timedelta +from decimal import Decimal from django.core.files.base import ContentFile from django.utils.timezone import now @@ -11,10 +13,12 @@ from pretix.base.models import ( OrderPosition, ) from pretix.base.services.tasks import ProfiledTask -from pretix.base.signals import register_ticket_outputs +from pretix.base.signals import allow_ticket_download, register_ticket_outputs from pretix.celery_app import app from pretix.helpers.database import rolledback_transaction +logger = logging.getLogger(__name__) + @app.task(base=ProfiledTask) def generate(order_position: str, provider: str): @@ -97,7 +101,8 @@ def preview(event: int, provider: str): return prov.generate(p) -def get_cachedticket_for_position(pos, identifier): +def get_cachedticket_for_position(pos, identifier, generate_async=True): + apply_method = 'apply_async' if generate_async else 'apply' try: ct = CachedTicket.objects.filter( order_position=pos, provider=identifier @@ -109,15 +114,20 @@ def get_cachedticket_for_position(pos, identifier): ct = CachedTicket.objects.create( order_position=pos, provider=identifier, extension='', type='', file=None) - generate.apply_async(args=(pos.id, identifier)) + getattr(generate, apply_method)(args=(pos.id, identifier)) + if not generate_async: + ct.refresh_from_db() if not ct.file: if now() - ct.created > timedelta(minutes=5): - generate.apply_async(args=(pos.id, identifier)) + getattr(generate, apply_method)(args=(pos.id, identifier)) + if not generate_async: + ct.refresh_from_db() return ct -def get_cachedticket_for_order(order, identifier): +def get_cachedticket_for_order(order, identifier, generate_async=True): + apply_method = 'apply_async' if generate_async else 'apply' try: ct = CachedCombinedTicket.objects.filter( order=order, provider=identifier @@ -129,9 +139,67 @@ def get_cachedticket_for_order(order, identifier): ct = CachedCombinedTicket.objects.create( order=order, provider=identifier, extension='', type='', file=None) - generate_order.apply_async(args=(order.id, identifier)) + getattr(generate_order, apply_method)(args=(order.id, identifier)) + if not generate_async: + ct.refresh_from_db() if not ct.file: if now() - ct.created > timedelta(minutes=5): - generate_order.apply_async(args=(order.id, identifier)) + getattr(generate_order, apply_method)(args=(order.id, identifier)) + if not generate_async: + ct.refresh_from_db() return ct + + +def get_tickets_for_order(order): + can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)]) + if not can_download: + return [] + if order.status != Order.STATUS_PAID and order.total != Decimal("0.00"): + return [] + if (not order.event.settings.ticket_download + or (order.event.settings.ticket_download_date is not None + and now() < order.ticket_download_date)): + return [] + + providers = [ + response(order.event) + for receiver, response + in register_ticket_outputs.send(order.event) + ] + + tickets = [] + + for p in providers: + if not p.is_enabled: + continue + + if p.multi_download_enabled: + try: + ct = get_cachedticket_for_order(order, p.identifier, generate_async=False) + tickets.append(( + "{}-{}-{}{}".format( + order.event.slug.upper(), order.code, ct.provider, ct.extension, + ), + ct + )) + except: + logger.exception('Failed to generate ticket.') + else: + for pos in order.positions.all(): + if pos.addon_to and not order.event.settings.ticket_download_addons: + continue + if not pos.item.admission and not order.event.settings.ticket_download_nonadm: + continue + try: + ct = get_cachedticket_for_position(pos, p.identifier, generate_async=False) + tickets.append(( + "{}-{}-{}-{}{}".format( + order.event.slug.upper(), order.code, pos.positionid, ct.provider, ct.extension, + ), + ct + )) + except: + logger.exception('Failed to generate ticket.') + + return tickets diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index e7d9800938..a81ef10ebd 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -866,7 +866,8 @@ class OrderResendLink(OrderView): email_subject = _('Your order: %(code)s') % {'code': self.order.code} self.order.send_mail( email_subject, email_template, email_context, - 'pretix.event.order.email.resend', user=self.request.user + 'pretix.event.order.email.resend', user=self.request.user, + attach_tickets=True ) except SendMailException: messages.error(self.request, _('There was an error sending the mail. Please try again later.'))