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.
There are two difficulties in doing this:
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:
Pros: