From 320f6ab6eba0d21ed5c762571a7c9b4f765125d0 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 14 Oct 2016 10:27:22 -0700 Subject: [PATCH 1/7] First step refactoring ReportView into a class --- registrasion/reporting/reports.py | 37 +++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/registrasion/reporting/reports.py b/registrasion/reporting/reports.py index ab8e924d..37ac4032 100644 --- a/registrasion/reporting/reports.py +++ b/registrasion/reporting/reports.py @@ -194,19 +194,39 @@ def report_view(title, form_type=None): ''' + # Consolidate form_type so it has format and section + # Create & return view + def _report(view): @wraps(view) @user_passes_test(views._staff_only) def inner_view(request, *a, **k): + return ReportView(request, view, title, form_type).render(*a, **k) - if form_type is not None: - form = form_type(request.GET) + # Add this report to the list of reports. + _all_report_views.append(inner_view) + + # Return the callable + return inner_view + return _report + +class ReportView(object): + + def __init__(self, request, view, title, form_type): + self.request = request + self.view = view + self.title = title + self.form_type = form_type + + def render(self, *a, **k): + if self.form_type is not None: + form = self.form_type(self.request.GET) form.is_valid() else: form = None - reports = view(request, form, *a, **k) + reports = self.view(self.request, form, *a, **k) if isinstance(reports, Report): reports = [reports] @@ -217,19 +237,12 @@ def report_view(title, form_type=None): ] ctx = { - "title": title, + "title": self.title, "form": form, "reports": reports, } - return render(request, "registrasion/report.html", ctx) - - # Add this report to the list of reports. - _all_report_views.append(inner_view) - - # Return the callable - return inner_view - return _report + return render(self.request, "registrasion/report.html", ctx) def get_all_reports(): From ea7a8d9ae77206f2347aaec5011dc713c248e738 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 14 Oct 2016 10:28:38 -0700 Subject: [PATCH 2/7] Indentation --- registrasion/reporting/reports.py | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/registrasion/reporting/reports.py b/registrasion/reporting/reports.py index 37ac4032..a04e786b 100644 --- a/registrasion/reporting/reports.py +++ b/registrasion/reporting/reports.py @@ -220,29 +220,29 @@ class ReportView(object): self.form_type = form_type def render(self, *a, **k): - if self.form_type is not None: - form = self.form_type(self.request.GET) - form.is_valid() - else: - form = None + if self.form_type is not None: + form = self.form_type(self.request.GET) + form.is_valid() + else: + form = None - reports = self.view(self.request, form, *a, **k) + reports = self.view(self.request, form, *a, **k) - if isinstance(reports, Report): - reports = [reports] + if isinstance(reports, Report): + reports = [reports] - reports = [ - _ReportTemplateWrapper("text/html", report) - for report in reports - ] + reports = [ + _ReportTemplateWrapper("text/html", report) + for report in reports + ] - ctx = { - "title": self.title, - "form": form, - "reports": reports, - } + ctx = { + "title": self.title, + "form": form, + "reports": reports, + } - return render(self.request, "registrasion/report.html", ctx) + return render(self.request, "registrasion/report.html", ctx) def get_all_reports(): From 263793099627aa0bb9d76f6a5fad740931e6ba55 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 14 Oct 2016 11:11:27 -0700 Subject: [PATCH 3/7] Adds CSV output support --- registrasion/reporting/forms.py | 13 ++++++ registrasion/reporting/reports.py | 71 +++++++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/registrasion/reporting/forms.py b/registrasion/reporting/forms.py index e9f8cf0a..c275fc01 100644 --- a/registrasion/reporting/forms.py +++ b/registrasion/reporting/forms.py @@ -81,3 +81,16 @@ def model_fields_form_factory(model): ) return ModelFieldsForm + + +class SectionContentTypeForm(forms.Form): + section = forms.IntegerField( + required=False, + min_value=0, + widget=forms.HiddenInput(), + ) + + content_type = forms.CharField( + required=False, + widget=forms.HiddenInput(), + ) diff --git a/registrasion/reporting/reports.py b/registrasion/reporting/reports.py index a04e786b..cf232f8a 100644 --- a/registrasion/reporting/reports.py +++ b/registrasion/reporting/reports.py @@ -1,6 +1,10 @@ +import csv +import forms + from django.contrib.auth.decorators import user_passes_test from django.shortcuts import render from django.core.urlresolvers import reverse +from django.http import HttpResponse from functools import wraps from registrasion import views @@ -195,6 +199,10 @@ def report_view(title, form_type=None): ''' # Consolidate form_type so it has format and section + bases = [forms.SectionContentTypeForm, form_type] + bases = [base for base in bases if base is not None] + form_type = forms.mix_form(*bases) + # Create & return view def _report(view): @@ -202,7 +210,7 @@ def report_view(title, form_type=None): @wraps(view) @user_passes_test(views._staff_only) def inner_view(request, *a, **k): - return ReportView(request, view, title, form_type).render(*a, **k) + return ReportView(request, view, title, form_type).view(*a, **k) # Add this report to the list of reports. _all_report_views.append(inner_view) @@ -213,37 +221,84 @@ def report_view(title, form_type=None): class ReportView(object): - def __init__(self, request, view, title, form_type): + def __init__(self, request, inner_view, title, form_type): self.request = request - self.view = view + self.inner_view = inner_view self.title = title self.form_type = form_type + self._prepare() - def render(self, *a, **k): + def view(self, *a, **k): + self._prepare_reports(*a, **k) + + return self._render() + + def _prepare(self): + + # Create a form instance if self.form_type is not None: form = self.form_type(self.request.GET) + + # Pre-validate it form.is_valid() else: form = None - reports = self.view(self.request, form, *a, **k) + self.form = form + self.content_type = form.cleaned_data["content_type"] + self.section = form.cleaned_data["section"] + + renderers = { + "text/csv": self._render_as_csv, + "text/html": self._render_as_html, + "": self._render_as_html, + } + self._render = renderers[self.content_type] + + def _prepare_reports(self, *a, **k): + reports = self.inner_view(self.request, self.form, *a, **k) if isinstance(reports, Report): reports = [reports] + self.reports = self._wrap_reports(reports) + + def _render(self): + ''' Replace with a specialist _render function ''' + + def _wrap_reports(self, reports): reports = [ - _ReportTemplateWrapper("text/html", report) + _ReportTemplateWrapper(self.content_type, report) for report in reports ] + return reports + + def _render_as_html(self): + ctx = { "title": self.title, - "form": form, - "reports": reports, + "form": self.form, + "reports": self.reports, } return render(self.request, "registrasion/report.html", ctx) + def _render_as_csv(self): + report = self.reports[self.section] + + # Create the HttpResponse object with the appropriate CSV header. + response = HttpResponse(content_type='text/csv') + #response['Content-Disposition'] = 'attachment; filename="somefilename.csv"' + + writer = csv.writer(response) + writer.writerow(list(report.headings())) + for row in report.rows(): + writer.writerow(list(row)) + + return response + + def get_all_reports(): ''' Returns all the views that have been registered with @report ''' From 517da705366cdd2084d6d9c18182472296ff3ace Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 14 Oct 2016 11:19:10 -0700 Subject: [PATCH 4/7] CSV fixes --- registrasion/reporting/reports.py | 6 +++--- registrasion/reporting/views.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/registrasion/reporting/reports.py b/registrasion/reporting/reports.py index cf232f8a..49df9fbd 100644 --- a/registrasion/reporting/reports.py +++ b/registrasion/reporting/reports.py @@ -289,12 +289,12 @@ class ReportView(object): # Create the HttpResponse object with the appropriate CSV header. response = HttpResponse(content_type='text/csv') - #response['Content-Disposition'] = 'attachment; filename="somefilename.csv"' writer = csv.writer(response) - writer.writerow(list(report.headings())) + encode = lambda i: i.encode("utf8") if isinstance(i, unicode) else i + writer.writerow(list(encode(i) for i in report.headings())) for row in report.rows(): - writer.writerow(list(row)) + writer.writerow(list(encode(i) for i in row)) return response diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index a5784014..2aaa3c31 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -408,7 +408,7 @@ 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 user_id is None and not form.has_changed(): + if user_id is None: return attendee_list(request) if form.cleaned_data["user"] is not None: From 67ac01e599d4e7c45410f083eb3001ae7e0fe1ab Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 14 Oct 2016 11:36:31 -0700 Subject: [PATCH 5/7] Adds a tag to take the CSV version of a report --- registrasion/templatetags/registrasion_tags.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/registrasion/templatetags/registrasion_tags.py b/registrasion/templatetags/registrasion_tags.py index fda5b2c0..7d2bb56e 100644 --- a/registrasion/templatetags/registrasion_tags.py +++ b/registrasion/templatetags/registrasion_tags.py @@ -4,6 +4,7 @@ from registrasion.controllers.item import ItemController from django import template from django.db.models import Sum +from urllib import urlencode register = template.Library() @@ -74,3 +75,14 @@ def items_purchased(context, category=None): return ItemController(context.request.user).items_purchased( category=category ) + + +@register.assignment_tag(takes_context=True) +def report_as_csv(context, section): + + query = dict(context.request.GET) + query["section"] = section + query["content_type"] = "text/csv" + querystring = urlencode(query) + + return context.request.path + "?" + querystring From ed2327beddb3ba476f718ca94d13962f22538f5b Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 14 Oct 2016 16:10:36 -0700 Subject: [PATCH 6/7] Cleans up the architecture for report views --- registrasion/reporting/reports.py | 113 ++++++++++++++++-------------- 1 file changed, 61 insertions(+), 52 deletions(-) diff --git a/registrasion/reporting/reports.py b/registrasion/reporting/reports.py index 49df9fbd..dce5358f 100644 --- a/registrasion/reporting/reports.py +++ b/registrasion/reporting/reports.py @@ -198,94 +198,78 @@ def report_view(title, form_type=None): ''' - # Consolidate form_type so it has format and section - bases = [forms.SectionContentTypeForm, form_type] - bases = [base for base in bases if base is not None] - form_type = forms.mix_form(*bases) - # Create & return view - def _report(view): - - @wraps(view) - @user_passes_test(views._staff_only) - def inner_view(request, *a, **k): - return ReportView(request, view, title, form_type).view(*a, **k) + report_view = ReportView(view, title, form_type) + report_view = user_passes_test(views._staff_only)(report_view) + report_view = wraps(view)(report_view) # Add this report to the list of reports. - _all_report_views.append(inner_view) + _all_report_views.append(report_view) + + return report_view - # Return the callable - return inner_view return _report + class ReportView(object): - def __init__(self, request, inner_view, title, form_type): - self.request = request + def __init__(self, inner_view, title, form_type): + # Consolidate form_type so it has content type and section + bases = [forms.SectionContentTypeForm, form_type] + bases = [base for base in bases if base is not None] + form_type = forms.mix_form(*bases) + self.inner_view = inner_view self.title = title self.form_type = form_type - self._prepare() - def view(self, *a, **k): - self._prepare_reports(*a, **k) + def __call__(self, request, *a, **k): + data = ReportViewRequestData(self, request, *a, **k) + return self.render(data) - return self._render() - - def _prepare(self): + def get_form(self, request): # Create a form instance if self.form_type is not None: - form = self.form_type(self.request.GET) + form = self.form_type(request.GET) # Pre-validate it form.is_valid() else: form = None - self.form = form - self.content_type = form.cleaned_data["content_type"] - self.section = form.cleaned_data["section"] + return form - renderers = { - "text/csv": self._render_as_csv, - "text/html": self._render_as_html, - "": self._render_as_html, - } - self._render = renderers[self.content_type] - - def _prepare_reports(self, *a, **k): - reports = self.inner_view(self.request, self.form, *a, **k) - - if isinstance(reports, Report): - reports = [reports] - - self.reports = self._wrap_reports(reports) - - def _render(self): - ''' Replace with a specialist _render function ''' - - def _wrap_reports(self, reports): + @classmethod + def wrap_reports(cls, reports, content_type): reports = [ - _ReportTemplateWrapper(self.content_type, report) + _ReportTemplateWrapper(content_type, report) for report in reports ] return reports - def _render_as_html(self): + def render(self, data): + renderers = { + "text/csv": self._render_as_csv, + "text/html": self._render_as_html, + "": self._render_as_html, + } + render = renderers[data.content_type] + return render(data) + def _render_as_html(self, data): ctx = { "title": self.title, - "form": self.form, - "reports": self.reports, + "form": data.form, + "reports": data.reports, } - return render(self.request, "registrasion/report.html", ctx) + return render(data.request, "registrasion/report.html", ctx) - def _render_as_csv(self): - report = self.reports[self.section] + def _render_as_csv(self, data): + report = data.reports[data.section] # Create the HttpResponse object with the appropriate CSV header. response = HttpResponse(content_type='text/csv') @@ -299,6 +283,31 @@ class ReportView(object): return response +class ReportViewRequestData(object): + + def __init__(self, report_view, request, *a, **k): + self.report_view = report_view + self.request = request + + # Calculate other data + self.form = report_view.get_form(request) + + # Content type and section come from the form + self.content_type = self.form.cleaned_data["content_type"] + self.section = self.form.cleaned_data["section"] + + # Reports come from calling the inner view + reports = report_view.inner_view(request, self.form, *a, **k) + + # Normalise to a list + if isinstance(reports, Report): + reports = [reports] + + # Wrap them in appropriate format + reports = ReportView.wrap_reports(reports, self.content_type) + + self.reports = reports + def get_all_reports(): ''' Returns all the views that have been registered with @report ''' From 6a37134172c3814c651686d07b28b0876e4ea422 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Fri, 14 Oct 2016 16:26:36 -0700 Subject: [PATCH 7/7] Stops relying on a form --- registrasion/reporting/forms.py | 13 ------------- registrasion/reporting/reports.py | 13 +++++-------- registrasion/templatetags/registrasion_tags.py | 8 +++++--- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/registrasion/reporting/forms.py b/registrasion/reporting/forms.py index c275fc01..e9f8cf0a 100644 --- a/registrasion/reporting/forms.py +++ b/registrasion/reporting/forms.py @@ -81,16 +81,3 @@ def model_fields_form_factory(model): ) return ModelFieldsForm - - -class SectionContentTypeForm(forms.Form): - section = forms.IntegerField( - required=False, - min_value=0, - widget=forms.HiddenInput(), - ) - - content_type = forms.CharField( - required=False, - widget=forms.HiddenInput(), - ) diff --git a/registrasion/reporting/reports.py b/registrasion/reporting/reports.py index dce5358f..eb2c33a6 100644 --- a/registrasion/reporting/reports.py +++ b/registrasion/reporting/reports.py @@ -216,10 +216,6 @@ class ReportView(object): def __init__(self, inner_view, title, form_type): # Consolidate form_type so it has content type and section - bases = [forms.SectionContentTypeForm, form_type] - bases = [base for base in bases if base is not None] - form_type = forms.mix_form(*bases) - self.inner_view = inner_view self.title = title self.form_type = form_type @@ -254,7 +250,7 @@ class ReportView(object): renderers = { "text/csv": self._render_as_csv, "text/html": self._render_as_html, - "": self._render_as_html, + None: self._render_as_html, } render = renderers[data.content_type] return render(data) @@ -292,9 +288,10 @@ class ReportViewRequestData(object): # Calculate other data self.form = report_view.get_form(request) - # Content type and section come from the form - self.content_type = self.form.cleaned_data["content_type"] - self.section = self.form.cleaned_data["section"] + # Content type and section come from request.GET + self.content_type = request.GET.get("content_type") + self.section = request.GET.get("section") + self.section = int(self.section) if self.section else None # Reports come from calling the inner view reports = report_view.inner_view(request, self.form, *a, **k) diff --git a/registrasion/templatetags/registrasion_tags.py b/registrasion/templatetags/registrasion_tags.py index 7d2bb56e..1f408ed1 100644 --- a/registrasion/templatetags/registrasion_tags.py +++ b/registrasion/templatetags/registrasion_tags.py @@ -80,9 +80,11 @@ def items_purchased(context, category=None): @register.assignment_tag(takes_context=True) def report_as_csv(context, section): - query = dict(context.request.GET) - query["section"] = section - query["content_type"] = "text/csv" + old_query = context.request.META["QUERY_STRING"] + query = dict([("section", section), ("content_type", "text/csv")]) querystring = urlencode(query) + if old_query: + querystring = old_query + "&" + querystring + return context.request.path + "?" + querystring