Login is mandatory for adding things to a card

This commit is contained in:
Raphael Michel
2015-02-17 23:27:43 +01:00
parent c827579a8e
commit 54b494890e
8 changed files with 418 additions and 40 deletions

View File

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

View File

@@ -0,0 +1,102 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Login" %}{% endblock %}
{% block content %}
<h2>{% trans "Login" %}</h2>
<p>{% trans "You need to login or register to continue" %}</p>
<div class="panel-group" id="login_accordion">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a data-toggle="collapse" href="#loginForm" data-parent="#login_accordion">
{% trans "I already have an account" %}
</a>
</h4>
</div>
<div id="loginForm" class="panel-collapse collapse {% if request.POST.form == 'login' %}in{% endif %}">
<div class="panel-body">
<form class="form-horizontal" method="post">
{% 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" %}
<input type="hidden" name="form" value="login" />
<div class="form-group">
<div class="submit-group col-md-offset-2 col-md-4 text-right">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Login" %}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a data-toggle="collapse" href="#localRegisterForm" data-parent="#login_accordion">
{% if global_registration_form %}
{% trans "I want to create a new account just for this event" %}
{% else %}
{% trans "I want to create a new account" %}
{% endif %}
</a>
</h4>
</div>
<div id="localRegisterForm" class="panel-collapse collapse {% if request.POST.form == 'local_registration' %}in{% endif %}">
<div class="panel-body">
<div class="panel-body">
<form class="form-horizontal" method="post">
{% 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" %}
<input type="hidden" name="form" value="local_registration" />
<div class="form-group">
<div class="submit-group col-md-offset-2 col-md-4 text-right">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Register" %}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% if global_registration_form %}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a data-toggle="collapse" href="#globalRegistrationForm" data-parent="#login_accordion">
{% trans "I want to create a permanent account" %}
</a>
</h4>
</div>
<div id="globalRegistrationForm" class="panel-collapse collapse {% if request.POST.form == 'global_registration' %}in{% endif %}">
<div class="panel-body">
<form class="form-horizontal" method="post">
{% 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" %}
<input type="hidden" name="form" value="global_registration" />
<div class="form-group">
<div class="submit-group col-md-offset-2 col-md-4 text-right">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Register" %}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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