Adds a formset for dealing with long-and-thin product categories.
This commit is contained in:
parent
02e415c104
commit
d52fc6eb9d
2 changed files with 104 additions and 18 deletions
|
@ -2,6 +2,7 @@ from registrasion.models import commerce
|
||||||
from registrasion.models import inventory
|
from registrasion.models import inventory
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class ApplyCreditNoteForm(forms.Form):
|
class ApplyCreditNoteForm(forms.Form):
|
||||||
|
@ -64,6 +65,13 @@ def ProductsForm(category, products):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
ProductsForm.set_fields(category, products)
|
ProductsForm.set_fields(category, products)
|
||||||
|
|
||||||
|
if category.render_type == inventory.Category.RENDER_TYPE_ITEM_QUANTITY:
|
||||||
|
ProductsForm = forms.formset_factory(
|
||||||
|
ProductsForm,
|
||||||
|
formset=_ItemQuantityProductsFormSet,
|
||||||
|
)
|
||||||
|
|
||||||
return ProductsForm
|
return ProductsForm
|
||||||
|
|
||||||
|
|
||||||
|
@ -100,6 +108,18 @@ class _HasProductsFields(object):
|
||||||
cleaned form data. '''
|
cleaned form data. '''
|
||||||
return iter([])
|
return iter([])
|
||||||
|
|
||||||
|
def add_product_error(self, product, error):
|
||||||
|
''' Adds an error to the given product's field '''
|
||||||
|
|
||||||
|
''' if product in field_names:
|
||||||
|
field = field_names[product]
|
||||||
|
elif isinstance(product, inventory.Product):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
field = None '''
|
||||||
|
|
||||||
|
self.add_error(self.field_name(product), error)
|
||||||
|
|
||||||
|
|
||||||
class _ProductsForm(_HasProductsFields, forms.Form):
|
class _ProductsForm(_HasProductsFields, forms.Form):
|
||||||
pass
|
pass
|
||||||
|
@ -140,7 +160,7 @@ class _QuantityBoxProductsForm(_ProductsForm):
|
||||||
for name, value in self.cleaned_data.items():
|
for name, value in self.cleaned_data.items():
|
||||||
if name.startswith(self.PRODUCT_PREFIX):
|
if name.startswith(self.PRODUCT_PREFIX):
|
||||||
product_id = int(name[len(self.PRODUCT_PREFIX):])
|
product_id = int(name[len(self.PRODUCT_PREFIX):])
|
||||||
yield (product_id, value, name)
|
yield (product_id, value)
|
||||||
|
|
||||||
|
|
||||||
class _RadioButtonProductsForm(_ProductsForm):
|
class _RadioButtonProductsForm(_ProductsForm):
|
||||||
|
@ -190,10 +210,14 @@ class _RadioButtonProductsForm(_ProductsForm):
|
||||||
self.FIELD,
|
self.FIELD,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def add_product_error(self, product, error):
|
||||||
|
self.add_error(cls.FIELD, error)
|
||||||
|
|
||||||
class _ItemQuantityProductsForm(_ProductsForm):
|
class _ItemQuantityProductsForm(_ProductsForm):
|
||||||
''' Products entry form that allows users to select a product type, and
|
''' Products entry form that allows users to select a product type, and
|
||||||
enter a quantity of that product. This version _only_ allows a specific
|
enter a quantity of that product. This version _only_ allows a single
|
||||||
product type to be purchased.'''
|
product type to be purchased. This form is usually used in concert with the
|
||||||
|
_ItemQuantityProductsFormSet to allow selection of multiple products.'''
|
||||||
|
|
||||||
CHOICE_FIELD = "choice"
|
CHOICE_FIELD = "choice"
|
||||||
QUANTITY_FIELD = "quantity"
|
QUANTITY_FIELD = "quantity"
|
||||||
|
@ -201,17 +225,19 @@ class _ItemQuantityProductsForm(_ProductsForm):
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_fields(cls, category, products):
|
def set_fields(cls, category, products):
|
||||||
choices = []
|
choices = []
|
||||||
|
|
||||||
|
if not category.required:
|
||||||
|
choices.append((0, "---"))
|
||||||
|
|
||||||
for product in products:
|
for product in products:
|
||||||
choice_text = "%s -- $%d each" % (product.name, product.price)
|
choice_text = "%s -- $%d each" % (product.name, product.price)
|
||||||
choices.append((product.id, choice_text))
|
choices.append((product.id, choice_text))
|
||||||
|
|
||||||
if not category.required:
|
|
||||||
choices.append((0, "No selection"))
|
|
||||||
|
|
||||||
cls.base_fields[cls.CHOICE_FIELD] = forms.TypedChoiceField(
|
cls.base_fields[cls.CHOICE_FIELD] = forms.TypedChoiceField(
|
||||||
label=category.name,
|
label=category.name,
|
||||||
widget=forms.Select,
|
widget=forms.Select,
|
||||||
choices=choices,
|
choices=choices,
|
||||||
|
initial=0,
|
||||||
empty_value=0,
|
empty_value=0,
|
||||||
coerce=int,
|
coerce=int,
|
||||||
)
|
)
|
||||||
|
@ -244,9 +270,73 @@ class _ItemQuantityProductsForm(_ProductsForm):
|
||||||
yield (
|
yield (
|
||||||
choice_value,
|
choice_value,
|
||||||
our_quantity if our_choice == choice_value else 0,
|
our_quantity if our_choice == choice_value else 0,
|
||||||
self.CHOICE_FIELD,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def add_product_error(self, product, error):
|
||||||
|
if self.CHOICE_FIELD not in self.cleaned_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
if product.id == self.cleaned_data[self.CHOICE_FIELD]:
|
||||||
|
self.add_error(self.QUANTITY_FIELD, error)
|
||||||
|
|
||||||
|
|
||||||
|
class _ItemQuantityProductsFormSet(_HasProductsFields, forms.BaseFormSet):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_fields(cls, category, products):
|
||||||
|
raise ValueError("set_fields must be called on the underlying Form")
|
||||||
|
|
||||||
|
@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 '''
|
||||||
|
|
||||||
|
f = [
|
||||||
|
{
|
||||||
|
_ItemQuantityProductsForm.CHOICE_FIELD: product.id,
|
||||||
|
_ItemQuantityProductsForm.QUANTITY_FIELD: quantity,
|
||||||
|
}
|
||||||
|
for product, quantity in product_quantities
|
||||||
|
if quantity > 0
|
||||||
|
]
|
||||||
|
return f
|
||||||
|
|
||||||
|
def product_quantities(self):
|
||||||
|
''' Yields a sequence of (product, quantity) tuples from the
|
||||||
|
cleaned form data. '''
|
||||||
|
|
||||||
|
products = set()
|
||||||
|
# Track everything so that we can yield some zeroes
|
||||||
|
all_products = set()
|
||||||
|
|
||||||
|
for form in self:
|
||||||
|
if form.empty_permitted and not form.cleaned_data:
|
||||||
|
# This is the magical empty form at the end of the list.
|
||||||
|
continue
|
||||||
|
|
||||||
|
for product, quantity in form.product_quantities():
|
||||||
|
all_products.add(product)
|
||||||
|
if quantity == 0:
|
||||||
|
continue
|
||||||
|
if product in products:
|
||||||
|
form.add_error(
|
||||||
|
_ItemQuantityProductsForm.CHOICE_FIELD,
|
||||||
|
"You may only choose each product type once.",
|
||||||
|
)
|
||||||
|
form.add_error(
|
||||||
|
_ItemQuantityProductsForm.QUANTITY_FIELD,
|
||||||
|
"You may only choose each product type once.",
|
||||||
|
)
|
||||||
|
products.add(product)
|
||||||
|
yield product, quantity
|
||||||
|
|
||||||
|
for product in (all_products - products):
|
||||||
|
yield product, 0
|
||||||
|
|
||||||
|
def add_product_error(self, product, error):
|
||||||
|
for form in self.forms:
|
||||||
|
form.add_product_error(product, error)
|
||||||
|
|
||||||
|
|
||||||
class VoucherForm(forms.Form):
|
class VoucherForm(forms.Form):
|
||||||
voucher = forms.CharField(
|
voucher = forms.CharField(
|
||||||
|
|
|
@ -446,33 +446,29 @@ def _handle_products(request, category, products, prefix):
|
||||||
|
|
||||||
def _set_quantities_from_products_form(products_form, current_cart):
|
def _set_quantities_from_products_form(products_form, current_cart):
|
||||||
|
|
||||||
|
# Makes id_to_quantity, a dictionary from product ID to its quantity
|
||||||
quantities = list(products_form.product_quantities())
|
quantities = list(products_form.product_quantities())
|
||||||
id_to_quantity = dict(i[:2] for i in quantities)
|
id_to_quantity = dict(quantities)
|
||||||
|
|
||||||
|
# Get the actual product objects
|
||||||
pks = [i[0] for i in quantities]
|
pks = [i[0] for i in quantities]
|
||||||
products = inventory.Product.objects.filter(
|
products = inventory.Product.objects.filter(
|
||||||
id__in=pks,
|
id__in=pks,
|
||||||
).select_related("category").order_by("id")
|
).select_related("category").order_by("id")
|
||||||
|
|
||||||
quantities.sort(key = lambda i: i[0])
|
quantities.sort(key = lambda i: i[0])
|
||||||
|
|
||||||
|
# Match the product objects to their quantities
|
||||||
product_quantities = [
|
product_quantities = [
|
||||||
(product, id_to_quantity[product.id]) for product in products
|
(product, id_to_quantity[product.id]) for product in products
|
||||||
]
|
]
|
||||||
field_names = dict(
|
|
||||||
(i[0][0], i[1][2]) for i in zip(product_quantities, quantities)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_cart.set_quantities(product_quantities)
|
current_cart.set_quantities(product_quantities)
|
||||||
except CartValidationError as ve:
|
except CartValidationError as ve:
|
||||||
for ve_field in ve.error_list:
|
for ve_field in ve.error_list:
|
||||||
product, message = ve_field.message
|
product, message = ve_field.message
|
||||||
if product in field_names:
|
products_form.add_product_error(product, message)
|
||||||
field = field_names[product]
|
|
||||||
elif isinstance(product, inventory.Product):
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
field = None
|
|
||||||
products_form.add_error(field, message)
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_voucher(request, prefix):
|
def _handle_voucher(request, prefix):
|
||||||
|
|
Loading…
Reference in a new issue