Report

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

<i class="fa fa-table"></i>

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 report based on the ReportMixin class e.g:

from report.service import ReportMixin

class EnquiryMonthReport(ReportMixin):

    REPORT_SLUG = "enquiry-month"
    REPORT_TITLE = "Enquiries received"

    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 user_passes_test method should return True if the user has permission to run the report.

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<slug>[-\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:

<a href="{% url 'project.report.specification.schedule' report.slug %}" class="pure-menu-link">
  <i class="fa fa-table"></i>
  {{ report.title }}
</a>

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 app / project so the ReportSpecification is created e.g:

from django.core.management import call_command
call_command("init_app_invoice")

Get the report, prepare the parameters and schedule the report:

from report.models import ReportSchedule, ReportSpecification
report_specification = ReportSpecification.objects.get(
    slug="invoice-time-analysis-contact"
)
parameters = {"contact_pk": contact.pk}
report_specification.schedule(user, parameters=parameters)

Run the report and find the schedule:

schedule_pks = ReportSchedule.objects.run_reports()
assert 1 == len(schedule_pks)
schedule_pk = schedule_pks[0]
schedule = ReportSchedule.objects.get(pk=schedule_pk)

Check the report output:

import csv
reader = csv.reader(open(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()