Django Admin Custom Change List Arguments: Override /?e=1
Asked Answered
J

4

28

I'm trying to pass in a custom argument to the Django Admin change list view so I can filter the list in a specialized way. I'd like to filter the queryset on 2 fields, start_date and end_date, based on the GET parameter called 'active_pp'. I've gotten the filtering to work correctly, but I'm not able to pass in a GET query parameter that specifies whether I should display the filtered results or the normal results.

I know that, due to security, the Django Admin filters out any query parameters passed to it that aren't related to specified model fields; upon finding bad arguments, the admin redirects the user to the current view but replaces the GET query parameters with e=1. I'd like to whitelist my custom 'active_pp' parameter so the page won't be redirected and I'll be able to use the parameter.

Here is an example of the ModelAdmin in admin.py with the queryset customization.

class FeaduredAdmin(admin.ModelAdmin): 

    ....

    def get_changelist(self, request, **kwargs):
        from django.contrib.admin.views.main import ChangeList

        # Try to get the 'active_pp' query parameter
        active_pp = request.GET.get('active_pp',None)

        # Define a custom ChangeList class with a custom queryset
        class ActiveChangeList(ChangeList):
            def get_query_set(self, *args, **kwargs):
                now = datetime.datetime.now()
                qs = super(ActiveChangeList, self).get_query_set(*args, **kwargs)
                return qs.filter((Q(start_date=None) | Q(start_date__lte=now))
                             & (Q(end_date=None) | Q(end_date__gte=now)))

        # use the custom ChangeList class if the parameter exists
        if active_pp:
             return ActiveChangeList

        return ChangeList

Does anyone know how to whitelist custom GET querystring arguments passed to the change_list?

Thanks for reading and for your consideration, Joe

UPDATE:

Using Uvasal's provided links, I was able to properly whitelist the GET parameter.

class ActiveFilterAminForm(forms.Form):
    active_pp = forms.CharField()

class FeaduredAdmin(admin.ModelAdmin): 

    ....

    # Based on: http://djangosnippets.org/snippets/2322/
    advanced_search_form = ActiveFilterAminForm()

    def get_changelist(self, request, **kwargs):

        from django.contrib.admin.views.main import ChangeList
        active_pp = self.other_search_fields.get('active_pp',None)
        # now we have the active_pp parameter that was passed in and can use it.

        class ActiveChangeList(ChangeList):

            def get_query_set(self, *args, **kwargs):
                now = datetime.datetime.now()
                qs = super(ActiveChangeList, self).get_query_set(*args, **kwargs)
                return qs.filter((Q(start_date=None) | Q(start_date__lte=now))
                                 & (Q(end_date=None) | Q(end_date__gte=now)))

        if not active_pp is None:
            return ActiveChangeList

        return ChangeList


    def lookup_allowed(self, lookup):
        if lookup in self.advanced_search_form.fields.keys():
            return True
        return super(MyModelAdmin, self).lookup_allowed(lookup)


    def changelist_view(self, request, extra_context=None, **kwargs):
        self.other_search_fields = {} 
        asf = self.advanced_search_form
        extra_context = {'asf':asf}

        request.GET._mutable=True

        for key in asf.fields.keys():
            try:
                temp = request.GET.pop(key)
            except KeyError:
                pass 
            else:
                if temp!=['']: 
                    self.other_search_fields[key] = temp 

        request.GET_mutable=False
        return super(FeaduredProductAdmin, self)\
               .changelist_view(request, extra_context=extra_context)
Jonejonell answered 13/12, 2011 at 18:23 Comment(2)
This seems to be a useful enough feature to have proper DRY django support.Lubricious
Note that you will need to update this to "get_queryset" instead of "get_query_set" for later versions of DjangoKinsella
M
8

I think you just need to put your custom filter fields in the search_fields class variable as outlined in the Advanced Search Django Snippet.

You should be able to modify the snippet to support date ranges as well.

Menendez answered 13/12, 2011 at 19:28 Comment(1)
Thanks Uvasal. The link that you provided really helped. I've updated my code above with the implementation based on what you provided. - JoeJonejonell
T
11

I know this is an old post, but just ran into a need for this and discovered a very short and simple solution that I thought I would share. Key here is to make a filter that doesn't affect the queryset and accepts anything passed to it in the lookups as valid option. Something like the following:

from django.contrib.admin import SimpleListFilter    

class PassThroughFilter(SimpleListFilter):
    title = ''
    parameter_name = 'pt'
    template = 'admin/hidden_filter.html'

    def lookups(self, request, model_admin):
        return (request.GET.get(self.parameter_name), ''),

    def queryset(self, request, queryset):
        return queryset

The hidden_filter template is blank to prevent adding anything to the filter area, and the lookups method will always return whatever I have entered for the pt parameter as a valid filter entry. This will prevent the ?e=1 error from popping up as the page loads.

This can be reused with any admin, using the pt parameter. If you need to pass multiple parameters for a single admin, then just subclass this into separate filters and override parameter_name with whatever parameters you need. This will have the effect of allowing those parameters in the query string without affecting the queryset or showing up in the filter column, and you can then use them for whatever purpose you needed them elsewhere.

Hope this helps someone down the road.

Tonettetoney answered 4/6, 2019 at 13:44 Comment(2)
100% best solution!Romona
To clarify: the hidden filter template has to be created as empty file under templates/admin/hidden_filter.html.Dirac
M
8

I think you just need to put your custom filter fields in the search_fields class variable as outlined in the Advanced Search Django Snippet.

You should be able to modify the snippet to support date ranges as well.

Menendez answered 13/12, 2011 at 19:28 Comment(1)
Thanks Uvasal. The link that you provided really helped. I've updated my code above with the implementation based on what you provided. - JoeJonejonell
L
6

In summary, here is the undocumented hack used above:

set request.GET._mutable = True, then request.GET.pop() off the custom GET argument(s) you are using.

Lupine answered 21/4, 2014 at 20:36 Comment(0)
A
0

I have simplified accepted solution This will not work as ModelAdmin instance is reused between requests and self.active_pp stores previous value for e.g. change_view request:

class FeaduredAdmin(admin.ModelAdmin):
    ...

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        if self.active_pp:
            qs = qs.filter(...)
        return qs

    def changelist_view(self, request, extra_context=None):
        if request.GET:
            request.GET._mutable=True
            try:
                self.active_pp = request.GET.pop('pp')[0]
            except KeyError:
                self.active_pp = None
            request.GET_mutable=False
        return super().changelist_view(request, extra_context=extra_context)

This solution intentionally makes pp not participate in other admin filters. If you need pp to be honored by other filters use @meesterguyperson solution.

Antiproton answered 29/3, 2022 at 12:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.