Rendering reports¶
From a view¶
Three steps, always the same:
from django.shortcuts import render
from flexible_reports.models import Report
from .models import Book
def library_report(request):
report = Report.objects.get(slug="library-report")
report.set_base_queryset(Book.objects.select_related("author"))
report.set_context({"min_pages": 300})
return render(request, "library/report.html", {"report": report})
set_base_queryset(queryset)-
Mandatory. Every datasource in the report narrows this queryset; nothing outside it can appear in the report. Reading
report.base_querysetwithout having set it raisesdjango.core.exceptions.ImproperlyConfigured.This is also the right place for
select_related()/prefetch_related(): columns reaching across relations (attr_name="author.name") will otherwise issue a query per row. set_context(context)- Optional. A dict used to render parametrised queries -- see
Query languages. Defaults to
None, which the backends treat as an empty context.
Both setters only touch the in-memory instance; nothing is saved.
The template tags¶
flexible_reports_tags provides two tags:
{% flexible report %}- Renders the whole report -- every element, its table, its columns, its sort
order -- and returns safe HTML. It is a
simple_tagtaking the surrounding template context, which it passes on to the renderer. {% render some_string %}- Compiles
some_stringas a Django template and renders it with the current context. Handy inside a report layout template that keeps snippets of markup in the database.
Important
django.template.context_processors.request must be enabled. The tag
hands the parent context to the django-tables2 adapter, which reads
request from it in order to build the table (sorting comes from the
query string).
Layout template¶
Report.template holds the Django template source for the report's layout.
Its default is the contents of
flexible_reports/templates/flexible_reports/report.html:
{% load render_table from django_tables2 %}
<h1>{{ self.title }}</h1>
{% for element in elements.values %}
<h3>{{ element.title }}</h3>
{% render_table element.table %}
{% empty %}
No tables defined for this report. Please add some tables in the
admin interface.
{% endfor %}
Edit it in the admin (Reports → Template) to change the layout. It is validated on save: a template that does not compile is rejected.
The layout template is rendered with the page context plus:
| Variable | Meaning |
|---|---|
self |
The Report instance. |
elements |
OrderedDict keyed by ReportElement.slug, in position order. Each value is a dict with title, object_list and table (a ready django-tables2 table instance). |
catchall |
Per model (applabel_modelname), the list of filtered querysets the datasource-backed elements produced. |
except_catchall |
Per model, the queryset of everything those datasources did not catch. |
Because elements are keyed by slug you can lay a report out by hand instead of looping:
{% load render_table from django_tables2 %}
<h1>{{ self.title }}</h1>
<section class="highlight">
<h2>{{ elements.long_books.title }}</h2>
{% render_table elements.long_books.table %}
</section>
<section>
<h2>The rest</h2>
{% render_table elements.everything_else.table %}
</section>
Warning
Django's template language does not allow - in a variable lookup, so
{{ elements.long-books.title }} is a TemplateSyntaxError. If you
intend to address elements by slug from the layout template, give them
underscored slugs (long_books); SlugField accepts both. The
looping form above works with any slug.
Cell templates¶
A column's template is a Django template snippet rendered once per cell,
with the surrounding page context plus:
| Variable | Meaning |
|---|---|
value |
The value resolved through attr_name. |
record |
The model instance for the current row. |
default |
The column's fallback value. |
column |
The bound django-tables2 column. |
row_counter |
0-based index of the row being rendered. |
request |
The current request. |
{# a column with attr_name="title" #}
<strong>{{ value }}</strong><br>
<small>{{ record.pages }} pages</small>
{# a column with no attr_name at all -- pure presentation #}
{{ row_counter|add:1 }}
Cell templates are compiled once per distinct template source and cached for
the process (flexible_reports.adapters.django_tables2._compiled_template).
The cache is keyed by the source text, so editing a template in the admin
produces a different key and takes effect immediately -- no restart, no
invalidation.
Note
The table class is rebuilt on every render, deliberately. It used to be
cached per Table.pk for the lifetime of the process, which meant a
renamed or deleted column stayed visible until Django was restarted -- and,
with several workers, the page flip-flopped between old and new headers
depending on which worker answered. That cache was removed in 0.4.0.
Footers¶
A column with display_totals renders footer_template with:
| Variable | Meaning |
|---|---|
value |
sum() of the column's values across all rows. If the values cannot be added (text, or no attr_name at all) it falls back to the number of rows. |
count |
Number of rows. |
error |
str() of the exception raised while summing, or None. |
{# numeric column #}
{{ value|floatformat:2 }} PLN
{# column with no attr_name: value == count #}
{{ count }} book(s)
Empty tables¶
When an element's queryset is empty, django-tables2 renders the table's
empty_template instead of rows. It is marked safe, so it may contain
markup.
Performance notes¶
- Put
select_related()/prefetch_related()on the base queryset. per_pageis set tosys.maxsize: reports are rendered whole, never paginated. A report over a very large queryset will render a very large page.Datasource.distinct(on by default) adds aDISTINCTto each element's queryset. Turn it off for datasources that do not join across to-many relations if it costs you.