'Django Admin: How to dynamically set list_per_page

I have run into the case where I am managing the codebase for a project that utilizes the django-admin portion of a site. All of the functionality of the django admin exists in normal views as well, but for some reason the client prefers to work on the admin views as opposed to the function based views... Normally, adding in a dropdown and adjusting the pagination/filter would be easy in a function based view, but the only way I can see to modify this is with list_per_page

How do I add a dropdown to the admin page (preferably with the pagination buttons) and then how do I retrieve the results on the server side to alter the list_per_page value dynamically based on what the user has selected? Would adding a form to the template and retrieving a POST in the admin work?



Solution 1:[1]

Inspired by plum-0 answer, I've ended up with this :

import django.contrib.admin.views.main

class DynPaginationChangeList(django.contrib.admin.views.main.ChangeList):
    def __init__(self, request, model, list_display, list_display_links,
                 list_filter, date_hierarchy, search_fields, list_select_related,
                 list_per_page, list_max_show_all, list_editable, model_admin, sortable_by):
        page_param = request.GET.get('list_per_page', None)
        if page_param is not None:
            # Override list_per_page if present in URL 
            # Need to be before super call to be applied on filters
            list_per_page = int(page_param)
        super(DynPaginationChangeList, self).__init__(request, model, list_display, list_display_links,
                 list_filter, date_hierarchy, search_fields, list_select_related,
                 list_per_page, list_max_show_all, list_editable, model_admin, sortable_by)

    def get_filters_params(self, params=None):
        """
        Return all params except IGNORED_PARAMS and 'list_per_page'
        """
        lookup_params = super(DynPaginationChangeList, self).get_filters_params(params)
        if 'list_per_page' in lookup_params:
            del lookup_params['list_per_page']
        return lookup_params
    

class AdminDynPaginationMixin:
    def get_changelist(self, request, **kwargs):
        return DynPaginationChangeList

If you use the javascript code propose in the original answer you just need to use this Mixin in your AdminClass and voilà.

I personally override the pagination.html template like this :

{% load admin_list %}
{% load i18n %}
<p class="paginator">
{% if pagination_required %}
{% for i in page_range %}
    {% paginator_number cl i %}
{% endfor %}
{% endif %}
{{ cl.result_count }} {% if cl.result_count == 1 %}{{ cl.opts.verbose_name }}{% else %}{{ cl.opts.verbose_name_plural }}{% endif %}
{% if show_all_url %}<a href="{{ show_all_url }}" class="showall">{% translate 'Show all' %}</a>{% endif %}
{% with '5 10 25 50 100 250 500 1000' as list %} — {% translate 'Number of items per page' %}
  <select>
  {% if cl.list_per_page|slugify not in list.split %}
    <option selected>{{ cl.list_per_page }}</option>
  {% endif %}
  {% for i in list.split %}
    <option value="{{ i }}" {% if cl.list_per_page|slugify == i %}selected{% else %}onclick="var p = new URLSearchParams(location.search);p.set('list_per_page', '{{ i }}');window.location.search = p.toString();"{% endif %}>{{ i }}</option>
  {% endfor %}
  </select>
{% endwith %}
{% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% translate 'Save' %}">{% endif %}
</p>

Solution 2:[2]

DISCLAIMER:

I'm answering my own question, so I am not sure if this is best practice or the best way to achieve this. However all of my searching yielded 0 results for this so I have decided to share it in case someone else needs this functionality. If anyone has any better ways to achieve this, or knows that I am doing something not secure please let me know!

I would try appending a query parameter to the URL then retrieving that parameter through the request on the server side to set the list_per_page. For adding the dropdown as well as the parameter, modifying the Media class for a particular admin result to include some extra javascript should allow you to create the dropdown as well as append the query parameter. We will need to remove this parameter from the request.GET otherwise we will run into an issue inside of get_queryset() since the parameter does not match a field on the model. For that we will need to override the changelist_view() method inside admin.py

admin_paginator_dropdown.js

window.addEventListener('load', function() {
    (function($) {
        // Jquery should be loaded now
        // Table paginator has class paginator. We want to append to this
        var paginator = $(".paginator");
        var list_per_page = $("<select id='list_per_page_selector'><option value=\"50\">50</option><option value=\"100\" selected>100</option><option value=\"150\">150</option><option value=\"200\">200</option><option value=\"250\">250</option></select>")
        var url = new URL(window.location);
        // Retrieve the current value for updating the selected dropdown on page refresh
        var initial_list_per_page = url.searchParams.get("list_per_page")
        paginator.append(list_per_page)
        if(initial_list_per_page === null) {
            // No choice has been made, set dropdown to default value
            $("#list_per_page_selector").val("100")
        }
        else{
            // User has a query parameter with a selection. Update the selected accordingly
            $("#list_per_page_selector").val(initial_list_per_page)
        }
        $("#list_per_page_selector").on("change", function(event) {
            // Add the list_per_page parameter to the url to be used in admin.py
            url.searchParams.set("list_per_page", event.target.value);
            //Take us to the new page.
            window.location.href = url.href;
        });
    })(django.jQuery);
});

admin.py

class someModelAdmin(admin.ModelAdmin)
    class Media:
        js = ("js/admin_paginator_dropdown.js",)
    
    def changelist_view(self, request, extra_context=None):
        # Copy the request.GET so we can modify it (no longer immutable querydict)
        request.GET = request.GET.copy()
        # Pop the custom non-model parameter off the request (Comes out as an array?)
        # Force it to int
        page_param = int(request.GET.pop('list_per_page', [100])[0])
        # Dynamically set the django admin list size based on query parameter.
        self.list_per_page = page_param
        return super(someModelAdmin, self).changelist_view(request, extra_context)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Olivier
Solution 2 plum 0