Enabled asynchronous cart/order actions

This commit is contained in:
Raphael Michel
2015-10-04 19:54:17 +02:00
parent 4c6b292968
commit c4638a3402
28 changed files with 572 additions and 196 deletions

View File

@@ -8,7 +8,7 @@ class PretixBaseConfig(AppConfig):
def ready(self): def ready(self):
from . import exporter # NOQA from . import exporter # NOQA
from . import payment # NOQA from . import payment # NOQA
from .services import export, mail, tickets # NOQA from .services import export, mail, tickets, cart, orders # NOQA
try: try:
from .celery import app as celery_app # NOQA from .celery import app as celery_app # NOQA

View File

@@ -251,7 +251,7 @@ class BasePaymentProvider:
""" """
After the user confirmed his purchase, this method will be called to complete After the user confirmed his purchase, this method will be called to complete
the payment process. This is the place to actually move the money, if applicable. the payment process. This is the place to actually move the money, if applicable.
If you need any speical behaviour, you can return a string If you need any special behaviour, you can return a string
containing an URL the user will be redirected to. If you are done with your process containing an URL the user will be redirected to. If you are done with your process
you should return the user to the order's detail page. you should return the user to the order's detail page.

View File

@@ -1,5 +1,6 @@
from datetime import timedelta from datetime import timedelta
from django.conf import settings
from django.db.models import Q from django.db.models import Q
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -16,7 +17,7 @@ class CartError(Exception):
error_messages = { error_messages = {
'busy': _('We were not able to process your request completely as the ' 'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'), 'server was too busy. Please try again.'),
'empty': _('You did not select any items.'), 'empty': _('You did not select any products.'),
'not_for_sale': _('You selected a product which is not available for sale.'), 'not_for_sale': _('You selected a product which is not available for sale.'),
'unavailable': _('Some of the products you selected were no longer available. ' 'unavailable': _('Some of the products you selected were no longer available. '
'Please see below for details.'), 'Please see below for details.'),
@@ -128,6 +129,27 @@ def _add_items(event, items, session, expiry):
return err return err
def _add_items_to_cart(event: Event, items: list, session: str=None):
with event.lock():
_check_date(event)
existing = CartPosition.objects.current.filter(Q(session=session) & Q(event=event)).count()
if sum(i[2] for i in items) + existing > int(event.settings.max_items_per_order):
# TODO: i18n plurals
raise CartError(error_messages['max_items'] % event.settings.max_items_per_order)
expiry = now() + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
_extend_existing(event, session, expiry)
expired = _re_add_expired_positions(items, event, session)
if not items:
raise CartError(error_messages['empty'])
err = _add_items(event, items, session, expiry)
_delete_expired(expired)
if err:
raise CartError(err)
def add_items_to_cart(event: str, items: list, session: str=None): def add_items_to_cart(event: str, items: list, session: str=None):
""" """
Adds a list of items to a user's cart. Adds a list of items to a user's cart.
@@ -138,24 +160,7 @@ def add_items_to_cart(event: str, items: list, session: str=None):
""" """
event = Event.objects.current.get(identity=event) event = Event.objects.current.get(identity=event)
try: try:
with event.lock(): return _add_items_to_cart(event, items, session)
_check_date(event)
existing = CartPosition.objects.current.filter(Q(session=session) & Q(event=event)).count()
if sum(i[2] for i in items) + existing > int(event.settings.max_items_per_order):
# TODO: i18n plurals
raise CartError(error_messages['max_items'] % event.settings.max_items_per_order)
expiry = now() + timedelta(minutes=event.settings.get('reservation_time', as_type=int))
_extend_existing(event, session, expiry)
expired = _re_add_expired_positions(items, event, session)
if not items:
raise CartError(error_messages['empty'])
err = _add_items(event, items, session, expiry)
_delete_expired(expired)
if err:
raise CartError(err)
except EventLock.LockTimeoutException: except EventLock.LockTimeoutException:
raise CartError(error_messages['busy']) raise CartError(error_messages['busy'])
@@ -177,3 +182,17 @@ def remove_items_from_cart(event: str, items: list, session: str=None):
cw &= Q(variation__isnull=True) cw &= Q(variation__isnull=True)
for cp in CartPosition.objects.current.filter(cw).order_by("-price")[:cnt]: for cp in CartPosition.objects.current.filter(cw).order_by("-price")[:cnt]:
cp.delete() cp.delete()
if settings.HAS_CELERY:
from pretix.celery import app
@app.task(bind=True, max_retries=5, default_retry_delay=2)
def add_items_to_cart_task(self, event: str, items: list, session: str):
event = Event.objects.current.get(identity=event)
try:
return _add_items_to_cart(event, items, session)
except EventLock.LockTimeoutException:
self.retry(exc=CartError(error_messages['busy']))
add_items_to_cart.task = add_items_to_cart_task

View File

@@ -1,13 +1,19 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.conf import settings
from django.db import transaction from django.db import transaction
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event, EventLock, Order, OrderPosition, Quota from pretix.base.models import (
CartPosition, Event, EventLock, Order, OrderPosition, Quota,
)
from pretix.base.payment import BasePaymentProvider from pretix.base.payment import BasePaymentProvider
from pretix.base.services.cart import CartError
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
from pretix.base.signals import order_paid, order_placed from pretix.base.signals import (
order_paid, order_placed, register_payment_providers,
)
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import build_absolute_uri
error_messages = { error_messages = {
@@ -18,6 +24,7 @@ error_messages = {
'price_changed': _('The price of some of the items in your cart has changed in the ' 'price_changed': _('The price of some of the items in your cart has changed in the '
'meantime. Please see below for details.'), 'meantime. Please see below for details.'),
'max_items': _("You cannot select more than %s items per order"), 'max_items': _("You cannot select more than %s items per order"),
'internal': _("An internal error occured, please try again."),
'busy': _('We were not able to process your request completely as the ' 'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'), 'server was too busy. Please try again.'),
} }
@@ -85,7 +92,7 @@ def _check_date(event):
raise OrderError(error_messages['ended']) raise OrderError(error_messages['ended'])
def check_positions(event: Event, dt: datetime, positions: list): def _check_positions(event: Event, dt: datetime, positions: list):
err = None err = None
_check_date(event) _check_date(event)
@@ -130,41 +137,9 @@ def check_positions(event: Event, dt: datetime, positions: list):
raise OrderError(err) raise OrderError(err)
def perform_order(event: Event, payment_provider: BasePaymentProvider, positions: list,
email: str=None, locale: str=None):
dt = now()
try:
with event.lock():
check_positions(event, dt, positions)
order = place_order(event, email, positions, dt, payment_provider,
locale=locale)
mail(
order.email, _('Your order: %(code)s') % {'code': order.code},
'pretixpresale/email/order_placed.txt',
{
'order': order,
'event': event,
'url': build_absolute_uri('presale:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret
}),
'payment': payment_provider.order_pending_mail_render(order)
},
event, locale=order.locale
)
return order
except EventLock.LockTimeoutException:
# Is raised when there are too many threads asking for event locks and we were
# unable to get one
raise OrderError(error_messages['busy'])
@transaction.atomic() @transaction.atomic()
def place_order(event: Event, email: str, positions: list, dt: datetime, def _create_order(event: Event, email: str, positions: list, dt: datetime,
payment_provider: BasePaymentProvider, locale: str=None): payment_provider: BasePaymentProvider, locale: str=None):
total = sum([c.price for c in positions]) total = sum([c.price for c in positions])
payment_fee = payment_provider.calculate_fee(total) payment_fee = payment_provider.calculate_fee(total)
total += payment_fee total += payment_fee
@@ -180,8 +155,72 @@ def place_order(event: Event, email: str, positions: list, dt: datetime,
locale=locale, locale=locale,
total=total, total=total,
payment_fee=payment_fee, payment_fee=payment_fee,
payment_provider=payment_provider.identifier, payment_provider=payment_provider.identifier
) )
OrderPosition.transform_cart_positions(positions, order) OrderPosition.transform_cart_positions(positions, order)
order_placed.send(event, order=order) order_placed.send(event, order=order)
return order return order
def _perform_order(event: Event, payment_provider: BasePaymentProvider, position_ids: list,
email: str, locale: str):
event = Event.objects.current.get(identity=event)
responses = register_payment_providers.send(event)
pprov = None
for receiver, response in responses:
provider = response(event)
if provider.identifier == payment_provider:
pprov = provider
if not pprov:
raise OrderError(error_messages['internal'])
dt = now()
with event.lock():
positions = list(CartPosition.objects.current.filter(
identity__in=position_ids).select_related('item', 'variation'))
if len(position_ids) != len(positions):
raise OrderError(error_messages['internal'])
_check_positions(event, dt, positions)
order = _create_order(event, email, positions, dt, pprov,
locale=locale)
mail(
order.email, _('Your order: %(code)s') % {'code': order.code},
'pretixpresale/email/order_placed.txt',
{
'order': order,
'event': event,
'url': build_absolute_uri('presale:event.order', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'order': order.code,
'secret': order.secret
}),
'payment': pprov.order_pending_mail_render(order)
},
event, locale=order.locale
)
return order.identity
def perform_order(event: str, payment_provider: str, positions: list,
email: str=None, locale: str=None):
try:
return _perform_order(event, payment_provider, positions, email, locale)
except EventLock.LockTimeoutException:
# Is raised when there are too many threads asking for event locks and we were
# unable to get one
raise OrderError(error_messages['busy'])
if settings.HAS_CELERY:
from pretix.celery import app
@app.task(bind=True, max_retries=5, default_retry_delay=2)
def perform_order_task(self, event: str, payment_provider: str, positions: list,
email: str=None, locale: str=None):
try:
return _perform_order(event, payment_provider, positions, email, locale)
except EventLock.LockTimeoutException:
self.retry(exc=OrderError(error_messages['busy']))
perform_order.task = perform_order_task

View File

@@ -181,7 +181,6 @@ class Paypal(BasePaymentProvider):
try: try:
mark_order_paid(order, 'paypal', json.dumps(payment.to_dict())) mark_order_paid(order, 'paypal', json.dumps(payment.to_dict()))
messages.success(request, _('We successfully received your payment. Thank you!'))
except Quota.QuotaExceededException as e: except Quota.QuotaExceededException as e:
messages.error(request, str(e)) messages.error(request, str(e))
return None return None

View File

@@ -23,9 +23,10 @@ def success(request):
request.session['payment_paypal_payer'] = payer request.session['payment_paypal_payer'] = payer
try: try:
event = Event.objects.current.get(identity=request.session['payment_paypal_event']) event = Event.objects.current.get(identity=request.session['payment_paypal_event'])
return redirect('presale:event.checkout.confirm', return redirect('presale:event.checkout',
event=event.slug, event=event.slug,
organizer=event.organizer.slug) organizer=event.organizer.slug,
step='confirm')
except Event.DoesNotExist: except Event.DoesNotExist:
pass # TODO: Handle this pass # TODO: Handle this
else: else:
@@ -37,9 +38,10 @@ def abort(request):
messages.error(request, _('It looks like you cancelled the PayPal payment')) messages.error(request, _('It looks like you cancelled the PayPal payment'))
try: try:
event = Event.objects.current.get(identity=request.session['payment_paypal_event']) event = Event.objects.current.get(identity=request.session['payment_paypal_event'])
return redirect('presale:event.checkout.payment', return redirect('presale:event.checkout',
event=event.slug, event=event.slug,
organizer=event.organizer.slug) organizer=event.organizer.slug,
step='payment')
except Event.DoesNotExist: except Event.DoesNotExist:
pass # TODO: Handle this pass # TODO: Handle this
@@ -104,4 +106,5 @@ def retry(request, order):
return redirect('presale:event.order', return redirect('presale:event.order',
event=order.event.slug, event=order.event.slug,
organizer=order.event.organizer.slug, organizer=order.event.organizer.slug,
order=order.code) order=order.code,
secret=order.secret) + '?paid=yes'

View File

@@ -116,7 +116,6 @@ class Stripe(BasePaymentProvider):
if charge.status == 'succeeded' and charge.paid: if charge.status == 'succeeded' and charge.paid:
try: try:
mark_order_paid(order, 'stripe', str(charge)) mark_order_paid(order, 'stripe', str(charge))
messages.success(request, _('We successfully received your payment. Thank you!'))
except Quota.QuotaExceededException as e: except Quota.QuotaExceededException as e:
messages.error(request, str(e)) messages.error(request, str(e))
else: else:

View File

@@ -20,7 +20,7 @@ def html_head_presale(sender, request=None, **kwargs):
provider = Stripe(sender) provider = Stripe(sender)
url = resolve(request.path_info) url = resolve(request.path_info)
if provider.is_enabled and ("checkout.payment" in url.url_name or "order.pay" in url.url_name): if provider.is_enabled and ("checkout" in url.url_name or "order.pay" in url.url_name):
template = get_template('pretixplugins/stripe/presale_head.html') template = get_template('pretixplugins/stripe/presale_head.html')
ctx = Context({'event': sender, 'settings': provider.settings}) ctx = Context({'event': sender, 'settings': provider.settings})
return template.render(ctx) return template.render(ctx)

View File

@@ -1,3 +1,4 @@
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse 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.utils.translation import ugettext_lazy as _
from django.views.generic.base import TemplateResponseMixin 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.services.orders import OrderError, perform_order
from pretix.base.signals import register_payment_providers from pretix.base.signals import register_payment_providers
from pretix.presale.forms.checkout import ContactForm from pretix.presale.forms.checkout import ContactForm
from pretix.presale.signals import checkout_flow_steps from pretix.presale.signals import checkout_flow_steps
from pretix.presale.views import CartMixin from pretix.presale.views import CartMixin
from pretix.presale.views.async import AsyncAction
from pretix.presale.views.questions import QuestionsViewMixin from pretix.presale.views.questions import QuestionsViewMixin
@@ -278,10 +280,11 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
return True return True
class ConfirmStep(CartMixin, TemplateFlowStep): class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
priority = 1001 priority = 1001
identifier = "confirm" identifier = "confirm"
template_name = "pretixpresale/event/checkout_confirm.html" template_name = "pretixpresale/event/checkout_confirm.html"
task = perform_order
def is_applicable(self, request): def is_applicable(self, request):
return True return True
@@ -304,28 +307,47 @@ class ConfirmStep(CartMixin, TemplateFlowStep):
if provider.identifier == self.request.session['payment']: if provider.identifier == self.request.session['payment']:
return provider 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): def post(self, request):
self.request = request self.request = request
try: return self.do(self.request.event.identity, self.payment_provider.identifier,
order = perform_order(self.request.event, self.payment_provider, self.positions, [p.identity for p in self.positions], request.session.get('email'),
email=request.session.get('email', None), translation.get_language())
locale=translation.get_language())
except OrderError as e: def get_success_message(self, value):
messages.error(request, str(e)) return None
return redirect(self.get_step_url())
else: def success(self, value):
# Message is delivered via GET parameter # Message is delivered via GET parameter
# messages.success(request, _('Your order has been placed.')) # messages.success(request, _('Your order has been placed.'))
resp = self.payment_provider.payment_perform(request, order) return redirect(self.get_success_url(value))
return redirect(resp or self.get_order_url(order))
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): 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, 'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug, 'organizer': self.request.event.organizer.slug,
'order': order.code, 'order': order.code,
'secret': order.secret 'secret': order.secret
}) + '?thanks=yes' })
DEFAULT_FLOW = ( DEFAULT_FLOW = (

View File

@@ -13,6 +13,7 @@
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script> <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 "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/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/asynctask.js" %}"></script>
{% endcompress %} {% endcompress %}
{{ html_head|safe }} {{ html_head|safe }}
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -63,6 +64,16 @@
{% endblocktrans %} {% endblocktrans %}
{% endwith %} {% endwith %}
</footer> </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 %} {% compress js %}
<script type="text/javascript" src="{% static "lightbox/js/lightbox.min.js" %}"></script> <script type="text/javascript" src="{% static "lightbox/js/lightbox.min.js" %}"></script>
{% endcompress %} {% endcompress %}

View File

@@ -5,7 +5,7 @@
{% block content %} {% block content %}
<h2>{% trans "Confirm order" %}</h2> <h2>{% trans "Confirm order" %}</h2>
<p>{% trans "Please review the details below and confirm your order." %}</p> <p>{% trans "Please review the details below and confirm your order." %}</p>
<form method="post"> <form method="post" data-asynctask>
{% csrf_token %} {% csrf_token %}
<div class="panel panel-primary cart"> <div class="panel panel-primary cart">
<div class="panel-heading"> <div class="panel-heading">

View File

@@ -37,7 +37,7 @@
{{ line.count }} {{ line.count }}
{% if editable %} {% if editable %}
<form action="{% url "presale:event.cart.add" event=event.slug organizer=event.organizer.slug %}" <form action="{% url "presale:event.cart.add" event=event.slug organizer=event.organizer.slug %}"
method="post"> method="post" data-asynctask>
{% csrf_token %} {% csrf_token %}
{% if line.variation %} {% if line.variation %}
<input type="hidden" name="variation_{{ line.item.identity }}_{{ line.variation.identity }}" <input type="hidden" name="variation_{{ line.item.identity }}_{{ line.variation.identity }}"

View File

@@ -50,7 +50,7 @@
</div> </div>
{% endif %} {% endif %}
{% if event.presale_is_running or event.settings.show_items_outside_presale_period %} {% 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 }}"> action="{% url "presale:event.cart.add" organizer=request.event.organizer.slug event=request.event.slug %}?next={{ request.path|urlencode }}">
{% csrf_token %} {% csrf_token %}
{% for tup in items_by_category %} {% for tup in items_by_category %}

View File

@@ -3,11 +3,16 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% block title %}{% trans "Order details" %}{% endblock %} {% block title %}{% trans "Order details" %}{% endblock %}
{% block content %} {% block content %}
{% if "thanks" in request.GET %} {% if "thanks" in request.GET or "paid" in request.GET %}
<div class="thank-you"> <div class="thank-you">
<span class="fa fa-check-circle"></span> <span class="fa fa-check-circle"></span>
<h2>{% trans "Thank you!" %}</h2> <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> <p>{% trans "We also sent you an email with a link to this page if you want to come back later." %}</p>
</div> </div>
{% endif %} {% endif %}
@@ -18,7 +23,7 @@
{% include "pretixpresale/event/fragment_order_status.html" with order=order class="pull-right" %} {% include "pretixpresale/event/fragment_order_status.html" with order=order class="pull-right" %}
<div class="clearfix"></div> <div class="clearfix"></div>
</h2> </h2>
{% if order.status == "n" %} {% if order.status == "n" %}
<div class="panel panel-danger"> <div class="panel panel-danger">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title"> <h3 class="panel-title">
@@ -28,12 +33,14 @@
<div class="panel-body"> <div class="panel-body">
{% if can_retry %} {% 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 %}" <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 %} {% endif %}
{{ payment }} {{ payment }}
<strong>{% blocktrans trimmed with date=order.expires|date:"SHORT_DATE_FORMAT" %} <strong>{% blocktrans trimmed with date=order.expires|date:"SHORT_DATE_FORMAT" %}
Please complete your payment before {{ date }} Please complete your payment before {{ date }}
{% endblocktrans %}</strong> {% endblocktrans %}</strong>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
</div> </div>
@@ -88,7 +95,7 @@
<div class="col-md-12 text-right"> <div class="col-md-12 text-right">
<p> <p>
<a href="{% url 'presale:event.order.cancel' event=request.event.slug organizer=request.event.organizer.slug secret=order.secret order=order.code %}" <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> <span class="fa fa-remove"></span>
{% trans "Cancel order" %} {% trans "Cancel order" %}
</a> </a>

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

View File

@@ -27,6 +27,9 @@ urlpatterns = [
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/confirm$', url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/confirm$',
pretix.presale.views.order.OrderPayDo.as_view(), pretix.presale.views.order.OrderPayDo.as_view(),
name='event.order.pay.confirm'), 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>[^/]+)$', url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$',
pretix.presale.views.order.OrderDownload.as_view(), pretix.presale.views.order.OrderDownload.as_view(),
name='event.order.download'), name='event.order.download'),

View 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')

View File

@@ -1,13 +1,15 @@
from django.contrib import messages from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import JsonResponse
from django.shortcuts import redirect 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 django.views.generic import View
from pretix.base.services.cart import ( from pretix.base.services.cart import (
CartError, add_items_to_cart, remove_items_from_cart, CartError, add_items_to_cart, remove_items_from_cart,
) )
from pretix.presale.views import EventViewMixin from pretix.presale.views import EventViewMixin
from pretix.presale.views.async import AsyncAction
class CartActionMixin: class CartActionMixin:
@@ -15,18 +17,16 @@ class CartActionMixin:
def get_next_url(self): def get_next_url(self):
if "next" in self.request.GET and '://' not in self.request.GET: if "next" in self.request.GET and '://' not in self.request.GET:
return self.request.GET.get('next') return self.request.GET.get('next')
elif "HTTP_REFERER" in self.request.META:
return self.request.META.get('HTTP_REFERER')
else: else:
return reverse('presale:event.index', kwargs={ return reverse('presale:event.index', kwargs={
'event': self.request.event.slug, 'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug, 'organizer': self.request.event.organizer.slug,
}) })
def get_success_url(self): def get_success_url(self, value=None):
return self.get_next_url() return self.get_next_url()
def get_failure_url(self): def get_error_url(self):
return self.get_next_url() return self.get_next_url()
def _items_from_post_data(self): def _items_from_post_data(self):
@@ -61,27 +61,34 @@ class CartRemove(EventViewMixin, CartActionMixin, View):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
items = self._items_from_post_data() items = self._items_from_post_data()
if not items: 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) remove_items_from_cart(self.request.event.identity, items, self.request.session.session_key)
messages.success(self.request, _('Your cart has been updated.')) messages.success(self.request, _('Your cart has been updated.'))
return redirect(self.get_success_url()) 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): def get_success_message(self, value):
super().__init__(*args, **kwargs) 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): def post(self, request, *args, **kwargs):
items = self._items_from_post_data() items = self._items_from_post_data()
return self.process(items) if items:
return self.do(self.request.event.identity, items, self.request.session.session_key)
def process(self, items): else:
try: if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
add_items_to_cart(self.request.event.identity, items, self.request.session.session_key) return JsonResponse({
messages.success(self.request, _('The products have been successfully added to your cart.')) 'redirect': self.get_error_url()
return redirect(self.get_success_url()) })
except CartError as e: else:
messages.error(self.request, str(e)) return redirect(self.get_error_url())
return redirect(self.get_failure_url())

View File

@@ -12,7 +12,7 @@ from pretix.presale.views import CartMixin
class CheckoutView(CartMixin, View): class CheckoutView(CartMixin, View):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.request = request 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")) messages.error(request, _("Your cart is empty"))
return redirect(reverse('presale:event.index', kwargs={ return redirect(reverse('presale:event.index', kwargs={
'organizer': self.request.event.organizer.slug, 'organizer': self.request.event.organizer.slug,

View File

@@ -107,7 +107,7 @@ class OrderPay(EventViewMixin, OrderDetailMixin, TemplateView):
or not self.payment_provider.order_can_retry(self.order) or not self.payment_provider.order_can_retry(self.order)
or not self.payment_provider.is_enabled): or not self.payment_provider.is_enabled):
messages.error(request, _('The payment for this order cannot be continued.')) 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) return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -168,9 +168,30 @@ class OrderPayDo(EventViewMixin, OrderDetailMixin, TemplateView):
ctx['payment_provider'] = self.payment_provider ctx['payment_provider'] = self.payment_provider
return ctx return ctx
@cached_property def get_payment_url(self):
def form(self): return reverse('presale:event.order.pay', kwargs={
return self.payment_provider.payment_form_render(self.request) '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): def get_payment_url(self):
return reverse('presale:event.order.pay', kwargs={ return reverse('presale:event.order.pay', kwargs={

View File

@@ -0,0 +1,65 @@
var async_task_id = null;
var async_task_timeout = null;
var async_task_check_url = null;
$(function () {
$("body").on('submit', 'form[data-asynctask]', function (e) {
e.preventDefault();
if ($(this).data('ajaxing')) return;
$(this).data('ajaxing', true);
waitingDialog.show(default_loading_message);
$.ajax(
{
'type': 'POST',
'url': $(this).attr('action'),
'data': $(this).serialize() + '&ajax=1',
'success': async_task_callback,
'error': async_task_error,
'context': this,
'dataType': 'json'
}
);
});
});
function async_task_check() {
$.ajax(
{
'type': 'GET',
'url': async_task_check_url,
'success': async_task_check_callback,
'error': async_task_error,
'context': this,
'dataType': 'json'
}
);
}
function async_task_check_callback(data, jqXHR, status) {
if (data.ready && data.redirect) {
location.href = data.redirect;
return;
}
async_task_timeout = window.setTimeout(async_task_check, 500);
}
function async_task_callback(data, jqXHR, status) {
$(this).data('ajaxing', false);
if (data.redirect) {
location.href = data.redirect;
return;
}
async_task_id = data.async_id;
async_task_check_url = data.check_url;
async_task_timeout = window.setTimeout(async_task_check, 500);
}
function async_task_error(jqXHR, textStatus, errorThrown) {
waitingDialog.hide();
// TODO
// if(jqXHR.status == 500) {
// } if(jqXHR.status == 403) {
// } if(jqXHR.status == 503) {
// }
}

View File

@@ -13,62 +13,15 @@ $(function () {
$(".collapsed").removeClass("collapsed").addClass("collapse"); $(".collapsed").removeClass("collapsed").addClass("collapse");
}); });
/**
* Module for displaying "Waiting for..." dialog using Bootstrap
*
* @author Eugene Maslovich <ehpc@em42.ru>
* MIT License
*/
var waitingDialog = (function ($) { var waitingDialog = (function ($) {
// Creating modal dialog's DOM
var $dialog = $(
'<div class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1" role="dialog" aria-hidden="true" style="padding-top:15%; overflow-y:visible;">' +
'<div class="modal-dialog modal-m">' +
'<div class="modal-content">' +
'<div class="modal-header"><h3 style="margin:0;"></h3></div>' +
'<div class="modal-body">' +
'<div class="progress progress-striped active" style="margin-bottom:0;"><div class="progress-bar" style="width: 100%"></div></div>' +
'</div>' +
'</div></div></div>');
return { return {
/**
* Opens our dialog
* @param message Custom message
* @param options Custom options:
* options.dialogSize - bootstrap postfix for dialog size, e.g. "sm", "m";
* options.progressType - bootstrap postfix for progress bar type, e.g. "success", "warning".
*/
show: function (message, options) { show: function (message, options) {
// Assigning defaults $("#loadingmodal h1").html(message);
var settings = $.extend({ $("body").addClass("loading");
dialogSize: 'm',
progressType: ''
}, options);
if (typeof message === 'undefined') {
message = 'Loading';
}
if (typeof options === 'undefined') {
options = {};
}
// Configuring dialog
$dialog.find('.modal-dialog').attr('class', 'modal-dialog').addClass('modal-' + settings.dialogSize);
$dialog.find('.progress-bar').attr('class', 'progress-bar');
if (settings.progressType) {
$dialog.find('.progress-bar').addClass('progress-bar-' + settings.progressType);
}
$dialog.find('h3').text(message);
// Opening dialog
$dialog.modal();
}, },
/**
* Closes dialog
*/
hide: function () { hide: function () {
$dialog.modal('hide'); $("body").removeClass("loading");
} }
} }

View File

@@ -56,6 +56,44 @@ a:hover .panel-primary > .panel-heading {
color: @brand-success; color: @brand-success;
} }
} }
body.loading .container {
-webkit-filter: blur(2px);
-moz-filter: blur(2px);
-ms-filter: blur(2px);
-o-filter: blur(2px);
filter: blur(2px);
}
#loadingmodal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(255, 255, 255, .7);
opacity: 0;
text-align: center;
z-index: 900000;
visibility: hidden;
.big-rotating-icon {
margin-top: 50px;
-webkit-animation: fa-spin 8s infinite linear;
animation: fa-spin 8s infinite linear;
font-size: 200px;
color: @brand-primary;
}
}
.loading #loadingmodal {
opacity: 1;
visibility: visible;
transition: opacity .5s ease-in-out;
-moz-transition: opacity .5s ease-in-out;
-webkit-transition: opacity .5s ease-in-out;
}
@media (min-width: @screen-md-min) { @media (min-width: @screen-md-min) {
.thank-you { .thank-you {
height: 170px; height: 170px;

View File

@@ -0,0 +1,19 @@
@import "../../bootstrap/less/bootstrap.less";
@import "../../fontawesome/less/font-awesome.less";
@import "../../pretixbase/less/colors.less";
@fa-font-path: "../../fontawesome/fonts";
body {
background: #ececec;
text-align: center;
padding: 50px 0;
}
.big-rotating-icon {
margin-top: 50px;
-webkit-animation: fa-spin 8s infinite linear;
animation: fa-spin 8s infinite linear;
font-size: 200px;
color: @brand-primary;
}

View File

@@ -5,7 +5,7 @@ from django.utils.timezone import now
from pretix.base.models import Event, Organizer from pretix.base.models import Event, Organizer
from pretix.base.payment import FreeOrderProvider from pretix.base.payment import FreeOrderProvider
from pretix.base.services.orders import place_order from pretix.base.services.orders import _create_order
@pytest.fixture @pytest.fixture
@@ -22,9 +22,9 @@ def event():
def test_expiry_days(event): def test_expiry_days(event):
today = now() today = now()
event.settings.set('payment_term_days', 5) event.settings.set('payment_term_days', 5)
order = place_order(event, email='dummy@example.org', positions=[], order = _create_order(event, email='dummy@example.org', positions=[],
dt=today, payment_provider=FreeOrderProvider(event), dt=today, payment_provider=FreeOrderProvider(event),
locale='de') locale='de')
assert (order.expires - today).days == 5 assert (order.expires - today).days == 5
@@ -33,12 +33,12 @@ def test_expiry_last(event):
today = now() today = now()
event.settings.set('payment_term_days', 5) event.settings.set('payment_term_days', 5)
event.settings.set('payment_term_last', now() + timedelta(days=3)) event.settings.set('payment_term_last', now() + timedelta(days=3))
order = place_order(event, email='dummy@example.org', positions=[], order = _create_order(event, email='dummy@example.org', positions=[],
dt=today, payment_provider=FreeOrderProvider(event), dt=today, payment_provider=FreeOrderProvider(event),
locale='de') locale='de')
assert (order.expires - today).days == 3 assert (order.expires - today).days == 3
event.settings.set('payment_term_last', now() + timedelta(days=7)) event.settings.set('payment_term_last', now() + timedelta(days=7))
order = place_order(event, email='dummy@example.org', positions=[], order = _create_order(event, email='dummy@example.org', positions=[],
dt=today, payment_provider=FreeOrderProvider(event), dt=today, payment_provider=FreeOrderProvider(event),
locale='de') locale='de')
assert (order.expires - today).days == 5 assert (order.expires - today).days == 5

View File

@@ -217,23 +217,20 @@ class CartTest(CartTestMixin, TestCase):
self.assertGreater(cp.expires, now()) self.assertGreater(cp.expires, now())
def test_renew_expired_successfully(self): def test_renew_expired_successfully(self):
CartPosition.objects.create( cp1 = CartPosition.objects.create(
event=self.event, session=self.session_key, item=self.ticket, event=self.event, session=self.session_key, item=self.ticket,
price=23, expires=now() - timedelta(minutes=10) price=23, expires=now() - timedelta(minutes=10)
) )
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'variation_%s_%s' % (self.shirt.identity, self.shirt_red.identity): '1'
}, follow=True) }, follow=True)
objs = list(CartPosition.objects.current.filter(session=self.session_key, event=self.event)) obj = CartPosition.objects.current.get(identity=cp1.identity)
self.assertEqual(len(objs), 1) self.assertEqual(obj.item, self.ticket)
self.assertEqual(objs[0].item, self.ticket) self.assertIsNone(obj.variation)
self.assertIsNone(objs[0].variation) self.assertEqual(obj.price, 23)
self.assertEqual(objs[0].price, 23) self.assertGreater(obj.expires, now())
self.assertGreater(objs[0].expires, now())
def test_renew_questions(self): def test_renew_questions(self):
"""
Currently fails. See: https://github.com/pretix/pretix/issues/20
"""
cr1 = CartPosition.objects.create( cr1 = CartPosition.objects.create(
event=self.event, session=self.session_key, item=self.ticket, event=self.event, session=self.session_key, item=self.ticket,
price=23, expires=now() - timedelta(minutes=10) price=23, expires=now() - timedelta(minutes=10)
@@ -247,23 +244,24 @@ class CartTest(CartTestMixin, TestCase):
cartposition=cr1, question=q1, answer='23' cartposition=cr1, question=q1, answer='23'
)) ))
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_' + self.ticket.identity: '1',
}, follow=True) }, follow=True)
objs = list(CartPosition.objects.current.filter(session=self.session_key, event=self.event)) obj = CartPosition.objects.current.get(identity=cr1.identity)
self.assertEqual(len(objs), 1) self.assertEqual(obj.answers.get(question=q1).answer, '23')
self.assertEqual(objs[0].answers.get(question=q1).answer, '23')
def test_renew_expired_failed(self): def test_renew_expired_failed(self):
self.quota_tickets.size = 0 self.quota_tickets.size = 0
self.quota_tickets.save() self.quota_tickets.save()
CartPosition.objects.create( cp1 = CartPosition.objects.create(
event=self.event, session=self.session_key, item=self.ticket, event=self.event, session=self.session_key, item=self.ticket,
price=23, expires=now() - timedelta(minutes=10) price=23, expires=now() - timedelta(minutes=10)
) )
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_' + self.ticket.identity: '1',
}, follow=True) }, follow=True)
doc = BeautifulSoup(response.rendered_content) doc = BeautifulSoup(response.rendered_content)
self.assertIn('no longer available', doc.select('.alert-danger')[0].text) self.assertIn('no longer available', doc.select('.alert-danger')[0].text)
self.assertFalse(CartPosition.objects.current.filter(session=self.session_key, event=self.event).exists()) self.assertFalse(CartPosition.objects.current.filter(identity=cp1.identity).exists())
def test_restriction_ok(self): def test_restriction_ok(self):
self.event.plugins = 'tests.testdummy' self.event.plugins = 'tests.testdummy'

View File

@@ -12,7 +12,6 @@ from pretix.base.models import (
class EventTestMixin: class EventTestMixin:
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.orga = Organizer.objects.create(name='CCC', slug='ccc') self.orga = Organizer.objects.create(name='CCC', slug='ccc')
@@ -23,7 +22,6 @@ class EventTestMixin:
class EventMiddlewareTest(EventTestMixin, BrowserTest): class EventMiddlewareTest(EventTestMixin, BrowserTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.driver.implicitly_wait(10) self.driver.implicitly_wait(10)
@@ -38,7 +36,6 @@ class EventMiddlewareTest(EventTestMixin, BrowserTest):
class ItemDisplayTest(EventTestMixin, BrowserTest): class ItemDisplayTest(EventTestMixin, BrowserTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.driver.implicitly_wait(10) self.driver.implicitly_wait(10)
@@ -141,6 +138,11 @@ class ItemDisplayTest(EventTestMixin, BrowserTest):
class DeadlineTest(EventTestMixin, TestCase): class DeadlineTest(EventTestMixin, TestCase):
def setUp(self):
super().setUp()
q = Quota.objects.create(event=self.event, name='Quota', size=2)
self.item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=False)
q.items.add(self.item)
def test_not_yet_started(self): def test_not_yet_started(self):
self.event.presale_start = now() + datetime.timedelta(days=1) self.event.presale_start = now() + datetime.timedelta(days=1)
@@ -153,6 +155,9 @@ class DeadlineTest(EventTestMixin, TestCase):
self.assertNotIn('checkout-button-row', response.rendered_content) self.assertNotIn('checkout-button-row', response.rendered_content)
response = self.client.post( response = self.client.post(
'/%s/%s/cart/add' % (self.orga.slug, self.event.slug), '/%s/%s/cart/add' % (self.orga.slug, self.event.slug),
{
'item_' + self.item.identity: '1',
},
follow=True follow=True
) )
self.assertIn('alert-danger', response.rendered_content) self.assertIn('alert-danger', response.rendered_content)
@@ -169,6 +174,9 @@ class DeadlineTest(EventTestMixin, TestCase):
self.assertNotIn('checkout-button-row', response.rendered_content) self.assertNotIn('checkout-button-row', response.rendered_content)
response = self.client.post( response = self.client.post(
'/%s/%s/cart/add' % (self.orga.slug, self.event.slug), '/%s/%s/cart/add' % (self.orga.slug, self.event.slug),
{
'item_' + self.item.identity: '1'
},
follow=True follow=True
) )
self.assertIn('alert-danger', response.rendered_content) self.assertIn('alert-danger', response.rendered_content)
@@ -185,7 +193,10 @@ class DeadlineTest(EventTestMixin, TestCase):
self.assertNotIn('alert-info', response.rendered_content) self.assertNotIn('alert-info', response.rendered_content)
self.assertIn('checkout-button-row', response.rendered_content) self.assertIn('checkout-button-row', response.rendered_content)
response = self.client.post( response = self.client.post(
'/%s/%s/cart/add' % (self.orga.slug, self.event.slug) '/%s/%s/cart/add' % (self.orga.slug, self.event.slug),
{
'item_' + self.item.identity: '1'
}
) )
self.assertNotEqual(response.status_code, 403) self.assertNotEqual(response.status_code, 403)
@@ -200,6 +211,9 @@ class DeadlineTest(EventTestMixin, TestCase):
self.assertNotIn('alert-info', response.rendered_content) self.assertNotIn('alert-info', response.rendered_content)
self.assertIn('checkout-button-row', response.rendered_content) self.assertIn('checkout-button-row', response.rendered_content)
response = self.client.post( response = self.client.post(
'/%s/%s/cart/add' % (self.orga.slug, self.event.slug) '/%s/%s/cart/add' % (self.orga.slug, self.event.slug),
{
'item_' + self.item.identity: '1'
}
) )
self.assertNotEqual(response.status_code, 403) self.assertNotEqual(response.status_code, 403)

View File

@@ -7,7 +7,7 @@ from django.utils.timezone import now
from pretix.base.models import ( from pretix.base.models import (
Event, Item, ItemCategory, ItemVariation, Order, OrderPosition, Organizer, Event, Item, ItemCategory, ItemVariation, Order, OrderPosition, Organizer,
Property, PropertyValue, Question, Quota, User, Property, PropertyValue, Question, Quota,
) )