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