DataTables Server Side Rendering
The allianceauth.framework.datatables.DataTablesView module provides a simple class based view to
implement simple server side filtering ordering and searching of DataTables.
This is intended to make the life of our community apps developer a little easier, so they don’t have to reinvent the wheel.
Usage
To use this view is as easy as defining your stub templates, and fields and adding the view to the urls.py
Given the EveCharacter Model as our model of choice we would define our stubs like so
Add our Templates
template/appname/stubs/icon.html
{% load evelinks %}
<img src="{{ row|character_portrait_url:32 }}">
template/appname/stubs/name.html
{{ row.character_name }} <span class="text-small">({{ row.character_ownership.user.username }})</span>
template/appname/stubs/corp.html
{{ row.corporation_name }}
template/appname/list.html
{% extends "allianceauth/base-bs5.html" %}
{% load i18n %}
{% load aa_i18n %}
{% block page_title %}
{% translate "App Name" %}
{% endblock page_title %}
{% block content %}
<table class="table table-striped w-100" id="table">
<!-- Normal Header Rows -->
<thead>
<tr>
<th></th>
<th>{% translate "Name" %}</th>
<th>{% translate "Corporation" %}</th>
<th>{% translate "Alliance" %}</th>
</tr>
</thead>
</table>
{% endblock content %}
{% block extra_css %}
{% include "bundles/datatables-2-css-bs5.html" %}
{% comment %} If you don't use the ColumnControl Extension, remove the next line {% endcomment %}
{% include "bundles/datatables-2-columncontrol-css-bs5.html" %}
{% endblock %}
{% block extra_javascript %}
{% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
{% include "bundles/datatables-2-js-bs5.html" %}
{% comment %} If you don't use the ColumnControl Extension, remove the next line {% endcomment %}
{% include "bundles/datatables-2-columncontrol-js-bs5.html" %}
<script>
$(document).ready(() => {
// Assuming you have a table with the ID 'table'
// A jQuery HTML Element `$('#table')` can also be passed instead of a selector string
const dt = new DataTable('#table', {
language: {
url: '{{ DT_LANG_PATH }}',
// Important: The value for `language.processing` must be passed
// as a JS template string, not as a normal string. This is to
// allow for Django template rendering inside the processing
// indicator.
processing: `{% include "framework/datatables/process-indicator.html" %}`
},
layout: { // See: https://datatables.net/reference/option/layout
topStart: 'pageLength',
topEnd: 'search',
bottomStart: 'info',
bottomEnd: 'paging'
},
ordering: { // See: https://datatables.net/reference/option/ordering
handler: true, // Enable ordering by clicking on column headers
indicators: false, // Disable ordering indicators on column headers (important when ColumnControl is used)
},
processing: true, // Show processing indicator when loading data
serverSide: true, // Enable server-side processing
ajax: {
url: '{% url "appname:eve_characters_table_data" %}',
error: (xhr, error) => console.error('Error fetching data:', xhr, error)
},
columnDefs: [
{
targets: [0],
columnControl: [],
sortable: false,
searchable: false
},
{
targets: [1,2,3],
columnControl: [
{
target: 0,
content: []
},
{
target: 1,
content: ['search']
}
],
}
],
order: [
[1, "asc"]
],
pageLength: 10, // Override default page length if desired
responsive : true,
initComplete: () => {
// Add code here that should run after the table has been initialized
}
});
});
</script>
{% endblock extra_javascript %}
Add our Views
Then we can setup out view in our appname/views.py file.
Columns definition
The columns must be defined as a 2 part tuple
Part 1 is the database field that will be used for filtering and ordering. If this is a foreign key you need to point to a field that is compatible with
__icontainslikecharFieldortextField. It can beNone/False/""if no ordering for filtering is required for this row.Examples for the EveCharacter Model:
character_namecharacter_ownership__user__usernamecharacter_ownership__user__profile__main_character__character_name
Part 2 is a string that is used to the render the column for each row. This can be a html stub or a string containing django style template language.
Examples for the EveCharacter Model
{{ row.character_name }}{{ row.character_ownership.user.username }}{{ row.character_ownership.user.profile.main_character.character_name }}appname/stubs/character_img.html
appname/views.py
from django.shortcuts import render
# Alliance Auth
from allianceauth.framework.datatables import DataTablesView
from allianceauth.eveonline.models import EveCharacter
## Datatables server side view
class EveCharacterTableData(DataTablesView):
model = EveCharacter
# Define the columns as a tuple.
# String for field name for filtering and ordering
# String for the render template
# Templates can be a html file or template language directly in the list below
columns = [
# ("field_for_queries_or_sort", template: str)
("", "appname/stubs/icon.html"),
("character_name", "appname/stubs/name.html"),
("corporation_name", "appname/stubs/corp.html"),
("alliance_name", "{{ row.alliance_name }} {{ row.alliance_id }}"),
]
# if you need to do some prefetch or pre-filtering you can override this function
def get_model_qs(self, request: HttpRequest):
qs = self.model.objects
if not request.user.is_superuser:
# eg only show unlinked characters to non-superusers
# just an example
# filtering here will prevent people searching things that may not be visible to them
qs = qs.filter(character_ownership__isnull=True)
# maybe some character ownership select related for performance?
return qs.select_related("character_ownership", "character_ownership__user")
## Main Page View
def eve_characters_table(request):
return render(request, "appname/list.html")
Add our Urls
appname/urls.py
from django.urls import path
from . import views
app_name = 'appname'
urlpatterns = [
path("eve_characters/", views.eve_characters_table, name='eve_characters_table_view'),
path("eve_characters/data/", views.EveCharacterTableData.as_view(), name='eve_character_table_data'),
]
and you are done.
Dropdown Filters
If you want to add dropdown filters to your server-side DataTable, you have to add a bit of code to the above.
Modify template/appname/list.html
Right above the table add the following code to add dropdown filters for characters, corporations and alliances. You can of course add more or less filters as you see fit.
{% load i18n %}
<div class="aasrp-datatable-filters">
<p class="mb-1 fw-bold">{% translate "Filter by" %}:</p>
<div class="align-items-center d-flex flex-wrap gap-3 mb-3">
<!-- Character filter -->
<div>
<label for="filter-character" class="col-auto">{% translate "Character name" %}</label>
<select id="filter-character" class="form-select w-auto">
<option value="">{% translate "All characters" %}</option>
{% for character in characters %}
<option value="{{ character.id }}">{{ character.character_name }}</option>
{% endfor %}
</select>
</div>
<!-- Corporation filter -->
<div>
<label for="filter-corporation" class="col-auto">{% translate "Corporation" %}</label>
<select id="filter-corporation" class="form-select w-auto">
<option value="">{% translate "All corporations" %}</option>
{% for corporation in corporations %}
<option value="{{ corporation.corporation_id }}">{{ corporation.corporation_name }}</option>
{% endfor %}
</select>
</div>
<!-- Alliance filter -->
<div>
<label for="filter-alliance" class="col-auto">{% translate "Alliance" %}</label>
<select id="filter-alliance" class="form-select w-auto">
<option value="">{% translate "All alliances" %}</option>
{% for alliance in alliances %}
<option value="{{ alliance.alliance_id }}">{{ alliance.alliance_name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
Now you need to add the necessary JavaScript code to make the filters work. Add the following code to the JavaScript block in the same template.
// Get the filter elements
const custom_dt_filter = {
character: $('#filter-character'),
corporation: $('#filter-corporation'),
alliance: $('#filter-alliance')
};
Then, extend the ajax configuration of your DataTable to include the filter values in the request data. You can do this by adding a data callback function to it.
This will take the current values of the filters and add them to the data sent to the server, prefixed with filter_ to distinguish them from other parameters.
ajax: {
url: '{% url "appname:eve_characters_table_data" %}',
data: (data) => {
const mappedFilters = Object.fromEntries(
Object.entries(custom_dt_filter).map(([key, $el]) => [`filter_${key}`, $el.val()])
);
return {...data, ...mappedFilters};
},
error: (xhr, error) => console.error('Error fetching data:', xhr, error)
},
To reload the DataTable data when the filter values change, add event listeners to the filter elements that trigger a redraw of the DataTable. You can do this in the initComplete callback of the DataTable initialization.
initComplete: () => {
// Redraw table when filter values change
Object.values(custom_dt_filter).forEach(($el) => {
$el.on('change', () => dt.draw());
});
}
Modify appname/views.py
Finally, you need to modify the get_model_qs method of your DataTablesView to apply the filters to the queryset based on the request parameters.
def get_model_qs(self, request: HttpRequest):
qs = self.model.objects
if not request.user.is_superuser:
# eg only show unlinked characters to non-superusers
# just an example
# filtering here will prevent people searching things that may not be visible to them
qs = qs.filter(character_ownership__isnull=True)
# Custom filters
get_params = request.GET.dict()
filter_character = get_params.get("filter_character", None)
filter_corporation = get_params.get("filter_corporation", None)
filter_alliance = get_params.get("filter_alliance", None)
if filter_character:
qs = qs.filter(character_id=filter_character)
if filter_corporation:
qs = qs.filter(corporation_id=filter_corporation)
if filter_alliance
qs = qs.filter(alliance_id=filter_alliance):
# maybe some character ownership select related for performance?
return qs.select_related("character_ownership", "character_ownership__user")
Populating the Filter Options
To populate the filter options in the dropdowns, you need to pass the necessary data to the template context in your main page view. You can do this by querying the relevant models and adding them to the context.
def eve_characters_table(request):
qs = EveCharacter.objects
if not request.user.is_superuser:
# eg only show unlinked characters to non-superusers
# just an example
# filtering here will prevent people searching things that may not be visible to them
qs = qs.filter(character_ownership__isnull=True)
characters = qs.values("id", "character_name").distinct().order_by("character_name")
corporations = qs.values("corporation_id", "corporation_name").distinct().order_by("corporation_name")
alliances = qs.values("alliance_id", "alliance_name").distinct().order_by("alliance_name")
context = {
"characters": characters,
"corporations": corporations,
"alliances": alliances
}
return render(request, "appname/list.html", context)