Files

13 KiB

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 __icontains like charField or textField. It can be None/False/"" if no ordering for filtering is required for this row.
    • Examples for the EveCharacter Model:
      • character_name
      • character_ownership__user__username
      • character_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)