From 7086ea87292dc5ac8c5d101b998599493163e202 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Wed, 23 Mar 2016 19:36:22 +1100 Subject: [PATCH 01/11] Moves product disabling code into the form class --- registrasion/forms.py | 10 ++++++++++ registrasion/views.py | 8 ++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/registrasion/forms.py b/registrasion/forms.py index 54b3115a..5027e068 100644 --- a/registrasion/forms.py +++ b/registrasion/forms.py @@ -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: diff --git a/registrasion/views.py b/registrasion/views.py index 3957da7d..f7d0f9c5 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -30,6 +30,7 @@ def product_category(request, category_id): 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(): @@ -75,15 +76,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, From d50d6bac482c852cd343f9bc3ac2e0f94fa6bfc2 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Wed, 23 Mar 2016 19:36:54 +1100 Subject: [PATCH 02/11] Fixes voucher handling form to not be compulsory --- registrasion/forms.py | 2 +- registrasion/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/registrasion/forms.py b/registrasion/forms.py index 5027e068..5cada77a 100644 --- a/registrasion/forms.py +++ b/registrasion/forms.py @@ -61,5 +61,5 @@ 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, ) diff --git a/registrasion/views.py b/registrasion/views.py index f7d0f9c5..5aefbfeb 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -33,7 +33,7 @@ def product_category(request, category_id): 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"] From eb530bd4855ee14344ebd238dcd4a2e89ebb73bb Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Wed, 23 Mar 2016 19:39:07 +1100 Subject: [PATCH 03/11] =?UTF-8?q?Adds=20the=20first=20pass=20at=20a=20?= =?UTF-8?q?=E2=80=9Cguided=E2=80=9D=20registration=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- registrasion/urls.py | 2 ++ registrasion/views.py | 64 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/registrasion/urls.py b/registrasion/urls.py index 746d163d..c19b6ab8 100644 --- a/registrasion/urls.py +++ b/registrasion/urls.py @@ -2,6 +2,8 @@ from django.conf.urls import url, patterns urlpatterns = patterns( "registrasion.views", + url(r"^register$", "guided_registration", name="guided_registration"), + url(r"^register/([0-9]+)$", "guided_registration", name="guided_registration"), url(r"^category/([0-9]+)$", "product_category", name="product_category"), url(r"^checkout$", "checkout", name="checkout"), url(r"^invoice/([0-9]+)$", "invoice", name="invoice"), diff --git a/registrasion/views.py b/registrasion/views.py index 5aefbfeb..21e41970 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -12,9 +12,45 @@ 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, page_id simply + refers to the category_id. Future versions will have pages containing + categories. + ''' + + page_id = int(page_id) + if page_id != 0: + ret = product_category_inner(request, page_id) + if ret is not True: + return ret + + # Go to next page in the guided registration + cats = rego.Category.objects + cats = cats.filter(id__gt=page_id).order_by("order") + + if len(cats) > 0: + return redirect("guided_registration", cats[0].id) + else: + return redirect("dashboard") + @login_required def product_category(request, category_id): - ''' Registration selections form for a specific category of items ''' + ret = product_category_inner(request, category_id) + if ret is not True: + return ret + else: + return redirect("dashboard") + +def product_category_inner(request, category_id): + ''' Registration selections form for a specific category of items. + It returns a rendered template if this page needs to display stuff, + otherwise it returns True. + ''' PRODUCTS_FORM_PREFIX = "products" VOUCHERS_FORM_PREFIX = "vouchers" @@ -41,20 +77,11 @@ 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) + return True except ValidationError as ve: pass @@ -89,6 +116,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): From 236c61eefa0da3a3fc7d6dbe2a46e04f3adf384b Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 24 Mar 2016 11:33:11 +1100 Subject: [PATCH 04/11] Fleshes out badge model, and adds first pass at display of the badge form --- registrasion/forms.py | 9 +++ .../migrations/0002_auto_20160323_2029.py | 54 ++++++++++++++++ .../migrations/0003_auto_20160323_2044.py | 24 +++++++ .../migrations/0004_auto_20160323_2137.py | 55 ++++++++++++++++ .../migrations/0005_auto_20160323_2141.py | 19 ++++++ registrasion/models.py | 64 +++++++++++++++++-- registrasion/templates/profile_form.html | 22 +++++++ registrasion/urls.py | 5 +- registrasion/views.py | 9 +++ 9 files changed, 252 insertions(+), 9 deletions(-) create mode 100644 registrasion/migrations/0002_auto_20160323_2029.py create mode 100644 registrasion/migrations/0003_auto_20160323_2044.py create mode 100644 registrasion/migrations/0004_auto_20160323_2137.py create mode 100644 registrasion/migrations/0005_auto_20160323_2141.py create mode 100644 registrasion/templates/profile_form.html diff --git a/registrasion/forms.py b/registrasion/forms.py index 5cada77a..df363d7a 100644 --- a/registrasion/forms.py +++ b/registrasion/forms.py @@ -57,6 +57,15 @@ 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", diff --git a/registrasion/migrations/0002_auto_20160323_2029.py b/registrasion/migrations/0002_auto_20160323_2029.py new file mode 100644 index 00000000..491c6865 --- /dev/null +++ b/registrasion/migrations/0002_auto_20160323_2029.py @@ -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), + ), + ] diff --git a/registrasion/migrations/0003_auto_20160323_2044.py b/registrasion/migrations/0003_auto_20160323_2044.py new file mode 100644 index 00000000..931dc77f --- /dev/null +++ b/registrasion/migrations/0003_auto_20160323_2044.py @@ -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)'), + ), + ] diff --git a/registrasion/migrations/0004_auto_20160323_2137.py b/registrasion/migrations/0004_auto_20160323_2137.py new file mode 100644 index 00000000..e6514b59 --- /dev/null +++ b/registrasion/migrations/0004_auto_20160323_2137.py @@ -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', + ), + ] diff --git a/registrasion/migrations/0005_auto_20160323_2141.py b/registrasion/migrations/0005_auto_20160323_2141.py new file mode 100644 index 00000000..26124f7d --- /dev/null +++ b/registrasion/migrations/0005_auto_20160323_2141.py @@ -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', + ), + ] diff --git a/registrasion/models.py b/registrasion/models.py index 96ecf219..1a8df463 100644 --- a/registrasion/models.py +++ b/registrasion/models.py @@ -15,29 +15,79 @@ 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 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) + attendee = models.OneToOneField(Attendee, on_delete=models.CASCADE) - name = models.CharField(max_length=256) - company = models.CharField(max_length=256) + # 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 diff --git a/registrasion/templates/profile_form.html b/registrasion/templates/profile_form.html new file mode 100644 index 00000000..56f3010c --- /dev/null +++ b/registrasion/templates/profile_form.html @@ -0,0 +1,22 @@ + + +{% extends "site_base.html" %} +{% block body %} + +

Attendee Profile

+ +

Something something fill in your attendee details here!

+ +
+ {% csrf_token %} + + + {{ form }} +
+ + + +
+ + +{% endblock %} diff --git a/registrasion/urls.py b/registrasion/urls.py index c19b6ab8..ec0229e1 100644 --- a/registrasion/urls.py +++ b/registrasion/urls.py @@ -2,10 +2,11 @@ from django.conf.urls import url, patterns urlpatterns = patterns( "registrasion.views", - url(r"^register$", "guided_registration", name="guided_registration"), - url(r"^register/([0-9]+)$", "guided_registration", name="guided_registration"), url(r"^category/([0-9]+)$", "product_category", name="product_category"), 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$", "profile", name="profile"), + url(r"^register$", "guided_registration", name="guided_registration"), + url(r"^register/([0-9]+)$", "guided_registration", name="guided_registration"), ) diff --git a/registrasion/views.py b/registrasion/views.py index 21e41970..976a3e59 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -38,6 +38,15 @@ def guided_registration(request, page_id=0): else: return redirect("dashboard") +@login_required +def profile(request): + + form = forms.ProfileForm() + data = { + "form": form, + } + return render(request, "profile_form.html", data) + @login_required def product_category(request, category_id): ret = product_category_inner(request, category_id) From 05923a9a8f588fa08e9a01b01d1b29e6d1e8887a Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 24 Mar 2016 12:58:23 +1100 Subject: [PATCH 05/11] Profile form view now edits the relevant form --- registrasion/models.py | 12 ++++++++++++ registrasion/urls.py | 2 +- registrasion/views.py | 15 +++++++++++++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/registrasion/models.py b/registrasion/models.py index 1a8df463..2cb9ad63 100644 --- a/registrasion/models.py +++ b/registrasion/models.py @@ -21,6 +21,18 @@ class Attendee(models.Model): 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/profile is linked completed_registration = models.BooleanField(default=False) diff --git a/registrasion/urls.py b/registrasion/urls.py index ec0229e1..01946839 100644 --- a/registrasion/urls.py +++ b/registrasion/urls.py @@ -6,7 +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$", "profile", name="profile"), + 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"), ) diff --git a/registrasion/views.py b/registrasion/views.py index 976a3e59..5099e701 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -39,9 +39,20 @@ def guided_registration(request, page_id=0): return redirect("dashboard") @login_required -def profile(request): +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() - form = forms.ProfileForm() data = { "form": form, } From dcad2d5f7cb2e58c65c3b747f7ebb631acd32a15 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 24 Mar 2016 13:43:06 +1100 Subject: [PATCH 06/11] Second pass at guided registration, including profile page --- registrasion/models.py | 10 +++++++ registrasion/views.py | 60 ++++++++++++++++++++++++++---------------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/registrasion/models.py b/registrasion/models.py index 2cb9ad63..55d7ee4a 100644 --- a/registrasion/models.py +++ b/registrasion/models.py @@ -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 @@ -46,6 +47,15 @@ class BadgeAndProfile(models.Model): def __str__(self): return "Badge for: %s of %s" % (self.name, self.company) + @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 + attendee = models.OneToOneField(Attendee, on_delete=models.CASCADE) # Things that appear on badge diff --git a/registrasion/views.py b/registrasion/views.py index 5099e701..da32364e 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -18,25 +18,45 @@ def guided_registration(request, page_id=0): 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, page_id simply - refers to the category_id. Future versions will have pages containing - categories. + grouping of categories into a specific page. Currently, it just goes + through each category one by one ''' - page_id = int(page_id) - if page_id != 0: - ret = product_category_inner(request, page_id) - if ret is not True: + dashboard = redirect("dashboard") + next_step = redirect("guided_registration") + + # Step 1: Fill in a badge + attendee = rego.Attendee.get_instance(request.user) + 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 - # Go to next page in the guided registration + # 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=page_id).order_by("order") + cats = cats.filter(id__gt=category).order_by("order") - if len(cats) > 0: - return redirect("guided_registration", cats[0].id) + if len(cats) == 0: + # We've filled in every category + 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 redirect("dashboard") + return next_step + @login_required def edit_profile(request): @@ -60,16 +80,7 @@ def edit_profile(request): @login_required def product_category(request, category_id): - ret = product_category_inner(request, category_id) - if ret is not True: - return ret - else: - return redirect("dashboard") - -def product_category_inner(request, category_id): ''' Registration selections form for a specific category of items. - It returns a rendered template if this page needs to display stuff, - otherwise it returns True. ''' PRODUCTS_FORM_PREFIX = "products" @@ -81,6 +92,8 @@ def product_category_inner(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") @@ -101,7 +114,10 @@ def product_category_inner(request, category_id): elif cat_form.is_valid(): try: handle_valid_cat_form(cat_form, current_cart) - return True + if category_id > attendee.highest_complete_category: + attendee.highest_complete_category = category_id + attendee.save() + return redirect("dashboard") except ValidationError as ve: pass From eff5686dcf293f8f3b51d566e34a67a7ff74356a Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 24 Mar 2016 14:19:33 +1100 Subject: [PATCH 07/11] Adds logic for required categories --- .../migrations/0006_category_required.py | 20 +++++++++++++++++ registrasion/models.py | 1 + registrasion/views.py | 22 +++++++++++++++++-- 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 registrasion/migrations/0006_category_required.py diff --git a/registrasion/migrations/0006_category_required.py b/registrasion/migrations/0006_category_required.py new file mode 100644 index 00000000..46cc6a51 --- /dev/null +++ b/registrasion/migrations/0006_category_required.py @@ -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, + ), + ] diff --git a/registrasion/models.py b/registrasion/models.py index 55d7ee4a..a1101fd9 100644 --- a/registrasion/models.py +++ b/registrasion/models.py @@ -132,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")) diff --git a/registrasion/views.py b/registrasion/views.py index da32364e..6288f4b2 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -114,12 +114,30 @@ def product_category(request, category_id): elif cat_form.is_valid(): try: 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") - except ValidationError as ve: - pass else: # Create initial data for each of products in category From 83b11cd7224d66ae67ccf6df34d15ff915b2b8a7 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 24 Mar 2016 14:20:29 +1100 Subject: [PATCH 08/11] Fixes invoicing payment logic --- registrasion/controllers/invoice.py | 2 +- registrasion/templates/invoice.html | 14 ++++++++++++++ registrasion/views.py | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index e9a654e4..b8087c98 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -122,7 +122,7 @@ class InvoiceController(object): ) payment.save() - payments = rego.Payment.objects .filter(invoice=self.invoice) + payments = rego.Payment.objects.filter(invoice=self.invoice) agg = payments.aggregate(Sum("amount")) total = agg["amount__sum"] diff --git a/registrasion/templates/invoice.html b/registrasion/templates/invoice.html index a66132cf..bd503657 100644 --- a/registrasion/templates/invoice.html +++ b/registrasion/templates/invoice.html @@ -33,5 +33,19 @@ + + + + + + + {% for payment in invoice.payment_set.all %} + + + + + + {% endfor %} +
Payment timeReferenceAmount
{{payment.time}}{{payment.reference}}{{payment.amount}}
{% endblock %} diff --git a/registrasion/views.py b/registrasion/views.py index 6288f4b2..64f4e4b1 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -217,7 +217,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) From 8e6364d02adb422d1fda56205eeed7038bd66ac0 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 25 Mar 2016 12:50:34 +1100 Subject: [PATCH 09/11] Fixes bug where discount quantity applied to all users rather than specific user. Adds test case. --- registrasion/controllers/cart.py | 1 + registrasion/tests/test_cart.py | 2 ++ registrasion/tests/test_discount.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/registrasion/controllers/cart.py b/registrasion/controllers/cart.py index 59b308ed..d9307b39 100644 --- a/registrasion/controllers/cart.py +++ b/registrasion/controllers/cart.py @@ -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")) diff --git a/registrasion/tests/test_cart.py b/registrasion/tests/test_cart.py index c65011b9..d000b946 100644 --- a/registrasion/tests/test_cart.py +++ b/registrasion/tests/test_cart.py @@ -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() diff --git a/registrasion/tests/test_discount.py b/registrasion/tests/test_discount.py index 5d325eff..6f943d0f 100644 --- a/registrasion/tests/test_discount.py +++ b/registrasion/tests/test_discount.py @@ -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)) From 478b328e41a81aee78158bf6672c3247c833bb83 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 25 Mar 2016 12:50:59 +1100 Subject: [PATCH 10/11] Uses the completed_registration flag on the Attendee model --- registrasion/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/registrasion/views.py b/registrasion/views.py index 64f4e4b1..73d04150 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -25,8 +25,11 @@ def guided_registration(request, page_id=0): dashboard = redirect("dashboard") next_step = redirect("guided_registration") - # Step 1: Fill in a badge 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: @@ -47,6 +50,8 @@ def guided_registration(request, page_id=0): 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) From c192fef491c4b58dfddea21c7fbffbd8b2a9e5bc Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 25 Mar 2016 14:16:30 +1100 Subject: [PATCH 11/11] Adds basic template tag for available categories. Currently does not check enabling conditions. --- registrasion/templatetags/__init__.py | 0 registrasion/templatetags/registrasion_tags.py | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 registrasion/templatetags/__init__.py create mode 100644 registrasion/templatetags/registrasion_tags.py diff --git a/registrasion/templatetags/__init__.py b/registrasion/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/registrasion/templatetags/registrasion_tags.py b/registrasion/templatetags/registrasion_tags.py new file mode 100644 index 00000000..3193a017 --- /dev/null +++ b/registrasion/templatetags/registrasion_tags.py @@ -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()