Skip to content

Instantly share code, notes, and snippets.

@pratyushmittal
Last active November 15, 2022 09:12
Show Gist options
  • Save pratyushmittal/04329db091cd00bf3e15e5034ed09b0d to your computer and use it in GitHub Desktop.
Save pratyushmittal/04329db091cd00bf3e15e5034ed09b0d to your computer and use it in GitHub Desktop.
Using Django ChangeList in custom views

Using Django's changelist feature in custom views

Django has a fabulous ChangeList feature. It is used to show lists in admin. It also provides:

  1. Search bar
  2. Date Hierarchy filters
  3. Other filters in sidebar (including option to define custom filters)
  4. Handles pagination

How it works in Admin

We define a ModelAdmin class. The ModelAdmin class then creates a ChangeList instance. The ChangeList class is responsible for filters, pagination and date hierarchy

The search functionaity is handled by ModelAdmin class.

The ChangeList object is then rendered in templates using admin's templatetags. These templatetags are result_list, pagination, date_hierarchy and search_form.

We can re-use these template-tags in our templates too.

Idea to decouple ChangeList

If we can decouple the ChangeList class from ModelAdmin, then it can do wonderful things. It might then be possible to use this class at other places. It can function just like Paginator class.

{% extends 'sidebar.html' %}
{% load admin_list %}
{% block title %}{{ cl.model_admin.admin_site.name }}{% endblock %}
{% block content_area %}
<h1 class="h2">{{ cl.model_admin.admin_site.name }}</h1>
<div class="card card-large change-list">
{% block search %}{% search_form cl %}{% endblock %}
{% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}
{% block content %}
<div class="fill-card-width margin-top-24 striped-first">
{% result_list cl %}
</div>
{% block pagination %}{% pagination cl %}{% endblock %}
{% endblock %}
</div>
{% endblock %}
{% block sidebar %}
{% if cl.has_filters %}
<div class="sidebar-panel change-list">
<div class="title flex flex-space-between flex-align-center">
<span>Filter</span>
{% if cl.has_active_filters %}
<small>
<a href="{{ cl.clear_all_filters_qs }}">&#10006; Clear all</a>
</small>
{% endif %}
</div>
{% for spec in cl.filter_specs %}
<div class="change-list-filter">
{% admin_list_filter cl spec %}
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}
from django.contrib.admin import ModelAdmin
from django.contrib.admin.options import IncorrectLookupParameters # noqa
from django.contrib.admin.sites import AdminSite
class GenericChangeListAdmin:
def get_queryset(self, *args, **kwargs):
return self._queryset
def has_change_permission(self, *args, **kwargs):
return False
def __init__(self, title, queryset, **kwargs):
# create a dummy model admin instance
model = queryset.model
admin_site = AdminSite(name=title)
model_admin = ModelAdmin(model=model, admin_site=admin_site)
model_admin.get_queryset = self.get_queryset
model_admin.has_change_permission = self.has_change_permission
# set input attributes
for name, value in kwargs.items():
setattr(model_admin, name, value)
# some defaults to disallow editing
model_admin.actions = None
model_admin.list_editable = ()
self._queryset = queryset
self._model_admin = model_admin
def __getattr__(self, name):
# called when attribute not found
allowed_attrs = [
"opts",
"show_full_result_count",
"ordering",
"admin_site",
"paginator",
# functions
"get_preserved_filters",
"lookup_allowed",
"get_ordering",
"get_paginator",
"get_search_results",
"get_empty_value_display",
"get_changelist_instance",
]
if name in allowed_attrs:
return getattr(self._model_admin, name)
else:
raise AttributeError("Attribute not allowed", name)
def _url_for_result(result):
return result.get_absolute_url()
def get_change_list(
title,
request,
queryset,
list_display=("__str__",),
list_display_links=(),
list_filter=(),
date_hierarchy=None,
search_fields=(),
list_select_related=False,
list_per_page=25,
list_max_show_all=100,
sortable_by=None,
search_help_text=None,
show_full_result_count=True,
url_for_result=None,
):
# prepare generic model-admin
model_admin = GenericChangeListAdmin(
title=title,
queryset=queryset,
list_display=list_display,
list_display_links=list_display_links,
list_filter=list_filter,
date_hierarchy=date_hierarchy,
search_fields=search_fields,
list_select_related=list_select_related,
list_per_page=list_per_page,
list_max_show_all=list_max_show_all,
sortable_by=sortable_by,
search_help_text=search_help_text,
show_full_result_count=show_full_result_count,
)
cl = model_admin.get_changelist_instance(request)
cl.formset = None
cl.url_for_result = url_for_result or _url_for_result
return cl
from .models import Post
from . import change_list
def upcoming_results(request):
queryset = Post.objects.filter(create_date__lte=dt.date.today())
try:
cl = change_list.get_change_list(
title="Posts",
request=request,
queryset=queryset,
list_display=["title", "content"],
list_filter=['category'],
date_hierarchy="create_date",
search_fields=["title"],
)
except change_list.IncorrectLookupParameters:
return redirect("posts:index")
return render(request, "change_list.html", {"cl": cl, "opts": cl.opts})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment