From 7def097dcde532a0a676f1344451faca6cdcab90 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 17 Sep 2015 00:52:36 +0200 Subject: [PATCH 1/2] Refs #96 -- Completely removed local users --- doc/admin/config.rst | 5 - doc/development/concepts.rst | 23 +- src/pretix/base/exporter.py | 2 +- src/pretix/base/forms/auth.py | 158 ++++++++++++ src/pretix/base/forms/user.py | 3 +- .../migrations/0015_auto_20150916_2219.py | 47 ++++ src/pretix/base/models.py | 127 ++-------- src/pretix/control/forms/auth.py | 85 ------- src/pretix/control/views/auth.py | 16 +- src/pretix/control/views/event.py | 2 +- src/pretix/control/views/orders.py | 3 +- src/pretix/presale/forms/auth.py | 227 ------------------ .../templates/pretixpresale/event/forgot.html | 2 +- .../templates/pretixpresale/event/login.html | 82 +++---- src/pretix/presale/views/__init__.py | 21 +- src/pretix/presale/views/cart.py | 9 +- src/pretix/presale/views/checkout.py | 8 +- src/pretix/presale/views/event.py | 63 ++--- src/pretix/presale/views/order.py | 14 +- src/pretix/settings.py | 1 - src/tests/base/test_mail.py | 2 +- src/tests/base/test_middleware.py | 2 +- src/tests/base/test_models.py | 31 +-- src/tests/base/test_settings.py | 2 +- src/tests/control/test_auth.py | 4 +- src/tests/control/test_events.py | 2 +- src/tests/control/test_items.py | 2 +- src/tests/control/test_orders.py | 12 +- src/tests/control/test_permissions.py | 16 +- src/tests/control/test_user.py | 4 +- src/tests/plugins/banktransfer/test_import.py | 4 +- src/tests/plugins/test_pretixdroid.py | 4 +- src/tests/presale/test_account.py | 19 +- src/tests/presale/test_cart.py | 38 +-- src/tests/presale/test_checkout.py | 4 +- src/tests/presale/test_event.py | 24 +- src/tests/presale/test_orders.py | 6 +- 37 files changed, 367 insertions(+), 707 deletions(-) create mode 100644 src/pretix/base/forms/auth.py create mode 100644 src/pretix/base/migrations/0015_auto_20150916_2219.py delete mode 100644 src/pretix/control/forms/auth.py delete mode 100644 src/pretix/presale/forms/auth.py diff --git a/doc/admin/config.rst b/doc/admin/config.rst index e0c4d6d72..7825ee58f 100644 --- a/doc/admin/config.rst +++ b/doc/admin/config.rst @@ -25,7 +25,6 @@ Example:: [pretix] instance_name=pretix.de - global_registration=off url=http://localhost currency=EUR cookiedomain=.pretix.de @@ -36,10 +35,6 @@ Example:: ``instance_name`` The name of this installation. Default: ``pretix.de`` -``global_registration`` - Whether or not this installation supports global user accounts (in addition to - event-bound accounts). Defaults to ``True``. - ``url`` The installation's full URL, without a trailing slash. diff --git a/doc/development/concepts.rst b/doc/development/concepts.rst index 403881567..1baf60822 100644 --- a/doc/development/concepts.rst +++ b/doc/development/concepts.rst @@ -30,28 +30,7 @@ Every event is managed by the **organizer**, an abstract entity running the even Pretix is used by **users**. We want to enable global users who can just login into pretix and buy tickets for as many events as they like but at the same time it -should be possible to create some kind of local user to have a temporary account -just to buy tickets for one single event. - -The problem is, we cannot use usernames as primary keys for our users, as we -do not want one username to be blocked forever just because of one temporary -account using it (people would have to think of a new username for every temporary -account they create). On the other hand, we can not use e-mail addresses either, -as those are not unique (imagine one person having multiple temporary accounts) -and they should not be required for temporary account (to enable anonymity). - -Therefore, we split our users into two groups and use an internal **identifier** -as our primary key: - -**Local users** - Local users do only exist inside the scope of one event. They are identified by - usernames, which are only valid for exactly one event. Internally, their identifier - is "{username}@{event.id}.event.pretix" - -**Global users** - Global users exist everywhere in the installation of Tixl. They can buy tickets - for multiple events and they can be managers of one or more Organizers/Events. - Global users are identified by e-mail addresses. +should be possible to order products **without** needing an user account. Items and variations diff --git a/src/pretix/base/exporter.py b/src/pretix/base/exporter.py index 4b8c61e52..fa25da2fc 100644 --- a/src/pretix/base/exporter.py +++ b/src/pretix/base/exporter.py @@ -121,7 +121,7 @@ class JSONExporter(BaseExporter): { 'code': o.code, 'status': o.status, - 'user': o.user.identifier, + 'user': o.user.email, 'datetime': o.datetime, 'payment_fee': o.payment_fee, 'total': o.total, diff --git a/src/pretix/base/forms/auth.py b/src/pretix/base/forms/auth.py new file mode 100644 index 000000000..6421cd1ce --- /dev/null +++ b/src/pretix/base/forms/auth.py @@ -0,0 +1,158 @@ +from django import forms +from django.contrib.auth import authenticate +from django.utils.translation import ugettext_lazy as _ + +from pretix.base.models import User + + +class LoginForm(forms.Form): + """ + Base class for authenticating users. Extend this to get a form that accepts + username/password logins. + """ + email = forms.EmailField(label=_("E-mail"), max_length=254) + password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) + + error_messages = { + 'invalid_login': _("Please enter a correct e-mail address and password."), + 'inactive': _("This account is inactive.") + } + + def __init__(self, request=None, *args, **kwargs): + """ + The 'request' parameter is set for custom auth use by subclasses. + The form data comes in via the standard 'data' kwarg. + """ + self.request = request + self.user_cache = None + super().__init__(*args, **kwargs) + + def clean(self): + email = self.cleaned_data.get('email') + password = self.cleaned_data.get('password') + + if email and password: + self.user_cache = authenticate(email=email.lower(), password=password) + if self.user_cache is None: + raise forms.ValidationError( + self.error_messages['invalid_login'], + code='invalid_login' + ) + else: + self.confirm_login_allowed(self.user_cache) + + return self.cleaned_data + + def confirm_login_allowed(self, user): + """ + Controls whether the given User may log in. This is a policy setting, + independent of end-user authentication. This default behavior is to + allow login by active users, and reject login by inactive users. + + If the given user cannot log in, this method should raise a + ``forms.ValidationError``. + + If the given user may log in, this method should return None. + """ + if not user.is_active: + raise forms.ValidationError( + self.error_messages['inactive'], + code='inactive', + ) + + def get_user(self): + return self.user_cache + + +class RegistrationForm(forms.Form): + error_messages = { + 'duplicate_email': _("You already registered with that e-mail address, please use the login form."), + 'pw_mismatch': _("Please enter the same password twice"), + } + email = forms.EmailField( + label=_('Email address'), + required=True + ) + password = forms.CharField( + label=_('Password'), + widget=forms.PasswordInput, + required=True + ) + password_repeat = forms.CharField( + label=_('Repeat password'), + widget=forms.PasswordInput + ) + + def clean(self): + password1 = self.cleaned_data.get('password') + password2 = self.cleaned_data.get('password_repeat') + + if password1 and password1 != password2: + raise forms.ValidationError( + self.error_messages['pw_mismatch'], + code='pw_mismatch' + ) + + return self.cleaned_data + + def clean_email(self): + email = self.cleaned_data['email'] + if User.objects.filter(email=email).exists(): + raise forms.ValidationError( + self.error_messages['duplicate_email'], + code='duplicate_email' + ) + return email + + +class PasswordRecoverForm(forms.Form): + error_messages = { + 'pw_mismatch': _("Please enter the same password twice") + } + password = forms.CharField( + label=_('Password'), + widget=forms.PasswordInput, + required=True + ) + password_repeat = forms.CharField( + label=_('Repeat password'), + widget=forms.PasswordInput + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def clean(self): + password1 = self.cleaned_data.get('password') + password2 = self.cleaned_data.get('password_repeat') + + if password1 and password1 != password2: + raise forms.ValidationError( + self.error_messages['pw_mismatch'], + code='pw_mismatch' + ) + + return self.cleaned_data + + +class PasswordForgotForm(forms.Form): + email = forms.EmailField( + label=_('E-mail'), + ) + + def __init__(self, event, *args, **kwargs): + self.event = event + super().__init__(*args, **kwargs) + + def clean_email(self): + email = self.cleaned_data['email'] + try: + self.cleaned_data['user'] = User.objects.get( + email=email, event__isnull=True + ) + return email + except User.DoesNotExist: + raise forms.ValidationError( + _("We are unable to find a user matching the data you provided."), + code='unknown_user' + ) diff --git a/src/pretix/base/forms/user.py b/src/pretix/base/forms/user.py index a97dd51e4..0ff68895a 100644 --- a/src/pretix/base/forms/user.py +++ b/src/pretix/base/forms/user.py @@ -59,7 +59,7 @@ class UserSettingsForm(forms.ModelForm): def clean_email(self): email = self.cleaned_data['email'] - if User.objects.filter(Q(identifier=email) & ~Q(pk=self.instance.pk)).exists(): + if User.objects.filter(Q(email=email) & ~Q(pk=self.instance.pk)).exists(): raise forms.ValidationError( self.error_messages['duplicate_identifier'], code='duplicate_identifier', @@ -88,6 +88,5 @@ class UserSettingsForm(forms.ModelForm): if password1: self.instance.set_password(password1) - self.instance.identifier = email return self.cleaned_data diff --git a/src/pretix/base/migrations/0015_auto_20150916_2219.py b/src/pretix/base/migrations/0015_auto_20150916_2219.py new file mode 100644 index 000000000..7fd26f839 --- /dev/null +++ b/src/pretix/base/migrations/0015_auto_20150916_2219.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + +import pretix.base.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0014_auto_20150916_1319'), + ] + + operations = [ + migrations.AddField( + model_name='cartposition', + name='session', + field=models.CharField(max_length=255, verbose_name='Session', null=True, blank=True), + ), + migrations.AddField( + model_name='order', + name='secret', + field=models.CharField(max_length=32, default=pretix.base.models.generate_secret), + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=254, verbose_name='E-mail', blank=True, db_index=True, unique=True, null=True), + ), + migrations.AlterUniqueTogether( + name='user', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='user', + name='event', + ), + migrations.RemoveField( + model_name='user', + name='identifier', + ), + migrations.RemoveField( + model_name='user', + name='username', + ), + ] diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index cf2ded385..52678fdf7 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -1,5 +1,6 @@ import copy import random +import string import uuid from datetime import datetime from itertools import product @@ -88,34 +89,17 @@ class UserManager(BaseUserManager): model documentation to see what's so special about our user model. """ - def create_user(self, identifier, username, password=None): - user = self.model(identifier=identifier) + def create_user(self, email, password=None, **kwargs): + user = self.model(email=email, **kwargs) user.set_password(password) user.save() return user - def create_global_user(self, email, password=None, **kwargs): - user = self.model(**kwargs) - user.identifier = email - user.email = email - user.set_password(password) - user.save() - return user - - def create_local_user(self, event, username, password=None, **kwargs): - user = self.model(**kwargs) - user.identifier = '%s@%s.event.pretix' % (username, event.identity) - user.username = username - user.event = event - user.set_password(password) - user.save() - return user - - def create_superuser(self, identifier, password=None): # NOQA + def create_superuser(self, email, password=None): # NOQA # Not used in the software but required by Django if password is None: raise Exception("You must provide a password") - user = self.model(identifier=identifier, email=identifier) + user = self.model(email=email) user.is_staff = True user.is_superuser = True user.set_password(password) @@ -126,44 +110,8 @@ class UserManager(BaseUserManager): class User(AbstractBaseUser, PermissionsMixin): """ This is the user model used by pretix for authentication. - Handling users is somehow complicated, as we try to have two - classes of users in one system: - (1) We want *global* users who can just login into pretix and - buy tickets for multiple events -- we also need those - global users for event organizers who should not need - multiple users for managing multiple events. - (2) We want *local* users who exist only in the scope of a - certain event - - The hard part is to find a primary key to identify all of these - users. Letting the users choose usernames is a bad idea, as - the primary key needs to be unique and there is no reason for a - local user to block a name for all time. Using e-mail addresses - is not a good idea either, for two reasons: First, a user might - have multiple local users (so they are not unique), and second, - it should be possible to create anonymous users without having - to supply an e-mail address. - Therefore, we use an abstract "identifier" field as the primary - key. The identifier is: - - (1) the e-mail address for global users. An e-mail address - is and should be required for them and global users use - their e-mail address for login. - (2) "{username}@{event.identity}.event.pretix" for local users, who - use their username to login on the event page. - - The model's save() method automatically fills the identifier field - according to this scheme when it is empty. The __str__() method - returns the identifier. - - :param identifier: The identifier of the user, as described above - :type identifier: str - :param username: The username, null for global users. - :type username: str - :param event: The event the user belongs to, null for global users - :type event: Event - :param email: The user's e-mail address. May be empty or null for local users + :param email: The user's e-mail address, used for identification. :type email: str :param givenname: The user's given name. May be empty or null. :type givenname: str @@ -183,24 +131,14 @@ class User(AbstractBaseUser, PermissionsMixin): :type timezone: str """ - USERNAME_FIELD = 'identifier' - REQUIRED_FIELDS = ['username'] + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = [] - identifier = models.CharField(max_length=255, unique=True) - username = models.CharField(max_length=120, blank=True, - null=True, - help_text=_('Letters, digits and ./+/-/_ only.')) - event = models.ForeignKey('Event', related_name="users", - null=True, blank=True, - on_delete=models.PROTECT) - email = models.EmailField(unique=False, db_index=True, - null=True, blank=True, + email = models.EmailField(unique=True, db_index=True, null=True, blank=True, verbose_name=_('E-mail')) - givenname = models.CharField(max_length=255, blank=True, - null=True, + givenname = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('Given name')) - familyname = models.CharField(max_length=255, blank=True, - null=True, + familyname = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('Family name')) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) @@ -221,32 +159,20 @@ class User(AbstractBaseUser, PermissionsMixin): class Meta: verbose_name = _("User") verbose_name_plural = _("Users") - unique_together = (("event", "username"),) - - def __str__(self): - return self.identifier def save(self, *args, **kwargs): - """ - Before passing the call to the default ``save()`` method, this will fill the ``identifier`` - field if it is empty, according to the scheme descriped in the model docstring. - """ - if not self.identifier: - if self.event is None: - self.identifier = self.email.lower() - else: - self.identifier = "%s@%s.event.pretix" % (self.username.lower(), self.event.id) - if not self.pk: - self.identifier = self.identifier.lower() + self.email = self.email.lower() super().save(*args, **kwargs) + def __str__(self): + return self.email + def get_short_name(self) -> str: """ Returns the first of the following user properties that is found to exist: * Given name * Family name - * User name * E-mail address """ if self.givenname: @@ -254,7 +180,7 @@ class User(AbstractBaseUser, PermissionsMixin): elif self.familyname: return self.familyname else: - return self.get_local_name() + return self.email def get_full_name(self) -> str: """ @@ -264,7 +190,6 @@ class User(AbstractBaseUser, PermissionsMixin): * Given name * Family name * User name - * E-mail address """ if self.givenname and not self.familyname: return self.givenname @@ -276,18 +201,7 @@ class User(AbstractBaseUser, PermissionsMixin): 'given': self.givenname } else: - return self.get_local_name() - - def get_local_name(self) -> str: - """ - Returns the username for local users and the e-mail address for global - users. - """ - if self.username: - return self.username - if self.email: return self.email - return self.identifier # NOQA def cachedfile_name(instance, filename): @@ -1450,6 +1364,10 @@ class Quota(Versionable): pass +def generate_secret(): + return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32)) + + class Order(Versionable): """ An order is created when a user clicks 'buy' on his cart. It holds @@ -1524,6 +1442,7 @@ class Order(Versionable): verbose_name=_("User"), related_name="orders" ) + secret = models.CharField(max_length=32, default=generate_secret) datetime = models.DateTimeField( verbose_name=_("Date") ) @@ -1824,6 +1743,10 @@ class CartPosition(ObjectWithAnswers, Versionable): User, null=True, blank=True, verbose_name=_("User") ) + session = models.CharField( + max_length=255, null=True, blank=True, + verbose_name=_("Session") + ) item = VersionedForeignKey( Item, verbose_name=_("Item") diff --git a/src/pretix/control/forms/auth.py b/src/pretix/control/forms/auth.py deleted file mode 100644 index ac224e0d3..000000000 --- a/src/pretix/control/forms/auth.py +++ /dev/null @@ -1,85 +0,0 @@ -from django import forms -from django.contrib.auth import authenticate -from django.contrib.auth.forms import \ - AuthenticationForm as BaseAuthenticationForm -from django.utils.translation import ugettext as _ - -from pretix.base.models import User - - -class AuthenticationForm(BaseAuthenticationForm): - """ - The login form, providing an email and password field. The form already implements - validation for correct user data. - """ - email = forms.EmailField(label=_("Email address"), max_length=254) - password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) - username = None - - error_messages = { - 'invalid_login': _("Please enter a correct e-mail address and password."), - 'inactive': _("This account is inactive.") - } - - def __init__(self, request=None, *args, **kwargs): - self.request = request - self.user_cache = None - super(forms.Form, self).__init__(*args, **kwargs) - - def clean(self): - email = self.cleaned_data.get('email') - password = self.cleaned_data.get('password') - - if email and password: - self.user_cache = authenticate(identifier=email.lower(), - password=password) - if self.user_cache is None: - raise forms.ValidationError( - self.error_messages['invalid_login'], - code='invalid_login' - ) - else: - self.confirm_login_allowed(self.user_cache) - - return self.cleaned_data - - -class GlobalRegistrationForm(forms.Form): - error_messages = { - 'duplicate_email': _("You already registered with that e-mail address, please use the login form."), - 'pw_mismatch': _("Please enter the same password twice") - } - email = forms.EmailField( - label=_('Email address'), - required=True - ) - password = forms.CharField( - label=_('Password'), - widget=forms.PasswordInput, - required=True - ) - password_repeat = forms.CharField( - label=_('Repeat password'), - widget=forms.PasswordInput - ) - - def clean(self): - password1 = self.cleaned_data.get('password') - password2 = self.cleaned_data.get('password_repeat') - - if password1 and password1 != password2: - raise forms.ValidationError( - self.error_messages['pw_mismatch'], - code='pw_mismatch', - ) - - return self.cleaned_data - - def clean_email(self): - email = self.cleaned_data['email'] - if User.objects.filter(identifier=email).exists(): - raise forms.ValidationError( - self.error_messages['duplicate_email'], - code='duplicate_email', - ) - return email diff --git a/src/pretix/control/views/auth.py b/src/pretix/control/views/auth.py index 3fe1c476d..73a1e16a0 100644 --- a/src/pretix/control/views/auth.py +++ b/src/pretix/control/views/auth.py @@ -4,10 +4,8 @@ from django.contrib.auth import ( ) from django.shortcuts import redirect, render +from pretix.base.forms.auth import LoginForm, RegistrationForm from pretix.base.models import User -from pretix.control.forms.auth import ( - AuthenticationForm, GlobalRegistrationForm, -) def login(request): @@ -21,14 +19,14 @@ def login(request): return redirect(request.GET.get("next", 'control:index')) return redirect('control:index') if request.method == 'POST': - form = AuthenticationForm(data=request.POST) + form = LoginForm(data=request.POST) if form.is_valid() and form.user_cache: auth_login(request, form.user_cache) if "next" in request.GET: return redirect(request.GET.get("next", 'control:index')) return redirect('control:index') else: - form = AuthenticationForm() + form = LoginForm() ctx['form'] = form return render(request, 'pretixcontrol/auth/login.html', ctx) @@ -51,17 +49,17 @@ def register(request): return redirect(request.GET.get("next", 'control:index')) return redirect('control:index') if request.method == 'POST': - form = GlobalRegistrationForm(data=request.POST) + form = RegistrationForm(data=request.POST) if form.is_valid(): - user = User.objects.create_global_user( + 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(identifier=user.identifier, password=form.cleaned_data['password']) + user = authenticate(email=user.email, password=form.cleaned_data['password']) auth_login(request, user) return redirect('control:index') else: - form = GlobalRegistrationForm() + form = RegistrationForm() ctx['form'] = form return render(request, 'pretixcontrol/auth/register.html', ctx) diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 7fcd6616d..16371e51e 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -314,7 +314,7 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView): if self.formset.is_valid() and self.add_form.is_valid(): if self.add_form.has_changed(): try: - self.add_form.instance.user = User.objects.get(identifier=self.add_form.cleaned_data['user']) + 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.identity diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index c4936066d..490108adb 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -39,8 +39,7 @@ class OrderList(EventPermissionRequiredMixin, ListView): if self.request.GET.get("user", "") != "": u = self.request.GET.get("user", "") qs = qs.filter( - Q(user__identifier__icontains=u) | Q(user__email__icontains=u) - | Q(user__givenname__icontains=u) | Q(user__familyname__icontains=u) + Q(user__email__icontains=u) | Q(user__givenname__icontains=u) | Q(user__familyname__icontains=u) ) if self.request.GET.get("status", "") != "": s = self.request.GET.get("status", "") diff --git a/src/pretix/presale/forms/auth.py b/src/pretix/presale/forms/auth.py deleted file mode 100644 index e9bbb5e27..000000000 --- a/src/pretix/presale/forms/auth.py +++ /dev/null @@ -1,227 +0,0 @@ -from django import forms -from django.conf import settings -from django.contrib.auth import authenticate -from django.contrib.auth.forms import \ - AuthenticationForm as BaseAuthenticationForm -from django.core.validators import RegexValidator -from django.forms import Form -from django.utils.translation import ugettext_lazy as _ - -from pretix.base.models import User - - -class LoginForm(BaseAuthenticationForm): - username = forms.CharField( - label=_('Username'), - help_text=( - _('If you registered for multiple events, your username is your email address.') - if settings.PRETIX_GLOBAL_REGISTRATION - else None - ) - ) - password = forms.CharField( - label=_('Password'), - widget=forms.PasswordInput - ) - - error_messages = { - 'invalid_login': _("Please enter a correct username and password."), - 'inactive': _("This account is inactive."), - } - - def __init__(self, request=None, *args, **kwargs): - self.request = request - self.user_cache = None - super(forms.Form, self).__init__(*args, **kwargs) - - def clean(self): - username = self.cleaned_data.get('username') - password = self.cleaned_data.get('password') - - if username and password: - if '@' in username: - identifier = username.lower() - else: - identifier = "%s@%s.event.pretix" % (username, self.request.event.identity) - self.user_cache = authenticate(identifier=identifier, - password=password) - if self.user_cache is None: - raise forms.ValidationError( - self.error_messages['invalid_login'], - code='invalid_login', - ) - else: - self.confirm_login_allowed(self.user_cache) - - return self.cleaned_data - - -class GlobalRegistrationForm(forms.Form): - error_messages = { - 'duplicate_email': _("You already registered with that e-mail address, please use the login form."), - 'pw_mismatch': _("Please enter the same password twice"), - } - email = forms.EmailField( - label=_('Email address'), - required=True - ) - password = forms.CharField( - label=_('Password'), - widget=forms.PasswordInput, - required=True - ) - password_repeat = forms.CharField( - label=_('Repeat password'), - widget=forms.PasswordInput - ) - - def clean(self): - password1 = self.cleaned_data.get('password') - password2 = self.cleaned_data.get('password_repeat') - - if password1 and password1 != password2: - raise forms.ValidationError( - self.error_messages['pw_mismatch'], - code='pw_mismatch', - ) - - return self.cleaned_data - - def clean_email(self): - email = self.cleaned_data['email'] - if User.objects.filter(identifier=email).exists(): - raise forms.ValidationError( - self.error_messages['duplicate_email'], - code='duplicate_email', - ) - return email - - -class LocalRegistrationForm(forms.Form): - error_messages = { - 'invalid_username': _("Please only use characters, numbers or ./+/-/_ in your username."), - 'duplicate_username': _("This username is already taken. Please choose a different one."), - 'pw_mismatch': _("Please enter the same password twice"), - } - username = forms.CharField( - label=_('Username'), - validators=[ - RegexValidator( - regex='^[a-zA-Z0-9\.+\-_]*$', - code='invalid_username', - message=error_messages['invalid_username'] - ), - ], - required=True - ) - email = forms.EmailField( - label=_('E-mail address'), - required=False - ) - password = forms.CharField( - label=_('Password'), - widget=forms.PasswordInput, - required=True - ) - password_repeat = forms.CharField( - label=_('Repeat password'), - widget=forms.PasswordInput - ) - - def __init__(self, request, *args, **kwargs): - super().__init__(*args, **kwargs) - self.request = request - self.fields['email'].required = request.event.settings.user_mail_required - - def clean(self): - password1 = self.cleaned_data.get('password') - password2 = self.cleaned_data.get('password_repeat') - - if password1 and password1 != password2: - raise forms.ValidationError( - self.error_messages['pw_mismatch'], - code='pw_mismatch', - ) - - return self.cleaned_data - - def clean_username(self): - username = self.cleaned_data['username'] - if User.objects.filter(event=self.request.event, username=username).exists(): - raise forms.ValidationError( - self.error_messages['duplicate_username'], - code='duplicate_username', - ) - return username - - -class PasswordRecoverForm(Form): - error_messages = { - 'pw_mismatch': _("Please enter the same password twice"), - } - password = forms.CharField( - label=_('Password'), - widget=forms.PasswordInput, - required=True - ) - password_repeat = forms.CharField( - label=_('Repeat password'), - widget=forms.PasswordInput - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def clean(self): - password1 = self.cleaned_data.get('password') - password2 = self.cleaned_data.get('password_repeat') - - if password1 and password1 != password2: - raise forms.ValidationError( - self.error_messages['pw_mismatch'], - code='pw_mismatch', - ) - - return self.cleaned_data - - -class PasswordForgotForm(Form): - username = forms.CharField( - label=_('Username or E-mail'), - ) - - def __init__(self, event, *args, **kwargs): - self.event = event - super().__init__(*args, **kwargs) - - def clean_username(self): - username = self.cleaned_data['username'] - try: - self.cleaned_data['user'] = User.objects.get( - identifier=username, event__isnull=True - ) - return username - except User.DoesNotExist: - pass - try: - self.cleaned_data['user'] = User.objects.get( - username=username, event=self.event - ) - return username - except User.DoesNotExist: - pass - try: - self.cleaned_data['user'] = User.objects.get( - email=username, event=self.event - ) - return username - except User.MultipleObjectsReturned: - raise forms.ValidationError( - _("We found multiple users with that e-mail address. Please specify the username instead"), - code='unknown_user', - ) - except User.DoesNotExist: - raise forms.ValidationError( - _("We are unable to find a user matching the data you provided."), - code='unknown_user', - ) diff --git a/src/pretix/presale/templates/pretixpresale/event/forgot.html b/src/pretix/presale/templates/pretixpresale/event/forgot.html index d67a2358c..88b5faa7c 100644 --- a/src/pretix/presale/templates/pretixpresale/event/forgot.html +++ b/src/pretix/presale/templates/pretixpresale/event/forgot.html @@ -7,7 +7,7 @@
{% csrf_token %} {% bootstrap_form_errors form type='all' layout='inline' %} - {% bootstrap_field form.username layout="horizontal" %} + {% bootstrap_field form.email layout="horizontal" %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/login.html b/src/pretix/presale/templates/pretixpresale/event/login.html index 77cb03faf..27aafb3e9 100644 --- a/src/pretix/presale/templates/pretixpresale/event/login.html +++ b/src/pretix/presale/templates/pretixpresale/event/login.html @@ -19,7 +19,7 @@ {% csrf_token %} {% bootstrap_form_errors login_form type='all' layout='inline' %} - {% bootstrap_field login_form.username layout="horizontal" %} + {% bootstrap_field login_form.email layout="horizontal" %} {% bootstrap_field login_form.password layout="horizontal" %}
@@ -39,67 +39,47 @@
- - {% if global_registration_form %} -
- -
diff --git a/src/pretix/plugins/sendmail/views.py b/src/pretix/plugins/sendmail/views.py index 03cc47317..3e1c1acd3 100644 --- a/src/pretix/plugins/sendmail/views.py +++ b/src/pretix/plugins/sendmail/views.py @@ -31,8 +31,8 @@ class SenderView(EventPermissionRequiredMixin, FormView): users = set([o.user for o in orders]) for u in users: - mail(u, form.cleaned_data['subject'], form.cleaned_data['message'], - None, self.request.event) + mail(u.email, form.cleaned_data['subject'], form.cleaned_data['message'], + None, self.request.event, locale=u.locale) messages.success(self.request, _('Your message will be sent to the selected users.')) diff --git a/src/pretix/presale/forms/checkout.py b/src/pretix/presale/forms/checkout.py index 4b8660fa6..321973510 100644 --- a/src/pretix/presale/forms/checkout.py +++ b/src/pretix/presale/forms/checkout.py @@ -4,6 +4,10 @@ from django.utils.translation import ugettext_lazy as _ from pretix.base.models import Question +class GuestForm(forms.Form): + email = forms.EmailField(label=_('E-mail')) + + class QuestionsForm(forms.Form): """ This form class is responsible for asking order-related questions. This includes diff --git a/src/pretix/presale/middleware.py b/src/pretix/presale/middleware.py index a674e39a2..bb2e7419f 100644 --- a/src/pretix/presale/middleware.py +++ b/src/pretix/presale/middleware.py @@ -5,13 +5,24 @@ from pretix.base.models import Event class EventMiddleware: - def process_request(self, request): url = resolve(request.path_info) url_namespace = url.namespace url_name = url.url_name if url_namespace != 'presale': return + + if 'order_secrets' not in request.session: + request.session['order_secrets'] = [] + if 'order_secret' in request.GET and request.GET.get('order_secret') not in request.session['order_secrets']: + # We can't use append here, because this would not trigger __setitem__ + # on the session store and would not be saved + request.session['order_secrets'] = request.session['order_secrets'] + [request.GET.get('order_secret')] + # Removal of the secret from the URL has been disabled so people can bookmark it + # g = request.GET.copy() + # del g['order_secret'] + # return redirect(request.path + '?' + g.urlencode()) + if 'event.' in url_name and 'event' in url.kwargs: try: request.event = Event.objects.current.filter( diff --git a/src/pretix/presale/templates/pretixpresale/email/order_paid.txt b/src/pretix/presale/templates/pretixpresale/email/order_paid.txt index f4e4d8951..40fafb648 100644 --- a/src/pretix/presale/templates/pretixpresale/email/order_paid.txt +++ b/src/pretix/presale/templates/pretixpresale/email/order_paid.txt @@ -11,7 +11,7 @@ Your {{ event }} team we successfully received your payment for {{ event }}. Thank you! -You can view the status of your order at +You can change your order details and view the status of your order at {{ url }} Best regards, diff --git a/src/pretix/presale/templates/pretixpresale/email/order_placed.txt b/src/pretix/presale/templates/pretixpresale/email/order_placed.txt index cb577edc3..64238275c 100644 --- a/src/pretix/presale/templates/pretixpresale/email/order_placed.txt +++ b/src/pretix/presale/templates/pretixpresale/email/order_placed.txt @@ -4,7 +4,7 @@ we successfully received your order for {{ event }} with a total value of {{ total }} {{ currency }}. Please complete your payment before {{ date }}. {{ paymentinfo }} -You can view the status of your order at +You can change your order details and view the status of your order at {{ url }} diff --git a/src/pretix/presale/templates/pretixpresale/event/forgot.html b/src/pretix/presale/templates/pretixpresale/event/forgot.html index 88b5faa7c..87d0ac24d 100644 --- a/src/pretix/presale/templates/pretixpresale/event/forgot.html +++ b/src/pretix/presale/templates/pretixpresale/event/forgot.html @@ -10,7 +10,7 @@ {% bootstrap_field form.email layout="horizontal" %}
-
+
diff --git a/src/pretix/presale/templates/pretixpresale/event/login.html b/src/pretix/presale/templates/pretixpresale/event/login.html index 27aafb3e9..cca01a439 100644 --- a/src/pretix/presale/templates/pretixpresale/event/login.html +++ b/src/pretix/presale/templates/pretixpresale/event/login.html @@ -23,13 +23,13 @@ {% bootstrap_field login_form.password layout="horizontal" %}
- @@ -47,7 +47,19 @@ @@ -71,7 +83,7 @@ {% bootstrap_field registration_form.password_repeat layout="horizontal" %}
-
+
diff --git a/src/pretix/presale/templates/pretixpresale/event/recover.html b/src/pretix/presale/templates/pretixpresale/event/recover.html index 7610ab0fc..8749c21b0 100644 --- a/src/pretix/presale/templates/pretixpresale/event/recover.html +++ b/src/pretix/presale/templates/pretixpresale/event/recover.html @@ -11,7 +11,7 @@ {% bootstrap_field form.password_repeat layout="horizontal" %}
-
+
diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py index bb44e2646..da2bd95ac 100644 --- a/src/pretix/presale/views/__init__.py +++ b/src/pretix/presale/views/__init__.py @@ -1,7 +1,6 @@ from datetime import timedelta from itertools import groupby -from django.contrib.auth.decorators import login_required from django.contrib.auth.views import redirect_to_login from django.core.urlresolvers import reverse from django.db.models import Q @@ -12,14 +11,54 @@ from pretix.base.models import CartPosition from pretix.base.signals import register_payment_providers -class LoginRequiredMixin: +def login_required(view_func): + def _wrapped_view(request, *args, **kwargs): + if request.user.is_authenticated(): + return view_func(request, *args, **kwargs) + path = request.path + return redirect_to_login( + path, reverse('presale:event.checkout.login', kwargs={ + 'organizer': request.event.organizer.slug, + 'event': request.event.slug, + }), 'next' + ) + return _wrapped_view + +def login_or_guest_required(view_func): + def _wrapped_view(request, *args, **kwargs): + if request.user.is_authenticated() or 'guest_email' in request.session: + return view_func(request, *args, **kwargs) + path = request.path + return redirect_to_login( + path, reverse('presale:event.checkout.login', kwargs={ + 'organizer': request.event.organizer.slug, + 'event': request.event.slug, + }), 'next' + ) + return _wrapped_view + + +class LoginRequiredMixin: @classmethod def as_view(cls, **initkwargs): view = super().as_view(**initkwargs) return login_required(view) +class LoginOrGuestRequiredMixin: + @classmethod + def as_view(cls, **initkwargs): + view = super().as_view(**initkwargs) + return login_or_guest_required(view) + + +def user_cart_q(request): + if request.user.is_authenticated(): + return Q(Q(user=request.user) | Q(session=request.session.session_key)) + return Q(Q(user__isnull=True) & Q(session=request.session.session_key)) + + class CartDisplayMixin: @cached_property @@ -28,7 +67,7 @@ class CartDisplayMixin: A list of this users cart position """ return list(CartPosition.objects.current.filter( - Q(user=self.request.user) & Q(event=self.request.event) + user_cart_q(self.request) & Q(event=self.request.event) ).order_by( 'item', 'variation' ).select_related( @@ -40,7 +79,7 @@ class CartDisplayMixin: def get_cart(self, answers=False, queryset=None, payment_fee=None): queryset = queryset or CartPosition.objects.current.filter( - Q(user=self.request.user) & Q(event=self.request.event) + user_cart_q(self.request) & Q(event=self.request.event) ) prefetch = ['variation__values', 'variation__values__prop'] @@ -106,7 +145,6 @@ class CartDisplayMixin: class EventViewMixin: - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['event'] = self.request.event diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 20a3d8bc8..b15744958 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -13,7 +13,9 @@ from django.views.generic import View from pretix.base.models import ( CartPosition, EventLock, Item, ItemVariation, Quota, ) -from pretix.presale.views import EventViewMixin, LoginRequiredMixin +from pretix.presale.views import ( + EventViewMixin, LoginOrGuestRequiredMixin, user_cart_q, +) class CartActionMixin: @@ -62,13 +64,13 @@ class CartActionMixin: return items -class CartRemove(EventViewMixin, CartActionMixin, LoginRequiredMixin, View): +class CartRemove(EventViewMixin, CartActionMixin, LoginOrGuestRequiredMixin, View): def post(self, *args, **kwargs): items = self._items_from_post_data() if not items: return redirect(self.get_failure_url()) - qw = Q(user=self.request.user) + qw = user_cart_q(self.request) for item, variation, cnt in items: cw = qw & Q(item_id=item) @@ -112,7 +114,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): # We do not use LoginRequiredMixin here, as we want to store stuff into the # session before redirecting to login - if not request.user.is_authenticated(): + if not request.user.is_authenticated() and 'guest_email' not in request.session: request.session['cart_tmp'] = json.dumps(self.items) return redirect_to_login( self.get_success_url(), reverse('presale:event.checkout.login', kwargs={ @@ -121,7 +123,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): }), 'next' ) - existing = CartPosition.objects.current.filter(user=self.request.user, event=self.request.event).count() + existing = CartPosition.objects.current.filter(user_cart_q(self.request) & Q(event=self.request.event)).count() if sum(i[2] for i in self.items) + existing > int(self.request.event.settings.max_items_per_order): # TODO: i18n plurals self.error_message(self.error_messages['max_items'] % self.request.event.settings.max_items_per_order) @@ -142,7 +144,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): # For items that are already expired, we have to delete and re-add them, as they might # be no longer available or prices might have changed. Sorry! for cp in CartPosition.objects.current.filter( - Q(user=self.request.user) & Q(event=self.request.event) & Q(expires__lte=now()) + user_cart_q(self.request) & Q(event=self.request.event) & Q(expires__lte=now()) ): self._re_add_position(cp) positions.add(cp) @@ -153,7 +155,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View): # cart expire at the same time # We can extend the reservation of items which are not yet expired without risk CartPosition.objects.current.filter( - Q(user=self.request.user) & Q(event=self.request.event) & Q(expires__gt=now()) + user_cart_q(self.request) & Q(event=self.request.event) & Q(expires__gt=now()) ).update(expires=expiry) def _delete_expired(self): @@ -237,14 +239,18 @@ class CartAdd(EventViewMixin, CartActionMixin, View): cp.price = price cp.save() else: - CartPosition.objects.create( + cp = CartPosition( event=self.request.event, - user=self.request.user, item=item, variation=variation, price=price, expires=expiry ) + if self.request.user.is_authenticated(): + cp.user = self.request.user + else: + cp.session = self.request.session.session_key + cp.save() self._delete_expired() diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py index f1eb09497..dbac1510a 100644 --- a/src/pretix/presale/views/checkout.py +++ b/src/pretix/presale/views/checkout.py @@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse from django.db.models import Q, Sum from django.http import HttpRequest from django.shortcuts import redirect +from django.utils import translation from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from django.views.generic import TemplateView @@ -12,11 +13,13 @@ from pretix.base.services.orders import OrderError, perform_order from pretix.base.signals import register_payment_providers from pretix.presale.forms.checkout import QuestionsForm from pretix.presale.views import ( - CartDisplayMixin, EventViewMixin, LoginRequiredMixin, + CartDisplayMixin, EventViewMixin, LoginOrGuestRequiredMixin, + LoginRequiredMixin, user_cart_q, ) class CheckoutView(TemplateView): + def get_payment_url(self): return reverse('presale:event.checkout.payment', kwargs={ 'event': self.request.event.slug, @@ -41,12 +44,12 @@ class CheckoutView(TemplateView): 'organizer': self.request.event.organizer.slug }) - def get_order_url(self, order): + def get_order_url(self, order, add_secret): return reverse('presale:event.order', kwargs={ 'event': self.request.event.slug, 'organizer': self.request.event.organizer.slug, 'order': order.code, - }) + }) + '?thanks=yes' + ('&order_secret=' + order.secret if add_secret else '') class QuestionsViewMixin: @@ -106,7 +109,7 @@ class QuestionsViewMixin: return not failed -class CheckoutStart(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, +class CheckoutStart(EventViewMixin, CartDisplayMixin, LoginOrGuestRequiredMixin, QuestionsViewMixin, CheckoutView): template_name = "pretixpresale/event/checkout_questions.html" @@ -138,13 +141,13 @@ class CheckoutStart(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, return ctx -class PaymentDetails(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, CheckoutView): +class PaymentDetails(EventViewMixin, CartDisplayMixin, LoginOrGuestRequiredMixin, CheckoutView): template_name = "pretixpresale/event/checkout_payment.html" @cached_property def _total_order_value(self): return CartPosition.objects.current.filter( - Q(user=self.request.user) & Q(event=self.request.event) + user_cart_q(self.request) & Q(event=self.request.event) ).aggregate(sum=Sum('price'))['sum'] @cached_property @@ -194,7 +197,7 @@ class PaymentDetails(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, Check return self.get_questions_url() + "?back=true" -class OrderConfirm(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, CheckoutView): +class OrderConfirm(EventViewMixin, CartDisplayMixin, LoginOrGuestRequiredMixin, CheckoutView): template_name = "pretixpresale/event/checkout_confirm.html" def __init__(self, *args, **kwargs): @@ -256,14 +259,18 @@ class OrderConfirm(EventViewMixin, CartDisplayMixin, LoginRequiredMixin, Checkou def perform_order(self, request: HttpRequest): try: - order = perform_order(self.request.event, self.request.user, self.payment_provider, self.positions) + order = perform_order(self.request.event, self.payment_provider, self.positions, + user=request.user if request.user.is_authenticated() else None, + email=request.session.get('guest_email', None), + locale=translation.get_language()) except OrderError as e: messages.error(request, str(e)) return redirect(self.get_confirm_url()) else: - messages.success(request, _('Your order has been placed.')) + # Message is delivered via GET parameter + # messages.success(request, _('Your order has been placed.')) resp = self.payment_provider.payment_perform(request, order) - return redirect(resp or self.get_order_url(order)) + return redirect(resp or self.get_order_url(order, not request.user.is_authenticated())) def get_previous_url(self): if self.payment_provider.identifier != "free": diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index 91d114de9..f01f5b11f 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -21,6 +21,7 @@ from pretix.base.forms.user import UserSettingsForm from pretix.base.models import User from pretix.base.services.mail import mail from pretix.helpers.urls import build_absolute_uri +from pretix.presale.forms.checkout import GuestForm from pretix.presale.views import ( CartDisplayMixin, EventViewMixin, LoginRequiredMixin, ) @@ -78,7 +79,7 @@ class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView): key=lambda group: (group[0].position, group[0].identity) if group[0] is not None else (0, "") ) - context['cart'] = self.get_cart() if self.request.user.is_authenticated() else None + context['cart'] = self.get_cart() return context @@ -111,6 +112,11 @@ class EventLogin(EventViewMixin, TemplateView): if form.is_valid() and form.user_cache: login(request, form.user_cache) return self.redirect_to_next() + elif request.POST.get('form') == 'guest': + form = self.guest_form + if form.is_valid(): + request.session['guest_email'] = form.cleaned_data['email'] + return self.redirect_to_next() elif request.POST.get('form') == 'registration': form = self.registration_form if form.is_valid(): @@ -131,6 +137,12 @@ class EventLogin(EventViewMixin, TemplateView): data=self.request.POST if self.request.POST.get('form', '') == 'login' else None ) + @cached_property + def guest_form(self): + return GuestForm( + data=self.request.POST if self.request.POST.get('form', '') == 'guest' else None + ) + @cached_property def registration_form(self): return RegistrationForm( @@ -141,6 +153,7 @@ class EventLogin(EventViewMixin, TemplateView): context = super().get_context_data(**kwargs) context['login_form'] = self.login_form context['registration_form'] = self.registration_form + context['guest_form'] = self.guest_form return context @@ -163,24 +176,19 @@ class EventForgot(EventViewMixin, TemplateView): def post(self, request, *args, **kwargs): if self.form.is_valid(): user = self.form.cleaned_data['user'] - if user.email: - mail( - user, _('Password recovery'), - 'pretixpresale/email/forgot.txt', - { - 'user': user, - 'event': self.request.event, - 'url': build_absolute_uri('presale:event.forgot.recover', kwargs={ - 'event': self.request.event.slug, - 'organizer': self.request.event.organizer.slug, - }) + '?token=' + self.generate_token(user), - }, - self.request.event - ) - messages.success(request, _('We sent you an e-mail containing further instructions.')) - else: - messages.success(request, _('We are unable to send you a new password, as you did not enter an e-mail ' - 'address at your registration.')) + mail( + user.email, _('Password recovery'), 'pretixpresale/email/forgot.txt', + { + 'user': user, + 'event': self.request.event, + 'url': build_absolute_uri('presale:event.forgot.recover', kwargs={ + 'event': self.request.event.slug, + 'organizer': self.request.event.organizer.slug, + }) + '?token=' + self.generate_token(user), + }, + self.request.event, locale=user.locale + ) + messages.success(request, _('We sent you an e-mail containing further instructions.')) return redirect('presale:event.forgot', organizer=self.request.event.organizer.slug, event=self.request.event.slug) diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index db2a49846..4534675e3 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -2,34 +2,28 @@ from datetime import timedelta from django.contrib import messages from django.core.urlresolvers import reverse +from django.db.models import Q from django.http import HttpResponseForbidden, HttpResponseNotFound from django.shortcuts import redirect from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from django.views.generic import TemplateView, View - from pretix.base.models import CachedFile, CachedTicket, Order, OrderPosition from pretix.base.services.tickets import generate -from pretix.base.signals import ( - register_payment_providers, register_ticket_outputs, -) -from pretix.presale.views import ( - CartDisplayMixin, EventViewMixin, LoginRequiredMixin, -) +from pretix.base.signals import register_payment_providers, register_ticket_outputs +from pretix.presale.views import CartDisplayMixin, EventViewMixin from pretix.presale.views.checkout import QuestionsViewMixin class OrderDetailMixin: - @cached_property def order(self): try: - return Order.objects.current.get( - user=self.request.user, - event=self.request.event, - code=self.kwargs['order'], - ) + q = Q(Q(secret__isnull=False) & Q(secret__in=self.request.session['order_secrets'])) + if self.request.user.is_authenticated(): + q |= Q(user=self.request.user) + return Order.objects.current.get(q & Q(event=self.request.event) & Q(code=self.kwargs['order'])) except Order.DoesNotExist: return None @@ -49,8 +43,7 @@ class OrderDetailMixin: }) -class OrderDetails(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, - CartDisplayMixin, TemplateView): +class OrderDetails(EventViewMixin, OrderDetailMixin, CartDisplayMixin, TemplateView): template_name = "pretixpresale/event/order.html" def get(self, request, *args, **kwargs): @@ -102,7 +95,7 @@ class OrderDetails(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, return ctx -class OrderPay(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, TemplateView): +class OrderPay(EventViewMixin, OrderDetailMixin, TemplateView): template_name = "pretixpresale/event/order_pay.html" def dispatch(self, request, *args, **kwargs): @@ -145,7 +138,7 @@ class OrderPay(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, TemplateVie }) -class OrderPayDo(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, TemplateView): +class OrderPayDo(EventViewMixin, OrderDetailMixin, TemplateView): template_name = "pretixpresale/event/order_pay_confirm.html" def dispatch(self, request, *args, **kwargs): @@ -185,8 +178,7 @@ class OrderPayDo(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, TemplateV }) -class OrderModify(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, - QuestionsViewMixin, TemplateView): +class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, TemplateView): template_name = "pretixpresale/event/order_modify.html" @cached_property @@ -227,8 +219,7 @@ class OrderModify(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, return ctx -class OrderCancel(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, - TemplateView): +class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView): template_name = "pretixpresale/event/order_cancel.html" def dispatch(self, request, *args, **kwargs): @@ -255,9 +246,7 @@ class OrderCancel(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, return ctx -class OrderDownload(EventViewMixin, LoginRequiredMixin, OrderDetailMixin, - View): - +class OrderDownload(EventViewMixin, OrderDetailMixin, View): @cached_property def output(self): responses = register_ticket_outputs.send(self.request.event) diff --git a/src/tests/base/test_mail.py b/src/tests/base/test_mail.py index 672984753..4f4092fab 100644 --- a/src/tests/base/test_mail.py +++ b/src/tests/base/test_mail.py @@ -26,8 +26,7 @@ def test_send_mail_with_prefix(client, env): djmail.outbox = [] event, user, organizer = env event.settings.set('mail_prefix', 'test') - mail(user, 'Test subject', - 'mailtest.txt', {}, event) + mail('dummy@dummy.dummy', 'Test subject', 'mailtest.txt', {}, event) assert len(djmail.outbox) == 1 assert djmail.outbox[0].to == [user.email] @@ -39,8 +38,7 @@ def test_send_mail_with_event_sender(client, env): djmail.outbox = [] event, user, organizer = env event.settings.set('mail_from', 'foo@bar') - mail(user, 'Test subject', - 'mailtest.txt', {}, event) + mail('dummy@dummy.dummy', 'Test subject', 'mailtest.txt', {}, event) assert len(djmail.outbox) == 1 assert djmail.outbox[0].to == [user.email] @@ -52,8 +50,7 @@ def test_send_mail_with_event_sender(client, env): def test_send_mail_with_default_sender(client, env): djmail.outbox = [] event, user, organizer = env - mail(user, 'Test subject', - 'mailtest.txt', {}, event) + mail('dummy@dummy.dummy', 'Test subject', 'mailtest.txt', {}, event) del event.settings['mail_from'] assert len(djmail.outbox) == 1 @@ -68,8 +65,7 @@ def test_send_mail_with_user_locale(client, env): event, user, organizer = env user.locale = 'de' user.save() - mail(user, _('User'), - 'mailtest.txt', {}, event) + mail('dummy@dummy.dummy', _('User'), 'mailtest.txt', {}, event, locale=user.locale) del event.settings['mail_from'] assert len(djmail.outbox) == 1