Changeset - 2b59151429fe
[Not reviewed]
Merge
0 9 1
Christopher Neugebauer - 8 years ago 2016-04-06 22:00:39
chrisjrn@gmail.com
Merge branch 'random_fixes'
10 files changed with 168 insertions and 42 deletions:
0 comments (0 inline, 0 general)
registrasion/controllers/cart.py
Show inline comments
...
 
@@ -35,96 +35,101 @@ class CartController(object):
 
                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)
 
        items_in_cart = items_in_cart.select_related(
 
            "product",
 
            "product__category",
 
        )
 

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

	
 
        errors = []
 

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

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

	
 
            if quantity > limit:
 
                errors.append((
 
                    product,
...
 
@@ -238,123 +243,127 @@ class CartController(object):
 
        try:
 
            self._test_limits(product_quantities)
 
        except ValidationError as ve:
 
            for error in ve.error_list:
 
                errors.append(error.message[1])
 

	
 
        # Validate the discounts
 
        discount_items = rego.DiscountItem.objects.filter(cart=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(user):
 
                errors.append(
 
                    ValidationError("Discounts are no longer available")
 
                )
 

	
 
        if errors:
 
            raise ValidationError(errors)
 

	
 
    @transaction.atomic
 
    def fix_simple_errors(self):
 
        ''' This attempts to fix the easy errors raised by ValidationError.
 
        This includes removing items from the cart that are no longer
 
        available, recalculating all of the discounts, and removing voucher
 
        codes that are no longer available. '''
 

	
 
        # Fix vouchers first (this affects available discounts)
 
        active_carts = rego.Cart.reserved_carts()
 
        to_remove = []
 
        for voucher in self.cart.vouchers.all():
 
            try:
 
                self._test_voucher(voucher)
 
            except ValidationError as ve:
 
                to_remove.append(voucher)
 

	
 
        for voucher in to_remove:
 
            self.cart.vouchers.remove(voucher)
 

	
 
        # Fix products and discounts
 
        items = rego.ProductItem.objects.filter(cart=self.cart)
 
        items = items.select_related("product")
 
        products = set(i.product for i in items)
 
        available = set(ProductController.available_products(
 
            self.cart.user,
 
            products=products,
 
        ))
 

	
 
        not_available = products - available
 
        zeros = [(product, 0) for product in not_available]
 

	
 
        self.set_quantities(zeros)
 

	
 
    @transaction.atomic
 
    def recalculate_discounts(self):
 
        ''' Calculates all of the discounts available for this product.
 
        '''
 

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

	
 
        product_items = self.cart.productitem_set.all()
 
        product_items = self.cart.productitem_set.all().select_related(
 
            "product", "product__category",
 
        )
 

	
 
        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.select_related("product")
 
        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:
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()
 
            products = rego.Product.objects.all().select_related("category")
 

	
 
        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
 
        return cat_limit - cat_count
registrasion/controllers/conditions.py
Show inline comments
 
import itertools
 
import operator
 

	
 
from collections import defaultdict
 
from collections import namedtuple
 

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

	
 

	
 
    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_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)
 
        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()
 

	
 
        prods = (
 
            product.enablingconditionbase_set.select_subclasses()
 
            for product in products
 
        ]
 
        all_conditions = set(itertools.chain(*all_conditions))
 
        )
 
        # Get the conditions covered by their categories
 
        cats = (
 
            category.enablingconditionbase_set.select_subclasses()
 
            for category in set(product.category for product in products)
 
        )
 

	
 
        if products:
 
            # Simplify the query.
 
            all_conditions = reduce(operator.or_, itertools.chain(prods, cats))
 
        else:
 
            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)
 

	
 
        messages = {}
 

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

	
 
            all_products = cond_products | from_category
 
            all_products = all_products.select_related("category")
 
            # Remove the products that we aren't asking about
 
            all_products = all_products & products
 
            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.mandatory:
 
                    mandatory[product] &= met
 
                else:
 
                    non_mandatory[product] |= met
 

	
 
                if not met and product not in messages:
 
                    messages[product] = message
 

	
 

	
 
        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, messages[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
 

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

	
 
from conditions import ConditionController
 
from registrasion import models as rego
 

	
 
from django.db.models import Sum
 

	
 

	
 
class DiscountAndQuantity(object):
 
    def __init__(self, discount, clause, quantity):
 
        self.discount = discount
 
        self.clause = clause
 
        self.quantity = quantity
 

	
 
    def __repr__(self):
 
        print "(discount=%s, clause=%s, quantity=%d)" % (
 
        return "(discount=%s, clause=%s, quantity=%d)" % (
 
            self.discount, self.clause, self.quantity,
 
        )
 

	
 

	
 
def available_discounts(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. '''
 

	
 
    # discounts that match provided categories
 
    category_discounts = rego.DiscountForCategory.objects.filter(
 
        category__in=categories
 
    )
 
    # discounts that match provided products
 
    product_discounts = rego.DiscountForProduct.objects.filter(
 
        product__in=products
 
    )
 
    # discounts that match categories for provided products
 
    product_category_discounts = rego.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(
 
        "product",
 
        "product__category",
 
    )
 

	
 
    all_category_discounts = category_discounts | product_category_discounts
 
    all_category_discounts = all_category_discounts.select_related(
 
        "category",
 
    )
 

	
 
    # The set of all potential discounts
 
    potential_discounts = set(itertools.chain(
 
        product_discounts,
 
        category_discounts,
 
        product_category_discounts,
 
        all_category_discounts,
 
    ))
 

	
 
    discounts = []
 

	
 
    # Markers so that we don't need to evaluate given conditions more than once
 
    accepted_discounts = set()
 
    failed_discounts = set()
 

	
 

	
 
    for discount in potential_discounts:
 
        real_discount = rego.DiscountBase.objects.get_subclass(
 
            pk=discount.discount.pk,
 
        )
 
        cond = ConditionController.for_condition(real_discount)
 

	
 
        # Count the past uses of the given discount item.
 
        # If this user has exceeded the limit for the clause, this clause
 
        # is not available any more.
 
        past_uses = rego.DiscountItem.objects.filter(
 
            cart__user=user,
 
            cart__active=False,  # Only past carts count
 
            cart__released=False,  # You can reuse refunded discounts
 
            discount=discount.discount,
 
            discount=real_discount,
 
        )
 
        agg = past_uses.aggregate(Sum("quantity"))
 
        past_use_count = agg["quantity__sum"]
 
        if past_use_count is None:
 
            past_use_count = 0
 

	
 
        if past_use_count >= discount.quantity:
 
            # This clause has exceeded its use count
 
            pass
 
        elif real_discount not in failed_discounts:
 
            # This clause is still available
 
            if real_discount in accepted_discounts or cond.is_met(user):
 
                # This clause is valid for this user
 
                discounts.append(DiscountAndQuantity(
 
                    discount=real_discount,
 
                    clause=discount,
 
                    quantity=discount.quantity - past_use_count,
 
                ))
 
                accepted_discounts.add(real_discount)
 
            else:
 
                # This clause is not valid for this user
 
                failed_discounts.add(real_discount)
 
    return discounts
registrasion/controllers/product.py
Show inline comments
 
import itertools
 

	
 
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)
 
            all_products = all_products.select_related("category")
 
        else:
 
            all_products = []
 

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

	
 
        cat_quants = dict(
 
            (
 
                category,
 
                CategoryController(category).user_quantity_remaining(user),
 
            )
 
            for category in set(product.category for product in all_products)
 
        )
 

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

	
 
        failed_and_messages = ConditionController.test_enabling_conditions(
 
            user, products=passed_limits
 
        )
 
        failed_conditions = set(i[0] for i in failed_and_messages)
 

	
 
        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/forms.py
Show inline comments
...
 
@@ -10,96 +10,97 @@ class _ProductsForm(forms.Form):
 

	
 
    PRODUCT_PREFIX = "product_"
 

	
 
    ''' Base class for product entry forms. '''
 
    def __init__(self, *a, **k):
 
        if "product_quantities" in k:
 
            initial = self.initial_data(k["product_quantities"])
 
            k["initial"] = initial
 
            del k["product_quantities"]
 
        super(_ProductsForm, self).__init__(*a, **k)
 

	
 
    @classmethod
 
    def field_name(cls, product):
 
        return cls.PRODUCT_PREFIX + ("%d" % product.id)
 

	
 
    @classmethod
 
    def set_fields(cls, category, products):
 
        ''' Sets the base_fields on this _ProductsForm to allow selecting
 
        from the provided products. '''
 
        pass
 

	
 
    @classmethod
 
    def initial_data(cls, product_quantites):
 
        ''' Prepares initial data for an instance of this form.
 
        product_quantities is a sequence of (product,quantity) tuples '''
 
        return {}
 

	
 
    def product_quantities(self):
 
        ''' Yields a sequence of (product, quantity) tuples from the
 
        cleaned form data. '''
 
        return iter([])
 

	
 

	
 
class _QuantityBoxProductsForm(_ProductsForm):
 
    ''' Products entry form that allows users to enter quantities
 
    of desired products. '''
 

	
 
    @classmethod
 
    def set_fields(cls, category, products):
 
        for product in products:
 
            if product.description:
 
                help_text = "$%d each -- %s" % (product.price, product.description)
 
            else:
 
                help_text = "$%d each" % product.price
 

	
 
            field = forms.IntegerField(
 
                label=product.name,
 
                help_text=help_text,
 
                min_value=0,
 
            )
 
            cls.base_fields[cls.field_name(product)] = field
 

	
 
    @classmethod
 
    def initial_data(cls, product_quantities):
 
        initial = {}
 
        for product, quantity in product_quantities:
 
            initial[cls.field_name(product)] = quantity
 

	
 
        return initial
 

	
 
    def product_quantities(self):
 
        for name, value in self.cleaned_data.items():
 
            if name.startswith(self.PRODUCT_PREFIX):
 
                product_id = int(name[len(self.PRODUCT_PREFIX):])
 
                yield (product_id, value, name)
 

	
 

	
 
class _RadioButtonProductsForm(_ProductsForm):
 
    ''' Products entry form that allows users to enter quantities
 
    of desired products. '''
 

	
 
    FIELD = "chosen_product"
 

	
 
    @classmethod
 
    def set_fields(cls, category, products):
 
        choices = []
 
        for product in products:
 

	
 
            choice_text = "%s -- $%d" % (product.name, product.price)
 
            choices.append((product.id, choice_text))
 

	
 
        cls.base_fields[cls.FIELD] = forms.TypedChoiceField(
 
            label=category.name,
 
            widget=forms.RadioSelect,
 
            choices=choices,
 
            empty_value=0,
 
            coerce=int,
 
        )
 

	
 
    @classmethod
 
    def initial_data(cls, product_quantities):
 
        initial = {}
 

	
 
        for product, quantity in product_quantities:
 
            if quantity > 0:
 
                initial[cls.FIELD] = product.id
 
                break
registrasion/migrations/0012_auto_20160406_1212.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8 -*-
 
# Generated by Django 1.9.2 on 2016-04-06 12:12
 
from __future__ import unicode_literals
 

	
 
from django.db import migrations, models
 

	
 

	
 
class Migration(migrations.Migration):
 

	
 
    dependencies = [
 
        ('registrasion', '0011_auto_20160401_0943'),
 
    ]
 

	
 
    operations = [
 
        migrations.AlterField(
 
            model_name='cart',
 
            name='active',
 
            field=models.BooleanField(db_index=True, default=True),
 
        ),
 
        migrations.AlterField(
 
            model_name='cart',
 
            name='released',
 
            field=models.BooleanField(db_index=True, default=False),
 
        ),
 
        migrations.AlterField(
 
            model_name='cart',
 
            name='time_last_updated',
 
            field=models.DateTimeField(db_index=True),
 
        ),
 
        migrations.AlterField(
 
            model_name='category',
 
            name='order',
 
            field=models.PositiveIntegerField(db_index=True, verbose_name='Display order'),
 
        ),
 
        migrations.AlterField(
 
            model_name='product',
 
            name='order',
 
            field=models.PositiveIntegerField(db_index=True, verbose_name='Display order'),
 
        ),
 
        migrations.AlterField(
 
            model_name='productitem',
 
            name='quantity',
 
            field=models.PositiveIntegerField(db_index=True),
 
        ),
 
        migrations.AlterIndexTogether(
 
            name='cart',
 
            index_together=set([('active', 'released'), ('released', 'user'), ('active', 'user'), ('active', 'time_last_updated')]),
 
        ),
 
    ]
registrasion/models.py
Show inline comments
...
 
@@ -54,144 +54,146 @@ class AttendeeProfileBase(models.Model):
 
        speaker profile. If it's None, that functionality is disabled. '''
 
        return None
 

	
 
    attendee = models.OneToOneField(Attendee, on_delete=models.CASCADE)
 

	
 

	
 
# Inventory Models
 

	
 
@python_2_unicode_compatible
 
class Category(models.Model):
 
    ''' Registration product categories '''
 

	
 
    class Meta:
 
        verbose_name_plural = _("categories")
 

	
 
    def __str__(self):
 
        return self.name
 

	
 
    RENDER_TYPE_RADIO = 1
 
    RENDER_TYPE_QUANTITY = 2
 

	
 
    CATEGORY_RENDER_TYPES = [
 
        (RENDER_TYPE_RADIO, _("Radio button")),
 
        (RENDER_TYPE_QUANTITY, _("Quantity boxes")),
 
    ]
 

	
 
    name = models.CharField(
 
        max_length=65,
 
        verbose_name=_("Name"),
 
    )
 
    description = models.CharField(
 
        max_length=255,
 
        verbose_name=_("Description"),
 
    )
 
    limit_per_user = models.PositiveIntegerField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Limit per user"),
 
        help_text=_("The total number of items from this category one "
 
                    "attendee may purchase."),
 
    )
 
    required = models.BooleanField(
 
        blank=True,
 
        help_text=_("If enabled, a user must select an "
 
                    "item from this category."),
 
    )
 
    order = models.PositiveIntegerField(
 
        verbose_name=("Display order"),
 
        db_index=True,
 
    )
 
    render_type = models.IntegerField(
 
        choices=CATEGORY_RENDER_TYPES,
 
        verbose_name=_("Render type"),
 
        help_text=_("The registration form will render this category in this "
 
                    "style."),
 
    )
 

	
 

	
 
@python_2_unicode_compatible
 
class Product(models.Model):
 
    ''' Registration products '''
 

	
 
    def __str__(self):
 
        return "%s - %s" % (self.category.name, self.name)
 

	
 
    name = models.CharField(
 
        max_length=65,
 
        verbose_name=_("Name"),
 
    )
 
    description = models.CharField(
 
        max_length=255,
 
        verbose_name=_("Description"),
 
        null=True,
 
        blank=True,
 
    )
 
    category = models.ForeignKey(
 
        Category,
 
        verbose_name=_("Product category")
 
    )
 
    price = models.DecimalField(
 
        max_digits=8,
 
        decimal_places=2,
 
        verbose_name=_("Price"),
 
    )
 
    limit_per_user = models.PositiveIntegerField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Limit per user"),
 
    )
 
    reservation_duration = models.DurationField(
 
        default=datetime.timedelta(hours=1),
 
        verbose_name=_("Reservation duration"),
 
        help_text=_("The length of time this product will be reserved before "
 
                    "it is released for someone else to purchase."),
 
    )
 
    order = models.PositiveIntegerField(
 
        verbose_name=("Display order"),
 
        db_index=True,
 
    )
 

	
 

	
 
@python_2_unicode_compatible
 
class Voucher(models.Model):
 
    ''' Registration vouchers '''
 

	
 
    # Vouchers reserve a cart for a fixed amount of time, so that
 
    # items may be added without the voucher being swiped by someone else
 
    RESERVATION_DURATION = datetime.timedelta(hours=1)
 

	
 
    def __str__(self):
 
        return "Voucher for %s" % self.recipient
 

	
 
    @classmethod
 
    def normalise_code(cls, code):
 
        return code.upper()
 

	
 
    def save(self, *a, **k):
 
        ''' Normalise the voucher code to be uppercase '''
 
        self.code = self.normalise_code(self.code)
 
        super(Voucher, self).save(*a, **k)
 

	
 
    recipient = models.CharField(max_length=64, verbose_name=_("Recipient"))
 
    code = models.CharField(max_length=16,
 
                            unique=True,
 
                            verbose_name=_("Voucher code"))
 
    limit = models.PositiveIntegerField(verbose_name=_("Voucher use limit"))
 

	
 

	
 
# Product Modifiers
 

	
 
@python_2_unicode_compatible
 
class DiscountBase(models.Model):
 
    ''' Base class for discounts. Each subclass has controller code that
 
    determines whether or not the given discount is available to be added to
 
    the current cart. '''
 

	
 
    objects = InheritanceManager()
 

	
 
    def __str__(self):
 
        return "Discount: " + self.description
 

	
 
    def effects(self):
 
        ''' Returns all of the effects of this discount. '''
 
        products = self.discountforproduct_set.all()
 
        categories = self.discountforcategory_set.all()
 
        return itertools.chain(products, categories)
...
 
@@ -268,96 +270,97 @@ class DiscountForCategory(models.Model):
 
                    "a product or its category"))
 
        if len(cats) > 1:
 
            raise ValidationError(
 
                _("You may only have one discount line per category"))
 

	
 
    discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
 
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
 
    percentage = models.DecimalField(
 
        max_digits=4,
 
        decimal_places=1)
 
    quantity = models.PositiveIntegerField()
 

	
 

	
 
class TimeOrStockLimitDiscount(DiscountBase):
 
    ''' Discounts that are generally available, but are limited by timespan or
 
    usage count. This is for e.g. Early Bird discounts. '''
 

	
 
    class Meta:
 
        verbose_name = _("Promotional discount")
 

	
 
    start_time = models.DateTimeField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Start time"),
 
        help_text=_("This discount will only be available after this time."),
 
    )
 
    end_time = models.DateTimeField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("End time"),
 
        help_text=_("This discount will only be available before this time."),
 
    )
 
    limit = models.PositiveIntegerField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Limit"),
 
        help_text=_("This discount may only be applied this many times."),
 
    )
 

	
 

	
 
class VoucherDiscount(DiscountBase):
 
    ''' Discounts that are enabled when a voucher code is in the current
 
    cart. '''
 

	
 
    voucher = models.OneToOneField(
 
        Voucher,
 
        on_delete=models.CASCADE,
 
        verbose_name=_("Voucher"),
 
        db_index=True,
 
    )
 

	
 

	
 
class IncludedProductDiscount(DiscountBase):
 
    ''' Discounts that are enabled because another product has been purchased.
 
    e.g. A conference ticket includes a free t-shirt. '''
 

	
 
    class Meta:
 
        verbose_name = _("Product inclusion")
 

	
 
    enabling_products = models.ManyToManyField(
 
        Product,
 
        verbose_name=_("Including product"),
 
        help_text=_("If one of these products are purchased, the discounts "
 
                    "below will be enabled."),
 
    )
 

	
 

	
 
class RoleDiscount(object):
 
    ''' Discounts that are enabled because the active user has a specific
 
    role. This is for e.g. volunteers who can get a discount ticket. '''
 
    # TODO: implement RoleDiscount
 
    pass
 

	
 

	
 
@python_2_unicode_compatible
 
class EnablingConditionBase(models.Model):
 
    ''' This defines a condition which allows products or categories to
 
    be made visible. If there is at least one mandatory enabling condition
 
    defined on a Product or Category, it will only be enabled if *all*
 
    mandatory conditions are met, otherwise, if there is at least one enabling
 
    condition defined on a Product or Category, it will only be enabled if at
 
    least one condition is met. '''
 

	
 
    objects = InheritanceManager()
 

	
 
    def __str__(self):
 
        return self.description
 

	
 
    def effects(self):
 
        ''' Returns all of the items enabled by this condition. '''
 
        return itertools.chain(self.products.all(), self.categories.all())
 

	
 
    description = models.CharField(max_length=255)
 
    mandatory = models.BooleanField(
 
        default=False,
 
        help_text=_("If there is at least one mandatory condition defined on "
 
                    "a product or category, all such conditions must be met. "
...
 
@@ -413,131 +416,147 @@ class ProductEnablingCondition(EnablingConditionBase):
 
    enabling_products = models.ManyToManyField(
 
        Product,
 
        help_text=_("If one of these products are purchased, this condition "
 
                    "is met."),
 
    )
 

	
 

	
 
@python_2_unicode_compatible
 
class CategoryEnablingCondition(EnablingConditionBase):
 
    ''' The condition is met because a product in a particular product is
 
    purchased. '''
 

	
 
    def __str__(self):
 
        return "Enabled by product in category: " + str(self.enabling_category)
 

	
 
    enabling_category = models.ForeignKey(
 
        Category,
 
        help_text=_("If a product from this category is purchased, this "
 
                    "condition is met."),
 
    )
 

	
 

	
 
@python_2_unicode_compatible
 
class VoucherEnablingCondition(EnablingConditionBase):
 
    ''' The condition is met because a Voucher is present. This is for e.g.
 
    enabling sponsor tickets. '''
 

	
 
    def __str__(self):
 
        return "Enabled by voucher: %s" % self.voucher
 

	
 
    voucher = models.OneToOneField(Voucher)
 

	
 

	
 
# @python_2_unicode_compatible
 
class RoleEnablingCondition(object):
 
    ''' The condition is met because the active user has a particular Role.
 
    This is for e.g. enabling Team tickets. '''
 
    # TODO: implement RoleEnablingCondition
 
    pass
 

	
 

	
 
# Commerce Models
 

	
 
@python_2_unicode_compatible
 
class Cart(models.Model):
 
    ''' Represents a set of product items that have been purchased, or are
 
    pending purchase. '''
 

	
 
    class Meta:
 
        index_together = [
 
            ("active", "time_last_updated"),
 
            ("active", "released"),
 
            ("active", "user"),
 
            ("released", "user"),
 
        ]
 

	
 
    def __str__(self):
 
        return "%d rev #%d" % (self.id, self.revision)
 

	
 
    user = models.ForeignKey(User)
 
    # ProductItems (foreign key)
 
    vouchers = models.ManyToManyField(Voucher, blank=True)
 
    time_last_updated = models.DateTimeField()
 
    time_last_updated = models.DateTimeField(
 
        db_index=True,
 
    )
 
    reservation_duration = models.DurationField()
 
    revision = models.PositiveIntegerField(default=1)
 
    active = models.BooleanField(default=True)
 
    released = models.BooleanField(default=False)  # Refunds etc
 
    active = models.BooleanField(
 
        default=True,
 
        db_index=True,
 
    )
 
    released = models.BooleanField(
 
        default=False,
 
        db_index=True
 
    )  # Refunds etc
 

	
 
    @classmethod
 
    def reserved_carts(cls):
 
        ''' Gets all carts that are 'reserved' '''
 
        return Cart.objects.filter(
 
            (Q(active=True) &
 
                Q(time_last_updated__gt=(
 
                    timezone.now()-F('reservation_duration')
 
                                        ))) |
 
            (Q(active=False) & Q(released=False))
 
        )
 

	
 

	
 
@python_2_unicode_compatible
 
class ProductItem(models.Model):
 
    ''' Represents a product-quantity pair in a Cart. '''
 

	
 
    def __str__(self):
 
        return "product: %s * %d in Cart: %s" % (
 
            self.product, self.quantity, self.cart)
 

	
 
    cart = models.ForeignKey(Cart)
 
    product = models.ForeignKey(Product)
 
    quantity = models.PositiveIntegerField()
 
    quantity = models.PositiveIntegerField(db_index=True)
 

	
 

	
 
@python_2_unicode_compatible
 
class DiscountItem(models.Model):
 
    ''' Represents a discount-product-quantity relation in a Cart. '''
 

	
 
    def __str__(self):
 
        return "%s: %s * %d in Cart: %s" % (
 
            self.discount, self.product, self.quantity, self.cart)
 

	
 
    cart = models.ForeignKey(Cart)
 
    product = models.ForeignKey(Product)
 
    discount = models.ForeignKey(DiscountBase)
 
    quantity = models.PositiveIntegerField()
 

	
 

	
 
@python_2_unicode_compatible
 
class Invoice(models.Model):
 
    ''' An invoice. Invoices can be automatically generated when checking out
 
    a Cart, in which case, it is attached to a given revision of a Cart. '''
 

	
 
    def __str__(self):
 
        return "Invoice #%d" % self.id
 

	
 
    def clean(self):
 
        if self.cart is not None and self.cart_revision is None:
 
            raise ValidationError(
 
                "If this is a cart invoice, it must have a revision")
 

	
 
    # Invoice Number
 
    user = models.ForeignKey(User)
 
    cart = models.ForeignKey(Cart, null=True)
 
    cart_revision = models.IntegerField(null=True)
 
    # Line Items (foreign key)
 
    void = models.BooleanField(default=False)
 
    paid = models.BooleanField(default=False)
 
    value = models.DecimalField(max_digits=8, decimal_places=2)
 

	
 

	
 
@python_2_unicode_compatible
 
class LineItem(models.Model):
 
    ''' Line items for an invoice. These are denormalised from the ProductItems
 
    and DiscountItems that belong to a cart (for consistency), but also allow
 
    for arbitrary line items when required. '''
 

	
 
    def __str__(self):
 
        return "Line: %s * %d @ %s" % (
 
            self.description, self.quantity, self.price)
registrasion/templatetags/registrasion_tags.py
Show inline comments
 
from registrasion import models as rego
 
from registrasion.controllers.category import CategoryController
 

	
 
from collections import namedtuple
 
from django import template
 
from django.db.models import Sum
 

	
 
register = template.Library()
 

	
 
ProductAndQuantity = namedtuple("ProductAndQuantity", ["product", "quantity"])
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
def available_categories(context):
 
    ''' Returns all of the available product categories '''
 
    return CategoryController.available_categories(context.request.user)
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
def invoices(context):
 
    ''' Returns all of the invoices that this user has. '''
 
    return rego.Invoice.objects.filter(cart__user=context.request.user)
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
def items_pending(context):
 
    ''' Returns all of the items that this user has in their current cart,
 
    and is awaiting payment. '''
 

	
 
    all_items = rego.ProductItem.objects.filter(
 
        cart__user=context.request.user,
 
        cart__active=True,
 
    )
 
    ).select_related("product", "product__category")
 
    return all_items
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
def items_purchased(context, category=None):
 
    ''' Returns all of the items that this user has purchased, optionally
 
    from the given category. '''
 

	
 
    all_items = rego.ProductItem.objects.filter(
 
        cart__user=context.request.user,
 
        cart__active=False,
 
    )
 
        cart__released=False,
 
    ).select_related("product", "product__category")
 

	
 
    if category:
 
        all_items = all_items.filter(product__category=category)
 

	
 
    products = set(item.product for item in all_items)
 
    pq = all_items.values("product").annotate(quantity=Sum("quantity")).all()
 
    products = rego.Product.objects.all()
 
    out = []
 
    for product in products:
 
        pp = all_items.filter(product=product)
 
        quantity = pp.aggregate(Sum("quantity"))["quantity__sum"]
 
        out.append(ProductAndQuantity(product, quantity))
 
    for item in pq:
 
        prod = products.get(pk=item["product"])
 
        out.append(ProductAndQuantity(prod, item["quantity"]))
 
    return out
 

	
 

	
 
@register.filter
 
def multiply(value, arg):
 
    ''' Multiplies value by arg '''
 
    return value * arg
registrasion/views.py
Show inline comments
...
 
@@ -70,101 +70,110 @@ def guided_registration(request, page_id=0):
 
    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:
 
            # Set title appropriately for remaining categories
 
            current_step = 3
 
            title = "Additional items"
 

	
 
        all_products = rego.Product.objects.filter(
 
            category__in=cats,
 
        ).select_related("category")
 

	
 
        available_products = set(ProductController.available_products(
 
            request.user,
 
            products=all_products,
 
        ))
 

	
 
        for category in cats:
 
            products = ProductController.available_products(
 
                request.user,
 
                category=category,
 
            )
 
            products = [
 
                i for i in available_products
 
                if i.category == category
 
            ]
 

	
 
            prefix = "category_" + str(category.id)
 
            p = handle_products(request, category, products, prefix)
 
            products_form, discounts, products_handled = p
 

	
 
            section = GuidedRegistrationSection(
 
                title=category.name,
 
                description=category.description,
 
                discounts=discounts,
 
                form=products_form,
 
            )
 
            if products:
 
                # This product category does not exist for this user
 
                sections.append(section)
 

	
 
            if request.method == "POST" and not products_form.errors:
 
                if category.id > attendee.highest_complete_category:
 
                    # This is only saved if we pass each form with no errors.
 
                    attendee.highest_complete_category = category.id
 

	
 
    if sections and request.method == "POST":
 
        for section in sections:
 
            if section.form.errors:
 
                break
 
        else:
 
            attendee.save()
 
            # We've successfully processed everything
 
            return next_step
 

	
 
    data = {
 
        "current_step": current_step,
 
        "sections": sections,
 
        "title": title,
 
        "total_steps": 3,
 
    }
 
    return render(request, "registrasion/guided_registration.html", data)
 

	
 

	
 
@login_required
 
def edit_profile(request):
 
    form, handled = handle_profile(request, "profile")
 

	
 
    if handled and not form.errors:
 
        messages.success(
 
            request,
 
            "Your attendee profile was updated.",
 
        )
 
        return redirect("dashboard")
...
 
@@ -235,141 +244,147 @@ def product_category(request, category_id):
 

	
 
    products = ProductController.available_products(
 
        request.user,
 
        category=category,
 
    )
 

	
 
    if not products:
 
        messages.warning(
 
            request,
 
            "There are no products available from category: " + category.name,
 
        )
 
        return redirect("dashboard")
 

	
 
    p = handle_products(request, category, products, PRODUCTS_FORM_PREFIX)
 
    products_form, discounts, products_handled = p
 

	
 
    if request.POST and not voucher_handled and not products_form.errors:
 
        # Only return to the dashboard if we didn't add a voucher code
 
        # and if there's no errors in the products form
 
        messages.success(
 
            request,
 
            "Your reservations have been updated.",
 
        )
 
        return redirect("dashboard")
 

	
 
    data = {
 
        "category": category,
 
        "discounts": discounts,
 
        "form": products_form,
 
        "voucher_form": voucher_form,
 
    }
 

	
 
    return render(request, "registrasion/product_category.html", data)
 

	
 

	
 
def handle_products(request, category, products, prefix):
 
    ''' Handles a products list form in the given request. Returns the
 
    form instance, the discounts applicable to this form, and whether the
 
    contents were handled. '''
 

	
 
    current_cart = CartController.for_user(request.user)
 

	
 
    ProductsForm = forms.ProductsForm(category, products)
 

	
 
    # Create initial data for each of products in category
 
    items = rego.ProductItem.objects.filter(
 
        product__in=products,
 
        cart=current_cart.cart,
 
    )
 
    ).select_related("product")
 
    quantities = []
 
    for product in products:
 
        # Only add items that are enabled.
 
        try:
 
            quantity = items.get(product=product).quantity
 
        except ObjectDoesNotExist:
 
            quantity = 0
 
        quantities.append((product, quantity))
 
    seen = set()
 

	
 
    for item in items:
 
        quantities.append((item.product, item.quantity))
 
        seen.add(item.product)
 

	
 
    zeros = set(products) - seen
 
    for product in zeros:
 
        quantities.append((product, 0))
 

	
 
    products_form = ProductsForm(
 
        request.POST or None,
 
        product_quantities=quantities,
 
        prefix=prefix,
 
    )
 

	
 
    if request.method == "POST" and products_form.is_valid():
 
        if products_form.has_changed():
 
            set_quantities_from_products_form(products_form, current_cart)
 

	
 
        # If category is required, the user must have at least one
 
        # in an active+valid cart
 
        if category.required:
 
            carts = rego.Cart.objects.filter(user=request.user)
 
            items = rego.ProductItem.objects.filter(
 
                product__category=category,
 
                cart=carts,
 
            )
 
            if len(items) == 0:
 
                products_form.add_error(
 
                    None,
 
                    "You must have at least one item from this category",
 
                )
 
    handled = False if products_form.errors else True
 

	
 
    discounts = discount.available_discounts(request.user, [], products)
 

	
 
    return products_form, discounts, handled
 

	
 

	
 
def set_quantities_from_products_form(products_form, current_cart):
 

	
 
    quantities = list(products_form.product_quantities())
 

	
 
    pks = [i[0] for i in quantities]
 
    products = rego.Product.objects.filter(id__in=pks).select_related("category")
 

	
 
    product_quantities = [
 
        (rego.Product.objects.get(pk=i[0]), i[1]) for i in quantities
 
        (products.get(pk=i[0]), i[1]) for i in quantities
 
    ]
 
    field_names = dict(
 
        (i[0][0], i[1][2]) for i in zip(product_quantities, quantities)
 
    )
 

	
 
    try:
 
        current_cart.set_quantities(product_quantities)
 
    except CartValidationError as ve:
 
        for ve_field in ve.error_list:
 
            product, message = ve_field.message
 
            if product in field_names:
 
                field = field_names[product]
 
            elif isinstance(product, rego.Product):
 
                continue
 
            else:
 
                field = None
 
            products_form.add_error(field, message)
 

	
 

	
 
def handle_voucher(request, prefix):
 
    ''' Handles a voucher form in the given request. Returns the voucher
 
    form instance, and whether the voucher code was handled. '''
 

	
 
    voucher_form = forms.VoucherForm(request.POST or None, prefix=prefix)
 
    current_cart = CartController.for_user(request.user)
 

	
 
    if (voucher_form.is_valid() and
 
            voucher_form.cleaned_data["voucher"].strip()):
 

	
 
        voucher = voucher_form.cleaned_data["voucher"]
 
        voucher = rego.Voucher.normalise_code(voucher)
 

	
 
        if len(current_cart.cart.vouchers.filter(code=voucher)) > 0:
 
            # This voucher has already been applied to this cart.
 
            # Do not apply code
 
            handled = False
 
        else:
 
            try:
 
                current_cart.apply_voucher(voucher)
 
            except Exception as e:
 
                voucher_form.add_error("voucher", e)
 
            handled = True
 
    else:
 
        handled = False
 

	
 
    return (voucher_form, handled)
 

	
 

	
0 comments (0 inline, 0 general)