Statistics
| Branch: | Revision:

root / env / lib / python2.7 / site-packages / django / contrib / admin / filters.py @ 1a305335

History | View | Annotate | Download (15.9 KB)

1
"""
2
This 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
import datetime
9

    
10
from django.db import models
11
from django.core.exceptions import ImproperlyConfigured, ValidationError
12
from django.utils.encoding import smart_unicode, force_unicode
13
from django.utils.translation import ugettext_lazy as _
14
from django.utils import timezone
15
from django.contrib.admin.util import (get_model_from_relation,
16
    reverse_field_path, get_limit_choices_to_from_path, prepare_lookup_value)
17
from django.contrib.admin.options import IncorrectLookupParameters
18

    
19
class ListFilter(object):
20
    title = None  # Human-readable title to appear in the right sidebar.
21
    template = 'admin/filter.html'
22

    
23
    def __init__(self, request, params, model, model_admin):
24
        # This dictionary will eventually contain the request's query string
25
        # parameters actually used by this filter.
26
        self.used_parameters = {}
27
        if self.title is None:
28
            raise ImproperlyConfigured(
29
                "The list filter '%s' does not specify "
30
                "a 'title'." % self.__class__.__name__)
31

    
32
    def has_output(self):
33
        """
34
        Returns True if some choices would be output for this filter.
35
        """
36
        raise NotImplementedError
37

    
38
    def choices(self, cl):
39
        """
40
        Returns choices ready to be output in the template.
41
        """
42
        raise NotImplementedError
43

    
44
    def queryset(self, request, queryset):
45
        """
46
        Returns the filtered queryset.
47
        """
48
        raise NotImplementedError
49

    
50
    def expected_parameters(self):
51
        """
52
        Returns the list of parameter names that are expected from the
53
        request's query string and that will be used by this filter.
54
        """
55
        raise NotImplementedError
56

    
57

    
58
class SimpleListFilter(ListFilter):
59
    # The parameter that should be used in the query string for that filter.
60
    parameter_name = None
61

    
62
    def __init__(self, request, params, model, model_admin):
63
        super(SimpleListFilter, self).__init__(
64
            request, params, model, model_admin)
65
        if self.parameter_name is None:
66
            raise ImproperlyConfigured(
67
                "The list filter '%s' does not specify "
68
                "a 'parameter_name'." % self.__class__.__name__)
69
        lookup_choices = self.lookups(request, model_admin)
70
        if lookup_choices is None:
71
            lookup_choices = ()
72
        self.lookup_choices = list(lookup_choices)
73
        if self.parameter_name in params:
74
            value = params.pop(self.parameter_name)
75
            self.used_parameters[self.parameter_name] = value
76

    
77
    def has_output(self):
78
        return len(self.lookup_choices) > 0
79

    
80
    def value(self):
81
        """
82
        Returns the value (in string format) provided in the request's
83
        query string for this filter, if any. If the value wasn't provided then
84
        returns None.
85
        """
86
        return self.used_parameters.get(self.parameter_name, None)
87

    
88
    def lookups(self, request, model_admin):
89
        """
90
        Must be overriden to return a list of tuples (value, verbose value)
91
        """
92
        raise NotImplementedError
93

    
94
    def expected_parameters(self):
95
        return [self.parameter_name]
96

    
97
    def choices(self, cl):
98
        yield {
99
            'selected': self.value() is None,
100
            'query_string': cl.get_query_string({}, [self.parameter_name]),
101
            'display': _('All'),
102
        }
103
        for lookup, title in self.lookup_choices:
104
            yield {
105
                'selected': self.value() == force_unicode(lookup),
106
                'query_string': cl.get_query_string({
107
                    self.parameter_name: lookup,
108
                }, []),
109
                'display': title,
110
            }
111

    
112

    
113
class FieldListFilter(ListFilter):
114
    _field_list_filters = []
115
    _take_priority_index = 0
116

    
117
    def __init__(self, field, request, params, model, model_admin, field_path):
118
        self.field = field
119
        self.field_path = field_path
120
        self.title = getattr(field, 'verbose_name', field_path)
121
        super(FieldListFilter, self).__init__(
122
            request, params, model, model_admin)
123
        for p in self.expected_parameters():
124
            if p in params:
125
                value = params.pop(p)
126
                self.used_parameters[p] = prepare_lookup_value(p, value)
127

    
128
    def has_output(self):
129
        return True
130

    
131
    def queryset(self, request, queryset):
132
        try:
133
            return queryset.filter(**self.used_parameters)
134
        except ValidationError, e:
135
            raise IncorrectLookupParameters(e)
136

    
137
    @classmethod
138
    def register(cls, test, list_filter_class, take_priority=False):
139
        if take_priority:
140
            # This is to allow overriding the default filters for certain types
141
            # of fields with some custom filters. The first found in the list
142
            # is used in priority.
143
            cls._field_list_filters.insert(
144
                cls._take_priority_index, (test, list_filter_class))
145
            cls._take_priority_index += 1
146
        else:
147
            cls._field_list_filters.append((test, list_filter_class))
148

    
149
    @classmethod
150
    def create(cls, field, request, params, model, model_admin, field_path):
151
        for test, list_filter_class in cls._field_list_filters:
152
            if not test(field):
153
                continue
154
            return list_filter_class(field, request, params,
155
                model, model_admin, field_path=field_path)
156

    
157

    
158
class RelatedFieldListFilter(FieldListFilter):
159
    def __init__(self, field, request, params, model, model_admin, field_path):
160
        other_model = get_model_from_relation(field)
161
        if hasattr(field, 'rel'):
162
            rel_name = field.rel.get_related_field().name
163
        else:
164
            rel_name = other_model._meta.pk.name
165
        self.lookup_kwarg = '%s__%s__exact' % (field_path, rel_name)
166
        self.lookup_kwarg_isnull = '%s__isnull' % field_path
167
        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
168
        self.lookup_val_isnull = request.GET.get(
169
                                      self.lookup_kwarg_isnull, None)
170
        self.lookup_choices = field.get_choices(include_blank=False)
171
        super(RelatedFieldListFilter, self).__init__(
172
            field, request, params, model, model_admin, field_path)
173
        if hasattr(field, 'verbose_name'):
174
            self.lookup_title = field.verbose_name
175
        else:
176
            self.lookup_title = other_model._meta.verbose_name
177
        self.title = self.lookup_title
178

    
179
    def has_output(self):
180
        if (isinstance(self.field, models.related.RelatedObject)
181
                and self.field.field.null or hasattr(self.field, 'rel')
182
                    and self.field.null):
183
            extra = 1
184
        else:
185
            extra = 0
186
        return len(self.lookup_choices) + extra > 1
187

    
188
    def expected_parameters(self):
189
        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
190

    
191
    def choices(self, cl):
192
        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
193
        yield {
194
            'selected': self.lookup_val is None and not self.lookup_val_isnull,
195
            'query_string': cl.get_query_string({},
196
                [self.lookup_kwarg, self.lookup_kwarg_isnull]),
197
            'display': _('All'),
198
        }
199
        for pk_val, val in self.lookup_choices:
200
            yield {
201
                'selected': self.lookup_val == smart_unicode(pk_val),
202
                'query_string': cl.get_query_string({
203
                    self.lookup_kwarg: pk_val,
204
                }, [self.lookup_kwarg_isnull]),
205
                'display': val,
206
            }
207
        if (isinstance(self.field, models.related.RelatedObject)
208
                and self.field.field.null or hasattr(self.field, 'rel')
209
                    and self.field.null):
210
            yield {
211
                'selected': bool(self.lookup_val_isnull),
212
                'query_string': cl.get_query_string({
213
                    self.lookup_kwarg_isnull: 'True',
214
                }, [self.lookup_kwarg]),
215
                'display': EMPTY_CHANGELIST_VALUE,
216
            }
217

    
218
FieldListFilter.register(lambda f: (
219
        hasattr(f, 'rel') and bool(f.rel) or
220
        isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
221

    
222

    
223
class BooleanFieldListFilter(FieldListFilter):
224
    def __init__(self, field, request, params, model, model_admin, field_path):
225
        self.lookup_kwarg = '%s__exact' % field_path
226
        self.lookup_kwarg2 = '%s__isnull' % field_path
227
        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
228
        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
229
        super(BooleanFieldListFilter, self).__init__(field,
230
            request, params, model, model_admin, field_path)
231

    
232
    def expected_parameters(self):
233
        return [self.lookup_kwarg, self.lookup_kwarg2]
234

    
235
    def choices(self, cl):
236
        for lookup, title in (
237
                (None, _('All')),
238
                ('1', _('Yes')),
239
                ('0', _('No'))):
240
            yield {
241
                'selected': self.lookup_val == lookup and not self.lookup_val2,
242
                'query_string': cl.get_query_string({
243
                        self.lookup_kwarg: lookup,
244
                    }, [self.lookup_kwarg2]),
245
                'display': title,
246
            }
247
        if isinstance(self.field, models.NullBooleanField):
248
            yield {
249
                'selected': self.lookup_val2 == 'True',
250
                'query_string': cl.get_query_string({
251
                        self.lookup_kwarg2: 'True',
252
                    }, [self.lookup_kwarg]),
253
                'display': _('Unknown'),
254
            }
255

    
256
FieldListFilter.register(lambda f: isinstance(f,
257
    (models.BooleanField, models.NullBooleanField)), BooleanFieldListFilter)
258

    
259

    
260
class ChoicesFieldListFilter(FieldListFilter):
261
    def __init__(self, field, request, params, model, model_admin, field_path):
262
        self.lookup_kwarg = '%s__exact' % field_path
263
        self.lookup_val = request.GET.get(self.lookup_kwarg)
264
        super(ChoicesFieldListFilter, self).__init__(
265
            field, request, params, model, model_admin, field_path)
266

    
267
    def expected_parameters(self):
268
        return [self.lookup_kwarg]
269

    
270
    def choices(self, cl):
271
        yield {
272
            'selected': self.lookup_val is None,
273
            'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
274
            'display': _('All')
275
        }
276
        for lookup, title in self.field.flatchoices:
277
            yield {
278
                'selected': smart_unicode(lookup) == self.lookup_val,
279
                'query_string': cl.get_query_string({
280
                                    self.lookup_kwarg: lookup}),
281
                'display': title,
282
            }
283

    
284
FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
285

    
286

    
287
class DateFieldListFilter(FieldListFilter):
288
    def __init__(self, field, request, params, model, model_admin, field_path):
289
        self.field_generic = '%s__' % field_path
290
        self.date_params = dict([(k, v) for k, v in params.items()
291
                                 if k.startswith(self.field_generic)])
292

    
293
        now = timezone.now()
294
        # When time zone support is enabled, convert "now" to the user's time
295
        # zone so Django's definition of "Today" matches what the user expects.
296
        if now.tzinfo is not None:
297
            current_tz = timezone.get_current_timezone()
298
            now = now.astimezone(current_tz)
299
            if hasattr(current_tz, 'normalize'):
300
                # available for pytz time zones
301
                now = current_tz.normalize(now)
302

    
303
        if isinstance(field, models.DateTimeField):
304
            today = now.replace(hour=0, minute=0, second=0, microsecond=0)
305
        else:       # field is a models.DateField
306
            today = now.date()
307
        tomorrow = today + datetime.timedelta(days=1)
308

    
309
        self.lookup_kwarg_since = '%s__gte' % field_path
310
        self.lookup_kwarg_until = '%s__lt' % field_path
311
        self.links = (
312
            (_('Any date'), {}),
313
            (_('Today'), {
314
                self.lookup_kwarg_since: str(today),
315
                self.lookup_kwarg_until: str(tomorrow),
316
            }),
317
            (_('Past 7 days'), {
318
                self.lookup_kwarg_since: str(today - datetime.timedelta(days=7)),
319
                self.lookup_kwarg_until: str(tomorrow),
320
            }),
321
            (_('This month'), {
322
                self.lookup_kwarg_since: str(today.replace(day=1)),
323
                self.lookup_kwarg_until: str(tomorrow),
324
            }),
325
            (_('This year'), {
326
                self.lookup_kwarg_since: str(today.replace(month=1, day=1)),
327
                self.lookup_kwarg_until: str(tomorrow),
328
            }),
329
        )
330
        super(DateFieldListFilter, self).__init__(
331
            field, request, params, model, model_admin, field_path)
332

    
333
    def expected_parameters(self):
334
        return [self.lookup_kwarg_since, self.lookup_kwarg_until]
335

    
336
    def choices(self, cl):
337
        for title, param_dict in self.links:
338
            yield {
339
                'selected': self.date_params == param_dict,
340
                'query_string': cl.get_query_string(
341
                                    param_dict, [self.field_generic]),
342
                'display': title,
343
            }
344

    
345
FieldListFilter.register(
346
    lambda f: isinstance(f, models.DateField), DateFieldListFilter)
347

    
348

    
349
# This should be registered last, because it's a last resort. For example,
350
# if a field is eligible to use the BooleanFieldListFilter, that'd be much
351
# more appropriate, and the AllValuesFieldListFilter won't get used for it.
352
class AllValuesFieldListFilter(FieldListFilter):
353
    def __init__(self, field, request, params, model, model_admin, field_path):
354
        self.lookup_kwarg = field_path
355
        self.lookup_kwarg_isnull = '%s__isnull' % field_path
356
        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
357
        self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull,
358
                                                 None)
359
        parent_model, reverse_path = reverse_field_path(model, field_path)
360
        queryset = parent_model._default_manager.all()
361
        # optional feature: limit choices base on existing relationships
362
        # queryset = queryset.complex_filter(
363
        #    {'%s__isnull' % reverse_path: False})
364
        limit_choices_to = get_limit_choices_to_from_path(model, field_path)
365
        queryset = queryset.filter(limit_choices_to)
366

    
367
        self.lookup_choices = (queryset
368
                               .distinct()
369
                               .order_by(field.name)
370
                               .values_list(field.name, flat=True))
371
        super(AllValuesFieldListFilter, self).__init__(
372
            field, request, params, model, model_admin, field_path)
373

    
374
    def expected_parameters(self):
375
        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
376

    
377
    def choices(self, cl):
378
        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
379
        yield {
380
            'selected': (self.lookup_val is None
381
                and self.lookup_val_isnull is None),
382
            'query_string': cl.get_query_string({},
383
                [self.lookup_kwarg, self.lookup_kwarg_isnull]),
384
            'display': _('All'),
385
        }
386
        include_none = False
387
        for val in self.lookup_choices:
388
            if val is None:
389
                include_none = True
390
                continue
391
            val = smart_unicode(val)
392
            yield {
393
                'selected': self.lookup_val == val,
394
                'query_string': cl.get_query_string({
395
                    self.lookup_kwarg: val,
396
                }, [self.lookup_kwarg_isnull]),
397
                'display': val,
398
            }
399
        if include_none:
400
            yield {
401
                'selected': bool(self.lookup_val_isnull),
402
                'query_string': cl.get_query_string({
403
                    self.lookup_kwarg_isnull: 'True',
404
                }, [self.lookup_kwarg]),
405
                'display': EMPTY_CHANGELIST_VALUE,
406
            }
407

    
408
FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)