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

@@ -141,6 +141,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'your account.'),
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
'from your account.'),
'pretix.user.settings.notifications.enabled': _('Notifications have been enabled.'),
'pretix.user.settings.notifications.disabled': _('Notifications have been disabled.'),
'pretix.user.settings.notifications.changed': _('Your notification settings have been changed.'),
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
'pretix.voucher.added': _('The voucher has been created.'),

View File

@@ -27,6 +27,7 @@ class PermissionMiddleware(MiddlewareMixin):
"auth.forgot",
"auth.forgot.recover",
"auth.invite",
"user.settings.notifications.off",
)
def _login_redirect(self, request):

View File

@@ -0,0 +1,89 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Notification settings" %}{% endblock %}
{% block content %}
<h1>{% trans "Notification settings" %}</h1>
<form method="post">
{% csrf_token %}
<fieldset>
{% if request.user.notifications_send %}
<div class="alert alert-info">
<button name="notifications_send" value="off" type="submit" class="pull-right btn btn-default">
<span class="fa fa-bell-slash"></span>
{% trans "Disable" %}
</button>
{% trans "Notifications are turned on according to the settings below." %}
<div class="clearfix"></div>
</div>
{% else %}
<div class="alert alert-warning">
<button name="notifications_send" value="on" type="submit" class="pull-right btn btn-default">
<span class="fa fa-bell"></span>
{% trans "Enable" %}
</button>
{% trans "All notifications are turned off globally." %}
<div class="clearfix"></div>
</div>
{% endif %}
</fieldset>
</form>
<form class="form-inline" method="get">
<fieldset>
<legend>{% trans "Choose event" %}</legend>
<p>
<select name="event" class="form-control">
<option value="">{% trans "All my events" %}</option>
{% for e in events %}
<option value="{{ e.pk }}"
{% if e.pk|floatformat:0 == request.GET.event %}selected="selected"{% endif %}>
{{ e.name }} {{ e.get_date_range_display }}
</option>
{% endfor %}
</select>
<button class="btn btn-primary" type="submit">{% trans "Choose" %}</button>
<span class="help-block">{% trans "Save your modifications before switching events." %}</span>
</p>
</fieldset>
</form>
<form method="post">
{% csrf_token %}
<fieldset>
<legend>{% trans "Choose notifications to get" %}</legend>
<table class="table">
<thead>
<tr>
<th>{% trans "Notification type" %}</th>
<th class="text-center">{% trans "E-Mail notification" %}</th>
</tr>
</thead>
<tbody>
{% for type, enabled, global in types %}
<tr>
<td>
{{ type.verbose_name }}
</td>
<td class="text-center">
{% if not event or type.required_permission in permset %}
<select name="mail:{{ type.action_type }}" class="form-control">
{% if event %}
<option value="global">{% trans "Global" %} ({% if global.mail %}{% trans "On" %}{% else %}{% trans "Off" %}{% endif %})</option>{% endif %}
<option value="off" {% if "mail" in enabled and enabled.mail == False %}selected{% endif %}>{% trans "Off" %}</option>
<option value="on" {% if enabled.mail %}selected{% endif %}>{% trans "On" %}</option>
</select>
{% else %}
<span class="fa fa-lock" data-toggle="tooltip" title="{% trans "You have no permission to receive this notification" %}"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -12,6 +12,24 @@
{% bootstrap_field form.fullname layout='horizontal' %}
{% bootstrap_field form.locale layout='horizontal' %}
{% bootstrap_field form.timezone layout='horizontal' %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Notifications" %}</label>
<div class="col-md-9 static-form-row">
{% if request.user.notifications_send and request.user.notification_settings.exists %}
<span class="label label-success">
<span class="fa fa-bell-o"></span> {% trans "On" %}
</span>
{% else %}
<span class="label label-warning">
<span class="fa fa-bell-slash-o"></span> {% trans "Off" %}
</span>
{% endif %}
&nbsp;
<a href="{% url "control:user.settings.notifications" %}">
{% trans "Change notification settings" %}
</a>
</div>
</div>
</fieldset>
<fieldset>
<legend>{% trans "Login settings" %}</legend>
@@ -23,12 +41,12 @@
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Two-factor authentication" %}</label>
<div class="col-md-9 static-form-row">
{% if user.require_2fa %}
<span class="label label-success">{% trans "Enabled" %}</span>
<span class="label label-success">{% trans "Enabled" %}</span> &nbsp;
<a href="{% url "control:user.settings.2fa" %}">
{% trans "Change two-factor settings" %}
</a>
{% else %}
<span class="label label-default">{% trans "Disabled" %}</span>
<span class="label label-default">{% trans "Disabled" %}</span> &nbsp;
<a href="{% url "control:user.settings.2fa" %}">
{% trans "Enable" %}
</a>

View File

@@ -18,8 +18,11 @@ urlpatterns = [
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
url(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
url(r'^settings/2fa/$', user.User2FAMainView.as_view(), name='user.settings.2fa'),
url(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
url(r'^settings/notifications/$', user.UserNotificationsEditView.as_view(), name='user.settings.notifications'),
url(r'^settings/notifications/off/(?P<id>\d+)/(?P<token>[^/]+)/$', user.UserNotificationsDisableView.as_view(),
name='user.settings.notifications.off'),
url(r'^settings/2fa/$', user.User2FAMainView.as_view(), name='user.settings.2fa'),
url(r'^settings/2fa/add$', user.User2FADeviceAddView.as_view(), name='user.settings.2fa.add'),
url(r'^settings/2fa/enable', user.User2FAEnableView.as_view(), name='user.settings.2fa.enable'),
url(r'^settings/2fa/disable', user.User2FADisableView.as_view(), name='user.settings.2fa.disable'),

View File

@@ -1,6 +1,7 @@
import base64
import logging
import time
from collections import defaultdict
from urllib.parse import quote
from django.conf import settings
@@ -19,7 +20,8 @@ from u2flib_server import u2f
from u2flib_server.jsapi import DeviceRegistration
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
from pretix.base.models import U2FDevice, User
from pretix.base.models import Event, NotificationSetting, U2FDevice, User
from pretix.base.notifications import get_all_notification_types
from pretix.control.views.auth import get_u2f_appid
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
@@ -352,3 +354,121 @@ class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, Template
messages.success(request, _('Your emergency codes have been newly generated. Remember to store them in a safe '
'place in case you lose access to your devices.'))
return redirect(reverse('control:user.settings.2fa'))
class UserNotificationsDisableView(TemplateView):
def get(self, request, *args, **kwargs):
user = get_object_or_404(User, notifications_token=kwargs.get('token'), pk=kwargs.get('id'))
user.notifications_send = False
user.save()
messages.success(request, _('Your notifications have been disabled.'))
if request.user.is_authenticated:
return redirect(
reverse('control:user.settings.notifications')
)
else:
return redirect(
reverse('control:auth.login')
)
class UserNotificationsEditView(TemplateView):
template_name = 'pretixcontrol/user/notifications.html'
@cached_property
def event(self):
if self.request.GET.get('event'):
try:
return self.request.user.get_events_with_any_permission().select_related(
'organizer'
).get(pk=self.request.GET.get('event'))
except Event.DoesNotExist:
return None
return None
@cached_property
def types(self):
return get_all_notification_types(self.event)
@cached_property
def currently_set(self):
set_per_method = defaultdict(dict)
for n in self.request.user.notification_settings.filter(event=self.event):
set_per_method[n.method][n.action_type] = n.enabled
return set_per_method
@cached_property
def global_set(self):
set_per_method = defaultdict(dict)
for n in self.request.user.notification_settings.filter(event__isnull=True):
set_per_method[n.method][n.action_type] = n.enabled
return set_per_method
def post(self, request, *args, **kwargs):
if "notifications_send" in request.POST:
request.user.notifications_send = request.POST.get("notifications_send", "") == "on"
request.user.save()
messages.success(request, _('Your notification settings have been saved.'))
if request.user.notifications_send:
self.request.user.log_action('pretix.user.settings.notifications.disabled', user=self.request.user)
else:
self.request.user.log_action('pretix.user.settings.notifications.enabled', user=self.request.user)
return redirect(
reverse('control:user.settings.notifications') +
('?event={}'.format(self.event.pk) if self.event else '')
)
else:
for method, __ in NotificationSetting.CHANNELS:
old_enabled = self.currently_set[method]
for at in self.types.keys():
val = request.POST.get('{}:{}'.format(method, at))
# True → False
if old_enabled.get(at) is True and val == 'off':
self.request.user.notification_settings.filter(
event=self.event, action_type=at, method=method
).update(enabled=False)
# True/False → None
if old_enabled.get(at) is not None and val == 'global':
self.request.user.notification_settings.filter(
event=self.event, action_type=at, method=method
).delete()
# None → True/False
if old_enabled.get(at) is None and val in ('on', 'off'):
self.request.user.notification_settings.create(
event=self.event, action_type=at, method=method, enabled=(val == 'on'),
)
# False → True
if old_enabled.get(at) is False and val == 'on':
self.request.user.notification_settings.filter(
event=self.event, action_type=at, method=method
).update(enabled=True)
messages.success(request, _('Your notification settings have been saved.'))
self.request.user.log_action('pretix.user.settings.notifications.changed', user=self.request.user)
return redirect(
reverse('control:user.settings.notifications') +
('?event={}'.format(self.event.pk) if self.event else '')
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['events'] = self.request.user.get_events_with_any_permission().order_by('-date_from')
ctx['types'] = [
(
tv,
{k: a.get(t) for k, a in self.currently_set.items()},
{k: a.get(t) for k, a in self.global_set.items()},
)
for t, tv in self.types.items()
]
ctx['event'] = self.event
if self.event:
ctx['permset'] = self.request.user.get_event_permission_set(self.event.organizer, self.event)
return ctx