Refs #96 -- Completely removed local users

This commit is contained in:
Raphael Michel
2015-09-17 00:52:36 +02:00
parent 0dccdcb0f7
commit 7def097dcd
37 changed files with 367 additions and 707 deletions

View File

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

View 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'
)

View File

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

View 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',
),
]

View File

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