commit
						1b40472835
					
				
					 4 changed files with 206 additions and 29 deletions
				
			
		|  | @ -2,6 +2,7 @@ from registrasion.models import commerce | |||
| from registrasion.models import inventory | ||||
| 
 | ||||
| from django import forms | ||||
| from django.core.exceptions import ValidationError | ||||
| 
 | ||||
| 
 | ||||
| class ApplyCreditNoteForm(forms.Form): | ||||
|  | @ -45,9 +46,36 @@ class ManualPaymentForm(forms.ModelForm): | |||
| 
 | ||||
| 
 | ||||
| # Products forms -- none of these have any fields: they are to be subclassed | ||||
| # and the fields added as needs be. | ||||
| # and the fields added as needs be. ProductsForm (the function) is responsible | ||||
| # for the subclassing. | ||||
| 
 | ||||
| class _ProductsForm(forms.Form): | ||||
| 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 = { | ||||
|         inventory.Category.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm, | ||||
|         inventory.Category.RENDER_TYPE_RADIO: _RadioButtonProductsForm, | ||||
|         inventory.Category.RENDER_TYPE_ITEM_QUANTITY: _ItemQuantityProductsForm, | ||||
|     } | ||||
| 
 | ||||
|     # 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) | ||||
| 
 | ||||
|     if category.render_type == inventory.Category.RENDER_TYPE_ITEM_QUANTITY: | ||||
|         ProductsForm = forms.formset_factory( | ||||
|             ProductsForm, | ||||
|             formset=_ItemQuantityProductsFormSet, | ||||
|         ) | ||||
| 
 | ||||
|     return ProductsForm | ||||
| 
 | ||||
| 
 | ||||
| class _HasProductsFields(object): | ||||
| 
 | ||||
|     PRODUCT_PREFIX = "product_" | ||||
| 
 | ||||
|  | @ -57,7 +85,7 @@ class _ProductsForm(forms.Form): | |||
|             initial = self.initial_data(k["product_quantities"]) | ||||
|             k["initial"] = initial | ||||
|             del k["product_quantities"] | ||||
|         super(_ProductsForm, self).__init__(*a, **k) | ||||
|         super(_HasProductsFields, self).__init__(*a, **k) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def field_name(cls, product): | ||||
|  | @ -80,6 +108,22 @@ class _ProductsForm(forms.Form): | |||
|         cleaned form data. ''' | ||||
|         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): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class _QuantityBoxProductsForm(_ProductsForm): | ||||
|     ''' Products entry form that allows users to enter quantities | ||||
|  | @ -116,7 +160,7 @@ class _QuantityBoxProductsForm(_ProductsForm): | |||
|         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) | ||||
|                 yield (product_id, value) | ||||
| 
 | ||||
| 
 | ||||
| class _RadioButtonProductsForm(_ProductsForm): | ||||
|  | @ -129,7 +173,6 @@ class _RadioButtonProductsForm(_ProductsForm): | |||
|     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)) | ||||
| 
 | ||||
|  | @ -167,23 +210,133 @@ class _RadioButtonProductsForm(_ProductsForm): | |||
|                 self.FIELD, | ||||
|             ) | ||||
| 
 | ||||
|     def add_product_error(self, product, error): | ||||
|         self.add_error(cls.FIELD, error) | ||||
| 
 | ||||
| def ProductsForm(category, products): | ||||
|     ''' Produces an appropriate _ProductsForm subclass for the given render | ||||
|     type. ''' | ||||
| class _ItemQuantityProductsForm(_ProductsForm): | ||||
|     ''' Products entry form that allows users to select a product type, and | ||||
|      enter a quantity of that product. This version _only_ allows a single | ||||
|      product type to be purchased. This form is usually used in concert with the | ||||
|      _ItemQuantityProductsFormSet to allow selection of multiple products.''' | ||||
| 
 | ||||
|     # Each Category.RENDER_TYPE value has a subclass here. | ||||
|     RENDER_TYPES = { | ||||
|         inventory.Category.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm, | ||||
|         inventory.Category.RENDER_TYPE_RADIO: _RadioButtonProductsForm, | ||||
|     } | ||||
|     CHOICE_FIELD = "choice" | ||||
|     QUANTITY_FIELD = "quantity" | ||||
| 
 | ||||
|     # Produce a subclass of _ProductsForm which we can alter the base_fields on | ||||
|     class ProductsForm(RENDER_TYPES[category.render_type]): | ||||
|         pass | ||||
|     @classmethod | ||||
|     def set_fields(cls, category, products): | ||||
|         choices = [] | ||||
| 
 | ||||
|     ProductsForm.set_fields(category, products) | ||||
|     return ProductsForm | ||||
|         if not category.required: | ||||
|             choices.append((0, "---")) | ||||
| 
 | ||||
|         for product in products: | ||||
|             choice_text = "%s -- $%d each" % (product.name, product.price) | ||||
|             choices.append((product.id, choice_text)) | ||||
| 
 | ||||
|         cls.base_fields[cls.CHOICE_FIELD] = forms.TypedChoiceField( | ||||
|             label=category.name, | ||||
|             widget=forms.Select, | ||||
|             choices=choices, | ||||
|             initial=0, | ||||
|             empty_value=0, | ||||
|             coerce=int, | ||||
|         ) | ||||
| 
 | ||||
|         cls.base_fields[cls.QUANTITY_FIELD] = forms.IntegerField( | ||||
|             label="Quantity",  # TODO: internationalise | ||||
|             min_value=0, | ||||
|             max_value=500,  # Issue #19. We should figure out real limit. | ||||
|         ) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def initial_data(cls, product_quantities): | ||||
|         initial = {} | ||||
| 
 | ||||
|         for product, quantity in product_quantities: | ||||
|             if quantity > 0: | ||||
|                 initial[cls.CHOICE_FIELD] = product.id | ||||
|                 initial[cls.QUANTITY_FIELD] = quantity | ||||
|                 break | ||||
| 
 | ||||
|         return initial | ||||
| 
 | ||||
|     def product_quantities(self): | ||||
|         our_choice = self.cleaned_data[self.CHOICE_FIELD] | ||||
|         our_quantity = self.cleaned_data[self.QUANTITY_FIELD] | ||||
|         choices = self.fields[self.CHOICE_FIELD].choices | ||||
|         for choice_value, choice_display in choices: | ||||
|             if choice_value == 0: | ||||
|                 continue | ||||
|             yield ( | ||||
|                 choice_value, | ||||
|                 our_quantity if our_choice == choice_value else 0, | ||||
|             ) | ||||
| 
 | ||||
|     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.CHOICE_FIELD, error) | ||||
|             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): | ||||
|  |  | |||
							
								
								
									
										20
									
								
								registrasion/migrations/0002_auto_20160822_0034.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								registrasion/migrations/0002_auto_20160822_0034.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9.2 on 2016-08-22 00:34 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('registrasion', '0001_initial'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='category', | ||||
|             name='render_type', | ||||
|             field=models.IntegerField(choices=[(1, 'Radio button'), (2, 'Quantity boxes'), (3, 'Product selector and quantity box')], help_text='The registration form will render this category in this style.', verbose_name='Render type'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -36,6 +36,12 @@ class Category(models.Model): | |||
|             where the user can specify a quantity of each Product type. This is | ||||
|             useful for additional extras, like Dinner Tickets. | ||||
| 
 | ||||
|             ``RENDER_TYPE_ITEM_QUANTITY`` shows a select menu to select a | ||||
|             Product type, and an input field, where the user can specify the | ||||
|             quantity for that Product type. This is useful for categories that | ||||
|             have a lot of options, from which the user is not going to select | ||||
|             all of the options. | ||||
| 
 | ||||
|         limit_per_user (Optional[int]): This restricts the number of items | ||||
|             from this Category that each attendee may claim. This extends | ||||
|             across multiple Invoices. | ||||
|  | @ -56,10 +62,12 @@ class Category(models.Model): | |||
| 
 | ||||
|     RENDER_TYPE_RADIO = 1 | ||||
|     RENDER_TYPE_QUANTITY = 2 | ||||
|     RENDER_TYPE_ITEM_QUANTITY = 3 | ||||
| 
 | ||||
|     CATEGORY_RENDER_TYPES = [ | ||||
|         (RENDER_TYPE_RADIO, _("Radio button")), | ||||
|         (RENDER_TYPE_QUANTITY, _("Quantity boxes")), | ||||
|         (RENDER_TYPE_ITEM_QUANTITY, _("Product selector and quantity box")), | ||||
|     ] | ||||
| 
 | ||||
|     name = models.CharField( | ||||
|  |  | |||
|  | @ -446,33 +446,29 @@ def _handle_products(request, category, products, prefix): | |||
| 
 | ||||
| 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()) | ||||
|     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] | ||||
|     products = inventory.Product.objects.filter( | ||||
|         id__in=pks, | ||||
|     ).select_related("category").order_by("id") | ||||
| 
 | ||||
|     quantities.sort(key = lambda i: i[0]) | ||||
| 
 | ||||
|     # Match the product objects to their quantities | ||||
|     product_quantities = [ | ||||
|         (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: | ||||
|         current_cart.set_quantities(product_quantities) | ||||
|     except CartValidationError as ve: | ||||
|         for ve_field in ve.error_list: | ||||
|             product, message = ve_field.message | ||||
|             if product in field_names: | ||||
|                 field = field_names[product] | ||||
|             elif isinstance(product, inventory.Product): | ||||
|                 continue | ||||
|             else: | ||||
|                 field = None | ||||
|             products_form.add_error(field, message) | ||||
|             products_form.add_product_error(product, message) | ||||
| 
 | ||||
| 
 | ||||
| def _handle_voucher(request, prefix): | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer