├── 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 | ![Server-side Table](resources/serverside.png) 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 | 45 | 46 | 47 | 48 | 49 | 50 |
Column AColumn BColumn CColumn D
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 |
12 |
13 |
14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "template.html" %} 2 | {% block title %} 3 | 4 | Welcome! 5 | 6 | {% endblock %} 7 | {% block body %} 8 | 9 |

10 | Welcome! 11 | These are the tables: 12 |

13 | 17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /app/templates/serverside_table.html: -------------------------------------------------------------------------------- 1 | {% extends "template.html" %} 2 | {% block title %} 3 | 4 | Serverside Table 5 | 6 | {% endblock %} 7 | {% block body %} 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
Column AColumn BColumn CColumn D
22 |
23 | 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /app/templates/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | {% block body %}{% endblock %} 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /resources/serverside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SergioLlana/datatables-flask-serverside/d968c95991b14ff9a485287be25d24d238c6eaf6/resources/serverside.png --------------------------------------------------------------------------------