Merge branch 'category_user_limits'
This commit is contained in:
commit
77ab00bc67
9 changed files with 426 additions and 185 deletions
|
@ -125,18 +125,25 @@ class CartController(object):
|
||||||
def apply_voucher(self, voucher_code):
|
def apply_voucher(self, voucher_code):
|
||||||
''' Applies the voucher with the given code to this cart. '''
|
''' 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?
|
# Is voucher exhausted?
|
||||||
active_carts = rego.Cart.reserved_carts()
|
active_carts = rego.Cart.reserved_carts()
|
||||||
|
|
||||||
# Try and find the voucher
|
# Try and find the voucher
|
||||||
voucher = rego.Voucher.objects.get(code=voucher_code.upper())
|
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)
|
carts_with_voucher = active_carts.filter(vouchers=voucher)
|
||||||
if len(carts_with_voucher) >= voucher.limit:
|
if len(carts_with_voucher) >= voucher.limit:
|
||||||
raise ValidationError("This voucher is no longer available")
|
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...
|
# If successful...
|
||||||
self.cart.vouchers.add(voucher)
|
self.cart.vouchers.add(voucher)
|
||||||
self.end_batch()
|
self.end_batch()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.db.models import Sum
|
||||||
from registrasion import models as rego
|
from registrasion import models as rego
|
||||||
|
|
||||||
from conditions import ConditionController
|
from conditions import ConditionController
|
||||||
|
@ -28,11 +29,13 @@ class ProductController(object):
|
||||||
if products is not None:
|
if products is not None:
|
||||||
all_products = itertools.chain(all_products, products)
|
all_products = itertools.chain(all_products, products)
|
||||||
|
|
||||||
return [
|
out = [
|
||||||
product
|
product
|
||||||
for product in all_products
|
for product in all_products
|
||||||
if cls(product).can_add_with_enabling_conditions(user, 0)
|
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):
|
def user_can_add_within_limit(self, user, quantity):
|
||||||
''' Return true if the user is able to add _quantity_ to their count of
|
''' 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)
|
carts = rego.Cart.objects.filter(user=user)
|
||||||
items = rego.ProductItem.objects.filter(
|
items = rego.ProductItem.objects.filter(
|
||||||
product=self.product,
|
cart=carts,
|
||||||
cart=carts)
|
)
|
||||||
|
|
||||||
count = 0
|
prod_items = items.filter(product=self.product)
|
||||||
for item in items:
|
cat_items = items.filter(product__category=self.product.category)
|
||||||
count += item.quantity
|
|
||||||
|
|
||||||
if quantity + count > self.product.limit_per_user:
|
prod_count = prod_items.aggregate(Sum("quantity"))["quantity__sum"]
|
||||||
return False
|
cat_count = cat_items.aggregate(Sum("quantity"))["quantity__sum"]
|
||||||
else:
|
|
||||||
|
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
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def can_add_with_enabling_conditions(self, user, quantity):
|
def can_add_with_enabling_conditions(self, user, quantity):
|
||||||
''' Returns true if the user is able to add _quantity_ to their count
|
''' Returns true if the user is able to add _quantity_ to their count
|
||||||
|
|
|
@ -3,51 +3,133 @@ import models as rego
|
||||||
from django import forms
|
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):
|
PRODUCT_PREFIX = "product_"
|
||||||
return PREFIX + ("%d" % product.id)
|
|
||||||
|
|
||||||
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):
|
@classmethod
|
||||||
if "product_quantities" in k:
|
def field_name(cls, product):
|
||||||
initial = _ProductsForm.initial_data(k["product_quantities"])
|
return cls.PRODUCT_PREFIX + ("%d" % product.id)
|
||||||
k["initial"] = initial
|
|
||||||
del k["product_quantities"]
|
|
||||||
super(_ProductsForm, self).__init__(*a, **k)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def initial_data(cls, product_quantities):
|
def set_fields(cls, category, products):
|
||||||
''' Prepares initial data for an instance of this form.
|
''' Sets the base_fields on this _ProductsForm to allow selecting
|
||||||
product_quantities is a sequence of (product,quantity) tuples '''
|
from the provided products. '''
|
||||||
initial = {}
|
pass
|
||||||
for product, quantity in product_quantities:
|
|
||||||
initial[field_name(product)] = quantity
|
|
||||||
|
|
||||||
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):
|
def product_quantities(self):
|
||||||
''' Yields a sequence of (product, quantity) tuples from the
|
''' Yields a sequence of (product, quantity) tuples from the
|
||||||
cleaned form data. '''
|
cleaned form data. '''
|
||||||
for name, value in self.cleaned_data.items():
|
return iter([])
|
||||||
if name.startswith(PREFIX):
|
|
||||||
product_id = int(name[len(PREFIX):])
|
|
||||||
yield (product_id, value, name)
|
|
||||||
|
|
||||||
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(
|
@classmethod
|
||||||
label=product.name,
|
def set_fields(cls, category, products):
|
||||||
help_text=help_text,
|
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):
|
class ProfileForm(forms.ModelForm):
|
||||||
|
|
24
registrasion/migrations/0007_auto_20160326_2105.py
Normal file
24
registrasion/migrations/0007_auto_20160326_2105.py
Normal file
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -132,6 +132,10 @@ class Category(models.Model):
|
||||||
name = models.CharField(max_length=65, verbose_name=_("Name"))
|
name = models.CharField(max_length=65, verbose_name=_("Name"))
|
||||||
description = models.CharField(max_length=255,
|
description = models.CharField(max_length=255,
|
||||||
verbose_name=_("Description"))
|
verbose_name=_("Description"))
|
||||||
|
limit_per_user = models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Limit per user"))
|
||||||
required = models.BooleanField(blank=True)
|
required = models.BooleanField(blank=True)
|
||||||
order = models.PositiveIntegerField(verbose_name=("Display order"))
|
order = models.PositiveIntegerField(verbose_name=("Display order"))
|
||||||
render_type = models.IntegerField(choices=CATEGORY_RENDER_TYPES,
|
render_type = models.IntegerField(choices=CATEGORY_RENDER_TYPES,
|
||||||
|
@ -153,6 +157,7 @@ class Product(models.Model):
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
verbose_name=_("Price"))
|
verbose_name=_("Price"))
|
||||||
limit_per_user = models.PositiveIntegerField(
|
limit_per_user = models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Limit per user"))
|
verbose_name=_("Limit per user"))
|
||||||
reservation_duration = models.DurationField(
|
reservation_duration = models.DurationField(
|
||||||
|
@ -172,9 +177,13 @@ class Voucher(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Voucher for %s" % self.recipient
|
return "Voucher for %s" % self.recipient
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def normalise_code(cls, code):
|
||||||
|
return code.upper()
|
||||||
|
|
||||||
def save(self, *a, **k):
|
def save(self, *a, **k):
|
||||||
''' Normalise the voucher code to be uppercase '''
|
''' 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)
|
super(Voucher, self).save(*a, **k)
|
||||||
|
|
||||||
recipient = models.CharField(max_length=64, verbose_name=_("Recipient"))
|
recipient = models.CharField(max_length=64, verbose_name=_("Recipient"))
|
||||||
|
|
|
@ -8,16 +8,19 @@ register = template.Library()
|
||||||
|
|
||||||
ProductAndQuantity = namedtuple("ProductAndQuantity", ["product", "quantity"])
|
ProductAndQuantity = namedtuple("ProductAndQuantity", ["product", "quantity"])
|
||||||
|
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def available_categories(context):
|
def available_categories(context):
|
||||||
''' Returns all of the available product categories '''
|
''' Returns all of the available product categories '''
|
||||||
return rego.Category.objects.all()
|
return rego.Category.objects.all()
|
||||||
|
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def invoices(context):
|
def invoices(context):
|
||||||
''' Returns all of the invoices that this user has. '''
|
''' Returns all of the invoices that this user has. '''
|
||||||
return rego.Invoice.objects.filter(cart__user=context.request.user)
|
return rego.Invoice.objects.filter(cart__user=context.request.user)
|
||||||
|
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def items_pending(context):
|
def items_pending(context):
|
||||||
''' Returns all of the items that this user has in their current cart,
|
''' Returns all of the items that this user has in their current cart,
|
||||||
|
@ -29,6 +32,7 @@ def items_pending(context):
|
||||||
)
|
)
|
||||||
return all_items
|
return all_items
|
||||||
|
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def items_purchased(context):
|
def items_purchased(context):
|
||||||
''' Returns all of the items that this user has purchased '''
|
''' Returns all of the items that this user has purchased '''
|
||||||
|
|
|
@ -32,69 +32,43 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
|
||||||
email='test2@example.com',
|
email='test2@example.com',
|
||||||
password='top_secret')
|
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.RESERVATION = datetime.timedelta(hours=1)
|
||||||
|
|
||||||
cls.PROD_1 = rego.Product.objects.create(
|
cls.categories = []
|
||||||
name="Product 1",
|
for i in xrange(2):
|
||||||
description="This is a test product. It costs $10. "
|
cat = rego.Category.objects.create(
|
||||||
"A user may have 10 of them.",
|
name="Category " + str(i + 1),
|
||||||
category=cls.CAT_1,
|
description="This is a test category",
|
||||||
price=Decimal("10.00"),
|
order=i,
|
||||||
reservation_duration=cls.RESERVATION,
|
render_type=rego.Category.RENDER_TYPE_RADIO,
|
||||||
limit_per_user=10,
|
required=False,
|
||||||
order=10,
|
)
|
||||||
)
|
cat.save()
|
||||||
cls.PROD_1.save()
|
cls.categories.append(cat)
|
||||||
|
|
||||||
cls.PROD_2 = rego.Product.objects.create(
|
cls.CAT_1 = cls.categories[0]
|
||||||
name="Product 2",
|
cls.CAT_2 = cls.categories[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"),
|
|
||||||
limit_per_user=10,
|
|
||||||
order=10,
|
|
||||||
)
|
|
||||||
cls.PROD_2.save()
|
|
||||||
|
|
||||||
cls.PROD_3 = rego.Product.objects.create(
|
cls.products = []
|
||||||
name="Product 3",
|
for i in xrange(4):
|
||||||
description="This is a test product. It costs $10. "
|
prod = rego.Product.objects.create(
|
||||||
"A user may have 10 of them.",
|
name="Product 1",
|
||||||
category=cls.CAT_2,
|
description="This is a test product.",
|
||||||
price=Decimal("10.00"),
|
category=cls.categories[i / 2], # 2 products per category
|
||||||
limit_per_user=10,
|
price=Decimal("10.00"),
|
||||||
order=10,
|
reservation_duration=cls.RESERVATION,
|
||||||
)
|
limit_per_user=10,
|
||||||
cls.PROD_3.save()
|
order=1,
|
||||||
|
)
|
||||||
|
prod.save()
|
||||||
|
cls.products.append(prod)
|
||||||
|
|
||||||
cls.PROD_4 = rego.Product.objects.create(
|
cls.PROD_1 = cls.products[0]
|
||||||
name="Product 4",
|
cls.PROD_2 = cls.products[1]
|
||||||
description="This is a test product. It costs $5. "
|
cls.PROD_3 = cls.products[2]
|
||||||
"A user may have 10 of them.",
|
cls.PROD_4 = cls.products[3]
|
||||||
category=cls.CAT_2,
|
|
||||||
price=Decimal("5.00"),
|
cls.PROD_4.price = Decimal("5.00")
|
||||||
limit_per_user=10,
|
|
||||||
order=10,
|
|
||||||
)
|
|
||||||
cls.PROD_4.save()
|
cls.PROD_4.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -205,7 +179,7 @@ class BasicCartTests(RegistrationCartTestCase):
|
||||||
current_cart.set_quantity(self.PROD_1, 2)
|
current_cart.set_quantity(self.PROD_1, 2)
|
||||||
self.assertEqual(2, get_item().quantity)
|
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)
|
current_cart = CartController.for_user(self.USER_1)
|
||||||
|
|
||||||
# User should be able to add 1 of PROD_1 to the current cart.
|
# 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 should not be affected by first user's limits
|
||||||
second_user_cart = CartController.for_user(self.USER_2)
|
second_user_cart = CartController.for_user(self.USER_2)
|
||||||
second_user_cart.add_to_cart(self.PROD_1, 10)
|
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)
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.db import IntegrityError
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion import models as rego
|
||||||
from registrasion.controllers.cart import CartController
|
from registrasion.controllers.cart import CartController
|
||||||
|
from registrasion.controllers.invoice import InvoiceController
|
||||||
|
|
||||||
from test_cart import RegistrationCartTestCase
|
from test_cart import RegistrationCartTestCase
|
||||||
|
|
||||||
|
@ -16,11 +17,11 @@ UTC = pytz.timezone('UTC')
|
||||||
class VoucherTestCases(RegistrationCartTestCase):
|
class VoucherTestCases(RegistrationCartTestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def new_voucher(self, code="VOUCHER"):
|
def new_voucher(self, code="VOUCHER", limit=1):
|
||||||
voucher = rego.Voucher.objects.create(
|
voucher = rego.Voucher.objects.create(
|
||||||
recipient="Voucher recipient",
|
recipient="Voucher recipient",
|
||||||
code=code,
|
code=code,
|
||||||
limit=1
|
limit=limit,
|
||||||
)
|
)
|
||||||
voucher.save()
|
voucher.save()
|
||||||
return voucher
|
return voucher
|
||||||
|
@ -107,3 +108,21 @@ class VoucherTestCases(RegistrationCartTestCase):
|
||||||
voucher = self.new_voucher(code="VOUCHeR")
|
voucher = self.new_voucher(code="VOUCHeR")
|
||||||
current_cart = CartController.for_user(self.USER_1)
|
current_cart = CartController.for_user(self.USER_1)
|
||||||
current_cart.apply_voucher(voucher.code.lower())
|
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)
|
||||||
|
|
|
@ -93,113 +93,141 @@ def product_category(request, category_id):
|
||||||
PRODUCTS_FORM_PREFIX = "products"
|
PRODUCTS_FORM_PREFIX = "products"
|
||||||
VOUCHERS_FORM_PREFIX = "vouchers"
|
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_id = int(category_id) # Routing is [0-9]+
|
||||||
category = rego.Category.objects.get(pk=category_id)
|
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(
|
products = ProductController.available_products(
|
||||||
request.user,
|
request.user,
|
||||||
products=products,
|
category=category,
|
||||||
)
|
)
|
||||||
ProductsForm = forms.ProductsForm(products)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
p = handle_products(request, category, products, PRODUCTS_FORM_PREFIX)
|
||||||
cat_form = ProductsForm(
|
products_form, discounts, products_handled = p
|
||||||
request.POST,
|
|
||||||
request.FILES,
|
|
||||||
prefix=PRODUCTS_FORM_PREFIX)
|
|
||||||
voucher_form = forms.VoucherForm(
|
|
||||||
request.POST,
|
|
||||||
prefix=VOUCHERS_FORM_PREFIX)
|
|
||||||
|
|
||||||
if (voucher_form.is_valid() and
|
if request.POST and not voucher_handled and not products_form.errors:
|
||||||
voucher_form.cleaned_data["voucher"].strip()):
|
# Only return to the dashboard if we didn't add a voucher code
|
||||||
# Apply voucher
|
# and if there's no errors in the products form
|
||||||
# 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 category is required, the user must have at least one
|
attendee = rego.Attendee.get_instance(request.user)
|
||||||
# in an active+valid cart
|
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 = {
|
data = {
|
||||||
"category": category,
|
"category": category,
|
||||||
"discounts": discounts,
|
"discounts": discounts,
|
||||||
"form": cat_form,
|
"form": products_form,
|
||||||
"voucher_form": voucher_form,
|
"voucher_form": voucher_form,
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, "product_category.html", data)
|
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
|
@transaction.atomic
|
||||||
def handle_valid_cat_form(cat_form, current_cart):
|
def set_quantities_from_products_form(products_form, current_cart):
|
||||||
for product_id, quantity, field_name in cat_form.product_quantities():
|
for product_id, quantity, field_name in products_form.product_quantities():
|
||||||
product = rego.Product.objects.get(pk=product_id)
|
product = rego.Product.objects.get(pk=product_id)
|
||||||
try:
|
try:
|
||||||
current_cart.set_quantity(product, quantity, batched=True)
|
current_cart.set_quantity(product, quantity, batched=True)
|
||||||
except ValidationError as ve:
|
except ValidationError as ve:
|
||||||
cat_form.add_error(field_name, ve)
|
products_form.add_error(field_name, ve)
|
||||||
if cat_form.errors:
|
if products_form.errors:
|
||||||
raise ValidationError("Cannot add that stuff")
|
raise ValidationError("Cannot add that stuff")
|
||||||
current_cart.end_batch()
|
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
|
@login_required
|
||||||
def checkout(request):
|
def checkout(request):
|
||||||
''' Runs checkout for the current cart of items, ideally generating an
|
''' Runs checkout for the current cart of items, ideally generating an
|
||||||
|
|
Loading…
Reference in a new issue