├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── datatables └── __init__.py ├── readme.rst ├── setup.py ├── tests ├── __init__.py ├── models.py └── test_basic.py └── travis_requirements.txt /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: trDaz6JVs4lTsjkVGDZiWXoNn76tkBvok -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "nightly" 8 | - "pypy" 9 | - "pypy3" 10 | # command to install dependencies 11 | install: 12 | - "pip install -r travis_requirements.txt" 13 | - "python setup.py install" 14 | # command to run tests 15 | script: coverage run --source=datatables -m pytest -v 16 | after_success: coveralls 17 | 18 | matrix: 19 | allow_failures: 20 | - "nightly" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Thomas Forbes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /datatables/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, namedtuple 2 | import re 3 | import inspect 4 | 5 | 6 | BOOLEAN_FIELDS = ( 7 | "search.regex", "searchable", "orderable", "regex" 8 | ) 9 | 10 | 11 | DataColumn = namedtuple("DataColumn", ("name", "model_name", "filter")) 12 | 13 | 14 | class DataTablesError(ValueError): 15 | pass 16 | 17 | 18 | class DataTable(object): 19 | def __init__(self, params, model, query, columns): 20 | self.params = params 21 | self.model = model 22 | self.query = query 23 | self.data = {} 24 | self.columns = [] 25 | self.columns_dict = {} 26 | self.search_func = lambda qs, s: qs 27 | self.column_search_func = lambda mc, qs, s: qs 28 | 29 | for col in columns: 30 | name, model_name, filter_func = None, None, None 31 | 32 | if isinstance(col, DataColumn): 33 | self.columns.append(col) 34 | continue 35 | elif isinstance(col, tuple): 36 | # col is either 1. (name, model_name), 2. (name, filter) or 3. (name, model_name, filter) 37 | if len(col) == 3: 38 | name, model_name, filter_func = col 39 | elif len(col) == 2: 40 | # Work out the second argument. If it is a function then it's type 2, else it is type 1. 41 | if callable(col[1]): 42 | name, filter_func = col 43 | model_name = name 44 | else: 45 | name, model_name = col 46 | else: 47 | raise ValueError("Columns must be a tuple of 2 to 3 elements") 48 | else: 49 | # It's just a string 50 | name, model_name = col, col 51 | 52 | d = DataColumn(name=name, model_name=model_name, filter=filter_func) 53 | self.columns.append(d) 54 | self.columns_dict[d.name] = d 55 | 56 | for column in (col for col in self.columns if "." in col.model_name): 57 | self.query = self.query.join(column.model_name.split(".")[0], aliased=True) 58 | 59 | def query_into_dict(self, key_start): 60 | returner = defaultdict(dict) 61 | 62 | # Matches columns[number][key] with an [optional_value] on the end 63 | pattern = "{}(?:\[(\d+)\])?\[(\w+)\](?:\[(\w+)\])?".format(key_start) 64 | 65 | columns = (param for param in self.params if re.match(pattern, param)) 66 | 67 | for param in columns: 68 | 69 | column_id, key, optional_subkey = re.search(pattern, param).groups() 70 | 71 | if column_id is None: 72 | returner[key] = self.coerce_value(key, self.params[param]) 73 | elif optional_subkey is None: 74 | returner[int(column_id)][key] = self.coerce_value(key, self.params[param]) 75 | else: 76 | # Oh baby a triple 77 | subdict = returner[int(column_id)].setdefault(key, {}) 78 | subdict[optional_subkey] = self.coerce_value("{}.{}".format(key, optional_subkey), 79 | self.params[param]) 80 | 81 | return dict(returner) 82 | 83 | @staticmethod 84 | def coerce_value(key, value): 85 | try: 86 | return int(value) 87 | except ValueError: 88 | if key in BOOLEAN_FIELDS: 89 | return value == "true" 90 | 91 | return value 92 | 93 | def get_integer_param(self, param_name): 94 | if param_name not in self.params: 95 | raise DataTablesError("Parameter {} is missing".format(param_name)) 96 | 97 | try: 98 | return int(self.params[param_name]) 99 | except ValueError: 100 | raise DataTablesError("Parameter {} is invalid".format(param_name)) 101 | 102 | def add_data(self, **kwargs): 103 | self.data.update(**kwargs) 104 | 105 | def json(self): 106 | try: 107 | return self._json() 108 | except DataTablesError as e: 109 | return { 110 | "error": str(e) 111 | } 112 | 113 | def get_column(self, column): 114 | if "." in column.model_name: 115 | column_path = column.model_name.split(".") 116 | relationship = getattr(self.model, column_path[0]) 117 | model_column = getattr(relationship.property.mapper.entity, column_path[1]) 118 | else: 119 | model_column = getattr(self.model, column.model_name) 120 | 121 | return model_column 122 | 123 | def searchable(self, func): 124 | self.search_func = func 125 | 126 | def searchable_column(self, func): 127 | self.column_search_func = func 128 | 129 | def _json(self): 130 | draw = self.get_integer_param("draw") 131 | start = self.get_integer_param("start") 132 | length = self.get_integer_param("length") 133 | 134 | columns = self.query_into_dict("columns") 135 | ordering = self.query_into_dict("order") 136 | search = self.query_into_dict("search") 137 | 138 | query = self.query 139 | total_records = query.count() 140 | 141 | if callable(self.search_func) and search.get("value", None): 142 | query = self.search_func(query, search["value"]) 143 | 144 | for column_data in columns.values(): 145 | search_value = column_data["search"]["value"] 146 | if ( 147 | not column_data["searchable"] 148 | or not search_value 149 | or not callable(self.column_search_func) 150 | ): 151 | continue 152 | 153 | column_name = column_data["data"] 154 | column = self.columns_dict[column_name] 155 | 156 | model_column = self.get_column(column) 157 | 158 | query = self.column_search_func(model_column, query, str(search_value)) 159 | 160 | for order in ordering.values(): 161 | direction, column = order["dir"], order["column"] 162 | 163 | if column not in columns: 164 | raise DataTablesError("Cannot order {}: column not found".format(column)) 165 | 166 | if not columns[column]["orderable"]: 167 | continue 168 | 169 | column_name = columns[column]["data"] 170 | column = self.columns_dict[column_name] 171 | 172 | model_column = self.get_column(column) 173 | 174 | if isinstance(model_column, property): 175 | raise DataTablesError("Cannot order by column {} as it is a property".format(column.model_name)) 176 | 177 | query = query.order_by(model_column.desc() if direction == "desc" else model_column.asc()) 178 | 179 | filtered_records = query.count() 180 | 181 | if length > 0: 182 | query = query.slice(start, start + length) 183 | 184 | return { 185 | "draw": draw, 186 | "recordsTotal": total_records, 187 | "recordsFiltered": filtered_records, 188 | "data": [ 189 | self.output_instance(instance) for instance in query.all() 190 | ] 191 | } 192 | 193 | def output_instance(self, instance): 194 | returner = { 195 | key.name: self.get_value(key, instance) for key in self.columns 196 | } 197 | 198 | if self.data: 199 | returner["DT_RowData"] = { 200 | k: v(instance) for k, v in self.data.items() 201 | } 202 | 203 | return returner 204 | 205 | def get_value(self, key, instance): 206 | attr = key.model_name 207 | if "." in attr: 208 | tmp_list=attr.split(".") 209 | attr=tmp_list[-1] 210 | for sub in tmp_list[:-1]: 211 | instance = getattr(instance, sub) 212 | 213 | if key.filter is not None: 214 | r = key.filter(instance) 215 | else: 216 | r = getattr(instance, attr) 217 | 218 | return r() if inspect.isroutine(r) else r 219 | -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | =============================================== 2 | datatables |PyPi Version| |TravisCI| |Coverage| 3 | =============================================== 4 | 5 | .. |PyPi Version| image:: http://img.shields.io/pypi/v/datatables.svg?style=flat 6 | :target: https://pypi.python.org/pypi/datatables 7 | 8 | .. |TravisCI| image:: https://api.travis-ci.org/orf/datatables.svg 9 | :target: https://travis-ci.org/orf/datatables 10 | 11 | .. |Coverage| image:: https://coveralls.io/repos/orf/datatables/badge.png?branch=master 12 | :target: https://coveralls.io/r/orf/datatables?branch=master 13 | 14 | 15 | 16 | 17 | Installation 18 | ------------ 19 | 20 | The package is available on `PyPI `_ and is tested on Python 2.7 to 3.4 21 | 22 | .. code-block:: bash 23 | 24 | pip install datatables 25 | 26 | Usage 27 | ----- 28 | 29 | Using Datatables is simple. Construct a DataTable instance by passing it your request parameters (or another dict-like 30 | object), your model class, a base query and a set of columns. The columns list can contain simple strings which are 31 | column names, or tuples containing (datatable_name, model_name), (datatable_name, model_name, filter_function) or 32 | (datatable_name, filter_function). 33 | 34 | Additional data such as hyperlinks can be added via DataTable.add_data, which accepts a callable that is called for 35 | each instance. Check out the usage example below for more info. 36 | 37 | 38 | Example 39 | ------- 40 | 41 | **models.py** 42 | 43 | .. code-block:: python 44 | 45 | class User(Base): 46 | __tablename__ = 'users' 47 | 48 | id = Column(Integer, primary_key=True) 49 | full_name = Column(Text) 50 | created_at = Column(DateTime, default=datetime.datetime.utcnow) 51 | 52 | # Use lazy=joined to prevent O(N) queries 53 | address = relationship("Address", uselist=False, backref="user", lazy="joined") 54 | 55 | class Address(Base): 56 | __tablename__ = 'addresses' 57 | 58 | id = Column(Integer, primary_key=True) 59 | description = Column(Text, unique=True) 60 | user_id = Column(Integer, ForeignKey('users.id')) 61 | 62 | **views.py (pyramid)** 63 | 64 | .. code-block:: python 65 | 66 | @view_config(route_name="data", request_method="GET", renderer="json") 67 | def users_data(request): 68 | # User.query = session.query(User) 69 | table = DataTable(request.GET, User, User.query, [ 70 | "id", 71 | ("name", "full_name", lambda i: "User: {}".format(i.full_name)), 72 | ("address", "address.description"), 73 | ]) 74 | table.add_data(link=lambda o: request.route_url("view_user", id=o.id)) 75 | table.searchable(lambda queryset, user_input: perform_search(queryset, user_input)) 76 | table.searchable_column( 77 | lambda model_column, queryset, user_input: 78 | perform_column_search(model_column, queryset, user_input) 79 | ) 80 | 81 | return table.json() 82 | 83 | **views.py (flask)** 84 | 85 | .. code-block:: python 86 | 87 | @app.route("/data") 88 | def datatables(): 89 | table = DataTable(request.args, User, db.session.query(User), [ 90 | "id", 91 | ("name", "full_name", lambda i: "User: {}".format(i.full_name)), 92 | ("address", "address.description"), 93 | ]) 94 | table.add_data(link=lambda obj: url_for('view_user', id=obj.id)) 95 | table.searchable(lambda queryset, user_input: perform_search(queryset, user_input)) 96 | table.searchable_column( 97 | lambda model_column, queryset, user_input: 98 | perform_column_search(model_column, queryset, user_input) 99 | ) 100 | 101 | return json.dumps(table.json()) 102 | 103 | **Global and individual column searching** 104 | 105 | .. code-block:: python 106 | 107 | def perform_search(queryset, user_input): 108 | return queryset.filter( 109 | db.or_( 110 | User.full_name.like('%' + user_input + '%'), 111 | Address.description.like('%' + user_input + '%') 112 | ) 113 | ) 114 | 115 | def perform_column_search(model_column, queryset, user_input): 116 | return queryset.filter(model_column.like("%" + user_input + "%")) 117 | 118 | **template.jinja2** 119 | 120 | .. code-block:: html 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
IdUser nameAddress
133 | 134 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os 3 | 4 | desc = open("readme.rst").read() if os.path.isfile("readme.rst") else "" 5 | 6 | 7 | setup( 8 | name='datatables', 9 | version='0.4.9', 10 | packages=['datatables'], 11 | url='https://github.com/orf/datatables/', 12 | license='MIT', 13 | long_description=desc, 14 | keywords='sqlalchemy datatables jquery pyramid flask', 15 | author='Tom', 16 | author_email='tom@tomforb.es', 17 | description='Integrates SQLAlchemy with DataTables (framework agnostic)', 18 | zip_safe=False, 19 | include_package_data=True, 20 | classifiers=[ 21 | 'Environment :: Web Environment', 22 | 'Framework :: Pyramid', 23 | 'Framework :: Flask', 24 | 'Framework :: Django', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Programming Language :: Python', 28 | 'Programming Language :: Python :: 2', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.2', 32 | 'Programming Language :: Python :: 3.3', 33 | 'Topic :: Internet :: WWW/HTTP', 34 | 'Topic :: Software Development :: Libraries', 35 | 'Topic :: Software Development :: Libraries :: Python Modules', 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'tom' 2 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy import Column, Integer, Text, DateTime, ForeignKey 5 | from sqlalchemy.orm import relationship 6 | 7 | Base = declarative_base() 8 | 9 | 10 | class User(Base): 11 | __tablename__ = 'users' 12 | 13 | id = Column(Integer, primary_key=True) 14 | full_name = Column(Text) 15 | created_at = Column(DateTime, default=datetime.datetime.utcnow) 16 | 17 | address = relationship("Address", uselist=False, backref="user") 18 | 19 | 20 | class Address(Base): 21 | __tablename__ = 'addresses' 22 | 23 | id = Column(Integer, primary_key=True) 24 | description = Column(Text, unique=True) 25 | user_id = Column(Integer, ForeignKey('users.id')) 26 | -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | import faker 4 | 5 | from .models import User, Address, Base 6 | from datatables import DataTable, DataColumn 7 | 8 | 9 | class TestDataTables: 10 | def setup_method(self, method): 11 | engine = create_engine('sqlite://', echo=True) 12 | Base.metadata.create_all(engine) 13 | Session = sessionmaker(bind=engine) 14 | 15 | self.session = Session() 16 | 17 | def make_data(self, user_count): 18 | f = faker.Faker() 19 | users = [] 20 | 21 | for i in range(user_count): 22 | user, addr = self.make_user(f.name(), f.address()) 23 | users.append(user) 24 | 25 | self.session.add_all(users) 26 | self.session.commit() 27 | 28 | def make_user(self, name, address): 29 | addr = Address() 30 | addr.description = address 31 | 32 | u = User() 33 | u.full_name = name 34 | u.address = addr 35 | 36 | return u, addr 37 | 38 | def make_params(self, order=None, search=None, column_search=None, start=0, length=10): 39 | x = { 40 | "draw": "1", 41 | "start": str(start), 42 | "length": str(length) 43 | } 44 | 45 | if column_search is None: 46 | column_search = {} 47 | 48 | for i, item in enumerate(("id", "name", "address")): 49 | b = "columns[{}]".format(i) 50 | x[b + "[data]"] = item 51 | x[b + "[name]"] = "" 52 | x[b + "[searchable]"] = "true" 53 | x[b + "[orderable]"] = "true" 54 | x[b + "[search][regex]"] = "false" 55 | x[b + "[search][value]"] = column_search.get(item, {}).get("value", "") 56 | 57 | for i, item in enumerate(order or []): 58 | for key, value in item.items(): 59 | x["order[{}][{}]".format(i, key)] = str(value) 60 | 61 | if search: 62 | for key, value in search.items(): 63 | x["search[{}]".format(key)] = str(value) 64 | 65 | return x 66 | 67 | def test_basic_function(self): 68 | self.make_data(10) 69 | 70 | req = self.make_params() 71 | 72 | table = DataTable(req, User, self.session.query(User), [ 73 | "id", 74 | ("name", "full_name"), 75 | ("address", "address.description"), 76 | ]) 77 | 78 | x = table.json() 79 | 80 | assert len(x["data"]) == 10 81 | 82 | def test_relation_ordering(self): 83 | self.make_data(10) 84 | u1, addr_asc = self.make_user("SomeUser", "0" * 15) 85 | u2, addr_desc = self.make_user("SomeOtherUser", "z" * 15) 86 | self.session.add_all((u1, u2)) 87 | self.session.commit() 88 | 89 | req = self.make_params(order=[{"column": 2, "dir": "desc"}]) 90 | table = DataTable(req, 91 | User, 92 | self.session.query(User), 93 | [ 94 | "id", 95 | ("name", "full_name"), 96 | ("address", "address.description") 97 | ]) 98 | result = table.json() 99 | assert result["data"][0]["address"] == addr_desc.description 100 | 101 | req = self.make_params(order=[{"column": 2, "dir": "asc"}]) 102 | table = DataTable(req, 103 | User, 104 | self.session.query(User), 105 | [ 106 | "id", 107 | ("name", "full_name"), 108 | ("address", "address.description") 109 | ]) 110 | result = table.json() 111 | assert result["data"][0]["address"] == addr_asc.description 112 | 113 | def test_filter(self): 114 | self.make_data(10) 115 | req = self.make_params() 116 | 117 | table = DataTable(req, 118 | User, 119 | self.session.query(User), 120 | [ 121 | "id", 122 | ("name", "full_name", lambda i: "User: " + i.full_name) 123 | ]) 124 | result = table.json() 125 | assert all(r["name"].startswith("User: ") for r in result["data"]) 126 | 127 | def test_extra_data(self): 128 | self.make_data(10) 129 | 130 | req = self.make_params() 131 | table = DataTable(req, 132 | User, 133 | self.session.query(User), 134 | [ 135 | "id" 136 | ]) 137 | table.add_data(id_multiplied=lambda i: i.id * 10) 138 | 139 | result = table.json() 140 | assert all(r["id"] * 10 == r["DT_RowData"]["id_multiplied"] for r in result["data"]) 141 | 142 | def test_column_inputs(self): 143 | self.make_data(10) 144 | req = self.make_params() 145 | 146 | table = DataTable(req, 147 | User, 148 | self.session.query(User), 149 | [ 150 | DataColumn(name="id", model_name="id", filter=None), 151 | ("full_name", lambda i: str(i)), 152 | "address" 153 | ]) 154 | table.json() 155 | 156 | def test_ordering(self): 157 | self.make_data(10) 158 | desc_user, _ = self.make_user("z" * 20, "z" * 20) 159 | self.session.add(desc_user) 160 | self.session.commit() 161 | 162 | req = self.make_params(order=[{"column": 1, "dir": "desc"}]) 163 | 164 | table = DataTable(req, 165 | User, 166 | self.session.query(User), 167 | [ 168 | "id", 169 | ("name", "full_name"), 170 | ("address", "address.description") 171 | ]) 172 | 173 | x = table.json() 174 | 175 | assert x["data"][0]["name"] == desc_user.full_name 176 | 177 | req = self.make_params(order=[{"column": 1, "dir": "asc"}], length=100) 178 | 179 | table = DataTable(req, 180 | User, 181 | self.session.query(User), 182 | [ 183 | "id", 184 | ("name", "full_name"), 185 | ("address", "address.description") 186 | ]) 187 | 188 | x = table.json() 189 | 190 | assert x["data"][-1]["name"] == desc_user.full_name 191 | 192 | def test_error(self): 193 | req = self.make_params() 194 | req["start"] = "invalid" 195 | 196 | table = DataTable(req, 197 | User, 198 | self.session.query(User), 199 | ["id"]) 200 | assert "error" in table.json() 201 | 202 | req = self.make_params() 203 | del req["start"] 204 | 205 | table = DataTable(req, 206 | User, 207 | self.session.query(User), 208 | ["id"]) 209 | assert "error" in table.json() 210 | 211 | def test_search(self): 212 | user, addr = self.make_user("Silly Sally", "Silly Sally Road") 213 | user2, addr2 = self.make_user("Silly Sall", "Silly Sally Roa") 214 | self.session.add_all((user, user2)) 215 | self.session.commit() 216 | 217 | req = self.make_params(search={ 218 | "value": "Silly Sally" 219 | }) 220 | 221 | table = DataTable(req, User, self.session.query(User), [("name", "full_name")]) 222 | table.searchable(lambda qs, sq: qs.filter(User.full_name.startswith(sq))) 223 | results = table.json() 224 | assert len(results["data"]) == 1 225 | 226 | req = self.make_params(search={ 227 | "value": "Silly Sall" 228 | }) 229 | 230 | table = DataTable(req, User, self.session.query(User), [("name", "full_name")]) 231 | table.searchable(lambda qs, sq: qs.filter(User.full_name.startswith(sq))) 232 | results = table.json() 233 | assert len(results["data"]) == 2 234 | 235 | def test_column_search(self): 236 | user, addr = self.make_user("Silly Sally", "Silly Sally Road") 237 | user2, addr2 = self.make_user("Silly Sall", "Silly Sally Roa") 238 | self.session.add_all((user, user2)) 239 | self.session.commit() 240 | 241 | req = self.make_params(column_search={ 242 | "name": { 243 | "value": "Silly Sally" 244 | } 245 | }) 246 | 247 | table = DataTable(req, User, self.session.query(User), [("name", "full_name")]) 248 | table.searchable_column(lambda mc, qs, sq: qs.filter(mc.startswith(sq))) 249 | results = table.json() 250 | assert len(results["data"]) == 1 251 | 252 | req = self.make_params(column_search={ 253 | "name": { 254 | "value": "Silly Sall" 255 | } 256 | }) 257 | 258 | table = DataTable(req, User, self.session.query(User), [("name", "full_name")]) 259 | table.searchable_column(lambda mc, qs, sq: qs.filter(mc.startswith(sq))) 260 | results = table.json() 261 | assert len(results["data"]) == 2 262 | 263 | req = self.make_params(column_search={ 264 | "name": { 265 | "value": "Silly Sall" 266 | }, 267 | "address": { 268 | "value": "Silly Sally Road" 269 | } 270 | }) 271 | 272 | table = DataTable( 273 | req, 274 | User, self.session.query(User), 275 | [("name", "full_name"), ("address", "address.description")] 276 | ) 277 | table.searchable_column(lambda mc, qs, sq: qs.filter(mc.startswith(sq))) 278 | results = table.json() 279 | assert len(results["data"]) == 1 280 | -------------------------------------------------------------------------------- /travis_requirements.txt: -------------------------------------------------------------------------------- 1 | sqlalchemy 2 | sqlalchemy_utils 3 | faker 4 | pytest 5 | coveralls 6 | --------------------------------------------------------------------------------