Customer accounts & Memberships (#2024)

This commit is contained in:
Raphael Michel
2021-05-04 16:56:06 +02:00
committed by GitHub
parent 62e412bbc0
commit 8e79eb570e
116 changed files with 7975 additions and 279 deletions

View File

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

View File

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

View 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

View File

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

View File

@@ -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" %}">
&laquo; {% 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 %}

View File

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

View File

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

View File

@@ -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 }} &middot; {{ 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 %}

View File

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

View File

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

View File

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

View File

@@ -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>&nbsp;</p>
{% endblock %}

View File

@@ -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>&nbsp;</p>
{% endblock %}

View File

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

View File

@@ -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>&nbsp;</p>
{% endblock %}

View File

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

View File

@@ -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>&nbsp;</p>
{% endblock %}

View File

@@ -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>&nbsp;</p>
{% endblock %}

View File

@@ -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>&nbsp;</p>
{% endblock %}

View File

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

View File

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

View File

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

View 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={})

View File

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