Added team invitations

This commit is contained in:
Raphael Michel
2017-01-07 13:05:36 +01:00
parent 981d82b0ee
commit d134dcf6a9
11 changed files with 223 additions and 12 deletions

View File

@@ -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),
),
]

View File

@@ -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 (

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
{% extends "pretixcontrol/auth/base.html" %}
{% load bootstrap3 %}
{% load staticfiles %}
{% load i18n %}
{% block content %}
<form class="form-signin" action="" method="post">
<h3>{% trans "Accept an invitation" %}</h3>
<p>
{% 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
<a {{ login_href }}>log in</a> first and then click this link again to accept the
invitation with your existing account.
{% endblocktrans %}
</p>
{% bootstrap_form_errors form type='all' layout='inline' %}
{% csrf_token %}
{% bootstrap_field form.email %}
{% bootstrap_field form.password %}
{% bootstrap_field form.password_repeat %}
<div class="form-group buttons">
<a href="{% url "control:auth.login" %}" class="btn btn-link">
&laquo; {% trans "Login" %}
</a>
<button type="submit" class="btn btn-primary">
{% trans "Register" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -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 %}

View File

@@ -26,7 +26,16 @@
<tbody>
{% for form in formset %}
<tr>
<td>{{ form.id }}{{ form.instance.user }}</td>
<td>
{{ form.id }}
{% if form.instance.user %}
{{ form.instance.user }}
{% else %}
{{ form.instance.invite_email }}
<span class="fa fa-envelope-o" data-toggle="tooltip"
title="{% trans "invited, pending response" %}"></span>
{% endif %}
</td>
<td>{{ form.can_change_settings }}</td>
<td>{{ form.can_change_items }}</td>
<td>{{ form.can_view_orders }}</td>
@@ -37,6 +46,19 @@
<td>{{ form.DELETE }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="9">
<strong>{% trans "Adding a new user" %}</strong><br>
{% 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 %}
</td>
</tr>
<tr>
<td>
<div class="row-fluid">
@@ -53,7 +75,7 @@
<td>{{ add_form.can_change_vouchers }}</td>
<td>{{ add_form.can_view_vouchers }}</td>
</tr>
</tbody>
</tfoot>
</table>
</div>
</fieldset>

View File

@@ -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<token>[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'),

View File

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

View File

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