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. 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 .. .. .. .. .. {{ report.title }} .. .. .. 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()