From 9b1d7cc5228b5faf6af2d9dc7100d6fabdc62cb0 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 10 Jan 2023 12:03:50 +0100 Subject: [PATCH] Sendmail: Abstract away to allow more types of recipients (#2994) Co-authored-by: Richard Schreiber --- src/pretix/base/email.py | 12 +- src/pretix/base/models/waitinglist.py | 82 ++- src/pretix/control/forms/event.py | 4 +- src/pretix/plugins/sendmail/forms.py | 133 +++-- src/pretix/plugins/sendmail/signals.py | 22 +- src/pretix/plugins/sendmail/tasks.py | 34 +- .../pretixplugins/sendmail/history.html | 30 +- .../sendmail/history_fragment_orders.html | 28 + .../history_fragment_waitinglist.html | 9 + .../pretixplugins/sendmail/index.html | 16 + .../pretixplugins/sendmail/send_form.html | 44 +- .../sendmail/send_form_fragment_orders.html | 29 + .../send_form_fragment_waitinglist.html | 8 + src/pretix/plugins/sendmail/urls.py | 6 +- src/pretix/plugins/sendmail/views.py | 513 +++++++++++++----- .../static/pretixcontrol/scss/main.scss | 21 + src/tests/plugins/sendmail/test_sendmail.py | 73 ++- 17 files changed, 780 insertions(+), 284 deletions(-) create mode 100644 src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_orders.html create mode 100644 src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_waitinglist.html create mode 100644 src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/index.html create mode 100644 src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form_fragment_orders.html create mode 100644 src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form_fragment_waitinglist.html diff --git a/src/pretix/base/email.py b/src/pretix/base/email.py index feeded31f..5e2fa5c5e 100644 --- a/src/pretix/base/email.py +++ b/src/pretix/base/email.py @@ -520,20 +520,20 @@ def base_placeholders(sender, **kwargs): lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display() ), SimpleFunctionalMailTextPlaceholder( - 'url_remove', ['waiting_list_entry', 'event'], - lambda waiting_list_entry, event: build_absolute_uri( + 'url_remove', ['waiting_list_voucher', 'event'], + lambda waiting_list_voucher, event: build_absolute_uri( event, 'presale:event.waitinglist.remove' - ) + '?voucher=' + waiting_list_entry.voucher.code, + ) + '?voucher=' + waiting_list_voucher.code, lambda event: build_absolute_uri( event, 'presale:event.waitinglist.remove', ) + '?voucher=68CYU2H6ZTP3WLK5', ), SimpleFunctionalMailTextPlaceholder( - 'url', ['waiting_list_entry', 'event'], - lambda waiting_list_entry, event: build_absolute_uri( + 'url', ['waiting_list_voucher', 'event'], + lambda waiting_list_voucher, event: build_absolute_uri( event, 'presale:event.redeem' - ) + '?voucher=' + waiting_list_entry.voucher.code, + ) + '?voucher=' + waiting_list_voucher.code, lambda event: build_absolute_uri( event, 'presale:event.redeem', diff --git a/src/pretix/base/models/waitinglist.py b/src/pretix/base/models/waitinglist.py index 5e9002e29..98571d39b 100644 --- a/src/pretix/base/models/waitinglist.py +++ b/src/pretix/base/models/waitinglist.py @@ -20,6 +20,7 @@ # . # from datetime import timedelta +from typing import Any, Dict, Union from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db import models, transaction @@ -27,14 +28,16 @@ from django.db.models import F, Q, Sum from django.utils.timezone import now from django.utils.translation import gettext_lazy as _, pgettext_lazy from django_scopes import ScopedManager +from i18nfield.strings import LazyI18nString from phonenumber_field.modelfields import PhoneNumberField from pretix.base.email import get_email_context from pretix.base.i18n import language -from pretix.base.models import Voucher -from pretix.base.services.mail import mail +from pretix.base.models import User, Voucher +from pretix.base.services.mail import SendMailException, mail, render_mail from pretix.base.settings import PERSON_NAME_SCHEMES +from ...helpers.format import format_map from .base import LoggedModel from .event import Event, SubEvent from .items import Item, ItemVariation @@ -213,15 +216,74 @@ class WaitingListEntry(LoggedModel): self.voucher = v self.save() + self.send_mail( + self.event.settings.mail_subject_waiting_list, + self.event.settings.mail_text_waiting_list, + get_email_context( + event=self.event, + waiting_list_entry=self, + waiting_list_voucher=v, + event_or_subevent=self.subevent or self.event, + ), + user=user, + auth=auth, + ) + + def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString], + context: Dict[str, Any]=None, log_entry_type: str='pretix.waitinglist.email.sent', + user: User=None, headers: dict=None, sender: str=None, auth=None, auto_email=True, + attach_other_files: list=None, attach_cached_files: list=None): + """ + Sends an email to the entry's contact address. + + * Call ``pretix.base.services.mail.mail`` with useful values for the ``event``, ``locale``, and ``recipient`` + parameters. + + * Create a ``LogEntry`` with the email contents. + + :param subject: Subject of the email + :param template: LazyI18nString or template filename, see ``pretix.base.services.mail.mail`` for more details + :param context: Dictionary to use for rendering the template + :param log_entry_type: Key to be used for the log entry + :param user: Administrative user who triggered this mail to be sent + :param headers: Dictionary with additional mail headers + :param sender: Custom email sender. + """ + if not self.email: + return + + for k, v in self.event.meta_data.items(): + context['meta_' + k] = v + with language(self.locale, self.event.settings.region): - mail( - self.email, - self.event.settings.mail_subject_waiting_list, - self.event.settings.mail_text_waiting_list, - get_email_context(event=self.event, waiting_list_entry=self), - self.event, - locale=self.locale - ) + recipient = self.email + + try: + email_content = render_mail(template, context) + subject = format_map(subject, context) + mail( + recipient, subject, template, context, + self.event, + self.locale, + headers=headers, + sender=sender, + auto_email=auto_email, + attach_other_files=attach_other_files, + attach_cached_files=attach_cached_files, + ) + except SendMailException: + raise + else: + self.log_action( + log_entry_type, + user=user, + auth=auth, + data={ + 'subject': subject, + 'message': email_content, + 'recipient': recipient, + } + ) @staticmethod def clean_itemvar(event, item, variation): diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 0885e03d6..98843b979 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -1209,8 +1209,8 @@ class MailSettingsForm(SettingsForm): 'mail_text_resend_link': ['event', 'order'], 'mail_subject_resend_link': ['event', 'order'], 'mail_subject_resend_link_attendee': ['event', 'order'], - 'mail_text_waiting_list': ['event', 'waiting_list_entry'], - 'mail_subject_waiting_list': ['event', 'waiting_list_entry'], + 'mail_text_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'], + 'mail_subject_waiting_list': ['event', 'waiting_list_entry', 'waiting_list_voucher'], 'mail_text_resend_all_links': ['event', 'orders'], 'mail_subject_resend_all_links': ['event', 'orders'], 'mail_attach_ical_description': ['event', 'event_or_subevent'], diff --git a/src/pretix/plugins/sendmail/forms.py b/src/pretix/plugins/sendmail/forms.py index 67109e77d..ca23bed4b 100644 --- a/src/pretix/plugins/sendmail/forms.py +++ b/src/pretix/plugins/sendmail/forms.py @@ -70,14 +70,7 @@ class FormPlaceholderMixin: ) -class MailForm(FormPlaceholderMixin, forms.Form): - recipients = forms.ChoiceField( - label=_('Send email to'), - widget=forms.RadioSelect, - initial='orders', - choices=[] - ) - sendto = forms.MultipleChoiceField() # overridden later +class BaseMailForm(FormPlaceholderMixin, forms.Form): subject = forms.CharField(label=_("Subject")) message = forms.CharField(label=_("Message")) attachment = CachedFileField( @@ -91,12 +84,100 @@ class MailForm(FormPlaceholderMixin, forms.Form): help_text=_('Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs ' 'of no more than 2 MB in size.'), max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - ) # TODO i18n + ) + + def __init__(self, *args, **kwargs): + event = self.event = kwargs.pop('event') + context_parameters = kwargs.pop('context_parameters') + super().__init__(*args, **kwargs) + self.fields['subject'] = I18nFormField( + label=_('Subject'), + widget=I18nTextInput, required=True, + locales=event.settings.get('locales'), + ) + self.fields['message'] = I18nFormField( + label=_('Message'), + widget=I18nTextarea, required=True, + locales=event.settings.get('locales'), + ) + self._set_field_placeholders('subject', context_parameters) + self._set_field_placeholders('message', context_parameters) + + +class WaitinglistMailForm(BaseMailForm): items = forms.ModelMultipleChoiceField( widget=forms.CheckboxSelectMultiple( attrs={'class': 'scrolling-multiple-choice'} ), - label=_('Only send to people who bought'), + label=pgettext_lazy('sendmail_form', 'Waiting for'), + required=True, + queryset=Item.objects.none() + ) + subevent = forms.ModelChoiceField( + SubEvent.objects.none(), + label=pgettext_lazy('sendmail_form', 'Restrict to a specific event date'), + required=False, + empty_label=pgettext_lazy('subevent', 'All dates') + ) + subevents_from = forms.SplitDateTimeField( + widget=SplitDateTimePickerWidget(), + label=pgettext_lazy('sendmail_form', 'Restrict to event dates starting at or after'), + required=False, + ) + subevents_to = forms.SplitDateTimeField( + widget=SplitDateTimePickerWidget(), + label=pgettext_lazy('sendmail_form', 'Restrict to event dates starting before'), + required=False, + ) + + def clean(self): + d = super().clean() + if d.get('subevent') and (d.get('subevents_from') or d.get('subevents_to')): + raise ValidationError(pgettext_lazy('subevent', 'Please either select a specific date or a date range, not both.')) + if bool(d.get('subevents_from')) != bool(d.get('subevents_to')): + raise ValidationError(pgettext_lazy('subevent', 'If you set a date range, please set both a start and an end.')) + return d + + def __init__(self, *args, **kwargs): + event = self.event = kwargs['event'] + super().__init__(*args, **kwargs) + + self.fields['items'].queryset = event.items.all() + if not self.initial.get('items'): + self.initial['items'] = event.items.all() + + if event.has_subevents: + self.fields['subevent'].queryset = event.subevents.all() + self.fields['subevent'].widget = Select2( + attrs={ + 'data-model-select2': 'event', + 'data-select2-url': reverse('control:event.subevents.select2', kwargs={ + 'event': event.slug, + 'organizer': event.organizer.slug, + }), + 'data-placeholder': pgettext_lazy('subevent', 'Date') + } + ) + self.fields['subevent'].widget.choices = self.fields['subevent'].choices + else: + del self.fields['subevent'] + del self.fields['subevents_from'] + del self.fields['subevents_to'] + + +class OrderMailForm(BaseMailForm): + recipients = forms.ChoiceField( + label=pgettext_lazy('sendmail_from', 'Send to'), + widget=forms.RadioSelect, + initial='orders', + choices=[] + ) + sendto = forms.MultipleChoiceField() # overridden later + items = forms.ModelMultipleChoiceField( + widget=forms.CheckboxSelectMultiple( + attrs={'class': 'scrolling-multiple-choice'} + ), + label=pgettext_lazy('sendmail_form', 'Restrict to products'), required=True, queryset=Item.objects.none() ) @@ -105,31 +186,31 @@ class MailForm(FormPlaceholderMixin, forms.Form): required=False ) checkin_lists = SafeModelMultipleChoiceField(queryset=CheckinList.objects.none(), required=False) # overridden later - not_checked_in = forms.BooleanField(label=_("Send to customers not checked in"), required=False) + not_checked_in = forms.BooleanField(label=pgettext_lazy('sendmail_from', 'Restrict to recipients without check-in'), required=False) subevent = forms.ModelChoiceField( SubEvent.objects.none(), - label=_('Only send to customers of'), + label=pgettext_lazy('sendmail_form', 'Restrict to a specific event date'), required=False, empty_label=pgettext_lazy('subevent', 'All dates') ) subevents_from = forms.SplitDateTimeField( widget=SplitDateTimePickerWidget(), - label=pgettext_lazy('subevent', 'Only send to customers of dates starting at or after'), + label=pgettext_lazy('sendmail_form', 'Restrict to event dates starting at or after'), required=False, ) subevents_to = forms.SplitDateTimeField( widget=SplitDateTimePickerWidget(), - label=pgettext_lazy('subevent', 'Only send to customers of dates starting before'), + label=pgettext_lazy('sendmail_form', 'Restrict to event dates starting before'), required=False, ) created_from = forms.SplitDateTimeField( widget=SplitDateTimePickerWidget(), - label=pgettext_lazy('subevent', 'Only send to customers with orders created after'), + label=pgettext_lazy('sendmail_form', 'Restrict to orders created at or after'), required=False, ) created_to = forms.SplitDateTimeField( widget=SplitDateTimePickerWidget(), - label=pgettext_lazy('subevent', 'Only send to customers with orders created before'), + label=pgettext_lazy('sendmail_form', 'Restrict to orders created before'), required=False, ) attach_tickets = forms.BooleanField( @@ -147,7 +228,7 @@ class MailForm(FormPlaceholderMixin, forms.Form): return d def __init__(self, *args, **kwargs): - event = self.event = kwargs.pop('event') + event = self.event = kwargs['event'] super().__init__(*args, **kwargs) recp_choices = [ @@ -161,18 +242,6 @@ class MailForm(FormPlaceholderMixin, forms.Form): ] self.fields['recipients'].choices = recp_choices - self.fields['subject'] = I18nFormField( - label=_('Subject'), - widget=I18nTextInput, required=True, - locales=event.settings.get('locales'), - ) - self.fields['message'] = I18nFormField( - label=_('Message'), - widget=I18nTextarea, required=True, - locales=event.settings.get('locales'), - ) - self._set_field_placeholders('subject', ['event', 'order', 'position_or_address']) - self._set_field_placeholders('message', ['event', 'order', 'position_or_address']) choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n'] choices.insert(0, ('na', _('payment pending (except unapproved)'))) choices.insert(0, ('pa', _('approval pending'))) @@ -181,7 +250,7 @@ class MailForm(FormPlaceholderMixin, forms.Form): ('overdue', _('pending with payment overdue')) ) self.fields['sendto'] = forms.MultipleChoiceField( - label=_("Send to customers with order status"), + label=pgettext_lazy('sendmail_from', 'Restrict to orders with status'), widget=forms.CheckboxSelectMultiple( attrs={'class': 'scrolling-multiple-choice no-search'} ), @@ -205,11 +274,11 @@ class MailForm(FormPlaceholderMixin, forms.Form): 'event': event.slug, 'organizer': event.organizer.slug, }), - 'data-placeholder': _('Send to customers checked in on list'), + 'data-placeholder': pgettext_lazy('sendmail_from', 'Restrict to recipients with check-in on list') } ) self.fields['checkin_lists'].widget.choices = self.fields['checkin_lists'].choices - self.fields['checkin_lists'].label = _('Send to customers checked in on list') + self.fields['checkin_lists'].label = pgettext_lazy('sendmail_from', 'Restrict to recipients with check-in on list') if event.has_subevents: self.fields['subevent'].queryset = event.subevents.all() diff --git a/src/pretix/plugins/sendmail/signals.py b/src/pretix/plugins/sendmail/signals.py index 4a5cd674e..6e3c7e2cf 100644 --- a/src/pretix/plugins/sendmail/signals.py +++ b/src/pretix/plugins/sendmail/signals.py @@ -48,10 +48,11 @@ from django_scopes import scope, scopes_disabled from pretix.base.models import SubEvent from pretix.base.signals import ( - event_copy_data, logentry_display, periodic_task, + EventPluginSignal, event_copy_data, logentry_display, periodic_task, ) from pretix.control.signals import nav_event from pretix.plugins.sendmail.models import ScheduledMail +from pretix.plugins.sendmail.views import OrderSendView, WaitinglistSendView logger = logging.getLogger(__name__) @@ -91,7 +92,7 @@ def control_nav_import(sender, request=None, **kwargs): 'event': request.event.slug, 'organizer': request.event.organizer.slug, }), - 'active': (url.namespace == 'plugins:sendmail' and url.url_name == 'send'), + 'active': (url.namespace == 'plugins:sendmail' and url.url_name.startswith('send')), }, { 'label': _('Automated emails'), @@ -117,7 +118,8 @@ def control_nav_import(sender, request=None, **kwargs): @receiver(signal=logentry_display) def pretixcontrol_logentry_display(sender, logentry, **kwargs): plains = { - 'pretix.plugins.sendmail.sent': _('Email was sent'), + 'pretix.plugins.sendmail.sent': _('Mass email was sent to customers or attendees.'), + 'pretix.plugins.sendmail.sent.waitinglist': _('Mass email was sent to waiting list entries.'), 'pretix.plugins.sendmail.order.email.sent': _('The order received a mass email.'), 'pretix.plugins.sendmail.order.email.sent.attendee': _('A ticket holder of this order received a mass email.'), 'pretix.plugins.sendmail.rule.added': _('An email rule was created'), @@ -219,3 +221,17 @@ def sendmail_copy_data_receiver(sender, other, item_map, **kwargs): r.save() if limit_products: r.limit_products.add(*[item_map[p.id] for p in limit_products if p.id in item_map]) + + +sendmail_view_classes = EventPluginSignal() +""" +This signal allows you to register subclasses of ``pretix.plugins.sendmail.views.BaseSenderView`` that should be +discovered by this plugin. + +As with all plugin signals, the ``sender`` keyword will contain the event. +""" + + +@receiver(signal=sendmail_view_classes, dispatch_uid="sendmail_register_sendmail_view_classes") +def register_view_classes(sender, **kwargs): + return [OrderSendView, WaitinglistSendView] diff --git a/src/pretix/plugins/sendmail/tasks.py b/src/pretix/plugins/sendmail/tasks.py index 0a4cdd453..af04f369d 100644 --- a/src/pretix/plugins/sendmail/tasks.py +++ b/src/pretix/plugins/sendmail/tasks.py @@ -44,12 +44,12 @@ from pretix.helpers.format import format_map @app.task(base=ProfiledEventTask, acks_late=True) -def send_mails(event: Event, user: int, subject: dict, message: dict, orders: list, items: list, - recipients: str, filter_checkins: bool, not_checked_in: bool, checkin_lists: list, - attachments: list = None, attach_tickets: bool = False) -> None: +def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict, objects: list, items: list, + recipients: str, filter_checkins: bool, not_checked_in: bool, checkin_lists: list, + attachments: list = None, attach_tickets: bool = False) -> None: failures = [] user = User.objects.get(pk=user) if user else None - orders = Order.objects.filter(pk__in=orders, event=event) + orders = Order.objects.filter(pk__in=objects, event=event) subject = LazyI18nString(subject) message = LazyI18nString(message) @@ -138,7 +138,7 @@ def send_mails(event: Event, user: int, subject: dict, message: dict, orders: li locale=o.locale, order=o, attach_tickets=attach_tickets, - attach_cached_files=attachments + attach_cached_files=attachments, ) o.log_action( 'pretix.plugins.sendmail.order.email.sent', @@ -151,3 +151,27 @@ def send_mails(event: Event, user: int, subject: dict, message: dict, orders: li ) except SendMailException: failures.append(o.email) + + +@app.task(base=ProfiledEventTask, acks_late=True) +def send_mails_to_waitinglist(event: Event, user: int, subject: dict, message: dict, objects: list, + attachments: list = None) -> None: + user = User.objects.get(pk=user) if user else None + entries = event.waitinglistentries.filter(pk__in=objects).select_related( + 'subevent' + ) + subject = LazyI18nString(subject) + message = LazyI18nString(message) + + for e in entries: + e.send_mail( + subject, + message, + get_email_context( + event=e.event, + waiting_list_entry=e, + event_or_subevent=e.subevent or e.event, + ), + user=user, + attach_cached_files=attachments, + ) diff --git a/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history.html b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history.html index 57480a9f2..970ba287e 100644 --- a/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history.html +++ b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history.html @@ -22,33 +22,7 @@ {% if log.display %}
{{ log.display }} {% endif %} -
{% trans "Sent to orders:" %} - {% for status in log.parsed_data.sendto %} - {{ status }}{% if forloop.revcounter > 1 %},{% endif %} - {% endfor %} - {% if log.pdata.items %} -
{{ log.pdata.items|join:", " }} - {% endif %} - {% if log.pdata.filter_checkins %} - {% if log.pdata.not_checked_in %} -
{% trans "All customers not checked in" %} - {% endif %} - {% if log.pdata.checkin_lists %} -
{{ log.pdata.checkin_lists|join:", " }} - {% endif %} - {% endif %} - {% if log.pdata.subevent_obj %} -
{{ log.pdata.subevent_obj }} - {% elif log.pdata.subevents_from %} -
{{ log.pdata.subevents_from }} – {{ log.pdata.subevents_to }} - {% endif %} - {% if log.pdata.recipients == "attendees" %} -
{% trans "Attendee contact addresses" %} - {% elif log.pdata.recipients == "both" %} -
{% trans "All contact addresses" %} - {% else%} -
{% trans "Order contact addresses" %} - {% endif %} + {{ log.view.rendered_data }}

{% for locale, value in log.pdata.locales.items %} @@ -58,7 +32,7 @@

{{ value.message|linebreaksbr }}
{% endfor %}

- {% trans "Send a new email based on this" %} + {% trans "Send a new email based on this" %} {% endfor %} diff --git a/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_orders.html b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_orders.html new file mode 100644 index 000000000..ebfb6ca27 --- /dev/null +++ b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_orders.html @@ -0,0 +1,28 @@ +{% load i18n %} +
{% trans "Sent to orders:" %} +{% for status in log.parsed_data.sendto %} + {{ status }}{% if forloop.revcounter > 1 %},{% endif %} +{% endfor %} +{% if log.pdata.items %} +
{{ log.pdata.items|join:", " }} +{% endif %} +{% if log.pdata.filter_checkins %} + {% if log.pdata.not_checked_in %} +
{% trans "All customers not checked in" %} + {% endif %} + {% if log.pdata.checkin_lists %} +
{{ log.pdata.checkin_lists|join:", " }} + {% endif %} +{% endif %} +{% if log.pdata.subevent_obj %} +
{{ log.pdata.subevent_obj }} +{% elif log.pdata.subevents_from %} +
{{ log.pdata.subevents_from }} – {{ log.pdata.subevents_to }} +{% endif %} +{% if log.pdata.recipients == "attendees" %} +
{% trans "Attendee contact addresses" %} +{% elif log.pdata.recipients == "both" %} +
{% trans "All contact addresses" %} +{% else%} +
{% trans "Order contact addresses" %} +{% endif %} diff --git a/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_waitinglist.html b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_waitinglist.html new file mode 100644 index 000000000..8bea24ac7 --- /dev/null +++ b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/history_fragment_waitinglist.html @@ -0,0 +1,9 @@ +{% load i18n %} +{% if log.pdata.items %} +
{{ log.pdata.items|join:", " }} +{% endif %} +{% if log.pdata.subevent_obj %} +
{{ log.pdata.subevent_obj }} +{% elif log.pdata.subevents_from %} +
{{ log.pdata.subevents_from }} – {{ log.pdata.subevents_to }} +{% endif %} diff --git a/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/index.html b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/index.html new file mode 100644 index 000000000..e491905a9 --- /dev/null +++ b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/index.html @@ -0,0 +1,16 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% block title %}{% trans "Send out emails" %}{% endblock %} +{% block content %} +

{% trans "Send out emails" %}

+ +{% endblock %} diff --git a/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form.html b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form.html index 4250aa6c7..c562fb959 100644 --- a/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form.html +++ b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form.html @@ -4,7 +4,10 @@ {% load humanize %} {% block title %}{% trans "Send out emails" %}{% endblock %} {% block content %} -

{% trans "Send out emails" %}

+

+ {% trans "Send out emails" %} + {{ view_title }} +

{% block inner %}
{% csrf_token %} @@ -20,40 +23,16 @@ {% bootstrap_form_errors form %}
{% trans "Recipients" %} - {% bootstrap_field form.recipients layout='horizontal' %} - {% bootstrap_field form.sendto layout='horizontal' %} - {% if form.subevent %} - {% bootstrap_field form.subevent layout='horizontal' %} - {% bootstrap_field form.subevents_from layout='horizontal' %} - {% bootstrap_field form.subevents_to layout='horizontal' %} - {% endif %} - {% bootstrap_field form.created_from layout='horizontal' %} - {% bootstrap_field form.created_to layout='horizontal' %} - {% bootstrap_field form.items layout='horizontal' %} -
-
-
-
-
- -
-
- {% bootstrap_field form.not_checked_in layout='horizontal' %} - {% bootstrap_field form.checkin_lists layout='horizontal' %} -
-
-
-
-
+ {% include form_fragment_name with form=form %}
{% trans "Content" %} {% bootstrap_field form.subject layout='horizontal' %} {% bootstrap_field form.message layout='horizontal' %} {% bootstrap_field form.attachment layout='horizontal' %} - {% bootstrap_field form.attach_tickets layout='horizontal' %} + {% if form.attach_tickets %} + {% bootstrap_field form.attach_tickets layout='horizontal' %} + {% endif %}
{% if is_preview %}
@@ -97,12 +76,7 @@ {% endif %} diff --git a/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form_fragment_orders.html b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form_fragment_orders.html new file mode 100644 index 000000000..2bb3e4bf3 --- /dev/null +++ b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form_fragment_orders.html @@ -0,0 +1,29 @@ +{% load i18n %} +{% load bootstrap3 %} +{% bootstrap_field form.recipients layout='horizontal' %} +{% bootstrap_field form.sendto layout='horizontal' %} +{% if form.subevent %} + {% bootstrap_field form.subevent layout='horizontal' %} + {% bootstrap_field form.subevents_from layout='horizontal' %} + {% bootstrap_field form.subevents_to layout='horizontal' %} +{% endif %} +{% bootstrap_field form.created_from layout='horizontal' %} +{% bootstrap_field form.created_to layout='horizontal' %} +{% bootstrap_field form.items layout='horizontal' %} +
+
+
+
+
+ +
+
+ {% bootstrap_field form.not_checked_in layout='horizontal' %} + {% bootstrap_field form.checkin_lists layout='horizontal' %} +
+
+
+
+
diff --git a/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form_fragment_waitinglist.html b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form_fragment_waitinglist.html new file mode 100644 index 000000000..ac2731bae --- /dev/null +++ b/src/pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form_fragment_waitinglist.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% load bootstrap3 %} +{% bootstrap_field form.items layout='horizontal' %} +{% if form.subevent %} + {% bootstrap_field form.subevent layout='horizontal' %} + {% bootstrap_field form.subevents_from layout='horizontal' %} + {% bootstrap_field form.subevents_to layout='horizontal' %} +{% endif %} diff --git a/src/pretix/plugins/sendmail/urls.py b/src/pretix/plugins/sendmail/urls.py index ae58d2be9..d972f438d 100644 --- a/src/pretix/plugins/sendmail/urls.py +++ b/src/pretix/plugins/sendmail/urls.py @@ -40,8 +40,12 @@ from . import views from .api import RuleViewSet urlpatterns = [ - re_path(r'^control/event/(?P[^/]+)/(?P[^/]+)/sendmail/$', views.SenderView.as_view(), + re_path(r'^control/event/(?P[^/]+)/(?P[^/]+)/sendmail/$', views.IndexView.as_view(), name='send'), + re_path(r'^control/event/(?P[^/]+)/(?P[^/]+)/sendmail/orders/$', views.OrderSendView.as_view(), + name='send.orders'), + re_path(r'^control/event/(?P[^/]+)/(?P[^/]+)/sendmail/waitinglist/$', views.WaitinglistSendView.as_view(), + name='send.waitinglist'), re_path(r'^control/event/(?P[^/]+)/(?P[^/]+)/sendmail/history/', views.EmailHistoryView.as_view(), name='history'), re_path(r'^control/event/(?P[^/]+)/(?P[^/]+)/sendmail/rules/create', views.CreateRule.as_view(), diff --git a/src/pretix/plugins/sendmail/views.py b/src/pretix/plugins/sendmail/views.py index 957107238..602a21847 100644 --- a/src/pretix/plugins/sendmail/views.py +++ b/src/pretix/plugins/sendmail/views.py @@ -38,15 +38,17 @@ import logging import bleach import dateutil from django.contrib import messages +from django.contrib.humanize.templatetags.humanize import intcomma from django.db import transaction from django.db.models import Count, Exists, Max, Min, OuterRef, Q from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect +from django.template.loader import get_template from django.urls import reverse from django.utils.functional import cached_property from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _ -from django.views.generic import DeleteView, FormView, ListView +from django.utils.translation import gettext_lazy as _, ngettext +from django.views.generic import DeleteView, FormView, ListView, TemplateView from pretix.base.email import get_available_placeholders from pretix.base.i18n import LazyI18nString, language @@ -55,7 +57,9 @@ from pretix.base.models.event import SubEvent from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.views import CreateView, PaginationMixin, UpdateView -from pretix.plugins.sendmail.tasks import send_mails +from pretix.plugins.sendmail.tasks import ( + send_mails_to_orders, send_mails_to_waitinglist, +) from ...helpers.format import format_map from . import forms @@ -64,59 +68,104 @@ from .models import Rule, ScheduledMail logger = logging.getLogger('pretix.plugins.sendmail') -class SenderView(EventPermissionRequiredMixin, FormView): +class IndexView(EventPermissionRequiredMixin, TemplateView): + template_name = 'pretixplugins/sendmail/index.html' + permission = 'can_change_orders' + + def get_context_data(self, **kwargs): + from .signals import sendmail_view_classes + classes = [] + for recv, resp in sendmail_view_classes.send(self.request.event): + if isinstance(resp, (list, tuple)): + classes += resp + else: + classes.append(resp) + return super().get_context_data(**kwargs, views=[ + { + 'title': cls.TITLE, + 'description': cls.DESCRIPTION, + 'url': cls.get_url(self.request.event) + } for cls in classes + ]) + + +class BaseSenderView(EventPermissionRequiredMixin, FormView): + # These parameters usually SHOULD NOT be overridden template_name = 'pretixplugins/sendmail/send_form.html' permission = 'can_change_orders' - form_class = forms.MailForm + + # These parameters MUST be overridden by subclasses + form_fragment_name = None + context_parameters = ['event'] + task = None + + # These parameters MUST be overriden by subclasses in a way that allows static access + + ACTION_TYPE = None + TITLE = "" + DESCRIPTION = "" + + # The following methods MUST be overridden by subclasses + + @staticmethod + def get_url(self, event): + """Returns the URL for this view for a given event.""" + raise NotImplementedError + + def get_object_queryset(self, form): + """Returns a queryset of objects that will become recipients.""" + return Order.objects.none() + + def describe_match_size(self, cnt): + """Returns a short human-readable description of the recipient set, such as '3 attendees'.""" + raise NotImplementedError + + @classmethod + def show_history_meta_data(cls, logentry, _cache_store): + """Returns an HTML component for the history view.""" + raise NotImplementedError + + # The following methods MAY be overridden by subclasses + + def initial_from_logentry(self, logentry): + return { + 'message': LazyI18nString(logentry.parsed_data['message']), + 'subject': LazyI18nString(logentry.parsed_data['subject']), + } + + def get_success_url(self): + return self.request.get_full_path() + + def get_task_kwargs(self, form, objects): + kwargs = { + 'event': self.request.event.pk, + 'user': self.request.user.pk, + 'subject': form.cleaned_data['subject'].data, + 'message': form.cleaned_data['message'].data, + 'objects': [o.pk for o in objects], + } + attachment = form.cleaned_data.get('attachment') + if attachment is not None and attachment is not False: + kwargs['attachments'] = [form.cleaned_data['attachment'].id] + return kwargs + + # The following methods SHOULD NOT Be overridden by subclasses, but in some cases it may be necessary def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['event'] = self.request.event + kwargs['context_parameters'] = self.context_parameters if 'from_log' in self.request.GET: try: from_log_id = self.request.GET.get('from_log') logentry = LogEntry.objects.get( id=from_log_id, event=self.request.event, - action_type='pretix.plugins.sendmail.sent' + action_type=self.ACTION_TYPE ) kwargs['initial'] = { - 'recipients': logentry.parsed_data.get('recipients', 'orders'), - 'message': LazyI18nString(logentry.parsed_data['message']), - 'subject': LazyI18nString(logentry.parsed_data['subject']), - 'sendto': logentry.parsed_data['sendto'], + **self.initial_from_logentry(logentry), } - if 'items' in logentry.parsed_data: - kwargs['initial']['items'] = self.request.event.items.filter( - id__in=[a['id'] for a in logentry.parsed_data['items']] - ) - elif logentry.parsed_data.get('item'): - kwargs['initial']['items'] = self.request.event.items.filter( - id=logentry.parsed_data['item']['id'] - ) - if 'checkin_lists' in logentry.parsed_data: - kwargs['initial']['checkin_lists'] = self.request.event.checkin_lists.filter( - id__in=[c['id'] for c in logentry.parsed_data['checkin_lists']] - ) - kwargs['initial']['filter_checkins'] = logentry.parsed_data.get('filter_checkins', False) - kwargs['initial']['not_checked_in'] = logentry.parsed_data.get('not_checked_in', False) - if logentry.parsed_data.get('subevents_from'): - kwargs['initial']['subevents_from'] = dateutil.parser.parse(logentry.parsed_data['subevents_from']) - if logentry.parsed_data.get('subevents_to'): - kwargs['initial']['subevents_to'] = dateutil.parser.parse(logentry.parsed_data['subevents_to']) - if logentry.parsed_data.get('created_from'): - kwargs['initial']['created_from'] = dateutil.parser.parse(logentry.parsed_data['created_from']) - if logentry.parsed_data.get('created_to'): - kwargs['initial']['created_to'] = dateutil.parser.parse(logentry.parsed_data['created_to']) - if logentry.parsed_data.get('attach_tickets'): - kwargs['initial']['attach_tickets'] = logentry.parsed_data['attach_tickets'] - if logentry.parsed_data.get('subevent'): - try: - kwargs['initial']['subevent'] = self.request.event.subevents.get( - pk=logentry.parsed_data['subevent']['id'] - ) - except SubEvent.DoesNotExist: - pass except LogEntry.DoesNotExist: raise Http404(_('You supplied an invalid log entry ID')) return kwargs @@ -126,6 +175,170 @@ class SenderView(EventPermissionRequiredMixin, FormView): return super().form_invalid(form) def form_valid(self, form): + objects = self.get_object_queryset(form) + ocnt = objects.count() + + self.output = {} + if not ocnt: + messages.error(self.request, _('There are no matching recipients for your selection.')) + self.request.POST = self.request.POST.copy() + self.request.POST.pop("action", "") + return self.get(self.request, *self.args, **self.kwargs) + + if self.request.POST.get("action") != "send": + for l in self.request.event.settings.locales: + with language(l, self.request.event.settings.region): + context_dict = {} + for k, v in get_available_placeholders(self.request.event, self.context_parameters).items(): + context_dict[k] = '{}'.format( + _('This value will be replaced based on dynamic parameters.'), + v.render_sample(self.request.event) + ) + + subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=[]) + preview_subject = format_map(subject, context_dict) + message = form.cleaned_data['message'].localize(l) + preview_text = markdown_compile_email(format_map(message, context_dict)) + + self.output[l] = { + 'subject': _('Subject: {subject}').format(subject=preview_subject), + 'html': preview_text, + 'attachment': form.cleaned_data.get('attachment') + } + + self.object_count = ocnt + return self.get(self.request, *self.args, **self.kwargs) + + self.task.apply_async( + kwargs=self.get_task_kwargs(form, objects) + ) + self.request.event.log_action( + self.ACTION_TYPE, + user=self.request.user, + data=dict(form.cleaned_data) + ) + messages.success(self.request, _('Your message has been queued and will be sent to the contact addresses of %s ' + 'in the next few minutes.') % self.describe_match_size(len(objects))) + + return redirect(self.get_success_url()) + + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(*args, **kwargs) + ctx['output'] = getattr(self, 'output', None) + ctx['match_size'] = self.describe_match_size(getattr(self, 'object_count', None)) + ctx['form_fragment_name'] = self.form_fragment_name + ctx['is_preview'] = self.request.method == 'POST' and self.request.POST.get('action') == 'preview' and ctx['form'].is_valid() + ctx['view_title'] = self.TITLE + return ctx + + def get_form(self, form_class=None): + f = super().get_form(form_class) + if self.request.method == 'POST' and self.request.POST.get('action') == 'preview': + if f.is_valid(): + for fname, field in f.fields.items(): + field.widget.attrs['disabled'] = 'disabled' + return f + + +class OrderSendView(BaseSenderView): + form_class = forms.OrderMailForm + form_fragment_name = "pretixplugins/sendmail/send_form_fragment_orders.html" + context_parameters = ['event', 'order', 'position_or_address'] + task = send_mails_to_orders + + ACTION_TYPE = 'pretix.plugins.sendmail.sent' + TITLE = _("Orders or attendees") + DESCRIPTION = _("Send an email to every customer, or to every person a ticket has been " + "purchased for, or a combination of both.") + + @classmethod + def show_history_meta_data(cls, logentry, _cache_store): + if 'itemcache' not in _cache_store: + _cache_store['itemcache'] = { + i.pk: str(i) for i in logentry.event.items.all() + } + if 'checkin_list_cache' not in _cache_store: + _cache_store['checkin_list_cache'] = { + i.pk: str(i) for i in logentry.event.checkin_lists.all() + } + if 'status' not in _cache_store: + status = dict(Order.STATUS_CHOICE) + status['overdue'] = _('pending with payment overdue') + status['na'] = _('payment pending (except unapproved)') + status['pa'] = _('approval pending') + status['r'] = status['c'] + _cache_store['status'] = status + + tpl = get_template('pretixplugins/sendmail/history_fragment_orders.html') + logentry.pdata['sendto'] = [ + _cache_store['status'][s] for s in logentry.pdata['sendto'] + ] + logentry.pdata['items'] = [ + _cache_store['itemcache'].get(i['id'], '?') for i in logentry.pdata.get('items', []) + ] + logentry.pdata['checkin_lists'] = [ + _cache_store['checkin_list_cache'].get(i['id'], '?') + for i in logentry.pdata.get('checkin_lists', []) if i['id'] in _cache_store['checkin_list_cache'] + ] + if logentry.pdata.get('subevent'): + try: + logentry.pdata['subevent_obj'] = logentry.event.subevents.get(pk=logentry.pdata['subevent']['id']) + except SubEvent.DoesNotExist: + pass + return tpl.render({ + 'log': logentry, + }) + + @classmethod + def get_url(cls, event): + return reverse( + 'plugins:sendmail:send.orders', + kwargs={ + 'event': event.slug, + 'organizer': event.organizer.slug, + } + ) + + def initial_from_logentry(self, logentry: LogEntry): + initial = super().initial_from_logentry(logentry) + if 'recipients' in logentry.parsed_data: + initial['recipients'] = logentry.parsed_data.get('recipients', 'orders') + if 'sendto' in logentry.parsed_data: + initial['sendto'] = logentry.parsed_data.get('recipients', 'sendto') + if 'items' in logentry.parsed_data: + initial['items'] = self.request.event.items.filter( + id__in=[a['id'] for a in logentry.parsed_data['items']] + ) + elif logentry.parsed_data.get('item'): + initial['items'] = self.request.event.items.filter( + id=logentry.parsed_data['item']['id'] + ) + if 'checkin_lists' in logentry.parsed_data: + initial['checkin_lists'] = self.request.event.checkin_lists.filter( + id__in=[c['id'] for c in logentry.parsed_data['checkin_lists']] + ) + initial['filter_checkins'] = logentry.parsed_data.get('filter_checkins', False) + initial['not_checked_in'] = logentry.parsed_data.get('not_checked_in', False) + if logentry.parsed_data.get('subevents_from'): + initial['subevents_from'] = dateutil.parser.parse(logentry.parsed_data['subevents_from']) + if logentry.parsed_data.get('subevents_to'): + initial['subevents_to'] = dateutil.parser.parse(logentry.parsed_data['subevents_to']) + if logentry.parsed_data.get('created_from'): + initial['created_from'] = dateutil.parser.parse(logentry.parsed_data['created_from']) + if logentry.parsed_data.get('created_to'): + initial['created_to'] = dateutil.parser.parse(logentry.parsed_data['created_to']) + if logentry.parsed_data.get('attach_tickets'): + initial['attach_tickets'] = logentry.parsed_data['attach_tickets'] + if logentry.parsed_data.get('subevent'): + try: + initial['subevent'] = self.request.event.subevents.get( + pk=logentry.parsed_data['subevent']['id'] + ) + except SubEvent.DoesNotExist: + pass + return initial + + def get_object_queryset(self, form): qs = Order.objects.filter(event=self.request.event) statusq = Q(status__in=form.cleaned_data['sendto']) if 'overdue' in form.cleaned_data['sendto']: @@ -189,88 +402,111 @@ class SenderView(EventPermissionRequiredMixin, FormView): if form.cleaned_data.get('created_to'): opq = opq.filter(order__datetime__lt=form.cleaned_data.get('created_to')) - orders = orders.annotate(match_pos=Exists(opq)).filter(match_pos=True).distinct() + return orders.annotate(match_pos=Exists(opq)).filter(match_pos=True).distinct() - ocnt = orders.count() + def describe_match_size(self, cnt): + return ngettext( + '%(number)s matching order', + '%(number)s matching orders', + cnt or 0, + ) % { + 'number': intcomma(cnt or 0), + } - self.output = {} - if not ocnt: - messages.error(self.request, _('There are no orders matching this selection.')) - self.request.POST = self.request.POST.copy() - self.request.POST.pop("action", "") - return self.get(self.request, *self.args, **self.kwargs) - - if self.request.POST.get("action") != "send": - for l in self.request.event.settings.locales: - with language(l, self.request.event.settings.region): - context_dict = {} - for k, v in get_available_placeholders(self.request.event, ['event', 'order', - 'position_or_address']).items(): - context_dict[k] = '{}'.format( - _('This value will be replaced based on dynamic parameters.'), - v.render_sample(self.request.event) - ) - - subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=[]) - preview_subject = format_map(subject, context_dict) - message = form.cleaned_data['message'].localize(l) - preview_text = markdown_compile_email(format_map(message, context_dict)) - - self.output[l] = { - 'subject': _('Subject: {subject}').format(subject=preview_subject), - 'html': preview_text, - 'attachment': form.cleaned_data.get('attachment') - } - - self.order_count = ocnt - return self.get(self.request, *self.args, **self.kwargs) - - kwargs = { + def get_task_kwargs(self, form, objects): + kwargs = super().get_task_kwargs(form, objects) + kwargs.update({ 'recipients': form.cleaned_data['recipients'], - 'event': self.request.event.pk, - 'user': self.request.user.pk, - 'subject': form.cleaned_data['subject'].data, - 'message': form.cleaned_data['message'].data, - 'orders': [o.pk for o in orders], 'items': [i.pk for i in form.cleaned_data.get('items')], 'not_checked_in': form.cleaned_data.get('not_checked_in'), 'checkin_lists': [i.pk for i in form.cleaned_data.get('checkin_lists')], 'filter_checkins': form.cleaned_data.get('filter_checkins'), 'attach_tickets': form.cleaned_data.get('attach_tickets'), + }) + return kwargs + + +class WaitinglistSendView(BaseSenderView): + form_class = forms.WaitinglistMailForm + form_fragment_name = "pretixplugins/sendmail/send_form_fragment_waitinglist.html" + context_parameters = ['event', 'waiting_list_entry', 'event_or_subevent'] + task = send_mails_to_waitinglist + + ACTION_TYPE = 'pretix.plugins.sendmail.sent.waitinglist' + TITLE = _("Waiting list") + DESCRIPTION = _("Send an email to every person currently waiting to receive a voucher through the waiting " + "list feature.") + + @classmethod + def show_history_meta_data(cls, logentry, _cache_store): + if 'itemcache' not in _cache_store: + _cache_store['itemcache'] = { + i.pk: str(i) for i in logentry.event.items.all() + } + + tpl = get_template('pretixplugins/sendmail/history_fragment_waitinglist.html') + logentry.pdata['items'] = [ + _cache_store['itemcache'].get(i['id'], '?') for i in logentry.pdata.get('items', []) + ] + if logentry.pdata.get('subevent'): + try: + logentry.pdata['subevent_obj'] = logentry.event.subevents.get(pk=logentry.pdata['subevent']['id']) + except SubEvent.DoesNotExist: + pass + return tpl.render({ + 'log': logentry, + }) + + @classmethod + def get_url(cls, event): + return reverse( + 'plugins:sendmail:send.waitinglist', + kwargs={ + 'event': event.slug, + 'organizer': event.organizer.slug, + } + ) + + def initial_from_logentry(self, logentry: LogEntry): + initial = super().initial_from_logentry(logentry) + if 'items' in logentry.parsed_data: + initial['items'] = self.request.event.items.filter( + id__in=[a['id'] for a in logentry.parsed_data['items']] + ) + if logentry.parsed_data.get('subevents_from'): + initial['subevents_from'] = dateutil.parser.parse(logentry.parsed_data['subevents_from']) + if logentry.parsed_data.get('subevents_to'): + initial['subevents_to'] = dateutil.parser.parse(logentry.parsed_data['subevents_to']) + if logentry.parsed_data.get('subevent'): + try: + initial['subevent'] = self.request.event.subevents.get( + pk=logentry.parsed_data['subevent']['id'] + ) + except SubEvent.DoesNotExist: + pass + return initial + + def get_object_queryset(self, form): + qs = self.request.event.waitinglistentries.filter(voucher__isnull=True) + + qs = qs.filter(item__in=[i.pk for i in form.cleaned_data.get('items')]) + if form.cleaned_data.get('subevent'): + qs = qs.filter(subevent=form.cleaned_data.get('subevent')) + if form.cleaned_data.get('subevents_from'): + qs = qs.filter(subevent__date_from__gte=form.cleaned_data.get('subevents_from')) + if form.cleaned_data.get('subevents_to'): + qs = qs.filter(subevent__date_from__lt=form.cleaned_data.get('subevents_to')) + + return qs + + def describe_match_size(self, cnt): + return ngettext( + '%(number)s waiting list entry', + '%(number)s waiting list entries', + cnt or 0, + ) % { + 'number': intcomma(cnt or 0), } - attachment = form.cleaned_data.get('attachment') - if attachment is not None and attachment is not False: - kwargs['attachments'] = [form.cleaned_data['attachment'].id] - - send_mails.apply_async( - kwargs=kwargs - ) - self.request.event.log_action('pretix.plugins.sendmail.sent', - user=self.request.user, - data=dict(form.cleaned_data)) - messages.success(self.request, _('Your message has been queued and will be sent to the contact addresses of %d ' - 'orders in the next few minutes.') % len(orders)) - - return redirect( - 'plugins:sendmail:send', - event=self.request.event.slug, - organizer=self.request.event.organizer.slug - ) - - def get_context_data(self, *args, **kwargs): - ctx = super().get_context_data(*args, **kwargs) - ctx['output'] = getattr(self, 'output', None) - ctx['order_count'] = getattr(self, 'order_count', None) - ctx['is_preview'] = self.request.method == 'POST' and self.request.POST.get('action') == 'preview' and ctx['form'].is_valid() - return ctx - - def get_form(self, form_class=None): - f = super().get_form(form_class) - if self.request.method == 'POST' and self.request.POST.get('action') == 'preview': - if f.is_valid(): - for fname, field in f.fields.items(): - field.widget.attrs['disabled'] = 'disabled' - return f class EmailHistoryView(EventPermissionRequiredMixin, ListView): @@ -280,27 +516,30 @@ class EmailHistoryView(EventPermissionRequiredMixin, ListView): context_object_name = 'logs' paginate_by = 5 + @cached_property + def type_map(self): + from .signals import sendmail_view_classes + classes = [] + for recv, resp in sendmail_view_classes.send(self.request.event): + if isinstance(resp, (list, tuple)): + classes += resp + else: + classes.append(resp) + return { + cls.ACTION_TYPE: cls + for cls in classes + } + def get_queryset(self): qs = LogEntry.objects.filter( event=self.request.event, - action_type='pretix.plugins.sendmail.sent' + action_type__in=self.type_map.keys(), ).select_related('event', 'user') return qs def get_context_data(self, **kwargs): ctx = super().get_context_data() - - itemcache = { - i.pk: str(i) for i in self.request.event.items.all() - } - checkin_list_cache = { - i.pk: str(i) for i in self.request.event.checkin_lists.all() - } - status = dict(Order.STATUS_CHOICE) - status['overdue'] = _('pending with payment overdue') - status['na'] = _('payment pending (except unapproved)') - status['pa'] = _('approval pending') - status['r'] = status['c'] + _cache = {} for log in ctx['logs']: log.pdata = log.parsed_data log.pdata['locales'] = {} @@ -309,21 +548,11 @@ class EmailHistoryView(EventPermissionRequiredMixin, ListView): 'message': msg, 'subject': log.pdata['subject'][locale] } - log.pdata['sendto'] = [ - status[s] for s in log.pdata['sendto'] - ] - log.pdata['items'] = [ - itemcache.get(i['id'], '?') for i in log.pdata.get('items', []) - ] - log.pdata['checkin_lists'] = [ - checkin_list_cache.get(i['id'], '?') - for i in log.pdata.get('checkin_lists', []) if i['id'] in checkin_list_cache - ] - if log.pdata.get('subevent'): - try: - log.pdata['subevent_obj'] = self.request.event.subevents.get(pk=log.pdata['subevent']['id']) - except SubEvent.DoesNotExist: - pass + log.view = { + 'url': self.type_map[log.action_type].get_url(self.request.event), + 'title': self.type_map[log.action_type].TITLE, + 'rendered_data': self.type_map[log.action_type].show_history_meta_data(log, _cache) + } return ctx diff --git a/src/pretix/static/pretixcontrol/scss/main.scss b/src/pretix/static/pretixcontrol/scss/main.scss index 512b56b50..9fb0ca74e 100644 --- a/src/pretix/static/pretixcontrol/scss/main.scss +++ b/src/pretix/static/pretixcontrol/scss/main.scss @@ -792,6 +792,27 @@ tbody th { background: $table-bg-hover; } +.large-link-group { + a.list-group-item { + &::before { + font-family: FontAwesome; + font-size: 30px; + content: $fa-var-chevron-right; + float: right; + color: $link-color; + } + h4 { + color: $link-color; + } + &:hover &::before { + color: $link-hover-color; + } + &:hover h4 { + color: $link-hover-color; + } + } +} + .withoutjs { display: none !important; diff --git a/src/tests/plugins/sendmail/test_sendmail.py b/src/tests/plugins/sendmail/test_sendmail.py index cd25e0248..ff7ef79c9 100644 --- a/src/tests/plugins/sendmail/test_sendmail.py +++ b/src/tests/plugins/sendmail/test_sendmail.py @@ -68,13 +68,21 @@ def subevent(event): event.has_subevents = True event.save() se = event.subevents.create(name='se1', date_from=now()) - return se +@pytest.fixture +def waitinglistentry(event, item): + return event.waitinglistentries.create( + item=item, + created=now(), + email='john@example.org', + ) + + @pytest.mark.django_db def test_sendmail_view(logged_in_client, sendmail_url, expected=200): - response = logged_in_client.get(sendmail_url) + response = logged_in_client.get(sendmail_url + 'orders/') assert response.status_code == expected @@ -82,7 +90,7 @@ def test_sendmail_view(logged_in_client, sendmail_url, expected=200): @pytest.mark.django_db def test_sendmail_simple_case(logged_in_client, sendmail_url, event, order, pos): djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'orders', @@ -109,7 +117,7 @@ def test_sendmail_simple_case(logged_in_client, sendmail_url, event, order, pos) @pytest.mark.django_db def test_sendmail_email_not_sent_if_order_not_match(logged_in_client, sendmail_url, event, order, pos): djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'p', 'action': 'send', 'recipients': 'orders', @@ -126,7 +134,7 @@ def test_sendmail_email_not_sent_if_order_not_match(logged_in_client, sendmail_u @pytest.mark.django_db def test_sendmail_preview(logged_in_client, sendmail_url, event, order, pos): djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'recipients': 'orders', 'items': pos.item_id, @@ -144,7 +152,7 @@ def test_sendmail_preview(logged_in_client, sendmail_url, event, order, pos): @pytest.mark.django_db def test_sendmail_invalid_data(logged_in_client, sendmail_url, event, order, pos): djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'orders', @@ -172,7 +180,7 @@ def test_sendmail_multi_locales(logged_in_client, sendmail_url, event, item): locale='de') OrderPosition.objects.create(order=o, item=item, price=13) - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'p', 'action': 'send', 'recipients': 'orders', @@ -211,7 +219,7 @@ def test_sendmail_subevents(logged_in_client, sendmail_url, event, order, pos): op.save() djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'orders', @@ -226,7 +234,7 @@ def test_sendmail_subevents(logged_in_client, sendmail_url, event, order, pos): assert len(djmail.outbox) == 1 djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'orders', @@ -248,7 +256,7 @@ def test_sendmail_subevents(logged_in_client, sendmail_url, event, order, pos): @pytest.mark.django_db def test_sendmail_placeholder(logged_in_client, sendmail_url, event, order, pos): djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'recipients': 'orders', 'items': pos.item_id, @@ -272,7 +280,7 @@ def test_sendmail_attendee_mails(logged_in_client, sendmail_url, event, order, p p.save() djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'attendees', @@ -297,7 +305,7 @@ def test_sendmail_both_mails(logged_in_client, sendmail_url, event, order, pos): p.save() djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'both', @@ -325,7 +333,7 @@ def test_sendmail_both_but_same_address(logged_in_client, sendmail_url, event, o p.save() djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'both', @@ -350,7 +358,7 @@ def test_sendmail_attendee_fallback(logged_in_client, sendmail_url, event, order p.save() djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'attendees', @@ -380,7 +388,7 @@ def test_sendmail_attendee_product_filter(logged_in_client, sendmail_url, event, ) djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'attendees', @@ -409,7 +417,7 @@ def test_sendmail_attendee_checkin_filter(logged_in_client, sendmail_url, event, Checkin.objects.create(position=pos2, list=chkl2) djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'attendees', @@ -428,7 +436,7 @@ def test_sendmail_attendee_checkin_filter(logged_in_client, sendmail_url, event, assert '/order/' not in djmail.outbox[0].body djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'attendees', @@ -447,7 +455,7 @@ def test_sendmail_attendee_checkin_filter(logged_in_client, sendmail_url, event, assert '/order/' not in djmail.outbox[0].body djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'attendees', @@ -467,7 +475,7 @@ def test_sendmail_attendee_checkin_filter(logged_in_client, sendmail_url, event, # Test that filtering is ignored if filter_checkins is not set djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'attendees', @@ -489,7 +497,7 @@ def test_sendmail_attendee_checkin_filter(logged_in_client, sendmail_url, event, # Test that filtering is ignored if filter_checkins is not set djmail.outbox = [] - response = logged_in_client.post(sendmail_url, + response = logged_in_client.post(sendmail_url + 'orders/', {'sendto': 'na', 'action': 'send', 'recipients': 'attendees', @@ -508,3 +516,28 @@ def test_sendmail_attendee_checkin_filter(logged_in_client, sendmail_url, event, assert '/order/' not in djmail.outbox[1].body to_emails = set(*zip(*[mail.to for mail in djmail.outbox])) assert to_emails == {'attendee1@dummy.test', 'attendee2@dummy.test'} + + +@pytest.mark.django_db +def test_waitinglist_sendmail_simple_case(logged_in_client, sendmail_url, event, waitinglistentry): + djmail.outbox = [] + response = logged_in_client.post(sendmail_url + 'waitinglist/', + {'action': 'send', + 'items': waitinglistentry.item_id, + 'subject_0': 'Test subject', + 'message_0': 'This is a test file for sending mails.', + }, + follow=True) + assert response.status_code == 200 + assert 'alert-success' in response.rendered_content + + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == [waitinglistentry.email] + assert djmail.outbox[0].subject == 'Test subject' + assert 'This is a test file for sending mails.' in djmail.outbox[0].body + + url = sendmail_url + 'history/' + response = logged_in_client.get(url) + + assert response.status_code == 200 + assert 'Test subject' in response.rendered_content