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/migrations/0016_order_guest_email.py b/src/pretix/base/migrations/0016_order_guest_email.py new file mode 100644 index 000000000..fef35d8b0 --- /dev/null +++ b/src/pretix/base/migrations/0016_order_guest_email.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0015_auto_20150916_2219'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='guest_email', + field=models.EmailField(max_length=254, verbose_name='E-mail', blank=True, null=True), + ), + ] diff --git a/src/pretix/base/migrations/0017_order_guest_locale.py b/src/pretix/base/migrations/0017_order_guest_locale.py new file mode 100644 index 000000000..79a63f7e2 --- /dev/null +++ b/src/pretix/base/migrations/0017_order_guest_locale.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0016_order_guest_email'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='guest_locale', + field=models.CharField(max_length=32, null=True, blank=True, verbose_name='Locale'), + ), + ] diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index cf2ded385..7478e1602 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,15 @@ class Order(Versionable): verbose_name=_("User"), related_name="orders" ) + guest_email = models.EmailField( + null=True, blank=True, + verbose_name=_('E-mail') + ) + guest_locale = models.CharField( + null=True, blank=True, max_length=32, + verbose_name=_('Locale') + ) + secret = models.CharField(max_length=32, default=generate_secret) datetime = models.DateTimeField( verbose_name=_("Date") ) @@ -1671,6 +1598,18 @@ class Order(Versionable): return error_messages['busy'] return True + @property + def locale(self): + if self.user: + return self.user.locale + return self.guest_locale + + @property + def email(self): + if self.user: + return self.user.email + return self.guest_email + class CachedTicket(models.Model): order = VersionedForeignKey(Order, on_delete=models.CASCADE) @@ -1824,6 +1763,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/base/payment.py b/src/pretix/base/payment.py index 626e4917b..060549bc6 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -14,6 +14,7 @@ from pretix.base.models import CartPosition, Order from pretix.base.services.orders import mark_order_paid from pretix.base.settings import SettingsSandbox from pretix.base.signals import register_payment_providers +from pretix.presale.views import user_cart_q class BasePaymentProvider: @@ -441,7 +442,7 @@ class FreeOrderProvider(BasePaymentProvider): def is_allowed(self, request: HttpRequest) -> bool: return CartPosition.objects.current.filter( - Q(user=request.user) & Q(event=request.event) + user_cart_q(request) & Q(event=request.event) ).aggregate(sum=Sum('price'))['sum'] == 0 diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 37ec41f86..8893ebff3 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -13,7 +13,7 @@ from pretix.helpers.urls import build_absolute_uri logger = logging.getLogger('pretix.base.mail') -def mail(user: User, subject: str, template: str, context: dict=None, event: Event=None): +def mail(email: str, subject: str, template: str, context: dict=None, event: Event=None, locale: str=None): """ Sends out an email to a user. @@ -30,11 +30,8 @@ def mail(user: User, subject: str, template: str, context: dict=None, event: Eve the email has been sent, just that it has been queued by the e-mail backend. """ - if not user.email: - return False - _lng = translation.get_language() - translation.activate(user.locale or settings.LANGUAGE_CODE) + translation.activate(locale or settings.LANGUAGE_CODE) if isinstance(template, LazyI18nString): body = str(template) @@ -66,7 +63,7 @@ def mail(user: User, subject: str, template: str, context: dict=None, event: Eve ) body += "\r\n" try: - return mail_send([user.email], subject, body, sender) + return mail_send([email], subject, body, sender) finally: translation.activate(_lng) diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index a998adfa5..0683d56b6 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -4,13 +4,16 @@ from django.db import transaction from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ -from pretix.base.models import EventLock, Order, OrderPosition, Quota +from pretix.base.models import ( + Event, EventLock, Order, OrderPosition, Quota, User, +) from pretix.base.services.mail import mail from pretix.base.signals import order_paid, order_placed from pretix.helpers.urls import build_absolute_uri -def mark_order_paid(order, provider=None, info=None, date=None, manual=None, force=False): +def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None, + force: bool=False): """ Marks an order as paid. This clones the order object, sets the payment provider, info and date and returns the cloned order object. @@ -44,20 +47,19 @@ def mark_order_paid(order, provider=None, info=None, date=None, manual=None, for from pretix.base.services.mail import mail mail( - order.user, _('Payment received for your order: %(code)s') % {'code': order.code}, + order.email, _('Payment received for your order: %(code)s') % {'code': order.code}, 'pretixpresale/email/order_paid.txt', { - 'user': order.user, 'order': order, 'event': order.event, 'url': build_absolute_uri('presale:event.order', kwargs={ 'event': order.event.slug, 'organizer': order.event.organizer.slug, 'order': order.code, - }), + }) + '?order_secret=' + order.secret, 'downloads': order.event.settings.get('ticket_download', as_type=bool) }, - order.event + order.event, locale=order.locale ) return order @@ -66,7 +68,7 @@ class OrderError(Exception): pass -def check_positions(event, dt, positions): +def check_positions(event: Event, dt: datetime, positions: list): error_messages = { 'unavailable': _('Some of the products you selected were no longer available. ' 'Please see below for details.'), @@ -117,7 +119,8 @@ def check_positions(event, dt, positions): raise OrderError(err) -def perform_order(event, user, payment_provider, positions): +def perform_order(event: Event, payment_provider: str, positions: list, user: User=None, email: str=None, + locale: str=None): error_messages = { 'busy': _('We were not able to process your request completely as the ' 'server was too busy. Please try again.'), @@ -127,21 +130,22 @@ def perform_order(event, user, payment_provider, positions): try: with event.lock(): check_positions(event, dt, positions) - order = place_order(event, user, positions, dt, payment_provider) + order = place_order(event, user, email if user is None else None, positions, dt, payment_provider, + locale=locale) mail( - user, _('Your order: %(code)s') % {'code': order.code}, + order.email, _('Your order: %(code)s') % {'code': order.code}, 'pretixpresale/email/order_placed.txt', { - 'user': user, 'order': order, + 'order': order, 'event': event, 'url': build_absolute_uri('presale:event.order', kwargs={ 'event': event.slug, 'organizer': event.organizer.slug, 'order': order.code, - }), + }) + '?order_secret=' + order.secret, 'payment': payment_provider.order_pending_mail_render(order) }, - event + event, locale=order.locale ) return order except EventLock.LockTimeoutException: @@ -151,7 +155,8 @@ def perform_order(event, user, payment_provider, positions): @transaction.atomic() -def place_order(event, user, positions, dt, payment_provider): +def place_order(event: Event, user: User, email: str, positions: list, dt: datetime, payment_provider: str, + locale: str=None): total = sum([c.price for c in positions]) payment_fee = payment_provider.calculate_fee(total) total += payment_fee @@ -162,8 +167,10 @@ def place_order(event, user, positions, dt, payment_provider): status=Order.STATUS_PENDING, event=event, user=user, + guest_email=email, datetime=dt, expires=min(expires), + locale=locale, total=total, payment_fee=payment_fee, payment_provider=payment_provider.identifier, 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/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index 51ee169f4..b36f78a8d 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -66,8 +66,8 @@