Django-tables: User customizable columns
How to extend django-tables to provide your users the ability to customize which columns are shown.
Photo by Jesse Bauer on Unsplash
Django-tables: User customizable columns
I’ve previously written about the wonderful Django plugin that is django-tables2, specifically about how easy it can make building tables views, and how to let users set the number of results per page. It’s an incredibly powerful Django extension with tons of functionality out of the box.
Django-tables makes adding columns so simple it’s quite easy to go overboard and add too many of them. Luckily, reducing the number of visible columns using django_tables is quite simple using the Meta.fields attribute on the Table class, but choosing which columns to omit is another challenge altogether. Some users really want to see column A, and others absolutely need column B. Can’t get rid of either column, but there isn’t enough real estate on the page for both!
In this article, I’ll dive further into django-tables and teach you how to extend it to grant your users the capability to select which subset of columns they want to see displayed in the table.
Sample Model and Tables
For this article, we’ll start by first installing the django_tables2 plugin into our environment. Then, we’ll create a generic Model which has a large number of fields:
from django.db import models
class MyModel(models.Model):
name = models.CharField(max_length=128)
description = models.TextField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)
field1 = models.CharField(max_length=64, null=True, blank=True)
field2 = models.IntegerField(null=True, blank=True)
field3 = models.BooleanField(default=False)
field4 = models.DateField(null=True, blank=True)
field5 = models.TextField(null=True, blank=True)
field6 = models.FloatField(null=True, blank=True)
field7 = models.CharField(max_length=128, null=True, blank=True)
field8 = models.PositiveIntegerField(null=True, blank=True)
From that new model, we generate a simple Table class which includes all the fields. We’ll customize this further later:
from django_tables2 import tables
from .models import MyModel
class MyModelTable(tables.Table):
class Meta:
model = MyModel
Then, we connect the Model and Table together in a very straightforward CBV:
from django_tables2 import SingleTableView
from .models import MyModel
from .tables import MyModelTable
class MyModelTableView(SingleTableView):
model = MyModel
template_name = 'mymodel_list.html'
table_class = MyModelTable
Don’t forget to add the new view to urls.py! And finally, a quick HTML template which simply loads the table:
{% extends 'base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Table Example{% endblock %}
{% block content %}
<div class="row">
<div class="col">
{% render_table table %}
</div>
</div>
{% endblock %}
Our page now loads with all the fields in a single table:

Too many columns to fit on the page!
As we can see, it’s an awful lot of columns to fit in one table. The space is very crowded and as a result the entries are quite hard to read. The last column isn’t even visible without having to scroll horizontally.
Let’s improve this experience for our users! We can do this by giving them the capability to pick and choose which columns they want to see on the page. Implementing this new functionality will require two distinct steps:
- First, we’ll need to be able to control which columns to toggle on and off from the View based on the logged in user’s preferences.
- Second, we’ll need to provide the user with a mechanism to pick which columns to display from the table.
We’ll begin with the first step: let’s get our CBV to dynamically change the columns displayed from the table class.
Exploring the get_table_kwargs() method
The Table class’s init method comes with some useful kwargs we can use. The first one worth exploring is the sequence kwarg, whose docs specify the following:
sequence (iterable) — The sequence/order of the columns (from left to right). Items in the sequence must be column names, or
**"..."(string containing three periods). `'...'`** can be used as a catch-all for columns that are not specified.
This sounds interesting, but how do we change what value to pass in to the sequence kwarg when the Table class’s __init__ method is called? Inside the CBV, the get_table_kwargs method allows us to better control the values being passed to the Table class when it is being instantiated. In this case we’re adding in the sequence kwarg in order to specify what columns to show:
from django_tables2 import SingleTableView
from .models import MyModel
from .tables import MyModelTable
class MyModelTableView(SingleTableView):
model = MyModel
template_name = 'mymodel_list.html'
table_class = MyModelTable
def get_table_kwargs(self):
kwargs = super().get_table_kwargs()
kwargs['sequence'] = ('id', 'name', 'created', 'modified', 'active', 'field2', 'field5', 'field8')
return kwargs
While this change seems to have updated the column order, the other columns are still showing up at the end of the table. In order to remove them altogether, we can leverage another kwarg in the Table class: the exclude kwarg, whose documentation reads as follows:
exclude (iterable or str) — The names of columns that should not be included in the table.
This allows us to dynamically remove entire columns from being displayed, which is exactly what we want. Here is what our CBV now looks like:
from django_tables2 import SingleTableView
from .models import MyModel
from .tables import MyModelTable
class MyModelTableView(SingleTableView):
model = MyModel
template_name = 'mymodel_list.html'
table_class = MyModelTable
def get_table_kwargs(self):
kwargs = super().get_table_kwargs()
kwargs['sequence'] = ('id', 'name', 'created', 'modified', 'active', 'field2', 'field5', 'field8')
kwargs['exclude'] = ('description', 'field1', 'field3', 'field4', 'field6', 'field7')
return kwargs
Looking at our page, we can see the number of columns has indeed been significantly reduced to just the list above:

No more side scrolling!
Now that we’ve figured out how to control the columns from the CBV, we need to connect that back to the user for input!
User-provided input
Now that we’ve hard-coded changing the Table class’s columns from the CBV, let’s make it a bit more dynamic. We can first do this by taking input from the GET params — maybe a simple list of all the column names to display on the table.
In order to implement this, we’ll need to automatically generate the excludes kwarg by removing the columns the user wants to see from the list of all columns:
from django_tables2 import SingleTableView
from .models import MyModel
from .tables import MyModelTable
class MyModelTableView(SingleTableView):
model = MyModel
template_name = 'mymodel_list.html'
table_class = MyModelTable
def get_table_kwargs(self):
kwargs = super().get_table_kwargs()
# User provided columns:
if self.request.GET.get('columns', None):
display_columns = self.request.GET.getlist('columns')
kwargs['sequence'] = display_columns
all_columns = self.get_table_class()._meta.fields
kwargs['exclude'] = set(all_columns) - set(display_columns)
return kwargs
Unfortunately, this approach has the downside that we now need to hard-code the fields attribute in the table class and list out every single column by hand:
from django_tables2 import tables
from .models import MyModel
class MyModelTable(tables.Table):
class Meta:
model = MyModel
fields = ('id', 'name', 'description', 'created', 'modified', 'active', 'field1', 'field2', 'field3', 'field4', 'field5', 'field6', 'field7', 'field8')
One way to improve on this is to override the Table class’s __init__ method and introduce a new kwarg which would dynamically generate both lists, leveraging the self.columns.names() method which automatically gets us the complete list of defined columns without having to rely on the Meta.fields attribute, which may or may not be set.
from django_tables2 import tables
from .models import MyModel
class MyModelTable(tables.Table):
def __init__(self, *args, only_show: list = None, **kwargs):
super().__init__(*args, **kwargs)
if only_show:
valid_columns = [c for c in only_show if c in self.columns.names()]
self.sequence = valid_columns
self.exclude = tuple(set(self.columns.names()) - set(valid_columns))
class Meta:
model = MyModel
From the CBV, the approach is now much simpler:
from django_tables2 import SingleTableView
from .models import MyModel
from .tables import MyModelTable
class MyModelTableView(SingleTableView):
model = MyModel
template_name = 'mymodel_list.html'
table_class = MyModelTable
def get_table_kwargs(self):
kwargs = super().get_table_kwargs()
kwargs['only_show'] = self.request.GET.getlist('columns')
return kwargs
Either approach works, and each has their pros and cons. For the rest of this article, I’ll proceed with the second approach.
Next, we need to extend the UI to provide some checkboxes for the user to be able to easily pick and choose which column names to send to the GET query param.
Selecting columns using a Form
The next step is updating the template with a new Form that displays a checkbox form field for each of the possible columns that the user can choose to see.
We can accomplish this by dynamically generating a new django Form based on the complete list of columns in the Table class. Let’s encapsulate this into a single method we can call into later:
from django import forms
class MyModelTableView(SingleTableView):
...
def get_columns_form(self) -> forms.Form:
"""
Dynamically create a form containing a checkbox for each column in the table.
"""
class ColumnsForm(forms.Form):
columns = forms.MultipleChoiceField(
choices=[
(col_name, col.verbose_name or col_name)
for col_name, col in self.table_class.base_columns.items()
],
widget=forms.CheckboxSelectMultiple(),
required=False,
initial=self.request.GET.getlist('columns'),
label="Select the columns that will be shown in the table",
)
return ColumnsForm()
We leverage the MultipleChoiceField so that the user can select one or more columns at once, and generate the list of options dynamically based on the table’s base_column.items() which provides an iterator over all the columns in the table. I’ve also changed the widget to CheckboxSelectMultiple which replaces the default multi-select dropdown with a collection of checkboxes, as I personally prefer those better. Finally, we set the initial value so that the current columns are pre-selected.
Naturally, we also need to override the get_context_data method to pass along the new form. Here is what our complete CBV now looks like:
from django import forms
from django_tables2 import SingleTableView
from .tables import MyModelTable
class MyModelTableView(SingleTableView):
model = MyModel
template_name = 'mymodel_list.html'
table_class = MyModelTable
def get_table_kwargs(self):
kwargs = super().get_table_kwargs()
kwargs['only_show'] = self.request.GET.getlist('columns')
return kwargs
def get_columns_form(self) -> forms.Form:
class ColumnsForm(forms.Form):
columns = forms.MultipleChoiceField(
choices=[
(col_name, col.verbose_name or col_name)
for col_name, col in self.table_class.base_columns.items()
],
widget=forms.CheckboxSelectMultiple(),
required=False,
initial=self.request.GET.getlist('columns'),
label="Select the columns that will be shown in the table",
)
return ColumnsForm()
def get_context_data(self, **kwargs) -> dict:
context = super().get_context_data(**kwargs)
context['columns_form'] = self.get_columns_form()
return context
Finally, we need to add our new form to the template:
{% extends 'base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Table Example{% endblock %}
{% block content %}
<div class="row">
<div class="col">
{% render_table table %}
</div>
</div>
<form id="columns_form" method="get">
{{ columns_form }}
<div class="float-right">
<button type="submit" class="btn btn-sm btn-primary mb-2">
Update Columns
</button>
</div>
</form>
{% endblock %}
With that last piece in place, we can now change our columns by checking the boxes and submitting the form:

User can now control which columns appear in the table
Much better! As a user I can now navigate to the page and select only the columns I want to see appear on the page for a much improved viewing experience.
Other Consideration: Storing the user’s column preferences
We now have a fully-functioning selector for columns, but with a major drawback: the user’s selection is not preserved across browsing sessions. If the user were to navigate elsewhere or close the browser and come back to the page, their column selection would be lost.
There are a couple solutions to addressing that. One easy option would be to store the column selection in the user’s cookie or session. One simple example of such an implementation would be to add some javascript to the form submit to create/update the cookie, for example:
var selected = $('#columns_form input[type=checkbox]:checked')
.map(function () { return $(this).val(); })
.get();
document.cookie = "table_columns=" + selected.join(",") + ";path=/;max-age=" + (60*60*24*30);
On the Django side, we’d need to also update the CBV to read out the value from the cookie:
columns = self.request.COOKIES.get('table_columns', None)
An even better way to handle this would be to store the user’s column configuration in the DB in a custom UserPreference model. However, this carries the downside that the project would need a migration with every new customizable table. Here is an example model that would accomplish this:
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
class UserPreferences(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
table_columns = models.CharField(max_length=128, blank=True, null=True)
The CBV would also need to be updated to store the value in the new model for the record matching self.request.user then read back out at load time to determine the columns to display.
That’s it! Utilizing all the functionality already present in the django-tables framework, we are able to easily extend the framework to provide the ability for our users to pick and choose which columns they personally want to see on the table.
The goal of this article is to arm you with the knowledge to be able to navigate the django-tables framework with confidence. There are so many ways to continue expanding on this new functionality, whether that be columns that will always be shown, allowing users to change the order of the columns, a default set of columns to show for first-time users, etc. The possibilities are endless!
First, we learned about the SingleTableView.get_table_kwargs method which allows us to control how the Table object is being instantiated. We overrode that method and used it to pass in the sequence and excludes kwargs to the Table class in order to dynamically toggle columns to be shown on the page.
Next, we built the Form elements for the user to be able to choose which of the columns to be shown on the table.
Finally, we explored some options for persisting the user’s column preference.
Because the Table framework abstracts all the table logic from the rest of the app, we can then re-use this table on any number of views/pages and the same functionality will be present automatically! What are your thoughts on this approach? Be sure to sound off in the comments below!
Recommended Reading
A message from our Founder
Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.
Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? We don’t receive any funding, we do this to support the community. ❤️
If you want to show some love, please take a moment to follow me on LinkedIn, TikTok, **Instagram. You can also subscribe to our [weekly newsletter](https://newsletter.plainenglish.io/)**.
And before you go, don’t forget to clap and follow the writer️!
메타데이터
- post_id
- 2f4682cb7f9b
- slug
- django-tables-user-customizable-columns-2f4682cb7f9b
- url
- https://python.plainenglish.io/django-tables-user-customizable-columns-2f4682cb7f9b
- canonical_url
- https://python.plainenglish.io/django-tables-user-customizable-columns-2f4682cb7f9b
- author_url
- https://medium.com/@adrienvanthong
- status
- ok
- fetched_at
- 2026-09-03 06:07:45