From 54b494890e5342d150be22e633212758e7249afe Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 17 Feb 2015 23:27:43 +0100 Subject: [PATCH] Login is mandatory for adding things to a card --- src/pretix/base/models.py | 27 +- .../templates/pretixpresale/event/login.html | 102 ++++++++ src/pretix/presale/urls.py | 1 + src/pretix/presale/views/__init__.py | 35 ++- src/pretix/presale/views/cart.py | 53 ++-- src/pretix/presale/views/checkout.py | 4 +- src/pretix/presale/views/event.py | 233 +++++++++++++++++- src/pretix/settings.py | 3 +- 8 files changed, 418 insertions(+), 40 deletions(-) create mode 100644 src/pretix/presale/templates/pretixpresale/event/login.html 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" %}

+
+
+ +
+
+
+ {% csrf_token %} + {% bootstrap_form_errors login_form type='all' layout='inline' %} + {% bootstrap_field login_form.username layout="horizontal" %} + {% bootstrap_field login_form.password layout="horizontal" %} + +
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ {% csrf_token %} + {% bootstrap_form_errors local_registration_form type='all' layout='inline' %} + {% bootstrap_field local_registration_form.username layout="horizontal" %} + {% bootstrap_field local_registration_form.email layout="horizontal" %} + {% bootstrap_field local_registration_form.password layout="horizontal" %} + {% bootstrap_field local_registration_form.password_repeat layout="horizontal" %} + +
+
+ +
+
+
+
+
+
+
+ {% if global_registration_form %} +
+ +
+
+
+ {% csrf_token %} + {% bootstrap_form_errors global_registration_form type='all' layout='inline' %} + {% bootstrap_field global_registration_form.email layout="horizontal" %} + {% bootstrap_field global_registration_form.password layout="horizontal" %} + {% bootstrap_field global_registration_form.password_repeat layout="horizontal" %} + +
+
+ +
+
+
+
+
+
+ {% 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')