Allow to disable some e-mails depending on sales channel (#1726)

Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
Raphael Michel
2020-07-28 09:26:18 +02:00
committed by GitHub
12 changed files with 137 additions and 11 deletions

View File

@@ -928,7 +928,8 @@ Creating orders
during order generation and is not respected automatically when the order changes later.) during order generation and is not respected automatically when the order changes later.)
* ``force`` (optional). If set to ``true``, quotas will be ignored. * ``force`` (optional). If set to ``true``, quotas will be ignored.
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order. Defaults to * ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order, regardless of
whether these emails are enabled for certain sales channels. Defaults to
``false``. ``false``.
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually

View File

@@ -47,6 +47,7 @@ gunicorn
guid guid
hardcoded hardcoded
hostname hostname
ics
idempotency idempotency
iframe iframe
incrementing incrementing

View File

@@ -26,6 +26,9 @@ Sender address
we strongly recommend to use the SMTP settings below as well, otherwise your e-mails might be detected as spam we strongly recommend to use the SMTP settings below as well, otherwise your e-mails might be detected as spam
due to the `Sender Policy Framework`_ and similar mechanisms. due to the `Sender Policy Framework`_ and similar mechanisms.
Sender name
This is the name associated with the sender address. By default, this is your event name.
Signature Signature
This text will be appended to all e-mails in form of a signature. This might be useful e.g. to add your contact This text will be appended to all e-mails in form of a signature. This might be useful e.g. to add your contact
details or any legal information that needs to be included with the e-mails. details or any legal information that needs to be included with the e-mails.
@@ -33,6 +36,15 @@ Signature
Bcc address Bcc address
This email address will receive a copy of every event-related email. This email address will receive a copy of every event-related email.
Attach calendar files
With this option, every order confirmation mail will include an ics file with name, date and location of
your event. It can be imported into many digital calendars.
Sales Channels for Checkout Emails
When you are using multiple sales channel, you may want to decide that mails for order and payment confirmation
are only to be sent for some sales channels. For orders created through the default online shop, these emails
must always be send. A similar option is available for ticket download reminders.
E-mail design E-mail design
------------- -------------

View File

@@ -0,0 +1,39 @@
# Generated by Django 3.0.8 on 2020-07-24 09:05
from django.db import migrations
from pretix.base.channels import get_all_sales_channels
def set_sales_channels(apps, schema_editor):
# We now allow restricting some mails to certain sales channels
# The default is changing from all channels to "web" only
# Therefore, for existing events, we enable all sales channels
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
Event = apps.get_model('pretixbase', 'Event')
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channels()) + "]"
batch_size = 1000
Event_SettingsStore.objects.bulk_create([
Event_SettingsStore(
object=event,
key="mail_sales_channel_placed_paid",
value=all_sales_channels)
for event in Event.objects.all()
], batch_size=batch_size)
Event_SettingsStore.objects.bulk_create([
Event_SettingsStore(
object=event,
key="mail_sales_channel_download_reminder",
value=all_sales_channels)
for event in Event.objects.all()
], batch_size=batch_size)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0158_auto_20200724_0754'),
]
operations = [
migrations.RunPython(set_sales_channels, migrations.RunPython.noop),
]

View File

@@ -1491,7 +1491,7 @@ class OrderPayment(models.Model):
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
) )
if send_mail: if send_mail and self.order.sales_channel in self.order.event.settings.mail_sales_channel_placed_paid:
self._send_paid_mail(invoice, user, mail_text) self._send_paid_mail(invoice, user, mail_text)
if self.order.event.settings.mail_send_order_paid_attendee: if self.order.event.settings.mail_send_order_paid_attendee:
for p in self.order.positions.all(): for p in self.order.positions.all():

View File

@@ -960,11 +960,12 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
email_attendees = event.settings.mail_send_order_placed_attendee email_attendees = event.settings.mail_send_order_placed_attendee
email_attendees_template = event.settings.mail_text_order_placed_attendee email_attendees_template = event.settings.mail_text_order_placed_attendee
_order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment) if sales_channel in event.settings.mail_sales_channel_placed_paid:
if email_attendees: _order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment)
for p in order.positions.all(): if email_attendees:
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email: for p in order.positions.all():
_order_placed_email_attendee(event, order, p, email_attendees_template, log_entry) if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(event, order, p, email_attendees_template, log_entry)
return order.id return order.id
@@ -1056,6 +1057,9 @@ def send_download_reminders(sender, **kwargs):
if days is None: if days is None:
continue continue
if o.sales_channel 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) 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: if now() < reminder_date or o.datetime > reminder_date:
continue continue

View File

@@ -1190,6 +1190,14 @@ DEFAULTS = {
"Defaults to your event name."), "Defaults to your event name."),
) )
}, },
'mail_sales_channel_placed_paid': {
'default': ['web'],
'type': list,
},
'mail_sales_channel_download_reminder': {
'default': ['web'],
'type': list,
},
'mail_text_signature': { 'mail_text_signature': {
'type': LazyI18nString, 'type': LazyI18nString,
'default': "" 'default': ""

View File

@@ -1,5 +1,3 @@
from urllib.parse import urlencode, urlparse
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -17,6 +15,7 @@ from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput, I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
) )
from pytz import common_timezones, timezone from pytz import common_timezones, timezone
from urllib.parse import urlencode, urlparse
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders from pretix.base.email import get_available_placeholders
@@ -750,6 +749,11 @@ def multimail_validate(val):
return s return s
def contains_web_channel_validate(val):
if "web" not in val:
raise ValidationError(_("The online shop must be selected to receive these emails."))
class MailSettingsForm(SettingsForm): class MailSettingsForm(SettingsForm):
auto_fields = [ auto_fields = [
'mail_prefix', 'mail_prefix',
@@ -758,6 +762,27 @@ class MailSettingsForm(SettingsForm):
'mail_attach_ical', 'mail_attach_ical',
] ]
mail_sales_channel_placed_paid = forms.MultipleChoiceField(
choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
label=_('Sales channels for checkout emails'),
help_text=_('The order placed and paid emails will only be send to orders from these sales channels. '
'The online shop must be enabled.'),
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
validators=[contains_web_channel_validate],
)
mail_sales_channel_download_reminder = forms.MultipleChoiceField(
choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
label=_('Sales channels'),
help_text=_('This email will only be send to orders from these sales channels. The online shop must be enabled.'),
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
validators=[contains_web_channel_validate],
)
mail_bcc = forms.CharField( mail_bcc = forms.CharField(
label=_("Bcc address"), label=_("Bcc address"),
help_text=_("All emails will be sent to this address as a Bcc copy"), help_text=_("All emails will be sent to this address as a Bcc copy"),

View File

@@ -17,6 +17,7 @@
{% bootstrap_field form.mail_text_signature layout="control" %} {% bootstrap_field form.mail_text_signature layout="control" %}
{% bootstrap_field form.mail_bcc layout="control" %} {% bootstrap_field form.mail_bcc layout="control" %}
{% bootstrap_field form.mail_attach_ical layout="control" %} {% bootstrap_field form.mail_attach_ical layout="control" %}
{% bootstrap_field form.mail_sales_channel_placed_paid layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "E-mail design" %}</legend> <legend>{% trans "E-mail design" %}</legend>
@@ -70,7 +71,7 @@
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %} {% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %} {% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_text_download_reminder_attendee" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee" %} {% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_text_download_reminder_attendee,mail_sales_channel_download_reminder" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee,mail_sales_channel_download_reminder" %}
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %} {% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %} {% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %}

View File

@@ -1,7 +1,7 @@
from decimal import Decimal from decimal import Decimal
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.base.models import Quota from pretix.base.models import Quota
from pretix.base.models.tax import TaxedPrice from pretix.base.models.tax import TaxedPrice

View File

@@ -543,6 +543,13 @@ class DownloadReminderTests(TestCase):
send_download_reminders(sender=self.event) send_download_reminders(sender=self.event)
assert len(djmail.outbox) == 0 assert len(djmail.outbox) == 0
@classscope(attr='o')
def test_not_sent_for_disabled_sales_channel(self):
self.event.settings.mail_days_download_reminder = 2
self.event.settings.mail_sales_channel_download_reminder = []
send_download_reminders(sender=self.event)
assert len(djmail.outbox) == 0
class OrderCancelTests(TestCase): class OrderCancelTests(TestCase):
def setUp(self): def setUp(self):

View File

@@ -8,6 +8,7 @@ from unittest import mock
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.conf import settings from django.conf import settings
from django.core import mail as djmail
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase from django.test import TestCase
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
@@ -2510,6 +2511,33 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
assert not Order.objects.last().testmode assert not Order.objects.last().testmode
assert "0" not in Order.objects.last().code assert "0" not in Order.objects.last().code
def test_receive_order_confirmation_and_paid_mail(self):
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
djmail.outbox = []
oid = _perform_order(self.event, 'manual', [cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
assert len(djmail.outbox) == 1
o = Order.objects.get(pk=oid)
o.payments.first().confirm()
assert len(djmail.outbox) == 2
def test_order_confirmation_and_paid_mail_not_send_on_disabled_sales_channel(self):
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
djmail.outbox = []
self.event.settings.mail_sales_channel_placed_paid = []
oid = _perform_order(self.event, 'manual', [cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
assert len(djmail.outbox) == 0
o = Order.objects.get(pk=oid)
o.payments.first().confirm()
assert len(djmail.outbox) == 0
class QuestionsTestCase(BaseCheckoutTestCase, TestCase): class QuestionsTestCase(BaseCheckoutTestCase, TestCase):