From 2ed0a47f15bde1dfd1bdc4281043d21d0e99d3c9 Mon Sep 17 00:00:00 2001
From: Christopher Neugebauer <chrisjrn@gmail.com>
Date: Tue, 20 Sep 2016 13:36:49 +1000
Subject: [PATCH] Adds attendance by field report

Fixes #93
---
 registrasion/reporting/forms.py |  19 ++++++
 registrasion/reporting/views.py | 103 ++++++++++++++++++++++++++++++--
 registrasion/urls.py            |   1 +
 3 files changed, 119 insertions(+), 4 deletions(-)

diff --git a/registrasion/reporting/forms.py b/registrasion/reporting/forms.py
index 8543209b..2e983491 100644
--- a/registrasion/reporting/forms.py
+++ b/registrasion/reporting/forms.py
@@ -29,3 +29,22 @@ class UserIdForm(forms.Form):
         label="User ID",
         required=False,
     )
+
+
+def model_fields_form_factory(model):
+    ''' Creates a form for specifying fields from a model to display. '''
+
+    fields = model._meta.get_fields()
+
+    choices = []
+    for field in fields:
+        if hasattr(field, "verbose_name"):
+            choices.append((field.name, field.verbose_name))
+
+    class ModelFieldsForm(forms.Form):
+        fields = forms.MultipleChoiceField(
+            choices=choices,
+            required=False,
+        )
+
+    return ModelFieldsForm
diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py
index 3e2fa514..628102b7 100644
--- a/registrasion/reporting/views.py
+++ b/registrasion/reporting/views.py
@@ -364,6 +364,12 @@ def credit_notes(request, form):
     )
 
 
+class AttendeeListReport(ListReport):
+
+    def get_link(self, argument):
+        return reverse(self._link_view) + "?user=%d" % int(argument)
+
+
 @report_view("Attendee", form_type=forms.UserIdForm)
 def attendee(request, form, user_id=None):
     ''' Returns a list of all manifested attendees if no attendee is specified,
@@ -484,9 +490,98 @@ def attendee_list(request):
     # Sort by whether they've registered, then ID.
     data.sort(key=lambda a: (-a[3], a[0]))
 
-    class Report(ListReport):
+    return AttendeeListReport("Attendees", headings, data, link_view=attendee)
 
-        def get_link(self, argument):
-            return reverse(self._link_view) + "?user=%d" % int(argument)
 
-    return Report("Attendees", headings, data, link_view=attendee)
+ProfileForm = forms.model_fields_form_factory(AttendeeProfile)
+class ProductCategoryProfileForm(forms.ProductAndCategoryForm, ProfileForm):
+    pass
+
+
+@report_view(
+    "Attendees By Product/Category",
+    form_type=ProductCategoryProfileForm,
+)
+def attendee_data(request, form, user_id=None):
+    ''' Lists attendees for a given product/category selection along with
+    profile data.'''
+
+    status_display = {
+        commerce.Cart.STATUS_ACTIVE: "Unpaid",
+        commerce.Cart.STATUS_PAID: "Paid",
+        commerce.Cart.STATUS_RELEASED: "Refunded",
+    }
+
+    output = []
+
+    products = form.cleaned_data["product"]
+    categories = form.cleaned_data["category"]
+    fields = form.cleaned_data["fields"]
+    name_field = AttendeeProfile.name_field()
+
+    items = commerce.ProductItem.objects.filter(
+        Q(product__in=products) | Q(product__category__in=categories),
+    ).exclude(
+        cart__status=commerce.Cart.STATUS_RELEASED
+    ).select_related(
+        "cart", "product"
+    ).order_by("cart__status")
+
+    # Get all of the relevant attendee profiles in one hit.
+    profiles = AttendeeProfile.objects.filter(
+        attendee__user__cart__productitem__in=items
+    ).select_related("attendee__user")
+    by_user = {}
+    for profile in profiles:
+        by_user[profile.attendee.user] = profile
+
+    for field in fields:
+        field_verbose = AttendeeProfile._meta.get_field(field).verbose_name
+
+        cart = "attendee__user__cart"
+        cart_status = cart + "__status"
+        product = cart + "__productitem__product"
+        product_name = product + "__name"
+        category_name = product + "__category__name"
+
+        p = profiles.order_by(product, field).values(
+            cart_status, product, product_name, category_name, field
+        ).annotate(count=Count("id"))
+        output.append(ListReport(
+            "Grouped by %s" % field_verbose,
+            ["Product", "Status", field_verbose, "count"],
+            [
+                (
+                    "%s - %s" % (i[category_name], i[product_name]),
+                    status_display[i[cart_status]],
+                    i[field],
+                    i["count"] or 0,
+                )
+                for i in p
+            ],
+        ))
+
+    # DO the report for individual attendees
+
+    field_names = [
+        AttendeeProfile._meta.get_field(field).verbose_name for field in fields
+    ]
+
+    headings = ["User ID", "Name", "Product", "Item Status"] + field_names
+    data = []
+    for item in items:
+        profile = by_user[item.cart.user]
+        line = [
+            item.cart.user.id,
+            getattr(profile, name_field),
+            item.product,
+            status_display[item.cart.status],
+        ] + [
+            getattr(profile, field) for field in fields
+        ]
+        data.append(line)
+
+    output.append(AttendeeListReport(
+        "Attendees by item with profile data", headings, data, link_view=attendee
+    ))
+    return output
diff --git a/registrasion/urls.py b/registrasion/urls.py
index d6850ef7..d7e440df 100644
--- a/registrasion/urls.py
+++ b/registrasion/urls.py
@@ -43,6 +43,7 @@ public = [
 reports = [
     url(r"^$", rv.reports_list, name="reports_list"),
     url(r"^attendee/?$", rv.attendee, name="attendee"),
+    url(r"^attendee_data/?$", rv.attendee_data, name="attendee_data"),
     url(r"^attendee/([0-9]*)$", rv.attendee, name="attendee"),
     url(r"^credit_notes/?$", rv.credit_notes, name="credit_notes"),
     url(r"^discount_status/?$", rv.discount_status, name="discount_status"),