From 897915f1217dedb96bdefe378b9d0012a9f2574d Mon Sep 17 00:00:00 2001
From: Christopher Neugebauer <chrisjrn@gmail.com>
Date: Sat, 3 Sep 2016 14:22:32 +1000
Subject: [PATCH 1/7] Adds the amend_registration view, which currently can
 display all of the products that the user has added to their current cart,
 and not much else.

---
 registrasion/forms.py | 15 +++++++++++++++
 registrasion/urls.py  |  2 ++
 registrasion/views.py | 28 ++++++++++++++++++++++++++++
 3 files changed, 45 insertions(+)

diff --git a/registrasion/forms.py b/registrasion/forms.py
index d6e7878e..229f7bc5 100644
--- a/registrasion/forms.py
+++ b/registrasion/forms.py
@@ -347,3 +347,18 @@ class VoucherForm(forms.Form):
         help_text="If you have a voucher code, enter it here",
         required=False,
     )
+
+
+class StaffProductsForm(forms.Form):
+    ''' Form for allowing staff to add an item to a user's cart. '''
+
+    product = forms.ModelChoiceField(
+        widget=forms.Select,
+        queryset=inventory.Product.objects.all(),
+    )
+
+    quantity = forms.IntegerField(
+        min_value=0,
+    )
+
+StaffProductsFormSet = forms.formset_factory(StaffProductsForm)
diff --git a/registrasion/urls.py b/registrasion/urls.py
index ae6ac8a1..dab49b00 100644
--- a/registrasion/urls.py
+++ b/registrasion/urls.py
@@ -13,10 +13,12 @@ from .views import (
     invoice_access,
     edit_profile,
     guided_registration,
+    amend_registration,
 )
 
 
 public = [
+    url(r"^amend/([0-9]+)$", amend_registration, name="amend_registration"),
     url(r"^category/([0-9]+)$", product_category, name="product_category"),
     url(r"^checkout$", checkout, name="checkout"),
     url(r"^credit_note/([0-9]+)$", credit_note, name="credit_note"),
diff --git a/registrasion/views.py b/registrasion/views.py
index 95e97793..f4368be8 100644
--- a/registrasion/views.py
+++ b/registrasion/views.py
@@ -18,6 +18,7 @@ from collections import namedtuple
 from django.conf import settings
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.decorators import user_passes_test
+from django.contrib.auth.models import User
 from django.contrib import messages
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ValidationError
@@ -790,3 +791,30 @@ def credit_note(request, note_id, access_code=None):
     }
 
     return render(request, "registrasion/credit_note.html", data)
+
+
+@user_passes_test(_staff_only)
+def amend_registration(request, user_id):
+    ''' Allows staff to amend a user's current registration cart, and etc etc.
+    '''
+
+    user = User.objects.get(id=int(user_id))
+    current_cart = CartController.for_user(user)
+
+    items = commerce.ProductItem.objects.filter(
+        cart=current_cart.cart,
+    ).select_related("product")
+
+    initial = [{"product": i.product, "quantity": i.quantity} for i in items]
+
+    form = forms.StaffProductsFormSet(
+        request.POST or None,
+        initial=initial,
+        prefix="products",
+    )
+
+    data = {
+        "form": form,
+    }
+
+    return render(request, "registrasion/amend_registration.html", data)

From 83b8b62d7457bd1261c03efb78ab31f618cda9a1 Mon Sep 17 00:00:00 2001
From: Christopher Neugebauer <chrisjrn@gmail.com>
Date: Sat, 3 Sep 2016 14:24:58 +1000
Subject: [PATCH 2/7] Attendee view now uses user_id, like the rest of the app

---
 registrasion/reporting/views.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py
index eb512454..6a507135 100644
--- a/registrasion/reporting/views.py
+++ b/registrasion/reporting/views.py
@@ -236,17 +236,17 @@ def credit_notes(request, form):
 
 
 @report_view("Attendee", form_type=forms.UserIdForm)
-def attendee(request, form, attendee_id=None):
+def attendee(request, form, user_id=None):
     ''' Returns a list of all manifested attendees if no attendee is specified,
     else displays the attendee manifest. '''
 
-    if attendee_id is None and not form.has_changed():
+    if user_id is None and not form.has_changed():
         return attendee_list(request)
 
     if form.cleaned_data["user"] is not None:
-        attendee_id = form.cleaned_data["user"]
+        user_id = form.cleaned_data["user"]
 
-    attendee = people.Attendee.objects.get(id=attendee_id)
+    attendee = people.Attendee.objects.get(user__id=user_id)
 
     reports = []
 
@@ -349,7 +349,7 @@ def attendee_list(request):
 
     for attendee in attendees:
         data.append([
-            attendee.id,
+            attendee.user.id,
             attendee.attendeeprofilebase.attendee_name(),
             attendee.user.email,
             attendee.has_registered > 0,

From 84c40a1e1f28d630f3aa0c65de8203e8a75ddb17 Mon Sep 17 00:00:00 2001
From: Christopher Neugebauer <chrisjrn@gmail.com>
Date: Sat, 3 Sep 2016 15:08:25 +1000
Subject: [PATCH 3/7] Refactors ItemController, add items_released

---
 registrasion/controllers/item.py | 42 ++++++++++++++++++++++----------
 1 file changed, 29 insertions(+), 13 deletions(-)

diff --git a/registrasion/controllers/item.py b/registrasion/controllers/item.py
index a36604e5..f2d0a2ae 100644
--- a/registrasion/controllers/item.py
+++ b/registrasion/controllers/item.py
@@ -30,7 +30,7 @@ class ItemController(object):
     def __init__(self, user):
         self.user = user
 
-    def items_purchased(self, category=None):
+    def _items(self, cart_status, category=None):
         ''' Aggregates the items that this user has purchased.
 
         Arguments:
@@ -45,7 +45,7 @@ class ItemController(object):
 
         in_cart = (
             Q(productitem__cart__user=self.user) &
-            Q(productitem__cart__status=commerce.Cart.STATUS_PAID)
+            Q(productitem__cart__status=cart_status)
         )
 
         quantities_in_cart = When(
@@ -72,6 +72,20 @@ class ItemController(object):
             out.append(ProductAndQuantity(prod, prod.quantity))
         return out
 
+    def items_purchased(self, category=None):
+        ''' Aggregates the items that this user has purchased.
+
+        Arguments:
+            category (Optional[models.inventory.Category]): the category
+                of items to restrict to.
+
+        Returns:
+            [ProductAndQuantity, ...]: A list of product-quantity pairs,
+                aggregating like products from across multiple invoices.
+
+        '''
+        return self._items(commerce.Cart.STATUS_PAID)
+
     def items_pending(self):
         ''' Gets all of the items that the user has reserved, but has not yet
         paid for.
@@ -82,14 +96,16 @@ class ItemController(object):
 
         '''
 
-        all_items = commerce.ProductItem.objects.filter(
-            cart__user=self.user,
-            cart__status=commerce.Cart.STATUS_ACTIVE,
-        ).select_related(
-            "product",
-            "product__category",
-        ).order_by(
-            "product__category__order",
-            "product__order",
-        )
-        return all_items
+        return self._items(commerce.Cart.STATUS_ACTIVE)
+
+    def items_released(self):
+        ''' Gets all of the items that the user previously paid for, but has
+        since refunded.
+
+        Returns:
+            [ProductAndQuantity, ...]: A list of product-quantity pairs for the
+                items that the user has not yet paid for.
+
+        '''
+
+        return self._items(commerce.Cart.STATUS_RELEASED)

From c2065dd4b90a20e430c98ad6221a6d4021e3c714 Mon Sep 17 00:00:00 2001
From: Christopher Neugebauer <chrisjrn@gmail.com>
Date: Sat, 3 Sep 2016 15:08:44 +1000
Subject: [PATCH 4/7] =?UTF-8?q?The=20form=20can=20now=20amend=20a=20user?=
 =?UTF-8?q?=E2=80=99s=20registration.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 registrasion/views.py | 30 +++++++++++++++++++++++++++---
 1 file changed, 27 insertions(+), 3 deletions(-)

diff --git a/registrasion/views.py b/registrasion/views.py
index f4368be8..58e2891c 100644
--- a/registrasion/views.py
+++ b/registrasion/views.py
@@ -10,6 +10,7 @@ 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
 
@@ -804,17 +805,40 @@ def amend_registration(request, user_id):
     items = commerce.ProductItem.objects.filter(
         cart=current_cart.cart,
     ).select_related("product")
-
     initial = [{"product": i.product, "quantity": i.quantity} for i in items]
 
-    form = forms.StaffProductsFormSet(
+    formset = forms.StaffProductsFormSet(
         request.POST or None,
         initial=initial,
         prefix="products",
     )
 
+    if request.POST and formset.is_valid():
+        print formset._errors
+
+        pq = [
+            (f.cleaned_data["product"], f.cleaned_data["quantity"])
+            for f in formset
+            if "product" in f.cleaned_data and
+            f.cleaned_data["product"] is not None
+        ]
+
+        try:
+            current_cart.set_quantities(pq)
+            return redirect(amend_registration, user_id)
+        except ValidationError as ve:
+            for ve_field in ve.error_list:
+                product, message = ve_field.message
+                for form in formset:
+                    if form.cleaned_data["product"] == product:
+                        form.add_error("quantity", message)
+
+    ic = ItemController(user)
     data = {
-        "form": form,
+        "user": user,
+        "paid": ic.items_purchased(),
+        "cancelled": ic.items_released(),
+        "form": formset,
     }
 
     return render(request, "registrasion/amend_registration.html", data)

From 1152e185d136ca50a448086907c8814e5e4ad64b Mon Sep 17 00:00:00 2001
From: Christopher Neugebauer <chrisjrn@gmail.com>
Date: Sat, 3 Sep 2016 15:16:46 +1000
Subject: [PATCH 5/7] Staff can now check out an invoice for a user

---
 registrasion/urls.py  |  1 +
 registrasion/views.py | 16 ++++++++++++++--
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/registrasion/urls.py b/registrasion/urls.py
index dab49b00..ac7faace 100644
--- a/registrasion/urls.py
+++ b/registrasion/urls.py
@@ -21,6 +21,7 @@ public = [
     url(r"^amend/([0-9]+)$", amend_registration, name="amend_registration"),
     url(r"^category/([0-9]+)$", product_category, name="product_category"),
     url(r"^checkout$", checkout, name="checkout"),
+    url(r"^checkout/([0-9]+)$", checkout, name="checkout"),
     url(r"^credit_note/([0-9]+)$", credit_note, name="credit_note"),
     url(r"^invoice/([0-9]+)$", invoice, name="invoice"),
     url(r"^invoice/([0-9]+)/([A-Z0-9]+)$", invoice, name="invoice"),
diff --git a/registrasion/views.py b/registrasion/views.py
index 58e2891c..f457a629 100644
--- a/registrasion/views.py
+++ b/registrasion/views.py
@@ -506,7 +506,7 @@ def _handle_voucher(request, prefix):
 
 
 @login_required
-def checkout(request):
+def checkout(request, user_id=None):
     ''' Runs the checkout process for the current cart.
 
     If the query string contains ``fix_errors=true``, Registrasion will attempt
@@ -514,6 +514,10 @@ def checkout(request):
     cancelling expired discounts and vouchers, and removing any unavailable
     products.
 
+    Arguments:
+        user_id (castable to int):
+            If the requesting user is staff, then the user ID can be used to
+            run checkout for another user.
     Returns:
         render or redirect:
             If the invoice is generated successfully, or there's already a
@@ -527,7 +531,15 @@ def checkout(request):
 
     '''
 
-    current_cart = CartController.for_user(request.user)
+    if user_id is not None:
+        if request.user.is_staff:
+            user = User.objects.get(id=int(user_id))
+        else:
+            raise Http404()
+    else:
+        user = request.user
+
+    current_cart = CartController.for_user(user)
 
     if "fix_errors" in request.GET and request.GET["fix_errors"] == "true":
         current_cart.fix_simple_errors()

From b9ee438b891e9414d3391606858807a2da5fbf05 Mon Sep 17 00:00:00 2001
From: Christopher Neugebauer <chrisjrn@gmail.com>
Date: Sat, 3 Sep 2016 15:43:04 +1000
Subject: [PATCH 6/7] Registration amendments are now limited the products that
 the user is allowed to add.

---
 registrasion/forms.py | 36 ++++++++++++++++++++++++++----------
 registrasion/views.py |  3 ++-
 2 files changed, 28 insertions(+), 11 deletions(-)

diff --git a/registrasion/forms.py b/registrasion/forms.py
index 229f7bc5..90bf1b48 100644
--- a/registrasion/forms.py
+++ b/registrasion/forms.py
@@ -1,3 +1,4 @@
+from registrasion.controllers.product import ProductController
 from registrasion.models import commerce
 from registrasion.models import inventory
 
@@ -349,16 +350,31 @@ class VoucherForm(forms.Form):
     )
 
 
-class StaffProductsForm(forms.Form):
-    ''' Form for allowing staff to add an item to a user's cart. '''
+def staff_products_form_factory(user):
+    ''' Creates a StaffProductsForm that restricts the available products to
+    those that are available to a user. '''
 
-    product = forms.ModelChoiceField(
-        widget=forms.Select,
-        queryset=inventory.Product.objects.all(),
-    )
+    products = inventory.Product.objects.all()
+    products = ProductController.available_products(user, products=products)
 
-    quantity = forms.IntegerField(
-        min_value=0,
-    )
+    product_ids = [product.id for product in products]
+    product_set = inventory.Product.objects.filter(id__in=product_ids)
 
-StaffProductsFormSet = forms.formset_factory(StaffProductsForm)
+    class StaffProductsForm(forms.Form):
+        ''' Form for allowing staff to add an item to a user's cart. '''
+
+        product = forms.ModelChoiceField(
+            widget=forms.Select,
+            queryset=product_set,
+        )
+
+        quantity = forms.IntegerField(
+            min_value=0,
+        )
+
+    return StaffProductsForm
+
+def staff_products_formset_factory(user):
+    ''' Creates a formset of StaffProductsForm for the given user. '''
+    form_type = staff_products_form_factory(user)
+    return forms.formset_factory(form_type)
diff --git a/registrasion/views.py b/registrasion/views.py
index f457a629..a7cf2391 100644
--- a/registrasion/views.py
+++ b/registrasion/views.py
@@ -819,7 +819,8 @@ def amend_registration(request, user_id):
     ).select_related("product")
     initial = [{"product": i.product, "quantity": i.quantity} for i in items]
 
-    formset = forms.StaffProductsFormSet(
+    StaffProductsFormSet = forms.staff_products_formset_factory(user)
+    formset = StaffProductsFormSet(
         request.POST or None,
         initial=initial,
         prefix="products",

From 5703221fbaf6d2969de179af3264d46137142a0b Mon Sep 17 00:00:00 2001
From: Christopher Neugebauer <chrisjrn@gmail.com>
Date: Sat, 3 Sep 2016 15:53:54 +1000
Subject: [PATCH 7/7] Adds voucher form to registration amendment

---
 registrasion/views.py | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/registrasion/views.py b/registrasion/views.py
index a7cf2391..c457295f 100644
--- a/registrasion/views.py
+++ b/registrasion/views.py
@@ -826,8 +826,12 @@ def amend_registration(request, user_id):
         prefix="products",
     )
 
+    voucher_form = forms.VoucherForm(
+        request.POST or None,
+        prefix="voucher",
+    )
+
     if request.POST and formset.is_valid():
-        print formset._errors
 
         pq = [
             (f.cleaned_data["product"], f.cleaned_data["quantity"])
@@ -846,12 +850,20 @@ def amend_registration(request, user_id):
                     if form.cleaned_data["product"] == product:
                         form.add_error("quantity", message)
 
+    if request.POST and voucher_form.is_valid():
+        try:
+            current_cart.apply_voucher(voucher_form.cleaned_data["voucher"])
+            return redirect(amend_registration, user_id)
+        except ValidationError as ve:
+            voucher_form.add_error(None, ve)
+
     ic = ItemController(user)
     data = {
         "user": user,
         "paid": ic.items_purchased(),
         "cancelled": ic.items_released(),
         "form": formset,
+        "voucher_form": voucher_form,
     }
 
     return render(request, "registrasion/amend_registration.html", data)