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…
Reference in a new issue