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 %}
+
+{% 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())