From d134dcf6a906d996e0226acc63d033850f0718a1 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Sat, 7 Jan 2017 13:05:36 +0100 Subject: [PATCH] Added team invitations --- .../migrations/0054_auto_20170107_1058.py | 34 +++++++++++ src/pretix/base/models/__init__.py | 1 + src/pretix/base/models/event.py | 9 ++- src/pretix/control/logdisplay.py | 4 ++ src/pretix/control/middleware.py | 3 +- .../templates/pretixcontrol/auth/invite.html | 31 ++++++++++ .../pretixcontrol/email/invitation.txt | 16 ++++++ .../pretixcontrol/event/permissions.html | 26 ++++++++- src/pretix/control/urls.py | 1 + src/pretix/control/views/auth.py | 56 ++++++++++++++++++- src/pretix/control/views/event.py | 54 +++++++++++++++--- 11 files changed, 223 insertions(+), 12 deletions(-) create mode 100644 src/pretix/base/migrations/0054_auto_20170107_1058.py create mode 100644 src/pretix/control/templates/pretixcontrol/auth/invite.html create mode 100644 src/pretix/control/templates/pretixcontrol/email/invitation.txt diff --git a/src/pretix/base/migrations/0054_auto_20170107_1058.py b/src/pretix/base/migrations/0054_auto_20170107_1058.py new file mode 100644 index 0000000000..62e26783dd --- /dev/null +++ b/src/pretix/base/migrations/0054_auto_20170107_1058.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2017-01-07 10:58 +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.event + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0053_auto_20170104_1252'), + ] + + operations = [ + migrations.AddField( + model_name='eventpermission', + name='invite_email', + field=models.EmailField(blank=True, max_length=254, null=True), + ), + migrations.AddField( + model_name='eventpermission', + name='invite_token', + field=models.CharField(blank=True, default=pretix.base.models.event.generate_invite_token, max_length=64, null=True), + ), + migrations.AlterField( + model_name='eventpermission', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='event_perms', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index 5e8e722538..2ea64e59f7 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -3,6 +3,7 @@ from .base import CachedFile, LoggedModel, cachedfile_name from .checkin import Checkin from .event import ( Event, EventLock, EventPermission, EventSetting, RequiredAction, + generate_invite_token, ) from .invoices import Invoice, InvoiceLine, invoice_filename from .items import ( diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 7cb28195cf..dfb9d57527 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -1,3 +1,4 @@ +import string import uuid from datetime import date, datetime, time @@ -294,6 +295,10 @@ class Event(LoggedModel): s.save() +def generate_invite_token(): + return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits) + + class EventPermission(models.Model): """ The relation between an Event and a User who has permissions to @@ -314,7 +319,9 @@ class EventPermission(models.Model): """ event = models.ForeignKey(Event, related_name="user_perms", on_delete=models.CASCADE) - user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE) + user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE, null=True, blank=True) + invite_email = models.EmailField(null=True, blank=True) + invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True) can_change_settings = models.BooleanField( default=True, verbose_name=_("Can change event settings") diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 7ad27cb93c..8f4f48d6d8 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -113,6 +113,10 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): 'pretix.event.question.option.added': _('An answer option has been added to the question.'), 'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'), 'pretix.event.question.option.changed': _('An answer option has been changed.'), + 'pretix.event.permissions.added': _('A user has been added to the event team.'), + 'pretix.event.permissions.invited': _('A user has been invited to the event team.'), + 'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'), + 'pretix.event.permissions.deleted': _('A user has been removed from the event team.'), } data = json.loads(logentry.data) diff --git a/src/pretix/control/middleware.py b/src/pretix/control/middleware.py index 2a79457220..fb76dc9ce8 100644 --- a/src/pretix/control/middleware.py +++ b/src/pretix/control/middleware.py @@ -24,7 +24,8 @@ class PermissionMiddleware(MiddlewareMixin): "auth.login.2fa", "auth.register", "auth.forgot", - "auth.forgot.recover" + "auth.forgot.recover", + "auth.invite", ) def process_request(self, request): diff --git a/src/pretix/control/templates/pretixcontrol/auth/invite.html b/src/pretix/control/templates/pretixcontrol/auth/invite.html new file mode 100644 index 0000000000..b10a5a563c --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/auth/invite.html @@ -0,0 +1,31 @@ +{% extends "pretixcontrol/auth/base.html" %} +{% load bootstrap3 %} +{% load staticfiles %} +{% load i18n %} +{% block content %} +
+

{% trans "Accept an invitation" %}

+

+ {% url "control:auth.login" as loginurl %} + {% blocktrans trimmed with login_href='href="'|add:loginurl|add:'"'|safe %} + If you already have an account on this site with a different email address, you can + log in first and then click this link again to accept the + invitation with your existing account. + {% endblocktrans %} +

+ {% bootstrap_form_errors form type='all' layout='inline' %} + {% csrf_token %} + {% bootstrap_field form.email %} + {% bootstrap_field form.password %} + {% bootstrap_field form.password_repeat %} +
+ + « {% trans "Login" %} + + + +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/email/invitation.txt b/src/pretix/control/templates/pretixcontrol/email/invitation.txt new file mode 100644 index 0000000000..f625b981ad --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/email/invitation.txt @@ -0,0 +1,16 @@ +{% load i18n %}{% blocktrans with url=url|safe %}Hello, + +you have been invited to the team of an event that uses pretix for their +ticket sales. + +Event: {{ event }} + +If you want to join that team, just click on the following link: +{{ url }} + +If you do not want to join, you can safely ignore or delete this email. + +Best regards, + +Your pretix team +{% endblocktrans %} diff --git a/src/pretix/control/templates/pretixcontrol/event/permissions.html b/src/pretix/control/templates/pretixcontrol/event/permissions.html index 0fdaa1346d..a9efff984c 100644 --- a/src/pretix/control/templates/pretixcontrol/event/permissions.html +++ b/src/pretix/control/templates/pretixcontrol/event/permissions.html @@ -26,7 +26,16 @@ {% for form in formset %} - {{ form.id }}{{ form.instance.user }} + + {{ form.id }} + {% if form.instance.user %} + {{ form.instance.user }} + {% else %} + {{ form.instance.invite_email }} + + {% endif %} + {{ form.can_change_settings }} {{ form.can_change_items }} {{ form.can_view_orders }} @@ -37,6 +46,19 @@ {{ form.DELETE }} {% endfor %} + + + + + {% trans "Adding a new user" %}
+ {% blocktrans trimmed %} + To add a new user, you can enter their email address here. If they already have a + pretix account, they will immediately be added to the event. Otherwise, they will + be sent an email with an invitation. + {% endblocktrans %} + + +
@@ -53,7 +75,7 @@ {{ add_form.can_change_vouchers }} {{ add_form.can_view_vouchers }} - +
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index c1beb830bc..907b503a17 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -10,6 +10,7 @@ urlpatterns = [ url(r'^login$', auth.login, name='auth.login'), url(r'^login/2fa$', auth.Login2FAView.as_view(), name='auth.login.2fa'), url(r'^register$', auth.register, name='auth.register'), + url(r'^invite/(?P[a-zA-Z0-9]+)$', auth.invite, name='auth.invite'), url(r'^forgot$', auth.Forgot.as_view(), name='auth.forgot'), url(r'^forgot/recover$', auth.Recover.as_view(), name='auth.forgot.recover'), url(r'^$', dashboards.user_index, name='index'), diff --git a/src/pretix/control/views/auth.py b/src/pretix/control/views/auth.py index a698ad580f..a08b13cd5a 100644 --- a/src/pretix/control/views/auth.py +++ b/src/pretix/control/views/auth.py @@ -23,7 +23,7 @@ from u2flib_server.utils import rand_bytes from pretix.base.forms.auth import ( LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm, ) -from pretix.base.models import U2FDevice, User +from pretix.base.models import EventPermission, U2FDevice, User from pretix.base.services.mail import SendMailException, mail from pretix.helpers.urls import build_absolute_uri @@ -99,6 +99,60 @@ def register(request): return render(request, 'pretixcontrol/auth/register.html', ctx) +def invite(request, token): + """ + Registration form in case of an invite + """ + ctx = {} + + try: + perm = EventPermission.objects.get(invite_token=token) + except EventPermission.DoesNotExist: + messages.error(request, _('You used an invalid link. Please copy the link from your email to the address bar ' + 'and make sure it is correct and that the link has not been used before.')) + return redirect('control:auth.login') + + if request.user.is_authenticated: + try: + EventPermission.objects.get(event=perm.event, user=request.user) + messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of ' + 'that event\'s team.').format(perm.event.name)) + return redirect('control:index') + except EventPermission.DoesNotExist: + pass + + perm.invite_token = None + perm.invite_email = None + perm.user = request.user + perm.save() + messages.success(request, _('You have now access to "{}".').format(perm.event.name)) + return redirect('control:index') + + if request.method == 'POST': + form = RegistrationForm(data=request.POST) + if form.is_valid(): + user = User.objects.create_user( + form.cleaned_data['email'], form.cleaned_data['password'], + locale=request.LANGUAGE_CODE, + timezone=request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE + ) + user = authenticate(email=user.email, password=form.cleaned_data['password']) + user.log_action('pretix.control.auth.user.created', user=user) + auth_login(request, user) + request.session['pretix_auth_login_time'] = int(time.time()) + + perm.invite_token = None + perm.invite_email = None + perm.user = user + perm.save() + messages.success(request, _('Welcome to pretix! You have now access to "{}".').format(perm.event.name)) + return redirect('control:index') + else: + form = RegistrationForm(initial={'email': perm.invite_email}) + ctx['form'] = form + return render(request, 'pretixcontrol/auth/invite.html', ctx) + + class Forgot(TemplateView): template_name = 'pretixcontrol/auth/forgot.html' diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 19ee125c85..e9896abdb5 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -22,6 +22,7 @@ from pretix.base.models import ( ) from pretix.base.services import tickets from pretix.base.services.invoices import build_preview_invoice_pdf +from pretix.base.services.mail import SendMailException, mail from pretix.base.signals import ( register_payment_providers, register_ticket_outputs, ) @@ -31,6 +32,7 @@ from pretix.control.forms.event import ( TicketSettingsForm, ) from pretix.control.permissions import EventPermissionRequiredMixin +from pretix.helpers.urls import build_absolute_uri from pretix.presale.style import regenerate_css from . import UpdateView @@ -544,27 +546,57 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView): ctx['add_form'] = self.add_form return ctx + def _send_invite(self, instance): + try: + mail( + instance.invite_email, + _('Account information changed'), + 'pretixcontrol/email/invitation.txt', + { + 'user': self, + 'event': self.request.event.name, + 'url': build_absolute_uri('control:auth.invite', kwargs={ + 'token': instance.invite_token + }) + }, + event=None, + locale=self.request.LANGUAGE_CODE + ) + except SendMailException: + pass # Already logged + @transaction.atomic def post(self, *args, **kwargs): if self.formset.is_valid() and self.add_form.is_valid(): if self.add_form.has_changed(): + logdata = { + k: v for k, v in self.add_form.cleaned_data.items() + } + try: - self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user']) - self.add_form.instance.user_id = self.add_form.instance.user.id self.add_form.instance.event = self.request.event self.add_form.instance.event_id = self.request.event.id + self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user']) + self.add_form.instance.user_id = self.add_form.instance.user.id except User.DoesNotExist: - messages.error(self.request, _('There is no user with the email address you entered.')) - return self.get(*args, **kwargs) + self.add_form.instance.invite_email = self.add_form.cleaned_data['user'] + if EventPermission.objects.filter(invite_email=self.add_form.instance.invite_email, + event=self.request.event).exists(): + messages.error(self.request, _('This user already has been invited for this event.')) + return self.get(*args, **kwargs) + + self.add_form.save() + self._send_invite(self.add_form.instance) + + self.request.event.log_action( + 'pretix.event.permissions.invited', user=self.request.user, data=logdata + ) else: if EventPermission.objects.filter(user=self.add_form.instance.user, event=self.request.event).exists(): messages.error(self.request, _('This user already has permissions for this event.')) return self.get(*args, **kwargs) self.add_form.save() - logdata = { - k: v for k, v in self.add_form.cleaned_data.items() - } logdata['user'] = self.add_form.instance.user_id self.request.event.log_action( 'pretix.event.permissions.added', user=self.request.user, data=logdata @@ -583,6 +615,14 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView): messages.error(self.request, _('You cannot remove your own permission to view this page.')) return self.get(*args, **kwargs) + for form in self.formset.deleted_forms: + logdata = { + k: v for k, v in form.cleaned_data.items() + } + self.request.event.log_action( + 'pretix.event.permissions.deleted', user=self.request.user, data=logdata + ) + self.formset.save() messages.success(self.request, _('Your changes have been saved.')) return redirect(self.get_success_url())