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,
 
                time_last_updated=timezone.now(),
 
                reservation_duration=datetime.timedelta(),
 
                 )
 
            existing.save()
 
        return cls(existing)
 

	
 
    def extend_reservation(self):
 
        ''' Updates the cart's time last updated value, which is used to
 
        determine whether the cart has reserved the items and discounts it
 
        holds. '''
 

	
 
        reservations = [datetime.timedelta()]
 

	
 
        # If we have vouchers, we're entitled to an hour at minimum.
 
        if len(self.cart.vouchers.all()) >= 1:
 
            reservations.append(rego.Voucher.RESERVATION_DURATION)
 

	
 
        # 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,
 
                    product=product,
 
                )
 
                product_item.quantity = quantity
 
                product_item.save()
 
            except ObjectDoesNotExist:
 
                rego.ProductItem.objects.create(
 
                    cart=self.cart,
 
                    product=product,
 
                    quantity=quantity,
 
                )
 

	
 
        items_in_cart.filter(quantity=0).delete()
 

	
 
        self.end_batch()
 

	
 
    def _test_limits(self, product_quantities):
 
        ''' Tests that the quantity changes we intend to make do not violate
 
        the limits and enabling conditions imposed on the products. '''
 

	
 
        # Test each product limit here
 
        for product, quantity in product_quantities:
 
            if quantity < 0:
 
                # TODO: batch errors
 
                raise ValidationError("Value must be zero or greater.")
 

	
 
            prod = ProductController(product)
 
            limit = prod.user_quantity_remaining(self.cart.user)
 

	
 
            if quantity > limit:
 
                # TODO: batch errors
 
                raise ValidationError(
 
                    "You may only have %d of product: %s" % (
 
                        limit, product.name,
 
                    )
 
                )
 

	
 
        # Collect by category
 
        by_cat = collections.defaultdict(list)
 
        for product, quantity in product_quantities:
 
            by_cat[product.category].append((product, quantity))
 

	
 
        # Test each category limit here
 
        for cat in by_cat:
 
            ctrl = CategoryController(cat)
 
            limit = ctrl.user_quantity_remaining(self.cart.user)
 

	
 
            # Get the amount so far in the cart
 
            to_add = sum(i[1] for i in by_cat[cat])
 

	
 
            if to_add > limit:
 
                # TODO: batch errors
 
                raise ValidationError(
 
                    "You may only have %d items in category: %s" % (
 
                        limit, cat.name,
 
                    )
 
                )
 

	
 
        # Test the enabling conditions
 
        errs = ConditionController.test_enabling_conditions(
 
            self.cart.user,
 
            product_quantities=product_quantities,
 
        )
 

	
 
        if errs:
 
            # TODO: batch errors
 
            raise ValidationError("An enabling condition failed")
 

	
 
    def apply_voucher(self, voucher_code):
 
        ''' Applies the voucher with the given code to this cart. '''
 

	
 
        # Is voucher exhausted?
 
        active_carts = rego.Cart.reserved_carts()
 

	
 
        # Try and find the voucher
 
        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):
 
        ''' Calculates all of the discounts available for this product.
 
        NB should be transactional, and it's terribly inefficient.
 
        '''
 

	
 
        # Delete the existing entries.
 
        rego.DiscountItem.objects.filter(cart=self.cart).delete()
 

	
 
        product_items = self.cart.productitem_set.all()
 

	
 
        products = [i.product for i in product_items]
 
        discounts = discount.available_discounts(self.cart.user, [], products)
 

	
 
        # The highest-value discounts will apply to the highest-value
 
        # products first.
 
        product_items = self.cart.productitem_set.all()
 
        product_items = product_items.order_by('product__price')
 
        product_items = reversed(product_items)
 
        for item in product_items:
 
            self._add_discount(item.product, item.quantity, discounts)
 

	
 
    def _add_discount(self, product, quantity, discounts):
 
        ''' Applies the best discounts on the given product, from the given
 
        discounts.'''
 

	
 
        def matches(discount):
 
            ''' Returns True if and only if the given discount apples to
 
            our product. '''
 
            if isinstance(discount.clause, rego.DiscountForCategory):
 
                return discount.clause.category == product.category
 
            else:
 
                return discount.clause.product == product
 

	
 
        def value(discount):
 
            ''' Returns the value of this discount clause
 
            as applied to this product '''
 
            if discount.clause.percentage is not None:
 
                return discount.clause.percentage * product.price
 
            else:
 
                return discount.clause.price
 

	
 
        discounts = [i for i in discounts if matches(i)]
 
        discounts.sort(key=value)
 

	
 
        for candidate in reversed(discounts):
 
            if quantity == 0:
 
                break
 
            elif candidate.quantity == 0:
 
                # This discount clause has been exhausted by this cart
 
                continue
 

	
 
            # Get a provisional instance for this DiscountItem
 
            # with the quantity set to as much as we have in the cart
 
            discount_item = rego.DiscountItem.objects.create(
 
                product=product,
 
                cart=self.cart,
 
                discount=candidate.discount,
 
                quantity=quantity,
 
            )
 

	
 
            # Truncate the quantity for this DiscountItem if we exceed quantity
 
            ours = discount_item.quantity
 
            allowed = candidate.quantity
 
            if ours > allowed:
 
                discount_item.quantity = allowed
 
                # Update the remaining quantity.
 
                quantity = ours - allowed
 
            else:
 
                quantity = 0
 

	
 
            candidate.quantity -= discount_item.quantity
 

	
 
            discount_item.save()
registrasion/controllers/category.py
Show inline comments
 
from registrasion import models as rego
 

	
 
from django.db.models import Sum
 

	
 

	
 
class AllProducts(object):
 
    pass
 

	
 

	
 
class CategoryController(object):
 

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

	
 
    @classmethod
 
    def available_categories(cls, user, products=AllProducts):
 
        ''' Returns the categories available to the user. Specify `products` if
 
        you want to restrict to just the categories that hold the specified
 
        products, otherwise it'll do all. '''
 

	
 
        # STOPGAP -- this needs to be elsewhere tbqh
 
        from product import ProductController
 

	
 
        if products is AllProducts:
 
            products = rego.Product.objects.all()
 

	
 
        available = ProductController.available_products(
 
            user,
 
            products=products,
 
        )
 

	
 
        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):
 
        CONTROLLERS = {
 
            rego.CategoryEnablingCondition: CategoryConditionController,
 
            rego.IncludedProductDiscount: ProductConditionController,
 
            rego.ProductEnablingCondition: ProductConditionController,
 
            rego.TimeOrStockLimitDiscount:
 
                TimeOrStockLimitDiscountController,
 
            rego.TimeOrStockLimitEnablingCondition:
 
                TimeOrStockLimitEnablingConditionController,
 
            rego.VoucherDiscount: VoucherConditionController,
 
            rego.VoucherEnablingCondition: VoucherConditionController,
 
        }
 

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

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

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

	
 
        carts = rego.Cart.objects.filter(user=user, released=False)
 
        enabling_products = rego.Product.objects.filter(
 
            category=self.condition.enabling_category,
 
        )
 
        products_count = rego.ProductItem.objects.filter(
 
            cart__in=carts,
 
            product__in=enabling_products,
 
        ).count()
 
        return products_count > 0
 

	
 

	
 
class ProductConditionController(ConditionController):
 
    ''' Condition tests for ProductEnablingCondition and
 
    IncludedProductDiscount. '''
 

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

	
 
    def is_met(self, user):
 
        ''' returns True if the user has a product that invokes this
 
        condition in one of their carts '''
 

	
 
        carts = rego.Cart.objects.filter(user=user, released=False)
 
        products_count = rego.ProductItem.objects.filter(
 
            cart__in=carts,
 
            product__in=self.condition.enabling_products.all(),
 
        ).count()
 
        return products_count > 0
 

	
 

	
 
class TimeOrStockLimitConditionController(ConditionController):
 
    ''' Common condition tests for TimeOrStockLimit EnablingCondition and
 
    Discount.'''
 

	
 
    def __init__(self, ceiling):
 
        self.ceiling = 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 0
 

	
 
        return self._get_remaining_stock(user)
 

	
 
    def _test_date_range(self):
 
        now = timezone.now()
 

	
 
        if self.ceiling.start_time is not None:
 
            if now < self.ceiling.start_time:
 
                return False
 

	
 
        if self.ceiling.end_time is not None:
 
            if now > self.ceiling.end_time:
 
                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
 

	
 

	
 
class VoucherConditionController(ConditionController):
 
    ''' Condition test for VoucherEnablingCondition and VoucherDiscount.'''
 

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

	
 
    def is_met(self, user):
 
        ''' returns True if the user has the given voucher attached. '''
 
        carts_count = rego.Cart.objects.filter(
 
            user=user,
 
            vouchers=self.condition.voucher,
 
        ).count()
 
        return carts_count > 0
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,
 
            active=False,
 
            released=False,
 
        )
 

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

	
 
        prod_count = items.aggregate(Sum("quantity"))["quantity__sum"] or 0
 

	
 
        return prod_limit - prod_count
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
...
 
@@ -33,193 +33,192 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
 
            email='test2@example.com',
 
            password='top_secret')
 

	
 
        cls.RESERVATION = datetime.timedelta(hours=1)
 

	
 
        cls.categories = []
 
        for i in xrange(2):
 
            cat = rego.Category.objects.create(
 
                name="Category " + str(i + 1),
 
                description="This is a test category",
 
                order=i,
 
                render_type=rego.Category.RENDER_TYPE_RADIO,
 
                required=False,
 
            )
 
            cat.save()
 
            cls.categories.append(cat)
 

	
 
        cls.CAT_1 = cls.categories[0]
 
        cls.CAT_2 = cls.categories[1]
 

	
 
        cls.products = []
 
        for i in xrange(4):
 
            prod = rego.Product.objects.create(
 
                name="Product " + str(i + 1),
 
                description="This is a test product.",
 
                category=cls.categories[i / 2],  # 2 products per category
 
                price=Decimal("10.00"),
 
                reservation_duration=cls.RESERVATION,
 
                limit_per_user=10,
 
                order=1,
 
            )
 
            prod.save()
 
            cls.products.append(prod)
 

	
 
        cls.PROD_1 = cls.products[0]
 
        cls.PROD_2 = cls.products[1]
 
        cls.PROD_3 = cls.products[2]
 
        cls.PROD_4 = cls.products[3]
 

	
 
        cls.PROD_4.price = Decimal("5.00")
 
        cls.PROD_4.save()
 

	
 
        # Burn through some carts -- this made some past EC tests fail
 
        current_cart = TestingCartController.for_user(cls.USER_1)
 
        current_cart.cart.active = False
 
        current_cart.cart.save()
 

	
 
        current_cart = TestingCartController.for_user(cls.USER_2)
 
        current_cart.cart.active = False
 
        current_cart.cart.save()
 

	
 
    @classmethod
 
    def make_ceiling(cls, name, limit=None, start_time=None, end_time=None):
 
        limit_ceiling = rego.TimeOrStockLimitEnablingCondition.objects.create(
 
            description=name,
 
            mandatory=True,
 
            limit=limit,
 
            start_time=start_time,
 
            end_time=end_time
 
        )
 
        limit_ceiling.save()
 
        limit_ceiling.products.add(cls.PROD_1, cls.PROD_2)
 
        limit_ceiling.save()
 

	
 
    @classmethod
 
    def make_category_ceiling(
 
            cls, name, limit=None, start_time=None, end_time=None):
 
        limit_ceiling = rego.TimeOrStockLimitEnablingCondition.objects.create(
 
            description=name,
 
            mandatory=True,
 
            limit=limit,
 
            start_time=start_time,
 
            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)
 

	
 
        current_cart2 = TestingCartController.for_user(self.USER_1)
 
        self.assertEqual(current_cart.cart, current_cart2.cart)
 

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

	
 
        # Add a product twice
 
        current_cart.add_to_cart(self.PROD_1, 1)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        # Count of products for a given user should be collapsed.
 
        items = rego.ProductItem.objects.filter(
 
            cart=current_cart.cart,
 
            product=self.PROD_1)
 
        self.assertEqual(1, len(items))
 
        item = items[0]
 
        self.assertEquals(2, item.quantity)
 

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

	
 
        def get_item():
 
            return rego.ProductItem.objects.get(
 
                cart=current_cart.cart,
 
                product=self.PROD_1)
 

	
 
        current_cart.set_quantity(self.PROD_1, 1)
 
        self.assertEqual(1, get_item().quantity)
 

	
 
        # Setting the quantity to zero should remove the entry from the cart.
 
        current_cart.set_quantity(self.PROD_1, 0)
 
        with self.assertRaises(ObjectDoesNotExist):
 
            get_item()
 

	
 
        current_cart.set_quantity(self.PROD_1, 9)
 
        self.assertEqual(9, get_item().quantity)
 

	
 
        with self.assertRaises(ValidationError):
 
            current_cart.set_quantity(self.PROD_1, 11)
 

	
 
        self.assertEqual(9, get_item().quantity)
 

	
 
        with self.assertRaises(ValidationError):
 
            current_cart.set_quantity(self.PROD_1, -1)
 

	
 
        self.assertEqual(9, get_item().quantity)
 

	
 
        current_cart.set_quantity(self.PROD_1, 2)
 
        self.assertEqual(2, get_item().quantity)
 

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

	
 
        # User should be able to add 1 of PROD_1 to the current cart.
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        # User should be able to add 1 of PROD_1 to the current cart.
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        # User should not be able to add 10 of PROD_1 to the current cart now,
 
        # because they have a limit of 10.
 
        with self.assertRaises(ValidationError):
 
            current_cart.add_to_cart(self.PROD_1, 10)
 

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

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        # User should not be able to add 10 of PROD_1 to the current cart now,
 
        # even though it's a new cart.
 
        with self.assertRaises(ValidationError):
 
            current_cart.add_to_cart(self.PROD_1, 10)
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)
 

	
 

	
 
@login_required
 
def guided_registration(request, page_id=0):
 
    ''' Goes through the registration process in order,
 
    making sure user sees all valid categories.
 

	
 
    WORK IN PROGRESS: the finalised version of this view will allow
 
    grouping of categories into a specific page. Currently, it just goes
 
    through each category one by one
 
    '''
 

	
 
    next_step = redirect("guided_registration")
 

	
 
    sections = []
 

	
 
    attendee = rego.Attendee.get_instance(request.user)
 

	
 
    if attendee.completed_registration:
 
        return render(
 
            request,
 
            "registrasion/guided_registration_complete.html",
 
            {},
 
        )
 

	
 
    # Step 1: Fill in a badge and collect a voucher code
 
    try:
 
        profile = attendee.attendeeprofilebase
 
    except ObjectDoesNotExist:
 
        profile = None
 

	
 
    if not profile:
 
        # TODO: if voucherform is invalid, make sure
 
        # that profileform does not save
 
        voucher_form, voucher_handled = handle_voucher(request, "voucher")
 
        profile_form, profile_handled = handle_profile(request, "profile")
 

	
 
        voucher_section = GuidedRegistrationSection(
 
            title="Voucher Code",
 
            form=voucher_form,
 
        )
 

	
 
        profile_section = GuidedRegistrationSection(
 
            title="Profile and Personal Information",
 
            form=profile_form,
 
        )
 

	
 
        title = "Attendee information"
 
        current_step = 1
 
        sections.append(voucher_section)
 
        sections.append(profile_section)
 
    else:
 
        # We're selling products
 

	
 
        last_category = attendee.highest_complete_category
 

	
 
        # Get the next category
 
        cats = rego.Category.objects
 
        cats = cats.filter(id__gt=last_category).order_by("order")
 

	
 
        if cats.count() == 0:
 
            # We've filled in every category
 
            attendee.completed_registration = True
 
            attendee.save()
 
            return next_step
 

	
 
        if last_category == 0:
 
            # Only display the first Category
 
            title = "Select ticket type"
 
            current_step = 2
 
            cats = [cats[0]]
 
        else:
0 comments (0 inline, 0 general)