From 145fd057aca06e846f55d33c23c79dd0249a6569 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 28 Apr 2016 12:13:42 +1000 Subject: [PATCH] Breaks out flag-handling code into flag.py and FlagController --- registrasion/controllers/cart.py | 9 +- registrasion/controllers/conditions.py | 259 +------------------------ registrasion/controllers/flag.py | 257 ++++++++++++++++++++++++ registrasion/controllers/product.py | 7 +- 4 files changed, 268 insertions(+), 264 deletions(-) create mode 100644 registrasion/controllers/flag.py diff --git a/registrasion/controllers/cart.py b/registrasion/controllers/cart.py index e7282e9a..3eb4ce37 100644 --- a/registrasion/controllers/cart.py +++ b/registrasion/controllers/cart.py @@ -15,9 +15,10 @@ from registrasion.models import commerce from registrasion.models import conditions from registrasion.models import inventory -from category import CategoryController -from conditions import ConditionController -from product import ProductController +from .category import CategoryController +from .conditions import ConditionController +from .flag import FlagController +from .product import ProductController def _modifies_cart(func): @@ -185,7 +186,7 @@ class CartController(object): )) # Test the flag conditions - errs = ConditionController.test_flags( + errs = FlagController.test_flags( self.cart.user, product_quantities=product_quantities, ) diff --git a/registrasion/controllers/conditions.py b/registrasion/controllers/conditions.py index 291a573a..c0847b10 100644 --- a/registrasion/controllers/conditions.py +++ b/registrasion/controllers/conditions.py @@ -18,74 +18,7 @@ from registrasion.models import inventory -ConditionAndRemainder = namedtuple( - "ConditionAndRemainder", - ( - "condition", - "remainder", - ), -) - - -_FlagCounter = namedtuple( - "_FlagCounter", - ( - "products", - "categories", - ), -) - - -_ConditionsCount = namedtuple( - "ConditionsCount", - ( - "dif", - "eit", - ), -) - - -class FlagCounter(_FlagCounter): - - @classmethod - def count(cls): - # Get the count of how many conditions should exist per product - flagbases = conditions.FlagBase.objects - - types = ( - conditions.FlagBase.ENABLE_IF_TRUE, - conditions.FlagBase.DISABLE_IF_FALSE, - ) - keys = ("eit", "dif") - flags = [ - flagbases.filter( - condition=condition_type - ).values( - 'products', 'categories' - ).annotate( - count=Count('id') - ) - for condition_type in types - ] - - cats = defaultdict(lambda: defaultdict(int)) - prod = defaultdict(lambda: defaultdict(int)) - - for key, flagcounts in zip(keys, flags): - for row in flagcounts: - if row["products"] is not None: - prod[row["products"]][key] = row["count"] - if row["categories"] is not None: - cats[row["categories"]][key] = row["count"] - - return cls(products=prod, categories=cats) - - def get(self, product): - p = self.products[product.id] - c = self.categories[product.category.id] - eit = p["eit"] + c["eit"] - dif = p["dif"] + c["dif"] - return _ConditionsCount(dif=dif, eit=eit) +_BIG_QUANTITY = 99999999 # A big quantity class ConditionController(object): @@ -120,184 +53,6 @@ class ConditionController(object): except KeyError: return ConditionController() - SINGLE = True - PLURAL = False - NONE = True - SOME = False - MESSAGE = { - NONE: { - SINGLE: - "%(items)s is no longer available to you", - PLURAL: - "%(items)s are no longer available to you", - }, - SOME: { - SINGLE: - "Only %(remainder)d of the following item remains: %(items)s", - PLURAL: - "Only %(remainder)d of the following items remain: %(items)s" - }, - } - - @classmethod - def test_flags( - cls, user, products=None, product_quantities=None): - ''' Evaluates all of the flag 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 flag 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 = {} - - if products: - # Simplify the query. - all_conditions = cls._filtered_flags(user, products) - else: - all_conditions = [] - - # All disable-if-false conditions on a product need to be met - do_not_disable = defaultdict(lambda: True) - # At least one enable-if-true condition on a product must be met - do_enable = defaultdict(lambda: False) - # (if either sort of condition is present) - - # Count the number of conditions for a product - dif_count = defaultdict(int) - eit_count = defaultdict(int) - - messages = {} - - for condition in all_conditions: - cond = cls.for_condition(condition) - remainder = cond.user_quantity_remaining(user, filtered=True) - - # Get all products covered by this condition, and the products - # from the categories covered by this condition - cond_products = condition.products.all() - from_category = inventory.Product.objects.filter( - category__in=condition.categories.all(), - ).all() - all_products = cond_products | from_category - all_products = all_products.select_related("category") - # Remove the products that we aren't asking about - all_products = [ - product - for product in all_products - if product in products - ] - - if quantities: - consumed = sum(quantities[i] for i in all_products) - else: - consumed = 1 - met = consumed <= remainder - - if not met: - items = ", ".join(str(product) for product in all_products) - base = cls.MESSAGE[remainder == 0][len(all_products) == 1] - message = base % {"items": items, "remainder": remainder} - - for product in all_products: - if condition.is_disable_if_false: - do_not_disable[product] &= met - dif_count[product] += 1 - else: - do_enable[product] |= met - eit_count[product] += 1 - - if not met and product not in messages: - messages[product] = message - - total_flags = FlagCounter.count() - - valid = {} - - # the problem is that now, not every condition falls into - # do_not_disable or do_enable ''' - # You should look into this, chris :) - - for product in products: - if quantities: - if quantities[product] == 0: - continue - - f = total_flags.get(product) - if f.dif > 0 and f.dif != dif_count[product]: - do_not_disable[product] = False - if product not in messages: - messages[product] = "Some disable-if-false " \ - "conditions were not met" - if f.eit > 0 and product not in do_enable: - do_enable[product] = False - if product not in messages: - messages[product] = "Some enable-if-true " \ - "conditions were not met" - - for product in itertools.chain(do_not_disable, do_enable): - f = total_flags.get(product) - if product in do_enable: - # If there's an enable-if-true, we need need of those met too. - # (do_not_disable will default to true otherwise) - valid[product] = do_not_disable[product] and do_enable[product] - elif product in do_not_disable: - # If there's a disable-if-false condition, all must be met - valid[product] = do_not_disable[product] - - error_fields = [ - (product, messages[product]) - for product in valid if not valid[product] - ] - - return error_fields - - @classmethod - def _filtered_flags(cls, user, products): - ''' - - Returns: - Sequence[flagbase]: All flags that passed the filter function. - - ''' - - types = list(ConditionController._controllers()) - flagtypes = [i for i in types if issubclass(i, conditions.FlagBase)] - - # Get all flags for the products and categories. - prods = ( - product.flagbase_set.all() - for product in products - ) - cats = ( - category.flagbase_set.all() - for category in set(product.category for product in products) - ) - all_flags = reduce(operator.or_, itertools.chain(prods, cats)) - - all_subsets = [] - - for flagtype in flagtypes: - flags = flagtype.objects.filter(id__in=all_flags) - ctrl = ConditionController.for_type(flagtype) - flags = ctrl.pre_filter(flags, user) - all_subsets.append(flags) - - return itertools.chain(*all_subsets) - @classmethod def pre_filter(cls, queryset, user): ''' Returns only the flag conditions that might be available for this @@ -369,16 +124,6 @@ class IsMetByFilter(object): return self.passes_filter(user) - carts = commerce.Cart.objects.filter(user=user) - carts = carts.exclude(status=commerce.Cart.STATUS_RELEASED) - enabling_products = inventory.Product.objects.filter( - category=self.condition.enabling_category, - ) - products_count = commerce.ProductItem.objects.filter( - cart__in=carts, - product__in=enabling_products, - ).count() - return products_count > 0 class RemainderSetByFilter(object): @@ -491,7 +236,7 @@ class TimeOrStockLimitFlagController( # Calculate category lines cat_items = F('categories__product__productitem__product__category') reserved_category_products = ( - Q(categories=cat_items) & + Q(categories=F('categories__product__productitem__product__category')) & Q(categories__product__productitem__cart__in=reserved_carts) ) diff --git a/registrasion/controllers/flag.py b/registrasion/controllers/flag.py new file mode 100644 index 00000000..40901931 --- /dev/null +++ b/registrasion/controllers/flag.py @@ -0,0 +1,257 @@ +import itertools +import operator + +from collections import defaultdict +from collections import namedtuple +from django.db.models import Count + +from .conditions import ConditionController + +from registrasion.models import conditions +from registrasion.models import inventory + + +class FlagController(object): + + SINGLE = True + PLURAL = False + NONE = True + SOME = False + MESSAGE = { + NONE: { + SINGLE: + "%(items)s is no longer available to you", + PLURAL: + "%(items)s are no longer available to you", + }, + SOME: { + SINGLE: + "Only %(remainder)d of the following item remains: %(items)s", + PLURAL: + "Only %(remainder)d of the following items remain: %(items)s" + }, + } + + @classmethod + def test_flags( + cls, user, products=None, product_quantities=None): + ''' Evaluates all of the flag 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 flag conditions pass, an empty list is returned, otherwise + a list is returned containing all of the products that are *not + enabled*. ''' + + print "GREPME: test_flags()" + + 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 = {} + + if products: + # Simplify the query. + all_conditions = cls._filtered_flags(user, products) + else: + all_conditions = [] + + # All disable-if-false conditions on a product need to be met + do_not_disable = defaultdict(lambda: True) + # At least one enable-if-true condition on a product must be met + do_enable = defaultdict(lambda: False) + # (if either sort of condition is present) + + # Count the number of conditions for a product + dif_count = defaultdict(int) + eit_count = defaultdict(int) + + messages = {} + + for condition in all_conditions: + cond = ConditionController.for_condition(condition) + remainder = cond.user_quantity_remaining(user, filtered=True) + + # Get all products covered by this condition, and the products + # from the categories covered by this condition + cond_products = condition.products.all() + from_category = inventory.Product.objects.filter( + category__in=condition.categories.all(), + ).all() + all_products = cond_products | from_category + all_products = all_products.select_related("category") + # Remove the products that we aren't asking about + all_products = [ + product + for product in all_products + if product in products + ] + + if quantities: + consumed = sum(quantities[i] for i in all_products) + else: + consumed = 1 + met = consumed <= remainder + + if not met: + items = ", ".join(str(product) for product in all_products) + base = cls.MESSAGE[remainder == 0][len(all_products) == 1] + message = base % {"items": items, "remainder": remainder} + + for product in all_products: + if condition.is_disable_if_false: + do_not_disable[product] &= met + dif_count[product] += 1 + else: + do_enable[product] |= met + eit_count[product] += 1 + + if not met and product not in messages: + messages[product] = message + + total_flags = FlagCounter.count() + + valid = {} + + # the problem is that now, not every condition falls into + # do_not_disable or do_enable ''' + # You should look into this, chris :) + + for product in products: + if quantities: + if quantities[product] == 0: + continue + + f = total_flags.get(product) + if f.dif > 0 and f.dif != dif_count[product]: + do_not_disable[product] = False + if product not in messages: + messages[product] = "Some disable-if-false " \ + "conditions were not met" + if f.eit > 0 and product not in do_enable: + do_enable[product] = False + if product not in messages: + messages[product] = "Some enable-if-true " \ + "conditions were not met" + + for product in itertools.chain(do_not_disable, do_enable): + f = total_flags.get(product) + if product in do_enable: + # If there's an enable-if-true, we need need of those met too. + # (do_not_disable will default to true otherwise) + valid[product] = do_not_disable[product] and do_enable[product] + elif product in do_not_disable: + # If there's a disable-if-false condition, all must be met + valid[product] = do_not_disable[product] + + error_fields = [ + (product, messages[product]) + for product in valid if not valid[product] + ] + + return error_fields + + @classmethod + def _filtered_flags(cls, user, products): + ''' + + Returns: + Sequence[flagbase]: All flags that passed the filter function. + + ''' + + types = list(ConditionController._controllers()) + flagtypes = [i for i in types if issubclass(i, conditions.FlagBase)] + + # Get all flags for the products and categories. + prods = ( + product.flagbase_set.all() + for product in products + ) + cats = ( + category.flagbase_set.all() + for category in set(product.category for product in products) + ) + all_flags = reduce(operator.or_, itertools.chain(prods, cats)) + + all_subsets = [] + + for flagtype in flagtypes: + flags = flagtype.objects.filter(id__in=all_flags) + ctrl = ConditionController.for_type(flagtype) + flags = ctrl.pre_filter(flags, user) + all_subsets.append(flags) + + return itertools.chain(*all_subsets) + + +ConditionAndRemainder = namedtuple( + "ConditionAndRemainder", + ( + "condition", + "remainder", + ), +) + + +_FlagCounter = namedtuple( + "_FlagCounter", + ( + "products", + "categories", + ), +) + + +_ConditionsCount = namedtuple( + "ConditionsCount", + ( + "dif", + "eit", + ), +) + + +class FlagCounter(_FlagCounter): + + @classmethod + def count(cls): + # Get the count of how many conditions should exist per product + flagbases = conditions.FlagBase.objects + + types = (conditions.FlagBase.ENABLE_IF_TRUE, conditions.FlagBase.DISABLE_IF_FALSE) + keys = ("eit", "dif") + flags = [ + flagbases.filter(condition=condition_type + ).values('products', 'categories' + ).annotate(count=Count('id')) + for condition_type in types + ] + + cats = defaultdict(lambda: defaultdict(int)) + prod = defaultdict(lambda: defaultdict(int)) + + for key, flagcounts in zip(keys, flags): + for row in flagcounts: + if row["products"] is not None: + prod[row["products"]][key] = row["count"] + if row["categories"] is not None: + cats[row["categories"]][key] = row["count"] + + return cls(products=prod, categories=cats) + + def get(self, product): + p = self.products[product.id] + c = self.categories[product.category.id] + eit = p["eit"] + c["eit"] + dif = p["dif"] + c["dif"] + return _ConditionsCount(dif=dif, eit=eit) diff --git a/registrasion/controllers/product.py b/registrasion/controllers/product.py index 99618ded..09f66bb3 100644 --- a/registrasion/controllers/product.py +++ b/registrasion/controllers/product.py @@ -4,8 +4,9 @@ from django.db.models import Sum from registrasion.models import commerce from registrasion.models import inventory -from category import CategoryController -from conditions import ConditionController +from .category import CategoryController +from .conditions import ConditionController +from .flag import FlagController class ProductController(object): @@ -46,7 +47,7 @@ class ProductController(object): if cls(product).user_quantity_remaining(user) > 0 ) - failed_and_messages = ConditionController.test_flags( + failed_and_messages = FlagController.test_flags( user, products=passed_limits ) failed_conditions = set(i[0] for i in failed_and_messages)