Debatably Beta
Gettin' <div>ivise.
Home | About | Contact

Using ExtJS's Grid Filtering with Django

This particular post was originally published on July 16, 2008.
Jump to: Related Content or Comments

Tags: ajax datagrid django extjs javascript queryset


Introduction

ExtJS 2.1 saw inclusion of the popular (at least in my world) user extension for AJAX filtering of data by grid columns. As useful as this is (provided you can abide by the GPL or paid licensing options), the filter parameters serialized as a PHP array, which is not particularly useful for other languages. With a quick modification to the grid filtering serialization method and a helper function for Django, we can rewire the system to send a JSON string that Django can use to directly apply filtering to a QuerySet. I've found that most clients love having this sort of command and flexibility over their data -- especially the kind that don't necessarily realize that this visualization and control exists. I like that. Additionally, the amount of work and code required is minimal, and the code both reusable and straight-forward. I like that, too.

There are 4 steps to this process:

  1. Modify the serialization method in GridFilters.js
  2. Set up the helper function that will process the QuerySet with the grid filter parameters
  3. Slightly customize your models (see below)
  4. Add (simple) helper code to your AJAX view

Modify the Grid's Serialization Method

First, open the GridFilters.js file, and locate the buildQuery function. Replace it with this snippet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
buildQuery: function(filters) {
    
    // turn the filters into a valid JSON object
    // to send back to the server for filtering
    
    var processed_filters = []
    
    for(var i=0; i<filters.length; i++) {
        
        var pf = {}
        var filter = filters[i];

        pf['field'] = filter.field;
        pf['data'] = {}

        for(var key in filter.data) {
            pf['data'][key] = filter.data[key];
        }
        
        processed_filters.push(pf);
        
    }
    
    return {q: Ext.encode(processed_filters)};
    
},

Now, when serialization has been triggered, the grid will send JSON. Serialization is triggered after you've entered data like a string, a number, selecting a date, etc. When serialization is triggered, the grid will make a POST request to the data source you've defined with the filtering parameters.

The Helpful Helper Function

In your view, decode the JSON string into a dictionary using something like simplejson. Once you have the dictionary of filtering parameters, you can use the helper function below to filter your QuerySet and send the results back to the user.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def get_queryset_from_ext_filters(qs, filter_params):
    """
    This helper function accepts a ``QuerySet`` and ``dict`` of fields and values
    to iteratively apply as filters.
    
    For ``ForeignKey`` handling, this helper will check the model's ``FilterMeta`` class
    for a property called ``title_filter_field``, which is the name of the property to search
    when filtering across relationships.
    
    This helper does not, at this time, support ``ManyToMany`` relationships.
    
    Returns ``QuerySet``.
    """
    from django.db.models import ForeignKey

    if not filter_params:
        return qs

    filters = {}

    for param in filter_params:

        if param['data']['type'] == 'string':
            if isinstance(qs.model._meta.get_field(param['field']), ForeignKey):
                # grab the FilterMeta.title_field property to resolve the name
                field = qs.model._meta.get_field(param['field'])
                filter_by_field = getattr(field.rel.to.FilterMeta, 'title_filter_field')
                key = str("%s%s" % (param['field'], '__%s__icontains' % filter_by_field))
            else:
                key = str("%s%s" % (param['field'], '__icontains'))

            data = param['data']['value']

        if param['data']['type'] == 'date':
            # get the comparison type
            comparison = param['data']['comparison']
            
            # on this date
            if comparison == 'eq':
                comparison_key = ""
                data = date(*strptime(param['data']['value'], "%m/%d/%Y")[:3])
                
            # after this date
            elif comparison == 'gt':
                comparison_key = "__gte"
                data = date(*strptime(param['data']['value'], "%m/%d/%Y")[:3])
            
            # before this date
            elif comparison == 'lt':
                comparison_key = "__lte"
                data = date(*strptime(param['data']['value'], "%m/%d/%Y")[:3])

            key = str("%s%s" % (param['field'], comparison_key))

        if param['data']['type'] == 'list':
            if isinstance(qs.model._meta.get_field(param['field']), ForeignKey):
                # grab the FilterMeta.title_field property to resolve the name
                field = qs.model._meta.get_field(param['field'])
                filter_by_field = field.rel.to.FilterMeta.title_filter_field
                key = str("%s%s" % (param['field'], '__%s__in' % filter_by_field))
            else:
                key = str("%s%s" % (param['field'], '__in'))

            data = param['data']['value']

        filters[key] = data

    return qs.filter(**filters)

Please note that it does not handle ManyToManyFields yet. 

Handling Foreign Key Representation

Most users don't understand why they're seeing a number under certain columns. In reality, that number is most often an ID (primary key or otherwise), but is worthless to the user.

To handle foreign key relationships when using this helper, you'll need to add another class to your Django models. I've called this class FilterMeta, and it has one property, called "title_filter_field". The point of this property is to define a field in your model to use when filtering when the model is being accessed via foreign key relationship.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# in models.py ...

class Venue(models.Model):
    name = models.CharField(max_length=60)
    city = models.CharField(max_length=100)
    state = models.CharField(max_length=100) # or province
    zip_code = models.CharField()
    country = models.CharField(max_length=70)
    
    # ...

    class FilterMeta:
        title_filter_field = "name"
        

class Event(models.Model):
    venue = models.ForeignKey(Venue, related_name="events")
    title = models.CharField(max_length=120)
    start = models.DateTimeField()
    end = models.DateTimeField()

    # ...

If I was looking at a data grid of Events, I could filter manually by Venue name because I've determined that Venue objects should be filtered by their name property. If I were to type "Empt" into the filter input, the ORM filter generated would be akin to Event.objects.filter(venue__name__icontains="Empt"). Cool.

NOTE: Feel free to email and tell me if this needs any more clarification! If there's a better way anyone can think to do this, I'm all ears.

Add the following class to your models if there will be any filtering across a foreign key relationship:

1
2
3
4
5
# after Meta class: 
class FilterMeta:
    # this should be the name of the field to use
    # when filtering across a relationship
    title_filter_field = 'name'

Using the Helper Function

At this point, you should have everything you need to filter via an AJAX view. Your AJAX view will be pulling a list of items to supply to the grid's data store, and accept filtering parameters to use with the aforementioned helper. In your AJAX view, you need only pass the QuerySet and JSON string from the grid to the helper and work with the QuerySet it returns.

Setting up this method of grid filtering to be generic is a great way to give data grid views for each model. I like to make a "generic list" AJAX view that accepts a content type model name and generates a JSON collection for representation with the data grid. Using this helper is as simple as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# take a ContentType model name (say, from the URL)
# and create a QuerySet
ctype = ContentType.objects.get(model=ctype_model)
model_class = ctype.model_class()
obj_qs = model_class.objects.filter()

# 'q' is the set of POSTed filtering parameters
filter_params = request.POST.get('q', '[]')

# decode the filtering parameters w/ simplejson
filter_params = simplejson.loads(filter_params)

# apply the filtering params
filtered_qs = get_queryset_from_ext_filters(obj_qs, filter_params)

Wrap-up

Filtering your data this way can be a quick and easy way to navigate your data. ExtJS makes it super-easy to put together a powerful grid with all the trimmings: pagination, sortability, and AJAX data loading, smart grouping and filtering, and more. It's also incredibly easy to use the sorted and filtered results for things like generating bulk reports. Much has been said about ExtJS's GPL-or-cash licensing, but that aside, its a fantastic system for easily building helpful widgets.


Related Content

There is no related content for this post. Yet.


Comments on Using ExtJS's Grid Filtering with Django

blog comments powered by Disqus