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