Created
August 16, 2012 09:30
-
-
Save dokterbob/3368744 to your computer and use it in GitHub Desktop.
Generic filtering and counting
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Counting the amount of projects by distinct tag in a queryset | |
Project.objects.values('tags__name').annotate(projects=Count('id')).order_by() | |
""" | |
For filtering, this leads to the following design pattern: | |
1. Given a set of filters, apply each filter except for the current one. | |
2. Run `qs.values(<filtered_field>).annotate(projects=Count('id')).order_by()` to determine available options and counts. | |
3. Return to the client (either through AJAX or through a form) and render appropriately. | |
Ideally, we could generalize this case by use of an API. A first approach to this is below (loosehand, untested stuff): | |
""" | |
class FilterBase(object): | |
""" Base class for generic filtering by field. """ | |
filtered_field = None | |
def get_options(self, qs): | |
""" | |
Given a particular queryset, return a ValueQuerySet with the available | |
options and their object counts. | |
Ref: https://docs.djangoproject.com/en/dev/ref/models/querysets/#values | |
""" | |
assert isinstance(self.filtered_field, basestring), \ | |
'filtered_field not specifed' | |
values = qs.values(self.filtered_field) | |
return values.annotate(count=Count('id')).order_by() | |
def get_single_filter(self, option): | |
return Q(**{self.filtered_field: option}) | |
def get_filter(self, options): | |
""" | |
Given a particular option, return a Q object for filtering a queryset | |
such that only objects for the specified option are left. | |
If options is a list for tuple, filter by each options (AND). | |
""" | |
assert isinstance(self.filtered_field, basestring), \ | |
'filtered_field not specifed' | |
# Iterable: multiple options filtered | |
if isinstance(options, list) or isinstance(options, tuple): | |
my_filter = Q() | |
for option in options: | |
my_filter = my_filter & self.get_single_filter(option) | |
# No iterable; single filter | |
return self.get_single_filter(options) | |
class TagFilter(FilterBase): | |
filtered_field = 'tag__name' | |
class FilterSetBase(object): | |
""" Base class for complete set of filters. """ | |
queryset = None | |
filters = {} | |
def __init__(self, **kwargs): | |
""" Initialize, setting currently chosen filter options. """ | |
self.options = kwargs | |
def _get_filters(self, **kwargs): | |
""" Return filters for given options. """ | |
filters = Q() | |
for (field, option) in kwargs.iteritems(): | |
assert field in filters, 'Filter not found!' | |
field_filter = self.filters.get(field) | |
current_filter = field_filter.get_filter(option) | |
filters = filters & current_filter | |
return filters | |
def get_field_options(self, name): | |
""" Get options for specified field. """ | |
filtered_options = self.options.copy() | |
# Remove the filter option for the current field | |
filtered_options.pop(name) | |
# Get relevant filters | |
filters = self._get_filters(filtered_options) | |
# Filter the current queryset | |
qs = self.queryset.filter(filters) | |
# Get options for the current field | |
field_filter = self.filters.get(name) | |
options = field_filter.get_options(qs) | |
return options | |
def get_options(self): | |
""" Get available options for all fields. """ | |
options = {} | |
for field_name in self.filters.iterkeys(): | |
options[field_name] = self.get_field_options(field_name) | |
return options | |
def get_objects(self): | |
""" Get result set of specified options. """ | |
# Get relevant filters | |
filters = self._get_filters(self.options) | |
# Filter the current queryset | |
qs = self.queryset.filter(filters) | |
return qs | |
class MyFilterSet(FilterSetBase): | |
""" | |
>>> f = MyFilterSet(tags=['Machismo', 'Communication']) | |
>>> f.get_options() | |
[<ALL PROJECTS>] | |
>>> f.get_objects() | |
[<Project: 1%COACH Communicating forward>] | |
""" | |
queryset = Project.objects.all() | |
filters = { | |
'tags': TagFilter() | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment