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

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