Makes DiscountController a class and puts available_discounts inside it

This commit is contained in:
Christopher Neugebauer 2016-04-28 12:20:36 +10:00
parent 145fd057ac
commit 71de0df5dc
5 changed files with 128 additions and 134 deletions

View file

@ -17,6 +17,7 @@ from registrasion.models import inventory
from .category import CategoryController from .category import CategoryController
from .conditions import ConditionController from .conditions import ConditionController
from .discount import DiscountController
from .flag import FlagController from .flag import FlagController
from .product import ProductController from .product import ProductController
@ -377,7 +378,7 @@ class CartController(object):
) )
products = [i.product for i in product_items] products = [i.product for i in product_items]
discounts = discount.available_discounts(self.cart.user, [], products) discounts = DiscountController.available_discounts(self.cart.user, [], products)
# The highest-value discounts will apply to the highest-value # The highest-value discounts will apply to the highest-value
# products first. # products first.

View file

@ -42,141 +42,134 @@ class DiscountAndQuantity(object):
) )
def available_discounts(user, categories, products): class DiscountController(object):
''' Returns all discounts available to this user for the given categories
and products. The discounts also list the available quantity for this user, @classmethod
not including products that are pending purchase. ''' def available_discounts(cls, user, categories, products):
''' Returns all discounts available to this user for the given categories
and products. The discounts also list the available quantity for this user,
not including products that are pending purchase. '''
filtered_clauses = cls._filtered_discounts(user, categories, products)
filtered_clauses = _filtered_discounts(user, categories, products) discounts = []
discounts = [] # Markers so that we don't need to evaluate given conditions more than once
accepted_discounts = set()
failed_discounts = set()
# Markers so that we don't need to evaluate given conditions more than once for clause in filtered_clauses:
accepted_discounts = set() discount = clause.discount
failed_discounts = set() cond = ConditionController.for_condition(discount)
for clause in filtered_clauses: past_use_count = discount.past_use_count
discount = clause.discount
cond = ConditionController.for_condition(discount)
past_use_count = discount.past_use_count
# TODO: add test case --
# discount covers 2x prod_1 and 1x prod_2
# add 1x prod_2
# add 1x prod_1
# checkout
# discount should be available for prod_1
if past_use_count >= clause.quantity:
# This clause has exceeded its use count
pass
elif discount not in failed_discounts:
# This clause is still available
is_accepted = discount in accepted_discounts
if is_accepted or cond.is_met(user, filtered=True):
# This clause is valid for this user
discounts.append(DiscountAndQuantity(
discount=discount,
clause=clause,
quantity=clause.quantity - past_use_count,
))
accepted_discounts.add(discount)
else:
# This clause is not valid for this user
failed_discounts.add(discount)
return discounts
def _filtered_discounts(user, categories, products): if past_use_count >= clause.quantity:
''' # This clause has exceeded its use count
pass
elif discount not in failed_discounts:
# This clause is still available
if discount in accepted_discounts or cond.is_met(user, filtered=True):
# This clause is valid for this user
discounts.append(DiscountAndQuantity(
discount=discount,
clause=clause,
quantity=clause.quantity - past_use_count,
))
accepted_discounts.add(discount)
else:
# This clause is not valid for this user
failed_discounts.add(discount)
return discounts
Returns: @classmethod
Sequence[discountbase]: All discounts that passed the filter function. def _filtered_discounts(cls, user, categories, products):
'''
''' Returns:
Sequence[discountbase]: All discounts that passed the filter function.
types = list(ConditionController._controllers()) '''
discounttypes = [
i for i in types if issubclass(i, conditions.DiscountBase)
]
# discounts that match provided categories types = list(ConditionController._controllers())
category_discounts = conditions.DiscountForCategory.objects.filter( discounttypes = [i for i in types if issubclass(i, conditions.DiscountBase)]
category__in=categories
)
# discounts that match provided products
product_discounts = conditions.DiscountForProduct.objects.filter(
product__in=products
)
# discounts that match categories for provided products
product_category_discounts = conditions.DiscountForCategory.objects.filter(
category__in=(product.category for product in products)
)
# (Not relevant: discounts that match products in provided categories)
product_discounts = product_discounts.select_related( # discounts that match provided categories
"product", category_discounts = conditions.DiscountForCategory.objects.filter(
"product__category", category__in=categories
) )
# discounts that match provided products
product_discounts = conditions.DiscountForProduct.objects.filter(
product__in=products
)
# discounts that match categories for provided products
product_category_discounts = conditions.DiscountForCategory.objects.filter(
category__in=(product.category for product in products)
)
# (Not relevant: discounts that match products in provided categories)
all_category_discounts = category_discounts | product_category_discounts product_discounts = product_discounts.select_related(
all_category_discounts = all_category_discounts.select_related( "product",
"category", "product__category",
) )
valid_discounts = conditions.DiscountBase.objects.filter( all_category_discounts = category_discounts | product_category_discounts
Q(discountforproduct__in=product_discounts) | all_category_discounts = all_category_discounts.select_related(
Q(discountforcategory__in=all_category_discounts) "category",
) )
all_subsets = [] valid_discounts = conditions.DiscountBase.objects.filter(
Q(discountforproduct__in=product_discounts) |
Q(discountforcategory__in=all_category_discounts)
)
for discounttype in discounttypes: all_subsets = []
discounts = discounttype.objects.filter(id__in=valid_discounts)
ctrl = ConditionController.for_type(discounttype)
discounts = ctrl.pre_filter(discounts, user)
discounts = _annotate_with_past_uses(discounts, user)
all_subsets.append(discounts)
filtered_discounts = list(itertools.chain(*all_subsets)) for discounttype in discounttypes:
discounts = discounttype.objects.filter(id__in=valid_discounts)
ctrl = ConditionController.for_type(discounttype)
discounts = ctrl.pre_filter(discounts, user)
discounts = cls._annotate_with_past_uses(discounts, user)
all_subsets.append(discounts)
# Map from discount key to itself (contains annotations added by filter) filtered_discounts = list(itertools.chain(*all_subsets))
from_filter = dict((i.id, i) for i in filtered_discounts)
# The set of all potential discounts # Map from discount key to itself (contains annotations added by filter)
discount_clauses = set(itertools.chain( from_filter = dict((i.id, i) for i in filtered_discounts)
product_discounts.filter(discount__in=filtered_discounts),
all_category_discounts.filter(discount__in=filtered_discounts),
))
# Replace discounts with the filtered ones # The set of all potential discounts
# These are the correct subclasses (saves query later on), and have discount_clauses = set(itertools.chain(
# correct annotations from filters if necessary. product_discounts.filter(discount__in=filtered_discounts),
for clause in discount_clauses: all_category_discounts.filter(discount__in=filtered_discounts),
clause.discount = from_filter[clause.discount.id] ))
return discount_clauses # Replace discounts with the filtered ones
# These are the correct subclasses (saves query later on), and have
# correct annotations from filters if necessary.
for clause in discount_clauses:
clause.discount = from_filter[clause.discount.id]
return discount_clauses
def _annotate_with_past_uses(queryset, user): @classmethod
''' Annotates the queryset with a usage count for that discount by the def _annotate_with_past_uses(cls, queryset, user):
given user. ''' ''' Annotates the queryset with a usage count for that discount by the
given user. '''
past_use_quantity = When( past_use_quantity = When(
( (
Q(discountitem__cart__user=user) & Q(discountitem__cart__user=user) &
Q(discountitem__cart__status=commerce.Cart.STATUS_PAID) Q(discountitem__cart__status=commerce.Cart.STATUS_PAID)
), ),
then="discountitem__quantity", then="discountitem__quantity",
) )
past_use_quantity_or_zero = Case( past_use_quantity_or_zero = Case(
past_use_quantity, past_use_quantity,
default=Value(0), default=Value(0),
) )
queryset = queryset.annotate(past_use_count=Sum(past_use_quantity_or_zero)) queryset = queryset.annotate(past_use_count=Sum(past_use_quantity_or_zero))
return queryset return queryset

View file

@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError
from controller_helpers import TestingCartController from controller_helpers import TestingCartController
from test_cart import RegistrationCartTestCase from test_cart import RegistrationCartTestCase
from registrasion.controllers.discount import available_discounts from registrasion.controllers.discount import DiscountController
from registrasion.controllers.product import ProductController from registrasion.controllers.product import ProductController
from registrasion.models import commerce from registrasion.models import commerce
from registrasion.models import conditions from registrasion.models import conditions
@ -149,7 +149,7 @@ class CeilingsTestCases(RegistrationCartTestCase):
cart.add_to_cart(self.PROD_1, 1) cart.add_to_cart(self.PROD_1, 1)
cart.next_cart() cart.next_cart()
discounts = available_discounts(self.USER_1, [], [self.PROD_1]) discounts = DiscountController.available_discounts(self.USER_1, [], [self.PROD_1])
self.assertEqual(0, len(discounts)) self.assertEqual(0, len(discounts))

View file

@ -4,7 +4,7 @@ from decimal import Decimal
from registrasion.models import commerce from registrasion.models import commerce
from registrasion.models import conditions from registrasion.models import conditions
from registrasion.controllers import discount from registrasion.controllers.discount import DiscountController
from controller_helpers import TestingCartController from controller_helpers import TestingCartController
from test_cart import RegistrationCartTestCase from test_cart import RegistrationCartTestCase
@ -243,22 +243,22 @@ class DiscountTestCase(RegistrationCartTestCase):
# The discount is applied. # The discount is applied.
self.assertEqual(1, len(discount_items)) self.assertEqual(1, len(discount_items))
# Tests for the discount.available_discounts enumerator # Tests for the DiscountController.available_discounts enumerator
def test_enumerate_no_discounts_for_no_input(self): def test_enumerate_no_discounts_for_no_input(self):
discounts = discount.available_discounts(self.USER_1, [], []) discounts = DiscountController.available_discounts(self.USER_1, [], [])
self.assertEqual(0, len(discounts)) self.assertEqual(0, len(discounts))
def test_enumerate_no_discounts_if_condition_not_met(self): def test_enumerate_no_discounts_if_condition_not_met(self):
self.add_discount_prod_1_includes_cat_2(quantity=1) self.add_discount_prod_1_includes_cat_2(quantity=1)
discounts = discount.available_discounts( discounts = DiscountController.available_discounts(
self.USER_1, self.USER_1,
[], [],
[self.PROD_3], [self.PROD_3],
) )
self.assertEqual(0, len(discounts)) self.assertEqual(0, len(discounts))
discounts = discount.available_discounts(self.USER_1, [self.CAT_2], []) discounts = DiscountController.available_discounts(self.USER_1, [self.CAT_2], [])
self.assertEqual(0, len(discounts)) self.assertEqual(0, len(discounts))
def test_category_discount_appears_once_if_met_twice(self): def test_category_discount_appears_once_if_met_twice(self):
@ -267,7 +267,7 @@ class DiscountTestCase(RegistrationCartTestCase):
cart = TestingCartController.for_user(self.USER_1) cart = TestingCartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1) # Enable the discount cart.add_to_cart(self.PROD_1, 1) # Enable the discount
discounts = discount.available_discounts( discounts = DiscountController.available_discounts(
self.USER_1, self.USER_1,
[self.CAT_2], [self.CAT_2],
[self.PROD_3], [self.PROD_3],
@ -280,7 +280,7 @@ class DiscountTestCase(RegistrationCartTestCase):
cart = TestingCartController.for_user(self.USER_1) cart = TestingCartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1) # Enable the discount cart.add_to_cart(self.PROD_1, 1) # Enable the discount
discounts = discount.available_discounts(self.USER_1, [self.CAT_2], []) discounts = DiscountController.available_discounts(self.USER_1, [self.CAT_2], [])
self.assertEqual(1, len(discounts)) self.assertEqual(1, len(discounts))
def test_category_discount_appears_with_product(self): def test_category_discount_appears_with_product(self):
@ -289,7 +289,7 @@ class DiscountTestCase(RegistrationCartTestCase):
cart = TestingCartController.for_user(self.USER_1) cart = TestingCartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1) # Enable the discount cart.add_to_cart(self.PROD_1, 1) # Enable the discount
discounts = discount.available_discounts( discounts = DiscountController.available_discounts(
self.USER_1, self.USER_1,
[], [],
[self.PROD_3], [self.PROD_3],
@ -302,7 +302,7 @@ class DiscountTestCase(RegistrationCartTestCase):
cart = TestingCartController.for_user(self.USER_1) cart = TestingCartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1) # Enable the discount cart.add_to_cart(self.PROD_1, 1) # Enable the discount
discounts = discount.available_discounts( discounts = DiscountController.available_discounts(
self.USER_1, self.USER_1,
[], [],
[self.PROD_3, self.PROD_4] [self.PROD_3, self.PROD_4]
@ -315,7 +315,7 @@ class DiscountTestCase(RegistrationCartTestCase):
cart = TestingCartController.for_user(self.USER_1) cart = TestingCartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1) # Enable the discount cart.add_to_cart(self.PROD_1, 1) # Enable the discount
discounts = discount.available_discounts( discounts = DiscountController.available_discounts(
self.USER_1, self.USER_1,
[], [],
[self.PROD_2], [self.PROD_2],
@ -328,7 +328,7 @@ class DiscountTestCase(RegistrationCartTestCase):
cart = TestingCartController.for_user(self.USER_1) cart = TestingCartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1) # Enable the discount cart.add_to_cart(self.PROD_1, 1) # Enable the discount
discounts = discount.available_discounts(self.USER_1, [self.CAT_1], []) discounts = DiscountController.available_discounts(self.USER_1, [self.CAT_1], [])
self.assertEqual(0, len(discounts)) self.assertEqual(0, len(discounts))
def test_discount_quantity_is_correct_before_first_purchase(self): def test_discount_quantity_is_correct_before_first_purchase(self):
@ -338,7 +338,7 @@ class DiscountTestCase(RegistrationCartTestCase):
cart.add_to_cart(self.PROD_1, 1) # Enable the discount cart.add_to_cart(self.PROD_1, 1) # Enable the discount
cart.add_to_cart(self.PROD_3, 1) # Exhaust the quantity cart.add_to_cart(self.PROD_3, 1) # Exhaust the quantity
discounts = discount.available_discounts(self.USER_1, [self.CAT_2], []) discounts = DiscountController.available_discounts(self.USER_1, [self.CAT_2], [])
self.assertEqual(2, discounts[0].quantity) self.assertEqual(2, discounts[0].quantity)
cart.next_cart() cart.next_cart()
@ -349,21 +349,21 @@ class DiscountTestCase(RegistrationCartTestCase):
cart = TestingCartController.for_user(self.USER_1) cart = TestingCartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_3, 1) # Exhaust the quantity cart.add_to_cart(self.PROD_3, 1) # Exhaust the quantity
discounts = discount.available_discounts(self.USER_1, [self.CAT_2], []) discounts = DiscountController.available_discounts(self.USER_1, [self.CAT_2], [])
self.assertEqual(1, discounts[0].quantity) self.assertEqual(1, discounts[0].quantity)
cart.next_cart() cart.next_cart()
def test_discount_is_gone_after_quantity_exhausted(self): def test_discount_is_gone_after_quantity_exhausted(self):
self.test_discount_quantity_is_correct_after_first_purchase() self.test_discount_quantity_is_correct_after_first_purchase()
discounts = discount.available_discounts(self.USER_1, [self.CAT_2], []) discounts = DiscountController.available_discounts(self.USER_1, [self.CAT_2], [])
self.assertEqual(0, len(discounts)) self.assertEqual(0, len(discounts))
def test_product_discount_enabled_twice_appears_twice(self): def test_product_discount_enabled_twice_appears_twice(self):
self.add_discount_prod_1_includes_prod_3_and_prod_4(quantity=2) self.add_discount_prod_1_includes_prod_3_and_prod_4(quantity=2)
cart = TestingCartController.for_user(self.USER_1) cart = TestingCartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1) # Enable the discount cart.add_to_cart(self.PROD_1, 1) # Enable the discount
discounts = discount.available_discounts( discounts = DiscountController.available_discounts(
self.USER_1, self.USER_1,
[], [],
[self.PROD_3, self.PROD_4], [self.PROD_3, self.PROD_4],
@ -374,7 +374,7 @@ class DiscountTestCase(RegistrationCartTestCase):
self.add_discount_prod_1_includes_prod_2(quantity=2) self.add_discount_prod_1_includes_prod_2(quantity=2)
cart = TestingCartController.for_user(self.USER_1) cart = TestingCartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1) # Enable the discount cart.add_to_cart(self.PROD_1, 1) # Enable the discount
discounts = discount.available_discounts( discounts = DiscountController.available_discounts(
self.USER_1, self.USER_1,
[], [],
[self.PROD_2], [self.PROD_2],
@ -388,7 +388,7 @@ class DiscountTestCase(RegistrationCartTestCase):
cart.next_cart() cart.next_cart()
discounts = discount.available_discounts( discounts = DiscountController.available_discounts(
self.USER_1, self.USER_1,
[], [],
[self.PROD_2], [self.PROD_2],
@ -398,7 +398,7 @@ class DiscountTestCase(RegistrationCartTestCase):
cart.cart.status = commerce.Cart.STATUS_RELEASED cart.cart.status = commerce.Cart.STATUS_RELEASED
cart.cart.save() cart.cart.save()
discounts = discount.available_discounts( discounts = DiscountController.available_discounts(
self.USER_1, self.USER_1,
[], [],
[self.PROD_2], [self.PROD_2],

View file

@ -5,7 +5,7 @@ from registrasion import util
from registrasion.models import commerce from registrasion.models import commerce
from registrasion.models import inventory from registrasion.models import inventory
from registrasion.models import people from registrasion.models import people
from registrasion.controllers import discount from registrasion.controllers.discount import DiscountController
from registrasion.controllers.cart import CartController from registrasion.controllers.cart import CartController
from registrasion.controllers.credit_note import CreditNoteController from registrasion.controllers.credit_note import CreditNoteController
from registrasion.controllers.invoice import InvoiceController from registrasion.controllers.invoice import InvoiceController
@ -427,7 +427,7 @@ def _handle_products(request, category, products, prefix):
) )
handled = False if products_form.errors else True handled = False if products_form.errors else True
discounts = discount.available_discounts(request.user, [], products) discounts = DiscountController.available_discounts(request.user, [], products)
return products_form, discounts, handled return products_form, discounts, handled