morlandi / django-ajax-datatable

A Django app which provides the integration of a Django project with the jQuery Javascript library DataTables.net
MIT License
204 stars 64 forks source link

django-ajax-datatable

django-ajax-datatable is a Django app (previously named morlandi/django-datatables-view) which provides advanced integration for a Django project with the jQuery Javascript library DataTables.net, when used in server-side processing mode.

In this context, the rendering of the table is the result of a serie of Ajax requests to the server following user interactions (i.e. when paging, ordering, searching, etc.).

With django-ajax-datatable, basically you have to provide a AjaxDatatableView-derived view to describe the desired table content and behaviour, and the app manages the interaction with DataTables.net by reacting to the ajax requests with suitable responses.

Notes:

Since someone asked ...

Features:

Inspired from:

https://github.com/monnierj/django-datatables-server-side

.. contents::

.. sectnum::

Demo site and examples

A very minimal working Django project which uses django-ajax-datatable can be found in the folder example_minimal.

A more realistic solution, with a frontend based on Bootstrap4, can be found in example, and is published as a demo site at the address: http://django-ajax-datatable-demo.brainstorm.it/.

.. image:: screenshots/examples.png

Installation

Install the package by running:

.. code:: bash

pip install django-ajax-datatable

or:

.. code:: bash

pip install git+https://github.com/morlandi/django-ajax-datatable@v4.0.0

then add 'ajax_datatable' to your INSTALLED_APPS:

.. code:: bash

INSTALLED_APPS = [
    ...
    'ajax_datatable',
]

Optional dependencies (for better debug tracing):

- sqlparse
- termcolor
- pygments

Pre-requisites

Your base template should include what required by datatables.net, plus:

Example (plain jQuery from CDN):

.. code:: html

{% block extrastyle %}

    <link href="https://github.com/morlandi/django-ajax-datatable/blob/master/{% static 'ajax_datatable/css/style.css' %}" rel="stylesheet" />
    <link href="https://github.com/morlandi/django-ajax-datatable/blob/master//cdn.datatables.net/1.10.22/css/jquery.dataTables.min.css" />

{% endblock extrastyle %}

{% block extrajs %}

    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script type="text/javascript" src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'ajax_datatable/js/utils.js' %}"></script>
    <script src="https://github.com/morlandi/django-ajax-datatable/raw/master//cdn.datatables.net/1.10.22/js/jquery.dataTables.min.js"></script>

{% endcompress %}

Example (with Bootstrap4 support):

.. code:: html

{% block extrastyle %}

    <link href="https://github.com/morlandi/django-ajax-datatable/blob/master/{% static 'ajax_datatable/css/style.css' %}" rel="stylesheet" />
    <!-- link rel='stylesheet' href="https://github.com/morlandi/django-ajax-datatable/blob/master/{% static 'datatables.net-bs/css/dataTables.bootstrap.min.css' %}" -->
    <link rel='stylesheet' href="https://github.com/morlandi/django-ajax-datatable/blob/master/{% static 'datatables.net-bs4/css/dataTables.bootstrap4.min.css' %}">
    <link rel='stylesheet' href="https://github.com/morlandi/django-ajax-datatable/blob/master/{% static 'datatables.net-buttons-bs/css/buttons.bootstrap.min.css' %}">

{% endblock extrastyle %}

{% block extrajs %}

    <script type="text/javascript" src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'ajax_datatable/js/utils.js' %}"></script>

    <script src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'datatables.net/js/jquery.dataTables.min.js' %}"></script>
    <!-- script src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'datatables.net-bs/js/dataTables.bootstrap.min.js' %}"></script -->
    <script src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'datatables.net-bs4/js/dataTables.bootstrap4.min.js' %}"></script>
    <script src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'datatables.net-buttons/js/dataTables.buttons.min.js' %}"></script>
    <script src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'datatables.net-buttons/js/buttons.print.min.js' %}"></script>
    <script src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'datatables.net-buttons/js/buttons.html5.min.js' %}"></script>
    <script src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'datatables.net-buttons-bs/js/buttons.bootstrap.min.js' %}"></script>
    <script src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'jszip/dist/jszip.min.js' %}"></script>
    <script src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'pdfmake/build/pdfmake.min.js' %}"></script>
    <script src="https://github.com/morlandi/django-ajax-datatable/raw/master/{% static 'pdfmake/build/vfs_fonts.js' %}"></script>

{% endcompress %}

Basic AjaxDatatableView

To provide server-side rendering of a Django Model, you will need:

  1. an ordinary view which will render an HTML page containing:

    • an empty HTML element
    • a javascript code which links this HTML table to the (AjaxDatatableView-derived) second view
    • a specific view derived from AjaxDatatableView() which will be called multiple times via Ajax during data navigation; this second view has two duties:

      • render the initial table layout based on specified columns
      • respond to datatables.net requests, as a consequence of the user interaction with the table
    • Example:

      We start by rendering an HTML page from this template:

      file permissions_list.html

      .. code:: python

      <table id="datatable_permissions">
      </table>
      
      or:
      
      <div class="table-responsive">
          <table id="datatable_permissions" width="100%" class="table table-striped table-bordered dt-responsive compact nowrap">
          </table>
      </div>
      
      ...
      
      <script language="javascript">
      
          $(document).ready(function() {
              AjaxDatatableViewUtils.initialize_table(
                  $('#datatable_permissions'),
                  "{% url 'ajax_datatable_permissions' %}",
                  {
                      // extra_options (example)
                      processing: false,
                      autoWidth: false,
                      full_row_select: true,
                      scrollX: false
                  }, {
                      // extra_data
                      // ...
                  },
              );
          });
      
      </script>

      Here, "{% url 'ajax_datatable_permissions' %}" is the endpoint to the specialized view:

      file urls.py

      .. code:: python

      from django.urls import path
      from . import ajax_datatable_views
      
      app_name = 'frontend'
      
      urlpatterns = [
          ...
          path('ajax_datatable/permissions/', ajax_datatable_views.PermissionAjaxDatatableView.as_view(), name="ajax_datatable_permissions"),
      ]

      The javascript helper AjaxDatatableViewUtils.initialize_table(element, url, extra_options={}, extra_data={}) connects the HTML table element to the "server-size table rendering" machinery, and performs a first call (identified by the action=initialize parameter) to the AjaxDatatableView-derived view.

      This in turn populates the HTML empty table with a suitable layout, while subsequent calls to the view will be performed to populate the table with real data.

      This strategy allows the placement of one or more dynamic tables in the same page.

      I often keep all AjaxDatatableView-derived views in a separate "ajax_datatable_views.py" source file, to make it crystal clear that you should never call them directly:

      file ajax_datatable_views.py

      .. code:: python

      from ajax_datatable.views import AjaxDatatableView
      from django.contrib.auth.models import Permission
      
      class PermissionAjaxDatatableView(AjaxDatatableView):
      
          model = Permission
          title = 'Permissions'
          initial_order = [["app_label", "asc"], ]
          length_menu = [[10, 20, 50, 100, -1], [10, 20, 50, 100, 'all']]
          search_values_separator = '+'
      
          column_defs = [
              AjaxDatatableView.render_row_tools_column_def(),
              {'name': 'id', 'visible': False, },
              {'name': 'codename', 'visible': True, },
              {'name': 'name', 'visible': True, },
              {'name': 'app_label', 'foreign_field': 'content_type__app_label', 'visible': True, },
              {'name': 'model', 'foreign_field': 'content_type__model', 'visible': True, },
          ]

      In the previous example, row id is included in the first column of the table, but hidden to the user.

      AjaxDatatableView will serialize the required data during table navigation.

      This is the resulting table:

      .. image:: screenshots/001a.png

      You can use common CSS style to customize the final rendering:

      .. image:: screenshots/001.png

      AjaxDatatableViewUtils.initialize_table() parameters are:

      element
          table element
      
      url
          action (remote url to be called via Ajax)
      
      extra_options={}
          custom options for dataTable()
      
      extra_data={}
          extra parameters to be sent via ajax for global "initial queryset" filtering;
          see: `Provide "extra data" to narrow down the initial queryset`_

      AjaxDatatableView Class attributes

      Required:

      • model
      • column_defs

      Optional:

      • initial_order = [[1, "asc"], [5, "desc"]] # positions can also be expressed as column names: [['surname', 'asc'], ]
      • length_menu = [[10, 20, 50, 100], [10, 20, 50, 100]]
      • latest_by = None
      • show_date_filters = None
      • show_column_filters = None
      • disable_queryset_optimization = False
      • disable_queryset_optimization_only = False
      • disable_queryset_optimization_select_related = False
      • disable_queryset_optimization_prefetch_related = False
      • table_row_id_prefix = 'row-'
      • table_row_id_fieldname = 'id'
      • render_row_details_template_name = "render_row_details.html"
      • search_values_separator = ''
      • sort_field: None

      or override the following methods to provide attribute values at run-time, based on request:

      .. code:: python

      def get_column_defs(self):
          return self.column_defs
      
      def get_initial_order(self):
          return self.initial_order
      
      def get_length_menu(self):
          return self.length_menu
      
      def get_latest_by(self, request):
          """
          Override to customize based on request.
      
          Provides the name of the column to be used for global date range filtering.
          Return either '', a fieldname or None.
      
          When None is returned, in model's Meta 'get_latest_by' attributed will be used.
          """
          return self.latest_by
      
      def get_show_date_filters(self, request):
          """
          Override to customize based on request.
      
          Defines whether to use the global date range filter.
          Return either True, False or None.
      
          When None is returned, will'll check whether 'latest_by' is defined
          """
          return self.show_date_filters
      
      def get_show_column_filters(self, request):
          """
          Override to customize based on request.
      
          Defines whether to use the column filters.
          Return either True, False or None.
      
          When None is returned, check if at least one visible column in searchable.
          """
          return self.show_column_filters
      
      def get_table_row_id(self, request, obj):
          """
          Provides a specific ID for the table row; default: "row-ID"
          Override to customize as required.
          """
          result = ''
          if self.table_row_id_fieldname:
              try:
                  result = self.table_row_id_prefix + str(getattr(obj, self.table_row_id_fieldname))
              except:
                  result = ''
          return result

      column_defs customizations

      Example::

      column_defs = [{
          'name': 'currency',                 # required
          'data': None,
          'title': 'Currency',                # optional: default = field verbose_name or column name
          'visible': True,                    # optional: default = True
          'searchable': True,                 # optional: default = True if visible, False otherwise
          'orderable': True,                  # optional: default = True if visible, False otherwise
          'foreign_field': 'manager__name',   # optional: follow relation
          'm2m_foreign_field': 'manager__name',   # optional: follow m2m relation
          'placeholder': False,               # ???
          'className': 'css-class-currency',  # optional class name for cell
          'defaultContent': '<h1>test</h1>',  # ???
          'width': 300,                       # optional: controls the minimum with of each single column
          'choices': None,                    # see `Filtering single columns` below
          'initialSearchValue': None,         # see `Filtering single columns` below
          'autofilter': False,                # see `Filtering single columns` below
          'boolean': False,                   # treat calculated column as BooleanField
          'max_length': 0,                    # if > 0, clip result longer then max_length
          'lookup_field': '__icontains',      # used for searches; default: '__iexact' for columns with choices, '__icontains' in all other cases
      }, {
          ...

      Notes:

      - **title**: if not supplied, the verbose name of the model column (when available)
        or **name** will be used
      - **width**: for this to be effective, you need to add **table-layout: fixed;** style
        to the HTML table, but in some situations this causes problems in the computation
        of the table columns' widths (at least in the current version 1.10.19 of Datatables.net)

      Provide "extra data" to narrow down the initial queryset

      Sometimes you might need to restrict the initial queryset based on the context.

      To that purpose, you can provide a dictionary of additional filters during table initialization; this dictionary will be sent to the View, where you can use it for queryset filtering.

      Provide as many key as required; assign either constant values or callables. The special keys 'date_from' and 'date_to' may be used to override values collected by the optional global date range filter (format: 'YYYY-MM-DD').

      Example:

      .. code:: javascript

          AjaxDatatableViewUtils.initialize_table(
              element,
              url,
              {
                  // extra_options (example)
                  processing: false,
                  autoWidth: false,
                  full_row_select: false,
                  scrollX: true,
                  bFilter: false
              }, {
                  // extra_data
                  client_id: '{{client.id}}',
                  date_from: function() { return date_input_to_isoformat('#date_from'); },
                  date_to: function() { return date_input_to_isoformat('#date_to'); }
              }
          );

      then:

      .. code:: python

      class SampleAjaxDatatableView(AjaxDatatableView):
      
          ...
      
          def get_initial_queryset(self, request=None):
      
              if not request.user.is_authenticated:
                  raise PermissionDenied
      
              # We accept either GET or POST
              if not getattr(request, 'REQUEST', None):
                  request.REQUEST = request.GET if request.method=='GET' else request.POST
      
              queryset = self.model.objects.all()
      
              if 'client_id' in request.REQUEST:
                  client_id = int(request.REQUEST.get('client_id'))
                  queryset = queryset.filter(client_id=client_id)
      
              return queryset

      Add a sidebar with custom filters

      Sometimes you need to provide complex or very specific filters to let the user control the content of the table in an advanced manner.

      In those cases, the global or column filters provided by AjaxDatatableView, which are based on simple and