forked from CGM_Public/pretix_original
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:
59
src/pretix/base/migrations/0078_auto_20171206_1603.py
Normal file
59
src/pretix/base/migrations/0078_auto_20171206_1603.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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',)
|
||||
|
||||
36
src/pretix/base/models/notifications.py
Normal file
36
src/pretix/base/models/notifications.py
Normal 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')
|
||||
233
src/pretix/base/notifications.py
Normal file
233
src/pretix/base/notifications.py
Normal 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,
|
||||
)
|
||||
)
|
||||
104
src/pretix/base/services/notifications.py
Normal file
104
src/pretix/base/services/notifications.py
Normal 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': {},
|
||||
})
|
||||
@@ -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=[]
|
||||
)
|
||||
|
||||
167
src/pretix/base/templates/pretixbase/email/base.html
Normal file
167
src/pretix/base/templates/pretixbase/email/base.html
Normal 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>
|
||||
53
src/pretix/base/templates/pretixbase/email/notification.html
Normal file
53
src/pretix/base/templates/pretixbase/email/notification.html
Normal 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 %}
|
||||
18
src/pretix/base/templates/pretixbase/email/notification.txt
Normal file
18
src/pretix/base/templates/pretixbase/email/notification.txt
Normal 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 }}
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user