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…
Reference in a new issue