Changeset - 941caa30d9f4
[Not reviewed]
0 2 0
Christopher Neugebauer - 8 years ago 2016-04-30 10:30:21
chrisjrn@gmail.com
Replaces ProductController.attach_user_remainders with ProductController.user_remainders
2 files changed with 9 insertions and 22 deletions:
0 comments (0 inline, 0 general)
registrasion/controllers/cart.py
Show inline comments
...
 
@@ -124,202 +124,200 @@ class CartController(object):
 

	
 
    def _autoextend_reservation(self):
 
        ''' Updates the cart's time last updated value, which is used to
 
        determine whether the cart has reserved the items and discounts it
 
        holds. '''
 

	
 
        reservations = [datetime.timedelta()]
 

	
 
        # If we have vouchers, we're entitled to an hour at minimum.
 
        if len(self.cart.vouchers.all()) >= 1:
 
            reservations.append(inventory.Voucher.RESERVATION_DURATION)
 

	
 
        # Else, it's the maximum of the included products
 
        items = commerce.ProductItem.objects.filter(cart=self.cart)
 
        agg = items.aggregate(Max("product__reservation_duration"))
 
        product_max = agg["product__reservation_duration__max"]
 

	
 
        if product_max is not None:
 
            reservations.append(product_max)
 

	
 
        self.cart.time_last_updated = timezone.now()
 
        self.cart.reservation_duration = max(reservations)
 

	
 
    def _end_batch(self):
 
        ''' Performs operations that occur occur at the end of a batch of
 
        product changes/voucher applications etc.
 

	
 
        You need to call this after you've finished modifying the user's cart.
 
        This is normally done by wrapping a block of code using
 
        ``operations_batch``.
 

	
 
        '''
 

	
 
        self.cart.refresh_from_db()
 

	
 
        self._recalculate_discounts()
 

	
 
        self._autoextend_reservation()
 
        self.cart.revision += 1
 
        self.cart.save()
 

	
 
    @_modifies_cart
 
    def set_quantities(self, product_quantities):
 
        ''' Sets the quantities on each of the products on each of the
 
        products specified. Raises an exception (ValidationError) if a limit
 
        is violated. `product_quantities` is an iterable of (product, quantity)
 
        pairs. '''
 

	
 
        items_in_cart = commerce.ProductItem.objects.filter(cart=self.cart)
 
        items_in_cart = items_in_cart.select_related(
 
            "product",
 
            "product__category",
 
        )
 

	
 
        product_quantities = list(product_quantities)
 

	
 
        # n.b need to add have the existing items first so that the new
 
        # items override the old ones.
 
        all_product_quantities = dict(itertools.chain(
 
            ((i.product, i.quantity) for i in items_in_cart.all()),
 
            product_quantities,
 
        )).items()
 

	
 
        # Validate that the limits we're adding are OK
 
        self._test_limits(all_product_quantities)
 

	
 
        new_items = []
 
        products = []
 
        for product, quantity in product_quantities:
 
            products.append(product)
 

	
 
            if quantity == 0:
 
                continue
 

	
 
            item = commerce.ProductItem(
 
                cart=self.cart,
 
                product=product,
 
                quantity=quantity,
 
            )
 
            new_items.append(item)
 

	
 
        to_delete = (
 
            Q(quantity=0) |
 
            Q(product__in=products)
 
        )
 

	
 
        items_in_cart.filter(to_delete).delete()
 
        commerce.ProductItem.objects.bulk_create(new_items)
 

	
 
    def _test_limits(self, product_quantities):
 
        ''' Tests that the quantity changes we intend to make do not violate
 
        the limits and flag conditions imposed on the products. '''
 

	
 
        errors = []
 

	
 
        # Pre-annotate products
 
        products = [p for (p, q) in product_quantities]
 
        r = ProductController.attach_user_remainders(self.cart.user, products)
 
        with_remainders = dict((p, p) for p in r)
 
        remainders = ProductController.user_remainders(self.cart.user)
 

	
 
        # Test each product limit here
 
        for product, quantity in product_quantities:
 
            if quantity < 0:
 
                errors.append((product, "Value must be zero or greater."))
 

	
 
            limit = with_remainders[product].remainder
 
            limit = remainders[product.id]
 

	
 
            if quantity > limit:
 
                errors.append((
 
                    product,
 
                    "You may only have %d of product: %s" % (
 
                        limit, product,
 
                    )
 
                ))
 

	
 
        # Collect by category
 
        by_cat = collections.defaultdict(list)
 
        for product, quantity in product_quantities:
 
            by_cat[product.category].append((product, quantity))
 

	
 
        # Pre-annotate categories
 
        r = CategoryController.attach_user_remainders(self.cart.user, by_cat)
 
        with_remainders = dict((cat, cat) for cat in r)
 

	
 
        # Test each category limit here
 
        for category in by_cat:
 
            limit = with_remainders[category].remainder
 

	
 
            # Get the amount so far in the cart
 
            to_add = sum(i[1] for i in by_cat[category])
 

	
 
            if to_add > limit:
 
                errors.append((
 
                    category,
 
                    "You may only have %d items in category: %s" % (
 
                        limit, category.name,
 
                    )
 
                ))
 

	
 
        # Test the flag conditions
 
        errs = FlagController.test_flags(
 
            self.cart.user,
 
            product_quantities=product_quantities,
 
        )
 

	
 
        if errs:
 
            for error in errs:
 
                errors.append(error)
 

	
 
        if errors:
 
            raise CartValidationError(errors)
 

	
 
    @_modifies_cart
 
    def apply_voucher(self, voucher_code):
 
        ''' Applies the voucher with the given code to this cart. '''
 

	
 
        # Try and find the voucher
 
        voucher = inventory.Voucher.objects.get(code=voucher_code.upper())
 

	
 
        # Re-applying vouchers should be idempotent
 
        if voucher in self.cart.vouchers.all():
 
            return
 

	
 
        self._test_voucher(voucher)
 

	
 
        # If successful...
 
        self.cart.vouchers.add(voucher)
 

	
 
    def _test_voucher(self, voucher):
 
        ''' Tests whether this voucher is allowed to be applied to this cart.
 
        Raises ValidationError if not. '''
 

	
 
        # Is voucher exhausted?
 
        active_carts = commerce.Cart.reserved_carts()
 

	
 
        # It's invalid for a user to enter a voucher that's exhausted
 
        carts_with_voucher = active_carts.filter(vouchers=voucher)
 
        carts_with_voucher = carts_with_voucher.exclude(pk=self.cart.id)
 
        if carts_with_voucher.count() >= voucher.limit:
 
            raise ValidationError(
 
                "Voucher %s is no longer available" % voucher.code)
 

	
 
        # It's not valid for users to re-enter a voucher they already have
 
        user_carts_with_voucher = carts_with_voucher.filter(
 
            user=self.cart.user,
 
        )
 

	
 
        if user_carts_with_voucher.count() > 0:
 
            raise ValidationError("You have already entered this voucher.")
 

	
 
    def _test_vouchers(self, vouchers):
 
        ''' Tests each of the vouchers against self._test_voucher() and raises
 
        the collective ValidationError.
 
        Future work will refactor _test_voucher in terms of this, and save some
 
        queries. '''
 
        errors = []
 
        for voucher in vouchers:
 
            try:
 
                self._test_voucher(voucher)
 
            except ValidationError as ve:
 
                errors.append(ve)
 

	
registrasion/controllers/product.py
Show inline comments
 
import itertools
 

	
 
from django.db.models import Case
 
from django.db.models import F, Q
 
from django.db.models import Sum
 
from django.db.models import When
 
from django.db.models import Value
 

	
 
from registrasion.models import commerce
 
from registrasion.models import inventory
 

	
 
from .category import CategoryController
 
from .flag import FlagController
 

	
 

	
 
class ProductController(object):
 

	
 
    def __init__(self, product):
 
        self.product = product
 

	
 
    @classmethod
 
    def available_products(cls, user, category=None, products=None):
 
        ''' Returns a list of all of the products that are available per
 
        flag conditions from the given categories. '''
 
        if category is None and products is None:
 
            raise ValueError("You must provide products or a category")
 

	
 
        if category is not None:
 
            all_products = inventory.Product.objects.filter(category=category)
 
            all_products = all_products.select_related("category")
 
        else:
 
            all_products = []
 

	
 
        if products is not None:
 
            all_products = set(itertools.chain(all_products, products))
 

	
 
        categories = set(product.category for product in all_products)
 
        r = CategoryController.attach_user_remainders(user, categories)
 
        cat_quants = dict((c, c) for c in r)
 

	
 
        r = ProductController.attach_user_remainders(user, all_products)
 
        prod_quants = dict((p, p) for p in r)
 
        product_remainders = ProductController.user_remainders(user)
 

	
 
        passed_limits = set(
 
            product
 
            for product in all_products
 
            if cat_quants[product.category].remainder > 0
 
            if prod_quants[product].remainder > 0
 
            if product_remainders[product.id] > 0
 
        )
 

	
 
        failed_and_messages = FlagController.test_flags(
 
            user, products=passed_limits
 
        )
 
        failed_conditions = set(i[0] for i in failed_and_messages)
 

	
 
        out = list(passed_limits - failed_conditions)
 
        out.sort(key=lambda product: product.order)
 

	
 
        return out
 

	
 
    @classmethod
 
    def attach_user_remainders(cls, user, products):
 
    def user_remainders(cls, user):
 
        '''
 

	
 
        Return:
 
            queryset(inventory.Product): A queryset containing items from
 
            ``product``, with an extra attribute -- remainder = the amount of
 
            this item that is remaining.
 
            Mapping[int->int]: A dictionary that maps the product ID to the
 
            user's remainder for that product.
 
        '''
 

	
 
        ids = [product.id for product in products]
 
        products = inventory.Product.objects.filter(id__in=ids)
 
        products = inventory.Product.objects.all()
 

	
 
        cart_filter = (
 
            Q(productitem__cart__user=user) &
 
            Q(productitem__cart__status=commerce.Cart.STATUS_PAID)
 
        )
 

	
 
        quantity = When(
 
            cart_filter,
 
            then='productitem__quantity'
 
        )
 

	
 
        quantity_or_zero = Case(
 
            quantity,
 
            default=Value(0),
 
        )
 

	
 
        remainder = Case(
 
            When(limit_per_user=None, then=Value(99999999)),
 
            default=F('limit_per_user') - Sum(quantity_or_zero),
 
        )
 

	
 
        products = products.annotate(remainder=remainder)
 

	
 
        return products
 

	
 
    def user_quantity_remaining(self, user):
 
        ''' Returns the quantity of this product that the user add in the
 
        current cart. '''
 

	
 
        with_remainders = self.attach_user_remainders(user, [self.product])
 

	
 
        return with_remainders[0].remainder
 
        return dict((product.id, product.remainder) for product in products)
0 comments (0 inline, 0 general)