Fix #277 -- Embeddable shop (#622)

* Vendor vue.js

* Refactor item_group_by_category to support vouchers

* Widget: Show product list

* Widget: free prices

* Widget: pictures and loading indicator

* Widget: First iframe steps

* Widget: Do not rerender iframe

* Widget: Error handling

* Improve widget

* Widget: localization tech

* Fix invoice style

* Voucher attribute and waiting list

* Add some iframe chrome

* First step to namespaced carts

* More isolation steps

* More cart isolation things

* More cart isolation things

* Mobile stuff

* Show cart on checkout pages

* PayPal and Stripe support

* Enable downloads

* Locale handling

* change text "save URL to this exact page"

* Widget: voucher redemption

* Widget: CSS

* CSS: Responsive

* Widget: CSS improvements

* Widget: Add embedding code generator

* Widget: Error messages and SSL check

* First tests

* Widget: tests

* Don't use IDs in widgets

* Widget: static files caching
This commit is contained in:
Raphael Michel
2017-10-28 21:54:27 +02:00
committed by GitHub
parent df7fbe5a66
commit 9767243a6d
56 changed files with 12819 additions and 317 deletions

View File

@@ -427,6 +427,14 @@ Your {event} team"""))
'default': None,
'type': str
},
'presale_widget_css_file': {
'default': None,
'type': str
},
'presale_widget_css_checksum': {
'default': None,
'type': str
},
'logo_image': {
'default': None,
'type': File

View File

@@ -4,6 +4,7 @@ import bleach
import markdown
from bleach import DEFAULT_CALLBACKS
from django import template
from django.conf import settings
from django.core import signing
from django.urls import reverse
from django.utils.http import is_safe_url
@@ -63,6 +64,12 @@ def safelink_callback(attrs, new=False):
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
def rich_text(text: str, **kwargs):
"""
@@ -73,5 +80,5 @@ def rich_text(text: str, **kwargs):
markdown.markdown(text),
tags=ALLOWED_TAGS,
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)

View File

@@ -33,6 +33,7 @@ class EventSlugBlacklistValidator(BlacklistValidator):
'api',
'events',
'csp_report',
'widget',
]
@@ -53,4 +54,5 @@ class OrganizerSlugBlacklistValidator(BlacklistValidator):
'about',
'api',
'csp_report',
'widget',
]

View File

@@ -51,6 +51,9 @@ class AsyncAction:
return self.get_result(request)
return self.http_method_not_allowed(request)
def _ajax_response_data(self):
return {}
def _return_ajax_result(self, res, timeout=.5):
if not res.ready():
try:
@@ -59,10 +62,11 @@ class AsyncAction:
pass
ready = res.ready()
data = {
data = self._ajax_response_data()
data.update({
'async_id': res.id,
'ready': ready
}
})
if ready:
if res.successful() and not isinstance(res.info, Exception):
smes = self.get_success_message(res.info)

View File

@@ -4,13 +4,13 @@ from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db.models import Q
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 pytz import common_timezones, timezone
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
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.control.forms import (
ExtFileField, SlugWidget, SplitDateTimePickerWidget,
@@ -897,3 +897,44 @@ class TaxRuleForm(I18nModelForm):
class Meta:
model = TaxRule
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

View File

@@ -75,6 +75,11 @@
{% trans "Permissions" %}
</a>
</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 %}
{% for nav in nav_event_settings %}
<li {% if nav.active %}class="active"{% endif %}>

View File

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

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/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/widget$', event.WidgetSettings.as_view(), name='event.settings.widget'),
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+)/delete$', subevents.SubEventDelete.as_view(),

View File

@@ -1,6 +1,7 @@
import re
from collections import OrderedDict
from datetime import timedelta
from urllib.parse import urlsplit
from django.conf import settings
from django.contrib import messages
@@ -36,10 +37,12 @@ from pretix.control.forms.event import (
CommentForm, DisplaySettingsForm, EventMetaValueForm, EventSettingsForm,
EventUpdateForm, InvoiceSettingsForm, MailSettingsForm,
PaymentSettingsForm, ProviderForm, TaxRuleForm, TicketSettingsForm,
WidgetCodeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import nav_event_settings
from pretix.helpers.urls import build_absolute_uri
from pretix.multidomain.urlreverse import get_domain
from pretix.presale.style import regenerate_css
from . import CreateView, UpdateView
@@ -989,3 +992,31 @@ class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, DeleteView
context = super().get_context_data(*args, **kwargs)
context['possible'] = self.object.allow_delete()
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

View File

@@ -1,10 +1,12 @@
import json
import logging
import urllib.parse
from collections import OrderedDict
import paypalrestsdk
from django import forms
from django.contrib import messages
from django.core import signing
from django.template.loader import get_template
from django.utils.translation import ugettext as __, ugettext_lazy as _
@@ -89,14 +91,18 @@ class Paypal(BasePaymentProvider):
def checkout_prepare(self, request, cart):
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({
'intent': 'sale',
'payer': {
"payment_method": "paypal",
},
"redirect_urls": {
"return_url": build_absolute_uri(request.event, 'plugins:paypal:return'),
"cancel_url": build_absolute_uri(request.event, 'plugins:paypal:abort'),
"return_url": build_absolute_uri(request.event, 'plugins:paypal:return', kwargs=kwargs),
"cancel_url": build_absolute_uri(request.event, 'plugins:paypal:abort', kwargs=kwargs),
},
"transactions": [
{
@@ -131,7 +137,14 @@ class Paypal(BasePaymentProvider):
request.session['payment_paypal_id'] = payment.id
for link in payment.links:
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:
messages.error(request, _('We had trouble communicating with PayPal'))
logger.error('Error on creating payment: ' + str(payment.error))

View File

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

View File

@@ -2,12 +2,17 @@ from django.conf.urls import include, 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 = [
url(r'^paypal/', include([
url(r'^abort/$', abort, name='abort'),
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),
])),
]

View File

@@ -3,11 +3,13 @@ import logging
import paypalrestsdk
from django.contrib import messages
from django.core import signing
from django.db import transaction
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
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.http import require_POST
@@ -17,12 +19,25 @@ from pretix.control.permissions import event_permission_required
from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.paypal.payment import Paypal
from pretix.plugins.stripe.models import ReferencedStripeObject
from pretix.presale.utils import event_view
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):
pid = request.GET.get('paymentId')
token = request.GET.get('token')
@@ -30,6 +45,10 @@ def success(request, *args, **kwargs):
request.session['payment_paypal_token'] = token
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'):
order = Order.objects.get(pk=request.session.get('payment_paypal_order'))
else:
@@ -44,7 +63,8 @@ def success(request, *args, **kwargs):
else:
messages.error(request, _('Invalid response from PayPal received.'))
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:
return redirect(eventreverse(request.event, 'presale:event.order', kwargs={
@@ -52,7 +72,8 @@ def success(request, *args, **kwargs):
'secret': order.secret
}) + ('?paid=yes' if order.status == Order.STATUS_PAID 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):

View File

@@ -8,9 +8,11 @@ from django.db import transaction
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
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.http import require_POST
@@ -193,6 +195,7 @@ class StripeOrderView:
return self.request.event.get_payment_providers()[self.order.payment_provider]
@method_decorator(xframe_options_exempt, 'dispatch')
class ReturnView(StripeOrderView, View):
def get(self, request, *args, **kwargs):
prov = self.pprov

View File

@@ -70,20 +70,26 @@ class BaseCheckoutFlowStep:
def post(self, request):
return HttpResponseNotAllowed([])
def get_step_url(self):
return eventreverse(self.event, 'presale:event.checkout', kwargs={'step': self.identifier})
def get_step_url(self, request):
kwargs = {'step': self.identifier}
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
return eventreverse(self.event, 'presale:event.checkout', kwargs=kwargs)
def get_prev_url(self, request):
prev = self.get_prev_applicable(request)
if not prev:
return eventreverse(self.event, 'presale:event.index')
kwargs = {}
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
return eventreverse(self.request.event, 'presale:event.index', kwargs=kwargs)
else:
return prev.get_step_url()
return prev.get_step_url(request)
def get_next_url(self, request):
n = self.get_next_applicable(request)
if n:
return n.get_step_url()
return n.get_step_url(request)
@cached_property
def cart_session(self):
@@ -225,6 +231,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['forms'] = self.forms
ctx['cart'] = self.get_cart()
return ctx
def get_success_message(self, value):
@@ -234,7 +241,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
return self.get_next_url(self.request)
def get_error_url(self):
return self.get_step_url()
return self.get_step_url(self.request)
def get(self, request):
self.request = request
@@ -382,6 +389,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
ctx['contact_form'] = self.contact_form
ctx['invoice_form'] = self.invoice_form
ctx['reverse_charge_relevant'] = self.eu_reverse_charge_relevant
ctx['cart'] = self.get_cart()
return ctx
@@ -436,6 +444,7 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
ctx['selected'] = self.request.POST.get('payment', self.cart_session.get('payment', ''))
if len(self.provider_forms) == 1:
ctx['selected'] = self.provider_forms[0]['provider'].identifier
ctx['cart'] = self.get_cart()
return ctx
@cached_property
@@ -557,7 +566,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
return super().get_error_message(exception)
def get_error_url(self):
return self.get_step_url()
return self.get_step_url(self.request)
def get_order_url(self, order):
return eventreverse(self.request.event, 'presale:event.order.pay.complete', kwargs={

View File

@@ -56,6 +56,9 @@ def contextprocessor(request):
ctx['event'] = request.event
ctx['languages'] = [get_language_info(code) for code in request.event.settings.locales]
if request.resolver_match:
ctx['cart_namespace'] = request.resolver_match.kwargs.get('cart_namespace', '')
if hasattr(request, 'organizer'):
if request.organizer.settings.presale_css_file and not hasattr(request, 'event'):
ctx['css_file'] = default_storage.url(request.organizer.settings.presale_css_file)

View File

@@ -1,15 +1,38 @@
import hashlib
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.core.management.base import BaseCommand
from pretix.base.models import Event_SettingsStore, Organizer_SettingsStore
from pretix.base.settings import GlobalSettingsObject
from pretix.presale.views.widget import generate_widget_js
from ...style import regenerate_css, regenerate_organizer_css
class Command(BaseCommand):
help = "Re-generate all custom stylesheets"
help = "Re-generate all custom stylesheets and scripts"
def handle(self, *args, **options):
for es in Event_SettingsStore.objects.filter(key="presale_css_file"):
regenerate_css.apply_async(args=(es.object_id,))
for es in Organizer_SettingsStore.objects.filter(key="presale_css_file"):
regenerate_organizer_css.apply_async(args=(es.object_id,))
gs = GlobalSettingsObject()
for lc, ll in settings.LANGUAGES:
data = generate_widget_js(lc).encode()
checksum = hashlib.sha1(data).hexdigest()
fname = gs.settings.get('widget_file_{}'.format(lc))
if not fname or gs.settings.get('widget_checksum_{}'.format(lc), '') != checksum:
newname = default_storage.save(
'widget/widget.{}.{}.js'.format(lc, checksum),
ContentFile(data)
)
gs.settings.set('widget_file_{}'.format(lc), 'file://' + newname)
gs.settings.set('widget_checksum_{}'.format(lc), checksum)
if fname:
default_storage.delete(fname)

View File

@@ -20,7 +20,7 @@ logger = logging.getLogger('pretix.presale.style')
affected_keys = ['primary_font', 'primary_color']
def compile_scss(object):
def compile_scss(object, file="main.scss", fonts=True):
sassdir = os.path.join(settings.STATIC_ROOT, 'pretixpresale/scss')
def static(path):
@@ -41,14 +41,14 @@ def compile_scss(object):
sassrules.append('$brand-primary: {};'.format(object.settings.get('primary_color')))
font = object.settings.get('primary_font')
if font != 'Open Sans':
if font != 'Open Sans' and fonts:
sassrules.append(get_font_stylesheet(font))
sassrules.append(
'$font-family-sans-serif: "{}", "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif !default'.format(
font
))
sassrules.append('@import "main.scss";')
sassrules.append('@import "{}";'.format(file))
cf = dict(django_libsass.CUSTOM_FUNCTIONS)
cf['static'] = static
@@ -64,32 +64,46 @@ def compile_scss(object):
@app.task(base=ProfiledTask)
def regenerate_css(event_id: int):
event = Event.objects.select_related('organizer').get(pk=event_id)
css, checksum = compile_scss(event)
fname = '{}/{}/presale.{}.css'.format(
event.organizer.slug, event.slug, checksum[:16]
)
# main.scss
css, checksum = compile_scss(event)
fname = '{}/{}/presale.{}.css'.format(event.organizer.slug, event.slug, checksum[:16])
if event.settings.get('presale_css_checksum', '') != checksum:
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
event.settings.set('presale_css_file', newname)
event.settings.set('presale_css_checksum', checksum)
# widget.scss
css, checksum = compile_scss(event, file='widget.scss', fonts=False)
fname = '{}/{}/widget.{}.css'.format(event.organizer.slug, event.slug, checksum[:16])
if event.settings.get('presale_widget_css_checksum', '') != checksum:
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
event.settings.set('presale_widget_css_file', newname)
event.settings.set('presale_widget_css_checksum', checksum)
@app.task(base=ProfiledTask)
def regenerate_organizer_css(organizer_id: int):
organizer = Organizer.objects.get(pk=organizer_id)
# main.scss
css, checksum = compile_scss(organizer)
fname = '{}/presale.{}.css'.format(
organizer.slug, checksum[:16]
)
fname = '{}/presale.{}.css'.format(organizer.slug, checksum[:16])
if organizer.settings.get('presale_css_checksum', '') != checksum:
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
organizer.settings.set('presale_css_file', newname)
organizer.settings.set('presale_css_checksum', checksum)
# widget.scss
css, checksum = compile_scss(organizer)
fname = '{}/widget.{}.css'.format(organizer.slug, checksum[:16])
if organizer.settings.get('presale_widget_css_checksum', '') != checksum:
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
organizer.settings.set('presale_widget_css_file', newname)
organizer.settings.set('presale_widget_css_checksum', checksum)
non_inherited_events = set(Event_SettingsStore.objects.filter(
object__organizer=organizer, key__in=affected_keys
).values_list('object_id', flat=True))

View File

@@ -1,7 +1,7 @@
{% load i18n %}
{% load safelink %}
{% safelink "https://pretix.eu" as pretixurl %}
{% with 'href="'|add:pretixurl|add:'"'|safe as a_attr %}
{% with 'target="_blank" href="'|add:pretixurl|add:'"'|safe as a_attr %}
{% blocktrans trimmed %}
powered by <a {{ a_attr }}>pretix</a>
{% endblocktrans %}

View File

@@ -8,6 +8,7 @@
{% block title %}{% endblock %}{% if url_name != "event.index" %} :: {% endif %}{{ event.name }}
{% endblock %}
{% block above %}
<script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script>
{% if not event.live %}
<div class="offline-banner">
<div class="container">
@@ -24,12 +25,13 @@
<div class="page-header">
<div class="pull-left">
{% if event_logo %}
<a href="{% eventurl event "presale:event.index" %}" title="{{ event.name }}">
<a href="{% eventurl event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}"
title="{{ event.name }}">
<img src="{{ event_logo|thumbnail_url:'logo' }}" alt="{{ event.name }}" class="event-logo" />
</a>
{% else %}
<h1>
<a href="{% eventurl event "presale:event.index" %}">{{ event.name }}</a>
<a href="{% eventurl event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}">{{ event.name }}</a>
{% if not event.has_subevents %}
<small>{{ event.get_date_range_display }}</small>
{% endif %}

View File

@@ -1,9 +1,7 @@
{% extends "pretixpresale/event/base.html" %}
{% extends "pretixpresale/event/checkout_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Checkout" %}{% endblock %}
{% block content %}
<h2>{% trans "Checkout" %}</h2>
{% block inner %}
<p>
{% trans "For some of the products in your cart, you can choose additional options before you continue." %}
</p>

View File

@@ -0,0 +1,45 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Checkout" %}{% endblock %}
{% block content %}
<div class="panel panel-default cart">
<div class="panel-heading">
<h3 class="panel-title">
<a class="collapsed" data-toggle="collapse" href="#cart">
<span>
<i class="fa fa-shopping-cart"></i>
<strong>{% trans "Your cart" %}</strong>
</span>
<span>
<strong id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
{% else %}
{% trans "Cart expired" %}
{% endif %}
</strong>
<i class="fa fa-angle-down collapse-indicator"></i>
</span>
</a>
</h3>
</div>
<div class="panel-collapse collapse" id="cart">
<div class="panel-body">
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event %}
<em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
{% blocktrans trimmed with minutes=cart.minutes_left %}
The items in your cart are reserved for you for {{ minutes }} minutes.
{% endblocktrans %}
{% else %}
{% trans "The items in your cart are no longer reserved for you." %}
{% endif %}
</em>
</div>
</div>
</div>
<h2>{% trans "Checkout" %}</h2>
{% block inner %}
{% endblock %}
{% endblock %}

View File

@@ -11,14 +11,22 @@
{% csrf_token %}
<div class="panel panel-primary cart">
<div class="panel-heading">
<div class="pull-right">
<a href="
{% eventurl request.event "presale:event.index" %}">
<div class="pull-right cart-modify">
<a href="{% eventurl request.event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}">
<span class="fa fa-edit"></span>
{% trans "Modify" %}
</a>
</div>
<strong id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}"
class="pull-right">
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
{% else %}
{% trans "Cart expired" %}
{% endif %}
</strong>
<h3 class="panel-title">
<i class="fa fa-shopping-cart"></i>
{% trans "Your cart" %}
</h3>
</div>
@@ -27,7 +35,7 @@
<div class="cart-row row">
<div class="col-md-6 col-xs-12">
<em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
{% if cart.minutes_left > 0 %}
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
{% blocktrans trimmed with minutes=cart.minutes_left %}
The items in your cart are reserved for you for {{ minutes }} minutes.
{% endblocktrans %}

View File

@@ -1,9 +1,7 @@
{% extends "pretixpresale/event/base.html" %}
{% extends "pretixpresale/event/checkout_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Checkout" %}{% endblock %}
{% block content %}
<h2>{% trans "Checkout" %}</h2>
{% block inner %}
<p>{% trans "Please select how you want to pay." %}</p>
<form method="post">
{% csrf_token %}

View File

@@ -1,9 +1,7 @@
{% extends "pretixpresale/event/base.html" %}
{% extends "pretixpresale/event/checkout_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Checkout" %}{% endblock %}
{% block content %}
<h2>{% trans "Checkout" %}</h2>
{% block inner %}
<p>{% trans "Before we continue, we need you to answer some questions." %}</p>
<p class="required-legend">
{% blocktrans trimmed %}

View File

@@ -5,7 +5,7 @@
<strong>{% trans "SOLD OUT" %}</strong>
{% if event.settings.waiting_list_enabled %}
<br/>
<a href="{% eventurl event "presale:event.waitinglist" %}?item={{ item.pk }}{% if var %}&var={{ var.pk }}{% endif %}{% if subevent %}&subevent={{ subevent.pk }}{% endif %}">
<a href="{% eventurl event "presale:event.waitinglist" cart_namespace=cart_namespace|default_if_none:"" %}?item={{ item.pk }}{% if var %}&var={{ var.pk }}{% endif %}{% if subevent %}&subevent={{ subevent.pk }}{% endif %}">
<span class="fa fa-plus-circle"></span>
{% trans "Waiting list" %}
</a>
@@ -19,7 +19,7 @@
</strong>
{% if event.settings.waiting_list_enabled %}
<br/>
<a href="{% eventurl event "presale:event.waitinglist" %}?item={{ item.pk }}{% if var %}&var={{ var.pk }}{% endif %}{% if subevent %}&subevent={{ subevent.pk }}{% endif %}">
<a href="{% eventurl event "presale:event.waitinglist" cart_namespace=cart_namespace|default_if_none:"" %}?item={{ item.pk }}{% if var %}&var={{ var.pk }}{% endif %}{% if subevent %}&subevent={{ subevent.pk }}{% endif %}">
<span class="fa fa-plus-circle"></span>
{% trans "Waiting list" %}
</a>

View File

@@ -79,7 +79,7 @@
{% else %}
<div class="count">
{% if editable %}
<form action="{% eventurl event "presale:event.cart.remove" %}"
<form action="{% eventurl event "presale:event.cart.remove" cart_namespace=cart_namespace|default_if_none:"" %}"
method="post" data-asynctask>
{% csrf_token %}
<input type="hidden" name="id" value="{{ line.id }}" />
@@ -90,7 +90,7 @@
{% endif %}
{{ line.count }}
{% if editable %}
<form action="{% eventurl event "presale:event.cart.add" %}"
<form action="{% eventurl event "presale:event.cart.add" cart_namespace=cart_namespace|default_if_none:"" %}"
method="post" data-asynctask>
<input type="hidden" name="subevent" value="{{ line.subevent_id|default_if_none:"" }}" />
{% csrf_token %}

View File

@@ -17,45 +17,64 @@
{% if show_cart %}
<div class="panel panel-primary cart">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Your cart" %}</h3>
<h3 class="panel-title">
<a class="collapsed" data-toggle="collapse" href="#cart">
<span>
<i class="fa fa-shopping-cart"></i>
<strong>{% trans "Your cart" %}</strong>
</span>
<span>
<strong id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
{% else %}
{% trans "Cart expired" %}
{% endif %}
</strong>
<i class="fa fa-angle-down collapse-indicator"></i>
</span>
</a>
</h3>
</div>
<div class="panel-body">
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
<em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
{% if cart.minutes_left > 0 %}
{% blocktrans trimmed with minutes=cart.minutes_left %}
The items in your cart are reserved for you for {{ minutes }} minutes.
{% endblocktrans %}
{% else %}
{% trans "The items in your cart are no longer reserved for you." %}
{% endif %}
</em>
<div class="row checkout-button-row">
<div class="col-md-4 col-xs-12">
<form method="post" data-asynctask action="{% eventurl request.event "presale:event.cart.clear" %}">
{% csrf_token %}
<button class="btn btn-block btn-default btn-lg" type="submit">
<i class="fa fa-close"></i> {% trans "Empty cart" %}</button>
</form>
<div class="panel-collapse collapse in" id="cart">
<div class="panel-body">
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
<em id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
{% blocktrans trimmed with minutes=cart.minutes_left %}
The items in your cart are reserved for you for {{ minutes }} minutes.
{% endblocktrans %}
{% else %}
{% trans "The items in your cart are no longer reserved for you." %}
{% endif %}
</em>
<div class="row checkout-button-row">
<div class="col-md-4 col-xs-12">
<form method="post" data-asynctask action="{% eventurl request.event "presale:event.cart.clear" cart_namespace=cart_namespace %}">
{% csrf_token %}
<button class="btn btn-block btn-default btn-lg" type="submit">
<i class="fa fa-close"></i> {% trans "Empty cart" %}</button>
</form>
</div>
<div class="col-md-4 col-md-offset-4 col-xs-12">
<a class="btn btn-block btn-primary btn-lg"
href="{% eventurl request.event "presale:event.checkout.start" cart_namespace=cart_namespace %}">
{% if has_addon_choices %}
<i class="fa fa-shopping-cart"></i> {% trans "Continue" %}
</a>
{% else %}
<i class="fa fa-shopping-cart"></i> {% trans "Proceed with checkout" %}
{% endif %}
</a>
</div>
<div class="clearfix"></div>
</div>
<div class="col-md-4 col-md-offset-4 col-xs-12">
<a class="btn btn-block btn-primary btn-lg"
href="{% eventurl request.event "presale:event.checkout.start" %}">
{% if has_addon_choices %}
<i class="fa fa-shopping-cart"></i> {% trans "Continue" %}
</a>
{% else %}
<i class="fa fa-shopping-cart"></i> {% trans "Proceed with checkout" %}
{% endif %}
</a>
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
{% endif %}
{% if event.has_subevents %}
{% if event.has_subevents and not cart_namespace %}
{% if subevent %}
<a class="subevent-toggle">
{% trans "View other date" %}
@@ -75,7 +94,7 @@
{% endif %}
{% endif %}
{% if frontpage_text %}
{% if frontpage_text and not cart_namespace %}
<div>
{{ frontpage_text|rich_text }}
</div>
@@ -99,60 +118,63 @@
{% endif %}
</div>
{% endif %}
<div>
{% if ev.location %}
{% if not cart_namespace %}
<div>
{% if ev.location %}
<div class="info-row">
<span class="fa fa-map-marker fa-fw"></span>
<p>
{{ ev.location|linebreaksbr }}
</p>
</div>
{% endif %}
<div class="info-row">
<span class="fa fa-map-marker fa-fw"></span>
<span class="fa fa-clock-o fa-fw"></span>
<p>
{{ ev.location|linebreaksbr }}
{{ ev.get_date_range_display }}
{% if event.settings.show_times %}
<br>
{% blocktrans trimmed with time=ev.date_from|date:"TIME_FORMAT" %}
Begin: {{ time }}
{% endblocktrans %}
{% if event.settings.show_date_to %}
<br>
{% blocktrans trimmed with time=ev.date_to|date:"TIME_FORMAT" %}
End: {{ time }}
{% endblocktrans %}
{% endif %}
{% endif %}
{% if ev.date_admission %}
<br>
{% if ev.date_admission|date:"SHORT_DATE_FORMAT" == ev.date_from|date:"SHORT_DATE_FORMAT" %}
{% blocktrans trimmed with time=ev.date_admission|date:"TIME_FORMAT" %}
Admission: {{ time }}
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with datetime=ev.date_admission|date:"SHORT_DATETIME_FORMAT" %}
Admission: {{ datetime }}
{% endblocktrans %}
{% endif %}
{% endif %}
<br>
{% if subevent %}
<a href="{% eventurl event "presale:event.ical.download" subevent=subevent.pk %}">
{% else %}
<a href="{% eventurl event "presale:event.ical.download" %}">
{% endif %}
{% trans "Add to Calendar" %}
</a>
</p>
</div>
{% endif %}
<div class="info-row">
<span class="fa fa-clock-o fa-fw"></span>
<p>
{{ ev.get_date_range_display }}
{% if event.settings.show_times %}
<br>
{% blocktrans trimmed with time=ev.date_from|date:"TIME_FORMAT" %}
Begin: {{ time }}
{% endblocktrans %}
{% if event.settings.show_date_to %}
<br>
{% blocktrans trimmed with time=ev.date_to|date:"TIME_FORMAT" %}
End: {{ time }}
{% endblocktrans %}
{% endif %}
{% endif %}
{% if ev.date_admission %}
<br>
{% if ev.date_admission|date:"SHORT_DATE_FORMAT" == ev.date_from|date:"SHORT_DATE_FORMAT" %}
{% blocktrans trimmed with time=ev.date_admission|date:"TIME_FORMAT" %}
Admission: {{ time }}
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with datetime=ev.date_admission|date:"SHORT_DATETIME_FORMAT" %}
Admission: {{ datetime }}
{% endblocktrans %}
{% endif %}
{% endif %}
<br>
{% if subevent %}
<a href="{% eventurl event "presale:event.ical.download" subevent=subevent.pk %}">
{% else %}
<a href="{% eventurl event "presale:event.ical.download" %}">
{% endif %}
{% trans "Add to Calendar" %}
</a>
</p>
</div>
</div>
{% eventsignal event "pretix.presale.signals.front_page_top" %}
{% endif %}
{% eventsignal event "pretix.presale.signals.front_page_top" %}
{% if ev.presale_is_running or event.settings.show_items_outside_presale_period %}
<form method="post" data-asynctask
action="{% eventurl request.event "presale:event.cart.add" %}?next={{ request.path|urlencode }}">
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={{ request.path|urlencode }}">
{% csrf_token %}
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" />
{% for tup in items_by_category %}
@@ -392,7 +414,7 @@
{% if vouchers_exist %}
<section class="front-page">
<h3>{% trans "Redeem a voucher" %}</h3>
<form method="get" action="{% eventurl event "presale:event.redeem" %}">
<form method="get" action="{% eventurl event "presale:event.redeem" cart_namespace=cart_namespace %}">
<div class="row-voucher">
<div class="col-md-8 col-sm-6 col-xs-12">
<div class="input-group">
@@ -412,25 +434,27 @@
</form>
</section>
{% endif %}
{% eventsignal event "pretix.presale.signals.front_page_bottom" %}
<section class="front-page">
<h3>{% trans "If you already ordered a ticket" %}</h3>
<div>
<div class="col-md-8 col-xs-12">
<p>
{% blocktrans trimmed %}
If you want to see or change the status and details of your order, click on the link in one of the
emails we sent you during the order process. If you cannot find the link, click on the
following button to request the link to your order to be sent to you again.
{% endblocktrans %}
</p>
{% if not cart_namespace %}
{% eventsignal event "pretix.presale.signals.front_page_bottom" %}
<section class="front-page">
<h3>{% trans "If you already ordered a ticket" %}</h3>
<div>
<div class="col-md-8 col-xs-12">
<p>
{% blocktrans trimmed %}
If you want to see or change the status and details of your order, click on the link in one of the
emails we sent you during the order process. If you cannot find the link, click on the
following button to request the link to your order to be sent to you again.
{% endblocktrans %}
</p>
</div>
<div class="col-md-4 col-xs-12 text-right">
<a class="btn btn-block btn-primary" href="{% eventurl event "presale:event.resend_link" %}">
{% trans "Resend order links" %}
</a>
</div>
<div class="clearfix"></div>
</div>
<div class="col-md-4 col-xs-12 text-right">
<a class="btn btn-block btn-primary" href="{% eventurl event "presale:event.resend_link" %}">
{% trans "Resend order links" %}
</a>
</div>
<div class="clearfix"></div>
</div>
</section>
</section>
{% endif %}
{% endblock %}

View File

@@ -22,10 +22,15 @@
{% else %}
<p>{% trans "We successfully received your payment. See below for details." %}</p>
{% endif %}
<p>{% blocktrans trimmed %}
<p class="iframe-hidden">{% blocktrans trimmed %}
Please bookmark or save the link to this exact page if you want to download your ticket or change
your details later. We also sent you an email containing the link to the address you specified.
{% endblocktrans %}</p>
<p class="iframe-only">{% blocktrans trimmed %}
Please save the following link if you want to download your ticket or change your details later. We
also sent you an email containing the link to the address you specified.
{% endblocktrans %}<br>
<code>{{ url }}</code></p>
<div class="clearfix"></div>
</div>
{% endif %}

View File

@@ -19,7 +19,7 @@
</p>
{% if event.presale_is_running or event.settings.show_items_outside_presale_period %}
<form method="post" data-asynctask
action="{% eventurl request.event "presale:event.cart.add" %}?next={{ request.path|urlencode }}">
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={% eventurl request.event "presale:event.index" cart_namespace=cart_namespace %}">
{% csrf_token %}
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" />
<input type="hidden" name="_voucher_code" value="{{ voucher.code }}">

View File

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

View File

@@ -1,4 +1,5 @@
from django.conf.urls import url
from django.conf.urls import include, url
from django.views.decorators.csrf import csrf_exempt
import pretix.presale.views.cart
import pretix.presale.views.checkout
@@ -9,23 +10,44 @@ import pretix.presale.views.organizer
import pretix.presale.views.robots
import pretix.presale.views.user
import pretix.presale.views.waiting
import pretix.presale.views.widget
# This is not a valid Django URL configuration, as the final
# configuration is done by the pretix.multidomain package.
event_patterns = [
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
frame_wrapped_urls = [
url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'),
url(r'^cart/clear$', pretix.presale.views.cart.CartClear.as_view(), name='event.cart.clear'),
url(r'^cart/answer/(?P<answer>[^/]+)/$',
pretix.presale.views.cart.AnswerDownload.as_view(),
name='event.cart.download.answer'),
url(r'^waitinglist', pretix.presale.views.waiting.WaitingView.as_view(), name='event.waitinglist'),
url(r'^checkout/start$', pretix.presale.views.checkout.CheckoutView.as_view(), name='event.checkout.start'),
url(r'^redeem/?$', pretix.presale.views.cart.RedeemView.as_view(),
name='event.redeem'),
url(r'^checkout/(?P<step>[^/]+)/$', pretix.presale.views.checkout.CheckoutView.as_view(),
name='event.checkout'),
url(r'^redeem/?$', pretix.presale.views.cart.RedeemView.as_view(),
name='event.redeem'),
url(r'^(?P<subevent>[0-9]+)/$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
url(r'^waitinglist', pretix.presale.views.waiting.WaitingView.as_view(), name='event.waitinglist'),
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
]
event_patterns = [
# Cart/checkout patterns are a bit more complicated, as they should have simple URLs like cart/clear in normal
# cases, but need to have versions with unguessable URLs like w/8l4Y83XNonjLxoBb/cart/clear to be used in widget
# mode. This is required to prevent all clickjacking and CSRF attacks that would otherwise be possible.
# First, we define the normal version
url(r'', include(frame_wrapped_urls)),
# Second, the widget version
url(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/', include(frame_wrapped_urls)),
# Third, a fake version that is defined like the first (and never gets called), but makes reversing URLs easier
url(r'(?P<cart_namespace>[_]{0})', include(frame_wrapped_urls)),
# CartAdd goes extra since it also gets a csrf_exempt decorator in one of the cases
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
url(r'^(?P<cart_namespace>[_]{0})cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
url(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/cart/add',
csrf_exempt(pretix.presale.views.cart.CartAdd.as_view()),
name='event.cart.add'),
url(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
name='event.order'),
@@ -71,8 +93,12 @@ event_patterns = [
pretix.presale.views.event.EventIcalDownload.as_view(),
name='event.ical.download'),
url(r'^auth/$', pretix.presale.views.event.EventAuth.as_view(), name='event.auth'),
url(r'^(?P<subevent>[0-9]+)/$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
url(r'^widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(),
name='event.widget.productlist'),
url(r'^widget/v1.css$', pretix.presale.views.widget.widget_css, name='event.widget.css'),
url(r'^(?P<subevent>\d+)/widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(),
name='event.widget.productlist'),
]
organizer_patterns = [
@@ -85,4 +111,5 @@ organizer_patterns = [
locale_patterns = [
url(r'^locale/set$', pretix.presale.views.locale.LocaleSet.as_view(), name='locale.set'),
url(r'^robots.txt$', pretix.presale.views.robots.robots_txt, name='robots.txt'),
url(r'^widget/v1\.(?P<lang>[a-zA-Z0-9_\-]+)\.js$', pretix.presale.views.widget.widget_js, name='widget.js'),
]

View File

@@ -1,13 +1,18 @@
from collections import defaultdict
from datetime import timedelta
from datetime import datetime, timedelta
from functools import wraps
from itertools import groupby
from django.conf import settings
from django.db.models import Sum
from django.utils.decorators import available_attrs
from django.utils.functional import cached_property
from django.utils.timezone import now
from pretix.base.i18n import language
from pretix.base.models import CartPosition, InvoiceAddress, OrderPosition
from pretix.base.services.cart import get_fees
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.signals import question_form_fields
@@ -129,9 +134,11 @@ class CartMixin:
try:
first_expiry = min(p.expires for p in positions) if positions else now()
minutes_left = max(first_expiry - now(), timedelta()).seconds // 60
seconds_left = max(first_expiry - now(), timedelta()).seconds % 60
except AttributeError:
first_expiry = None
minutes_left = None
seconds_left = None
return {
'positions': positions,
@@ -142,6 +149,7 @@ class CartMixin:
'fees': fees,
'answers': answers,
'minutes_left': minutes_left,
'seconds_left': seconds_left,
'first_expiry': first_expiry,
}
@@ -182,9 +190,53 @@ class EventViewMixin:
context['event'] = self.request.event
return context
def get_index_url(self):
kwargs = {}
if 'cart_namespace' in self.kwargs:
kwargs['cart_namespace'] = self.kwargs['cart_namespace']
return eventreverse(self.request.event, 'presale:event.index', kwargs=kwargs)
class OrganizerViewMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['organizer'] = self.request.organizer
return context
def allow_frame_if_namespaced(view_func):
def wrapped_view(request, *args, **kwargs):
resp = view_func(request, *args, **kwargs)
if request.resolver_match and request.resolver_match.kwargs.get('cart_namespace'):
resp.xframe_options_exempt = True
return resp
return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
def allow_cors_if_namespaced(view_func):
def wrapped_view(request, *args, **kwargs):
resp = view_func(request, *args, **kwargs)
if request.resolver_match and request.resolver_match.kwargs.get('cart_namespace'):
resp['Access-Control-Allow-Origin'] = '*'
return resp
return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
def iframe_entry_view_wrapper(view_func):
def wrapped_view(request, *args, **kwargs):
if 'iframe' in request.GET:
request.session['iframe_session'] = True
locale = request.GET.get('locale')
if locale and locale in [lc for lc, ll in settings.LANGUAGES]:
with language(locale):
resp = view_func(request, *args, **kwargs)
max_age = 10 * 365 * 24 * 60 * 60
resp.set_cookie(settings.LANGUAGE_COOKIE_NAME, locale, max_age=max_age,
expires=(datetime.utcnow() + timedelta(seconds=max_age)).strftime('%a, %d-%b-%Y %H:%M:%S GMT'),
domain=settings.SESSION_COOKIE_DOMAIN)
return resp
resp = view_func(request, *args, **kwargs)
return resp
return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)

View File

@@ -2,37 +2,47 @@ import mimetypes
import os
from django.contrib import messages
from django.db.models import Count, Prefetch, Q
from django.db.models import Q
from django.http import FileResponse, Http404, JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.utils import translation
from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.http import is_safe_url
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import TemplateView, View
from pretix.base.models import (
CartPosition, InvoiceAddress, ItemVariation, QuestionAnswer, Quota,
SubEvent, Voucher,
CartPosition, InvoiceAddress, QuestionAnswer, SubEvent, Voucher,
)
from pretix.base.services.cart import (
CartError, add_items_to_cart, clear_cart, remove_cart_position,
)
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.views import EventViewMixin
from pretix.presale.views import (
EventViewMixin, allow_cors_if_namespaced, allow_frame_if_namespaced,
iframe_entry_view_wrapper,
)
from pretix.presale.views.async import AsyncAction
from pretix.presale.views.event import item_group_by_category
from pretix.presale.views.event import (
get_grouped_items, item_group_by_category,
)
from pretix.presale.views.robots import NoSearchIndexViewMixin
class CartActionMixin:
def get_next_url(self):
if "next" in self.request.GET and '://' not in self.request.GET.get('next'):
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next")):
return self.request.GET.get('next')
else:
return eventreverse(self.request.event, 'presale:event.index')
kwargs = {}
if 'cart_namespace' in self.kwargs:
kwargs['cart_namespace'] = self.kwargs['cart_namespace']
return eventreverse(self.request.event, 'presale:event.index', kwargs=kwargs)
def get_success_url(self, value=None):
return self.get_next_url()
@@ -129,25 +139,54 @@ class CartActionMixin:
return items
def create_empty_cart_id(request):
current_id = request.session.get('current_cart_event_{}'.format(request.event.pk))
if current_id and current_id in request.session.get('carts', {}):
del request.session['carts'][current_id]
del request.session['current_cart_event_{}'.format(request.event.pk)]
return get_or_create_cart_id(request)
def generate_cart_id(prefix=''):
while True:
new_id = prefix + get_random_string(length=32 - len(prefix))
if not CartPosition.objects.filter(cart_id=new_id).exists():
return new_id
def create_empty_cart_id(request, replace_current=True):
session_keyname = 'current_cart_event_{}'.format(request.event.pk)
prefix = ''
if request.resolver_match and request.resolver_match.kwargs.get('cart_namespace'):
session_keyname += '_' + request.resolver_match.kwargs.get('cart_namespace')
prefix = request.resolver_match.kwargs.get('cart_namespace')
if 'carts' not in request.session:
request.session['carts'] = {}
new_id = generate_cart_id(prefix=prefix)
request.session['carts'][new_id] = {}
if replace_current:
current_id = request.session.get(session_keyname)
if current_id and current_id in request.session.get('carts', {}):
del request.session['carts'][current_id]
del request.session[session_keyname]
request.session[session_keyname] = new_id
return new_id
def get_or_create_cart_id(request):
current_id = request.session.get('current_cart_event_{}'.format(request.event.pk))
session_keyname = 'current_cart_event_{}'.format(request.event.pk)
prefix = ''
if request.resolver_match and request.resolver_match.kwargs.get('cart_namespace'):
session_keyname += '_' + request.resolver_match.kwargs.get('cart_namespace')
prefix = request.resolver_match.kwargs.get('cart_namespace')
current_id = request.session.get(session_keyname)
if current_id and current_id in request.session.get('carts', {}):
return current_id
else:
cart_data = {}
while True:
new_id = get_random_string(length=32)
if not CartPosition.objects.filter(cart_id=new_id).exists():
break
if prefix and 'take_cart_id' in request.GET:
if CartPosition.objects.filter(event=request.event, cart_id=request.GET.get('take_cart_id')).exists():
new_id = request.GET.get('take_cart_id')
else:
new_id = generate_cart_id(prefix=prefix)
else:
new_id = generate_cart_id(prefix=prefix)
# Migrate legacy data
# TODO: This is for the upgrade 1.7→1.8. We should remove this around April 2018
@@ -165,8 +204,9 @@ def get_or_create_cart_id(request):
if 'carts' not in request.session:
request.session['carts'] = {}
request.session['carts'][new_id] = cart_data
request.session['current_cart_event_{}'.format(request.event.pk)] = new_id
if new_id not in request.session['carts']:
request.session['carts'][new_id] = cart_data
request.session[session_keyname] = new_id
return new_id
@@ -176,6 +216,7 @@ def cart_session(request):
return request.session['carts'][cart_id]
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
task = remove_cart_position
known_errortypes = ['CartError']
@@ -198,6 +239,7 @@ class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View):
return redirect(self.get_error_url())
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
task = clear_cart
known_errortypes = ['CartError']
@@ -209,6 +251,9 @@ class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
return self.do(self.request.event.id, get_or_create_cart_id(self.request), translation.get_language())
@method_decorator(allow_cors_if_namespaced, 'dispatch')
@method_decorator(allow_frame_if_namespaced, 'dispatch')
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
task = add_items_to_cart
known_errortypes = ['CartError']
@@ -216,6 +261,13 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
def get_success_message(self, value):
return _('The products have been successfully added to your cart.')
def _ajax_response_data(self):
cart_id = get_or_create_cart_id(self.request)
return {
'cart_id': cart_id,
'has_cart': CartPosition.objects.filter(cart_id=cart_id, event=self.request.event).exists()
}
def post(self, request, *args, **kwargs):
items = self._items_from_post_data()
if items:
@@ -230,6 +282,8 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
return redirect(self.get_error_url())
@method_decorator(allow_frame_if_namespaced, 'dispatch')
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
template_name = "pretixpresale/event/voucher.html"
@@ -240,90 +294,11 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
context['max_times'] = self.voucher.max_usages - self.voucher.redeemed
# Fetch all items
items = self.request.event.items.all().filter(
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& ~Q(category__is_addon=True)
)
items, display_add_to_cart = get_grouped_items(self.request.event, self.subevent,
voucher=self.voucher)
vouchq = Q(hide_without_voucher=False)
if self.voucher.item_id:
vouchq |= Q(pk=self.voucher.item_id)
items = items.filter(pk=self.voucher.item_id)
elif self.voucher.quota_id:
items = items.filter(quotas__in=[self.voucher.quota_id])
items = items.filter(vouchq).select_related(
'category', 'tax_rule', # for re-grouping
).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=self.request.event.quotas.filter(subevent=self.subevent)),
Prefetch('variations', to_attr='available_variations',
queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=self.request.event.quotas.filter(subevent=self.subevent))
).distinct()),
).annotate(
quotac=Count('quotas'),
has_variations=Count('variations')
).filter(
quotac__gt=0
).distinct().order_by('category__position', 'category_id', 'position', 'name')
quota_cache = {}
if self.subevent:
item_price_override = self.subevent.item_price_overrides
var_price_override = self.subevent.var_price_overrides
else:
item_price_override = {}
var_price_override = {}
for item in items:
if self.voucher.item_id and self.voucher.variation_id:
item.available_variations = [v for v in item.available_variations if v.pk == self.voucher.variation_id]
item.order_max = item.max_per_order or int(self.request.event.settings.max_items_per_order)
if not item.has_variations:
item._remove = not bool(item._subevent_quotas)
if self.voucher.allow_ignore_quota or self.voucher.block_quota:
item.cached_availability = (Quota.AVAILABILITY_OK, 1)
else:
item.cached_availability = item.check_quotas(subevent=self.subevent, _cache=quota_cache)
price = item_price_override.get(item.pk, item.default_price)
price = self.voucher.calculate_price(price)
item.display_price = item.tax(price)
else:
item._remove = False
for var in item.available_variations:
if self.voucher.allow_ignore_quota or self.voucher.block_quota:
var.cached_availability = (Quota.AVAILABILITY_OK, 1)
else:
var.cached_availability = list(var.check_quotas(subevent=self.subevent, _cache=quota_cache))
price = var_price_override.get(var.pk, var.price)
price = self.voucher.calculate_price(price)
var.display_price = item.tax(price)
item.available_variations = [
v for v in item.available_variations if v._subevent_quotas
]
if self.voucher.variation_id:
item.available_variations = [v for v in item.available_variations
if v.pk == self.voucher.variation_id]
if len(item.available_variations) > 0:
item.min_price = min([v.display_price.net if self.request.event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
item.max_price = max([v.display_price.net if self.request.event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
items = [item for item in items
if (len(item.available_variations) > 0 or not item.has_variations) and not item._remove]
# Calculate how many options the user still has. If there is only one option, we can
# check the box right away ;)
context['options'] = sum([(len(item.available_variations) if item.has_variations else 1)
for item in items])
@@ -359,7 +334,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
except Voucher.DoesNotExist:
err = error_messages['voucher_invalid']
else:
return redirect(eventreverse(request.event, 'presale:event.index'))
return redirect(self.get_index_url())
if request.event.presale_start and now() < request.event.presale_start:
err = error_messages['not_started']
@@ -379,11 +354,12 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
if err:
messages.error(request, _(err))
return redirect(eventreverse(request.event, 'presale:event.index'))
return redirect(self.get_index_url())
return super().dispatch(request, *args, **kwargs)
@method_decorator(xframe_options_exempt, 'dispatch')
class AnswerDownload(EventViewMixin, View):
def get(self, request, *args, **kwargs):
answid = kwargs.get('answer')

View File

@@ -1,6 +1,7 @@
from django.contrib import messages
from django.http import Http404
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _
from django.views.generic import View
@@ -8,21 +9,31 @@ from pretix.base.services.cart import CartError
from pretix.base.signals import validate_cart
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.checkoutflow import get_checkout_flow
from pretix.presale.views import get_cart
from pretix.presale.views import (
allow_frame_if_namespaced, get_cart, iframe_entry_view_wrapper,
)
@method_decorator(allow_frame_if_namespaced, 'dispatch')
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
class CheckoutView(View):
def dispatch(self, request, *args, **kwargs):
def get_index_url(self, request):
kwargs = {}
if 'cart_namespace' in self.kwargs:
kwargs['cart_namespace'] = self.kwargs['cart_namespace']
return eventreverse(self.request.event, 'presale:event.index', kwargs=kwargs)
def dispatch(self, request, *args, **kwargs):
self.request = request
if not get_cart(request) and "async_id" not in request.GET:
messages.error(request, _("Your cart is empty"))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
return redirect(self.get_index_url(self.request))
if not request.event.presale_is_running:
messages.error(request, _("The presale for this event is over or has not yet started."))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
return redirect(self.get_index_url(self.request))
cart_error = None
try:
@@ -37,14 +48,14 @@ class CheckoutView(View):
continue
if step.requires_valid_cart and cart_error:
messages.error(request, str(cart_error))
return redirect(previous_step.get_step_url() if previous_step
else eventreverse(self.request.event, 'presale:event.index'))
return redirect(previous_step.get_step_url(request) if previous_step
else self.get_index_url(request))
if 'step' not in kwargs:
return redirect(step.get_step_url())
return redirect(step.get_step_url(request))
is_selected = (step.identifier == kwargs.get('step', ''))
if "async_id" not in request.GET and not is_selected and not step.is_completed(request, warn=not is_selected):
return redirect(step.get_step_url())
return redirect(step.get_step_url(request))
if is_selected:
if request.method.lower() in self.http_method_names:
handler = getattr(step, request.method.lower(), self.http_method_not_allowed)

View File

@@ -17,7 +17,7 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
from pretix.base.models import ItemVariation
from pretix.base.models import ItemVariation, Quota
from pretix.base.models.event import SubEvent
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.ical import get_ical
@@ -25,7 +25,10 @@ from pretix.presale.views.organizer import (
add_subevents_for_days, weeks_for_template,
)
from . import CartMixin, EventViewMixin, get_cart
from . import (
CartMixin, EventViewMixin, allow_frame_if_namespaced, get_cart,
iframe_entry_view_wrapper,
)
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@@ -44,14 +47,23 @@ def item_group_by_category(items):
)
def get_grouped_items(event, subevent=None):
def get_grouped_items(event, subevent=None, voucher=None):
items = event.items.all().filter(
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(hide_without_voucher=False)
& ~Q(category__is_addon=True)
).select_related(
)
vouchq = Q(hide_without_voucher=False)
if voucher:
if voucher.item_id:
vouchq |= Q(pk=voucher.item_id)
items = items.filter(pk=voucher.item_id)
elif voucher.quota_id:
items = items.filter(quotas__in=[voucher.quota_id])
items = items.filter(vouchq).select_related(
'category', 'tax_rule', # for re-grouping
).prefetch_related(
Prefetch('quotas',
@@ -81,36 +93,74 @@ def get_grouped_items(event, subevent=None):
var_price_override = {}
for item in items:
if voucher and voucher.item_id and voucher.variation_id:
# Restrict variations if the voucher only allows one
item.available_variations = [v for v in item.available_variations
if v.pk == voucher.variation_id]
max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
if not item.has_variations:
item._remove = not bool(item._subevent_quotas)
item.cached_availability = list(item.check_quotas(subevent=subevent, _cache=quota_cache))
item.order_max = min(item.cached_availability[1]
if item.cached_availability[1] is not None else sys.maxsize,
max_per_order)
price = item.default_price
item.display_price = item.tax(item_price_override.get(item.pk, price))
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
item.cached_availability = (
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
)
else:
item.cached_availability = list(
item.check_quotas(subevent=subevent, _cache=quota_cache)
)
item.order_max = min(
item.cached_availability[1]
if item.cached_availability[1] is not None else sys.maxsize,
max_per_order
)
price = item_price_override.get(item.pk, item.default_price)
if voucher:
price = voucher.calculate_price(price)
item.display_price = item.tax(price)
display_add_to_cart = display_add_to_cart or item.order_max > 0
else:
for var in item.available_variations:
var.cached_availability = list(var.check_quotas(subevent=subevent, _cache=quota_cache))
var.order_max = min(var.cached_availability[1]
if var.cached_availability[1] is not None else sys.maxsize,
max_per_order)
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
var.cached_availability = (
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
)
else:
var.cached_availability = list(
var.check_quotas(subevent=subevent, _cache=quota_cache)
)
var.display_price = var.tax(var_price_override.get(var.pk, var.price))
var.order_max = min(
var.cached_availability[1]
if var.cached_availability[1] is not None else sys.maxsize,
max_per_order
)
price = var_price_override.get(var.pk, var.price)
if voucher:
price = voucher.calculate_price(price)
var.display_price = var.tax(price)
display_add_to_cart = display_add_to_cart or var.order_max > 0
item.available_variations = [
v for v in item.available_variations if v._subevent_quotas
]
if voucher and voucher.variation_id:
item.available_variations = [v for v in item.available_variations
if v.pk == voucher.variation_id]
if len(item.available_variations) > 0:
item.min_price = min([v.display_price.net if event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
item.max_price = max([v.display_price.net if event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
item._remove = not bool(item.available_variations)
if not external_quota_cache:
@@ -120,6 +170,8 @@ def get_grouped_items(event, subevent=None):
return items, display_add_to_cart
@method_decorator(allow_frame_if_namespaced, 'dispatch')
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
class EventIndex(EventViewMixin, CartMixin, TemplateView):
template_name = "pretixpresale/event/index.html"
@@ -135,7 +187,7 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
return super().get(request, *args, **kwargs)
else:
if 'subevent' in kwargs:
return redirect(eventreverse(request.event, 'presale:event.index'))
return redirect(self.get_index_url())
else:
return super().get(request, *args, **kwargs)

View File

@@ -7,9 +7,11 @@ from django.db import transaction
from django.db.models import Sum
from django.http import FileResponse, Http404, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.generic import TemplateView, View
from pretix.base.models import CachedTicket, Invoice, Order, OrderPosition
@@ -24,7 +26,7 @@ from pretix.base.services.tickets import (
)
from pretix.base.signals import allow_ticket_download, register_ticket_outputs
from pretix.helpers.safedownload import check_token
from pretix.multidomain.urlreverse import eventreverse
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.presale.forms.checkout import InvoiceAddressForm
from pretix.presale.views import CartMixin, EventViewMixin
from pretix.presale.views.async import AsyncAction
@@ -59,6 +61,7 @@ class OrderDetailMixin(NoSearchIndexViewMixin):
})
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
template_name = "pretixpresale/event/order.html"
@@ -112,6 +115,12 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
ctx['can_generate_invoice'] = invoice_qualified(self.order) and (
self.request.event.settings.invoice_generate == 'user'
)
ctx['url'] = build_absolute_uri(
self.request.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
}
)
if self.order.status == Order.STATUS_PENDING:
ctx['payment'] = self.payment_provider.order_pending_render(self.request, self.order)
@@ -134,6 +143,7 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
return ctx
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView):
"""
This is used if a payment is retried or the payment method is changed. It shows the payment
@@ -186,6 +196,7 @@ class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView):
})
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
"""
This is used if a payment is retried or the payment method is changed. It is shown after the
@@ -232,6 +243,7 @@ class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
})
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderPaymentComplete(EventViewMixin, OrderDetailMixin, View):
"""
This is used for the first try of a payment. This means the user just entered payment
@@ -273,6 +285,7 @@ class OrderPaymentComplete(EventViewMixin, OrderDetailMixin, View):
})
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
template_name = 'pretixpresale/event/order_pay_change.html'
@@ -384,6 +397,7 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
})
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
def dispatch(self, request, *args, **kwargs):
@@ -406,6 +420,7 @@ class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
return redirect(self.get_order_url())
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, TemplateView):
template_name = "pretixpresale/event/order_modify.html"
@@ -471,6 +486,7 @@ class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, Template
return ctx
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
template_name = "pretixpresale/event/order_cancel.html"
@@ -493,6 +509,7 @@ class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
return ctx
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
task = cancel_order
known_errortypes = ['OrderError']
@@ -520,6 +537,7 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
return _('The order has been canceled.')
@method_decorator(xframe_options_exempt, 'dispatch')
class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
def get(self, request, *args, **kwargs):
answid = kwargs.get('answer')
@@ -541,6 +559,7 @@ class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
return resp
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderDownload(EventViewMixin, OrderDetailMixin, View):
def get_self_url(self):
@@ -636,6 +655,7 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
return resp
@method_decorator(xframe_options_exempt, 'dispatch')
class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
def get(self, request, *args, **kwargs):

View File

@@ -1,18 +1,21 @@
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect
from django.utils import translation
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.views.generic import FormView
from pretix.base.models.event import SubEvent
from pretix.presale.views import EventViewMixin
from . import allow_frame_if_namespaced
from ...base.models import Item, ItemVariation, WaitingListEntry
from ...multidomain.urlreverse import eventreverse
from ..forms.waitinglist import WaitingListForm
class WaitingView(FormView):
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class WaitingView(EventViewMixin, FormView):
template_name = 'pretixpresale/event/waitinglist.html'
form_class = WaitingListForm
@@ -52,11 +55,11 @@ class WaitingView(FormView):
if not self.request.event.settings.waiting_list_enabled:
messages.error(request, _("Waiting lists are disabled for this event."))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
return redirect(self.get_index_url())
if not self.item_and_variation:
messages.error(request, _("We could not identify the product you selected."))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
return redirect(self.get_index_url())
self.subevent = None
if request.event.has_subevents:
@@ -65,7 +68,7 @@ class WaitingView(FormView):
active=True)
else:
messages.error(request, pgettext_lazy('subevent', "You need to select a date."))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
return redirect(self.get_index_url())
return super().dispatch(request, *args, **kwargs)
@@ -78,7 +81,7 @@ class WaitingView(FormView):
if availability[0] == 100:
messages.error(self.request, _("You cannot add yourself to the waiting list as this product is currently "
"available."))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
return redirect(self.get_index_url())
form.save()
messages.success(self.request, _("We've added you to the waiting list. You will receive "
@@ -86,4 +89,4 @@ class WaitingView(FormView):
return super().form_valid(form)
def get_success_url(self):
return eventreverse(self.request.event, 'presale:event.index')
return self.get_index_url()

View File

@@ -0,0 +1,274 @@
import hashlib
import json
from urllib.parse import urljoin
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.db.models import Q
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
from django.template import Context, Engine
from django.template.loader import get_template
from django.utils.formats import date_format
from django.utils.timezone import now
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.decorators.http import condition
from django.views.i18n import (
get_formats, get_javascript_catalog, js_catalog_template,
)
from easy_thumbnails.files import get_thumbnailer
from lxml import etree
from pretix.base.i18n import language
from pretix.base.models import CartPosition, Voucher
from pretix.base.services.cart import error_messages
from pretix.base.settings import GlobalSettingsObject
from pretix.base.templatetags.rich_text import rich_text
from pretix.presale.views.cart import get_or_create_cart_id
from pretix.presale.views.event import (
get_grouped_items, item_group_by_category,
)
def indent(s):
return s.replace('\n', '\n ')
def widget_css_etag(request, **kwargs):
return request.event.settings.presale_widget_css_checksum or request.organizer.settings.presale_widget_css_checksum
def widget_js_etag(request, lang, **kwargs):
gs = GlobalSettingsObject()
return gs.settings.get('widget_checksum_{}'.format(lang))
@condition(etag_func=widget_css_etag)
@cache_page(60)
def widget_css(request, **kwargs):
if request.event.settings.presale_widget_css_file:
resp = FileResponse(default_storage.open(request.event.settings.presale_widget_css_file),
content_type='text/css')
return resp
else:
tpl = get_template('pretixpresale/widget_dummy.html')
et = etree.fromstring(tpl.render({})).attrib['href'].replace(settings.STATIC_URL, '')
f = finders.find(et)
resp = FileResponse(open(f, 'rb'), content_type='text/css')
return resp
def generate_widget_js(lang):
code = []
with language(lang):
# Provide isolation
code.append('(function (siteglobals) {\n')
code.append('var module = {}, exports = {};\n')
code.append('var lang = "%s";\n' % lang)
catalog, plural = get_javascript_catalog(lang, 'djangojs', ['pretix'])
catalog = dict((k, v) for k, v in catalog.items() if k.startswith('widget\u0004'))
template = Engine().from_string(js_catalog_template)
context = Context({
'catalog_str': indent(json.dumps(
catalog, sort_keys=True, indent=2)) if catalog else None,
'formats_str': indent(json.dumps(
get_formats(), sort_keys=True, indent=2)),
'plural': plural,
})
code.append(template.render(context))
files = [
'vuejs/vue.js' if settings.DEBUG else 'vuejs/vue.min.js',
'pretixpresale/js/widget/docready.js',
'pretixpresale/js/widget/floatformat.js',
'pretixpresale/js/widget/widget.js',
]
for fname in files:
f = finders.find(fname)
with open(f, 'r') as fp:
code.append(fp.read())
if settings.DEBUG:
code.append('})(this);\n')
else:
# Do not expose debugging variables
code.append('})({});\n')
return ''.join(code)
@condition(etag_func=widget_js_etag)
@cache_page(60)
def widget_js(request, lang, **kwargs):
if lang not in [lc for lc, ll in settings.LANGUAGES]:
raise Http404()
gs = GlobalSettingsObject()
fname = gs.settings.get('widget_file_{}'.format(lang))
print(fname, settings.DEBUG)
if not fname or settings.DEBUG:
data = generate_widget_js(lang).encode()
checksum = hashlib.sha1(data).hexdigest()
if not fname:
newname = default_storage.save(
'widget/widget.{}.{}.js'.format(lang, checksum),
ContentFile(data)
)
gs.settings.set('widget_file_{}'.format(lang), 'file://' + newname)
gs.settings.set('widget_checksum_{}'.format(lang), checksum)
resp = HttpResponse(data, content_type='text/javascript')
else:
resp = FileResponse(default_storage.open(fname), content_type='text/javascript')
return resp
def price_dict(price):
return {
'gross': price.gross,
'net': price.net,
'tax': price.tax,
'rate': price.rate,
'name': str(price.name)
}
def get_picture(picture):
thumb = get_thumbnailer(picture)['productlist']
return urljoin(settings.SITE_URL, thumb.url)
class WidgetAPIProductList(View):
def _get_items(self):
items, display_add_to_cart = get_grouped_items(
self.request.event, subevent=self.subevent, voucher=self.voucher
)
grps = []
for cat, g in item_group_by_category(items):
grps.append({
'id': cat.pk if cat else None,
'name': str(cat.name) if cat else None,
'description': str(rich_text(cat.description, safelinks=False)) if cat and cat.description else None,
'items': [
{
'id': item.pk,
'name': str(item.name),
'picture': get_picture(item.picture) if item.picture else None,
'description': str(rich_text(item.description, safelinks=False)) if item.description else None,
'has_variations': item.has_variations,
'require_voucher': item.require_voucher,
'order_min': item.min_per_order,
'order_max': item.order_max if not item.has_variations else None,
'price': price_dict(item.display_price) if not item.has_variations else None,
'min_price': item.min_price if item.has_variations else None,
'max_price': item.max_price if item.has_variations else None,
'free_price': item.free_price,
'avail': [
item.cached_availability[0],
item.cached_availability[1] if self.request.event.settings.show_quota_left else None
] if not item.has_variations else None,
'variations': [
{
'id': var.id,
'value': str(var.value),
'order_max': var.order_max,
'description': str(rich_text(var.description, safelinks=False)) if var.description else None,
'price': price_dict(var.display_price),
'avail': [
var.cached_availability[0],
var.cached_availability[1] if self.request.event.settings.show_quota_left else None
],
} for var in item.available_variations
]
} for item in g
]
})
return grps, display_add_to_cart
def dispatch(self, request, *args, **kwargs):
self.subevent = None
if request.event.has_subevents:
if 'subevent' in kwargs:
self.subevent = request.event.subevents.filter(pk=kwargs['subevent'], active=True).first()
if not self.subevent:
raise Http404()
else:
if 'subevent' in kwargs:
raise Http404()
if 'lang' in request.GET and request.GET.get('lang') in [lc for lc, ll in settings.LANGUAGES]:
with language(request.GET.get('lang')):
return super().dispatch(request, *args, **kwargs)
else:
return super().dispatch(request, *args, **kwargs)
def get(self, request, **kwargs):
data = {
'currency': request.event.currency,
'display_net_prices': request.event.settings.display_net_prices,
'show_variations_expanded': request.event.settings.show_variations_expanded,
'waiting_list_enabled': request.event.settings.waiting_list_enabled,
'error': None,
'cart_exists': False
}
if 'cart_id' in request.GET and CartPosition.objects.filter(event=request.event, cart_id=request.GET.get('cart_id')).exists():
data['cart_exists'] = True
ev = self.subevent or request.event
fail = False
if not ev.presale_is_running:
if ev.presale_has_ended:
data['error'] = 'The presale period for this event is over.'
elif request.event.settings.presale_start_show_date:
data['error'] = 'The presale for this event will start on %(date)s at %(time)s.' % {
'date': date_format(ev.presale_start, "SHORT_DATE_FORMAT"),
'time': date_format(ev.presale_start, "TIME_FORMAT"),
}
else:
data['error'] = 'The presale for this event has not yet started.'
self.voucher = None
if 'voucher' in request.GET:
try:
self.voucher = request.event.vouchers.get(code=request.GET.get('voucher').strip())
if self.voucher.redeemed >= self.voucher.max_usages:
data['error'] = error_messages['voucher_redeemed']
fail = True
if self.voucher.valid_until is not None and self.voucher.valid_until < now():
data['error'] = error_messages['voucher_expired']
fail = True
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=self.voucher) & Q(event=request.event) &
(Q(expires__gte=now()) | Q(cart_id=get_or_create_cart_id(request)))
)
v_avail = self.voucher.max_usages - self.voucher.redeemed - redeemed_in_carts.count()
if v_avail < 1:
data['error'] = error_messages['voucher_redeemed']
fail = True
except Voucher.DoesNotExist:
data['error'] = error_messages['voucher_invalid']
fail = True
if not fail and (ev.presale_is_running or request.event.settings.show_items_outside_presale_period):
data['items_by_category'], data['display_add_to_cart'] = self._get_items()
data['display_add_to_cart'] = data['display_add_to_cart'] and ev.presale_is_running
else:
data['items_by_category'] = []
data['display_add_to_cart'] = False
vouchers_exist = self.request.event.get_cache().get('vouchers_exist')
if vouchers_exist is None:
vouchers_exist = self.request.event.vouchers.exists()
self.request.event.get_cache().set('vouchers_exist', vouchers_exist)
data['vouchers_exist'] = vouchers_exist
resp = JsonResponse(data)
resp['Access-Control-Allow-Origin'] = '*'
return resp

View File

@@ -1,33 +1,45 @@
/*global $,gettext,ngettext */
var cart = {
_deadline: null,
_deadline_interval: null,
_deadline_call: 0,
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++;
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
return;
}
var diff = Math.floor(cart._deadline.diff(moment()) / 1000 / 60);
if (diff < 0) {
var diff_minutes = Math.floor(cart._deadline.diff(moment()) / 1000 / 60);
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-short").text(
gettext("Cart expired")
);
window.clearInterval(cart._deadline_interval);
} else {
$("#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 {num} minutes.",
diff
).replace(/\{num\}/g, diff));
diff_minutes
).replace(/\{num\}/g, diff_minutes));
$("#cart-deadline-short").text(
pad(diff_minutes.toString(), 2) + ':' + pad(diff_seconds.toString(), 2)
);
}
},
init: function () {
"use strict";
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();
}
};

View File

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

View File

@@ -12,9 +12,9 @@ function ngettext(singular, plural, count) {
}
return plural;
}
$(function () {
"use strict";
$("input[data-toggle=radiocollapse]").change(function () {
$($(this).attr("data-parent")).find(".collapse.in").collapse('hide');
$($(this).attr("data-target")).collapse('show');

View File

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

View File

@@ -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];
};

View File

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

View File

@@ -81,3 +81,10 @@
}
}
}
#cart-deadline-short {
font-variant-numeric: tabular-nums;
}
.cart-modify {
margin-left: 10px;
}

View File

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

View File

@@ -201,6 +201,12 @@ body.loading .container {
text-decoration: none;
}
.panel-title a[data-toggle="collapse"]:hover {
.panel-default .panel-title a[data-toggle="collapse"]:hover {
background-color: #eeeeee;
}
.panel-primary .panel-title a[data-toggle="collapse"]:hover {
background-color: darken($btn-primary-bg, 10%);
}
@import "_iframe.scss";

View File

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

10057
src/pretix/static/vuejs/vue.js Normal file

File diff suppressed because it is too large Load Diff

6
src/pretix/static/vuejs/vue.min.js vendored Normal file

File diff suppressed because one or more lines are too long