Skip to content

Instantly share code, notes, and snippets.

@jurrian
Created February 7, 2024 11:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jurrian/7ce23f918f8d06f7b9d5a3dcebb3db38 to your computer and use it in GitHub Desktop.
Save jurrian/7ce23f918f8d06f7b9d5a3dcebb3db38 to your computer and use it in GitHub Desktop.
FilterColumnsMixin for adding a button for selecting which columns to show in Django admin changelist
from app.forms import FilterColumnsForm
from django.contrib.admin.utils import label_for_field
from django.template.response import TemplateResponse
from django.urls import path
from django.utils.translation import gettext_lazy as _
class FilterColumnsMixin:
"""Allow users to filter the columns in the changelist.
This will add a "Columns" button to the changelist page to select the columns to display.
"""
filter_columns_flag = True # Just a flag to see in template if mixin is present
def get_list_display(self, request):
"""Filter the list_display based on the form response.
If session middleware is active, store it per model on current user session.
When for some reason session is disabled then save it on the class instance itself,
will be the same for all users and will reset on application restart.
"""
model_name = self.model._meta.model_name # pylint: disable=protected-access
filter_columns = request.GET.getlist('filter_columns')
is_reset = request.GET.get('reset_columns')
default_list_display = super().get_list_display(request)
if hasattr(request, 'session'):
# In most cases session middleware is used
if is_reset:
try:
del request.session['filter_columns'][model_name]
request.session.modified = True
except KeyError:
pass
# If all columns are selected, don't store it in session
elif filter_columns and filter_columns != list(default_list_display):
request.session['filter_columns'] = {model_name: filter_columns}
try:
return request.session['filter_columns'][model_name]
except (KeyError, TypeError):
return default_list_display
else:
# Fall back to "persisting" on the class instance list_display attribute
if is_reset:
self.list_display = default_list_display
elif filter_columns:
self.list_display = filter_columns
return self.list_display
def get_list_display_links(self, request, list_display):
"""When display link is not in list_display, use the first column as fallback.
Otherwise, there will be no link to click on.
"""
result = super().get_list_display_links(request, list_display)
list_display = self.get_list_display(request)
if list_display and not [x for x in result if x in list_display]:
result = list_display[0]
return result
def get_urls(self):
"""Adds a intermediary columns page to this admin.
"""
name = f'{self.model._meta.app_label}_{self.model._meta.model_name}_columns' # pylint: disable=protected-access
urls = super().get_urls()
return [
path('columns/', self.admin_site.admin_view(self.filter_columns), name=name),
] + urls
def filter_columns(self, request):
"""View for intermediary page when clicking on the "Columns" button.
Will render an admin view with standard context, showing the `FilterColumnsForm`.
"""
cl = self.get_changelist_instance(request)
# Determine which to display in the form and which are already selected
display = []
selected = []
all_fields = type(self).list_display
for field_name in all_fields:
text = label_for_field(field_name, cl.model, model_admin=cl.model_admin, return_attr=False)
display.append((field_name, text))
if field_name in cl.list_display:
selected.append(field_name)
# Initialize form with selected and set the available choices
form = FilterColumnsForm(initial={'filter_columns': selected})
form['filter_columns'].field.choices = display
# Add context for showing the menu and bars
context = self.admin_site.each_context(request)
context['title'] = _('Choose which columns to show')
context['form'] = form
context['opts'] = self.model._meta # pylint: disable=protected-access
request.current_app = self.admin_site.name
return TemplateResponse(request, 'admin/filter_columns.html', context)
{% extends 'admin/change_list.html' %}
{% load i18n admin_urls %}
{% block object-tools-items %}
{% if cl.pk_attname != 'history_id' %}
{% url opts|admin_urlname:'changelist_history' as history_url %}
{% if history_url %}
<li>
<a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
</li>
{% endif %}
{% endif %}
{% if cl.model_admin.filter_columns_flag %}
<li><a href="{% url opts|admin_urlname:'columns' %}">Columns</a></li>
{% endif %}
{{ block.super }}
{% endblock %}
{% extends 'admin/base_site.html' %}
{% load admin_urls static %}
{% block extrahead %}
{{ block.super }}
<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">
{{ form.media }}
{% endblock %}
{% block content %}
<form action="{% url opts|admin_urlname:'changelist' %}" method="GET">
{{ form.as_div }}
<div class="submit-row" style="clear: both">
<input type="submit" value="OK" class="default">
<input type="submit" name="reset_columns" value="Reset">
</div>
</form>
{% endblock %}
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
class FilterColumnsForm(forms.Form):
"""Form with only one field to filter the columns.
Django widget for horizontal selection, works only in the admin and only with media included.
"""
filter_columns = forms.MultipleChoiceField(
label=False,
widget=FilteredSelectMultiple('columns', is_stacked=False)
)
class Media:
js = ('/admin/jsi18n',)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment