Refactors testing of enabling conditions so that they are done in bulk in ConditionsController, rather than one product at a time.
This commit is contained in:
		
							parent
							
								
									5716af0afa
								
							
						
					
					
						commit
						1e7a2abc7f
					
				
					 4 changed files with 151 additions and 37 deletions
				
			
		|  | @ -117,7 +117,18 @@ class CartController(object): | |||
|                     ) | ||||
|                 ) | ||||
| 
 | ||||
|         product_quantities_all = list(product_quantities) + [ | ||||
|             (i.product, i.quantity) for i in items_in_cart.all() | ||||
|         ] | ||||
| 
 | ||||
|         # Test each enabling condition here | ||||
|         errs = ConditionController.test_enabling_conditions( | ||||
|             self.cart.user, | ||||
|             product_quantities=product_quantities_all, | ||||
|         ) | ||||
| 
 | ||||
|         if errs: | ||||
|             raise ValidationError("Whoops") | ||||
| 
 | ||||
|         for product, quantity in product_quantities: | ||||
|             self._set_quantity_old(product, quantity) | ||||
|  | @ -162,10 +173,6 @@ class CartController(object): | |||
|         adjustment = quantity - old_quantity | ||||
|         prod = ProductController(product) | ||||
| 
 | ||||
|         if not prod.can_add_with_enabling_conditions( | ||||
|                 self.cart.user, adjustment): | ||||
|             raise ValidationError("Not enough of that product left (ec)") | ||||
| 
 | ||||
|         product_item.quantity = quantity | ||||
|         product_item.save() | ||||
| 
 | ||||
|  | @ -245,7 +252,7 @@ class CartController(object): | |||
| 
 | ||||
|             quantity = 0 if is_reserved else discount_item.quantity | ||||
| 
 | ||||
|             if not cond.is_met(self.cart.user, quantity): | ||||
|             if not cond.is_met(self.cart.user): # TODO: REPLACE WITH QUANTITY CHECKER WHEN FIXING CEILINGS | ||||
|                 raise ValidationError("Discounts are no longer available") | ||||
| 
 | ||||
|     def recalculate_discounts(self): | ||||
|  |  | |||
|  | @ -1,3 +1,8 @@ | |||
| import itertools | ||||
| 
 | ||||
| from collections import defaultdict | ||||
| from collections import namedtuple | ||||
| 
 | ||||
| from django.db.models import Q | ||||
| from django.db.models import Sum | ||||
| from django.utils import timezone | ||||
|  | @ -5,6 +10,15 @@ from django.utils import timezone | |||
| from registrasion import models as rego | ||||
| 
 | ||||
| 
 | ||||
| ConditionAndRemainder = namedtuple( | ||||
|     "ConditionAndRemainder", | ||||
|     ( | ||||
|         "condition", | ||||
|         "remainder", | ||||
|     ), | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| class ConditionController(object): | ||||
|     ''' Base class for testing conditions that activate EnablingCondition | ||||
|     or Discount objects. ''' | ||||
|  | @ -31,8 +45,103 @@ class ConditionController(object): | |||
|         except KeyError: | ||||
|             return ConditionController() | ||||
| 
 | ||||
|     def is_met(self, user, quantity): | ||||
|         return True | ||||
|     @classmethod | ||||
|     def test_enabling_conditions( | ||||
|             cls, user, products=None, product_quantities=None): | ||||
|         ''' Evaluates all of the enabling conditions on the given products. | ||||
| 
 | ||||
|         If `product_quantities` is supplied, the condition is only met if it | ||||
|         will permit the sum of the product quantities for all of the products | ||||
|         it covers. Otherwise, it will be met if at least one item can be | ||||
|         accepted. | ||||
| 
 | ||||
|         If all enabling conditions pass, an empty list is returned, otherwise | ||||
|         a list is returned containing all of the products that are *not | ||||
|         enabled*. ''' | ||||
| 
 | ||||
|         if products is not None and product_quantities is not None: | ||||
|             raise ValueError("Please specify only products or " | ||||
|                              "product_quantities") | ||||
|         elif products is None: | ||||
|             products = set(i[0] for i in product_quantities) | ||||
|             quantities = dict( (product, quantity) | ||||
|                 for product, quantity in product_quantities ) | ||||
|         elif product_quantities is None: | ||||
|             products = set(products) | ||||
|             quantities = {} | ||||
| 
 | ||||
|         # Get the conditions covered by the products themselves | ||||
|         all_conditions = [ | ||||
|             product.enablingconditionbase_set.select_subclasses() | | ||||
|             product.category.enablingconditionbase_set.select_subclasses() | ||||
|             for product in products | ||||
|         ] | ||||
|         all_conditions = set(itertools.chain(*all_conditions)) | ||||
| 
 | ||||
|         # All mandatory conditions on a product need to be met | ||||
|         mandatory = defaultdict(lambda: True) | ||||
|         # At least one non-mandatory condition on a product must be met | ||||
|         # if there are no mandatory conditions | ||||
|         non_mandatory = defaultdict(lambda: False) | ||||
| 
 | ||||
|         remainders = [] | ||||
|         for condition in all_conditions: | ||||
|             cond = cls.for_condition(condition) | ||||
|             remainder = cond.user_quantity_remaining(user) | ||||
| 
 | ||||
|             # Get all products covered by this condition, and the products | ||||
|             # from the categories covered by this condition | ||||
|             cond_products = condition.products.all() | ||||
|             from_category = rego.Product.objects.filter( | ||||
|                 category__in=condition.categories.all(), | ||||
|             ).all() | ||||
|             all_products = set(itertools.chain(cond_products, from_category)) | ||||
| 
 | ||||
|             # Remove the products that we aren't asking about | ||||
|             all_products = all_products & products | ||||
| 
 | ||||
|             if quantities: | ||||
|                 consumed = sum(quantities[i] for i in all_products) | ||||
|             else: | ||||
|                 consumed = 1 | ||||
|             met = consumed <= remainder | ||||
| 
 | ||||
|             for product in all_products: | ||||
|                 if condition.mandatory: | ||||
|                     mandatory[product] &= met | ||||
|                 else: | ||||
|                     non_mandatory[product] |= met | ||||
| 
 | ||||
|         valid = defaultdict(lambda: True) | ||||
|         for product in itertools.chain(mandatory, non_mandatory): | ||||
|             if product in mandatory: | ||||
|                 # If there's a mandatory condition, all must be met | ||||
|                 valid[product] = mandatory[product] | ||||
|             else: | ||||
|                 # Otherwise, we need just one non-mandatory condition met | ||||
|                 valid[product] = non_mandatory[product] | ||||
| 
 | ||||
|         error_fields = [product for product in valid if not valid[product]] | ||||
|         return error_fields | ||||
| 
 | ||||
|     def user_quantity_remaining(self, user): | ||||
|         ''' Returns the number of items covered by this enabling condition the | ||||
|         user can add to the current cart. This default implementation returns | ||||
|         a big number if is_met() is true, otherwise 0. | ||||
| 
 | ||||
|         Either this method, or is_met() must be overridden in subclasses. | ||||
|         ''' | ||||
| 
 | ||||
|         return 99999999 if self.is_met(user) else 0 | ||||
| 
 | ||||
|     def is_met(self, user): | ||||
|         ''' Returns True if this enabling condition is met, otherwise returns | ||||
|         False. | ||||
| 
 | ||||
|         Either this method, or user_quantity_remaining() must be overridden | ||||
|         in subclasses. | ||||
|         ''' | ||||
|         return self.user_quantity_remaining(user) > 0 | ||||
| 
 | ||||
| 
 | ||||
| class CategoryConditionController(ConditionController): | ||||
|  | @ -40,7 +149,7 @@ class CategoryConditionController(ConditionController): | |||
|     def __init__(self, condition): | ||||
|         self.condition = condition | ||||
| 
 | ||||
|     def is_met(self, user, quantity): | ||||
|     def is_met(self, user): | ||||
|         ''' returns True if the user has a product from a category that invokes | ||||
|         this condition in one of their carts ''' | ||||
| 
 | ||||
|  | @ -62,7 +171,7 @@ class ProductConditionController(ConditionController): | |||
|     def __init__(self, condition): | ||||
|         self.condition = condition | ||||
| 
 | ||||
|     def is_met(self, user, quantity): | ||||
|     def is_met(self, user): | ||||
|         ''' returns True if the user has a product that invokes this | ||||
|         condition in one of their carts ''' | ||||
| 
 | ||||
|  | @ -81,22 +190,17 @@ class TimeOrStockLimitConditionController(ConditionController): | |||
|     def __init__(self, ceiling): | ||||
|         self.ceiling = ceiling | ||||
| 
 | ||||
|     def is_met(self, user, quantity): | ||||
|         ''' returns True if adding _quantity_ of _product_ will not vioilate | ||||
|         this ceiling. ''' | ||||
|     def user_quantity_remaining(self, user): | ||||
|         ''' returns 0 if the date range is violated, otherwise, it will return | ||||
|         the quantity remaining under the stock limit. ''' | ||||
| 
 | ||||
|         # Test date range | ||||
|         if not self.test_date_range(): | ||||
|             return False | ||||
|         if not self._test_date_range(): | ||||
|             return 0 | ||||
| 
 | ||||
|         # Test limits | ||||
|         if not self.test_limits(quantity): | ||||
|             return False | ||||
|         return self._get_remaining_stock(user) | ||||
| 
 | ||||
|         # All limits have been met | ||||
|         return True | ||||
| 
 | ||||
|     def test_date_range(self): | ||||
|     def _test_date_range(self): | ||||
|         now = timezone.now() | ||||
| 
 | ||||
|         if self.ceiling.start_time is not None: | ||||
|  | @ -114,7 +218,7 @@ class TimeOrStockLimitConditionController(ConditionController): | |||
|         list products differently to discounts. ''' | ||||
|         if isinstance(self.ceiling, rego.TimeOrStockLimitEnablingCondition): | ||||
|             category_products = rego.Product.objects.filter( | ||||
|                 category=self.ceiling.categories.all(), | ||||
|                 category__in=self.ceiling.categories.all(), | ||||
|             ) | ||||
|             return self.ceiling.products.all() | category_products | ||||
|         else: | ||||
|  | @ -123,28 +227,31 @@ class TimeOrStockLimitConditionController(ConditionController): | |||
|             ) | ||||
|             return rego.Product.objects.filter( | ||||
|                 Q(discountforproduct__discount=self.ceiling) | | ||||
|                 Q(category=categories.all()) | ||||
|                 Q(category__in=categories.all()) | ||||
|             ) | ||||
| 
 | ||||
|     def test_limits(self, quantity): | ||||
|         if self.ceiling.limit is None: | ||||
|             return True | ||||
|     def _get_remaining_stock(self, user): | ||||
|         ''' Returns the stock that remains under this ceiling, excluding the | ||||
|         user's current cart. ''' | ||||
| 
 | ||||
|         if self.ceiling.limit is None: | ||||
|             return 99999999 | ||||
| 
 | ||||
|         # We care about all reserved carts, but not the user's current cart | ||||
|         reserved_carts = rego.Cart.reserved_carts() | ||||
|         reserved_carts = reserved_carts.exclude( | ||||
|             user=user, | ||||
|             active=True, | ||||
|         ) | ||||
| 
 | ||||
|         product_items = rego.ProductItem.objects.filter( | ||||
|             product__in=self._products().all(), | ||||
|         ) | ||||
|         product_items = product_items.filter(cart=reserved_carts) | ||||
| 
 | ||||
|         agg = product_items.aggregate(Sum("quantity")) | ||||
|         count = agg["quantity__sum"] | ||||
|         if count is None: | ||||
|             count = 0 | ||||
|         count = product_items.aggregate(Sum("quantity"))["quantity__sum"] or 0 | ||||
| 
 | ||||
|         if count + quantity > self.ceiling.limit: | ||||
|             return False | ||||
| 
 | ||||
|         return True | ||||
|         return self.ceiling.limit - count | ||||
| 
 | ||||
| 
 | ||||
| class VoucherConditionController(ConditionController): | ||||
|  | @ -153,7 +260,7 @@ class VoucherConditionController(ConditionController): | |||
|     def __init__(self, condition): | ||||
|         self.condition = condition | ||||
| 
 | ||||
|     def is_met(self, user, quantity): | ||||
|     def is_met(self, user): | ||||
|         ''' returns True if the user has the given voucher attached. ''' | ||||
|         carts_count = rego.Cart.objects.filter( | ||||
|             user=user, | ||||
|  |  | |||
|  | @ -75,7 +75,7 @@ def available_discounts(user, categories, products): | |||
|             pass | ||||
|         elif real_discount not in failed_discounts: | ||||
|             # This clause is still available | ||||
|             if real_discount in accepted_discounts or cond.is_met(user, 0): | ||||
|             if real_discount in accepted_discounts or cond.is_met(user): | ||||
|                 # This clause is valid for this user | ||||
|                 discounts.append(DiscountAndQuantity( | ||||
|                     discount=real_discount, | ||||
|  |  | |||
|  | @ -79,7 +79,7 @@ class ProductController(object): | |||
| 
 | ||||
|         for condition in conditions: | ||||
|             cond = ConditionController.for_condition(condition) | ||||
|             met = cond.is_met(user, quantity) | ||||
|             met = cond.is_met(user) | ||||
| 
 | ||||
|             if condition.mandatory and not met: | ||||
|                 mandatory_violated = True | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer