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:
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.
There is no related content for this post. Yet.