Adds a formset for dealing with long-and-thin product categories.

This commit is contained in:
Christopher Neugebauer 2016-08-22 13:17:24 +10:00
parent 02e415c104
commit d52fc6eb9d
2 changed files with 104 additions and 18 deletions

View file

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

View file

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