Fix #277 -- Embeddable shop (#622)

* Vendor vue.js

* Refactor item_group_by_category to support vouchers

* Widget: Show product list

* Widget: free prices

* Widget: pictures and loading indicator

* Widget: First iframe steps

* Widget: Do not rerender iframe

* Widget: Error handling

* Improve widget

* Widget: localization tech

* Fix invoice style

* Voucher attribute and waiting list

* Add some iframe chrome

* First step to namespaced carts

* More isolation steps

* More cart isolation things

* More cart isolation things

* Mobile stuff

* Show cart on checkout pages

* PayPal and Stripe support

* Enable downloads

* Locale handling

* change text "save URL to this exact page"

* Widget: voucher redemption

* Widget: CSS

* CSS: Responsive

* Widget: CSS improvements

* Widget: Add embedding code generator

* Widget: Error messages and SSL check

* First tests

* Widget: tests

* Don't use IDs in widgets

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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