Search code examples
djangodjango-admin

How to go to "next" object in Django admin?


I am creating an admin interface in which the user will have a button Save and go to next. I would like that button to move to the next item in the admin change list, respecting the queryset in the list - ie, the user could perform a search, make filters, change the ordering and start at the top of the list and work his/her way down.

This is what I have to implement:

from django.shortcuts import redirect

class MyModelAdmin(admin.ModelAdmin):
    change_form_template = ['mymodel-savenext.html']

    def response_change(self, request, obj):
        """Determines the HttpResponse for the change_view stage."""
        if '_save_next' in request.POST:
            # -> How do I get the queryset in the original order and find `obj` it it?
            nextobj = ?
            return redirect('admin:app_mymodel_admin', nextobj.id)
        return super().response_change(request, obj)

Please consider the queryset may be large. If the user would go directly into the changeform url (pasting a URL for example), it would make sense to redirect him back to the list or assume a "default" ordering, like getting the next ID.


Solution

  • There are two difficulties in doing this:

    1. To assemble the same queryset used to list the objects, and
    2. Possibly modifying this queryset to obtain only the object following the current one (without having to iterate through all of it).

    As for the first: Django saves the parameters used to generate the list in a GET parameter called _changelist_filters, but this argument is not used by changeform_view. It is only "restored" as GET parameters when returning to the list. In order to use this argument, we must change the HttpRequest (which sounds bad) so we can create an instance of django.contrib.admin.views.main.ChangeList - the object that will process these filters and generate the queryset.

    Secondly, to find a primary key in the middle of a query that may be ordered by any other field, I found no other solution other than to perform a linear search through the query itself. (Perhaps someone can shed some light in this.)

    The result is this mixin to add to the ModelAdmin class:

    from django.http import QueryDict
    
    
    class GotoNextAdminMixin(object):
    
        def get_next_instance_pk(self, request, current):
            """Returns the primary key of the next object in the query (considering filters and ordering).
            Returns None if the object is not in the queryset.
            """
            querystring = request.GET.get('_changelist_filters')
            if querystring:
                # Alters the HttpRequest object to make it function as a list request
                original_get = request.GET
                try:
                    request.GET = QueryDict(querystring)
                    # from django.contrib.admin.options: ModelAdmin.changelist_view
                    ChangeList = self.get_changelist(request)
                    list_display = self.get_list_display(request)
                    changelist = ChangeList(
                        request, self.model, list_display,
                        self.get_list_display_links(request, list_display),
                        self.get_list_filter(request),
                        self.date_hierarchy,
                        self.get_search_fields(request),
                        self.get_list_select_related(request),
                        self.list_per_page,
                        self.list_max_show_all,
                        self.list_editable,
                        self,
                        self.sortable_by)  # New in Django 2.0
                    queryset = changelist.get_queryset(request)
                finally:
                    request.GET = original_get
            else:
                queryset = self.get_queryset(request)
    
            # Try to find pk in this list:
            iterator = queryset.values_list('pk', flat=True).iterator()
            try:
                while next(iterator) != current.pk:
                    continue
                return next(iterator)
            except StopIteration:
                pass # Not found or it was the last item
    

    Then in my ModelAdmin class:

    class MyModelAdmin(admin.ModelAdmin, GotoNextAdminMixin):
    
        def response_change(self, request, obj):
            """Determines the HttpResponse for the change_view stage."""
            if '_save_next' in request.POST:
                next_pk = self.get_next_instance_pk(request, obj)
                if next_pk:
                    response = redirect('admin:app_mymodel_change', next_pk)
                    qs = request.GET.urlencode()  # keeps _changelist_filters
                else:
                    # Last item (or no longer in list) - go back to list in the same position
                    response = redirect('admin:app_mymodel_changelist')
                    qs = request.GET.get('_changelist_filters')
    
                if qs:
                    response['Location'] += '?' + qs
                return response
    
            return super().response_change(request, obj)
    

    Drawbacks of this approach:

    • Must alter the variable HttpRequest.GET. In current Django version, it's not a property, so it's changeable, but in the future it may not be so.
    • Must iterate through the whole queryset (although we only retrieve PKs) - bad for large tables or costly queries.
    • Only works for models with a single-field primary key (as most things in Django, so it may not be a problem).
    • If the object is no longer in the list (for example there was a filter and the current object was filtered out), the effort was wasted.

    Pros:

    • It works!