114

How can I change the default filter choice from 'ALL'? I have a field named as status which has three values: activate, pending and rejected. When I use list_filter in Django admin, the filter is by default set to 'All' but I want to set it to pending by default.

Cedric Druck
  • 1,032
  • 7
  • 20
ha22109
  • 8,036
  • 13
  • 44
  • 48

18 Answers18

128

In order to achieve this and have a usable 'All' link in your sidebar (ie one that shows all rather than showing pending), you'd need to create a custom list filter, inheriting from django.contrib.admin.filters.SimpleListFilter and filtering on 'pending' by default. Something along these lines should work:

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: Requires Django 1.4 (thanks Simon)

Greg
  • 9,963
  • 5
  • 43
  • 46
  • 5
    This is the cleanest solution of all, yet it has the fewest upvotes... it requires Django 1.4, though, although that should be a given by now. – Simon May 16 '13 at 21:58
  • @Greg How do you **completely remove the functionality of the filter** and the filter tab out of the admin page? –  Jul 16 '15 at 04:43
  • Um... https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter – Greg Jul 16 '15 at 20:37
  • 2
    This solution has a small drawback. When filters is empty (actually used 'pending' filter), Django 1.8 incorrectly determine the full result count and not show result count if **show_full_result_count** is True (by default). – – Alexander Fedotov Aug 06 '15 at 08:30
  • 2
    Note that if you fail to override the `choices` method in the solution, it will annoyingly continue to add its own **_All_** option at the top of the list of choices. – richard Dec 16 '16 at 04:27
  • Good answer, leading to the solution. However if your default filter cannot be None, I would propose one more feature that would also indicate the selection within the filter view In the `StatusFilter` class set a variable with your default option lookup value: `default_filter = 'rejected'` and check for it within the `choices` callback: `...'selected': self.value() == lookup if self.value() else (True if lookup == self.default_filter else False),...` – EvilSmurf Jun 19 '20 at 10:32
  • Great Answer thanks ! Just noticing "ugettext_lazy" has been deprecated, and now we can use "gettext_lazy" – averta Jun 01 '23 at 08:56
49
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)
Dominic Rodger
  • 97,747
  • 36
  • 197
  • 212
ha22109
  • 8,036
  • 13
  • 44
  • 48
  • 24
    This solution has the drawback that although the "All" choice is still displayed in the UI, selecting it still applies the default filtering. – akaihola May 29 '09 at 14:47
  • i have the same question, but i can understand the replay...sorry im new with Django... but maybe this will work http://blog.dougalmatthews.com/2008/10/filter-the-django-modeladmin-set/ – Asinox Aug 15 '09 at 05:01
  • This is good but I needed to see the get parameter in the url so that my filter can pick it up and show it selected. Posting my solution shortly. – radtek Sep 11 '14 at 15:09
  • explanation missing. just posting a piece of code may not help everyone. on top of it it's not working and without a little context it is hard to find out why – EvilSmurf Jun 19 '20 at 10:05
22

I know this question is quite old now, but it's still valid. I believe this is the most correct way of doing this. It's essentially the same as Greg's method, but formulated as an extendible class for easy re-use.

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
  • 1,429
  • 10
  • 16
22

Took ha22109's answer above and modified to allow the selection of "All" by comparing HTTP_REFERER and 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)
iridescent
  • 3,303
  • 1
  • 16
  • 10
  • 3
    This broke for me because HTTP_REFERER was not always present. I did 'referer = request.META.get('HTTP_REFERER', ''); test = referer.split(request.META['PATH_INFO'])` – ben author Jan 24 '12 at 21:51
  • @Ben I am using your two lines referer = request.META.get('HTTP_REFERER', '') test = referer.split(request.META['PATH_INFO']). I don't much about HTTP_REFERER . Is the problem fixed completely from these lines if HTTP_REFERER is not present. – the_game May 02 '12 at 08:55
  • @the_game yeah, the idea is if you use square brackets to attempt to access a key that doesn't exists, it throws `KeyError`, wheras if you use the dict's `get()` method you can specify a default. I specified a default of empty-string so that split() doesn't throw `AttributeError`. That's all. – ben author May 05 '12 at 13:24
  • @Ben .Thanks it works for me. Also can you answer this question i believe this is an extension to this question only http://stackoverflow.com/questions/10410982/default-django-admin-list-filter . Can you please provide me a solution for this. – the_game May 06 '12 at 05:29
  • 3
    This works well. `has_key()` is deprecated in favor of `key in d`, though. But I know you just took from ha22109's answer. One question: why use `request.META['PATH_INFO']` when you could just use `request.path_info` (shorter)? – Nick Dec 05 '12 at 18:23
  • @Nick No real reason other than the fact that it _may_ help in understanding the snippet by referencing only the request.META dict. – iridescent Jan 07 '13 at 03:48
12

Here is my generic solution using redirect, it just checks if there are any GET parameters, if none exist then it redirects with the default get parameter. I also have a list_filter set so it picks that up and displays the default.

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)

The only caveat is when you do a direct get to the page with "?" present in the url, there is no HTTP_REFERER set so it will use the default parameter and redirect. This is fine for me, it works great when you click through the admin filter.

UPDATE:

In order to get around the caveat, I ended up writing a custom filter function which simplified the changelist_view functionality. Here is the filter:

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)

And the changelist_view now only passes the default parameter if none are present. The idea was to get rid of the generics filters capability to view all by using no get parameters. To view all I assigned the status = 8 for that purpose.:

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
  • 34,210
  • 11
  • 144
  • 111
  • I have a fix for my caveat, a custom filter. I'll present it as an alternative solution. – radtek Oct 07 '14 at 15:00
  • Thank you, I find the redirect to be the cleanest and simplest solution. I also don't understand "the caveat". I always get the desired result, whether by clicking or using direct link (I didn't use the custom filter). – Dennis Golomazov Apr 28 '15 at 14:14
6

Created a reusable Filter sub-class, inspired by some of the answers here (mostly Greg's).

Advantages:

Reusable - Pluggable in any standard ModelAdmin classes

Extendable - Easy to add additional/custom logic for QuerySet filtering

Easy to use - In its most basic form, only one custom attribute and one custom method need to be implemented (apart from those required for SimpleListFilter subclassing)

Intuitive admin - The "All" filter link is working as expected; as are all the others

No redirects - No need to inspect GET request payload, agnostic of HTTP_REFERER (or any other request related stuff, in its basic form)

No (changelist) view manipulation - And no template manipulations (god forbid)

Code:

(most of the imports are just for type hints and exceptions)

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

Full usage example:

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

Hope this helps somebody; feedback always appreciated.

JohnGalt
  • 797
  • 1
  • 9
  • 21
  • I liked your solution the best, in terms of design, so thanks! However, something's not working for me. I can't seem to see the "All" option when using your solution. I'm using Django 2.2.12. I can see the overridden "choices" being called instead of the parent, and I can see the overridden "lookups" generate the All choice correctly. Any tips? – Guy May 25 '22 at 06:07
  • This is the best answer in 2023 as far as I can tell. Works perfectly, and easily adaptable. Thank you! – matthewn Apr 28 '23 at 22:29
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
  • 69
  • 1
  • 2
4

You can simply usereturn queryset.filter() or if self.value() is None and Override method of 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,
        }
dqd
  • 1,501
  • 14
  • 10
Jay Dave
  • 147
  • 8
4

Note that if instead of pre-selecting a filter value you want to always pre-filter the data before showing it in the admin, you should override the ModelAdmin.queryset() method instead.

akaihola
  • 26,309
  • 7
  • 59
  • 69
  • This is a pretty clean and quick solution although it may still cause problems. When the filtering options are enabled in the admin the user may get seemingly incorrect results. If the overriden queryset contains an .exclude() clause then records caught by that will never be listed but the admin filtering options to explicitly show them will still be offered by the admin UI. – Tomas Andrle Jul 16 '09 at 20:25
  • There are other more correct answers with lower votes that apply to this situation since the OP has clearly requested that he is going to put a filter in which a queryset would be the wrong solution as also pointed by @TomasAndrle above. – eskhool Sep 20 '15 at 11:11
  • Thanks for pointing this out @eskhool, I tried to downvote my answer to zero but seems it's not allowed to downvote oneself. – akaihola Oct 28 '15 at 04:42
3

A slight improvement on Greg's answer using DjangoChoices, Python >= 2.5 and of course 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] 

Thanks to Greg for the nice solution!

Ben Konrath
  • 423
  • 4
  • 8
2

I know that is not the best solution, but i changed the index.html in the admin template, line 25 and 37 like this:

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
  • 394
  • 2
  • 11
  • You might be interested in my solution which does a similar thing but without changing templates: https://stackoverflow.com/a/76433512/519074 – eggbert Jun 08 '23 at 15:40
2

Here's the Cleanest version I was able to generate of a filter with a redefined 'All' and a Default value that is selected.

If shows me by default the Trips currently happening.

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
  • 51
  • 1
  • 2
1

I had to make a modification to get filtering to work correctly. The previous solution worked for me when the page loaded. If an 'action' was performed, the filter went back to 'All' and not my default. This solution loads the admin change page with the default filter, but also maintains filter changes or the current filter when other activity occurs on the page. I haven't tested all cases, but in reality it may be limiting the setting of a default filter to occur only when the page loads.

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
  • 873
  • 1
  • 10
  • 20
1

A bit off-topic but my search for a similar question led me here. I was looking to have a default query by a date (ie if no input is provided, show only objects with timestamp of 'Today'), which complicates the question a bit. Here is what I came up with:

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

This is a simple override of the default DateFieldListFilter. By setting self.date_params, it insures that the filter dropdown will update to whatever option matches the self.used_parameters. For this reason, you must insure that the self.used_parameters are exactly what would be used by one of those dropdown selections (ie, find out what the date_params would be when using the 'Today' or 'Last 7 Days' and construct the self.used_parameters to match those).

This was built to work with Django 1.4.10

alukach
  • 5,921
  • 3
  • 39
  • 40
1

This may be an old thread, but thought I would add my solution as I couldn't find better answers on google searches.

Do what (not sure if its Deminic Rodger, or ha22109) answered in the 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)

Then we need to create a custom 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
warath-coder
  • 2,087
  • 1
  • 17
  • 21
  • I found I needed to use the 'force_text' (aka force_unicode) function in the yield call in the choices function, else the selected filter option would not show up as 'selected'. That is " 'selected': self.value() == force_text(lookup)," – MagicLAMP Nov 06 '15 at 00:14
0

Using ha22109's answer I wrote a mixin for ModelAdmin class:

from urllib.parse import urlencode
from django.contrib.admin.views.main import SEARCH_VAR
from django.http import HttpRequest, QueryDict


class DefaultFilterMixin:

    default_filters: Sequence[tuple[str, Any]] | dict[str, Any] | None = None

    def get_default_filters(
        self,
        request: HttpRequest,
    ) -> Sequence[tuple[str, Any]] | dict[str, Any] | None:
        return self.default_filters

    def changelist_view(
        self,
        request: HttpRequest,
        extra_context: dict[str, str] | None = None,
    ):
        if request.method == 'GET' and not request.GET:
            if default_filters := self.get_default_filters(request):
                request.GET = QueryDict(
                    f"{urlencode(default_filters)}&{SEARCH_VAR}=",
                    encoding=request.encoding,
                )
                request.META['QUERY_STRING'] = request.GET.urlencode()

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

Simple example:

class MyModelAdmin(DefaultFilterMixin, admin.ModelAdmin):

    default_filters = (("status__exact", "pending"),)
    ...

Or more complex dynamic filter for rangefilter.DateTimeRangeFilter:

class MyModelAdmin(DefaultFilterMixin, admin.ModelAdmin):

        def get_default_filters(
            self,
            request: HttpRequest,
        ) -> Sequence[tuple[str, Any]] | dict[str, Any] | None:
            now = timezone.now()
            date_fmt = '%d.%m.%Y'
            return (
                ('created_at__range__gte_0', now.strftime(date_fmt)),
                ('created_at__range__gte_1', '00:00:00'),
                ('created_at__range__lte_0', (now + timedelta(1)).strftime(date_fmt)),
                ('created_at__range__lte_1', '00:00:00'),
            )
Evgeni Shudzel
  • 231
  • 2
  • 5
0

replying to the first answer (from Evgeni Shudzel) with the default mixin... here is a solution with less imports and complications

from urllib.parse import urlencode
from django.contrib.admin.views.main import SEARCH_VAR
from django.http import HttpRequest, QueryDict

class DefaultFilterMixin:
    default_filters: None

    def get_default_filters(self, request: HttpRequest):
        return self.default_filters

    def changelist_view(self, request: HttpRequest, extra_context=None):
        if request.method == 'GET' and not request.GET:
            if default_filters := self.get_default_filters(request):
                request.GET = QueryDict(
                    f"{urlencode(default_filters)}&{SEARCH_VAR}=",
                    encoding=request.encoding,
                )
                request.META['QUERY_STRING'] = request.GET.urlencode()

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

class SampleUsageAdmin(DefaultFilterMixin, admin.ModelAdmin):
    default_filters = (("fulfillments_complete__exact", "0"),)
johnfidel
  • 62
  • 3
0

I did this a slightly different way because none of the answers worked quite right for my use case. I just overrode AdminSite.get_app_list and changed the actual url to include the filter:

class MyAdminSite(admin.AdminSite):
    def get_app_list(self, request, app_label=None):
        app_list = super().get_app_list(request, app_label)
        for app_dict in app_list:
            for model_dict in app_dict["models"]:
                model = model_dict["model"]
                model_admin = self._registry[model]
                if default_filters := getattr(model_admin, "default_list_filters", None):
                    model_dict["admin_url"] += "?" + urlencode(default_filters)

        return app_list

See https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#overriding-default-admin-site for how to override AdminSite.

This then picks up default_list_filters if defined on a model admin, like so:

@admin.register(Job)
class JobAdmin(admin.ModelAdmin, ):
    default_list_filters = {
        "date-range": "today",
    }

This therefore has the following advantages over the other solutions:

  • you don't have to make a custom Filter
  • the actual url in the address bar is correct
  • doesn't require a redirect
  • simple

Update: Unfortunately it breaks the highlighting of the currently selected model on the left sidebar

eggbert
  • 3,105
  • 5
  • 30
  • 39