A Django library for building interactive data tables powered by DataTables.js, with server-side processing, dynamic JavaScript filters, inline editing, and a rich plugin system.
Define your tables in Python using Django models and ORM queries -- columns, filters, sorting, annotations, and rendering are all configured from your views, with minimal JavaScript required.
- 20+ column types -- dates, currency, booleans, links, many-to-many, choice fields, lambdas, and more
- AJAX and non-AJAX modes -- load data dynamically or render inline
- JavaScript filters -- pivot, tag, select2, date range, and text search filters
- Modal filter dialogs -- form-based filtering with crispy-forms support
- Inline editing -- edit cells directly in the table with select2 dropdowns or text inputs
- Row selection -- checkbox-based row selection with bulk actions
- Plugins -- column totals, conditional row colouring, drag-and-drop reordering, save/restore filter state
- Data export -- Excel (.xlsx) download and clipboard copy
- Client-side rendering -- render functions for badges, links, conditional formatting, and lookups
- Django ORM integration -- annotations, aggregations, foreign key traversal, callable model methods
- Model-defined columns -- configure columns directly on your Django models via an inner
Datatableclass - Multiple tables per view -- display several tables on a single page
- Spreadsheet mode -- JSpreadsheet integration for spreadsheet-like editing
- Form widgets --
DataTableWidgetandDataTableReorderWidgetfor use in Django forms - Mobile responsive -- automatic device detection with per-device column visibility
- Redis caching -- optional cache layer with signal-based invalidation
pip install django-filtered-datatablesAdd to your INSTALLED_APPS:
INSTALLED_APPS = [
# ...
'django_datatables',
'ajax_helpers',
# ...
]Include the required JavaScript and CSS in your base template. The library depends on DataTables.js, jQuery, and Bootstrap.
from django_datatables.datatables import DatatableView
class CompanyList(DatatableView):
model = Company
@staticmethod
def setup_table(table):
table.add_columns(
'id',
'name',
'dissolved',
)urlpatterns = [
path('companies/', CompanyList.as_view(), name='company-list'),
]The default template renders a complete DataTables table. You can override it via template_name on your view.
Columns can be added in several ways:
def setup_table(table):
table.add_columns(
'name', # model field name
('id', {'title': 'ID', 'width': '30px'}), # tuple with options
DateColumn('date_entered', column_name='Date'), # column class instance
'company__name', # foreign key traversal
'Tags', # model-defined column
)String prefix syntax for quick column options:
.field-- hidden column (still available for filtering/rendering)_field-- calculated column (not from the database)$field-- secure column (hidden from client source)
| Column | Purpose |
|---|---|
DatatableColumn |
Standard column with customisable row_result and col_setup |
DateColumn |
Formats dates (dd/mm/yyyy) |
DateTimeColumn |
Formats datetimes |
ChoiceColumn |
Maps integer choices to display labels |
CurrencyColumn |
Currency formatting |
BooleanColumn |
Boolean display |
ColumnLink |
Clickable link using Django URL names |
ManyToManyColumn |
Displays many-to-many relationships |
CallableColumn |
Calls a model method |
LambdaColumn |
Processes values with a lambda function |
SelectColumn |
Row selection checkboxes |
TextFieldColumn |
Long text with truncation |
Configure columns directly on your models:
class Company(models.Model):
name = models.CharField(max_length=80)
class Datatable(DatatableModel):
# Annotation-based column
people = {'annotations': {'people': Count('person__id')}}
# Link column
collink_1 = ColumnLink(title='View', field=['id', 'name'], url_name='company-detail')
# Column list (renders multiple fields as a group)
company_list = ['id', 'name']
# Custom column class
class ModelIdColumn(DatatableColumn):
def col_setup(self):
self.field = 'id'
self.title = 'Custom ID'These columns can then be referenced by name in add_columns():
table.add_columns('id', 'name', 'people', 'collink_1', 'ModelIdColumn')def setup_table(table):
table.add_columns('id', 'name', 'company__name', 'Tags')
# JavaScript filters
table.add_js_filters('tag', 'Tags') # tag filter
table.add_js_filters('select2', 'company__name') # select2 dropdown
table.add_js_filters('totals', 'id') # totals/count filter
table.add_js_filters('date', 'date_entered') # date range
table.add_js_filters('expand', 'level', id_column='id') # tree expand
# ORM-level filters
table.filter = {'company__id': 1}
table.exclude = {'dissolved': True}from django_datatables.modal_filter.mixins import DatatableFilterMixin, DatatableFilterField
class CompanyList(DatatableFilterMixin, DatatableView):
model = Company
filter_fields = [
DatatableFilterField('Company Name', CharField(required=False), datatable_field='name__contains'),
DatatableFilterField('Tags', FilterModelMultipleChoiceField(queryset=Tags.objects.all()), datatable_field='tags__in'),
]table.sort('name') # ascending
table.sort('-name') # descending
table.sort('name', '-id') # multi-columnPer-column search boxes appear below the header by default. Customise them:
table.add_columns(
ColumnBase(column_name='status', field='dissolved',
col_search_select=[['true', 'Dissolved'], ['false', 'Active']]),
ColumnBase(column_name='order', field='order', no_col_search=True),
)
table.table_options['no_col_search'] = True # disable all column searchRender functions transform cell data in the browser without extra server round-trips:
from django_datatables.helpers import render_replace, row_button
table.add_columns(
# Badge rendering
ColumnBase(column_name='people', field='people', render=[
render_replace(column='people', html='<span class="badge badge-primary">%1%</span>'),
]),
# Row action button
ColumnBase(column_name='action', render=[row_button('delete', 'Delete')]),
# Lookup-based rendering
('id', {'title': 'Status', 'render': [
{'function': 'ReplaceLookup', 'html': '%1%', 'var': '%1%'}
], 'lookup': [[1, 'Active'], [2, 'Inactive']]}),
)Available render functions: Replace, ReplaceLookup, Html, MergeArray, ValueInColumn.
from django_datatables.plugins.colour_rows import ColourRows
from django_datatables.plugins.column_totals import ColumnTotals
from django_datatables.plugins.reorder import Reorder
# Conditional row colouring
table.add_plugin(ColourRows, [
{'column': 'status', 'values': {'overdue': 'table-danger', 'pending': 'table-warning'}}
])
# Column totals in footer
table.add_plugin(ColumnTotals, {
'amount': {'sum': True},
'id': {'text': 'Total', 'css_class': 'text-danger'},
'percentage': {'sum': 'percentage', 'numerator': 'vans', 'denominator': 'total', 'decimal_places': 1},
})
# Drag-and-drop row reordering
table.add_plugin(Reorder)def setup_table(table):
table.edit_fields = ['first_name', 'title']
table.edit_options = {'company__name': {'select2': True}}Handle row-level actions from buttons:
class MyView(DatatableView):
ajax_commands = ['row']
def row_delete(self, **kwargs):
return self.command_response('delete_row', row_no=kwargs['row_no'], table_id=kwargs['table_id'])
def row_toggle_tag(self, **kwargs):
row_data = json.loads(kwargs['row_data'])
# ... process action ...
table = self.tables[kwargs['table_id']]
return table.refresh_row(self.request, kwargs['row_no'])from django_datatables.downloads.excel_download import ExcelDownload
from django_datatables.downloads.clipboard import ClipboardCopy
class MyView(ExcelDownload, ClipboardCopy, DatatableView):
model = MyModel
def setup_menu(self):
self.add_menu('menu', 'buttons').add_items(
self.download_menu_item(),
self.clipboard_menu_item(),
)
super().setup_menu()class TwoTableView(DatatableView):
template_name = 'two_tables.html'
def add_tables(self):
self.add_table('t1', model=Company)
self.add_table('t2', model=Person)
@staticmethod
def setup_t1(table):
table.add_columns('id', 'name')
@staticmethod
def setup_t2(table):
table.add_columns('id', 'first_name')Load data from any source (JSON files, APIs, etc.):
class JsonTableView(DatatableView):
@staticmethod
def setup_table(table):
table.add_columns(
ColumnBase(column_name='field1'),
ColumnBase(column_name='field2'),
)
@staticmethod
def get_table_query(table, **kwargs):
with open('data.json') as f:
return json.load(f)from django_datatables.columns import DatatableColumn
class FullName(DatatableColumn):
def col_setup(self):
self.field = ['first_name', 'surname']
def row_result(self, data_dict, _page_results):
return f'{data_dict["first_name"]} {data_dict["surname"]}'Common options passed via table.table_options:
table.table_options['pageLength'] = 50
table.table_options['no_col_search'] = True
table.table_options['scrollX'] = True
table.table_options['ajax_url'] = '/custom/ajax/url/'
table.table_options['row_href'] = [render_replace(column='id', html='/detail/%1%/')]A Docker-based demo is included with sample data and 15+ examples covering filters, plugins, editing, rendering, and more.
docker-compose upThen visit http://localhost:8006.
- Python >= 3.6
- Django
- django-ajax-helpers >= 0.0.16
Optional:
openpyxl-- for Excel exportredis-- for table caching
MIT