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
+1
View File
@@ -5,6 +5,7 @@ src/static/moment/* linguist-vendored
src/static/datetimepicker/* linguist-vendored src/static/datetimepicker/* linguist-vendored
src/static/colorpicker/* linguist-vendored src/static/colorpicker/* linguist-vendored
src/static/fileupload/* linguist-vendored src/static/fileupload/* linguist-vendored
src/static/vuejs/* linguist-vendored
src/static/charts/* linguist-vendored src/static/charts/* linguist-vendored
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored
+8
View File
@@ -427,6 +427,14 @@ Your {event} team"""))
'default': None, 'default': None,
'type': str 'type': str
}, },
'presale_widget_css_file': {
'default': None,
'type': str
},
'presale_widget_css_checksum': {
'default': None,
'type': str
},
'logo_image': { 'logo_image': {
'default': None, 'default': None,
'type': File 'type': File
+8 -1
View File
@@ -4,6 +4,7 @@ import bleach
import markdown import markdown
from bleach import DEFAULT_CALLBACKS from bleach import DEFAULT_CALLBACKS
from django import template from django import template
from django.conf import settings
from django.core import signing from django.core import signing
from django.urls import reverse from django.urls import reverse
from django.utils.http import is_safe_url from django.utils.http import is_safe_url
@@ -63,6 +64,12 @@ def safelink_callback(attrs, new=False):
return attrs return attrs
def abslink_callback(attrs, new=False):
attrs[None, 'href'] = urllib.parse.urljoin(settings.SITE_URL, attrs.get((None, 'href'), '/'))
attrs[None, 'target'] = '_blank'
return attrs
@register.filter @register.filter
def rich_text(text: str, **kwargs): def rich_text(text: str, **kwargs):
""" """
@@ -73,5 +80,5 @@ def rich_text(text: str, **kwargs):
markdown.markdown(text), markdown.markdown(text),
tags=ALLOWED_TAGS, tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES, attributes=ALLOWED_ATTRIBUTES,
), callbacks=DEFAULT_CALLBACKS + [safelink_callback]) ), callbacks=DEFAULT_CALLBACKS + ([safelink_callback] if kwargs.get('safelinks', True) else [abslink_callback]))
return mark_safe(body_md) return mark_safe(body_md)
+2
View File
@@ -33,6 +33,7 @@ class EventSlugBlacklistValidator(BlacklistValidator):
'api', 'api',
'events', 'events',
'csp_report', 'csp_report',
'widget',
] ]
@@ -53,4 +54,5 @@ class OrganizerSlugBlacklistValidator(BlacklistValidator):
'about', 'about',
'api', 'api',
'csp_report', 'csp_report',
'widget',
] ]
+6 -2
View File
@@ -51,6 +51,9 @@ class AsyncAction:
return self.get_result(request) return self.get_result(request)
return self.http_method_not_allowed(request) return self.http_method_not_allowed(request)
def _ajax_response_data(self):
return {}
def _return_ajax_result(self, res, timeout=.5): def _return_ajax_result(self, res, timeout=.5):
if not res.ready(): if not res.ready():
try: try:
@@ -59,10 +62,11 @@ class AsyncAction:
pass pass
ready = res.ready() ready = res.ready()
data = { data = self._ajax_response_data()
data.update({
'async_id': res.id, 'async_id': res.id,
'ready': ready 'ready': ready
} })
if ready: if ready:
if res.successful() and not isinstance(res.info, Exception): if res.successful() and not isinstance(res.info, Exception):
smes = self.get_success_message(res.info) smes = self.get_success_message(res.info)
+43 -2
View File
@@ -4,13 +4,13 @@ from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db.models import Q from django.db.models import Q
from django.utils.timezone import get_current_timezone_name from django.utils.timezone import get_current_timezone_name
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea from i18nfield.forms import I18nFormField, I18nTextarea
from pytz import common_timezones, timezone from pytz import common_timezones, timezone
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule from pretix.base.models import Event, Organizer, TaxRule
from pretix.base.models.event import EventMetaValue from pretix.base.models.event import EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.control.forms import ( from pretix.control.forms import (
ExtFileField, SlugWidget, SplitDateTimePickerWidget, ExtFileField, SlugWidget, SplitDateTimePickerWidget,
@@ -897,3 +897,44 @@ class TaxRuleForm(I18nModelForm):
class Meta: class Meta:
model = TaxRule model = TaxRule
fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country'] fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country']
class WidgetCodeForm(forms.Form):
subevent = forms.ModelChoiceField(
label=pgettext_lazy('subevent', "Date"),
required=True,
queryset=SubEvent.objects.none()
)
language = forms.ChoiceField(
label=_("Language"),
required=True,
choices=settings.LANGUAGES
)
voucher = forms.CharField(
label=_("Pre-selected voucher"),
required=False,
help_text=_("If set, the widget will show products as if this voucher has been entered and when a product is "
"bought via the widget, this voucher will be used. This can for example be used to provide "
"widgets that give discounts or unlock secret products.")
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)
if self.event.has_subevents:
self.fields['subevent'].queryset = self.event.subevents.all()
else:
del self.fields['subevent']
self.fields['language'].choices = [(l, n) for l, n in settings.LANGUAGES if l in self.event.settings.locales]
def clean_voucher(self):
v = self.cleaned_data.get('voucher')
if not v:
return
if not self.event.vouchers.filter(code=v).exists():
raise ValidationError(_('The given voucher code does not exist.'))
return v
@@ -75,6 +75,11 @@
{% trans "Permissions" %} {% trans "Permissions" %}
</a> </a>
</li> </li>
<li {% if "event.settings.widget" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.widget' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Widget" %}
</a>
</li>
{% endif %} {% endif %}
{% for nav in nav_event_settings %} {% for nav in nav_event_settings %}
<li {% if nav.active %}class="active"{% endif %}> <li {% if nav.active %}class="active"{% endif %}>
@@ -0,0 +1,63 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load bootstrap3 %}
{% load eventurl %}
{% block inside %}
<legend>{% trans "Widget" %}</legend>
<p>
{% blocktrans trimmed %}
The pretix widget is a way to embed your ticket shop into your event website. This way, your visitors can
buy their ticket right away without leaving your website.
{% endblocktrans %}
</p>
{% if valid %}
<p>
{% blocktrans trimmed %}
To embed the widget onto your website, simply copy the following code to the <code>&lt;head&gt;</code>
section of your website:
{% endblocktrans %}
</p>
<pre>&lt;link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" %}"&gt;
&lt;script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language %}" async&gt;&lt;/script&gt;</pre>
<p>
{% blocktrans trimmed %}
Then, copy the following code to the place of your website where you want the widget to show up:
{% endblocktrans %}
</p>
{% if form.cleaned_data.subevent %}
{% abseventurl request.event "presale:event.index" subevent=form.cleaned_data.subevent.pk as indexurl %}
{% else %}
{% abseventurl request.event "presale:event.index" as indexurl %}
{% endif %}
<pre>&lt;pretix-widget event="http://192.168.0.10:8000/democon/"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}&gt;&lt;/pretix-widget&gt;
&lt;noscript&gt;
&lt;div class="pretix-widget"&gt;
&lt;div class="pretix-widget-info-message"&gt;
{% blocktrans trimmed with a_attr='target="_blank" href="'|add:indexurl|add:'"'|safe %}
JavaScript is disabled in your browser. To access our ticket shop without javascript,
please &lt;a {{ a_attr }}&gt;click here&lt;/a&gt;.
{% endblocktrans %}
&lt;/div&gt;
&lt;/div&gt;
&lt;/noscript&gt;
</pre>
{% else %}
<p>
{% blocktrans trimmed %}
Using this form, you can generate a code to copy and paste to your website source.
{% endblocktrans %}
</p>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form form layout="control" %}
<div class="form-group">
<div class="col-md-offset-3 col-md-9">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Generate widget code" %}
</button>
</div>
</div>
</form>
{% endif %}
{% endblock %}
+1
View File
@@ -75,6 +75,7 @@ urlpatterns = [
url(r'^settings/tax/(?P<rule>\d+)/$', event.TaxUpdate.as_view(), name='event.settings.tax.edit'), url(r'^settings/tax/(?P<rule>\d+)/$', event.TaxUpdate.as_view(), name='event.settings.tax.edit'),
url(r'^settings/tax/add$', event.TaxCreate.as_view(), name='event.settings.tax.add'), url(r'^settings/tax/add$', event.TaxCreate.as_view(), name='event.settings.tax.add'),
url(r'^settings/tax/(?P<rule>\d+)/delete$', event.TaxDelete.as_view(), name='event.settings.tax.delete'), url(r'^settings/tax/(?P<rule>\d+)/delete$', event.TaxDelete.as_view(), name='event.settings.tax.delete'),
url(r'^settings/widget$', event.WidgetSettings.as_view(), name='event.settings.widget'),
url(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'), url(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'),
url(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'), url(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'),
url(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(), url(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(),
+31
View File
@@ -1,6 +1,7 @@
import re import re
from collections import OrderedDict from collections import OrderedDict
from datetime import timedelta from datetime import timedelta
from urllib.parse import urlsplit
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
@@ -36,10 +37,12 @@ from pretix.control.forms.event import (
CommentForm, DisplaySettingsForm, EventMetaValueForm, EventSettingsForm, CommentForm, DisplaySettingsForm, EventMetaValueForm, EventSettingsForm,
EventUpdateForm, InvoiceSettingsForm, MailSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm,
PaymentSettingsForm, ProviderForm, TaxRuleForm, TicketSettingsForm, PaymentSettingsForm, ProviderForm, TaxRuleForm, TicketSettingsForm,
WidgetCodeForm,
) )
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import nav_event_settings from pretix.control.signals import nav_event_settings
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import build_absolute_uri
from pretix.multidomain.urlreverse import get_domain
from pretix.presale.style import regenerate_css from pretix.presale.style import regenerate_css
from . import CreateView, UpdateView from . import CreateView, UpdateView
@@ -989,3 +992,31 @@ class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, DeleteView
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
context['possible'] = self.object.allow_delete() context['possible'] = self.object.allow_delete()
return context return context
class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/event/widget.html'
permission = 'can_change_event_settings'
form_class = WidgetCodeForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
return kwargs
def form_valid(self, form):
ctx = self.get_context_data()
ctx['form'] = form
ctx['valid'] = True
return self.render_to_response(ctx)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['urlprefix'] = settings.SITE_URL
domain = get_domain(self.request.organizer)
if domain:
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
domain = '%s:%d' % (domain, siteurlsplit.port)
ctx['urlprefix'] = '%s://%s' % (siteurlsplit.scheme, domain)
return ctx
+16 -3
View File
@@ -1,10 +1,12 @@
import json import json
import logging import logging
import urllib.parse
from collections import OrderedDict from collections import OrderedDict
import paypalrestsdk import paypalrestsdk
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.core import signing
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import ugettext as __, ugettext_lazy as _ from django.utils.translation import ugettext as __, ugettext_lazy as _
@@ -89,14 +91,18 @@ class Paypal(BasePaymentProvider):
def checkout_prepare(self, request, cart): def checkout_prepare(self, request, cart):
self.init_api() self.init_api()
kwargs = {}
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
payment = paypalrestsdk.Payment({ payment = paypalrestsdk.Payment({
'intent': 'sale', 'intent': 'sale',
'payer': { 'payer': {
"payment_method": "paypal", "payment_method": "paypal",
}, },
"redirect_urls": { "redirect_urls": {
"return_url": build_absolute_uri(request.event, 'plugins:paypal:return'), "return_url": build_absolute_uri(request.event, 'plugins:paypal:return', kwargs=kwargs),
"cancel_url": build_absolute_uri(request.event, 'plugins:paypal:abort'), "cancel_url": build_absolute_uri(request.event, 'plugins:paypal:abort', kwargs=kwargs),
}, },
"transactions": [ "transactions": [
{ {
@@ -131,7 +137,14 @@ class Paypal(BasePaymentProvider):
request.session['payment_paypal_id'] = payment.id request.session['payment_paypal_id'] = payment.id
for link in payment.links: for link in payment.links:
if link.method == "REDIRECT" and link.rel == "approval_url": if link.method == "REDIRECT" and link.rel == "approval_url":
return str(link.href) if request.session.get('iframe_session', False):
signer = signing.Signer(salt='safe-redirect')
return (
build_absolute_uri(request.event, 'plugins:paypal:redirect') + '?url=' +
urllib.parse.quote(signer.sign(link.href))
)
else:
return str(link.href)
else: else:
messages.error(request, _('We had trouble communicating with PayPal')) messages.error(request, _('We had trouble communicating with PayPal'))
logger.error('Error on creating payment: ' + str(payment.error)) logger.error('Error on creating payment: ' + str(payment.error))
@@ -0,0 +1,32 @@
{% load compress %}
{% load i18n %}
{% load staticfiles %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank">
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>
+6 -1
View File
@@ -2,12 +2,17 @@ from django.conf.urls import include, url
from pretix.multidomain import event_url from pretix.multidomain import event_url
from .views import abort, refund, success, webhook from .views import abort, redirect_view, refund, success, webhook
event_patterns = [ event_patterns = [
url(r'^paypal/', include([ url(r'^paypal/', include([
url(r'^abort/$', abort, name='abort'), url(r'^abort/$', abort, name='abort'),
url(r'^return/$', success, name='return'), url(r'^return/$', success, name='return'),
url(r'^redirect/$', redirect_view, name='redirect'),
url(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
url(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'),
event_url(r'^webhook/$', webhook, name='webhook', require_live=False), event_url(r'^webhook/$', webhook, name='webhook', require_live=False),
])), ])),
] ]
+27 -6
View File
@@ -3,11 +3,13 @@ import logging
import paypalrestsdk import paypalrestsdk
from django.contrib import messages from django.contrib import messages
from django.core import signing
from django.db import transaction from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
@@ -17,12 +19,25 @@ from pretix.control.permissions import event_permission_required
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.paypal.payment import Paypal from pretix.plugins.paypal.payment import Paypal
from pretix.plugins.stripe.models import ReferencedStripeObject from pretix.plugins.stripe.models import ReferencedStripeObject
from pretix.presale.utils import event_view
logger = logging.getLogger('pretix.plugins.paypal') logger = logging.getLogger('pretix.plugins.paypal')
@event_view(require_live=False) @xframe_options_exempt
def redirect_view(request, *args, **kwargs):
signer = signing.Signer(salt='safe-redirect')
try:
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
r = render(request, 'pretixplugins/paypal/redirect.html', {
'url': url,
})
r._csp_ignore = True
return r
def success(request, *args, **kwargs): def success(request, *args, **kwargs):
pid = request.GET.get('paymentId') pid = request.GET.get('paymentId')
token = request.GET.get('token') token = request.GET.get('token')
@@ -30,6 +45,10 @@ def success(request, *args, **kwargs):
request.session['payment_paypal_token'] = token request.session['payment_paypal_token'] = token
request.session['payment_paypal_payer'] = payer request.session['payment_paypal_payer'] = payer
urlkwargs = {}
if 'cart_namespace' in kwargs:
urlkwargs['cart_namespace'] = kwargs['cart_namespace']
if request.session.get('payment_paypal_order'): if request.session.get('payment_paypal_order'):
order = Order.objects.get(pk=request.session.get('payment_paypal_order')) order = Order.objects.get(pk=request.session.get('payment_paypal_order'))
else: else:
@@ -44,7 +63,8 @@ def success(request, *args, **kwargs):
else: else:
messages.error(request, _('Invalid response from PayPal received.')) messages.error(request, _('Invalid response from PayPal received.'))
logger.error('Session did not contain payment_paypal_id') logger.error('Session did not contain payment_paypal_id')
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'})) urlkwargs['step'] = 'payment'
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
if order: if order:
return redirect(eventreverse(request.event, 'presale:event.order', kwargs={ return redirect(eventreverse(request.event, 'presale:event.order', kwargs={
@@ -52,7 +72,8 @@ def success(request, *args, **kwargs):
'secret': order.secret 'secret': order.secret
}) + ('?paid=yes' if order.status == Order.STATUS_PAID else '')) }) + ('?paid=yes' if order.status == Order.STATUS_PAID else ''))
else: else:
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'confirm'})) urlkwargs['step'] = 'confirm'
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
def abort(request, *args, **kwargs): def abort(request, *args, **kwargs):
+3
View File
@@ -8,9 +8,11 @@ from django.db import transaction
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views import View from django.views import View
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
@@ -193,6 +195,7 @@ class StripeOrderView:
return self.request.event.get_payment_providers()[self.order.payment_provider] return self.request.event.get_payment_providers()[self.order.payment_provider]
@method_decorator(xframe_options_exempt, 'dispatch')
class ReturnView(StripeOrderView, View): class ReturnView(StripeOrderView, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
prov = self.pprov prov = self.pprov
+16 -7
View File
@@ -70,20 +70,26 @@ class BaseCheckoutFlowStep:
def post(self, request): def post(self, request):
return HttpResponseNotAllowed([]) return HttpResponseNotAllowed([])
def get_step_url(self): def get_step_url(self, request):
return eventreverse(self.event, 'presale:event.checkout', kwargs={'step': self.identifier}) 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): def get_prev_url(self, request):
prev = self.get_prev_applicable(request) prev = self.get_prev_applicable(request)
if not prev: 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: else:
return prev.get_step_url() return prev.get_step_url(request)
def get_next_url(self, request): def get_next_url(self, request):
n = self.get_next_applicable(request) n = self.get_next_applicable(request)
if n: if n:
return n.get_step_url() return n.get_step_url(request)
@cached_property @cached_property
def cart_session(self): def cart_session(self):
@@ -225,6 +231,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['forms'] = self.forms ctx['forms'] = self.forms
ctx['cart'] = self.get_cart()
return ctx return ctx
def get_success_message(self, value): def get_success_message(self, value):
@@ -234,7 +241,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
return self.get_next_url(self.request) return self.get_next_url(self.request)
def get_error_url(self): def get_error_url(self):
return self.get_step_url() return self.get_step_url(self.request)
def get(self, request): def get(self, request):
self.request = request self.request = request
@@ -382,6 +389,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
ctx['contact_form'] = self.contact_form ctx['contact_form'] = self.contact_form
ctx['invoice_form'] = self.invoice_form ctx['invoice_form'] = self.invoice_form
ctx['reverse_charge_relevant'] = self.eu_reverse_charge_relevant ctx['reverse_charge_relevant'] = self.eu_reverse_charge_relevant
ctx['cart'] = self.get_cart()
return ctx return ctx
@@ -436,6 +444,7 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
ctx['selected'] = self.request.POST.get('payment', self.cart_session.get('payment', '')) ctx['selected'] = self.request.POST.get('payment', self.cart_session.get('payment', ''))
if len(self.provider_forms) == 1: if len(self.provider_forms) == 1:
ctx['selected'] = self.provider_forms[0]['provider'].identifier ctx['selected'] = self.provider_forms[0]['provider'].identifier
ctx['cart'] = self.get_cart()
return ctx return ctx
@cached_property @cached_property
@@ -557,7 +566,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
return super().get_error_message(exception) return super().get_error_message(exception)
def get_error_url(self): def get_error_url(self):
return self.get_step_url() return self.get_step_url(self.request)
def get_order_url(self, order): def get_order_url(self, order):
return eventreverse(self.request.event, 'presale:event.order.pay.complete', kwargs={ return eventreverse(self.request.event, 'presale:event.order.pay.complete', kwargs={
+3
View File
@@ -56,6 +56,9 @@ def contextprocessor(request):
ctx['event'] = request.event ctx['event'] = request.event
ctx['languages'] = [get_language_info(code) for code in request.event.settings.locales] 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 hasattr(request, 'organizer'):
if request.organizer.settings.presale_css_file and not hasattr(request, 'event'): if request.organizer.settings.presale_css_file and not hasattr(request, 'event'):
ctx['css_file'] = default_storage.url(request.organizer.settings.presale_css_file) 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 django.core.management.base import BaseCommand
from pretix.base.models import Event_SettingsStore, Organizer_SettingsStore 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 from ...style import regenerate_css, regenerate_organizer_css
class Command(BaseCommand): class Command(BaseCommand):
help = "Re-generate all custom stylesheets" help = "Re-generate all custom stylesheets and scripts"
def handle(self, *args, **options): def handle(self, *args, **options):
for es in Event_SettingsStore.objects.filter(key="presale_css_file"): for es in Event_SettingsStore.objects.filter(key="presale_css_file"):
regenerate_css.apply_async(args=(es.object_id,)) regenerate_css.apply_async(args=(es.object_id,))
for es in Organizer_SettingsStore.objects.filter(key="presale_css_file"): for es in Organizer_SettingsStore.objects.filter(key="presale_css_file"):
regenerate_organizer_css.apply_async(args=(es.object_id,)) 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)
+26 -12
View File
@@ -20,7 +20,7 @@ logger = logging.getLogger('pretix.presale.style')
affected_keys = ['primary_font', 'primary_color'] 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') sassdir = os.path.join(settings.STATIC_ROOT, 'pretixpresale/scss')
def static(path): def static(path):
@@ -41,14 +41,14 @@ def compile_scss(object):
sassrules.append('$brand-primary: {};'.format(object.settings.get('primary_color'))) sassrules.append('$brand-primary: {};'.format(object.settings.get('primary_color')))
font = object.settings.get('primary_font') font = object.settings.get('primary_font')
if font != 'Open Sans': if font != 'Open Sans' and fonts:
sassrules.append(get_font_stylesheet(font)) sassrules.append(get_font_stylesheet(font))
sassrules.append( sassrules.append(
'$font-family-sans-serif: "{}", "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !default'.format( '$font-family-sans-serif: "{}", "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !default'.format(
font font
)) ))
sassrules.append('@import "main.scss";') sassrules.append('@import "{}";'.format(file))
cf = dict(django_libsass.CUSTOM_FUNCTIONS) cf = dict(django_libsass.CUSTOM_FUNCTIONS)
cf['static'] = static cf['static'] = static
@@ -64,32 +64,46 @@ def compile_scss(object):
@app.task(base=ProfiledTask) @app.task(base=ProfiledTask)
def regenerate_css(event_id: int): def regenerate_css(event_id: int):
event = Event.objects.select_related('organizer').get(pk=event_id) event = Event.objects.select_related('organizer').get(pk=event_id)
css, checksum = compile_scss(event)
fname = '{}/{}/presale.{}.css'.format( # main.scss
event.organizer.slug, event.slug, checksum[:16] css, checksum = compile_scss(event)
) fname = '{}/{}/presale.{}.css'.format(event.organizer.slug, event.slug, checksum[:16])
if event.settings.get('presale_css_checksum', '') != checksum: if event.settings.get('presale_css_checksum', '') != checksum:
newname = default_storage.save(fname, ContentFile(css.encode('utf-8'))) newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
event.settings.set('presale_css_file', newname) event.settings.set('presale_css_file', newname)
event.settings.set('presale_css_checksum', checksum) 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) @app.task(base=ProfiledTask)
def regenerate_organizer_css(organizer_id: int): def regenerate_organizer_css(organizer_id: int):
organizer = Organizer.objects.get(pk=organizer_id) organizer = Organizer.objects.get(pk=organizer_id)
# main.scss
css, checksum = compile_scss(organizer) 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: if organizer.settings.get('presale_css_checksum', '') != checksum:
newname = default_storage.save(fname, ContentFile(css.encode('utf-8'))) newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
organizer.settings.set('presale_css_file', newname) organizer.settings.set('presale_css_file', newname)
organizer.settings.set('presale_css_checksum', checksum) 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( non_inherited_events = set(Event_SettingsStore.objects.filter(
object__organizer=organizer, key__in=affected_keys object__organizer=organizer, key__in=affected_keys
).values_list('object_id', flat=True)) ).values_list('object_id', flat=True))
@@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
{% load safelink %} {% load safelink %}
{% safelink "https://pretix.eu" as pretixurl %} {% 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 %} {% blocktrans trimmed %}
powered by <a {{ a_attr }}>pretix</a> powered by <a {{ a_attr }}>pretix</a>
{% endblocktrans %} {% endblocktrans %}
@@ -8,6 +8,7 @@
{% block title %}{% endblock %}{% if url_name != "event.index" %} :: {% endif %}{{ event.name }} {% block title %}{% endblock %}{% if url_name != "event.index" %} :: {% endif %}{{ event.name }}
{% endblock %} {% endblock %}
{% block above %} {% block above %}
<script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script>
{% if not event.live %} {% if not event.live %}
<div class="offline-banner"> <div class="offline-banner">
<div class="container"> <div class="container">
@@ -24,12 +25,13 @@
<div class="page-header"> <div class="page-header">
<div class="pull-left"> <div class="pull-left">
{% if event_logo %} {% 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" /> <img src="{{ event_logo|thumbnail_url:'logo' }}" alt="{{ event.name }}" class="event-logo" />
</a> </a>
{% else %} {% else %}
<h1> <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 %} {% if not event.has_subevents %}
<small>{{ event.get_date_range_display }}</small> <small>{{ event.get_date_range_display }}</small>
{% endif %} {% endif %}
@@ -1,9 +1,7 @@
{% extends "pretixpresale/event/base.html" %} {% extends "pretixpresale/event/checkout_base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block title %}{% trans "Checkout" %}{% endblock %} {% block inner %}
{% block content %}
<h2>{% trans "Checkout" %}</h2>
<p> <p>
{% trans "For some of the products in your cart, you can choose additional options before you continue." %} {% trans "For some of the products in your cart, you can choose additional options before you continue." %}
</p> </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 %} {% csrf_token %}
<div class="panel panel-primary cart"> <div class="panel panel-primary cart">
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right"> <div class="pull-right cart-modify">
<a href=" <a href="{% eventurl request.event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}">
{% eventurl request.event "presale:event.index" %}">
<span class="fa fa-edit"></span> <span class="fa fa-edit"></span>
{% trans "Modify" %} {% trans "Modify" %}
</a> </a>
</div> </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"> <h3 class="panel-title">
<i class="fa fa-shopping-cart"></i>
{% trans "Your cart" %} {% trans "Your cart" %}
</h3> </h3>
</div> </div>
@@ -27,7 +35,7 @@
<div class="cart-row row"> <div class="cart-row row">
<div class="col-md-6 col-xs-12"> <div class="col-md-6 col-xs-12">
<em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}"> <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 %} {% blocktrans trimmed with minutes=cart.minutes_left %}
The items in your cart are reserved for you for {{ minutes }} minutes. The items in your cart are reserved for you for {{ minutes }} minutes.
{% endblocktrans %} {% endblocktrans %}
@@ -1,9 +1,7 @@
{% extends "pretixpresale/event/base.html" %} {% extends "pretixpresale/event/checkout_base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block title %}{% trans "Checkout" %}{% endblock %} {% block inner %}
{% block content %}
<h2>{% trans "Checkout" %}</h2>
<p>{% trans "Please select how you want to pay." %}</p> <p>{% trans "Please select how you want to pay." %}</p>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
@@ -1,9 +1,7 @@
{% extends "pretixpresale/event/base.html" %} {% extends "pretixpresale/event/checkout_base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block title %}{% trans "Checkout" %}{% endblock %} {% block inner %}
{% block content %}
<h2>{% trans "Checkout" %}</h2>
<p>{% trans "Before we continue, we need you to answer some questions." %}</p> <p>{% trans "Before we continue, we need you to answer some questions." %}</p>
<p class="required-legend"> <p class="required-legend">
{% blocktrans trimmed %} {% blocktrans trimmed %}
@@ -5,7 +5,7 @@
<strong>{% trans "SOLD OUT" %}</strong> <strong>{% trans "SOLD OUT" %}</strong>
{% if event.settings.waiting_list_enabled %} {% if event.settings.waiting_list_enabled %}
<br/> <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> <span class="fa fa-plus-circle"></span>
{% trans "Waiting list" %} {% trans "Waiting list" %}
</a> </a>
@@ -19,7 +19,7 @@
</strong> </strong>
{% if event.settings.waiting_list_enabled %} {% if event.settings.waiting_list_enabled %}
<br/> <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> <span class="fa fa-plus-circle"></span>
{% trans "Waiting list" %} {% trans "Waiting list" %}
</a> </a>
@@ -79,7 +79,7 @@
{% else %} {% else %}
<div class="count"> <div class="count">
{% if editable %} {% 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> method="post" data-asynctask>
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ line.id }}" /> <input type="hidden" name="id" value="{{ line.id }}" />
@@ -90,7 +90,7 @@
{% endif %} {% endif %}
{{ line.count }} {{ line.count }}
{% if editable %} {% 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> method="post" data-asynctask>
<input type="hidden" name="subevent" value="{{ line.subevent_id|default_if_none:"" }}" /> <input type="hidden" name="subevent" value="{{ line.subevent_id|default_if_none:"" }}" />
{% csrf_token %} {% csrf_token %}
@@ -17,45 +17,64 @@
{% if show_cart %} {% if show_cart %}
<div class="panel panel-primary cart"> <div class="panel panel-primary cart">
<div class="panel-heading"> <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>
<div class="panel-body"> <div class="panel-collapse collapse in" id="cart">
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %} <div class="panel-body">
<em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}"> {% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
{% if cart.minutes_left > 0 %} <em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
{% blocktrans trimmed with minutes=cart.minutes_left %} {% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
The items in your cart are reserved for you for {{ minutes }} minutes. {% blocktrans trimmed with minutes=cart.minutes_left %}
{% endblocktrans %} The items in your cart are reserved for you for {{ minutes }} minutes.
{% else %} {% endblocktrans %}
{% trans "The items in your cart are no longer reserved for you." %} {% else %}
{% endif %} {% trans "The items in your cart are no longer reserved for you." %}
</em> {% endif %}
<div class="row checkout-button-row"> </em>
<div class="col-md-4 col-xs-12"> <div class="row checkout-button-row">
<form method="post" data-asynctask action="{% eventurl request.event "presale:event.cart.clear" %}"> <div class="col-md-4 col-xs-12">
{% csrf_token %} <form method="post" data-asynctask action="{% eventurl request.event "presale:event.cart.clear" cart_namespace=cart_namespace %}">
<button class="btn btn-block btn-default btn-lg" type="submit"> {% csrf_token %}
<i class="fa fa-close"></i> {% trans "Empty cart" %}</button> <button class="btn btn-block btn-default btn-lg" type="submit">
</form> <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>
<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> </div>
</div> </div>
{% endif %} {% endif %}
{% if event.has_subevents %} {% if event.has_subevents and not cart_namespace %}
{% if subevent %} {% if subevent %}
<a class="subevent-toggle"> <a class="subevent-toggle">
{% trans "View other date" %} {% trans "View other date" %}
@@ -75,7 +94,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if frontpage_text %} {% if frontpage_text and not cart_namespace %}
<div> <div>
{{ frontpage_text|rich_text }} {{ frontpage_text|rich_text }}
</div> </div>
@@ -99,60 +118,63 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<div> {% if not cart_namespace %}
{% if ev.location %} <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"> <div class="info-row">
<span class="fa fa-map-marker fa-fw"></span> <span class="fa fa-clock-o fa-fw"></span>
<p> <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> </p>
</div> </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>
</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 %} {% if ev.presale_is_running or event.settings.show_items_outside_presale_period %}
<form method="post" data-asynctask <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 %} {% csrf_token %}
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" /> <input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" />
{% for tup in items_by_category %} {% for tup in items_by_category %}
@@ -392,7 +414,7 @@
{% if vouchers_exist %} {% if vouchers_exist %}
<section class="front-page"> <section class="front-page">
<h3>{% trans "Redeem a voucher" %}</h3> <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="row-voucher">
<div class="col-md-8 col-sm-6 col-xs-12"> <div class="col-md-8 col-sm-6 col-xs-12">
<div class="input-group"> <div class="input-group">
@@ -412,25 +434,27 @@
</form> </form>
</section> </section>
{% endif %} {% endif %}
{% eventsignal event "pretix.presale.signals.front_page_bottom" %} {% if not cart_namespace %}
<section class="front-page"> {% eventsignal event "pretix.presale.signals.front_page_bottom" %}
<h3>{% trans "If you already ordered a ticket" %}</h3> <section class="front-page">
<div> <h3>{% trans "If you already ordered a ticket" %}</h3>
<div class="col-md-8 col-xs-12"> <div>
<p> <div class="col-md-8 col-xs-12">
{% blocktrans trimmed %} <p>
If you want to see or change the status and details of your order, click on the link in one of the {% blocktrans trimmed %}
emails we sent you during the order process. If you cannot find the link, click on the If you want to see or change the status and details of your order, click on the link in one of the
following button to request the link to your order to be sent to you again. emails we sent you during the order process. If you cannot find the link, click on the
{% endblocktrans %} following button to request the link to your order to be sent to you again.
</p> {% 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>
<div class="col-md-4 col-xs-12 text-right"> </section>
<a class="btn btn-block btn-primary" href="{% eventurl event "presale:event.resend_link" %}"> {% endif %}
{% trans "Resend order links" %}
</a>
</div>
<div class="clearfix"></div>
</div>
</section>
{% endblock %} {% endblock %}
@@ -22,10 +22,15 @@
{% else %} {% else %}
<p>{% trans "We successfully received your payment. See below for details." %}</p> <p>{% trans "We successfully received your payment. See below for details." %}</p>
{% endif %} {% 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 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. your details later. We also sent you an email containing the link to the address you specified.
{% endblocktrans %}</p> {% 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 class="clearfix"></div>
</div> </div>
{% endif %} {% endif %}
@@ -19,7 +19,7 @@
</p> </p>
{% if event.presale_is_running or event.settings.show_items_outside_presale_period %} {% if event.presale_is_running or event.settings.show_items_outside_presale_period %}
<form method="post" data-asynctask <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 %} {% csrf_token %}
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" /> <input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" />
<input type="hidden" name="_voucher_code" value="{{ voucher.code }}"> <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 %}
+35 -8
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.cart
import pretix.presale.views.checkout import pretix.presale.views.checkout
@@ -9,23 +10,44 @@ import pretix.presale.views.organizer
import pretix.presale.views.robots import pretix.presale.views.robots
import pretix.presale.views.user import pretix.presale.views.user
import pretix.presale.views.waiting import pretix.presale.views.waiting
import pretix.presale.views.widget
# This is not a valid Django URL configuration, as the final # This is not a valid Django URL configuration, as the final
# configuration is done by the pretix.multidomain package. # configuration is done by the pretix.multidomain package.
event_patterns = [ frame_wrapped_urls = [
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'), 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/clear$', pretix.presale.views.cart.CartClear.as_view(), name='event.cart.clear'),
url(r'^cart/answer/(?P<answer>[^/]+)/$', url(r'^cart/answer/(?P<answer>[^/]+)/$',
pretix.presale.views.cart.AnswerDownload.as_view(), pretix.presale.views.cart.AnswerDownload.as_view(),
name='event.cart.download.answer'), 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'^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(), url(r'^checkout/(?P<step>[^/]+)/$', pretix.presale.views.checkout.CheckoutView.as_view(),
name='event.checkout'), 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'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(), url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
name='event.order'), name='event.order'),
@@ -71,8 +93,12 @@ event_patterns = [
pretix.presale.views.event.EventIcalDownload.as_view(), pretix.presale.views.event.EventIcalDownload.as_view(),
name='event.ical.download'), name='event.ical.download'),
url(r'^auth/$', pretix.presale.views.event.EventAuth.as_view(), name='event.auth'), 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 = [ organizer_patterns = [
@@ -85,4 +111,5 @@ organizer_patterns = [
locale_patterns = [ locale_patterns = [
url(r'^locale/set$', pretix.presale.views.locale.LocaleSet.as_view(), name='locale.set'), 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'^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'),
] ]
+53 -1
View File
@@ -1,13 +1,18 @@
from collections import defaultdict from collections import defaultdict
from datetime import timedelta from datetime import datetime, timedelta
from functools import wraps
from itertools import groupby from itertools import groupby
from django.conf import settings
from django.db.models import Sum from django.db.models import Sum
from django.utils.decorators import available_attrs
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from pretix.base.i18n import language
from pretix.base.models import CartPosition, InvoiceAddress, OrderPosition from pretix.base.models import CartPosition, InvoiceAddress, OrderPosition
from pretix.base.services.cart import get_fees from pretix.base.services.cart import get_fees
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.signals import question_form_fields from pretix.presale.signals import question_form_fields
@@ -129,9 +134,11 @@ class CartMixin:
try: try:
first_expiry = min(p.expires for p in positions) if positions else now() first_expiry = min(p.expires for p in positions) if positions else now()
minutes_left = max(first_expiry - now(), timedelta()).seconds // 60 minutes_left = max(first_expiry - now(), timedelta()).seconds // 60
seconds_left = max(first_expiry - now(), timedelta()).seconds % 60
except AttributeError: except AttributeError:
first_expiry = None first_expiry = None
minutes_left = None minutes_left = None
seconds_left = None
return { return {
'positions': positions, 'positions': positions,
@@ -142,6 +149,7 @@ class CartMixin:
'fees': fees, 'fees': fees,
'answers': answers, 'answers': answers,
'minutes_left': minutes_left, 'minutes_left': minutes_left,
'seconds_left': seconds_left,
'first_expiry': first_expiry, 'first_expiry': first_expiry,
} }
@@ -182,9 +190,53 @@ class EventViewMixin:
context['event'] = self.request.event context['event'] = self.request.event
return context 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: class OrganizerViewMixin:
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['organizer'] = self.request.organizer context['organizer'] = self.request.organizer
return context 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)
+82 -106
View File
@@ -2,37 +2,47 @@ import mimetypes
import os import os
from django.contrib import messages 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.http import FileResponse, Http404, JsonResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.utils import translation from django.utils import translation
from django.utils.crypto import get_random_string 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.functional import cached_property
from django.utils.http import is_safe_url
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from pretix.base.models import ( from pretix.base.models import (
CartPosition, InvoiceAddress, ItemVariation, QuestionAnswer, Quota, CartPosition, InvoiceAddress, QuestionAnswer, SubEvent, Voucher,
SubEvent, Voucher,
) )
from pretix.base.services.cart import ( from pretix.base.services.cart import (
CartError, add_items_to_cart, clear_cart, remove_cart_position, CartError, add_items_to_cart, clear_cart, remove_cart_position,
) )
from pretix.multidomain.urlreverse import eventreverse 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.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 from pretix.presale.views.robots import NoSearchIndexViewMixin
class CartActionMixin: class CartActionMixin:
def get_next_url(self): 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') return self.request.GET.get('next')
else: 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): def get_success_url(self, value=None):
return self.get_next_url() return self.get_next_url()
@@ -129,25 +139,54 @@ class CartActionMixin:
return items return items
def create_empty_cart_id(request): def generate_cart_id(prefix=''):
current_id = request.session.get('current_cart_event_{}'.format(request.event.pk)) while True:
if current_id and current_id in request.session.get('carts', {}): new_id = prefix + get_random_string(length=32 - len(prefix))
del request.session['carts'][current_id] if not CartPosition.objects.filter(cart_id=new_id).exists():
del request.session['current_cart_event_{}'.format(request.event.pk)] return new_id
return get_or_create_cart_id(request)
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): 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', {}): if current_id and current_id in request.session.get('carts', {}):
return current_id return current_id
else: else:
cart_data = {} cart_data = {}
if prefix and 'take_cart_id' in request.GET:
while True: if CartPosition.objects.filter(event=request.event, cart_id=request.GET.get('take_cart_id')).exists():
new_id = get_random_string(length=32) new_id = request.GET.get('take_cart_id')
if not CartPosition.objects.filter(cart_id=new_id).exists(): else:
break new_id = generate_cart_id(prefix=prefix)
else:
new_id = generate_cart_id(prefix=prefix)
# Migrate legacy data # Migrate legacy data
# TODO: This is for the upgrade 1.7→1.8. We should remove this around April 2018 # 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: if 'carts' not in request.session:
request.session['carts'] = {} request.session['carts'] = {}
request.session['carts'][new_id] = cart_data if new_id not in request.session['carts']:
request.session['current_cart_event_{}'.format(request.event.pk)] = new_id request.session['carts'][new_id] = cart_data
request.session[session_keyname] = new_id
return new_id return new_id
@@ -176,6 +216,7 @@ def cart_session(request):
return request.session['carts'][cart_id] return request.session['carts'][cart_id]
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View): class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
task = remove_cart_position task = remove_cart_position
known_errortypes = ['CartError'] known_errortypes = ['CartError']
@@ -198,6 +239,7 @@ class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
return redirect(self.get_error_url()) return redirect(self.get_error_url())
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View): class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
task = clear_cart task = clear_cart
known_errortypes = ['CartError'] 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()) 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): class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
task = add_items_to_cart task = add_items_to_cart
known_errortypes = ['CartError'] known_errortypes = ['CartError']
@@ -216,6 +261,13 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
def get_success_message(self, value): def get_success_message(self, value):
return _('The products have been successfully added to your cart.') 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): def post(self, request, *args, **kwargs):
items = self._items_from_post_data() items = self._items_from_post_data()
if items: if items:
@@ -230,6 +282,8 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
return redirect(self.get_error_url()) 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): class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
template_name = "pretixpresale/event/voucher.html" 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 context['max_times'] = self.voucher.max_usages - self.voucher.redeemed
# Fetch all items # Fetch all items
items = self.request.event.items.all().filter( items, display_add_to_cart = get_grouped_items(self.request.event, self.subevent,
Q(active=True) voucher=self.voucher)
& 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)
)
vouchq = Q(hide_without_voucher=False) # Calculate how many options the user still has. If there is only one option, we can
# check the box right away ;)
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]
context['options'] = sum([(len(item.available_variations) if item.has_variations else 1) context['options'] = sum([(len(item.available_variations) if item.has_variations else 1)
for item in items]) for item in items])
@@ -359,7 +334,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
except Voucher.DoesNotExist: except Voucher.DoesNotExist:
err = error_messages['voucher_invalid'] err = error_messages['voucher_invalid']
else: 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: if request.event.presale_start and now() < request.event.presale_start:
err = error_messages['not_started'] err = error_messages['not_started']
@@ -379,11 +354,12 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
if err: if err:
messages.error(request, _(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) return super().dispatch(request, *args, **kwargs)
@method_decorator(xframe_options_exempt, 'dispatch')
class AnswerDownload(EventViewMixin, View): class AnswerDownload(EventViewMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
answid = kwargs.get('answer') answid = kwargs.get('answer')
+19 -8
View File
@@ -1,6 +1,7 @@
from django.contrib import messages from django.contrib import messages
from django.http import Http404 from django.http import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import View 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.base.signals import validate_cart
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.checkoutflow import get_checkout_flow 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): 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 self.request = request
if not get_cart(request) and "async_id" not in request.GET: if not get_cart(request) and "async_id" not in request.GET:
messages.error(request, _("Your cart is empty")) 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: if not request.event.presale_is_running:
messages.error(request, _("The presale for this event is over or has not yet started.")) 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 cart_error = None
try: try:
@@ -37,14 +48,14 @@ class CheckoutView(View):
continue continue
if step.requires_valid_cart and cart_error: if step.requires_valid_cart and cart_error:
messages.error(request, str(cart_error)) messages.error(request, str(cart_error))
return redirect(previous_step.get_step_url() if previous_step return redirect(previous_step.get_step_url(request) if previous_step
else eventreverse(self.request.event, 'presale:event.index')) else self.get_index_url(request))
if 'step' not in kwargs: 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', '')) 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): 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 is_selected:
if request.method.lower() in self.http_method_names: if request.method.lower() in self.http_method_names:
handler = getattr(step, request.method.lower(), self.http_method_not_allowed) handler = getattr(step, request.method.lower(), self.http_method_not_allowed)
+69 -17
View File
@@ -17,7 +17,7 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView 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.base.models.event import SubEvent
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.ical import get_ical from pretix.presale.ical import get_ical
@@ -25,7 +25,10 @@ from pretix.presale.views.organizer import (
add_subevents_for_days, weeks_for_template, 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 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( items = event.items.all().filter(
Q(active=True) Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now())) & Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now())) & Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(hide_without_voucher=False)
& ~Q(category__is_addon=True) & ~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 'category', 'tax_rule', # for re-grouping
).prefetch_related( ).prefetch_related(
Prefetch('quotas', Prefetch('quotas',
@@ -81,36 +93,74 @@ def get_grouped_items(event, subevent=None):
var_price_override = {} var_price_override = {}
for item in items: 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) max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
if not item.has_variations: if not item.has_variations:
item._remove = not bool(item._subevent_quotas) 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 voucher and (voucher.allow_ignore_quota or voucher.block_quota):
if item.cached_availability[1] is not None else sys.maxsize, item.cached_availability = (
max_per_order) Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
price = item.default_price )
item.display_price = item.tax(item_price_override.get(item.pk, price)) 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 display_add_to_cart = display_add_to_cart or item.order_max > 0
else: else:
for var in item.available_variations: for var in item.available_variations:
var.cached_availability = list(var.check_quotas(subevent=subevent, _cache=quota_cache)) if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
var.order_max = min(var.cached_availability[1] var.cached_availability = (
if var.cached_availability[1] is not None else sys.maxsize, Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
max_per_order) )
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 display_add_to_cart = display_add_to_cart or var.order_max > 0
item.available_variations = [ item.available_variations = [
v for v in item.available_variations if v._subevent_quotas 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: if len(item.available_variations) > 0:
item.min_price = min([v.display_price.net if event.settings.display_net_prices else 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]) 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 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]) v.display_price.gross for v in item.available_variations])
item._remove = not bool(item.available_variations) item._remove = not bool(item.available_variations)
if not external_quota_cache: if not external_quota_cache:
@@ -120,6 +170,8 @@ def get_grouped_items(event, subevent=None):
return items, display_add_to_cart 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): class EventIndex(EventViewMixin, CartMixin, TemplateView):
template_name = "pretixpresale/event/index.html" template_name = "pretixpresale/event/index.html"
@@ -135,7 +187,7 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
else: else:
if 'subevent' in kwargs: if 'subevent' in kwargs:
return redirect(eventreverse(request.event, 'presale:event.index')) return redirect(self.get_index_url())
else: else:
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
+21 -1
View File
@@ -7,9 +7,11 @@ from django.db import transaction
from django.db.models import Sum from django.db.models import Sum
from django.http import FileResponse, Http404, JsonResponse from django.http import FileResponse, Http404, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render 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.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from pretix.base.models import CachedTicket, Invoice, Order, OrderPosition 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.base.signals import allow_ticket_download, register_ticket_outputs
from pretix.helpers.safedownload import check_token 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.forms.checkout import InvoiceAddressForm
from pretix.presale.views import CartMixin, EventViewMixin from pretix.presale.views import CartMixin, EventViewMixin
from pretix.presale.views.async import AsyncAction 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): class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
template_name = "pretixpresale/event/order.html" 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 ( ctx['can_generate_invoice'] = invoice_qualified(self.order) and (
self.request.event.settings.invoice_generate == 'user' 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: if self.order.status == Order.STATUS_PENDING:
ctx['payment'] = self.payment_provider.order_pending_render(self.request, self.order) ctx['payment'] = self.payment_provider.order_pending_render(self.request, self.order)
@@ -134,6 +143,7 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
return ctx return ctx
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView): class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView):
""" """
This is used if a payment is retried or the payment method is changed. It shows the payment 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): class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
""" """
This is used if a payment is retried or the payment method is changed. It is shown after the 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): class OrderPaymentComplete(EventViewMixin, OrderDetailMixin, View):
""" """
This is used for the first try of a payment. This means the user just entered payment 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): class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
template_name = 'pretixpresale/event/order_pay_change.html' 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): class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@@ -406,6 +420,7 @@ class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
return redirect(self.get_order_url()) return redirect(self.get_order_url())
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, TemplateView): class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, TemplateView):
template_name = "pretixpresale/event/order_modify.html" template_name = "pretixpresale/event/order_modify.html"
@@ -471,6 +486,7 @@ class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, Template
return ctx return ctx
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView): class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
template_name = "pretixpresale/event/order_cancel.html" template_name = "pretixpresale/event/order_cancel.html"
@@ -493,6 +509,7 @@ class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
return ctx return ctx
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View): class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
task = cancel_order task = cancel_order
known_errortypes = ['OrderError'] known_errortypes = ['OrderError']
@@ -520,6 +537,7 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
return _('The order has been canceled.') return _('The order has been canceled.')
@method_decorator(xframe_options_exempt, 'dispatch')
class AnswerDownload(EventViewMixin, OrderDetailMixin, View): class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
answid = kwargs.get('answer') answid = kwargs.get('answer')
@@ -541,6 +559,7 @@ class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
return resp return resp
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderDownload(EventViewMixin, OrderDetailMixin, View): class OrderDownload(EventViewMixin, OrderDetailMixin, View):
def get_self_url(self): def get_self_url(self):
@@ -636,6 +655,7 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
return resp return resp
@method_decorator(xframe_options_exempt, 'dispatch')
class InvoiceDownload(EventViewMixin, OrderDetailMixin, View): class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
+10 -7
View File
@@ -1,18 +1,21 @@
from django.contrib import messages from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.utils import translation from django.utils import translation
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.views.generic import FormView from django.views.generic import FormView
from pretix.base.models.event import SubEvent 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 ...base.models import Item, ItemVariation, WaitingListEntry
from ...multidomain.urlreverse import eventreverse
from ..forms.waitinglist import WaitingListForm 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' template_name = 'pretixpresale/event/waitinglist.html'
form_class = WaitingListForm form_class = WaitingListForm
@@ -52,11 +55,11 @@ class WaitingView(FormView):
if not self.request.event.settings.waiting_list_enabled: if not self.request.event.settings.waiting_list_enabled:
messages.error(request, _("Waiting lists are disabled for this event.")) 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: if not self.item_and_variation:
messages.error(request, _("We could not identify the product you selected.")) 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 self.subevent = None
if request.event.has_subevents: if request.event.has_subevents:
@@ -65,7 +68,7 @@ class WaitingView(FormView):
active=True) active=True)
else: else:
messages.error(request, pgettext_lazy('subevent', "You need to select a date.")) 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) return super().dispatch(request, *args, **kwargs)
@@ -78,7 +81,7 @@ class WaitingView(FormView):
if availability[0] == 100: if availability[0] == 100:
messages.error(self.request, _("You cannot add yourself to the waiting list as this product is currently " messages.error(self.request, _("You cannot add yourself to the waiting list as this product is currently "
"available.")) "available."))
return redirect(eventreverse(self.request.event, 'presale:event.index')) return redirect(self.get_index_url())
form.save() form.save()
messages.success(self.request, _("We've added you to the waiting list. You will receive " 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) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
return eventreverse(self.request.event, 'presale:event.index') return self.get_index_url()
+274
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
+18 -6
View File
@@ -1,33 +1,45 @@
/*global $,gettext,ngettext */ /*global $,gettext,ngettext */
var cart = { var cart = {
_deadline: null, _deadline: null,
_deadline_interval: null, _deadline_interval: null,
_deadline_call: 0, _deadline_call: 0,
draw_deadline: function () { draw_deadline: function () {
function pad(n, width, z) {
z = z || '0';
n = n + '';
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
}
cart._deadline_call++; cart._deadline_call++;
if ((typeof django === 'undefined' || typeof django.gettext === 'undefined') && cart._deadline_call < 5) { if ((typeof django === 'undefined' || typeof django.gettext === 'undefined') && cart._deadline_call < 5) {
// Language files are not loaded yet, don't run during the first seconds // Language files are not loaded yet, don't run during the first seconds
return; return;
} }
var diff = Math.floor(cart._deadline.diff(moment()) / 1000 / 60); var diff_minutes = Math.floor(cart._deadline.diff(moment()) / 1000 / 60);
if (diff < 0) { var diff_seconds = Math.floor(cart._deadline.diff(moment()) / 1000 % 60);
if (diff_minutes < 0) {
$("#cart-deadline").text(gettext("The items in your cart are no longer reserved for you.")); $("#cart-deadline").text(gettext("The items in your cart are no longer reserved for you."));
$("#cart-deadline-short").text(
gettext("Cart expired")
);
window.clearInterval(cart._deadline_interval); window.clearInterval(cart._deadline_interval);
} else { } else {
$("#cart-deadline").text(ngettext( $("#cart-deadline").text(ngettext(
"The items in your cart are reserved for you for one minute.", "The items in your cart are reserved for you for one minute.",
"The items in your cart are reserved for you for {num} minutes.", "The items in your cart are reserved for you for {num} minutes.",
diff diff_minutes
).replace(/\{num\}/g, diff)); ).replace(/\{num\}/g, diff_minutes));
$("#cart-deadline-short").text(
pad(diff_minutes.toString(), 2) + ':' + pad(diff_seconds.toString(), 2)
);
} }
}, },
init: function () { init: function () {
"use strict"; "use strict";
cart._deadline = moment($("#cart-deadline").attr("data-expires")); cart._deadline = moment($("#cart-deadline").attr("data-expires"));
cart._deadline_interval = window.setInterval(cart.draw_deadline, 2000); cart._deadline_interval = window.setInterval(cart.draw_deadline, 500);
cart.draw_deadline(); cart.draw_deadline();
} }
}; };
@@ -0,0 +1,10 @@
var inIframe = function () {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
};
if (inIframe()) {
document.body.classList.add('in-iframe');
}
@@ -12,9 +12,9 @@ function ngettext(singular, plural, count) {
} }
return plural; return plural;
} }
$(function () { $(function () {
"use strict"; "use strict";
$("input[data-toggle=radiocollapse]").change(function () { $("input[data-toggle=radiocollapse]").change(function () {
$($(this).attr("data-parent")).find(".collapse.in").collapse('hide'); $($(this).attr("data-parent")).find(".collapse.in").collapse('hide');
$($(this).attr("data-target")).collapse('show'); $($(this).attr("data-target")).collapse('show');
@@ -0,0 +1,81 @@
// Pure-js replacement for $.ready
// by John Friend, https://github.com/jfriend00/docReady
// MIT License
(function(funcName, baseObj) {
"use strict";
// The public function name defaults to window.docReady
// but you can modify the last line of this function to pass in a different object or method name
// if you want to put them in a different namespace and those will be used instead of
// window.docReady(...)
funcName = funcName || "docReady";
baseObj = baseObj || window;
var readyList = [];
var readyFired = false;
var readyEventHandlersInstalled = false;
// call this when the document is ready
// this function protects itself against being called more than once
function ready() {
if (!readyFired) {
// this must be set to true before we start calling callbacks
readyFired = true;
for (var i = 0; i < readyList.length; i++) {
// if a callback here happens to add new ready handlers,
// the docReady() function will see that it already fired
// and will schedule the callback to run right after
// this event loop finishes so all handlers will still execute
// in order and no new ones will be added to the readyList
// while we are processing the list
readyList[i].fn.call(window, readyList[i].ctx);
}
// allow any closures held by these functions to free
readyList = [];
}
}
function readyStateChange() {
if ( document.readyState === "complete" ) {
ready();
}
}
// This is the one public interface
// docReady(fn, context);
// the context argument is optional - if present, it will be passed
// as an argument to the callback
baseObj[funcName] = function(callback, context) {
if (typeof callback !== "function") {
throw new TypeError("callback for docReady(fn) must be a function");
}
// if ready has already fired, then just schedule the callback
// to fire asynchronously, but right away
if (readyFired) {
setTimeout(function() {callback(context);}, 1);
return;
} else {
// add the function and context to the list
readyList.push({fn: callback, ctx: context});
}
// if document already ready to go, schedule the ready function to run
// IE only safe when readyState is "complete", others safe when readyState is "interactive"
if (document.readyState === "complete" || (!document.attachEvent && document.readyState === "interactive")) {
setTimeout(ready, 1);
} else if (!readyEventHandlersInstalled) {
// otherwise if we don't have event handlers installed, install them
if (document.addEventListener) {
// first choice is DOMContentLoaded event
document.addEventListener("DOMContentLoaded", ready, false);
// backup is window load event
window.addEventListener("load", ready, false);
} else {
// must be IE
document.attachEvent("onreadystatechange", readyStateChange);
window.attachEvent("onload", ready);
}
readyEventHandlersInstalled = true;
}
}
})("docReady", window);
// modify this previous line to pass in your own method name
// and object for the method to be attached to
@@ -0,0 +1,25 @@
/*global django*/
var roundTo = function (n, digits) {
if (digits === undefined) {
digits = 0;
}
var multiplicator = Math.pow(10, digits);
n = parseFloat((n * multiplicator).toFixed(11));
return Math.round(n) / multiplicator;
};
var floatformat = function (val, places) {
"use strict";
if (places === undefined) {
places = 2;
}
if (typeof val === "string") {
val = parseFloat(val);
}
var parts = roundTo(val, places).toFixed(places).split(".");
parts[0] = parts[0].replace(new RegExp("\\B(?=(\\d{" + django.get_format("NUMBER_GROUPING") + "})+(?!\\d))", "g"), django.get_format("THOUSAND_SEPARATOR"));
return parts[0] + django.get_format("DECIMAL_SEPARATOR") + parts[1];
};
@@ -0,0 +1,680 @@
/*global siteglobals, module, lang, django*/
/* PRETIX WIDGET BEGINS HERE */
/* This is embedded in an isolation wrapper that exposes siteglobals as the global
scope. */
var Vue = module.exports;
var strings = {
'sold_out': django.pgettext('widget', 'Sold out'),
'buy': django.pgettext('widget', 'Buy'),
'reserved': django.pgettext('widget', 'Reserved'),
'free': django.pgettext('widget', 'FREE'),
'price_from': django.pgettext('widget', 'from %(currency)s %(price)s'),
'tax_incl': django.pgettext('widget', 'incl. %(rate)s% %(taxname)s'),
'tax_plus': django.pgettext('widget', 'plus %(rate)s% %(taxname)s'),
'quota_left': django.pgettext('widget', 'currently available: %s'),
'voucher_required': django.pgettext('widget', 'Only available with a voucher'),
'order_min': django.pgettext('widget', 'minimum amount to order: %s'),
'exit': django.pgettext('widget', 'Close ticket shop'),
'loading_error': django.pgettext('widget', 'The ticket shop could not be loaded.'),
'cart_error': django.pgettext('widget', 'The cart could not be created. Please try again later'),
'waiting_list': django.pgettext('widget', 'Waiting list'),
'cart_exists': django.pgettext('widget', 'You currently have an active cart for this event. If you select more' +
' products, they will be added to your existing cart. Click on this message to continue checkout with your' +
' cart.'),
'poweredby': django.pgettext('widget', 'ticketing powered by <a href="https://pretix.eu" target="_blank">pretix</a>'),
'redeem_voucher': django.pgettext('widget', 'Redeem a voucher'),
'redeem': django.pgettext('widget', 'Redeem'),
'voucher_code': django.pgettext('widget', 'Voucher code'),
'close': django.pgettext('widget', 'Close'),
'continue': django.pgettext('widget', 'Continue'),
};
var setCookie = function (cname, cvalue, exdays) {
var d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
};
var getCookie = function (name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift() || null;
else return null;
};
/* HTTP API Call helpers */
var api = {
'_getXHR': function () {
try {
return new window.XMLHttpRequest();
} catch (e) {
// explicitly bubble up the exception if not found
return new window.ActiveXObject('Microsoft.XMLHTTP');
}
},
'_getJSON': function (endpoint, callback, err_callback) {
var xhr = api._getXHR();
xhr.open("GET", endpoint, true);
xhr.onload = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(JSON.parse(xhr.responseText));
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
err_callback(xhr, e);
};
xhr.send(null);
},
'_postFormJSON': function (endpoint, form, callback, err_callback) {
var params = [].filter.call(form.elements, function (el) {
return (el.type !== 'checkbox' && el.type !== 'radio') || el.checked;
})
.filter(function (el) {
return !!el.name && !!el.value;
})
.filter(function (el) {
return !el.disabled;
})
.map(function (el) {
return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value);
}).join('&');
var xhr = api._getXHR();
xhr.open("POST", endpoint, true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.onload = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(JSON.parse(xhr.responseText));
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
err_callback(xhr, e);
};
xhr.send(params);
}
};
var makeid = function (length) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
var site_is_secure = function () {
return /https.*/.test(document.location.protocol)
};
var widget_id = makeid(16);
/* Vue Components */
Vue.component('availbox', {
template: ('<div class="pretix-widget-availability-box">'
+ '<div class="pretix-widget-availability-unavailable" v-if="item.require_voucher">'
+ '<small>' + strings.voucher_required + '</small>'
+ '</div>'
+ '<div class="pretix-widget-availability-unavailable"'
+ ' v-if="!item.require_voucher && avail[0] < 100 && avail[0] > 10">'
+ strings.reserved
+ '</div>'
+ '<div class="pretix-widget-availability-gone" '
+ ' v-if="!item.require_voucher && avail[0] <= 10">'
+ strings.sold_out
+ '</div>'
+ '<div class="pretix-widget-waiting-list-link"'
+ ' v-if="waiting_list_show">'
+ '<a :href="waiting_list_url" @click.prevent="$root.open_link_in_frame">' + strings.waiting_list + '</a>'
+ '</div>'
+ '<div class="pretix-widget-availability-available" v-if="!item.require_voucher && avail[0] === 100">'
+ '<label class="pretix-widget-item-count-single-label" v-if="order_max === 1">'
+ '<input type="checkbox" value="1" v-bind:name="input_name">'
+ '</label>'
+ '<input type="number" class="pretix-widget-item-count-multiple" placeholder="0" min="0"'
+ ' v-bind:max="order_max" v-bind:name="input_name" v-if="order_max !== 1">'
+ '</div>'
+ '</div>'),
props: {
item: Object,
variation: Object
},
computed: {
input_name: function () {
if (this.item.has_variations) {
return 'variation_' + this.item.id + '_' + this.variation.id;
} else {
return 'item_' + this.item.id;
}
},
order_max: function () {
return this.item.has_variations ? this.variation.order_max : this.item.order_max;
},
avail: function () {
return this.item.has_variations ? this.variation.avail : this.item.avail;
},
waiting_list_show: function () {
return this.avail[0] < 100 && this.$root.waiting_list_enabled;
},
waiting_list_url: function () {
if (this.item.has_variations) {
return this.$root.event_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&var=' + this.variation.id;
} else {
return this.$root.event_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id;
}
}
}
});
Vue.component('pricebox', {
template: ('<div class="pretix-widget-pricebox">'
+ '<span v-if="!free_price">{{ priceline }}</span>'
+ '<div v-if="free_price">'
+ '{{ $root.currency }} '
+ '<input type="number" class="pretix-widget-pricebox-price-input" placeholder="0" '
+ ' :min="display_price" :value="display_price" :name="field_name"'
+ ' step="any">'
+ '</div>'
+ '<small class="pretix-widget-pricebox-tax" v-if="price.rate != \'0.00\' && price.gross != \'0.00\'">'
+ '{{ taxline }}'
+ '</small>'
+ '</div>'),
props: {
price: Object,
free_price: Boolean,
field_name: String
},
computed: {
display_price: function () {
if (this.$root.display_net_prices) {
return floatformat(this.price.net, 2);
} else {
return floatformat(this.price.gross, 2);
}
},
priceline: function () {
if (this.price.gross === "0.00") {
return strings.free;
} else {
return this.$root.currency + " " + floatformat(this.display_price, 2);
}
},
taxline: function () {
if (this.$root.display_net_prices) {
return django.interpolate(strings.tax_plus, {
'rate': floatformat(this.price.rate, 2),
'taxname': this.price.name
}, true);
} else {
return django.interpolate(strings.tax_incl, {
'rate': floatformat(this.price.rate, 2),
'taxname': this.price.name
}, true);
}
}
}
});
Vue.component('variation', {
template: ('<div class="pretix-widget-variation">'
+ '<div class="pretix-widget-item-row">'
+ '<div class="pretix-widget-item-info-col">'
+ '<div class="pretix-widget-item-title-and-description">'
+ '<strong class="pretix-widget-item-title">{{ variation.value }}</strong>'
+ '<div class="pretix-widget-item-description" v-if="variation.description" v-html="variation.description"></div>'
+ '<p class="pretix-widget-item-meta" '
+ ' v-if="!variation.has_variations && variation.avail[1] !== null && variation.avail[0] === 100">'
+ '<small>{{ quota_left_str }}</small>'
+ '</p>'
+ '</div>'
+ '</div>'
+ '<div class="pretix-widget-item-price-col">'
+ '<pricebox :price="variation.price" :free_price="item.free_price"'
+ ' :field_name="\'price_\' + item.id + \'_\' + variation.id">'
+ '</pricebox>'
+ '</div>'
+ '<div class="pretix-widget-item-availability-col">'
+ '<availbox :item="item" :variation="variation"></availbox>'
+ '</div>'
+ '<div class="pretix-widget-clear"></div>'
+ '</div>'
+ '</div>'),
props: {
variation: Object,
item: Object,
},
computed: {
quota_left_str: function () {
return django.interpolate(strings["quota_left"], [this.variation.avail[1]]);
},
}
});
Vue.component('item', {
template: ('<div v-bind:class="classObject">'
+ '<div class="pretix-widget-item-row pretix-widget-main-item-row">'
+ '<div class="pretix-widget-item-info-col">'
+ '<img :src="item.picture" v-if="item.picture" class="pretix-widget-item-picture">'
+ '<div class="pretix-widget-item-title-and-description">'
+ '<a v-if="item.has_variations && show_toggle" class="pretix-widget-item-title" href="#"'
+ ' @click.prevent="expand">'
+ '{{ item.name }}'
+ '</a>'
+ '<strong v-else class="pretix-widget-item-title">{{ item.name }}</strong>'
+ '<div class="pretix-widget-item-description" v-if="item.description" v-html="item.description"></div>'
+ '<p class="pretix-widget-item-meta" v-if="item.order_min && item.order_min > 1">'
+ '<small>{{ min_order_str }}</small>'
+ '</p>'
+ '<p class="pretix-widget-item-meta" '
+ ' v-if="!item.has_variations && item.avail[1] !== null && item.avail[0] === 100">'
+ '<small>{{ quota_left_str }}</small>'
+ '</p>'
+ '</div>'
+ '</div>'
+ '<div class="pretix-widget-item-price-col">'
+ '<pricebox :price="item.price" :free_price="item.free_price" v-if="!item.has_variations"'
+ ' :field_name="\'price_\' + item.id">'
+ '</pricebox>'
+ '<div class="pretix-widget-pricebox" v-if="item.has_variations">{{ pricerange }}</div>'
+ '</div>'
+ '<div class="pretix-widget-item-availability-col">'
+ '<a v-if="show_toggle" href="#" @click.prevent="expand">See variations</a>'
+ '<availbox v-if="!item.has_variations" :item="item"></availbox>'
+ '</div>'
+ '<div class="pretix-widget-clear"></div>'
+ '</div>'
+ '<div :class="varClasses" v-if="item.has_variations">'
+ '<variation v-for="variation in item.variations" :variation="variation" :item="item" :key="variation.id">'
+ '</variation>'
+ '</div>'
+ '</div>'),
props: {
item: Object,
},
data: function () {
return {
expanded: this.$root.show_variations_expanded
};
},
methods: {
expand: function () {
this.expanded = !this.expanded;
}
},
computed: {
classObject: function () {
return {
'pretix-widget-item': true,
'pretix-widget-item-with-picture': !!this.item.picture,
'pretix-widget-item-with-variations': this.item.has_variations
}
},
varClasses: function () {
return {
'pretix-widget-item-variations': true,
'pretix-widget-item-variations-expanded': this.expanded,
}
},
min_order_str: function () {
return django.interpolate(strings["order_min"], [this.item.order_min]);
},
quota_left_str: function () {
return django.interpolate(strings["quota_left"], [this.item.avail[1]]);
},
show_toggle: function () {
return this.item.has_variations && !this.$root.show_variations_expanded;
},
pricerange: function () {
if (this.item.min_price !== this.item.max_price || this.item.free_price) {
return django.interpolate(strings.price_from, {
'currency': this.$root.currency,
'price': floatformat(this.$root.price, 2)
}, true);
} else if (this.item.min_price === "0.00" && this.item.max_price === "0.00") {
return strings.free;
} else {
return this.$root.currency + " " + floatformat(this.item.min_price, 2);
}
},
}
});
Vue.component('category', {
template: ('<div class="pretix-widget-category">'
+ '<h3 class="pretix-widget-category-name" v-if="category.name">{{ category.name }}</h3>'
+ '<div class="pretix-widget-category-description" v-if="category.description" v-html="category.description">'
+ '</div>'
+ '<div class="pretix-widget-category-items">'
+ '<item v-for="item in category.items" :item="item" :key="item.id"></item>'
+ '</div>'
+ '</div>'),
props: {
category: Object
}
});
Vue.component('pretix-widget', {
template: ('<div>'
+ '<div class="pretix-widget">'
+ '<div class="pretix-widget-loading" v-show="$root.loading > 0">'
+ '<svg width="128" height="128" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path class="pretix-widget-primary-color" d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z"/></svg>'
+ '</div>'
+ '<form method="post" :action="$root.formTarget" ref="form" target="_blank">'
+ '<input type="hidden" name="_voucher_code" :value="$root.voucher_code" v-if="$root.voucher_code">'
+ '<input type="hidden" name="subevent" :value="$root.subevent" />'
+ '<div class="pretix-widget-error-message" v-if="$root.error">{{ $root.error }}</div>'
+ '<div class="pretix-widget-info-message pretix-widget-clickable" @click.prevent="resume"'
+ ' v-if="$root.cart_exists">'
+ strings['cart_exists']
+ '</div>'
+ '<category v-for="category in this.$root.categories" :category="category" :key="category.id"></category>'
+ '<div class="pretix-widget-action" v-if="$root.display_add_to_cart">'
+ '<button @click="buy">' + strings.buy + '</button>'
+ '</div>'
+ '</form>'
+ '<form method="get" :action="$root.voucherFormTarget" target="_blank" v-if="$root.vouchers_exist && !$root.voucher_code">'
+ '<div class="pretix-widget-voucher">'
+ '<h3 class="pretix-widget-voucher-headline">'+ strings['redeem_voucher'] +'</h3>'
+ '<div class="pretix-widget-voucher-input-wrap">'
+ '<input class="pretix-widget-voucher-input" type="text" v-model="voucher" name="voucher" placeholder="'+strings.voucher_code+'">'
+ '</div>'
+ '<input type="hidden" name="subevent" :value="$root.subevent" />'
+ '<input type="hidden" name="locale" value="' + lang + '" />'
+ '<div class="pretix-widget-voucher-button-wrap">'
+ '<button @click="redeem">' + strings.redeem + '</button>'
+ '</div>'
+ '</div>'
+ '</form>'
+ '<div class="pretix-widget-clear"></div>'
+ '<div class="pretix-widget-attribution">'
+ strings.poweredby
+ '</div>'
+ '</div>'
+ '<div :class="frameClasses">'
+ '<div class="pretix-widget-frame-loading" v-show="$root.frame_loading">'
+ '<svg width="256" height="256" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path class="pretix-widget-primary-color" d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z"/></svg>'
+ '</div>'
+ '<div class="pretix-widget-frame-inner" ref="frame-container" v-show="$root.frame_shown">'
+ '<iframe frameborder="0" width="650px" height="650px" @load="iframeLoaded" '
+ ' :name="$root.widget_id" src="about:blank" v-once>'
+ 'Please enable frames in your browser!'
+ '</iframe>'
+ '<div class="pretix-widget-frame-close"><a href="#" @click.prevent="close">X</a></div>'
+ '</div>'
+ '</div>'
+ '<div :class="alertClasses">'
+ '<transition name="bounce">'
+ '<div class="pretix-widget-alert-box" v-if="$root.error_message">'
+ '<p>{{ $root.error_message }}</p>'
+ '<p><button v-if="$root.error_url_after" @click.prevent="errorContinue">' + strings.continue + '</button>'
+ '<button v-else @click.prevent="errorClose">' + strings.close + '</button></p>'
+ '</div>'
+ '</transition>'
+ '<svg width="64" height="64" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" class="pretix-widget-alert-icon"><path style="fill:#ffffff;" d="M 599.86438,303.72882 H 1203.5254 V 1503.4576 H 599.86438 Z" /><path class="pretix-widget-primary-color" d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5-103 385.5-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103zm128 1247v-190q0-14-9-23.5t-22-9.5h-192q-13 0-23 10t-10 23v190q0 13 10 23t23 10h192q13 0 22-9.5t9-23.5zm-2-344l18-621q0-12-10-18-10-8-24-8h-220q-14 0-24 8-10 6-10 18l17 621q0 10 10 17.5t24 7.5h185q14 0 23.5-7.5t10.5-17.5z"/></svg>'
+ '</div>'
+ '</div>'
+ '</div>'
),
data: function () {
return {
async_task_id: null,
async_task_check_url: null,
async_task_timeout: null,
async_task_interval: 100,
voucher: null,
}
},
methods: {
buy: function (event) {
if (this.$root.useIframe) {
event.preventDefault();
} else {
return;
}
var url = this.$root.formTarget + "&locale=" + lang + "&ajax=1";
this.$root.frame_loading = true;
this.async_task_interval = 100;
api._postFormJSON(url, this.$refs.form, this.buy_callback, this.buy_error_callback);
},
buy_error_callback: function (xhr, data) {
this.$root.error_message = strings['cart_error'];
this.$root.frame_loading = false;
},
buy_check_error_callback: function (xhr, data) {
if (xhr.status == 200 || (xhr.status >= 400 && xhr.status < 500)) {
this.$root.error_message = strings['cart_error'];
this.$root.frame_loading = false;
} else {
this.async_task_timeout = window.setTimeout(this.buy_check, 1000);
}
},
buy_callback: function (data) {
if (data.redirect) {
var iframe = this.$refs['frame-container'].children[0];
this.$root.cart_id = data.cart_id;
setCookie(this.$root.cookieName, data.cart_id, 30);
if (data.redirect.substr(0, 1) === '/') {
data.redirect = this.$root.event_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.redirect;
}
var url = data.redirect + '?iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
if (data.success === false) {
url = url.replace(/checkout\/start/g, "");
this.$root.error_message = data.message;
if (data.has_cart) {
this.$root.error_url_after = url;
}
this.$root.frame_loading = false;
} else {
iframe.src = url;
}
} else {
this.async_task_id = data.async_id;
if (data.check_url) {
this.async_task_check_url = this.$root.event_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.check_url;
}
this.async_task_timeout = window.setTimeout(this.buy_check, this.async_task_interval);
this.async_task_interval = 250;
}
},
buy_check: function () {
api._getJSON(this.async_task_check_url, this.buy_callback, this.buy_check_error_callback);
},
errorContinue: function () {
var iframe = this.$refs['frame-container'].children[0];
iframe.src = this.$root.error_url_after;
this.$root.frame_loading = true;
this.$root.error_message = null;
this.$root.error_url_after = null;
},
errorClose: function () {
this.$root.error_message = null;
this.$root.error_url_after = null;
},
redeem: function () {
if (this.$root.useIframe) {
event.preventDefault();
} else {
return;
}
var redirect_url = this.$root.voucherFormTarget + '&voucher=' + this.voucher + '&subevent=' + this.$root.subevent;
var iframe = this.$refs['frame-container'].children[0];
this.$root.frame_loading = true;
iframe.src = redirect_url;
},
resume: function () {
var redirect_url = this.$root.event_url + 'w/' + widget_id + '/checkout/start?iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
if (this.$root.useIframe) {
var iframe = this.$refs['frame-container'].children[0];
this.$root.frame_loading = true;
iframe.src = redirect_url;
} else {
window.open(redirect_url);
}
},
close: function () {
this.$root.frame_shown = false;
},
iframeLoaded: function () {
if (this.$root.frame_loading) {
this.$root.frame_loading = false;
this.$root.frame_shown = true;
}
}
},
computed: {
frameClasses: function () {
return {
'pretix-widget-frame-holder': true,
'pretix-widget-frame-shown': this.$root.frame_shown || this.$root.frame_loading,
};
},
alertClasses: function () {
return {
'pretix-widget-alert-holder': true,
'pretix-widget-alert-shown': this.$root.error_message,
};
},
}
});
/* Function to create the actual Vue instances */
var create_widget = function (element) {
var event_url = element.attributes.event.value;
if (!event_url.match(/\/$/)) {
event_url += "/";
}
var voucher = element.attributes.voucher ? element.attributes.voucher.value : null;
var subevent = element.attributes.subevent ? element.attributes.subevent.value : null;
var skip_ssl = element.attributes["skip-ssl-check"] ? true : false;
var app = new Vue({
el: element,
data: function () {
return {
event_url: event_url,
subevent: subevent,
categories: null,
currency: null,
voucher_code: voucher,
display_net_prices: false,
show_variations_expanded: false,
error: null,
display_add_to_cart: false,
loading: 1,
widget_id: 'pretix-widget-' + widget_id,
frame_loading: false,
frame_shown: false,
error_message: null,
error_url_after: null,
vouchers_exist: false,
cart_exists: false
}
},
created: function () {
var url;
if (subevent) {
url = event_url + subevent + '/widget/product_list?lang=' + lang;
} else {
url = event_url + 'widget/product_list?lang=' + lang;
}
var cart_id = getCookie(this.cookieName);
if (voucher) {
url += '&voucher=' + escape(voucher);
}
if (cart_id) {
url += "&cart_id=" + cart_id;
}
api._getJSON(url, function (data) {
app.categories = data.items_by_category;
app.currency = data.currency;
app.display_net_prices = data.display_net_prices;
app.error = data.error;
app.display_add_to_cart = data.display_add_to_cart;
app.waiting_list_enabled = data.waiting_list_enabled;
app.show_variations_expanded = data.show_variations_expanded;
app.cart_id = cart_id;
app.cart_exists = data.cart_exists;
app.vouchers_exist = data.vouchers_exist;
app.loading--;
}, function (error) {
app.categories = [];
app.currency = '';
app.error = strings['loading_error'];
app.loading--;
});
},
computed: {
cookieName: function () {
return "pretix_widget_" + this.event_url.replace(/[^a-zA-Z0-9]+/g, "_");
},
voucherFormTarget: function () {
var form_target = this.event_url + 'w/' + widget_id + '/redeem?iframe=1&locale=' + lang;
if (getCookie(this.cookieName)) {
form_target += "&take_cart_id=" + getCookie(this.cookieName);
}
if (this.subevent) {
form_target += "&subevent=" + this.subevent;
}
return form_target;
},
formTarget: function () {
var checkout_url = "/" + this.event_url.replace(/^[^\/]+:\/\/([^\/]+)\//, "") + "w/" + widget_id + "/checkout/start";
var form_target = this.event_url + 'w/' + widget_id + '/cart/add?iframe=1&next=' + checkout_url;
if (getCookie(this.cookieName)) {
form_target += "&take_cart_id=" + getCookie(this.cookieName);
}
return form_target;
},
useIframe: function () {
return window.innerWidth >= 800 && (skip_ssl || site_is_secure());
}
},
methods: {
open_link_in_frame: function (event) {
var url = event.target.attributes.href.value;
this.$children[0].$refs['frame-container'].children[0].src = url;
this.frame_loading = true;
}
}
});
return app;
};
/* Find all widgets on the page and render them */
widgetlist = [];
document.createElement("pretix-widget");
docReady(function () {
var widgets = document.querySelectorAll("pretix-widget");
var wlength = widgets.length;
for (var i = 0; i < wlength; i++) {
var widget = widgets[i];
widgetlist.push(create_widget(widget));
}
});
/* Set a global variable for debugging. In DEBUG mode, siteglobals will be window, otherwise it will be something
unnamed. */
siteglobals.pretixwidget = {
'Vue': Vue,
'widgets': widgetlist
};
@@ -81,3 +81,10 @@
} }
} }
} }
#cart-deadline-short {
font-variant-numeric: tabular-nums;
}
.cart-modify {
margin-left: 10px;
}
@@ -0,0 +1,13 @@
.in-iframe .page-header {
display: none;
}
.in-iframe .container {
padding-top: 10px;
}
.iframe-only,
.in-iframe .iframe-hidden {
display: none;
}
.in-iframe .iframe-only {
display: block;
}
@@ -201,6 +201,12 @@ body.loading .container {
text-decoration: none; text-decoration: none;
} }
.panel-title a[data-toggle="collapse"]:hover { .panel-default .panel-title a[data-toggle="collapse"]:hover {
background-color: #eeeeee; background-color: #eeeeee;
} }
.panel-primary .panel-title a[data-toggle="collapse"]:hover {
background-color: darken($btn-primary-bg, 10%);
}
@import "_iframe.scss";
@@ -0,0 +1,531 @@
@import "_variables.scss";
@import "../../bootstrap/scss/bootstrap/variables";
@import "../../bootstrap/scss/bootstrap/mixins";
.pretix-widget, .pretix-widget-alert-box {
a {
color: $link-color;
text-decoration: none;
&:hover,
&:focus {
color: $link-hover-color;
text-decoration: $link-hover-decoration;
}
&:focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
}
img {
border: 0;
}
b, strong {
font-weight: bold;
}
h3 {
font-size: $font-size-h3;
font-weight: bold;
padding: 0 15px;
}
button, input[type="button"] {
overflow: visible;
text-transform: none;
cursor: pointer;
display: inline-block;
margin-bottom: 0;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
background-image: none;
border: 1px solid transparent;
white-space: nowrap;
@include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $btn-border-radius-base);
@include user-select(none);
@include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border);
&,
&:active,
&.active {
&:focus,
&.focus {
@include tab-focus;
}
}
}
input[type="text"], input[type="number"] {
line-height: normal;
border: 1px solid $input-border;
border-radius: $input-border-radius;
height: $input-height-base;
padding: $padding-base-vertical $padding-base-horizontal;
color: $input-color;
background-color: $input-bg;
@include box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075));
@include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
@include placeholder;
$color-rgba: rgba(red($input-border-focus), green($input-border-focus), blue($input-border-focus), .6);
&:focus {
border-color: $input-border-focus;
outline: 0;
@include box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px $color-rgba);
}
}
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; // 1
padding: 0; // 2
&:focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
}
}
.pretix-widget {
margin: 10px 0;
padding: 0 10px;
border: 1px solid #ccc;
position: relative;
min-height: 208px;
border-radius: $input-border-radius;
.pretix-widget-clickable {
cursor: pointer;
}
.pretix-widget-info-message {
padding: 10px;
text-align: center;
margin: 10px 0;
background-color: $alert-info-bg;
border-color: $alert-info-border;
color: $alert-info-text;
border-radius: $alert-border-radius;
}
.pretix-widget-error-message {
padding: 10px;
text-align: center;
margin: 10px 0;
background-color: $alert-danger-bg;
border-color: $alert-danger-border;
color: $alert-danger-text;
border-radius: $alert-border-radius;
}
.pretix-widget-loading {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, .8);
text-align: center;
}
@-moz-keyframes pretix-widget-spin {
100% {
-moz-transform: rotate(360deg);
}
}
@-webkit-keyframes pretix-widget-spin {
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes pretix-widget-spin {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.pretix-widget-loading svg {
margin: 40px;
-webkit-animation: pretix-widget-spin 6s linear infinite;
-moz-animation: pretix-widget-spin 6s linear infinite;
animation: pretix-widget-spin 6s linear infinite;
}
.pretix-widget-item-row, .pretix-widget-category {
clear: both;
}
.pretix-widget-item-title {
font-weight: bold;
}
.pretix-widget-item-row {
padding: 10px 0;
}
.pretix-widget-category {
margin: 10px 0;
}
.pretix-widget-category-description {
padding: 0 15px;
}
.pretix-widget-category-name {
margin: 10px 0 0 0;
}
.pretix-widget-item-info-col {
width: 50%;
float: left;
padding: 0 15px;
box-sizing: border-box;
}
.pretix-widget-item-price-col, .pretix-widget-item-availability-col {
width: 25%;
float: left;
padding: 0 15px;
box-sizing: border-box;
}
.pretix-widget-item-description p, .pretix-widget-item-meta {
margin: 0;
}
.pretix-widget-item-price-col {
text-align: right;
}
.pretix-widget-clear {
clear: both;
}
.pretix-widget-category-description p {
margin: 0 0 10px;
}
.pretix-widget-pricebox-tax {
display: block;
}
.pretix-widget-item-count-multiple {
display: block;
width: 100%;
box-sizing: border-box;
padding: 5px;
text-align: center;
}
.pretix-widget-pricebox-price-input {
display: inline;
width: 100px;
box-sizing: border-box;
text-align: right;
}
.pretix-widget-item-count-single-label {
display: block;
text-align: center;
width: 100%;
}
.pretix-widget-attribution {
padding: 10px 15px;
text-align: center;
font-size: 12px;
}
.pretix-widget-item-picture {
width: 60px;
height: 60px;
margin-right: 10px;
float: left;
}
.pretix-widget-action {
margin-left: 75%;
width: 25%;
padding: 0 15px;
box-sizing: border-box;
}
.pretix-widget-action button {
width: 100%;
}
.pretix-widget-voucher-headline {
margin: 10px 0 0 0;
}
.pretix-widget-voucher-input-wrap {
padding: 0 15px;
width: 75%;
box-sizing: border-box;
float: left;
}
.pretix-widget-voucher input {
width: 100%;
box-sizing: border-box;
}
.pretix-widget-voucher-button-wrap {
padding: 0 15px;
width: 25%;
box-sizing: border-box;
float: left;
}
.pretix-widget-voucher button {
width: 100%;
}
.pretix-widget-item-with-picture .pretix-widget-main-item-row .pretix-widget-item-title-and-description {
margin-left: 70px;
}
.pretix-widget-item-availability-col {
text-align: center;
}
.pretix-widget-availability-gone {
font-weight: bold;
color: $brand-danger;
text-transform: uppercase;
}
.pretix-widget-availability-unavailable {
color: $brand-danger;
}
.pretix-widget-item-variations {
overflow: hidden;
max-height: 0;
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
-moz-transition-duration: 0.5s;
-webkit-transition-duration: 0.5s;
-o-transition-duration: 0.5s;
transition-duration: 0.5s;
-moz-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
-webkit-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
-o-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
.pretix-widget-item-variations-expanded {
-moz-transition-duration: 0.5s;
-webkit-transition-duration: 0.5s;
-o-transition-duration: 0.5s;
transition-duration: 0.5s;
-moz-transition-timing-function: ease-in;
-webkit-transition-timing-function: ease-in;
-o-transition-timing-function: ease-in;
transition-timing-function: ease-in;
max-height: 1000px;
overflow: hidden;
}
}
@keyframes pretix-widget-bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
.pretix-widget-alert-holder {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
z-index: 16777271;
visibility: hidden;
opacity: 0;
transition: opacity 0.5s, visibility 0.5s;
&.pretix-widget-alert-shown {
visibility: visible;
opacity: 1;
transition: opacity 0.5s, visibility 0.5s;
}
.bounce-enter-active {
animation: pretix-widget-bounce-in .5s;
}
.bounce-leave-active {
animation: pretix-widget-bounce-in .5s reverse;
}
.pretix-widget-alert-box {
position: fixed;
left: 50%;
width: 600px;
margin-left: -300px;
top: 100px;
background: white;
border-radius: 5px 5px 5px 5px;
-moz-border-radius: 5px 5px 5px 5px;
-webkit-border-radius: 5px 5px 5px 5px;
box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09);
-webkit-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09);
-moz-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09);
box-sizing: border-box;
padding: 42px 20px 20px 20px;
text-align: center;
font-size: 20px;
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
}
.pretix-widget-alert-icon {
position: fixed;
left: 50%;
width: 64px;
margin-left: -32px;
top: 68px;
}
}
.pretix-widget-frame-holder {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
z-index: 16777271;
visibility: hidden;
opacity: 0;
transition: opacity 0.5s, visibility 0.5s;
.pretix-widget-frame-loading {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
position: fixed;
left: 0;
top: 0;
}
.pretix-widget-frame-loading svg {
margin: 40px;
-webkit-animation: pretix-widget-spin 6s linear infinite;
-moz-animation: pretix-widget-spin 6s linear infinite;
animation: pretix-widget-spin 6s linear infinite;
}
&.pretix-widget-frame-shown {
visibility: visible;
opacity: 1;
transition: opacity 0.5s, visibility 0.5s;
}
.pretix-widget-frame-inner {
position: fixed;
left: 10%;
width: 80%;
height: 80%;
top: 10%;
background: white;
border-radius: 5px 5px 5px 5px;
-moz-border-radius: 5px 5px 5px 5px;
-webkit-border-radius: 5px 5px 5px 5px;
box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09);
-webkit-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09);
-moz-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09);
box-sizing: border-box;
padding: 10px;
}
.pretix-widget-frame-close {
position: fixed;
right: 10%;
top: 10%;
width: 24px;
height: 24px;
background: $brand-primary;
margin: -12px -12px 0 0;
border-radius: 12px;
-moz-border-radius: 12px;
-webkit-border-radius: 12px;
text-align: center;
}
.pretix-widget-frame-close a {
color: white;
font-weight: bold;
font-family: sans-serif;
text-decoration: none;
padding: 4px 0;
display: block;
}
.pretix-widget-frame-inner iframe {
width: 100% !important;
height: 100% !important;
}
}
.pretix-widget-primary-color {
/* in SVG */
fill: $brand-primary;
}
@media (max-width: $screen-sm-max) {
.pretix-widget {
.pretix-widget-item-info-col {
width: 100%;
float: none;
margin-bottom: 5px;
}
.pretix-widget-item-price-col, .pretix-widget-item-availability-col {
width: 50%;
}
.pretix-widget-action {
width: 100%;
margin-left: 0;
}
.pretix-widget-voucher-input-wrap {
width: 100%;
float: none;
}
.pretix-widget-voucher-button-wrap {
width: 100%;
float: none;
margin-top: 10px;
}
}
}
@media (min-width: 1200px) {
.pretix-widget-frame-holder {
.pretix-widget-frame-inner {
left: 50%;
margin-left: -540px;
width: 1080px;
}
.pretix-widget-frame-close {
left: 50%;
margin-left: 528px;
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+2
View File
@@ -41,6 +41,7 @@ event_urls = [
"settings/invoice", "settings/invoice",
"settings/invoice/preview", "settings/invoice/preview",
"settings/display", "settings/display",
"settings/widget",
"settings/tax/", "settings/tax/",
"settings/tax/add", "settings/tax/add",
"settings/tax/1/", "settings/tax/1/",
@@ -167,6 +168,7 @@ event_permission_urls = [
("can_change_event_settings", "settings/email", 200), ("can_change_event_settings", "settings/email", 200),
("can_change_event_settings", "settings/display", 200), ("can_change_event_settings", "settings/display", 200),
("can_change_event_settings", "settings/invoice", 200), ("can_change_event_settings", "settings/invoice", 200),
("can_change_event_settings", "settings/widget", 200),
("can_change_event_settings", "settings/invoice/preview", 200), ("can_change_event_settings", "settings/invoice/preview", 200),
("can_change_event_settings", "settings/tax/", 200), ("can_change_event_settings", "settings/tax/", 200),
("can_change_event_settings", "settings/tax/1/", 404), ("can_change_event_settings", "settings/tax/1/", 404),
+1
View File
@@ -90,6 +90,7 @@ def logged_in_client(client, event):
('/control/event/{orga}/{event}/settings/permissions', 200), ('/control/event/{orga}/{event}/settings/permissions', 200),
('/control/event/{orga}/{event}/settings/payment', 200), ('/control/event/{orga}/{event}/settings/payment', 200),
('/control/event/{orga}/{event}/settings/tickets', 200), ('/control/event/{orga}/{event}/settings/tickets', 200),
('/control/event/{orga}/{event}/settings/widget', 200),
# ('/control/event/{orga}/{event}/settings/tickets/preview/(?P<output>[^/]+)', 200), # ('/control/event/{orga}/{event}/settings/tickets/preview/(?P<output>[^/]+)', 200),
('/control/event/{orga}/{event}/settings/email', 200), ('/control/event/{orga}/{event}/settings/email', 200),
('/control/event/{orga}/{event}/settings/invoice', 200), ('/control/event/{orga}/{event}/settings/invoice', 200),
+3 -2
View File
@@ -26,9 +26,10 @@ class CartTestMixin:
self.event = Event.objects.create( self.event = Event.objects.create(
organizer=self.orga, name='30C3', slug='30c3', organizer=self.orga, name='30C3', slug='30c3',
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc), date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
live=True live=True,
plugins="pretix.plugins.banktransfer"
) )
self.tr19 = self.event.tax_rules.create(rate=19) self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'))
self.category = ItemCategory.objects.create(event=self.event, name="Everything", position=0) self.category = ItemCategory.objects.create(event=self.event, name="Everything", position=0)
self.quota_shirts = Quota.objects.create(event=self.event, name='Shirts', size=2) self.quota_shirts = Quota.objects.create(event=self.event, name='Shirts', size=2)
self.shirt = Item.objects.create(event=self.event, name='T-Shirt', category=self.category, default_price=12, self.shirt = Item.objects.create(event=self.event, name='T-Shirt', category=self.category, default_price=12,
+286
View File
@@ -0,0 +1,286 @@
import datetime
import json
from decimal import Decimal
from bs4 import BeautifulSoup
from django.conf import settings
from django.test import TestCase
from django.utils.timezone import now
from pretix.base.models import Order, OrderPosition
from pretix.presale.style import regenerate_css, regenerate_organizer_css
from .test_cart import CartTestMixin
class WidgetCartTest(CartTestMixin, TestCase):
def setUp(self):
super().setUp()
self.order = Order.objects.create(
status=Order.STATUS_PENDING,
event=self.event,
email='admin@localhost',
datetime=now() - datetime.timedelta(days=3),
expires=now() + datetime.timedelta(days=11),
total=Decimal("23"),
payment_provider='banktransfer',
locale='en'
)
self.ticket_pos = OrderPosition.objects.create(
order=self.order,
item=self.ticket,
variation=None,
price=Decimal("23"),
attendee_name="Peter"
)
def test_iframe_entry_view_wrapper(self):
self.client.get('/%s/%s/?iframe=1&locale=de' % (self.orga.slug, self.event.slug))
assert 'iframe_session' in self.client.session
assert self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value == "de"
def test_allow_frame_if_namespaced(self):
response = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
assert 'X-Frame-Options' in response
response = self.client.get('/%s/%s/w/aaaaaaaaaaaaaaaa/' % (self.orga.slug, self.event.slug))
assert 'X-Frame-Options' not in response
response = self.client.get('/%s/%s/waitinglist' % (self.orga.slug, self.event.slug))
assert 'X-Frame-Options' in response
response = self.client.get('/%s/%s/w/aaaaaaaaaaaaaaaa/waitinglist' % (self.orga.slug, self.event.slug))
assert 'X-Frame-Options' not in response
def test_allow_frame_on_order(self):
response = self.client.get('/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret))
assert 'X-Frame-Options' not in response
response = self.client.get('/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret))
assert 'X-Frame-Options' not in response
response = self.client.get('/%s/%s/order/%s/%s/pay' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret))
assert 'X-Frame-Options' not in response
response = self.client.get('/%s/%s/order/%s/%s/cancel' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret))
assert 'X-Frame-Options' not in response
def test_allow_cors_if_namespaced(self):
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
'ajax': 1
})
assert 'Access-Control-Allow-Origin' not in response
response = self.client.post('/%s/%s/w/aaaaaaaaaaaaaaaa/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
'ajax': 1
})
assert response['Access-Control-Allow-Origin'] == '*'
def test_cart_isolation(self):
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1'
}, follow=True)
self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug),
target_status_code=200)
doc = BeautifulSoup(response.rendered_content, "lxml")
assert len(doc.select('.cart .cart-row')) == 2
self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text)
self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text)
self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[0].text)
self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[1].text)
response = self.client.get('/%s/%s/w/aaaaaaaaaaaaaaaa/' % (self.orga.slug, self.event.slug))
doc = BeautifulSoup(response.rendered_content, "lxml")
assert len(doc.select('.cart .cart-row')) == 0
response = self.client.post('/%s/%s/w/aaaaaaaaaaaaaaaa/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1'
}, follow=True)
self.assertRedirects(response, '/%s/%s/w/aaaaaaaaaaaaaaaa/' % (self.orga.slug, self.event.slug),
target_status_code=200)
doc = BeautifulSoup(response.rendered_content, "lxml")
assert len(doc.select('.cart .cart-row')) == 2
self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text)
self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text)
self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[0].text)
self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[1].text)
response = self.client.get('/%s/%s/w/aaaaaaaaaaaaaaab/' % (self.orga.slug, self.event.slug))
doc = BeautifulSoup(response.rendered_content, "lxml")
assert len(doc.select('.cart .cart-row')) == 0
response = self.client.post('/%s/%s/w/aaaaaaaaaaaaaaab/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1'
}, follow=True)
self.assertRedirects(response, '/%s/%s/w/aaaaaaaaaaaaaaab/' % (self.orga.slug, self.event.slug),
target_status_code=200)
doc = BeautifulSoup(response.rendered_content, "lxml")
assert len(doc.select('.cart .cart-row')) == 2
self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text)
self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text)
self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[0].text)
self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[1].text)
def test_product_list_view(self):
response = self.client.get('/%s/%s/widget/product_list' % (self.orga.slug, self.event.slug))
assert response['Access-Control-Allow-Origin'] == '*'
data = json.loads(response.content.decode())
assert data == {
"currency": "EUR",
"show_variations_expanded": False,
"display_net_prices": False,
"vouchers_exist": False,
"waiting_list_enabled": False,
"error": None,
"items_by_category": [
{
"items": [
{
"require_voucher": False,
"order_min": None,
"max_price": None,
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00"},
"picture": None,
"has_variations": 0,
"description": None,
"min_price": None,
"avail": [100, None],
"variations": [],
"id": self.ticket.pk,
"free_price": False,
"name": "Early-bird ticket",
"order_max": 4
},
{
"require_voucher": False,
"order_min": None,
"max_price": "14.00",
"price": None,
"picture": None,
"has_variations": 4,
"description": None,
"min_price": "12.00",
"avail": None,
"variations": [
{
"value": "Red",
"id": self.shirt_red.pk,
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
"rate": "19.00"},
"description": None,
"avail": [100, None],
"order_max": 2
},
{
"value": "Blue",
"id": self.shirt_blue.pk,
"price": {"gross": "12.00", "net": "10.08", "tax": "1.92", "name": "",
"rate": "19.00"},
"description": None,
"avail": [100, None],
"order_max": 2
}
],
"id": self.shirt.pk,
"free_price": False,
"name": "T-Shirt",
"order_max": None
}
],
"description": None,
"id": self.category.pk,
"name": "Everything"
}
],
"display_add_to_cart": True,
"cart_exists": False
}
def test_product_list_view_with_voucher(self):
self.event.vouchers.create(item=self.ticket, code="ABCDE")
response = self.client.get('/%s/%s/widget/product_list?voucher=ABCDE' % (self.orga.slug, self.event.slug))
assert response['Access-Control-Allow-Origin'] == '*'
data = json.loads(response.content.decode())
assert data == {
"currency": "EUR",
"show_variations_expanded": False,
"display_net_prices": False,
"vouchers_exist": True,
"waiting_list_enabled": False,
"error": None,
"items_by_category": [
{
"items": [
{
"require_voucher": False,
"order_min": None,
"max_price": None,
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00"},
"picture": None,
"has_variations": 0,
"description": None,
"min_price": None,
"avail": [100, None],
"variations": [],
"id": self.ticket.pk,
"free_price": False,
"name": "Early-bird ticket",
"order_max": 4
},
],
"description": None,
"id": self.category.pk,
"name": "Everything"
}
],
"display_add_to_cart": True,
"cart_exists": False
}
def test_product_list_view_with_voucher_expired(self):
self.event.vouchers.create(item=self.ticket, code="ABCDE", valid_until=now() - datetime.timedelta(days=1))
response = self.client.get('/%s/%s/widget/product_list?voucher=ABCDE' % (self.orga.slug, self.event.slug))
assert response['Access-Control-Allow-Origin'] == '*'
data = json.loads(response.content.decode())
assert data == {
"currency": "EUR",
"show_variations_expanded": False,
"display_net_prices": False,
"vouchers_exist": True,
"waiting_list_enabled": False,
"error": "This voucher is expired.",
"items_by_category": [],
"display_add_to_cart": False,
"cart_exists": False
}
def test_css_customized(self):
response = self.client.get('/%s/%s/widget/v1.css' % (self.orga.slug, self.event.slug))
c = b"".join(response.streaming_content).decode()
assert '#8E44B3' in c
assert '#33c33c' not in c
assert '#34c34c' not in c
self.orga.settings.primary_color = "#33c33c"
regenerate_organizer_css.apply(args=(self.orga.pk,))
response = self.client.get('/%s/%s/widget/v1.css' % (self.orga.slug, self.event.slug))
c = b"".join(response.streaming_content).decode()
assert '#8E44B3' not in c
assert '#33c33c' in c
assert '#34c34c' not in c
self.event.settings.primary_color = "#34c34c"
regenerate_css.apply(args=(self.event.pk,))
response = self.client.get('/%s/%s/widget/v1.css' % (self.orga.slug, self.event.slug))
c = b"".join(response.streaming_content).decode()
assert '#8E44B3' not in c
assert '#33c33c' not in c
assert '#34c34c' in c
def test_js_localized(self):
response = self.client.get('/widget/v1.en.js')
c = response.content.decode()
assert '%m/%d/%Y' in c
assert '%d.%m.%Y' not in c
response = self.client.get('/widget/v1.de.js')
c = response.content.decode()
assert '%m/%d/%Y' not in c
assert '%d.%m.%Y' in c