Фильтр по умолчанию в админке Django


94

Как я могу изменить выбор фильтра по умолчанию с «ВСЕ»? У меня есть поле с именем , как у statusкоторого есть три значения: activate, pendingи rejected. Когда я использую list_filterв админке Django, для фильтра по умолчанию установлено значение «Все», но я хочу установить его как ожидающий по умолчанию.

Ответы:


105

Чтобы добиться этого и иметь доступную ссылку «Все» на боковой панели (то есть такую, которая показывает все, а не показывает ожидающие обработки), вам необходимо создать настраиваемый фильтр списка, наследующий от django.contrib.admin.filters.SimpleListFilter«ожидающего» и фильтрующий его по умолчанию. Что-то в этом роде должно работать:

from datetime import date

from django.utils.translation import ugettext_lazy as _
from django.contrib.admin import SimpleListFilter

class StatusFilter(SimpleListFilter):
    title = _('Status')

    parameter_name = 'status'

    def lookups(self, request, model_admin):
        return (
            (None, _('Pending')),
            ('activate', _('Activate')),
            ('rejected', _('Rejected')),
            ('all', _('All')),
        )

    def choices(self, cl):
        for lookup, title in self.lookup_choices:
            yield {
                'selected': self.value() == lookup,
                'query_string': cl.get_query_string({
                    self.parameter_name: lookup,
                }, []),
                'display': title,
            }

    def queryset(self, request, queryset):
        if self.value() in ('activate', 'rejected'):
            return queryset.filter(status=self.value())    
        elif self.value() == None:
            return queryset.filter(status='pending')


class Admin(admin.ModelAdmin): 
    list_filter = [StatusFilter] 

РЕДАКТИРОВАТЬ: требуется Django 1.4 (спасибо Саймону)


3
Это самое чистое решение из всех, но оно имеет наименьшее количество голосов ... для этого требуется Django 1.4, хотя это уже должно быть дано.
Саймон

@Greg Как полностью убрать функциональность фильтра и вкладки фильтра со страницы администратора?


2
У этого решения есть небольшой недостаток. Когда фильтры пусты (фактически используется «ожидающий» фильтр), Django 1.8 неправильно определяет полный счетчик результатов и не показывает счетчик результатов, если show_full_result_count имеет значение True (по умолчанию). -
Александр Федотов

Обратите внимание: если вам не удастся переопределить choicesметод в решении, он продолжит досадно добавлять свой собственный параметр « Все» вверху списка вариантов.
Ричард

47
class MyModelAdmin(admin.ModelAdmin):   

    def changelist_view(self, request, extra_context=None):

        if not request.GET.has_key('decommissioned__exact'):

            q = request.GET.copy()
            q['decommissioned__exact'] = 'N'
            request.GET = q
            request.META['QUERY_STRING'] = request.GET.urlencode()
        return super(MyModelAdmin,self).changelist_view(request, extra_context=extra_context)

18
Это решение имеет недостаток, заключающийся в том, что, хотя выбор «Все» по-прежнему отображается в пользовательском интерфейсе, при его выборе применяется фильтрация по умолчанию.
akaihola

У меня тот же вопрос, но я могу понять воспроизведение ... извините, я новичок в Django ... но, возможно, это сработает blog.dougalmatthews.com/2008/10/…
Asinox

Это хорошо, но мне нужно было увидеть параметр get в URL-адресе, чтобы мой фильтр мог его подобрать и показать выбранным. Вскоре опубликую свое решение.
radtek

объяснение отсутствует. просто размещение фрагмента кода может помочь не всем.
кроме

19

Взял ответ ha22109 выше и изменил, чтобы разрешить выбор «Все» путем сравнения HTTP_REFERERи PATH_INFO.

class MyModelAdmin(admin.ModelAdmin):

    def changelist_view(self, request, extra_context=None):

        test = request.META['HTTP_REFERER'].split(request.META['PATH_INFO'])

        if test[-1] and not test[-1].startswith('?'):
            if not request.GET.has_key('decommissioned__exact'):

                q = request.GET.copy()
                q['decommissioned__exact'] = 'N'
                request.GET = q
                request.META['QUERY_STRING'] = request.GET.urlencode()
        return super(MyModelAdmin,self).changelist_view(request, extra_context=extra_context)

3
Это сломалось для меня, потому что HTTP_REFERER не всегда присутствовал. Я сделал 'referer = request.META.get (' HTTP_REFERER ',' '); test = referer.split (request.META ['PATH_INFO']) `
автор Бен,

@Ben Я использую две ваши строки referer = request.META.get ('HTTP_REFERER', '') test = referer.split (request.META ['PATH_INFO']). Я не особо разбираюсь в HTTP_REFERER. Устранена ли проблема полностью из этих строк, если HTTP_REFERER не указан.
the_game

@the_game да, идея в том, что если вы используете квадратные скобки, чтобы попытаться получить доступ к ключу, которого не существует, он выдает KeyError, а если вы используете метод dict, get()вы можете указать значение по умолчанию. Я указал по умолчанию пустую строку, чтобы split () не генерировал AttributeError. Вот и все.
Бен автор

@Ben. Спасибо, что у меня работает. Вы также можете ответить на этот вопрос, я считаю, что это расширение только для этого вопроса stackoverflow.com/questions/10410982/… . Не могли бы вы предоставить мне решение для этого.
the_game

1
Это хорошо работает. has_key()устарел в пользу key in d. Но я знаю, что вы только что взяли из ответа ha22109. Один вопрос: зачем использовать, request.META['PATH_INFO']если можно было просто использовать request.path_info(короче)?
Ник

19

Я знаю, что этот вопрос довольно старый, но он все еще актуален. Я считаю, что это наиболее правильный способ сделать это. По сути, это то же самое, что и метод Грега, но сформулированный как расширяемый класс для удобства повторного использования.

from django.contrib.admin import SimpleListFilter
from django.utils.encoding import force_text
from django.utils.translation import ugettext as _

class DefaultListFilter(SimpleListFilter):
    all_value = '_all'

    def default_value(self):
        raise NotImplementedError()

    def queryset(self, request, queryset):
        if self.parameter_name in request.GET and request.GET[self.parameter_name] == self.all_value:
            return queryset

        if self.parameter_name in request.GET:
            return queryset.filter(**{self.parameter_name:request.GET[self.parameter_name]})

        return queryset.filter(**{self.parameter_name:self.default_value()})

    def choices(self, cl):
        yield {
            'selected': self.value() == self.all_value,
            'query_string': cl.get_query_string({self.parameter_name: self.all_value}, []),
            'display': _('All'),
        }
        for lookup, title in self.lookup_choices:
            yield {
                'selected': self.value() == force_text(lookup) or (self.value() == None and force_text(self.default_value()) == force_text(lookup)),
                'query_string': cl.get_query_string({
                    self.parameter_name: lookup,
                }, []),
                'display': title,
            }

class StatusFilter(DefaultListFilter):
    title = _('Status ')
    parameter_name = 'status__exact'

    def lookups(self, request, model_admin):
        return ((0,'activate'), (1,'pending'), (2,'rejected'))

    def default_value(self):
        return 1

class MyModelAdmin(admin.ModelAdmin):
    list_filter = (StatusFilter,)

8

Вот мое общее решение с использованием перенаправления, оно просто проверяет, есть ли какие-либо параметры GET, если их нет, перенаправляет с параметром get по умолчанию. У меня также есть набор list_filter, поэтому он выбирает его и отображает значение по умолчанию.

from django.shortcuts import redirect

class MyModelAdmin(admin.ModelAdmin):   

    ...

    list_filter = ('status', )

    def changelist_view(self, request, extra_context=None):
        referrer = request.META.get('HTTP_REFERER', '')
        get_param = "status__exact=5"
        if len(request.GET) == 0 and '?' not in referrer:
            return redirect("{url}?{get_parms}".format(url=request.path, get_parms=get_param))
        return super(MyModelAdmin,self).changelist_view(request, extra_context=extra_context)

Единственное предостережение - когда вы попадаете на страницу напрямую с помощью "?" присутствует в URL-адресе, HTTP_REFERER не установлен, поэтому он будет использовать параметр по умолчанию и перенаправлять. Для меня это нормально, отлично работает, когда вы щелкаете через фильтр администратора.

ОБНОВЛЕНИЕ :

Чтобы обойти предостережение, я написал специальную функцию фильтра, которая упростила функциональность changelist_view. Вот фильтр:

class MyModelStatusFilter(admin.SimpleListFilter):
    title = _('Status')
    parameter_name = 'status'

    def lookups(self, request, model_admin):  # Available Values / Status Codes etc..
        return (
            (8, _('All')),
            (0, _('Incomplete')),
            (5, _('Pending')),
            (6, _('Selected')),
            (7, _('Accepted')),
        )

    def choices(self, cl):  # Overwrite this method to prevent the default "All"
        from django.utils.encoding import force_text
        for lookup, title in self.lookup_choices:
            yield {
                'selected': self.value() == force_text(lookup),
                'query_string': cl.get_query_string({
                    self.parameter_name: lookup,
                }, []),
                'display': title,
            }

    def queryset(self, request, queryset):  # Run the queryset based on your lookup values
        if self.value() is None:
            return queryset.filter(status=5)
        elif int(self.value()) == 0:
            return queryset.filter(status__lte=4)
        elif int(self.value()) == 8:
            return queryset.all()
        elif int(self.value()) >= 5:
            return queryset.filter(status=self.value())
        return queryset.filter(status=5)

И changelist_view теперь передает параметр по умолчанию, только если его нет. Идея заключалась в том, чтобы избавиться от возможности универсальных фильтров просматривать все без использования параметров получения. Чтобы просмотреть все, я присвоил для этой цели статус = 8:

class MyModelAdmin(admin.ModelAdmin):   

    ...

    list_filter = ('status', )

    def changelist_view(self, request, extra_context=None):
        if len(request.GET) == 0:
            get_param = "status=5"
            return redirect("{url}?{get_parms}".format(url=request.path, get_parms=get_param))
        return super(MyModelAdmin, self).changelist_view(request, extra_context=extra_context)

У меня есть исправление для моего предупреждения - настраиваемый фильтр. Представлю это как альтернативное решение.
radtek

Спасибо, я считаю перенаправление самым чистым и простым решением. Я тоже не понимаю "нюанс". Я всегда получаю желаемый результат, щелкая или используя прямую ссылку (я не использовал настраиваемый фильтр).
Денис Голомазов

6
def changelist_view( self, request, extra_context = None ):
    default_filter = False
    try:
        ref = request.META['HTTP_REFERER']
        pinfo = request.META['PATH_INFO']
        qstr = ref.split( pinfo )

        if len( qstr ) < 2:
            default_filter = True
    except:
        default_filter = True

    if default_filter:
        q = request.GET.copy()
        q['registered__exact'] = '1'
        request.GET = q
        request.META['QUERY_STRING'] = request.GET.urlencode()

    return super( InterestAdmin, self ).changelist_view( request, extra_context = extra_context )

4

Вы можете просто использовать return queryset.filter()или if self.value() is Noneи Override метод SimpleListFilter

from django.utils.encoding import force_text

def choices(self, changelist):
    for lookup, title in self.lookup_choices:
        yield {
            'selected': force_text(self.value()) == force_text(lookup),
            'query_string': changelist.get_query_string(
                {self.parameter_name: lookup}, []
            ),
            'display': title,
        }

3

Обратите внимание: если вместо предварительного выбора значения фильтра вы хотите всегда предварительно фильтровать данные перед их отображением в админке, вам следует ModelAdmin.queryset()вместо этого переопределить метод.


Это довольно простое и быстрое решение, хотя оно может вызывать проблемы. Когда в админке включены параметры фильтрации, пользователь может получить неверные результаты. Если переопределенный набор запросов содержит предложение .exclude (), то записи, перехваченные им, никогда не будут перечислены, но параметры фильтрации администратора, чтобы явно показать их, по-прежнему будут предлагаться пользовательским интерфейсом администратора.
Tomas Andrle

Есть и другие более правильные ответы с меньшим количеством голосов, которые применимы к этой ситуации, поскольку OP четко просил, чтобы он собирался поставить фильтр, в котором набор запросов был бы неправильным решением, как также указано @TomasAndrle выше.
eskhool

Спасибо, что указали на это @eskhool, я попытался снизить свой ответ до нуля, но, похоже, нельзя голосовать за себя.
akaihola

3

Небольшое улучшение ответа Грега с использованием DjangoChoices, Python> = 2.5 и, конечно же, Django> = 1.4.

from django.utils.translation import ugettext_lazy as _
from django.contrib.admin import SimpleListFilter

class OrderStatusFilter(SimpleListFilter):
    title = _('Status')

    parameter_name = 'status__exact'
    default_status = OrderStatuses.closed

    def lookups(self, request, model_admin):
        return (('all', _('All')),) + OrderStatuses.choices

    def choices(self, cl):
        for lookup, title in self.lookup_choices:
            yield {
                'selected': self.value() == lookup if self.value() else lookup == self.default_status,
                'query_string': cl.get_query_string({self.parameter_name: lookup}, []),
                'display': title,
            }

    def queryset(self, request, queryset):
        if self.value() in OrderStatuses.values:
            return queryset.filter(status=self.value())
        elif self.value() is None:
            return queryset.filter(status=self.default_status)


class Admin(admin.ModelAdmin):
    list_filter = [OrderStatusFilter] 

Спасибо Грегу за прекрасное решение!


2

Я знаю, что это не лучшее решение, но я изменил index.html в шаблоне администратора, строки 25 и 37 следующим образом:

25: <th scope="row"><a href="{{ model.admin_url }}{% ifequal model.name "yourmodelname" %}?yourflag_flag__exact=1{% endifequal %}">{{ model.name }}</a></th>

37: <td><a href="{{ model.admin_url }}{% ifequal model.name "yourmodelname" %}?yourflag__exact=1{% endifequal %}" class="changelink">{% trans 'Change' %}</a></td>


1

Мне пришлось внести изменения, чтобы фильтрация работала правильно. Предыдущее решение сработало у меня при загрузке страницы. Если было выполнено «действие», фильтр возвращался к «Все», а не к моему значению по умолчанию. Это решение загружает страницу изменения администратора с фильтром по умолчанию, но также сохраняет изменения фильтра или текущий фильтр, когда на странице происходит другая активность. Я не проверял все случаи, но на самом деле это может ограничивать настройку фильтра по умолчанию только при загрузке страницы.

def changelist_view(self, request, extra_context=None):
    default_filter = False

    try:
        ref = request.META['HTTP_REFERER']
        pinfo = request.META['PATH_INFO']
        qstr = ref.split(pinfo)
        querystr = request.META['QUERY_STRING']

        # Check the QUERY_STRING value, otherwise when
        # trying to filter the filter gets reset below
        if querystr is None:
            if len(qstr) < 2 or qstr[1] == '':
                default_filter = True
    except:
        default_filter = True

    if default_filter:
        q = request.GET.copy()
        q['registered__isnull'] = 'True'
        request.GET = q
        request.META['QUERY_STRING'] = request.GET.urlencode()

    return super(MyAdmin, self).changelist_view(request, extra_context=extra_context)

1

Немного не по теме, но мои поиски аналогичного вопроса привели меня сюда. Я искал запрос по умолчанию по дате (то есть, если ввод не предоставлен, показывать только объекты с timestamp«Сегодня»), что немного усложняет вопрос. Вот что я придумал:

from django.contrib.admin.options import IncorrectLookupParameters
from django.core.exceptions import ValidationError

class TodayDefaultDateFieldListFilter(admin.DateFieldListFilter):
    """ If no date is query params are provided, query for Today """

    def queryset(self, request, queryset):
        try:
            if not self.used_parameters:
                now = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
                self.used_parameters = {
                    ('%s__lt' % self.field_path): str(now + datetime.timedelta(days=1)),
                    ('%s__gte' % self.field_path): str(now),
                }
                # Insure that the dropdown reflects 'Today'
                self.date_params = self.used_parameters
            return queryset.filter(**self.used_parameters)
        except ValidationError, e:
            raise IncorrectLookupParameters(e)

class ImagesAdmin(admin.ModelAdmin):
    list_filter = (
        ('timestamp', TodayDefaultDateFieldListFilter),
    )

Это простая замена значения по умолчанию DateFieldListFilter. При настройке self.date_paramsон гарантирует, что раскрывающийся список фильтра будет обновлен до любого варианта, соответствующего параметру self.used_parameters. По этой причине вы должны убедиться, что self.used_parametersэто именно то, что будет использоваться одним из этих раскрывающихся списков (т. Е. Выяснить, что date_paramsбудет при использовании «Сегодня» или «Последние 7 дней», и сконструировать self.used_parametersсоответствующие им).

Это было создано для работы с Django 1.4.10.


1

Это может быть старый поток, но я подумал, что добавлю свое решение, так как я не мог найти лучших ответов по поиску в Google.

Сделайте то, что (не уверен, что его Deminic Rodger или ha22109) ответил в ModelAdmin для changelist_view

class MyModelAdmin(admin.ModelAdmin):   
    list_filter = (CustomFilter,)

    def changelist_view(self, request, extra_context=None):

        if not request.GET.has_key('decommissioned__exact'):

            q = request.GET.copy()
            q['decommissioned__exact'] = 'N'
            request.GET = q
            request.META['QUERY_STRING'] = request.GET.urlencode()
        return super(MyModelAdmin,self).changelist_view(request, extra_context=extra_context)

Затем нам нужно создать собственный SimpleListFilter

class CustomFilter(admin.SimpleListFilter):
    title = 'Decommissioned'
    parameter_name = 'decommissioned'  # i chose to change it

def lookups(self, request, model_admin):
    return (
        ('All', 'all'),
        ('1', 'Decommissioned'),
        ('0', 'Active (or whatever)'),
    )

# had to override so that we could remove the default 'All' option
# that won't work with our default filter in the ModelAdmin class
def choices(self, cl):
    yield {
        'selected': self.value() is None,
        'query_string': cl.get_query_string({}, [self.parameter_name]),
        # 'display': _('All'),
    }
    for lookup, title in self.lookup_choices:
        yield {
            'selected': self.value() == lookup,
            'query_string': cl.get_query_string({
                self.parameter_name: lookup,
            }, []),
            'display': title,
        }

def queryset(self, request, queryset):
    if self.value() == '1':
        return queryset.filter(decommissioned=1)
    elif self.value() == '0':
        return queryset.filter(decommissioned=0)
    return queryset

Я обнаружил, что мне нужно использовать функцию force_text (также известную как force_unicode) в вызове yield в функции choices, иначе выбранный параметр фильтра не будет отображаться как «выбранный». Это «'selected': self.value () == force_text (lookup)»,
MagicLAMP

1

Вот самая чистая версия, которую мне удалось создать, фильтра с переопределенным «Все» и выбранным значением по умолчанию.

Если по умолчанию отображает текущие поездки.

class HappeningTripFilter(admin.SimpleListFilter):
    """
    Filter the Trips Happening in the Past, Future or now.
    """
    default_value = 'now'
    title = 'Happening'
    parameter_name = 'happening'

    def lookups(self, request, model_admin):
        """
        List the Choices available for this filter.
        """
        return (
            ('all', 'All'),
            ('future', 'Not yet started'),
            ('now', 'Happening now'),
            ('past', 'Already finished'),
        )

    def choices(self, changelist):
        """
        Overwrite this method to prevent the default "All".
        """
        value = self.value() or self.default_value
        for lookup, title in self.lookup_choices:
            yield {
                'selected': value == force_text(lookup),
                'query_string': changelist.get_query_string({
                    self.parameter_name: lookup,
                }, []),
                'display': title,
            }

    def queryset(self, request, queryset):
        """
        Returns the Queryset depending on the Choice.
        """
        value = self.value() or self.default_value
        now = timezone.now()
        if value == 'future':
            return queryset.filter(start_date_time__gt=now)
        if value == 'now':
            return queryset.filter(start_date_time__lte=now, end_date_time__gte=now)
        if value == 'past':
            return queryset.filter(end_date_time__lt=now)
        return queryset.all()

0

Создал подкласс многоразового фильтра, вдохновленный некоторыми ответами здесь (в основном Грегом).

Преимущества:

Многоразовый - подключается к любым стандартным ModelAdminклассам

Расширяемость - легко добавлять дополнительную / настраиваемую логику для QuerySetфильтрации

Простота использования - в самой базовой форме необходимо реализовать только один настраиваемый атрибут и один настраиваемый метод (помимо тех, которые требуются для создания подклассов SimpleListFilter)

Интуитивно понятный администратор - ссылка фильтра «Все» работает должным образом; как и все остальные

Без перенаправлений - нет необходимости проверять GETполезную нагрузку запроса, независимо от HTTP_REFERER(или любого другого материала, связанного с запросом, в его базовой форме)

Нет (список изменений) манипуляции с просмотром - И никаких манипуляций с шаблоном (не дай бог)

Код:

(большинство imports предназначены только для подсказок типов и исключений)

from typing import List, Tuple, Any

from django.contrib.admin.filters import SimpleListFilter
from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.views.main import ChangeList
from django.db.models.query import QuerySet
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError


class PreFilteredListFilter(SimpleListFilter):

    # Either set this or override .get_default_value()
    default_value = None

    no_filter_value = 'all'
    no_filter_name = _("All")

    # Human-readable title which will be displayed in the
    # right admin sidebar just above the filter options.
    title = None

    # Parameter for the filter that will be used in the URL query.
    parameter_name = None

    def get_default_value(self):
        if self.default_value is not None:
            return self.default_value
        raise NotImplementedError(
            'Either the .default_value attribute needs to be set or '
            'the .get_default_value() method must be overridden to '
            'return a URL query argument for parameter_name.'
        )

    def get_lookups(self) -> List[Tuple[Any, str]]:
        """
        Returns a list of tuples. The first element in each
        tuple is the coded value for the option that will
        appear in the URL query. The second element is the
        human-readable name for the option that will appear
        in the right sidebar.
        """
        raise NotImplementedError(
            'The .get_lookups() method must be overridden to '
            'return a list of tuples (value, verbose value).'
        )

    # Overriding parent class:
    def lookups(self, request, model_admin) -> List[Tuple[Any, str]]:
        return [(self.no_filter_value, self.no_filter_name)] + self.get_lookups()

    # Overriding parent class:
    def queryset(self, request, queryset: QuerySet) -> QuerySet:
        """
        Returns the filtered queryset based on the value
        provided in the query string and retrievable via
        `self.value()`.
        """
        if self.value() is None:
            return self.get_default_queryset(queryset)
        if self.value() == self.no_filter_value:
            return queryset.all()
        return self.get_filtered_queryset(queryset)

    def get_default_queryset(self, queryset: QuerySet) -> QuerySet:
        return queryset.filter(**{self.parameter_name: self.get_default_value()})

    def get_filtered_queryset(self, queryset: QuerySet) -> QuerySet:
        try:
            return queryset.filter(**self.used_parameters)
        except (ValueError, ValidationError) as e:
            # Fields may raise a ValueError or ValidationError when converting
            # the parameters to the correct type.
            raise IncorrectLookupParameters(e)

    # Overriding parent class:
    def choices(self, changelist: ChangeList):
        """
        Overridden to prevent the default "All".
        """
        value = self.value() or force_str(self.get_default_value())
        for lookup, title in self.lookup_choices:
            yield {
                'selected': value == force_str(lookup),
                'query_string': changelist.get_query_string({self.parameter_name: lookup}),
                'display': title,
            }

Пример полного использования:

from django.contrib import admin
from .models import SomeModelWithStatus


class StatusFilter(PreFilteredListFilter):
    default_value = SomeModelWithStatus.Status.FOO
    title = _('Status')
    parameter_name = 'status'

    def get_lookups(self):
        return SomeModelWithStatus.Status.choices


@admin.register(SomeModelWithStatus)
class SomeModelAdmin(admin.ModelAdmin):
    list_filter = (StatusFilter, )

Надеюсь, это кому-то поможет; обратная связь всегда ценится.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.