diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py
index 7bd883452..66c76a4df 100644
--- a/src/pretix/base/models.py
+++ b/src/pretix/base/models.py
@@ -86,6 +86,23 @@ class UserManager(BaseUserManager):
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, username, password=None):
if password is None:
raise Exception("You must provide a password")
@@ -121,7 +138,7 @@ class User(AbstractBaseUser, PermissionsMixin):
(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.id}.event.pretix" for local users, who
+ (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
@@ -136,7 +153,7 @@ class User(AbstractBaseUser, PermissionsMixin):
identifier = models.CharField(max_length=255, unique=True)
username = models.CharField(max_length=120, blank=True,
null=True,
- help_text=_('Letters, digits and @/./+/-/_ only.'))
+ help_text=_('Letters, digits and ./+/-/_ only.'))
event = models.ForeignKey('Event', related_name="users",
null=True, blank=True,
on_delete=models.PROTECT)
@@ -1398,10 +1415,6 @@ class CartPosition(Versionable):
User, null=True, blank=True,
verbose_name=_("User")
)
- session = models.CharField(
- max_length=255, null=True, blank=True,
- verbose_name=_("Session key")
- )
item = VersionedForeignKey(
Item,
verbose_name=_("Item")
@@ -1444,7 +1457,7 @@ class OrganizerSetting(Versionable):
organizer. It will be inherited by the events of this organizer
"""
DEFAULTS = {
-
+ 'user_mail_required': 'False'
}
organizer = VersionedForeignKey(Organizer, related_name='setting_objects')
key = models.CharField(max_length=255)
diff --git a/src/pretix/presale/templates/pretixpresale/event/login.html b/src/pretix/presale/templates/pretixpresale/event/login.html
new file mode 100644
index 000000000..b4e74ea89
--- /dev/null
+++ b/src/pretix/presale/templates/pretixpresale/event/login.html
@@ -0,0 +1,102 @@
+{% extends "pretixpresale/event/base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% block title %}{% trans "Login" %}{% endblock %}
+{% block content %}
+
{% trans "Login" %}
+ {% trans "You need to login or register to continue" %}
+
+
+
+ {% if global_registration_form %}
+
+ {% endif %}
+
+{% endblock %}
diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py
index 9491ed578..6dba7a4a9 100644
--- a/src/pretix/presale/urls.py
+++ b/src/pretix/presale/urls.py
@@ -14,6 +14,7 @@ urlpatterns = patterns(
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'),
url(r'^checkout$', pretix.presale.views.checkout.CheckoutStart.as_view(), name='event.checkout.start'),
+ url(r'^login$', pretix.presale.views.event.EventLogin.as_view(), name='event.checkout.login'),
)
)),
)
diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py
index 7a0285c1d..2940eef04 100644
--- a/src/pretix/presale/views/__init__.py
+++ b/src/pretix/presale/views/__init__.py
@@ -1,6 +1,8 @@
import uuid
from itertools import groupby
from datetime import timedelta
+from django.contrib.auth.views import redirect_to_login
+from django.core.urlresolvers import reverse
from django.db.models import Q
from django.utils.timezone import now
@@ -8,21 +10,32 @@ from django.utils.timezone import now
from pretix.base.models import CartPosition
-class CartMixin:
- def get_session_key(self):
- if 'cart_key' in self.request.session:
- return self.request.session.get('cart_key')
- key = str(uuid.uuid4())
- self.request.session['cart_key'] = key
- return key
+class EventLoginRequiredMixin:
+
+ @classmethod
+ def as_view(cls, **initkwargs):
+ view = super(EventLoginRequiredMixin, cls).as_view(**initkwargs)
+
+ def decorator(view_func):
+ def _wrapped_view(request, *args, **kwargs):
+ if request.user.is_authenticated() and \
+ (request.user.event is None or request.user.event == request.event):
+ 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
+ return decorator(view)
-class CartDisplayMixin(CartMixin):
+class CartDisplayMixin:
def get_cart(self):
- qw = Q(session=self.get_session_key())
- if self.request.user.is_authenticated():
- qw |= Q(user=self.request.user)
+ qw = Q(user=self.request.user)
cartpos = list(CartPosition.objects.current.filter(
qw & Q(event=self.request.event)
diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py
index 89cffc76b..77b1fb839 100644
--- a/src/pretix/presale/views/cart.py
+++ b/src/pretix/presale/views/cart.py
@@ -1,6 +1,8 @@
from datetime import timedelta
+import json
from django.contrib import messages
+from django.contrib.auth.views import redirect_to_login
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.shortcuts import redirect
@@ -9,10 +11,10 @@ from django.views.generic import View
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Item, ItemVariation, Quota, CartPosition
-from pretix.presale.views import CartMixin, EventViewMixin
+from pretix.presale.views import EventLoginRequiredMixin, EventViewMixin
-class CartActionMixin(CartMixin):
+class CartActionMixin:
def get_next_url(self):
if "next" in self.request.GET and '://' not in self.request.GET:
@@ -66,15 +68,13 @@ class CartActionMixin(CartMixin):
return items
-class CartRemove(EventViewMixin, CartActionMixin, View):
+class CartRemove(EventViewMixin, CartActionMixin, EventLoginRequiredMixin, View):
def post(self, *args, **kwargs):
items = self._items_from_post_data()
if not items:
return redirect(self.get_failure_url())
- qw = Q(session=self.get_session_key())
- if self.request.user.is_authenticated():
- qw |= Q(user=self.request.user)
+ qw = Q(user=self.request.user)
for item, variation, cnt in items:
cw = qw & Q(item_id=item)
@@ -91,33 +91,44 @@ class CartRemove(EventViewMixin, CartActionMixin, View):
class CartAdd(EventViewMixin, CartActionMixin, View):
- def post(self, *args, **kwargs):
+ def post(self, request, *args, **kwargs):
items = self._items_from_post_data()
+
+ # We do not use EventLoginRequiredMixin here, as we want to store stuff into the
+ # session beforehand
+ if not request.user.is_authenticated() or \
+ (request.user.event is None or request.user.event == request.event):
+ request.session['cart_tmp'] = json.dumps(items)
+ return redirect_to_login(
+ request.path, reverse('presale:event.checkout.login', kwargs={
+ 'organizer': request.event.organizer.slug,
+ 'event': request.event.slug,
+ }), 'next'
+ )
+ return self.process(items)
+
+ def process(self, items):
if not items:
return redirect(self.get_failure_url())
-
- if sum(i[2] for i in items) > self.request.event.max_items_per_order:
+ existing = CartPosition.objects.current.filter(user=self.request.user, event=self.request.event).count()
+ if sum(i[2] for i in items) + existing > self.request.event.max_items_per_order:
# TODO: i18n plurals
messages.error(self.request,
_("You cannot select more than %d items per order") % self.event.max_items_per_order)
return redirect(self.get_failure_url())
# Extend this user's cart session to 30 minutes from now to ensure all items in the
- # cart expire at the same time
- qw = Q(session=self.get_session_key())
- if self.request.user.is_authenticated():
- qw |= Q(user=self.request.user)
-
+ # cart expire at the same
# We can extend the reservation of items which are not yet expired without
# risk
CartPosition.objects.current.filter(
- qw & Q(event=self.request.event) & Q(expires__gt=now())
+ Q(user=self.request.user) & Q(event=self.request.event) & Q(expires__gt=now())
).update(expires=now() + timedelta(minutes=30))
# For items that are already expired, we have to delete and re-add them, as they might
# be no longer available. Sorry!
for cp in CartPosition.objects.current.filter(
- qw & Q(event=self.request.event) & Q(expires__lte=now())):
+ Q(user=self.request.user) & Q(event=self.request.event) & Q(expires__lte=now())):
items = self._re_add_position(items, cp)
cp.delete()
@@ -203,8 +214,7 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
for k in range(quota_ok):
CartPosition.objects.create(
event=self.request.event,
- session=self.get_session_key(),
- user=(self.request.user if self.request.user.is_authenticated() else None),
+ user=self.request.user,
item=item,
variation=variation,
price=price,
@@ -227,3 +237,10 @@ class CartAdd(EventViewMixin, CartActionMixin, View):
messages.success(self.request, _('The items have been successfully added to your cart.'))
return redirect(self.get_success_url())
+
+ def get(self, request, *args, **kwargs):
+ if 'cart_tmp' in request.session and request.user.is_authenticated():
+ items = json.loads(request.session['cart_tmp'])
+ del request.session['cart_tmp']
+ return self.process(items)
+ return redirect(self.get_failure_url())
diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py
index ed494e626..69d88e2c9 100644
--- a/src/pretix/presale/views/checkout.py
+++ b/src/pretix/presale/views/checkout.py
@@ -4,10 +4,10 @@ from django.shortcuts import redirect
from django.views.generic import View
from django.utils.translation import ugettext_lazy as _
-from pretix.presale.views import EventViewMixin, CartDisplayMixin
+from pretix.presale.views import EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin
-class CheckoutStart(EventViewMixin, CartDisplayMixin, View):
+class CheckoutStart(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, View):
def get_failure_url(self):
return reverse('presale:event.index', kwargs={
diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py
index 7318c1fed..f3172e353 100644
--- a/src/pretix/presale/views/event.py
+++ b/src/pretix/presale/views/event.py
@@ -1,5 +1,17 @@
+from django.contrib.auth import authenticate
+from django.core.urlresolvers import reverse
+from django.core.validators import RegexValidator
from django.db.models import Count
+from django import forms
+from django.shortcuts import redirect
+from django.utils.functional import cached_property
+from django.contrib.auth.forms import AuthenticationForm as BaseAuthenticationForm
+from django.contrib.auth import login
from django.views.generic import TemplateView
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+from pretix.base.models import User
+
from pretix.presale.views import EventViewMixin, CartDisplayMixin
@@ -41,5 +53,224 @@ class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView):
for cat in set([i.category for i in items]) # insert categories into a set for uniqueness
], key=lambda group: (group[0].position, group[0].pk)) # a set is unsorted, so sort again by category
- context['cart'] = self.get_cart()
+ context['cart'] = self.get_cart() if self.request.user.is_authenticated() else None
+ return context
+
+
+class LoginForm(BaseAuthenticationForm):
+ username = forms.CharField(
+ label=_('Username'),
+ help_text=_('If you registered for multiple events, your username is your email address.')
+ )
+ 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 == 'True')
+
+ 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 EventLogin(EventViewMixin, TemplateView):
+ template_name = 'pretixpresale/event/login.html'
+
+ def redirect_to_next(self):
+ if 'next' in self.request.GET:
+ return redirect(self.request.GET.get('next'))
+ else:
+ return redirect(reverse(
+ 'presale:event.index', kwargs={
+ 'organizer': self.request.event.organizer.slug,
+ 'event': self.request.event.slug,
+ }
+ ))
+
+ def get(self, request, *args, **kwargs):
+ if request.user.is_authenticated() and \
+ (request.user.event is None or request.user.event == request.event):
+ return self.redirect_to_next()
+ return super().get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ if request.POST.get('form') == 'login':
+ form = self.login_form
+ if form.is_valid() and form.user_cache:
+ login(request, form.user_cache)
+ return self.redirect_to_next()
+ elif request.POST.get('form') == 'local_registration':
+ form = self.local_registration_form
+ if form.is_valid():
+ user = User.objects.create_local_user(
+ request.event, form.cleaned_data['username'], form.cleaned_data['password'],
+ email=form.cleaned_data['email'] if form.cleaned_data['email'] != '' else None
+ )
+ user = authenticate(identifier=user.identifier, password=form.cleaned_data['password'])
+ login(request, user)
+ return self.redirect_to_next()
+ elif request.POST.get('form') == 'global_registration':
+ form = self.global_registration_form
+ if form.is_valid():
+ user = User.objects.create_global_user(
+ form.cleaned_data['email'], form.cleaned_data['password'],
+ )
+ user = authenticate(identifier=user.identifier, password=form.cleaned_data['password'])
+ login(request, user)
+ return self.redirect_to_next()
+ return super().get(request, *args, **kwargs)
+
+ @cached_property
+ def login_form(self):
+ return LoginForm(
+ self.request,
+ data=self.request.POST if self.request.POST.get('form', '') == 'login' else None
+ )
+
+ @cached_property
+ def global_registration_form(self):
+ if settings.PRETIX_GLOBAL_REGISTRATION:
+ return GlobalRegistrationForm(
+ data=self.request.POST if self.request.POST.get('form', '') == 'global_registration' else None
+ )
+ else:
+ return None
+
+ @cached_property
+ def local_registration_form(self):
+ return LocalRegistrationForm(
+ self.request,
+ data=self.request.POST if self.request.POST.get('form', '') == 'local_registration' else None
+ )
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['login_form'] = self.login_form
+ context['global_registration_form'] = self.global_registration_form
+ context['local_registration_form'] = self.local_registration_form
return context
diff --git a/src/pretix/settings.py b/src/pretix/settings.py
index 27d332b12..70f81cc97 100644
--- a/src/pretix/settings.py
+++ b/src/pretix/settings.py
@@ -149,8 +149,9 @@ DEBUG_TOOLBAR_CONFIG = {
# Pretix specific settings
-
PRETIX_INSTANCE_NAME = 'pretix.de'
+PRETIX_GLOBAL_REGISTRATION = True
+
DEFAULT_CURRENCY = 'EUR'
INTERNAL_IPS = ('127.0.0.1', '::1')