Fix #277 -- Embeddable shop (#622)

* 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:
Raphael Michel
2017-10-28 21:54:27 +02:00
committed by GitHub
parent df7fbe5a66
commit 9767243a6d
56 changed files with 12819 additions and 317 deletions

View File

@@ -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={

View File

@@ -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)

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

View File

@@ -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))

View File

@@ -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 %}

View File

@@ -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 %}

View File

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

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

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

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 }}">

View File

@@ -0,0 +1,5 @@
{% load compress %}
{% load staticfiles %}
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/widget.scss" %}"/>
{% endcompress %}

View File

@@ -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'),
]

View File

@@ -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)

View File

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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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):

View File

@@ -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()

View 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