mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
* Vendor vue.js * Refactor item_group_by_category to support vouchers * Widget: Show product list * Widget: free prices * Widget: pictures and loading indicator * Widget: First iframe steps * Widget: Do not rerender iframe * Widget: Error handling * Improve widget * Widget: localization tech * Fix invoice style * Voucher attribute and waiting list * Add some iframe chrome * First step to namespaced carts * More isolation steps * More cart isolation things * More cart isolation things * Mobile stuff * Show cart on checkout pages * PayPal and Stripe support * Enable downloads * Locale handling * change text "save URL to this exact page" * Widget: voucher redemption * Widget: CSS * CSS: Responsive * Widget: CSS improvements * Widget: Add embedding code generator * Widget: Error messages and SSL check * First tests * Widget: tests * Don't use IDs in widgets * Widget: static files caching
This commit is contained in:
@@ -70,20 +70,26 @@ class BaseCheckoutFlowStep:
|
||||
def post(self, request):
|
||||
return HttpResponseNotAllowed([])
|
||||
|
||||
def get_step_url(self):
|
||||
return eventreverse(self.event, 'presale:event.checkout', kwargs={'step': self.identifier})
|
||||
def get_step_url(self, request):
|
||||
kwargs = {'step': self.identifier}
|
||||
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
|
||||
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
|
||||
return eventreverse(self.event, 'presale:event.checkout', kwargs=kwargs)
|
||||
|
||||
def get_prev_url(self, request):
|
||||
prev = self.get_prev_applicable(request)
|
||||
if not prev:
|
||||
return eventreverse(self.event, 'presale:event.index')
|
||||
kwargs = {}
|
||||
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
|
||||
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
|
||||
return eventreverse(self.request.event, 'presale:event.index', kwargs=kwargs)
|
||||
else:
|
||||
return prev.get_step_url()
|
||||
return prev.get_step_url(request)
|
||||
|
||||
def get_next_url(self, request):
|
||||
n = self.get_next_applicable(request)
|
||||
if n:
|
||||
return n.get_step_url()
|
||||
return n.get_step_url(request)
|
||||
|
||||
@cached_property
|
||||
def cart_session(self):
|
||||
@@ -225,6 +231,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['forms'] = self.forms
|
||||
ctx['cart'] = self.get_cart()
|
||||
return ctx
|
||||
|
||||
def get_success_message(self, value):
|
||||
@@ -234,7 +241,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
return self.get_next_url(self.request)
|
||||
|
||||
def get_error_url(self):
|
||||
return self.get_step_url()
|
||||
return self.get_step_url(self.request)
|
||||
|
||||
def get(self, request):
|
||||
self.request = request
|
||||
@@ -382,6 +389,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
ctx['contact_form'] = self.contact_form
|
||||
ctx['invoice_form'] = self.invoice_form
|
||||
ctx['reverse_charge_relevant'] = self.eu_reverse_charge_relevant
|
||||
ctx['cart'] = self.get_cart()
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -436,6 +444,7 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
ctx['selected'] = self.request.POST.get('payment', self.cart_session.get('payment', ''))
|
||||
if len(self.provider_forms) == 1:
|
||||
ctx['selected'] = self.provider_forms[0]['provider'].identifier
|
||||
ctx['cart'] = self.get_cart()
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
@@ -557,7 +566,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
return super().get_error_message(exception)
|
||||
|
||||
def get_error_url(self):
|
||||
return self.get_step_url()
|
||||
return self.get_step_url(self.request)
|
||||
|
||||
def get_order_url(self, order):
|
||||
return eventreverse(self.request.event, 'presale:event.order.pay.complete', kwargs={
|
||||
|
||||
@@ -56,6 +56,9 @@ def contextprocessor(request):
|
||||
ctx['event'] = request.event
|
||||
ctx['languages'] = [get_language_info(code) for code in request.event.settings.locales]
|
||||
|
||||
if request.resolver_match:
|
||||
ctx['cart_namespace'] = request.resolver_match.kwargs.get('cart_namespace', '')
|
||||
|
||||
if hasattr(request, 'organizer'):
|
||||
if request.organizer.settings.presale_css_file and not hasattr(request, 'event'):
|
||||
ctx['css_file'] = default_storage.url(request.organizer.settings.presale_css_file)
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
import hashlib
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from pretix.base.models import Event_SettingsStore, Organizer_SettingsStore
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.presale.views.widget import generate_widget_js
|
||||
|
||||
from ...style import regenerate_css, regenerate_organizer_css
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Re-generate all custom stylesheets"
|
||||
help = "Re-generate all custom stylesheets and scripts"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for es in Event_SettingsStore.objects.filter(key="presale_css_file"):
|
||||
regenerate_css.apply_async(args=(es.object_id,))
|
||||
|
||||
for es in Organizer_SettingsStore.objects.filter(key="presale_css_file"):
|
||||
regenerate_organizer_css.apply_async(args=(es.object_id,))
|
||||
|
||||
gs = GlobalSettingsObject()
|
||||
for lc, ll in settings.LANGUAGES:
|
||||
data = generate_widget_js(lc).encode()
|
||||
checksum = hashlib.sha1(data).hexdigest()
|
||||
fname = gs.settings.get('widget_file_{}'.format(lc))
|
||||
if not fname or gs.settings.get('widget_checksum_{}'.format(lc), '') != checksum:
|
||||
newname = default_storage.save(
|
||||
'widget/widget.{}.{}.js'.format(lc, checksum),
|
||||
ContentFile(data)
|
||||
)
|
||||
gs.settings.set('widget_file_{}'.format(lc), 'file://' + newname)
|
||||
gs.settings.set('widget_checksum_{}'.format(lc), checksum)
|
||||
if fname:
|
||||
default_storage.delete(fname)
|
||||
|
||||
@@ -20,7 +20,7 @@ logger = logging.getLogger('pretix.presale.style')
|
||||
affected_keys = ['primary_font', 'primary_color']
|
||||
|
||||
|
||||
def compile_scss(object):
|
||||
def compile_scss(object, file="main.scss", fonts=True):
|
||||
sassdir = os.path.join(settings.STATIC_ROOT, 'pretixpresale/scss')
|
||||
|
||||
def static(path):
|
||||
@@ -41,14 +41,14 @@ def compile_scss(object):
|
||||
sassrules.append('$brand-primary: {};'.format(object.settings.get('primary_color')))
|
||||
|
||||
font = object.settings.get('primary_font')
|
||||
if font != 'Open Sans':
|
||||
if font != 'Open Sans' and fonts:
|
||||
sassrules.append(get_font_stylesheet(font))
|
||||
sassrules.append(
|
||||
'$font-family-sans-serif: "{}", "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !default'.format(
|
||||
font
|
||||
))
|
||||
|
||||
sassrules.append('@import "main.scss";')
|
||||
sassrules.append('@import "{}";'.format(file))
|
||||
|
||||
cf = dict(django_libsass.CUSTOM_FUNCTIONS)
|
||||
cf['static'] = static
|
||||
@@ -64,32 +64,46 @@ def compile_scss(object):
|
||||
@app.task(base=ProfiledTask)
|
||||
def regenerate_css(event_id: int):
|
||||
event = Event.objects.select_related('organizer').get(pk=event_id)
|
||||
css, checksum = compile_scss(event)
|
||||
|
||||
fname = '{}/{}/presale.{}.css'.format(
|
||||
event.organizer.slug, event.slug, checksum[:16]
|
||||
)
|
||||
# main.scss
|
||||
css, checksum = compile_scss(event)
|
||||
fname = '{}/{}/presale.{}.css'.format(event.organizer.slug, event.slug, checksum[:16])
|
||||
|
||||
if event.settings.get('presale_css_checksum', '') != checksum:
|
||||
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
|
||||
event.settings.set('presale_css_file', newname)
|
||||
event.settings.set('presale_css_checksum', checksum)
|
||||
|
||||
# widget.scss
|
||||
css, checksum = compile_scss(event, file='widget.scss', fonts=False)
|
||||
fname = '{}/{}/widget.{}.css'.format(event.organizer.slug, event.slug, checksum[:16])
|
||||
|
||||
if event.settings.get('presale_widget_css_checksum', '') != checksum:
|
||||
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
|
||||
event.settings.set('presale_widget_css_file', newname)
|
||||
event.settings.set('presale_widget_css_checksum', checksum)
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask)
|
||||
def regenerate_organizer_css(organizer_id: int):
|
||||
organizer = Organizer.objects.get(pk=organizer_id)
|
||||
|
||||
# main.scss
|
||||
css, checksum = compile_scss(organizer)
|
||||
|
||||
fname = '{}/presale.{}.css'.format(
|
||||
organizer.slug, checksum[:16]
|
||||
)
|
||||
|
||||
fname = '{}/presale.{}.css'.format(organizer.slug, checksum[:16])
|
||||
if organizer.settings.get('presale_css_checksum', '') != checksum:
|
||||
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
|
||||
organizer.settings.set('presale_css_file', newname)
|
||||
organizer.settings.set('presale_css_checksum', checksum)
|
||||
|
||||
# widget.scss
|
||||
css, checksum = compile_scss(organizer)
|
||||
fname = '{}/widget.{}.css'.format(organizer.slug, checksum[:16])
|
||||
if organizer.settings.get('presale_widget_css_checksum', '') != checksum:
|
||||
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
|
||||
organizer.settings.set('presale_widget_css_file', newname)
|
||||
organizer.settings.set('presale_widget_css_checksum', checksum)
|
||||
|
||||
non_inherited_events = set(Event_SettingsStore.objects.filter(
|
||||
object__organizer=organizer, key__in=affected_keys
|
||||
).values_list('object_id', flat=True))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% load safelink %}
|
||||
{% safelink "https://pretix.eu" as pretixurl %}
|
||||
{% with 'href="'|add:pretixurl|add:'"'|safe as a_attr %}
|
||||
{% with 'target="_blank" href="'|add:pretixurl|add:'"'|safe as a_attr %}
|
||||
{% blocktrans trimmed %}
|
||||
powered by <a {{ a_attr }}>pretix</a>
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{% block title %}{% endblock %}{% if url_name != "event.index" %} :: {% endif %}{{ event.name }}
|
||||
{% endblock %}
|
||||
{% block above %}
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script>
|
||||
{% if not event.live %}
|
||||
<div class="offline-banner">
|
||||
<div class="container">
|
||||
@@ -24,12 +25,13 @@
|
||||
<div class="page-header">
|
||||
<div class="pull-left">
|
||||
{% if event_logo %}
|
||||
<a href="{% eventurl event "presale:event.index" %}" title="{{ event.name }}">
|
||||
<a href="{% eventurl event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}"
|
||||
title="{{ event.name }}">
|
||||
<img src="{{ event_logo|thumbnail_url:'logo' }}" alt="{{ event.name }}" class="event-logo" />
|
||||
</a>
|
||||
{% else %}
|
||||
<h1>
|
||||
<a href="{% eventurl event "presale:event.index" %}">{{ event.name }}</a>
|
||||
<a href="{% eventurl event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}">{{ event.name }}</a>
|
||||
{% if not event.has_subevents %}
|
||||
<small>{{ event.get_date_range_display }}</small>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% extends "pretixpresale/event/checkout_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Checkout" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>{% trans "Checkout" %}</h2>
|
||||
{% block inner %}
|
||||
<p>
|
||||
{% trans "For some of the products in your cart, you can choose additional options before you continue." %}
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Checkout" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="panel panel-default cart">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a class="collapsed" data-toggle="collapse" href="#cart">
|
||||
<span>
|
||||
<i class="fa fa-shopping-cart"></i>
|
||||
<strong>{% trans "Your cart" %}</strong>
|
||||
</span>
|
||||
<span>
|
||||
<strong id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
|
||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
|
||||
{% else %}
|
||||
{% trans "Cart expired" %}
|
||||
{% endif %}
|
||||
</strong>
|
||||
<i class="fa fa-angle-down collapse-indicator"></i>
|
||||
</span>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-collapse collapse" id="cart">
|
||||
<div class="panel-body">
|
||||
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event %}
|
||||
<em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
|
||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||
{% blocktrans trimmed with minutes=cart.minutes_left %}
|
||||
The items in your cart are reserved for you for {{ minutes }} minutes.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "The items in your cart are no longer reserved for you." %}
|
||||
{% endif %}
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>{% trans "Checkout" %}</h2>
|
||||
{% block inner %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -11,14 +11,22 @@
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-primary cart">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right">
|
||||
<a href="
|
||||
{% eventurl request.event "presale:event.index" %}">
|
||||
<div class="pull-right cart-modify">
|
||||
<a href="{% eventurl request.event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Modify" %}
|
||||
</a>
|
||||
</div>
|
||||
<strong id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}"
|
||||
class="pull-right">
|
||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
|
||||
{% else %}
|
||||
{% trans "Cart expired" %}
|
||||
{% endif %}
|
||||
</strong>
|
||||
<h3 class="panel-title">
|
||||
<i class="fa fa-shopping-cart"></i>
|
||||
{% trans "Your cart" %}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -27,7 +35,7 @@
|
||||
<div class="cart-row row">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
|
||||
{% if cart.minutes_left > 0 %}
|
||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||
{% blocktrans trimmed with minutes=cart.minutes_left %}
|
||||
The items in your cart are reserved for you for {{ minutes }} minutes.
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% extends "pretixpresale/event/checkout_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Checkout" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>{% trans "Checkout" %}</h2>
|
||||
{% block inner %}
|
||||
<p>{% trans "Please select how you want to pay." %}</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% extends "pretixpresale/event/checkout_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Checkout" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>{% trans "Checkout" %}</h2>
|
||||
{% block inner %}
|
||||
<p>{% trans "Before we continue, we need you to answer some questions." %}</p>
|
||||
<p class="required-legend">
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<strong>{% trans "SOLD OUT" %}</strong>
|
||||
{% if event.settings.waiting_list_enabled %}
|
||||
<br/>
|
||||
<a href="{% eventurl event "presale:event.waitinglist" %}?item={{ item.pk }}{% if var %}&var={{ var.pk }}{% endif %}{% if subevent %}&subevent={{ subevent.pk }}{% endif %}">
|
||||
<a href="{% eventurl event "presale:event.waitinglist" cart_namespace=cart_namespace|default_if_none:"" %}?item={{ item.pk }}{% if var %}&var={{ var.pk }}{% endif %}{% if subevent %}&subevent={{ subevent.pk }}{% endif %}">
|
||||
<span class="fa fa-plus-circle"></span>
|
||||
{% trans "Waiting list" %}
|
||||
</a>
|
||||
@@ -19,7 +19,7 @@
|
||||
</strong>
|
||||
{% if event.settings.waiting_list_enabled %}
|
||||
<br/>
|
||||
<a href="{% eventurl event "presale:event.waitinglist" %}?item={{ item.pk }}{% if var %}&var={{ var.pk }}{% endif %}{% if subevent %}&subevent={{ subevent.pk }}{% endif %}">
|
||||
<a href="{% eventurl event "presale:event.waitinglist" cart_namespace=cart_namespace|default_if_none:"" %}?item={{ item.pk }}{% if var %}&var={{ var.pk }}{% endif %}{% if subevent %}&subevent={{ subevent.pk }}{% endif %}">
|
||||
<span class="fa fa-plus-circle"></span>
|
||||
{% trans "Waiting list" %}
|
||||
</a>
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
{% else %}
|
||||
<div class="count">
|
||||
{% if editable %}
|
||||
<form action="{% eventurl event "presale:event.cart.remove" %}"
|
||||
<form action="{% eventurl event "presale:event.cart.remove" cart_namespace=cart_namespace|default_if_none:"" %}"
|
||||
method="post" data-asynctask>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ line.id }}" />
|
||||
@@ -90,7 +90,7 @@
|
||||
{% endif %}
|
||||
{{ line.count }}
|
||||
{% if editable %}
|
||||
<form action="{% eventurl event "presale:event.cart.add" %}"
|
||||
<form action="{% eventurl event "presale:event.cart.add" cart_namespace=cart_namespace|default_if_none:"" %}"
|
||||
method="post" data-asynctask>
|
||||
<input type="hidden" name="subevent" value="{{ line.subevent_id|default_if_none:"" }}" />
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -17,45 +17,64 @@
|
||||
{% if show_cart %}
|
||||
<div class="panel panel-primary cart">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Your cart" %}</h3>
|
||||
<h3 class="panel-title">
|
||||
<a class="collapsed" data-toggle="collapse" href="#cart">
|
||||
<span>
|
||||
<i class="fa fa-shopping-cart"></i>
|
||||
<strong>{% trans "Your cart" %}</strong>
|
||||
</span>
|
||||
<span>
|
||||
<strong id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
|
||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
|
||||
{% else %}
|
||||
{% trans "Cart expired" %}
|
||||
{% endif %}
|
||||
</strong>
|
||||
<i class="fa fa-angle-down collapse-indicator"></i>
|
||||
</span>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
|
||||
<em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
|
||||
{% if cart.minutes_left > 0 %}
|
||||
{% blocktrans trimmed with minutes=cart.minutes_left %}
|
||||
The items in your cart are reserved for you for {{ minutes }} minutes.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "The items in your cart are no longer reserved for you." %}
|
||||
{% endif %}
|
||||
</em>
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4 col-xs-12">
|
||||
<form method="post" data-asynctask action="{% eventurl request.event "presale:event.cart.clear" %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-block btn-default btn-lg" type="submit">
|
||||
<i class="fa fa-close"></i> {% trans "Empty cart" %}</button>
|
||||
</form>
|
||||
<div class="panel-collapse collapse in" id="cart">
|
||||
<div class="panel-body">
|
||||
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
|
||||
<em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
|
||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||
{% blocktrans trimmed with minutes=cart.minutes_left %}
|
||||
The items in your cart are reserved for you for {{ minutes }} minutes.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "The items in your cart are no longer reserved for you." %}
|
||||
{% endif %}
|
||||
</em>
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4 col-xs-12">
|
||||
<form method="post" data-asynctask action="{% eventurl request.event "presale:event.cart.clear" cart_namespace=cart_namespace %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-block btn-default btn-lg" type="submit">
|
||||
<i class="fa fa-close"></i> {% trans "Empty cart" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4 col-xs-12">
|
||||
<a class="btn btn-block btn-primary btn-lg"
|
||||
href="{% eventurl request.event "presale:event.checkout.start" cart_namespace=cart_namespace %}">
|
||||
{% if has_addon_choices %}
|
||||
<i class="fa fa-shopping-cart"></i> {% trans "Continue" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<i class="fa fa-shopping-cart"></i> {% trans "Proceed with checkout" %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4 col-xs-12">
|
||||
<a class="btn btn-block btn-primary btn-lg"
|
||||
href="{% eventurl request.event "presale:event.checkout.start" %}">
|
||||
{% if has_addon_choices %}
|
||||
<i class="fa fa-shopping-cart"></i> {% trans "Continue" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<i class="fa fa-shopping-cart"></i> {% trans "Proceed with checkout" %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if event.has_subevents %}
|
||||
{% if event.has_subevents and not cart_namespace %}
|
||||
{% if subevent %}
|
||||
<a class="subevent-toggle">
|
||||
{% trans "View other date" %}
|
||||
@@ -75,7 +94,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if frontpage_text %}
|
||||
{% if frontpage_text and not cart_namespace %}
|
||||
<div>
|
||||
{{ frontpage_text|rich_text }}
|
||||
</div>
|
||||
@@ -99,60 +118,63 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
{% if ev.location %}
|
||||
{% if not cart_namespace %}
|
||||
<div>
|
||||
{% if ev.location %}
|
||||
<div class="info-row">
|
||||
<span class="fa fa-map-marker fa-fw"></span>
|
||||
<p>
|
||||
{{ ev.location|linebreaksbr }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="info-row">
|
||||
<span class="fa fa-map-marker fa-fw"></span>
|
||||
<span class="fa fa-clock-o fa-fw"></span>
|
||||
<p>
|
||||
{{ ev.location|linebreaksbr }}
|
||||
{{ ev.get_date_range_display }}
|
||||
{% if event.settings.show_times %}
|
||||
<br>
|
||||
{% blocktrans trimmed with time=ev.date_from|date:"TIME_FORMAT" %}
|
||||
Begin: {{ time }}
|
||||
{% endblocktrans %}
|
||||
{% if event.settings.show_date_to %}
|
||||
<br>
|
||||
{% blocktrans trimmed with time=ev.date_to|date:"TIME_FORMAT" %}
|
||||
End: {{ time }}
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if ev.date_admission %}
|
||||
<br>
|
||||
{% if ev.date_admission|date:"SHORT_DATE_FORMAT" == ev.date_from|date:"SHORT_DATE_FORMAT" %}
|
||||
{% blocktrans trimmed with time=ev.date_admission|date:"TIME_FORMAT" %}
|
||||
Admission: {{ time }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with datetime=ev.date_admission|date:"SHORT_DATETIME_FORMAT" %}
|
||||
Admission: {{ datetime }}
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if subevent %}
|
||||
<a href="{% eventurl event "presale:event.ical.download" subevent=subevent.pk %}">
|
||||
{% else %}
|
||||
<a href="{% eventurl event "presale:event.ical.download" %}">
|
||||
{% endif %}
|
||||
{% trans "Add to Calendar" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="info-row">
|
||||
<span class="fa fa-clock-o fa-fw"></span>
|
||||
<p>
|
||||
{{ ev.get_date_range_display }}
|
||||
{% if event.settings.show_times %}
|
||||
<br>
|
||||
{% blocktrans trimmed with time=ev.date_from|date:"TIME_FORMAT" %}
|
||||
Begin: {{ time }}
|
||||
{% endblocktrans %}
|
||||
{% if event.settings.show_date_to %}
|
||||
<br>
|
||||
{% blocktrans trimmed with time=ev.date_to|date:"TIME_FORMAT" %}
|
||||
End: {{ time }}
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if ev.date_admission %}
|
||||
<br>
|
||||
{% if ev.date_admission|date:"SHORT_DATE_FORMAT" == ev.date_from|date:"SHORT_DATE_FORMAT" %}
|
||||
{% blocktrans trimmed with time=ev.date_admission|date:"TIME_FORMAT" %}
|
||||
Admission: {{ time }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with datetime=ev.date_admission|date:"SHORT_DATETIME_FORMAT" %}
|
||||
Admission: {{ datetime }}
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if subevent %}
|
||||
<a href="{% eventurl event "presale:event.ical.download" subevent=subevent.pk %}">
|
||||
{% else %}
|
||||
<a href="{% eventurl event "presale:event.ical.download" %}">
|
||||
{% endif %}
|
||||
{% trans "Add to Calendar" %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% eventsignal event "pretix.presale.signals.front_page_top" %}
|
||||
{% endif %}
|
||||
|
||||
{% eventsignal event "pretix.presale.signals.front_page_top" %}
|
||||
{% if ev.presale_is_running or event.settings.show_items_outside_presale_period %}
|
||||
<form method="post" data-asynctask
|
||||
action="{% eventurl request.event "presale:event.cart.add" %}?next={{ request.path|urlencode }}">
|
||||
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={{ request.path|urlencode }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" />
|
||||
{% for tup in items_by_category %}
|
||||
@@ -392,7 +414,7 @@
|
||||
{% if vouchers_exist %}
|
||||
<section class="front-page">
|
||||
<h3>{% trans "Redeem a voucher" %}</h3>
|
||||
<form method="get" action="{% eventurl event "presale:event.redeem" %}">
|
||||
<form method="get" action="{% eventurl event "presale:event.redeem" cart_namespace=cart_namespace %}">
|
||||
<div class="row-voucher">
|
||||
<div class="col-md-8 col-sm-6 col-xs-12">
|
||||
<div class="input-group">
|
||||
@@ -412,25 +434,27 @@
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% eventsignal event "pretix.presale.signals.front_page_bottom" %}
|
||||
<section class="front-page">
|
||||
<h3>{% trans "If you already ordered a ticket" %}</h3>
|
||||
<div>
|
||||
<div class="col-md-8 col-xs-12">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If you want to see or change the status and details of your order, click on the link in one of the
|
||||
emails we sent you during the order process. If you cannot find the link, click on the
|
||||
following button to request the link to your order to be sent to you again.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if not cart_namespace %}
|
||||
{% eventsignal event "pretix.presale.signals.front_page_bottom" %}
|
||||
<section class="front-page">
|
||||
<h3>{% trans "If you already ordered a ticket" %}</h3>
|
||||
<div>
|
||||
<div class="col-md-8 col-xs-12">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If you want to see or change the status and details of your order, click on the link in one of the
|
||||
emails we sent you during the order process. If you cannot find the link, click on the
|
||||
following button to request the link to your order to be sent to you again.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 col-xs-12 text-right">
|
||||
<a class="btn btn-block btn-primary" href="{% eventurl event "presale:event.resend_link" %}">
|
||||
{% trans "Resend order links" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="col-md-4 col-xs-12 text-right">
|
||||
<a class="btn btn-block btn-primary" href="{% eventurl event "presale:event.resend_link" %}">
|
||||
{% trans "Resend order links" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -22,10 +22,15 @@
|
||||
{% else %}
|
||||
<p>{% trans "We successfully received your payment. See below for details." %}</p>
|
||||
{% endif %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
<p class="iframe-hidden">{% blocktrans trimmed %}
|
||||
Please bookmark or save the link to this exact page if you want to download your ticket or change
|
||||
your details later. We also sent you an email containing the link to the address you specified.
|
||||
{% endblocktrans %}</p>
|
||||
<p class="iframe-only">{% blocktrans trimmed %}
|
||||
Please save the following link if you want to download your ticket or change your details later. We
|
||||
also sent you an email containing the link to the address you specified.
|
||||
{% endblocktrans %}<br>
|
||||
<code>{{ url }}</code></p>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</p>
|
||||
{% if event.presale_is_running or event.settings.show_items_outside_presale_period %}
|
||||
<form method="post" data-asynctask
|
||||
action="{% eventurl request.event "presale:event.cart.add" %}?next={{ request.path|urlencode }}">
|
||||
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={% eventurl request.event "presale:event.index" cart_namespace=cart_namespace %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" />
|
||||
<input type="hidden" name="_voucher_code" value="{{ voucher.code }}">
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{% load compress %}
|
||||
{% load staticfiles %}
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/widget.scss" %}"/>
|
||||
{% endcompress %}
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.conf.urls import url
|
||||
from django.conf.urls import include, url
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
import pretix.presale.views.cart
|
||||
import pretix.presale.views.checkout
|
||||
@@ -9,23 +10,44 @@ import pretix.presale.views.organizer
|
||||
import pretix.presale.views.robots
|
||||
import pretix.presale.views.user
|
||||
import pretix.presale.views.waiting
|
||||
import pretix.presale.views.widget
|
||||
|
||||
# This is not a valid Django URL configuration, as the final
|
||||
# configuration is done by the pretix.multidomain package.
|
||||
|
||||
event_patterns = [
|
||||
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
|
||||
frame_wrapped_urls = [
|
||||
url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'),
|
||||
url(r'^cart/clear$', pretix.presale.views.cart.CartClear.as_view(), name='event.cart.clear'),
|
||||
url(r'^cart/answer/(?P<answer>[^/]+)/$',
|
||||
pretix.presale.views.cart.AnswerDownload.as_view(),
|
||||
name='event.cart.download.answer'),
|
||||
url(r'^waitinglist', pretix.presale.views.waiting.WaitingView.as_view(), name='event.waitinglist'),
|
||||
url(r'^checkout/start$', pretix.presale.views.checkout.CheckoutView.as_view(), name='event.checkout.start'),
|
||||
url(r'^redeem/?$', pretix.presale.views.cart.RedeemView.as_view(),
|
||||
name='event.redeem'),
|
||||
url(r'^checkout/(?P<step>[^/]+)/$', pretix.presale.views.checkout.CheckoutView.as_view(),
|
||||
name='event.checkout'),
|
||||
url(r'^redeem/?$', pretix.presale.views.cart.RedeemView.as_view(),
|
||||
name='event.redeem'),
|
||||
url(r'^(?P<subevent>[0-9]+)/$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
|
||||
url(r'^waitinglist', pretix.presale.views.waiting.WaitingView.as_view(), name='event.waitinglist'),
|
||||
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
|
||||
]
|
||||
event_patterns = [
|
||||
|
||||
# Cart/checkout patterns are a bit more complicated, as they should have simple URLs like cart/clear in normal
|
||||
# cases, but need to have versions with unguessable URLs like w/8l4Y83XNonjLxoBb/cart/clear to be used in widget
|
||||
# mode. This is required to prevent all clickjacking and CSRF attacks that would otherwise be possible.
|
||||
# First, we define the normal version
|
||||
url(r'', include(frame_wrapped_urls)),
|
||||
# Second, the widget version
|
||||
url(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/', include(frame_wrapped_urls)),
|
||||
# Third, a fake version that is defined like the first (and never gets called), but makes reversing URLs easier
|
||||
url(r'(?P<cart_namespace>[_]{0})', include(frame_wrapped_urls)),
|
||||
# CartAdd goes extra since it also gets a csrf_exempt decorator in one of the cases
|
||||
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
|
||||
url(r'^(?P<cart_namespace>[_]{0})cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
|
||||
url(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/cart/add',
|
||||
csrf_exempt(pretix.presale.views.cart.CartAdd.as_view()),
|
||||
name='event.cart.add'),
|
||||
|
||||
url(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'),
|
||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
|
||||
name='event.order'),
|
||||
@@ -71,8 +93,12 @@ event_patterns = [
|
||||
pretix.presale.views.event.EventIcalDownload.as_view(),
|
||||
name='event.ical.download'),
|
||||
url(r'^auth/$', pretix.presale.views.event.EventAuth.as_view(), name='event.auth'),
|
||||
url(r'^(?P<subevent>[0-9]+)/$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
|
||||
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
|
||||
|
||||
url(r'^widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(),
|
||||
name='event.widget.productlist'),
|
||||
url(r'^widget/v1.css$', pretix.presale.views.widget.widget_css, name='event.widget.css'),
|
||||
url(r'^(?P<subevent>\d+)/widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(),
|
||||
name='event.widget.productlist'),
|
||||
]
|
||||
|
||||
organizer_patterns = [
|
||||
@@ -85,4 +111,5 @@ organizer_patterns = [
|
||||
locale_patterns = [
|
||||
url(r'^locale/set$', pretix.presale.views.locale.LocaleSet.as_view(), name='locale.set'),
|
||||
url(r'^robots.txt$', pretix.presale.views.robots.robots_txt, name='robots.txt'),
|
||||
url(r'^widget/v1\.(?P<lang>[a-zA-Z0-9_\-]+)\.js$', pretix.presale.views.widget.widget_js, name='widget.js'),
|
||||
]
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from itertools import groupby
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Sum
|
||||
from django.utils.decorators import available_attrs
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CartPosition, InvoiceAddress, OrderPosition
|
||||
from pretix.base.services.cart import get_fees
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.signals import question_form_fields
|
||||
|
||||
|
||||
@@ -129,9 +134,11 @@ class CartMixin:
|
||||
try:
|
||||
first_expiry = min(p.expires for p in positions) if positions else now()
|
||||
minutes_left = max(first_expiry - now(), timedelta()).seconds // 60
|
||||
seconds_left = max(first_expiry - now(), timedelta()).seconds % 60
|
||||
except AttributeError:
|
||||
first_expiry = None
|
||||
minutes_left = None
|
||||
seconds_left = None
|
||||
|
||||
return {
|
||||
'positions': positions,
|
||||
@@ -142,6 +149,7 @@ class CartMixin:
|
||||
'fees': fees,
|
||||
'answers': answers,
|
||||
'minutes_left': minutes_left,
|
||||
'seconds_left': seconds_left,
|
||||
'first_expiry': first_expiry,
|
||||
}
|
||||
|
||||
@@ -182,9 +190,53 @@ class EventViewMixin:
|
||||
context['event'] = self.request.event
|
||||
return context
|
||||
|
||||
def get_index_url(self):
|
||||
kwargs = {}
|
||||
if 'cart_namespace' in self.kwargs:
|
||||
kwargs['cart_namespace'] = self.kwargs['cart_namespace']
|
||||
return eventreverse(self.request.event, 'presale:event.index', kwargs=kwargs)
|
||||
|
||||
|
||||
class OrganizerViewMixin:
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['organizer'] = self.request.organizer
|
||||
return context
|
||||
|
||||
|
||||
def allow_frame_if_namespaced(view_func):
|
||||
def wrapped_view(request, *args, **kwargs):
|
||||
resp = view_func(request, *args, **kwargs)
|
||||
if request.resolver_match and request.resolver_match.kwargs.get('cart_namespace'):
|
||||
resp.xframe_options_exempt = True
|
||||
return resp
|
||||
return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
|
||||
|
||||
|
||||
def allow_cors_if_namespaced(view_func):
|
||||
def wrapped_view(request, *args, **kwargs):
|
||||
resp = view_func(request, *args, **kwargs)
|
||||
if request.resolver_match and request.resolver_match.kwargs.get('cart_namespace'):
|
||||
resp['Access-Control-Allow-Origin'] = '*'
|
||||
return resp
|
||||
return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
|
||||
|
||||
|
||||
def iframe_entry_view_wrapper(view_func):
|
||||
def wrapped_view(request, *args, **kwargs):
|
||||
if 'iframe' in request.GET:
|
||||
request.session['iframe_session'] = True
|
||||
|
||||
locale = request.GET.get('locale')
|
||||
if locale and locale in [lc for lc, ll in settings.LANGUAGES]:
|
||||
with language(locale):
|
||||
resp = view_func(request, *args, **kwargs)
|
||||
max_age = 10 * 365 * 24 * 60 * 60
|
||||
resp.set_cookie(settings.LANGUAGE_COOKIE_NAME, locale, max_age=max_age,
|
||||
expires=(datetime.utcnow() + timedelta(seconds=max_age)).strftime('%a, %d-%b-%Y %H:%M:%S GMT'),
|
||||
domain=settings.SESSION_COOKIE_DOMAIN)
|
||||
return resp
|
||||
|
||||
resp = view_func(request, *args, **kwargs)
|
||||
return resp
|
||||
return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
|
||||
|
||||
@@ -2,37 +2,47 @@ import mimetypes
|
||||
import os
|
||||
|
||||
from django.contrib import messages
|
||||
from django.db.models import Count, Prefetch, Q
|
||||
from django.db.models import Q
|
||||
from django.http import FileResponse, Http404, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils import translation
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.generic import TemplateView, View
|
||||
|
||||
from pretix.base.models import (
|
||||
CartPosition, InvoiceAddress, ItemVariation, QuestionAnswer, Quota,
|
||||
SubEvent, Voucher,
|
||||
CartPosition, InvoiceAddress, QuestionAnswer, SubEvent, Voucher,
|
||||
)
|
||||
from pretix.base.services.cart import (
|
||||
CartError, add_items_to_cart, clear_cart, remove_cart_position,
|
||||
)
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.views import EventViewMixin
|
||||
from pretix.presale.views import (
|
||||
EventViewMixin, allow_cors_if_namespaced, allow_frame_if_namespaced,
|
||||
iframe_entry_view_wrapper,
|
||||
)
|
||||
from pretix.presale.views.async import AsyncAction
|
||||
from pretix.presale.views.event import item_group_by_category
|
||||
from pretix.presale.views.event import (
|
||||
get_grouped_items, item_group_by_category,
|
||||
)
|
||||
from pretix.presale.views.robots import NoSearchIndexViewMixin
|
||||
|
||||
|
||||
class CartActionMixin:
|
||||
|
||||
def get_next_url(self):
|
||||
if "next" in self.request.GET and '://' not in self.request.GET.get('next'):
|
||||
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next")):
|
||||
return self.request.GET.get('next')
|
||||
else:
|
||||
return eventreverse(self.request.event, 'presale:event.index')
|
||||
kwargs = {}
|
||||
if 'cart_namespace' in self.kwargs:
|
||||
kwargs['cart_namespace'] = self.kwargs['cart_namespace']
|
||||
return eventreverse(self.request.event, 'presale:event.index', kwargs=kwargs)
|
||||
|
||||
def get_success_url(self, value=None):
|
||||
return self.get_next_url()
|
||||
@@ -129,25 +139,54 @@ class CartActionMixin:
|
||||
return items
|
||||
|
||||
|
||||
def create_empty_cart_id(request):
|
||||
current_id = request.session.get('current_cart_event_{}'.format(request.event.pk))
|
||||
if current_id and current_id in request.session.get('carts', {}):
|
||||
del request.session['carts'][current_id]
|
||||
del request.session['current_cart_event_{}'.format(request.event.pk)]
|
||||
return get_or_create_cart_id(request)
|
||||
def generate_cart_id(prefix=''):
|
||||
while True:
|
||||
new_id = prefix + get_random_string(length=32 - len(prefix))
|
||||
if not CartPosition.objects.filter(cart_id=new_id).exists():
|
||||
return new_id
|
||||
|
||||
|
||||
def create_empty_cart_id(request, replace_current=True):
|
||||
session_keyname = 'current_cart_event_{}'.format(request.event.pk)
|
||||
prefix = ''
|
||||
if request.resolver_match and request.resolver_match.kwargs.get('cart_namespace'):
|
||||
session_keyname += '_' + request.resolver_match.kwargs.get('cart_namespace')
|
||||
prefix = request.resolver_match.kwargs.get('cart_namespace')
|
||||
|
||||
if 'carts' not in request.session:
|
||||
request.session['carts'] = {}
|
||||
|
||||
new_id = generate_cart_id(prefix=prefix)
|
||||
request.session['carts'][new_id] = {}
|
||||
|
||||
if replace_current:
|
||||
current_id = request.session.get(session_keyname)
|
||||
if current_id and current_id in request.session.get('carts', {}):
|
||||
del request.session['carts'][current_id]
|
||||
del request.session[session_keyname]
|
||||
request.session[session_keyname] = new_id
|
||||
return new_id
|
||||
|
||||
|
||||
def get_or_create_cart_id(request):
|
||||
current_id = request.session.get('current_cart_event_{}'.format(request.event.pk))
|
||||
session_keyname = 'current_cart_event_{}'.format(request.event.pk)
|
||||
prefix = ''
|
||||
if request.resolver_match and request.resolver_match.kwargs.get('cart_namespace'):
|
||||
session_keyname += '_' + request.resolver_match.kwargs.get('cart_namespace')
|
||||
prefix = request.resolver_match.kwargs.get('cart_namespace')
|
||||
|
||||
current_id = request.session.get(session_keyname)
|
||||
if current_id and current_id in request.session.get('carts', {}):
|
||||
return current_id
|
||||
else:
|
||||
cart_data = {}
|
||||
|
||||
while True:
|
||||
new_id = get_random_string(length=32)
|
||||
if not CartPosition.objects.filter(cart_id=new_id).exists():
|
||||
break
|
||||
if prefix and 'take_cart_id' in request.GET:
|
||||
if CartPosition.objects.filter(event=request.event, cart_id=request.GET.get('take_cart_id')).exists():
|
||||
new_id = request.GET.get('take_cart_id')
|
||||
else:
|
||||
new_id = generate_cart_id(prefix=prefix)
|
||||
else:
|
||||
new_id = generate_cart_id(prefix=prefix)
|
||||
|
||||
# Migrate legacy data
|
||||
# TODO: This is for the upgrade 1.7→1.8. We should remove this around April 2018
|
||||
@@ -165,8 +204,9 @@ def get_or_create_cart_id(request):
|
||||
|
||||
if 'carts' not in request.session:
|
||||
request.session['carts'] = {}
|
||||
request.session['carts'][new_id] = cart_data
|
||||
request.session['current_cart_event_{}'.format(request.event.pk)] = new_id
|
||||
if new_id not in request.session['carts']:
|
||||
request.session['carts'][new_id] = cart_data
|
||||
request.session[session_keyname] = new_id
|
||||
return new_id
|
||||
|
||||
|
||||
@@ -176,6 +216,7 @@ def cart_session(request):
|
||||
return request.session['carts'][cart_id]
|
||||
|
||||
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
task = remove_cart_position
|
||||
known_errortypes = ['CartError']
|
||||
@@ -198,6 +239,7 @@ class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
return redirect(self.get_error_url())
|
||||
|
||||
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
task = clear_cart
|
||||
known_errortypes = ['CartError']
|
||||
@@ -209,6 +251,9 @@ class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
return self.do(self.request.event.id, get_or_create_cart_id(self.request), translation.get_language())
|
||||
|
||||
|
||||
@method_decorator(allow_cors_if_namespaced, 'dispatch')
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
|
||||
class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
task = add_items_to_cart
|
||||
known_errortypes = ['CartError']
|
||||
@@ -216,6 +261,13 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
def get_success_message(self, value):
|
||||
return _('The products have been successfully added to your cart.')
|
||||
|
||||
def _ajax_response_data(self):
|
||||
cart_id = get_or_create_cart_id(self.request)
|
||||
return {
|
||||
'cart_id': cart_id,
|
||||
'has_cart': CartPosition.objects.filter(cart_id=cart_id, event=self.request.event).exists()
|
||||
}
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
items = self._items_from_post_data()
|
||||
if items:
|
||||
@@ -230,6 +282,8 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
return redirect(self.get_error_url())
|
||||
|
||||
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
|
||||
class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/voucher.html"
|
||||
|
||||
@@ -240,90 +294,11 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
|
||||
context['max_times'] = self.voucher.max_usages - self.voucher.redeemed
|
||||
|
||||
# Fetch all items
|
||||
items = self.request.event.items.all().filter(
|
||||
Q(active=True)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||
& ~Q(category__is_addon=True)
|
||||
)
|
||||
items, display_add_to_cart = get_grouped_items(self.request.event, self.subevent,
|
||||
voucher=self.voucher)
|
||||
|
||||
vouchq = Q(hide_without_voucher=False)
|
||||
|
||||
if self.voucher.item_id:
|
||||
vouchq |= Q(pk=self.voucher.item_id)
|
||||
items = items.filter(pk=self.voucher.item_id)
|
||||
elif self.voucher.quota_id:
|
||||
items = items.filter(quotas__in=[self.voucher.quota_id])
|
||||
|
||||
items = items.filter(vouchq).select_related(
|
||||
'category', 'tax_rule', # for re-grouping
|
||||
).prefetch_related(
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=self.request.event.quotas.filter(subevent=self.subevent)),
|
||||
Prefetch('variations', to_attr='available_variations',
|
||||
queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).prefetch_related(
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=self.request.event.quotas.filter(subevent=self.subevent))
|
||||
).distinct()),
|
||||
).annotate(
|
||||
quotac=Count('quotas'),
|
||||
has_variations=Count('variations')
|
||||
).filter(
|
||||
quotac__gt=0
|
||||
).distinct().order_by('category__position', 'category_id', 'position', 'name')
|
||||
quota_cache = {}
|
||||
|
||||
if self.subevent:
|
||||
item_price_override = self.subevent.item_price_overrides
|
||||
var_price_override = self.subevent.var_price_overrides
|
||||
else:
|
||||
item_price_override = {}
|
||||
var_price_override = {}
|
||||
|
||||
for item in items:
|
||||
if self.voucher.item_id and self.voucher.variation_id:
|
||||
item.available_variations = [v for v in item.available_variations if v.pk == self.voucher.variation_id]
|
||||
|
||||
item.order_max = item.max_per_order or int(self.request.event.settings.max_items_per_order)
|
||||
|
||||
if not item.has_variations:
|
||||
item._remove = not bool(item._subevent_quotas)
|
||||
if self.voucher.allow_ignore_quota or self.voucher.block_quota:
|
||||
item.cached_availability = (Quota.AVAILABILITY_OK, 1)
|
||||
else:
|
||||
item.cached_availability = item.check_quotas(subevent=self.subevent, _cache=quota_cache)
|
||||
|
||||
price = item_price_override.get(item.pk, item.default_price)
|
||||
price = self.voucher.calculate_price(price)
|
||||
item.display_price = item.tax(price)
|
||||
else:
|
||||
item._remove = False
|
||||
for var in item.available_variations:
|
||||
if self.voucher.allow_ignore_quota or self.voucher.block_quota:
|
||||
var.cached_availability = (Quota.AVAILABILITY_OK, 1)
|
||||
else:
|
||||
var.cached_availability = list(var.check_quotas(subevent=self.subevent, _cache=quota_cache))
|
||||
|
||||
price = var_price_override.get(var.pk, var.price)
|
||||
price = self.voucher.calculate_price(price)
|
||||
var.display_price = item.tax(price)
|
||||
|
||||
item.available_variations = [
|
||||
v for v in item.available_variations if v._subevent_quotas
|
||||
]
|
||||
if self.voucher.variation_id:
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.pk == self.voucher.variation_id]
|
||||
if len(item.available_variations) > 0:
|
||||
item.min_price = min([v.display_price.net if self.request.event.settings.display_net_prices else
|
||||
v.display_price.gross for v in item.available_variations])
|
||||
item.max_price = max([v.display_price.net if self.request.event.settings.display_net_prices else
|
||||
v.display_price.gross for v in item.available_variations])
|
||||
|
||||
items = [item for item in items
|
||||
if (len(item.available_variations) > 0 or not item.has_variations) and not item._remove]
|
||||
# Calculate how many options the user still has. If there is only one option, we can
|
||||
# check the box right away ;)
|
||||
context['options'] = sum([(len(item.available_variations) if item.has_variations else 1)
|
||||
for item in items])
|
||||
|
||||
@@ -359,7 +334,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
|
||||
except Voucher.DoesNotExist:
|
||||
err = error_messages['voucher_invalid']
|
||||
else:
|
||||
return redirect(eventreverse(request.event, 'presale:event.index'))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
if request.event.presale_start and now() < request.event.presale_start:
|
||||
err = error_messages['not_started']
|
||||
@@ -379,11 +354,12 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
|
||||
|
||||
if err:
|
||||
messages.error(request, _(err))
|
||||
return redirect(eventreverse(request.event, 'presale:event.index'))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class AnswerDownload(EventViewMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
answid = kwargs.get('answer')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.contrib import messages
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import View
|
||||
|
||||
@@ -8,21 +9,31 @@ from pretix.base.services.cart import CartError
|
||||
from pretix.base.signals import validate_cart
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.checkoutflow import get_checkout_flow
|
||||
from pretix.presale.views import get_cart
|
||||
from pretix.presale.views import (
|
||||
allow_frame_if_namespaced, get_cart, iframe_entry_view_wrapper,
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
|
||||
class CheckoutView(View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
||||
def get_index_url(self, request):
|
||||
kwargs = {}
|
||||
if 'cart_namespace' in self.kwargs:
|
||||
kwargs['cart_namespace'] = self.kwargs['cart_namespace']
|
||||
return eventreverse(self.request.event, 'presale:event.index', kwargs=kwargs)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
|
||||
if not get_cart(request) and "async_id" not in request.GET:
|
||||
messages.error(request, _("Your cart is empty"))
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.index'))
|
||||
return redirect(self.get_index_url(self.request))
|
||||
|
||||
if not request.event.presale_is_running:
|
||||
messages.error(request, _("The presale for this event is over or has not yet started."))
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.index'))
|
||||
return redirect(self.get_index_url(self.request))
|
||||
|
||||
cart_error = None
|
||||
try:
|
||||
@@ -37,14 +48,14 @@ class CheckoutView(View):
|
||||
continue
|
||||
if step.requires_valid_cart and cart_error:
|
||||
messages.error(request, str(cart_error))
|
||||
return redirect(previous_step.get_step_url() if previous_step
|
||||
else eventreverse(self.request.event, 'presale:event.index'))
|
||||
return redirect(previous_step.get_step_url(request) if previous_step
|
||||
else self.get_index_url(request))
|
||||
|
||||
if 'step' not in kwargs:
|
||||
return redirect(step.get_step_url())
|
||||
return redirect(step.get_step_url(request))
|
||||
is_selected = (step.identifier == kwargs.get('step', ''))
|
||||
if "async_id" not in request.GET and not is_selected and not step.is_completed(request, warn=not is_selected):
|
||||
return redirect(step.get_step_url())
|
||||
return redirect(step.get_step_url(request))
|
||||
if is_selected:
|
||||
if request.method.lower() in self.http_method_names:
|
||||
handler = getattr(step, request.method.lower(), self.http_method_not_allowed)
|
||||
|
||||
@@ -17,7 +17,7 @@ from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from pretix.base.models import ItemVariation
|
||||
from pretix.base.models import ItemVariation, Quota
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.ical import get_ical
|
||||
@@ -25,7 +25,10 @@ from pretix.presale.views.organizer import (
|
||||
add_subevents_for_days, weeks_for_template,
|
||||
)
|
||||
|
||||
from . import CartMixin, EventViewMixin, get_cart
|
||||
from . import (
|
||||
CartMixin, EventViewMixin, allow_frame_if_namespaced, get_cart,
|
||||
iframe_entry_view_wrapper,
|
||||
)
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
@@ -44,14 +47,23 @@ def item_group_by_category(items):
|
||||
)
|
||||
|
||||
|
||||
def get_grouped_items(event, subevent=None):
|
||||
def get_grouped_items(event, subevent=None, voucher=None):
|
||||
items = event.items.all().filter(
|
||||
Q(active=True)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||
& Q(hide_without_voucher=False)
|
||||
& ~Q(category__is_addon=True)
|
||||
).select_related(
|
||||
)
|
||||
|
||||
vouchq = Q(hide_without_voucher=False)
|
||||
if voucher:
|
||||
if voucher.item_id:
|
||||
vouchq |= Q(pk=voucher.item_id)
|
||||
items = items.filter(pk=voucher.item_id)
|
||||
elif voucher.quota_id:
|
||||
items = items.filter(quotas__in=[voucher.quota_id])
|
||||
|
||||
items = items.filter(vouchq).select_related(
|
||||
'category', 'tax_rule', # for re-grouping
|
||||
).prefetch_related(
|
||||
Prefetch('quotas',
|
||||
@@ -81,36 +93,74 @@ def get_grouped_items(event, subevent=None):
|
||||
var_price_override = {}
|
||||
|
||||
for item in items:
|
||||
if voucher and voucher.item_id and voucher.variation_id:
|
||||
# Restrict variations if the voucher only allows one
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.pk == voucher.variation_id]
|
||||
|
||||
max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
|
||||
|
||||
if not item.has_variations:
|
||||
item._remove = not bool(item._subevent_quotas)
|
||||
item.cached_availability = list(item.check_quotas(subevent=subevent, _cache=quota_cache))
|
||||
item.order_max = min(item.cached_availability[1]
|
||||
if item.cached_availability[1] is not None else sys.maxsize,
|
||||
max_per_order)
|
||||
price = item.default_price
|
||||
item.display_price = item.tax(item_price_override.get(item.pk, price))
|
||||
|
||||
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
|
||||
item.cached_availability = (
|
||||
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
|
||||
)
|
||||
else:
|
||||
item.cached_availability = list(
|
||||
item.check_quotas(subevent=subevent, _cache=quota_cache)
|
||||
)
|
||||
|
||||
item.order_max = min(
|
||||
item.cached_availability[1]
|
||||
if item.cached_availability[1] is not None else sys.maxsize,
|
||||
max_per_order
|
||||
)
|
||||
|
||||
price = item_price_override.get(item.pk, item.default_price)
|
||||
if voucher:
|
||||
price = voucher.calculate_price(price)
|
||||
item.display_price = item.tax(price)
|
||||
|
||||
display_add_to_cart = display_add_to_cart or item.order_max > 0
|
||||
else:
|
||||
for var in item.available_variations:
|
||||
var.cached_availability = list(var.check_quotas(subevent=subevent, _cache=quota_cache))
|
||||
var.order_max = min(var.cached_availability[1]
|
||||
if var.cached_availability[1] is not None else sys.maxsize,
|
||||
max_per_order)
|
||||
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
|
||||
var.cached_availability = (
|
||||
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
|
||||
)
|
||||
else:
|
||||
var.cached_availability = list(
|
||||
var.check_quotas(subevent=subevent, _cache=quota_cache)
|
||||
)
|
||||
|
||||
var.display_price = var.tax(var_price_override.get(var.pk, var.price))
|
||||
var.order_max = min(
|
||||
var.cached_availability[1]
|
||||
if var.cached_availability[1] is not None else sys.maxsize,
|
||||
max_per_order
|
||||
)
|
||||
|
||||
price = var_price_override.get(var.pk, var.price)
|
||||
if voucher:
|
||||
price = voucher.calculate_price(price)
|
||||
var.display_price = var.tax(price)
|
||||
|
||||
display_add_to_cart = display_add_to_cart or var.order_max > 0
|
||||
|
||||
item.available_variations = [
|
||||
v for v in item.available_variations if v._subevent_quotas
|
||||
]
|
||||
if voucher and voucher.variation_id:
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.pk == voucher.variation_id]
|
||||
|
||||
if len(item.available_variations) > 0:
|
||||
item.min_price = min([v.display_price.net if event.settings.display_net_prices else
|
||||
v.display_price.gross for v in item.available_variations])
|
||||
item.max_price = max([v.display_price.net if event.settings.display_net_prices else
|
||||
v.display_price.gross for v in item.available_variations])
|
||||
|
||||
item._remove = not bool(item.available_variations)
|
||||
|
||||
if not external_quota_cache:
|
||||
@@ -120,6 +170,8 @@ def get_grouped_items(event, subevent=None):
|
||||
return items, display_add_to_cart
|
||||
|
||||
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
|
||||
class EventIndex(EventViewMixin, CartMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/index.html"
|
||||
|
||||
@@ -135,7 +187,7 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
|
||||
return super().get(request, *args, **kwargs)
|
||||
else:
|
||||
if 'subevent' in kwargs:
|
||||
return redirect(eventreverse(request.event, 'presale:event.index'))
|
||||
return redirect(self.get_index_url())
|
||||
else:
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ from django.db import transaction
|
||||
from django.db.models import Sum
|
||||
from django.http import FileResponse, Http404, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.generic import TemplateView, View
|
||||
|
||||
from pretix.base.models import CachedTicket, Invoice, Order, OrderPosition
|
||||
@@ -24,7 +26,7 @@ from pretix.base.services.tickets import (
|
||||
)
|
||||
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
|
||||
from pretix.helpers.safedownload import check_token
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
|
||||
from pretix.presale.forms.checkout import InvoiceAddressForm
|
||||
from pretix.presale.views import CartMixin, EventViewMixin
|
||||
from pretix.presale.views.async import AsyncAction
|
||||
@@ -59,6 +61,7 @@ class OrderDetailMixin(NoSearchIndexViewMixin):
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/order.html"
|
||||
|
||||
@@ -112,6 +115,12 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
|
||||
ctx['can_generate_invoice'] = invoice_qualified(self.order) and (
|
||||
self.request.event.settings.invoice_generate == 'user'
|
||||
)
|
||||
ctx['url'] = build_absolute_uri(
|
||||
self.request.event, 'presale:event.order', kwargs={
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
}
|
||||
)
|
||||
|
||||
if self.order.status == Order.STATUS_PENDING:
|
||||
ctx['payment'] = self.payment_provider.order_pending_render(self.request, self.order)
|
||||
@@ -134,6 +143,7 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
|
||||
return ctx
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
"""
|
||||
This is used if a payment is retried or the payment method is changed. It shows the payment
|
||||
@@ -186,6 +196,7 @@ class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
"""
|
||||
This is used if a payment is retried or the payment method is changed. It is shown after the
|
||||
@@ -232,6 +243,7 @@ class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderPaymentComplete(EventViewMixin, OrderDetailMixin, View):
|
||||
"""
|
||||
This is used for the first try of a payment. This means the user just entered payment
|
||||
@@ -273,6 +285,7 @@ class OrderPaymentComplete(EventViewMixin, OrderDetailMixin, View):
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
template_name = 'pretixpresale/event/order_pay_change.html'
|
||||
|
||||
@@ -384,6 +397,7 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
})
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
@@ -406,6 +420,7 @@ class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/order_modify.html"
|
||||
|
||||
@@ -471,6 +486,7 @@ class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, Template
|
||||
return ctx
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
template_name = "pretixpresale/event/order_cancel.html"
|
||||
|
||||
@@ -493,6 +509,7 @@ class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
return ctx
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
|
||||
task = cancel_order
|
||||
known_errortypes = ['OrderError']
|
||||
@@ -520,6 +537,7 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
|
||||
return _('The order has been canceled.')
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
answid = kwargs.get('answer')
|
||||
@@ -541,6 +559,7 @@ class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
return resp
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class OrderDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
|
||||
def get_self_url(self):
|
||||
@@ -636,6 +655,7 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
return resp
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils import translation
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django.views.generic import FormView
|
||||
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.presale.views import EventViewMixin
|
||||
|
||||
from . import allow_frame_if_namespaced
|
||||
from ...base.models import Item, ItemVariation, WaitingListEntry
|
||||
from ...multidomain.urlreverse import eventreverse
|
||||
from ..forms.waitinglist import WaitingListForm
|
||||
|
||||
|
||||
class WaitingView(FormView):
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
class WaitingView(EventViewMixin, FormView):
|
||||
template_name = 'pretixpresale/event/waitinglist.html'
|
||||
form_class = WaitingListForm
|
||||
|
||||
@@ -52,11 +55,11 @@ class WaitingView(FormView):
|
||||
|
||||
if not self.request.event.settings.waiting_list_enabled:
|
||||
messages.error(request, _("Waiting lists are disabled for this event."))
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.index'))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
if not self.item_and_variation:
|
||||
messages.error(request, _("We could not identify the product you selected."))
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.index'))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
self.subevent = None
|
||||
if request.event.has_subevents:
|
||||
@@ -65,7 +68,7 @@ class WaitingView(FormView):
|
||||
active=True)
|
||||
else:
|
||||
messages.error(request, pgettext_lazy('subevent', "You need to select a date."))
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.index'))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -78,7 +81,7 @@ class WaitingView(FormView):
|
||||
if availability[0] == 100:
|
||||
messages.error(self.request, _("You cannot add yourself to the waiting list as this product is currently "
|
||||
"available."))
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.index'))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
form.save()
|
||||
messages.success(self.request, _("We've added you to the waiting list. You will receive "
|
||||
@@ -86,4 +89,4 @@ class WaitingView(FormView):
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return eventreverse(self.request.event, 'presale:event.index')
|
||||
return self.get_index_url()
|
||||
|
||||
274
src/pretix/presale/views/widget.py
Normal file
274
src/pretix/presale/views/widget.py
Normal file
@@ -0,0 +1,274 @@
|
||||
import hashlib
|
||||
import json
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db.models import Q
|
||||
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
|
||||
from django.template import Context, Engine
|
||||
from django.template.loader import get_template
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.views import View
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.http import condition
|
||||
from django.views.i18n import (
|
||||
get_formats, get_javascript_catalog, js_catalog_template,
|
||||
)
|
||||
from easy_thumbnails.files import get_thumbnailer
|
||||
from lxml import etree
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CartPosition, Voucher
|
||||
from pretix.base.services.cart import error_messages
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
from pretix.presale.views.event import (
|
||||
get_grouped_items, item_group_by_category,
|
||||
)
|
||||
|
||||
|
||||
def indent(s):
|
||||
return s.replace('\n', '\n ')
|
||||
|
||||
|
||||
def widget_css_etag(request, **kwargs):
|
||||
return request.event.settings.presale_widget_css_checksum or request.organizer.settings.presale_widget_css_checksum
|
||||
|
||||
|
||||
def widget_js_etag(request, lang, **kwargs):
|
||||
gs = GlobalSettingsObject()
|
||||
return gs.settings.get('widget_checksum_{}'.format(lang))
|
||||
|
||||
|
||||
@condition(etag_func=widget_css_etag)
|
||||
@cache_page(60)
|
||||
def widget_css(request, **kwargs):
|
||||
if request.event.settings.presale_widget_css_file:
|
||||
resp = FileResponse(default_storage.open(request.event.settings.presale_widget_css_file),
|
||||
content_type='text/css')
|
||||
return resp
|
||||
else:
|
||||
tpl = get_template('pretixpresale/widget_dummy.html')
|
||||
et = etree.fromstring(tpl.render({})).attrib['href'].replace(settings.STATIC_URL, '')
|
||||
f = finders.find(et)
|
||||
resp = FileResponse(open(f, 'rb'), content_type='text/css')
|
||||
return resp
|
||||
|
||||
|
||||
def generate_widget_js(lang):
|
||||
code = []
|
||||
with language(lang):
|
||||
# Provide isolation
|
||||
code.append('(function (siteglobals) {\n')
|
||||
code.append('var module = {}, exports = {};\n')
|
||||
code.append('var lang = "%s";\n' % lang)
|
||||
|
||||
catalog, plural = get_javascript_catalog(lang, 'djangojs', ['pretix'])
|
||||
catalog = dict((k, v) for k, v in catalog.items() if k.startswith('widget\u0004'))
|
||||
template = Engine().from_string(js_catalog_template)
|
||||
context = Context({
|
||||
'catalog_str': indent(json.dumps(
|
||||
catalog, sort_keys=True, indent=2)) if catalog else None,
|
||||
'formats_str': indent(json.dumps(
|
||||
get_formats(), sort_keys=True, indent=2)),
|
||||
'plural': plural,
|
||||
})
|
||||
code.append(template.render(context))
|
||||
|
||||
files = [
|
||||
'vuejs/vue.js' if settings.DEBUG else 'vuejs/vue.min.js',
|
||||
'pretixpresale/js/widget/docready.js',
|
||||
'pretixpresale/js/widget/floatformat.js',
|
||||
'pretixpresale/js/widget/widget.js',
|
||||
]
|
||||
for fname in files:
|
||||
f = finders.find(fname)
|
||||
with open(f, 'r') as fp:
|
||||
code.append(fp.read())
|
||||
|
||||
if settings.DEBUG:
|
||||
code.append('})(this);\n')
|
||||
else:
|
||||
# Do not expose debugging variables
|
||||
code.append('})({});\n')
|
||||
return ''.join(code)
|
||||
|
||||
|
||||
@condition(etag_func=widget_js_etag)
|
||||
@cache_page(60)
|
||||
def widget_js(request, lang, **kwargs):
|
||||
if lang not in [lc for lc, ll in settings.LANGUAGES]:
|
||||
raise Http404()
|
||||
|
||||
gs = GlobalSettingsObject()
|
||||
fname = gs.settings.get('widget_file_{}'.format(lang))
|
||||
print(fname, settings.DEBUG)
|
||||
if not fname or settings.DEBUG:
|
||||
data = generate_widget_js(lang).encode()
|
||||
checksum = hashlib.sha1(data).hexdigest()
|
||||
if not fname:
|
||||
newname = default_storage.save(
|
||||
'widget/widget.{}.{}.js'.format(lang, checksum),
|
||||
ContentFile(data)
|
||||
)
|
||||
gs.settings.set('widget_file_{}'.format(lang), 'file://' + newname)
|
||||
gs.settings.set('widget_checksum_{}'.format(lang), checksum)
|
||||
resp = HttpResponse(data, content_type='text/javascript')
|
||||
else:
|
||||
resp = FileResponse(default_storage.open(fname), content_type='text/javascript')
|
||||
return resp
|
||||
|
||||
|
||||
def price_dict(price):
|
||||
return {
|
||||
'gross': price.gross,
|
||||
'net': price.net,
|
||||
'tax': price.tax,
|
||||
'rate': price.rate,
|
||||
'name': str(price.name)
|
||||
}
|
||||
|
||||
|
||||
def get_picture(picture):
|
||||
thumb = get_thumbnailer(picture)['productlist']
|
||||
return urljoin(settings.SITE_URL, thumb.url)
|
||||
|
||||
|
||||
class WidgetAPIProductList(View):
|
||||
|
||||
def _get_items(self):
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
self.request.event, subevent=self.subevent, voucher=self.voucher
|
||||
)
|
||||
grps = []
|
||||
for cat, g in item_group_by_category(items):
|
||||
grps.append({
|
||||
'id': cat.pk if cat else None,
|
||||
'name': str(cat.name) if cat else None,
|
||||
'description': str(rich_text(cat.description, safelinks=False)) if cat and cat.description else None,
|
||||
'items': [
|
||||
{
|
||||
'id': item.pk,
|
||||
'name': str(item.name),
|
||||
'picture': get_picture(item.picture) if item.picture else None,
|
||||
'description': str(rich_text(item.description, safelinks=False)) if item.description else None,
|
||||
'has_variations': item.has_variations,
|
||||
'require_voucher': item.require_voucher,
|
||||
'order_min': item.min_per_order,
|
||||
'order_max': item.order_max if not item.has_variations else None,
|
||||
'price': price_dict(item.display_price) if not item.has_variations else None,
|
||||
'min_price': item.min_price if item.has_variations else None,
|
||||
'max_price': item.max_price if item.has_variations else None,
|
||||
'free_price': item.free_price,
|
||||
'avail': [
|
||||
item.cached_availability[0],
|
||||
item.cached_availability[1] if self.request.event.settings.show_quota_left else None
|
||||
] if not item.has_variations else None,
|
||||
'variations': [
|
||||
{
|
||||
'id': var.id,
|
||||
'value': str(var.value),
|
||||
'order_max': var.order_max,
|
||||
'description': str(rich_text(var.description, safelinks=False)) if var.description else None,
|
||||
'price': price_dict(var.display_price),
|
||||
'avail': [
|
||||
var.cached_availability[0],
|
||||
var.cached_availability[1] if self.request.event.settings.show_quota_left else None
|
||||
],
|
||||
} for var in item.available_variations
|
||||
]
|
||||
|
||||
} for item in g
|
||||
]
|
||||
})
|
||||
return grps, display_add_to_cart
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.subevent = None
|
||||
if request.event.has_subevents:
|
||||
if 'subevent' in kwargs:
|
||||
self.subevent = request.event.subevents.filter(pk=kwargs['subevent'], active=True).first()
|
||||
if not self.subevent:
|
||||
raise Http404()
|
||||
else:
|
||||
if 'subevent' in kwargs:
|
||||
raise Http404()
|
||||
|
||||
if 'lang' in request.GET and request.GET.get('lang') in [lc for lc, ll in settings.LANGUAGES]:
|
||||
with language(request.GET.get('lang')):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
else:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
data = {
|
||||
'currency': request.event.currency,
|
||||
'display_net_prices': request.event.settings.display_net_prices,
|
||||
'show_variations_expanded': request.event.settings.show_variations_expanded,
|
||||
'waiting_list_enabled': request.event.settings.waiting_list_enabled,
|
||||
'error': None,
|
||||
'cart_exists': False
|
||||
}
|
||||
|
||||
if 'cart_id' in request.GET and CartPosition.objects.filter(event=request.event, cart_id=request.GET.get('cart_id')).exists():
|
||||
data['cart_exists'] = True
|
||||
|
||||
ev = self.subevent or request.event
|
||||
fail = False
|
||||
|
||||
if not ev.presale_is_running:
|
||||
if ev.presale_has_ended:
|
||||
data['error'] = 'The presale period for this event is over.'
|
||||
elif request.event.settings.presale_start_show_date:
|
||||
data['error'] = 'The presale for this event will start on %(date)s at %(time)s.' % {
|
||||
'date': date_format(ev.presale_start, "SHORT_DATE_FORMAT"),
|
||||
'time': date_format(ev.presale_start, "TIME_FORMAT"),
|
||||
}
|
||||
else:
|
||||
data['error'] = 'The presale for this event has not yet started.'
|
||||
|
||||
self.voucher = None
|
||||
if 'voucher' in request.GET:
|
||||
try:
|
||||
self.voucher = request.event.vouchers.get(code=request.GET.get('voucher').strip())
|
||||
if self.voucher.redeemed >= self.voucher.max_usages:
|
||||
data['error'] = error_messages['voucher_redeemed']
|
||||
fail = True
|
||||
if self.voucher.valid_until is not None and self.voucher.valid_until < now():
|
||||
data['error'] = error_messages['voucher_expired']
|
||||
fail = True
|
||||
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=self.voucher) & Q(event=request.event) &
|
||||
(Q(expires__gte=now()) | Q(cart_id=get_or_create_cart_id(request)))
|
||||
)
|
||||
v_avail = self.voucher.max_usages - self.voucher.redeemed - redeemed_in_carts.count()
|
||||
|
||||
if v_avail < 1:
|
||||
data['error'] = error_messages['voucher_redeemed']
|
||||
fail = True
|
||||
except Voucher.DoesNotExist:
|
||||
data['error'] = error_messages['voucher_invalid']
|
||||
fail = True
|
||||
|
||||
if not fail and (ev.presale_is_running or request.event.settings.show_items_outside_presale_period):
|
||||
data['items_by_category'], data['display_add_to_cart'] = self._get_items()
|
||||
data['display_add_to_cart'] = data['display_add_to_cart'] and ev.presale_is_running
|
||||
else:
|
||||
data['items_by_category'] = []
|
||||
data['display_add_to_cart'] = False
|
||||
|
||||
vouchers_exist = self.request.event.get_cache().get('vouchers_exist')
|
||||
if vouchers_exist is None:
|
||||
vouchers_exist = self.request.event.vouchers.exists()
|
||||
self.request.event.get_cache().set('vouchers_exist', vouchers_exist)
|
||||
data['vouchers_exist'] = vouchers_exist
|
||||
|
||||
resp = JsonResponse(data)
|
||||
resp['Access-Control-Allow-Origin'] = '*'
|
||||
return resp
|
||||
Reference in New Issue
Block a user