Skip to content

Instantly share code, notes, and snippets.

@dokterbob
Created August 16, 2012 09:30
Show Gist options
  • Save dokterbob/3368744 to your computer and use it in GitHub Desktop.
Save dokterbob/3368744 to your computer and use it in GitHub Desktop.
Generic filtering and counting
# 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