Implement notifications for admin users (#700)

* First stab at notification settings

* Add "global" setting for notification levels

* Trigger notification task

* Get users with permission for event

* Actually send notification emails

* More notifications

* Allow to turn off notifications

* Link in email to pause all notifications

* Add NotificationType to wordlist

* Add notification tests

* Add documentation

* Rebase fixes
This commit is contained in:
Raphael Michel
2017-12-14 22:06:08 +01:00
committed by GitHub
parent f0a1397eea
commit 128203800c
28 changed files with 1363 additions and 172 deletions

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-12-06 16:03
from __future__ import unicode_literals
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.auth
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0077_auto_20171124_1629'),
]
operations = [
migrations.CreateModel(
name='NotificationSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action_type', models.CharField(max_length=255)),
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('enabled', models.BooleanField(default=True)),
],
),
migrations.AlterUniqueTogether(
name='notificationsetting',
unique_together=set([('user', 'action_type', 'event', 'method')]),
),
migrations.AddField(
model_name='logentry',
name='visible',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='notificationsetting',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notification_settings', to='pretixbase.Event'),
),
migrations.AlterField(
model_name='notificationsetting',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_settings', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='user',
name='notifications_send',
field=models.BooleanField(default=True, help_text='If turned off, you will not get any notifications.', verbose_name='Receive notifications according to my settings below'),
),
migrations.AddField(
model_name='user',
name='notifications_token',
field=models.CharField(default=pretix.base.models.auth.generate_notifications_token, max_length=255),
),
]

View File

@@ -12,6 +12,7 @@ from .items import (
Quota, SubEventItem, SubEventItemVariation, itempicture_upload_to,
)
from .log import LogEntry
from .notifications import NotificationSetting
from .orders import (
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
InvoiceAddress, Order, OrderPosition, QuestionAnswer,

View File

@@ -7,6 +7,7 @@ from django.contrib.auth.models import (
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from django_otp.models import Device
@@ -40,6 +41,10 @@ class UserManager(BaseUserManager):
return user
def generate_notifications_token():
return get_random_string(length=32)
class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
"""
This is the user model used by pretix for authentication.
@@ -81,6 +86,12 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
default=settings.TIME_ZONE,
verbose_name=_('Timezone'))
require_2fa = models.BooleanField(default=False)
notifications_send = models.BooleanField(
default=True,
verbose_name=_('Receive notifications according to my settings below'),
help_text=_('If turned off, you will not get any notifications.')
)
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
objects = UserManager()

View File

@@ -47,6 +47,8 @@ class LoggingMixin:
"""
from .log import LogEntry
from .event import Event
from ..notifications import get_all_notification_types
from ..services.notifications import notify
event = None
if isinstance(self, Event):
@@ -60,6 +62,9 @@ class LoggingMixin:
logentry.data = json.dumps(data, cls=CustomJSONEncoder)
logentry.save()
if action in get_all_notification_types():
notify.apply_async(args=(logentry.pk,))
class LoggedModel(models.Model, LoggingMixin):

View File

@@ -10,7 +10,7 @@ from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Q
from django.db.models import Exists, OuterRef, Q
from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -26,7 +26,7 @@ from pretix.helpers.daterange import daterange
from pretix.helpers.json import safe_string
from ..settings import settings_hierarkey
from .organizer import Organizer
from .organizer import Organizer, Team
class EventMixin:
@@ -511,6 +511,39 @@ class Event(EventMixin, LoggedModel):
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
return data
def get_users_with_any_permission(self):
"""
Returns a queryset of users who have any permission to this event.
:return: Iterable of User
"""
return self.get_users_with_permission(None)
def get_users_with_permission(self, permission):
"""
Returns a queryset of users who have a specific permission to this event.
:return: Iterable of User
"""
from .auth import User
if permission:
kwargs = {permission: True}
else:
kwargs = {}
team_with_perm = Team.objects.filter(
members__pk=OuterRef('pk'),
organizer=self.organizer,
**kwargs
).filter(
Q(all_events=True) | Q(limit_events__pk=self.pk)
)
return User.objects.annotate(twp=Exists(team_with_perm)).filter(
Q(is_superuser=True) | Q(twp=True)
)
class SubEvent(EventMixin, LoggedModel):
"""
@@ -665,6 +698,21 @@ class RequiredAction(models.Model):
return response
return self.action_type
def save(self, *args, **kwargs):
created = not self.pk
super().save(*args, **kwargs)
if created:
from .log import LogEntry
from ..services.notifications import notify
logentry = LogEntry.objects.create(
content_object=self,
action_type='pretix.event.action_required',
event=self.event,
visible=False
)
notify.apply_async(args=(logentry.pk,))
class EventMetaProperty(LoggedModel):
"""

View File

@@ -11,6 +11,11 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.signals import logentry_object_link
class VisibleOnlyManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(visible=True)
class LogEntry(models.Model):
"""
Represents a change or action that has been performed on another object
@@ -39,6 +44,10 @@ class LogEntry(models.Model):
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.CASCADE)
action_type = models.CharField(max_length=255)
data = models.TextField(default='{}')
visible = models.BooleanField(default=True)
objects = VisibleOnlyManager()
all = models.Manager()
class Meta:
ordering = ('-datetime',)

View File

@@ -0,0 +1,36 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
class NotificationSetting(models.Model):
"""
Stores that a user wants to get notifications of a certain type via a certain
method for a certain event. If event is None, the notification shall be sent
for all events the user has access to.
:param user: The user to nofify.
:type user: User
:param action_type: The type of action to notify for.
:type action_type: str
:param event: The event to notify for.
:type event: Event
:param method: The method to notify with.
:type method: str
:param enabled: Indicates whether the specified notification is enabled. If no
event is set, this must always be true. If no event is set, setting
this to false is equivalent to deleting the object.
:type enabled: bool
"""
CHANNELS = (
('mail', _('E-mail')),
)
user = models.ForeignKey('User', on_delete=models.CASCADE,
related_name='notification_settings')
action_type = models.CharField(max_length=255)
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.CASCADE,
related_name='notification_settings')
method = models.CharField(max_length=255, choices=CHANNELS)
enabled = models.BooleanField(default=True)
class Meta:
unique_together = ('user', 'action_type', 'event', 'method')

View File

@@ -0,0 +1,233 @@
import logging
from collections import OrderedDict, namedtuple
from django.dispatch import receiver
from django.utils.formats import date_format, localize
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event, LogEntry
from pretix.base.signals import register_notification_types
from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger(__name__)
_ALL_TYPES = None
NotificationAttribute = namedtuple('NotificationAttribute', ('title', 'value'))
NotificationAction = namedtuple('NotificationAction', ('label', 'url'))
class Notification:
"""
Represents a notification that is sent/shown to a user. A notification consists of:
* one ``event`` reference
* one ``title`` text that is shown e.g. in the email subject or in a headline
* optionally one ``detail`` text that may or may not be shown depending on the notification method
* optionally one ``url`` that should be absolute and point to the context of an notification (e.g. an order)
* optionally a number of attributes consisting of a title and a value that can be used to add additional details
to the notification (e.g. "Customer: ABC")
* optionally a number of actions that may or may not be shown as buttons depending on the notification method,
each consisting of a button label and an absolute URL to point to.
"""
def __init__(self, event: Event, title: str, detail: str=None, url: str=None):
self.title = title
self.event = event
self.detail = detail
self.url = url
self.attributes = []
self.actions = []
def add_action(self, label, url):
"""
Add an action to the notification, defined by a label and an url. An example could be a label of "View order"
and an url linking to the order detail page.
"""
self.actions.append(NotificationAction(label, url))
def add_attribute(self, title, value):
"""
Add an attribute to the notification, defined by a title and a value. An example could be a title of
"Date" and a value of "2017-12-14".
"""
self.attributes.append(NotificationAttribute(title, value))
class NotificationType:
def __init__(self, event: Event = None):
self.event = event
def __repr__(self):
return '<NotificationType: {}>'.format(self.action_type)
@property
def action_type(self) -> str:
"""
The action_type string that this notification handles, for example
``"pretix.event.order.paid"``. Only one notification type should be registered
per action type.
"""
raise NotImplementedError() # NOQA
@property
def verbose_name(self) -> str:
"""
A human-readable name of this notification type.
"""
raise NotImplementedError() # NOQA
@property
def required_permission(self) -> str:
"""
The permission a user needs to hold for the related event to receive this
notification.
"""
raise NotImplementedError() # NOQA
def build_notification(self, logentry: LogEntry) -> Notification:
"""
This is the main function that you should override. It is supposed to turn a log entry
object into a notification object that can then be rendered e.g. into an email.
"""
return Notification(
logentry.event,
logentry.display()
)
def get_all_notification_types(event=None):
global _ALL_TYPES
if event is None and _ALL_TYPES:
return _ALL_TYPES
types = OrderedDict()
for recv, ret in register_notification_types.send(event):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.action_type] = r
else:
types[ret.action_type] = ret
if event is None:
_ALL_TYPES = types
return types
class ActionRequiredNotificationType(NotificationType):
required_permission = "can_change_orders"
action_type = "pretix.event.action_required"
verbose_name = _("Administrative action required")
def build_notification(self, logentry: LogEntry):
control_url = build_absolute_uri(
'control:event.requiredactions',
kwargs={
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
}
)
n = Notification(
event=logentry.event,
title=_('Administrative action required'),
detail=_('Something happened in your event that our system cannot handle automatically, e.g. an external '
'refund. You need to resolve it manually or choose to ignore it, depending on the issue at hand.'),
url=control_url
)
n.add_action(_('View all unresolved problems'), control_url)
return n
class ParametrizedOrderNotificationType(NotificationType):
required_permission = "can_view_orders"
def __init__(self, event, action_type, verbose_name, title):
self._action_type = action_type
self._verbose_name = verbose_name
self._title = title
super().__init__(event)
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
def build_notification(self, logentry: LogEntry):
order = logentry.content_object
order_url = build_absolute_uri(
'control:event.order',
kwargs={
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
'code': order.code
}
)
n = Notification(
event=logentry.event,
title=self._title.format(order=order, event=logentry.event),
url=order_url
)
n.add_attribute(_('Order code'), order.code)
n.add_attribute(_('Order total'), '{} {}'.format(localize(order.total), logentry.event.currency))
n.add_attribute(_('Order date'), date_format(order.datetime, 'SHORT_DATETIME_FORMAT'))
n.add_attribute(_('Order status'), order.get_status_display())
n.add_attribute(_('Order positions'), str(order.positions.count()))
n.add_action(_('View order details'), order_url)
return n
@receiver(register_notification_types, dispatch_uid="base_register_default_notification_types")
def register_default_notification_types(sender, **kwargs):
return (
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.placed',
_('New order placed'),
_('A new order has been placed: {order.code}'),
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.paid',
_('Order marked as paid'),
_('Order {order.code} has been marked as paid.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.canceled',
_('Order canceled'),
_('Order {order.code} has been canceled.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.modified',
_('Order information changed'),
_('The ticket information of order {order.code} has been changed.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.contact.changed',
_('Order contact address changed'),
_('The contact address of order {order.code} has been changed.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.changed',
_('Order changed'),
_('Order {order.code} has been changed.')
),
ParametrizedOrderNotificationType(
sender,
'pretix.event.order.refunded',
_('Order refunded'),
_('Order {order.code} has been refunded.')
),
ActionRequiredNotificationType(
sender,
)
)

View File

@@ -0,0 +1,104 @@
from django.conf import settings
from django.template.loader import get_template
from pretix.base.i18n import language
from pretix.base.models import LogEntry, NotificationSetting, User
from pretix.base.notifications import Notification, get_all_notification_types
from pretix.base.services.async import ProfiledTask, TransactionAwareTask
from pretix.base.services.mail import mail_send_task
from pretix.celery_app import app
from pretix.helpers.urls import build_absolute_uri
@app.task(base=TransactionAwareTask)
def notify(logentry_id: int):
logentry = LogEntry.all.get(id=logentry_id)
if not logentry.event:
return # Ignore, we only have event-related notifications right now
types = get_all_notification_types(logentry.event)
notification_type = types.get(logentry.action_type)
if not notification_type:
return # Ignore, e.g. plugin not active for this event
# All users that have the permission to get the notification
users = logentry.event.get_users_with_permission(
notification_type.required_permission
).filter(notifications_send=True)
if logentry.user:
users = users.exclude(pk=logentry.user.pk)
# Get all notification settings, both specific to this event as well as global
notify_specific = {
(ns.user, ns.method): ns.enabled
for ns in NotificationSetting.objects.filter(
event=logentry.event,
action_type=logentry.action_type,
user__pk__in=users.values_list('pk', flat=True)
)
}
notify_global = {
(ns.user, ns.method): ns.enabled
for ns in NotificationSetting.objects.filter(
action_type=logentry.action_type,
user__pk__in=users.values_list('pk', flat=True)
)
}
for um, enabled in notify_specific.items():
user, method = um
if enabled:
send_notification.apply_async(args=(logentry_id, user.pk, method))
for um, enabled in notify_global.items():
user, method = um
if enabled and um not in notify_specific:
send_notification.apply_async(args=(logentry_id, user.pk, method))
@app.task(base=ProfiledTask)
def send_notification(logentry_id: int, user_id: int, method: str):
logentry = LogEntry.all.get(id=logentry_id)
user = User.objects.get(id=user_id)
types = get_all_notification_types(logentry.event)
notification_type = types.get(logentry.action_type)
if not notification_type:
return # Ignore, e.g. plugin not active for this event
with language(user.locale):
notification = notification_type.build_notification(logentry)
if method == "mail":
send_notification_mail(notification, user)
def send_notification_mail(notification: Notification, user: User):
ctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
'color': '#8E44B3',
'notification': notification,
'settings_url': build_absolute_uri(
'control:user.settings.notifications',
),
'disable_url': build_absolute_uri(
'control:user.settings.notifications.off',
kwargs={
'token': user.notifications_token,
'id': user.pk
}
)
}
tpl_html = get_template('pretixbase/email/notification.html')
body_html = tpl_html.render(ctx)
tpl_plain = get_template('pretixbase/email/notification.txt')
body_plain = tpl_plain.render(ctx)
mail_send_task.apply_async(kwargs={
'to': [user.email],
'subject': '[{}] {}'.format(settings.PRETIX_INSTANCE_NAME, notification.title),
'body': body_plain,
'html': body_html,
'sender': settings.MAIL_FROM,
'headers': {},
})

View File

@@ -26,6 +26,10 @@ class EventPluginSignal(django.dispatch.Signal):
"""
def _is_active(self, sender, receiver):
if sender is None:
# Send to all events!
return True
# Find the Django application this belongs to
searchpath = receiver.__module__
core_module = any([searchpath.startswith(cm) for cm in settings.CORE_MODULES])
@@ -140,6 +144,19 @@ subclass of pretix.base.ticketoutput.BaseTicketOutput
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_notification_types = EventPluginSignal(
providing_args=[]
)
"""
This signal is sent out to get all known notification types. Receivers should return an
instance of a subclass of pretix.base.notifications.NotificationType or a list of such
instances.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event,
however for this signal, the ``sender`` **may also be None** to allow creating the general
notification settings!
"""
register_data_exporters = EventPluginSignal(
providing_args=[]
)

View File

@@ -0,0 +1,167 @@
{% load eventurl %}
{% load i18n %}
<!DOCTYPE html>
<html>
<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 {
margin: 0;
padding: 12px 0 8px;
}
}
td.containertd {
background-color: #FFFFFF;
border: 1px solid #cccccc;
}
{% block addcss %}{% endblock %}
</style>
<body>
<table class="layout">
<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>
</tr>
{% block content %}
{% endblock %}
<tr>
<td class="footer">
<div>
{% include "pretixbase/email/email_footer.html" %}
</div>
</td>
</tr>
</table>
<br/>
<br/>
</body>
</html>

View File

@@ -0,0 +1,53 @@
{% extends "pretixbase/email/base.html" %}
{% load eventurl %}
{% load i18n %}
{% block content %}
<tr>
<td class="containertd">
<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 %}
{% if notification.attributes %}
<table>
{% for attr in notification.attributes %}
<tr>
<td>
<strong>{{ attr.title }}</strong>
</td>
<td>
{{ attr.value }}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% if notification.actions %}
<p class="actions" style="text-align: center">
{% for action in notification.actions %}
<a href="{{ action.url }}" class="button">{{ action.label }}</a>
{% endfor %}
</p>
{% endif %}
</div>
</td>
</tr>
<tr>
<td class="containertd">
<div class="content">
{% trans "You receive these emails based on your notification settings." %}<br>
<a href="{{ settings_url }}">
{% trans "Click here to view and change your notification settings" %}
</a><br>
<a href="{{ disable_url }}">
{% trans "Click here disable all notifications immediately." %}
</a>
</div>
</td>
</tr>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% load i18n %}
{{ notification.title }}{% if notification.detail %}
{{ notification.detail }}
{% endif %}{% if notification.url %}
{{ notification.url }}{% endif %}{% for attr in notification.attributes %}
{{ attr.title }}: {{ attr.value }}{% endfor %}{% for action in notification.actions %}
{{ action.label }}
{{ action.url }}{% endfor %}
{% trans "You receive these emails based on your notification settings." %}
{% trans "Click here to view and change your notification settings:" %}
{{ settings_url }}
{% trans "Click here disable all notifications immediately:" %}
{{ disable_url }}

View File

@@ -1,174 +1,42 @@
{% extends "pretixbase/email/base.html" %}
{% load eventurl %}
{% load i18n %}
<!DOCTYPE html>
<html>
<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 {
text-decoration: none;
}
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 {
width: 90%;
max-width: 900px;
border-spacing: 0px;
border-collapse: separate;
margin: auto;
}
@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 {
margin: 0;
padding: 12px 0 8px;
}
}
td.containertd {
background-color: #FFFFFF;
border: 1px solid #cccccc;
}
{% block addcss %}{% endblock %}
</style>
<body>
<table>
{% block content %}
<tr>
<td class="containertd">
<div class="content">
{{ body|safe }}
</div>
</td>
</tr>
{% if order %}
<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>
<td class="gap"></td>
</tr>
<tr>
<td class="containertd">
<td class="order containertd">
<div class="content">
{{ body|safe }}
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order" order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
</div>
</td>
</tr>
{% if order %}
<tr>
<td class="gap"></td>
</tr>
<tr>
<td class="order containertd">
<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>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order" order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
</div>
</td>
</tr>
{% endif %}
{% endif %}
{% if signature %}
<tr>
<td class="gap"></td>
</tr>
<tr>
<td class="order containertd">
<div class="content">
{{ signature | safe }}
</div>
</td>
</tr>
{% endif %}
<tr>
<td class="footer">
<div>
{% include "pretixbase/email/email_footer.html" %}
<td class="gap"></td>
</tr>
<tr>
<td class="order containertd">
<div class="content">
{{ signature | safe }}
</div>
</td>
</tr>
</table>
<br/>
<br/>
</body>
</html>
{% endif %}
{% endblock %}