Git Product home page Git Product logo

django-ajax-datatable's Introduction

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 ...

  • I use this app for my own projects, and improve it from time to time as new needs arises.
  • I received so much from the Django community, so I'm more than happy to share something hopefully useful for others. The app is intended to be opensource; feel free to use it we no restrictions at all. I added a MIT Licence file to the github repo, to make this more explicit.
  • Since v4.0.0, the package has been renamed from django-datatables-view to django-ajax-datatable to avoid a conflict on PyPI
  • Unfortunately I only have a few unit tests, and didn't bother (yet) to add a TOX procedure to run then with different Python/Django versions. Having said this, I can confirm that I do happen to use it with no problems in projects based on Django 2.x. However, most recent improvements have been tested mainly with Django 3. As far as I know, no Django3-specific features have been applied. In case, please open an issue, and I will fix it.
  • I'm not willing to support Python 2.x and Django 1.x any more; in case, use a previous release (tagged as v2.x.x); old releases will be in place in the repo forever

Features:

  • Pagination
  • Column ordering
  • Global generic search
  • Global date-range search over "get_latest_by" column
  • Column specific filtering
  • Foreign key fields can be used, using the "model1__model2__field" notation
  • Customizable rendering of table rows
  • and more ...

Inspired from:

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

Contents

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/.

screenshots/examples.png

Install the package by running:

pip install django-ajax-datatable

or:

pip install git+https://github.com/morlandi/[email protected]

then add 'ajax_datatable' to your INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'ajax_datatable',
]

Optional dependencies (for better debug tracing):

  • sqlparse
  • termcolor
  • pygments

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

  • /static/ajax_datatable/css/style.css
  • /static/ajax_datatable/js/utils.js

Example (plain jQuery from CDN):

{% block extrastyle %}

    <link href="{% static 'ajax_datatable/css/style.css' %}" rel="stylesheet" />
    <link href="//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="{% static 'ajax_datatable/js/utils.js' %}"></script>
    <script src="//cdn.datatables.net/1.10.22/js/jquery.dataTables.min.js"></script>

{% endcompress %}

Example (with Bootstrap4 support):

{% block extrastyle %}

    <link href="{% static 'ajax_datatable/css/style.css' %}" rel="stylesheet" />
    <!-- link rel='stylesheet' href="{% static 'datatables.net-bs/css/dataTables.bootstrap.min.css' %}" -->
    <link rel='stylesheet' href="{% static 'datatables.net-bs4/css/dataTables.bootstrap4.min.css' %}">
    <link rel='stylesheet' href="{% static 'datatables.net-buttons-bs/css/buttons.bootstrap.min.css' %}">

{% endblock extrastyle %}

{% block extrajs %}

    <script type="text/javascript" src="{% static 'ajax_datatable/js/utils.js' %}"></script>

    <script src="{% static 'datatables.net/js/jquery.dataTables.min.js' %}"></script>
    <!-- script src="{% static 'datatables.net-bs/js/dataTables.bootstrap.min.js' %}"></script -->
    <script src="{% static 'datatables.net-bs4/js/dataTables.bootstrap4.min.js' %}"></script>
    <script src="{% static 'datatables.net-buttons/js/dataTables.buttons.min.js' %}"></script>
    <script src="{% static 'datatables.net-buttons/js/buttons.print.min.js' %}"></script>
    <script src="{% static 'datatables.net-buttons/js/buttons.html5.min.js' %}"></script>
    <script src="{% static 'datatables.net-buttons-bs/js/buttons.bootstrap.min.js' %}"></script>
    <script src="{% static 'jszip/dist/jszip.min.js' %}"></script>
    <script src="{% static 'pdfmake/build/pdfmake.min.js' %}"></script>
    <script src="{% static 'pdfmake/build/vfs_fonts.js' %}"></script>

{% endcompress %}

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 <table> element
    • a javascript code which links this HTML table to the (AjaxDatatableView-derived) second view
  2. 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

<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

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

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:

screenshots/001a.png

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

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

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:

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

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)

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:

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:

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

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 <input> and <select> widgets, may not be enought.

Still, you can easily add a sidebar with custom filters, and apply to them the concepts explained in the previous paragraph (Provide "extra data" to narrow down the initial queryset).

An example of this technique has been added to the Example project; the result and a detailed explanation is presented here:

http://django-ajax-datatable-demo.brainstorm.it/side_filters/

screenshots/side_filters.png

Starting from v3.2.0, each table row is characterized with a specific ID on each row (tipically, the primary key value from the queryset)

screenshots/table_row_id.png

The default behaviour is to provide the string "row-ID", where:

  • "row-" is retrieved from self.table_row_id_prefix
  • "ID" is retrieved from the row object, using the field with name self.table_row_id_fieldname (default: "id")

Note that, for this to work, you are required to list the field "id" in the column list (maybe hidden).

This default behaviour can be customized by either:

  • replacing the values for table_row_id_fieldname and/or table_row_id_prefix, or
  • overriding def get_table_row_id(self, request, obj)

Sorting is managed the by the overridable method sort_queryset(), and fully delegated to the database for better performances.

For each orderable column, the column name will be used, unless a sort_field has been specified; in which case, the latter will be used instead.

DatatableView.show_column_filters (or DatatableView.get_show_column_filters(request)) defines whether to show specific filters for searchable columns as follows:

  • None (default): show if at least one visible column in searchable
  • True: always show
  • False: always hide

By default, a column filter for a searchable column is rendered as a text input box; you can instead provide a select box using the following attributes:

choices
  • None (default) or False: no choices (use text input box)
  • True: use Model's field choices;
    • failing that, we might use "autofilter"; that is: collect the list of distinct values from db table
    • or, for BooleanField columns, provide (None)/Yes/No choice sequence
    • calculated columns with attribute 'boolean'=True are treated as BooleanFields
  • ((key1, value1), (key2, values), ...) : use supplied sequence of choices
autofilter
  • default = False
  • when set: if choices == True and no Model's field choices are available, collects distinct values from db table (much like Excel "autofilter" feature)

For the first rendering of the table:

initialSearchValue
  • optional initial value for column filter

Note that initialSearchValue can be a value or a callable object. If callable it will be called every time a new object is created.

For example:

class MyAjaxDatatableView(AjaxDatatableView):

    def today():
        return datetime.datetime.now().date()

    ...

    column_defs = [
        ...
        {
            'name': 'created',
            'choices': True,
            'autofilter': True,
            'initialSearchValue': today
        },
        ...
    ]

screenshots/column_filtering.png

Searching on multiple values can be obtained by assigning a "search value separator" as in the following example:

search_values_separator = '+'

In this case, if the user inputs "aaa + bbb", the following search will be issued:

Q("aaa") | Q("bbb")

This works for text search on both global and columns filters.

TODO: test with dates, choices and autofilter.

You can insert placeholder columns in the table, and feed their content with arbitrary HTML.

Example:

@method_decorator(login_required, name='dispatch')
class RegisterAjaxDatatableView(AjaxDatatableView):

    model = Register
    title = _('Registers')

    column_defs = [
        {
            'name': 'id',
            'visible': False,
        }, {
            'name': 'created',
        }, {
            'name': 'dow',
            'title': 'Day of week',
            'placeholder': True,
            'searchable': False,
            'orderable': False,
            'className': 'highlighted',
        }, {
            ...
        }
    ]

    def customize_row(self, row, obj):
        days = ['monday', 'tuesday', 'wednesday', 'thyrsday', 'friday', 'saturday', 'sunday']
        if obj.created is not None:
            row['dow'] = '<b>%s</b>' % days[obj.created.weekday()]
        else:
            row['dow'] = ''
        return

screenshots/003.png

Sometimes you might want to clip results up to a given maximum length, to control the column width.

This can be obtained by specifying a positive value for the max_length column_spec attribute.

Results will be clipped in both the column cells and in the column filter.

screenshots/clipping_results.png

Clipped results are rendered as html text as follows:

def render_clip_value_as_html(self, long_text, short_text, is_clipped):
    """
    Given long and shor version of text, the following html representation:
        <span title="long_text">short_text[ellipsis]</span>

    To be overridden for further customisations.
    """
    return '<span title="{long_text}">{short_text}{ellipsis}</span>'.format(
        long_text=long_text,
        short_text=short_text,
        ellipsis='&hellip;' if is_clipped else ''
    )

You can customise the rendering by overriding render_clip_value_as_html()

The following table events are broadcasted to your custom handlers, provided you subscribe them:

  • initComplete(table)
  • drawCallback(table, settings)
  • rowCallback(table, row, data)
  • footerCallback(table, row, data, start, end, display)

Please note the the first parameter of the callback is always the event, and next parameters are additional data:

.trigger('foo', [1, 2]);

.on('foo', function(event, one, two) { ... });

More events triggers sent directly by DataTables.net are listed here:

https://datatables.net/reference/event/

Example:

<div class="table-responsive">
    <table id="datatable" width="100%" class="table table-striped table-bordered dataTables-log">
    </table>
</div>

<script language="javascript">
    $(document).ready(function() {

        // Subscribe "rowCallback" event
        $('#datatable').on('rowCallback', function(event, table, row, data ) {
            //$(e.target).show();
            console.log('rowCallback(): table=%o', table);
            console.log('rowCallback(): row=%o', row);
            console.log('rowCallback(): data=%o', data);
        });

        // Initialize table
        AjaxDatatableViewUtils.initialize_table(
            $('#datatable'),
            "{% url 'frontend:object-datatable' model|app_label model|model_name %}",
            extra_option={},
            extra_data={}
        );
    });
</script>

Provides the queryset to work with; defaults to self.model.objects.all()

Example:

def get_initial_queryset(self, request=None):
    if not request.user.view_all_clients:
        queryset = request.user.related_clients.all()
    else:
        queryset = super().get_initial_queryset(request)
    return queryset

When collecting data for autofiltering in a "foreign_field" column, we need some data source for doing the lookup.

The default implementation is as follows:

def get_foreign_queryset(self, request, field):
    queryset = field.model.objects.all()
    return queryset

You can override it for further reducing the resulting list.

Called every time a new data row is required by the client, to let you further customize cell content

Example:

def customize_row(self, row, obj):
    # 'row' is a dictionary representing the current row, and 'obj' is the current object.
    row['code'] = '<a class="client-status client-status-%s" href="%s">%s</a>' % (
        obj.status,
        reverse('frontend:client-detail', args=(obj.id,)),
        obj.code
    )
    if obj.recipe is not None:
        row['recipe'] = obj.recipe.display_as_tile() + ' ' + str(obj.recipe)
    return

Renders an HTML fragment to show table row content in "detailed view" fashion, as previously explained later in the Add row tools as first column section. Having "pk" in your column_defs list is needed to have the script get the object to render.

See also: row details customization

Example:

def render_row_details(self, pk, request=None):
    client = self.model.objects.get(pk=pk)
    ...
    return render_to_string('frontend/pages/includes/client_row_details.html', {
        'client': client,
        ...
    })

OR you can have your own callback called instead (thanks to PetrDlouhy):

AjaxDatatableViewUtils.initialize_table(
    element,
    url,
    {
        // extra_options
        ...
        detail_callback: function(data, tr) {
            console.log('tr: %o', tr);
            console.log('data: %o', data);

            // for example: open a Bootstrap3 modal
            $('.modal-body').html(data, 'details');
            $('.modal').modal();
        }
    }, {
        // extra_data
        ...
    },
);

You can annotate the table footer with a custom message by overridding the following View method.

def footer_message(self, qs, params):
    """
    Overriden to append a message to the bottom of the table
    """
    return None

Example:

def footer_message(self, qs, params):
    return 'Selected rows: %d' % qs.count()
<style>
    .dataTables_wrapper .dataTables_extraFooter {
        border: 1px solid blue;
        color: blue;
        padding: 8px;
        margin-top: 8px;
        text-align: center;
    }
</style>

screenshots/005.png

Same as footer_message() but appends message to toolbar:

def footer_message(self, qs, params):
    return 'Selected rows: %d' % qs.count()

Renders clipped results as html span tag, providing the non-clipped value as title:

def render_clip_value_as_html(self, long_text, short_text, is_clipped):
    """
    Given long and shor version of text, the following html representation:
        <span title="long_text">short_text[ellipsis]</span>

    To be overridden for further customisations.
    """
    return '<span title="{long_text}">{short_text}{ellipsis}</span>'.format(
        long_text=long_text,
        short_text=short_text,
        ellipsis='&hellip;' if is_clipped else ''
    )

Override to customise the rendering of clipped cells.

As the purpose of this module is all about querysets rendering, any chance to optimize data extractions from the database is more then appropriate.

Starting with v2.3.0, AjaxDatatableView tries to burst performances in two ways:

  1. by using only to limit the number of columns in the result set
  2. by using select_related to minimize the number of queries involved

The parameters passed to only() and select_related() are inferred from column_defs.

Should this cause any problem, you can disable queryset optimization in two ways:

  • globally: by activating the AJAX_DATATABLE_DISABLE_QUERYSET_OPTIMIZATION setting
  • per table: by setting to True the value of the disable_queryset_optimization attribute

Alternatively, you can selectively disable the only or select_related optimization with the following flags:

  • self.disable_queryset_optimization_only
  • self.disable_queryset_optimization_select_related
  1. Plain queryset:

    SELECT "tasks_devicetesttask"."id",
           "tasks_devicetesttask"."description",
           "tasks_devicetesttask"."created_on",
           "tasks_devicetesttask"."created_by_id",
           "tasks_devicetesttask"."started_on",
           "tasks_devicetesttask"."completed_on",
           "tasks_devicetesttask"."job_id",
           "tasks_devicetesttask"."status",
           "tasks_devicetesttask"."mode",
           "tasks_devicetesttask"."failure_reason",
           "tasks_devicetesttask"."progress",
           "tasks_devicetesttask"."log_text",
           "tasks_devicetesttask"."author",
           "tasks_devicetesttask"."order",
           "tasks_devicetesttask"."appliance_id",
           "tasks_devicetesttask"."serial_number",
           "tasks_devicetesttask"."program_id",
           "tasks_devicetesttask"."position",
           "tasks_devicetesttask"."hidden",
           "tasks_devicetesttask"."is_duplicate",
           "tasks_devicetesttask"."notes"
    FROM "tasks_devicetesttask"
    WHERE "tasks_devicetesttask"."hidden" = FALSE
    ORDER BY "tasks_devicetesttask"."created_on" DESC
    

    [sql] (233ms) 203 queries with 182 duplicates

  2. With select_related():

    SELECT "tasks_devicetesttask"."id",
           "tasks_devicetesttask"."description",
           "tasks_devicetesttask"."created_on",
           "tasks_devicetesttask"."created_by_id",
           "tasks_devicetesttask"."started_on",
           "tasks_devicetesttask"."completed_on",
           "tasks_devicetesttask"."job_id",
           "tasks_devicetesttask"."status",
           "tasks_devicetesttask"."mode",
           "tasks_devicetesttask"."failure_reason",
           "tasks_devicetesttask"."progress",
           "tasks_devicetesttask"."log_text",
           "tasks_devicetesttask"."author",
           "tasks_devicetesttask"."order",
           "tasks_devicetesttask"."appliance_id",
           "tasks_devicetesttask"."serial_number",
           "tasks_devicetesttask"."program_id",
           "tasks_devicetesttask"."position",
           "tasks_devicetesttask"."hidden",
           "tasks_devicetesttask"."is_duplicate",
           "tasks_devicetesttask"."notes",
           "backend_appliance"."id",
           "backend_appliance"."description",
           "backend_appliance"."hidden",
           "backend_appliance"."created",
           "backend_appliance"."created_by_id",
           "backend_appliance"."updated",
           "backend_appliance"."updated_by_id",
           "backend_appliance"."type",
           "backend_appliance"."rotation",
           "backend_appliance"."code",
           "backend_appliance"."barcode",
           "backend_appliance"."mechanical_efficiency_min",
           "backend_appliance"."mechanical_efficiency_max",
           "backend_appliance"."volumetric_efficiency_min",
           "backend_appliance"."volumetric_efficiency_max",
           "backend_appliance"."displacement",
           "backend_appliance"."speed_min",
           "backend_appliance"."speed_max",
           "backend_appliance"."pressure_min",
           "backend_appliance"."pressure_max",
           "backend_appliance"."oil_temperature_min",
           "backend_appliance"."oil_temperature_max",
           "backend_program"."id",
           "backend_program"."description",
           "backend_program"."hidden",
           "backend_program"."created",
           "backend_program"."created_by_id",
           "backend_program"."updated",
           "backend_program"."updated_by_id",
           "backend_program"."code",
           "backend_program"."start_datetime",
           "backend_program"."end_datetime",
           "backend_program"."favourite"
    FROM "tasks_devicetesttask"
    LEFT OUTER JOIN "backend_appliance" ON ("tasks_devicetesttask"."appliance_id" = "backend_appliance"."id")
    LEFT OUTER JOIN "backend_program" ON ("tasks_devicetesttask"."program_id" = "backend_program"."id")
    WHERE "tasks_devicetesttask"."hidden" = FALSE
    ORDER BY "tasks_devicetesttask"."created_on" DESC
    

    [sql] (38ms) 3 queries with 0 duplicates

  3. With select_related() and only():

    SELECT "tasks_devicetesttask"."id",
           "tasks_devicetesttask"."started_on",
           "tasks_devicetesttask"."completed_on",
           "tasks_devicetesttask"."status",
           "tasks_devicetesttask"."failure_reason",
           "tasks_devicetesttask"."author",
           "tasks_devicetesttask"."order",
           "tasks_devicetesttask"."appliance_id",
           "tasks_devicetesttask"."serial_number",
           "tasks_devicetesttask"."program_id",
           "tasks_devicetesttask"."position",
           "backend_appliance"."id",
           "backend_appliance"."code",
           "backend_program"."id",
           "backend_program"."code"
    FROM "tasks_devicetesttask"
    LEFT OUTER JOIN "backend_appliance" ON ("tasks_devicetesttask"."appliance_id" = "backend_appliance"."id")
    LEFT OUTER JOIN "backend_program" ON ("tasks_devicetesttask"."program_id" = "backend_program"."id")
    WHERE "tasks_devicetesttask"."hidden" = FALSE
    ORDER BY "tasks_devicetesttask"."created_on" DESC
    

    [sql] (19ms) 3 queries with 0 duplicates

AJAX_DATATABLE_MAX_COLUMNS = 30
AJAX_DATATABLE_TRACE_COLUMNDEFS = False               #  enables debug tracing of applied column defs
AJAX_DATATABLE_TRACE_QUERYDICT = False                #  enables debug tracing of datatables requests
AJAX_DATATABLE_TRACE_QUERYSET = False                 #  enables debug tracing of applied query
AJAX_DATATABLE_TEST_FILTERS = False                   # trace results for each individual filter, for debugging purposes
AJAX_DATATABLE_DISABLE_QUERYSET_OPTIMIZATION = False  # all queryset optimizations are disabled
AJAX_DATATABLE_STRIP_HTML_TAGS = True                 # string HTML tags when rendering the table

You can insert AjaxDatatableView.render_row_tools_column_def() as the first element in column_defs to obtain some tools at the beginning of each table row.

If full_row_select=true is specified as extra-option during table initialization, row details can be toggled by clicking anywhere in the row.

datatables_views.py

from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator

from ajax_datatable.views import AjaxDatatableView
from backend.models import Register


@method_decorator(login_required, name='dispatch')
class RegisterAjaxDatatableView(AjaxDatatableView):

    model = Register
    title = 'Registers'

    column_defs = [
        AjaxDatatableView.render_row_tools_column_def(),
        {
            'name': 'id',
            'visible': False,
        }, {
        ...

By default, these tools will provide an icon to show and hide a detailed view below each table row.

The tools are rendered according to the template ajax_datatable/row_tools.html, which can be overridden.

Row details are automatically collected via Ajax by calling again the views with a specific ?action=details parameters, and will be rendered by the method:

def render_row_details(self, pk, request=None)

which you can further customize when needed.

The default behaviour provided by the base class if shown below:

screenshots/002.png

The default implementation of render_row_details() tries to load a template in the following order:

  • ajax_datatable/<app_label>/<model_name>/<render_row_details_template_name>
  • ajax_datatable/<app_label>/<render_row_details_template_name>
  • ajax_datatable/<render_row_details_template_name>

(where the default value for <render_row_details_template_name> is "render_row_details.html")

and, when found, uses it for rendering.

The template receives the following context:

html = template.render({
    'model': self.model,
    'model_admin': self.get_model_admin(),
    'object': obj,
    'extra_data': [extra_data dict retrieved from request]
}, request)

model_admin, when available, can be used to navigate fieldsets (if defined) in the template, much like django's admin/change_form.html does.

If no template is available, a simple HTML table with all field values is built instead.

In all cases, the resulting HTML will be wrapped in the following structure:

<tr class="details">
    <td class="details">
        <div class="row-details-wrapper" data-parent-row-id="PARENT-ROW-ID">
            ...

When a latest_by column has been specified and show_date_filter is active, a global date range filtering widget is provided, based on jquery-ui.datepicker:

screenshots/004a.png

The header of the column used for date filtering is decorated with the class "latest_by"; you can use it to customize it's rendering.

You can fully replace the widget with your own by providing a custom fn_daterange_widget_initialize() callback at Module's initialization, as in the following example, where we use bootstrap.datepicker:

AjaxDatatableViewUtils.init({
    search_icon_html: '<i class="fa fa-search"></i>',
    language: {
    },
    fn_daterange_widget_initialize: function(table, data) {
        var wrapper = table.closest('.dataTables_wrapper');
        var toolbar = wrapper.find(".toolbar");
        toolbar.html(
            '<div class="daterange" style="float: left; margin-right: 6px;">' +
            '{% trans "From" %}: <input type="text" class="date_from" autocomplete="off">' +
            '&nbsp;&nbsp;' +
            '{% trans "To" %}: <input type="text" class="date_to" autocomplete="off">' +
            '</div>'
        );
        var date_pickers = toolbar.find('.date_from, .date_to');
        date_pickers.datepicker();
        date_pickers.on('change', function(event) {
            // Annotate table with values retrieved from date widgets
            var dt_from = toolbar.find('.date_from').data("datepicker");
            var dt_to = toolbar.find('.date_to').data("datepicker");
            table.data('date_from', dt_from ? dt_from.getFormattedDate("yyyy-mm-dd") : '');
            table.data('date_to', dt_to ? dt_to.getFormattedDate("yyyy-mm-dd") : '');
            // Redraw table
            table.api().draw();
        });
    }
});

screenshots/004b.png

In case of errors, Datatables.net shows an alert popup:

screenshots/006.png

You can change it to trace the error in the browser console, insted:

// change DataTables' error reporting mechanism to throw a Javascript
// error to the browser's console, rather than alerting it.
$.fn.dataTable.ext.errMode = 'throw';

All details of Datatables.net requests can be logged to the console by activating these setting:

AJAX_DATATABLE_TRACE_COLUMNDEFS = True
AJAX_DATATABLE_TRACE_QUERYDICT = True

The resulting query (before pagination) can be traced as well with:

AJAX_DATATABLE_TRACE_QUERYSET = True

Debugging traces for date range filtering, column filtering or global filtering can be displayed by activating this setting:

AJAX_DATATABLE_TEST_FILTERS

screenshots/007.png

Chances are you might want to supply a standard user interface for listing several models.

In this case, it is possible to use a generic approach and avoid code duplications, as detailed below.

First, we supply a generic view which receives a model as parameter, and passes it to the template used for rendering the page:

file frontend/datatables_views.py:

@login_required
def object_list_view(request, model, template_name="frontend/pages/object_list.html"):
    """
    Render the page which contains the table.
    That will in turn invoke (via Ajax) object_datatable_view(), to fill the table content
    """
    return render(request, template_name, {
        'model': model,
    })

In the urlconf, link to specific models as in the example below:

file frontend/urls.py:

path('channel/', datatables_views.object_list_view, {'model': backend.models.Channel, }, name="channel-list"),

The template uses the model received in the context to display appropriate verbose_name and verbose_name_plural attributes, and to extract app_label and model_name as needed; unfortunately, we also had to supply some very basic helper templatetags, as the _meta attribute of the model is not directly visible in this context.

{% extends 'frontend/base.html' %}
{% load static datatables_view_tags i18n %}

{% block breadcrumbs %}
    <li>
        <a href="{% url 'frontend:index' %}">{% trans 'Home' %}</a>
    </li>
    <li class="active">
        <strong>{{model|model_verbose_name_plural}}</strong>
    </li>
{% endblock breadcrumbs %}

{% block content %}

    {% testhasperm model 'view' as can_view_objects %}
    {% if not can_view_objects %}
        <h2>{% trans "Sorry, you don't have the permission to view these objects" %}</h2>
    {% else %}

        <div>
            <h5>{% trans 'All' %} {{ model|model_verbose_name_plural }}</h5>
            {% ifhasperm model 'add' %}
                <a href="#">{% trans 'Add ...' %}</a>
            {% endifhasperm %}
        </div>
        <div class="table-responsive">
            <table id="datatable" width="100%" class="table table-striped table-bordered table-hover dataTables-example">
            </table>
        </div>

        {% ifhasperm model 'add' %}
            <a href="#">{% trans 'Add ...' %}</a>
        {% endifhasperm %}

    {% endif %}

{% endblock content %}


{% block extrajs %}
    <script language="javascript">

        $(document).ready(function() {
            AjaxDatatableViewUtils.initialize_table(
                $('#datatable'),
                "{% url 'frontend:object-datatable' model|app_label model|model_name %}",
                extra_option={},
                extra_data={}
            );
        });

    </script>
{% endblock %}

app_label and model_name are just strings, and as such can be specified in an url.

The connection with the Django backend uses the following generic url:

{% url 'frontend:object-datatable' model|app_label model|model_name %}

from urls.py:

# List any Model
path('datatable/<str:app_label>/<str:model_name>/', datatables_views.object_datatable_view, name="object-datatable"),

object_datatable_view() is a lookup helper which navigates all AjaxDatatableView-derived classes in the module and selects the view appropriate for the specific model in use:

file frontend/datatables_views.py:

import inspect

def object_datatable_view(request, app_label, model_name):

    # List all AjaxDatatableView in this module
    datatable_views = [
        klass
        for name, klass in inspect.getmembers(sys.modules[__name__])
        if inspect.isclass(klass) and issubclass(klass, AjaxDatatableView)
    ]

    # Scan AjaxDatatableView until we find the right one
    for datatable_view in datatable_views:
        model = datatable_view.model
        if (model is not None and (model._meta.app_label, model._meta.model_name) == (app_label, model_name)):
            view = datatable_view
            break

    return view.as_view()(request)

which for this example happens to be:

@method_decorator(login_required, name='dispatch')
class ChannelAjaxDatatableView(BaseAjaxDatatableView):

    model = Channel
    title = 'Channels'

    column_defs = [
        AjaxDatatableView.render_row_tools_column_def(),
        {
            'name': 'id',
            'visible': False,
        }, {
            'name': 'description',
        }, {
            'name': 'code',
        }
    ]
setTimeout(function () {
    AjaxDatatableViewUtils.adjust_table_columns();
}, 200);

or maybe better:

var table = element.DataTable({
    ...
    "initComplete": function(settings) {
        setTimeout(function () {
            AjaxDatatableViewUtils.adjust_table_columns();
        }, 200);
    }

where:

function adjust_table_columns() {
    // Adjust the column widths of all visible tables
    // https://datatables.net/reference/api/%24.fn.dataTable.tables()
    $.fn.dataTable
        .tables({
            visible: true,
            api: true
        })
        .columns.adjust();
}
$.fn.dataTable.tables({
    api: true
}).draw();
table = $(element).closest('table.dataTable');
$.ajax({
    type: 'GET',
    url: ...
}).done(function(data, textStatus, jqXHR) {
    table.DataTable().ajax.reload(null, false);
});
table.DataTable().row(tr).invalidate().draw(false);

Working example:

{% get_current_language as LANGUAGE_CODE %}

function onToggleQueueStatus(event) {

    // The link is a table cell
    event.preventDefault();
    let td = $(event.target).closest('td');

    // Retrieve the table row and the record id
    let tr = td.closest('tr');
    // Es: "row-692255dc-7eaa-4150-be19-a555a8b34188"
    let row_id = tr.attr('id').substr(4);

    // Call the server via AJAX to process the record
    let url = sprintf('/{{LANGUAGE_CODE}}/j/product_order/%s/toggle_queue_status/', row_id);
    FrontendForms.overlay_show(tr);
    var promise = $.ajax({
        type: 'POST',
        url: url,
        data: null,
        cache: false,
        crossDomain: false,
        headers: {
            'X-CSRFToken': FrontendForms.getCookie('csrftoken')
        }
    }).done(function(data, textStatus, jqXHR) {
        //console.log('OK; data=%o', data);
    }).fail(function(jqXHR, textStatus, errorThrown) {
        console.log('ERROR: ' + jqXHR.responseText);
        Frontend.display_server_error_ex(jqXHR);
    }).always(function() {

        // Since the record has been changed, we need to update the table row;
        // Redraw the row holding the current paging position
        let table = $(tr).closest('table.dataTable');
        table.DataTable().row(tr).invalidate().draw(false);

    });
    return promise;
}

Another (very old) Example:

var table = $(element).closest('table.dataTable');
var table_row_id = table.find('tr.shown').attr('id');
$.ajax({
    type: 'POST',
    url: ...
}).done(function(data, textStatus, jqXHR) {
    table.DataTable().ajax.reload(null, false);

    // Since we've update the record via Ajax, we need to redraw this table row
    var tr = table.find('#' + table_row_id);
    var row = table.DataTable().row(tr)
    row.invalidate().draw();

    // Hack: here we would like to enhance the updated row, by adding the 'updated' class;
    // Since a callback is not available upon draw completion,
    // let's use a timer to try later, and cross fingers
    setTimeout(function() {
        table.find('#' + table_row_id).addClass('updated');
    }, 200);
    setTimeout(function() {
        table.find('#' + table_row_id).addClass('updated');
    }, 1000);

});
// change DataTables' error reporting mechanism to throw a Javascript
// error to the browser's console, rather than alerting it.
$.fn.dataTable.ext.errMode = 'throw';
  • AjaxDatatableViewUtils.init(options)
  • AjaxDatatableViewUtils.initialize_table(element, url, extra_options={}, extra_data={})
  • AjaxDatatableViewUtils.after_table_initialization(table, data, url)
  • AjaxDatatableViewUtils.adjust_table_columns()
  • AjaxDatatableViewUtils.redraw_all_tables()
  • AjaxDatatableViewUtils.redraw_table(element)

You can provide localized messages by initializing the AjaxDatatableViewUtils JS module as follow (example in italian):

AjaxDatatableViewUtils.init({
    search_icon_html: '<i class="fa fa-search" style="font-size: 16px"></i>',
    language: {
        "decimal":        "",
        "emptyTable":     "Nessun dato disponibile",
        "info":           "Visualizzate da _START_ a _END_ di _TOTAL_ righe",
        "infoEmpty":      "",
        "infoFiltered":   "(filtered from _MAX_ total entries)",
        "infoPostFix":    "",
        "thousands":      ",",
        "lengthMenu":     "Visualizza _MENU_ righe per pagina",
        "loadingRecords": "Caricamento in corso ...",
        "processing":     "Elaborazione in corso ...",
        "search":         "Cerca:",
        "zeroRecords":    "Nessun record trovato",
        "paginate": {
            "first":      "Prima",
            "last":       "Ultima",
            "next":       ">>",
            "previous":   "<<"
        },
        "aria": {
            "sortAscending":  ": activate to sort column ascending",
            "sortDescending": ": activate to sort column descending"
        }
    }
});

You can do this, for example, in your "base.html" template, and it will be in effect for all subsequent instantiations:

<script language="javascript">
    $(document).ready(function() {
        AjaxDatatableViewUtils.init({
            ...
        });
    });
</script>

Since the list of table columns is controlled by the library, based on column_defs list specified in the AjaxDatatableView class, you can't insert a custom column "javascript-side".

However, you can easily do it "python-side":

class ArtistAjaxDatatableView(AjaxDatatableView):

    ...

    column_defs = [
        ...
        {'name': 'edit', 'title': 'Edit', 'placeholder': True, 'searchable': False, 'orderable': False, },
        ...
    ]

    def customize_row(self, row, obj):
        row['edit'] = """
            <a href="#" class="btn btn-info btn-edit"
               onclick="var id=this.closest('tr').id.substr(4); alert('Editing Artist: ' + id); return false;">
               Edit
            </a>
        """
        ...

screenshots/custom-row-button.png

In the snippet above, we added an 'edit' column, customizing it's content via customize_row().

Note how we retrieved the object id from the "row-NNN" table row attribute in the "onclick" handler.

screenshots/009.png

import jsonfield
from ajax_datatable.views import AjaxDatatableView
from .utils import json_prettify


class MyAjaxDatatableView(AjaxDatatableView):

    ...

    def render_row_details(self, pk, request=None):

        obj = self.model.objects.get(pk=pk)
        fields = [f for f in self.model._meta.get_fields() if f.concrete]
        html = '<table class="row-details">'
        for field in fields:
            value = getattr(obj, field.name)
            if isinstance(field, jsonfield.JSONField):
                value = json_prettify(value)
            html += '<tr><td>%s</td><td>%s</td></tr>' % (field.name, value)
        html += '</table>'
        return html

where:

import json
from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import HtmlFormatter
from django.utils.safestring import mark_safe


def json_prettify_styles():
    """
    Used to generate Pygment styles (to be included in a .CSS file) as follows:
        print(json_prettify_styles())
    """
    formatter = HtmlFormatter(style='colorful')
    return formatter.get_style_defs()


def json_prettify(json_data):
    """
    Adapted from:
    https://www.pydanny.com/pretty-formatting-json-django-admin.html
    """

    # Get the Pygments formatter
    formatter = HtmlFormatter(style='colorful')

    # Highlight the data
    json_text = highlight(
        json.dumps(json_data, indent=2),
        JsonLexer(),
        formatter
    )

    # # remove leading and trailing brances
    # json_text = json_text \
    #     .replace('<span class="p">{</span>\n', '') \
    #     .replace('<span class="p">}</span>\n', '')

    # Get the stylesheet
    #style = "<style>" + formatter.get_style_defs() + "</style>"
    style = ''

    # Safe the output
    return mark_safe(style + json_text)

screenshots/010.png

First, we mark the relevant info with a specific CSS class, so we can search for it later

column_defs = [
    ...
    }, {
        'name': 'error_counter',
        'title': 'errors',
        'className': 'error_counter',
    }, {
    ...
]

Have a callback called after each table redraw

var table = element.DataTable({
    ...
});

table.on('draw.dt', function(event) {
    onTableDraw(event);
});

then change the rendered table as needed

var onTableDraw = function (event) {

    var html_table = $(event.target);
    html_table.find('tr').each(function(index, item) {

        try {
            var row = $(item);
            text = row.children('td.error_counter').first().text();
            var error_counter = isNaN(text) ? 0 : parseInt(text);

            if (error_counter > 0) {
                row.addClass('bold');
            }
            else {
                row.addClass('grayed');
            }
        }
            catch(err) {
        }

    });
}

or use a rowCallback as follows:

// Subscribe "rowCallback" event
$('#datatable').on('rowCallback', function(event, table, row, data ) {
    $(row).addClass(data.read ? 'read' : 'unread');
}

This works even if the 'read' column we're interested in is actually not visible.

screenshots/008.png

Row details customization:

def render_row_details(self, pk, request=None):

    obj = self.model.objects.get(pk=pk)
    html = '<table class="row-details">'
    html += "<tr><td>alarm status:</td><td>"
    for choice in BaseTask.ALARM_STATUS_CHOICES:
        # Lo stato corrente lo visualizziamo in grassetto
        if choice[0] == obj.alarm:
            html += '<b>%s</b>&nbsp;' % (choice[1])
        else:
            # Se non "unalarmed", mostriamo i link per cambiare lo stato
            # (tutti tranne "unalarmed")
            if obj.alarm != BaseTask.ALARM_STATUS_UNALARMED and choice[0] != BaseTask.ALARM_STATUS_UNALARMED:
                html += '<a class="set-alarm" href="#" onclick="set_row_alarm(this, \'%s\', %d); return false">%s</a>&nbsp;' % (
                    str(obj.pk),
                    choice[0],
                    choice[1]
                )
    html += '</td></tr>'

Client-side code:

<script language="javascript">

    function set_row_alarm(element, task_id, value) {
        $("body").css("cursor", "wait");
        //console.log('set_row_alarm: %o %o %o', element, task_id, value);
        table = $(element).closest('table.dataTable');
        //console.log('table id: %o', table.attr('id'));

        $.ajax({
            type: 'GET',
            url: sprintf('/set_alarm/%s/%s/%d/', table.attr('id'), task_id, value),
            dataType: 'html'
        }).done(function(data, textStatus, jqXHR) {
            table.DataTable().ajax.reload(null, false);
        }).always(function( data, textStatus, jqXHR) {
            $("body").css("cursor", "default");
        });
    }

Server-side code:

urlpatterns = [
    ...
    path('set_alarm/<str:table_id>/<uuid:task_id>/<int:new_status>/',
        views.set_alarm,
        name="set_alarm"),
]

@login_required
def set_alarm(request, table_id, task_id, new_status):

    # Retrieve model from table id
    # Example table_id:
    #   'datatable_walletreceivetransactionstask'
    #   'datatable_walletcheckstatustask_summary'
    model_name = table_id.split('_')[1]
    model = apps.get_model('tasks', model_name)

    # Retrieve task
    task = get_object_by_uuid_or_404(model, task_id)

    # Set alarm value
    task.set_alarm(request, new_status)

    return HttpResponse('ok')
  • specific rendering for boolean columns

Currently, an exact match is applied; a date-range selection would be better; references:

A checkbox or a select

django-ajax-datatable's People

Contributors

ivi3 avatar klavman avatar maps82 avatar mike3285 avatar morlandi avatar petrdlouhy avatar shuki25 avatar tboulogne avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-ajax-datatable's Issues

How to apply filtering for model dynamic field

I have dynamic field name, constructed based on combination for two or more model field values.
Now if I want to apply filter on this field, is it possible to apply by passing any column def argument like lookup_field or any other.

I want to pass search value = model.filed1 or model.filed2 or model.field3

Discuss HTML injection in the documentation

The table doesn't prevent HTML injection in any way. So everything is left on the implementator.
It should be at least mentioned in the documentation with solution (I found it by using customize_row and from django.utils.html import escape).

support for django 3.2

hi
could you please add support for django 3.2 >
bcz im using django 3.2
but when installing this module its automatically downgrading to 3.1.6

Null/empty searches?

Wanted to reach out to see if there were any options for null/empty searches? Doesn't seem possible with text searches, nor with the select searches.

Thank you in advance,

Juan

Edit cell

Hi @morlandi !

I am trying to allow users to click on a cell, modify a cell and send back to django the new value.
I tried using this tutorial but I haven't achieved my goal.

Do you know how I can could do such a thing ?

Thanks a lot :)

Column Visibility Bug

I found some weird things when I implement the 'colvis' function (column visibility) to the buttons. When I click on one of the columns in the column menu to hide, the selected column data is successfully hidden, however, the column headers are still there and even duplicated.

Screen Shot 2021-07-06 at 11 03 07

I want to add custom date range filter, but couldn't success

I want to make same as below example

i am using the following code

Screen Shot 2021-03-08 at 4 22 43 AM

ajax_datatable_views.py

`
class UserLogAjaxDatatableView(AjaxDatatableView):
model = Change
initial_order = [["date_created", "desc"], ]
Rec_Count = model.objects.all().count()
if Rec_Count > 500:
length_menu = [[10, 25, 50, 100], [10, 25, 50, 100]]
else:
length_menu = [[10, 25, 50, 100, -1], [10, 25, 50, 100, _("All")]]
search_values_separator = '+'
latest_by = "date_created"
show_date_filter = True

column_defs = [
    {'name': 'id', 'visible': False},
    {'name': 'date_created', 'searchable': True},
    {'name': 'user', 'foreign_field': 'user__username','visible': True, 'searchable': True},
    {'name': 'content_type', 'foreign_field': 'content_type__model','visible': True, 'searchable': True},
    {'name': 'object_repr', 'searchable': True},
    {'name': 'action', 'searchable': True},
    {'name': 'action1', 'data': None, 'visible': True, 'searchable': False, 'orderable': False, 'title':_('Action'), 'width': '50',},
]

def customize_row(self, row, obj):
    # 'row' is a dictionary representing the current row, and 'obj' is the current object.
    change_perm = '<button type="button" class="view-Log btn btn-outline-warning effect waves-light btn-xs" data-id="%s" title= "%s" ><i class="fe-eye"></i></button>'
    row['action1'] = ''

    if self.request.user.has_perm('crm.change_Log'):
        row['action1'] = \
            change_perm \
            % (reverse('crm:view_logging', args=(obj.pk,)),_("Edit"))

    return

`

list-log.html

`
$( document ).ready(function() {
function hideSearchInputs(columns) {
for (i=0; i<columns.length; i++) {
if (columns[i]) {
$('.filterhead:eq(' + i + ')' ).show();
} else {
$('.filterhead:eq(' + i + ')' ).hide();
}
}
}
AjaxDatatableViewUtils.initialize_table(
$('#table_id'),
"{% url 'crm:ajax_datatable_logging' %}",
{
// extra_options (example)
processing: false,
autoWidth: false,
full_row_select: false,
scrollX: true,
colReorder: true,
"pagingType": "full_numbers",
responsive: true,

                dom: '<"top"<"row"<"col-md-6"l><"col-md-6">>>Prt<"bottom"<"row"<"col-md-6"i><"col-md-6"pB>>><"clear">',
                    language: {
                        buttons: {
                            "collection": "{% trans "Columns" %}",
                            "copy": "{% trans "Copy" %}",
                            "csv": "{% trans "CSV" %}",
                            "print": "{% trans "Print" %}",
                            copyTitle: '{% trans "Copy to clipboard" %}',
                            copySuccess: {
                                _: '{% trans "Copied %d rows to clipboard" %}',
                                1: '{% trans "Copied one row to clipboard" %}'
                            }
                        },
                        "sEmptyTable":     "{% trans "No data available in table" %}",
                        "sInfo":           "{% trans "Showing _START_ to _END_ of _TOTAL_ entries" %}",
                        "sInfoEmpty":      "{% trans "Showing 0 to 0 of 0 entries" %}",
                        "sInfoFiltered":   "{% trans "(filtered from _MAX_ total entries)" %}",
                        "sInfoPostFix":    "",
                        "sInfoThousands":  ",",
                        "sLengthMenu":     "{% trans "Show _MENU_ entries" %}",
                        "sLoadingRecords": "{% trans "Loading..." %}",
                        "sProcessing":     "{% trans "Processing..." %}",
                        "sSearch":         "{% trans "Search:" %}",
                        "sZeroRecords":    "{% trans "No matching records found" %}",
                        "oPaginate": {
                            "sFirst":    "{% trans "First" %}",
                            "sLast":     "{% trans "Last" %}",
                            "sNext":     "{% trans "Next" %}",
                            "sPrevious": "{% trans "Previous" %}"
                        },
                        "oAria": {
                            "sSortAscending":  "{% trans ": activate to sort column ascending" %}",
                            "sSortDescending": "{% trans ": activate to sort column descending" %}"
                        }
                    },
                    columnDefs: [
                        { responsivePriority: 1, targets: 1,},
                        { responsivePriority: 2, targets: 6,},
                    ],
                    buttons: [
                            { extend: 'collection', className: 'btn-light', buttons: [ 'columnsToggle' ]},
                            {% if perms.crm.can_copy %}
                                { extend: 'copy', className: 'btn-light'},
                            {% endif %}
                            {% if perms.crm.can_csv %}
                                { extend: 'csv', className: 'btn-light' },
                            {% endif %}
                            {% if perms.crm.can_print %}
                                { extend: 'print', className: 'btn-light' },
                            {% endif %}
                    ],
                    drawCallback: function (settings) {
                        $(".view-Log").each(function () {
                          $(this).modalForm({formURL: $(this).data('id')});
                        });
                        $('table select').selectize({
                            sortField: 'text',
                            dropdownParent: "body",
                            allowEmptyOption: false,
                        });
                      $('.dt-buttons').detach().prependTo('#controlPanel')
                      $('table input').addClass("form-control");
                },
            }, {
                // extra_data
                // ...
            },
        );
    });

`

request.is_ajax() is returning false

I have followed the steps and trying to use the plugin in a simple way, but I am getting an assertion error. I have looked views.py line number 381, request.is_ajax()

m2m_foreign_field TypeError when foreign field is null in database

current_value = ', '.join(list_values)

Hi,

Congrats for this project, it is very useful and easy to get results even when we do not know how it works deeply.
I'm just starting using your module but I think I found a bug. So forgive me if I'm mistaking.

I want to display a ManyToMany field value which is not always available in the database (the field can be NULL).
When the DB field is NULL, the code crashes at line 227 with the following error :

File "/usr/local/lib/python3.9/site-packages/ajax_datatable/columns.py", line 227, in get_foreign_value
    current_value = ', '.join(list_values)
TypeError: sequence item 0: expected str instance, NoneType found

It would be interesting to check if list_values is None and put blank or a default value if it is None.

Thank you

Date-range filter

Hello,

I face with a strange behaviour with date range filter. (native one without change or my custom below) : The table return only on row :-(.

fields are set:

    show_date_filters = True
    latest_by = "ren_departure_date"

ren_departure_date is a DateField.

My custom range-filter (flatpickr)

 AjaxDatatableViewUtils.init({
    search_icon_html: '<i class="fa fa-search"></i>',
    language: {
    },
    fn_daterange_widget_initialize: function(table, data) {
        var wrapper = table.closest('.dataTables_wrapper');
        var toolbar = wrapper.find(".toolbar");
        toolbar.html(
            '<div class="daterange" style="float: left; margin-right: 6px;">' +
            'Du: <input type="date" id="date_from" name="date_from" class="date_from" autocomplete="off">' +
            '&nbsp;&nbsp;' +
            'Au: <input type="date" id="date_to" name="date_to" class="date_to" autocomplete="off">' +
            '</div>'
        );
        var date_pickers = toolbar.find('.date_from, .date_to');
        var dt_to ;
        var dt_from ;
        dict_date = {
            dateFormat: "Y-m-d",
            enableTime: false,
            locale: "fr",
            noCalendar: false,
            altFormat: "Y-m-d",
            //defaultDate: today
            onChange: function(selectedDates, dateStr, instance){
                var id = instance._input.getAttribute("id")
                if(id == 'date_from'){
                    dt_from = moment(dateStr).format("YYYY-MM-DD")
                }
                if(id == 'date_to'){
                    dt_to = moment(dateStr).format("YYYY-MM-DD")
                }
                console.log(dt_from, dt_to)
                if(dt_from != undefined && dt_to != undefined){
                    table.data('date_from', dt_from );
                    table.data('date_to', dt_to);
                    table.api().draw();
                }
            }
        }
        date_pickers.flatpickr(dict_date);
    }
});

Thanks for help.

Regards

Hidden columns are possible to filter

I wanted to add filter on hidden column by javascript, but even if I explicitly set the column as:

 {'name': 'is_private', 'visible': False, 'searchable': True},

it is not possible to filter single hidden column by Javascript calls like

javascript:$('#datatable_assets').DataTable().columns(16).search('window').draw();

I fixed this by overriding the searchable value:

    def filter_queryset(self, params, qs):
        # fix hidden column searchable
        params['column_links'][11].searchable = True
        return super().filter_queryset(params, qs)

Showing related objects as columns. Howto?

Hi,
I just found this package: Thanks already for providing this awesome piece of work!
I was wondering if I could use it for my 1-to-many situation though:

I have two Django models, Item and Prop.
Each Item can have up to 2 related Prop objects where Prop.ptype isalways in ['P1', 'P2'].
(In reality, there are many more Prop ptypes).

class Item(models.Model):
    name = models.TextField(blank=True, null=True)

class Prop(models.Model):
    ptype = models.TextField(blank=True, null=True)
    value = models.TextField(blank=True, null=True)
    item = models.ForeignKey(Item, on_delete=models.CASCADE)


# add some content
i1 = Item.objects.create(name='I1')
i2 = Item.objects.create(name='I2')
Prop.objects.create(ptype='P1', value='a', item=i1)
Prop.objects.create(ptype='P2', value='b', item=i1)
Prop.objects.create(ptype='P1', value='x', item=i2)

This situation in the database:

> select * from item;
 id | name |
----+------+-
  1 | I1   |
  2 | I2   |

> select * from prop;
 | ptype | value | item_id |
-+------+-------+---------+-
 | P1   | a     | 1       |
 | P2   | b     | 1       |
 | P1   | x     | 2       |

For all Item objects, I want to display a datatable of the following format:

 Name | P1 | P2 |
------+----+----+-
  I1  | a  | b  |
  I2  | x  | -  |

I can do this by using:

from . models import Item, Prop
class AjaxTest_Callback(AjaxDatatableView):
    model = Item
    initial_order = [['name', 'asc'], ]
    PROP_TYPES = ['P1', 'P2']

    # first column is item id
    column_defs = [
        {'name': 'name', 'visible': True, 'searchable': False},
    ]

    # add prop columns for all properties
    for p in PROP_TYPES:
        column_defs.append({'name': p, 'visible': True, 'searchable': False, 'orderable': True})

    # row by row, add foreign key values to the prop columns
    def customize_row(self, row, obj):
        for p in self.PROP_TYPES:
            entry = obj.prop_set.filter(ptype=p)
            row[p] = entry[0].value if entry else '-'

However, if i want to have the prop columns searchable, this does not work any more since P1/P2/P3 are not part of the Item model.
So I assume my approach is not good after all.
I was looking into defining foreign_field entries in column_defs, but I do not see how this could we used to solve my problem.

Could somebody pls give me a pointer to the right direction?

TIA

Single view for ajax and template render views

Hi @morlandi
Thanks for the perfect project, I found it extremely useful (I just migrated from the pivotal datatables module).
I found it a bit strange, why we need to create two views (along with two URLs) for the single table. One view for the ajax requests and a second one for the template rendering?
I'm a bit edited source code to inherit AjaxDatatableView from the TemplateView instead View and separate ajax requests from the plain GET's to render template on GET requests and it seems working great.
Do you think it is a good idea to submit PR with these changes?

Update: I realized that fact it is ideologically incompatible with the multiple tables on the single page but for general purposes (one table per page), it can be useful. It could be something like SingleAjaxDatatableTemplateView inherited from the AjaxDatatableView or so.

Support for ManyToMany fields

By now, it's very limited; what I'm doing in the example project is:

  • render the M2M column "manually" in customize_row()
  • disable searching on it

Some specific code is needed in build_column_filter() to allow searching,
based on some variation of the following snippet:

tags = Tag.objects.filter(name__in=[search_value, ]), or Tag.objects.filter(name__icontains=search_value) ???
then:
queryset = queryset.filter(tags__in=tags)

For this to work, an indication of the field to be used in the related Model ("name" in the example above)
must the supplied (probably in 'foreign_field' attribute)

Clarification regarding the date-range search function

I tried to implement date range on my app but it does not work, always "No data in table". Does it needs to have specific date format? (YYYY-MM-DD HH:mm:SS)? From what I understand, all you need is to provide column data with date on it (latest_by = "date_time").

Thank You for this project.

If csrftoken cookie is set to HttpOnly, getCookie will not be able to fetch the token

Relates to issue #4 with more information.

image

If a Django project setting sets the CSRF_COOKIE_HTTPONLY = True then GetCookie function will not work. It becomes a problem for us if we want to keep our site secure, including cookies to prevent CSRF. Refer to Django documentation for an alternative: Getting CSRF token value when CSRF_COOKIE_HTTPONLY=True

For more information about HttpOnly: HTTP cookie - Secure and HttpOnly

XSS Vulnerability: HTML Code in field values is being executed by the datatable

If some HTML code is put in a model that is going to be rendered into a datatable with this plugin, any HTML code typed into a field of that instance will be executed and rendered by the AJAX datatable, including <script> tags.
This can lead to severe vulnerabilities.

Proof:
My datatable (all settings are default)
immagine
Adding a model with HTML in its name:
immagine
Result:
immagine

I think this behavior should absolutely be prevented by the default settings.

My initialization:

..........
    <div class="row">
        <div class="col-12">
            <div class="card">
                <div class="card-body">
                    <div class="table-responsive">
                        <table id="datatable_items" class="table align-items-center nowrap">
                        </table>
                    </div>
                </div>
            </div>
        </div>
    </div>
.......
    <script>
        $(document).ready(function () {
            AjaxDatatableViewUtils.initialize_table(
                $('#datatable_items'),
                "{% url "warehouseDatatable" %}",
                {
                    // extra_options (example)
                    processing: true,
                    autoWidth: false,
                    full_row_select: true,
                    scrollX: false,
                    dom: '<"cont-sup"Bfl>rt<"cont-inf"ip>',
                    buttons: [
                        {extend: "copy", text: "Copia", className: "badge badge-primary text-sm"},
                        {extend: "csv", text: "Scarica .CSV", className: "badge badge-primary text-sm"},
                        {extend: "excel", text: "Scarica Excel", className: "badge badge-primary text-sm"},
                        {extend: "print", text: "Stampa", className: "badge badge-primary text-sm"},
                        {extend: "pdfHtml5", text: "Esporta PDF", className: "badge badge-primary text-sm"},
                    ],
                    search_icon_html: '<i class="fa fa-search" style="font-size: 16px"></i>',
                    language: {
                        "decimal": "",
                        "emptyTable": "Nessun dato disponibile",
                        "info": "Visualizzate da _START_ a _END_ di _TOTAL_ righe",
                        "infoEmpty": "",
                        "infoFiltered": "(filtered from _MAX_ total entries)",
                        "infoPostFix": "",
                        "thousands": ",",
                        "lengthMenu": "Visualizza _MENU_ righe per pagina",
                        "loadingRecords": "Caricamento...",
                        "processing": "Caricamento...",
                        "search": "",
                        "zeroRecords": "Nessun record trovato",
                        "paginate": {
                            "first": "Prima",
                            "last": "Ultima",
                            "next": ">>",
                            "previous": "<<"
                        },
                        "aria": {
                            "sortAscending": ": activate to sort column ascending",
                            "sortDescending": ": activate to sort column descending"
                        }
                    }
                },
            );
        });

    </script>

my settings.py:

##### ajax dt

AJAX_DATATABLE_MAX_COLUMNS = 30
AJAX_DATATABLE_TRACE_COLUMNDEFS = False  # enables debug tracing of applied column defs
AJAX_DATATABLE_TRACE_QUERYDICT = False  # enables debug tracing of datatables requests
AJAX_DATATABLE_TRACE_QUERYSET = False  # enables debug tracing of applied query
AJAX_DATATABLE_TEST_FILTERS = False  # trace results for each individual filter, for debugging purposes
AJAX_DATATABLE_DISABLE_QUERYSET_OPTIMIZATION = False  # all queryset optimizations are disabled

my view:

class WarehouseAjaxDatatableView(AjaxDatatableView):
    model = Warehouse
    title = 'I tuoi magazzini'
    initial_order = [["name", "asc"], ]
    length_menu = [[10, 20, 50, 100, -1], [10, 20, 50, 100, 'Tutti']]
    search_values_separator = '+'

    column_defs = [
        {'name': 'azioni', 'title': 'azioni', 'placeholder': True, 'searchable': False, 'orderable': False, },
        {'name': 'name', 'visible': True, },
        {'name': 'items', 'visible': True, },
        {'name': 'Valore totale', 'visible': True, },
        {'name': 'Valore d\'affitto', 'visible': True, },
    ]

    def customize_row(self, row, obj):
        pk = obj.pk
        row['azioni'] = f"""<a href="{reverse(f'detailWarehouseAdvanced', args={pk})}" class="action-item mr-2"><i
                                                     data-toggle="tooltip" title="" data-original-title="Visualizza dettagli" data-placement="right" class="fas fa-external-link-alt"></i></a>
                                                <a href="{reverse(f'updateWarehouse', args={pk})}" class="action-item mr-2"><i
                                                         data-toggle="tooltip" title="" data-original-title="Modifica" data-placement="right" class="fas fa-pencil-alt"></i></a>
                                                         <a href="{reverse(f'deleteWarehouse', args={pk})}" class="action-item text-danger mr-2"><i
                                                         data-toggle="tooltip" title="" data-original-title="Elimina" data-placement="right" class="fas fa-trash"></i></a>
                                                        """
        n_oggetti = obj.itemlist.all().count()
        row['items'] = f""" {n_oggetti} """
        row['Valore totale'] = f"""{str(obj.get_total_value).replace('.', ',')} €"""
        row['Valore d\'affitto'] = f"""{str(obj.get_total_rent).replace('.', ',')} €"""

You shouldn't do this in the class declaration:

You shouldn't do this in the class declaration:

Rec_Count = model.objects.all().count()
if Rec_Count > 500:
    length_menu = [[10, 25, 50, 100], [10, 25, 50, 100]]
else:
    length_menu = [[10, 25, 50, 100, -1], [10, 25, 50, 100, _("All")]]

since RecCount would be evaluated only once when the file is imported.
I would rather override get_length_menu as follows:

def get_length_menu(self):
    Rec_Count = model.objects.all().count()
    if Rec_Count > 500:
        length_menu = [[10, 25, 50, 100], [10, 25, 50, 100]]
    else:
        length_menu = [[10, 25, 50, 100, -1], [10, 25, 50, 100, _("All")]]
    return self.length_menu

Did you encounter other problems?

Originally posted by @morlandi in #11 (comment)

Column search does not work

Hello everyone. Added general search and automatically added column search. So, the general search works, but the column does not.

search1
search2

If you need more information, please tell me what

Pass *args **kwargs to get_initial_queryset()

Hi, I had to monkey patch get like this:

def get(self, request, *args, **kwargs):
    self.milestone = kwargs['milestone']
    return super(TaskListInProgAPI, self).get(request, *args, **kwargs)

so that I could read my url paramters. We could pass these directly to get_initial_queryset()? Is there another way to use these when overriding get_initial_queryset()?

nested choices/autofilter columns

Any suggestions how one would implemented nested choices/autofilter columns? I have a table with a number of columns, which can be filtered by choices, but if you select a filter for one column, then the choices for the other columns would need to be reduced based on the selection for the first column

AssertionError: No exception message supplied

Hello,

I've done everything from readme to build a basic setup but I get the following error:

AssertionError at /denuncia/report_search/
No exception message supplied

Request Method: GET
http://localhost:8000/denuncia/report_search/
3.0.7
AssertionError
/Users/keniobats/.pyenv/versions/bigeye/lib/python3.7/site-packages/ajax_datatable/views.py in dispatch, line 383
/Users/keniobats/.pyenv/versions/bigeye/bin/python
3.7.4
['/Users/keniobats/Dev/bigeye', '/Users/keniobats/.pyenv/versions/3.7.4/lib/python37.zip', '/Users/keniobats/.pyenv/versions/3.7.4/lib/python3.7', '/Users/keniobats/.pyenv/versions/3.7.4/lib/python3.7/lib-dynload', '/Users/keniobats/.pyenv/versions/bigeye/lib/python3.7/site-packages', '/Users/keniobats/.pyenv/versions/bigeye/lib/python3.7/site-packages/odf', '/Users/keniobats/.pyenv/versions/bigeye/lib/python3.7/site-packages/odf', '/Users/keniobats/.pyenv/versions/bigeye/lib/python3.7/site-packages/odf', '/Users/keniobats/.pyenv/versions/bigeye/lib/python3.7/site-packages/odf', '/Users/keniobats/.pyenv/versions/bigeye/lib/python3.7/site-packages/odf', '/Users/keniobats/.pyenv/versions/bigeye/lib/python3.7/site-packages/odf', '/Users/keniobats/.pyenv/versions/bigeye/lib/python3.7/site-packages/odf'

/Users/keniobats/.pyenv/versions/bigeye/lib/python3.7/site-packages/ajax_datatable/views.py in dispatch

                return JsonResponse({
                    'html': self.render_row_details(row_id, request),
                    'parent-row-id': row_id,
                })
            response = super(AjaxDatatableView, self).dispatch(request, *args, **kwargs)
        else:
            assert False
            #response = HttpResponse(self.render_table(request))
        return response
    def get_model_admin(self):
        from django.contrib import admin
        if self.model in admin.site._registry:

Variable Value
class <class 'ajax_datatable.views.AjaxDatatableView'>
args ()
kwargs {}
request <WSGIRequest: GET '/denuncia/report_search/'>
self <whistleblower.datatables_views.RegisterAjaxDatatableView object at 0x10f5c7f10>

I have been trying to get around this with no success so any help will be extremely appreciated!
Django version: 3.0.7
Python version: 3.7.4

AssertError

I tried to use the package bu I got the issue: AssertError

ZeroDivisionError: integer division or modulo by zero on filtered or empty table when selecting page

Buongiorno,

We're having some problems with the server side of this library. The problem occurs when selecting " show all entries" when the table is filtered or there are no records in the table; see below.
image

The minimum reproducible example app:
https://github.com/oshawa-connection/django-ajax-datatable-min-example

The error in full:

ZeroDivisionError at /ajax_datatable/permissions/

integer division or modulo by zero
Request Method: POST
http://127.0.0.1:8000/ajax_datatable/permissions/
3.2.5
ZeroDivisionError
integer division or modulo by zero
C:\Python39\lib\site-packages\ajax_datatable\views.py, line 689, in get_response_dict
C:\Python39\python.exe
3.9.5
['C:\Users\fleminjw\Documents\temp\django\mysite', 'C:\Python39\python39.zip', 'C:\Python39\DLLs', 'C:\Python39\lib', 'C:\Python39', 'C:\Users\fleminjw\AppData\Roaming\Python\Python39\site-packages', 'C:\Python39\lib\site-packages', 'C:\Python39\lib\site-packages\win32', 'C:\Python39\lib\site-packages\win32\lib', 'C:\Python39\lib\site-packages\Pythonwin']
Wed, 28 Jul 2021 07:50:28 +0000

You’re seeing this error because you have DEBUG = True in your Django settings file. Change that to False, and Django will display a standard page generated by the handler for this status code.

We are happy to provide a PR for this issue if necessary.
Django==3.2.5
django-ajax-datatable==4.2.1

Cordiali saluti

filtering multiple values - checkbox like interface

Hi,
Firstly - great project - extremely helpful !

Though, I wonder if there's a good way on top of the choices and text filters to have a predefined filter for multiple selections within a list of choices given to a user- i.e. as in excel "filter" where user can select multiple values.

f.e. where drop down filter would be:

  • A
  • B
  • C
  • D
  • E

Query itself would be myQuery(A | B | C)

You present a similar example function with text based input and "OR" operator, though I wonder if there's a good way to integrate such a drop down selection within the table styling, so it's more natural for the user, instead of having a list of these selections somewhere detached from the table.

I'm afraid this may be a missing feature within datatable, though looking at the examples & your codebase it appears that you have spent a good time with this extension - therefore there's a chance that you have already tackled this problem.

In the end, what I am looking foris a similar menu when you click funnel on this example:
https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/Filtering/jQuery/Light/

Thanks for any suggestions

Reload table with new data

Hi Morlandi,
Thanks for your good library, I was able to initialize my table with some extra data in 'initialize_table' function. now I'm looking for a method to reinitialize or reload the table with a bunch of new extra data. is there any way? I tried a few methods with table.ajax.reload() but they produced confusing errors.

any help is appreciated.

get_template_name : doc out of date or regression ?

Hello,

Did not find anymore get_template_name in code, overriding have no effect.
And template_name value too.

Did you remove this feature (and keep it in doc ?) or a badly regression ?

Thanks for answer (and for this great works :-) ).

Regards

DateFields are not searchable

Hello,
I noticed that a table containing a datefield in its columns won't enable the search function on that column.
if you type text into the column's searchbox, nothing will happen.

This behavior can be observer even in the example datatable provided by the readme:
http://django-ajax-datatable-demo.brainstorm.it/tracks/
The problem happens only when using the search box in the column, and not with the generic searchbox that search on all the fields, nor in the custom filter added there.

immagine

issue with CSRF token

i am using Django with the
but i am always getting this error Forbidden (CSRF token missing or incorrect.)

i have tried to include the token within the AjaxDatatableViewUtils.initialize_table but not failed to do so
can you please advice
i am using the below script

<script language="javascript">

    $( document ).ready(function() {
    var crf_token = $('[name="csrfmiddlewaretoken"]').attr('value');
        AjaxDatatableViewUtils.initialize_table(
            $('#datatable-customer'),
            "{% url 'crm:ajax_datatable_customer' %}",
            {
                // extra_options (example)
                processing: false,
                autoWidth: false,
                full_row_select: false,
                scrollX: false
            }, {
                // extra_data
                // ...
            },
        );
    });

</script>

filter display issue on small screens

Hi again,

First i would like to thank you for this very useful and fast library.

one more issue i would like to report:

When i set the responsive option to true under the table options, on small devices some fields will be shown under the plus sign which is normal but the filter for those fields remain appearing on the table header which i think is better to hide

Thanks again

Border : 1px added in div in when 'searchable': False

Hi,

I have a column_defs with first column a button to Edit.
This column is not searchable or order-able. So on top there should not be such elements

However there is a styling added to the div that will reassemble like this s an searchable filed but in fact this is a border.

class CustomerAjaxDatatableView(AjaxDatatableView):

    model = Customer
    title = 'Customer'
    initial_order = [["name", "asc"], ]
    length_menu = [[10, 20, 50, 100, -1], [10, 20, 50, 100, 'all']]
    search_values_separator = '+'

    column_defs = [
        {'name': 'edit', 'title': '', 'searchable': False, 'orderable': False, },
        {'name': 'name', 'visible': True, },
        {'name': 'address1', 'visible': True, },
        {'name': 'address2', 'visible': True, },
        {'name': 'postciode', 'visible': True, },
        {'name': 'city', 'visible': True, },
        {'name': 'country', 'visible': True, },
    ]

Selection_094

Support for Django 4.0

Currently, this happens when using it in a project with Django 4.0a1

  File "/Users/fraimondo/anaconda3/envs/microbiom/lib/python3.9/site-packages/ajax_datatable/columns.py", line 2, in <module>
    from django.utils.translation import ugettext_lazy as _
ImportError: cannot import name 'ugettext_lazy' from 'django.utils.translation' (/Users/fraimondo/anaconda3/envs/microbiom/lib/python3.9/site-packages/django/utils/translation/__init__.py)

This is because these functions where deprecated in Django 3.0:

https://docs.djangoproject.com/en/dev/releases/3.0/#deprecated-features-3-0

I guess it's just a matter of removing the u

Disable row-details

Hi

Thanks for the great tool.

I was wondering if there is a way to disable the row-details. my datatable contains edit button on each row. I am having a problem with my edit button if clicked, the row-details shows and then I am taken to the edit page. Is there a way to disable the row-details so it doesn't show when the edit button is clicked?

Adding raw SQL support?

Would it be possible to add support for RawQuerySet data type if we use raw SQL instead of Django's OMS? For example Manager.objects.raw_as_qs("select * from mytable")? My project has complex SQL statements for report generation and would like to be able to use raw SQL.

For example, code snippet below:

from django.db import connection
from django.db.models import Manager, Model
from ajax_datatable.views import AjaxDatatableView

class MyManager(Manager):
    def raw_as_qs(self, raw_query, params=()):
        """Execute a raw query and return a QuerySet.  The first column in the
        result set must be the id field for the model.
        :type raw_query: str | unicode
        :type params: tuple[T] | dict[str | unicode, T]
        :rtype: django.db.models.query.QuerySet
        """
        cursor = connection.cursor()
        try:
            cursor.execute(raw_query, params)
            return self.filter(id__in=(x[0] for x in cursor))
        finally:
            cursor.close()


class RawModel(Model):
    objects = MyManager()

And for the ajax call:

class AjaxJournalLog(AjaxDatatableView):
    model = RawModel
    title = "Journal Log"
    initial_order = [["game_start", "asc"]]
    length_menu = [[10, 20, 50, 100], [10, 20, 50, 100]]
    search_values_separator = "+"

    column_defs = [
        AjaxDatatableView.render_row_tools_column_def(),
        {"name": "id", "visible": False},
        {"name": "file", "visible": True},
        {"name": "game_start", "visible": True},
        {"name": "game_end", "visible": True},
        {"name": "progress_code", "visible": True},
        {"name": "rows_processed", "visible": True},
        {"name": "parser_time", "visible": True},
    ]

    def get_initial_queryset(self, request=None):
        return RawModel.objects.raw_as_qs("select * from journal_log")

Here's the traceback:

Traceback (most recent call last):
  File "/venv/lib/python3.8/site-packages/asgiref/sync.py", line 458, in thread_handler
    raise exc_info[1]
  File "/venv/lib/python3.8/site-packages/django/core/handlers/exception.py", line 38, in inner
    response = await get_response(request)
  File "/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 233, in _get_response_async
    response = await wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/venv/lib/python3.8/site-packages/asgiref/sync.py", line 423, in __call__
    ret = await asyncio.wait_for(future, timeout=None)
  File "/Users/josh/opt/anaconda3/lib/python3.8/asyncio/tasks.py", line 455, in wait_for
    return await fut
  File "/venv/lib/python3.8/site-packages/asgiref/current_thread_executor.py", line 22, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/venv/lib/python3.8/site-packages/asgiref/sync.py", line 462, in thread_handler
    return func(*args, **kwargs)
  File "/venv/lib/python3.8/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/venv/lib/python3.8/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/venv/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/venv/lib/python3.8/site-packages/ajax_datatable/views.py", line 405, in dispatch
    response = super(AjaxDatatableView, self).dispatch(request, *args, **kwargs)
  File "/venv/lib/python3.8/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/venv/lib/python3.8/site-packages/ajax_datatable/views.py", line 491, in post
    return self.get(request, *args, **kwargs)
  File "/venv/lib/python3.8/site-packages/ajax_datatable/views.py", line 520, in get
    qs = self.prepare_queryset(params, qs)
  File "/venv/lib/python3.8/site-packages/ajax_datatable/views.py", line 770, in prepare_queryset
    qs = self.sort_queryset(params, qs)
  File "/venv/lib/python3.8/site-packages/ajax_datatable/views.py", line 788, in sort_queryset
    qs = qs.order_by(
  File "/venv/lib/python3.8/site-packages/django/db/models/query.py", line 1149, in order_by
    obj.query.add_ordering(*field_names)
  File "/venv/lib/python3.8/site-packages/django/db/models/sql/query.py", line 1991, in add_ordering
    self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)
  File "/venv/lib/python3.8/site-packages/django/db/models/sql/query.py", line 1537, in names_to_path
    raise FieldError("Cannot resolve keyword '%s' into field. "
django.core.exceptions.FieldError: Cannot resolve keyword 'game_start' into field. Choices are: id

I don't think it would work well when converting RawQuerySet to QuerySet, it just introduces new problems. I'm thinking we may have to create a separate class to deal with raw SQL. I like your package and would love to use it with raw SQL as well.

I could make a PR to support that if you could direct me to the best starting point.

Cheers,
Josh

Allow multiple sorting for each column

Use case: you have a list of addresses like:

street number
aaa 3
aaa 10
aaa 10/A
aaa 2/B

Suppose that an hidden "normalized_number" has been added to support "natural sorting":

street number normalized_number
aaa 3 00000003
aaa 10 00000010
aaa 10/A 00000010/A
aaa 2/B 00000002/B

In this case, you wish to show (street + number) but sort on (street, normalized_number).

This might be solved with the optional "sort_field" column attribute, i.e.:

'sort_field': 'street,normalized_number'

then refactoring sort_queryset() accordingly

Filter a Placeholder column

Hello @morlandi,

Firstly bravo for this project, I am using it and it is so helpful.

I have an issue with filtering a Placeholder column.
This column is a concatenation of its value and 3 other columns.

My column is defined as such
{'name': 'column_1', 'placeholder': True, 'visible': True, 'searchable': True, 'orderable': False,},
And then
def customize_row(self, row, obj):
row['column_1'] = "

" + column_1+ column_2+ column_3+ column_4+ "
"

The filter only works for values issued from column_1, so I guess it is filtering before the row is assigned with my concatenation.

Is there a way I can filter a Placeholder column using its final value ?

Thanks a lot

ps : column_1 / 2/ / 3/ 4 being the different columns of my model

filter field

Hi,
is it possible to make the filter select field when the 'choices': True to be searchable as well this will be useful for the field has a lot of values

Thanks

How to ise Tailwindcss classes

I ve tried to apply tailwindcss classes using tailwind's customised stylesheet. The classes get picked up in the markup but are not applied. They are being overridden by other styles. Perhaps datatables stylesheet. I ve tried moving the tailwind inclusion in the base template to after the datatable css but it does not work. Can you help me please?

Showing recordsTotal and recordsFiltered as same value

As i tried to filter records on table, it shows only filtered record count and not actual records count before filtering -
http://django-ajax-datatable-demo.brainstorm.it/tracks/

It should as excepted like actual jquery datatable plugin - https://datatables.net/
Showing 1 to N of N entries (filtered from 57 total entries)

"recordsTotal": paginator.count,

"recordsFiltered": paginator.count,

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.