Skip to content

Commit 1d57496

Browse files
MiltonLnMiltonLn
MiltonLn
authored and
MiltonLn
committed
Initial work for the datatables_listview, this is far away to be functional, just commit to do PC switch and take a bath.
1 parent 701dfd3 commit 1d57496

File tree

9 files changed

+412
-0
lines changed

9 files changed

+412
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,6 @@ ENV/
8787

8888
# Rope project settings
8989
.ropeproject
90+
91+
# Pycharm
92+
.idea/

datatables_listview/core/__init__.py

Whitespace-only changes.

datatables_listview/core/utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django.db.models import Q
2+
3+
4+
def generate_q_objects_by_fields_and_words(fields, search_text):
5+
"""
6+
Author: Milton Lenis
7+
Date: 16 April 2017
8+
A handy method to generate Q Objects for filtering the queryset, this generates a Q Object by each field with each
9+
word, that means, if you have a search_term with 3 words and 3 fields this generates 9 Q Objects.
10+
11+
This also looks if the field has choices, if so, it cast to the choices internal representation to do the Q Object
12+
creation correctly
13+
14+
Because Q Objects internally are subclasses of django.utils.tree.Node we can 'add' Q Objects with connectors,
15+
see: https://bradmontgomery.net/blog/adding-q-objects-in-django/
16+
"""
17+
q = Q()
18+
search_text = search_text.split(" ")
19+
for field in fields:
20+
for word in search_text:
21+
if field.choices:
22+
# Build a dict with the dictionary to do search by choices display
23+
field_choices = {key.lower(): value for key, value in field.choices}
24+
# Search if the searched word exists in the field choices
25+
display_to_value = field_choices.get(word.lower(), "")
26+
if display_to_value:
27+
search_criteria = {field.name: display_to_value}
28+
q.add(
29+
Q(**search_criteria),
30+
Q.OR
31+
)
32+
else:
33+
instruction = "%s__icontains" % field.name
34+
search_criteria = {instruction: word}
35+
# Adding q objects with OR connector
36+
q.add(
37+
Q(**search_criteria),
38+
Q.OR
39+
)
40+
return q

datatables_listview/core/views.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from collections import namedtuple
2+
3+
from django.core.exceptions import ImproperlyConfigured
4+
from django.http import JsonResponse
5+
6+
from .utils import generate_q_objects_by_fields_and_words
7+
8+
9+
class DatatablesListView:
10+
"""
11+
Author: Milton Lenis
12+
Date: 16 April 2017
13+
View implementation for datatables_listview
14+
"""
15+
model = None
16+
queryset = None
17+
fields = None
18+
19+
def get_queryset(self):
20+
"""
21+
Author: Milton Lenis
22+
Date: 16 April 2017
23+
Method to get the queryset or generate one with the model
24+
"""
25+
if self.queryset is None:
26+
if self.model:
27+
return self.model._default_manager.all()
28+
else:
29+
raise ImproperlyConfigured(
30+
"%(cls)s is missing a QuerySet. Define "
31+
"%(cls)s.model, %(cls)s.queryset, or override "
32+
"%(cls)s.get_queryset()." % {
33+
'cls': self.__class__.__name__
34+
}
35+
)
36+
return self.queryset.all()
37+
38+
def get_field_names(self):
39+
"""
40+
Author: Milton Lenis
41+
Date: 16 April 2017
42+
Method to get the fields definition for this listview, return all of the model fields if the developer doesn't
43+
setup the fields
44+
"""
45+
if self.fields is None:
46+
return [field.name for field in self.model._meta.get_field_names()]
47+
else:
48+
return self.fields
49+
50+
def get_field_instances(self):
51+
"""
52+
Author: Milton Lenis
53+
Date: 16 April 2017
54+
Method to get all the field instances using the Django's Model _meta API
55+
"""
56+
field_names = self.get_field_names()
57+
return [self.model._meta.get_field(field_name) for field_name in field_names]
58+
59+
def get_draw_parameters(self, request):
60+
"""
61+
Author: Milton Lenis
62+
Date: 16 April 2017
63+
Method to extracting the draw parameters from the request, those parameters are sent by datatables server-side
64+
"""
65+
# Using namedtuple for readability, the var name is capitalized because this namedtuple is used like a class
66+
Draw = namedtuple('Draw', ['start', 'end', 'sort_column', 'sort_order', 'search', 'draw'])
67+
68+
start = int(request.GET.get('start', 0))
69+
end = start + int(request.GET.get('length', 0))
70+
sort_column = self.get_field_names()[int(request.GET.get('order[0][column]', 0))]
71+
sort_order = request.GET.get('order[0][dir]', "")
72+
search = request.GET.get('search[value]', "")
73+
draw = int(request.GET.get('draw', 0))
74+
75+
return Draw(start, end, sort_column, sort_order, search, draw)
76+
77+
def filter_by_search_text(self, queryset, search_text):
78+
"""
79+
Author: Milton Lenis
80+
Date: 16 April 2017
81+
Method to filter the queryset given a search_text, this filters the queryset by each one of the fields doing
82+
icontains lookup and spliting the search_text into words
83+
"""
84+
# First we need to generate Q objects to do the filtering all over the queryset
85+
q_objects_for_filtering = generate_q_objects_by_fields_and_words(self.get_field_instances(), search_text)
86+
return queryset.filter(q_objects_for_filtering)
87+
88+
def filter_by_draw_parameters(self, queryset, draw_parameters):
89+
"""
90+
Author: Milton Lenis
91+
Date: 16 April 2017
92+
Method ot filter the queryset with the draw parameters, this returns the queryset sorted (Depending of the
93+
order) by a column and slices the queryset by a start position and end position
94+
"""
95+
sort_order = ""
96+
if draw_parameters.sort_order == "desc":
97+
sort_order = "-"
98+
sort_criteria = "{0}{1}".format(sort_order, draw_parameters.sort_column)
99+
return queryset.order_by(sort_criteria)[draw_parameters.start:draw_parameters.end]
100+
101+
def generate_data(self, request):
102+
"""
103+
Author: Milton Lenis
104+
Date: 16 April 2017
105+
Method to generate the final data required for the JsonResponse, it returns a dictionary with the data
106+
formated as datatables requires it
107+
"""
108+
draw_parameters = self.get_draw_parameters(request)
109+
queryset = self.get_queryset()
110+
111+
if draw_parameters.search:
112+
queryset = self.filter_by_search_text(queryset, draw_parameters.search)
113+
114+
objects_count = queryset.count()
115+
# This uses the 'filter_by_draw_params' to filter the queryset according with the draw parameters
116+
# This means: it's the filter for the displaying page of the datatable
117+
queryset = self.filter_by_draw_parameters(queryset, draw_parameters)
118+
if self.get_options_list():
119+
generated_rows = self.generate_rows_with_options()
120+
else:
121+
generated_rows = self.generate_rows()
122+
return {
123+
'draw': draw_parameters.draw,
124+
'recordsTotal': objects_count,
125+
'recodsFiltered': objects_count,
126+
'data': generated_rows
127+
}
128+
129+
def get(self, request, *args, **kwargs):
130+
"""
131+
Author: Milton Lenis
132+
Date: April 16 2017
133+
If the request is ajax it returns the requested data as a JSON, if not, it calls the super to give a normal
134+
http response and show the template
135+
"""
136+
if request.is_ajax():
137+
final_data = self.generate_data(request)
138+
return JsonResponse(final_data)
139+
else:
140+
return super(DatatablesListView, self).get(request, *args, **kwargs)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// Pipelining function for DataTables. To be used to the `ajax` option of DataTables
3+
//
4+
$.fn.dataTable.pipeline = function ( opts ) {
5+
// Configuration options
6+
var conf = $.extend( {
7+
pages: 5, // number of pages to cache
8+
url: '', // script url
9+
data: null, // function or object with parameters to send to the server
10+
// matching how `ajax.data` works in DataTables
11+
method: 'GET' // Ajax HTTP method
12+
}, opts );
13+
14+
// Private variables for storing the cache
15+
var cacheLower = -1;
16+
var cacheUpper = null;
17+
var cacheLastRequest = null;
18+
var cacheLastJson = null;
19+
20+
return function ( request, drawCallback, settings ) {
21+
var ajax = false;
22+
var requestStart = request.start;
23+
var drawStart = request.start;
24+
var requestLength = request.length;
25+
var requestEnd = requestStart + requestLength;
26+
27+
if ( settings.clearCache ) {
28+
// API requested that the cache be cleared
29+
ajax = true;
30+
settings.clearCache = false;
31+
}
32+
else if ( cacheLower < 0 || requestStart < cacheLower || requestEnd > cacheUpper ) {
33+
// outside cached data - need to make a request
34+
ajax = true;
35+
}
36+
else if ( JSON.stringify( request.order ) !== JSON.stringify( cacheLastRequest.order ) ||
37+
JSON.stringify( request.columns ) !== JSON.stringify( cacheLastRequest.columns ) ||
38+
JSON.stringify( request.search ) !== JSON.stringify( cacheLastRequest.search )
39+
) {
40+
// properties changed (ordering, columns, searching)
41+
ajax = true;
42+
}
43+
44+
// Store the request for checking next time around
45+
cacheLastRequest = $.extend( true, {}, request );
46+
47+
if ( ajax ) {
48+
// Need data from the server
49+
if ( requestStart < cacheLower ) {
50+
requestStart = requestStart - (requestLength*(conf.pages-1));
51+
52+
if ( requestStart < 0 ) {
53+
requestStart = 0;
54+
}
55+
}
56+
57+
cacheLower = requestStart;
58+
cacheUpper = requestStart + (requestLength * conf.pages);
59+
60+
request.start = requestStart;
61+
request.length = requestLength*conf.pages;
62+
63+
// Provide the same `data` options as DataTables.
64+
if ( $.isFunction ( conf.data ) ) {
65+
// As a function it is executed with the data object as an arg
66+
// for manipulation. If an object is returned, it is used as the
67+
// data object to submit
68+
var d = conf.data( request );
69+
if ( d ) {
70+
$.extend( request, d );
71+
}
72+
}
73+
else if ( $.isPlainObject( conf.data ) ) {
74+
// As an object, the data given extends the default
75+
$.extend( request, conf.data );
76+
}
77+
78+
settings.jqXHR = $.ajax( {
79+
"type": conf.method,
80+
"url": conf.url,
81+
"data": request,
82+
"dataType": "json",
83+
"cache": false,
84+
"success": function ( json ) {
85+
cacheLastJson = $.extend(true, {}, json);
86+
87+
if ( cacheLower != drawStart ) {
88+
json.data.splice( 0, drawStart-cacheLower );
89+
}
90+
if ( requestLength >= -1 ) {
91+
json.data.splice( requestLength, json.data.length );
92+
}
93+
94+
drawCallback( json );
95+
}
96+
} );
97+
}
98+
else {
99+
json = $.extend( true, {}, cacheLastJson );
100+
json.draw = request.draw; // Update the echo for each response
101+
json.data.splice( 0, requestStart-cacheLower );
102+
json.data.splice( requestLength, json.data.length );
103+
104+
drawCallback(json);
105+
}
106+
}
107+
};
108+
109+
// Register an API method that will empty the pipelined data, forcing an Ajax
110+
// fetch on the next draw (i.e. `table.clearPipeline().draw()`)
111+
$.fn.dataTable.Api.register( 'clearPipeline()', function () {
112+
return this.iterator( 'table', function ( settings ) {
113+
settings.clearCache = true;
114+
} );
115+
} );
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
$(document).ready(function () {
2+
3+
// Buttons definition for the datatable
4+
try{
5+
var custom_buttons = custom_buttons;
6+
}catch(err){
7+
var custom_buttons = null;
8+
}
9+
var final_buttons;
10+
if(custom_buttons){
11+
final_buttons = custom_buttons;
12+
}else{
13+
final_buttons = [
14+
{
15+
extend: 'copy',
16+
exportOptions: {
17+
columns: ':not(:last)'
18+
},
19+
text: 'Copy'
20+
},
21+
{
22+
extend: 'csv',
23+
exportOptions: {
24+
columns: ':not(:last)'
25+
},
26+
text: 'CSV'
27+
},
28+
{
29+
extend: 'excel',
30+
exportOptions: {
31+
columns: ':not(:last)'
32+
},
33+
text: 'Excel'
34+
},
35+
{
36+
extend: 'pdf',
37+
exportOptions: {
38+
columns: ':not(:last)'
39+
},
40+
text: 'PDF'
41+
},
42+
43+
{
44+
extend: 'print',
45+
customize: function (win) {
46+
$(win.document.body).addClass('white-bg');
47+
$(win.document.body).css('font-size', '10px');
48+
49+
$(win.document.body).find('table')
50+
.addClass('compact')
51+
.css('font-size', 'inherit');
52+
},
53+
exportOptions: {
54+
columns: ':not(:last)'
55+
},
56+
text: 'Imprimir'
57+
}
58+
]
59+
}
60+
// =========================================================================================================
61+
62+
// Columns definition
63+
var columns = column_defs;
64+
65+
if(opciones){
66+
columns.push({"title": "Options", "targets": columns.length, "orderable": false, "searchable":false});
67+
}
68+
69+
//==========================================================================================================
70+
71+
$("#datatable-rady").DataTable({
72+
dom: '<"html5buttons"B>lTfgitp',
73+
language: {
74+
"url": "//cdn.datatables.net/plug-ins/1.10.11/i18n/Spanish.json"
75+
},
76+
responsive: true,
77+
bAutoWidth: false,
78+
pageLength: 10,
79+
columnDefs: columns,
80+
serverSide: true,
81+
processing: true,
82+
ajax: $.fn.dataTable.pipeline({
83+
url: "",
84+
pages: 5 // number of pages to cache
85+
}),
86+
buttons: final_buttons
87+
88+
});
89+
});

0 commit comments

Comments
 (0)