2016-03-25 07:09:24 +00:00
|
|
|
import itertools
|
|
|
|
|
2016-05-01 02:42:06 +00:00
|
|
|
from .batch import BatchController
|
|
|
|
from .conditions import ConditionController
|
|
|
|
|
2016-04-22 05:06:24 +00:00
|
|
|
from registrasion.models import commerce
|
|
|
|
from registrasion.models import conditions
|
2016-03-25 07:09:24 +00:00
|
|
|
|
2016-04-27 01:46:44 +00:00
|
|
|
from django.db.models import Case
|
2016-04-29 00:08:21 +00:00
|
|
|
from django.db.models import F, Q
|
2016-03-25 07:09:24 +00:00
|
|
|
from django.db.models import Sum
|
2016-04-27 01:46:44 +00:00
|
|
|
from django.db.models import Value
|
|
|
|
from django.db.models import When
|
2016-03-25 07:09:24 +00:00
|
|
|
|
2016-09-02 01:43:27 +00:00
|
|
|
|
2016-03-25 07:09:24 +00:00
|
|
|
class DiscountAndQuantity(object):
|
2016-04-26 00:52:56 +00:00
|
|
|
''' Represents a discount that can be applied to a product or category
|
|
|
|
for a given user.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
|
|
|
|
discount (conditions.DiscountBase): The discount object that the
|
|
|
|
clause arises from. A given DiscountBase can apply to multiple
|
|
|
|
clauses.
|
|
|
|
|
|
|
|
clause (conditions.DiscountForProduct|conditions.DiscountForCategory):
|
|
|
|
A clause describing which product or category this discount item
|
|
|
|
applies to. This casts to ``str()`` to produce a human-readable
|
|
|
|
version of the clause.
|
|
|
|
|
|
|
|
quantity (int): The number of times this discount item can be applied
|
|
|
|
for the given user.
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
2016-03-25 07:09:24 +00:00
|
|
|
def __init__(self, discount, clause, quantity):
|
|
|
|
self.discount = discount
|
|
|
|
self.clause = clause
|
|
|
|
self.quantity = quantity
|
|
|
|
|
2016-03-27 08:25:24 +00:00
|
|
|
def __repr__(self):
|
2016-04-06 08:28:33 +00:00
|
|
|
return "(discount=%s, clause=%s, quantity=%d)" % (
|
2016-03-27 08:25:24 +00:00
|
|
|
self.discount, self.clause, self.quantity,
|
|
|
|
)
|
|
|
|
|
2016-03-25 07:09:24 +00:00
|
|
|
|
2016-04-28 02:20:36 +00:00
|
|
|
class DiscountController(object):
|
2016-04-27 01:46:44 +00:00
|
|
|
|
2016-04-28 02:20:36 +00:00
|
|
|
@classmethod
|
|
|
|
def available_discounts(cls, user, categories, products):
|
2016-04-28 02:39:20 +00:00
|
|
|
''' 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. '''
|
2016-04-27 01:46:44 +00:00
|
|
|
|
2016-04-30 11:42:02 +00:00
|
|
|
filtered_clauses = cls._filtered_clauses(user)
|
2016-04-30 22:47:53 +00:00
|
|
|
|
|
|
|
# clauses that match provided categories
|
|
|
|
categories = set(categories)
|
|
|
|
# clauses that match provided products
|
|
|
|
products = set(products)
|
|
|
|
# clauses that match categories for provided products
|
|
|
|
product_categories = set(product.category for product in products)
|
|
|
|
# (Not relevant: clauses that match products in provided categories)
|
|
|
|
all_categories = categories | product_categories
|
|
|
|
|
|
|
|
filtered_clauses = (
|
|
|
|
clause for clause in filtered_clauses
|
|
|
|
if hasattr(clause, 'product') and clause.product in products or
|
|
|
|
hasattr(clause, 'category') and clause.category in all_categories
|
|
|
|
)
|
2016-04-28 02:20:36 +00:00
|
|
|
|
|
|
|
discounts = []
|
|
|
|
|
2016-04-28 02:39:20 +00:00
|
|
|
# Markers so that we don't need to evaluate given conditions
|
|
|
|
# more than once
|
2016-04-28 02:20:36 +00:00
|
|
|
accepted_discounts = set()
|
|
|
|
failed_discounts = set()
|
|
|
|
|
|
|
|
for clause in filtered_clauses:
|
|
|
|
discount = clause.discount
|
|
|
|
cond = ConditionController.for_condition(discount)
|
|
|
|
|
2016-04-29 00:08:21 +00:00
|
|
|
past_use_count = clause.past_use_count
|
2016-04-28 02:20:36 +00:00
|
|
|
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
|
2016-04-28 02:39:20 +00:00
|
|
|
is_accepted = discount in accepted_discounts
|
|
|
|
if is_accepted or cond.is_met(user, filtered=True):
|
2016-04-28 02:20:36 +00:00
|
|
|
# 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
|
|
|
|
|
|
|
|
@classmethod
|
2016-05-01 02:42:06 +00:00
|
|
|
@BatchController.memoise
|
2016-04-30 22:47:53 +00:00
|
|
|
def _filtered_clauses(cls, user):
|
2016-04-28 02:20:36 +00:00
|
|
|
'''
|
|
|
|
|
|
|
|
Returns:
|
2016-04-30 11:42:02 +00:00
|
|
|
Sequence[DiscountForProduct | DiscountForCategory]: All clauses
|
|
|
|
that passed the filter function.
|
2016-04-28 02:20:36 +00:00
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
types = list(ConditionController._controllers())
|
2016-04-28 02:39:20 +00:00
|
|
|
discounttypes = [
|
|
|
|
i for i in types if issubclass(i, conditions.DiscountBase)
|
|
|
|
]
|
2016-04-28 02:20:36 +00:00
|
|
|
|
2016-04-30 22:47:53 +00:00
|
|
|
product_clauses = conditions.DiscountForProduct.objects.all()
|
|
|
|
product_clauses = product_clauses.select_related(
|
2016-05-01 04:07:29 +00:00
|
|
|
"discount",
|
2016-04-28 02:20:36 +00:00
|
|
|
"product",
|
|
|
|
"product__category",
|
|
|
|
)
|
2016-04-30 22:47:53 +00:00
|
|
|
category_clauses = conditions.DiscountForCategory.objects.all()
|
|
|
|
category_clauses = category_clauses.select_related(
|
2016-04-28 02:20:36 +00:00
|
|
|
"category",
|
2016-05-01 04:07:29 +00:00
|
|
|
"discount",
|
2016-04-28 02:20:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
all_subsets = []
|
|
|
|
|
|
|
|
for discounttype in discounttypes:
|
2016-05-01 09:12:40 +00:00
|
|
|
discounts = discounttype.objects.all()
|
2016-04-28 02:20:36 +00:00
|
|
|
ctrl = ConditionController.for_type(discounttype)
|
|
|
|
discounts = ctrl.pre_filter(discounts, user)
|
|
|
|
all_subsets.append(discounts)
|
|
|
|
|
|
|
|
filtered_discounts = list(itertools.chain(*all_subsets))
|
|
|
|
|
2016-04-28 02:39:20 +00:00
|
|
|
# Map from discount key to itself
|
|
|
|
# (contains annotations needed in the future)
|
2016-04-28 02:20:36 +00:00
|
|
|
from_filter = dict((i.id, i) for i in filtered_discounts)
|
|
|
|
|
2016-04-29 00:08:21 +00:00
|
|
|
clause_sets = (
|
2016-04-30 22:47:53 +00:00
|
|
|
product_clauses.filter(discount__in=filtered_discounts),
|
|
|
|
category_clauses.filter(discount__in=filtered_discounts),
|
2016-04-29 00:08:21 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
clause_sets = (
|
|
|
|
cls._annotate_with_past_uses(i, user) for i in clause_sets
|
|
|
|
)
|
|
|
|
|
|
|
|
# The set of all potential discount clauses
|
|
|
|
discount_clauses = set(itertools.chain(*clause_sets))
|
2016-04-28 02:20:36 +00:00
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _annotate_with_past_uses(cls, queryset, user):
|
2016-04-29 00:08:21 +00:00
|
|
|
''' Annotates the queryset with a usage count for that discount claus
|
|
|
|
by the given user. '''
|
|
|
|
|
|
|
|
if queryset.model == conditions.DiscountForCategory:
|
|
|
|
matches = (
|
|
|
|
Q(category=F('discount__discountitem__product__category'))
|
|
|
|
)
|
|
|
|
elif queryset.model == conditions.DiscountForProduct:
|
|
|
|
matches = (
|
|
|
|
Q(product=F('discount__discountitem__product'))
|
|
|
|
)
|
|
|
|
|
|
|
|
in_carts = (
|
|
|
|
Q(discount__discountitem__cart__user=user) &
|
|
|
|
Q(discount__discountitem__cart__status=commerce.Cart.STATUS_PAID)
|
|
|
|
)
|
2016-04-28 02:20:36 +00:00
|
|
|
|
|
|
|
past_use_quantity = When(
|
2016-04-29 00:08:21 +00:00
|
|
|
in_carts & matches,
|
|
|
|
then="discount__discountitem__quantity",
|
2016-04-28 02:20:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
past_use_quantity_or_zero = Case(
|
|
|
|
past_use_quantity,
|
|
|
|
default=Value(0),
|
|
|
|
)
|
|
|
|
|
2016-04-28 02:39:20 +00:00
|
|
|
queryset = queryset.annotate(
|
|
|
|
past_use_count=Sum(past_use_quantity_or_zero)
|
|
|
|
)
|
2016-04-28 02:20:36 +00:00
|
|
|
return queryset
|