Changeset - 760996588372
[Not reviewed]
0 7 0
Christopher Neugebauer - 8 years ago 2016-04-03 03:21:57
chrisjrn@gmail.com
flake8 compliance
7 files changed with 9 insertions and 19 deletions:
0 comments (0 inline, 0 general)
registrasion/controllers/cart.py
Show inline comments
 
import collections
 
import datetime
 
import discount
 
import itertools
 

	
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.core.exceptions import ValidationError
 
from django.db import transaction
 
from django.db.models import Max, Sum
 
from django.db.models import Max
 
from django.utils import timezone
 

	
 
from registrasion import models as rego
 

	
 
from category import CategoryController
 
from conditions import ConditionController
 
from product import ProductController
 

	
 

	
 
class CartController(object):
 

	
 
    def __init__(self, cart):
 
        self.cart = cart
 

	
 
    @classmethod
 
    def for_user(cls, user):
 
        ''' Returns the user's current cart, or creates a new cart
 
        if there isn't one ready yet. '''
 

	
 
        try:
 
            existing = rego.Cart.objects.get(user=user, active=True)
 
        except ObjectDoesNotExist:
 
            existing = rego.Cart.objects.create(
 
                user=user,
...
 
@@ -50,49 +50,48 @@ class CartController(object):
 

	
 
        # Else, it's the maximum of the included products
 
        items = rego.ProductItem.objects.filter(cart=self.cart)
 
        agg = items.aggregate(Max("product__reservation_duration"))
 
        product_max = agg["product__reservation_duration__max"]
 

	
 
        if product_max is not None:
 
            reservations.append(product_max)
 

	
 
        self.cart.time_last_updated = timezone.now()
 
        self.cart.reservation_duration = max(reservations)
 

	
 
    def end_batch(self):
 
        ''' Performs operations that occur occur at the end of a batch of
 
        product changes/voucher applications etc.
 
        THIS SHOULD BE PRIVATE
 
        '''
 

	
 
        self.recalculate_discounts()
 

	
 
        self.extend_reservation()
 
        self.cart.revision += 1
 
        self.cart.save()
 

	
 

	
 
    @transaction.atomic
 
    def set_quantities(self, product_quantities):
 
        ''' Sets the quantities on each of the products on each of the
 
        products specified. Raises an exception (ValidationError) if a limit
 
        is violated. `product_quantities` is an iterable of (product, quantity)
 
        pairs. '''
 

	
 
        items_in_cart = rego.ProductItem.objects.filter(cart=self.cart)
 
        product_quantities = list(product_quantities)
 

	
 
        # n.b need to add have the existing items first so that the new
 
        # items override the old ones.
 
        all_product_quantities = dict(itertools.chain(
 
            ((i.product, i.quantity) for i in items_in_cart.all()),
 
            product_quantities,
 
        )).items()
 

	
 
        # Validate that the limits we're adding are OK
 
        self._test_limits(all_product_quantities)
 

	
 
        for product, quantity in product_quantities:
 
            try:
 
                product_item = rego.ProductItem.objects.get(
 
                    cart=self.cart,
...
 
@@ -173,50 +172,48 @@ class CartController(object):
 
        voucher = rego.Voucher.objects.get(code=voucher_code.upper())
 

	
 
        # It's invalid for a user to enter a voucher that's exhausted
 
        carts_with_voucher = active_carts.filter(vouchers=voucher)
 
        if len(carts_with_voucher) >= voucher.limit:
 
            raise ValidationError("This voucher is no longer available")
 

	
 
        # It's not valid for users to re-enter a voucher they already have
 
        user_carts_with_voucher = rego.Cart.objects.filter(
 
            user=self.cart.user,
 
            released=False,
 
            vouchers=voucher,
 
        )
 
        if len(user_carts_with_voucher) > 0:
 
            raise ValidationError("You have already entered this voucher.")
 

	
 
        # If successful...
 
        self.cart.vouchers.add(voucher)
 
        self.end_batch()
 

	
 
    def validate_cart(self):
 
        ''' Determines whether the status of the current cart is valid;
 
        this is normally called before generating or paying an invoice '''
 

	
 
        is_reserved = self.cart in rego.Cart.reserved_carts()
 

	
 
        # TODO: validate vouchers
 

	
 
        items = rego.ProductItem.objects.filter(cart=self.cart)
 

	
 
        product_quantities = list((i.product, i.quantity) for i in items)
 
        self._test_limits(product_quantities)
 

	
 
        # Validate the discounts
 
        discount_items = rego.DiscountItem.objects.filter(cart=self.cart)
 
        seen_discounts = set()
 

	
 
        for discount_item in discount_items:
 
            discount = discount_item.discount
 
            if discount in seen_discounts:
 
                continue
 
            seen_discounts.add(discount)
 
            real_discount = rego.DiscountBase.objects.get_subclass(
 
                pk=discount.pk)
 
            cond = ConditionController.for_condition(real_discount)
 

	
 
            if not cond.is_met(self.cart.user):
 
                raise ValidationError("Discounts are no longer available")
 

	
 
    def recalculate_discounts(self):
registrasion/controllers/category.py
Show inline comments
...
 
@@ -32,31 +32,25 @@ class CategoryController(object):
 
        return set(i.category for i in available)
 

	
 
    def user_quantity_remaining(self, user):
 
        ''' Returns the number of items from this category that the user may
 
        add in the current cart. '''
 

	
 
        cat_limit = self.category.limit_per_user
 

	
 
        if cat_limit is None:
 
            # We don't need to waste the following queries
 
            return 99999999
 

	
 
        carts = rego.Cart.objects.filter(
 
            user=user,
 
            active=False,
 
            released=False,
 
        )
 

	
 
        items = rego.ProductItem.objects.filter(
 
            cart__in=carts,
 
            product__category=self.category,
 
        )
 

	
 
        cat_count = items.aggregate(Sum("quantity"))["quantity__sum"] or 0
 
        
 
        cat_limit = self.category.limit_per_user
 

	
 
        if cat_limit is None:
 
            return 999999  # We should probably work on this.
 
        else:
 
            return cat_limit - cat_count
 
        cat_limit - cat_count
registrasion/controllers/conditions.py
Show inline comments
 
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
 

	
 
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. '''
 

	
 
    def __init__(self):
 
        pass
 

	
 
    @staticmethod
 
    def for_condition(condition):
...
 
@@ -43,69 +42,68 @@ class ConditionController(object):
 
        try:
 
            return CONTROLLERS[type(condition)](condition)
 
        except KeyError:
 
            return ConditionController()
 

	
 
    @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 )
 
            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
...
 
@@ -212,48 +210,49 @@ class TimeOrStockLimitConditionController(ConditionController):
 
                return False
 

	
 
        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,
 
        )
 

	
 
        items = self._items()
 
        items = items.filter(cart__in=reserved_carts)
 
        count = items.aggregate(Sum("quantity"))["quantity__sum"] or 0
 

	
 
        return self.ceiling.limit - count
 

	
 

	
 
class TimeOrStockLimitEnablingConditionController(
 
        TimeOrStockLimitConditionController):
 

	
 
    def _items(self):
 
        category_products = rego.Product.objects.filter(
 
            category__in=self.ceiling.categories.all(),
 
        )
 
        products = self.ceiling.products.all() | category_products
 

	
 
        product_items = rego.ProductItem.objects.filter(
 
            product__in=products.all(),
 
        )
 
        return product_items
 

	
 

	
 
class TimeOrStockLimitDiscountController(TimeOrStockLimitConditionController):
 

	
 
    def _items(self):
 
        discount_items = rego.DiscountItem.objects.filter(
 
            discount=self.ceiling,
 
        )
 
        return discount_items
 

	
 

	
registrasion/controllers/product.py
Show inline comments
 
import itertools
 

	
 
from django.db.models import Q
 
from django.db.models import Sum
 
from registrasion import models as rego
 

	
 
from category import CategoryController
 
from conditions import ConditionController
 

	
 

	
 
class ProductController(object):
 

	
 
    def __init__(self, product):
 
        self.product = product
 

	
 
    @classmethod
 
    def available_products(cls, user, category=None, products=None):
 
        ''' Returns a list of all of the products that are available per
 
        enabling conditions from the given categories.
 
        TODO: refactor so that all conditions are tested here and
 
        can_add_with_enabling_conditions calls this method. '''
 
        if category is None and products is None:
 
            raise ValueError("You must provide products or a category")
 

	
 
        if category is not None:
 
            all_products = rego.Product.objects.filter(category=category)
 
        else:
 
            all_products = []
 

	
 
        if products is not None:
 
            all_products = itertools.chain(all_products, products)
 

	
 
        passed_limits = set(
 
            product
 
            for product in all_products
 
            if CategoryController(product.category).user_quantity_remaining(user) > 0
 
            if CategoryController(product.category).user_quantity_remaining(
 
                user
 
            ) > 0
 
            if cls(product).user_quantity_remaining(user) > 0
 
        )
 

	
 
        failed_conditions = set(ConditionController.test_enabling_conditions(
 
            user, products=passed_limits
 
        ))
 

	
 
        out = list(passed_limits - failed_conditions)
 
        out.sort(key=lambda product: product.order)
 

	
 
        return out
 

	
 
    def user_quantity_remaining(self, user):
 
        ''' Returns the quantity of this product that the user add in the
 
        current cart. '''
 

	
 
        prod_limit = self.product.limit_per_user
 

	
 
        if prod_limit is None:
 
            # Don't need to run the remaining queries
 
            return 999999  # We can do better
 

	
 
        carts = rego.Cart.objects.filter(
 
            user=user,
registrasion/tests/cart_controller_helper.py
Show inline comments
 
from registrasion.controllers.cart import CartController
 
from registrasion import models as rego
 

	
 
from django.core.exceptions import ObjectDoesNotExist
 

	
 

	
 
class TestingCartController(CartController):
 

	
 
    def set_quantity(self, product, quantity, batched=False):
 
        ''' Sets the _quantity_ of the given _product_ in the cart to the given
 
        _quantity_. '''
 

	
 
        self.set_quantities(((product, quantity),))
 

	
 
    def add_to_cart(self, product, quantity):
 
        ''' Adds _quantity_ of the given _product_ to the cart. Raises
 
        ValidationError if constraints are violated.'''
 

	
 
        try:
 
            product_item = rego.ProductItem.objects.get(
 
                cart=self.cart,
 
                product=product)
 
            old_quantity = product_item.quantity
 
        except ObjectDoesNotExist:
 
            old_quantity = 0
 
        self.set_quantity(product, old_quantity + quantity)
registrasion/tests/test_cart.py
Show inline comments
...
 
@@ -105,49 +105,48 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
 
            end_time=end_time
 
        )
 
        limit_ceiling.save()
 
        limit_ceiling.categories.add(cls.CAT_1)
 
        limit_ceiling.save()
 

	
 
    @classmethod
 
    def make_discount_ceiling(
 
            cls, name, limit=None, start_time=None, end_time=None,
 
            percentage=100):
 
        limit_ceiling = rego.TimeOrStockLimitDiscount.objects.create(
 
            description=name,
 
            start_time=start_time,
 
            end_time=end_time,
 
            limit=limit,
 
        )
 
        limit_ceiling.save()
 
        rego.DiscountForProduct.objects.create(
 
            discount=limit_ceiling,
 
            product=cls.PROD_1,
 
            percentage=percentage,
 
            quantity=10,
 
        ).save()
 

	
 

	
 
    @classmethod
 
    def new_voucher(self, code="VOUCHER", limit=1):
 
        voucher = rego.Voucher.objects.create(
 
            recipient="Voucher recipient",
 
            code=code,
 
            limit=limit,
 
        )
 
        voucher.save()
 
        return voucher
 

	
 

	
 
class BasicCartTests(RegistrationCartTestCase):
 

	
 
    def test_get_cart(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 

	
 
        current_cart.cart.active = False
 
        current_cart.cart.save()
 

	
 
        old_cart = current_cart
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        self.assertNotEqual(old_cart.cart, current_cart.cart)
 

	
registrasion/views.py
Show inline comments
 
import sys
 

	
 
from registrasion import forms
 
from registrasion import models as rego
 
from registrasion.controllers import discount
 
from registrasion.controllers.cart import CartController
 
from registrasion.controllers.invoice import InvoiceController
 
from registrasion.controllers.product import ProductController
 

	
 
from collections import namedtuple
 

	
 
from django.conf import settings
 
from django.contrib.auth.decorators import login_required
 
from django.contrib import messages
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.core.exceptions import ValidationError
 
from django.db import transaction
 
from django.http import Http404
 
from django.shortcuts import redirect
 
from django.shortcuts import render
 

	
 

	
 
GuidedRegistrationSection = namedtuple(
 
    "GuidedRegistrationSection",
 
    (
 
        "title",
 
        "discounts",
 
        "description",
 
        "form",
 
    )
 
)
 
GuidedRegistrationSection.__new__.__defaults__ = (
 
    (None,) * len(GuidedRegistrationSection._fields)
 
)
 

	
 

	
 
def get_form(name):
 
    dot = name.rindex(".")
 
    mod_name, form_name = name[:dot], name[dot + 1:]
 
    __import__(mod_name)
 
    return getattr(sys.modules[mod_name], form_name)
0 comments (0 inline, 0 general)