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]
remote = git@gitlab.com:tchaypo/registrasion.git
branch = lca2018
commit = c1e194aef92e4c06a8855fad22ca819c08736dad
parent = dd8a42e9f67cfebc39347cb87bacf79046bfbfec
commit = 7cf314adae9f8de1519db63828f55a10aa09f0ee
parent = 7c5c6c02f20ddcbc6c54b3065311880fce586192
cmdver = 0.3.1

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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,148 +93,225 @@ 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,
)
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)
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:
cats = cats.exclude(
id__in=attendee.guided_categories_complete.all(),
# 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"
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 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(
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()
if sections and request.method == "POST":
for section in sections:
if section.form.errors:
break
else:
# We've successfully processed everything
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 = {
"current_step": current_step,
"current_step": page_number,
"sections": sections,
"title": title,
"total_steps": 3,
"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.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
def review(request):
''' View for the review page. '''
@ -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