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…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer