git subrepo clone git@gitlab.com:tchaypo/registrasion.git vendor/registrasion
subrepo: subdir: "vendor/registrasion" merged: "7cf314a" upstream: origin: "git@gitlab.com:tchaypo/registrasion.git" branch: "lca2018" commit: "7cf314a" git-subrepo: version: "0.3.1" origin: "???" commit: "???"
This commit is contained in:
		
							parent
							
								
									2580584597
								
							
						
					
					
						commit
						162b5edc20
					
				
					 19 changed files with 477 additions and 211 deletions
				
			
		
							
								
								
									
										4
									
								
								vendor/registrasion/.gitrepo
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								vendor/registrasion/.gitrepo
									
										
									
									
										vendored
									
									
								
							|  | @ -6,6 +6,6 @@ | ||||||
| [subrepo] | [subrepo] | ||||||
| 	remote = git@gitlab.com:tchaypo/registrasion.git | 	remote = git@gitlab.com:tchaypo/registrasion.git | ||||||
| 	branch = lca2018 | 	branch = lca2018 | ||||||
| 	commit = c1e194aef92e4c06a8855fad22ca819c08736dad | 	commit = 7cf314adae9f8de1519db63828f55a10aa09f0ee | ||||||
| 	parent = dd8a42e9f67cfebc39347cb87bacf79046bfbfec | 	parent = 7c5c6c02f20ddcbc6c54b3065311880fce586192 | ||||||
| 	cmdver = 0.3.1 | 	cmdver = 0.3.1 | ||||||
|  |  | ||||||
|  | @ -9,6 +9,8 @@ from django.db.models import Value | ||||||
| 
 | 
 | ||||||
| from .batch import BatchController | from .batch import BatchController | ||||||
| 
 | 
 | ||||||
|  | from operator import attrgetter | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class AllProducts(object): | class AllProducts(object): | ||||||
|     pass |     pass | ||||||
|  | @ -26,7 +28,7 @@ class CategoryController(object): | ||||||
|         products, otherwise it'll do all. ''' |         products, otherwise it'll do all. ''' | ||||||
| 
 | 
 | ||||||
|         # STOPGAP -- this needs to be elsewhere tbqh |         # STOPGAP -- this needs to be elsewhere tbqh | ||||||
|         from registrasion.controllers.product import ProductController |         from .product import ProductController | ||||||
| 
 | 
 | ||||||
|         if products is AllProducts: |         if products is AllProducts: | ||||||
|             products = inventory.Product.objects.all().select_related( |             products = inventory.Product.objects.all().select_related( | ||||||
|  | @ -38,7 +40,7 @@ class CategoryController(object): | ||||||
|             products=products, |             products=products, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         return set(i.category for i in available) |         return sorted(set(i.category for i in available), key=attrgetter("order")) | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     @BatchController.memoise |     @BatchController.memoise | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ from django.db import transaction | ||||||
| 
 | 
 | ||||||
| from registrasion.models import commerce | from registrasion.models import commerce | ||||||
| 
 | 
 | ||||||
| from registrasion.controllers.for_id import ForId | from .for_id import ForId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class CreditNoteController(ForId, object): | class CreditNoteController(ForId, object): | ||||||
|  | @ -40,8 +40,8 @@ class CreditNoteController(ForId, object): | ||||||
|         paid. |         paid. | ||||||
|         ''' |         ''' | ||||||
| 
 | 
 | ||||||
|         # Circular Import |         # Local import to fix import cycles. Can we do better? | ||||||
|         from registrasion.controllers.invoice import InvoiceController |         from .invoice import InvoiceController | ||||||
|         inv = InvoiceController(invoice) |         inv = InvoiceController(invoice) | ||||||
|         inv.validate_allowed_to_pay() |         inv.validate_allowed_to_pay() | ||||||
| 
 | 
 | ||||||
|  | @ -65,8 +65,8 @@ class CreditNoteController(ForId, object): | ||||||
|         a cancellation fee. Must be 0 <= percentage <= 100. |         a cancellation fee. Must be 0 <= percentage <= 100. | ||||||
|         ''' |         ''' | ||||||
| 
 | 
 | ||||||
|         # Circular Import |         # Local import to fix import cycles. Can we do better? | ||||||
|         from registrasion.controllers.invoice import InvoiceController |         from .invoice import InvoiceController | ||||||
| 
 | 
 | ||||||
|         assert(percentage >= 0 and percentage <= 100) |         assert(percentage >= 0 and percentage <= 100) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -45,6 +45,28 @@ class FlagController(object): | ||||||
|         else: |         else: | ||||||
|             all_conditions = [] |             all_conditions = [] | ||||||
| 
 | 
 | ||||||
|  |         all_conditions = conditions.FlagBase.objects.filter( | ||||||
|  |             id__in=set(i.id for i in all_conditions) | ||||||
|  |         ).select_subclasses() | ||||||
|  | 
 | ||||||
|  |         # Prefetch all of the products and categories (Saves a LOT of queries) | ||||||
|  |         all_conditions = all_conditions.prefetch_related( | ||||||
|  |             "products", "categories", "products__category", | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # Now pre-select all of the products attached to those categories | ||||||
|  |         all_categories = set( | ||||||
|  |             cat for condition in all_conditions | ||||||
|  |                 for cat in condition.categories.all() | ||||||
|  |         ) | ||||||
|  |         all_category_ids = (i.id for i in all_categories) | ||||||
|  |         all_category_products = inventory.Product.objects.filter( | ||||||
|  |             category__in=all_category_ids | ||||||
|  |         ).select_related("category") | ||||||
|  | 
 | ||||||
|  |         products_by_category_ = itertools.groupby(all_category_products, lambda prod: prod.category) | ||||||
|  |         products_by_category = dict((k.id, list(v)) for (k, v) in products_by_category_) | ||||||
|  | 
 | ||||||
|         # All disable-if-false conditions on a product need to be met |         # All disable-if-false conditions on a product need to be met | ||||||
|         do_not_disable = defaultdict(lambda: True) |         do_not_disable = defaultdict(lambda: True) | ||||||
|         # At least one enable-if-true condition on a product must be met |         # At least one enable-if-true condition on a product must be met | ||||||
|  | @ -64,17 +86,19 @@ class FlagController(object): | ||||||
|             # Get all products covered by this condition, and the products |             # Get all products covered by this condition, and the products | ||||||
|             # from the categories covered by this condition |             # from the categories covered by this condition | ||||||
| 
 | 
 | ||||||
|             ids = [product.id for product in products] |             condition_products = condition.products.all() | ||||||
| 
 |             category_products = ( | ||||||
|             # TODO: This is re-evaluated a lot. |                 product for cat in condition.categories.all() for product in products_by_category[cat.id] | ||||||
|             all_products = inventory.Product.objects.filter(id__in=ids) |  | ||||||
|             cond = ( |  | ||||||
|                 Q(flagbase_set=condition) | |  | ||||||
|                 Q(category__in=condition.categories.all()) |  | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             all_products = all_products.filter(cond) |             all_products = itertools.chain( | ||||||
|             all_products = all_products.select_related("category") |                 condition_products, category_products | ||||||
|  |             ) | ||||||
|  |             all_products = set(all_products) | ||||||
|  | 
 | ||||||
|  |             # Filter out the products from this condition that | ||||||
|  |             # are not part of this query. | ||||||
|  |             all_products = set(i for i in all_products if i in products) | ||||||
| 
 | 
 | ||||||
|             if quantities: |             if quantities: | ||||||
|                 consumed = sum(quantities[i] for i in all_products) |                 consumed = sum(quantities[i] for i in all_products) | ||||||
|  |  | ||||||
|  | @ -10,9 +10,9 @@ from registrasion.models import commerce | ||||||
| from registrasion.models import conditions | from registrasion.models import conditions | ||||||
| from registrasion.models import people | from registrasion.models import people | ||||||
| 
 | 
 | ||||||
| from registrasion.controllers.cart import CartController | from .cart import CartController | ||||||
| from registrasion.controllers.credit_note import CreditNoteController | from .credit_note import CreditNoteController | ||||||
| from registrasion.controllers.for_id import ForId | from .for_id import ForId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class InvoiceController(ForId, object): | class InvoiceController(ForId, object): | ||||||
|  |  | ||||||
							
								
								
									
										44
									
								
								vendor/registrasion/registrasion/forms.py
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										44
									
								
								vendor/registrasion/registrasion/forms.py
									
										
									
									
										vendored
									
									
								
							|  | @ -1,6 +1,6 @@ | ||||||
| from registrasion.controllers.product import ProductController | from .controllers.product import ProductController | ||||||
| from registrasion.models import commerce | from .models import commerce | ||||||
| from registrasion.models import inventory | from .models import inventory | ||||||
| 
 | 
 | ||||||
| from django import forms | from django import forms | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
|  | @ -31,12 +31,13 @@ class ApplyCreditNoteForm(forms.Form): | ||||||
|                 "user_email": users[invoice["user_id"]].email, |                 "user_email": users[invoice["user_id"]].email, | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         key = lambda inv: (0 - (inv["user_id"] == self.user.id), inv["id"])  # noqa |         key = lambda inv: (0 - (inv["user_id"] == self.user.id), inv["id"])  # noqa | ||||||
|         invoices_annotated.sort(key=key) |         invoices_annotated.sort(key=key) | ||||||
| 
 | 
 | ||||||
|         template = ('Invoice %(id)d - user: %(user_email)s (%(user_id)d) ' |         template = ( | ||||||
|                     '-  $%(value)d') |             'Invoice %(id)d - user: %(user_email)s (%(user_id)d) ' | ||||||
|  |             '-  $%(value)d' | ||||||
|  |         ) | ||||||
|         return [ |         return [ | ||||||
|             (invoice["id"], template % invoice) |             (invoice["id"], template % invoice) | ||||||
|             for invoice in invoices_annotated |             for invoice in invoices_annotated | ||||||
|  | @ -94,6 +95,7 @@ def ProductsForm(category, products): | ||||||
|         cat.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm, |         cat.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm, | ||||||
|         cat.RENDER_TYPE_RADIO: _RadioButtonProductsForm, |         cat.RENDER_TYPE_RADIO: _RadioButtonProductsForm, | ||||||
|         cat.RENDER_TYPE_ITEM_QUANTITY: _ItemQuantityProductsForm, |         cat.RENDER_TYPE_ITEM_QUANTITY: _ItemQuantityProductsForm, | ||||||
|  |         cat.RENDER_TYPE_CHECKBOX: _CheckboxProductsForm, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     # Produce a subclass of _ProductsForm which we can alter the base_fields on |     # Produce a subclass of _ProductsForm which we can alter the base_fields on | ||||||
|  | @ -252,6 +254,35 @@ class _RadioButtonProductsForm(_ProductsForm): | ||||||
|         self.add_error(self.FIELD, error) |         self.add_error(self.FIELD, error) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class _CheckboxProductsForm(_ProductsForm): | ||||||
|  |     ''' Products entry form that allows users to say yes or no | ||||||
|  |     to desired products. Basically, it's a quantity form, but the quantity | ||||||
|  |     is either zero or one.''' | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def set_fields(cls, category, products): | ||||||
|  |         for product in products: | ||||||
|  |             field = forms.BooleanField( | ||||||
|  |                 label='%s -- %s' % (product.name, product.price), | ||||||
|  |                 required=False, | ||||||
|  |             ) | ||||||
|  |             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)] = bool(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, int(value)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| 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 single |      enter a quantity of that product. This version _only_ allows a single | ||||||
|  | @ -449,7 +480,6 @@ class InvoicesWithProductAndStatusForm(forms.Form): | ||||||
|         product = [int(i) for i in product] |         product = [int(i) for i in product] | ||||||
| 
 | 
 | ||||||
|         super(InvoicesWithProductAndStatusForm, self).__init__(*a, **k) |         super(InvoicesWithProductAndStatusForm, self).__init__(*a, **k) | ||||||
|         print(status) |  | ||||||
| 
 | 
 | ||||||
|         qs = commerce.Invoice.objects.filter( |         qs = commerce.Invoice.objects.filter( | ||||||
|             status=status or commerce.Invoice.STATUS_UNPAID, |             status=status or commerce.Invoice.STATUS_UNPAID, | ||||||
|  |  | ||||||
|  | @ -1,16 +0,0 @@ | ||||||
| # -*- coding: utf-8 -*- |  | ||||||
| # Generated by Django 1.11.5 on 2017-09-29 12:59 |  | ||||||
| from __future__ import unicode_literals |  | ||||||
| 
 |  | ||||||
| from django.db import migrations |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
| 
 |  | ||||||
|     dependencies = [ |  | ||||||
|         ('registrasion', '0006_auto_20170526_1624'), |  | ||||||
|         ('registrasion', '0006_auto_20170702_2233'), |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
|     operations = [ |  | ||||||
|     ] |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| from registrasion.models.commerce import *  # NOQA | from .commerce import *  # NOQA | ||||||
| from registrasion.models.conditions import *  # NOQA | from .conditions import *  # NOQA | ||||||
| from registrasion.models.inventory import *  # NOQA | from .inventory import *  # NOQA | ||||||
| from registrasion.models.people import *  # NOQA | from .people import *  # NOQA | ||||||
|  |  | ||||||
|  | @ -324,7 +324,6 @@ class CreditNote(PaymentBase): | ||||||
| 
 | 
 | ||||||
|         elif hasattr(self, 'creditnoterefund'): |         elif hasattr(self, 'creditnoterefund'): | ||||||
|             reference = self.creditnoterefund.reference |             reference = self.creditnoterefund.reference | ||||||
|             print(reference) |  | ||||||
|             return "Refunded with reference: %s" % reference |             return "Refunded with reference: %s" % reference | ||||||
| 
 | 
 | ||||||
|         raise ValueError("This should never happen.") |         raise ValueError("This should never happen.") | ||||||
|  |  | ||||||
|  | @ -42,6 +42,8 @@ class Category(models.Model): | ||||||
|             have a lot of options, from which the user is not going to select |             have a lot of options, from which the user is not going to select | ||||||
|             all of the options. |             all of the options. | ||||||
| 
 | 
 | ||||||
|  |             ``RENDER_TYPE_CHECKBOX`` shows a checkbox beside each product. | ||||||
|  | 
 | ||||||
|         limit_per_user (Optional[int]): This restricts the number of items |         limit_per_user (Optional[int]): This restricts the number of items | ||||||
|             from this Category that each attendee may claim. This extends |             from this Category that each attendee may claim. This extends | ||||||
|             across multiple Invoices. |             across multiple Invoices. | ||||||
|  | @ -63,11 +65,13 @@ class Category(models.Model): | ||||||
|     RENDER_TYPE_RADIO = 1 |     RENDER_TYPE_RADIO = 1 | ||||||
|     RENDER_TYPE_QUANTITY = 2 |     RENDER_TYPE_QUANTITY = 2 | ||||||
|     RENDER_TYPE_ITEM_QUANTITY = 3 |     RENDER_TYPE_ITEM_QUANTITY = 3 | ||||||
|  |     RENDER_TYPE_CHECKBOX = 4 | ||||||
| 
 | 
 | ||||||
|     CATEGORY_RENDER_TYPES = [ |     CATEGORY_RENDER_TYPES = [ | ||||||
|         (RENDER_TYPE_RADIO, _("Radio button")), |         (RENDER_TYPE_RADIO, _("Radio button")), | ||||||
|         (RENDER_TYPE_QUANTITY, _("Quantity boxes")), |         (RENDER_TYPE_QUANTITY, _("Quantity boxes")), | ||||||
|         (RENDER_TYPE_ITEM_QUANTITY, _("Product selector and quantity box")), |         (RENDER_TYPE_ITEM_QUANTITY, _("Product selector and quantity box")), | ||||||
|  |         (RENDER_TYPE_CHECKBOX, _("Checkbox button")), | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     name = models.CharField( |     name = models.CharField( | ||||||
|  |  | ||||||
|  | @ -177,7 +177,6 @@ class Links(Report): | ||||||
|         return [] |         return [] | ||||||
| 
 | 
 | ||||||
|     def rows(self, content_type): |     def rows(self, content_type): | ||||||
|         print(self._links) |  | ||||||
|         for url, link_text in self._links: |         for url, link_text in self._links: | ||||||
|             yield [ |             yield [ | ||||||
|                 self._linked_text(content_type, url, link_text) |                 self._linked_text(content_type, url, link_text) | ||||||
|  | @ -299,9 +298,10 @@ class ReportView(object): | ||||||
|         response = HttpResponse(content_type='text/csv') |         response = HttpResponse(content_type='text/csv') | ||||||
| 
 | 
 | ||||||
|         writer = csv.writer(response) |         writer = csv.writer(response) | ||||||
|         writer.writerow(report.headings()) |         encode = lambda i: i.encode("utf8") if isinstance(i, unicode) else i  # NOQA | ||||||
|  |         writer.writerow(list(encode(i) for i in report.headings())) | ||||||
|         for row in report.rows(): |         for row in report.rows(): | ||||||
|             writer.writerow(row) |             writer.writerow(list(encode(i) for i in row)) | ||||||
| 
 | 
 | ||||||
|         return response |         return response | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| from registrasion.reporting import forms | from . import forms | ||||||
| 
 | 
 | ||||||
| import collections | import collections | ||||||
| import datetime | import datetime | ||||||
|  | @ -24,11 +24,11 @@ from registrasion import views | ||||||
| 
 | 
 | ||||||
| from symposion.schedule import models as schedule_models | from symposion.schedule import models as schedule_models | ||||||
| 
 | 
 | ||||||
| from registrasion.reporting.reports import get_all_reports | from .reports import get_all_reports | ||||||
| from registrasion.reporting.reports import Links | from .reports import Links | ||||||
| from registrasion.reporting.reports import ListReport | from .reports import ListReport | ||||||
| from registrasion.reporting.reports import QuerysetReport | from .reports import QuerysetReport | ||||||
| from registrasion.reporting.reports import report_view | from .reports import report_view | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def CURRENCY(): | def CURRENCY(): | ||||||
|  | @ -95,8 +95,6 @@ def items_sold(): | ||||||
|         total_quantity=Sum("quantity"), |         total_quantity=Sum("quantity"), | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     print(line_items) |  | ||||||
| 
 |  | ||||||
|     headings = ["Description", "Quantity", "Price", "Total"] |     headings = ["Description", "Quantity", "Price", "Total"] | ||||||
| 
 | 
 | ||||||
|     data = [] |     data = [] | ||||||
|  | @ -312,6 +310,55 @@ def discount_status(request, form): | ||||||
|     return ListReport("Usage by item", headings, data) |     return ListReport("Usage by item", headings, data) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @report_view("Product Line Items By Date & Customer", form_type=forms.ProductAndCategoryForm) | ||||||
|  | def product_line_items(request, form): | ||||||
|  |     ''' Shows each product line item from invoices, including their date and | ||||||
|  |     purchashing customer. ''' | ||||||
|  | 
 | ||||||
|  |     products = form.cleaned_data["product"] | ||||||
|  |     categories = form.cleaned_data["category"] | ||||||
|  | 
 | ||||||
|  |     invoices = commerce.Invoice.objects.filter( | ||||||
|  |         ( | ||||||
|  |             Q(lineitem__product__in=products) | | ||||||
|  |             Q(lineitem__product__category__in=categories) | ||||||
|  |         ), | ||||||
|  |         status=commerce.Invoice.STATUS_PAID, | ||||||
|  |     ).select_related( | ||||||
|  |         "cart", | ||||||
|  |         "user", | ||||||
|  |         "user__attendee", | ||||||
|  |         "user__attendee__attendeeprofilebase" | ||||||
|  |     ).order_by("issue_time") | ||||||
|  | 
 | ||||||
|  |     headings = [ | ||||||
|  |         'Invoice', 'Invoice Date', 'Attendee', 'Qty', 'Product', 'Status' | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     data = [] | ||||||
|  |     for invoice in invoices: | ||||||
|  |         for item in invoice.cart.productitem_set.all(): | ||||||
|  |             if item.product in products or item.product.category in categories: | ||||||
|  |                 output = [] | ||||||
|  |                 output.append(invoice.id) | ||||||
|  |                 output.append(invoice.issue_time.strftime('%Y-%m-%d %H:%M:%S')) | ||||||
|  |                 output.append( | ||||||
|  |                     invoice.user.attendee.attendeeprofilebase.attendee_name() | ||||||
|  |                 ) | ||||||
|  |                 output.append(item.quantity) | ||||||
|  |                 output.append(item.product) | ||||||
|  |                 cart = invoice.cart | ||||||
|  |                 if cart.status == commerce.Cart.STATUS_PAID: | ||||||
|  |                     output.append('PAID') | ||||||
|  |                 elif cart.status == commerce.Cart.STATUS_ACTIVE: | ||||||
|  |                     output.append('UNPAID') | ||||||
|  |                 elif cart.status == commerce.Cart.STATUS_RELEASED: | ||||||
|  |                     output.append('REFUNDED') | ||||||
|  |                 data.append(output) | ||||||
|  | 
 | ||||||
|  |     return ListReport("Line Items", headings, data) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @report_view("Paid invoices by date", form_type=forms.ProductAndCategoryForm) | @report_view("Paid invoices by date", form_type=forms.ProductAndCategoryForm) | ||||||
| def paid_invoices_by_date(request, form): | def paid_invoices_by_date(request, form): | ||||||
|     ''' Shows the number of paid invoices containing given products or |     ''' Shows the number of paid invoices containing given products or | ||||||
|  | @ -353,8 +400,8 @@ def paid_invoices_by_date(request, form): | ||||||
|         ) |         ) | ||||||
|         by_date[date] += 1 |         by_date[date] += 1 | ||||||
| 
 | 
 | ||||||
|     data = [(date, count) for date, count in sorted(by_date.items())] |     data = [(date_, count) for date_, count in sorted(by_date.items())] | ||||||
|     data = [(date.strftime("%Y-%m-%d"), count) for date, count in data] |     data = [(date_.strftime("%Y-%m-%d"), count) for date_, count in data] | ||||||
| 
 | 
 | ||||||
|     return ListReport( |     return ListReport( | ||||||
|         "Paid Invoices By Date", |         "Paid Invoices By Date", | ||||||
|  | @ -417,8 +464,6 @@ def attendee(request, form, user_id=None): | ||||||
|     if user_id is None: |     if user_id is None: | ||||||
|         return attendee_list(request) |         return attendee_list(request) | ||||||
| 
 | 
 | ||||||
|     print(user_id) |  | ||||||
| 
 |  | ||||||
|     attendee = people.Attendee.objects.get(user__id=user_id) |     attendee = people.Attendee.objects.get(user__id=user_id) | ||||||
|     name = attendee.attendeeprofilebase.attendee_name() |     name = attendee.attendeeprofilebase.attendee_name() | ||||||
| 
 | 
 | ||||||
|  | @ -846,9 +891,10 @@ def manifest(request, form): | ||||||
|     headings = ["User ID", "Name", "Paid", "Unpaid", "Refunded"] |     headings = ["User ID", "Name", "Paid", "Unpaid", "Refunded"] | ||||||
| 
 | 
 | ||||||
|     def format_items(item_list): |     def format_items(item_list): | ||||||
|         strings = [] |         strings = [ | ||||||
|         for item in item_list: |             '%d x %s' % (item.quantity, str(item.product)) | ||||||
|             strings.append('%d x %s' % (item.quantity, str(item.product))) |             for item in item_list | ||||||
|  |         ] | ||||||
|         return ", \n".join(strings) |         return ", \n".join(strings) | ||||||
| 
 | 
 | ||||||
|     output = [] |     output = [] | ||||||
|  |  | ||||||
|  | @ -3,8 +3,9 @@ from registrasion.controllers.category import CategoryController | ||||||
| from registrasion.controllers.item import ItemController | from registrasion.controllers.item import ItemController | ||||||
| 
 | 
 | ||||||
| from django import template | from django import template | ||||||
|  | from django.conf import settings | ||||||
| from django.db.models import Sum | from django.db.models import Sum | ||||||
| from urllib.parse import urlencode | from urllib import urlencode  # TODO: s/urllib/six.moves.urllib/ | ||||||
| 
 | 
 | ||||||
| register = template.Library() | register = template.Library() | ||||||
| 
 | 
 | ||||||
|  | @ -117,3 +118,69 @@ def report_as_csv(context, section): | ||||||
|         querystring = old_query + "&" + querystring |         querystring = old_query + "&" + querystring | ||||||
| 
 | 
 | ||||||
|     return context.request.path + "?" + querystring |     return context.request.path + "?" + querystring | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @register.assignment_tag(takes_context=True) | ||||||
|  | def sold_out_and_unregistered(context): | ||||||
|  |     ''' If the current user is unregistered, returns True if there are no | ||||||
|  |     products in the TICKET_PRODUCT_CATEGORY that are available to that user. | ||||||
|  | 
 | ||||||
|  |     If there *are* products available, the return False. | ||||||
|  | 
 | ||||||
|  |     If the current user *is* registered, then return None (it's not a | ||||||
|  |     pertinent question for people who already have a ticket). | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  | 
 | ||||||
|  |     user = user_for_context(context) | ||||||
|  |     if hasattr(user, "attendee") and user.attendee.completed_registration: | ||||||
|  |         # This user has completed registration, and so we don't need to answer | ||||||
|  |         # whether they have sold out yet. | ||||||
|  | 
 | ||||||
|  |         # TODO: what if a user has got to the review phase? | ||||||
|  |         # currently that user will hit the review page, click "Check out and | ||||||
|  |         # pay", and that will fail. Probably good enough for now. | ||||||
|  | 
 | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     ticket_category = settings.TICKET_PRODUCT_CATEGORY | ||||||
|  |     categories = available_categories(context) | ||||||
|  | 
 | ||||||
|  |     return ticket_category not in [cat.id for cat in categories] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class IncludeNode(template.Node): | ||||||
|  |     ''' https://djangosnippets.org/snippets/2058/ ''' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def __init__(self, template_name): | ||||||
|  |         # template_name as passed in includes quotmarks? | ||||||
|  |         # strip them from the start and end | ||||||
|  |         self.template_name = template_name[1:-1] | ||||||
|  | 
 | ||||||
|  |     def render(self, context): | ||||||
|  |         try: | ||||||
|  |             # Loading the template and rendering it | ||||||
|  |             return template.loader.render_to_string( | ||||||
|  |                 self.template_name, context=context, | ||||||
|  |             ) | ||||||
|  |         except template.TemplateDoesNotExist: | ||||||
|  |             return "" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @register.tag | ||||||
|  | def include_if_exists(parser, token): | ||||||
|  |     """Usage: {% include_if_exists "head.html" %} | ||||||
|  | 
 | ||||||
|  |     This will fail silently if the template doesn't exist. If it does, it will | ||||||
|  |     be rendered with the current context. | ||||||
|  | 
 | ||||||
|  |     From: https://djangosnippets.org/snippets/2058/ | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         tag_name, template_name = token.split_contents() | ||||||
|  |     except ValueError: | ||||||
|  |         raise (template.TemplateSyntaxError, | ||||||
|  |             "%r tag requires a single argument" % token.contents.split()[0]) | ||||||
|  | 
 | ||||||
|  |     return IncludeNode(template_name) | ||||||
|  |  | ||||||
|  | @ -85,7 +85,7 @@ class RegistrationCartTestCase(MixInPatches, TestCase): | ||||||
|             prod = inventory.Product.objects.create( |             prod = inventory.Product.objects.create( | ||||||
|                 name="Product " + str(i + 1), |                 name="Product " + str(i + 1), | ||||||
|                 description="This is a test product.", |                 description="This is a test product.", | ||||||
|                 category=cls.categories[int(i / 2)],  # 2 products per category |                 category=cls.categories[i / 2],  # 2 products per category | ||||||
|                 price=Decimal("10.00"), |                 price=Decimal("10.00"), | ||||||
|                 reservation_duration=cls.RESERVATION, |                 reservation_duration=cls.RESERVATION, | ||||||
|                 limit_per_user=10, |                 limit_per_user=10, | ||||||
|  |  | ||||||
|  | @ -98,11 +98,7 @@ class InvoiceTestCase(TestHelperMixin, RegistrationCartTestCase): | ||||||
| 
 | 
 | ||||||
|     def test_total_payments_balance_due(self): |     def test_total_payments_balance_due(self): | ||||||
|         invoice = self._invoice_containing_prod_1(2) |         invoice = self._invoice_containing_prod_1(2) | ||||||
|         # range only takes int, and the following logic fails if not a round |         for i in xrange(0, invoice.invoice.value): | ||||||
|         # number.  So fail if we are not a round number so developer may fix |  | ||||||
|         # this test or the product. |  | ||||||
|         self.assertTrue((invoice.invoice.value % 1).is_zero()) |  | ||||||
|         for i in range(0, int(invoice.invoice.value)): |  | ||||||
|             self.assertTrue( |             self.assertTrue( | ||||||
|                 i + 1, invoice.invoice.total_payments() |                 i + 1, invoice.invoice.total_payments() | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  | @ -67,7 +67,7 @@ class SpeakerTestCase(RegistrationCartTestCase): | ||||||
|             kind=kind_1, |             kind=kind_1, | ||||||
|             title="Proposal 1", |             title="Proposal 1", | ||||||
|             abstract="Abstract", |             abstract="Abstract", | ||||||
|             private_abstract="Private Abstract", |             description="Description", | ||||||
|             speaker=speaker_1, |             speaker=speaker_1, | ||||||
|         ) |         ) | ||||||
|         proposal_models.AdditionalSpeaker.objects.create( |         proposal_models.AdditionalSpeaker.objects.create( | ||||||
|  | @ -80,7 +80,7 @@ class SpeakerTestCase(RegistrationCartTestCase): | ||||||
|             kind=kind_2, |             kind=kind_2, | ||||||
|             title="Proposal 2", |             title="Proposal 2", | ||||||
|             abstract="Abstract", |             abstract="Abstract", | ||||||
|             private_abstract="Private Abstract", |             description="Description", | ||||||
|             speaker=speaker_1, |             speaker=speaker_1, | ||||||
|         ) |         ) | ||||||
|         proposal_models.AdditionalSpeaker.objects.create( |         proposal_models.AdditionalSpeaker.objects.create( | ||||||
|  |  | ||||||
							
								
								
									
										9
									
								
								vendor/registrasion/registrasion/urls.py
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								vendor/registrasion/registrasion/urls.py
									
										
									
									
										vendored
									
									
								
							|  | @ -1,4 +1,4 @@ | ||||||
| from registrasion.reporting import views as rv | from .reporting import views as rv | ||||||
| 
 | 
 | ||||||
| from django.conf.urls import include | from django.conf.urls import include | ||||||
| from django.conf.urls import url | from django.conf.urls import url | ||||||
|  | @ -19,6 +19,7 @@ from .views import ( | ||||||
|     product_category, |     product_category, | ||||||
|     refund, |     refund, | ||||||
|     review, |     review, | ||||||
|  |     voucher_code, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -43,6 +44,7 @@ public = [ | ||||||
|     url(r"^profile$", edit_profile, name="attendee_edit"), |     url(r"^profile$", edit_profile, name="attendee_edit"), | ||||||
|     url(r"^register$", guided_registration, name="guided_registration"), |     url(r"^register$", guided_registration, name="guided_registration"), | ||||||
|     url(r"^review$", review, name="review"), |     url(r"^review$", review, name="review"), | ||||||
|  |     url(r"^voucher$", voucher_code, name="voucher_code"), | ||||||
|     url(r"^register/([0-9]+)$", guided_registration, |     url(r"^register/([0-9]+)$", guided_registration, | ||||||
|         name="guided_registration"), |         name="guided_registration"), | ||||||
| ] | ] | ||||||
|  | @ -55,6 +57,11 @@ reports = [ | ||||||
|     url(r"^attendee/([0-9]*)$", rv.attendee, name="attendee"), |     url(r"^attendee/([0-9]*)$", rv.attendee, name="attendee"), | ||||||
|     url(r"^credit_notes/?$", rv.credit_notes, name="credit_notes"), |     url(r"^credit_notes/?$", rv.credit_notes, name="credit_notes"), | ||||||
|     url(r"^manifest/?$", rv.manifest, name="manifest"), |     url(r"^manifest/?$", rv.manifest, name="manifest"), | ||||||
|  |     url( | ||||||
|  |         r"^product_line_items/?$", | ||||||
|  |         rv.product_line_items, | ||||||
|  |         name="product_line_items", | ||||||
|  |     ), | ||||||
|     url(r"^discount_status/?$", rv.discount_status, name="discount_status"), |     url(r"^discount_status/?$", rv.discount_status, name="discount_status"), | ||||||
|     url(r"^invoices/?$", rv.invoices, name="invoices"), |     url(r"^invoices/?$", rv.invoices, name="invoices"), | ||||||
|     url( |     url( | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								vendor/registrasion/registrasion/util.py
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/registrasion/registrasion/util.py
									
										
									
									
										vendored
									
									
								
							|  | @ -12,7 +12,7 @@ def generate_access_code(): | ||||||
| 
 | 
 | ||||||
|     length = 6 |     length = 6 | ||||||
|     # all upper-case letters + digits 1-9 (no 0 vs O confusion) |     # all upper-case letters + digits 1-9 (no 0 vs O confusion) | ||||||
|     chars = string.ascii_uppercase + string.digits[1:] |     chars = string.uppercase + string.digits[1:] | ||||||
|     # 6 chars => 35 ** 6 = 1838265625 (should be enough for anyone) |     # 6 chars => 35 ** 6 = 1838265625 (should be enough for anyone) | ||||||
|     return get_random_string(length=length, allowed_chars=chars) |     return get_random_string(length=length, allowed_chars=chars) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										373
									
								
								vendor/registrasion/registrasion/views.py
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										373
									
								
								vendor/registrasion/registrasion/views.py
									
										
									
									
										vendored
									
									
								
							|  | @ -1,19 +1,20 @@ | ||||||
| import datetime | import datetime | ||||||
| import zipfile | import zipfile | ||||||
| 
 | 
 | ||||||
| from registrasion import forms | from . import forms | ||||||
| from registrasion import util | from . import util | ||||||
| from registrasion.models import commerce | from .models import commerce | ||||||
| from registrasion.models import inventory | from .models import inventory | ||||||
| from registrasion.models import people | from .models import people | ||||||
| from registrasion.controllers.batch import BatchController | from .controllers.batch import BatchController | ||||||
| from registrasion.controllers.cart import CartController | from .controllers.cart import CartController | ||||||
| from registrasion.controllers.credit_note import CreditNoteController | from .controllers.category import CategoryController | ||||||
| from registrasion.controllers.discount import DiscountController | from .controllers.credit_note import CreditNoteController | ||||||
| from registrasion.controllers.invoice import InvoiceController | from .controllers.discount import DiscountController | ||||||
| from registrasion.controllers.item import ItemController | from .controllers.invoice import InvoiceController | ||||||
| from registrasion.controllers.product import ProductController | from .controllers.item import ItemController | ||||||
| from registrasion.exceptions import CartValidationError | from .controllers.product import ProductController | ||||||
|  | from .exceptions import CartValidationError | ||||||
| 
 | 
 | ||||||
| from collections import namedtuple | from collections import namedtuple | ||||||
| 
 | 
 | ||||||
|  | @ -64,12 +65,19 @@ class GuidedRegistrationSection(_GuidedRegistrationSection): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @login_required | @login_required | ||||||
| def guided_registration(request): | def guided_registration(request, page_number=None): | ||||||
|     ''' Goes through the registration process in order, making sure user sees |     ''' Goes through the registration process in order, making sure user sees | ||||||
|     all valid categories. |     all valid categories. | ||||||
| 
 | 
 | ||||||
|     The user must be logged in to see this view. |     The user must be logged in to see this view. | ||||||
| 
 | 
 | ||||||
|  |     Parameter: | ||||||
|  |         page_number: | ||||||
|  |             1) Profile form (and e-mail address?) | ||||||
|  |             2) Ticket type | ||||||
|  |             3) Remaining products | ||||||
|  |             4) Mark registration as complete | ||||||
|  | 
 | ||||||
|     Returns: |     Returns: | ||||||
|         render: Renders ``registrasion/guided_registration.html``, |         render: Renders ``registrasion/guided_registration.html``, | ||||||
|             with the following data:: |             with the following data:: | ||||||
|  | @ -85,148 +93,225 @@ def guided_registration(request): | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
| 
 | 
 | ||||||
|     SESSION_KEY = "guided_registration_categories" |     PAGE_PROFILE = 1 | ||||||
|     ASK_FOR_PROFILE = 777  # Magic number. Meh. |     PAGE_TICKET = 2 | ||||||
|  |     PAGE_PRODUCTS = 3 | ||||||
|  |     PAGE_PRODUCTS_MAX = 4 | ||||||
|  |     TOTAL_PAGES = 4 | ||||||
| 
 | 
 | ||||||
|     next_step = redirect("guided_registration") |     ticket_category = inventory.Category.objects.get( | ||||||
| 
 |         id=settings.TICKET_PRODUCT_CATEGORY | ||||||
|     sections = [] |     ) | ||||||
|  |     cart = CartController.for_user(request.user) | ||||||
| 
 | 
 | ||||||
|     attendee = people.Attendee.get_instance(request.user) |     attendee = people.Attendee.get_instance(request.user) | ||||||
| 
 | 
 | ||||||
|  |     # This guided registration process is only for people who have | ||||||
|  |     # not completed registration (and has confusing behaviour if you go | ||||||
|  |     # back to it.) | ||||||
|     if attendee.completed_registration: |     if attendee.completed_registration: | ||||||
|         return redirect(review) |         return redirect(review) | ||||||
| 
 | 
 | ||||||
|     # Step 1: Fill in a badge and collect a voucher code |     # Calculate the current maximum page number for this user. | ||||||
|     try: |     has_profile = hasattr(attendee, "attendeeprofilebase") | ||||||
|         profile = attendee.attendeeprofilebase |     if not has_profile: | ||||||
|     except ObjectDoesNotExist: |         # If there's no profile, they have to go to the profile page. | ||||||
|         profile = None |         max_page = PAGE_PROFILE | ||||||
| 
 |         redirect_page = PAGE_PROFILE | ||||||
|     # Figure out if we need to show the profile form and the voucher form |  | ||||||
|     show_profile_and_voucher = False |  | ||||||
|     if SESSION_KEY not in request.session: |  | ||||||
|         if not profile: |  | ||||||
|             show_profile_and_voucher = True |  | ||||||
|     else: |     else: | ||||||
|         if request.session[SESSION_KEY] == ASK_FOR_PROFILE: |         # We have a profile. | ||||||
|             show_profile_and_voucher = True |         # Do they have a ticket? | ||||||
| 
 |         products = inventory.Product.objects.filter( | ||||||
|     if show_profile_and_voucher: |             productitem__cart=cart.cart | ||||||
|         # Keep asking for the profile until everything passes. |  | ||||||
|         request.session[SESSION_KEY] = ASK_FOR_PROFILE |  | ||||||
| 
 |  | ||||||
|         voucher_form, voucher_handled = _handle_voucher(request, "voucher") |  | ||||||
|         profile_form, profile_handled = _handle_profile(request, "profile") |  | ||||||
| 
 |  | ||||||
|         voucher_section = GuidedRegistrationSection( |  | ||||||
|             title="Voucher Code", |  | ||||||
|             form=voucher_form, |  | ||||||
|         ) |         ) | ||||||
|  |         products = products.filter(category=ticket_category) | ||||||
| 
 | 
 | ||||||
|         profile_section = GuidedRegistrationSection( |         if products.count() == 0: | ||||||
|             title="Profile and Personal Information", |             # If no ticket, they can only see the profile or ticket page. | ||||||
|             form=profile_form, |             max_page = PAGE_TICKET | ||||||
|         ) |             redirect_page = PAGE_TICKET | ||||||
| 
 |  | ||||||
|         title = "Attendee information" |  | ||||||
|         current_step = 1 |  | ||||||
|         sections.append(voucher_section) |  | ||||||
|         sections.append(profile_section) |  | ||||||
|     else: |  | ||||||
|         # We're selling products |  | ||||||
| 
 |  | ||||||
|         starting = attendee.guided_categories_complete.count() == 0 |  | ||||||
| 
 |  | ||||||
|         # Get the next category |  | ||||||
|         cats = inventory.Category.objects |  | ||||||
|         if SESSION_KEY in request.session: |  | ||||||
|             _cats = request.session[SESSION_KEY] |  | ||||||
|             cats = cats.filter(id__in=_cats) |  | ||||||
|         else: |         else: | ||||||
|             cats = cats.exclude( |             # If there's a ticket, they should *see* the general products page# | ||||||
|                 id__in=attendee.guided_categories_complete.all(), |             # but be able to go to the overflow page if needs be. | ||||||
|  |             max_page = PAGE_PRODUCTS_MAX | ||||||
|  |             redirect_page = PAGE_PRODUCTS | ||||||
|  | 
 | ||||||
|  |     if page_number is None or int(page_number) > max_page: | ||||||
|  |         return redirect("guided_registration", redirect_page) | ||||||
|  | 
 | ||||||
|  |     page_number = int(page_number) | ||||||
|  | 
 | ||||||
|  |     next_step = redirect("guided_registration", page_number + 1) | ||||||
|  | 
 | ||||||
|  |     with BatchController.batch(request.user): | ||||||
|  | 
 | ||||||
|  |         # This view doesn't work if the conference has sold out. | ||||||
|  |         available = ProductController.available_products( | ||||||
|  |             request.user, category=ticket_category | ||||||
|  |         ) | ||||||
|  |         if not available: | ||||||
|  |             messages.error(request, "There are no more tickets available.") | ||||||
|  |             return redirect("dashboard") | ||||||
|  | 
 | ||||||
|  |         sections = [] | ||||||
|  | 
 | ||||||
|  |         # Build up the list of sections | ||||||
|  |         if page_number == PAGE_PROFILE: | ||||||
|  |             # Profile bit | ||||||
|  |             title = "Attendee information" | ||||||
|  |             sections = _guided_registration_profile_and_voucher(request) | ||||||
|  |         elif page_number == PAGE_TICKET: | ||||||
|  |             # Select ticket | ||||||
|  |             title = "Select ticket type" | ||||||
|  |             sections = _guided_registration_products( | ||||||
|  |                 request, GUIDED_MODE_TICKETS_ONLY | ||||||
|  |             ) | ||||||
|  |         elif page_number == PAGE_PRODUCTS: | ||||||
|  |             # Select additional items | ||||||
|  |             title = "Additional items" | ||||||
|  |             sections = _guided_registration_products( | ||||||
|  |                 request, GUIDED_MODE_ALL_ADDITIONAL | ||||||
|  |             ) | ||||||
|  |         elif page_number == PAGE_PRODUCTS_MAX: | ||||||
|  |             # Items enabled by things on page 3 -- only shows things | ||||||
|  |             # that have not been marked as complete. | ||||||
|  |             title = "More additional items" | ||||||
|  |             sections = _guided_registration_products( | ||||||
|  |                 request, GUIDED_MODE_EXCLUDE_COMPLETE | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         cats = cats.order_by("order") |         if not sections: | ||||||
|  |             # We've filled in every category | ||||||
|  |             attendee.completed_registration = True | ||||||
|  |             attendee.save() | ||||||
|  |             return redirect("review") | ||||||
| 
 | 
 | ||||||
|         request.session[SESSION_KEY] = [] |         if sections and request.method == "POST": | ||||||
| 
 |             for section in sections: | ||||||
|         if starting: |                 if section.form.errors: | ||||||
|             # Only display the first Category |                     break | ||||||
|             title = "Select ticket type" |             else: | ||||||
|             current_step = 2 |                 # We've successfully processed everything | ||||||
|             cats = [cats[0]] |  | ||||||
|         else: |  | ||||||
|             # Set title appropriately for remaining categories |  | ||||||
|             current_step = 3 |  | ||||||
|             title = "Additional items" |  | ||||||
| 
 |  | ||||||
|         all_products = inventory.Product.objects.filter( |  | ||||||
|             category__in=cats, |  | ||||||
|         ).select_related("category") |  | ||||||
| 
 |  | ||||||
|         with BatchController.batch(request.user): |  | ||||||
|             available_products = set(ProductController.available_products( |  | ||||||
|                 request.user, |  | ||||||
|                 products=all_products, |  | ||||||
|             )) |  | ||||||
| 
 |  | ||||||
|             if len(available_products) == 0: |  | ||||||
|                 # We've filled in every category |  | ||||||
|                 attendee.completed_registration = True |  | ||||||
|                 attendee.save() |  | ||||||
|                 return next_step |                 return next_step | ||||||
| 
 | 
 | ||||||
|             for category in cats: |  | ||||||
|                 products = [ |  | ||||||
|                     i for i in available_products |  | ||||||
|                     if i.category == category |  | ||||||
|                 ] |  | ||||||
| 
 |  | ||||||
|                 prefix = "category_" + str(category.id) |  | ||||||
|                 p = _handle_products(request, category, products, prefix) |  | ||||||
|                 products_form, discounts, products_handled = p |  | ||||||
| 
 |  | ||||||
|                 section = GuidedRegistrationSection( |  | ||||||
|                     title=category.name, |  | ||||||
|                     description=category.description, |  | ||||||
|                     discounts=discounts, |  | ||||||
|                     form=products_form, |  | ||||||
|                 ) |  | ||||||
| 
 |  | ||||||
|                 if products: |  | ||||||
|                     # This product category has items to show. |  | ||||||
|                     sections.append(section) |  | ||||||
|                     # Add this to the list of things to show if the form |  | ||||||
|                     # errors. |  | ||||||
|                     request.session[SESSION_KEY].append(category.id) |  | ||||||
| 
 |  | ||||||
|                     if request.method == "POST" and not products_form.errors: |  | ||||||
|                         # This is only saved if we pass each form with no |  | ||||||
|                         # errors, and if the form actually has products. |  | ||||||
|                         attendee.guided_categories_complete.add(category) |  | ||||||
| 
 |  | ||||||
|     if sections and request.method == "POST": |  | ||||||
|         for section in sections: |  | ||||||
|             if section.form.errors: |  | ||||||
|                 break |  | ||||||
|         else: |  | ||||||
|             attendee.save() |  | ||||||
|             if SESSION_KEY in request.session: |  | ||||||
|                 del request.session[SESSION_KEY] |  | ||||||
|             # We've successfully processed everything |  | ||||||
|             return next_step |  | ||||||
| 
 |  | ||||||
|     data = { |     data = { | ||||||
|         "current_step": current_step, |         "current_step": page_number, | ||||||
|         "sections": sections, |         "sections": sections, | ||||||
|         "title": title, |         "title": title, | ||||||
|         "total_steps": 3, |         "total_steps": TOTAL_PAGES, | ||||||
|     } |     } | ||||||
|     return render(request, "registrasion/guided_registration.html", data) |     return render(request, "registrasion/guided_registration.html", data) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | GUIDED_MODE_TICKETS_ONLY = 2 | ||||||
|  | GUIDED_MODE_ALL_ADDITIONAL = 3 | ||||||
|  | GUIDED_MODE_EXCLUDE_COMPLETE = 4 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @login_required | ||||||
|  | def _guided_registration_products(request, mode): | ||||||
|  |     sections = [] | ||||||
|  | 
 | ||||||
|  |     SESSION_KEY = "guided_registration" | ||||||
|  |     MODE_KEY = "mode" | ||||||
|  |     CATS_KEY = "cats" | ||||||
|  | 
 | ||||||
|  |     attendee = people.Attendee.get_instance(request.user) | ||||||
|  | 
 | ||||||
|  |     # Get the next category | ||||||
|  |     cats = inventory.Category.objects.order_by("order")  # TODO: default order? | ||||||
|  | 
 | ||||||
|  |     # Fun story: If _any_ of the category forms result in an error, but other | ||||||
|  |     # new products get enabled with a flag, those new products will appear. | ||||||
|  |     # We need to make sure that we only display the products that were valid | ||||||
|  |     # in the first place. So we track them in a session, and refresh only if | ||||||
|  |     # the page number does not change. Cheap! | ||||||
|  | 
 | ||||||
|  |     if SESSION_KEY in request.session: | ||||||
|  |         session_struct = request.session[SESSION_KEY] | ||||||
|  |         old_mode = session_struct[MODE_KEY] | ||||||
|  |         old_cats = session_struct[CATS_KEY] | ||||||
|  |     else: | ||||||
|  |         old_mode = None | ||||||
|  |         old_cats = [] | ||||||
|  | 
 | ||||||
|  |     if mode == old_mode: | ||||||
|  |         cats = cats.filter(id__in=old_cats) | ||||||
|  |     elif mode == GUIDED_MODE_TICKETS_ONLY: | ||||||
|  |         cats = cats.filter(id=settings.TICKET_PRODUCT_CATEGORY) | ||||||
|  |     elif mode == GUIDED_MODE_ALL_ADDITIONAL: | ||||||
|  |         cats = cats.exclude(id=settings.TICKET_PRODUCT_CATEGORY) | ||||||
|  |     elif mode == GUIDED_MODE_EXCLUDE_COMPLETE: | ||||||
|  |         cats = cats.exclude(id=settings.TICKET_PRODUCT_CATEGORY) | ||||||
|  |         cats = cats.exclude(id__in=old_cats) | ||||||
|  | 
 | ||||||
|  |     # We update the session key at the end of this method | ||||||
|  |     # once we've found all the categories that have available products | ||||||
|  | 
 | ||||||
|  |     all_products = inventory.Product.objects.filter( | ||||||
|  |         category__in=cats, | ||||||
|  |     ).select_related("category") | ||||||
|  | 
 | ||||||
|  |     seen_categories = [] | ||||||
|  | 
 | ||||||
|  |     with BatchController.batch(request.user): | ||||||
|  |         available_products = set(ProductController.available_products( | ||||||
|  |             request.user, | ||||||
|  |             products=all_products, | ||||||
|  |         )) | ||||||
|  | 
 | ||||||
|  |         if len(available_products) == 0: | ||||||
|  |             return [] | ||||||
|  | 
 | ||||||
|  |         has_errors = False | ||||||
|  | 
 | ||||||
|  |         for category in cats: | ||||||
|  |             products = [ | ||||||
|  |                 i for i in available_products | ||||||
|  |                 if i.category == category | ||||||
|  |             ] | ||||||
|  | 
 | ||||||
|  |             prefix = "category_" + str(category.id) | ||||||
|  |             p = _handle_products(request, category, products, prefix) | ||||||
|  |             products_form, discounts, products_handled = p | ||||||
|  | 
 | ||||||
|  |             section = GuidedRegistrationSection( | ||||||
|  |                 title=category.name, | ||||||
|  |                 description=category.description, | ||||||
|  |                 discounts=discounts, | ||||||
|  |                 form=products_form, | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             if products: | ||||||
|  |                 # This product category has items to show. | ||||||
|  |                 sections.append(section) | ||||||
|  |                 seen_categories.append(category) | ||||||
|  | 
 | ||||||
|  |     # Update the cache with the newly calculated values | ||||||
|  |     cat_ids = [cat.id for cat in seen_categories] | ||||||
|  |     request.session[SESSION_KEY] = {MODE_KEY: mode, CATS_KEY: cat_ids} | ||||||
|  | 
 | ||||||
|  |     return sections | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @login_required | ||||||
|  | def _guided_registration_profile_and_voucher(request): | ||||||
|  |     voucher_form, voucher_handled = _handle_voucher(request, "voucher") | ||||||
|  |     profile_form, profile_handled = _handle_profile(request, "profile") | ||||||
|  | 
 | ||||||
|  |     voucher_section = GuidedRegistrationSection( | ||||||
|  |         title="Voucher Code", | ||||||
|  |         form=voucher_form, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     profile_section = GuidedRegistrationSection( | ||||||
|  |         title="Profile and Personal Information", | ||||||
|  |         form=profile_form, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     return [voucher_section, profile_section] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @login_required | @login_required | ||||||
| def review(request): | def review(request): | ||||||
|     ''' View for the review page. ''' |     ''' View for the review page. ''' | ||||||
|  | @ -399,6 +484,28 @@ def product_category(request, category_id): | ||||||
|     return render(request, "registrasion/product_category.html", data) |     return render(request, "registrasion/product_category.html", data) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def voucher_code(request): | ||||||
|  |     ''' A view *just* for entering a voucher form. ''' | ||||||
|  | 
 | ||||||
|  |     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 | ||||||
|  | 
 | ||||||
|  |     if voucher_handled: | ||||||
|  |         messages.success(request, "Your voucher code was accepted.") | ||||||
|  |         return redirect("dashboard") | ||||||
|  | 
 | ||||||
|  |     data = { | ||||||
|  |         "voucher_form": voucher_form, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return render(request, "registrasion/voucher_code.html", data) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def _handle_products(request, category, products, prefix): | def _handle_products(request, category, products, prefix): | ||||||
|     ''' Handles a products list form in the given request. Returns the |     ''' Handles a products list form in the given request. Returns the | ||||||
|     form instance, the discounts applicable to this form, and whether the |     form instance, the discounts applicable to this form, and whether the | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 James Polley
						James Polley