Files
pretix_cgo/src/pretix/presale/checkoutflow.py
2016-02-22 13:49:15 +01:00

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
)