├── README.md
├── app
├── __init__.py
├── common
│ ├── __init__.py
│ └── routes.py
├── config.py
├── mod_tables
│ ├── __init__.py
│ ├── controllers.py
│ ├── models.py
│ └── serverside
│ │ ├── __init__.py
│ │ ├── serverside_table.py
│ │ └── table_schemas.py
├── static
│ └── js
│ │ ├── clientside_table.js
│ │ └── serverside_table.js
└── templates
│ ├── clientside_table.html
│ ├── index.html
│ ├── serverside_table.html
│ └── template.html
└── resources
└── serverside.png
/README.md:
--------------------------------------------------------------------------------
1 | # datatables-flask-serverside
2 |
3 | The main purpose of this repository is to create a reusable class (ServerSideTable) that manages the server-side data processing for DataTables in Flask back-ends.
4 |
5 | Although it contains all the boilerplate to make the example runnable, the reusable part is the folder called [serverside](app/mod_tables/serverside) and it is composed by two files:
6 | * [serverside_table.py](app/mod_tables/serverside/serverside_table.py):
7 | * It contains the **ServerSideTable** class. It is NOT necessary to touch it.
8 | * [table_schemas.py](app/mod_tables/serverside/table_schemas.py):
9 | * It defines the schemas of the server-side tables we want to display.
10 | * Each schema is a list of Python dictionaries that represents each of the table's columns. The columns can be configured with the following fields:
11 | * **data_name**: Name of the field in the data source.
12 | * **column_name**: Name of the column in the table.
13 | * **default**: Value that will be displayed in case there's no data for the previous data_name.
14 | * **order**: Order of the column in the table.
15 | * **searchable**: Whether the column will be taken into account while searching a value.
16 |
17 | ## How to run the example?
18 |
19 | In order to run this example, you just need to have flask installed and run the following command from the root of the repository:
20 |
21 | `FLASK_APP=app/__init__.py flask run`
22 |
23 | Then, go to [127.0.0.1:5000/](http://127.0.0.1:5000/) in any browser and you will be able to see both the client-side and the server-side tables:
24 |
25 |
26 | 
27 |
28 |
29 | ## How to adapt the example to your own project?
30 |
31 | Assuming that you already have a Flask app with DataTables and you want to add a server-side table, you have to follow these steps:
32 | 1. Include the [serverside](app/mod_tables/serverside) directory into your project.
33 | 2. Add the schema of your table in the [table_schemas.py](app/mod_tables/serverside/table_schemas.py), as it is done with *SERVERSIDE_TABLE_COLUMNS*.
34 | 3. In your Flask back-end, as it is done [here](app/mod_tables/models.py), create a **ServerSideTable** object by passing the following parameters to that constructor of the class:
35 | * The request object provided by Flask.
36 | * A list of dictionaries with the data that will fill the table (A dictionary per row).
37 | * The schema that was defined in the previous step.
38 | 4. In the HTML file, add a table tag, specifying the column names:
39 |
40 | ```HTML
41 |
42 |
43 |
44 |
Column A
45 |
Column B
46 |
Column C
47 |
Column D
48 |
49 |
50 |
51 | ```
52 |
53 | 5. In the JS file, define the table with the **bProcessing** and **bServerSide** attributes as true. Don't forget to specify the endpoint that will process the data in your Flask back-end with the attribute **sAjaxSource** (e.g. */tables/serverside_table*).
54 |
55 | ```javascript
56 | $(document).ready(function () {
57 | $('#table_id').DataTable({
58 | bProcessing: true,
59 | bServerSide: true,
60 | sPaginationType: "full_numbers",
61 | lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]],
62 | bjQueryUI: true,
63 | sAjaxSource: '',
64 | columns: [
65 | {"data": "Column A"},
66 | {"data": "Column B"},
67 | {"data": "Column C"},
68 | {"data": "Column D"}
69 | ]
70 | });
71 | });
72 | ```
73 | 6. Enjoy your brand new table!
74 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, redirect, session
2 | from app.mod_tables.models import TableBuilder
3 |
4 |
5 | flask_app = Flask(__name__)
6 |
7 | table_builder = TableBuilder()
8 |
9 |
10 | from app.common.routes import main
11 | from app.mod_tables.controllers import tables
12 |
13 |
14 | # Register the different blueprints
15 | flask_app.register_blueprint(main)
16 | flask_app.register_blueprint(tables)
17 |
--------------------------------------------------------------------------------
/app/common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SergioLlana/datatables-flask-serverside/d968c95991b14ff9a485287be25d24d238c6eaf6/app/common/__init__.py
--------------------------------------------------------------------------------
/app/common/routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template
2 |
3 | main = Blueprint('main', __name__, url_prefix='')
4 |
5 |
6 | @main.route("/")
7 | def index():
8 | return render_template("index.html")
9 |
10 | @main.route("/clientside_table")
11 | def clientside_table():
12 | return render_template("clientside_table.html")
13 |
14 | @main.route("/serverside_table")
15 | def serverside_table():
16 | return render_template("serverside_table.html")
17 |
--------------------------------------------------------------------------------
/app/config.py:
--------------------------------------------------------------------------------
1 | DEBUG = False
2 |
--------------------------------------------------------------------------------
/app/mod_tables/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SergioLlana/datatables-flask-serverside/d968c95991b14ff9a485287be25d24d238c6eaf6/app/mod_tables/__init__.py
--------------------------------------------------------------------------------
/app/mod_tables/controllers.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request
2 | from app import table_builder
3 |
4 |
5 | tables = Blueprint('tables', __name__, url_prefix='/tables')
6 |
7 |
8 | @tables.route("/clientside_table", methods=['GET'])
9 | def clientside_table_content():
10 | data = table_builder.collect_data_clientside()
11 | return jsonify(data)
12 |
13 |
14 | @tables.route("/serverside_table", methods=['GET'])
15 | def serverside_table_content():
16 | data = table_builder.collect_data_serverside(request)
17 | return jsonify(data)
18 |
--------------------------------------------------------------------------------
/app/mod_tables/models.py:
--------------------------------------------------------------------------------
1 | from app.mod_tables.serverside.serverside_table import ServerSideTable
2 | from app.mod_tables.serverside import table_schemas
3 |
4 | DATA_SAMPLE = [
5 | {'A': 'Hello!', 'B': 'How is it going?', 'C': 3, 'D': 4},
6 | {'A': 'These are sample texts', 'B': 0, 'C': 5, 'D': 6},
7 | {'A': 'Mmmm', 'B': 'I do not know what to say', 'C': 7, 'D': 16},
8 | {'A': 'Is it enough?', 'B': 'Okay', 'C': 8, 'D': 9},
9 | {'A': 'Just one more', 'B': '...', 'C': 10, 'D': 11},
10 | {'A': 'Thanks!', 'B': 'Goodbye.', 'C': 12, 'D': 13}
11 | ]
12 |
13 | class TableBuilder(object):
14 |
15 | def collect_data_clientside(self):
16 | return {'data': DATA_SAMPLE}
17 |
18 | def collect_data_serverside(self, request):
19 | columns = table_schemas.SERVERSIDE_TABLE_COLUMNS
20 | return ServerSideTable(request, DATA_SAMPLE, columns).output_result()
21 |
--------------------------------------------------------------------------------
/app/mod_tables/serverside/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SergioLlana/datatables-flask-serverside/d968c95991b14ff9a485287be25d24d238c6eaf6/app/mod_tables/serverside/__init__.py
--------------------------------------------------------------------------------
/app/mod_tables/serverside/serverside_table.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | class ServerSideTable(object):
5 | '''
6 | Retrieves the values specified by Datatables in the request and processes
7 | the data that will be displayed in the table (filtering, sorting and
8 | selecting a subset of it).
9 |
10 | Attributes:
11 | request: Values specified by DataTables in the request.
12 | data: Data to be displayed in the table.
13 | column_list: Schema of the table that will be built. It contains
14 | the name of each column (both in the data and in the
15 | table), the default values (if available) and the
16 | order in the HTML.
17 | '''
18 | def __init__(self, request, data, column_list):
19 | self.result_data = None
20 | self.cardinality_filtered = 0
21 | self.cardinality = 0
22 |
23 | self.request_values = request.values
24 | self.columns = sorted(column_list, key=lambda col: col['order'])
25 |
26 | rows = self._extract_rows_from_data(data)
27 | self._run(rows)
28 |
29 | def _run(self, data):
30 | '''
31 | Prepares the data, and values that will be generated as output.
32 | It does the actual filtering, sorting and paging of the data.
33 |
34 | Args:
35 | data: Data to be displayed by DataTables.
36 | '''
37 | self.cardinality = len(data) # Total num. of rows
38 |
39 | filtered_data = self._custom_filter(data)
40 | self.cardinality_filtered = len(filtered_data) # Num. displayed rows
41 |
42 | sorted_data = self._custom_sort(filtered_data)
43 | self.result_data = self._custom_paging(sorted_data)
44 |
45 | def _extract_rows_from_data(self, data):
46 | '''
47 | Extracts the value of each column from the original data using the
48 | schema of the table.
49 |
50 | Args:
51 | data: Data to be displayed by DataTables.
52 |
53 | Returns:
54 | List of dicts that represents the table's rows.
55 | '''
56 | rows = []
57 | for x in data:
58 | row = {}
59 | for column in self.columns:
60 | default = column['default']
61 | data_name = column['data_name']
62 | column_name = column['column_name']
63 | row[column_name] = x.get(data_name, default)
64 | rows.append(row)
65 | return rows
66 |
67 | def _custom_filter(self, data):
68 | '''
69 | Filters out those rows that do not contain the values specified by the
70 | user using a case-insensitive regular expression.
71 |
72 | It takes into account only those columns that are 'searchable'.
73 |
74 | Args:
75 | data: Data to be displayed by DataTables.
76 |
77 | Returns:
78 | Filtered data.
79 | '''
80 | def check_row(row):
81 | ''' Checks whether a row should be displayed or not. '''
82 | for i in range(len(self.columns)):
83 | if self.columns[i]['searchable']:
84 | value = row[self.columns[i]['column_name']]
85 | regex = '(?i)' + self.request_values['sSearch']
86 | if re.compile(regex).search(str(value)):
87 | return True
88 | return False
89 |
90 | if self.request_values.get('sSearch', ""):
91 | return [row for row in data if check_row(row)]
92 | else:
93 | return data
94 |
95 | def _custom_sort(self, data):
96 | '''
97 | Sorts the rows taking in to account the column (or columns) that the
98 | user has selected.
99 |
100 | Args:
101 | data: Filtered data.
102 |
103 | Returns:
104 | Sorted data by the columns specified by the user.
105 | '''
106 | def is_reverse(str_direction):
107 | ''' Maps the 'desc' and 'asc' words to True or False. '''
108 | return True if str_direction == 'desc' else False
109 |
110 | if (self.request_values['iSortCol_0'] != "") and (int(self.request_values['iSortingCols']) > 0):
111 | for i in range(0, int(self.request_values['iSortingCols'])):
112 | column_number = int(self.request_values['iSortCol_' + str(i)])
113 | column_name = self.columns[column_number]['column_name']
114 | sort_direction = self.request_values['sSortDir_' + str(i)]
115 | data = sorted(data,
116 | key=lambda x: x[column_name],
117 | reverse=is_reverse(sort_direction))
118 |
119 | return data
120 | else:
121 | return data
122 |
123 | def _custom_paging(self, data):
124 | '''
125 | Selects a subset of the filtered and sorted data based on if the table
126 | has pagination, the current page and the size of each page.
127 |
128 | Args:
129 | data: Filtered and sorted data.
130 |
131 | Returns:
132 | Subset of the filtered and sorted data that will be displayed by
133 | the DataTables if the pagination is enabled.
134 | '''
135 | def requires_pagination():
136 | ''' Check if the table is going to be paginated '''
137 | if self.request_values['iDisplayStart'] != "":
138 | if int(self.request_values['iDisplayLength']) != -1:
139 | return True
140 | return False
141 |
142 | if not requires_pagination():
143 | return data
144 |
145 | start = int(self.request_values['iDisplayStart'])
146 | length = int(self.request_values['iDisplayLength'])
147 |
148 | # if search returns only one page
149 | if len(data) <= length:
150 | # display only one page
151 | return data[start:]
152 | else:
153 | limit = -len(data) + start + length
154 | if limit < 0:
155 | # display pagination
156 | return data[start:limit]
157 | else:
158 | # display last page of pagination
159 | return data[start:]
160 |
161 | def output_result(self):
162 | '''
163 | Generates a dict with the content of the response. It contains the
164 | required values by DataTables (echo of the reponse and cardinality
165 | values) and the data that will be displayed.
166 |
167 | Return:
168 | Content of the response.
169 | '''
170 | output = {}
171 | output['sEcho'] = str(int(self.request_values['sEcho']))
172 | output['iTotalRecords'] = str(self.cardinality)
173 | output['iTotalDisplayRecords'] = str(self.cardinality_filtered)
174 | output['data'] = self.result_data
175 | return output
176 |
--------------------------------------------------------------------------------
/app/mod_tables/serverside/table_schemas.py:
--------------------------------------------------------------------------------
1 | SERVERSIDE_TABLE_COLUMNS = [
2 | {
3 | "data_name": "A",
4 | "column_name": "Column A",
5 | "default": "",
6 | "order": 1,
7 | "searchable": True
8 | },
9 | {
10 | "data_name": "B",
11 | "column_name": "Column B",
12 | "default": "",
13 | "order": 2,
14 | "searchable": True
15 | },
16 | {
17 | "data_name": "C",
18 | "column_name": "Column C",
19 | "default": 0,
20 | "order": 3,
21 | "searchable": False
22 | },
23 | {
24 | "data_name": "D",
25 | "column_name": "Column D",
26 | "default": 0,
27 | "order": 4,
28 | "searchable": False
29 | }
30 | ]
31 |
--------------------------------------------------------------------------------
/app/static/js/clientside_table.js:
--------------------------------------------------------------------------------
1 | /*jslint browser: true*/
2 | /*global $*/
3 |
4 |
5 | $(document).ready(function () {
6 | $.get('/tables/clientside_table', function (data) {
7 | $('#clientside_table').DataTable({
8 | data: data.data,
9 | paging: true,
10 | dom: 'frtipB',
11 | columns: [
12 | {"data": "A", "title": "Column A"},
13 | {"data": "B", "title": "Column B"},
14 | {"data": "C", "title": "Column C"},
15 | {"data": "D", "title": "Column D"},
16 | ]
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/app/static/js/serverside_table.js:
--------------------------------------------------------------------------------
1 | /*jslint browser: true*/
2 | /*global $*/
3 |
4 |
5 | $(document).ready(function () {
6 | $('#serverside_table').DataTable({
7 | bProcessing: true,
8 | bServerSide: true,
9 | sPaginationType: "full_numbers",
10 | lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]],
11 | bjQueryUI: true,
12 | sAjaxSource: '/tables/serverside_table',
13 | columns: [
14 | {"data": "Column A"},
15 | {"data": "Column B"},
16 | {"data": "Column C"},
17 | {"data": "Column D"}
18 | ]
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/app/templates/clientside_table.html:
--------------------------------------------------------------------------------
1 | {% extends "template.html" %}
2 | {% block title %}
3 |
4 | Clientside Table
5 |
6 | {% endblock %}
7 | {% block body %}
8 |
9 |
10 |
11 |