Report
******
.. highlight:: python
.. tip:: All projects which use the ``report`` app should provide a URL named
``project.report``. This should be the *home* page for reports.
If you don't have a natural URL for this, then see *Project home*
in Snippets_ below.
Icon
====
::
New Report
==========
To write a new report:
.. note:: The report must be created in the app folder in ``reports.py``
e.g. ``example_report/reports.py``.
Create a CSV report based on the ``ReportMixin`` class e.g::
from report.forms import ReportParametersEmptyForm
from report.service import ReportMixin
class EnquiryMonthReport(ReportMixin):
# PROCESS_TASK = process_reports_example
# QUEUE_NAME = "test_report_queue_name"
REPORT_SLUG = "enquiry-month"
REPORT_TITLE = "Enquiries received"
form_class = ReportParametersEmptyForm
def run_csv_report(self, csv_writer, parameters=None):
count = 0
csv_writer.writerow(("name", "email", "phone"))
enquiries = Enquiry.objects.all().order_by("created")
for x in enquiries:
count = count + 1
csv_writer.writerow([x.name, x.email, x.phone])
return count
def user_passes_test(self, user):
return user.is_staff
.. tip:: When naming a report (using the ``slug``), it might be a good idea to
start with the name of the app e.g. ``invoice-time-analysis-contact``.
.. tip:: The ``form_class`` will be used to enter report parameters.
For information on the ``clean`` method see `Report Form`_ below...
.. tip:: The ``user_passes_test`` method should return ``True`` if the user has
permission to run the report.
.. tip:: The (optional) ``PROCESS_TASK`` can be used to trigger the
background task using a specific task function.
The ``queue_name`` is set by the ``dramatiq.actor`` function
decorator, so you will need a specific task function if you want to
use a different queue.
For an example, see ``process_task``
in ``reports.models.ReportSchedule``.
.. tip:: The (optional) ``QUEUE_NAME`` on a report can be used to filter
``outstanding`` tasks (``base.model_utils.RetryModel``).
Report Form
-----------
.. tip:: Use the supplied ``ReportParametersEmptyForm`` if you have no
parameters.
Sample report form::
class ReportParametersKanbanLaneForm(forms.Form):
kanban_lane = forms.ModelChoiceField(
label="Lane",
queryset=KanbanLane.objects.none(),
required=True,
widget=forms.Select(
attrs={
"class": "chosen-select pure-u-1 pure-u-md-1-3",
"data-placeholder": "Select a Kanban lane",
}
),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
f = self.fields["kanban_lane"]
f.queryset = KanbanLane.objects.all().order_by("title")
def clean(self, *args, **kwargs):
cleaned_data = super().clean()
kanban_lane = cleaned_data.get("kanban_lane")
if kanban_lane:
cleaned_data["kanban_lane"] = kanban_lane.title
return cleaned_data
If you have parameters and your form returns a Django queryset, then use the
``clean`` method to convert the queryset to a list of primary keys e.g::
def clean(self, *args, **kwargs):
cleaned = super().clean()
cleaned["roles"] = [x.pk for x in cleaned.get("roles", [])]
return cleaned
PDF Report
----------
To create a PDF report::
from django.utils import timezone
from reportlab import platypus
from reportlab.lib.pagesizes import A4
from report.pdf import NumberedCanvas, PDFReport
from report.models import ReportSpecification
# 1. Create a 'PDFReport' with a 'create_pdf' method
class MyPdfReport(PDFReport):
def create_pdf(self, buff, title):
doc = platypus.SimpleDocTemplate(buff, title=title, pagesize=A4)
elements = []
elements.append(self._para("Printed {}".format(timezone.now())))
# write the document to disk
doc.build(elements, canvasmaker=NumberedCanvas)
# 2. Add a 'REPORT_FORMAT' a 'run_pdf_report' method, and call your report
class EnquiryMonthReport(ReportMixin):
REPORT_FORMAT = ReportSpecification.FORMAT_PDF
REPORT_SLUG = "enquiry-month"
REPORT_TITLE = "Enquiries received"
form_class = ReportParametersEmptyForm
def run_pdf_report(self, buff, parameters=None):
count = 1
report = MyPdfReport()
report.create_pdf(buff, self.REPORT_TITLE)
return count
Views
=====
List of reports (report dashboard)::
from report.views import ReportsMenuViewMixin
# dash/views.py
class ReportsMenuView(
LoginRequiredMixin,
StaffuserRequiredMixin,
ReportsMenuViewMixin,
BaseMixin,
TemplateView,
):
reports_menu = [
(
"Enquiries",
[
EnquiryMonthReport,
# AnotherReport
],
),
(
"Another Section",
[
# AnotherReport
# AnotherReport
],
),
]
# dash/urls.py
re_path(
r"^report/$",
view=ReportsMenuView.as_view(),
name="project.report",
),
To run the report, add it to ``report_classes`` e.g::
from report.views import ReportFormViewMixin
class ReportBySlugFormView(
LoginRequiredMixin,
ReportFormViewMixin,
BaseMixin,
FormView,
):
# process_task = process_enquiry_month_report
report_classes = [
EnquiryMonthReport,
# AnotherReport
]
# dash/urls.py
re_path(
r"^report/(?P[-\w\d]+)/$",
view=ReportBySlugFormView.as_view(),
name="project.report.slug",
),
.. tip:: ``process_task`` is optional and can be used to trigger the
background task using a specific task function.
.. Initialise the report in your app management command e.g. ``init_app_invoice``::
..
.. EnquiryMonthReport().init_report()
..
.. Schedule a Report
.. =================
..
.. To schedule a simple report (with no parameters) using a view, override the
.. ``ReportSpecificationScheduleMixin`` e.g::
..
.. from report.views import ReportSpecificationScheduleMixin
..
.. class ReportSpecificationScheduleView(
.. LoginRequiredMixin,
.. ReportSpecificationScheduleMixin,
.. UserPassesTestMixin,
.. BaseMixin,
.. UpdateView,
.. ):
.. """Create a new schedule for this report specification."""
.. pass
..
.. .. tip:: This view is created as a mixin because most projects will need
.. permission checks (using ``UserPassesTestMixin``) to make sure the
.. user can run the report.
..
.. .. tip:: ``ReportSpecificationScheduleMixin`` needs to appear before
.. ``UserPassesTestMixin`` in the list of classes.
..
.. The report will need a ``slug`` URL e.g::
..
.. url(
.. regex=r"^example/(?P[-\w\d]+)/report/$",
.. view=ReportSpecificationScheduleView.as_view(),
.. name="project.report.specification.schedule",
.. ),
..
.. To schedule a simple report (with no parameters) from the Django template, use
.. the ``REPORT_SLUG`` as the parameter e.g:
..
.. .. code-block:: html
..
..
..
..
.. Parameters
.. ----------
..
.. To handle report parameters, you could create a ``_check_parameters`` method in
.. your report class e.g.
.. https://gitlab.com/kb/invoice/blob/master/invoice/service.py#L432
..
.. e.g:
..
.. class TimeAnalysisByContact(ReportMixin):
.. def _check_parameters(self, parameters):
.. if not parameters:
.. raise ReportError("Cannot run report without any parameters")
.. contact_pk = parameters.get("contact_pk")
.. return contact_pk
..
.. def run_csv_report(self, csv_writer, parameters=None):
.. contact_pk = self._check_parameters(parameters)
Snippets
========
List of available reports::
from report.models import ReportSpecification
[(x.app, x.module, x.report_class) for x in ReportSpecification.objects.all()]
To get a list of outstanding reports::
from report.models import ReportSchedule
ReportSchedule.objects.outstanding()
Project home (``project.report``)::
from django.urls import reverse_lazy
from django.views.generic import RedirectView
url(
regex=r"^report/$",
view=RedirectView.as_view(url=reverse_lazy("report.schedule.list")),
name="project.report",
),
Testing
=======
Content
-------
To check the CSV content:
Initialise the report (can also be added to ``init_project.py`` if required)::
from enquiry.reports import EnquiryMonthReport
EnquiryMonthReport().init_report()
Get the report, prepare the parameters and schedule the report::
from enquiry.reports import EnquiryMonthReport
from report.models import ReportSpecification
parameters = {"contact_pk": contact.pk}
report_schedule = ReportSpecification.objects.schedule(
EnquiryMonthReport.REPORT_SLUG,
user,
parameters=parameters,
)
Run the report and find the schedule::
from report.models import ReportSchedule
schedule_pks = ReportSchedule.objects.process()
assert 1 == len(schedule_pks)
schedule_pk = schedule_pks[0]
report_schedule = ReportSchedule.objects.get(pk=schedule_pk)
Check the report output::
import csv
reader = csv.reader(open(report_schedule.output_file.path), "excel")
first_row = None
result = []
for row in reader:
if not first_row:
first_row = row
else:
result.append(row)
assert ["Ticket", "Contact", "Charge", "Fixed", "Non-Charge"] == first_row
assert [
["Apple", "pat", "30.0", "0", "0"],
] == result
Scheduled
---------
To check a report has been scheduled::
# make sure the report is scheduled
qs = ReportSchedule.objects.current()
assert 1 == qs.count()
# optional checks
schedule = qs.first()
assert report_specification.slug == schedule.report.slug
assert timezone.now().date() == schedule.created.date()