Ticket #19755: 19755.diff

File 19755.diff, 15.9 KB (added by suligap, 23 months ago)
  • django/contrib/admin/filters.py

    diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py
    index ff66d3e..c4f7ca7 100644
    a b class ListFilter(object): 
    5454        """
    5555        raise NotImplementedError
    5656
     57    def limit_lookup_choices(self, queryset):
     58        pass
     59
    5760
    5861class SimpleListFilter(ListFilter):
    5962    # The parameter that should be used in the query string for that filter.
    class FieldListFilter(ListFilter): 
    118121        self.field = field
    119122        self.field_path = field_path
    120123        self.title = getattr(field, 'verbose_name', field_path)
     124        self.limited_qs = None
    121125        super(FieldListFilter, self).__init__(
    122126            request, params, model, model_admin)
    123127        for p in self.expected_parameters():
    class FieldListFilter(ListFilter): 
    134138        except ValidationError as e:
    135139            raise IncorrectLookupParameters(e)
    136140
     141    def limit_lookup_choices(self, queryset):
     142        self.limited_qs = queryset
     143
    137144    @classmethod
    138145    def register(cls, test, list_filter_class, take_priority=False):
    139146        if take_priority:
    class FieldListFilter(ListFilter): 
    157164
    158165class RelatedFieldListFilter(FieldListFilter):
    159166    def __init__(self, field, request, params, model, model_admin, field_path):
    160         other_model = get_model_from_relation(field)
     167        self.other_model = get_model_from_relation(field)
    161168        if hasattr(field, 'rel'):
    162169            rel_name = field.rel.get_related_field().name
    163170        else:
    164             rel_name = other_model._meta.pk.name
     171            rel_name = self.other_model._meta.pk.name
    165172        self.lookup_kwarg = '%s__%s__exact' % (field_path, rel_name)
    166173        self.lookup_kwarg_isnull = '%s__isnull' % field_path
    167174        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    168175        self.lookup_val_isnull = request.GET.get(
    169176                                      self.lookup_kwarg_isnull, None)
    170         self.lookup_choices = field.get_choices(include_blank=False)
     177        self._lookup_choices = None
    171178        super(RelatedFieldListFilter, self).__init__(
    172179            field, request, params, model, model_admin, field_path)
    173180        if hasattr(field, 'verbose_name'):
    174181            self.lookup_title = field.verbose_name
    175182        else:
    176             self.lookup_title = other_model._meta.verbose_name
     183            self.lookup_title = self.other_model._meta.verbose_name
    177184        self.title = self.lookup_title
    178185
    179186    def has_output(self):
    class RelatedFieldListFilter(FieldListFilter): 
    183190            extra = 1
    184191        else:
    185192            extra = 0
    186         return len(self.lookup_choices) + extra > 1
     193        return len(self.lookup_choices) + extra > 0
    187194
    188195    def expected_parameters(self):
    189196        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
    190197
     198    def _get_lookup_choices(self):
     199        if self._lookup_choices is None:
     200            if self.limited_qs is None:
     201                self._lookup_choices = self.field.get_choices(include_blank=False)
     202            else:
     203                pks = (self.limited_qs
     204                       .distinct()
     205                       .values_list(self.field_path, flat=True))
     206                self._lookup_choices = [(x._get_pk_val(), smart_text(x)) for x in
     207                                        self.other_model.objects.filter(pk__in=pks)]
     208        return self._lookup_choices
     209    lookup_choices = property(_get_lookup_choices)
     210
    191211    def choices(self, cl):
    192212        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    193213        yield {
    class BooleanFieldListFilter(FieldListFilter): 
    233253        return [self.lookup_kwarg, self.lookup_kwarg2]
    234254
    235255    def choices(self, cl):
    236         for lookup, title in (
    237                 (None, _('All')),
    238                 ('1', _('Yes')),
    239                 ('0', _('No'))):
     256        lookup, title = None, _('All')
     257        yield {
     258            'selected': self.lookup_val == lookup and not self.lookup_val2,
     259            'query_string': cl.get_query_string({
     260                    self.lookup_kwarg: lookup,
     261                }, [self.lookup_kwarg2]),
     262            'display': title,
     263        }
     264        choices = self.lookup_choices()
     265        for choice in choices:
     266            if choice is None:
     267                continue
     268            elif choice:
     269                lookup, title = '1', _('Yes')
     270            else:
     271                lookup, title = '0', _('No')
    240272            yield {
    241273                'selected': self.lookup_val == lookup and not self.lookup_val2,
    242274                'query_string': cl.get_query_string({
    class BooleanFieldListFilter(FieldListFilter): 
    244276                    }, [self.lookup_kwarg2]),
    245277                'display': title,
    246278            }
    247         if isinstance(self.field, models.NullBooleanField):
     279        if None in choices:
    248280            yield {
    249281                'selected': self.lookup_val2 == 'True',
    250282                'query_string': cl.get_query_string({
    class BooleanFieldListFilter(FieldListFilter): 
    253285                'display': _('Unknown'),
    254286            }
    255287
     288    def lookup_choices(self):
     289        if self.limited_qs is None:
     290            if isinstance(self.field, models.NullBooleanField):
     291                choices = (True, False, None)
     292            else:
     293                choices = (True, False)
     294        else:
     295            choices = (self.limited_qs
     296                       .distinct()
     297                       .values_list(self.field_path, flat=True))
     298            choices = tuple(ch for ch in (True, False, None) if ch in choices)
     299        return choices
     300
    256301FieldListFilter.register(lambda f: isinstance(f,
    257302    (models.BooleanField, models.NullBooleanField)), BooleanFieldListFilter)
    258303
    class ChoicesFieldListFilter(FieldListFilter): 
    261306    def __init__(self, field, request, params, model, model_admin, field_path):
    262307        self.lookup_kwarg = '%s__exact' % field_path
    263308        self.lookup_val = request.GET.get(self.lookup_kwarg)
     309        self._lookup_choices = None
    264310        super(ChoicesFieldListFilter, self).__init__(
    265311            field, request, params, model, model_admin, field_path)
    266312
     313    def has_output(self):
     314        return len(self.lookup_choices) > 0
     315
    267316    def expected_parameters(self):
    268317        return [self.lookup_kwarg]
    269318
    class ChoicesFieldListFilter(FieldListFilter): 
    273322            'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
    274323            'display': _('All')
    275324        }
    276         for lookup, title in self.field.flatchoices:
     325        for lookup, title in self.lookup_choices:
    277326            yield {
    278327                'selected': smart_text(lookup) == self.lookup_val,
    279328                'query_string': cl.get_query_string({
    class ChoicesFieldListFilter(FieldListFilter): 
    281330                'display': title,
    282331            }
    283332
     333    def _get_lookup_choices(self):
     334        if self._lookup_choices is None:
     335            if self.limited_qs is None:
     336                self._lookup_choices = self.field.flatchoices
     337            else:
     338                choices_dict = dict(self.field.flatchoices)
     339                choices = (self.limited_qs
     340                           .distinct()
     341                           .order_by(self.field_path)
     342                           .values_list(self.field_path, flat=True))
     343                self._lookup_choices = []
     344                for choice in choices:
     345                    if choice is not None:
     346                        title = force_text(choices_dict.get(choice, choice),
     347                                           strings_only=True)
     348                        self._lookup_choices.append((choice, title))
     349        return self._lookup_choices
     350    lookup_choices = property(_get_lookup_choices)
     351
    284352FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
    285353
    286354
    class DateFieldListFilter(FieldListFilter): 
    302370            today = now.date()
    303371        tomorrow = today + datetime.timedelta(days=1)
    304372
     373        self.today = today
     374        self.tomorrow = tomorrow
     375
    305376        self.lookup_kwarg_since = '%s__gte' % field_path
    306377        self.lookup_kwarg_until = '%s__lt' % field_path
    307         self.links = (
    308             (_('Any date'), {}),
    309             (_('Today'), {
    310                 self.lookup_kwarg_since: str(today),
    311                 self.lookup_kwarg_until: str(tomorrow),
    312             }),
    313             (_('Past 7 days'), {
    314                 self.lookup_kwarg_since: str(today - datetime.timedelta(days=7)),
    315                 self.lookup_kwarg_until: str(tomorrow),
    316             }),
    317             (_('This month'), {
    318                 self.lookup_kwarg_since: str(today.replace(day=1)),
    319                 self.lookup_kwarg_until: str(tomorrow),
    320             }),
    321             (_('This year'), {
    322                 self.lookup_kwarg_since: str(today.replace(month=1, day=1)),
    323                 self.lookup_kwarg_until: str(tomorrow),
    324             }),
    325         )
     378        self._links = None
    326379        super(DateFieldListFilter, self).__init__(
    327380            field, request, params, model, model_admin, field_path)
    328381
     382    def has_output(self):
     383        return len(self.links) > 1
     384
    329385    def expected_parameters(self):
    330386        return [self.lookup_kwarg_since, self.lookup_kwarg_until]
    331387
    class DateFieldListFilter(FieldListFilter): 
    338394                'display': title,
    339395            }
    340396
     397    def _get_links(self):
     398        if self._links is None:
     399            self._links = []
     400            qs = self.limited_qs
     401            queries = (
     402                (_('Today'), (
     403                    (self.lookup_kwarg_since, self.today),
     404                    (self.lookup_kwarg_until, self.tomorrow),
     405                )),
     406                (_('Past 7 days'), (
     407                    (self.lookup_kwarg_since, self.today - datetime.timedelta(days=7)),
     408                    (self.lookup_kwarg_until, self.tomorrow),
     409                )),
     410                (_('This month'), (
     411                    (self.lookup_kwarg_since, self.today.replace(day=1)),
     412                    (self.lookup_kwarg_until, self.tomorrow),
     413                )),
     414                (_('This year'), (
     415                    (self.lookup_kwarg_since, self.today.replace(day=1, month=1)),
     416                    (self.lookup_kwarg_until, self.tomorrow),
     417                )),
     418            )
     419            self._links.append((_('Any date'), {}))
     420            for title, query in queries:
     421                if qs is None or qs.filter(**dict(query)).exists():
     422                    self._links.append((title, dict((k, str(v)) for k, v in query)))
     423        return self._links
     424    links = property(_get_links)
     425
     426
    341427FieldListFilter.register(
    342428    lambda f: isinstance(f, models.DateField), DateFieldListFilter)
    343429
    class AllValuesFieldListFilter(FieldListFilter): 
    352438        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    353439        self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull,
    354440                                                 None)
    355         parent_model, reverse_path = reverse_field_path(model, field_path)
     441        parent_model, self.reverse_path = reverse_field_path(model, field_path)
    356442        queryset = parent_model._default_manager.all()
    357443        # optional feature: limit choices base on existing relationships
    358444        # queryset = queryset.complex_filter(
    359445        #    {'%s__isnull' % reverse_path: False})
    360446        limit_choices_to = get_limit_choices_to_from_path(model, field_path)
    361447        queryset = queryset.filter(limit_choices_to)
     448        self.field_qs = queryset
    362449
    363         self.lookup_choices = (queryset
    364                                .distinct()
    365                                .order_by(field.name)
    366                                .values_list(field.name, flat=True))
     450        self._lookup_choices = None
    367451        super(AllValuesFieldListFilter, self).__init__(
    368452            field, request, params, model, model_admin, field_path)
    369453
     454    def has_output(self):
     455        return len(self.lookup_choices) > 0
     456
    370457    def expected_parameters(self):
    371458        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
    372459
    class AllValuesFieldListFilter(FieldListFilter): 
    401488                'display': EMPTY_CHANGELIST_VALUE,
    402489            }
    403490
     491    def _get_lookup_choices(self):
     492        if self._lookup_choices is None:
     493            if self.limited_qs is None:
     494                qs = self.field_qs
     495                field_name = self.field.name
     496            else:
     497                qs = self.limited_qs
     498                field_name = self.field_path
     499            self._lookup_choices = (qs
     500                                    .distinct()
     501                                    .order_by(field_name)
     502                                    .values_list(field_name, flat=True))
     503        return self._lookup_choices
     504    lookup_choices = property(_get_lookup_choices)
     505
    404506FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index b35f100..ee8759f 100644
    a b class ModelAdmin(BaseModelAdmin): 
    345345    list_display = ('__str__',)
    346346    list_display_links = ()
    347347    list_filter = ()
     348    list_filter_incremental = False
    348349    list_select_related = False
    349350    list_per_page = 100
    350351    list_max_show_all = 200
  • django/contrib/admin/validation.py

    diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
    index a02bb7a..1efbcd7 100644
    a b def validate(cls, model): 
    163163    if hasattr(cls, "readonly_fields"):
    164164        check_readonly_fields(cls, model, opts)
    165165
     166    # list_filter_incremental = False
    166167    # list_select_related = False
    167168    # save_as = False
    168169    # save_on_top = False
    169     for attr in ('list_select_related', 'save_as', 'save_on_top'):
     170    for attr in ('list_filter_incremental', 'list_select_related', 'save_as', 'save_on_top'):
    170171        if not isinstance(getattr(cls, attr), bool):
    171172            raise ImproperlyConfigured("'%s.%s' should be a boolean."
    172173                    % (cls.__name__, attr))
  • django/contrib/admin/views/main.py

    diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
    index 050d477..9b78561 100644
    a b class ChangeList(six.with_metaclass(RenameChangeListMethods)): 
    158158                    use_distinct = (use_distinct or
    159159                                    lookup_needs_distinct(self.lookup_opts,
    160160                                                          field_path))
    161                 if spec and spec.has_output():
     161                incremental = self.model_admin.list_filter_incremental
     162                if spec and (incremental or spec.has_output()):
    162163                    filter_specs.append(spec)
    163164
    164165        # At this point, all the parameters used by the various ListFilters
    class ChangeList(six.with_metaclass(RenameChangeListMethods)): 
    330331
    331332    def get_queryset(self, request):
    332333        # First, we collect all the declared list filters.
    333         (self.filter_specs, self.has_filters, remaining_lookup_params,
     334        (filter_specs, self.has_filters, remaining_lookup_params,
    334335         use_distinct) = self.get_filters(request)
    335336
    336337        # Then, we let every list filter modify the queryset to its liking.
    337338        qs = self.root_queryset
    338         for filter_spec in self.filter_specs:
     339        for filter_spec in filter_specs:
    339340            new_qs = filter_spec.queryset(request, qs)
    340341            if new_qs is not None:
    341342                qs = new_qs
    342343
     344        # incremental list filter, limit lookup choices
     345        if self.model_admin.list_filter_incremental:
     346            for filter_spec in filter_specs:
     347                filter_spec.limit_lookup_choices(qs)
     348
     349        # only filters with output are saved
     350        self.filter_specs = [fs for fs in filter_specs if fs.has_output()]
     351
    343352        try:
    344353            # Finally, we apply the remaining lookup parameters from the query
    345354            # string (i.e. those that haven't already been processed by the
Back to Top