| 1 | """
|
|---|
| 2 | FilterSpec encapsulates the logic for displaying filters in the Django admin.
|
|---|
| 3 | Filters are specified in models with the "list_filter" option.
|
|---|
| 4 |
|
|---|
| 5 | Each filter subclass knows how to display a filter for a field that passes a
|
|---|
| 6 | certain test -- e.g. being a DateField or ForeignKey.
|
|---|
| 7 | """
|
|---|
| 8 |
|
|---|
| 9 | from django.db import models
|
|---|
| 10 | from django.utils.encoding import smart_unicode, iri_to_uri
|
|---|
| 11 | from django.utils.translation import ugettext as _
|
|---|
| 12 | from django.utils.html import escape
|
|---|
| 13 | from django.utils.safestring import mark_safe
|
|---|
| 14 | import datetime
|
|---|
| 15 |
|
|---|
| 16 | class FilterSpec(object):
|
|---|
| 17 | filter_specs = []
|
|---|
| 18 | default = None
|
|---|
| 19 | def __init__(self, f, request, params, model, model_admin):
|
|---|
| 20 | self.field = f
|
|---|
| 21 | self.params = params
|
|---|
| 22 |
|
|---|
| 23 | def register(cls, test, factory):
|
|---|
| 24 | cls.filter_specs.append((test, factory))
|
|---|
| 25 | register = classmethod(register)
|
|---|
| 26 |
|
|---|
| 27 | def create(cls, f, request, params, model, model_admin):
|
|---|
| 28 | for test, factory in cls.filter_specs:
|
|---|
| 29 | if test(f):
|
|---|
| 30 | return factory(f, request, params, model, model_admin)
|
|---|
| 31 | if callable(default):
|
|---|
| 32 | return default(f, request, params, model, model_admin)
|
|---|
| 33 | create = classmethod(create)
|
|---|
| 34 |
|
|---|
| 35 | def set_default(cls, factory):
|
|---|
| 36 | if callable(factory):
|
|---|
| 37 | cls.default = factory
|
|---|
| 38 | set_default = classmethod(set_default)
|
|---|
| 39 |
|
|---|
| 40 | def has_output(self):
|
|---|
| 41 | return True
|
|---|
| 42 |
|
|---|
| 43 | def choices(self, cl):
|
|---|
| 44 | raise NotImplementedError()
|
|---|
| 45 |
|
|---|
| 46 | def title(self):
|
|---|
| 47 | return self.field.verbose_name
|
|---|
| 48 |
|
|---|
| 49 | def output(self, cl):
|
|---|
| 50 | t = []
|
|---|
| 51 | if self.has_output():
|
|---|
| 52 | t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title()))
|
|---|
| 53 |
|
|---|
| 54 | for choice in self.choices(cl):
|
|---|
| 55 | t.append(u'<li%s><a href="%s">%s</a></li>\n' % \
|
|---|
| 56 | ((choice['selected'] and ' class="selected"' or ''),
|
|---|
| 57 | iri_to_uri(choice['query_string']),
|
|---|
| 58 | choice['display']))
|
|---|
| 59 | t.append('</ul>\n\n')
|
|---|
| 60 | return mark_safe("".join(t))
|
|---|
| 61 |
|
|---|
| 62 | class RelatedFilterSpec(FilterSpec):
|
|---|
| 63 | def __init__(self, f, request, params, model, model_admin):
|
|---|
| 64 | super(RelatedFilterSpec, self).__init__(f, request, params, model, model_admin)
|
|---|
| 65 | if isinstance(f, models.ManyToManyField):
|
|---|
| 66 | self.lookup_title = f.rel.to._meta.verbose_name
|
|---|
| 67 | else:
|
|---|
| 68 | self.lookup_title = f.verbose_name
|
|---|
| 69 | self.lookup_kwarg = '%s__%s__exact' % (f.name, f.rel.to._meta.pk.name)
|
|---|
| 70 | self.lookup_val = request.GET.get(self.lookup_kwarg, None)
|
|---|
| 71 | self.lookup_choices = f.rel.to._default_manager.all()
|
|---|
| 72 |
|
|---|
| 73 | def has_output(self):
|
|---|
| 74 | return len(self.lookup_choices) > 1
|
|---|
| 75 |
|
|---|
| 76 | def title(self):
|
|---|
| 77 | return self.lookup_title
|
|---|
| 78 |
|
|---|
| 79 | def choices(self, cl):
|
|---|
| 80 | yield {'selected': self.lookup_val is None,
|
|---|
| 81 | 'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
|
|---|
| 82 | 'display': _('All')}
|
|---|
| 83 | for val in self.lookup_choices:
|
|---|
| 84 | pk_val = getattr(val, self.field.rel.to._meta.pk.attname)
|
|---|
| 85 | yield {'selected': self.lookup_val == smart_unicode(pk_val),
|
|---|
| 86 | 'query_string': cl.get_query_string({self.lookup_kwarg: pk_val}),
|
|---|
| 87 | 'display': val}
|
|---|
| 88 |
|
|---|
| 89 | FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec)
|
|---|
| 90 |
|
|---|
| 91 | class ChoicesFilterSpec(FilterSpec):
|
|---|
| 92 | def __init__(self, f, request, params, model, model_admin):
|
|---|
| 93 | super(ChoicesFilterSpec, self).__init__(f, request, params, model, model_admin)
|
|---|
| 94 | self.lookup_kwarg = '%s__exact' % f.name
|
|---|
| 95 | self.lookup_val = request.GET.get(self.lookup_kwarg, None)
|
|---|
| 96 |
|
|---|
| 97 | def choices(self, cl):
|
|---|
| 98 | yield {'selected': self.lookup_val is None,
|
|---|
| 99 | 'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
|
|---|
| 100 | 'display': _('All')}
|
|---|
| 101 | for k, v in self.field.choices:
|
|---|
| 102 | yield {'selected': smart_unicode(k) == self.lookup_val,
|
|---|
| 103 | 'query_string': cl.get_query_string({self.lookup_kwarg: k}),
|
|---|
| 104 | 'display': v}
|
|---|
| 105 |
|
|---|
| 106 | FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
|
|---|
| 107 |
|
|---|
| 108 | class DateFieldFilterSpec(FilterSpec):
|
|---|
| 109 | def __init__(self, f, request, params, model, model_admin):
|
|---|
| 110 | super(DateFieldFilterSpec, self).__init__(f, request, params, model, model_admin)
|
|---|
| 111 |
|
|---|
| 112 | self.field_generic = '%s__' % self.field.name
|
|---|
| 113 |
|
|---|
| 114 | self.date_params = dict([(k, v) for k, v in params.items() if k.startswith(self.field_generic)])
|
|---|
| 115 |
|
|---|
| 116 | today = datetime.date.today()
|
|---|
| 117 | one_week_ago = today - datetime.timedelta(days=7)
|
|---|
| 118 | today_str = isinstance(self.field, models.DateTimeField) and today.strftime('%Y-%m-%d 23:59:59') or today.strftime('%Y-%m-%d')
|
|---|
| 119 |
|
|---|
| 120 | self.links = (
|
|---|
| 121 | (_('Any date'), {}),
|
|---|
| 122 | (_('Today'), {'%s__year' % self.field.name: str(today.year),
|
|---|
| 123 | '%s__month' % self.field.name: str(today.month),
|
|---|
| 124 | '%s__day' % self.field.name: str(today.day)}),
|
|---|
| 125 | (_('Past 7 days'), {'%s__gte' % self.field.name: one_week_ago.strftime('%Y-%m-%d'),
|
|---|
| 126 | '%s__lte' % f.name: today_str}),
|
|---|
| 127 | (_('This month'), {'%s__year' % self.field.name: str(today.year),
|
|---|
| 128 | '%s__month' % f.name: str(today.month)}),
|
|---|
| 129 | (_('This year'), {'%s__year' % self.field.name: str(today.year)})
|
|---|
| 130 | )
|
|---|
| 131 |
|
|---|
| 132 | def title(self):
|
|---|
| 133 | return self.field.verbose_name
|
|---|
| 134 |
|
|---|
| 135 | def choices(self, cl):
|
|---|
| 136 | for title, param_dict in self.links:
|
|---|
| 137 | yield {'selected': self.date_params == param_dict,
|
|---|
| 138 | 'query_string': cl.get_query_string(param_dict, [self.field_generic]),
|
|---|
| 139 | 'display': title}
|
|---|
| 140 |
|
|---|
| 141 | FilterSpec.register(lambda f: isinstance(f, models.DateField), DateFieldFilterSpec)
|
|---|
| 142 |
|
|---|
| 143 | class BooleanFieldFilterSpec(FilterSpec):
|
|---|
| 144 | def __init__(self, f, request, params, model, model_admin):
|
|---|
| 145 | super(BooleanFieldFilterSpec, self).__init__(f, request, params, model, model_admin)
|
|---|
| 146 | self.lookup_kwarg = '%s__exact' % f.name
|
|---|
| 147 | self.lookup_kwarg2 = '%s__isnull' % f.name
|
|---|
| 148 | self.lookup_val = request.GET.get(self.lookup_kwarg, None)
|
|---|
| 149 | self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
|
|---|
| 150 |
|
|---|
| 151 | def title(self):
|
|---|
| 152 | return self.field.verbose_name
|
|---|
| 153 |
|
|---|
| 154 | def choices(self, cl):
|
|---|
| 155 | for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
|
|---|
| 156 | yield {'selected': self.lookup_val == v and not self.lookup_val2,
|
|---|
| 157 | 'query_string': cl.get_query_string({self.lookup_kwarg: v}, [self.lookup_kwarg2]),
|
|---|
| 158 | 'display': k}
|
|---|
| 159 | if isinstance(self.field, models.NullBooleanField):
|
|---|
| 160 | yield {'selected': self.lookup_val2 == 'True',
|
|---|
| 161 | 'query_string': cl.get_query_string({self.lookup_kwarg2: 'True'}, [self.lookup_kwarg]),
|
|---|
| 162 | 'display': _('Unknown')}
|
|---|
| 163 |
|
|---|
| 164 | FilterSpec.register(lambda f: isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField), BooleanFieldFilterSpec)
|
|---|
| 165 |
|
|---|
| 166 | # This should be registered last, because it's a last resort. For example,
|
|---|
| 167 | # if a field is eligible to use the BooleanFieldFilterSpec, that'd be much
|
|---|
| 168 | # more appropriate, and the AllValuesFilterSpec won't get used for it.
|
|---|
| 169 | class AllValuesFilterSpec(FilterSpec):
|
|---|
| 170 | def __init__(self, f, request, params, model, model_admin):
|
|---|
| 171 | super(AllValuesFilterSpec, self).__init__(f, request, params, model, model_admin)
|
|---|
| 172 | self.lookup_val = request.GET.get(f.name, None)
|
|---|
| 173 | self.lookup_choices = model_admin.queryset(request).distinct().order_by(f.name).values(f.name)
|
|---|
| 174 |
|
|---|
| 175 | def title(self):
|
|---|
| 176 | return self.field.verbose_name
|
|---|
| 177 |
|
|---|
| 178 | def choices(self, cl):
|
|---|
| 179 | yield {'selected': self.lookup_val is None,
|
|---|
| 180 | 'query_string': cl.get_query_string({}, [self.field.name]),
|
|---|
| 181 | 'display': _('All')}
|
|---|
| 182 | for val in self.lookup_choices:
|
|---|
| 183 | val = smart_unicode(val[self.field.name])
|
|---|
| 184 | yield {'selected': self.lookup_val == val,
|
|---|
| 185 | 'query_string': cl.get_query_string({self.field.name: val}),
|
|---|
| 186 | 'display': val}
|
|---|
| 187 | FilterSpec.set_default(AllValuesFilterSpec)
|
|---|