├── .gitignore
├── CHANGELOG.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── datatables_view
├── __init__.py
├── app_settings.py
├── apps.py
├── columns.py
├── exceptions.py
├── filters.py
├── static
│ └── datatables_view
│ │ ├── css
│ │ └── style.css
│ │ ├── images
│ │ ├── details_close.png
│ │ └── details_open.png
│ │ └── js
│ │ └── utils.js
├── templates
│ ├── base.html
│ └── datatables_view
│ │ ├── datatable.html
│ │ └── row_tools.html
├── templatetags
│ ├── __init__.py
│ └── datatables_view_tags.py
├── tests
│ ├── __init__.py
│ ├── test_autofilter.py
│ ├── test_choices_filters.py
│ ├── test_columndefs.py
│ ├── test_datatables_qs.py
│ └── test_date_filters.py
├── utils.py
└── views.py
├── requirements_test.txt
├── runtests.py
├── screenshots
├── 001.png
├── 002.png
├── 003.png
├── 004a.png
├── 004b.png
├── 005.png
├── 006.png
├── 007.png
├── 008.png
├── 009.png
├── 010.png
├── clipping_results.png
├── column_filtering.png
└── table_row_id.png
├── setup.cfg
├── setup.py
└── tests
├── __init__.py
├── settings.py
├── test_models.py
└── urls.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.py[cod]
3 | __pycache__
4 |
5 | # C extensions
6 | *.so
7 | .vscode
8 |
9 | # Packages
10 | *.egg
11 | *.egg-info
12 | dist
13 | build
14 | eggs
15 | parts
16 | bin
17 | var
18 | sdist
19 | develop-eggs
20 | .installed.cfg
21 | lib
22 | lib64
23 |
24 | # Installer logs
25 | pip-log.txt
26 |
27 | # Unit test / coverage reports
28 | .coverage
29 | .tox
30 | nosetests.xml
31 | htmlcov
32 |
33 | # Translations
34 | *.mo
35 |
36 | # Mr Developer
37 | .mr.developer.cfg
38 | .project
39 | .pydevproject
40 |
41 | # Pycharm/Intellij
42 | .idea
43 |
44 | # Complexity
45 | output/*.html
46 | output/*/index.html
47 |
48 | # Sphinx
49 | docs/_build
50 |
51 | .vagrant
52 | db.sqlite3
53 |
54 | .ipynb*
55 | .cache
56 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | .. :changelog:
2 |
3 | History
4 | =======
5 |
6 | v3.2.3
7 | ------
8 | * "data-parent-row-id" attribute added to details row
9 |
10 | v3.2.2
11 | ------
12 | * accept positions expressed as column names in initial_order[]
13 |
14 | v3.2.1
15 | ------
16 | * add className to filters
17 | * improved filtering with choices by including foreign_fields
18 | * optional "boolean" column attribute to treat calculated column as booleans on explicit request
19 | * optional "max_length" column attribute to clip results
20 |
21 | v3.2.0
22 | ------
23 | * Automatic addition of table row ID (see `get_table_row_id()`)
24 | * `request` parameter added to `prepare_results()` and `get_response_dict()`
25 |
26 | v3.1.4
27 | ------
28 | * fix checkbox and radio buttons not working in a form embedded in the details row when full_row_select is active
29 |
30 | v3.1.3
31 | ------
32 | * Better behaviour for full_row_select
33 |
34 | v3.1.2
35 | ------
36 | * `initialSearchValue` can now be a value or a callable object
37 |
38 | v3.1.1
39 | ------
40 | * Silly JS fix
41 |
42 | v3.1.0
43 | ------
44 | * choices / autofilter support for column filters
45 | * optional *initialSearchValue* for column filters
46 | * **Backward incompatible change**: any unrecognized column_defs attribute will raises an exception
47 |
48 | v3.0.4
49 | ------
50 | * Support length_menu = -1 (which means: "all")
51 |
52 | v3.0.3
53 | ------
54 | * Use `full_row_select=true` to toggled row details by clicking anywhere in the row
55 |
56 | v3.0.2
57 | ------
58 | * Sanity check for initial_order[]
59 |
60 | v3.0.1
61 | ------
62 | * js fix (same as v2.3.5)
63 |
64 | * js fix
65 | v3.0.0
66 | ------
67 | * Bump major version to welcome Django 3
68 |
69 | v2.3.5
70 | ------
71 | * js fix
72 |
73 | v2.3.4
74 | ------
75 | * Add support for Django 3.0, drop Python 2
76 |
77 | v.2.3.3
78 | -------
79 | * Some JS utilities added
80 |
81 | v2.3.2
82 | ------
83 | * improved queryset optimization
84 |
85 | v2.3.1
86 | ------
87 | * fix queryset optimization
88 |
89 | v2.3.0
90 | ------
91 | * queryset optimization
92 |
93 | v2.2.9
94 | ------
95 | * optional extra_data dictionary accepted by initialize_table()
96 |
97 | v2.2.8
98 | ------
99 | * Remove `table-layout: fixed;` style from HTML table, as this causes problems in the columns' widths computation
100 |
101 | v2.2.7
102 | ------
103 | * Explicitly set width of "row tools" column
104 | * Localize "search" prompt in column filters
105 |
106 | v2.2.6
107 | ------
108 | * Experimental: Optionally control the (minimum) width of each single column
109 |
110 | v2.2.5
111 | ------
112 | * cleanup
113 |
114 | v2.2.4
115 | ------
116 | * optionally specified extra options to initialize_table()
117 |
118 | v2.2.3
119 | ------
120 | * accept language options
121 |
122 | v2.2.2
123 | ------
124 | * fix default footer
125 |
126 | v2.2.1
127 | ------
128 | * README revised
129 |
130 | v2.2.0
131 | ------
132 | * Merge into master
133 |
134 | v2.1.3
135 | ------
136 | * Remove initialize_datatable() from main project and replace with DatatablesViewUtils.initialize_table() to share common behaviour
137 | * Notify Datatable subscribers with various events
138 | * Cleanup global filtering on dates range
139 | * Derived view class can now specify 'latest_by' when different from model.get_latest_by
140 | * Documentation revised
141 |
142 | v2.1.2
143 | ------
144 | * basic support for DateField and DateTimeField filtering (exact date match)
145 |
146 | v2.1.1
147 | ------
148 | * choices lookup revised
149 |
150 | v2.1.0
151 | ------
152 | * `static/datatables_view/js/datatables_utils.js` renamed as `static/datatables_view/js/utils.js`
153 | * js helper encapsulated in DatatablesViewUtils module
154 | * First "almost" working column filtering - good enought for text search
155 |
156 | v2.0.6
157 | ------
158 | * Accept either GET or POST requests
159 |
160 | v2.0.5
161 | ------
162 | * Global "get_latest_by" filtering improved
163 |
164 | v2.0.4
165 | ------
166 | * Filter tracing (for debugging)
167 |
168 | v2.0.0
169 | ------
170 | * DatatablesView refactoring: columns_specs[] used as a substitute for columns[],searchable_columns[] and foreign_fields[]
171 |
172 | v1.2.4
173 | ------
174 | * recognize datatime.date column type
175 |
176 | v1.2.3
177 | ------
178 | * render_row_details() passes model_admin to the context, to permit fieldsets navigation
179 |
180 | v1.2.2
181 | ------
182 | * generic tables explained
183 | * render_row_details customizable via templates
184 |
185 | v1.2.1
186 | ------
187 | * merged PR #1 from Thierry BOULOGNE
188 |
189 | v1.2.0
190 | ------
191 | * Incompatible change: postpone column initialization and pass the request to get_column_defs() for runtime table layout customization
192 |
193 | v1.0.1
194 | ------
195 | * fix choices lookup
196 |
197 | v1.0.0
198 | ------
199 | * fix search
200 | * better distribution (make sure templates and statics are included)
201 |
202 | v0.0.2
203 | ------
204 | * Package version added
205 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | MIT License
3 |
4 | Copyright (c) 2017, Mario Orlandi
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9 |
10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | include LICENSE
3 | include CHANGELOG.rst
4 | recursive-include datatables_view/static *
5 | recursive-include datatables_view/templates *
6 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 |
2 | **⚠️ Current status of this project**
3 |
4 | **This package has been renamed as `django-ajax-datatable` since v4.0.0, to avoid a conflict in PyPI, and moved to https://github.com/morlandi/django-ajax-datatable**.
5 |
6 | You are strongly suggested to switch to the new project https://github.com/morlandi/django-ajax-datatable, which is actively maintained and provides several improvements;
7 |
8 | however, all releases published in the current repo will rest in place forever.
9 |
10 | A check list of actions required to migrate your Django project to the new package is available here:
11 | https://github.com/morlandi/django-ajax-datatable/blob/master/MIGRATE_FROM_DATATABLES_VIEW_CHECKLIST.rst
12 |
13 | django-datatables-view
14 | ======================
15 |
16 | django-datatables-view is a Django app which provides the integration of a Django
17 | project with the jQuery Javascript library DataTables.net,
18 | when used with server-side processing mode.
19 |
20 | In this context, the rendering of the table is the result of a serie of Ajax
21 | requests to the server following user interactions (i.e. when paging, ordering, searching, etc.).
22 |
23 | With django-datatables-view, basically you have to provide a DatatablesView-derived view
24 | to describe the desired table content and behaviour, and the app manages the interaction
25 | with DataTables.net by reacting to the ajax requests with suitable responses.
26 |
27 | Notes:
28 |
29 | Since someone asked ...
30 |
31 | - I use this app for my own projects, and improve it from time to time as new needs arises.
32 |
33 | - I received so much from the Django community, so I'm more than happy to share something hopefully useful for others.
34 | The app is intended to be opensource; feel free to use it we no restrictions at all.
35 | I added a MIT Licence file to the github repo, to make this more explicit.
36 |
37 | - The app hasn't been published on PyPI since, due to a name conflict, that would require renaming it.
38 | Is it worth it ? If you think so, please let me know opening an issue in the Github project.
39 |
40 | - Unfortunately I only have a few unit tests, and didn't bother (yet) to add a TOX procedure to run then with
41 | different Python/Django versions.
42 | Having said this, I can confirm that I do happen to use it with no problems in projects based on Django 2.x.
43 | However, most recent improvements have been tested mainly with Django 3.
44 | As far as I know, no Django3-specific features have been applied.
45 | In case, please open an issue, and I will fix it.
46 |
47 | - 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);
48 | old releases will be in place in the repo forever
49 |
50 | Features:
51 |
52 | - Pagination
53 | - Column ordering
54 | - Global generic search
55 | - Global date-range search over "get_latest_by" column
56 | - Column specific filtering
57 | - Foreign key fields can be used, using the "model1__model2__field" notation
58 | - Customizable rendering of table rows
59 | - ...
60 |
61 | Inspired from:
62 |
63 | https://github.com/monnierj/django-datatables-server-side
64 |
65 | .. contents::
66 |
67 | .. sectnum::
68 |
69 | Installation
70 | ------------
71 |
72 | Install the package by running:
73 |
74 | .. code:: bash
75 |
76 | pip install git+https://github.com/morlandi/django-datatables-view
77 |
78 | or possibly a specific version:
79 |
80 | .. code:: bash
81 |
82 | pip install git+https://github.com/morlandi/django-datatables-view@v3.0.0
83 |
84 | then add 'datatables_view' to your INSTALLED_APPS:
85 |
86 | .. code:: bash
87 |
88 | INSTALLED_APPS = [
89 | ...
90 | 'datatables_view',
91 | ]
92 |
93 |
94 | Pre-requisites
95 | --------------
96 |
97 | Your base template should include what required by `datatables.net`, plus:
98 |
99 | - /static/datatables_view/css/style.css
100 | - /static/datatables_view/js/utils.js
101 |
102 | Example:
103 |
104 | .. code:: html
105 |
106 | {% block extrastyle %}
107 |
108 |
109 |
110 |
111 |
112 |
113 | {% endblock extrastyle %}
114 |
115 | {% block extrajs %}
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | {% endcompress %}
130 |
131 |
132 |
133 | Basic DatatablesView
134 | --------------------
135 |
136 | To provide server-side rendering of a Django Model, you need a specific
137 | view derived from DatatablesView() which will be called multiple times via Ajax during data navigation.
138 |
139 | At the very minimum, you shoud specify a suitable `column_defs` list.
140 |
141 | Example:
142 |
143 | `urls.py`
144 |
145 | .. code:: python
146 |
147 | from django.urls import path
148 | from . import datatables_views
149 |
150 | app_name = 'frontend'
151 |
152 | urlpatterns = [
153 | ...
154 | path('datatable/registers/', datatables_views.RegisterDatatablesView.as_view(), name="datatable_registers"),
155 | ]
156 |
157 |
158 | `datatables_views.py`
159 |
160 | .. code:: python
161 |
162 | from django.contrib.auth.decorators import login_required
163 | from django.utils.decorators import method_decorator
164 |
165 | from datatables_view.views import DatatablesView
166 | from backend.models import Register
167 |
168 |
169 | @method_decorator(login_required, name='dispatch')
170 | class RegisterDatatablesView(DatatablesView):
171 |
172 | model = Register
173 | title = 'Registers'
174 |
175 | column_defs = [
176 | {
177 | 'name': 'id',
178 | 'visible': False,
179 | }, {
180 | 'name': 'created',
181 | }, {
182 | 'name': 'type',
183 | }, {
184 | 'name': 'address',
185 | }, {
186 | 'name': 'readonly',
187 | }, {
188 | 'name': 'min',
189 | }, {
190 | 'name': 'max',
191 | }, {
192 | 'name': 'widget_type',
193 | }
194 | ]
195 |
196 |
197 | In the previous example, row id is included in the first column of the table,
198 | but hidden to the user.
199 |
200 | DatatablesView will serialize the required data during table navigation;
201 | in order to render the initial web page which should contain the table,
202 | you need another "application" view, normally based on a template.
203 |
204 | `Usage: (file register_list.html)`
205 |
206 | .. code:: html
207 |
208 |
209 |
210 |
211 | ...
212 |
213 |
233 |
234 | In the template, insert a
element and connect it to the DataTable machinery,
235 | calling **DatatablesViewUtils.initialize_table(element, url, extra_options={}, extra_data={})**, which will in turn
236 | perform a first call (identified by the `action=initialize` parameter)
237 | to render the initial table layout.
238 |
239 | In this initial phase, the (base) view's responsibility is that of providing to DataTables
240 | the suitable columns specifications (and other details), based on the `column_defs`
241 | attribute specified in the (derived) view class.
242 |
243 | Then, subsequent calls to the view will be performed to populate the table with real data.
244 |
245 | This strategy allows the placement of one or more dynamic tables in the same page.
246 |
247 | In simpler situations, where only one table is needed, you can use a single view
248 | (the one derived from DatatablesView); the rendered page is based on the default
249 | template `datatables_view/database.html`, unless overridden.
250 |
251 | This is the resulting table:
252 |
253 | .. image:: screenshots/001.png
254 |
255 |
256 | DatatablesViewUtils.initialize_table() parameters are:
257 |
258 | element
259 | table element
260 |
261 | url
262 | action (remote url to be called via Ajax)
263 |
264 | extra_options={}
265 | custom options for dataTable()
266 |
267 | extra_data={}
268 | extra parameters to be sent via ajax for custom filtering
269 |
270 |
271 | DatatablesView Class attributes
272 | -------------------------------
273 |
274 | Required:
275 |
276 | - model
277 | - column_defs
278 |
279 | Optional:
280 |
281 | - template_name = 'datatables_view/datatable.html'
282 | - initial_order = [[1, "asc"], [5, "desc"]] # positions can also be expressed as column names: [['surname', 'asc'], ]
283 | - length_menu = [[10, 20, 50, 100], [10, 20, 50, 100]]
284 | - latest_by = None
285 | - show_date_filters = None
286 | - show_column_filters = None
287 | - disable_queryset_optimization = False
288 | - table_row_id_prefix = 'row-'
289 | - table_row_id_fieldname = 'id'
290 |
291 | or override the following methods to provide attribute values at run-time,
292 | based on request:
293 |
294 | .. code:: python
295 |
296 | def get_column_defs(self):
297 | return self.column_defs
298 |
299 | def get_initial_order(self):
300 | return self.initial_order
301 |
302 | def get_length_menu(self):
303 | return self.length_menu
304 |
305 | def get_template_name(self):
306 | return self.template_name
307 |
308 | def get_latest_by(self, request):
309 | """
310 | Override to customize based on request.
311 |
312 | Provides the name of the column to be used for global date range filtering.
313 | Return either '', a fieldname or None.
314 |
315 | When None is returned, in model's Meta 'get_latest_by' attributed will be used.
316 | """
317 | return self.latest_by
318 |
319 | def get_show_date_filters(self, request):
320 | """
321 | Override to customize based on request.
322 |
323 | Defines whether to use the global date range filter.
324 | Return either True, False or None.
325 |
326 | When None is returned, will'll check whether 'latest_by' is defined
327 | """
328 | return self.show_date_filters
329 |
330 | def get_show_column_filters(self, request):
331 | """
332 | Override to customize based on request.
333 |
334 | Defines whether to use the column filters.
335 | Return either True, False or None.
336 |
337 | When None is returned, check if at least one visible column in searchable.
338 | """
339 | return self.show_column_filters
340 |
341 | def get_table_row_id(self, request, obj):
342 | """
343 | Provides a specific ID for the table row; default: "row-ID"
344 | Override to customize as required.
345 | """
346 | result = ''
347 | if self.table_row_id_fieldname:
348 | try:
349 | result = self.table_row_id_prefix + str(getattr(obj, self.table_row_id_fieldname))
350 | except:
351 | result = ''
352 | return result
353 |
354 | column_defs customizations
355 | --------------------------
356 |
357 | Example::
358 |
359 | column_defs = [{
360 | 'name': 'currency', # required
361 | 'data': None,
362 | 'title': 'Currency', # optional: default = field verbose_name or column name
363 | 'visible': True, # optional: default = True
364 | 'searchable': True, # optional: default = True if visible, False otherwise
365 | 'orderable': True, # optional: default = True if visible, False otherwise
366 | 'foreign_field': 'manager__name', # optional: follow relation
367 | 'placeholder': False, # ???
368 | 'className': 'css-class-currency', # optional class name for cell
369 | 'defaultContent': '
test
', # ???
370 | 'width': 300, # optional: controls the minimum with of each single column
371 | 'choices': None, # see `Filtering single columns` below
372 | 'initialSearchValue': None, # see `Filtering single columns` below
373 | 'autofilter': False, # see `Filtering single columns` below
374 | 'boolean': False, # treat calculated column as BooleanField
375 | 'max_length': 0, # if > 0, clip result longer then max_length
376 | }, {
377 | ...
378 |
379 | Notes:
380 |
381 | - **title**: if not supplied, the verbose name of the model column (when available)
382 | or **name** will be used
383 | - **width**: for this to be effective, you need to add **table-layout: fixed;** style
384 | to the HTML table, but in some situations this causes problems in the computation
385 | of the table columns' widths (at least in the current version 1.10.19 of Datatables.net)
386 |
387 | Automatic addition of table row ID
388 | ----------------------------------
389 |
390 | Starting from v3.2.0, each table row is characterized with a specific ID on each row
391 | (tipically, the primary key value from the queryset)
392 |
393 | .. image:: screenshots/table_row_id.png
394 |
395 | The default behaviour is to provide the string "row-ID", where:
396 |
397 | - "row-" is retrieved from self.table_row_id_prefix
398 | - "ID" is retrieved from the row object, using the field with name self.table_row_id_fieldname (default: "id")
399 |
400 | Note that, for this to work, you are required to list the field "id" in the column list (maybe hidden).
401 |
402 | This default behaviour can be customized by either:
403 |
404 | - replacing the values for `table_row_id_fieldname` and/or `table_row_id_prefix`, or
405 | - overriding `def get_table_row_id(self, request, obj)`
406 |
407 | Filtering single columns
408 | ------------------------
409 |
410 | **DatatableView.show_column_filters** (or **DatatableView.get_show_column_filters(request)**)
411 | defines whether to show specific filters for searchable columns as follows:
412 |
413 | - None (default): show if at least one visible column in searchable
414 | - True: always show
415 | - False: always hide
416 |
417 | By default, a column filter for a searchable column is rendered as a **text input** box;
418 | you can instead provide a **select** box using the following attributes:
419 |
420 | choices
421 | - None (default) or False: no choices (use text input box)
422 | - True: use Model's field choices;
423 | + failing that, we might use "autofilter"; that is: collect the list of distinct values from db table
424 | + or, for **BooleanField** columns, provide (None)/Yes/No choice sequence
425 | + calculated columns with attribute 'boolean'=True are treated as BooleanFields
426 | - ((key1, value1), (key2, values), ...) : use supplied sequence of choices
427 |
428 | autofilter
429 | - default = False
430 | - when set: if choices == True and no Model's field choices are available,
431 | collects distinct values from db table (much like Excel "autofilter" feature)
432 |
433 | For the first rendering of the table:
434 |
435 | initialSearchValue
436 | - optional initial value for column filter
437 |
438 | Note that `initialSearchValue` can be a value or a callable object.
439 | If callable it will be called every time a new object is created.
440 |
441 | For example:
442 |
443 | .. code:: python
444 |
445 | class MyDatatablesView(DatatablesView):
446 |
447 | def today():
448 | return datetime.datetime.now().date()
449 |
450 | ...
451 |
452 | column_defs = [
453 | ...
454 | {
455 | 'name': 'created',
456 | 'choices': True,
457 | 'autofilter': True,
458 | 'initialSearchValue': today
459 | },
460 | ...
461 | ]
462 |
463 | .. image:: screenshots/column_filtering.png
464 |
465 |
466 | Computed (placeholder) columns
467 | ------------------------------
468 |
469 | You can insert placeholder columns in the table, and feed their content with
470 | arbitrary HTML.
471 |
472 | Example:
473 |
474 | .. code:: python
475 |
476 | @method_decorator(login_required, name='dispatch')
477 | class RegisterDatatablesView(DatatablesView):
478 |
479 | model = Register
480 | title = _('Registers')
481 |
482 | column_defs = [
483 | {
484 | 'name': 'id',
485 | 'visible': False,
486 | }, {
487 | 'name': 'created',
488 | }, {
489 | 'name': 'dow',
490 | 'title': 'Day of week',
491 | 'placeholder': True,
492 | 'searchable': False,
493 | 'orderable': False,
494 | 'className': 'highlighted',
495 | }, {
496 | ...
497 | }
498 | ]
499 |
500 | def customize_row(self, row, obj):
501 | days = ['monday', 'tuesday', 'wednesday', 'thyrsday', 'friday', 'saturday', 'sunday']
502 | if obj.created is not None:
503 | row['dow'] = '%s' % days[obj.created.weekday()]
504 | else:
505 | row['dow'] = ''
506 | return
507 |
508 | .. image:: screenshots/003.png
509 |
510 | Clipping results
511 | ----------------
512 |
513 | Sometimes you might want to clip results up to a given maximum length, to control the column width.
514 |
515 | This can be obtained by specifying a positive value for the `max_length` column_spec attribute.
516 |
517 | Results will be clipped in both the column cells and in the column filter.
518 |
519 | .. image:: screenshots/clipping_results.png
520 |
521 | Clipped results are rendered as html text as follows:
522 |
523 | .. code:: python
524 |
525 | def render_clip_value_as_html(self, long_text, short_text, is_clipped):
526 | """
527 | Given long and shor version of text, the following html representation:
528 | short_text[ellipsis]
529 |
530 | To be overridden for further customisations.
531 | """
532 | return '{short_text}{ellipsis}'.format(
533 | long_text=long_text,
534 | short_text=short_text,
535 | ellipsis='…' if is_clipped else ''
536 | )
537 |
538 | You can customise the rendering by overriding `render_clip_value_as_html()`
539 |
540 | Receiving table events
541 | ----------------------
542 |
543 | The following table events are broadcasted to your custom handlers, provided
544 | you subscribe them:
545 |
546 | - initComplete(table)
547 | - drawCallback(table, settings)
548 | - rowCallback(table, row, data)
549 | - footerCallback(table, row, data, start, end, display)
550 |
551 | Please note the the first parameter of the callback is always the event,
552 | and next parameters are additional data::
553 |
554 | .trigger('foo', [1, 2]);
555 |
556 | .on('foo', function(event, one, two) { ... });
557 |
558 |
559 | More events triggers sent directly by DataTables.net are listed here:
560 |
561 | https://datatables.net/reference/event/
562 |
563 | Example:
564 |
565 | .. code :: html
566 |
567 |
568 |
569 |
570 |
571 |
572 |
592 |
593 |
594 | Overridable DatatablesView methods
595 | ----------------------------------
596 |
597 | get_initial_queryset()
598 | ......................
599 |
600 | Provides the queryset to work with; defaults to **self.model.objects.all()**
601 |
602 | Example:
603 |
604 | .. code:: python
605 |
606 | def get_initial_queryset(self, request=None):
607 | if not request.user.view_all_clients:
608 | queryset = request.user.related_clients.all()
609 | else:
610 | queryset = super().get_initial_queryset(request)
611 | return queryset
612 |
613 | get_foreign_queryset()
614 | ......................
615 |
616 | When collecting data for autofiltering in a "foreign_field" column, we need some data
617 | source for doing the lookup.
618 |
619 | The default implementation is as follows:
620 |
621 | .. code:: python
622 |
623 | def get_foreign_queryset(self, request, field):
624 | queryset = field.model.objects.all()
625 | return queryset
626 |
627 | You can override it for further reducing the resulting list.
628 |
629 | customize_row()
630 | ...............
631 |
632 | Called every time a new data row is required by the client, to let you further
633 | customize cell content
634 |
635 | Example:
636 |
637 | .. code:: python
638 |
639 | def customize_row(self, row, obj):
640 | # 'row' is a dictionary representing the current row, and 'obj' is the current object.
641 | row['code'] = '%s' % (
642 | obj.status,
643 | reverse('frontend:client-detail', args=(obj.id,)),
644 | obj.code
645 | )
646 | if obj.recipe is not None:
647 | row['recipe'] = obj.recipe.display_as_tile() + ' ' + str(obj.recipe)
648 | return
649 |
650 | render_row_details()
651 | ....................
652 |
653 | Renders an HTML fragment to show table row content in "detailed view" fashion,
654 | as previously explained later in the **Add row tools as first column** section.
655 |
656 | See also: `row details customization`_
657 |
658 | Example:
659 |
660 | .. code:: python
661 |
662 | def render_row_details(self, id, request=None):
663 | client = self.model.objects.get(id=id)
664 | ...
665 | return render_to_string('frontend/pages/includes/client_row_details.html', {
666 | 'client': client,
667 | ...
668 | })
669 |
670 | footer_message()
671 | ................
672 |
673 | You can annotate the table footer with a custom message by overridding the
674 | following View method.
675 |
676 | .. code:: python
677 |
678 | def footer_message(self, qs, params):
679 | """
680 | Overriden to append a message to the bottom of the table
681 | """
682 | return None
683 |
684 | Example:
685 |
686 | .. code:: python
687 |
688 | def footer_message(self, qs, params):
689 | return 'Selected rows: %d' % qs.count()
690 |
691 | .. code:: html
692 |
693 |
702 |
703 | .. image:: screenshots/005.png
704 |
705 |
706 | render_clip_value_as_html()
707 | ...........................
708 |
709 | Renders clipped results as html span tag, providing the non-clipped value as title:
710 |
711 | .. code:: python
712 |
713 | def render_clip_value_as_html(self, long_text, short_text, is_clipped):
714 | """
715 | Given long and shor version of text, the following html representation:
716 | short_text[ellipsis]
717 |
718 | To be overridden for further customisations.
719 | """
720 | return '{short_text}{ellipsis}'.format(
721 | long_text=long_text,
722 | short_text=short_text,
723 | ellipsis='…' if is_clipped else ''
724 | )
725 |
726 | Override to customise the rendering of clipped cells.
727 |
728 | Queryset optimization
729 | =====================
730 |
731 | As the purpose of this module is all about querysets rendering, any chance to optimize
732 | data extractions from the database is more then appropriate.
733 |
734 | Starting with v2.3.0, DatatablesView tries to burst performances in two ways:
735 |
736 | 1) by using `only `_ to limit the number of columns in the result set
737 |
738 | 2) by using `select_related `_ to minimize the number of queries involved
739 |
740 | The parameters passed to only() and select_related() are inferred from `column_defs`.
741 |
742 | Should this cause any problem, you can disable queryset optimization in two ways:
743 |
744 | - globally: by activating the `DATATABLES_VIEW_DISABLE_QUERYSET_OPTIMIZATION` setting
745 | - per table: by setting to True the value of the `disable_queryset_optimization` attribute
746 |
747 |
748 | A real use case
749 | ---------------
750 |
751 | (1) Plain queryset::
752 |
753 | SELECT "tasks_devicetesttask"."id",
754 | "tasks_devicetesttask"."description",
755 | "tasks_devicetesttask"."created_on",
756 | "tasks_devicetesttask"."created_by_id",
757 | "tasks_devicetesttask"."started_on",
758 | "tasks_devicetesttask"."completed_on",
759 | "tasks_devicetesttask"."job_id",
760 | "tasks_devicetesttask"."status",
761 | "tasks_devicetesttask"."mode",
762 | "tasks_devicetesttask"."failure_reason",
763 | "tasks_devicetesttask"."progress",
764 | "tasks_devicetesttask"."log_text",
765 | "tasks_devicetesttask"."author",
766 | "tasks_devicetesttask"."order",
767 | "tasks_devicetesttask"."appliance_id",
768 | "tasks_devicetesttask"."serial_number",
769 | "tasks_devicetesttask"."program_id",
770 | "tasks_devicetesttask"."position",
771 | "tasks_devicetesttask"."hidden",
772 | "tasks_devicetesttask"."is_duplicate",
773 | "tasks_devicetesttask"."notes"
774 | FROM "tasks_devicetesttask"
775 | WHERE "tasks_devicetesttask"."hidden" = FALSE
776 | ORDER BY "tasks_devicetesttask"."created_on" DESC
777 |
778 | **[sql] (233ms) 203 queries with 182 duplicates**
779 |
780 |
781 | (2) With select_related()::
782 |
783 | SELECT "tasks_devicetesttask"."id",
784 | "tasks_devicetesttask"."description",
785 | "tasks_devicetesttask"."created_on",
786 | "tasks_devicetesttask"."created_by_id",
787 | "tasks_devicetesttask"."started_on",
788 | "tasks_devicetesttask"."completed_on",
789 | "tasks_devicetesttask"."job_id",
790 | "tasks_devicetesttask"."status",
791 | "tasks_devicetesttask"."mode",
792 | "tasks_devicetesttask"."failure_reason",
793 | "tasks_devicetesttask"."progress",
794 | "tasks_devicetesttask"."log_text",
795 | "tasks_devicetesttask"."author",
796 | "tasks_devicetesttask"."order",
797 | "tasks_devicetesttask"."appliance_id",
798 | "tasks_devicetesttask"."serial_number",
799 | "tasks_devicetesttask"."program_id",
800 | "tasks_devicetesttask"."position",
801 | "tasks_devicetesttask"."hidden",
802 | "tasks_devicetesttask"."is_duplicate",
803 | "tasks_devicetesttask"."notes",
804 | "backend_appliance"."id",
805 | "backend_appliance"."description",
806 | "backend_appliance"."hidden",
807 | "backend_appliance"."created",
808 | "backend_appliance"."created_by_id",
809 | "backend_appliance"."updated",
810 | "backend_appliance"."updated_by_id",
811 | "backend_appliance"."type",
812 | "backend_appliance"."rotation",
813 | "backend_appliance"."code",
814 | "backend_appliance"."barcode",
815 | "backend_appliance"."mechanical_efficiency_min",
816 | "backend_appliance"."mechanical_efficiency_max",
817 | "backend_appliance"."volumetric_efficiency_min",
818 | "backend_appliance"."volumetric_efficiency_max",
819 | "backend_appliance"."displacement",
820 | "backend_appliance"."speed_min",
821 | "backend_appliance"."speed_max",
822 | "backend_appliance"."pressure_min",
823 | "backend_appliance"."pressure_max",
824 | "backend_appliance"."oil_temperature_min",
825 | "backend_appliance"."oil_temperature_max",
826 | "backend_program"."id",
827 | "backend_program"."description",
828 | "backend_program"."hidden",
829 | "backend_program"."created",
830 | "backend_program"."created_by_id",
831 | "backend_program"."updated",
832 | "backend_program"."updated_by_id",
833 | "backend_program"."code",
834 | "backend_program"."start_datetime",
835 | "backend_program"."end_datetime",
836 | "backend_program"."favourite"
837 | FROM "tasks_devicetesttask"
838 | LEFT OUTER JOIN "backend_appliance" ON ("tasks_devicetesttask"."appliance_id" = "backend_appliance"."id")
839 | LEFT OUTER JOIN "backend_program" ON ("tasks_devicetesttask"."program_id" = "backend_program"."id")
840 | WHERE "tasks_devicetesttask"."hidden" = FALSE
841 | ORDER BY "tasks_devicetesttask"."created_on" DESC
842 |
843 | **[sql] (38ms) 3 queries with 0 duplicates**
844 |
845 |
846 | (3) With select_related() and only()::
847 |
848 | SELECT "tasks_devicetesttask"."id",
849 | "tasks_devicetesttask"."started_on",
850 | "tasks_devicetesttask"."completed_on",
851 | "tasks_devicetesttask"."status",
852 | "tasks_devicetesttask"."failure_reason",
853 | "tasks_devicetesttask"."author",
854 | "tasks_devicetesttask"."order",
855 | "tasks_devicetesttask"."appliance_id",
856 | "tasks_devicetesttask"."serial_number",
857 | "tasks_devicetesttask"."program_id",
858 | "tasks_devicetesttask"."position",
859 | "backend_appliance"."id",
860 | "backend_appliance"."code",
861 | "backend_program"."id",
862 | "backend_program"."code"
863 | FROM "tasks_devicetesttask"
864 | LEFT OUTER JOIN "backend_appliance" ON ("tasks_devicetesttask"."appliance_id" = "backend_appliance"."id")
865 | LEFT OUTER JOIN "backend_program" ON ("tasks_devicetesttask"."program_id" = "backend_program"."id")
866 | WHERE "tasks_devicetesttask"."hidden" = FALSE
867 | ORDER BY "tasks_devicetesttask"."created_on" DESC
868 |
869 | **[sql] (19ms) 3 queries with 0 duplicates**
870 |
871 |
872 | App settings
873 | ============
874 |
875 | DATATABLES_VIEW_MAX_COLUMNS
876 |
877 | Default: 30
878 |
879 | DATATABLES_VIEW_ENABLE_QUERYDICT_TRACING
880 |
881 | When True, enables debug tracing of datatables requests
882 |
883 | Default: False
884 |
885 | DATATABLES_VIEW_ENABLE_QUERYSET_TRACING
886 |
887 | When True, enables debug tracing of resulting query
888 |
889 | Default: False
890 |
891 | DATATABLES_VIEW_TEST_FILTERS
892 |
893 | When True, trace results for each individual filter, for debugging purposes
894 |
895 | Default: False
896 |
897 | DATATABLES_VIEW_DISABLE_QUERYSET_OPTIMIZATION
898 |
899 | When True, all queryset optimizations are disabled
900 |
901 | Default: False
902 |
903 |
904 | More details
905 | ============
906 |
907 | Add row tools as first column
908 | -----------------------------
909 |
910 | You can insert **DatatablesView.render_row_tools_column_def()** as the first element
911 | in `column_defs` to obtain some tools at the beginning of each table row.
912 |
913 | If `full_row_select=true` is specified as extra-option during table initialization,
914 | row details can be toggled by clicking anywhere in the row.
915 |
916 | `datatables_views.py`
917 |
918 | .. code:: python
919 |
920 | from django.contrib.auth.decorators import login_required
921 | from django.utils.decorators import method_decorator
922 |
923 | from datatables_view.views import DatatablesView
924 | from backend.models import Register
925 |
926 |
927 | @method_decorator(login_required, name='dispatch')
928 | class RegisterDatatablesView(DatatablesView):
929 |
930 | model = Register
931 | title = 'Registers'
932 |
933 | column_defs = [
934 | DatatablesView.render_row_tools_column_def(),
935 | {
936 | 'name': 'id',
937 | 'visible': False,
938 | }, {
939 | ...
940 |
941 | By default, these tools will provide an icon to show and hide a detailed view
942 | below each table row.
943 |
944 | The tools are rendered according to the template **datatables_view/row_tools.html**,
945 | which can be overridden.
946 |
947 | Row details are automatically collected via Ajax by calling again the views
948 | with a specific **?action=details** parameters, and will be rendered by the
949 | method::
950 |
951 | def render_row_details(self, id, request=None)
952 |
953 | which you can further customize when needed.
954 |
955 | The default behaviour provided by the base class if shown below:
956 |
957 | .. image:: screenshots/002.png
958 |
959 | row details customization
960 | -------------------------
961 |
962 | The default implementation of render_row_details() tries to load a template
963 | in the following order:
964 |
965 | - datatables_view///render_row_details.html
966 | - datatables_view//render_row_details.html
967 | - datatables_view/render_row_details.html
968 |
969 | and, when found, uses it for rendering.
970 |
971 | The template receives the following context::
972 |
973 | html = template.render({
974 | 'model': self.model,
975 | 'model_admin': self.get_model_admin(),
976 | 'object': obj,
977 | }, request)
978 |
979 | `model_admin`, when available, can be used to navigate fieldsets (if defined)
980 | in the template, much like django's `admin/change_form.html` does.
981 |
982 | If no template is available, a simple HTML table with all field values
983 | is built instead.
984 |
985 | In all cases, the resulting HTML will be wrapped in the following structure:
986 |
987 | .. code :: html
988 |
989 |
990 |
991 |
992 | ...
993 |
994 | Filter by global date range
995 | ---------------------------
996 |
997 | When a `latest_by` column has been specified and `show_date_filter` is active,
998 | a global date range filtering widget is provided, based on `jquery-ui.datepicker`:
999 |
1000 | .. image:: screenshots/004a.png
1001 |
1002 | The header of the column used for date filtering is decorated with the class
1003 | "latest_by"; you can use it to customize it's rendering.
1004 |
1005 | You can fully replace the widget with your own by providing a custom **fn_daterange_widget_initialize()**
1006 | callback at Module's initialization, as in the following example, where we
1007 | use `bootstrap.datepicker`:
1008 |
1009 | .. code:: html
1010 |
1011 | DatatablesViewUtils.init({
1012 | search_icon_html: '',
1013 | language: {
1014 | },
1015 | fn_daterange_widget_initialize: function(table, data) {
1016 | var wrapper = table.closest('.dataTables_wrapper');
1017 | var toolbar = wrapper.find(".toolbar");
1018 | toolbar.html(
1019 | '
'
1024 | );
1025 | var date_pickers = toolbar.find('.date_from, .date_to');
1026 | date_pickers.datepicker();
1027 | date_pickers.on('change', function(event) {
1028 | // Annotate table with values retrieved from date widgets
1029 | var dt_from = toolbar.find('.date_from').data("datepicker");
1030 | var dt_to = toolbar.find('.date_to').data("datepicker");
1031 | table.data('date_from', dt_from ? dt_from.getFormattedDate("yyyy-mm-dd") : '');
1032 | table.data('date_to', dt_to ? dt_to.getFormattedDate("yyyy-mm-dd") : '');
1033 | // Redraw table
1034 | table.api().draw();
1035 | });
1036 | }
1037 | });
1038 |
1039 | .. image:: screenshots/004b.png
1040 |
1041 | Debugging
1042 | ---------
1043 |
1044 | In case of errors, Datatables.net shows an alert popup:
1045 |
1046 | .. image:: screenshots/006.png
1047 |
1048 | You can change it to trace the error in the browser console, insted:
1049 |
1050 | .. code:: javascript
1051 |
1052 | // change DataTables' error reporting mechanism to throw a Javascript
1053 | // error to the browser's console, rather than alerting it.
1054 | $.fn.dataTable.ext.errMode = 'throw';
1055 |
1056 | All details of Datatables.net requests can be logged to the console by activating
1057 | this setting::
1058 |
1059 | DATATABLES_VIEW_ENABLE_QUERYDICT_TRACING = True
1060 |
1061 | The resulting query (before pagination) can be traced as well with::
1062 |
1063 | DATATABLES_VIEW_ENABLE_QUERYSET_TRACING = True
1064 |
1065 | Debugging traces for date range filtering, column filtering or global filtering can be displayed
1066 | by activating this setting::
1067 |
1068 | DATATABLES_VIEW_TEST_FILTERS
1069 |
1070 | .. image:: screenshots/007.png
1071 |
1072 |
1073 | Generic tables (advanced topic)
1074 | ===============================
1075 |
1076 | Chances are you might want to supply a standard user interface for listing
1077 | several models.
1078 |
1079 | In this case, it is possible to use a generic approach and avoid code duplications,
1080 | as detailed below.
1081 |
1082 | First, we supply a generic view which receives a model as parameter,
1083 | and passes it to the template used for rendering the page:
1084 |
1085 | file `frontend/datatables_views.py`:
1086 |
1087 | .. code:: python
1088 |
1089 | @login_required
1090 | def object_list_view(request, model, template_name="frontend/pages/object_list.html"):
1091 | """
1092 | Render the page which contains the table.
1093 | That will in turn invoke (via Ajax) object_datatable_view(), to fill the table content
1094 | """
1095 | return render(request, template_name, {
1096 | 'model': model,
1097 | })
1098 |
1099 | In the urlconf, link to specific models as in the example below:
1100 |
1101 | file `frontend/urls.py`:
1102 |
1103 | .. code:: python
1104 |
1105 | path('channel/', datatables_views.object_list_view, {'model': backend.models.Channel, }, name="channel-list"),
1106 |
1107 | The template uses the `model` received in the context to display appropriate `verbose_name`
1108 | and `verbose_name_plural` attributes, and to extract `app_label` and `model_name`
1109 | as needed; unfortunately, we also had to supply some very basic helper templatetags,
1110 | as the `_meta` attribute of the model is not directly visible in this context.
1111 |
1112 | .. code:: html
1113 |
1114 | {% extends 'frontend/base.html' %}
1115 | {% load static datatables_view_tags i18n %}
1116 |
1117 | {% block breadcrumbs %}
1118 |
1143 |
1144 | {% ifhasperm model 'add' %}
1145 | {% trans 'Add ...' %}
1146 | {% endifhasperm %}
1147 |
1148 | {% endif %}
1149 |
1150 | {% endblock content %}
1151 |
1152 |
1153 | {% block extrajs %}
1154 |
1166 | {% endblock %}
1167 |
1168 |
1169 | app_label and model_name are just strings, and as such can be specified in an url.
1170 |
1171 | The connection with the Django backend uses the following generic url::
1172 |
1173 | {% url 'frontend:object-datatable' model|app_label model|model_name %}
1174 |
1175 | from `urls.py`::
1176 |
1177 | # List any Model
1178 | path('datatable///', datatables_views.object_datatable_view, name="object-datatable"),
1179 |
1180 | object_datatable_view() is a lookup helper which navigates all DatatablesView-derived
1181 | classes in the module and selects the view appropriate for the specific model
1182 | in use:
1183 |
1184 | file `frontend/datatables_views.py`:
1185 |
1186 | .. code:: python
1187 |
1188 | import inspect
1189 |
1190 | def object_datatable_view(request, app_label, model_name):
1191 |
1192 | # List all DatatablesView in this module
1193 | datatable_views = [
1194 | klass
1195 | for name, klass in inspect.getmembers(sys.modules[__name__])
1196 | if inspect.isclass(klass) and issubclass(klass, DatatablesView)
1197 | ]
1198 |
1199 | # Scan DatatablesView until we find the right one
1200 | for datatable_view in datatable_views:
1201 | model = datatable_view.model
1202 | if (model is not None and (model._meta.app_label, model._meta.model_name) == (app_label, model_name)):
1203 | view = datatable_view
1204 | break
1205 |
1206 | return view.as_view()(request)
1207 |
1208 | which for this example happens to be:
1209 |
1210 | .. code:: python
1211 |
1212 | @method_decorator(login_required, name='dispatch')
1213 | class ChannelDatatablesView(BaseDatatablesView):
1214 |
1215 | model = Channel
1216 | title = 'Channels'
1217 |
1218 | column_defs = [
1219 | DatatablesView.render_row_tools_column_def(),
1220 | {
1221 | 'name': 'id',
1222 | 'visible': False,
1223 | }, {
1224 | 'name': 'description',
1225 | }, {
1226 | 'name': 'code',
1227 | }
1228 | ]
1229 |
1230 | Javascript Code Snippets
1231 | ========================
1232 |
1233 | Workaround: Adjust the column widths of all visible tables
1234 | ----------------------------------------------------------
1235 |
1236 | .. code:: javascript
1237 |
1238 | setTimeout(function () {
1239 | DatatablesViewUtils.adjust_table_columns();
1240 | }, 200);
1241 |
1242 | or maybe better:
1243 |
1244 | .. code:: javascript
1245 |
1246 | var table = element.DataTable({
1247 | ...
1248 | "initComplete": function(settings) {
1249 | setTimeout(function () {
1250 | DatatablesViewUtils.adjust_table_columns();
1251 | }, 200);
1252 | }
1253 |
1254 | where:
1255 |
1256 | .. code:: javascript
1257 |
1258 | function adjust_table_columns() {
1259 | // Adjust the column widths of all visible tables
1260 | // https://datatables.net/reference/api/%24.fn.dataTable.tables()
1261 | $.fn.dataTable
1262 | .tables({
1263 | visible: true,
1264 | api: true
1265 | })
1266 | .columns.adjust();
1267 | }
1268 |
1269 |
1270 | Redraw all tables
1271 | -----------------
1272 |
1273 | .. code:: javascript
1274 |
1275 | $.fn.dataTable.tables({
1276 | api: true
1277 | }).draw();
1278 |
1279 | Redraw table holding the current paging position
1280 | ------------------------------------------------
1281 |
1282 | .. code:: javascript
1283 |
1284 | table = $(element).closest('table.dataTable');
1285 | $.ajax({
1286 | type: 'GET',
1287 | url: ...
1288 | }).done(function(data, textStatus, jqXHR) {
1289 | table.DataTable().ajax.reload(null, false);
1290 | });
1291 |
1292 | Redraw a single table row
1293 | -------------------------
1294 |
1295 | TODO: THIS DOESN'T SEEM TO WORK PROPERLY 😭
1296 |
1297 | .. code:: javascript
1298 |
1299 | table.DataTable().row(tr).invalidate().draw();
1300 |
1301 | Example:
1302 |
1303 | .. code:: javascript
1304 |
1305 | var table = $(element).closest('table.dataTable');
1306 | var table_row_id = table.find('tr.shown').attr('id');
1307 | $.ajax({
1308 | type: 'POST',
1309 | url: ...
1310 | }).done(function(data, textStatus, jqXHR) {
1311 | table.DataTable().ajax.reload(null, false);
1312 |
1313 | // Since we've update the record via Ajax, we need to redraw this table row
1314 | var tr = table.find('#' + table_row_id);
1315 | var row = table.DataTable().row(tr)
1316 | row.invalidate().draw();
1317 |
1318 | // Hack: here we would like to enhance the updated row, by adding the 'updated' class;
1319 | // Since a callback is not available upon draw completion,
1320 | // let's use a timer to try later, and cross fingers
1321 | setTimeout(function() {
1322 | table.find('#' + table_row_id).addClass('updated');
1323 | }, 200);
1324 | setTimeout(function() {
1325 | table.find('#' + table_row_id).addClass('updated');
1326 | }, 1000);
1327 |
1328 | });
1329 |
1330 | change DataTables' error reporting mechanism
1331 | --------------------------------------------
1332 |
1333 | .. code:: javascript
1334 |
1335 | // change DataTables' error reporting mechanism to throw a Javascript
1336 | // error to the browser's console, rather than alerting it.
1337 | $.fn.dataTable.ext.errMode = 'throw';
1338 |
1339 |
1340 | JS Utilities
1341 | ============
1342 |
1343 | - DatatablesViewUtils.init(options)
1344 | - DatatablesViewUtils.initialize_table(element, url, extra_options={}, extra_data={})
1345 | - DatatablesViewUtils.after_table_initialization(table, data, url)
1346 | - DatatablesViewUtils.adjust_table_columns()
1347 | - DatatablesViewUtils.redraw_all_tables()
1348 | - DatatablesViewUtils.redraw_table(element)
1349 |
1350 | Internationalization
1351 | --------------------
1352 |
1353 | You can provide localized messages by initializing the DatatablesViewUtils JS module
1354 | as follow (example in italian):
1355 |
1356 | .. code:: javascript
1357 |
1358 | DatatablesViewUtils.init({
1359 | search_icon_html: '',
1360 | language: {
1361 | "decimal": "",
1362 | "emptyTable": "Nessun dato disponibile",
1363 | "info": "Visualizzate da _START_ a _END_ di _TOTAL_ righe",
1364 | "infoEmpty": "",
1365 | "infoFiltered": "(filtered from _MAX_ total entries)",
1366 | "infoPostFix": "",
1367 | "thousands": ",",
1368 | "lengthMenu": "Visualizza _MENU_ righe per pagina",
1369 | "loadingRecords": "Caricamento in corso ...",
1370 | "processing": "Elaborazione in corso ...",
1371 | "search": "Cerca:",
1372 | "zeroRecords": "Nessun record trovato",
1373 | "paginate": {
1374 | "first": "Prima",
1375 | "last": "Ultima",
1376 | "next": ">>",
1377 | "previous": "<<"
1378 | },
1379 | "aria": {
1380 | "sortAscending": ": activate to sort column ascending",
1381 | "sortDescending": ": activate to sort column descending"
1382 | }
1383 | }
1384 | });
1385 |
1386 |
1387 | You can do this, for example, in your "base.html" template, and it will be in effect
1388 | for all subsequent instantiations:
1389 |
1390 | .. code:: html
1391 |
1392 |
1399 |
1400 |
1401 | Application examples
1402 | ====================
1403 |
1404 | Customize row details by rendering prettified json fields
1405 | ---------------------------------------------------------
1406 |
1407 | .. image:: screenshots/009.png
1408 |
1409 | .. code:: python
1410 |
1411 | import jsonfield
1412 | from datatables_view.views import DatatablesView
1413 | from .utils import json_prettify
1414 |
1415 |
1416 | class MyDatatablesView(DatatablesView):
1417 |
1418 | ...
1419 |
1420 | def render_row_details(self, id, request=None):
1421 |
1422 | obj = self.model.objects.get(id=id)
1423 | fields = [f for f in self.model._meta.get_fields() if f.concrete]
1424 | html = '
'
1425 | for field in fields:
1426 | value = getattr(obj, field.name)
1427 | if isinstance(field, jsonfield.JSONField):
1428 | value = json_prettify(value)
1429 | html += '
%s
%s
' % (field.name, value)
1430 | html += '
'
1431 | return html
1432 |
1433 | where:
1434 |
1435 | .. code:: python
1436 |
1437 | import json
1438 | from pygments import highlight
1439 | from pygments.lexers import JsonLexer
1440 | from pygments.formatters import HtmlFormatter
1441 | from django.utils.safestring import mark_safe
1442 |
1443 |
1444 | def json_prettify_styles():
1445 | """
1446 | Used to generate Pygment styles (to be included in a .CSS file) as follows:
1447 | print(json_prettify_styles())
1448 | """
1449 | formatter = HtmlFormatter(style='colorful')
1450 | return formatter.get_style_defs()
1451 |
1452 |
1453 | def json_prettify(json_data):
1454 | """
1455 | Adapted from:
1456 | https://www.pydanny.com/pretty-formatting-json-django-admin.html
1457 | """
1458 |
1459 | # Get the Pygments formatter
1460 | formatter = HtmlFormatter(style='colorful')
1461 |
1462 | # Highlight the data
1463 | json_text = highlight(
1464 | json.dumps(json_data, indent=2),
1465 | JsonLexer(),
1466 | formatter
1467 | )
1468 |
1469 | # # remove leading and trailing brances
1470 | # json_text = json_text \
1471 | # .replace('{\n', '') \
1472 | # .replace('}\n', '')
1473 |
1474 | # Get the stylesheet
1475 | #style = ""
1476 | style = ''
1477 |
1478 | # Safe the output
1479 | return mark_safe(style + json_text)
1480 |
1481 |
1482 | Change row color based on row content
1483 | -------------------------------------
1484 |
1485 | .. image:: screenshots/010.png
1486 |
1487 | First, we mark the relevant info with a specific CSS class, so we can search
1488 | for it later
1489 |
1490 | .. code:: html
1491 |
1492 | column_defs = [
1493 | ...
1494 | }, {
1495 | 'name': 'error_counter',
1496 | 'title': 'errors',
1497 | 'className': 'error_counter',
1498 | }, {
1499 | ...
1500 | ]
1501 |
1502 | Have a callback called after each table redraw
1503 |
1504 | .. code:: javascript
1505 |
1506 | var table = element.DataTable({
1507 | ...
1508 | });
1509 |
1510 | table.on('draw.dt', function(event) {
1511 | onTableDraw(event);
1512 | });
1513 |
1514 | then change the rendered table as needed
1515 |
1516 | .. code:: javascript
1517 |
1518 | var onTableDraw = function (event) {
1519 |
1520 | var html_table = $(event.target);
1521 | html_table.find('tr').each(function(index, item) {
1522 |
1523 | try {
1524 | var row = $(item);
1525 | text = row.children('td.error_counter').first().text();
1526 | var error_counter = isNaN(text) ? 0 : parseInt(text);
1527 |
1528 | if (error_counter > 0) {
1529 | row.addClass('bold');
1530 | }
1531 | else {
1532 | row.addClass('grayed');
1533 | }
1534 | }
1535 | catch(err) {
1536 | }
1537 |
1538 | });
1539 | }
1540 |
1541 | **or use a rowCallback as follows:**
1542 |
1543 | .. code:: html
1544 |
1545 | // Subscribe "rowCallback" event
1546 | $('#datatable').on('rowCallback', function(event, table, row, data ) {
1547 | $(row).addClass(data.read ? 'read' : 'unread');
1548 | }
1549 |
1550 | This works even if the 'read' column we're interested in is actually not visible.
1551 |
1552 |
1553 | Modify table content on the fly (via ajax)
1554 | ------------------------------------------
1555 |
1556 | .. image:: screenshots/008.png
1557 |
1558 | Row details customization:
1559 |
1560 | .. code:: javascript
1561 |
1562 | def render_row_details(self, id, request=None):
1563 |
1564 | obj = self.model.objects.get(id=id)
1565 | html = '
'
1566 | html += "
alarm status:
"
1567 | for choice in BaseTask.ALARM_STATUS_CHOICES:
1568 | # Lo stato corrente lo visualizziamo in grassetto
1569 | if choice[0] == obj.alarm:
1570 | html += '%s ' % (choice[1])
1571 | else:
1572 | # Se non "unalarmed", mostriamo i link per cambiare lo stato
1573 | # (tutti tranne "unalarmed")
1574 | if obj.alarm != BaseTask.ALARM_STATUS_UNALARMED and choice[0] != BaseTask.ALARM_STATUS_UNALARMED:
1575 | html += '%s ' % (
1576 | str(obj.id),
1577 | choice[0],
1578 | choice[1]
1579 | )
1580 | html += '
'
1581 |
1582 | Client-side code:
1583 |
1584 | .. code:: javascript
1585 |
1586 |
71 |
72 | */
73 | _options = options;
74 |
75 | if (!('language' in _options)) {
76 | _options.language = {};
77 | }
78 | }
79 |
80 |
81 | function _handle_column_filter(table, data, target) {
82 | var index = target.data('index');
83 | var value = target.val();
84 |
85 | var column = table.api().column(index);
86 | var old_value = column.search();
87 | console.log('Request to search value %o in column %o (current value: %o)', value, index, old_value);
88 | if (value != old_value) {
89 | console.log('searching ...');
90 | column.search(value).draw();
91 | }
92 | else {
93 | console.log('skipped');
94 | }
95 | };
96 |
97 | /*
98 | function getCookie(cname) {
99 | var name = cname + "=";
100 | var ca = document.cookie.split(';');
101 | for(var i=0; i');
130 | $(item.choices).each(function(index, choice) {
131 | var option = $("