forked from CGM_Public/pretix_original
371 lines
13 KiB
Python
371 lines
13 KiB
Python
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 Q, Sum
|
|
from django.http import HttpResponseNotAllowed
|
|
from django.shortcuts import redirect
|
|
from django.utils import translation
|
|
from django.utils.functional import cached_property
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.views.generic.base import TemplateResponseMixin
|
|
|
|
from pretix.base.models import CartPosition, Order
|
|
from pretix.base.models.orders import InvoiceAddress
|
|
from pretix.base.services.orders import OrderError, perform_order
|
|
from pretix.base.signals import register_payment_providers
|
|
from pretix.multidomain.urlreverse import eventreverse
|
|
from pretix.presale.forms.checkout import ContactForm, InvoiceAddressForm
|
|
from pretix.presale.signals import checkout_flow_steps
|
|
from pretix.presale.views import CartMixin
|
|
from pretix.presale.views.async import AsyncAction
|
|
from pretix.presale.views.questions import QuestionsViewMixin
|
|
|
|
|
|
class BaseCheckoutFlowStep:
|
|
def __init__(self, event):
|
|
self.event = event
|
|
self.request = None
|
|
|
|
@property
|
|
def identifier(self):
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def priority(self):
|
|
return 100
|
|
|
|
def is_applicable(self, request):
|
|
return True
|
|
|
|
def is_completed(self, request, warn=False):
|
|
raise NotImplementedError()
|
|
|
|
def get_next_applicable(self, request):
|
|
if hasattr(self, '_next') and self._next:
|
|
if not self._next.is_applicable(request):
|
|
return self._next.get_next_applicable(request)
|
|
return self._next
|
|
|
|
def get_prev_applicable(self, request):
|
|
if hasattr(self, '_previous') and self._previous:
|
|
if not self._previous.is_applicable(request):
|
|
return self._previous.get_prev_applicable(request)
|
|
return self._previous
|
|
|
|
def get(self, request):
|
|
return HttpResponseNotAllowed([])
|
|
|
|
def post(self, request):
|
|
return HttpResponseNotAllowed([])
|
|
|
|
def get_step_url(self):
|
|
return eventreverse(self.event, 'presale:event.checkout', kwargs={'step': self.identifier})
|
|
|
|
def get_prev_url(self, request):
|
|
prev = self.get_prev_applicable(request)
|
|
if not prev:
|
|
return eventreverse(self.event, 'presale:event.index')
|
|
else:
|
|
return prev.get_step_url()
|
|
|
|
def get_next_url(self, request):
|
|
n = self.get_next_applicable(request)
|
|
if n:
|
|
return n.get_step_url()
|
|
|
|
|
|
def get_checkout_flow(event):
|
|
flow = list([step(event) for step in DEFAULT_FLOW])
|
|
for receiver, response in checkout_flow_steps.send(event):
|
|
step = response(event=event)
|
|
if step.priority > 1000:
|
|
raise ValueError('Plugins are not allowed to define a priority greater than 1000')
|
|
flow.append(step)
|
|
|
|
# Sort by priority
|
|
flow.sort(key=lambda p: p.priority)
|
|
|
|
# Create a double-linked-list for esasy forwards/backwards traversal
|
|
last = None
|
|
for step in flow:
|
|
step._previous = last
|
|
if last:
|
|
last._next = step
|
|
last = step
|
|
return flow
|
|
|
|
|
|
class TemplateFlowStep(TemplateResponseMixin, BaseCheckoutFlowStep):
|
|
template_name = ""
|
|
|
|
def get_context_data(self, **kwargs):
|
|
kwargs.setdefault('step', self)
|
|
kwargs.setdefault('event', self.event)
|
|
kwargs.setdefault('prev_url', self.get_prev_url(self.request))
|
|
return kwargs
|
|
|
|
def render(self, **kwargs):
|
|
context = self.get_context_data(**kwargs)
|
|
return self.render_to_response(context)
|
|
|
|
def get(self, request):
|
|
self.request = request
|
|
return self.render()
|
|
|
|
def post(self, request):
|
|
self.request = request
|
|
return self.render()
|
|
|
|
def is_completed(self, request, warn=False):
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def identifier(self):
|
|
raise NotImplementedError()
|
|
|
|
|
|
class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
|
priority = 50
|
|
identifier = "questions"
|
|
template_name = "pretixpresale/event/checkout_questions.html"
|
|
|
|
def is_applicable(self, request):
|
|
return True
|
|
|
|
@cached_property
|
|
def contact_form(self):
|
|
return ContactForm(data=self.request.POST if self.request.method == "POST" else None,
|
|
initial={
|
|
'email': self.request.session.get('email', '')
|
|
})
|
|
|
|
@cached_property
|
|
def invoice_address(self):
|
|
try:
|
|
return InvoiceAddress.objects.get(
|
|
pk=self.request.session.get('invoice_address'),
|
|
order__isnull=True
|
|
)
|
|
except InvoiceAddress.DoesNotExist:
|
|
return InvoiceAddress()
|
|
|
|
@cached_property
|
|
def invoice_form(self):
|
|
return InvoiceAddressForm(data=self.request.POST if self.request.method == "POST" else None,
|
|
event=self.request.event,
|
|
instance=self.invoice_address)
|
|
|
|
def post(self, request):
|
|
self.request = request
|
|
failed = not self.save() or not self.contact_form.is_valid()
|
|
if request.event.settings.invoice_address_asked:
|
|
failed = failed or not self.invoice_form.is_valid()
|
|
if failed:
|
|
messages.error(request,
|
|
_("We had difficulties processing your input. Please review the errors below."))
|
|
return self.render()
|
|
request.session['email'] = self.contact_form.cleaned_data['email']
|
|
if request.event.settings.invoice_address_asked:
|
|
addr = self.invoice_form.save()
|
|
request.session['invoice_address'] = addr.pk
|
|
|
|
return redirect(self.get_next_url(request))
|
|
|
|
def is_completed(self, request, warn=False):
|
|
self.request = request
|
|
try:
|
|
emailval = EmailValidator()
|
|
if 'email' not in request.session:
|
|
if warn:
|
|
messages.warning(request, _('Please enter a valid e-mail address.'))
|
|
return False
|
|
emailval(request.session.get('email'))
|
|
except ValidationError:
|
|
if warn:
|
|
messages.warning(request, _('Please enter a valid e-mail address.'))
|
|
return False
|
|
|
|
for cp in self.positions:
|
|
answ = {
|
|
aw.question_id: aw.answer for aw in cp.answers.all()
|
|
}
|
|
for q in cp.item.questions.all():
|
|
if q.required and q.id not in answ:
|
|
if warn:
|
|
messages.warning(request, _('Please fill in answers to all required questions.'))
|
|
return False
|
|
if cp.item.admission and self.request.event.settings.get('attendee_names_required', as_type=bool) \
|
|
and cp.attendee_name is None:
|
|
if warn:
|
|
messages.warning(request, _('Please fill in answers to all required questions.'))
|
|
return False
|
|
return True
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['forms'] = self.forms
|
|
ctx['contact_form'] = self.contact_form
|
|
ctx['invoice_form'] = self.invoice_form
|
|
return ctx
|
|
|
|
|
|
class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
|
priority = 200
|
|
identifier = "payment"
|
|
template_name = "pretixpresale/event/checkout_payment.html"
|
|
|
|
@cached_property
|
|
def _total_order_value(self):
|
|
return CartPosition.objects.filter(
|
|
Q(cart_id=self.request.session.session_key) & Q(event=self.request.event)
|
|
).aggregate(sum=Sum('price'))['sum']
|
|
|
|
@cached_property
|
|
def provider_forms(self):
|
|
providers = []
|
|
responses = register_payment_providers.send(self.request.event)
|
|
for receiver, response in responses:
|
|
provider = response(self.request.event)
|
|
if not provider.is_enabled or not provider.is_allowed(self.request):
|
|
continue
|
|
fee = provider.calculate_fee(self._total_order_value)
|
|
providers.append({
|
|
'provider': provider,
|
|
'fee': fee,
|
|
'form': provider.payment_form_render(self.request)
|
|
})
|
|
return providers
|
|
|
|
def post(self, request):
|
|
self.request = request
|
|
for p in self.provider_forms:
|
|
if p['provider'].identifier == request.POST.get('payment', ''):
|
|
request.session['payment'] = p['provider'].identifier
|
|
resp = p['provider'].checkout_prepare(
|
|
request, self.get_cart(payment_fee=p['provider'].calculate_fee(self._total_order_value)))
|
|
if isinstance(resp, str):
|
|
return redirect(resp)
|
|
elif resp is True:
|
|
return redirect(self.get_next_url(request))
|
|
else:
|
|
return self.render()
|
|
messages.error(self.request, _("Please select a payment method."))
|
|
return self.render()
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['providers'] = self.provider_forms
|
|
ctx['selected'] = self.request.POST.get('payment', self.request.session.get('payment', ''))
|
|
return ctx
|
|
|
|
@cached_property
|
|
def payment_provider(self):
|
|
responses = register_payment_providers.send(self.request.event)
|
|
for receiver, response in responses:
|
|
provider = response(self.request.event)
|
|
if provider.identifier == self.request.session['payment']:
|
|
return provider
|
|
|
|
def is_completed(self, request, warn=False):
|
|
self.request = request
|
|
if 'payment' not in request.session or not self.payment_provider:
|
|
if warn:
|
|
messages.error(request, _('The payment information you entered was incomplete.'))
|
|
return False
|
|
if not self.payment_provider.payment_is_valid_session(request) or \
|
|
not self.payment_provider.is_enabled or \
|
|
not self.payment_provider.is_allowed(request):
|
|
if warn:
|
|
messages.error(request, _('The payment information you entered was incomplete.'))
|
|
return False
|
|
return True
|
|
|
|
def is_applicable(self, request):
|
|
self.request = request
|
|
if self._total_order_value == 0:
|
|
request.session['payment'] = 'free'
|
|
return False
|
|
return True
|
|
|
|
|
|
class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|
priority = 1001
|
|
identifier = "confirm"
|
|
template_name = "pretixpresale/event/checkout_confirm.html"
|
|
task = perform_order
|
|
|
|
def is_applicable(self, request):
|
|
return True
|
|
|
|
def is_completed(self, request, warn=False):
|
|
pass
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx['cart'] = self.get_cart(answers=True)
|
|
ctx['payment'] = self.payment_provider.checkout_confirm_render(self.request)
|
|
ctx['payment_provider'] = self.payment_provider
|
|
ctx['addr'] = self.invoice_address
|
|
return ctx
|
|
|
|
@cached_property
|
|
def payment_provider(self):
|
|
responses = register_payment_providers.send(self.request.event)
|
|
for receiver, response in responses:
|
|
provider = response(self.request.event)
|
|
if provider.identifier == self.request.session['payment']:
|
|
return provider
|
|
|
|
@cached_property
|
|
def invoice_address(self):
|
|
try:
|
|
return InvoiceAddress.objects.get(
|
|
pk=self.request.session.get('invoice_address'),
|
|
order__isnull=True
|
|
)
|
|
except InvoiceAddress.DoesNotExist:
|
|
return InvoiceAddress()
|
|
|
|
def get(self, request):
|
|
self.request = request
|
|
if 'async_id' in request.GET and settings.HAS_CELERY:
|
|
return self.get_result(request)
|
|
return TemplateFlowStep.get(self, request)
|
|
|
|
def post(self, request):
|
|
self.request = request
|
|
return self.do(self.request.event.id, self.payment_provider.identifier,
|
|
[p.id for p in self.positions], request.session.get('email'),
|
|
translation.get_language(), self.invoice_address.pk)
|
|
|
|
def get_success_message(self, value):
|
|
return None
|
|
|
|
def get_success_url(self, value):
|
|
order = Order.objects.get(id=value)
|
|
return self.get_order_url(order)
|
|
|
|
def get_error_message(self, exception):
|
|
if isinstance(exception, dict) and exception['exc_type'] == 'OrderError':
|
|
return exception['exc_message']
|
|
elif isinstance(exception, OrderError):
|
|
return str(exception)
|
|
return super().get_error_message(exception)
|
|
|
|
def get_error_url(self):
|
|
return self.get_step_url()
|
|
|
|
def get_order_url(self, order):
|
|
return eventreverse(self.request.event, 'presale:event.order.pay.complete', kwargs={
|
|
'order': order.code,
|
|
'secret': order.secret
|
|
})
|
|
|
|
|
|
DEFAULT_FLOW = (
|
|
QuestionsStep,
|
|
PaymentStep,
|
|
ConfirmStep
|
|
)
|