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:
James Polley 2017-09-29 23:30:11 +10:00
parent 2580584597
commit 162b5edc20
19 changed files with 477 additions and 211 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = [
]

View file

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

View file

@ -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.")

View file

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

View file

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

View file

@ -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 = []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,87 +93,167 @@ 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
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" title = "Attendee information"
current_step = 1 sections = _guided_registration_profile_and_voucher(request)
sections.append(voucher_section) elif page_number == PAGE_TICKET:
sections.append(profile_section) # Select ticket
else: title = "Select ticket type"
# We're selling products 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 # 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: if SESSION_KEY in request.session:
_cats = request.session[SESSION_KEY] session_struct = request.session[SESSION_KEY]
cats = cats.filter(id__in=_cats) old_mode = session_struct[MODE_KEY]
old_cats = session_struct[CATS_KEY]
else: else:
cats = cats.exclude( old_mode = None
id__in=attendee.guided_categories_complete.all(), 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] = [] # We update the session key at the end of this method
# once we've found all the categories that have available products
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"
all_products = inventory.Product.objects.filter( all_products = inventory.Product.objects.filter(
category__in=cats, category__in=cats,
).select_related("category") ).select_related("category")
seen_categories = []
with BatchController.batch(request.user): with BatchController.batch(request.user):
available_products = set(ProductController.available_products( available_products = set(ProductController.available_products(
request.user, request.user,
@ -173,10 +261,9 @@ def guided_registration(request):
)) ))
if len(available_products) == 0: if len(available_products) == 0:
# We've filled in every category return []
attendee.completed_registration = True
attendee.save() has_errors = False
return next_step
for category in cats: for category in cats:
products = [ products = [
@ -198,33 +285,31 @@ def guided_registration(request):
if products: if products:
# This product category has items to show. # This product category has items to show.
sections.append(section) sections.append(section)
# Add this to the list of things to show if the form seen_categories.append(category)
# errors.
request.session[SESSION_KEY].append(category.id)
if request.method == "POST" and not products_form.errors: # Update the cache with the newly calculated values
# This is only saved if we pass each form with no cat_ids = [cat.id for cat in seen_categories]
# errors, and if the form actually has products. request.session[SESSION_KEY] = {MODE_KEY: mode, CATS_KEY: cat_ids}
attendee.guided_categories_complete.add(category)
if sections and request.method == "POST": return sections
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 = {
"current_step": current_step, @login_required
"sections": sections, def _guided_registration_profile_and_voucher(request):
"title": title, voucher_form, voucher_handled = _handle_voucher(request, "voucher")
"total_steps": 3, profile_form, profile_handled = _handle_profile(request, "profile")
}
return render(request, "registrasion/guided_registration.html", data) 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
@ -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