diff --git a/registrasion/controllers/cart.py b/registrasion/controllers/cart.py index ad680225..06eef3ef 100644 --- a/registrasion/controllers/cart.py +++ b/registrasion/controllers/cart.py @@ -125,18 +125,25 @@ class CartController(object): def apply_voucher(self, voucher_code): ''' Applies the voucher with the given code to this cart. ''' - # TODO: is it valid for a cart to re-add a voucher that they have? - # 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, + 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() diff --git a/registrasion/controllers/product.py b/registrasion/controllers/product.py index 2d0f2963..88beb8f8 100644 --- a/registrasion/controllers/product.py +++ b/registrasion/controllers/product.py @@ -1,6 +1,7 @@ import itertools from django.db.models import Q +from django.db.models import Sum from registrasion import models as rego from conditions import ConditionController @@ -28,11 +29,13 @@ class ProductController(object): if products is not None: all_products = itertools.chain(all_products, products) - return [ + out = [ product for product in all_products if cls(product).can_add_with_enabling_conditions(user, 0) ] + out.sort(key=lambda product: product.order) + return out def user_can_add_within_limit(self, user, quantity): ''' Return true if the user is able to add _quantity_ to their count of @@ -40,17 +43,25 @@ class ProductController(object): carts = rego.Cart.objects.filter(user=user) items = rego.ProductItem.objects.filter( - product=self.product, - cart=carts) + cart=carts, + ) - count = 0 - for item in items: - count += item.quantity + prod_items = items.filter(product=self.product) + cat_items = items.filter(product__category=self.product.category) - if quantity + count > self.product.limit_per_user: - return False - else: + prod_count = prod_items.aggregate(Sum("quantity"))["quantity__sum"] + cat_count = cat_items.aggregate(Sum("quantity"))["quantity__sum"] + + prod_limit = self.product.limit_per_user + prod_met = prod_limit is None or quantity + prod_count <= prod_limit + + cat_limit = self.product.category.limit_per_user + cat_met = cat_limit is None or quantity + cat_count <= cat_limit + + if prod_met and cat_met: return True + else: + return False def can_add_with_enabling_conditions(self, user, quantity): ''' Returns true if the user is able to add _quantity_ to their count diff --git a/registrasion/forms.py b/registrasion/forms.py index fd0359bb..eb6f2949 100644 --- a/registrasion/forms.py +++ b/registrasion/forms.py @@ -3,51 +3,133 @@ import models as rego from django import forms -def ProductsForm(products): +# Products forms -- none of these have any fields: they are to be subclassed +# and the fields added as needs be. - PREFIX = "product_" +class _ProductsForm(forms.Form): - def field_name(product): - return PREFIX + ("%d" % product.id) + PRODUCT_PREFIX = "product_" - class _ProductsForm(forms.Form): + ''' 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) - def __init__(self, *a, **k): - if "product_quantities" in k: - initial = _ProductsForm.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 initial_data(cls, product_quantities): - ''' Prepares initial data for an instance of this form. - product_quantities is a sequence of (product,quantity) tuples ''' - initial = {} - for product, quantity in product_quantities: - initial[field_name(product)] = quantity + @classmethod + def set_fields(cls, category, products): + ''' Sets the base_fields on this _ProductsForm to allow selecting + from the provided products. ''' + pass - return initial + @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. ''' - for name, value in self.cleaned_data.items(): - if name.startswith(PREFIX): - product_id = int(name[len(PREFIX):]) - yield (product_id, value, name) + def product_quantities(self): + ''' Yields a sequence of (product, quantity) tuples from the + cleaned form data. ''' + return iter([]) - for product in products: - help_text = "$%d -- %s" % (product.price, product.description) +class _QuantityBoxProductsForm(_ProductsForm): + ''' Products entry form that allows users to enter quantities + of desired products. ''' - field = forms.IntegerField( - label=product.name, - help_text=help_text, + @classmethod + def set_fields(cls, category, products): + for product in products: + help_text = "$%d -- %s" % (product.price, product.description) + + field = forms.IntegerField( + label=product.name, + help_text=help_text, + ) + 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, ) - _ProductsForm.base_fields[field_name(product)] = field - return _ProductsForm + @classmethod + def initial_data(cls, product_quantities): + initial = {} + + for product, quantity in product_quantities: + if quantity > 0: + initial[cls.FIELD] = product.id + break + + return initial + + def product_quantities(self): + ours = self.cleaned_data[self.FIELD] + choices = self.fields[self.FIELD].choices + for choice_value, choice_display in choices: + yield ( + choice_value, + 1 if ours == choice_value else 0, + self.FIELD, + ) + + +def ProductsForm(category, products): + ''' Produces an appropriate _ProductsForm subclass for the given render + type. ''' + + # Each Category.RENDER_TYPE value has a subclass here. + RENDER_TYPES = { + rego.Category.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm, + rego.Category.RENDER_TYPE_RADIO: _RadioButtonProductsForm, + } + + # Produce a subclass of _ProductsForm which we can alter the base_fields on + class ProductsForm(RENDER_TYPES[category.render_type]): + pass + + ProductsForm.set_fields(category, products) + return ProductsForm class ProfileForm(forms.ModelForm): diff --git a/registrasion/migrations/0007_auto_20160326_2105.py b/registrasion/migrations/0007_auto_20160326_2105.py new file mode 100644 index 00000000..dbcf2ac7 --- /dev/null +++ b/registrasion/migrations/0007_auto_20160326_2105.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasion', '0006_category_required'), + ] + + operations = [ + migrations.AddField( + model_name='category', + name='limit_per_user', + field=models.PositiveIntegerField(null=True, verbose_name='Limit per user', blank=True), + ), + migrations.AlterField( + model_name='product', + name='limit_per_user', + field=models.PositiveIntegerField(null=True, verbose_name='Limit per user', blank=True), + ), + ] diff --git a/registrasion/models.py b/registrasion/models.py index a1101fd9..c95e1740 100644 --- a/registrasion/models.py +++ b/registrasion/models.py @@ -132,6 +132,10 @@ class Category(models.Model): 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")) required = models.BooleanField(blank=True) order = models.PositiveIntegerField(verbose_name=("Display order")) render_type = models.IntegerField(choices=CATEGORY_RENDER_TYPES, @@ -153,6 +157,7 @@ class Product(models.Model): decimal_places=2, verbose_name=_("Price")) limit_per_user = models.PositiveIntegerField( + null=True, blank=True, verbose_name=_("Limit per user")) reservation_duration = models.DurationField( @@ -172,9 +177,13 @@ class Voucher(models.Model): 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.code.upper() + self.code = self.normalise_code(self.code) super(Voucher, self).save(*a, **k) recipient = models.CharField(max_length=64, verbose_name=_("Recipient")) diff --git a/registrasion/templatetags/registrasion_tags.py b/registrasion/templatetags/registrasion_tags.py index a6741c0e..a583f1ea 100644 --- a/registrasion/templatetags/registrasion_tags.py +++ b/registrasion/templatetags/registrasion_tags.py @@ -8,16 +8,19 @@ 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 rego.Category.objects.all() + @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, @@ -29,6 +32,7 @@ def items_pending(context): ) return all_items + @register.assignment_tag(takes_context=True) def items_purchased(context): ''' Returns all of the items that this user has purchased ''' diff --git a/registrasion/tests/test_cart.py b/registrasion/tests/test_cart.py index d000b946..5989f6e2 100644 --- a/registrasion/tests/test_cart.py +++ b/registrasion/tests/test_cart.py @@ -32,69 +32,43 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase): email='test2@example.com', password='top_secret') - cls.CAT_1 = rego.Category.objects.create( - name="Category 1", - description="This is a test category", - order=10, - render_type=rego.Category.RENDER_TYPE_RADIO, - required=False, - ) - cls.CAT_1.save() - - cls.CAT_2 = rego.Category.objects.create( - name="Category 2", - description="This is a test category", - order=10, - render_type=rego.Category.RENDER_TYPE_RADIO, - required=False, - ) - cls.CAT_2.save() - cls.RESERVATION = datetime.timedelta(hours=1) - cls.PROD_1 = rego.Product.objects.create( - name="Product 1", - description="This is a test product. It costs $10. " - "A user may have 10 of them.", - category=cls.CAT_1, - price=Decimal("10.00"), - reservation_duration=cls.RESERVATION, - limit_per_user=10, - order=10, - ) - cls.PROD_1.save() + 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.PROD_2 = rego.Product.objects.create( - name="Product 2", - description="This is a test product. It costs $10. " - "A user may have 10 of them.", - category=cls.CAT_1, - price=Decimal("10.00"), - limit_per_user=10, - order=10, - ) - cls.PROD_2.save() + cls.CAT_1 = cls.categories[0] + cls.CAT_2 = cls.categories[1] - cls.PROD_3 = rego.Product.objects.create( - name="Product 3", - description="This is a test product. It costs $10. " - "A user may have 10 of them.", - category=cls.CAT_2, - price=Decimal("10.00"), - limit_per_user=10, - order=10, - ) - cls.PROD_3.save() + cls.products = [] + for i in xrange(4): + prod = rego.Product.objects.create( + name="Product 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_4 = rego.Product.objects.create( - name="Product 4", - description="This is a test product. It costs $5. " - "A user may have 10 of them.", - category=cls.CAT_2, - price=Decimal("5.00"), - limit_per_user=10, - order=10, - ) + 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() @classmethod @@ -205,7 +179,7 @@ class BasicCartTests(RegistrationCartTestCase): current_cart.set_quantity(self.PROD_1, 2) self.assertEqual(2, get_item().quantity) - def test_add_to_cart_per_user_limit(self): + def test_add_to_cart_product_per_user_limit(self): current_cart = CartController.for_user(self.USER_1) # User should be able to add 1 of PROD_1 to the current cart. @@ -231,3 +205,86 @@ class BasicCartTests(RegistrationCartTestCase): # Second user should not be affected by first user's limits second_user_cart = CartController.for_user(self.USER_2) second_user_cart.add_to_cart(self.PROD_1, 10) + + def set_limits(self): + self.CAT_2.limit_per_user = 10 + self.PROD_2.limit_per_user = None + self.PROD_3.limit_per_user = None + self.PROD_4.limit_per_user = 6 + + self.CAT_2.save() + self.PROD_2.save() + self.PROD_3.save() + self.PROD_4.save() + + def test_per_user_product_limit_ignored_if_blank(self): + self.set_limits() + + current_cart = CartController.for_user(self.USER_1) + # There is no product limit on PROD_2, and there is no cat limit + current_cart.add_to_cart(self.PROD_2, 1) + # There is no product limit on PROD_3, but there is a cat limit + current_cart.add_to_cart(self.PROD_3, 1) + + def test_per_user_category_limit_ignored_if_blank(self): + self.set_limits() + current_cart = CartController.for_user(self.USER_1) + # There is no product limit on PROD_2, and there is no cat limit + current_cart.add_to_cart(self.PROD_2, 1) + # There is no cat limit on PROD_1, but there is a prod limit + current_cart.add_to_cart(self.PROD_1, 1) + + def test_per_user_category_limit_only(self): + self.set_limits() + + current_cart = CartController.for_user(self.USER_1) + + # Cannot add to cart if category limit is filled by one product. + current_cart.set_quantity(self.PROD_3, 10) + with self.assertRaises(ValidationError): + current_cart.set_quantity(self.PROD_4, 1) + + # Can add to cart if category limit is not filled by one product + current_cart.set_quantity(self.PROD_3, 5) + current_cart.set_quantity(self.PROD_4, 5) + # Cannot add to cart if category limit is filled by two products + with self.assertRaises(ValidationError): + current_cart.add_to_cart(self.PROD_3, 1) + + current_cart.cart.active = False + current_cart.cart.save() + + current_cart = CartController.for_user(self.USER_1) + # The category limit should extend across carts + with self.assertRaises(ValidationError): + current_cart.add_to_cart(self.PROD_3, 10) + + def test_per_user_category_and_product_limits(self): + self.set_limits() + + current_cart = CartController.for_user(self.USER_1) + + # Hit both the product and category edges: + current_cart.set_quantity(self.PROD_3, 4) + current_cart.set_quantity(self.PROD_4, 6) + with self.assertRaises(ValidationError): + # There's unlimited PROD_3, but limited in the category + current_cart.add_to_cart(self.PROD_3, 1) + + current_cart.set_quantity(self.PROD_3, 0) + with self.assertRaises(ValidationError): + # There's only 6 allowed of PROD_4 + current_cart.add_to_cart(self.PROD_4, 1) + + # The limits should extend across carts... + current_cart.cart.active = False + current_cart.cart.save() + + current_cart = CartController.for_user(self.USER_1) + current_cart.set_quantity(self.PROD_3, 4) + + with self.assertRaises(ValidationError): + current_cart.set_quantity(self.PROD_3, 5) + + with self.assertRaises(ValidationError): + current_cart.set_quantity(self.PROD_4, 1) diff --git a/registrasion/tests/test_voucher.py b/registrasion/tests/test_voucher.py index f0d1be61..db59d094 100644 --- a/registrasion/tests/test_voucher.py +++ b/registrasion/tests/test_voucher.py @@ -7,6 +7,7 @@ from django.db import IntegrityError from registrasion import models as rego from registrasion.controllers.cart import CartController +from registrasion.controllers.invoice import InvoiceController from test_cart import RegistrationCartTestCase @@ -16,11 +17,11 @@ UTC = pytz.timezone('UTC') class VoucherTestCases(RegistrationCartTestCase): @classmethod - def new_voucher(self, code="VOUCHER"): + def new_voucher(self, code="VOUCHER", limit=1): voucher = rego.Voucher.objects.create( recipient="Voucher recipient", code=code, - limit=1 + limit=limit, ) voucher.save() return voucher @@ -107,3 +108,21 @@ class VoucherTestCases(RegistrationCartTestCase): voucher = self.new_voucher(code="VOUCHeR") current_cart = CartController.for_user(self.USER_1) current_cart.apply_voucher(voucher.code.lower()) + + def test_voucher_can_only_be_applied_once(self): + voucher = self.new_voucher(limit=2) + current_cart = CartController.for_user(self.USER_1) + current_cart.apply_voucher(voucher.code) + with self.assertRaises(ValidationError): + current_cart.apply_voucher(voucher.code) + + def test_voucher_can_only_be_applied_once_across_multiple_carts(self): + voucher = self.new_voucher(limit=2) + current_cart = CartController.for_user(self.USER_1) + current_cart.apply_voucher(voucher.code) + + inv = InvoiceController.for_cart(current_cart.cart) + inv.pay("Hello!", inv.invoice.value) + + with self.assertRaises(ValidationError): + current_cart.apply_voucher(voucher.code) diff --git a/registrasion/views.py b/registrasion/views.py index 83e80a0d..2a82db1d 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -93,113 +93,141 @@ def product_category(request, category_id): PRODUCTS_FORM_PREFIX = "products" VOUCHERS_FORM_PREFIX = "vouchers" + # Handle the voucher form *before* listing products. + # Products can change as vouchers are entered. + v = handle_voucher(request, VOUCHERS_FORM_PREFIX) + voucher_form, voucher_handled = v + category_id = int(category_id) # Routing is [0-9]+ category = rego.Category.objects.get(pk=category_id) - current_cart = CartController.for_user(request.user) - attendee = rego.Attendee.get_instance(request.user) - - products = rego.Product.objects.filter(category=category) - products = products.order_by("order") products = ProductController.available_products( request.user, - products=products, + category=category, ) - ProductsForm = forms.ProductsForm(products) - if request.method == "POST": - cat_form = ProductsForm( - request.POST, - request.FILES, - prefix=PRODUCTS_FORM_PREFIX) - voucher_form = forms.VoucherForm( - request.POST, - prefix=VOUCHERS_FORM_PREFIX) + p = handle_products(request, category, products, PRODUCTS_FORM_PREFIX) + products_form, discounts, products_handled = p - if (voucher_form.is_valid() and - voucher_form.cleaned_data["voucher"].strip()): - # Apply voucher - # leave - voucher = voucher_form.cleaned_data["voucher"] - try: - current_cart.apply_voucher(voucher) - except Exception as e: - voucher_form.add_error("voucher", e) - # Re-visit current page. - elif cat_form.is_valid(): - try: - handle_valid_cat_form(cat_form, current_cart) - except ValidationError: - pass + 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 - # If category is required, the user must have at least one - # in an active+valid cart + attendee = rego.Attendee.get_instance(request.user) + if category_id > attendee.highest_complete_category: + attendee.highest_complete_category = category_id + attendee.save() + return redirect("dashboard") - if category.required: - carts = rego.Cart.reserved_carts() - carts = carts.filter(user=request.user) - items = rego.ProductItem.objects.filter( - product__category=category, - cart=carts, - ) - if len(items) == 0: - cat_form.add_error( - None, - "You must have at least one item from this category", - ) - - if not cat_form.errors: - if category_id > attendee.highest_complete_category: - attendee.highest_complete_category = category_id - attendee.save() - return redirect("dashboard") - - else: - # Create initial data for each of products in category - items = rego.ProductItem.objects.filter( - product__category=category, - cart=current_cart.cart, - ) - 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)) - - cat_form = ProductsForm( - prefix=PRODUCTS_FORM_PREFIX, - product_quantities=quantities, - ) - - voucher_form = forms.VoucherForm(prefix=VOUCHERS_FORM_PREFIX) - - discounts = discount.available_discounts(request.user, [], products) data = { "category": category, "discounts": discounts, - "form": cat_form, + "form": products_form, "voucher_form": voucher_form, } return render(request, "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, + ) + 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)) + + products_form = ProductsForm( + request.POST or None, + product_quantities=quantities, + prefix=prefix, + ) + + if request.method == "POST" and products_form.is_valid(): + try: + if products_form.has_changed(): + set_quantities_from_products_form(products_form, current_cart) + except ValidationError: + # There were errors, but they've already been added to the form. + pass + + # 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 + + @transaction.atomic -def handle_valid_cat_form(cat_form, current_cart): - for product_id, quantity, field_name in cat_form.product_quantities(): +def set_quantities_from_products_form(products_form, current_cart): + for product_id, quantity, field_name in products_form.product_quantities(): product = rego.Product.objects.get(pk=product_id) try: current_cart.set_quantity(product, quantity, batched=True) except ValidationError as ve: - cat_form.add_error(field_name, ve) - if cat_form.errors: + products_form.add_error(field_name, ve) + if products_form.errors: raise ValidationError("Cannot add that stuff") current_cart.end_batch() +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) + + @login_required def checkout(request): ''' Runs checkout for the current cart of items, ideally generating an