mirror of
https://github.com/pretix/pretix.git
synced 2026-05-09 15:54:03 +00:00
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:
@@ -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.'),
|
||||
|
||||
@@ -27,6 +27,7 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
"auth.forgot",
|
||||
"auth.forgot.recover",
|
||||
"auth.invite",
|
||||
"user.settings.notifications.off",
|
||||
)
|
||||
|
||||
def _login_redirect(self, request):
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<a href="{% url "control:user.settings.2fa" %}">
|
||||
{% trans "Enable" %}
|
||||
</a>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user