forked from CGM_Public/pretix_original
Enabled asynchronous cart/order actions
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 }}"
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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$',
|
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'),
|
||||||
|
|||||||
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.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())
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
65
src/static/pretixpresale/js/ui/asynctask.js
Normal file
65
src/static/pretixpresale/js/ui/asynctask.js
Normal 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) {
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
19
src/static/pretixpresale/less/waiting.less
Normal file
19
src/static/pretixpresale/less/waiting.less
Normal 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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user