From 0026309b85b3d16f9a03bec331f53bee38a9f003 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 12 Feb 2015 18:51:23 +0100 Subject: [PATCH] First implementation of adding things to your cart --- src/pretixbase/models.py | 87 ++++++++++++- .../templates/pretixpresale/event/index.html | 2 +- src/pretixpresale/views/cart.py | 123 +++++++++++++++++- 3 files changed, 200 insertions(+), 12 deletions(-) diff --git a/src/pretixbase/models.py b/src/pretixbase/models.py index bf15478e64..6701edb811 100644 --- a/src/pretixbase/models.py +++ b/src/pretixbase/models.py @@ -723,7 +723,7 @@ class Item(Versionable): self._get_all_variations_cache = result return result - def get_all_available_variations(self): + def get_all_available_variations(self, use_cache: bool=False): """ This method returns a list of all variations which are theoretically possible for sale. It DOES call all activated restriction plugins, and it @@ -740,6 +740,9 @@ class Item(Versionable): prefetch_related('properties', 'variations__values__prop') when retrieving Item objects you are going to use this method on. """ + if use_cache and hasattr(self, '_get_all_available_variations_cache'): + return self._get_all_available_variations_cache + from .signals import determine_availability if self.properties.count() == 0: variations = [VariationDict()] @@ -775,6 +778,7 @@ class Item(Versionable): and response[i]['price'] < var['price']: var['price'] = response[i]['price'] + self._get_all_available_variations_cache = variations return variations def availability(self): @@ -787,6 +791,31 @@ class Item(Versionable): 'but call this on their ItemVariation objects') return min([q.availability() for q in self.quotas.all()]) + def execute_restrictions(self): + """ + This method is used to determine whether this ItemVariation is restricted + in sale by any restriction plugins. + It returns False, if the item is unavailable or the item's price, if it is + available. + """ + if self.properties.count() > 0: + raise ValueError('Do not call this directly on items which have properties ' + 'but call this on their ItemVariation objects') + from .signals import determine_availability + vd = VariationDict() + responses = determine_availability.send( + self.event, item=self, + variations=[vd], context=None, + cache=self.event.get_cache() + ) + price = self.default_price + for receiver, response in responses: + if 'available' in response[0] and not response[0]['available']: + return False + elif 'price' in response[0] and response[0]['price'] < price: + price = response[0]['price'] + return price + class ItemVariation(Versionable): """ @@ -841,11 +870,40 @@ class ItemVariation(Versionable): def availability(self): """ - This method is used to determine whether this Item is currently available - for sale. It may return any of the return codes of Quota.availability() + This method is used to determine whether this ItemVariation is currently + available for sale in terms of quotas. It may return any of the return codes + of Quota.availability() """ return min([q.availability() for q in self.quotas.all()]) + def to_variation_dict(self): + vd = VariationDict() + for v in self.values.all(): + vd[v.prop.identity] = v + vd['variation'] = self + return vd + + def execute_restrictions(self): + """ + This method is used to determine whether this ItemVariation is restricted + in sale by any restriction plugins. + It returns False, if the item is unavailable or the item's price, if it is + available. + """ + from .signals import determine_availability + responses = determine_availability.send( + self.item.event, item=self.item, + variations=[self.to_variation_dict()], context=None, + cache=self.item.event.get_cache() + ) + price = self.default_price if self.default_price is not None else self.item.default_price + for receiver, response in responses: + if 'available' in response[0] and not response[0]['available']: + return False + elif 'price' in response[0] and response[0]['price'] < price: + price = response[0]['price'] + return price + class VariationsField(VersionedManyToManyField): """ @@ -1028,7 +1086,7 @@ class Quota(Versionable): & quotalookup ).count() if paid_orders >= self.size: - return (Quota.AVAILABILITY_GONE, 0) + return Quota.AVAILABILITY_GONE, 0 pending_valid_orders = OrderPosition.objects.filter( Q(order__status=Order.STATUS_PENDING) @@ -1036,16 +1094,31 @@ class Quota(Versionable): & quotalookup ).count() if (paid_orders + pending_valid_orders) >= self.size: - return (Quota.AVAILABILITY_ORDERED, 0) + return Quota.AVAILABILITY_ORDERED, 0 valid_cart_positions = CartPosition.objects.filter( Q(expires__gte=now()) & quotalookup ).count() if (paid_orders + pending_valid_orders + valid_cart_positions) >= self.size: - return (Quota.AVAILABILITY_RESERVED, 0) + return Quota.AVAILABILITY_RESERVED, 0 - return (Quota.AVAILABILITY_OK, self.size - paid_orders - pending_valid_orders - valid_cart_positions) + return Quota.AVAILABILITY_OK, self.size - paid_orders - pending_valid_orders - valid_cart_positions + + def lock(self): + """ + Issue a lock on this quota so nobody can take tickets from this quota until + you release the lock + """ + pass + + def release(self, force=False): + """ + Release a lock placed by lock(). If the parameter force is not set, + the lock will only be released if it was issued in _this_ python + representation of the database object. + """ + pass class Order(Versionable): diff --git a/src/pretixpresale/templates/pretixpresale/event/index.html b/src/pretixpresale/templates/pretixpresale/event/index.html index 6d13e12819..3119de4d61 100644 --- a/src/pretixpresale/templates/pretixpresale/event/index.html +++ b/src/pretixpresale/templates/pretixpresale/event/index.html @@ -34,7 +34,7 @@
+ name="variation_{{ item.identity }}_{{ var.variation.identity }}">
{% else %} {% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability.0 %} diff --git a/src/pretixpresale/views/cart.py b/src/pretixpresale/views/cart.py index 82be31d723..ad8eed35c2 100644 --- a/src/pretixpresale/views/cart.py +++ b/src/pretixpresale/views/cart.py @@ -1,27 +1,142 @@ +from datetime import timedelta +import uuid + from django.contrib import messages from django.core.urlresolvers import reverse from django.shortcuts import redirect +from django.utils.timezone import now from django.views.generic import View from django.utils.translation import ugettext_lazy as _ from .event import EventViewMixin +from pretixbase.models import Item, ItemVariation, Quota, CartPosition class CartActionMixin: - def get_success_url(self): + def get_next_url(self): if "next" in self.request.GET and '://' not in self.request.GET: return self.request.GET.get('next') else: return reverse('presale:event.index', kwargs={ 'event': self.request.event.slug, 'organizer': self.request.event.organizer.slug, - }) + }) + + def get_success_url(self): + return self.get_next_url() + + def get_failure_url(self): + return self.get_next_url() + + def get_session_key(self): + if 'cart_key' in self.request.session: + return self.request.session.get('cart_key') + key = str(uuid.uuid4()) + self.request.session['cart_key'] = key + return key class CartAdd(EventViewMixin, CartActionMixin, View): def post(self, *args, **kwargs): - messages.error(self.request, _('Cart is not yet implemented')) - print("hi") + # Parse input + items = [] + for key, value in self.request.POST.items(): + if value.strip() == '': + continue + if key.startswith('item_'): + try: + items.append((key.split("_")[1], None, int(value))) + except ValueError: + messages.error(self.request, _('Please only enter numbers.')) + return redirect(self.get_failure_url()) + elif key.startswith('variation_'): + try: + items.append((key.split("_")[1], key.split("_")[2], int(value))) + except ValueError: + messages.error(self.request, _('Please only enter numbers.')) + return redirect(self.get_failure_url()) + + if sum(i[2] for i in items) > self.request.event.max_items_per_order: + # TODO: Plurals + messages.error(self.request, + _("You cannot select more than %d items per order") % self.event.max_items_per_order) + return redirect(self.get_failure_url()) + + # items is now a list of tuples of the form + # (item id, variation id or None, number) + + # Fetch items from the database + items_cache = { + i.identity: i for i + in Item.objects.filter( + event=self.request.event, + id__in=[i[0] for i in items] + ).prefetch_related("quotas") + } + variations_cache = { + v.identity: v for v + in ItemVariation.objects.filter( + item__event=self.request.event, + id__in=[i[1] for i in items if i[1] is not None] + ).select_related("item", "item__event").prefetch_related("quotas", "values", "values__prop") + } + + # Process the request itself + msg_some_unavailable = False + for i in items: + if i[0] not in items_cache or (i[1] is not None and i[1] not in variations_cache): + messages.error(self.request, _('You selected an item which is not available for sale.')) + return redirect(self.get_failure_url()) + item = items_cache[i[0]] + variation = variations_cache[i[1]] if i[1] is not None else None + price = item.execute_restrictions() if variation is None else variation.execute_restrictions() + + if price is False: + msg_some_unavailable = True + messages.error(self.request, + _('Some of the items you selected were no longer available. ' + 'Please see below for details.')) + continue + + quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all()) + quota_ok = i[2] + try: + for quota in quotas: + quota.lock() + avail = quota.availability() + if avail[0] != Quota.AVAILABILITY_OK: + if not msg_some_unavailable: + msg_some_unavailable = True + messages.error(self.request, + _('Some of the items you selected were no longer available. ' + 'Please see below for details.')) + quota_ok = 0 + break + elif avail[1] < i[2]: + if not msg_some_unavailable: + msg_some_unavailable = True + messages.error(self.request, + _('Some of the items you selected were no longer available in ' + 'the quantity you selected. Please see below for details.')) + quota_ok = min(quota_ok, avail[1]) + + for k in range(quota_ok): + CartPosition.objects.create( + event=self.request.event, + session=self.get_session_key(), + user=(self.request.user if self.request.user.is_authenticated() else None), + item=item, + variation=variation, + price=price, + expires=now() + timedelta(minutes=30) + ) + finally: + for quota in quotas: + quota.release() + + if not msg_some_unavailable: + messages.success(self.request, _('The items have been successfully added to your cart.')) + return redirect(self.get_success_url())