Фільтр за замовчуванням у адміністратора Django


94

Як я можу змінити вибір фільтру за замовчуванням на "ALL"? У мене є поле з ім'ям , як у statusякого є три значення: activate, pendingі rejected. Коли я використовую list_filterв адміністраторі Django, за замовчуванням для фільтра встановлено значення "Усі", але я хочу встановити його за замовчуванням на очікування.

Відповіді:


102

Для того, щоб досягти цього та мати на боковій панелі придатне посилання "Всі" (тобто таке, яке відображає все, а не відображає очікування), вам потрібно буде створити власний фільтр списку, успадковуючи 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] 

EDIT: Потрібна Django 1.4 (спасибі Саймон)


3
Це найчистіше рішення з усіх, однак воно має найменшу кількість голосів ... проте потрібен Django 1.4, хоча це повинно бути дано вже зараз.
Симон

@Greg Як ви повністю видалите функціональність фільтра та вкладки фільтру зі сторінки адміністратора?


2
Це рішення має невеликий недолік. Якщо фільтри порожні (фактично використовується фільтр "в очікуванні"), Django 1.8 неправильно визначає повний підрахунок результатів, а не показує кількість результатів, якщо показник show_full_result_count - True (за замовчуванням). -
Олександр Федотов

Зауважте, що якщо вам не вдасться перекрити choicesметод у рішенні, він буде дратівливо продовжувати додавати власну опцію All у верхній частині списку варіантів.
Річард

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 = referenr.split (request.META ['PATH_INFO']). Я не дуже багато про HTTP_REFERER. Чи повністю виправлена ​​проблема з цих рядків, якщо HTTP_REFERER відсутній.
the_game

@the_game Так, ідея полягає в тому, якщо ви використовуєте квадратні дужки для спроби отримати доступ до ключа, який не існує, він кидає KeyError, якщо ви використовуєте get()метод dict, ви можете вказати типовий замовчування. Я вказав за замовчуванням порожній рядок, щоб 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і Замінити 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 (), тоді записи, що потрапили в нього, ніколи не будуть перераховані, але параметри фільтрації адміністратора для явного їх відображення все ще будуть запропоновані інтерфейсом адміністратора.
Томаш Андрле

Існують інші більш правильні відповіді з меншими оцінками, які стосуються цієї ситуації, оскільки ОП чітко просив, щоб він ввів фільтр, в якому набір запитів був би неправильним рішенням, на що також вказував @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 for 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, інакше вибраний параметр фільтра не відображатиметься як 'обраний'. Тобто "'вибрано': self.value () == force_text (пошук),"
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.