mirror of
https://github.com/pretix/pretix.git
synced 2026-05-10 16:04:02 +00:00
Enabled asynchronous cart/order actions
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -10,12 +11,13 @@ 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
|
||||
from pretix.base.models import CartPosition, Order
|
||||
from pretix.base.services.orders import OrderError, perform_order
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.presale.forms.checkout import ContactForm
|
||||
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
|
||||
|
||||
|
||||
@@ -278,10 +280,11 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
return True
|
||||
|
||||
|
||||
class ConfirmStep(CartMixin, TemplateFlowStep):
|
||||
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
|
||||
@@ -304,28 +307,47 @@ class ConfirmStep(CartMixin, TemplateFlowStep):
|
||||
if provider.identifier == self.request.session['payment']:
|
||||
return provider
|
||||
|
||||
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
|
||||
try:
|
||||
order = perform_order(self.request.event, self.payment_provider, self.positions,
|
||||
email=request.session.get('email', None),
|
||||
locale=translation.get_language())
|
||||
except OrderError as e:
|
||||
messages.error(request, str(e))
|
||||
return redirect(self.get_step_url())
|
||||
else:
|
||||
# Message is delivered via GET parameter
|
||||
# messages.success(request, _('Your order has been placed.'))
|
||||
resp = self.payment_provider.payment_perform(request, order)
|
||||
return redirect(resp or self.get_order_url(order))
|
||||
return self.do(self.request.event.identity, self.payment_provider.identifier,
|
||||
[p.identity for p in self.positions], request.session.get('email'),
|
||||
translation.get_language())
|
||||
|
||||
def get_success_message(self, value):
|
||||
return None
|
||||
|
||||
def success(self, value):
|
||||
# Message is delivered via GET parameter
|
||||
# messages.success(request, _('Your order has been placed.'))
|
||||
return redirect(self.get_success_url(value))
|
||||
|
||||
def get_success_url(self, value):
|
||||
order = Order.objects.current.get(identity=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 reverse('presale:event.order', kwargs={
|
||||
return reverse('presale:event.order.pay.complete', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}) + '?thanks=yes'
|
||||
})
|
||||
|
||||
|
||||
DEFAULT_FLOW = (
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "bootstrap/dist/js/bootstrap.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/main.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/asynctask.js" %}"></script>
|
||||
{% endcompress %}
|
||||
{{ html_head|safe }}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -63,6 +64,16 @@
|
||||
{% endblocktrans %}
|
||||
{% endwith %}
|
||||
</footer>
|
||||
<script type="text/javascript">
|
||||
var default_loading_message = '{% trans "We are processing your request…" %}';
|
||||
</script>
|
||||
<div id="loadingmodal">
|
||||
<i class="fa fa-cog big-rotating-icon"></i>
|
||||
<h1>{% trans "We are processing your request…" %}</h1>
|
||||
<p>
|
||||
{% trans "If this takes longer than a few minutes, please contact us." %}
|
||||
</p>
|
||||
</div>
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "lightbox/js/lightbox.min.js" %}"></script>
|
||||
{% endcompress %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block content %}
|
||||
<h2>{% trans "Confirm order" %}</h2>
|
||||
<p>{% trans "Please review the details below and confirm your order." %}</p>
|
||||
<form method="post">
|
||||
<form method="post" data-asynctask>
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-primary cart">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
{{ line.count }}
|
||||
{% if editable %}
|
||||
<form action="{% url "presale:event.cart.add" event=event.slug organizer=event.organizer.slug %}"
|
||||
method="post">
|
||||
method="post" data-asynctask>
|
||||
{% csrf_token %}
|
||||
{% if line.variation %}
|
||||
<input type="hidden" name="variation_{{ line.item.identity }}_{{ line.variation.identity }}"
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if event.presale_is_running or event.settings.show_items_outside_presale_period %}
|
||||
<form method="post"
|
||||
<form method="post" data-asynctask
|
||||
action="{% url "presale:event.cart.add" organizer=request.event.organizer.slug event=request.event.slug %}?next={{ request.path|urlencode }}">
|
||||
{% csrf_token %}
|
||||
{% for tup in items_by_category %}
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Order details" %}{% endblock %}
|
||||
{% block content %}
|
||||
{% if "thanks" in request.GET %}
|
||||
{% if "thanks" in request.GET or "paid" in request.GET %}
|
||||
<div class="thank-you">
|
||||
<span class="fa fa-check-circle"></span>
|
||||
|
||||
<h2>{% trans "Thank you!" %}</h2>
|
||||
<p>{% trans "Your order has been placed successfully. See below for details." %}</p>
|
||||
{% if order.status != 'p' %}
|
||||
<p>{% trans "Your order has been placed successfully. See below for details." %}</p>
|
||||
{% else %}
|
||||
<p>{% trans "We successfully received your payment. See below for details." %}</p>
|
||||
{% endif %}
|
||||
<p>{% trans "We also sent you an email with a link to this page if you want to come back later." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -18,7 +23,7 @@
|
||||
{% include "pretixpresale/event/fragment_order_status.html" with order=order class="pull-right" %}
|
||||
<div class="clearfix"></div>
|
||||
</h2>
|
||||
{% if order.status == "n" %}
|
||||
{% if order.status == "n" %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
@@ -28,12 +33,14 @@
|
||||
<div class="panel-body">
|
||||
{% if can_retry %}
|
||||
<a href="{% url "presale:event.order.pay" organizer=request.event.organizer.slug event=request.event.slug secret=order.secret order=order.code %}"
|
||||
class="btn btn-primary pull-right"><i class="fa fa-money"></i> {% trans "Complete payment" %}</a>
|
||||
class="btn btn-primary pull-right"><i class="fa fa-money"></i> {% trans "Complete payment" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ payment }}
|
||||
<strong>{% blocktrans trimmed with date=order.expires|date:"SHORT_DATE_FORMAT" %}
|
||||
Please complete your payment before {{ date }}
|
||||
{% endblocktrans %}</strong>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,7 +95,7 @@
|
||||
<div class="col-md-12 text-right">
|
||||
<p>
|
||||
<a href="{% url 'presale:event.order.cancel' event=request.event.slug organizer=request.event.organizer.slug secret=order.secret order=order.code %}"
|
||||
class="btn btn-danger">
|
||||
class="btn btn-danger">
|
||||
<span class="fa fa-remove"></span>
|
||||
{% trans "Cancel order" %}
|
||||
</a>
|
||||
|
||||
37
src/pretix/presale/templates/pretixpresale/waiting.html
Normal file
37
src/pretix/presale/templates/pretixpresale/waiting.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/less" href="{% static "pretixpresale/less/waiting.less" %}"/>
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
||||
{% endcompress %}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="refresh" content="1">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<i class="fa fa-cog big-rotating-icon"></i>
|
||||
|
||||
<h1>{% trans "We are processing your request…" %}</h1>
|
||||
|
||||
<p>
|
||||
{% trans "If this takes longer than a few minutes, please contact us." %}
|
||||
</p>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
window.setInterval(function () {
|
||||
$.get(location.href + '&ajax=1', function (data, status) {
|
||||
if (data.ready && data.redirect) {
|
||||
location.href = data.redirect;
|
||||
}
|
||||
}, 'json');
|
||||
}, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -27,6 +27,9 @@ urlpatterns = [
|
||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/confirm$',
|
||||
pretix.presale.views.order.OrderPayDo.as_view(),
|
||||
name='event.order.pay.confirm'),
|
||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/complete$',
|
||||
pretix.presale.views.order.OrderPayComplete.as_view(),
|
||||
name='event.order.pay.complete'),
|
||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$',
|
||||
pretix.presale.views.order.OrderDownload.as_view(),
|
||||
name='event.order.download'),
|
||||
|
||||
122
src/pretix/presale/views/async.py
Normal file
122
src/pretix/presale/views/async.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
logger = logging.getLogger('pretix.presale.async')
|
||||
|
||||
|
||||
class AsyncAction:
|
||||
task = None
|
||||
success_url = None
|
||||
error_url = None
|
||||
|
||||
def do(self, *args):
|
||||
if settings.HAS_CELERY:
|
||||
from pretix.celery import app
|
||||
|
||||
if hasattr(self.task, 'task') and isinstance(self.task.task, app.Task):
|
||||
return self._do_celery(args)
|
||||
else:
|
||||
raise TypeError('Method has no task attached')
|
||||
else:
|
||||
return self._do_sync(args)
|
||||
|
||||
def get_success_url(self, value):
|
||||
return self.success_url
|
||||
|
||||
def get_error_url(self):
|
||||
return self.error_url
|
||||
|
||||
def get_check_url(self, task_id, ajax):
|
||||
return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '')
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return self.http_method_not_allowed(request)
|
||||
|
||||
def get_result(self, request):
|
||||
from celery.result import AsyncResult
|
||||
res = AsyncResult(request.GET.get('async_id'))
|
||||
if 'ajax' in self.request.GET:
|
||||
data = {
|
||||
'async_id': res.id,
|
||||
'ready': res.ready()
|
||||
}
|
||||
if res.ready():
|
||||
if res.successful():
|
||||
smes = self.get_success_message(res.info)
|
||||
if smes:
|
||||
messages.success(self.request, smes)
|
||||
# TODO: Do not store message if the ajax client stats that it will not redirect
|
||||
# but handle the mssage itself
|
||||
data.update({
|
||||
'redirect': self.get_success_url(res.info),
|
||||
'message': self.get_success_message(res.info)
|
||||
})
|
||||
else:
|
||||
messages.error(self.request, self.get_error_message(res.info))
|
||||
# TODO: Do not store message if the ajax client stats that it will not redirect
|
||||
# but handle the mssage itself
|
||||
data.update({
|
||||
'redirect': self.get_error_url(),
|
||||
'message': self.get_error_message(res.info)
|
||||
})
|
||||
return JsonResponse(data)
|
||||
else:
|
||||
if res.ready():
|
||||
if res.successful():
|
||||
return self.success(res.info)
|
||||
else:
|
||||
return self.error(res.info)
|
||||
return render(request, 'pretixpresale/waiting.html')
|
||||
|
||||
def _do_celery(self, args):
|
||||
rs = self.task.task.apply_async(args=args)
|
||||
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
|
||||
return JsonResponse({
|
||||
'async_id': rs.id,
|
||||
'check_url': self.get_check_url(rs.id, True)
|
||||
})
|
||||
else:
|
||||
return redirect(self.get_check_url(rs.id, False))
|
||||
|
||||
def _do_sync(self, args):
|
||||
try:
|
||||
rs = getattr(self.__class__, 'task')(*args)
|
||||
return self.success(rs)
|
||||
except Exception as e:
|
||||
logger.exception('Error while executing task synchronously')
|
||||
return self.error(e)
|
||||
|
||||
def success(self, value):
|
||||
smes = self.get_success_message(value)
|
||||
if smes:
|
||||
messages.success(self.request, smes)
|
||||
if "ajax" in self.request.POST or "ajax" in self.request.GET:
|
||||
return JsonResponse({
|
||||
'ready': True,
|
||||
'redirect': self.get_success_url(value),
|
||||
'message': self.get_success_message(value)
|
||||
})
|
||||
return redirect(self.get_success_url(value))
|
||||
|
||||
def error(self, exception):
|
||||
messages.error(self.request, self.get_error_message(exception))
|
||||
if "ajax" in self.request.POST or "ajax" in self.request.GET:
|
||||
return JsonResponse({
|
||||
'ready': True,
|
||||
'redirect': self.get_error_url(),
|
||||
'message': self.get_error_message(exception)
|
||||
})
|
||||
return redirect(self.get_error_url())
|
||||
|
||||
def get_error_message(self, exception):
|
||||
return _('An unexpected error has occured')
|
||||
|
||||
def get_success_message(self, value):
|
||||
return _('The task has been completed')
|
||||
@@ -1,13 +1,15 @@
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import View
|
||||
|
||||
from pretix.base.services.cart import (
|
||||
CartError, add_items_to_cart, remove_items_from_cart,
|
||||
)
|
||||
from pretix.presale.views import EventViewMixin
|
||||
from pretix.presale.views.async import AsyncAction
|
||||
|
||||
|
||||
class CartActionMixin:
|
||||
@@ -15,18 +17,16 @@ class CartActionMixin:
|
||||
def get_next_url(self):
|
||||
if "next" in self.request.GET and '://' not in self.request.GET:
|
||||
return self.request.GET.get('next')
|
||||
elif "HTTP_REFERER" in self.request.META:
|
||||
return self.request.META.get('HTTP_REFERER')
|
||||
else:
|
||||
return reverse('presale:event.index', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
})
|
||||
|
||||
def get_success_url(self):
|
||||
def get_success_url(self, value=None):
|
||||
return self.get_next_url()
|
||||
|
||||
def get_failure_url(self):
|
||||
def get_error_url(self):
|
||||
return self.get_next_url()
|
||||
|
||||
def _items_from_post_data(self):
|
||||
@@ -61,27 +61,34 @@ class CartRemove(EventViewMixin, CartActionMixin, View):
|
||||
def post(self, *args, **kwargs):
|
||||
items = self._items_from_post_data()
|
||||
if not items:
|
||||
return redirect(self.get_failure_url())
|
||||
return redirect(self.get_error_url())
|
||||
|
||||
remove_items_from_cart(self.request.event.identity, items, self.request.session.session_key)
|
||||
messages.success(self.request, _('Your cart has been updated.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
class CartAdd(EventViewMixin, CartActionMixin, View):
|
||||
class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
task = add_items_to_cart
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def get_success_message(self, value):
|
||||
return _('The products have been successfully added to your cart.')
|
||||
|
||||
def get_error_message(self, exception):
|
||||
if isinstance(exception, dict) and exception['exc_type'] == 'CartError':
|
||||
return exception['exc_message']
|
||||
elif isinstance(exception, CartError):
|
||||
return str(exception)
|
||||
return super().get_error_message(exception)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
items = self._items_from_post_data()
|
||||
return self.process(items)
|
||||
|
||||
def process(self, items):
|
||||
try:
|
||||
add_items_to_cart(self.request.event.identity, items, self.request.session.session_key)
|
||||
messages.success(self.request, _('The products have been successfully added to your cart.'))
|
||||
return redirect(self.get_success_url())
|
||||
except CartError as e:
|
||||
messages.error(self.request, str(e))
|
||||
return redirect(self.get_failure_url())
|
||||
if items:
|
||||
return self.do(self.request.event.identity, items, self.request.session.session_key)
|
||||
else:
|
||||
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
|
||||
return JsonResponse({
|
||||
'redirect': self.get_error_url()
|
||||
})
|
||||
else:
|
||||
return redirect(self.get_error_url())
|
||||
|
||||
@@ -12,7 +12,7 @@ from pretix.presale.views import CartMixin
|
||||
class CheckoutView(CartMixin, View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
if not self.positions:
|
||||
if not self.positions and "async_id" not in request.GET:
|
||||
messages.error(request, _("Your cart is empty"))
|
||||
return redirect(reverse('presale:event.index', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
|
||||
@@ -107,7 +107,7 @@ class OrderPay(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
or not self.payment_provider.order_can_retry(self.order)
|
||||
or not self.payment_provider.is_enabled):
|
||||
messages.error(request, _('The payment for this order cannot be continued.'))
|
||||
return redirect(self.get_order_url())
|
||||
return redirect(self.get_order_url() + '?paid=yes')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -168,9 +168,30 @@ class OrderPayDo(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
ctx['payment_provider'] = self.payment_provider
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def form(self):
|
||||
return self.payment_provider.payment_form_render(self.request)
|
||||
def get_payment_url(self):
|
||||
return reverse('presale:event.order.pay', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
})
|
||||
|
||||
|
||||
class OrderPayComplete(EventViewMixin, OrderDetailMixin, View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
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)):
|
||||
messages.error(request, _('The payment information you entered was incomplete.'))
|
||||
return redirect(self.get_payment_url())
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
resp = self.payment_provider.payment_perform(request, self.order)
|
||||
return redirect(resp or self.get_order_url() + '?paid=yes')
|
||||
|
||||
def get_payment_url(self):
|
||||
return reverse('presale:event.order.pay', kwargs={
|
||||
|
||||
Reference in New Issue
Block a user