forked from CGM_Public/pretix_original
Customer accounts & Memberships (#2024)
This commit is contained in:
@@ -40,6 +40,7 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db.models import F, Q
|
||||
from django.http import HttpResponseNotAllowed, JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils import translation
|
||||
@@ -50,26 +51,29 @@ from django.utils.translation import (
|
||||
from django.views.generic.base import TemplateResponseMixin
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import Order
|
||||
from pretix.base.models import Customer, Order
|
||||
from pretix.base.models.orders import InvoiceAddress, OrderPayment
|
||||
from pretix.base.models.tax import TaxedPrice, TaxRule
|
||||
from pretix.base.services.cart import (
|
||||
CartError, error_messages, get_fees, set_cart_addons, update_tax_rates,
|
||||
)
|
||||
from pretix.base.services.memberships import validate_memberships_in_order
|
||||
from pretix.base.services.orders import perform_order
|
||||
from pretix.base.signals import validate_cart_addons
|
||||
from pretix.base.templatetags.rich_text import rich_text_snippet
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.forms.checkout import (
|
||||
ContactForm, InvoiceAddressForm, InvoiceNameForm,
|
||||
ContactForm, InvoiceAddressForm, InvoiceNameForm, MembershipForm,
|
||||
)
|
||||
from pretix.presale.forms.customer import AuthenticationForm, RegistrationForm
|
||||
from pretix.presale.signals import (
|
||||
checkout_all_optional, checkout_confirm_messages, checkout_flow_steps,
|
||||
contact_form_fields, contact_form_fields_overrides,
|
||||
order_meta_from_request, question_form_fields,
|
||||
question_form_fields_overrides,
|
||||
)
|
||||
from pretix.presale.utils import customer_login
|
||||
from pretix.presale.views import (
|
||||
CartMixin, get_cart, get_cart_is_free, get_cart_total,
|
||||
)
|
||||
@@ -222,6 +226,200 @@ class TemplateFlowStep(TemplateResponseMixin, BaseCheckoutFlowStep):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CustomerStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
priority = 45
|
||||
identifier = "customer"
|
||||
template_name = "pretixpresale/event/checkout_customer.html"
|
||||
label = pgettext_lazy('checkoutflow', 'Customer account')
|
||||
icon = 'user'
|
||||
|
||||
def is_applicable(self, request):
|
||||
return request.organizer.settings.customer_accounts
|
||||
|
||||
@cached_property
|
||||
def login_form(self):
|
||||
f = AuthenticationForm(
|
||||
data=(
|
||||
self.request.POST
|
||||
if self.request.method == "POST" and self.request.POST.get('customer_mode') == 'login'
|
||||
else None
|
||||
),
|
||||
prefix='login',
|
||||
request=self.request.event,
|
||||
)
|
||||
for field in f.fields.values():
|
||||
field._show_required = field.required
|
||||
field.required = False
|
||||
field.widget.is_required = False
|
||||
return f
|
||||
|
||||
@cached_property
|
||||
def guest_allowed(self):
|
||||
return not any(
|
||||
p.item.require_membership or
|
||||
(p.variation and p.variation.require_membership) or
|
||||
p.item.grant_membership_type_id
|
||||
for p in self.positions
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def register_form(self):
|
||||
f = RegistrationForm(
|
||||
data=(
|
||||
self.request.POST
|
||||
if self.request.method == "POST" and self.request.POST.get('customer_mode') == 'register'
|
||||
else None
|
||||
),
|
||||
prefix='register',
|
||||
request=self.request,
|
||||
)
|
||||
for field in f.fields.values():
|
||||
field._show_required = field.required
|
||||
field.required = False
|
||||
field.widget.is_required = False
|
||||
return f
|
||||
|
||||
def post(self, request):
|
||||
self.request = request
|
||||
|
||||
if request.POST.get("customer_mode") == 'login':
|
||||
if 'customer' in self.cart_session:
|
||||
return redirect(self.get_next_url(request))
|
||||
elif request.customer:
|
||||
self.cart_session['customer_mode'] = 'login'
|
||||
self.cart_session['customer'] = request.customer.pk
|
||||
return redirect(self.get_next_url(request))
|
||||
elif self.login_form.is_valid():
|
||||
customer_login(self.request, self.login_form.get_customer())
|
||||
self.cart_session['customer_mode'] = 'login'
|
||||
self.cart_session['customer'] = self.login_form.get_customer().pk
|
||||
return redirect(self.get_next_url(request))
|
||||
else:
|
||||
return self.render()
|
||||
elif request.POST.get("customer_mode") == 'register':
|
||||
if self.register_form.is_valid():
|
||||
customer = self.register_form.create()
|
||||
self.cart_session['customer_mode'] = 'login'
|
||||
self.cart_session['customer'] = customer.pk
|
||||
return redirect(self.get_next_url(request))
|
||||
else:
|
||||
return self.render()
|
||||
elif request.POST.get("customer_mode") == 'guest' and self.guest_allowed:
|
||||
self.cart_session['customer'] = None
|
||||
self.cart_session['customer_mode'] = 'guest'
|
||||
return redirect(self.get_next_url(request))
|
||||
else:
|
||||
return self.render()
|
||||
|
||||
def is_completed(self, request, warn=False):
|
||||
self.request = request
|
||||
if self.guest_allowed:
|
||||
return 'customer_mode' in self.cart_session
|
||||
else:
|
||||
return self.cart_session.get('customer_mode') == 'login'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['cart'] = self.get_cart()
|
||||
ctx['cart_session'] = self.cart_session
|
||||
ctx['login_form'] = self.login_form
|
||||
ctx['register_form'] = self.register_form
|
||||
ctx['selected'] = self.request.POST.get(
|
||||
'customer_mode',
|
||||
self.cart_session.get('customer_mode', 'login' if self.request.customer else '')
|
||||
)
|
||||
ctx['guest_allowed'] = self.guest_allowed
|
||||
|
||||
if 'customer' in self.cart_session:
|
||||
try:
|
||||
ctx['customer'] = self.request.organizer.customers.get(pk=self.cart_session.get('customer', -1))
|
||||
except Customer.DoesNotExist:
|
||||
self.cart_session['customer'] = None
|
||||
self.cart_session['customer_mode'] = None
|
||||
elif self.request.customer:
|
||||
ctx['customer'] = self.request.customer
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class MembershipStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
priority = 47
|
||||
identifier = "membership"
|
||||
template_name = "pretixpresale/event/checkout_membership.html"
|
||||
label = pgettext_lazy('checkoutflow', 'Membership')
|
||||
icon = 'id-card'
|
||||
|
||||
def is_applicable(self, request):
|
||||
self.request = request
|
||||
return bool(self.applicable_positions)
|
||||
|
||||
@cached_property
|
||||
def applicable_positions(self):
|
||||
return [
|
||||
p for p in self.positions
|
||||
if p.item.require_membership or (p.variation and p.variation.require_membership)
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def forms(self):
|
||||
forms = []
|
||||
|
||||
memberships = list(self.cart_customer.memberships.with_usages().filter(
|
||||
Q(Q(membership_type__max_usages__isnull=True) | Q(usages__lt=F('membership_type__max_usages'))),
|
||||
).select_related('membership_type'))
|
||||
|
||||
for p in self.applicable_positions:
|
||||
form = MembershipForm(
|
||||
event=self.request.event,
|
||||
memberships=memberships,
|
||||
position=p,
|
||||
prefix=f"membership-{p.id}",
|
||||
initial={
|
||||
'membership': str(p.used_membership_id)
|
||||
},
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
)
|
||||
forms.append(form)
|
||||
|
||||
return forms
|
||||
|
||||
def post(self, request):
|
||||
self.request = request
|
||||
|
||||
for f in self.forms:
|
||||
if not f.is_valid():
|
||||
messages.error(request, _('Your cart includes a product that requires an active membership to be selected.'))
|
||||
return self.render()
|
||||
|
||||
f.position.used_membership = f.cleaned_data['membership']
|
||||
|
||||
try:
|
||||
validate_memberships_in_order(self.cart_customer, self.positions, self.request.event, lock=False)
|
||||
except ValidationError as e:
|
||||
messages.error(self.request, e.message)
|
||||
self.render()
|
||||
else:
|
||||
for f in self.forms:
|
||||
f.position.save(update_fields=['used_membership'])
|
||||
|
||||
return redirect(self.get_next_url(request))
|
||||
|
||||
def is_completed(self, request, warn=False):
|
||||
self.request = request
|
||||
ok = all([p.used_membership_id for p in self.applicable_positions])
|
||||
if not ok and warn:
|
||||
messages.error(request, _('Your cart includes a product that requires an active membership to be selected.'))
|
||||
return ok
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['cart'] = self.get_cart()
|
||||
ctx['cart_session'] = self.cart_session
|
||||
ctx['forms'] = self.forms
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
priority = 40
|
||||
identifier = "addons"
|
||||
@@ -486,12 +684,14 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
initial.update({
|
||||
k: v['initial'] for k, v in overrides.items() if 'initial' in v
|
||||
})
|
||||
if self.cart_customer:
|
||||
initial['email'] = self.cart_customer.email
|
||||
|
||||
f = ContactForm(data=self.request.POST if self.request.method == "POST" else None,
|
||||
event=self.request.event,
|
||||
request=self.request,
|
||||
initial=initial, all_optional=self.all_optional)
|
||||
if wd.get('email', '') and wd.get('fix', '') == "true":
|
||||
if wd.get('email', '') and wd.get('fix', '') == "true" or self.cart_customer:
|
||||
f.fields['email'].disabled = True
|
||||
|
||||
for overrides in override_sets:
|
||||
@@ -504,13 +704,31 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
return f
|
||||
|
||||
def get_question_override_sets(self, cart_position):
|
||||
return [
|
||||
o = []
|
||||
if self.cart_customer:
|
||||
o.append({
|
||||
'attendee_name_parts': {
|
||||
'initial': self.cart_customer.name_parts
|
||||
}
|
||||
})
|
||||
o += [
|
||||
resp for recv, resp in question_form_fields_overrides.send(
|
||||
self.request.event,
|
||||
position=cart_position,
|
||||
request=self.request
|
||||
)
|
||||
]
|
||||
if cart_position.used_membership:
|
||||
d = {
|
||||
'initial': cart_position.used_membership.attendee_name_parts
|
||||
}
|
||||
if not cart_position.used_membership.membership_type.transferable:
|
||||
d['disabled'] = True
|
||||
o.append({
|
||||
'attendee_name_parts': d
|
||||
})
|
||||
|
||||
return o
|
||||
|
||||
@cached_property
|
||||
def eu_reverse_charge_relevant(self):
|
||||
@@ -538,6 +756,11 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
wd_initial = {}
|
||||
initial = dict(wd_initial)
|
||||
|
||||
if self.cart_customer:
|
||||
initial.update({
|
||||
'name_parts': self.cart_customer.name_parts
|
||||
})
|
||||
|
||||
override_sets = self._contact_override_sets
|
||||
for overrides in override_sets:
|
||||
initial.update({
|
||||
@@ -852,6 +1075,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
ctx['confirm_messages'] = self.confirm_messages
|
||||
ctx['cart_session'] = self.cart_session
|
||||
ctx['invoice_address_asked'] = self.address_asked
|
||||
ctx['customer'] = self.cart_customer
|
||||
|
||||
self.cart_session['shown_total'] = str(ctx['cart']['total'])
|
||||
|
||||
@@ -928,11 +1152,19 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
for receiver, response in order_meta_from_request.send(sender=request.event, request=request):
|
||||
meta_info.update(response)
|
||||
|
||||
return self.do(self.request.event.id, self.payment_provider.identifier if self.payment_provider else None,
|
||||
[p.id for p in self.positions], self.cart_session.get('email'),
|
||||
translation.get_language(), self.invoice_address.pk, meta_info,
|
||||
request.sales_channel.identifier, self.cart_session.get('gift_cards'),
|
||||
self.cart_session.get('shown_total'))
|
||||
return self.do(
|
||||
self.request.event.id,
|
||||
payment_provider=self.payment_provider.identifier if self.payment_provider else None,
|
||||
positions=[p.id for p in self.positions],
|
||||
email=self.cart_session.get('email'),
|
||||
locale=translation.get_language(),
|
||||
address=self.invoice_address.pk,
|
||||
meta_info=meta_info,
|
||||
sales_channel=request.sales_channel.identifier,
|
||||
gift_cards=self.cart_session.get('gift_cards'),
|
||||
shown_total=self.cart_session.get('shown_total'),
|
||||
customer=self.cart_session.get('customer'),
|
||||
)
|
||||
|
||||
def get_success_message(self, value):
|
||||
create_empty_cart_id(self.request)
|
||||
@@ -966,6 +1198,8 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
|
||||
DEFAULT_FLOW = (
|
||||
AddOnsStep,
|
||||
CustomerStep,
|
||||
MembershipStep,
|
||||
QuestionsStep,
|
||||
PaymentStep,
|
||||
ConfirmStep
|
||||
|
||||
@@ -37,6 +37,9 @@ from itertools import chain
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
@@ -178,3 +181,60 @@ class AddOnVariationField(forms.ChoiceField):
|
||||
if value == k or text_value == force_str(k):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MembershipForm(forms.Form):
|
||||
required_css_class = 'required'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.memberships = kwargs.pop('memberships')
|
||||
event = kwargs.pop('event')
|
||||
self.position = kwargs.pop('position')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
ev = self.position.subevent or event
|
||||
if self.position.variation and self.position.variation.require_membership:
|
||||
types = self.position.variation.require_membership_types.all()
|
||||
else:
|
||||
types = self.position.item.require_membership_types.all()
|
||||
|
||||
initial = None
|
||||
|
||||
memberships = [
|
||||
m for m in self.memberships
|
||||
if m.is_valid(ev) and m.membership_type in types
|
||||
]
|
||||
|
||||
if len(memberships) == 1:
|
||||
initial = str(memberships[0].pk)
|
||||
|
||||
self.fields['membership'] = forms.ChoiceField(
|
||||
label=_('Membership'),
|
||||
choices=[
|
||||
(str(m.pk), self._label_from_instance(m))
|
||||
for m in memberships
|
||||
],
|
||||
initial=initial,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
self.is_empty = not memberships
|
||||
|
||||
def _label_from_instance(self, obj):
|
||||
ds = date_format(obj.date_start, 'SHORT_DATE_FORMAT')
|
||||
de = date_format(obj.date_end, 'SHORT_DATE_FORMAT')
|
||||
if obj.membership_type.max_usages is not None:
|
||||
usages = f'({obj.usages} / {obj.membership_type.max_usages})'
|
||||
else:
|
||||
usages = ''
|
||||
return mark_safe(
|
||||
f'<strong>{escape(obj.membership_type)}</strong> {usages}<br>'
|
||||
f'{escape(obj.attendee_name)}<br>'
|
||||
f'<span class="text-muted">{ds} – {de}</span>'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d.get('membership'):
|
||||
d['membership'] = [m for m in self.memberships if str(m.pk) == d['membership']][0]
|
||||
return d
|
||||
|
||||
456
src/pretix/presale/forms/customer.py
Normal file
456
src/pretix/presale/forms/customer.py
Normal file
@@ -0,0 +1,456 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import hashlib
|
||||
import ipaddress
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.forms.questions import NamePartsFormField
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import Customer
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.helpers.http import get_client_ip
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
|
||||
class TokenGenerator(PasswordResetTokenGenerator):
|
||||
key_salt = "pretix.presale.forms.customer.TokenGenerator"
|
||||
|
||||
|
||||
class AuthenticationForm(forms.Form):
|
||||
required_css_class = 'required'
|
||||
email = forms.EmailField(
|
||||
label=_("E-mail"),
|
||||
widget=forms.EmailInput(attrs={'autofocus': True})
|
||||
)
|
||||
password = forms.CharField(
|
||||
label=_("Password"),
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
|
||||
)
|
||||
|
||||
error_messages = {
|
||||
'incomplete': _('You need to fill out all fields.'),
|
||||
'invalid_login': _(
|
||||
"We have not found an account with this email address and password."
|
||||
),
|
||||
'inactive': _("This account is disabled."),
|
||||
'unverified': _("You have not yet activated your account and set a password. Please click the link in the "
|
||||
"email we sent you. Click \"Reset password\" to receive a new email in case you cannot find "
|
||||
"it again."),
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
self.request = request
|
||||
self.customer_cache = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
if email is not None and password:
|
||||
try:
|
||||
u = self.request.organizer.customers.get(email=email)
|
||||
except Customer.DoesNotExist:
|
||||
# Run the default password hasher once to reduce the timing
|
||||
# difference between an existing and a nonexistent user (django #20760).
|
||||
Customer().set_password(password)
|
||||
else:
|
||||
if u.check_password(password):
|
||||
self.customer_cache = u
|
||||
if self.customer_cache is None:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login',
|
||||
)
|
||||
else:
|
||||
self.confirm_login_allowed(self.customer_cache)
|
||||
else:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['incomplete'],
|
||||
code='incomplete'
|
||||
)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def confirm_login_allowed(self, user):
|
||||
if not user.is_active:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['inactive'],
|
||||
code='inactive',
|
||||
)
|
||||
if not user.is_verified:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['unverified'],
|
||||
code='unverified',
|
||||
)
|
||||
|
||||
def get_customer(self):
|
||||
return self.customer_cache
|
||||
|
||||
|
||||
class RegistrationForm(forms.Form):
|
||||
required_css_class = 'required'
|
||||
name_parts = forms.CharField()
|
||||
email = forms.EmailField(
|
||||
label=_("E-mail"),
|
||||
)
|
||||
|
||||
error_messages = {
|
||||
'rate_limit': _("We've received a lot of registration requests from you, please wait 10 minutes before you try again."),
|
||||
'duplicate': _(
|
||||
"An account with this email address is already registered. Please try to log in or reset your password "
|
||||
"instead."
|
||||
),
|
||||
'required': _('This field is required.'),
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
self.request = request
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=True,
|
||||
scheme=request.organizer.settings.name_scheme,
|
||||
titles=request.organizer.settings.name_scheme_titles,
|
||||
label=_('Name'),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def ratelimit_key(self):
|
||||
if not settings.HAS_REDIS:
|
||||
return None
|
||||
client_ip = get_client_ip(self.request)
|
||||
if not client_ip:
|
||||
return None
|
||||
try:
|
||||
client_ip = ipaddress.ip_address(client_ip)
|
||||
except ValueError:
|
||||
# Web server not set up correctly
|
||||
return None
|
||||
if client_ip.is_private:
|
||||
# This is the private IP of the server, web server not set up correctly
|
||||
return None
|
||||
return 'pretix_customer_registration_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
|
||||
if email is not None:
|
||||
try:
|
||||
self.request.organizer.customers.get(email=email)
|
||||
except Customer.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise forms.ValidationError(
|
||||
{'email': self.error_messages['duplicate']},
|
||||
code='duplicate',
|
||||
)
|
||||
|
||||
if not self.cleaned_data.get('email'):
|
||||
raise forms.ValidationError(
|
||||
{'email': self.error_messages['required']},
|
||||
code='incomplete'
|
||||
)
|
||||
else:
|
||||
if self.ratelimit_key:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr(self.ratelimit_key)
|
||||
rc.expire(self.ratelimit_key, 600)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
return self.cleaned_data
|
||||
|
||||
def create(self):
|
||||
customer = self.request.organizer.customers.create(
|
||||
email=self.cleaned_data['email'],
|
||||
name_parts=self.cleaned_data['name_parts'],
|
||||
is_active=True,
|
||||
is_verified=False,
|
||||
locale=get_language_without_region(),
|
||||
)
|
||||
customer.set_unusable_password()
|
||||
customer.save()
|
||||
customer.log_action('pretix.customer.created', {})
|
||||
ctx = customer.get_email_context()
|
||||
token = TokenGenerator().make_token(customer)
|
||||
ctx['url'] = build_absolute_uri(self.request.organizer,
|
||||
'presale:organizer.customer.activate') + '?id=' + customer.identifier + '&token=' + token
|
||||
mail(
|
||||
customer.email,
|
||||
_('Activate your account at {organizer}').format(organizer=self.request.organizer.name),
|
||||
self.request.organizer.settings.mail_text_customer_registration,
|
||||
ctx,
|
||||
locale=customer.locale,
|
||||
customer=customer,
|
||||
organizer=self.request.organizer,
|
||||
)
|
||||
return customer
|
||||
|
||||
|
||||
class SetPasswordForm(forms.Form):
|
||||
required_css_class = 'required'
|
||||
error_messages = {
|
||||
'pw_mismatch': _("Please enter the same password twice"),
|
||||
}
|
||||
email = forms.EmailField(
|
||||
label=_('E-mail'),
|
||||
disabled=True
|
||||
)
|
||||
password = forms.CharField(
|
||||
label=_('Password'),
|
||||
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}),
|
||||
required=True
|
||||
)
|
||||
password_repeat = forms.CharField(
|
||||
label=_('Repeat password'),
|
||||
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}),
|
||||
)
|
||||
|
||||
def __init__(self, customer=None, *args, **kwargs):
|
||||
self.customer = customer
|
||||
kwargs.setdefault('initial', {})
|
||||
kwargs['initial']['email'] = self.customer.email
|
||||
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({
|
||||
'password_repeat': self.error_messages['pw_mismatch'],
|
||||
}, code='pw_mismatch')
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def clean_password(self):
|
||||
password1 = self.cleaned_data.get('password', '')
|
||||
if validate_password(password1, user=self.customer) is not None:
|
||||
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
|
||||
return password1
|
||||
|
||||
|
||||
class ResetPasswordForm(forms.Form):
|
||||
required_css_class = 'required'
|
||||
error_messages = {
|
||||
'rate_limit': _("For security reasons, please wait 10 minutes before you try again."),
|
||||
'unknown': _("A user with this email address is not known in our system."),
|
||||
}
|
||||
email = forms.EmailField(
|
||||
label=_('E-mail'),
|
||||
)
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
self.request = request
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_email(self):
|
||||
if 'email' not in self.cleaned_data:
|
||||
return
|
||||
try:
|
||||
self.customer = self.request.organizer.customers.get(email=self.cleaned_data['email'])
|
||||
return self.customer.email
|
||||
except Customer.DoesNotExist:
|
||||
# Yup, this is an information leak. But it prevents dozens of support requests – and even if we didn't
|
||||
# have it, there'd be an info leak in the registration flow (trying to sign up for an account, which fails
|
||||
# if the email address already exists).
|
||||
raise forms.ValidationError(self.error_messages['unknown'], code='unknown')
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d.get('email') and settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwreset_customer_%s' % self.customer.pk)
|
||||
rc.expire('pretix_pwreset_customer_%s' % self.customer.pk, 600)
|
||||
if cnt > 2:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
return d
|
||||
|
||||
|
||||
class ChangePasswordForm(forms.Form):
|
||||
required_css_class = 'required'
|
||||
error_messages = {
|
||||
'pw_current_wrong': _("The current password you entered was not correct."),
|
||||
'pw_mismatch': _("Please enter the same password twice"),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
}
|
||||
email = forms.EmailField(
|
||||
label=_('E-mail'),
|
||||
disabled=True
|
||||
)
|
||||
password_current = forms.CharField(
|
||||
label=_('Your current password'),
|
||||
widget=forms.PasswordInput,
|
||||
required=True
|
||||
)
|
||||
password = forms.CharField(
|
||||
label=_('New password'),
|
||||
widget=forms.PasswordInput,
|
||||
required=True
|
||||
)
|
||||
password_repeat = forms.CharField(
|
||||
label=_('Repeat password'),
|
||||
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}),
|
||||
)
|
||||
|
||||
def __init__(self, customer, *args, **kwargs):
|
||||
self.customer = customer
|
||||
kwargs.setdefault('initial', {})
|
||||
kwargs['initial']['email'] = self.customer.email
|
||||
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({
|
||||
'password_repeat': self.error_messages['pw_mismatch'],
|
||||
}, code='pw_mismatch')
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def clean_password(self):
|
||||
password1 = self.cleaned_data.get('password', '')
|
||||
if validate_password(password1, user=self.customer) is not None:
|
||||
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
|
||||
return password1
|
||||
|
||||
def clean_password_current(self):
|
||||
old_pw = self.cleaned_data.get('password_current')
|
||||
|
||||
if old_pw and settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwchange_customer_%s' % self.customer.pk)
|
||||
rc.expire('pretix_pwchange_customer_%s' % self.customer.pk, 300)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if old_pw and not check_password(old_pw, self.customer.password):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['pw_current_wrong'],
|
||||
code='pw_current_wrong',
|
||||
)
|
||||
|
||||
|
||||
class ChangeInfoForm(forms.ModelForm):
|
||||
required_css_class = 'required'
|
||||
error_messages = {
|
||||
'pw_current_wrong': _("The current password you entered was not correct."),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
'duplicate': _("An account with this email address is already registered."),
|
||||
}
|
||||
password_current = forms.CharField(
|
||||
label=_('Your current password'),
|
||||
widget=forms.PasswordInput,
|
||||
help_text=_('Only required if you change your email address'),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = ('name_parts', 'email')
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
self.request = request
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=True,
|
||||
scheme=request.organizer.settings.name_scheme,
|
||||
titles=request.organizer.settings.name_scheme_titles,
|
||||
label=_('Name'),
|
||||
)
|
||||
|
||||
def clean_password_current(self):
|
||||
old_pw = self.cleaned_data.get('password_current')
|
||||
|
||||
if old_pw:
|
||||
if settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwchange_customer_%s' % self.instance.pk)
|
||||
rc.expire('pretix_pwchange_customer_%s' % self.instance.pk, 300)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if not check_password(old_pw, self.instance.password):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['pw_current_wrong'],
|
||||
code='pw_current_wrong',
|
||||
)
|
||||
|
||||
return "***valid***"
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
password_current = self.cleaned_data.get('password_current')
|
||||
|
||||
if email != self.instance.email and not password_current:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['pw_current_wrong'],
|
||||
code='pw_current_wrong',
|
||||
)
|
||||
|
||||
if email is not None:
|
||||
try:
|
||||
self.request.organizer.customers.exclude(pk=self.instance.pk).get(email=email)
|
||||
except Customer.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['duplicate'],
|
||||
code='duplicate',
|
||||
)
|
||||
|
||||
return self.cleaned_data
|
||||
@@ -120,7 +120,10 @@ class CheckoutFieldRenderer(FieldRenderer):
|
||||
def add_label(self, html):
|
||||
label = self.get_label()
|
||||
|
||||
if hasattr(self.field.field, '_required'):
|
||||
if hasattr(self.field.field, '_show_required'):
|
||||
# e.g. payment settings forms where a field is only required if the payment provider is active
|
||||
required = self.field.field._show_required
|
||||
elif hasattr(self.field.field, '_required'):
|
||||
# e.g. payment settings forms where a field is only required if the payment provider is active
|
||||
required = self.field.field._required
|
||||
else:
|
||||
|
||||
@@ -34,21 +34,24 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="container page-header-links {% if event.settings.theme_color_background|upper != "#FFFFFF" or event.settings.logo_image_large %}page-header-links-outside{% endif %}">
|
||||
{% if event.settings.locales|length > 1 %}
|
||||
{% if event.settings.locales|length > 1 or request.organizer.settings.customer_accounts %}
|
||||
{% if event.settings.theme_color_background|upper != "#FFFFFF" or event.settings.logo_image_large %}
|
||||
<div class="pull-right header-part flip hidden-print">
|
||||
<nav class="locales" aria-label="{% trans "select language" %}">
|
||||
{% for l in languages %}
|
||||
<a href="{% url "presale:locale.set" %}?locale={{ l.code }}&next={{ request.path }}{% if request.META.QUERY_STRING %}%3F{{ request.META.QUERY_STRING|urlencode }}{% endif %}" class="{% if l.code == request.LANGUAGE_CODE %}active{% endif %}" rel="nofollow" lang="{{ l.code }}" hreflang="{{ l.code }}">
|
||||
{{ l.name_local }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% if event.settings.locales|length > 1 %}
|
||||
<nav class="locales" aria-label="{% trans "select language" %}">
|
||||
{% for l in languages %}
|
||||
<a href="{% url "presale:locale.set" %}?locale={{ l.code }}&next={{ request.path }}{% if request.META.QUERY_STRING %}%3F{{ request.META.QUERY_STRING|urlencode }}{% endif %}" class="{% if l.code == request.LANGUAGE_CODE %}active{% endif %}" rel="nofollow" lang="{{ l.code }}" hreflang="{{ l.code }}">
|
||||
{{ l.name_local }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% include "pretixpresale/fragment_login_status.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if request.event.settings.organizer_link_back %}
|
||||
<div class="pull-left header-part flip hidden-print">
|
||||
<a href="{% eventurl request.organizer "presale:organizer.index" %}">
|
||||
<a href="{% abseventurl request.organizer "presale:organizer.index" %}">
|
||||
« {% blocktrans trimmed with name=request.organizer.name %}
|
||||
Show all events of {{ name }}
|
||||
{% endblocktrans %}
|
||||
@@ -80,15 +83,18 @@
|
||||
</h1>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if event.settings.locales|length > 1 %}
|
||||
{% if event.settings.locales|length > 1 or request.organizer.settings.customer_accounts %}
|
||||
{% if event.settings.theme_color_background|upper == "#FFFFFF" and not event.settings.logo_image_large %}
|
||||
<div class="{% if not event_logo or not event.settings.logo_image_large %}pull-right flip{% endif %} loginbox hidden-print">
|
||||
<nav class="locales" aria-label="{% trans "select language" %}">
|
||||
{% for l in languages %}
|
||||
<a href="{% url "presale:locale.set" %}?locale={{ l.code }}&next={{ request.path }}{% if request.META.QUERY_STRING %}%3F{{ request.META.QUERY_STRING|urlencode }}{% endif %}" class="{% if l.code == request.LANGUAGE_CODE %}active{% endif %}" rel="nofollow">
|
||||
{{ l.name_local }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% if event.settings.locales|length > 1 %}
|
||||
<nav class="locales" aria-label="{% trans "select language" %}">
|
||||
{% for l in languages %}
|
||||
<a href="{% url "presale:locale.set" %}?locale={{ l.code }}&next={{ request.path }}{% if request.META.QUERY_STRING %}%3F{{ request.META.QUERY_STRING|urlencode }}{% endif %}" class="{% if l.code == request.LANGUAGE_CODE %}active{% endif %}" rel="nofollow">
|
||||
{{ l.name_local }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% include "pretixpresale/fragment_login_status.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -139,6 +139,12 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if customer %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Customer account" %}</dt>
|
||||
<dd>{{ customer.email }}<br>{{ customer.name }}<br>#{{ customer.identifier }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% if not asked and event.settings.invoice_name_required %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
{% extends "pretixpresale/event/checkout_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% load eventurl %}
|
||||
{% load rich_text %}
|
||||
{% block inner %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel-group" id="customer">
|
||||
<div class="panel panel-default">
|
||||
<label class="accordion-radio">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<input type="radio" name="customer_mode" value="login"
|
||||
data-parent="#customer"
|
||||
{% if selected == "login" %}checked="checked"{% endif %}
|
||||
data-toggle="radiocollapse" data-target="#customer_login"/>
|
||||
<strong>
|
||||
{% trans "Log in with a customer account" %}
|
||||
</strong>
|
||||
</h4>
|
||||
</div>
|
||||
</label>
|
||||
<div id="customer_login"
|
||||
class="panel-collapse collapsed {% if selected == "login" %}in{% endif %}">
|
||||
<div class="panel-body form-horizontal">
|
||||
{% if customer %}
|
||||
<p>
|
||||
{% blocktrans trimmed with org=request.organizer.name %}
|
||||
You are currently logged in with the following credentials.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Email" %}</dt>
|
||||
<dd>
|
||||
{{ customer.email }}
|
||||
</dd>
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ customer.name }}</dd>
|
||||
<dt>{% trans "Customer ID" %}</dt>
|
||||
<dd>
|
||||
#{{ customer.identifier }}
|
||||
</dd>
|
||||
</dl>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed with org=request.organizer.name %}
|
||||
If you created a customer account at {{ org }} before, you can log in now and connect
|
||||
your order to your account. This will allow you to see all your orders in one place
|
||||
and access them at any time.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_form login_form layout="checkout" %}
|
||||
<div class="row">
|
||||
<div class="col-md-offset-3 col-md-9">
|
||||
<a
|
||||
href="{% abseventurl request.organizer "presale:organizer.customer.resetpw" %}"
|
||||
target="_blank">
|
||||
{% trans "Reset password" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<label class="accordion-radio">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<input type="radio" name="customer_mode" value="register"
|
||||
data-parent="#customer"
|
||||
{% if selected == "register" %}checked="checked"{% endif %}
|
||||
data-toggle="radiocollapse" data-target="#customer_register"/>
|
||||
<strong>
|
||||
{% trans "Create a new customer account" %}
|
||||
</strong>
|
||||
</h4>
|
||||
</div>
|
||||
</label>
|
||||
<div id="customer_register"
|
||||
class="panel-collapse collapsed {% if selected == "register" %}in{% endif %}">
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form register_form layout="checkout" %}
|
||||
<p>
|
||||
{% blocktrans trimmed with org=request.organizer.name %}
|
||||
We will send you an email with a link to activate your account and set a password, so
|
||||
you can use the account for future orders at {{ org }}. You can still go ahead with this
|
||||
purchase before you received the email.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if guest_allowed %}
|
||||
<div class="panel panel-default">
|
||||
<label class="accordion-radio">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<input type="radio" name="customer_mode" value="guest"
|
||||
data-parent="#customer"
|
||||
{% if selected == "guest" %}checked="checked"{% endif %}
|
||||
data-toggle="radiocollapse" data-target="#customer_guest"/>
|
||||
<strong>
|
||||
{% trans "Continue as a guest" %}
|
||||
</strong>
|
||||
</h4>
|
||||
</div>
|
||||
</label>
|
||||
<div id="customer_guest"
|
||||
class="panel-collapse collapsed {% if selected == "guest" %}in{% endif %}">
|
||||
<div class="panel-body form-horizontal">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You are not required to create an account. If you proceed as a guest, you will be able
|
||||
to access the details and status of your order any time through the secret link we will
|
||||
send you via email once the order is complete.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{{ prev_url }}">
|
||||
{% trans "Go back" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4 col-sm-6">
|
||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,97 @@
|
||||
{% extends "pretixpresale/event/checkout_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load rich_text %}
|
||||
{% block inner %}
|
||||
<p>{% trans "Some of the products in your cart can only be purchased if there is an active membership on your account." %}</p>
|
||||
<form class="form-horizontal" method="post">
|
||||
{% csrf_token %}
|
||||
{% for form in forms %}
|
||||
<details class="panel panel-default" open>
|
||||
<summary class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<strong>{{ form.position.item.name }}{% if form.position.variation %}
|
||||
– {{ form.position.variation }}
|
||||
{% endif %}</strong>
|
||||
<i class="fa fa-angle-down collapse-indicator" aria-hidden="true"></i>
|
||||
</h4>
|
||||
</summary>
|
||||
<div>
|
||||
<div class="panel-body questions-form">
|
||||
{% if form.position.seat %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Seat" %}
|
||||
</label>
|
||||
<div class="col-md-9 form-control-text">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 4.7624999 3.7041668" class="svg-icon">
|
||||
<path
|
||||
d="m 1.9592032,1.8522629e-4 c -0.21468,0 -0.38861,0.17394000371 -0.38861,0.38861000371 0,0.21466 0.17393,0.38861 0.38861,0.38861 0.21468,0 0.3886001,-0.17395 0.3886001,-0.38861 0,-0.21467 -0.1739201,-0.38861000371 -0.3886001,-0.38861000371 z m 0.1049,0.84543000371 c -0.20823,-0.0326 -0.44367,0.12499 -0.39998,0.40462997 l 0.20361,1.01854 c 0.0306,0.15316 0.15301,0.28732 0.3483,0.28732 h 0.8376701 v 0.92708 c 0,0.29313 0.41187,0.29447 0.41187,0.005 v -1.19115 c 0,-0.14168 -0.0995,-0.29507 -0.29094,-0.29507 l -0.65578,-10e-4 -0.1757,-0.87644 C 2.3042533,0.95300523 2.1890432,0.86500523 2.0641032,0.84547523 Z m -0.58549,0.44906997 c -0.0946,-0.0134 -0.20202,0.0625 -0.17829,0.19172 l 0.18759,0.91054 c 0.0763,0.33956 0.36802,0.55914 0.66042,0.55914 h 0.6015201 c 0.21356,0 0.21448,-0.32143 -0.003,-0.32143 H 2.1954632 c -0.19911,0 -0.36364,-0.11898 -0.41341,-0.34107 l -0.17777,-0.87126 c -0.0165,-0.0794 -0.0688,-0.11963 -0.12557,-0.12764 z"/>
|
||||
</svg>
|
||||
{{ form.position.seat }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.position.addons.all %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Selected add-ons" %}
|
||||
</label>
|
||||
<div class="col-md-9 form-control-text">
|
||||
<ul class="addon-list">
|
||||
{% for a in form.position.addons.all %}
|
||||
<li>{{ a.item.name }}{% if a.variation %} – {{ a.variation.value }}{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.position.subevent %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Date" context "subevent" %}
|
||||
</label>
|
||||
<div class="col-md-9 form-control-text">
|
||||
<ul class="addon-list">
|
||||
{{ form.position.subevent.name }} · {{ form.position.subevent.get_date_range_display }}
|
||||
{% if form.position.event.settings.show_times %}
|
||||
<span data-time="{{ form.position.subevent.date_from.isoformat }}" data-timezone="{{ request.event.timezone }}">
|
||||
<span class="fa fa-clock-o" aria-hidden="true"></span>
|
||||
{{ form.position.subevent.date_from|date:"TIME_FORMAT" }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.is_empty %}
|
||||
<div class="alert alert-danger">
|
||||
{% trans "Your account does not include an active membership that allows you to buy this product." %}
|
||||
{% trans "You will not be able to continue." %}
|
||||
</div>
|
||||
<div class="sr-only">
|
||||
{% bootstrap_form form layout="checkout" %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% bootstrap_form form layout="checkout" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{{ prev_url }}">
|
||||
{% trans "Go back" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4 col-sm-6">
|
||||
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -34,6 +34,9 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if line.used_membership %}
|
||||
<br /><span class="fa fa-id-card fa-fw" aria-hidden="true"></span> {{ line.used_membership }}
|
||||
{% endif %}
|
||||
|
||||
{% if line.issued_gift_cards %}
|
||||
<dl>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
|
||||
{% if request.organizer.settings.customer_accounts %}
|
||||
<nav class="loginstatus" aria-label="{% trans "customer account" %}">
|
||||
{% if request.customer %}
|
||||
<a href="{% abseventurl request.organizer "presale:organizer.customer.profile" %}"
|
||||
aria-label="{% trans "View customer account" %}" data-placement="bottom"
|
||||
title="{% trans "View user profile" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-user" aria-hidden="true"></span>
|
||||
{{ request.customer.name|default:request.customer.email }}</a>
|
||||
<a href="{% abseventurl request.organizer "presale:organizer.customer.logout" %}?next={{ request.path|urlencode }}%3F{{ request.META.QUERY_STRING|urlencode }}"
|
||||
aria-label="{% trans "Log out" %}" data-toggle="tooltip" data-placement="left"
|
||||
title="{% trans "Log out" %}">
|
||||
<span class="fa fa-sign-out" aria-hidden="true"></span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% abseventurl request.organizer "presale:organizer.customer.login" %}?next={{ request.path|urlencode }}%3F{{ request.META.QUERY_STRING|urlencode }}">
|
||||
{% trans "Log in" %}</a>
|
||||
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
@@ -8,16 +8,19 @@
|
||||
{% block title %}{% endblock %}{% if url_name != "organizer.index" %} :: {% endif %}{{ organizer.name }}
|
||||
{% endblock %}
|
||||
{% block above %}
|
||||
{% if organizer.settings.locales|length > 1 %}
|
||||
{% if organizer.settings.locales|length > 1 or request.organizer.settings.customer_accounts %}
|
||||
{% if organizer.settings.theme_color_background|upper != "#FFFFFF" or organizer.settings.organizer_logo_image_large %}
|
||||
<div class="container page-header-links">
|
||||
<div class="pull-right header-part flip">
|
||||
<div class="locales">
|
||||
{% for l in languages %}
|
||||
<a href="{% url "presale:locale.set" %}?locale={{ l.code }}&next={{ request.path }}%3F{{ request.META.QUERY_STRING|urlencode }}" class="{% if l.code == request.LANGUAGE_CODE %}active{% endif %}" rel="nofollow">
|
||||
{{ l.name_local }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if organizer.settings.locales|length > 1 %}
|
||||
<nav class="locales" aria-label="{% trans "select language" %}">
|
||||
{% for l in languages %}
|
||||
<a href="{% url "presale:locale.set" %}?locale={{ l.code }}&next={{ request.path }}%3F{{ request.META.QUERY_STRING|urlencode }}" class="{% if l.code == request.LANGUAGE_CODE %}active{% endif %}" rel="nofollow">
|
||||
{{ l.name_local }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% include "pretixpresale/fragment_login_status.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -40,15 +43,18 @@
|
||||
<h1><a href="{% eventurl organizer "presale:organizer.index" %}">{{ organizer.name }}</a></h1>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if organizer.settings.locales|length > 1 %}
|
||||
{% if organizer.settings.locales|length > 1 or request.organizer.settings.customer_accounts %}
|
||||
{% if organizer.settings.theme_color_background|upper == "#FFFFFF" and not organizer.settings.organizer_logo_image_large %}
|
||||
<div class="{% if not organizer_logo or not organizer.settings.organizer_logo_image_large %}pull-right flip{% endif %} loginbox">
|
||||
<div class="locales">
|
||||
{% for l in languages %}
|
||||
<a href="{% url "presale:locale.set" %}?locale={{ l.code }}&next={{ request.path }}%3F{{ request.META.QUERY_STRING|urlencode }}" class="{% if l.code == request.LANGUAGE_CODE %}active{% endif %}" rel="nofollow">
|
||||
{{ l.name_local }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if organizer.settings.locales|length > 1 %}
|
||||
<nav class="locales" aria-label="{% trans "select language" %}">
|
||||
{% for l in languages %}
|
||||
<a href="{% url "presale:locale.set" %}?locale={{ l.code }}&next={{ request.path }}%3F{{ request.META.QUERY_STRING|urlencode }}" class="{% if l.code == request.LANGUAGE_CODE %}active{% endif %}" rel="nofollow">
|
||||
{{ l.name_local }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% include "pretixpresale/fragment_login_status.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Account information" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h2>
|
||||
{% blocktrans trimmed %}
|
||||
Update your account information
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-group buttons">
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p> </p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Log in" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h2>
|
||||
{% blocktrans trimmed with org=request.organizer.name %}
|
||||
Sign in to your account at {{ org }}
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-group buttons">
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||
{% trans "Log in" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<a class="btn btn-link btn-block" href="{% eventurl request.organizer "presale:organizer.customer.register" %}">
|
||||
{% trans "Create account" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<a class="btn btn-link btn-block" href="{% eventurl request.organizer "presale:organizer.customer.resetpw" %}">
|
||||
{% trans "Reset password" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p> </p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,98 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Your membership" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% trans "Your membership" %}
|
||||
</h2>
|
||||
<div class="panel panel-primary items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Details" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Membership type" %}</dt>
|
||||
<dd>{{ membership.membership_type.name }}</dd>
|
||||
<dt>{% trans "Valid from" %}</dt>
|
||||
<dd>{{ membership.date_start|date:"SHORT_DATETIME_FORMAT" }}
|
||||
<dt>{% trans "Valid until" %}</dt>
|
||||
<dd>{{ membership.date_end|date:"SHORT_DATETIME_FORMAT" }}
|
||||
<dt>{% trans "Attendee name" %}</dt>
|
||||
<dd>{{ membership.attendee_name }}
|
||||
<dt>{% trans "Maximum usages" %}</dt>
|
||||
<dd>{{ membership.membership_type.max_usages|default_if_none:"–" }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Usages" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %}</th>
|
||||
<th>{% trans "Event" %}</th>
|
||||
<th>{% trans "Product" %}</th>
|
||||
<th>{% trans "Order date" %}</th>
|
||||
<th class="text-right">{% trans "Status" %}</th>
|
||||
<th class="text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for op in usages %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
{{ op.order.code }}-{{ op.positionid }}
|
||||
</strong>
|
||||
{% if op.order.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ op.order.event }}
|
||||
{% if op.subevent %}
|
||||
<br>
|
||||
{{ op.subevent|default:"" }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ op.item.name }}
|
||||
{% if op.variation %}– {{ op.variation }}{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ op.order.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if op.canceled %}
|
||||
<span class="label label-danger">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Canceled" %}
|
||||
</span>
|
||||
{% else %}
|
||||
{% include "pretixcontrol/orders/fragment_order_status.html" with order=op.order %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% abseventurl op.order.event "presale:event.order" order=op.order.code secret=op.order.secret %}"
|
||||
target="_blank"
|
||||
class="btn btn-default">
|
||||
{% trans "Details" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Password reset" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h2>
|
||||
{% blocktrans trimmed %}
|
||||
Set a new password for your account
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-group buttons">
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p> </p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,158 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Your account" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% trans "Your account" %}
|
||||
</h2>
|
||||
<div class="panel panel-primary items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Account information" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Customer ID" %}</dt>
|
||||
<dd>#{{ customer.identifier }}</dd>
|
||||
<dt>{% trans "E-mail" %}</dt>
|
||||
<dd>{{ customer.email }}
|
||||
</dd>
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ customer.name }}</dd>
|
||||
</dl>
|
||||
<div class="text-right">
|
||||
<a href="{% eventurl request.organizer "presale:organizer.customer.change" %}"
|
||||
class="btn btn-default">
|
||||
{% trans "Change account information" %}
|
||||
</a>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.customer.password" %}"
|
||||
class="btn btn-default">
|
||||
{% trans "Change password" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Memberships" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Membership type" %}</th>
|
||||
<th>{% trans "Valid from" %}</th>
|
||||
<th>{% trans "Valid until" %}</th>
|
||||
<th>{% trans "Attendee name" %}</th>
|
||||
<th>{% trans "Usages" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in memberships %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ m.membership_type.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.date_start|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.date_end|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.attendee_name }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="quotabox">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-{{ m.percent }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="numbers">
|
||||
{{ m.usages }} /
|
||||
{{ m.membership_type.max_usages|default_if_none:"∞" }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% abseventurl request.organizer "presale:organizer.customer.membership" id=m.id %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Details" %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-list"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Orders" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %}</th>
|
||||
<th>{% trans "Event" %}</th>
|
||||
<th>{% trans "Order date" %}</th>
|
||||
<th class="text-right">{% trans "Order total" %}</th>
|
||||
<th class="text-right">{% trans "Positions" %}</th>
|
||||
<th class="text-right">{% trans "Status" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for o in orders %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% abseventurl o.event "presale:event.order" order=o.code secret=o.secret %}" target="_blank">
|
||||
{{ o.code }}
|
||||
</a>
|
||||
</strong>
|
||||
{% if o.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.event }}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if o.customer_id != customer.pk %}
|
||||
<span class="fa fa-link text-muted"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Matched to the account based on the email address." %}"
|
||||
></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{{ o.total|money:o.event.currency }}
|
||||
</td>
|
||||
<td class="text-right flip">{{ o.count_positions|default_if_none:"0" }}</td>
|
||||
<td class="text-right flip">{% include "pretixpresale/event/fragment_order_status.html" with order=o event=o.event %}</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% abseventurl o.event "presale:event.order" order=o.code secret=o.secret %}"
|
||||
target="_blank"
|
||||
class="btn btn-default">
|
||||
{% trans "Details" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Registration" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h2>
|
||||
{% blocktrans trimmed with org=request.organizer.name %}
|
||||
Create a new account at {{ org }}
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-group buttons">
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||
{% trans "Create account" %}
|
||||
</button>
|
||||
</div>
|
||||
<a class="btn btn-link btn-block" href="{% eventurl request.organizer "presale:organizer.customer.login" %}">
|
||||
{% trans "Log in to an existing account" %}
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p> </p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Password reset" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h2>
|
||||
{% blocktrans trimmed %}
|
||||
Password reset
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-group buttons">
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||
{% trans "Request a new password" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p> </p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Password reset" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h2>
|
||||
{% blocktrans trimmed %}
|
||||
Set a new password for your account
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-group buttons">
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p> </p>
|
||||
{% endblock %}
|
||||
@@ -37,6 +37,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
import pretix.presale.views.cart
|
||||
import pretix.presale.views.checkout
|
||||
import pretix.presale.views.customer
|
||||
import pretix.presale.views.event
|
||||
import pretix.presale.views.locale
|
||||
import pretix.presale.views.order
|
||||
@@ -165,6 +166,17 @@ organizer_patterns = [
|
||||
url(r'^widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(),
|
||||
name='organizer.widget.productlist'),
|
||||
url(r'^widget/v1.css$', pretix.presale.views.widget.widget_css, name='organizer.widget.css'),
|
||||
url(r'^account/login$', pretix.presale.views.customer.LoginView.as_view(), name='organizer.customer.login'),
|
||||
url(r'^account/logout$', pretix.presale.views.customer.LogoutView.as_view(), name='organizer.customer.logout'),
|
||||
url(r'^account/register$', pretix.presale.views.customer.RegistrationView.as_view(), name='organizer.customer.register'),
|
||||
url(r'^account/pwreset$', pretix.presale.views.customer.ResetPasswordView.as_view(), name='organizer.customer.resetpw'),
|
||||
url(r'^account/pwrecover$', pretix.presale.views.customer.SetPasswordView.as_view(), name='organizer.customer.recoverpw'),
|
||||
url(r'^account/activate$', pretix.presale.views.customer.SetPasswordView.as_view(), name='organizer.customer.activate'),
|
||||
url(r'^account/password$', pretix.presale.views.customer.ChangePasswordView.as_view(), name='organizer.customer.password'),
|
||||
url(r'^account/change$', pretix.presale.views.customer.ChangeInformationView.as_view(), name='organizer.customer.change'),
|
||||
url(r'^account/confirmchange$', pretix.presale.views.customer.ConfirmChangeView.as_view(), name='organizer.customer.change.confirm'),
|
||||
url(r'^account/membership/(?P<id>\d+)/$', pretix.presale.views.customer.MembershipUsageView.as_view(), name='organizer.customer.membership'),
|
||||
url(r'^account/$', pretix.presale.views.customer.ProfileView.as_view(), name='organizer.customer.profile'),
|
||||
]
|
||||
|
||||
locale_patterns = [
|
||||
|
||||
@@ -39,15 +39,19 @@ from urllib.parse import urljoin
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.middleware.csrf import rotate_token
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import resolve
|
||||
from django.utils.crypto import constant_time_compare
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.defaults import permission_denied
|
||||
from django_scopes import scope
|
||||
|
||||
from pretix.base.middleware import LocaleMiddleware
|
||||
from pretix.base.models import Event, Organizer
|
||||
from pretix.base.models import Customer, Event, Organizer
|
||||
from pretix.multidomain.urlreverse import (
|
||||
get_event_domain, get_organizer_domain,
|
||||
)
|
||||
@@ -56,8 +60,96 @@ from pretix.presale.signals import process_request, process_response
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
def get_customer(request):
|
||||
if not hasattr(request, '_cached_customer'):
|
||||
session_key = f'customer_auth_id:{request.organizer.pk}'
|
||||
hash_session_key = f'customer_auth_hash:{request.organizer.pk}'
|
||||
|
||||
with scope(organizer=request.organizer):
|
||||
try:
|
||||
customer = request.organizer.customers.get(
|
||||
is_active=True, is_verified=True,
|
||||
pk=request.session[session_key]
|
||||
)
|
||||
except (Customer.DoesNotExist, KeyError):
|
||||
request._cached_customer = None
|
||||
else:
|
||||
session_hash = request.session.get(hash_session_key)
|
||||
session_hash_verified = session_hash and constant_time_compare(
|
||||
session_hash,
|
||||
customer.get_session_auth_hash()
|
||||
)
|
||||
if session_hash_verified:
|
||||
request._cached_customer = customer
|
||||
else:
|
||||
request.session.flush()
|
||||
request._cached_customer = None
|
||||
|
||||
return request._cached_customer
|
||||
|
||||
|
||||
def update_customer_session_auth_hash(request, customer):
|
||||
hash_session_key = f'customer_auth_hash:{request.organizer.pk}'
|
||||
session_auth_hash = customer.get_session_auth_hash()
|
||||
request.session.cycle_key()
|
||||
request.session[hash_session_key] = session_auth_hash
|
||||
|
||||
|
||||
def add_customer_to_request(request):
|
||||
request.customer = SimpleLazyObject(lambda: get_customer(request))
|
||||
|
||||
|
||||
def customer_login(request, customer):
|
||||
session_key = f'customer_auth_id:{request.organizer.pk}'
|
||||
hash_session_key = f'customer_auth_hash:{request.organizer.pk}'
|
||||
session_auth_hash = customer.get_session_auth_hash()
|
||||
|
||||
if session_key in request.session:
|
||||
if request.session[session_key] != customer.pk or (
|
||||
not constant_time_compare(request.session.get(hash_session_key, ''), session_auth_hash)):
|
||||
# To avoid reusing another user's session, create a new, empty
|
||||
# session if the existing session corresponds to a different
|
||||
# authenticated user.
|
||||
request.session.flush()
|
||||
else:
|
||||
request.session.cycle_key()
|
||||
|
||||
request.session[session_key] = customer.pk
|
||||
request.session[hash_session_key] = session_auth_hash
|
||||
request.customer = customer
|
||||
|
||||
customer.last_login = now()
|
||||
customer.save(update_fields=['last_login'])
|
||||
|
||||
rotate_token(request)
|
||||
|
||||
|
||||
def customer_logout(request):
|
||||
session_key = f'customer_auth_id:{request.organizer.pk}'
|
||||
hash_session_key = f'customer_auth_hash:{request.organizer.pk}'
|
||||
|
||||
# Remove user session
|
||||
customer_id = request.session.pop(session_key, None)
|
||||
request.session.pop(hash_session_key, None)
|
||||
|
||||
# Remove carts tied to this user
|
||||
carts = request.session.get('carts', {})
|
||||
for k, v in list(carts.items()):
|
||||
if v.get('customer') == customer_id:
|
||||
carts.pop(k)
|
||||
request.session['carts'] = carts
|
||||
|
||||
# Cycle session key and CSRF token
|
||||
request.session.cycle_key()
|
||||
rotate_token(request)
|
||||
|
||||
request.customer = None
|
||||
request._cached_customer = None
|
||||
|
||||
|
||||
@scope(organizer=None)
|
||||
def _detect_event(request, require_live=True, require_plugin=None):
|
||||
|
||||
if hasattr(request, '_event_detected'):
|
||||
return
|
||||
|
||||
@@ -132,6 +224,9 @@ def _detect_event(request, require_live=True, require_plugin=None):
|
||||
r['Access-Control-Allow-Origin'] = '*'
|
||||
return r
|
||||
|
||||
if not hasattr(request, 'customer'):
|
||||
add_customer_to_request(request)
|
||||
|
||||
if hasattr(request, 'event'):
|
||||
# Restrict locales to the ones available for this event
|
||||
LocaleMiddleware().process_request(request)
|
||||
|
||||
@@ -46,7 +46,7 @@ from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CartPosition, InvoiceAddress, ItemAddOn, OrderPosition, Question,
|
||||
CartPosition, Customer, InvoiceAddress, ItemAddOn, OrderPosition, Question,
|
||||
QuestionAnswer, QuestionOption,
|
||||
)
|
||||
from pretix.base.services.cart import get_fees
|
||||
@@ -91,6 +91,14 @@ class CartMixin:
|
||||
from pretix.presale.views.cart import cart_session
|
||||
return cart_session(self.request)
|
||||
|
||||
@cached_property
|
||||
def cart_customer(self):
|
||||
if self.cart_session.get('customer_mode', 'guest') == 'login':
|
||||
try:
|
||||
return self.request.organizer.customers.get(pk=self.cart_session.get('customer', -1))
|
||||
except Customer.DoesNotExist:
|
||||
return
|
||||
|
||||
@cached_property
|
||||
def invoice_address(self):
|
||||
return cached_invoice_address(self.request)
|
||||
@@ -273,7 +281,7 @@ def get_cart(request):
|
||||
'item__category__position', 'item__category_id', 'item__position', 'item__name', 'variation__value'
|
||||
).select_related(
|
||||
'item', 'variation', 'subevent', 'subevent__event', 'subevent__event__organizer',
|
||||
'item__tax_rule', 'addon_to'
|
||||
'item__tax_rule', 'addon_to', 'used_membership', 'used_membership__membership_type'
|
||||
).select_related(
|
||||
'addon_to'
|
||||
).prefetch_related(
|
||||
|
||||
472
src/pretix/presale/views/customer.py
Normal file
472
src/pretix/presale/views/customer.py
Normal file
@@ -0,0 +1,472 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.signing import BadSignature, dumps, loads
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, IntegerField, OuterRef, Q, Subquery
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic import FormView, ListView, View
|
||||
|
||||
from pretix.base.models import Customer, Order, OrderPosition
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
|
||||
from pretix.presale.forms.customer import (
|
||||
AuthenticationForm, ChangeInfoForm, ChangePasswordForm, RegistrationForm,
|
||||
ResetPasswordForm, SetPasswordForm, TokenGenerator,
|
||||
)
|
||||
from pretix.presale.utils import (
|
||||
customer_login, customer_logout, update_customer_session_auth_hash,
|
||||
)
|
||||
|
||||
|
||||
class RedirectBackMixin:
|
||||
redirect_field_name = 'next'
|
||||
|
||||
def get_redirect_url(self):
|
||||
"""Return the user-originating redirect URL if it's safe."""
|
||||
redirect_to = self.request.POST.get(
|
||||
self.redirect_field_name,
|
||||
self.request.GET.get(self.redirect_field_name, '')
|
||||
)
|
||||
url_is_safe = url_has_allowed_host_and_scheme(
|
||||
url=redirect_to,
|
||||
allowed_hosts=None,
|
||||
require_https=self.request.is_secure(),
|
||||
)
|
||||
return redirect_to if url_is_safe else ''
|
||||
|
||||
|
||||
class LoginView(RedirectBackMixin, FormView):
|
||||
"""
|
||||
Display the login form and handle the login action.
|
||||
"""
|
||||
form_class = AuthenticationForm
|
||||
template_name = 'pretixpresale/organizers/customer_login.html'
|
||||
redirect_authenticated_user = True
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
raise Http404('Feature not enabled')
|
||||
if self.redirect_authenticated_user and self.request.customer:
|
||||
redirect_to = self.get_success_url()
|
||||
if redirect_to == self.request.path:
|
||||
raise ValueError(
|
||||
"Redirection loop for authenticated user detected. Check that "
|
||||
"your LOGIN_REDIRECT_URL doesn't point to a login page."
|
||||
)
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['request'] = self.request
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
url = self.get_redirect_url()
|
||||
return url or eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Security check complete. Log the user in."""
|
||||
customer_login(self.request, form.get_customer())
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
class LogoutView(View):
|
||||
redirect_field_name = 'next'
|
||||
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
customer_logout(request)
|
||||
next_page = self.get_next_page()
|
||||
return HttpResponseRedirect(next_page)
|
||||
|
||||
def get_next_page(self):
|
||||
next_page = eventreverse(self.request.organizer, 'presale:organizer.index', kwargs={})
|
||||
|
||||
if (self.redirect_field_name in self.request.POST or
|
||||
self.redirect_field_name in self.request.GET):
|
||||
next_page = self.request.POST.get(
|
||||
self.redirect_field_name,
|
||||
self.request.GET.get(self.redirect_field_name)
|
||||
)
|
||||
url_is_safe = url_has_allowed_host_and_scheme(
|
||||
url=next_page,
|
||||
allowed_hosts=None,
|
||||
require_https=self.request.is_secure(),
|
||||
)
|
||||
# Security check -- Ensure the user-originating redirection URL is
|
||||
# safe.
|
||||
if not url_is_safe:
|
||||
next_page = self.request.path
|
||||
return next_page
|
||||
|
||||
|
||||
class RegistrationView(RedirectBackMixin, FormView):
|
||||
form_class = RegistrationForm
|
||||
template_name = 'pretixpresale/organizers/customer_registration.html'
|
||||
redirect_authenticated_user = True
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
raise Http404('Feature not enabled')
|
||||
if self.redirect_authenticated_user and self.request.customer:
|
||||
redirect_to = self.get_success_url()
|
||||
if redirect_to == self.request.path:
|
||||
raise ValueError(
|
||||
"Redirection loop for authenticated user detected. Check that "
|
||||
"your LOGIN_REDIRECT_URL doesn't point to a login page."
|
||||
)
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['request'] = self.request
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
url = self.get_redirect_url()
|
||||
return url or eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={})
|
||||
|
||||
def form_valid(self, form):
|
||||
with transaction.atomic():
|
||||
form.create()
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Your account has been created. Please follow the link in the email we sent you to activate your '
|
||||
'account and choose a password.')
|
||||
)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
class SetPasswordView(FormView):
|
||||
form_class = SetPasswordForm
|
||||
template_name = 'pretixpresale/organizers/customer_setpassword.html'
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
raise Http404('Feature not enabled')
|
||||
try:
|
||||
self.customer = request.organizer.customers.get(identifier=self.request.GET.get('id'))
|
||||
except Customer.DoesNotExist:
|
||||
messages.error(request, _('You clicked an invalid link.'))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
if not TokenGenerator().check_token(self.customer, self.request.GET.get('token', '')):
|
||||
messages.error(request, _('You clicked an invalid link.'))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['customer'] = self.customer
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
return eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={})
|
||||
|
||||
def form_valid(self, form):
|
||||
with transaction.atomic():
|
||||
self.customer.set_password(form.cleaned_data['password'])
|
||||
self.customer.is_verified = True
|
||||
self.customer.save()
|
||||
self.customer.log_action('pretix.customer.password.set', {})
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Your new password has been set! You can now use it to log in.'),
|
||||
)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
class ResetPasswordView(FormView):
|
||||
form_class = ResetPasswordForm
|
||||
template_name = 'pretixpresale/organizers/customer_resetpw.html'
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
raise Http404('Feature not enabled')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={})
|
||||
|
||||
def form_valid(self, form):
|
||||
customer = form.customer
|
||||
customer.log_action('pretix.customer.password.resetrequested', {})
|
||||
ctx = customer.get_email_context()
|
||||
token = TokenGenerator().make_token(customer)
|
||||
ctx['url'] = build_absolute_uri(self.request.organizer,
|
||||
'presale:organizer.customer.recoverpw') + '?id=' + customer.identifier + '&token=' + token
|
||||
mail(
|
||||
customer.email,
|
||||
_('Set a new password for your account at {organizer}').format(organizer=self.request.organizer.name),
|
||||
self.request.organizer.settings.mail_text_customer_reset,
|
||||
ctx,
|
||||
locale=customer.locale,
|
||||
customer=customer,
|
||||
organizer=self.request.organizer,
|
||||
)
|
||||
messages.success(
|
||||
self.request,
|
||||
_('We\'ve sent you an email with further instructions on resetting your password.')
|
||||
)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['request'] = self.request
|
||||
return kwargs
|
||||
|
||||
|
||||
class CustomerRequiredMixin:
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
raise Http404('Feature not enabled')
|
||||
if not getattr(request, 'customer', None):
|
||||
return redirect(
|
||||
eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={}) +
|
||||
'?next=' + quote(self.request.path_info + '?' + self.request.GET.urlencode())
|
||||
)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class ProfileView(CustomerRequiredMixin, ListView):
|
||||
template_name = 'pretixpresale/organizers/customer_profile.html'
|
||||
context_object_name = 'orders'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Order.objects.filter(
|
||||
Q(customer=self.request.customer)
|
||||
| Q(email__iexact=self.request.customer.email)
|
||||
# This is safe because we only let customers with verified emails log in
|
||||
).select_related('event').order_by('-datetime')
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['customer'] = self.request.customer
|
||||
ctx['memberships'] = self.request.customer.memberships.with_usages().select_related(
|
||||
'membership_type', 'granted_in', 'granted_in__order', 'granted_in__order__event'
|
||||
)
|
||||
ctx['is_paginated'] = True
|
||||
|
||||
for m in ctx['memberships']:
|
||||
if m.membership_type.max_usages:
|
||||
m.percent = int(m.usages / m.membership_type.max_usages * 100)
|
||||
else:
|
||||
m.percent = 0
|
||||
|
||||
s = OrderPosition.objects.filter(
|
||||
order=OuterRef('pk')
|
||||
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||
annotated = {
|
||||
o['pk']: o
|
||||
for o in
|
||||
Order.annotate_overpayments(Order.objects, sums=True).filter(
|
||||
pk__in=[o.pk for o in ctx['orders']]
|
||||
).annotate(
|
||||
pcnt=Subquery(s, output_field=IntegerField()),
|
||||
).values(
|
||||
'pk', 'pcnt',
|
||||
)
|
||||
}
|
||||
|
||||
for o in ctx['orders']:
|
||||
if o.pk not in annotated:
|
||||
continue
|
||||
o.count_positions = annotated.get(o.pk)['pcnt']
|
||||
return ctx
|
||||
|
||||
|
||||
class MembershipUsageView(CustomerRequiredMixin, ListView):
|
||||
template_name = 'pretixpresale/organizers/customer_membership.html'
|
||||
context_object_name = 'usages'
|
||||
paginate_by = 20
|
||||
|
||||
@cached_property
|
||||
def membership(self):
|
||||
return get_object_or_404(
|
||||
self.request.customer.memberships,
|
||||
pk=self.kwargs.get('id')
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.membership.orderposition_set.select_related(
|
||||
'order', 'order__event', 'subevent', 'item', 'variation',
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['membership'] = self.membership
|
||||
ctx['is_paginated'] = True
|
||||
return ctx
|
||||
|
||||
|
||||
class ChangePasswordView(CustomerRequiredMixin, FormView):
|
||||
template_name = 'pretixpresale/organizers/customer_password.html'
|
||||
form_class = ChangePasswordForm
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
raise Http404('Feature not enabled')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
|
||||
|
||||
@transaction.atomic()
|
||||
def form_valid(self, form):
|
||||
customer = form.customer
|
||||
customer.log_action('pretix.customer.password.set', {})
|
||||
customer.set_password(form.cleaned_data['password'])
|
||||
customer.save()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
update_customer_session_auth_hash(self.request, customer)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['customer'] = self.request.customer
|
||||
return kwargs
|
||||
|
||||
|
||||
class ChangeInformationView(CustomerRequiredMixin, FormView):
|
||||
template_name = 'pretixpresale/organizers/customer_info.html'
|
||||
form_class = ChangeInfoForm
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@method_decorator(csrf_protect)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
raise Http404('Feature not enabled')
|
||||
if self.request.customer:
|
||||
self.initial_email = self.request.customer.email
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.cleaned_data['email'] != self.initial_email:
|
||||
new_email = form.cleaned_data['email']
|
||||
form.cleaned_data['email'] = form.instance.email = self.initial_email
|
||||
ctx = form.instance.get_email_context()
|
||||
ctx['url'] = build_absolute_uri(
|
||||
self.request.organizer,
|
||||
'presale:organizer.customer.change.confirm'
|
||||
) + '?token=' + dumps({
|
||||
'customer': form.instance.pk,
|
||||
'email': new_email
|
||||
}, salt='pretix.presale.views.customer.ChangeInformationView')
|
||||
mail(
|
||||
new_email,
|
||||
_('Confirm email address for your account at {organizer}').format(organizer=self.request.organizer.name),
|
||||
self.request.organizer.settings.mail_text_customer_email_change,
|
||||
ctx,
|
||||
locale=form.instance.locale,
|
||||
customer=form.instance,
|
||||
organizer=self.request.organizer,
|
||||
)
|
||||
messages.success(self.request, _('Your changes have been saved. We\'ve sent you an email with a link to update your '
|
||||
'email address. The email address of your account will be changed as soon as you '
|
||||
'click that link.'))
|
||||
else:
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
|
||||
with transaction.atomic():
|
||||
form.save()
|
||||
d = dict(form.cleaned_data)
|
||||
del d['email']
|
||||
self.request.customer.log_action('pretix.customer.changed', d)
|
||||
|
||||
update_customer_session_auth_hash(self.request, form.instance)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['request'] = self.request
|
||||
kwargs['instance'] = self.request.customer
|
||||
return kwargs
|
||||
|
||||
|
||||
class ConfirmChangeView(View):
|
||||
template_name = 'pretixpresale/organizers/customer_info.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not request.organizer.settings.customer_accounts:
|
||||
raise Http404('Feature not enabled')
|
||||
|
||||
try:
|
||||
data = loads(request.GET.get('token', ''), salt='pretix.presale.views.customer.ChangeInformationView', max_age=3600 * 24)
|
||||
except BadSignature:
|
||||
messages.error(request, _('You clicked an invalid link.'))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
try:
|
||||
customer = request.organizer.customers.get(pk=data.get('customer'))
|
||||
except Customer.DoesNotExist:
|
||||
messages.error(request, _('You clicked an invalid link.'))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
with transaction.atomic():
|
||||
customer.email = data['email']
|
||||
customer.save()
|
||||
customer.log_action('pretix.customer.changed', {
|
||||
'email': data['email']
|
||||
})
|
||||
|
||||
messages.success(request, _('Your email address has been updated.'))
|
||||
|
||||
if customer == request.customer:
|
||||
update_customer_session_auth_hash(self.request, customer)
|
||||
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
|
||||
@@ -731,6 +731,14 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
|
||||
for k in override:
|
||||
# We don't want initial values to be modified, they should come from the order directly
|
||||
override[k].pop('initial', None)
|
||||
|
||||
if order_position.used_membership and not order_position.used_membership.membership_type.transferable:
|
||||
override_sets.append({
|
||||
'attendee_name_parts': {
|
||||
'disabled': True
|
||||
}
|
||||
})
|
||||
|
||||
return override_sets
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
Reference in New Issue
Block a user