Package: django-ajax-datatable
1django-ajax-datatabledjango-ajax-datatableis a Django app (previously namedmorlandi/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 fromdjango-datatables-viewtodjango-ajax-datatableto avoid a conflict on PyPIUnfortunately 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 foreverFeatures:PaginationColumn orderingGlobal generic searchGlobal date-range search over “get_latest_by” columnColumn specific filteringForeign key fields can be used, using the “model1__model2__field” notationCustomizable rendering of table rowsand more …Inspired from:https://github.com/monnierj/django-datatables-server-sideContents1django-ajax-datatable1.1Demo site and examples1.2Installation1.3Pre-requisites1.4Basic AjaxDatatableView1.5AjaxDatatableView Class attributes1.6column_defs customizations1.7Provide “extra data” to narrow down the initial queryset1.8Add a sidebar with custom filters1.9Automatic addition of table row ID1.10Sorting columns1.11Filtering single columns1.12Filtering multiple values1.13Computed (placeholder) columns1.14Clipping results1.15Receiving table events1.16Overridable AjaxDatatableView methods1.16.1get_initial_queryset()1.16.2get_foreign_queryset()1.16.3customize_row()1.16.4render_row_details()1.16.5footer_message()1.16.6toolbar_message()1.16.7render_clip_value_as_html()2Queryset optimization2.1A real use case3App settings4More details4.1Add row tools as first column4.2row details customization4.3Filter by global date range4.4Debugging5Generic tables (advanced topic)6Javascript Code Snippets6.1Workaround: Adjust the column widths of all visible tables6.2Redraw all tables6.3Redraw table holding the current paging position6.4Redraw a single table row6.5change DataTables’ error reporting mechanism7JS Utilities7.1Internationalization8Application examples8.1Adding a button for editing8.2Customize row details by rendering prettified json fields8.3Change row color based on row content8.4Modify table content on the fly (via ajax)9Possible future improvements9.1Check these extensions:9.2Column rendering9.3Column filtering9.3.1add a specific widget for dates9.3.2support for optional autocompletion widget9.3.3add a specific widget for boolean fields10History10.1v4.4.510.2v4.4.410.3v4.4.310.4v4.4.210.5v4.4.110.6v4.4.010.7v4.3.110.8v4.3.010.9v4.2.110.10v4.2.010.11v4.1.710.12v4.1.610.13v4.1.510.14v4.1.410.15v4.1.310.16v4.1.210.17v4.1.110.18v4.1.010.19v4.0.810.20v4.0.710.21v4.0.610.22v4.0.510.23v4.0.410.24v4.0.310.25v4.0.210.26v4.0.110.27v4.0.010.28v3.2.310.29v3.2.210.30v3.2.110.31v3.2.010.32v3.1.410.33v3.1.310.34v3.1.210.35v3.1.110.36v3.1.010.37v3.0.410.38v3.0.310.39v3.0.210.40v3.0.110.41v3.0.010.42v2.3.510.43v2.3.410.44v.2.3.310.45v2.3.210.46v2.3.110.47v2.3.010.48v2.2.910.49v2.2.810.50v2.2.710.51v2.2.610.52v2.2.510.53v2.2.410.54v2.2.310.55v2.2.210.56v2.2.110.57v2.2.010.58v2.1.310.59v2.1.210.60v2.1.110.61v2.1.010.62v2.0.610.63v2.0.510.64v2.0.410.65v2.0.010.66v1.2.410.67v1.2.310.68v1.2.210.69v1.2.110.70v1.2.010.71v1.0.110.72v1.0.010.73v0.0.21.1Demo site and examplesA very minimal working Django project which usesdjango-ajax-datatablecan be found in the folderexample_minimal.A more realistic solution, with a frontend based on Bootstrap4, can be found inexample,
and is published as a demo site at the address:http://django-ajax-datatable-demo.brainstorm.it/.1.2InstallationInstall the package by running:pipinstalldjango-ajax-datatableor:pipinstallgit+https://github.com/morlandi/django-ajax-datatable@v4.0.0then add ‘ajax_datatable’ to your INSTALLED_APPS:INSTALLED_APPS=[...'ajax_datatable',]Optional dependencies (for better debug tracing):sqlparsetermcolorpygments1.3Pre-requisitesYour base template should include what required bydatatables.net, plus:/static/ajax_datatable/css/style.css/static/ajax_datatable/js/utils.jsExample (plain jQuery from CDN):{% block extrastyle %}<linkhref="{% static 'ajax_datatable/css/style.css' %}"rel="stylesheet"/><linkhref="//cdn.datatables.net/1.10.22/css/jquery.dataTables.min.css"/>{% endblock extrastyle %}

{% block extrajs %}<scriptsrc="https://code.jquery.com/jquery-3.5.1.min.js"></script><scripttype="text/javascript"src="{% static 'ajax_datatable/js/utils.js' %}"></script><scriptsrc="//cdn.datatables.net/1.10.22/js/jquery.dataTables.min.js"></script>{% endcompress %}Example (with Bootstrap4 support):{% block extrastyle %}<linkhref="{% static 'ajax_datatable/css/style.css' %}"rel="stylesheet"/><!-- link rel='stylesheet' href="{% static 'datatables.net-bs/css/dataTables.bootstrap.min.css' %}" --><linkrel='stylesheet'href="{% static 'datatables.net-bs4/css/dataTables.bootstrap4.min.css' %}"><linkrel='stylesheet'href="{% static 'datatables.net-buttons-bs/css/buttons.bootstrap.min.css' %}">{% endblock extrastyle %}

{% block extrajs %}<scripttype="text/javascript"src="{% static 'ajax_datatable/js/utils.js' %}"></script><scriptsrc="{% static 'datatables.net/js/jquery.dataTables.min.js' %}"></script><!-- script src="{% static 'datatables.net-bs/js/dataTables.bootstrap.min.js' %}"></script --><scriptsrc="{% static 'datatables.net-bs4/js/dataTables.bootstrap4.min.js' %}"></script><scriptsrc="{% static 'datatables.net-buttons/js/dataTables.buttons.min.js' %}"></script><scriptsrc="{% static 'datatables.net-buttons/js/buttons.print.min.js' %}"></script><scriptsrc="{% static 'datatables.net-buttons/js/buttons.html5.min.js' %}"></script><scriptsrc="{% static 'datatables.net-buttons-bs/js/buttons.bootstrap.min.js' %}"></script><scriptsrc="{% static 'jszip/dist/jszip.min.js' %}"></script><scriptsrc="{% static 'pdfmake/build/pdfmake.min.js' %}"></script><scriptsrc="{% static 'pdfmake/build/vfs_fonts.js' %}"></script>{% endcompress %}1.4Basic AjaxDatatableViewTo provide server-side rendering of a Django Model, you will need:an ordinary view which will render an HTML page containing:an empty HTML <table> elementa javascript code which links this HTML table to the (AjaxDatatableView-derived) second viewa 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 columnsrespond to datatables.net requests, as a consequence of the user interaction with the tableExample:We start by rendering an HTML page from this template:filepermissions_list.html<tableid="datatable_permissions"></table>or:<divclass="table-responsive"><tableid="datatable_permissions"width="100%"class="table table-striped table-bordered dt-responsive compact nowrap"></table></div>...<scriptlanguage="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:fileurls.pyfromdjango.urlsimportpathfrom.importajax_datatable_viewsapp_name='frontend'urlpatterns=[...path('ajax_datatable/permissions/',ajax_datatable_views.PermissionAjaxDatatableView.as_view(),name="ajax_datatable_permissions"),]The javascript helperAjaxDatatableViewUtils.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 theaction=initializeparameter) 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:fileajax_datatable_views.pyfromajax_datatable.viewsimportAjaxDatatableViewfromdjango.contrib.auth.modelsimportPermissionclassPermissionAjaxDatatableView(AjaxDatatableView):model=Permissiontitle='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:You can use common CSS style to customize the final rendering:AjaxDatatableViewUtils.initialize_table() parameters are:elementtable elementurlaction (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 queryset1.5AjaxDatatableView Class attributesRequired:modelcolumn_defsOptional: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 = Noneshow_date_filters = Noneshow_column_filters = Nonedisable_queryset_optimization = Falsedisable_queryset_optimization_only = Falsedisable_queryset_optimization_select_related = Falsedisable_queryset_optimization_prefetch_related = Falsetable_row_id_prefix = ‘row-’table_row_id_fieldname = ‘id’render_row_details_template_name = “render_row_details.html”search_values_separator = ‘’sort_field: Noneor override the following methods to provide attribute values at run-time,
based on request:defget_column_defs(self):returnself.column_defsdefget_initial_order(self):returnself.initial_orderdefget_length_menu(self):returnself.length_menudefget_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.
    """returnself.latest_bydefget_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
    """returnself.show_date_filtersdefget_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.
    """returnself.show_column_filtersdefget_table_row_id(self,request,obj):"""
    Provides a specific ID for the table row; default: "row-ID"
    Override to customize as required.
    """result=''ifself.table_row_id_fieldname:try:result=self.table_row_id_prefix+str(getattr(obj,self.table_row_id_fieldname))except:result=''returnresult1.6column_defs customizationsExample: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)
ornamewill be usedwidth: for this to be effective, you need to addtable-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)1.7Provide “extra data” to narrow down the initial querysetSometimes 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_dataclient_id:'{{client.id}}',date_from:function(){returndate_input_to_isoformat('#date_from');},date_to:function(){returndate_input_to_isoformat('#date_to');}});then:classSampleAjaxDatatableView(AjaxDatatableView):...defget_initial_queryset(self,request=None):ifnotrequest.user.is_authenticated:raisePermissionDenied# We accept either GET or POSTifnotgetattr(request,'REQUEST',None):request.REQUEST=request.GETifrequest.method=='GET'elserequest.POSTqueryset=self.model.objects.all()if'client_id'inrequest.REQUEST:client_id=int(request.REQUEST.get('client_id'))queryset=queryset.filter(client_id=client_id)returnqueryset1.8Add a sidebar with custom filtersSometimes 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/1.9Automatic addition of table row IDStarting from v3.2.0, each table row is characterized with a specific ID on each row
(tipically, the primary key value from the queryset)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 fortable_row_id_fieldnameand/ortable_row_id_prefix, oroverridingdef get_table_row_id(self, request, obj)1.10Sorting columnsSorting is managed the by the overridable methodsort_queryset(), and fully
delegated to the database for better performances.For eachorderablecolumn, the columnnamewill be used, unless asort_fieldhas been specified; in which case, the latter will be used instead.1.11Filtering single columnsDatatableView.show_column_filters(orDatatableView.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 searchableTrue: always showFalse: always hideBy default, a column filter for a searchable column is rendered as atext inputbox;
you can instead provide aselectbox using the following attributes:choicesNone (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 tableor, forBooleanFieldcolumns, provide (None)/Yes/No choice sequencecalculated columns with attribute ‘boolean’=True are treated as BooleanFields((key1, value1), (key2, values), …) : use supplied sequence of choicesautofilterdefault = Falsewhen 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:initialSearchValueoptional initial value for column filterNote thatinitialSearchValuecan be a value or a callable object.
If callable it will be called every time a new object is created.For example:classMyAjaxDatatableView(AjaxDatatableView):deftoday():returndatetime.datetime.now().date()...column_defs=[...{'name':'created','choices':True,'autofilter':True,'initialSearchValue':today},...]1.12Filtering multiple valuesSearching 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.1.13Computed (placeholder) columnsYou can insert placeholder columns in the table, and feed their content with
arbitrary HTML.Example:@method_decorator(login_required,name='dispatch')classRegisterAjaxDatatableView(AjaxDatatableView):model=Registertitle=_('Registers')column_defs=[{'name':'id','visible':False,},{'name':'created',},{'name':'dow','title':'Day of week','placeholder':True,'searchable':False,'orderable':False,'className':'highlighted',},{...}]defcustomize_row(self,row,obj):days=['monday','tuesday','wednesday','thyrsday','friday','saturday','sunday']ifobj.createdisnotNone:row['dow']='<b>%s</b>'%days[obj.created.weekday()]else:row['dow']=''return1.14Clipping resultsSometimes 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 themax_lengthcolumn_spec attribute.Results will be clipped in both the column cells and in the column filter.Clipped results are rendered as html text as follows:defrender_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;'ifis_clippedelse'')You can customise the rendering by overridingrender_clip_value_as_html()1.15Receiving table eventsThe 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:<divclass="table-responsive"><tableid="datatable"width="100%"class="table table-striped table-bordered dataTables-log"></table></div><scriptlanguage="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 tableAjaxDatatableViewUtils.initialize_table($('#datatable'),"{% url 'frontend:object-datatable' model|app_label model|model_name %}",extra_option={},extra_data={});});</script>1.16Overridable AjaxDatatableView methods1.16.1get_initial_queryset()Provides the queryset to work with; defaults toself.model.objects.all()Example:defget_initial_queryset(self,request=None):ifnotrequest.user.view_all_clients:queryset=request.user.related_clients.all()else:queryset=super().get_initial_queryset(request)returnqueryset1.16.2get_foreign_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:defget_foreign_queryset(self,request,field):queryset=field.model.objects.all()returnquerysetYou can override it for further reducing the resulting list.1.16.3customize_row()Called every time a new data row is required by the client, to let you further
customize cell contentExample:defcustomize_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)ifobj.recipeisnotNone:row['recipe']=obj.recipe.display_as_tile()+' '+str(obj.recipe)return1.16.4render_row_details()Renders an HTML fragment to show table row content in “detailed view” fashion,
as previously explained later in theAdd row tools as first columnsection.
Having“pk”in your column_defs list is needed to have the script get the object to render.See also:row details customizationExample:defrender_row_details(self,pk,request=None):client=self.model.objects.get(pk=pk)...returnrender_to_string('frontend/pages/includes/client_row_details.html',{'client':client,...})OR you can have your own callback called instead (thanks toPetrDlouhy):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
        ...
    },
);1.16.5footer_message()You can annotate the table footer with a custom message by overridding the
following View method.deffooter_message(self,qs,params):"""
    Overriden to append a message to the bottom of the table
    """returnNoneExample:deffooter_message(self,qs,params):return'Selected rows:%d'%qs.count()<style>.dataTables_wrapper.dataTables_extraFooter{border:1pxsolidblue;color:blue;padding:8px;margin-top:8px;text-align:center;}</style>1.16.6toolbar_message()Same as footer_message() but appends message to toolbar:deffooter_message(self,qs,params):return'Selected rows:%d'%qs.count()1.16.7render_clip_value_as_html()Renders clipped results as html span tag, providing the non-clipped value as title:defrender_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;'ifis_clippedelse'')Override to customise the rendering of clipped cells.2Queryset optimizationAs 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:by usingonlyto limit the number of columns in the result setby usingselect_relatedto minimize the number of queries involvedThe parameters passed to only() and select_related() are inferred fromcolumn_defs.Should this cause any problem, you can disable queryset optimization in two ways:globally: by activating theAJAX_DATATABLE_DISABLE_QUERYSET_OPTIMIZATIONsettingper table: by setting to True the value of thedisable_queryset_optimizationattributeAlternatively, you can selectively disable theonlyorselect_relatedoptimization with the following flags:self.disable_queryset_optimization_onlyself.disable_queryset_optimization_select_related2.1A real use casePlain 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 duplicatesWith 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 duplicatesWith 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 duplicates3App settingsAJAX_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 table4More details4.1Add row tools as first columnYou can insertAjaxDatatableView.render_row_tools_column_def()as the first element
incolumn_defsto obtain some tools at the beginning of each table row.Iffull_row_select=trueis specified as extra-option during table initialization,
row details can be toggled by clicking anywhere in the row.datatables_views.pyfromdjango.contrib.auth.decoratorsimportlogin_requiredfromdjango.utils.decoratorsimportmethod_decoratorfromajax_datatable.viewsimportAjaxDatatableViewfrombackend.modelsimportRegister@method_decorator(login_required,name='dispatch')classRegisterAjaxDatatableView(AjaxDatatableView):model=Registertitle='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 templateajax_datatable/row_tools.html,
which can be overridden.Row details are automatically collected via Ajax by calling again the views
with a specific?action=detailsparameters, 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:4.2row details customizationThe 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’sadmin/change_form.htmldoes.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:<trclass="details"><tdclass="details"><divclass="row-details-wrapper"data-parent-row-id="PARENT-ROW-ID">...4.3Filter by global date rangeWhen alatest_bycolumn has been specified andshow_date_filteris active,
a global date range filtering widget is provided, based onjquery-ui.datepicker: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 customfn_daterange_widget_initialize()callback at Module’s initialization, as in the following example, where we
usebootstrap.datepicker:AjaxDatatableViewUtils.init({
    search_icon_html: '<iclass="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(
            '<divclass="daterange"style="float: left; margin-right: 6px;">' +
            '{% trans "From" %}:<inputtype="text"class="date_from"autocomplete="off">' +
            '&nbsp;&nbsp;' +
            '{% trans "To" %}:<inputtype="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();
        });
    }
});4.4DebuggingIn case of errors, Datatables.net shows an alert popup: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 = TrueThe resulting query (before pagination) can be traced as well with:AJAX_DATATABLE_TRACE_QUERYSET = TrueDebugging traces for date range filtering, column filtering or global filtering can be displayed
by activating this setting:AJAX_DATATABLE_TEST_FILTERS5Generic tables (advanced topic)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:filefrontend/datatables_views.py:@login_requireddefobject_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
    """returnrender(request,template_name,{'model':model,})In the urlconf, link to specific models as in the example below:filefrontend/urls.py:path('channel/',datatables_views.object_list_view,{'model':backend.models.Channel,},name="channel-list"),The template uses themodelreceived in the context to display appropriateverbose_nameandverbose_name_pluralattributes, and to extractapp_labelandmodel_nameas needed; unfortunately, we also had to supply some very basic helper templatetags,
as the_metaattribute of the model is not directly visible in this context.{% extends 'frontend/base.html' %}
{% load static datatables_view_tags i18n %}

{% block breadcrumbs %}<li><ahref="{% url 'frontend:index' %}">{% trans 'Home' %}</a></li><liclass="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' %}<ahref="#">{% trans 'Add ...' %}</a>{% endifhasperm %}</div><divclass="table-responsive"><tableid="datatable"width="100%"class="table table-striped table-bordered table-hover dataTables-example"></table></div>{% ifhasperm model 'add' %}<ahref="#">{% trans 'Add ...' %}</a>{% endifhasperm %}

    {% endif %}

{% endblock content %}


{% block extrajs %}<scriptlanguage="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 %}fromurls.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:filefrontend/datatables_views.py:importinspectdefobject_datatable_view(request,app_label,model_name):# List all AjaxDatatableView in this moduledatatable_views=[klassforname,klassininspect.getmembers(sys.modules[__name__])ifinspect.isclass(klass)andissubclass(klass,AjaxDatatableView)]# Scan AjaxDatatableView until we find the right onefordatatable_viewindatatable_views:model=datatable_view.modelif(modelisnotNoneand(model._meta.app_label,model._meta.model_name)==(app_label,model_name)):view=datatable_viewbreakreturnview.as_view()(request)which for this example happens to be:@method_decorator(login_required,name='dispatch')classChannelAjaxDatatableView(BaseAjaxDatatableView):model=Channeltitle='Channels'column_defs=[AjaxDatatableView.render_row_tools_column_def(),{'name':'id','visible':False,},{'name':'description',},{'name':'code',}]6Javascript Code Snippets6.1Workaround: Adjust the column widths of all visible tablessetTimeout(function(){AjaxDatatableViewUtils.adjust_table_columns();},200);or maybe better:vartable=element.DataTable({..."initComplete":function(settings){setTimeout(function(){AjaxDatatableViewUtils.adjust_table_columns();},200);}where:functionadjust_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();}6.2Redraw all tables$.fn.dataTable.tables({api:true}).draw();6.3Redraw table holding the current paging positiontable=$(element).closest('table.dataTable');$.ajax({type:'GET',url:...}).done(function(data,textStatus,jqXHR){table.DataTable().ajax.reload(null,false);});6.4Redraw a single table rowTODO: THIS DOESN’T SEEM TO WORK PROPERLY 😭table.DataTable().row(tr).invalidate().draw();Example:vartable=$(element).closest('table.dataTable');vartable_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 rowvartr=table.find('#'+table_row_id);varrow=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 fingerssetTimeout(function(){table.find('#'+table_row_id).addClass('updated');},200);setTimeout(function(){table.find('#'+table_row_id).addClass('updated');},1000);});6.5change DataTables’ error reporting mechanism// change DataTables' error reporting mechanism to throw a Javascript// error to the browser's console, rather than alerting it.$.fn.dataTable.ext.errMode='throw';7JS UtilitiesAjaxDatatableViewUtils.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)7.1InternationalizationYou 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:<scriptlanguage="javascript">$(document).ready(function(){AjaxDatatableViewUtils.init({...});});</script>8Application examples8.1Adding a button for editingSince 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”:classArtistAjaxDatatableView(AjaxDatatableView):...column_defs=[...{'name':'edit','title':'Edit','placeholder':True,'searchable':False,'orderable':False,},...]defcustomize_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>
        """...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.8.2Customize row details by rendering prettified json fieldsimportjsonfieldfromajax_datatable.viewsimportAjaxDatatableViewfrom.utilsimportjson_prettifyclassMyAjaxDatatableView(AjaxDatatableView):...defrender_row_details(self,pk,request=None):obj=self.model.objects.get(pk=pk)fields=[fforfinself.model._meta.get_fields()iff.concrete]html='<table class="row-details">'forfieldinfields:value=getattr(obj,field.name)ifisinstance(field,jsonfield.JSONField):value=json_prettify(value)html+='<tr><td>%s</td><td>%s</td></tr>'%(field.name,value)html+='</table>'returnhtmlwhere:importjsonfrompygmentsimporthighlightfrompygments.lexersimportJsonLexerfrompygments.formattersimportHtmlFormatterfromdjango.utils.safestringimportmark_safedefjson_prettify_styles():"""
    Used to generate Pygment styles (to be included in a .CSS file) as follows:
        print(json_prettify_styles())
    """formatter=HtmlFormatter(style='colorful')returnformatter.get_style_defs()defjson_prettify(json_data):"""
    Adapted from:
    https://www.pydanny.com/pretty-formatting-json-django-admin.html
    """# Get the Pygments formatterformatter=HtmlFormatter(style='colorful')# Highlight the datajson_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 outputreturnmark_safe(style+json_text)8.3Change row color based on row contentFirst, we mark the relevant info with a specific CSS class, so we can search
for it latercolumn_defs = [
    ...
    }, {
        'name': 'error_counter',
        'title': 'errors',
        'className': 'error_counter',
    }, {
    ...
]Have a callback called after each table redrawvartable=element.DataTable({...});table.on('draw.dt',function(event){onTableDraw(event);});then change the rendered table as neededvaronTableDraw=function(event){varhtml_table=$(event.target);html_table.find('tr').each(function(index,item){try{varrow=$(item);text=row.children('td.error_counter').first().text();varerror_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.8.4Modify table content on the fly (via ajax)Row details customization:defrender_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>"forchoiceinBaseTask.ALARM_STATUS_CHOICES:#Lostatocorrentelovisualizziamoingrassettoifchoice[0]==obj.alarm:html+='<b>%s</b>&nbsp;'%(choice[1])else:#Senon"unalarmed",mostriamoilinkpercambiarelostato#(tuttitranne"unalarmed")ifobj.alarm!=BaseTask.ALARM_STATUS_UNALARMEDandchoice[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:<scriptlanguage="javascript">functionset_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_requireddefset_alarm(request,table_id,task_id,new_status):#Retrievemodelfromtableid#Exampletable_id:#'datatable_walletreceivetransactionstask'#'datatable_walletcheckstatustask_summary'model_name=table_id.split('_')[1]model=apps.get_model('tasks',model_name)#Retrievetasktask=get_object_by_uuid_or_404(model,task_id)#Setalarmvaluetask.set_alarm(request,new_status)returnHttpResponse('ok')9Possible future improvements9.1Check these extensions:Table row selectionExport table data to excel of pdfResponsive table9.2Column renderingspecific rendering for boolean columns9.3Column filtering9.3.1add a specific widget for datesCurrently, an exact match is applied; a date-range selection would be better;
references:https://datatables.net/plug-ins/filtering/row-based/range_dateshttps://datatables.net/extensions/select/https://github.com/RobinDev/jquery.dataTables.columnFilter.js?files=19.3.2support for optional autocompletion widgethttps://github.com/yourlabs/django-autocomplete-lighthttps://github.com/crucialfelix/django-ajax-selects9.3.3add a specific widget for boolean fieldsA checkbox or a select10History10.1v4.4.5Remove pytz since it’s deprecated in Django 4.0 and will be removed in Django 5.010.2v4.4.4few changes to allow the initialization of a DatatableView offline (for example, to export a filtered queryset from a background process)10.3v4.4.3assign ‘__iexact’, instead of ‘__icontains’, as default ‘lookup_field’ value for columns with choicesadded toolbar_message(): same as footer_message(), but appends message to toolbar10.4v4.4.2fix: searching on a date/datetime field: when the value entered is not a valid date, we clear the table content to give a feedback to the user10.5v4.4.1fix: when STRIP_HTML_TAGS is active, None was rendered as ‘None’ instead of ‘’10.6v4.4.0Prepare for Django 4.0Support choice lookup for m2m_foreign_field (many thanks to Martin Schwier and Etienne Pouliot)Fix Multiple search values when you set search_values_separator = ‘+’ and try to search for term with + in it (many thanks to Petr Dlouhý)POSSIBLE INCOMPATIBLE CHANGE: for security reason, HTML tags are now stripped by default in the rendered table; you can disable this setting AJAX_DATATABLE_STRIP_HTML_TAGS = False (thus restoring the previous behaviour); many thanks to Mich “Mike3285”10.7v4.3.1Add custom lookup field; thanks to Javier Clavero Álvarez <jclaveroalvarez@gmail.com>10.8v4.3.0Add support for ManyToMany field (many thanks to Etienne Pouliot); REQUIRES PYTHON 3.6POSSIBLE INCOMPATIBLE CHANGE: PYTHON >= 3.6 REQUIREDFix #35: prevent ZeroDivisionError10.9v4.2.1Example project cleanupAdded “side filters” sampleReadme updated10.10v4.2.0deliver extra_data to render_row_details()Allow to set detail callback, i.e. to display it in modal (thanks toPetrDlouhy)Fetch ‘csrfmiddlewaretoken’ when csrftoken cookie is set to HttpOnly (thanks toshuki25)10.11v4.1.7Allow to set order field for column (thanks Petr Dlouhý)10.12v4.1.6Unused template ‘datatable.html’ removed.Fix issues #2 and #8Fix issue #9Raise an exception when searching over a ManyToManyField (not supported yet)10.13v4.1.5Edit button exampleBetter row-tools style (fix for Firefox)10.14v4.1.4foreign_fields: render with __str__() if no attribute has been specified by ‘foreign_field’[fix] added missing csrftoken header in first POST call (initialize_table())selectively disable theonlyorselect_relatedqueryset optimization10.15v4.1.3Filters: proper lookup choices for foreign columnssearch_in_choices(): match substring instead of startswith()Filter tracing: serialize with DjangoJSONEncoder10.16v4.1.2pass “extra_data” during table initialization10.17v4.1.1[fix] Restore capability to use both global and column filtering at the same timeimproved tracing (optionally uses sqlparse, termcolor and pygments)10.18v4.1.0Avoid duplicate column namesAdd a border to “plus” and “minus” iconsReadme: added an example on how to use extra_data for initial queryset filteringImproved layout of global date filters for easier style customizationExplicitly check field existence when initial order is expressed with fieldnamesCleanup full_row_select option[fix] Treat DateTimeField properly in filter_queryset_by_date_range()App settings for debug tracing renamed10.19v4.0.8Recover missing commits from develp10.20v4.0.7[TODO] check for unwanted side-effects here: …… Prevent click to move the HTML page to the topImproved example project10.21v4.0.6Accept the more generic “pk” column name instead of “id”; “id” still supported for backward compatibilityIn the example project: a new page has been added to play with a model with a custom PK column name10.22v4.0.5classifiers added to setup.py10.23v4.0.4fix Readme10.24v4.0.3[fix] accept anonymous POSTsAnother (this time really minimal) example working project10.25v4.0.2overridablerender_row_details_template_nameattribute addedsupport to search multiple values (seesearch_values_separator)10.26v4.0.1A few typo fixes here and there10.27v4.0.0package renamed fromdjango-datatables-viewtodjango-ajax-datatablepublished on PyPIexample project addedsetup of demo sitehttp://django-ajax-datatable-demo.brainstorm.it10.28v3.2.3“data-parent-row-id” attribute added to details row10.29v3.2.2accept positions expressed as column names in initial_order[]10.30v3.2.1add className to filtersimproved filtering with choices by including foreign_fieldsoptional “boolean” column attribute to treat calculated column as booleans on explicit requestoptional “max_length” column attribute to clip results10.31v3.2.0Automatic addition of table row ID (seeget_table_row_id())requestparameter added toprepare_results()andget_response_dict()10.32v3.1.4fix checkbox and radio buttons not working in a form embedded in the details row when full_row_select is active10.33v3.1.3Better behaviour for full_row_select10.34v3.1.2initialSearchValuecan now be a value or a callable object10.35v3.1.1Silly JS fix10.36v3.1.0choices / autofilter support for column filtersoptionalinitialSearchValuefor column filtersBackward incompatible change: any unrecognized column_defs attribute will raises an exception10.37v3.0.4Support length_menu = -1 (which means: “all”)10.38v3.0.3Usefull_row_select=trueto toggled row details by clicking anywhere in the row10.39v3.0.2Sanity check for initial_order[]10.40v3.0.1js fix (same as v2.3.5)10.41v3.0.0Bump major version to welcome Django 310.42v2.3.5js fix10.43v2.3.4Add support for Django 3.0, drop Python 210.44v.2.3.3Some JS utilities added10.45v2.3.2improved queryset optimization10.46v2.3.1fix queryset optimization10.47v2.3.0queryset optimization10.48v2.2.9optional extra_data dictionary accepted by initialize_table()10.49v2.2.8Removetable-layout: fixed;style from HTML table, as this causes problems in the columns’ widths computation10.50v2.2.7Explicitly set width of “row tools” columnLocalize “search” prompt in column filters10.51v2.2.6Experimental: Optionally control the (minimum) width of each single column10.52v2.2.5cleanup10.53v2.2.4optionally specified extra options to initialize_table()10.54v2.2.3accept language options10.55v2.2.2fix default footer10.56v2.2.1README revised10.57v2.2.0Merge into master10.58v2.1.3Remove initialize_datatable() from main project and replace with DatatablesViewUtils.initialize_table() to share common behaviourNotify Datatable subscribers with various eventsCleanup global filtering on dates rangeDerived view class can now specify ‘latest_by’ when different from model.get_latest_byDocumentation revised10.59v2.1.2basic support for DateField and DateTimeField filtering (exact date match)10.60v2.1.1choices lookup revised10.61v2.1.0static/datatables_view/js/datatables_utils.jsrenamed asstatic/datatables_view/js/utils.jsjs helper encapsulated in DatatablesViewUtils moduleFirst “almost” working column filtering - good enought for text search10.62v2.0.6Accept either GET or POST requests10.63v2.0.5Global “get_latest_by” filtering improved10.64v2.0.4Filter tracing (for debugging)10.65v2.0.0DatatablesView refactoring: columns_specs[] used as a substitute for columns[],searchable_columns[] and foreign_fields[]10.66v1.2.4recognize datatime.date column type10.67v1.2.3render_row_details() passes model_admin to the context, to permit fieldsets navigation10.68v1.2.2generic tables explainedrender_row_details customizable via templates10.69v1.2.1merged PR #1 from Thierry BOULOGNE10.70v1.2.0Incompatible change: postpone column initialization and pass the request to get_column_defs() for runtime table layout customization10.71v1.0.1fix choices lookup10.72v1.0.0fix searchbetter distribution (make sure templates and statics are included)10.73v0.0.2Package version added