Filtro predeterminado en el administrador de Django


94

¿Cómo puedo cambiar la opción de filtro predeterminada de 'TODOS'? Tengo un campo denominado como statusel que tiene tres valores: activate, pendingy rejected. Cuando lo uso list_filteren el administrador de Django, el filtro está configurado de forma predeterminada en 'Todos', pero quiero configurarlo como pendiente de forma predeterminada.

Respuestas:


102

Para lograr esto y tener un enlace 'Todos' utilizable en su barra lateral (es decir, uno que muestre todo en lugar de mostrar pendiente), deberá crear un filtro de lista personalizado, heredando django.contrib.admin.filters.SimpleListFiltery filtrando en 'pendiente' de forma predeterminada. Algo parecido a esto debería 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: Requiere Django 1.4 (gracias Simon)


3
Esta es la solución más limpia de todas, sin embargo, tiene la menor cantidad de votos positivos ... requiere Django 1.4, aunque eso debería ser un hecho por ahora.
Simon

@Greg ¿Cómo eliminas por completo la funcionalidad del filtro y la pestaña de filtro de la página de administración?


2
Esta solución tiene un pequeño inconveniente. Cuando los filtros están vacíos (en realidad se usa el filtro 'pendiente'), Django 1.8 determina incorrectamente el recuento completo de resultados y no muestra el recuento de resultados si show_full_result_count es True (por defecto). -
Alexander Fedotov

Tenga en cuenta que si no puede anular el choicesmétodo en la solución, continuará agregando su propia opción Todas en la parte superior de la lista de opciones.
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)

18
Esta solución tiene el inconveniente de que, aunque la opción "Todo" todavía se muestra en la interfaz de usuario, al seleccionarla se aplica el filtrado predeterminado.
akaihola

Tengo la misma pregunta, pero puedo entender la repetición ... lo siento, soy nuevo en Django ... pero tal vez esto funcione blog.dougalmatthews.com/2008/10/…
Asinox

Esto es bueno, pero necesitaba ver el parámetro get en la URL para que mi filtro pueda recogerlo y mostrarlo seleccionado. Publicando mi solución en breve.
radtek

Falta explicación. simplemente publicar un fragmento de código puede no ayudar a todos. Además, no está funcionando y sin un poco de contexto es difícil averiguar por qué
EvilSmurf

19

Tomó la respuesta de ha22109 anterior y la modificó para permitir la selección de "Todos" comparando HTTP_REFERERy 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
Esto se rompió para mí porque HTTP_REFERER no siempre estaba presente. Hice 'referer = request.META.get (' HTTP_REFERER ',' '); test = referer.split (request.META ['PATH_INFO']) `
ben autor

@Ben Estoy usando sus dos líneas referer = request.META.get ('HTTP_REFERER', '') test = referer.split (request.META ['PATH_INFO']). No me gusta mucho HTTP_REFERER. ¿Se solucionó el problema por completo desde estas líneas si HTTP_REFERER no está presente?
the_game

@the_game sí, la idea es que si usa corchetes para intentar acceder a una clave que no existe, arroja KeyError, mientras que si usa el get()método del dict, puede especificar un valor predeterminado. Especifiqué un valor predeterminado de cadena vacía para que split () no arroje AttributeError. Eso es todo.
ben autor

@Ben. Gracias, funciona para mí. También puede responder a esta pregunta. Creo que esta es una extensión de esta pregunta solo stackoverflow.com/questions/10410982/… . ¿Puede proporcionarme una solución para esto?
the_game

1
Esto funciona bien. sin embargo, has_key()está en desuso a favor de key in d. Pero sé que acabas de tomar de la respuesta de ha22109. Una pregunta: ¿por qué usarlo request.META['PATH_INFO']cuando solo podría usar request.path_info(más corto)?
Nick

19

Sé que esta pregunta es bastante antigua ahora, pero sigue siendo válida. Creo que esta es la forma más correcta de hacer esto. Es esencialmente el mismo que el método de Greg, pero formulado como una clase extensible para una fácil reutilización.

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

Aquí está mi solución genérica usando redireccionamiento, solo verifica si hay algún parámetro GET, si no existe ninguno, entonces redirige con el parámetro get predeterminado. También tengo un list_filter configurado, por lo que lo recoge y muestra el valor predeterminado.

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)

La única advertencia es cuando accedes directamente a la página con "?" presente en la URL, no hay HTTP_REFERER configurado, por lo que usará el parámetro predeterminado y redireccionará. Esto está bien para mí, funciona muy bien cuando haces clic en el filtro de administración.

ACTUALIZAR :

Para evitar la advertencia, terminé escribiendo una función de filtro personalizada que simplificó la funcionalidad changelist_view. Aquí está el 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)

Y changelist_view ahora solo pasa el parámetro predeterminado si no hay ninguno presente. La idea era deshacerse de la capacidad de los filtros genéricos para ver todo sin usar parámetros de obtención. Para ver todo lo que asigné el estado = 8 para ese 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)

Tengo una solución para mi advertencia, un filtro personalizado. Lo presentaré como una solución alternativa.
Radtek

Gracias, creo que la redirección es la solución más limpia y sencilla. Tampoco entiendo "la advertencia". Siempre obtengo el resultado deseado, ya sea haciendo clic o usando un enlace directo (no usé el 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 )

4

Simplemente puede usar return queryset.filter()o if self.value() is Noney el 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,
        }

3

Tenga en cuenta que si en lugar de preseleccionar un valor de filtro, siempre desea filtrar previamente los datos antes de mostrarlos en el administrador, debe anular el ModelAdmin.queryset()método.


Esta es una solución bastante limpia y rápida, aunque aún puede causar problemas. Cuando las opciones de filtrado están habilitadas en el administrador, el usuario puede obtener resultados aparentemente incorrectos. Si el conjunto de consultas anulado contiene una cláusula .exclude (), los registros capturados por eso nunca se enumerarán, pero la interfaz de usuario del administrador seguirá ofreciendo las opciones de filtrado de administrador para mostrarlos explícitamente.
Tomas Andrle

Hay otras respuestas más correctas con votos más bajos que se aplican a esta situación, ya que el OP ha solicitado claramente que vaya a poner un filtro en el que un conjunto de consultas sería la solución incorrecta, como también señaló @TomasAndrle anteriormente.
eskhool

Gracias por señalar esto @eskhool, traté de rechazar mi respuesta a cero, pero parece que no está permitido votar a sí mismo.
akaihola

3

Una ligera mejora en la respuesta de Greg usando DjangoChoices, Python> = 2.5 y, por supuesto, 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] 

¡Gracias a Greg por la buena solución!


2

Sé que no es la mejor solución, pero cambié el index.html en la plantilla de administración, línea 25 y 37 así:

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

Tuve que hacer una modificación para que el filtrado funcionara correctamente. La solución anterior funcionó para mí cuando se cargó la página. Si se realizó una 'acción', el filtro volvió a 'Todo' y no a mi predeterminado. Esta solución carga la página de cambio de administrador con el filtro predeterminado, pero también mantiene los cambios de filtro o el filtro actual cuando ocurre otra actividad en la página. No he probado todos los casos, pero en realidad puede estar limitando la configuración de un filtro predeterminado para que ocurra solo cuando se carga la página.

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

Un poco fuera de tema, pero mi búsqueda de una pregunta similar me llevó aquí. Estaba buscando tener una consulta predeterminada por fecha (es decir, si no se proporciona ninguna entrada, mostrar solo los objetos con timestamp"Hoy"), lo que complica un poco la pregunta. Esto es lo que se me ocurrió:

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 es una simple anulación del valor predeterminado DateFieldListFilter. Al establecerlo self.date_params, asegura que el menú desplegable del filtro se actualizará a cualquier opción que coincida con el self.used_parameters. Por esta razón, debe asegurarse de que self.used_parameterssean exactamente lo que usaría una de esas selecciones desplegables (es decir, averigüe cuál date_paramssería cuando use 'Hoy' o 'Últimos 7 días' y construya el self.used_parameterspara que coincida con esos).

Esto fue creado para funcionar con Django 1.4.10


1

Este puede ser un hilo antiguo, pero pensé en agregar mi solución ya que no pude encontrar mejores respuestas en las búsquedas de Google.

Haga lo que (no estoy seguro de si es Deminic Rodger o ha22109) respondió en 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)

Entonces necesitamos crear un 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

Descubrí que necesitaba usar la función 'force_text' (también conocida como force_unicode) en la llamada de rendimiento en la función de opciones, de lo contrario, la opción de filtro seleccionada no se mostraría como 'seleccionada'. Eso es "'seleccionado': self.value () == force_text (lookup),"
MagicLAMP

1

Aquí está la versión más limpia que pude generar de un filtro con un 'Todo' redefinido y un valor predeterminado que está seleccionado.

Si me muestra por defecto los viajes que están ocurriendo actualmente.

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

Creó una subclase de filtro reutilizable, inspirada en algunas de las respuestas aquí (principalmente las de Greg).

Ventajas:

Reutilizable : conectable en cualquier ModelAdminclase estándar

Ampliable : fácil de agregar lógica adicional / personalizada para el QuerySetfiltrado

Fácil de usar : en su forma más básica, solo es necesario implementar un atributo personalizado y un método personalizado (aparte de los requeridos para la subclasificación de SimpleListFilter)

Administración intuitiva : el enlace del filtro "Todos" funciona como se esperaba; como son todos los demas

Sin redirecciones : no es necesario inspeccionar la GETcarga útil de la solicitud, agnóstica de HTTP_REFERER(o cualquier otra cosa relacionada con la solicitud, en su forma básica)

Sin manipulación de vistas (lista de cambios) , y sin manipulaciones de plantillas (Dios no lo quiera)

Código:

(la mayoría de las imports son solo para sugerencias de tipo y excepciones)

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

Ejemplo 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 esto ayude a alguien; comentarios siempre apreciados.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.