import json from django.contrib import messages from django.contrib.auth import authenticate, logout from django.core import signing from django.core.signing import SignatureExpired, BadSignature from django.core.urlresolvers import reverse from django.core.validators import RegexValidator from django.db.models import Count from django import forms from django.forms import Form 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, View from django.utils.translation import ugettext_lazy as _ from django.conf import settings from pretix.base.mail import mail from pretix.base.models import User from pretix.presale.views import EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin from pretix.presale.views.cart import CartAdd class EventIndex(EventViewMixin, CartDisplayMixin, TemplateView): template_name = "pretixpresale/event/index.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Fetch all items items = self.request.event.items.all().select_related( 'category', # for re-grouping ).prefetch_related( 'properties', # for .get_all_available_variations() 'quotas', 'variations__quotas', 'quotas__event' # for .availability() ).annotate(quotac=Count('quotas')).filter( quotac__gt=0 ).order_by('category__position', 'category_id', 'position', 'name') for item in items: item.available_variations = sorted(item.get_all_available_variations(), key=lambda vd: vd.ordered_values()) item.has_variations = (len(item.available_variations) != 1 or not item.available_variations[0].empty()) if not item.has_variations: item.cached_availability = list(item.check_quotas()) item.cached_availability[1] = min(item.cached_availability[1], int(self.request.event.settings.max_items_per_order)) item.price = item.available_variations[0]['price'] else: for var in item.available_variations: var.cached_availability = list(var['variation'].check_quotas()) var.cached_availability[1] = min(var.cached_availability[1], int(self.request.event.settings.max_items_per_order)) var.price = var.get('price', item.default_price) if len(item.available_variations) > 0: item.min_price = min([v.price for v in item.available_variations]) items = [item for item in items if len(item.available_variations) > 0] # Regroup those by category context['items_by_category'] = sorted([ # a group is a tuple of a category and a list of items (cat, [i for i in items if i.category == cat]) for cat in set([i.category for i in items]) # insert categories into a set for uniqueness # a set is unsorted, so sort again by category ], key=lambda group: (group[0].position, group[0].identity) if group[0] is not None else (0, "")) 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.') if settings.PRETIX_GLOBAL_REGISTRATION else None ) ) 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 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 'cart_tmp' in self.request.session and self.request.user.is_authenticated(): items = json.loads(self.request.session['cart_tmp']) del self.request.session['cart_tmp'] ca = CartAdd() ca.request = self.request ca.items = items return ca.process() if 'next' in self.request.GET: return redirect(self.request.GET.get('next')) else: return redirect('presale:event.orders', 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, locale=request.LANGUAGE_CODE, timezone=request.timezone if hasattr(request, 'timezone') 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' and settings.PRETIX_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'], locale=request.LANGUAGE_CODE, timezone=request.timezone if hasattr(request, 'timezone') else None ) 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 class PasswordRecoverForm(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(Form): username = forms.CharField( label=_('Username or E-mail'), ) def __init__(self, event, *args, **kwargs): self.event = event super().__init__(*args, **kwargs) def clean_username(self): username = self.cleaned_data['username'] try: self.cleaned_data['user'] = User.objects.get( identifier=username, event__isnull=True ) return username except User.DoesNotExist: pass try: self.cleaned_data['user'] = User.objects.get( username=username, event=self.event ) return username except User.DoesNotExist: pass try: self.cleaned_data['user'] = User.objects.get( email=username, event=self.event ) return username except: raise forms.ValidationError( _("We are unable to find a user matching the data you provided."), code='unknown_user', ) class EventForgot(EventViewMixin, TemplateView): template_name = 'pretixpresale/event/forgot.html' def get(self, request, *args, **kwargs): if request.user.is_authenticated() and \ (request.user.event is None or request.user.event == request.event): return redirect('presale:event.orders', organizer=self.request.event.organizer.slug, event=self.request.event.slug) return super().get(request, *args, **kwargs) def generate_token(self, user): return signing.dumps({ "type": "reset", "user": user.id }) def post(self, request, *args, **kwargs): if self.form.is_valid(): user = self.form.cleaned_data['user'] if user.email: mail( user, _('Password recovery'), 'pretixpresale/email/forgot.txt', { 'user': user, 'event': self.request.event, 'url': settings.SITE_URL + reverse('presale:event.forgot.recover', kwargs={ 'event': self.request.event.slug, 'organizer': self.request.event.organizer.slug, }) + '?token=' + self.generate_token(user), }, self.request.event ) messages.success(request, _('We sent you an e-mail containing further instructions.')) else: messages.success(request, _('We are unable to send you a new password, as you did not enter an e-mail ' 'address at your registration.')) return redirect('presale:event.forgot', organizer=self.request.event.organizer.slug, event=self.request.event.slug) else: return self.get(request, *args, **kwargs) @cached_property def form(self): return PasswordForgotForm( event=self.request.event, data=self.request.POST if self.request.method == 'POST' else None ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['form'] = self.form return context class EventRecover(EventViewMixin, TemplateView): template_name = 'pretixpresale/event/recover.html' error_messages = { 'invalid': _('You clicked on an invalid link. Please check that you copied the full ' 'web address into your address bar.'), 'expired': _('This password recovery link has expired. Please request a new e-mail and ' 'use the recovery link within 24 hours.'), 'unknownuser': _('We were unable to find the user you requested a new password for.') } def get(self, request, *args, **kwargs): if request.user.is_authenticated() and \ (request.user.event is None or request.user.event == request.event): return redirect('presale:event.orders', organizer=self.request.event.organizer.slug, event=self.request.event.slug) try: self.get_user() except User.DoesNotExist: return self.invalid('unknownuser') except SignatureExpired: return self.invalid('expired') except BadSignature: return self.invalid('invalid') return super().get(request, *args, **kwargs) def get_user(self): token = signing.loads(self.request.GET.get('token', ''), max_age=3600 * 24) if token['type'] != 'reset': raise BadSignature() return User.objects.get(id=token['user']) def invalid(self, msg): messages.error(self.request, self.error_messages[msg]) return redirect('presale:event.forgot', organizer=self.request.event.organizer.slug, event=self.request.event.slug) def post(self, request, *args, **kwargs): if self.form.is_valid(): try: user = self.get_user() except User.DoesNotExist: return self.invalid('unknownuser') except SignatureExpired: return self.invalid('expired') except BadSignature: return self.invalid('invalid') else: user.set_password(self.form.cleaned_data['password']) messages.success(request, _('You can now login using your new password.')) return redirect('presale:event.checkout.login', organizer=self.request.event.organizer.slug, event=self.request.event.slug) else: return self.get(request, *args, **kwargs) @cached_property def form(self): return PasswordRecoverForm( data=self.request.POST if self.request.method == 'POST' else None ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['form'] = self.form return context class EventLogout(EventViewMixin, View): def get(self, request, *args, **kwargs): logout(request) return redirect('presale:event.index', organizer=self.request.event.organizer.slug, event=self.request.event.slug) class EventOrders(EventLoginRequiredMixin, EventViewMixin, TemplateView): template_name = 'pretixpresale/event/orders.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['orders'] = self.request.user.orders.current.all() return context