Custom HTML email renderers and new email style (#991)

* Custom HTML email renderers

* Move inline_css call

* Small fixes

* New HTML mail style for pretix

* Thumbs

* Inlinestyle for notifications

* Documentation

* Set line-height
This commit is contained in:
Raphael Michel
2018-08-16 12:01:23 +02:00
committed by GitHub
parent be3b890e2f
commit 4db4790270
27 changed files with 833 additions and 185 deletions

View File

@@ -12,6 +12,7 @@ class PretixBaseConfig(AppConfig):
from . import exporters # NOQA
from . import invoice # NOQA
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
try:

View File

@@ -1,7 +1,18 @@
import logging
from smtplib import SMTPRecipientsRefused, SMTPSenderRefused
import bleach
import markdown
from django.conf import settings
from django.core.mail.backends.smtp import EmailBackend
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.models import Event, Order
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile
logger = logging.getLogger('pretix.base.email')
@@ -24,3 +35,103 @@ class CustomSMTPBackend(EmailBackend):
raise SMTPRecipientsRefused(senderrs)
finally:
self.close()
class BaseHTMLMailRenderer:
"""
This is the base class for all HTML e-mail renderers.
"""
def __init__(self, event: Event):
self.event = event
def __str__(self):
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None) -> str:
"""
This method should generate the HTML part of the email.
:param plain_body: The body of the email in plain text.
:param plain_signature: The signature with event organizer contact details in plain text.
:param subject: The email subject.
:param order: The order if this email is connected to one, otherwise ``None``.
:return: An HTML string
"""
raise NotImplementedError()
@property
def verbose_name(self) -> str:
"""
A human-readable name for this renderer. This should be short but self-explanatory.
"""
raise NotImplementedError() # NOQA
@property
def identifier(self) -> str:
"""
A short and unique identifier for this renderer.
This should only contain lowercase letters and in most cases will be the same as your package name or prefixed
with your package name.
"""
raise NotImplementedError() # NOQA
@property
def thumbnail_filename(self) -> str:
"""
A file name discoverable in the static file storage that contains a preview of your renderer. This should
be with aspect resolution 4:3.
"""
raise NotImplementedError() # NOQA
@property
def is_available(self) -> bool:
"""
This renderer will only be available if this returns ``True``. You can use this to limit this renderer
to certain events. Defaults to ``True``.
"""
return True
class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
@property
def template_name(self):
raise NotImplemented
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
body_md = bleach.linkify(markdown_compile(plain_body))
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
'body': body_md,
'subject': str(subject),
'color': '#8E44B3'
}
if self.event:
htmlctx['event'] = self.event
htmlctx['color'] = self.event.settings.primary_color
if plain_signature:
signature_md = plain_signature.replace('\n', '<br>\n')
signature_md = bleach.linkify(bleach.clean(markdown.markdown(signature_md), tags=bleach.ALLOWED_TAGS + ['p', 'br']))
htmlctx['signature'] = signature_md
if order:
htmlctx['order'] = order
tpl = get_template(self.template_name)
body_html = inline_css(tpl.render(htmlctx))
return body_html
class ClassicMailRenderer(TemplateBasedMailRenderer):
verbose_name = _('pretix default')
identifier = 'classic'
thumbnail_filename = 'pretixbase/email/thumb.png'
template_name = 'pretixbase/email/plainwrapper.html'
@receiver(register_html_mail_renderers, dispatch_uid="pretixbase_email_renderers")
def base_renderers(sender, **kwargs):
return [ClassicMailRenderer]

View File

@@ -19,7 +19,6 @@ from django.utils.timezone import make_aware, now
from django.utils.translation import ugettext_lazy as _
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.email import CustomSMTPBackend
from pretix.base.models.base import LoggedModel
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.validators import EventSlugBlacklistValidator
@@ -327,6 +326,8 @@ class Event(EventMixin, LoggedModel):
Returns an email server connection, either by using the system-wide connection
or by returning a custom one based on the event's settings.
"""
from pretix.base.email import CustomSMTPBackend
if self.settings.smtp_use_custom or force_custom:
return CustomSMTPBackend(host=self.settings.smtp_host,
port=self.settings.smtp_port,
@@ -479,6 +480,31 @@ class Event(EventMixin, LoggedModel):
return OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
def get_html_mail_renderer(self):
"""
Returns the currently selected HTML email renderer
"""
return self.get_html_mail_renderers()[
self.settings.mail_html_renderer
]
def get_html_mail_renderers(self) -> dict:
"""
Returns a dictionary of initialized HTML email renderers mapped by their identifiers.
"""
from ..signals import register_html_mail_renderers
responses = register_html_mail_renderers.send(self)
renderers = {}
for receiver, response in responses:
if not isinstance(response, list):
response = [response]
for p in response:
pp = p(self)
if pp.is_available:
renderers[pp.identifier] = pp
return renderers
def get_invoice_renderers(self) -> dict:
"""
Returns a dictionary of initialized invoice renderers mapped by their identifiers.

View File

@@ -508,7 +508,7 @@ class Order(LoggedModel):
with language(self.locale):
recipient = self.email
try:
email_content = render_mail(template, context)[0]
email_content = render_mail(template, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender,

View File

@@ -2,22 +2,19 @@ import logging
from email.utils import formataddr
from typing import Any, Dict, List, Union
import bleach
import cssutils
import markdown
from celery import chain
from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import get_template
from django.utils.translation import ugettext as _
from i18nfield.strings import LazyI18nString
from inlinestyler.utils import inline_css
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.signals import email_filter
from pretix.base.templatetags.rich_text import markdown_compile
from pretix.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -88,7 +85,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
'invoice_name': '',
'invoice_company': ''
})
body, body_md = render_mail(template, context)
renderer = ClassicMailRenderer(None)
body_plain = render_mail(template, context)
subject = str(subject).format_map(context)
sender = sender or (event.settings.get('mail_from') if event else settings.MAIL_FROM)
if event:
@@ -97,19 +95,11 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
sender = formataddr((settings.PRETIX_INSTANCE_NAME, sender))
subject = str(subject)
body_plain = body
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
'body': body_md,
'color': '#8E44B3'
}
signature = ""
bcc = []
if event:
htmlctx['event'] = event
htmlctx['color'] = event.settings.primary_color
renderer = event.get_html_mail_renderer()
if event.settings.mail_bcc:
bcc.append(event.settings.mail_bcc)
@@ -127,9 +117,6 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
signature = str(event.settings.get('mail_text_signature'))
if signature:
signature = signature.format(event=event.name)
signature_md = signature.replace('\n', '<br>\n')
signature_md = bleach.linkify(bleach.clean(markdown.markdown(signature_md), tags=bleach.ALLOWED_TAGS + ['p', 'br']))
htmlctx['signature'] = signature_md
body_plain += signature
body_plain += "\r\n\r\n-- \r\n"
@@ -137,7 +124,6 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
body_plain += _(
"You are receiving this email because you placed an order for {event}."
).format(event=event.name)
htmlctx['order'] = order
body_plain += "\r\n"
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
@@ -151,8 +137,11 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
)
body_plain += "\r\n"
tpl = get_template('pretixbase/email/plainwrapper.html')
body_html = tpl.render(htmlctx)
try:
body_html = renderer.render(body_plain, signature, str(subject), order)
except:
logger.exception('Could not render HTML body')
body_html = None
send_task = mail_send_task.si(
to=[email],
@@ -182,7 +171,7 @@ def mail_send_task(*args, to: List[str], subject: str, body: str, html: str, sen
order: int=None) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
email.attach_alternative(inline_css(html), "text/html")
email.attach_alternative(html, "text/html")
if invoices:
invoices = Invoice.objects.filter(pk__in=invoices)
for inv in invoices:
@@ -225,5 +214,4 @@ def render_mail(template, context):
else:
tpl = get_template(template)
body = tpl.render(context)
body_md = bleach.linkify(markdown_compile(body))
return body, body_md
return body

View File

@@ -1,5 +1,6 @@
from django.conf import settings
from django.template.loader import get_template
from inlinestyler.utils import inline_css
from pretix.base.i18n import language
from pretix.base.models import LogEntry, NotificationSetting, User
@@ -91,7 +92,7 @@ def send_notification_mail(notification: Notification, user: User):
}
tpl_html = get_template('pretixbase/email/notification.html')
body_html = tpl_html.render(ctx)
body_html = inline_css(tpl_html.render(ctx))
tpl_plain = get_template('pretixbase/email/notification.txt')
body_plain = tpl_plain.render(ctx)

View File

@@ -225,6 +225,10 @@ DEFAULTS = {
'default': None,
'type': LazyI18nString
},
'mail_html_renderer': {
'default': 'classic',
'type': str
},
'mail_prefix': {
'default': None,
'type': str

View File

@@ -124,6 +124,16 @@ subclass of pretix.base.payment.BasePaymentProvider or a list of these
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_html_mail_renderers = EventPluginSignal(
providing_args=[]
)
"""
This signal is sent out to get all known HTML email renderers. Receivers should return a
subclass of pretix.base.email.BaseHTMLMailRenderer or a list of these
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_invoice_renderers = EventPluginSignal(
providing_args=[]
)

View File

@@ -5,163 +5,206 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=false">
</head>
<style type="text/css">
body {
background-color: #e8e8e8;
background-position: top;
background-repeat: repeat-x;
font-family: "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333;
margin: 0;
}
.header h1 {
margin-top: 20px;
margin-bottom: 20px;
}
.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;
}
.footer {
padding: 10px;
text-align: center;
font-size: 12px;
}
.content {
padding: 8px 18px 8px;
}
::selection {
background: {{ color }};
color: #FFF;
}
table.layout {
width: 90%;
max-width: 900px;
border-spacing: 0px;
border-collapse: separate;
margin: auto;
}
.content table {
width: 100%;
}
.content table td {
vertical-align: top;
text-align: left;
padding: 5px 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 }};
}
@media (max-width: 480px) {
.header h1 {
font-size: 19px;
line-height: 24px;
margin: 0 9px 3px 0;
border-radius: 5px 5px;
-webkit-border-radius: 5px 5px;
-moz-border-radius: 5px 5px;
}
.header h1 a {
padding: 3px 9px;
display: block;
}
.header {
<style type="text/css">
body {
background-color: #eee;
background-position: top;
background-repeat: repeat-x;
font-family: "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333;
margin: 0;
padding: 12px 0 8px;
padding-top: 20px;
}
}
td.containertd {
background-color: #FFFFFF;
border: 1px solid #cccccc;
}
table.layout > tr > td {
background-color: white;
padding: 0;
}
{% block addcss %}{% endblock %}
</style>
<body>
<table class="layout">
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 %}
</style>
<!--[if mso]>
<style type="text/css">
body, table, td {
font-family: "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
}
</style>
<![endif]-->
</head>
<body align="center">
<!--[if gte mso 9]>
<table width="100%"><tr><td align="center">
<table width="600"><tr><td align="center"
<![endif]-->
<table class="layout" width="600" border="0" cellspacing="0">
<!--[if !mso]><!-- -->
<tr>
<td class="header" background="">
{% if event %}
<h1><a href="{% abseventurl event "presale:event.index" %}" target="_blank">{{ event.name }}</a></h1>
{% else %}
<h1><a href="{{ site_url }}" target="_blank">{{ site }}</a></h1>
{% endif %}
<td>
<img class="wide" src="data:image/png;base64,
iVBORw0KGgoAAAANSUhEUgAAAlgAAAA8CAAAAACf95tlAAAAAXNCSVQI5gpbmQAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAG/SURBVHja7dvRboMwDIXhvf/DLiQQAwkku9+qDgq2hPyfN6j1qTlx06/uMunbLMnnhL98fuzRDtYILEeZ7GBNwAIWsIB1LdkOVgaWo4gdLAGWo6x2sFZgOUq1g1WB5SjNDlYDlqcEK1dDB5anmK3eE7C4FnIpBNbVFLo7sB7d3huwKFlULGA9pWQJsJxls4G1ActbooWr2IHlLbMFrBlY7rJbwNqBxb2QZ8nAuiUGO9ICLOo71R1YN0X9td8KLJ8ZeDEDrAd+Za3A4mLIz4TAujGqv+tUYPmN4v8LcweW3zS1t++hActzCrtRYD3pMJQOLOeJ7NyBpZFdoWaFDVjuJ6BRswpTBZbCAn5hpsDq/fbHpDMTBZbC1TAzT2ApyMIVsDROQ2GWwFJo8PR2YP3eOtywzwrsGYD1J9vlHXzcmSKw7q/wU2OEwHpdtALHILA00jJfV8DSaVofvYOPlckB658sp/8VNrBkANahqnXqfhhXJgasgymHD8REZwfWmezzga+tQdhcAet0qry1FYV3osD6dP1QJL3YbYUkhfUCsK6einWRPI0pxjROWZbK+QcsAiwCLEKARYBFgEXIu/wAYbjtwujw8KwAAAAASUVORK5CYII="
style="max-height: 60px;">
</td>
</tr>
<!--<![endif]-->
<tr>
<td class="header" align="center">
<!--[if gte mso 9]>
<table cellpadding="20"><tr><td align="center">
<![endif]-->
{% if event %}
<h2><a href="{% abseventurl event "presale:event.index" %}" target="_blank">{{ event.name }}</a>
</h2>
{% else %}
<h2><a href="{{ site_url }}" target="_blank">{{ site }}</a></h2>
{% endif %}
{% block header %}
<h1>{{ subject }}</h1>
{% endblock %}
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td>
</tr>
{% include "pretixbase/email/separator.html" %}
{% block content %}
{% endblock %}
<!--[if !mso]><!-- -->
<tr>
<td class="footer">
<div>
{% include "pretixbase/email/email_footer.html" %}
</div>
<td>
<br>
<img class="wide" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAA8CAYAAAC6nMS5AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAPnSURBVHic7d3dbuJIEAbQsg2Ecd7/TQeDf3svVuFmdjJLxsGm+xwJKXcpqS76U3VTVCmlFAAArKbeugAAgNwIWAAAKxOwAABWJmABAKxMwAIAWNlh6wIAIiKWZYllWWKe5/vfEREppfsnIqKqqvsnIqKu66jrOpqmuf8NsDUBC3i6lFJM0xTjOMY0TbEsS6y1MaaqqqjrOg6HQxyPxzgcDvcwBvAslT1YwDN8BKpxHGOe56f+76Zp4ng83gMXwHcTsIBvsyxLDMMQfd/fr/y2Vtd1nE6nOJ1O0TTN1uUAmRKwgNV9hKppmrYu5VOHwyHO53Mcj8etSwEyI2ABqxmGIW6329OvAP9WXddxPp/j7e1t61KATAhYwF/r+z5ut9turgG/StAC1iJgAV82z3N0Xbf7q8BHNU0Tbdt6EA98mYAFPCylFNfrNfq+37qUb3U6naJtW2segIcJWMBDhmGIrutW21u1d1VVRdu2cTqdti4FeCECFvC/lDK1+h3TLOARAhbwR/M8x+VyeblvB66taZp4f3+3Pwv4IwEL+NQ4jnG5XIq5EvyTqqri/f3d7izgUwIW8FvDMMTlctm6jF1q29Y6B+C3BCzgP91ut7her1uXsWvn8zl+/PixdRnADglYwC+6riv2Mfuj3t7eom3brcsAdqbeugBgX4Srx/R9H13XbV0GsDMCFnB3u92Eqy/4+KkggA8CFhAR/z5o9+bq60reEQb8SsAC7qsY+Dtd18U4jluXAeyAgAWFW5ZFuFqRhaxAhIAFxfv586cloitKKVnMCghYULKu60xbvsE8z96zQeEELCjUOI4eZX+jvu+9x4KCCVhQoI9rLL6Xq0Iol4AFBbperw7+J0gpuSqEQglYUJh5nl0NPlHf9zFN09ZlAE8mYEFh/KzL85liQXkELCiIaco2pmmKYRi2LgN4IgELCuL38rZjigVlEbCgEMMwxLIsW5dRrGVZTLGgIAIWFML0ant6AOUQsKAA4zja2L4D8zxbPgqFELCgACYn+6EXUAYBCzK3LItvDu7INE3ewkEBBCzInIfV+6MnkD8BCzLnMN8fPYH8CViQsXmePW7fIX2B/AlYkDGTkv3SG8ibgAUZ87h9v/QG8iZgQaZSSg7xHZumKVJKW5cBfBMBCzIlXO2fHkG+BCzIlMN7//QI8iVgQaYc3vunR5AvAQsyZQ3A/tnoDvkSsCBDKSUPqF/Asiz6BJkSsCBDplevQ68gTwIWZMjV0+vQK8iTgAUZMhV5HXoFeRKwIEPe9bwOvYI8CViQIYf269AryJOABQCwMgELMmQq8jr0CvIkYEGGHNqvQ68gT/8AETAn3pyLgvsAAAAASUVORK5CYII="
style="max-height: 60px;">
</td>
</tr>
<!--<![endif]-->
</table>
<div class="footer">
{% include "pretixbase/email/email_footer.html" %}
</div>
<br/>
<br/>
<!--[if gte mso 9]>
</td></tr></table>
</td></tr></table>
<![endif]-->
</body>
</html>

View File

@@ -1,15 +1,20 @@
{% extends "pretixbase/email/base.html" %}
{% load eventurl %}
{% load i18n %}
{% block header %}
<h1>
{% if notification.url %}<a href="{{ notification.url }}">{% endif %}
{{ notification.title }}
{% if notification.url %}</a>{% endif %}
</h1>
{% endblock %}
{% block content %}
<tr>
<td class="containertd">
<!--[if gte mso 9]>
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
<h3>
{% if notification.url %}<a href="{{ notification.url }}">{% endif %}
{{ notification.title }}
{% if notification.url %}</a>{% endif %}
</h3>
{% if notification.detail %}
<p>{{ notification.detail }}</p>
{% endif %}
@@ -35,10 +40,17 @@
</p>
{% endif %}
</div>
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td>
</tr>
{% include "pretixbase/email/separator.html" %}
<tr>
<td class="containertd">
<!--[if gte mso 9]>
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% trans "You receive these emails based on your notification settings." %}<br>
<a href="{{ settings_url }}">
@@ -48,6 +60,9 @@
{% trans "Click here disable all notifications immediately." %}
</a>
</div>
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td>
</tr>
{% endblock %}

View File

@@ -4,17 +4,24 @@
{% block content %}
<tr>
<td class="containertd">
<!--[if gte mso 9]>
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{{ body|safe }}
</div>
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td>
</tr>
{% if order %}
<tr>
<td class="gap"></td>
</tr>
{% include "pretixbase/email/separator.html" %}
<tr>
<td class="order containertd">
<!--[if gte mso 9]>
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
@@ -24,18 +31,25 @@
{% trans "View order details" %}
</a>
</div>
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td>
</tr>
{% endif %}
{% if signature %}
<tr>
<td class="gap"></td>
</tr>
{% include "pretixbase/email/separator.html" %}
<tr>
<td class="order containertd">
<!--[if gte mso 9]>
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{{ signature | safe }}
</div>
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td>
</tr>
{% endif %}

View File

@@ -0,0 +1,15 @@
<!--[if !mso]><!-- -->
<tr>
<td>
<img class="wide" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAAoBAMAAADQ9ZkHAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAC1QTFRF7u7u7+/v8PDw8fHx8vLy9PT09fX19/f3+Pj4+fn5+vr6/Pz8/f39/v7+////BLnnfgAAAKJJREFUaN7t1cGtgUEARtFfQkREDypQihpUoIQXpahADUqRiIgINdjehcXbSTjf8s5mchYzw9P+vQEBLFiwYMGChQAWLFiwYMFCAAsWLFhfipX9pe/S1+nH9EX6OX2Wfk2fpN/TR73QMgeH9E36Nn2fvko/pc/TL+nT9Fv6OP0xvB8sWLA+g+XZ9hvCggULFiwEsGDBggULFgJYsGDBgvXzewGTOlWA3NB0eQAAAABJRU5ErkJggg=="
style="max-height: 40px;">
</td>
</tr>
<!--<![endif]-->
<!--[if gte mso 9]>
<tr>
<td background="white" style="background-color: white;">
<hr background="white" style="background-color: white;"/>
</td>
</tr>
<![endif]-->

View File

@@ -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'):

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load static %}
{% block inside %}
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"
mail-preview-url="{% url "control:event.settings.mail.preview" event=request.event.slug organizer=request.event.organizer.slug %}">
@@ -13,6 +14,27 @@
{% bootstrap_field form.mail_text_signature layout="control" %}
{% bootstrap_field form.mail_bcc layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "E-mail design" %}</legend>
<div class="row">
{% for r in renderers.values %}
<div class="col-md-3">
<div class="well maildesignpreview text-center">
<label class="radio">
<input type="radio" name="mail_html_renderer" value="{{ r.identifier }}"
{% if request.event.settings.mail_html_renderer == r.identifier %}checked{% endif %}>
{{ r.verbose_name }}
</label>
<img src="{% static r.thumbnail_filename %}">
<a class="btn btn-default btn-sm" target="_blank"
href="{% url "control:event.settings.mail.preview.layout" event=request.event.slug organizer=request.event.organizer.slug %}?renderer={{ r.identifier }}">
{% trans "Preview" %}
</a>
</div>
</div>
{% endfor %}
</div>
</fieldset>
<fieldset>
<legend>{% trans "E-mail content" %}</legend>
<div class="panel-group" id="questions_group">

View File

@@ -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'),

View File

@@ -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:

View File

@@ -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(

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="600"
height="40"
viewBox="0 0 158.75 10.583334"
version="1.1"
id="svg8"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="divider.svg"
inkscape:export-filename="/home/raphael/proj/pretix/src/pretix/static/pretixbase/email/divider.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4"
inkscape:cx="270.51214"
inkscape:cy="55.592654"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:pagecheckerboard="true"
inkscape:window-width="1916"
inkscape:window-height="1023"
inkscape:window-x="1920"
inkscape:window-y="36"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-286.41665)">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.27458763;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4.54917542, 9.09835082000000028;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
id="rect1448"
width="158.75"
height="10.583334"
x="0"
y="286.41666" />
<path
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#eeeeee;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.39300003;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:8.78600006, 17.57200011000000117;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
d="M 0 17.986328 L 0 22.009766 L 22.455078 22.009766 L 22.455078 17.986328 L 0 17.986328 z M 46.601562 17.986328 L 46.601562 22.009766 L 70.755859 22.009766 L 70.755859 17.986328 L 46.601562 17.986328 z M 94.908203 17.986328 L 94.908203 22.009766 L 119.05469 22.009766 L 119.05469 17.986328 L 94.908203 17.986328 z M 143.20898 17.986328 L 143.20898 22.009766 L 167.36133 22.009766 L 167.36133 17.986328 L 143.20898 17.986328 z M 191.50781 17.986328 L 191.50781 22.009766 L 215.66211 22.009766 L 215.66211 17.986328 L 191.50781 17.986328 z M 239.81641 17.986328 L 239.81641 22.009766 L 263.96094 22.009766 L 263.96094 17.986328 L 239.81641 17.986328 z M 288.11523 17.986328 L 288.11523 22.009766 L 312.26953 22.009766 L 312.26953 17.986328 L 288.11523 17.986328 z M 336.42188 17.986328 L 336.42188 22.009766 L 360.56836 22.009766 L 360.56836 17.986328 L 336.42188 17.986328 z M 384.72266 17.986328 L 384.72266 22.009766 L 408.875 22.009766 L 408.875 17.986328 L 384.72266 17.986328 z M 433.02148 17.986328 L 433.02148 22.009766 L 457.17578 22.009766 L 457.17578 17.986328 L 433.02148 17.986328 z M 481.32812 17.986328 L 481.32812 22.009766 L 505.47461 22.009766 L 505.47461 17.986328 L 481.32812 17.986328 z M 529.62891 17.986328 L 529.62891 22.009766 L 553.78125 22.009766 L 553.78125 17.986328 L 529.62891 17.986328 z M 577.92773 17.986328 L 577.92773 22.009766 L 600 22.009766 L 600 17.986328 L 577.92773 17.986328 z "
transform="matrix(0.26458334,0,0,0.26458334,0,286.41665)"
id="path1445" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="600"
height="60"
viewBox="0 0 158.75 15.875001"
version="1.1"
id="svg8"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="foot.svg"
inkscape:export-filename="/home/raphael/proj/pretix/src/pretix/static/pretixbase/email/foot.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.9566667"
inkscape:cx="157.92164"
inkscape:cy="20.329242"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:pagecheckerboard="true"
inkscape:window-width="1916"
inkscape:window-height="1005"
inkscape:window-x="1920"
inkscape:window-y="54"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-281.12498)">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.78578949;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:5.57157912, 11.14315822;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
id="rect1448"
width="158.75"
height="15.875001"
x="-158.75"
y="281.12497"
transform="scale(-1,1)" />
<path
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#eeeeee;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.3798275;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:10.75965491, 21.5193097;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
d="M 300 6.15625 A 53.790624 53.790643 0 0 0 246.20703 59.949219 A 53.790624 53.790643 0 0 0 246.21094 60 L 353.78906 60 A 53.790624 53.790643 0 0 0 353.79297 59.949219 A 53.790624 53.790643 0 0 0 300 6.15625 z "
transform="matrix(0.26458334,0,0,0.26458334,0,281.12498)"
id="path1417" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="600"
height="60"
viewBox="0 0 158.75 15.875001"
version="1.1"
id="svg8"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="head.svg"
inkscape:export-filename="/home/raphael/proj/pretix/src/pretix/static/pretixbase/email/head.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.9566667"
inkscape:cx="300"
inkscape:cy="20.329242"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:pagecheckerboard="true"
inkscape:window-width="1916"
inkscape:window-height="1023"
inkscape:window-x="1920"
inkscape:window-y="36"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-281.12498)">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.78578949;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:5.57157912, 11.14315822;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
id="rect1448"
width="158.75"
height="15.875001"
x="-158.75"
y="281.125"
transform="scale(-1,1)" />
<path
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#eeeeee;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.42341268;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.84682543, 5.69365083;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
d="m 79.375002,295.3711 a 14.232103,14.232108 0 0 1 -14.232724,-14.23272 14.232103,14.232108 0 0 1 10e-4,-0.0134 h 28.463378 a 14.232103,14.232108 0 0 1 0.001,0.0134 14.232103,14.232108 0 0 1 -14.232724,14.23272 z"
id="path1417"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -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;
}
}