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 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.

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

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()