Filtro padrão no Django admin

94

Como posso alterar a opção de filtro padrão de 'TODOS'? Eu tenho um campo nomeado como statusque tem três valores: activate, pendinge rejected. Quando eu uso list_filterno Django admin, o filtro é por padrão definido como 'All', mas eu quero defini-lo como pendente por padrão.

ha22109
fonte

Respostas:

102

Para conseguir isso e ter um link 'Todos' utilizável em sua barra lateral (ou seja, um que mostra tudo em vez de mostrar pendentes), você precisa criar um filtro de lista personalizado, herdando de django.contrib.admin.filters.SimpleListFiltere filtrando por 'pendente' por padrão. Algo nesse sentido deve funcionar:

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] 

EDITAR: Requer Django 1.4 (obrigado Simon)

Greg
fonte
3
Esta é a solução mais limpa de todas, mas tem o menor número de votos positivos ... requer Django 1.4, embora isso já deva ser um dado adquirido agora.
Simon
@Greg Como você remove completamente a funcionalidade do filtro e a guia de filtro da página de administração?
2
Essa solução tem uma pequena desvantagem. Quando os filtros estão vazios (na verdade, o filtro 'pendente' usado), o Django 1.8 determina incorretamente a contagem completa do resultado e não mostra a contagem do resultado se show_full_result_count for True (por padrão). -
Alexander Fedotov
Observe que, se você não substituir o choicesmétodo na solução, ele continuará irritantemente a adicionar sua própria opção Todos no topo da lista de opções.
richard
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)
ha22109
fonte
18
Essa solução tem a desvantagem de que, embora a opção "Todos" ainda seja exibida na IU, selecioná-la ainda aplica a filtragem padrão.
akaihola
eu tenho a mesma pergunta, mas eu posso entender o replay ... desculpe, sou novo com Django ... mas talvez isso funcione blog.dougalmatthews.com/2008/10/…
Asinox
Isso é bom, mas eu precisava ver o parâmetro get na url para que meu filtro pudesse pegá-lo e mostrá-lo selecionado. Postando minha solução em breve.
radtek
explicação faltando. apenas postar um pedaço de código pode não ajudar a todos. além disso, não está funcionando e sem um pouco de contexto é difícil descobrir o porquê
EvilSmurf
19

Retirou a resposta de ha22109 acima e modificou para permitir a seleção de "Todos" comparando HTTP_REFERERe 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)
iridescente
fonte
3
Isso quebrou para mim porque HTTP_REFERER nem sempre estava presente. Eu fiz 'referer = request.META.get (' HTTP_REFERER ',' '); test = referer.split (request.META ['PATH_INFO']) `
ben autor
@Ben Estou usando suas duas linhas referer = request.META.get ('HTTP_REFERER', '') test = referer.split (request.META ['PATH_INFO']). Não gosto muito de HTTP_REFERER. O problema foi completamente corrigido a partir dessas linhas se HTTP_REFERER não estiver presente.
the_game
@ the_game sim, a ideia é se você usar colchetes para tentar acessar uma chave que não existe, ele joga KeyError, enquanto se você usar o get()método do dict, você pode especificar um padrão. Especifiquei um padrão de string vazia para que split () não lance AttributeError. Isso é tudo.
autor ben
@Ben. Obrigado, funciona para mim. Além disso, você pode responder a esta pergunta, eu acredito que esta é uma extensão desta pergunta apenas stackoverflow.com/questions/10410982/… . Você pode me fornecer uma solução para isso.
the_game
1
Isso funciona bem. has_key()é preterido em favor de key in d, no entanto. Mas eu sei que você acabou de tirar a resposta de ha22109. Uma pergunta: por que usar request.META['PATH_INFO']quando você poderia apenas usar request.path_info(mais curto)?
Nick
19

Eu sei que esta questão é bastante antiga agora, mas ainda é válida. Acredito que essa seja a maneira mais correta de fazer isso. É essencialmente o mesmo que o método de Greg, mas formulado como uma classe extensível para fácil reutilização.

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,)
Andrew Hows
fonte
8

Aqui está minha solução genérica usando redirecionamento, ele apenas verifica se há algum parâmetro GET; se nenhum existir, ele redireciona com o parâmetro get padrão. Eu também tenho um list_filter definido para que ele pegue e exiba o padrão.

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)

A única ressalva é quando você acessa diretamente a página com "?" presente na url, não há HTTP_REFERER definido, então ele usará o parâmetro padrão e o redirecionará. Isso é bom para mim, funciona muito bem quando você clica no filtro de administrador.

ATUALIZAÇÃO :

Para contornar a advertência, acabei escrevendo uma função de filtro personalizado que simplificou a funcionalidade changelist_view. Aqui está o filtro:

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)

E o changelist_view agora só passa o parâmetro padrão se nenhum estiver presente. A ideia era livrar-se da capacidade dos filtros genéricos de visualizar tudo sem usar parâmetros get. Para ver todos atribuímos o status = 8 para esse propósito:

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
fonte
Eu tenho uma correção para minha ressalva, um filtro personalizado. Vou apresentá-lo como uma solução alternativa.
radtek
Obrigado, acho que o redirecionamento é a solução mais limpa e simples. Eu também não entendo "a advertência". Sempre consigo o resultado desejado, seja clicando ou usando o link direto (não usei o filtro personalizado).
Dennis Golomazov
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 )
user1163719
fonte
4

Você pode simplesmente usar return queryset.filter()ou if self.value() is Nonee o método Override de 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,
        }
Jay Dave
fonte
3

Observe que, se em vez de pré-selecionar um valor de filtro, você quiser sempre pré-filtrar os dados antes de mostrá-los no admin, você deve substituir o ModelAdmin.queryset()método.

Akaihola
fonte
Esta é uma solução bastante limpa e rápida, embora ainda possa causar problemas. Quando as opções de filtragem são habilitadas no administrador, o usuário pode obter resultados aparentemente incorretos. Se o queryset substituído contiver uma cláusula .exclude (), os registros capturados por isso nunca serão listados, mas as opções de filtragem do administrador para mostrá-los explicitamente ainda serão oferecidas pela IU do administrador.
Tomas Andrle
Existem outras respostas mais corretas com votos mais baixos que se aplicam a esta situação, visto que o OP claramente solicitou que ele colocasse um filtro no qual um queryset seria a solução errada como também apontado por @TomasAndrle acima.
eskhool
Obrigado por apontar isso @eskhool, tentei rebaixar minha resposta para zero, mas parece que não é permitido rebaixar a si mesmo.
akaihola
3

Uma pequena melhoria na resposta de Greg usando DjangoChoices, Python> = 2.5 e claro 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] 

Obrigado a Greg pela boa solução!

Ben Konrath
fonte
2

Eu sei que não é a melhor solução, mas mudei o index.html no modelo de admin, linha 25 e 37 assim:

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>

Mauro De Giorgi
fonte
1

Tive que fazer uma modificação para que a filtragem funcionasse corretamente. A solução anterior funcionou para mim quando a página carregou. Se uma 'ação' foi realizada, o filtro voltou para 'Todos' e não para o meu padrão. Esta solução carrega a página de alteração do administrador com o filtro padrão, mas também mantém as alterações do filtro ou o filtro atual quando outra atividade ocorre na página. Não testei todos os casos, mas na realidade isso pode estar limitando a configuração de um filtro padrão para ocorrer apenas quando a página carrega.

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)
mhck
fonte
1

Um pouco fora do assunto, mas minha busca por uma questão semelhante me trouxe até aqui. Eu queria ter uma consulta padrão por uma data (ou seja, se nenhuma entrada for fornecida, mostre apenas objetos com timestampde 'Hoje'), o que complica um pouco a questão. Aqui está o que eu descobri:

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),
    )

Esta é uma substituição simples do padrão DateFieldListFilter. Ao definir self.date_params, ele garante que o menu suspenso do filtro será atualizado para qualquer opção que corresponda a self.used_parameters. Por esse motivo, você deve garantir que self.used_parameterssão exatamente o que seria usado por uma dessas seleções suspensas (ou seja, descobrir o que date_paramsseria ao usar 'Hoje' ou 'Últimos 7 dias' e construir oself.used_parameters para corresponder a eles).

Isto foi construído para funcionar com Django 1.4.10

Alukach
fonte
1

Este pode ser um tópico antigo, mas pensei em adicionar minha solução, pois não consegui encontrar respostas melhores nas pesquisas do Google.

Faça o que (não tenho certeza se é Deminic Rodger ou ha22109) respondeu no ModelAdmin para 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)

Em seguida, precisamos criar um SimpleListFilter personalizado

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
codificador de guerra
fonte
Descobri que precisava usar a função 'force_text' (também conhecida como force_unicode) na chamada de rendimento na função de escolhas, caso contrário, a opção de filtro selecionada não apareceria como 'selecionada'. Isso é "'selecionado': self.value () == force_text (lookup),"
MagicLAMP
1

Esta é a versão mais limpa que consegui gerar de um filtro com 'Todos' redefinido e um valor padrão selecionado.

Se me mostra por padrão as viagens que estão acontecendo no momento.

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()
Jerome Millet
fonte
0

Criou uma subclasse de Filtro reutilizável, inspirada por algumas das respostas aqui (principalmente de Greg).

Vantagens:

Reutilizável - conectável em qualquer ModelAdminclasse padrão

Extensível - Fácil de adicionar lógica adicional / personalizada paraQuerySet filtragem

Fácil de usar - Em sua forma mais básica, apenas um atributo personalizado e um método personalizado precisam ser implementados (além daqueles exigidos para a subclasse SimpleListFilter)

Admin intuitivo - O link de filtro "Todos" está funcionando conforme o esperado; como são todos os outros

Sem redirecionamentos - não há necessidade de inspecionar a GETcarga útil da solicitação, independente de HTTP_REFERER(ou qualquer outro material relacionado à solicitação, em sua forma básica)

Não (changelist) manipulação de visualização - E nenhuma manipulação de template (deus me livre)

Código:

(a maioria dos imports são apenas para dicas de tipo e exceções)

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,
            }

Exemplo de uso completo:

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, )

Espero que isso ajude alguém; feedback sempre apreciado.

JohnGalt
fonte