Introduction to Django Canary Reporting

This project is intended to allow you to build simple reports using models or QuerySets. You get quite a bit of functionality for free (choice filters, foreign key filters, multi-column sorting and pagination). However, you can do much more by extending the built-in columns, filters, sorts, or even views, which control report generation. The default styling is very simple, and intended to blend in with the default Django admin, but the templates are also very simple and therefore easy to extend or replace.

Getting started (Views)

Assume that we have the following Django models:

from django.db import models


class Company(models.Model):
    # constants
    INDUSTRY_CHOICE_TECHNOLOGY = 1
    INDUSTRY_CHOICE_MONKEYS = 2
    INDUSTRY_CHOICE_TECHNOLOGY_MONKEYS = 3
    INDUSTRY_CHOICE_MONKEY_TECHNOLOGY = 4
    INDUSTRY_CHOICES = (
        (INDUSTRY_CHOICE_TECHNOLOGY, 'Technology'),
        (INDUSTRY_CHOICE_MONKEYS, 'Monkeys'),
        (INDUSTRY_CHOICE_TECHNOLOGY_MONKEYS, 'Technology Monkeys'),
        (INDUSTRY_CHOICE_MONKEY_TECHNOLOGY, 'Monkey Technology')
    )

    # columns
    name = models.CharField(max_length=100)
    industry = models.IntegerField(max_length=1, choices=INDUSTRY_CHOICES)

    def __unicode__(self):
        return unicode(self.name)

    class Meta:
        verbose_name_plural = u'Companies'


class Contact(models.Model):
    first_name = models.CharField(max_length=100)
    middle_name = models.CharField(max_length=100, blank=True, null=True)
    last_name = models.CharField(max_length=100)

    active = models.BooleanField(default=True)

    company = models.ForeignKey(Company)

    def __unicode__(self):
        return unicode(self.get_full_name())

    def get_full_name(self):
        """
            Return a string containing the contact's full name.
        """
        name_fields = []
        for name_field in (self.first_name, self.middle_name, self.last_name):
            if name_field:
                name_fields.append(name_field)
        return ' '.join(name_fields)

    @property
    def has_first_name(self):
        """
            Return a boolean, indicating whether the contact has a middle name.
        """
        return bool(self.middle_name)

To create a simple report using these models, just create a report view. Put this in your views.py (or maybe admin_views.py or reports.py). We’ll start with the most basic option, which is a ModelReport:

from canary.views import ModelReport

from models import Contact


class ContactsAll(ModelReport):
    class Meta:
        model = Contact

contacts_all = ContactsAll.as_view()

There’s nothing terribly special about the view handling. All of the report views in the package are extensions of Django’s TemplateView, so you can configure your URLs in whatever way you like. Here, we’ll setup our urls.py like so:

from django.conf.urls.defaults import patterns, url
from django.contrib.admin.views.decorators import staff_member_required

import views


urlpatterns = patterns('',
    url(r'^contacts/all/$', staff_member_required(
        views.contacts_all), name='contacts_all'),
)

This will create a report containing all columns in the Contact model, each sortable and searchable. Not bad for just a few lines of code.

Now, let’s say that we want to have more control over the queryset used to generate the report. Maybe this report should only include active contacts. Well, this is easy to do by giving the queryset to the view. Our new view might look like this:

class ContactsActive(ModelReport):
    class Meta:
        model = Contact
        queryset = Contact.objects.filter(active=True)

What if we didn’t want the report to display all of the columns from the model? Well, that’s easy too, and very similar to Django’s ModelAdmin or ModelForm. You can either specify the columns that you want to include in the report:

class ContactsActiveInclude(ModelReport):
    class Meta:
        model = Contact
        queryset = Contact.objects.filter(active=True)
        include = ('first_name', 'middle_name', 'last_name', 'company', 'created', 'updated')

Or you can specify the columns that you want to exclude:

class ContactsActiveExclude(ModelReport):
    class Meta:
        model = Contact
        queryset = Contact.objects.filter(active=True)
        exclude = ('active',)

Both of the previous examples have the same result of including all columns but ‘active’.

A little more advanced (Columns)

Now, let’s say that we wanted to add the industry column from the Company field. The quickest way to do this is by upgrading from a ModelReport to a QueryReportView. This is a bit similar to moving from a Django ModelForm to a Form, only with a little more magic. You actually define each column that you want to include in the report, but the columns can get their data from any attribute, method, or even a chain of related models, all coming from a QuerySet that you provide. We’ll dig into more advanced configurations later, but for now, let’s see how we would add info from a related model:

class ContactsActiveColumns(QueryReport):
    first_name = columns.Text()
    middle_name = columns.Text()
    last_name = columns.Text()

    industry = columns.Text('company__get_industry_display')

    class Meta:
    queryset = Contact.objects.filter(active=True)

The first three columns are pretty basic. The Text column is the most basic column. It grabs its data from the correponding column from an object in the QuerySet. If the column name is different or you want to reference a related column, you can include an argument, as with the industry column in the example above. The format for referencing related columns should be familiar to Django folks. The string is very similar to a python dot path, where ‘__’ replaces the dot. So, industry = columns.Text('company__get_industry_display') tells the column to get it’s data from obj.company.get_industry_display, where obj is an object in the QuerySet.

What if you want to include the results of a view method in your report. That’s easy too. Just define a new method on the view and include the “path” to the attribute/method in your column definition:

class ContactsActiveViewMethod(QueryReport):
    name = columns.Text('get_full_name')
    industry = columns.Text('get_industry')

    def get_industry(self, contact):
        """
        The first argument is passed the current object from the QuerySet.  you can return anything you need.
        """
        return contact.company.get_industry_display()

        class Meta:
        queryset = Contact.objects.filter(active=True)

Read more about Attribute Resolution.

So now that you can control the columns that end up in your report, let’s look at how you can control the display of individual columns. There are a few built-in columns that you can use to change the display. For example, you might want a column’s data to display as a link on each row. Let’s say that we’d like to link to the admin view of the contact object:

class ActiveContactReport(QueryReportView):
    name = columns.AdminLink('get_full_name', reverse_path='contacts_contact_change', reverse_args=('id',))
    industry = columns.Text('company__industry')

    class Meta:
        queryset = Contact.objects.filter(active=True)

Whoah!! There’s a bit of magic going on here. If you’re used to reversing Django’s admin URLs, you probably already know what’s going on. If not, you might want to take a look at Django’s admin reversing docs. The second and third attributes to the AdminLink class are the reverse path and args for the Django reverse command. So, the name definition above results in calling reverse('admin:contacts_contact_change', args=(obj.id,)) for each object in the QuerySet, and thus providing the link to the change view of that object.

Pretty cool, yeah? What if you just want to provide a link using reverse? Well, check out the ReverseLink class. Actually, AdminLink is just a simple extension of ReverseLink. None of this suits you? Fine, make your own! Extend the Link class or even the base Text or Column classes to do whatever you want. In fact, the goal is to provide a lot of basic types of column widgets that most people can just use, but allow you to easily extend any of them for your own use.

Learn more about Columns.

Filters

Filters are a core concept to the project and where a lot of the power and flexibility comes from. Every column can accept a list of filters that can be applied to the QuerySet for that column. Some filters are so common that they are included by the Column class. For example, a date range filter is so common that there is a Date column that includes the DateRange filter by default. Of course, this is easy to override.

Let’s add date columns to our report to see how the DateRange filter is implemented by default:

class ActiveContactReport(QueryReportView):
    first_name = columns.Text()
    middle_name = columns.Text()
    last_name = columns.Text()

    created = columns.Date()
    updated = columns.Date()

    industry = columns.Text('company__get_industry_display')

    class Meta:
        queryset = Contact.objects.filter(active=True)

Both the created and updated columns get a DateRange filter by default. You may have also noticed by now that the Text column includes the Search filter by default, as long as the column can be queried via the Django ORM.

Now, what if the attribute that displays the data is not the same as the column that filters the data? Well, that’s not a big deal. You just manually define the filter, and provide it with the column(s) to use for filtering:

from canary import filters

class ActiveContactReport(QueryReportView):
    full_name = columns.Text('get_full_name', filters=[
        filters.Search(columns=('first_name, middle_name, last_name')])

    created = columns.Date()
    updated = columns.Date()

    industry = columns.Text('company__industry')

    class Meta:
        queryset = Contact.objects.filter(active=True)

Note

The Search filter may not have worked on the earlier examples for this very reason. For example, the ContactsActiveViewMethod in the demo project is updated from the example to specify the correct columns to search.

Filters are where you can add a lot of custom functionality to your reports. Learn more about Filters

Project Versions

Table Of Contents

Previous topic

Installation

Next topic

Views

This Page