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] | ||||
| 	remote = git@gitlab.com:tchaypo/registrasion.git | ||||
| 	branch = lca2018 | ||||
| 	commit = c1e194aef92e4c06a8855fad22ca819c08736dad | ||||
| 	parent = dd8a42e9f67cfebc39347cb87bacf79046bfbfec | ||||
| 	commit = 7cf314adae9f8de1519db63828f55a10aa09f0ee | ||||
| 	parent = 7c5c6c02f20ddcbc6c54b3065311880fce586192 | ||||
| 	cmdver = 0.3.1 | ||||
|  |  | |||
|  | @ -9,6 +9,8 @@ from django.db.models import Value | |||
| 
 | ||||
| from .batch import BatchController | ||||
| 
 | ||||
| from operator import attrgetter | ||||
| 
 | ||||
| 
 | ||||
| class AllProducts(object): | ||||
|     pass | ||||
|  | @ -26,7 +28,7 @@ class CategoryController(object): | |||
|         products, otherwise it'll do all. ''' | ||||
| 
 | ||||
|         # STOPGAP -- this needs to be elsewhere tbqh | ||||
|         from registrasion.controllers.product import ProductController | ||||
|         from .product import ProductController | ||||
| 
 | ||||
|         if products is AllProducts: | ||||
|             products = inventory.Product.objects.all().select_related( | ||||
|  | @ -38,7 +40,7 @@ class CategoryController(object): | |||
|             products=products, | ||||
|         ) | ||||
| 
 | ||||
|         return set(i.category for i in available) | ||||
|         return sorted(set(i.category for i in available), key=attrgetter("order")) | ||||
| 
 | ||||
|     @classmethod | ||||
|     @BatchController.memoise | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ from django.db import transaction | |||
| 
 | ||||
| from registrasion.models import commerce | ||||
| 
 | ||||
| from registrasion.controllers.for_id import ForId | ||||
| from .for_id import ForId | ||||
| 
 | ||||
| 
 | ||||
| class CreditNoteController(ForId, object): | ||||
|  | @ -40,8 +40,8 @@ class CreditNoteController(ForId, object): | |||
|         paid. | ||||
|         ''' | ||||
| 
 | ||||
|         # Circular Import | ||||
|         from registrasion.controllers.invoice import InvoiceController | ||||
|         # Local import to fix import cycles. Can we do better? | ||||
|         from .invoice import InvoiceController | ||||
|         inv = InvoiceController(invoice) | ||||
|         inv.validate_allowed_to_pay() | ||||
| 
 | ||||
|  | @ -65,8 +65,8 @@ class CreditNoteController(ForId, object): | |||
|         a cancellation fee. Must be 0 <= percentage <= 100. | ||||
|         ''' | ||||
| 
 | ||||
|         # Circular Import | ||||
|         from registrasion.controllers.invoice import InvoiceController | ||||
|         # Local import to fix import cycles. Can we do better? | ||||
|         from .invoice import InvoiceController | ||||
| 
 | ||||
|         assert(percentage >= 0 and percentage <= 100) | ||||
| 
 | ||||
|  |  | |||
|  | @ -45,6 +45,28 @@ class FlagController(object): | |||
|         else: | ||||
|             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 | ||||
|         do_not_disable = defaultdict(lambda: True) | ||||
|         # 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 | ||||
|             # from the categories covered by this condition | ||||
| 
 | ||||
|             ids = [product.id for product in products] | ||||
| 
 | ||||
|             # TODO: This is re-evaluated a lot. | ||||
|             all_products = inventory.Product.objects.filter(id__in=ids) | ||||
|             cond = ( | ||||
|                 Q(flagbase_set=condition) | | ||||
|                 Q(category__in=condition.categories.all()) | ||||
|             condition_products = condition.products.all() | ||||
|             category_products = ( | ||||
|                 product for cat in condition.categories.all() for product in products_by_category[cat.id] | ||||
|             ) | ||||
| 
 | ||||
|             all_products = all_products.filter(cond) | ||||
|             all_products = all_products.select_related("category") | ||||
|             all_products = itertools.chain( | ||||
|                 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: | ||||
|                 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 people | ||||
| 
 | ||||
| from registrasion.controllers.cart import CartController | ||||
| from registrasion.controllers.credit_note import CreditNoteController | ||||
| from registrasion.controllers.for_id import ForId | ||||
| from .cart import CartController | ||||
| from .credit_note import CreditNoteController | ||||
| from .for_id import ForId | ||||
| 
 | ||||
| 
 | ||||
| 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 registrasion.models import commerce | ||||
| from registrasion.models import inventory | ||||
| from .controllers.product import ProductController | ||||
| from .models import commerce | ||||
| from .models import inventory | ||||
| 
 | ||||
| from django import forms | ||||
| from django.db.models import Q | ||||
|  | @ -31,12 +31,13 @@ class ApplyCreditNoteForm(forms.Form): | |||
|                 "user_email": users[invoice["user_id"]].email, | ||||
|             }) | ||||
| 
 | ||||
| 
 | ||||
|         key = lambda inv: (0 - (inv["user_id"] == self.user.id), inv["id"])  # noqa | ||||
|         invoices_annotated.sort(key=key) | ||||
| 
 | ||||
|         template = ('Invoice %(id)d - user: %(user_email)s (%(user_id)d) ' | ||||
|                     '-  $%(value)d') | ||||
|         template = ( | ||||
|             'Invoice %(id)d - user: %(user_email)s (%(user_id)d) ' | ||||
|             '-  $%(value)d' | ||||
|         ) | ||||
|         return [ | ||||
|             (invoice["id"], template % invoice) | ||||
|             for invoice in invoices_annotated | ||||
|  | @ -94,6 +95,7 @@ def ProductsForm(category, products): | |||
|         cat.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm, | ||||
|         cat.RENDER_TYPE_RADIO: _RadioButtonProductsForm, | ||||
|         cat.RENDER_TYPE_ITEM_QUANTITY: _ItemQuantityProductsForm, | ||||
|         cat.RENDER_TYPE_CHECKBOX: _CheckboxProductsForm, | ||||
|     } | ||||
| 
 | ||||
|     # 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) | ||||
| 
 | ||||
| 
 | ||||
| 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): | ||||
|     ''' Products entry form that allows users to select a product type, and | ||||
|      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] | ||||
| 
 | ||||
|         super(InvoicesWithProductAndStatusForm, self).__init__(*a, **k) | ||||
|         print(status) | ||||
| 
 | ||||
|         qs = commerce.Invoice.objects.filter( | ||||
|             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 registrasion.models.conditions import *  # NOQA | ||||
| from registrasion.models.inventory import *  # NOQA | ||||
| from registrasion.models.people import *  # NOQA | ||||
| from .commerce import *  # NOQA | ||||
| from .conditions import *  # NOQA | ||||
| from .inventory import *  # NOQA | ||||
| from .people import *  # NOQA | ||||
|  |  | |||
|  | @ -324,7 +324,6 @@ class CreditNote(PaymentBase): | |||
| 
 | ||||
|         elif hasattr(self, 'creditnoterefund'): | ||||
|             reference = self.creditnoterefund.reference | ||||
|             print(reference) | ||||
|             return "Refunded with reference: %s" % reference | ||||
| 
 | ||||
|         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 | ||||
|             all of the options. | ||||
| 
 | ||||
|             ``RENDER_TYPE_CHECKBOX`` shows a checkbox beside each product. | ||||
| 
 | ||||
|         limit_per_user (Optional[int]): This restricts the number of items | ||||
|             from this Category that each attendee may claim. This extends | ||||
|             across multiple Invoices. | ||||
|  | @ -63,11 +65,13 @@ class Category(models.Model): | |||
|     RENDER_TYPE_RADIO = 1 | ||||
|     RENDER_TYPE_QUANTITY = 2 | ||||
|     RENDER_TYPE_ITEM_QUANTITY = 3 | ||||
|     RENDER_TYPE_CHECKBOX = 4 | ||||
| 
 | ||||
|     CATEGORY_RENDER_TYPES = [ | ||||
|         (RENDER_TYPE_RADIO, _("Radio button")), | ||||
|         (RENDER_TYPE_QUANTITY, _("Quantity boxes")), | ||||
|         (RENDER_TYPE_ITEM_QUANTITY, _("Product selector and quantity box")), | ||||
|         (RENDER_TYPE_CHECKBOX, _("Checkbox button")), | ||||
|     ] | ||||
| 
 | ||||
|     name = models.CharField( | ||||
|  |  | |||
|  | @ -177,7 +177,6 @@ class Links(Report): | |||
|         return [] | ||||
| 
 | ||||
|     def rows(self, content_type): | ||||
|         print(self._links) | ||||
|         for url, link_text in self._links: | ||||
|             yield [ | ||||
|                 self._linked_text(content_type, url, link_text) | ||||
|  | @ -299,9 +298,10 @@ class ReportView(object): | |||
|         response = HttpResponse(content_type='text/csv') | ||||
| 
 | ||||
|         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(): | ||||
|             writer.writerow(row) | ||||
|             writer.writerow(list(encode(i) for i in row)) | ||||
| 
 | ||||
|         return response | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| from registrasion.reporting import forms | ||||
| from . import forms | ||||
| 
 | ||||
| import collections | ||||
| import datetime | ||||
|  | @ -24,11 +24,11 @@ from registrasion import views | |||
| 
 | ||||
| from symposion.schedule import models as schedule_models | ||||
| 
 | ||||
| from registrasion.reporting.reports import get_all_reports | ||||
| from registrasion.reporting.reports import Links | ||||
| from registrasion.reporting.reports import ListReport | ||||
| from registrasion.reporting.reports import QuerysetReport | ||||
| from registrasion.reporting.reports import report_view | ||||
| from .reports import get_all_reports | ||||
| from .reports import Links | ||||
| from .reports import ListReport | ||||
| from .reports import QuerysetReport | ||||
| from .reports import report_view | ||||
| 
 | ||||
| 
 | ||||
| def CURRENCY(): | ||||
|  | @ -95,8 +95,6 @@ def items_sold(): | |||
|         total_quantity=Sum("quantity"), | ||||
|     ) | ||||
| 
 | ||||
|     print(line_items) | ||||
| 
 | ||||
|     headings = ["Description", "Quantity", "Price", "Total"] | ||||
| 
 | ||||
|     data = [] | ||||
|  | @ -312,6 +310,55 @@ def discount_status(request, form): | |||
|     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) | ||||
| def paid_invoices_by_date(request, form): | ||||
|     ''' 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 | ||||
| 
 | ||||
|     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_, count) for date_, count in sorted(by_date.items())] | ||||
|     data = [(date_.strftime("%Y-%m-%d"), count) for date_, count in data] | ||||
| 
 | ||||
|     return ListReport( | ||||
|         "Paid Invoices By Date", | ||||
|  | @ -417,8 +464,6 @@ def attendee(request, form, user_id=None): | |||
|     if user_id is None: | ||||
|         return attendee_list(request) | ||||
| 
 | ||||
|     print(user_id) | ||||
| 
 | ||||
|     attendee = people.Attendee.objects.get(user__id=user_id) | ||||
|     name = attendee.attendeeprofilebase.attendee_name() | ||||
| 
 | ||||
|  | @ -846,9 +891,10 @@ def manifest(request, form): | |||
|     headings = ["User ID", "Name", "Paid", "Unpaid", "Refunded"] | ||||
| 
 | ||||
|     def format_items(item_list): | ||||
|         strings = [] | ||||
|         for item in item_list: | ||||
|             strings.append('%d x %s' % (item.quantity, str(item.product))) | ||||
|         strings = [ | ||||
|             '%d x %s' % (item.quantity, str(item.product)) | ||||
|             for item in item_list | ||||
|         ] | ||||
|         return ", \n".join(strings) | ||||
| 
 | ||||
|     output = [] | ||||
|  |  | |||
|  | @ -3,8 +3,9 @@ from registrasion.controllers.category import CategoryController | |||
| from registrasion.controllers.item import ItemController | ||||
| 
 | ||||
| from django import template | ||||
| from django.conf import settings | ||||
| from django.db.models import Sum | ||||
| from urllib.parse import urlencode | ||||
| from urllib import urlencode  # TODO: s/urllib/six.moves.urllib/ | ||||
| 
 | ||||
| register = template.Library() | ||||
| 
 | ||||
|  | @ -117,3 +118,69 @@ def report_as_csv(context, section): | |||
|         querystring = old_query + "&" + 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( | ||||
|                 name="Product " + str(i + 1), | ||||
|                 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"), | ||||
|                 reservation_duration=cls.RESERVATION, | ||||
|                 limit_per_user=10, | ||||
|  |  | |||
|  | @ -98,11 +98,7 @@ class InvoiceTestCase(TestHelperMixin, RegistrationCartTestCase): | |||
| 
 | ||||
|     def test_total_payments_balance_due(self): | ||||
|         invoice = self._invoice_containing_prod_1(2) | ||||
|         # range only takes int, and the following logic fails if not a round | ||||
|         # 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)): | ||||
|         for i in xrange(0, invoice.invoice.value): | ||||
|             self.assertTrue( | ||||
|                 i + 1, invoice.invoice.total_payments() | ||||
|             ) | ||||
|  |  | |||
|  | @ -67,7 +67,7 @@ class SpeakerTestCase(RegistrationCartTestCase): | |||
|             kind=kind_1, | ||||
|             title="Proposal 1", | ||||
|             abstract="Abstract", | ||||
|             private_abstract="Private Abstract", | ||||
|             description="Description", | ||||
|             speaker=speaker_1, | ||||
|         ) | ||||
|         proposal_models.AdditionalSpeaker.objects.create( | ||||
|  | @ -80,7 +80,7 @@ class SpeakerTestCase(RegistrationCartTestCase): | |||
|             kind=kind_2, | ||||
|             title="Proposal 2", | ||||
|             abstract="Abstract", | ||||
|             private_abstract="Private Abstract", | ||||
|             description="Description", | ||||
|             speaker=speaker_1, | ||||
|         ) | ||||
|         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 url | ||||
|  | @ -19,6 +19,7 @@ from .views import ( | |||
|     product_category, | ||||
|     refund, | ||||
|     review, | ||||
|     voucher_code, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -43,6 +44,7 @@ public = [ | |||
|     url(r"^profile$", edit_profile, name="attendee_edit"), | ||||
|     url(r"^register$", guided_registration, name="guided_registration"), | ||||
|     url(r"^review$", review, name="review"), | ||||
|     url(r"^voucher$", voucher_code, name="voucher_code"), | ||||
|     url(r"^register/([0-9]+)$", guided_registration, | ||||
|         name="guided_registration"), | ||||
| ] | ||||
|  | @ -55,6 +57,11 @@ reports = [ | |||
|     url(r"^attendee/([0-9]*)$", rv.attendee, name="attendee"), | ||||
|     url(r"^credit_notes/?$", rv.credit_notes, name="credit_notes"), | ||||
|     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"^invoices/?$", rv.invoices, name="invoices"), | ||||
|     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 | ||||
|     # 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) | ||||
|     return get_random_string(length=length, allowed_chars=chars) | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										303
									
								
								vendor/registrasion/registrasion/views.py
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										303
									
								
								vendor/registrasion/registrasion/views.py
									
										
									
									
										vendored
									
									
								
							|  | @ -1,19 +1,20 @@ | |||
| import datetime | ||||
| import zipfile | ||||
| 
 | ||||
| from registrasion import forms | ||||
| from registrasion import util | ||||
| from registrasion.models import commerce | ||||
| from registrasion.models import inventory | ||||
| from registrasion.models import people | ||||
| from registrasion.controllers.batch import BatchController | ||||
| from registrasion.controllers.cart import CartController | ||||
| from registrasion.controllers.credit_note import CreditNoteController | ||||
| from registrasion.controllers.discount import DiscountController | ||||
| from registrasion.controllers.invoice import InvoiceController | ||||
| from registrasion.controllers.item import ItemController | ||||
| from registrasion.controllers.product import ProductController | ||||
| from registrasion.exceptions import CartValidationError | ||||
| from . import forms | ||||
| from . import util | ||||
| from .models import commerce | ||||
| from .models import inventory | ||||
| from .models import people | ||||
| from .controllers.batch import BatchController | ||||
| from .controllers.cart import CartController | ||||
| from .controllers.category import CategoryController | ||||
| from .controllers.credit_note import CreditNoteController | ||||
| from .controllers.discount import DiscountController | ||||
| from .controllers.invoice import InvoiceController | ||||
| from .controllers.item import ItemController | ||||
| from .controllers.product import ProductController | ||||
| from .exceptions import CartValidationError | ||||
| 
 | ||||
| from collections import namedtuple | ||||
| 
 | ||||
|  | @ -64,12 +65,19 @@ class GuidedRegistrationSection(_GuidedRegistrationSection): | |||
| 
 | ||||
| 
 | ||||
| @login_required | ||||
| def guided_registration(request): | ||||
| def guided_registration(request, page_number=None): | ||||
|     ''' Goes through the registration process in order, making sure user sees | ||||
|     all valid categories. | ||||
| 
 | ||||
|     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: | ||||
|         render: Renders ``registrasion/guided_registration.html``, | ||||
|             with the following data:: | ||||
|  | @ -85,87 +93,167 @@ def guided_registration(request): | |||
| 
 | ||||
|     ''' | ||||
| 
 | ||||
|     SESSION_KEY = "guided_registration_categories" | ||||
|     ASK_FOR_PROFILE = 777  # Magic number. Meh. | ||||
|     PAGE_PROFILE = 1 | ||||
|     PAGE_TICKET = 2 | ||||
|     PAGE_PRODUCTS = 3 | ||||
|     PAGE_PRODUCTS_MAX = 4 | ||||
|     TOTAL_PAGES = 4 | ||||
| 
 | ||||
|     next_step = redirect("guided_registration") | ||||
| 
 | ||||
|     sections = [] | ||||
|     ticket_category = inventory.Category.objects.get( | ||||
|         id=settings.TICKET_PRODUCT_CATEGORY | ||||
|     ) | ||||
|     cart = CartController.for_user(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: | ||||
|         return redirect(review) | ||||
| 
 | ||||
|     # Step 1: Fill in a badge and collect a voucher code | ||||
|     try: | ||||
|         profile = attendee.attendeeprofilebase | ||||
|     except ObjectDoesNotExist: | ||||
|         profile = None | ||||
| 
 | ||||
|     # 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 | ||||
|     # Calculate the current maximum page number for this user. | ||||
|     has_profile = hasattr(attendee, "attendeeprofilebase") | ||||
|     if not has_profile: | ||||
|         # If there's no profile, they have to go to the profile page. | ||||
|         max_page = PAGE_PROFILE | ||||
|         redirect_page = PAGE_PROFILE | ||||
|     else: | ||||
|         if request.session[SESSION_KEY] == ASK_FOR_PROFILE: | ||||
|             show_profile_and_voucher = True | ||||
| 
 | ||||
|     if show_profile_and_voucher: | ||||
|         # 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, | ||||
|         # We have a profile. | ||||
|         # Do they have a ticket? | ||||
|         products = inventory.Product.objects.filter( | ||||
|             productitem__cart=cart.cart | ||||
|         ) | ||||
|         products = products.filter(category=ticket_category) | ||||
| 
 | ||||
|         profile_section = GuidedRegistrationSection( | ||||
|             title="Profile and Personal Information", | ||||
|             form=profile_form, | ||||
|         if products.count() == 0: | ||||
|             # If no ticket, they can only see the profile or ticket page. | ||||
|             max_page = PAGE_TICKET | ||||
|             redirect_page = PAGE_TICKET | ||||
|         else: | ||||
|             # If there's a ticket, they should *see* the general products page# | ||||
|             # 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" | ||||
|         current_step = 1 | ||||
|         sections.append(voucher_section) | ||||
|         sections.append(profile_section) | ||||
|     else: | ||||
|         # We're selling products | ||||
|             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 | ||||
|             ) | ||||
| 
 | ||||
|         starting = attendee.guided_categories_complete.count() == 0 | ||||
|         if not sections: | ||||
|             # We've filled in every category | ||||
|             attendee.completed_registration = True | ||||
|             attendee.save() | ||||
|             return redirect("review") | ||||
| 
 | ||||
|         if sections and request.method == "POST": | ||||
|             for section in sections: | ||||
|                 if section.form.errors: | ||||
|                     break | ||||
|             else: | ||||
|                 # We've successfully processed everything | ||||
|                 return next_step | ||||
| 
 | ||||
|     data = { | ||||
|         "current_step": page_number, | ||||
|         "sections": sections, | ||||
|         "title": title, | ||||
|         "total_steps": TOTAL_PAGES, | ||||
|     } | ||||
|     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 | ||||
|     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: | ||||
|             _cats = request.session[SESSION_KEY] | ||||
|             cats = cats.filter(id__in=_cats) | ||||
|         session_struct = request.session[SESSION_KEY] | ||||
|         old_mode = session_struct[MODE_KEY] | ||||
|         old_cats = session_struct[CATS_KEY] | ||||
|     else: | ||||
|             cats = cats.exclude( | ||||
|                 id__in=attendee.guided_categories_complete.all(), | ||||
|             ) | ||||
|         old_mode = None | ||||
|         old_cats = [] | ||||
| 
 | ||||
|         cats = cats.order_by("order") | ||||
|     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) | ||||
| 
 | ||||
|         request.session[SESSION_KEY] = [] | ||||
| 
 | ||||
|         if starting: | ||||
|             # Only display the first Category | ||||
|             title = "Select ticket type" | ||||
|             current_step = 2 | ||||
|             cats = [cats[0]] | ||||
|         else: | ||||
|             # Set title appropriately for remaining categories | ||||
|             current_step = 3 | ||||
|             title = "Additional items" | ||||
|     # 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, | ||||
|  | @ -173,10 +261,9 @@ def guided_registration(request): | |||
|         )) | ||||
| 
 | ||||
|         if len(available_products) == 0: | ||||
|                 # We've filled in every category | ||||
|                 attendee.completed_registration = True | ||||
|                 attendee.save() | ||||
|                 return next_step | ||||
|             return [] | ||||
| 
 | ||||
|         has_errors = False | ||||
| 
 | ||||
|         for category in cats: | ||||
|             products = [ | ||||
|  | @ -198,33 +285,31 @@ def guided_registration(request): | |||
|             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) | ||||
|                 seen_categories.append(category) | ||||
| 
 | ||||
|                     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) | ||||
|     # 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} | ||||
| 
 | ||||
|     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 | ||||
|     return sections | ||||
| 
 | ||||
|     data = { | ||||
|         "current_step": current_step, | ||||
|         "sections": sections, | ||||
|         "title": title, | ||||
|         "total_steps": 3, | ||||
|     } | ||||
|     return render(request, "registrasion/guided_registration.html", data) | ||||
| 
 | ||||
| @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 | ||||
|  | @ -399,6 +484,28 @@ def product_category(request, category_id): | |||
|     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): | ||||
|     ''' Handles a products list form in the given request. Returns the | ||||
|     form instance, the discounts applicable to this form, and whether the | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 James Polley
						James Polley