Opened 10 years ago

Closed 5 years ago

#3777 closed New feature (duplicate)

Persistent change_list filtering in admin

Reported by: matt <matt.barry@…> Owned by: nobody
Component: contrib.admin Version: master
Severity: Normal Keywords: filter session
Cc: Mikhail Korobov Triage Stage: Accepted
Has patch: yes Needs documentation: yes
Needs tests: yes Patch needs improvement: yes
Easy pickings: no UI/UX: yes

Description

Attached is a patch which allows the list_filters on the change_list pages of the admin to be persistent (as described in e.g. this thread on django-users). Basically, it does this:

  • Saves the elements of the query string in the session each time the change_list is viewed.
  • Looks for a special GET variable, and if found, restores the items last saved from the query string.
  • Add the special GET variable to links and redirects from the change_form, so that when saving an object, the user is redirected back to the filtered list s/he was last viewing.

I'm not entirely sure this is the best way to solve this issue; something more transparent (i.e. without a magic GET variable) might be better, in which case the change_list filter links would have to explicitly unfilter themselves, rather than simply removing the query variable.

The patch admittedly also adds a setdefault method to request.session; the rest of the patch could trivially be adapted to not having this method available, of course.. I've found it useful on many occasions, though, and it seems to fit the "use session as dictionary" idea. If folks find the patch useful, I can resubmit with documentation and/or without request.session.setdefault.

Attachments (2)

admin-filters-in-session.diff (8.0 KB) - added by matt <matt.barry@…> 10 years ago.
persistent list_filter patch
admin-filters-in-session.2.diff (8.1 KB) - added by matt <matt.barry@…> 10 years ago.
persistent list_filter patch

Download all attachments as: .zip

Change History (12)

Changed 10 years ago by matt <matt.barry@…>

persistent list_filter patch

Changed 10 years ago by matt <matt.barry@…>

persistent list_filter patch

comment:1 Changed 10 years ago by matt <matt.barry@…>

Oops.. updated patch that will actually clear unset filter variables when saving the session.

comment:2 Changed 10 years ago by tony.perkins@…

I've implemented this also. I tried to do it without modifying any django code. I was mostly successful except for a bit of javascript in the filter.html file.

Below are the middleware class and filter.html. If you don't make the changes to the filter.html file you are unable to clear the last filter. Not shown in these files is a link I placed on the change list page that simply calls with the ?reset-filter=y to force a reset.

I also am not sure this is the best way. I would like to see this type of functionality built into django.

{% load i18n %}

<h3>{% blocktrans with title|escape as filter_title %} By {{ filter_title }} {% endblocktrans %}</h3>
<script>
function checkForReset(url){
	sArgs = url.search.slice(1).split('&');
	if (sArgs.length == 1){
		if (sArgs[0].length ==0 ){
			document.location = url+'reset-filter=true'
		}
	}
}</script>
<ul>
{% for choice in choices %}
    <li{% if choice.selected %} class="selected"{% endif %}>
    <a href="{{ choice.query_string }}" onclick="checkForReset(this)">{{ choice.display|escape }}</a></li>
{% endfor %}
</ul>
from django import http

class FilterPersistMiddleware(object):

    def process_request(self, request):
        query_string = request.META['QUERY_STRING']
        path = request.path
        session = request.session
        key = 'key'+path.replace('/','_')

        # check for the reset-filter flag
        if request.has_key('reset-filter'):
            if session.get(key,False):
                del session[key]
            return None
        
        # if we are not in the /media/ area then ignore
        # todo: this should be dynamic not hardcoded
        if path.find('/media/') < 0:
            return None


        if len(query_string) > 0:
            # save the filter under the key in session
            request.session[key] = query_string
            return None

        
        if session.get(key, False):
            query_string=request.session.get(key)
            redirect_to = path+'?'+query_string
            return http.HttpResponseRedirect(redirect_to)

comment:3 Changed 9 years ago by matt.barry@…

Resolution: worksforme
Status: newclosed

Wow.. that's much nicer, IMO.. and if you just override the filter.html admin template in the project, you don't need to patch Django at all. Thanks!

comment:4 Changed 8 years ago by etiennepouliot@…

Here is how I did it (based on the previous post by tony.perkins)
This does not require any modification to the templates, just the middleware
(tested With django 1.02)

from django import http

class FilterPersistMiddleware(object):

    def process_request(self, request):

        path = request.path
        if path.find('/admin/') != -1: #Dont waste time if we are not in admin
            query_string = request.META['QUERY_STRING']
            if not request.META.has_key('HTTP_REFERER'):
                return None

            session = request.session
            if session.get('redirected', False):#so that we dont loop once redirected
                del session['redirected']
                return None

            referrer = request.META['HTTP_REFERER'].split('?')[0]
            referrer = referrer[referrer.find('/admin'):len(referrer)]
            key = 'key'+path.replace('/','_')

            if path == referrer: #We are in same page as before
                if query_string == '': #Filter is empty, delete it
                    if session.get(key,False):
                        del session[key]
                    return None
                request.session[key] = query_string
            else: #We are are coming from another page, restore filter if available
                if session.get(key, False):
                    query_string=request.session.get(key)
                    redirect_to = path+'?'+query_string
                    request.session['redirected'] = True
                    return http.HttpResponseRedirect(redirect_to)
                else:
                    return None
        else:
            return None

comment:5 Changed 6 years ago by drhoden@…

I modified the above middleware so that is doesn't break admin's related object popups. This method tracks popup query strings separately from regular pages. This way you can use filters inside the popup windows as well. Hope it helps someone else.

from django import http

# based on http://code.djangoproject.com/ticket/3777#comment:4
class FilterPersistMiddleware(object):

    def process_request(self, request):

        if '/admin/' not in request.path:
            return None
        
        if not request.META.has_key('HTTP_REFERER'):
            return None
        
        popup = 'pop=1' in request.META['QUERY_STRING']
        path = request.path
        query_string = request.META['QUERY_STRING']
        session = request.session
        
        if session.get('redirected', False):#so that we dont loop once redirected
            del session['redirected']
            return None

        referrer = request.META['HTTP_REFERER'].split('?')[0]
        referrer = referrer[referrer.find('/admin'):len(referrer)]
        key = 'key'+path.replace('/','_')
        if popup:
            key = 'popup'+path.replace('/','_')

        if path == referrer: #We are in same page as before
            if query_string == '': #Filter is empty, delete it
                if session.get(key,False):
                    del session[key]
                return None
            request.session[key] = query_string
        else: #We are are coming from another page, restore filter if available
            if session.get(key, False):
                query_string=request.session.get(key)
                redirect_to = path+'?'+query_string
                request.session['redirected'] = True
                return http.HttpResponseRedirect(redirect_to)
        return None

comment:6 Changed 5 years ago by Maik Hoepfel

Easy pickings: unset
Severity: Normal
Type: Uncategorized
UI/UX: unset

Thanks a lot! Added functionality to set filters that are enabled by default.

class FilterPersistMiddleware(object):

    def _get_default(self, key):
        """ Gets any set default filters for the admin. Returns None if no 
            default is set. """
        default = settings.ADMIN_DEFAULT_FILTERS.get(key, None)
        # Filters are allowed to be functions. If this key is one, call it.
        if hasattr(default, '__call__'):
            default = default()
        return default

    def process_request(self, request):
        if '/admin/' not in request.path or request.method == 'POST':
            return None
        
        if request.META.has_key('HTTP_REFERER'):
            referrer = request.META['HTTP_REFERER'].split('?')[0]
            referrer = referrer[referrer.find('/admin'):len(referrer)]
        else:
            referrer = u''
        
        popup = 'pop=1' in request.META['QUERY_STRING']
        path = request.path
        query_string = request.META['QUERY_STRING']
        session = request.session
        
        if session.get('redirected', False):#so that we dont loop once redirected
            del session['redirected']
            return None

        key = 'key'+path.replace('/','_')
        if popup:
            key = 'popup'+key

        if path == referrer: 
            """ We are in the same page as before. We assume that filters were
                changed and update them. """
            if query_string == '': #Filter is empty, delete it
                if session.has_key(key):
                    del session[key]
                return None
            else:
                request.session[key] = query_string
        else: 
            """ We are are coming from another page. Set querystring to
                saved or default value. """
            query_string=session.get(key, self._get_default(key))
            if query_string is not None:
                redirect_to = path+'?'+query_string
                request.session['redirected'] = True
                return http.HttpResponseRedirect(redirect_to)
            else:
                return None

Sample default filters:

from datetime import date
def _today():
    return 'starttime__gte=' + date.today().isoformat()

# Default filters. Format: 'key_$url', where $url has slashes replaced
# with underscores
# value can either be a function or a string
ADMIN_DEFAULT_FILTERS= {
    # display only events starting today
    'key_admin_event_calendar_event_': _today, 
    # display active members
    'key_admin_users_member_': 'is_active__exact=1',
    # only show new suggestions
    'key_admin_suggestions_suggestion_': 'status__exact=new',
}

comment:7 Changed 5 years ago by Mikhail Korobov

Cc: Mikhail Korobov added
Needs tests: set
Patch needs improvement: set
Resolution: worksforme
Status: closedreopened
Type: UncategorizedNew feature
UI/UX: set

I think that shouldn't be closed as 'worksforme' because change_list filters are not persistent in django admin and provided workarounds have their own issues.

comment:8 Changed 5 years ago by Aymeric Augustin

Yes, this was closed by the original reporter after he found a workaround, not by a core developer.

I think it would be nice to preseve both filters and ordering.

comment:9 Changed 5 years ago by Aymeric Augustin

Triage Stage: UnreviewedAccepted

Let's accept this feature request. If it turns out to be controversial, the ticket can be moved to DDN.

comment:10 Changed 5 years ago by Julien Phalip

Resolution: duplicate
Status: reopenedclosed

Closing in favour of #6903 which is essentially trying to solve the same problem and has more recent patches.

Note: See TracTickets for help on using tickets.
Back to Top