from collections import defaultdict 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 class CartMixin: @cached_property def positions(self): """ A list of this users cart position """ return list(get_cart(self.request)) @cached_property def cart_session(self): from pretix.presale.views.cart import cart_session return cart_session(self.request) @cached_property def invoice_address(self): if not hasattr(self.request, '_checkout_flow_invoice_address'): iapk = self.cart_session.get('invoice_address') if not iapk: self.request._checkout_flow_invoice_address = InvoiceAddress() else: try: self.request._checkout_flow_invoice_address = InvoiceAddress.objects.get(pk=iapk, order__isnull=True) except InvoiceAddress.DoesNotExist: self.request._checkout_flow_invoice_address = InvoiceAddress() return self.request._checkout_flow_invoice_address def get_cart(self, answers=False, queryset=None, order=None, downloads=False): if queryset: prefetch = [] if answers: prefetch.append('item__questions') prefetch.append('answers') cartpos = queryset.order_by( 'item', 'variation' ).select_related( 'item', 'variation', 'addon_to', 'subevent', 'subevent__event', 'subevent__event__organizer' ).prefetch_related( *prefetch ) else: cartpos = self.positions lcp = list(cartpos) has_addons = {cp.addon_to.pk for cp in lcp if cp.addon_to} pos_additional_fields = defaultdict(list) for cp in lcp: responses = question_form_fields.send(sender=self.request.event, position=cp) data = cp.meta_info_data for r, response in sorted(responses, key=lambda r: str(r[0])): for key, value in response.items(): pos_additional_fields[cp.pk].append({ 'answer': data.get('question_form_data', {}).get(key), 'question': value.label }) # Group items of the same variation # We do this by list manipulations instead of a GROUP BY query, as # Django is unable to join related models in a .values() query def keyfunc(pos): if isinstance(pos, OrderPosition): if pos.addon_to: i = pos.addon_to.positionid else: i = pos.positionid else: if pos.addon_to: i = pos.addon_to.pk else: i = pos.pk has_attendee_data = pos.item.admission and ( self.request.event.settings.attendee_names_asked or self.request.event.settings.attendee_emails_asked or pos_additional_fields.get(pos.pk) ) addon_penalty = 1 if pos.addon_to else 0 if downloads or pos.pk in has_addons or pos.addon_to: return i, addon_penalty, pos.pk, 0, 0, 0, 0, (pos.subevent_id or 0) if answers and (has_attendee_data or pos.item.questions.all()): return i, addon_penalty, pos.pk, 0, 0, 0, 0, (pos.subevent_id or 0) return ( 0, addon_penalty, 0, pos.item_id, pos.variation_id, pos.price, (pos.voucher_id or 0), (pos.subevent_id or 0) ) positions = [] for k, g in groupby(sorted(lcp, key=keyfunc), key=keyfunc): g = list(g) group = g[0] group.count = len(g) group.total = group.count * group.price group.net_total = group.count * group.net_price group.has_questions = answers and k[0] != "" group.tax_rule = group.item.tax_rule if answers: group.cache_answers() group.additional_answers = pos_additional_fields.get(group.pk) positions.append(group) total = sum(p.total for p in positions) net_total = sum(p.net_total for p in positions) tax_total = sum(p.total - p.net_total for p in positions) if order: fees = order.fees.all() else: fees = get_fees(self.request.event, self.request, total, self.invoice_address, self.cart_session.get('payment')) total += sum([f.value for f in fees]) net_total += sum([f.net_value for f in fees]) tax_total += sum([f.tax_value for f in fees]) 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, 'raw': cartpos, 'total': total, 'net_total': net_total, 'tax_total': tax_total, 'fees': fees, 'answers': answers, 'minutes_left': minutes_left, 'seconds_left': seconds_left, 'first_expiry': first_expiry, } def get_cart(request): from pretix.presale.views.cart import get_or_create_cart_id if not hasattr(request, '_cart_cache'): request._cart_cache = CartPosition.objects.filter( cart_id=get_or_create_cart_id(request), event=request.event ).order_by( 'item', 'variation' ).select_related( 'item', 'variation', 'subevent', 'subevent__event', 'subevent__event__organizer', 'item__tax_rule' ).prefetch_related( 'item__questions', 'answers' ) return request._cart_cache def get_cart_total(request): from pretix.presale.views.cart import get_or_create_cart_id if not hasattr(request, '_cart_total_cache'): if hasattr(request, '_cart_cache'): request._cart_total_cache = sum(i.price for i in request._cart_cache) else: request._cart_total_cache = CartPosition.objects.filter( cart_id=get_or_create_cart_id(request), event=request.event ).aggregate(sum=Sum('price'))['sum'] or 0 return request._cart_total_cache class EventViewMixin: def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) 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)