DataTables ============= Django Gentelella Widgets integrates DataTables.js with Django REST Framework for powerful server-side data tables with filtering, searching, pagination, and sorting. Overview ----------- The DataTables integration uses a three-tier architecture: - **Backend**: Django REST Framework ViewSets with filtering and pagination - **Frontend**: DataTables.js with server-side processing - **Integration**: Custom JavaScript functions that bridge DRF and DataTables protocols Quick Start -------------- Here's a minimal example to get a DataTable working: 1. Create a ViewSet that returns data in DataTables format 2. Register the ViewSet in your URL router 3. Add a table element in your template 4. Initialize DataTables with JavaScript .. code:: python # views.py from rest_framework import viewsets from rest_framework.response import Response from rest_framework.pagination import LimitOffsetPagination class PersonViewSet(viewsets.ModelViewSet): queryset = Person.objects.all() serializer_class = PersonSerializer pagination_class = LimitOffsetPagination def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) data = self.paginate_queryset(queryset) response = { 'data': PersonSerializer(data, many=True).data, 'recordsTotal': Person.objects.count(), 'recordsFiltered': queryset.count(), 'draw': request.GET.get('draw', 1) } return Response(response) .. code:: html
Backend Setup ---------------- ViewSet """""""""""" The ViewSet handles server-side processing including pagination, filtering, and ordering. .. code:: python from rest_framework import viewsets from rest_framework.response import Response from rest_framework.pagination import LimitOffsetPagination from rest_framework.filters import SearchFilter, OrderingFilter from django_filters.rest_framework import DjangoFilterBackend class PersonViewSet(viewsets.ModelViewSet): queryset = Person.objects.all() serializer_class = PersonDataTableSerializer pagination_class = LimitOffsetPagination # Enable filtering backends filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) # Fields for global search (search box) search_fields = ['name', 'email', 'country__name'] # Fields that can be ordered ordering_fields = ['name', 'num_children', 'born_date', 'last_time'] ordering = ('-num_children',) # Default ordering # FilterSet for column-specific filtering filterset_class = PersonFilterSet def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) data = self.paginate_queryset(queryset) response = { 'data': data, 'recordsTotal': Person.objects.count(), 'recordsFiltered': queryset.count(), 'draw': self.request.GET.get('draw', 1) } return Response(self.get_serializer(response).data) Response Format ^^^^^^^^^^^^^^^^ The ``list()`` method must return a response matching the DataTables protocol: .. code:: javascript { "data": [...], // Array of row objects "recordsTotal": 1000, // Total records before filtering "recordsFiltered": 250, // Records after filtering "draw": 1 // Request counter (prevents out-of-order responses) } Serializers """""""""""""""" You need two serializers: one for individual records and one for the DataTables response wrapper. .. code:: python from rest_framework import serializers from .models import Person, Country class CountrySerializer(serializers.ModelSerializer): class Meta: model = Country fields = ['id', 'name'] class PersonSerializer(serializers.ModelSerializer): # Nested serializer for ForeignKey fields country = CountrySerializer(read_only=True) # Computed field for action buttons actions = serializers.SerializerMethodField() def get_actions(self, obj): return { 'edit_url': f'/person/{obj.pk}/update/', 'delete_url': f'/person/{obj.pk}/delete/', } class Meta: model = Person fields = ['id', 'name', 'email', 'num_children', 'country', 'born_date', 'last_time', 'actions'] class PersonDataTableSerializer(serializers.Serializer): """Wrapper serializer matching DataTables protocol""" data = serializers.ListField(child=PersonSerializer(), required=True) draw = serializers.IntegerField(required=True) recordsFiltered = serializers.IntegerField(required=True) recordsTotal = serializers.IntegerField(required=True) FilterSet """""""""""""""" Use django-filter's FilterSet for column-specific filtering: .. code:: python from django_filters import FilterSet, DateFromToRangeFilter from django_filters.widgets import DateRangeWidget from .models import Person class PersonFilterSet(FilterSet): # Date range filter born_date = DateFromToRangeFilter( widget=DateRangeWidget(attrs={'placeholder': 'YYYY-MM-DD'}) ) class Meta: model = Person fields = { 'name': ['icontains'], # Case-insensitive substring 'num_children': ['exact'], # Exact match 'country__name': ['icontains'] # Filter on related field } URL Configuration """""""""""""""""""" Register the ViewSet using DRF's router: .. code:: python from rest_framework.routers import DefaultRouter from .views import PersonViewSet router = DefaultRouter() router.register('person', PersonViewSet, basename='api-person') urlpatterns = [ path('api/', include(router.urls)), ] This generates the following endpoints: - ``GET /api/person/`` - List with filtering/pagination - ``POST /api/person/`` - Create - ``GET /api/person/{id}/`` - Retrieve - ``PUT /api/person/{id}/`` - Update - ``DELETE /api/person/{id}/`` - Delete Frontend Setup ----------------- HTML Structure """""""""""""""" The table element only needs an ID. DataTables builds the structure dynamically: .. code:: html {% extends 'gentelella/base.html' %} {% block content %}

People

{% endblock %} {% block js %} {% endblock %} JavaScript Initialization """"""""""""""""""""""""""" Use ``createDataTable()`` to initialize the table: .. code:: javascript var peopleTable = createDataTable( '#people-table', // Table selector '{% url "api-person-list" %}', // API endpoint URL { columns: [ {data: "name", name: "name", title: "Name", type: "string", visible: true}, {data: "email", name: "email", title: "Email", type: "string", visible: true}, {data: "num_children", name: "num_children", title: "Children", type: "number", visible: true}, {data: "country", name: "country__name", title: "Country", type: "string", render: selectobjprint({display_name: "name"})}, ] }, addfilter=true // Add column filter inputs ); Function Signature ^^^^^^^^^^^^^^^^^^^ .. code:: javascript createDataTable(id, url, extraoptions={}, addfilter=false, formatDataTableParamsfnc=formatDataTableParams) **Parameters:** - ``id`` - CSS selector for the table element (e.g., ``'#my-table'``) - ``url`` - API endpoint URL - ``extraoptions`` - DataTables configuration options (including ``columns``) - ``addfilter`` - If ``true``, adds filter inputs below column headers - ``formatDataTableParamsfnc`` - Custom function to format request parameters Column Configuration """""""""""""""""""""""" Each column in the ``columns`` array can have these properties: .. code:: javascript { data: "country", // Property name in JSON response name: "country__name", // Field name for filtering (Django lookup syntax) title: "Country", // Column header text type: "string", // Data type (affects filter UI) visible: true, // Show/hide column render: selectobjprint({ // Custom render function display_name: "name" }), orderable: true, // Enable sorting (default: true) searchable: true, // Enable searching (default: true) } Column Types ^^^^^^^^^^^^^ The ``type`` property determines what filter widget appears: .. list-table:: :header-rows: 1 :widths: 20 40 40 * - Type - Filter Widget - Description * - ``string`` - Text input - Text search with ``icontains`` * - ``number`` - Number input - Numeric exact match * - ``boolean`` - Select (Yes/No) - Boolean field filter * - ``date`` - DateRangePicker - Date range selection * - ``select`` - Static select - Predefined options * - ``select2`` - AJAX Select2 - Dynamic search options * - ``readonly`` - None - No filter for this column Select Column with Static Options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code:: javascript { data: "status", name: "status", title: "Status", type: "select", choices: [ ["pending", "Pending"], ["approved", "Approved"], ["rejected", "Rejected"] ], visible: true } Select2 Column with AJAX Search ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code:: javascript { data: "category", name: "category", title: "Category", type: "select2", url: "/api/categories/search/", // Endpoint for Select2 AJAX multiple: false, // Allow multiple selection visible: true } Render Functions ------------------- Render functions format cell data for display. Built-in Functions """""""""""""""""""" yesnoprint ^^^^^^^^^^^ Displays boolean values with icons: .. code:: javascript {data: "is_active", name: "is_active", title: "Active", type: "boolean", render: yesnoprint} Output: ``✓ Yes`` or ``✗ No`` objShowBool ^^^^^^^^^^^^ Alias for ``yesnoprint``. emptyprint ^^^^^^^^^^^^ Shows ``--`` for null/empty values: .. code:: javascript {data: "nickname", name: "nickname", title: "Nickname", type: "string", render: emptyprint} selectobjprint ^^^^^^^^^^^^^^^ Displays a property from nested objects (for ForeignKey fields): .. code:: javascript {data: "country", name: "country__name", title: "Country", type: "string", render: selectobjprint({display_name: "name"})} Given response ``{"country": {"id": 1, "name": "USA"}}``, displays ``USA``. gt_print_list_object ^^^^^^^^^^^^^^^^^^^^^ Displays a list of objects (for ManyToMany fields): .. code:: javascript {data: "tags", name: "tags__name", title: "Tags", type: "string", render: gt_print_list_object("name")} showlink ^^^^^^^^^ Creates a link button: .. code:: javascript {data: "document_url", name: "document_url", title: "Document", render: showlink} Output: ``More`` downloadlink ^^^^^^^^^^^^^ Creates a download link: .. code:: javascript {data: "file_url", name: "file_url", title: "File", render: downloadlink} objshowlink ^^^^^^^^^^^^^ Creates a link from structured data: .. code:: javascript {data: "profile_link", name: "profile_link", title: "Profile", render: objshowlink} Expects data format: .. code:: javascript { url: "https://example.com/profile/1", class: "btn btn-primary", display_name: "View Profile" } truncateTextRenderer ^^^^^^^^^^^^^^^^^^^^^ Truncates long text with tooltip: .. code:: javascript {data: "description", name: "description", title: "Description", render: truncateTextRenderer(100)} // Max 100 characters Date and Time Handling ------------------------- Frontend Display """""""""""""""""""" For proper date formatting, provide format strings from Django settings: .. code:: html {% load timejs %} Then use DataTables' built-in datetime renderer: .. code:: javascript { data: "born_date", name: "born_date", title: "Born Date", type: "date", render: DataTable.render.date(), dateformat: document.date_format, visible: true }, { data: "last_time", name: "last_time", title: "Last Updated", type: "date", render: DataTable.render.datetime(), dateformat: document.datetime_format, visible: true } Backend Serializers """""""""""""""""""" DRF's default DateField fails on empty strings. Use the custom fields provided: .. code:: python from djgentelella.serializers import GTDateField, GTDateTimeField class PersonSerializer(serializers.ModelSerializer): born_date = GTDateField(allow_empty_str=True) last_time = GTDateTimeField(allow_empty_str=True) class Meta: model = Person fields = '__all__' Events and Customization --------------------------- Filter Event """""""""""""""" Modify request parameters before sending: .. code:: javascript createDataTable('#my-table', url, { columns: [...], events: { filter: function(data) { // Add custom parameter data.custom_param = 'value'; return data; } } }, addfilter=true); Custom Parameter Formatting """"""""""""""""""""""""""""" Override the default parameter translation: .. code:: javascript function myFormatParams(dataTableParams, settings) { var data = { 'page': Math.floor(dataTableParams.start / dataTableParams.length) + 1, 'page_size': dataTableParams.length, 'draw': dataTableParams.draw }; // Add search if (dataTableParams.search.value) { data['q'] = dataTableParams.search.value; } return data; } createDataTable('#my-table', url, {columns: [...]}, true, myFormatParams); Reload Table Data """""""""""""""""""" .. code:: javascript // Reload keeping current page peopleTable.ajax.reload(null, false); // Reload and go to first page peopleTable.ajax.reload(); Clear All Filters """""""""""""""""""" .. code:: javascript clearDataTableFilters(peopleTable, '#people-table'); Complete Example ------------------- Here's a full implementation example: Model """""""" .. code:: python # models.py from django.db import models class Country(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Person(models.Model): name = models.CharField(max_length=150) email = models.EmailField() num_children = models.IntegerField(default=0) country = models.ForeignKey(Country, on_delete=models.CASCADE) born_date = models.DateField() last_time = models.DateTimeField(auto_now=True) is_active = models.BooleanField(default=True) Serializers """""""""""" .. code:: python # serializers.py from rest_framework import serializers from djgentelella.serializers import GTDateField, GTDateTimeField from .models import Person, Country class CountrySerializer(serializers.ModelSerializer): class Meta: model = Country fields = ['id', 'name'] class PersonSerializer(serializers.ModelSerializer): country = CountrySerializer(read_only=True) born_date = GTDateField() last_time = GTDateTimeField() class Meta: model = Person fields = ['id', 'name', 'email', 'num_children', 'country', 'born_date', 'last_time', 'is_active'] class PersonDataTableSerializer(serializers.Serializer): data = serializers.ListField(child=PersonSerializer()) draw = serializers.IntegerField() recordsTotal = serializers.IntegerField() recordsFiltered = serializers.IntegerField() FilterSet """""""""""" .. code:: python # filters.py from django_filters import FilterSet, DateFromToRangeFilter from .models import Person class PersonFilterSet(FilterSet): born_date = DateFromToRangeFilter() class Meta: model = Person fields = { 'name': ['icontains'], 'email': ['icontains'], 'num_children': ['exact', 'gte', 'lte'], 'country__name': ['icontains'], 'is_active': ['exact'], } ViewSet """""""""""" .. code:: python # views.py from rest_framework import viewsets from rest_framework.response import Response from rest_framework.pagination import LimitOffsetPagination from rest_framework.filters import SearchFilter, OrderingFilter from django_filters.rest_framework import DjangoFilterBackend from .models import Person from .serializers import PersonSerializer, PersonDataTableSerializer from .filters import PersonFilterSet class PersonViewSet(viewsets.ModelViewSet): queryset = Person.objects.select_related('country').all() serializer_class = PersonDataTableSerializer pagination_class = LimitOffsetPagination filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) search_fields = ['name', 'email', 'country__name'] filterset_class = PersonFilterSet ordering_fields = ['name', 'num_children', 'born_date', 'last_time'] ordering = ['-last_time'] def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) data = self.paginate_queryset(queryset) response = { 'data': data, 'recordsTotal': Person.objects.count(), 'recordsFiltered': queryset.count(), 'draw': int(request.GET.get('draw', 1)) } return Response(self.get_serializer(response).data) def get_serializer_class(self): if self.action == 'list': return PersonDataTableSerializer return PersonSerializer URLs """""""" .. code:: python # urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import PersonViewSet router = DefaultRouter() router.register('people', PersonViewSet, basename='api-person') urlpatterns = [ path('api/', include(router.urls)), path('people/', TemplateView.as_view(template_name='people_list.html'), name='people-list'), ] Template """""""""""" .. code:: html {% extends 'gentelella/base.html' %} {% load timejs %} {% block content %}

People Directory

{% endblock %} {% block js %} {% endblock %} Request/Response Flow ------------------------ Understanding how data flows between frontend and backend: 1. **User interacts** with table (pagination, sorting, filtering) 2. **DataTables.js** sends AJAX request with its protocol parameters 3. **formatDataTableParams()** converts to DRF-compatible format: .. code:: DataTables → DRF start: 0 → offset: 0 length: 10 → limit: 10 search[value]: "john" → search: "john" order[0][column]: 0 → ordering: "name" or "-name" order[0][dir]: "asc" 4. **DRF ViewSet** processes filters, search, ordering, pagination 5. **Response** returned in DataTables format: .. code:: javascript { "data": [{...}, {...}, ...], "recordsTotal": 1000, "recordsFiltered": 45, "draw": 1 } 6. **DataTables.js** renders rows and updates pagination API Reference ---------------- JavaScript Functions """""""""""""""""""""" createDataTable(id, url, extraoptions, addfilter, formatDataTableParamsfnc) Main initialization function for DataTables. gtCreateDataTable(id, url, table_options) Lower-level initialization with full options control. formatDataTableParams(dataTableParams, settings) Converts DataTables request format to DRF format. addSearchInputsAndFooterDataTable(dataTable, tableId, columns) Adds filter input row below headers. clearDataTableFilters(dataTable, tableId) Clears all column filters and reloads table. yesnoprint(data, type, row, meta) Renders boolean as Yes/No with icons. emptyprint(data, type, row, meta) Renders null/empty as ``--``. selectobjprint(config) Returns render function for nested objects. gt_print_list_object(display_name) Returns render function for object arrays. showlink(data, type, row, meta) Renders URL as link button. downloadlink(data, type, row, meta) Renders URL as download button. objshowlink(data, type, row, meta) Renders structured link object. objnode(data, type, row, meta) Renders structured data as custom HTML element. truncateTextRenderer(maxChars) Returns render function that truncates text. Python Classes """""""""""""""" .. autoclass:: djgentelella.serializers.GTDateField :members: .. autoclass:: djgentelella.serializers.GTDateTimeField :members: