diff --git a/doc/development/api/email.rst b/doc/development/api/email.rst new file mode 100644 index 0000000000..d0e219207f --- /dev/null +++ b/doc/development/api/email.rst @@ -0,0 +1,109 @@ +.. highlight:: python + :linenothreshold: 5 + +Writing an HTML e-mail renderer plugin +====================================== + +An email renderer class controls how the HTML part of e-mails sent by pretix is built. +The creation of such a plugin is very similar to creating an export output. + +Please read :ref:`Creating a plugin ` first, if you haven't already. + +Output registration +------------------- + +The email HTML renderer API does not make a lot of usage from signals, however, it +does use a signal to get a list of all available email renderers. Your plugin +should listen for this signal and return the subclass of ``pretix.base.email.BaseHTMLMailRenderer`` +that we'll provide in this plugin:: + + from django.dispatch import receiver + + from pretix.base.signals import register_html_mail_renderers + + + @receiver(register_html_mail_renderers, dispatch_uid="renderer_custom") + def register_mail_renderers(sender, **kwargs): + from .email import MyMailRenderer + return MyMailRenderer + + +The renderer class +------------------ + +.. class:: pretix.base.email.BaseHTMLMailRenderer + + The central object of each email renderer is the subclass of ``BaseHTMLMailRenderer``. + + .. py:attribute:: BaseHTMLMailRenderer.event + + The default constructor sets this property to the event we are currently + working for. + + .. autoattribute:: identifier + + This is an abstract attribute, you **must** override this! + + .. autoattribute:: verbose_name + + This is an abstract attribute, you **must** override this! + + .. autoattribute:: thumbnail_filename + + This is an abstract attribute, you **must** override this! + + .. autoattribute:: is_available + + .. automethod:: render + + This is an abstract method, you **must** implement this! + +Helper class for template-base renderers +---------------------------------------- + +The email renderer that ships with pretix is based on Django templates to generate HTML. +In case you also want to render emails based on a template, we provided a ready-made base +class ``TemplateBasedMailRenderer`` that you can re-use to perform the following steps: + +* Convert the body text and the signature to HTML using our markdown renderer + +* Render the template + +* Call `inlinestyler`_ to convert all `` - - + table.layout > tr > td.header { + padding: 0 20px; + text-align: center; + } + + .header h2 { + margin-top: 20px; + margin-bottom: 10px; + font-size: 22px; + line-height: 26px; + } + + .header h1 { + margin-top: 0; + margin-bottom: 20px; + font-size: 26px; + line-height: 30px; + } + + .header h2 a, .header h1 a, .content h2 a, .content h3 a { + text-decoration: none; + } + + .content h2, .content h3 { + margin-bottom: 20px; + margin-top: 10px; + } + + a { + color: {{ color }}; + font-weight: bold; + } + + a:hover, a:focus { + color: {{ color }}; + text-decoration: underline; + } + + a:hover, a:active { + outline: 0; + } + + p { + margin: 0 0 10px; + + /* These are technically the same, but use both */ + overflow-wrap: break-word; + word-wrap: break-word; + + -ms-word-break: break-all; + /* This is the dangerous one in WebKit, as it breaks things wherever */ + word-break: break-all; + /* Instead use this non-standard one: */ + word-break: break-word; + + /* Adds a hyphen where the word breaks, if supported (No Blink) */ + -ms-hyphens: auto; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; + } + p:last-child { + margin-bottom: 0; + } + + .footer { + padding: 10px; + text-align: center; + font-size: 12px; + } + + .content { + padding: 0 18px; + } + + ::selection { + background: {{ color }}; + color: #FFF; + } + + table.layout { + width: 100%; + max-width: 600px; + border-spacing: 0px; + border-collapse: separate; + margin: auto; + } + + img.wide { + width: 100%; + height: auto; + } + + .content table { + width: 100%; + } + + .content table td { + vertical-align: top; + text-align: left; + padding: 0; + } + + a.button { + display: inline-block; + padding: 10px 16px; + font-size: 14px; + line-height: 1.33333; + border: 1px solid #cccccc; + border-radius: 6px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + margin: 5px; + text-decoration: none; + color: {{ color }}; + } + + {% block addcss %}{% endblock %} + + + + + +
+ - + + + + + {% include "pretixbase/email/separator.html" %} {% block content %} {% endblock %} + - +
- {% if event %} -

{{ event.name }}

- {% else %} -

{{ site }}

- {% endif %} +
+
+ + {% if event %} +

{{ event.name }} +

+ {% else %} +

{{ site }}

+ {% endif %} + {% block header %} +

{{ subject }}

+ {% endblock %} + +
+
+
+

+ diff --git a/src/pretix/base/templates/pretixbase/email/notification.html b/src/pretix/base/templates/pretixbase/email/notification.html index b0b48ecd2c..1f70849460 100644 --- a/src/pretix/base/templates/pretixbase/email/notification.html +++ b/src/pretix/base/templates/pretixbase/email/notification.html @@ -1,15 +1,20 @@ {% extends "pretixbase/email/base.html" %} {% load eventurl %} {% load i18n %} +{% block header %} +

+ {% if notification.url %}{% endif %} + {{ notification.title }} + {% if notification.url %}{% endif %} +

+{% endblock %} {% block content %} +
-

- {% if notification.url %}{% endif %} - {{ notification.title }} - {% if notification.url %}{% endif %} -

{% if notification.detail %}

{{ notification.detail }}

{% endif %} @@ -35,10 +40,17 @@

{% endif %}
+ + {% include "pretixbase/email/separator.html" %} +
{% trans "You receive these emails based on your notification settings." %}
@@ -48,6 +60,9 @@ {% trans "Click here disable all notifications immediately." %}
+ {% endblock %} diff --git a/src/pretix/base/templates/pretixbase/email/plainwrapper.html b/src/pretix/base/templates/pretixbase/email/plainwrapper.html index db8dc8eb49..264dac759a 100644 --- a/src/pretix/base/templates/pretixbase/email/plainwrapper.html +++ b/src/pretix/base/templates/pretixbase/email/plainwrapper.html @@ -4,17 +4,24 @@ {% block content %} +
{{ body|safe }}
+ {% if order %} - - - + {% include "pretixbase/email/separator.html" %} +
{% trans "You are receiving this email because you placed an order for the following event:" %}
{% trans "Event:" %} {{ event.name }}
@@ -24,18 +31,25 @@ {% trans "View order details" %}
+ {% endif %} {% if signature %} - - - + {% include "pretixbase/email/separator.html" %} +
{{ signature | safe }}
+ {% endif %} diff --git a/src/pretix/base/templates/pretixbase/email/separator.html b/src/pretix/base/templates/pretixbase/email/separator.html new file mode 100644 index 0000000000..e3a2073b08 --- /dev/null +++ b/src/pretix/base/templates/pretixbase/email/separator.html @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index a64a17897a..2d9a7a48f3 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -688,6 +688,11 @@ class MailSettingsForm(SettingsForm): ) }} ) + mail_html_renderer = forms.ChoiceField( + label=_("HTML mail renderer"), + required=True, + choices=[] + ) mail_text_order_placed = I18nFormField( label=_("Text"), @@ -850,6 +855,13 @@ class MailSettingsForm(SettingsForm): required=False ) + def __init__(self, *args, **kwargs): + event = kwargs.get('obj') + super().__init__(*args, **kwargs) + self.fields['mail_html_renderer'].choices = [ + (r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values() + ] + def clean(self): data = self.cleaned_data if not data.get('smtp_password') and data.get('smtp_username'): diff --git a/src/pretix/control/templates/pretixcontrol/event/mail.html b/src/pretix/control/templates/pretixcontrol/event/mail.html index 2e2350117a..6cdb0654bc 100644 --- a/src/pretix/control/templates/pretixcontrol/event/mail.html +++ b/src/pretix/control/templates/pretixcontrol/event/mail.html @@ -1,6 +1,7 @@ {% extends "pretixcontrol/event/settings_base.html" %} {% load i18n %} {% load bootstrap3 %} +{% load static %} {% block inside %}
@@ -13,6 +14,27 @@ {% bootstrap_field form.mail_text_signature layout="control" %} {% bootstrap_field form.mail_bcc layout="control" %} +
+ {% trans "E-mail design" %} +
+ {% for r in renderers.values %} +
+
+ + + + {% trans "Preview" %} + +
+
+ {% endfor %} +
+
{% trans "E-mail content" %}
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index a62c02e50c..b6e99ee123 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -103,6 +103,8 @@ urlpatterns = [ name='event.settings.tickets.preview'), url(r'^settings/email$', event.MailSettings.as_view(), name='event.settings.mail'), url(r'^settings/email/preview$', event.MailSettingsPreview.as_view(), name='event.settings.mail.preview'), + url(r'^settings/email/layoutpreview$', event.MailSettingsRendererPreview.as_view(), + name='event.settings.mail.preview.layout'), url(r'^settings/invoice$', event.InvoiceSettings.as_view(), name='event.settings.invoice'), url(r'^settings/invoice/preview$', event.InvoicePreview.as_view(), name='event.settings.invoice.preview'), url(r'^settings/display', event.DisplaySettings.as_view(), name='event.settings.display'), diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 3e090b0003..8053566239 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -47,6 +47,7 @@ from pretix.control.forms.event import ( ) from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.signals import nav_event_settings +from pretix.helpers.database import rolledback_transaction from pretix.helpers.urls import build_absolute_uri from pretix.multidomain.urlreverse import get_domain from pretix.plugins.stripe.payment import StripeSettingsHolder @@ -462,6 +463,11 @@ class MailSettings(EventSettingsViewMixin, EventSettingsFormView): 'event': self.request.event.slug }) + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx['renderers'] = self.request.event.get_html_mail_renderers() + return ctx + @transaction.atomic def post(self, request, *args, **kwargs): form = self.get_form() @@ -514,7 +520,8 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View): 'date': date_format(now() + timedelta(days=7), 'SHORT_DATE_FORMAT'), 'expire_date': date_format(now() + timedelta(days=15), 'SHORT_DATE_FORMAT'), 'payment_info': _('{} has been transferred to account <9999-9999-9999-9999> at {}').format( - money_filter(Decimal('42.23'), self.request.event.currency), date_format(now(), 'SHORT_DATETIME_FORMAT')) + money_filter(Decimal('42.23'), self.request.event.currency), + date_format(now(), 'SHORT_DATETIME_FORMAT')) } # create index-language mapping @@ -614,6 +621,36 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View): }) +class MailSettingsRendererPreview(MailSettingsPreview): + permission = 'can_change_event_settings' + + def post(self, request, *args, **kwargs): + return HttpResponse(status=405) + + def get(self, request, *args, **kwargs): + v = str(request.event.settings.mail_text_order_placed) + v = v.format_map(self.placeholders('mail_text_order_placed')) + renderers = request.event.get_html_mail_renderers() + if request.GET.get('renderer') in renderers: + with rolledback_transaction(): + order = request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(), + expires=now(), code="PREVIEW", total=119) + item = request.event.items.create(name=ugettext("Sample product"), default_price=42.23, + description=ugettext("Sample product description")) + order.positions.create(item=item, attendee_name=ugettext("John Doe"), price=item.default_price) + v = renderers[request.GET.get('renderer')].render( + v, + str(request.event.settings.mail_text_signature), + ugettext('Your order: %(code)s') % {'code': order.code}, + order + ) + r = HttpResponse(v, content_type='text/html') + r._csp_ignore = True + return r + else: + raise Http404(_('Unknown e-mail renderer.')) + + class TicketSettingsPreview(EventPermissionRequiredMixin, View): permission = 'can_change_event_settings' @@ -1169,7 +1206,8 @@ class QuickSetupView(FormView): data={'plugin': 'pretix.plugins.banktransfer'}) plugins_active.append('pretix.plugins.banktransfer') self.request.event.settings.payment_banktransfer__enabled = True - self.request.event.settings.payment_banktransfer_bank_details = form.cleaned_data['payment_banktransfer_bank_details'] + self.request.event.settings.payment_banktransfer_bank_details = form.cleaned_data[ + 'payment_banktransfer_bank_details'] if form.cleaned_data.get('payment_stripe__enabled', None): if 'pretix.plugins.stripe' not in plugins_active: diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 2ac0352de1..108ad243a4 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -1286,7 +1286,7 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView): 'invoice_company': invoice_company, } email_template = LazyI18nString(form.cleaned_data['message']) - email_content = render_mail(email_template, email_context)[0] + email_content = render_mail(email_template, email_context) if self.request.POST.get('action') == 'preview': self.preview_output = [] self.preview_output.append( diff --git a/src/pretix/static/pretixbase/email/divider.png b/src/pretix/static/pretixbase/email/divider.png new file mode 100644 index 0000000000..45ec64c0e1 Binary files /dev/null and b/src/pretix/static/pretixbase/email/divider.png differ diff --git a/src/pretix/static/pretixbase/email/divider.svg b/src/pretix/static/pretixbase/email/divider.svg new file mode 100644 index 0000000000..d098dc4529 --- /dev/null +++ b/src/pretix/static/pretixbase/email/divider.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/pretix/static/pretixbase/email/foot.png b/src/pretix/static/pretixbase/email/foot.png new file mode 100644 index 0000000000..a9efb6555a Binary files /dev/null and b/src/pretix/static/pretixbase/email/foot.png differ diff --git a/src/pretix/static/pretixbase/email/foot.svg b/src/pretix/static/pretixbase/email/foot.svg new file mode 100644 index 0000000000..9874f35704 --- /dev/null +++ b/src/pretix/static/pretixbase/email/foot.svg @@ -0,0 +1,75 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/pretix/static/pretixbase/email/head.png b/src/pretix/static/pretixbase/email/head.png new file mode 100644 index 0000000000..5b0dc3a7e5 Binary files /dev/null and b/src/pretix/static/pretixbase/email/head.png differ diff --git a/src/pretix/static/pretixbase/email/head.svg b/src/pretix/static/pretixbase/email/head.svg new file mode 100644 index 0000000000..6100087c5b --- /dev/null +++ b/src/pretix/static/pretixbase/email/head.svg @@ -0,0 +1,75 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/pretix/static/pretixbase/email/thumb.png b/src/pretix/static/pretixbase/email/thumb.png new file mode 100644 index 0000000000..813b47b668 Binary files /dev/null and b/src/pretix/static/pretixbase/email/thumb.png differ diff --git a/src/pretix/static/pretixcontrol/scss/_forms.scss b/src/pretix/static/pretixcontrol/scss/_forms.scss index 3ebb48b734..839f6d9174 100644 --- a/src/pretix/static/pretixcontrol/scss/_forms.scss +++ b/src/pretix/static/pretixcontrol/scss/_forms.scss @@ -354,3 +354,15 @@ table td > .checkbox input[type="checkbox"] { top: 2px; } } +.maildesignpreview { + label { + display: block; + } + img { + display: block; + width: 90%; + margin: 10px auto; + height: auto; + box-shadow: 0 1px 3px 0 #aaa; + } +}