mirror of
https://github.com/pretix/pretix.git
synced 2026-05-03 14:54:04 +00:00
Refs #96 -- Completely removed local users
This commit is contained in:
@@ -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,
|
||||
|
||||
158
src/pretix/base/forms/auth.py
Normal file
158
src/pretix/base/forms/auth.py
Normal file
@@ -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'
|
||||
)
|
||||
@@ -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
|
||||
|
||||
47
src/pretix/base/migrations/0015_auto_20150916_2219.py
Normal file
47
src/pretix/base/migrations/0015_auto_20150916_2219.py
Normal file
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user