Merge branch 'guided_registration'

This commit is contained in:
Christopher Neugebauer 2016-03-25 14:34:36 +11:00
commit 4069d4bb32
17 changed files with 453 additions and 30 deletions

View file

@ -211,6 +211,7 @@ class CartController(object):
# Get the count of past uses of this discount condition
# as this affects the total amount we're allowed to use now.
past_uses = rego.DiscountItem.objects.filter(
cart__user=self.cart.user,
discount=discount.discount,
)
agg = past_uses.aggregate(Sum("quantity"))

View file

@ -1,5 +1,7 @@
import models as rego
from controllers.product import ProductController
from django import forms
@ -34,6 +36,14 @@ def CategoryForm(category):
''' Removes a given product from this form. '''
del self.fields[field_name(product)]
def disable_products_for_user(self, user):
for product in products:
# Remove fields that do not have an enabling condition.
prod = ProductController(product)
if not prod.can_add_with_enabling_conditions(user, 0):
self.disable_product(product)
products = rego.Product.objects.filter(category=category).order_by("order")
for product in products:
@ -47,9 +57,18 @@ def CategoryForm(category):
return _CategoryForm
class ProfileForm(forms.ModelForm):
''' A form for requesting badge and profile information. '''
class Meta:
model = rego.BadgeAndProfile
exclude = ['attendee']
class VoucherForm(forms.Form):
voucher = forms.CharField(
label="Voucher code",
help_text="If you have a voucher code, enter it here",
required=True,
required=False,
)

View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registrasion', '0001_squashed_0002_auto_20160304_1723'),
]
operations = [
migrations.AddField(
model_name='badge',
name='accessibility_requirements',
field=models.CharField(max_length=256, blank=True),
),
migrations.AddField(
model_name='badge',
name='dietary_requirements',
field=models.CharField(max_length=256, blank=True),
),
migrations.AddField(
model_name='badge',
name='free_text_1',
field=models.CharField(help_text="A line of free text that will appear on your badge. Use this for your Twitter handle, IRC nick, your preferred pronouns or anything else you'd like people to see on your badge.", max_length=64, verbose_name='Free text line 1', blank=True),
),
migrations.AddField(
model_name='badge',
name='free_text_2',
field=models.CharField(max_length=64, verbose_name='Free text line 2', blank=True),
),
migrations.AddField(
model_name='badge',
name='gender',
field=models.CharField(max_length=64, blank=True),
),
migrations.AddField(
model_name='badge',
name='of_legal_age',
field=models.BooleanField(default=False, verbose_name='18+?'),
),
migrations.AlterField(
model_name='badge',
name='company',
field=models.CharField(help_text="The name of your company, as you'd like it on your badge", max_length=64, blank=True),
),
migrations.AlterField(
model_name='badge',
name='name',
field=models.CharField(help_text="Your name, as you'd like it on your badge", max_length=64),
),
]

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registrasion', '0002_auto_20160323_2029'),
]
operations = [
migrations.AddField(
model_name='badge',
name='name_per_invoice',
field=models.CharField(help_text="If your legal name is different to the name on your badge, fill this in, and we'll put it on your invoice. Otherwise, leave it blank.", max_length=64, verbose_name='Your legal name (for invoicing purposes)', blank=True),
),
migrations.AlterField(
model_name='badge',
name='name',
field=models.CharField(help_text="Your name, as you'd like it to appear on your badge. ", max_length=64, verbose_name='Your name (for your conference nametag)'),
),
]

View file

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('registrasion', '0003_auto_20160323_2044'),
]
operations = [
migrations.CreateModel(
name='Attendee',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('completed_registration', models.BooleanField(default=False)),
('highest_complete_category', models.IntegerField(default=0)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='BadgeAndProfile',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(help_text="Your name, as you'd like it to appear on your badge. ", max_length=64, verbose_name='Your name (for your conference nametag)')),
('company', models.CharField(help_text="The name of your company, as you'd like it on your badge", max_length=64, blank=True)),
('free_text_1', models.CharField(help_text="A line of free text that will appear on your badge. Use this for your Twitter handle, IRC nick, your preferred pronouns or anything else you'd like people to see on your badge.", max_length=64, verbose_name='Free text line 1', blank=True)),
('free_text_2', models.CharField(max_length=64, verbose_name='Free text line 2', blank=True)),
('name_per_invoice', models.CharField(help_text="If your legal name is different to the name on your badge, fill this in, and we'll put it on your invoice. Otherwise, leave it blank.", max_length=64, verbose_name='Your legal name (for invoicing purposes)', blank=True)),
('of_legal_age', models.BooleanField(default=False, verbose_name='18+?')),
('dietary_requirements', models.CharField(max_length=256, blank=True)),
('accessibility_requirements', models.CharField(max_length=256, blank=True)),
('gender', models.CharField(max_length=64, blank=True)),
('profile', models.OneToOneField(to='registrasion.Attendee')),
],
),
migrations.RemoveField(
model_name='badge',
name='profile',
),
migrations.RemoveField(
model_name='profile',
name='user',
),
migrations.DeleteModel(
name='Badge',
),
migrations.DeleteModel(
name='Profile',
),
]

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registrasion', '0004_auto_20160323_2137'),
]
operations = [
migrations.RenameField(
model_name='badgeandprofile',
old_name='profile',
new_name='attendee',
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registrasion', '0005_auto_20160323_2141'),
]
operations = [
migrations.AddField(
model_name='category',
name='required',
field=models.BooleanField(default=False),
preserve_default=False,
),
]

View file

@ -3,6 +3,7 @@ from __future__ import unicode_literals
import datetime
from django.core.exceptions import ValidationError
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import User
from django.db import models
from django.db.models import F, Q
@ -15,29 +16,100 @@ from model_utils.managers import InheritanceManager
# User models
@python_2_unicode_compatible
class Profile(models.Model):
class Attendee(models.Model):
''' Miscellaneous user-related data. '''
def __str__(self):
return "%s" % self.user
@staticmethod
def get_instance(user):
''' Returns the instance of attendee for the given user, or creates
a new one. '''
attendees = Attendee.objects.filter(user=user)
if len(attendees) > 0:
return attendees[0]
else:
attendee = Attendee(user=user)
attendee.save()
return attendee
user = models.OneToOneField(User, on_delete=models.CASCADE)
# Badge is linked
# Badge/profile is linked
completed_registration = models.BooleanField(default=False)
highest_complete_category = models.IntegerField(default=0)
@python_2_unicode_compatible
class Badge(models.Model):
''' Information for an attendee's badge. '''
class BadgeAndProfile(models.Model):
''' Information for an attendee's badge and related preferences '''
def __str__(self):
return "Badge for: %s of %s" % (self.name, self.company)
profile = models.OneToOneField(Profile, on_delete=models.CASCADE)
@staticmethod
def get_instance(attendee):
''' Returns either None, or the instance that belongs
to this attendee. '''
try:
return BadgeAndProfile.objects.get(attendee=attendee)
except ObjectDoesNotExist:
return None
name = models.CharField(max_length=256)
company = models.CharField(max_length=256)
attendee = models.OneToOneField(Attendee, on_delete=models.CASCADE)
# Things that appear on badge
name = models.CharField(
verbose_name="Your name (for your conference nametag)",
max_length=64,
help_text="Your name, as you'd like it to appear on your badge. ",
)
company = models.CharField(
max_length=64,
help_text="The name of your company, as you'd like it on your badge",
blank=True,
)
free_text_1 = models.CharField(
max_length=64,
verbose_name="Free text line 1",
help_text="A line of free text that will appear on your badge. Use "
"this for your Twitter handle, IRC nick, your preferred "
"pronouns or anything else you'd like people to see on "
"your badge.",
blank=True,
)
free_text_2 = models.CharField(
max_length=64,
verbose_name="Free text line 2",
blank=True,
)
# Other important Information
name_per_invoice = models.CharField(
verbose_name="Your legal name (for invoicing purposes)",
max_length=64,
help_text="If your legal name is different to the name on your badge, "
"fill this in, and we'll put it on your invoice. Otherwise, "
"leave it blank.",
blank=True,
)
of_legal_age = models.BooleanField(
default=False,
verbose_name="18+?",
blank=True,
)
dietary_requirements = models.CharField(
max_length=256,
blank=True,
)
accessibility_requirements = models.CharField(
max_length=256,
blank=True,
)
gender = models.CharField(
max_length=64,
blank=True,
)
# Inventory Models
@ -60,6 +132,7 @@ class Category(models.Model):
name = models.CharField(max_length=65, verbose_name=_("Name"))
description = models.CharField(max_length=255,
verbose_name=_("Description"))
required = models.BooleanField(blank=True)
order = models.PositiveIntegerField(verbose_name=("Display order"))
render_type = models.IntegerField(choices=CATEGORY_RENDER_TYPES,
verbose_name=_("Render type"))

View file

@ -33,5 +33,19 @@
</tr>
</table>
<table>
<tr>
<th>Payment time</th>
<th>Reference</th>
<th>Amount</th>
</tr>
{% for payment in invoice.payment_set.all %}
<tr>
<td>{{payment.time}}</td>
<td>{{payment.reference}}</td>
<td>{{payment.amount}}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -0,0 +1,22 @@
<!--- Sample template. Move elsewhere once it's ready to go. -->
{% extends "site_base.html" %}
{% block body %}
<h1>Attendee Profile</h1>
<p>Something something fill in your attendee details here!</p>
<form method="post" action="">
{% csrf_token %}
<table>
{{ form }}
</table>
<input type="submit">
</form>
{% endblock %}

View file

View file

@ -0,0 +1,10 @@
from registrasion import models as rego
from django import template
register = template.Library()
@register.assignment_tag(takes_context=True)
def available_categories(context):
''' Returns all of the available product categories '''
return rego.Category.objects.all()

View file

@ -37,6 +37,7 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
description="This is a test category",
order=10,
render_type=rego.Category.RENDER_TYPE_RADIO,
required=False,
)
cls.CAT_1.save()
@ -45,6 +46,7 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
description="This is a test category",
order=10,
render_type=rego.Category.RENDER_TYPE_RADIO,
required=False,
)
cls.CAT_2.save()

View file

@ -200,3 +200,17 @@ class DiscountTestCase(RegistrationCartTestCase):
# There is one discount, and it should apply to the more expensive.
self.assertEqual(1, len(discount_items))
self.assertEqual(self.PROD_3, discount_items[0].product)
def test_discount_quantity_is_per_user(self):
self.add_discount_prod_1_includes_cat_2(quantity=1)
# Both users should be able to apply the same discount
# in the same way
for user in (self.USER_1, self.USER_2):
cart = CartController.for_user(user)
cart.add_to_cart(self.PROD_1, 1) # Enable the discount
cart.add_to_cart(self.PROD_3, 1)
discount_items = list(cart.cart.discountitem_set.all())
# The discount is applied.
self.assertEqual(1, len(discount_items))

View file

@ -6,4 +6,7 @@ urlpatterns = patterns(
url(r"^checkout$", "checkout", name="checkout"),
url(r"^invoice/([0-9]+)$", "invoice", name="invoice"),
url(r"^invoice/([0-9]+)/pay$", "pay_invoice", name="pay_invoice"),
url(r"^profile$", "edit_profile", name="profile"),
url(r"^register$", "guided_registration", name="guided_registration"),
url(r"^register/([0-9]+)$", "guided_registration", name="guided_registration"),
)

View file

@ -12,9 +12,81 @@ from django.shortcuts import redirect
from django.shortcuts import render
@login_required
def guided_registration(request, page_id=0):
''' Goes through the registration process in order,
making sure user sees all valid categories.
WORK IN PROGRESS: the finalised version of this view will allow
grouping of categories into a specific page. Currently, it just goes
through each category one by one
'''
dashboard = redirect("dashboard")
next_step = redirect("guided_registration")
attendee = rego.Attendee.get_instance(request.user)
if attendee.completed_registration:
return dashboard
# Step 1: Fill in a badge
profile = rego.BadgeAndProfile.get_instance(attendee)
if profile is None:
ret = edit_profile(request)
profile_new = rego.BadgeAndProfile.get_instance(attendee)
if profile_new is None:
# No new profile was created
return ret
else:
return next_step
# Step 2: Go through each of the categories in order
category = attendee.highest_complete_category
# Get the next category
cats = rego.Category.objects
cats = cats.filter(id__gt=category).order_by("order")
if len(cats) == 0:
# We've filled in every category
attendee.completed_registration = True
attendee.save()
return dashboard
ret = product_category(request, cats[0].id)
attendee_new = rego.Attendee.get_instance(request.user)
if attendee_new.highest_complete_category == category:
# We've not yet completed this category
return ret
else:
return next_step
@login_required
def edit_profile(request):
attendee = rego.Attendee.get_instance(request.user)
try:
profile = rego.BadgeAndProfile.objects.get(attendee=attendee)
except ObjectDoesNotExist:
profile = None
form = forms.ProfileForm(request.POST or None, instance=profile)
if request.POST and form.is_valid():
form.instance.attendee = attendee
form.save()
data = {
"form": form,
}
return render(request, "profile_form.html", data)
@login_required
def product_category(request, category_id):
''' Registration selections form for a specific category of items '''
''' Registration selections form for a specific category of items.
'''
PRODUCTS_FORM_PREFIX = "products"
VOUCHERS_FORM_PREFIX = "vouchers"
@ -25,14 +97,17 @@ def product_category(request, category_id):
CategoryForm = forms.CategoryForm(category)
attendee = rego.Attendee.get_instance(request.user)
products = rego.Product.objects.filter(category=category)
products = products.order_by("order")
if request.method == "POST":
cat_form = CategoryForm(request.POST, request.FILES, prefix=PRODUCTS_FORM_PREFIX)
cat_form.disable_products_for_user(request.user)
voucher_form = forms.VoucherForm(request.POST, prefix=VOUCHERS_FORM_PREFIX)
if voucher_form.is_valid():
if voucher_form.is_valid() and voucher_form.cleaned_data["voucher"].strip():
# Apply voucher
# leave
voucher = voucher_form.cleaned_data["voucher"]
@ -40,23 +115,35 @@ def product_category(request, category_id):
current_cart.apply_voucher(voucher)
except Exception as e:
voucher_form.add_error("voucher", e)
# Re-visit current page.
elif cat_form.is_valid():
try:
with transaction.atomic():
for product_id, quantity, field_name \
in cat_form.product_quantities():
product = rego.Product.objects.get(pk=product_id)
try:
current_cart.set_quantity(
product, quantity, batched=True)
except ValidationError as ve:
cat_form.add_error(field_name, ve)
if cat_form.errors:
raise ValidationError("Cannot add that stuff")
current_cart.end_batch()
handle_valid_cat_form(cat_form, current_cart)
except ValidationError as ve:
pass
# If category is required, the user must have at least one
# in an active+valid cart
if category.required:
carts = rego.Cart.reserved_carts()
carts = carts.filter(user=request.user)
items = rego.ProductItem.objects.filter(
product__category=category,
cart=carts,
)
if len(items) == 0:
cat_form.add_error(
None,
"You must have at least one item from this category",
)
if not cat_form.errors:
if category_id > attendee.highest_complete_category:
attendee.highest_complete_category = category_id
attendee.save()
return redirect("dashboard")
else:
# Create initial data for each of products in category
items = rego.ProductItem.objects.filter(
@ -75,15 +162,10 @@ def product_category(request, category_id):
initial = CategoryForm.initial_data(quantities)
cat_form = CategoryForm(prefix=PRODUCTS_FORM_PREFIX, initial=initial)
cat_form.disable_products_for_user(request.user)
voucher_form = forms.VoucherForm(prefix=VOUCHERS_FORM_PREFIX)
for product in products:
# Remove fields that do not have an enabling condition.
prod = ProductController(product)
if not prod.can_add_with_enabling_conditions(request.user, 0):
cat_form.disable_product(product)
data = {
"category": category,
@ -93,6 +175,17 @@ def product_category(request, category_id):
return render(request, "product_category.html", data)
@transaction.atomic
def handle_valid_cat_form(cat_form, current_cart):
for product_id, quantity, field_name in cat_form.product_quantities():
product = rego.Product.objects.get(pk=product_id)
try:
current_cart.set_quantity(product, quantity, batched=True)
except ValidationError as ve:
cat_form.add_error(field_name, ve)
if cat_form.errors:
raise ValidationError("Cannot add that stuff")
current_cart.end_batch()
@login_required
def checkout(request):
@ -129,7 +222,7 @@ def pay_invoice(request, invoice_id):
invoice_id = int(invoice_id)
inv = rego.Invoice.objects.get(pk=invoice_id)
current_invoice = InvoiceController(inv)
if not inv.paid and not current_invoice.is_valid():
if not inv.paid and current_invoice.is_valid():
current_invoice.pay("Demo invoice payment", inv.value)
return redirect("invoice", current_invoice.invoice.id)