Changeset - 6fb680931627
[Not reviewed]
0 4 0
Joel Addison - 5 years ago 2019-10-13 12:22:15
joel@addison.net.au
Improve registration report formatting

Show title in list instead of method name.
Add page title and head title to reports.
4 files changed with 11 insertions and 3 deletions:
0 comments (0 inline, 0 general)
pinaxcon/templates/registrasion/report.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load bootstrap %}
 
{% load registrasion_tags %}
 

	
 
{% block page_title %}Registration report{% endblock %}
 
{% block head_title %}Registration report - {{ title }}{% endblock %}
 

	
 
{% block content %}
 

	
 
  <h2>{{ title }}</h2>
 

	
 
  <p><a href="{% url 'reports_list' %}">Return to reports list</a></p>
 

	
 
  {% if form %}
 
    <form class="form-horizontal" method="GET">
 
      {{ form | bootstrap}}
 
      <br/>
 
      <input class="btn btn-primary" type="submit">
 
    </form>
 
  {% endif %}
 
<hr />
 

	
 
{% for report in reports %}
 
<h3>{{ report.title }}</h3>
 
{% if report.headings %}
 
<table class="table table-striped table-reportdata">
 
{% else %}
 
    <table class="table table-striped">
 
{% endif %}
 
    <thead>
 
        <tr>
 
            {% for heading in report.headings %}
 
            <th>{{ heading }}</th>
 
            {% endfor %}
 
        </tr>
 
    </thead>
 
    <tbody>
 
        {% for line in report.rows %}
 
        <tr>
 
            {% for item in line %}
 
            <td>
 
                {{ item|safe }}
 
            </td>
 
            {% endfor %}
 
        </tr>
 
        {% endfor %}
 
    </tbody>
 
    </table>
 

	
 
<hr />
 
{% endfor %}
 

	
 
{% endblock %}
 

	
 
{% block extra_script %}
 
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs/jszip-2.5.0/dt-1.10.16/b-1.4.2/b-colvis-1.4.2/b-flash-1.4.2/b-html5-1.4.2/b-print-1.4.2/cr-1.4.1/fc-3.2.3/fh-3.1.3/r-2.2.0/rg-1.0.2/datatables.min.css"/>
 
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/pdfmake.min.js"></script>
 
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/vfs_fonts.js"></script>
 
<script type="text/javascript" src="https://cdn.datatables.net/v/bs/jszip-2.5.0/dt-1.10.16/b-1.4.2/b-colvis-1.4.2/b-html5-1.4.2/b-print-1.4.2/cr-1.4.1/fc-3.2.3/fh-3.1.3/r-2.2.0/rg-1.0.2/datatables.min.js"></script>
 
<script type="text/javascript">
 
        $("table.table-reportdata").dataTable({
 
            "dom": "<'row'<'col-md-3'l><'col-md-3'B><'col-md-4'f>r>t<'row'<'col-md-3'i><'col-md-5'p>>",
 
            "stateSave": true,
 
            "lengthMenu": [[10, 50, 100, -1], [10, 50, 100, "All"]],
 
            "pageLength": 100,
 
            "buttons": [ "csv", "print", {
 
              extend: 'copy',
 
              text: 'Copy',
 
              exportOptions: {
 
                columns: ':visible'
 
              }},
 
              { extend: 'collection',
 
                text: 'Columns',
 
                buttons: [ 'columnsToggle']
 
            }]
 
        });
 
    </script>
 
{% endblock %}
pinaxcon/templates/registrasion/reports_list.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load registrasion_tags %}
 
{% block content %}
 

	
 
<h2>Registration reports</h2>
 
{% block page_title %}Registration reports{% endblock %}
 
{% block head_title %}Registration reports{% endblock %}
 

	
 
{% block content %}
 
<table class="table table-striped">
 
  {% for report in reports %}
 
    <tr>
 
      <td>
 
        <a href="{{ report.url }}">{{ report.name }}</a>
 
        <a href="{{ report.url }}">{{ report.title }}</a>
 
      </td>
 
      <td>
 
        {{ report.description }}
 
      </td>
 
    </tr>
 
  {% endfor %}
 
</table>
 

	
 
{% endblock %}
vendor/registrasion/registrasion/reporting/reports.py
Show inline comments
 
import csv
 

	
 
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
 

	
 

	
 
''' A list of report views objects that can be used to load a list of
 
reports. '''
 
_all_report_views = []
 

	
 

	
 
class Report(object):
 

	
 
    def __init__(self):
 
        pass
 

	
 
    def title():
 
        raise NotImplementedError
 

	
 
    def headings():
 
        ''' Returns the headings for the report. '''
 
        raise NotImplementedError
 

	
 
    def rows(content_type):
 
        '''
 

	
 
        Arguments:
 
            content_type (str): The content-type for the output format of this
 
            report.
 

	
 
        Returns:
 
            An iterator, which yields each row of the data. Each row should
 
            be an iterable containing the cells, rendered appropriately for
 
            content_type.
 
        '''
 
        raise NotImplementedError
 

	
 
    def _linked_text(self, content_type, address, text):
 
        '''
 

	
 
        Returns:
 
            an HTML linked version of text, if the content_type for this report
 
            is HTMLish, otherwise, the text.
 
        '''
 

	
 
        if content_type == "text/html":
 
            return Report._html_link(address, text)
 
        else:
 
            return text
 

	
 
    @staticmethod
 
    def _html_link(address, text):
 
        return '<a href="%s">%s</a>' % (address, text)
 

	
 

	
 
class _ReportTemplateWrapper(object):
 
    ''' Used internally to pass `Report` objects to templates. They effectively
 
    are used to specify the content_type for a report. '''
 

	
 
    def __init__(self, content_type, report):
 
        self.content_type = content_type
 
        self.report = report
 

	
 
    def title(self):
 
        return self.report.title()
 

	
 
    def headings(self):
 
        return self.report.headings()
 

	
 
    def rows(self):
 
        return self.report.rows(self.content_type)
 

	
 
    def count(self):
 
        return self.report.count()
 

	
 

	
 
class BasicReport(Report):
 

	
 
    def __init__(self, title, headings, link_view=None):
 
        super(BasicReport, self).__init__()
 
        self._title = title
 
        self._headings = headings
 
        self._link_view = link_view
 

	
 
    def title(self):
 
        ''' Returns the title for this report. '''
 
        return self._title
 

	
 
    def headings(self):
 
        ''' Returns the headings for the table. '''
 
        return self._headings
 

	
 
    def cell_text(self, content_type, index, text):
 
        if index > 0 or not self._link_view:
 
            return text
 
        else:
 
            address = self.get_link(text)
 
            return self._linked_text(content_type, address, text)
 

	
 
    def get_link(self, argument):
 
        return reverse(self._link_view, args=[argument])
 

	
 

	
 
class ListReport(BasicReport):
 

	
 
    def __init__(self, title, headings, data, link_view=None):
 
        super(ListReport, self).__init__(title, headings, link_view=link_view)
 
        self._data = data
 

	
 
    def rows(self, content_type):
 
        ''' Returns the data rows for the table. '''
 

	
 
        for row in self._data:
 
            yield [
 
                self.cell_text(content_type, i, cell)
 
                for i, cell in enumerate(row)
 
            ]
 

	
 
    def count(self):
 
        return len(self._data)
 

	
 

	
 
class QuerysetReport(BasicReport):
 

	
 
    def __init__(self, title, attributes, queryset, headings=None,
 
                 link_view=None):
 
        super(QuerysetReport, self).__init__(
 
            title, headings, link_view=link_view
 
        )
 
        self._attributes = attributes
 
        self._queryset = queryset
 

	
 
    def headings(self):
 
        if self._headings is not None:
 
            return self._headings
 

	
 
        return [
 
            " ".join(i.split("_")).capitalize() for i in self._attributes
 
        ]
 

	
 
    def rows(self, content_type):
 

	
 
        def rgetattr(item, attr):
 
            for i in attr.split("__"):
 
                item = getattr(item, i)
 

	
 
            if callable(item):
 
                try:
 
                    return item()
 
                except TypeError:
 
                    pass
 

	
 
            return item
 

	
 
        for row in self._queryset:
 
            yield [
 
                self.cell_text(content_type, i, rgetattr(row, attribute))
 
                for i, attribute in enumerate(self._attributes)
 
            ]
 

	
 

	
 
    def count(self):
 
        return self._queryset.count()
 

	
 

	
 
class Links(Report):
 

	
 
    def __init__(self, title, links):
 
        '''
 
        Arguments:
 
            links ([tuple, ...]): a list of 2-tuples:
 
                (url, link_text)
 

	
 
        '''
 
        self._title = title
 
        self._links = links
 

	
 
    def title(self):
 
        return self._title
 

	
 
    def headings(self):
 
        return []
 

	
 
    def rows(self, content_type):
 
        for url, link_text in self._links:
 
            yield [
 
                self._linked_text(content_type, url, link_text)
 
            ]
 

	
 
    def count(self):
 
        return len(self._links)
 

	
 

	
 
def report_view(title, form_type=None):
 
    ''' Decorator that converts a report view function into something that
 
    displays a Report.
 

	
 
    Arguments:
 
        title (str):
 
            The title of the report.
 
        form_type (Optional[forms.Form]):
 
            A form class that can make this report display things. If not
 
            supplied, no form will be displayed.
 

	
 
    '''
 

	
 
    # Create & return view
 
    def _report(view):
 
        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(report_view)
 

	
 
        return report_view
 

	
 
    return _report
 

	
 

	
 
class ReportView(object):
 
    ''' View objects that can render report data into HTML or CSV. '''
 

	
 
    def __init__(self, inner_view, title, form_type):
 
        '''
 

	
 
        Arguments:
 
            inner_view: Callable that returns either a Report or a sequence of
 
                Report objects.
 

	
 
            title: The title that appears at the top of all of the reports.
 

	
 
            form_type: A Form class that can be used to query the report.
 

	
 
        '''
 

	
 
        # Consolidate form_type so it has content type and section
 
        self.inner_view = inner_view
 
        self.title = title
 
        self.form_type = form_type
 

	
 
    def __call__(self, request, *a, **k):
 
        data = ReportViewRequestData(self, request, *a, **k)
 
        return self.render(data)
 

	
 
    def get_form(self, request):
 

	
 
        ''' Creates an instance of self.form_type using request.GET '''
 

	
 
        # Create a form instance
 
        if self.form_type is not None:
 
            form = self.form_type(request.GET)
 

	
 
            # Pre-validate it
 
            form.is_valid()
 
        else:
 
            form = None
 

	
 
        return form
 

	
 
    @classmethod
 
    def wrap_reports(cls, reports, content_type):
 
        ''' Wraps the reports in a _ReportTemplateWrapper for the given
 
        content_type -- this allows data to be returned as HTML links, for
 
        instance. '''
 

	
 
        reports = [
 
            _ReportTemplateWrapper(content_type, report)
 
            for report in reports
 
        ]
 

	
 
        return reports
 

	
 
    def render(self, data):
 
        ''' Renders the reports based on data.content_type's value.
 

	
 
        Arguments:
 
            data (ReportViewRequestData): The report data. data.content_type
 
                is used to determine how the reports are rendered.
 

	
 
        Returns:
 
            HTTPResponse: The rendered version of the report.
 

	
 
        '''
 
        renderers = {
 
            "text/csv": self._render_as_csv,
 
            "text/html": self._render_as_html,
 
            None: self._render_as_html,
 
        }
vendor/registrasion/registrasion/reporting/views.py
Show inline comments
 
from . import forms
 

	
 
import collections
 
import datetime
 
import itertools
 

	
 
from django.conf import settings
 
from django.contrib.auth.decorators import user_passes_test
 
from django.contrib.auth.models import User
 
from django.core.urlresolvers import reverse
 
from django.db import models
 
from django.db.models import F, Q, Subquery, OuterRef
 
from django.db.models import Count, Max, Sum
 
from django.db.models import Case, When, Value
 
from django.db.models.fields.related import RelatedField
 
from django.db.models.fields import CharField
 
from django.shortcuts import render
 

	
 
from registrasion.controllers.cart import CartController
 
from registrasion.controllers.item import ItemController
 
from registrasion.models import conditions
 
from registrasion.models import commerce
 
from registrasion.models import people
 
from registrasion import util
 
from registrasion import views
 

	
 
from symposion.schedule import models as schedule_models
 

	
 
from .reports import get_all_reports
 
from .reports import Links
 
from .reports import ListReport
 
from .reports import QuerysetReport
 
from .reports import report_view
 

	
 
import bleach
 

	
 

	
 
def CURRENCY():
 
    return models.DecimalField(decimal_places=2)
 

	
 

	
 
AttendeeProfile = util.get_object_from_name(settings.ATTENDEE_PROFILE_MODEL)
 

	
 

	
 
@user_passes_test(views._staff_only)
 
def reports_list(request):
 
    ''' Lists all of the reports currently available. '''
 

	
 
    reports = []
 

	
 
    for report in get_all_reports():
 
        reports.append({
 
            "name": report.__name__,
 
            "title": report.title,
 
            "url": reverse(report),
 
            "description": report.__doc__,
 
        })
 

	
 
    reports.sort(key=lambda report: report["name"])
 

	
 
    ctx = {
 
        "reports": reports,
 
    }
 

	
 
    return render(request, "registrasion/reports_list.html", ctx)
 

	
 

	
 
# Report functions
 

	
 
@report_view("Reconcilitation")
 
def reconciliation(request, form):
 
    ''' Shows the summary of sales, and the full history of payments and
 
    refunds into the system. '''
 

	
 
    return [
 
        sales_payment_summary(),
 
        items_sold(),
 
        payments(),
 
        credit_note_refunds(),
 
    ]
 

	
 

	
 
def items_sold():
 
    ''' Summarises the items sold and discounts granted for a given set of
 
    products, or products from categories. '''
 

	
 
    data = None
 
    headings = None
 

	
 
    line_items = commerce.LineItem.objects.filter(
 
        invoice__status=commerce.Invoice.STATUS_PAID,
 
    ).select_related("invoice")
 

	
 
    line_items = line_items.order_by(
 
        # sqlite requires an order_by for .values() to work
 
        "-price", "description",
 
    ).values(
 
        "price", "description",
 
    ).annotate(
 
        total_quantity=Sum("quantity"),
 
    )
 

	
 
    headings = ["Description", "Quantity", "Price", "Total"]
 

	
 
    data = []
 
    total_income = 0
 
    for line in line_items:
 
        cost = line["total_quantity"] * line["price"]
 
        data.append([
 
            line["description"], line["total_quantity"],
 
            line["price"], cost,
 
        ])
 
        total_income += cost
 

	
 
    data.append([
 
        "(TOTAL)", "--", "--", total_income,
 
    ])
 

	
 
    return ListReport("Items sold", headings, data)
 

	
 

	
 
def sales_payment_summary():
 
    ''' Summarises paid items and payments. '''
 

	
 
    def value_or_zero(aggregate, key):
 
        return aggregate[key] or 0
 

	
 
    def sum_amount(payment_set):
 
        a = payment_set.values("amount").aggregate(total=Sum("amount"))
 
        return value_or_zero(a, "total")
 

	
 
    headings = ["Category", "Total"]
 
    data = []
 

	
 
    # Summarise all sales made (= income.)
 
    sales = commerce.LineItem.objects.filter(
 
        invoice__status=commerce.Invoice.STATUS_PAID,
 
    ).values(
 
        "price", "quantity"
 
    ).aggregate(
 
        total=Sum(F("price") * F("quantity"), output_field=CURRENCY()),
 
    )
 
    sales = value_or_zero(sales, "total")
 

	
 
    all_payments = sum_amount(commerce.PaymentBase.objects.all())
 

	
 
    # Manual payments
 
    # Credit notes generated (total)
 
    # Payments made by credit note
 
    # Claimed credit notes
0 comments (0 inline, 0 general)