├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs └── index.md ├── examples ├── attr_list.py ├── classes.py ├── column_html_attrs.py ├── csrf.py ├── datetimecol.py ├── dynamic.py ├── external_url_col.py ├── link_subclass_app.py ├── rows.py ├── simple.py ├── simple_app.py ├── simple_nested.py ├── simple_sqlalchemy.py ├── sortable.py ├── subclassing.py ├── subclassing2.py └── subclassing3.py ├── flask_table ├── __init__.py ├── columns.py ├── compat.py ├── html.py └── table.py ├── mkdocs.yml ├── pep8.sh ├── release.sh ├── requirements.txt ├── setup.py └── tests ├── __init__.py └── html ├── attr_list_test ├── test_one.html └── test_two_one_empty.html ├── bool_test ├── test_one.html ├── test_one_custom_display.html └── test_one_na.html ├── border_test └── table_bordered.html ├── button_attrs_test └── test_one.html ├── button_form_attrs_test └── test_one.html ├── button_hidden_fields_test └── test_one.html ├── button_test └── test_one.html ├── class_test └── test_one.html ├── col_test ├── test_encoding.html ├── test_one.html ├── test_ten.html └── test_two.html ├── column_html_attrs_test ├── test_both_html_attrs.html ├── test_column_html_attrs.html ├── test_overwrite_attrs.html ├── test_td_html_attrs.html └── test_th_html_attrs.html ├── date_test └── test_one.html ├── date_test_format └── test_one.html ├── datetime_test └── test_one.html ├── datetime_test_format └── test_one.html ├── dynamic_cols_inherit_test └── test_one.html ├── dynamic_cols_num_cols_test ├── test_one.html └── test_ten.html ├── dynamic_cols_options_test ├── test_none.html └── test_one.html ├── empty_test └── test_none.html ├── escape_test └── test_one.html ├── generator_test ├── test_empty.html ├── test_one.html └── test_ten.html ├── html_attrs_test └── test_html_attrs.html ├── link_test ├── test_one.html ├── test_one_attrs.html ├── test_one_custom_content.html ├── test_one_extra_kwargs.html ├── test_one_no_url_kwargs.html └── test_one_override_content.html ├── nestedcol_test └── test_one.html ├── no_items_allow_empty └── test_zero.html ├── no_items_test └── test_zero.html ├── opt_test ├── test_one.html ├── test_one_default_key.html ├── test_one_default_value.html └── test_one_no_choices.html ├── override_tr_test └── test_ten.html ├── sorting_test ├── test_sorted.html ├── test_sorted_reverse.html └── test_start.html ├── tableid_test └── test_one.html └── thead_class_test └── test_one.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | .venv/ 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | # JetBrains 94 | .idea 95 | 96 | 97 | # Project specific 98 | 99 | # README.md is the true source, README is generated from it. 100 | README 101 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | - "3.6" 6 | install: 7 | - "pip install pep8 coverage coveralls" 8 | - "pip install -r requirements.txt" 9 | before_script: 10 | "./pep8.sh" 11 | script: 12 | "LANGUAGE=en_GB:en coverage run --source=flask_table setup.py test" 13 | after_success: 14 | coveralls 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.5.0 2 | ----- 3 | - Sort out html_attrs (#80) 4 | - Refactor test assert method (#81) 5 | - SQLAlchemy example (#82) 6 | - LinkCol and ButtonCol text_fallback option (#83) 7 | - Better docs for table options (#88) 8 | - ButtonCol form attributes (#89) 9 | - ButtonCol form hidden fields (#90) 10 | - Fix docs for BoolNaCol (#91) 11 | 12 | 0.4.1 13 | ----- 14 | - Add anchor_attrs to LinkCol (#73) 15 | - Test against more recent Python3 versions 16 | 17 | 0.4.0 18 | ----- 19 | - Add column_html_attrs, td_html_attrs and th_html_attrs kwargs to Col (#71) 20 | 21 | 0.3.4 22 | ----- 23 | - Remove any flask.ext imports (#69) 24 | 25 | 0.3.3 26 | ----- 27 | - Customisable BoolCol display values (#61) 28 | - Add BoolNaCol (#62) 29 | - Reduce TableTest boilerplate (#63) 30 | - Fix for deprecated Babel package (#65) 31 | 32 | 0.3.2 33 | ----- 34 | - Fix for non-existent rst README 35 | 36 | 0.3.1 37 | ----- 38 | - Update .gitignore 39 | - Mark Yes and No for translation (#51) (#58) 40 | - Mark 'No Items' for translation (#60) 41 | - Set long_description in setup.py 42 | 43 | 0.3.0 44 | ----- 45 | - Allow passing options to create_table (#41) 46 | - Fix to use super in LinkCol.get_attr_list (#43) 47 | - Refactor handling of HTML (#44) 48 | - Add option to allow empty table (#45) 49 | - Force env vars before imports in tests (#46) 50 | - Add option to set attributes on element in ButtonCol (#47) 51 | - Add option to pass url_kwargs_extra to LinkCol (#50) 52 | - Add release.sh script 53 | - Remove semi-duplicate README 54 | 55 | 0.2.13 56 | ------ 57 | - Add table border option (#40) 58 | 59 | 0.2.12 60 | ------ 61 | - Add NestedTableCol type 62 | - Add test for NestedTableCol type 63 | - Add entry to README.md for NestedTableCol type 64 | - Add simple_nested.py example for NestedTableCol 65 | - Add table_id keyword arg to Table initializer 66 | - Add test for table_id field in Table initializer 67 | 68 | 0.2.11 69 | ------ 70 | - Fix tests for update to Babel 71 | - Add config for read-the-docs 72 | - Add better docs for the column types 73 | - Fix some docs formatting 74 | - Call to td_format for LinkCol td_contents. 75 | 76 | 0.2.10 77 | ------ 78 | - Fix DatetimeCol format option 79 | - Formatting 80 | - Fix to use super for Col subclasses 81 | 82 | 0.2.8 83 | ----- 84 | - Add examples for setting classes attribute 85 | - Add info to README about classes attribute 86 | - Fix typos in README 87 | - Add example for simple table within a flask app 88 | - pep8 fix 89 | - Reorder statements in tests/__init__ to keep pep8 happy 90 | - Add option for setting a thead class 91 | - Inherit columns when inheriting a parent table class 92 | 93 | 0.2.8 94 | ----- 95 | - Add travis tests for python3.4 96 | - Correct info about PYTHONPATH 97 | - Use comprehensions rather than explicit for-loops 98 | - Add option for setting 'No Items' text 99 | - Add show option 100 | - Add more tests for dynamic columns 101 | - Add test for dynamic setting of no_items 102 | - Add tests for sort_url being not-set 103 | - Use coverage and coveralls in travis 104 | - Install coverage for travis 105 | - Don't use virtualenv path in travis 106 | - Add coverage status to README 107 | - Fix indenting in FuncItem in tests 108 | - Give tests better names 109 | - Use 'item' rather than 'i' for readability 110 | - Make it easier to manipulate trs 111 | - Add test and example for manipulating trs 112 | - Add README info for overriding tr_format 113 | - Change default value for attr_list 114 | - Change dangerous default values 115 | - Add docstrings for OptCol and BoolCol 116 | - Add info on the included Col types 117 | - Remove nested comprehension in tests 118 | - Fix for if no value set for choices in OptCol 119 | - Add test for when passing no value for choices to OptCol 120 | - Add test for when passing no value for url_kwargs to LinkCol 121 | 122 | 0.2.7 123 | ----- 124 | - Allow tables to be created dynamically 125 | - Fix HTML in examples/simple.py 126 | - Give dynamic example table a more classy name 127 | - Add info to README about dynamically creating tables 128 | 129 | 0.2.6 130 | ----- 131 | - Run pep8 with travis 132 | - Tell travis to install pep8 133 | - Add documentation about running examples 134 | - Remove pointless shebang from sortable example 135 | - Fix examples docs to not contain utter nonsense 136 | - Make sortable example pass pep8 137 | - Make tests pass pep8 138 | - Run all python files through pep8 in Travis 139 | - Make README and examples/simple.py use the same example 140 | - Output example html in README 141 | - Add newlines to output for readability 142 | - Remove requirement for items to be len-able 143 | - Add script for pep8 testing 144 | - Fix README and simple example 145 | - Tidy test_ten.html 146 | - Add proper rst README file 147 | 148 | 0.2.5 149 | ----- 150 | - Add build status to README (now that it's passing!) 151 | - Make README clearer about loading from database 152 | - Add subclassing example for RawCol 153 | - Use unicode literals 154 | - Prefer .format to % 155 | - Use unicode literals in tests 156 | - Prefer .format to % in tests 157 | - Adjust spacing of imports 158 | - PEP8 code 159 | 160 | 0.2.4 161 | ----- 162 | - Declare encoding to fix unicode error 163 | - Fix travis.yml 164 | 165 | 0.2.3 166 | ----- 167 | - Add Sortable Tables to README 168 | - Correct syntax error in __init__ method of table class 169 | 170 | 0.2.2 171 | ----- 172 | - fix date_format parameter for DateCol 173 | - fix unittests for people with different locale 174 | - run tests via setup.py 175 | - removed unnecessary str casting in order to support unicode 176 | - fix for python3.3 problems 177 | - Fix for travis to run tests with setup.py 178 | - added method for formatting th-elements 179 | - Update sortable example to include two-way sorting 180 | - Integrate sortable tables 181 | 182 | 0.2.1 183 | ----- 184 | - Use setuptools and fix dependencies 185 | 186 | 0.2.0 187 | ----- 188 | - Remove cols() getter in table class 189 | - Simplify use of attr and attr_list 190 | - Add tests for more complex attr values used in url_kwargs in LinkCol 191 | - Changed my mind about attr vs attr_list 192 | - Add test for when attr_list items have dots in 193 | - Add comment to attr_list example to justify having both attr and attr_list 194 | - Add test for BoolCol 195 | - Add tests for OptCol 196 | - Add test for when there are no items 197 | - Remove unused method 198 | - Simplify imports for running tests 199 | - Add test for setting table class when populating 200 | - Add tests for DateCol and DatetimeCol 201 | - Add test for getting content text for link from item via attr 202 | - Improve HTML equivalence testing to help debugging tests 203 | - Make tests not be locale dependent 204 | 205 | 0.1.7 206 | ----- 207 | - Add LICENSE and README.md to MANIFEST.in 208 | 209 | 0.1.5 210 | ----- 211 | - Initial release 212 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Andrew Plummer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flask Table 2 | =========== 3 | 4 | Because writing HTML is fiddly and all of your tables are basically 5 | the same. 6 | 7 | [![Build Status](https://travis-ci.org/plumdog/flask_table.svg?branch=master)](https://travis-ci.org/plumdog/flask_table) 8 | [![Coverage Status](https://coveralls.io/repos/plumdog/flask_table/badge.png?branch=master)](https://coveralls.io/r/plumdog/flask_table?branch=master) 9 | [![PyPI version](https://badge.fury.io/py/Flask-Table.svg)](https://badge.fury.io/py/Flask-Table) 10 | 11 | Installation 12 | ============ 13 | ``` 14 | pip install flask-table 15 | ``` 16 | 17 | Quick Start 18 | =========== 19 | 20 | ```python 21 | # import things 22 | from flask_table import Table, Col 23 | 24 | # Declare your table 25 | class ItemTable(Table): 26 | name = Col('Name') 27 | description = Col('Description') 28 | 29 | # Get some objects 30 | class Item(object): 31 | def __init__(self, name, description): 32 | self.name = name 33 | self.description = description 34 | items = [Item('Name1', 'Description1'), 35 | Item('Name2', 'Description2'), 36 | Item('Name3', 'Description3')] 37 | # Or, equivalently, some dicts 38 | items = [dict(name='Name1', description='Description1'), 39 | dict(name='Name2', description='Description2'), 40 | dict(name='Name3', description='Description3')] 41 | 42 | # Or, more likely, load items from your database with something like 43 | items = ItemModel.query.all() 44 | 45 | # Populate the table 46 | table = ItemTable(items) 47 | 48 | # Print the html 49 | print(table.__html__()) 50 | # or just {{ table }} from within a Jinja template 51 | ``` 52 | 53 | Which gives something like: 54 | 55 | ```html 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
NameDescription
Name1Description1
Name2Description2
Name3Description3
64 | ``` 65 | 66 | Or as HTML: 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
NameDescription
Name1Description1
Name2Description2
Name3Description3
76 | 77 | For more, see [the examples](#the-examples) for some complete, 78 | runnable demonstrations. 79 | 80 | Extra things: 81 | ------------- 82 | 83 | * The attribute used for each column in the declaration of the column 84 | is used as the default thing to lookup in each item. 85 | 86 | * The thing that you pass when you populate the table must: 87 | * be iterable 88 | * contain dicts or objects - there's nothing saying it can't contain 89 | some of each. See `examples/simple_sqlalchemy.py` for a database 90 | example. 91 | 92 | * You can pass attributes to the `td` and `th` elements by passing a 93 | dict of attributes as `td_html_attrs` or `th_html_attrs` when creating a 94 | Col. Or as `column_html_attrs` to apply the attributes to both the `th`s 95 | and the `td`s. (Any that you pass in `th_html_attrs` or `td_html_attrs` will 96 | overwrite any that you also pass with `column_html_attrs`.) See 97 | examples/column_html_attrs.py for more. 98 | 99 | * There are also LinkCol and ButtonCol that allow links and buttons, 100 | which is where the Flask-specific-ness comes in. 101 | 102 | * There are also DateCol and DatetimeCol that format dates and 103 | datetimes. 104 | 105 | * Oh, and BoolCol, which does Yes/No. 106 | 107 | * But most importantly, Col is easy to subclass. 108 | 109 | Table configuration and options 110 | =============================== 111 | 112 | The following options configure table-level options: 113 | 114 | * `thead_classes` - a list of classes to set on the `` element. 115 | 116 | * `no_items` - a string to display if no items are passed, defaults to 117 | `'No Items'`. 118 | 119 | * `html_attrs` - a dictionary of attributes to set on the `` element. 120 | 121 | * `classes` - a list of strings to be set as the `class` attribute on 122 | the `
` element. 123 | 124 | * `table_id` - a string to set as the `id` attribute on the `
` element. 125 | 126 | * `border` - whether the `border` should be set on the `
` element. 127 | 128 | These can be set in a few different ways: 129 | 130 | a) set when defining the table class 131 | ```python 132 | class MyTable 133 | classes = ['class1', 'class2'] 134 | ``` 135 | 136 | b) passed in the `options` argument to `create_table`. 137 | ```python 138 | MyTable = create_table(options={'table_id': 'my-table-id'}) 139 | ``` 140 | 141 | c) passed to the table's `__init__` 142 | ```python 143 | table = MyTable(items, no_items='There is nothing', ...) 144 | ``` 145 | 146 | Note that a) and b) define an attribute on the table class, but c) 147 | defines an attribute on the instance, so anything set like in c) will 148 | override anything set in a) or b). 149 | 150 | Eg: 151 | ```python 152 | class ItemTable(Table): 153 | classes = ['myclass'] 154 | name = Col('Name') 155 | table = ItemTable(items, classes=['otherclass']) 156 | ``` 157 | would create a table with `class="otherclass"`. 158 | 159 | Included Col Types 160 | ================== 161 | 162 | * [`OptCol`](#more-about-optcol) - converts values according to a 163 | dictionary of choices. Eg for turning stored codes into human 164 | readable text. 165 | 166 | * [`BoolCol`](#more-about-boolcol) (subclass of OptCol) - converts 167 | values to yes/no. 168 | 169 | * [`BoolNaCol`](#more-about-boolnacol) (subclass of BoolCol) - converts 170 | values to yes/no/na. 171 | 172 | * [`DateCol`](#more-about-datecol) - for dates (uses `format_date` 173 | from `babel.dates`). 174 | 175 | * [`DatetimeCol`](#more-about-datetimecol) - for date-times (uses 176 | `format_datetime` from `babel.dates`). 177 | 178 | * [`LinkCol`](#more-about-linkcol) - creates a link by specifying an 179 | endpoint and url_kwargs. 180 | 181 | * [`ButtonCol`](#more-about-buttoncol) (subclass of LinkCol) creates a 182 | button that posts the the given address. 183 | 184 | * [`NestedTableCol`](#more-about-nestedtablecol) - allows nesting of 185 | tables inside columns 186 | 187 | More about `OptCol` 188 | ------------------- 189 | 190 | When creating the column, you pass some `choices`. This should be a 191 | dict with the keys being the values that will be found on the item's 192 | attribute, and the values will be the text to be displayed. 193 | 194 | You can also set a `default_key`, or a `default_value`. The default 195 | value will be used if the value found from the item isn't in the 196 | choices dict. The default key works in much the same way, but means 197 | that if your default is already in your choices, you can just point to 198 | it rather than repeat it. 199 | 200 | And you can use `coerce_fn` if you need to alter the value from the 201 | item before looking it up in the dict. 202 | 203 | More about `BoolCol` 204 | -------------------- 205 | 206 | A subclass of `OptCol` where the `choices` are: 207 | 208 | ```python 209 | {True: 'Yes', False: 'No'} 210 | ``` 211 | 212 | and the `coerce_fn` is `bool`. So the value from the item is coerced 213 | to a `bool` and then looked up in the choices to get the text to 214 | display. 215 | 216 | If you want to specify something other than "Yes" and "No", you can 217 | pass `yes_display` and/or `no_display` when creating the column. Eg: 218 | 219 | ```python 220 | class MyTable(Table): 221 | mybool = BoolCol('myboolcol', yes_display='Affirmative', no_display='Negatory') 222 | ``` 223 | 224 | More about `BoolNaCol` 225 | ---------------------- 226 | 227 | Just like `BoolCol`, except displays `None` as "N/A". Can override 228 | with the `na_display` argument. 229 | 230 | More about `DateCol` 231 | -------------------- 232 | 233 | [Requires Babel configuration](#babel-configuration) 234 | 235 | Formats a date from the item. Can specify a `date_format` to use, 236 | which defaults to `'short'`, which is passed to 237 | `babel.dates.format_date`. 238 | 239 | More about `DatetimeCol` 240 | ------------------------ 241 | 242 | [Requires Babel configuration](#babel-configuration) 243 | 244 | Formats a datetime from the item. Can specify a `datetime_format` to 245 | use, which defaults to `'short'`, which is passed to 246 | `babel.dates.format_datetime`. 247 | 248 | Babel configuration 249 | ------------------- 250 | 251 | Babel uses a locale to determine how to format dates. It falls back to 252 | using environment variables (`LC_TIME`, `LANGUAGE`, `LC_ALL`, 253 | `LC_CTYPE`, `LANG`), or can be configured 254 | [within Flask](https://pythonhosted.org/Flask-Babel/#configuration), 255 | allowing dynamic selection of locale. 256 | 257 | Make sure that one of the following is true: 258 | 259 | - at least one of the above environment variables is set to a valid locale 260 | - `BABEL_DEFAULT_LOCALE` is set as config on the Flask app to a valid locale 261 | - a `@babel.localeselector` function is configured 262 | 263 | Note that Babel reads the environment variables at import time, so if 264 | you set these within Python, make sure it happens before you import 265 | Flask Table. The other two options would be considered "better", 266 | largely for this reason. 267 | 268 | More about `LinkCol` 269 | -------------------- 270 | 271 | Gives a way of putting a link into a `td`. You must specify an 272 | `endpoint` for the url. You should also specify some 273 | `url_kwargs`. This should be a dict which will be passed as the second 274 | argument of `url_for`, except the values will be treated as attributes 275 | to be looked up on the item. These keys obey the same rules as 276 | elsewhere, so can be things like `'category.name'` or `('category', 277 | 'name')`. 278 | 279 | The kwarg `url_kwargs_extra` allows passing of contants to the 280 | url. This can be useful for adding constant GET params to a url. 281 | 282 | The text for the link is acquired in *almost* the same way as with 283 | other columns. However, other columns can be given no `attr` or 284 | `attr_list` and will use the attribute that the column was given in 285 | the table class, but `LinkCol` does not, and instead falls back to the 286 | heading of the column. This make more sense for things like an "Edit" 287 | link. You can override this fallback with the `text_fallback` kwarg. 288 | 289 | Set attributes for anchor tag by passing `anchor_attrs`: 290 | ```python 291 | name = LinkCol('Name', 'single_item', url_kwargs=dict(id='id'), anchor_attrs={'class': 'myclass'}) 292 | ``` 293 | 294 | More about `ButtonCol` 295 | ---------------------- 296 | 297 | Has all the same options as `LinkCol` but instead adds a form and a 298 | button that gets posted to the url. 299 | 300 | You can pass a dict of attributes to add to the button element with 301 | the `button_attrs` kwarg. 302 | 303 | You can pass a dict of attributes to add to the form element with 304 | the `form_attrs` kwarg. 305 | 306 | You can pass a dict of hidden fields to add into the form element with 307 | the `form_hidden_fields` kwargs. The keys will be used as the `name` 308 | attributes and the values as the `value` attributes. 309 | 310 | More about `NestedTableCol` 311 | --------------------------- 312 | 313 | This column type makes it possible to nest tables in columns. For each 314 | nested table column you need to define a subclass of Table as you 315 | normally would when defining a table. The name of that Table sub-class 316 | is the second argument to NestedTableCol. 317 | 318 | Eg: 319 | 320 | ```python 321 | class MySubTable(Table): 322 | a = Col('1st nested table col') 323 | b = Col('2nd nested table col') 324 | 325 | class MainTable(Table): 326 | id = Col('id') 327 | objects = NestedTableCol('objects', MySubTable) 328 | ``` 329 | 330 | Subclassing Col 331 | =============== 332 | 333 | (Look in examples/subclassing.py for a more concrete example) 334 | 335 | Suppose our item has an attribute, but we don't want to output the 336 | value directly, we need to alter it first. If the value that we get 337 | from the item gives us all the information we need, then we can just 338 | override the td_format method: 339 | 340 | ```python 341 | class LangCol(Col): 342 | def td_format(self, content): 343 | if content == 'en_GB': 344 | return 'British English' 345 | elif content == 'de_DE': 346 | return 'German' 347 | elif content == 'fr_FR': 348 | return 'French' 349 | else: 350 | return 'Not Specified' 351 | ``` 352 | 353 | If you need access to all of information in the item, then we can go a 354 | stage earlier in the process and override the td_contents method: 355 | 356 | ```python 357 | from flask import Markup 358 | 359 | def td_contents(self, i, attr_list): 360 | # by default this does 361 | # return self.td_format(self.from_attr_list(i, attr_list)) 362 | return Markup.escape(self.from_attr_list(i, attr_list) + ' for ' + item.name) 363 | ``` 364 | 365 | At present, you do still need to be careful about escaping things as 366 | you override these methods. Also, because of the way that the Markup 367 | class works, you need to be careful about how you concatenate these 368 | with other strings. 369 | 370 | Manipulating ``s 371 | ==================== 372 | 373 | (Look in examples/rows.py for a more concrete example) 374 | 375 | Suppose you want to change something about the tr element for some or 376 | all items. You can do this by overriding your table's `get_tr_attrs` 377 | method. By default, this method returns an empty dict. 378 | 379 | So, we might want to use something like: 380 | 381 | ```python 382 | class ItemTable(Table): 383 | name = Col('Name') 384 | description = Col('Description') 385 | 386 | def get_tr_attrs(self, item): 387 | if item.important(): 388 | return {'class': 'important'} 389 | else: 390 | return {} 391 | ``` 392 | 393 | which would give all trs for items that returned a true value for the 394 | `important()` method, a class of "important". 395 | 396 | Dynamically Creating Tables 397 | =========================== 398 | 399 | (Look in examples/dynamic.py for a more concrete example) 400 | 401 | You can define a table dynamically too. 402 | 403 | ```python 404 | TableCls = create_table('TableCls')\ 405 | .add_column('name', Col('Name'))\ 406 | .add_column('description', Col('Description')) 407 | ``` 408 | 409 | which is equivalent to 410 | 411 | ```python 412 | class TableCls(Table): 413 | name = Col('Name') 414 | description = Col('Description') 415 | ``` 416 | 417 | but makes it easier to add columns dynamically. 418 | 419 | For example, you may wish to only add a column based on a condition. 420 | 421 | ```python 422 | TableCls = create_table('TableCls')\ 423 | .add_column('name', Col('Name')) 424 | 425 | if condition: 426 | TableCls.add_column('description', Col('Description')) 427 | ``` 428 | 429 | which is equivalent to 430 | 431 | ```python 432 | class TableCls(Table): 433 | name = Col('Name') 434 | description = Col('Description', show=condition) 435 | ``` 436 | 437 | thanks to the `show` option. Use whichever you think makes your code 438 | more readable. Though you may still need the dynamic option for 439 | something like 440 | 441 | ```python 442 | TableCls = create_table('TableCls') 443 | for i in range(num): 444 | TableCls.add_column(str(i), Col(str(i))) 445 | ``` 446 | 447 | We can also set some extra options to the table class by passing `options` parameter to `create_table()`: 448 | ```python 449 | tbl_options = dict( 450 | classes=['cls1', 'cls2'], 451 | thead_classes=['cls_head1', 'cls_head2'], 452 | no_items='Empty') 453 | TableCls = create_table(options=tbl_options) 454 | 455 | # equals to 456 | 457 | class TableCls(Table): 458 | classes = ['cls1', 'cls2'] 459 | thead_classes = ['cls_head1', 'cls_head2'] 460 | no_items = 'Empty' 461 | ``` 462 | 463 | Sortable Tables 464 | =============== 465 | 466 | (Look in examples/sortable.py for a more concrete example) 467 | 468 | Define a table and set its allow_sort attribute to True. Now all 469 | columns will be default try to turn their header into a link for 470 | sorting, unless you set allow_sort to False for a column. 471 | 472 | You also must declare a sort_url method for that table. Given a 473 | col_key, this determines the url for link in the header. If reverse is 474 | True, then that means that the table has just been sorted by that 475 | column and the url can adjust accordingly, ie to now give the address 476 | for the table sorted in the reverse direction. It is, however, 477 | entirely up to your flask view method to interpret the values given to 478 | it from this url and to order the results before giving the to the 479 | table. The table itself will not do any reordering of the items it is 480 | given. 481 | 482 | ```python 483 | class SortableTable(Table): 484 | name = Col('Name') 485 | allow_sort = True 486 | 487 | def sort_url(self, col_key, reverse=False): 488 | if reverse: 489 | direction = 'desc' 490 | else: 491 | direction = 'asc' 492 | return url_for('index', sort=col_key, direction=direction) 493 | ``` 494 | 495 | The Examples 496 | ============ 497 | 498 | The [`examples`](/examples) directory contains a few pieces of sample code to show 499 | some of the concepts and features. They are all intended to be 500 | runnable. Some of them just output the code they generate, but some 501 | (just one, `sortable.py`, at present) actually creates a Flask app 502 | that you can access. 503 | 504 | You should be able to just run them directly with `python`, but if you 505 | have cloned the repository for the sake of dev, and created a 506 | virtualenv, you may find that they generate an import error for 507 | `flask_table`. This is because `flask_table` hasn't been installed, 508 | and can be rectified by running something like 509 | `PYTHONPATH=.:./lib/python3.3/site-packages python examples/simple.py`, 510 | which will use the local version of `flask_table` 511 | including any changes. 512 | 513 | Also, if there is anything that you think is not clear and would be 514 | helped by an example, please just ask and I'll happily write one. Only 515 | you can help me realise which bits are tricky or non-obvious and help 516 | me to work on explaining the bits that need explaining. 517 | 518 | Other Things 519 | ============ 520 | 521 | At the time of first writing, I was not aware of the work of 522 | Django-Tables. However, I have now found it and started adapting ideas 523 | from it, where appropriate. For example, allowing items to be dicts as 524 | well as objects. 525 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /examples/attr_list.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col 2 | 3 | 4 | """Lets suppose that we have a class that we get an iterable of from 5 | somewhere, such as a database. We can declare a table that pulls out 6 | the relevant entries, escapes them and displays them. 7 | 8 | """ 9 | 10 | 11 | class Item(object): 12 | def __init__(self, name, category): 13 | self.name = name 14 | self.category = category 15 | 16 | 17 | class Category(object): 18 | def __init__(self, name): 19 | self.name = name 20 | 21 | 22 | class ItemTable(Table): 23 | name = Col('Name') 24 | category_name = Col('Category', attr_list=['category', 'name']) 25 | # Equivalently: Col('Category', attr='category.name') 26 | # Both syntaxes are kept as the second is more readable, but 27 | # doesn't cover all options. Such as if the items are dicts and 28 | # the keys have dots in. 29 | 30 | 31 | def main(): 32 | items = [Item('A', Category('catA')), 33 | Item('B', Category('catB'))] 34 | 35 | tab = ItemTable(items) 36 | print(tab.__html__()) 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /examples/classes.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col 2 | 3 | 4 | """If we want to put an HTML class onto the table element, we can set 5 | the "classes" attribute on the table class. This should be an iterable 6 | of that are joined together and all added as classes. If none are set, 7 | then no class is added to the table element. 8 | 9 | """ 10 | 11 | 12 | class Item(object): 13 | def __init__(self, name, description): 14 | self.name = name 15 | self.description = description 16 | 17 | 18 | class ItemTableOneClass(Table): 19 | classes = ['class1'] 20 | 21 | name = Col('Name') 22 | description = Col('Description') 23 | 24 | 25 | class ItemTableTwoClasses(Table): 26 | classes = ['class1', 'class2'] 27 | 28 | name = Col('Name') 29 | description = Col('Description') 30 | 31 | 32 | def one_class(items): 33 | table = ItemTableOneClass(items) 34 | 35 | # or {{ table }} in jinja 36 | print(table.__html__()) 37 | 38 | """Outputs: 39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
NameDescription
Name1Description1
54 | """ 55 | 56 | 57 | def two_classes(items): 58 | table = ItemTableTwoClasses(items) 59 | 60 | # or {{ table }} in jinja 61 | print(table.__html__()) 62 | 63 | """Outputs: 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
NameDescription
Name1Description1
79 | """ 80 | 81 | 82 | def main(): 83 | items = [Item('Name1', 'Description1')] 84 | 85 | # user ItemTableOneClass 86 | one_class(items) 87 | 88 | print('\n######################\n') 89 | 90 | # user ItemTableTwoClasses 91 | two_classes(items) 92 | 93 | 94 | if __name__ == '__main__': 95 | main() 96 | -------------------------------------------------------------------------------- /examples/column_html_attrs.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col 2 | 3 | 4 | """What if we need to apply some classes (or any other attribute) to 5 | the td and th HTML attributes? Maybe you want this so you can apply 6 | some styling or attach some javascript. 7 | 8 | NB: This example just handles the adding of some fixed attributes to a 9 | column. If you want to do something dynamic (eg, only applying an 10 | attribute to certain rows, see the rows.py example). 11 | 12 | This example is not very "real world" but should show how the setting 13 | of elements works and what things you can do. 14 | 15 | """ 16 | 17 | 18 | class Item(object): 19 | def __init__(self, name, description): 20 | self.name = name 21 | self.description = description 22 | 23 | 24 | class ItemTable(Table): 25 | name = Col( 26 | 'Name', 27 | # Apply this class to both the th and all tds in this column 28 | column_html_attrs={'class': 'my-name-class'}, 29 | ) 30 | description = Col( 31 | 'Description', 32 | # Apply these to both 33 | column_html_attrs={ 34 | 'data-something': 'my-data', 35 | 'class': 'my-description-class'}, 36 | # Apply this to just the th 37 | th_html_attrs={'data-something-else': 'my-description-th-class'}, 38 | # Apply this to just the td - note that this will things from 39 | # overwrite column_html_attrs. 40 | td_html_attrs={'data-something': 'my-td-only-data'}, 41 | ) 42 | 43 | 44 | def main(): 45 | items = [Item('Name1', 'Description1'), 46 | Item('Name2', 'Description2'), 47 | Item('Name3', 'Description3')] 48 | 49 | table = ItemTable(items) 50 | 51 | # or {{ table }} in jinja 52 | print(table.__html__()) 53 | 54 | """Outputs: 55 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 | 70 | 74 | 75 | 76 | 77 | 81 | 82 | 83 | 84 | 88 | 89 | 90 |
Name 63 | Description 64 |
Name1 72 | Description1 73 |
Name2 79 | Description2 80 |
Name3 86 | Description3 87 |
91 | 92 | Except it doesn't bother to prettify the output. 93 | """ 94 | 95 | if __name__ == '__main__': 96 | main() 97 | -------------------------------------------------------------------------------- /examples/csrf.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from flask_table import Table, Col, ButtonCol 4 | from flask import Flask, request 5 | 6 | app = Flask(__name__) 7 | 8 | CHARS = [str(i) for i in range(10)] 9 | 10 | 11 | def get_csrf_token(): 12 | # You should replace this with the token generator for the csrf 13 | # mechanism you are using. 14 | return ''.join(random.choice(CHARS) for i in range(20)) 15 | 16 | 17 | @app.route('/') 18 | def index(): 19 | items = Item.get_elements() 20 | table = get_table_class()(items) 21 | return table.__html__() 22 | 23 | 24 | @app.route('/item/', methods=['POST']) 25 | def single_item(id): 26 | element = Item.get_element_by_id(id) 27 | return ( 28 | '

{}

{}


id: {}' 29 | '

CSRF token: {}

' 30 | ).format( 31 | element.name, 32 | element.description, 33 | element.id, 34 | request.form['csrf_token'], 35 | ) 36 | 37 | 38 | def get_table_class(): 39 | csrf_token = get_csrf_token() 40 | class ItemTable(Table): 41 | name = Col('Name') 42 | description = Col('Description') 43 | button = ButtonCol( 44 | 'Button', 45 | 'single_item', 46 | url_kwargs=dict(id='id'), 47 | form_hidden_fields=dict(csrf_token=csrf_token) 48 | ) 49 | return ItemTable 50 | 51 | 52 | class Item(object): 53 | """ a little fake database """ 54 | def __init__(self, id, name, description): 55 | self.id = id 56 | self.name = name 57 | self.description = description 58 | 59 | @classmethod 60 | def get_elements(cls): 61 | return [ 62 | Item(1, 'Z', 'zzzzz'), 63 | Item(2, 'K', 'aaaaa'), 64 | Item(3, 'B', 'bbbbb')] 65 | 66 | @classmethod 67 | def get_element_by_id(cls, id): 68 | return [i for i in cls.get_elements() if i.id == id][0] 69 | 70 | def main(): 71 | app.run(debug=True) 72 | 73 | 74 | if __name__ == '__main__': 75 | main() 76 | -------------------------------------------------------------------------------- /examples/datetimecol.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | # Run this example with LC_TIME=[other locale] to use a different 5 | # locale's datetime formatting, eg: 6 | # 7 | # LC_TIME=en_US python examples/datetimecol.py 8 | # or 9 | # LC_TIME=en_GB python examples/datetimecol.py 10 | os.environ.setdefault('LC_TIME', 'en_GB') # noqa 11 | 12 | from flask_table import Table, Col, DatetimeCol 13 | 14 | 15 | class Item(object): 16 | def __init__(self, name, dt): 17 | self.name = name 18 | self.dt = dt 19 | 20 | 21 | class ItemTable(Table): 22 | name = Col('Name') 23 | dt = DatetimeCol('Datetime') 24 | 25 | 26 | def main(): 27 | items = [ 28 | Item('Name1', datetime.now()), 29 | Item('Name2', datetime(2018, 1, 1, 12, 34, 56)), 30 | ] 31 | 32 | table = ItemTable(items) 33 | 34 | # or {{ table }} in jinja 35 | print(table.__html__()) 36 | 37 | if __name__ == '__main__': 38 | main() 39 | -------------------------------------------------------------------------------- /examples/dynamic.py: -------------------------------------------------------------------------------- 1 | from flask_table import create_table, Col 2 | 3 | 4 | def main(): 5 | TableCls = create_table()\ 6 | .add_column('name', Col('Name'))\ 7 | .add_column('description', Col('Description')) 8 | 9 | items = [dict(name='Name1', description='Description1'), 10 | dict(name='Name2', description='Description2'), 11 | dict(name='Name3', description='Description3')] 12 | 13 | table = TableCls(items) 14 | 15 | print(table.__html__()) 16 | 17 | """Outputs: 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
NameDescription
Name1Description1
Name2Description2
Name3Description3
41 | """ 42 | 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /examples/external_url_col.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col 2 | from flask_table.html import element 3 | 4 | 5 | class Item(object): 6 | def __init__(self, name, url): 7 | self.name = name 8 | self.url = url 9 | 10 | 11 | class ExternalURLCol(Col): 12 | def __init__(self, name, url_attr, **kwargs): 13 | self.url_attr = url_attr 14 | super(ExternalURLCol, self).__init__(name, **kwargs) 15 | 16 | def td_contents(self, item, attr_list): 17 | text = self.from_attr_list(item, attr_list) 18 | url = self.from_attr_list(item, [self.url_attr]) 19 | return element('a', {'href': url}, content=text) 20 | 21 | 22 | class ItemTable(Table): 23 | url = ExternalURLCol('URL', url_attr='url', attr='name') 24 | 25 | 26 | def main(): 27 | items = [ 28 | Item('Google', 'https://google.com'), 29 | Item('Yahoo', 'https://yahoo.com'), 30 | ] 31 | 32 | tab = ItemTable(items) 33 | print(tab.__html__()) 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /examples/link_subclass_app.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col, LinkCol 2 | from flask import Flask, url_for 3 | 4 | """A example for creating a simple table within a working Flask app. 5 | 6 | This table has two columns, linking to two different pages. Then a 7 | subclass of LinkCol to conditionally select the target endpoint. 8 | 9 | """ 10 | 11 | app = Flask(__name__) 12 | 13 | 14 | class LinkDeciderCol(LinkCol): 15 | 16 | def url(self, item): 17 | if item.name == 'B': 18 | endpoint = self.endpoint['b'] 19 | else: 20 | endpoint = self.endpoint['a'] 21 | return url_for(endpoint, **self.url_kwargs(item)) 22 | 23 | 24 | class ItemTable(Table): 25 | name = Col('Name') 26 | link_a = LinkCol( 27 | 'Link A', 28 | 'single_item_a', 29 | url_kwargs=dict(id='id')) 30 | link_b = LinkCol( 31 | 'Link B', 32 | 'single_item_b', 33 | url_kwargs=dict(id='id')) 34 | link_decider = LinkDeciderCol( 35 | 'Decider Col', 36 | {'a': 'single_item_a', 37 | 'b': 'single_item_b'}, 38 | url_kwargs=dict(id='id')) 39 | 40 | 41 | @app.route('/') 42 | def index(): 43 | items = Item.get_elements() 44 | table = ItemTable(items) 45 | return table.__html__() 46 | 47 | 48 | @app.route('/item_a/') 49 | def single_item_a(id): 50 | element = Item.get_element_by_id(id) 51 | return '

A: {}


id: {}'.format( 52 | element.name, element.id) 53 | 54 | 55 | @app.route('/item_b/') 56 | def single_item_b(id): 57 | element = Item.get_element_by_id(id) 58 | return '

B: {}


id: {}'.format( 59 | element.name, element.id) 60 | 61 | 62 | class Item(object): 63 | """ a little fake database """ 64 | def __init__(self, id, name): 65 | self.id = id 66 | self.name = name 67 | 68 | @classmethod 69 | def get_elements(cls): 70 | return [ 71 | Item(1, 'Z'), 72 | Item(2, 'K'), 73 | Item(3, 'B')] 74 | 75 | @classmethod 76 | def get_element_by_id(cls, id): 77 | return [i for i in cls.get_elements() if i.id == id][0] 78 | 79 | 80 | if __name__ == '__main__': 81 | app.run(debug=True) 82 | -------------------------------------------------------------------------------- /examples/rows.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col 2 | 3 | 4 | class Item(object): 5 | def __init__(self, name, description): 6 | self.name = name 7 | self.description = description 8 | 9 | def important(self): 10 | """Items are important if their description starts with an a. 11 | 12 | """ 13 | 14 | return self.description.lower().startswith('a') 15 | 16 | 17 | class ItemTable(Table): 18 | name = Col('Name') 19 | description = Col('Description') 20 | 21 | def get_tr_attrs(self, item): 22 | if item.important(): 23 | return {'class': 'important'} 24 | else: 25 | return {} 26 | 27 | 28 | def main(): 29 | items = [Item('Name1', 'Boring'), 30 | Item('Name2', 'A very important item'), 31 | Item('Name3', 'Boring')] 32 | 33 | table = ItemTable(items) 34 | 35 | print(table.__html__()) 36 | 37 | """ 38 | Outputs: 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
NameDescription
Name1Boring
Name2A very important item
Name3Boring
62 | """ 63 | 64 | 65 | if __name__ == '__main__': 66 | main() 67 | -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col 2 | 3 | 4 | """Lets suppose that we have a class that we get an iterable of from 5 | somewhere, such as a database. We can declare a table that pulls out 6 | the relevant entries, escapes them and displays them. 7 | 8 | """ 9 | 10 | 11 | class Item(object): 12 | def __init__(self, name, description): 13 | self.name = name 14 | self.description = description 15 | 16 | 17 | class ItemTable(Table): 18 | name = Col('Name') 19 | description = Col('Description') 20 | 21 | 22 | def main(): 23 | items = [Item('Name1', 'Description1'), 24 | Item('Name2', 'Description2'), 25 | Item('Name3', 'Description3')] 26 | 27 | table = ItemTable(items) 28 | 29 | # or {{ table }} in jinja 30 | print(table.__html__()) 31 | 32 | """Outputs: 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
NameDescription
Name1Description1
Name2Description2
Name3Description3
56 | 57 | Except it doesn't bother to prettify the output. 58 | """ 59 | 60 | if __name__ == '__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /examples/simple_app.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col, LinkCol 2 | from flask import Flask 3 | 4 | """A example for creating a simple table within a working Flask app. 5 | 6 | Our table has just two columns, one of which shows the name and is a 7 | link to the item's page. The other shows the description. 8 | 9 | """ 10 | 11 | app = Flask(__name__) 12 | 13 | 14 | class ItemTable(Table): 15 | name = LinkCol('Name', 'single_item', 16 | url_kwargs=dict(id='id'), attr='name') 17 | description = Col('Description') 18 | 19 | 20 | @app.route('/') 21 | def index(): 22 | items = Item.get_elements() 23 | table = ItemTable(items) 24 | 25 | # You would usually want to pass this out to a template with 26 | # render_template. 27 | return table.__html__() 28 | 29 | 30 | @app.route('/item/') 31 | def single_item(id): 32 | element = Item.get_element_by_id(id) 33 | # Similarly, normally you would use render_template 34 | return '

{}

{}


id: {}'.format( 35 | element.name, element.description, element.id) 36 | 37 | 38 | class Item(object): 39 | """ a little fake database """ 40 | def __init__(self, id, name, description): 41 | self.id = id 42 | self.name = name 43 | self.description = description 44 | 45 | @classmethod 46 | def get_elements(cls): 47 | return [ 48 | Item(1, 'Z', 'zzzzz'), 49 | Item(2, 'K', 'aaaaa'), 50 | Item(3, 'B', 'bbbbb')] 51 | 52 | @classmethod 53 | def get_element_by_id(cls, id): 54 | return [i for i in cls.get_elements() if i.id == id][0] 55 | 56 | 57 | if __name__ == '__main__': 58 | app.run(debug=True) 59 | -------------------------------------------------------------------------------- /examples/simple_nested.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col, NestedTableCol 2 | 3 | 4 | """Lets suppose that we have a class that we get an iterable of from 5 | somewhere, such as a database. We can declare a table that pulls out 6 | the relevant entries, escapes them and displays them. Additionally, 7 | we show here how to used a NestedTableCol, by first defining a 8 | sub-table. 9 | 10 | """ 11 | 12 | 13 | class SubItem(object): 14 | def __init__(self, col1, col2): 15 | self.col1 = col1 16 | self.col2 = col2 17 | 18 | 19 | class Item(object): 20 | def __init__(self, name, description, subtable): 21 | self.name = name 22 | self.description = description 23 | self.subtable = subtable 24 | 25 | 26 | class SubItemTable(Table): 27 | col1 = Col('Sub-column 1') 28 | col2 = Col('Sub-column 2') 29 | 30 | 31 | class ItemTable(Table): 32 | name = Col('Name') 33 | description = Col('Description') 34 | subtable = NestedTableCol('Subtable', SubItemTable) 35 | 36 | 37 | def main(): 38 | items = [Item('Name1', 'Description1', [SubItem('r1sr1c1', 'r1sr1c2'), 39 | SubItem('r1sr2c1', 'r1sr2c2')]), 40 | Item('Name2', 'Description2', [SubItem('r2sr1c1', 'r2sr1c2'), 41 | SubItem('r2sr2c1', 'r2sr2c2')]), 42 | ] 43 | 44 | table = ItemTable(items) 45 | 46 | # or {{ table }} in jinja 47 | print(table.__html__()) 48 | 49 | """Outputs: 50 | 51 | 52 | 53 | 54 | 55 | 56 | 65 | 74 | 75 |
NameDescriptionSubtable
Name1Description1 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
Sub-column 1Sub-column 2
r1sr1c1r1sr1c2
r1sr2c1r1sr2c2
Name2Description2 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
Sub-column 1Sub-column 2
r2sr1c1r2sr1c2
r2sr2c1r2sr2c2
76 | 77 | Except it doesn't bother to prettify the output. 78 | """ 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /examples/simple_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from flask import Flask 4 | from flask_sqlalchemy import SQLAlchemy 5 | from flask_table import Table, Col 6 | 7 | 8 | # Some application and database setup. This should be taken care of 9 | # elsewhere in an application and is not specific to the tables. See 10 | # Flask-SQLAlchemy docs for more about what's going on for this first 11 | # bit. 12 | app = Flask(__name__) 13 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' 14 | # To suppress a warning. Not important. 15 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 16 | db = SQLAlchemy(app) 17 | 18 | 19 | # An example model 20 | class User(db.Model): 21 | id = db.Column(db.Integer, primary_key=True) 22 | username = db.Column(db.String(80), unique=True) 23 | email = db.Column(db.String(120), unique=True) 24 | 25 | def __init__(self, username, email): 26 | self.username = username 27 | self.email = email 28 | 29 | def __repr__(self): 30 | return '' % self.username 31 | 32 | 33 | # Create the database, and put some records in it. 34 | db.create_all() 35 | 36 | user1 = User('user1', 'test1@example.com') 37 | user2 = User('user2', 'test2@example.com') 38 | 39 | db.session.add(user1) 40 | db.session.add(user2) 41 | db.session.commit() 42 | 43 | 44 | # Define a table, then pass in the database records 45 | class UserTable(Table): 46 | username = Col('Username') 47 | email = Col('Email') 48 | 49 | users = User.query.all() 50 | print(UserTable(items=users).__html__()) 51 | -------------------------------------------------------------------------------- /examples/sortable.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col, LinkCol 2 | from flask import Flask, Markup, request, url_for 3 | 4 | """ 5 | A example for creating a Table that is sortable by its header 6 | """ 7 | 8 | app = Flask(__name__) 9 | 10 | 11 | class SortableTable(Table): 12 | id = Col('ID') 13 | name = Col('Name') 14 | description = Col('Description') 15 | link = LinkCol( 16 | 'Link', 'flask_link', url_kwargs=dict(id='id'), allow_sort=False) 17 | allow_sort = True 18 | 19 | def sort_url(self, col_key, reverse=False): 20 | if reverse: 21 | direction = 'desc' 22 | else: 23 | direction = 'asc' 24 | return url_for('index', sort=col_key, direction=direction) 25 | 26 | 27 | @app.route('/') 28 | def index(): 29 | sort = request.args.get('sort', 'id') 30 | reverse = (request.args.get('direction', 'asc') == 'desc') 31 | table = SortableTable(Item.get_sorted_by(sort, reverse), 32 | sort_by=sort, 33 | sort_reverse=reverse) 34 | return table.__html__() 35 | 36 | 37 | @app.route('/item/') 38 | def flask_link(id): 39 | element = Item.get_element_by_id(id) 40 | return '

{}

{}


id: {}'.format( 41 | element.name, element.description, element.id) 42 | 43 | 44 | class Item(object): 45 | """ a little fake database """ 46 | def __init__(self, id, name, description): 47 | self.id = id 48 | self.name = name 49 | self.description = description 50 | 51 | @classmethod 52 | def get_elements(cls): 53 | return [ 54 | Item(1, 'Z', 'zzzzz'), 55 | Item(2, 'K', 'aaaaa'), 56 | Item(3, 'B', 'bbbbb')] 57 | 58 | @classmethod 59 | def get_sorted_by(cls, sort, reverse=False): 60 | return sorted( 61 | cls.get_elements(), 62 | key=lambda x: getattr(x, sort), 63 | reverse=reverse) 64 | 65 | @classmethod 66 | def get_element_by_id(cls, id): 67 | return [i for i in cls.get_elements() if i.id == id][0] 68 | 69 | if __name__ == '__main__': 70 | app.run(debug=True) 71 | -------------------------------------------------------------------------------- /examples/subclassing.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col 2 | 3 | 4 | class Item(object): 5 | def __init__(self, name, language): 6 | self.name = name 7 | self.language = language 8 | 9 | 10 | class LangCol(Col): 11 | def td_format(self, content): 12 | if content == 'en_GB': 13 | return 'British English' 14 | elif content == 'de_DE': 15 | return 'German' 16 | elif content == 'fr_FR': 17 | return 'French' 18 | else: 19 | return 'Not Specified' 20 | 21 | 22 | class ItemTable(Table): 23 | name = Col('Name') 24 | language = LangCol('Language') 25 | 26 | 27 | def main(): 28 | items = [Item('A', 'en_GB'), 29 | Item('B', 'de_DE'), 30 | Item('C', 'fr_FR'), 31 | Item('D', None)] 32 | 33 | tab = ItemTable(items) 34 | 35 | # or {{ tab }} in jinja 36 | print(tab.__html__()) 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /examples/subclassing2.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col 2 | 3 | 4 | class RawCol(Col): 5 | """Class that will just output whatever it is given and will not 6 | escape it. 7 | """ 8 | 9 | def td_format(self, content): 10 | return content 11 | 12 | 13 | class ItemTable(Table): 14 | name = Col('Name') 15 | raw = RawCol('Raw') 16 | 17 | 18 | def main(): 19 | items = [{'name': 'A', 'raw': 'a'}, 20 | {'name': 'B', 'raw': 'b'}] 21 | 22 | tab = ItemTable(items) 23 | 24 | # or {{ tab }} in jinja 25 | print(tab.__html__()) 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /examples/subclassing3.py: -------------------------------------------------------------------------------- 1 | from flask_table import Table, Col, ButtonCol 2 | from flask import Flask, request 3 | 4 | """An example for creating LinkCol or ButtonCol with local attributes 5 | that can't be initially set when subclassing Table 6 | 7 | """ 8 | 9 | app = Flask(__name__) 10 | 11 | 12 | class LocalAttributeLinkTable(Table): 13 | name = Col('Name') 14 | 15 | def __init__(self, local_attribute, items): 16 | super(LocalAttributeLinkTable, self).__init__(items) 17 | 18 | # Add a column depending on a attribute passed to 19 | # the constructor 20 | self.add_column('redirectWithLocalID', 21 | ButtonCol('Select this', 'some_url', 22 | url_kwargs_extra=dict( 23 | someLocalID=local_attribute))) 24 | 25 | 26 | @app.route('/', methods=['POST']) 27 | def index(): 28 | items = [{'name': 'A'}, 29 | {'name': 'B'}] 30 | 31 | # This local attribute could be changed on each call to this route 32 | some_local_attribute = '68f36d30-6600-4a67-b2d4-7cc011ceea0e' 33 | 34 | # Pass some local static attribute to a table 35 | table = LocalAttributeLinkTable(some_local_attribute, items) 36 | 37 | # You would usually want to pass table out to a template with 38 | # render_template. 39 | return table.__html__() 40 | 41 | 42 | @app.route('/some_url', methods=['POST']) 43 | def some_url(): 44 | local_attribute_passed = request.args.get('someLocalID') 45 | 46 | # Display the local attribute for testing purposes 47 | return 'local_attribute: "' + local_attribute_passed + '"' 48 | 49 | 50 | if __name__ == '__main__': 51 | app.run(debug=True) 52 | -------------------------------------------------------------------------------- /flask_table/__init__.py: -------------------------------------------------------------------------------- 1 | from .table import Table, create_table 2 | from .columns import ( 3 | Col, 4 | BoolCol, 5 | DateCol, 6 | DatetimeCol, 7 | LinkCol, 8 | ButtonCol, 9 | OptCol, 10 | NestedTableCol, 11 | BoolNaCol, 12 | ) 13 | -------------------------------------------------------------------------------- /flask_table/columns.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from flask import Markup, url_for 4 | from babel.dates import format_date, format_datetime 5 | from flask_babel import gettext as _ 6 | 7 | from .html import element 8 | 9 | 10 | def _single_get(item, key): 11 | # First, try to lookup the key as if the item were a dict. If 12 | # that fails, lookup the key as an atrribute of an item. 13 | try: 14 | val = item[key] 15 | except (KeyError, TypeError): 16 | val = getattr(item, key) 17 | 18 | # once we have the value, try calling it as a function. If 19 | # that fails, the just return it. 20 | try: 21 | return val() 22 | except TypeError: 23 | return val 24 | 25 | 26 | def _recursive_getattr(item, keys): 27 | # See if keys is as string, if so, we need to split on the dots. 28 | try: 29 | keys = keys.split('.') 30 | except AttributeError: 31 | pass 32 | 33 | if item is None: 34 | return None 35 | if len(keys) == 1: 36 | return _single_get(item, keys[0]) 37 | else: 38 | return _recursive_getattr(_single_get(item, keys[0]), keys[1:]) 39 | 40 | 41 | class Col(object): 42 | """The subclass for all Columns, and the class that just gets some 43 | data from each item an outputs it. 44 | 45 | We use this hack with _counter to make sure that the columns end 46 | up in the same order as when declared. Each column must set a 47 | name, which is used in as the heading for that column, and can 48 | optionally set an attr or an attr_list. attr='foo' is equivalent 49 | to attr_list=['foo'] and attr_list=['foo', 'bar', 'baz'] will 50 | attempt to get item.foo.bar.baz for each item in the iterable 51 | given to the table. If item.foo.bar is None, then this process 52 | will terminate and will not error. However, if item.foo.bar is an 53 | object without an attribute 'baz', then this will currently error. 54 | 55 | """ 56 | 57 | _counter = 0 58 | 59 | def __init__(self, name, attr=None, attr_list=None, 60 | allow_sort=True, show=True, 61 | th_html_attrs=None, td_html_attrs=None, 62 | column_html_attrs=None): 63 | self.name = name 64 | self.allow_sort = allow_sort 65 | self._counter_val = Col._counter 66 | self.attr_list = attr_list 67 | if attr: 68 | self.attr_list = attr.split('.') 69 | self.show = show 70 | 71 | column_html_attrs = column_html_attrs or {} 72 | self.td_html_attrs = column_html_attrs.copy() 73 | self.td_html_attrs.update(td_html_attrs or {}) 74 | self.th_html_attrs = column_html_attrs.copy() 75 | self.th_html_attrs.update(th_html_attrs or {}) 76 | 77 | Col._counter += 1 78 | 79 | def get_attr_list(self, attr): 80 | if self.attr_list: 81 | return self.attr_list 82 | elif attr: 83 | return attr.split('.') 84 | else: 85 | return None 86 | 87 | def from_attr_list(self, item, attr_list): 88 | out = _recursive_getattr(item, attr_list) 89 | if out is None: 90 | return '' 91 | else: 92 | return out 93 | 94 | def td(self, item, attr): 95 | content = self.td_contents(item, self.get_attr_list(attr)) 96 | return element( 97 | 'td', 98 | content=content, 99 | escape_content=False, 100 | attrs=self.td_html_attrs) 101 | 102 | def td_contents(self, item, attr_list): 103 | """Given an item and an attr, return the contents of the td. 104 | 105 | This method is a likely candidate to override when extending 106 | the Col class, which is done in LinkCol and 107 | ButtonCol. Override this method if you need to get some extra 108 | data from the item. 109 | 110 | Note that the output of this function is NOT escaped. 111 | 112 | """ 113 | return self.td_format(self.from_attr_list(item, attr_list)) 114 | 115 | def td_format(self, content): 116 | """Given just the value extracted from the item, return what should 117 | appear within the td. 118 | 119 | This method is also a good choice to override when extending, 120 | which is done in the BoolCol, DateCol and DatetimeCol 121 | classes. Override this method when you just need the standard 122 | data that attr_list gets from the item, but need to adjust how 123 | it is represented. 124 | 125 | Note that the output of this function is escaped. 126 | 127 | """ 128 | return Markup.escape(content) 129 | 130 | 131 | class OptCol(Col): 132 | """Translate the contents according to a dictionary of choices. 133 | 134 | """ 135 | 136 | def __init__(self, name, choices=None, default_key=None, default_value='', 137 | coerce_fn=None, **kwargs): 138 | super(OptCol, self).__init__(name, **kwargs) 139 | if choices is None: 140 | self.choices = {} 141 | else: 142 | self.choices = choices 143 | self.default_value = self.choices.get(default_key, default_value) 144 | self.coerce_fn = coerce_fn 145 | 146 | def from_attr_list(self, item, attr_list): 147 | # Don't convert None to empty string here. 148 | return _recursive_getattr(item, attr_list) 149 | 150 | def coerce_content(self, content): 151 | if self.coerce_fn: 152 | return self.coerce_fn(content) 153 | else: 154 | return content 155 | 156 | def td_format(self, content): 157 | return self.choices.get( 158 | self.coerce_content(content), self.default_value) 159 | 160 | 161 | class BoolCol(OptCol): 162 | """Output Yes/No values for truthy or falsey values. 163 | 164 | """ 165 | 166 | yes_display = _('Yes') 167 | no_display = _('No') 168 | 169 | def __init__(self, name, yes_display=None, no_display=None, **kwargs): 170 | if yes_display is None: 171 | yes_display = self.yes_display 172 | if no_display is None: 173 | no_display = self.no_display 174 | super(BoolCol, self).__init__( 175 | name, 176 | choices={True: yes_display, False: no_display}, 177 | coerce_fn=bool, 178 | **kwargs) 179 | 180 | 181 | class BoolNaCol(OptCol): 182 | """Output Yes/No values for truthy or falsey values, or N/A for None. 183 | 184 | """ 185 | 186 | yes_display = _('Yes') 187 | no_display = _('No') 188 | na_display = _('N/A') 189 | 190 | def __init__(self, name, yes_display=None, no_display=None, 191 | na_display=None, **kwargs): 192 | if yes_display is None: 193 | yes_display = self.yes_display 194 | if no_display is None: 195 | no_display = self.no_display 196 | if na_display is None: 197 | na_display = self.na_display 198 | 199 | def bool_or_none(value): 200 | if value is None: 201 | return None 202 | return bool(value) 203 | 204 | super(BoolNaCol, self).__init__( 205 | name, 206 | choices={True: yes_display, False: no_display, None: na_display}, 207 | coerce_fn=bool_or_none, 208 | **kwargs) 209 | 210 | 211 | class DateCol(Col): 212 | """Format the content as a date, unless it is None, in which case, 213 | output empty. 214 | 215 | """ 216 | def __init__(self, name, date_format='short', **kwargs): 217 | super(DateCol, self).__init__(name, **kwargs) 218 | self.date_format = date_format 219 | 220 | def td_format(self, content): 221 | if content: 222 | return format_date(content, self.date_format) 223 | else: 224 | return '' 225 | 226 | 227 | class DatetimeCol(Col): 228 | """Format the content as a datetime, unless it is None, in which case, 229 | output empty. 230 | 231 | """ 232 | def __init__(self, name, datetime_format='short', **kwargs): 233 | super(DatetimeCol, self).__init__(name, **kwargs) 234 | self.datetime_format = datetime_format 235 | 236 | def td_format(self, content): 237 | if content: 238 | return format_datetime(content, self.datetime_format) 239 | else: 240 | return '' 241 | 242 | 243 | class LinkCol(Col): 244 | """Format the content as a link. Requires a endpoint to use to find 245 | the url and can also take a dict of url_kwargs which is expected 246 | to have values that are strings which are used to get data from 247 | the item. 248 | 249 | Eg: 250 | 251 | view = LinkCol('View', 'view_fn', url_kwargs=dict(id='id')) 252 | 253 | This will create a link to the address given by url_for('view_fn', 254 | id=item.id) for each item in the iterable. 255 | 256 | """ 257 | def __init__(self, name, endpoint, attr=None, attr_list=None, 258 | url_kwargs=None, url_kwargs_extra=None, 259 | anchor_attrs=None, text_fallback=None, **kwargs): 260 | super(LinkCol, self).__init__( 261 | name, 262 | attr=attr, 263 | attr_list=attr_list, 264 | **kwargs) 265 | self.endpoint = endpoint 266 | self._url_kwargs = url_kwargs or {} 267 | self._url_kwargs_extra = url_kwargs_extra or {} 268 | self.text_fallback = text_fallback 269 | self.anchor_attrs = anchor_attrs or {} 270 | 271 | def url_kwargs(self, item): 272 | # We give preference to the item kwargs, rather than the extra 273 | # kwargs. 274 | kwargs = self._url_kwargs_extra.copy() 275 | item_kwargs = {k: _recursive_getattr(item, v) 276 | for k, v in self._url_kwargs.items()} 277 | kwargs.update(item_kwargs) 278 | return kwargs 279 | 280 | def get_attr_list(self, attr): 281 | return super(LinkCol, self).get_attr_list(None) 282 | 283 | def text(self, item, attr_list): 284 | if attr_list: 285 | return self.from_attr_list(item, attr_list) 286 | elif self.text_fallback: 287 | return self.text_fallback 288 | else: 289 | return self.name 290 | 291 | def url(self, item): 292 | return url_for(self.endpoint, **self.url_kwargs(item)) 293 | 294 | def td_contents(self, item, attr_list): 295 | attrs = dict(href=self.url(item)) 296 | attrs.update(self.anchor_attrs) 297 | text = self.td_format(self.text(item, attr_list)) 298 | return element('a', attrs=attrs, content=text, escape_content=False) 299 | 300 | 301 | class ButtonCol(LinkCol): 302 | """Just the same a LinkCol, but creates an empty form which gets 303 | posted to the specified url. 304 | 305 | Eg: 306 | 307 | delete = ButtonCol('Delete', 'delete_fn', url_kwargs=dict(id='id')) 308 | 309 | When clicked, this will post to url_for('delete_fn', id=item.id). 310 | 311 | Can pass button_attrs to pass extra attributes to the button 312 | element. 313 | 314 | """ 315 | 316 | def __init__(self, name, endpoint, attr=None, attr_list=None, 317 | url_kwargs=None, button_attrs=None, form_attrs=None, 318 | form_hidden_fields=None, **kwargs): 319 | super(ButtonCol, self).__init__( 320 | name, 321 | endpoint, 322 | attr=attr, 323 | attr_list=attr_list, 324 | url_kwargs=url_kwargs, **kwargs) 325 | self.button_attrs = button_attrs or {} 326 | self.form_attrs = form_attrs or {} 327 | self.form_hidden_fields = form_hidden_fields or {} 328 | 329 | def td_contents(self, item, attr_list): 330 | button_attrs = dict(self.button_attrs) 331 | button_attrs['type'] = 'submit' 332 | button = element( 333 | 'button', 334 | attrs=button_attrs, 335 | content=self.text(item, attr_list), 336 | ) 337 | form_attrs = dict(self.form_attrs) 338 | form_attrs.update(dict( 339 | method='post', 340 | action=self.url(item), 341 | )) 342 | form_hidden_fields_elements = [ 343 | element( 344 | 'input', 345 | attrs=dict( 346 | type='hidden', 347 | name=name, 348 | value=value)) 349 | for name, value in sorted(self.form_hidden_fields.items())] 350 | return element( 351 | 'form', 352 | attrs=form_attrs, 353 | content=[ 354 | ''.join(form_hidden_fields_elements), 355 | button 356 | ], 357 | escape_content=False, 358 | ) 359 | 360 | 361 | class NestedTableCol(Col): 362 | """This column type allows for nesting tables into a column. The 363 | nested table is defined as a sub-class of Table as usual. Then in 364 | the main table, a column is defined using NestedTableCol with the 365 | second argument being the name of the Table sub-class object 366 | defined for the nested table. 367 | 368 | Eg: 369 | 370 | class MySubTable(Table): 371 | a = Col('1st nested table col') 372 | b = Col('2nd nested table col') 373 | 374 | class MainTable(Table): 375 | id = Col('id') 376 | objects = NestedTableCol('objects', MySubTable) 377 | 378 | """ 379 | 380 | def __init__(self, name, table_class, **kwargs): 381 | super(NestedTableCol, self).__init__(name, **kwargs) 382 | self.table_class = table_class 383 | 384 | def td_format(self, content): 385 | t = self.table_class(content).__html__() 386 | return t 387 | -------------------------------------------------------------------------------- /flask_table/compat.py: -------------------------------------------------------------------------------- 1 | def with_metaclass(meta, base=object): 2 | return meta("NewBase", (base,), {}) 3 | -------------------------------------------------------------------------------- /flask_table/html.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from functools import partial 4 | 5 | from flask import Markup 6 | 7 | 8 | def element(element, attrs=None, content='', 9 | escape_attrs=True, escape_content=True): 10 | return '<{element}{formatted_attrs}>{content}'.format( 11 | element=element, 12 | formatted_attrs=_format_attrs(attrs or {}, escape_attrs), 13 | content=_format_content(content, escape_content), 14 | ) 15 | 16 | 17 | def _format_attrs(attrs, escape_attrs=True): 18 | out = [] 19 | for name, value in sorted(attrs.items()): 20 | if escape_attrs: 21 | name = Markup.escape(name) 22 | value = Markup.escape(value) 23 | out.append(' {name}="{value}"'.format(name=name, value=value)) 24 | return ''.join(out) 25 | 26 | 27 | def _format_content(content, escape_content=True): 28 | if isinstance(content, (list, tuple)): 29 | content = ''.join(content) 30 | if escape_content: 31 | return Markup.escape(content) 32 | return content 33 | -------------------------------------------------------------------------------- /flask_table/table.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from collections import OrderedDict 4 | 5 | from flask import Markup 6 | from flask_babel import gettext as _ 7 | 8 | from .columns import Col 9 | from .compat import with_metaclass 10 | from .html import element 11 | 12 | 13 | class TableMeta(type): 14 | """The metaclass for the Table class. We use the metaclass to sort of 15 | the columns defined in the table declaration. 16 | 17 | """ 18 | 19 | def __new__(meta, name, bases, attrs): 20 | """Create the class as normal, but also iterate over the attributes 21 | set and gather up any that are Cols, and store them, so they 22 | can be iterated over later. 23 | 24 | """ 25 | cls = type.__new__(meta, name, bases, attrs) 26 | cls._cols = OrderedDict() 27 | # If there are any base classes with a `_cols` attribute, add 28 | # them to the columns for this table. 29 | for parent in bases: 30 | try: 31 | parent_cols = parent._cols 32 | except AttributeError: 33 | continue 34 | else: 35 | cls._cols.update(parent_cols) 36 | # Then add the columns from this class. 37 | this_cls_cols = sorted( 38 | ((k, v) for k, v in attrs.items() if isinstance(v, Col)), 39 | key=lambda x: x[1]._counter_val) 40 | cls._cols.update(OrderedDict(this_cls_cols)) 41 | return cls 42 | 43 | 44 | class Table(with_metaclass(TableMeta)): 45 | """The main table class that should be subclassed when to create a 46 | table. Initialise with an iterable of objects. Then either use the 47 | __html__ method, or just output in a template to output the table 48 | as html. Can also set a list of classes, either when declaring the 49 | table, or when initialising. Can also set the text to display if 50 | there are no items to display. 51 | 52 | """ 53 | 54 | # For setting attributes on the element. 55 | html_attrs = None 56 | classes = [] 57 | table_id = None 58 | border = False 59 | 60 | thead_attrs = None 61 | thead_classes = [] 62 | allow_sort = False 63 | no_items = _('No Items') 64 | allow_empty = False 65 | 66 | def __init__(self, items, classes=None, thead_classes=None, 67 | sort_by=None, sort_reverse=False, no_items=None, 68 | table_id=None, border=None, html_attrs=None): 69 | self.items = items 70 | self.sort_by = sort_by 71 | self.sort_reverse = sort_reverse 72 | if classes is not None: 73 | self.classes = classes 74 | if thead_classes is not None: 75 | self.thead_classes = thead_classes 76 | if no_items is not None: 77 | self.no_items = no_items 78 | if table_id is not None: 79 | self.table_id = table_id 80 | if html_attrs is not None: 81 | self.html_attrs = html_attrs 82 | if border is not None: 83 | self.border = border 84 | 85 | def get_html_attrs(self): 86 | attrs = dict(self.html_attrs) if self.html_attrs else {} 87 | if self.table_id: 88 | attrs['id'] = self.table_id 89 | if self.classes: 90 | attrs['class'] = ' '.join(self.classes) 91 | if self.border: 92 | attrs['border'] = 1 93 | return attrs 94 | 95 | def get_thead_attrs(self): 96 | attrs = dict(self.thead_attrs) if self.thead_attrs else {} 97 | if self.thead_classes: 98 | attrs['class'] = ' '.join(self.thead_classes) 99 | return attrs 100 | 101 | def __html__(self): 102 | tbody = self.tbody() 103 | if tbody or self.allow_empty: 104 | content = '\n{thead}\n{tbody}\n'.format( 105 | thead=self.thead(), 106 | tbody=tbody, 107 | ) 108 | return element( 109 | 'table', 110 | attrs=self.get_html_attrs(), 111 | content=content, 112 | escape_content=False) 113 | else: 114 | return element('p', content=self.no_items) 115 | 116 | def thead(self): 117 | ths = ''.join( 118 | self.th(col_key, col) 119 | for col_key, col in self._cols.items() 120 | if col.show) 121 | content = element('tr', content=ths, escape_content=False) 122 | return element( 123 | 'thead', 124 | attrs=self.get_thead_attrs(), 125 | content=content, 126 | escape_content=False, 127 | ) 128 | 129 | def tbody(self): 130 | out = [self.tr(item) for item in self.items] 131 | if not out: 132 | return '' 133 | content = '\n{}\n'.format('\n'.join(out)) 134 | return element('tbody', content=content, escape_content=False) 135 | 136 | def get_tr_attrs(self, item): 137 | return {} 138 | 139 | def tr(self, item): 140 | content = ''.join(c.td(item, attr) for attr, c in self._cols.items() 141 | if c.show) 142 | return element( 143 | 'tr', 144 | attrs=self.get_tr_attrs(item), 145 | content=content, 146 | escape_content=False) 147 | 148 | def th_contents(self, col_key, col): 149 | if not (col.allow_sort and self.allow_sort): 150 | return Markup.escape(col.name) 151 | 152 | if self.sort_by == col_key: 153 | if self.sort_reverse: 154 | href = self.sort_url(col_key) 155 | label_prefix = '↑' 156 | else: 157 | href = self.sort_url(col_key, reverse=True) 158 | label_prefix = '↓' 159 | else: 160 | href = self.sort_url(col_key) 161 | label_prefix = '' 162 | label = '{prefix}{label}'.format(prefix=label_prefix, label=col.name) 163 | return element('a', attrs=dict(href=href), content=label) 164 | 165 | def th(self, col_key, col): 166 | return element( 167 | 'th', 168 | content=self.th_contents(col_key, col), 169 | escape_content=False, 170 | attrs=col.th_html_attrs, 171 | ) 172 | 173 | def sort_url(self, col_id, reverse=False): 174 | raise NotImplementedError('sort_url not implemented') 175 | 176 | @classmethod 177 | def add_column(cls, name, col): 178 | cls._cols[name] = col 179 | return cls 180 | 181 | 182 | def create_table(name=str('_Table'), base=Table, options=None): 183 | """Creates and returns a new table class. You can specify a name for 184 | you class if you wish. You can also set the base class (or 185 | classes) that should be used when creating the class. 186 | 187 | """ 188 | try: 189 | base = tuple(base) 190 | except TypeError: 191 | # Then assume that what we have is a single class, so make it 192 | # into a 1-tuple. 193 | base = (base,) 194 | 195 | return TableMeta(name, base, options or {}) 196 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Flask Table 2 | pages: 3 | - Flask Table: index.md 4 | -------------------------------------------------------------------------------- /pep8.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pep8 flask_table/*.py examples/*.py tests/*.py setup.py 4 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | 6 | function error() { 7 | >&2 echo "$1" 8 | exit 1 9 | } 10 | 11 | function confirm() { 12 | msg="$1" 13 | if [[ -z "$msg" ]]; then 14 | msg="Are you sure?" 15 | fi 16 | read -r -p "$msg [Y/n] " response 17 | case $response in 18 | [yY][eE][sS]|[yY]|"") 19 | return 20 | ;; 21 | *) 22 | error "Abort." 23 | ;; 24 | esac 25 | } 26 | 27 | 28 | function main() { 29 | arg="$1" 30 | 31 | if [[ $# -eq 0 ]]; then 32 | error "No command given." 33 | fi 34 | 35 | case $arg in 36 | tag) 37 | tag "$2" 38 | exit 0; 39 | ;; 40 | publish) 41 | publish 42 | exit 0; 43 | ;; 44 | _generate_readme) 45 | generate_readme 46 | exit 0; 47 | ;; 48 | *) 49 | error "Unrecognised argument: $arg. Choose from tag,publish" 50 | ;; 51 | esac 52 | } 53 | 54 | 55 | function _trim_from_upto() { 56 | fname="$1" 57 | from="$2" 58 | upto="$3" 59 | 60 | from_line="$(grep -n "$from" "$fname" | sed 's/\:.*//')" 61 | upto_line="$(grep -n "$upto" "$fname" | sed 's/\:.*//')" 62 | 63 | if [[ -z $from_line ]]; then error "Unable to find $from in $fname"; fi 64 | if [[ -z $upto_line ]]; then error "Unable to find $upto in $fname"; fi 65 | 66 | upto_line=$(($upto_line-1)) 67 | 68 | sed -i "$from_line,${upto_line}d" "$fname" 69 | } 70 | 71 | 72 | function generate_readme() { 73 | pandoc --from=markdown --to=rst --output=README README.md || error "Unable to convert README.md" 74 | # Remove the Github status links 75 | sed -i '/Build Status.*Coverage Status.*PyPI/,+1d' README 76 | # And remove a big chunk of html that gets a bit exploded 77 | _trim_from_upto README "Or as HTML:" "Extra things:" 78 | } 79 | 80 | 81 | function tag() { 82 | up="${1:-patch}" 83 | 84 | [[ "$(git rev-parse --abbrev-ref HEAD)" == master ]] || error "Must be on master to tag." 85 | 86 | git diff-files --quiet || error "Working directory must be clean to tag" 87 | 88 | current="$(grep version setup.py | sed 's/^.*=//' | sed 's/,$//' | sed "s/['\"]//g")" 89 | re='\([0-9]\+\)\.\([0-9]\+\)\.\([0-9]\+\)' 90 | major="$(echo $current | sed "s/$re/\1/")" 91 | minor="$(echo $current | sed "s/$re/\2/")" 92 | patch="$(echo $current | sed "s/$re/\3/")" 93 | 94 | echo "Parsed current version as: $major.$minor.$patch" 95 | 96 | case $up in 97 | patch) 98 | patch=$(($patch+1)) 99 | ;; 100 | minor) 101 | minor=$(($minor+1)) 102 | patch=0 103 | ;; 104 | major) 105 | major=$(($major+1)) 106 | minor=0 107 | patch=0 108 | ;; 109 | *) 110 | error "Unknown version section to increase: $up. Please select from: major,minor,patch." 111 | ;; 112 | esac 113 | 114 | new="$major.$minor.$patch" 115 | 116 | echo "Ready to tag new version $new." 117 | confirm 118 | 119 | sed -i "s/$current/$new/" setup.py 120 | 121 | underline="$(echo "$new" | sed 's/./-/g')" 122 | log="$(git log "$(git describe --tags --abbrev=0)..." --pretty=format:'%s' --reverse | while read line; do echo "- $line"; done)" 123 | changelog_entry="$new 124 | $underline 125 | $log 126 | " 127 | 128 | touch CHANGELOG.md 129 | cat <(echo "$changelog_entry") CHANGELOG.md > CHANGELOG.md.new 130 | mv CHANGELOG.md.new CHANGELOG.md 131 | 132 | echo "CHANGES:" 133 | echo "$log" 134 | echo "/CHANGES" 135 | 136 | confirm "Written changes to CHANGELOG.md, but not yet added. Edit there and then continue." 137 | 138 | git add setup.py CHANGELOG.md 139 | git commit -m "Bump to version $new" 140 | git push origin master 141 | git tag -a "v$new" -m "Version $new" 142 | git push origin "v$new" 143 | } 144 | 145 | function publish() { 146 | generate_readme 147 | 148 | echo "Ready publish to PyPI." 149 | confirm 150 | rm -rf dist 151 | python setup.py sdist 152 | twine upload dist/Flask-Table-*.tar.gz 153 | } 154 | 155 | main $@ 156 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-Babel 3 | Flask-Testing 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | install_requires = [ 6 | 'Flask', 7 | 'Flask-Babel', 8 | ] 9 | 10 | if os.path.exists('README'): 11 | with open('README') as f: 12 | readme = f.read() 13 | else: 14 | readme = None 15 | 16 | setup( 17 | name='Flask-Table', 18 | packages=['flask_table'], 19 | version='0.5.0', 20 | author='Andrew Plummer', 21 | author_email='plummer574@gmail.com', 22 | url='https://github.com/plumdog/flask_table', 23 | description='HTML tables for use with the Flask micro-framework', 24 | install_requires=install_requires, 25 | test_suite='tests', 26 | tests_require=['flask-testing'], 27 | long_description=readme, 28 | classifiers=[ 29 | 'Development Status :: 3 - Alpha', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.3', 35 | 'Programming Language :: Python :: 3.4', 36 | 'Operating System :: OS Independent', 37 | 'Framework :: Flask', 38 | ]) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | # Set all of our environment variables before we import other things 6 | # that may read these at import time. We also use noqa to stop pep8 7 | # from worrying about imports not being at the top of the file. 8 | for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'LC_TIME']: # noqa 9 | os.environ[name] = '' 10 | os.environ['LANGUAGE'] = 'en_GB.UTF-8' # noqa 11 | 12 | import io 13 | import unittest 14 | from flask import Flask, url_for 15 | from flask_table import (Table, Col, LinkCol, ButtonCol, OptCol, BoolCol, 16 | DateCol, DatetimeCol, NestedTableCol, create_table, 17 | BoolNaCol) 18 | import flask_testing 19 | from datetime import date, datetime 20 | 21 | 22 | class Item(object): 23 | def __init__(self, **kwargs): 24 | for k, v in kwargs.items(): 25 | setattr(self, k, v) 26 | 27 | 28 | class Subitem(Item): 29 | pass 30 | 31 | 32 | def html_reduce(s): 33 | return ''.join(l.strip() for l in s.split('\n')) 34 | 35 | 36 | class TableTest(unittest.TestCase): 37 | def assert_in(self, x, y): 38 | if x not in y: 39 | raise AssertionError( 40 | '{x} is not in {y}, but should be.'.format(x=x, y=y)) 41 | 42 | def assert_in_html(self, x, y): 43 | return self.assert_in(x, y.__html__()) 44 | 45 | def assert_not_in(self, x, y): 46 | if x in y: 47 | raise AssertionError( 48 | '{x} is in {y}, but shouldn\'t be.'.format(x=x, y=y)) 49 | 50 | def assert_not_in_html(self, x, y): 51 | return self.assert_not_in(x, y.__html__()) 52 | 53 | def assert_html_equivalent(self, test_tab, reference): 54 | self.assertEqual( 55 | html_reduce(test_tab.__html__()), 56 | html_reduce(reference)) 57 | 58 | @classmethod 59 | def get_html(cls, d, name): 60 | path = os.path.join( 61 | os.path.abspath(os.path.dirname(__file__)), 62 | 'html', d, name + '.html') 63 | with io.open(path, encoding="utf8") as f: 64 | return f.read() 65 | 66 | @property 67 | def table_cls(self): 68 | try: 69 | return self._table_cls 70 | except AttributeError: 71 | return self.MyTable 72 | 73 | @table_cls.setter 74 | def table_cls(self, table_cls): 75 | self._table_cls = table_cls 76 | 77 | def assert_html_equivalent_from_file(self, d, name, items=None, table=None, 78 | table_kwargs=None, print_html=False): 79 | if table is None: 80 | items = items or [] 81 | table_kwargs = table_kwargs or {} 82 | 83 | table = self.table_cls( 84 | items, 85 | **table_kwargs) 86 | 87 | if print_html: 88 | print(tab.__html__()) 89 | html = self.get_html(d, name) 90 | self.assert_html_equivalent(table, html) 91 | 92 | 93 | def test_app(): 94 | app = Flask(__name__) 95 | 96 | @app.route('/', defaults=dict(sort=None, direction=None)) 97 | @app.route('/sort//', defaults=dict(direction=None)) 98 | @app.route('/sort//direction//') 99 | def index(sort, direction): 100 | return 'Index' 101 | 102 | @app.route('/view/') 103 | def view(id_): 104 | return 'View {}'.format(id_) 105 | 106 | @app.route('/delete/', methods=['POST']) 107 | def delete(id_): 108 | return 'Delete {}'.format(id_) 109 | 110 | return app 111 | 112 | 113 | class FlaskTableTest(flask_testing.TestCase, TableTest): 114 | def create_app(self): 115 | return test_app() 116 | 117 | 118 | class TableIDTest(TableTest): 119 | 120 | class MyTable(Table): 121 | name = Col('Name Heading') 122 | 123 | def test_one(self): 124 | items = [Item(name='one')] 125 | self.assert_html_equivalent_from_file( 126 | 'tableid_test', 'test_one', items, 127 | table_kwargs=dict(table_id='Test table ID')) 128 | 129 | 130 | class TableIDOnClassTest(TableTest): 131 | 132 | class MyTable(Table): 133 | table_id = 'Test table ID' 134 | name = Col('Name Heading') 135 | 136 | def test_one(self): 137 | items = [Item(name='one')] 138 | self.assert_html_equivalent_from_file( 139 | 'tableid_test', 'test_one', items) 140 | 141 | 142 | class BorderTest(TableTest): 143 | 144 | class MyTable(Table): 145 | name = Col('Name') 146 | description = Col('Description') 147 | 148 | items = [Item(name='Name1', description='Description1'), 149 | Item(name='Name2', description='Description2'), 150 | Item(name='Name3', description='Description3')] 151 | 152 | def test_one(self): 153 | self.assert_html_equivalent_from_file( 154 | 'border_test', 'table_bordered', self.items, 155 | table_kwargs=dict(border=True)) 156 | 157 | def test_two(self): 158 | table_bordered = self.table_cls(self.items, border=True) 159 | self.assert_html_equivalent_from_file( 160 | 'border_test', 'table_bordered', table=table_bordered) 161 | 162 | 163 | class ColTest(TableTest): 164 | 165 | class MyTable(Table): 166 | name = Col('Name Heading') 167 | 168 | def test_one(self): 169 | items = [Item(name='one')] 170 | self.assert_html_equivalent_from_file( 171 | 'col_test', 'test_one', items) 172 | 173 | def test_two(self): 174 | items = [Item(name='one'), Item(name='two')] 175 | self.assert_html_equivalent_from_file( 176 | 'col_test', 'test_two', items) 177 | 178 | def test_ten(self): 179 | items = [Item(name=str(i)) for i in range(10)] 180 | self.assert_html_equivalent_from_file( 181 | 'col_test', 'test_ten', items) 182 | 183 | def test_encoding(self): 184 | items = [Item(name='äöüß')] 185 | self.assert_html_equivalent_from_file( 186 | 'col_test', 'test_encoding', items) 187 | 188 | 189 | class HideTest(ColTest): 190 | 191 | class MyTable(Table): 192 | name = Col('Name Heading') 193 | hidden = Col('Hidden', show=False) 194 | 195 | 196 | class DynamicColsTest(ColTest): 197 | 198 | table_cls = create_table().add_column('name', Col('Name Heading')) 199 | 200 | 201 | class DynamicColsNumColsTest(TableTest): 202 | 203 | table_cls = create_table() 204 | for i in range(3): 205 | table_cls.add_column(str(i), Col(str(i))) 206 | 207 | def test_one(self): 208 | items = [{str(i): i for i in range(3)}] 209 | self.assert_html_equivalent_from_file( 210 | 'dynamic_cols_num_cols_test', 'test_one', items) 211 | 212 | def test_ten(self): 213 | items = [{str(i): i for i in range(3)}] * 10 214 | self.assert_html_equivalent_from_file( 215 | 'dynamic_cols_num_cols_test', 'test_ten', items) 216 | 217 | 218 | class DynamicColsInheritTest(TableTest): 219 | 220 | # Start with MyTable 221 | class MyTable(Table): 222 | name = Col('Name') 223 | 224 | # Then dynamically extend it. 225 | table_cls = create_table(base=MyTable) 226 | table_cls.add_column('number', Col('Number')) 227 | 228 | def test_one(self): 229 | items = [{'name': 'TestName', 'number': 10}] 230 | self.assert_html_equivalent_from_file( 231 | 'dynamic_cols_inherit_test', 'test_one', items) 232 | 233 | 234 | class DynamicColsOptionsTest(TableTest): 235 | 236 | tbl_options = dict( 237 | classes=['cls1', 'cls2'], 238 | thead_classes=['cls_head1', 'cls_head2'], 239 | no_items='Empty') 240 | table_cls = create_table(options=tbl_options) 241 | table_cls.add_column('name', Col('Name Heading')) 242 | 243 | def test_one(self): 244 | items = [Item(name='one')] 245 | self.assert_html_equivalent_from_file( 246 | 'dynamic_cols_options_test', 'test_one', items) 247 | 248 | def test_none(self): 249 | items = [] 250 | self.assert_html_equivalent_from_file( 251 | 'dynamic_cols_options_test', 'test_none', items) 252 | 253 | 254 | class OverrideTrTest(TableTest): 255 | 256 | class MyTable(Table): 257 | number = Col('Number') 258 | 259 | def get_tr_attrs(self, item): 260 | if item['number'] % 3 == 1: 261 | return {'class': 'threes-plus-one'} 262 | elif item['number'] % 3 == 2: 263 | return {'class': 'threes-plus-two'} 264 | return {} 265 | 266 | def test_ten(self): 267 | items = [{'number': i} for i in range(10)] 268 | self.assert_html_equivalent_from_file( 269 | 'override_tr_test', 'test_ten', items) 270 | 271 | 272 | class EmptyTest(TableTest): 273 | 274 | class MyTable(Table): 275 | name = Col('Name Heading') 276 | 277 | def test_none(self): 278 | items = [] 279 | self.assert_html_equivalent_from_file( 280 | 'empty_test', 'test_none', items) 281 | 282 | 283 | class ColDictTest(ColTest): 284 | def test_one(self): 285 | items = [dict(name='one')] 286 | self.assert_html_equivalent_from_file( 287 | 'col_test', 'test_one', items) 288 | 289 | def test_two(self): 290 | items = [dict(name='one'), dict(name='two')] 291 | self.assert_html_equivalent_from_file( 292 | 'col_test', 'test_two', items) 293 | 294 | def test_ten(self): 295 | items = [dict(name=str(i)) for i in range(10)] 296 | self.assert_html_equivalent_from_file( 297 | 'col_test', 'test_ten', items) 298 | 299 | def test_encoding(self): 300 | items = [dict(name='äöüß')] 301 | self.assert_html_equivalent_from_file( 302 | 'col_test', 'test_encoding', items) 303 | 304 | 305 | class FuncItem(Item): 306 | def get_name(self): 307 | return self.name 308 | 309 | 310 | class ColCallableTest(ColTest): 311 | 312 | class MyTable(Table): 313 | get_name = Col('Name Heading') 314 | 315 | def test_one(self): 316 | items = [FuncItem(name='one')] 317 | self.assert_html_equivalent_from_file( 318 | 'col_test', 'test_one', items) 319 | 320 | def test_two(self): 321 | items = [FuncItem(name='one'), FuncItem(name='two')] 322 | self.assert_html_equivalent_from_file( 323 | 'col_test', 'test_two', items) 324 | 325 | def test_ten(self): 326 | items = [FuncItem(name=str(i)) for i in range(10)] 327 | self.assert_html_equivalent_from_file( 328 | 'col_test', 'test_ten', items) 329 | 330 | def test_encoding(self): 331 | items = [FuncItem(name='äöüß')] 332 | self.assert_html_equivalent_from_file( 333 | 'col_test', 'test_encoding', items) 334 | 335 | 336 | class AttrListTest(TableTest): 337 | 338 | class MyTable(Table): 339 | name = Col('Subitem Name Heading', attr_list=['subitem', 'name']) 340 | 341 | def test_one(self): 342 | items = [Item(subitem=Subitem(name='one'))] 343 | self.assert_html_equivalent_from_file( 344 | 'attr_list_test', 'test_one', items) 345 | 346 | def test_two_one_empty(self): 347 | items = [Item(subitem=Subitem(name='one')), Item(subitem=None)] 348 | self.assert_html_equivalent_from_file( 349 | 'attr_list_test', 'test_two_one_empty', items) 350 | 351 | 352 | class WeirdAttrListTest(TableTest): 353 | 354 | class MyTable(Table): 355 | name = Col( 356 | 'Subitem Name Heading', attr_list=['subi.tem.', '.nam.e']) 357 | 358 | def test_one(self): 359 | items = [{'subi.tem.': {'.nam.e': 'one'}}] 360 | self.assert_html_equivalent_from_file( 361 | 'attr_list_test', 'test_one', items) 362 | 363 | 364 | class AltAttrTest(ColTest): 365 | 366 | class MyTable(Table): 367 | alt_name = Col('Name Heading', attr='name') 368 | 369 | 370 | class AttrListDotsTest(AttrListTest): 371 | 372 | class MyTable(Table): 373 | name = Col('Subitem Name Heading', attr='subitem.name') 374 | 375 | 376 | class ClassTest(TableTest): 377 | 378 | class MyTable(Table): 379 | classes = ['table'] 380 | name = Col('Name Heading') 381 | 382 | def test_one(self): 383 | items = [Item(name='one')] 384 | self.assert_html_equivalent_from_file( 385 | 'class_test', 'test_one', items) 386 | 387 | 388 | class TheadClassTest(TableTest): 389 | 390 | class MyTable(Table): 391 | thead_classes = ['table-head'] 392 | name = Col('Name Heading') 393 | 394 | def test_one(self): 395 | items = [Item(name='one')] 396 | self.assert_html_equivalent_from_file( 397 | 'thead_class_test', 'test_one', items) 398 | 399 | 400 | class SortUrlNotSetTest(TableTest): 401 | 402 | class MyTable1(Table): 403 | allow_sort = True 404 | name = Col('Name Heading') 405 | 406 | class MyTable2(Table): 407 | allow_sort = True 408 | name = Col('Name Heading') 409 | 410 | def sort_url(self, col_id, reverse=False): 411 | return '?sort={}&reverse={}'.format(col_id, reverse) 412 | 413 | def test_fail(self): 414 | items = [{'name': 'TestName'}] 415 | 416 | def _create_table1(): 417 | html = self.MyTable1(items, sort_by='name').__html__() 418 | 419 | def _create_table2(): 420 | html = self.MyTable2(items, sort_by='name').__html__() 421 | 422 | # table1 should raise a NotImplementedError 423 | self.assertRaises(NotImplementedError, _create_table1) 424 | # table2 should work fine 425 | try: 426 | _create_table2() 427 | except Exception: 428 | self.fail('Table creation failed unexpectedly') 429 | 430 | 431 | class NoItemsTest(TableTest): 432 | 433 | class MyTable(Table): 434 | no_items = 'There is nothing here' 435 | name = Col('Name Heading') 436 | 437 | def test_zero(self): 438 | items = [] 439 | self.assert_html_equivalent_from_file( 440 | 'no_items_test', 'test_zero', items) 441 | 442 | 443 | class NoItemsDynamicTest(TableTest): 444 | 445 | class MyTable(Table): 446 | name = Col('Name Heading') 447 | 448 | def test_zero(self): 449 | items = [] 450 | tab = self.table_cls(items, no_items='There is nothing here') 451 | self.assert_html_equivalent_from_file( 452 | 'no_items_test', 'test_zero', table=tab) 453 | 454 | 455 | class NoItemsAllowEmptyTest(TableTest): 456 | 457 | class MyTable(Table): 458 | name = Col('Name Heading') 459 | allow_empty = True 460 | 461 | def test_zero(self): 462 | items = [] 463 | self.assert_html_equivalent_from_file( 464 | 'no_items_allow_empty', 'test_zero', items) 465 | 466 | 467 | class ClassTestAtPopulate(TableTest): 468 | 469 | class MyTable(Table): 470 | name = Col('Name Heading') 471 | 472 | def test_one(self): 473 | items = [Item(name='one')] 474 | tab = self.table_cls(items, classes=['table']) 475 | self.assert_html_equivalent_from_file( 476 | 'class_test', 'test_one', table=tab) 477 | 478 | 479 | class TheadClassTestAtPopulate(TableTest): 480 | 481 | class MyTable(Table): 482 | name = Col('Name Heading') 483 | 484 | def test_one(self): 485 | items = [Item(name='one')] 486 | tab = self.table_cls(items, thead_classes=['table-head']) 487 | self.assert_html_equivalent_from_file( 488 | 'thead_class_test', 'test_one', table=tab) 489 | 490 | 491 | class LinkTest(FlaskTableTest): 492 | 493 | class MyTable(Table): 494 | name = Col('Name') 495 | view = LinkCol('View', 'view', url_kwargs=dict(id_='id')) 496 | 497 | def test_one(self): 498 | items = [Item(name='one', id=1)] 499 | self.assert_html_equivalent_from_file( 500 | 'link_test', 'test_one', items) 501 | 502 | 503 | class LinkAttrsTest(FlaskTableTest): 504 | class MyTable(Table): 505 | name = Col('Name') 506 | view = LinkCol( 507 | 'View', 'view', url_kwargs=dict(id_='id'), 508 | anchor_attrs={'class': 'btn btn-primary', 'role': 'button'}) 509 | 510 | def test_one_attrs(self): 511 | items = [Item(name='one', id=1)] 512 | self.assert_html_equivalent_from_file( 513 | 'link_test', 'test_one_attrs', items) 514 | 515 | 516 | class LinkExtraKwargsTest(FlaskTableTest): 517 | 518 | class MyTable(Table): 519 | name = Col('Name') 520 | view = LinkCol( 521 | 'View', 522 | 'view', 523 | url_kwargs=dict(id_='id'), 524 | url_kwargs_extra=dict(extra='extra')) 525 | 526 | def test_one(self): 527 | items = [Item(name='one', id=1)] 528 | self.assert_html_equivalent_from_file( 529 | 'link_test', 'test_one_extra_kwargs', items) 530 | 531 | 532 | class LinkExtraKwargsRepeatTest(FlaskTableTest): 533 | """Check that if both `url_kwargs` and `url_kwargs_extra` are given 534 | and share a key that we default to the value from the item, rather 535 | than the static value from `url_kwargs_extra`. 536 | 537 | """ 538 | 539 | class MyTable(Table): 540 | name = Col('Name') 541 | view = LinkCol( 542 | 'View', 543 | 'view', 544 | url_kwargs=dict(id_='id'), 545 | url_kwargs_extra=dict(id_='id-from-extra', extra='extra')) 546 | 547 | def test_one(self): 548 | items = [Item(name='one', id=1)] 549 | self.assert_html_equivalent_from_file( 550 | 'link_test', 'test_one_extra_kwargs', items) 551 | 552 | 553 | class LinkDictTest(LinkTest): 554 | def test_one(self): 555 | items = [dict(name='one', id=1)] 556 | self.assert_html_equivalent_from_file( 557 | 'link_test', 'test_one', items) 558 | 559 | 560 | class LinkNoUrlKwargsTest(FlaskTableTest): 561 | 562 | class MyTable(Table): 563 | name = Col('Name') 564 | view = LinkCol('View', 'index') 565 | 566 | def test_one(self): 567 | items = [Item(name='one')] 568 | self.assert_html_equivalent_from_file( 569 | 'link_test', 'test_one_no_url_kwargs', items) 570 | 571 | 572 | class LinkTestSubItemAttrList(LinkTest): 573 | 574 | class MyTable(Table): 575 | name = Col('Name') 576 | view = LinkCol( 577 | 'View', 'view', url_kwargs=dict(id_=['subitem', 'id'])) 578 | 579 | def test_one(self): 580 | items = [Item(name='one', subitem=Subitem(id=1))] 581 | self.assert_html_equivalent_from_file( 582 | 'link_test', 'test_one', items) 583 | 584 | 585 | class LinkTestSubItemAttrDots(LinkTestSubItemAttrList): 586 | 587 | class MyTable(Table): 588 | name = Col('Name') 589 | view = LinkCol('View', 'view', url_kwargs=dict(id_='subitem.id')) 590 | 591 | 592 | class LinkTestCustomContent(FlaskTableTest): 593 | 594 | class MyTable(Table): 595 | name = LinkCol( 596 | 'View', 'view', attr='name', url_kwargs=dict(id_='id')) 597 | 598 | def test_one(self): 599 | items = [Item(name='one', id=1)] 600 | self.assert_html_equivalent_from_file( 601 | 'link_test', 'test_one_custom_content', items) 602 | 603 | 604 | class LinkTestOverrideContent(FlaskTableTest): 605 | 606 | class MyTable(Table): 607 | name = LinkCol( 608 | 'View', 'view', text_fallback='MyText', url_kwargs=dict(id_='id')) 609 | 610 | def test_one(self): 611 | items = [Item(name='one', id=1)] 612 | self.assert_html_equivalent_from_file( 613 | 'link_test', 'test_one_override_content', items) 614 | 615 | 616 | class ButtonTest(FlaskTableTest): 617 | 618 | class MyTable(Table): 619 | name = Col('Name') 620 | view = ButtonCol('Delete', 'delete', url_kwargs=dict(id_='id')) 621 | 622 | def test_one(self): 623 | items = [Item(name='one', id=1)] 624 | self.assert_html_equivalent_from_file( 625 | 'button_test', 'test_one', items) 626 | 627 | 628 | class ButtonAttrsTest(FlaskTableTest): 629 | 630 | class MyTable(Table): 631 | name = Col('Name') 632 | view = ButtonCol( 633 | 'Delete', 634 | 'delete', 635 | url_kwargs=dict(id_='id'), 636 | button_attrs={'class': 'myclass'}) 637 | 638 | def test_one(self): 639 | items = [Item(name='one', id=1)] 640 | self.assert_html_equivalent_from_file( 641 | 'button_attrs_test', 'test_one', items) 642 | 643 | 644 | class ButtonFormAttrsTest(FlaskTableTest): 645 | 646 | class MyTable(Table): 647 | name = Col('Name') 648 | view = ButtonCol( 649 | 'Delete', 650 | 'delete', 651 | url_kwargs=dict(id_='id'), 652 | form_attrs={'class': 'myclass'}) 653 | 654 | def test_one(self): 655 | items = [Item(name='one', id=1)] 656 | self.assert_html_equivalent_from_file( 657 | 'button_form_attrs_test', 'test_one', items) 658 | 659 | 660 | class ButtonHiddenFieldsTest(FlaskTableTest): 661 | 662 | class MyTable(Table): 663 | name = Col('Name') 664 | view = ButtonCol( 665 | 'Delete', 666 | 'delete', 667 | url_kwargs=dict(id_='id'), 668 | form_hidden_fields=dict( 669 | name1='value1', 670 | name2='value2', 671 | ) 672 | ) 673 | 674 | maxDiff = None 675 | 676 | def test_one(self): 677 | items = [Item(name='one', id=1)] 678 | self.assert_html_equivalent_from_file( 679 | 'button_hidden_fields_test', 'test_one', items) 680 | 681 | 682 | class BoolTest(TableTest): 683 | 684 | class MyTable(Table): 685 | yesno = BoolCol('YesNo Heading') 686 | 687 | def test_one(self): 688 | items = [Item(yesno=True), 689 | Item(yesno='Truthy'), 690 | Item(yesno='')] 691 | self.assert_html_equivalent_from_file( 692 | 'bool_test', 'test_one', items) 693 | 694 | 695 | class BoolCustomDisplayTest(TableTest): 696 | 697 | class MyTable(Table): 698 | yesno = BoolCol( 699 | 'YesNo Heading', 700 | yes_display='Affirmative', 701 | no_display='Negatory') 702 | 703 | def test_one(self): 704 | items = [Item(yesno=True), 705 | Item(yesno='Truthy'), 706 | Item(yesno='')] 707 | self.assert_html_equivalent_from_file( 708 | 'bool_test', 'test_one_custom_display', items) 709 | 710 | 711 | class BoolNaTest(TableTest): 712 | 713 | class MyTable(Table): 714 | yesnona = BoolNaCol('YesNoNa Heading') 715 | 716 | def test_one(self): 717 | items = [Item(yesnona=True), 718 | Item(yesnona='Truthy'), 719 | Item(yesnona=''), 720 | Item(yesnona=None)] 721 | self.assert_html_equivalent_from_file( 722 | 'bool_test', 'test_one_na', items) 723 | 724 | 725 | class OptTest(TableTest): 726 | 727 | class MyTable(Table): 728 | choice = OptCol( 729 | 'Choice Heading', 730 | choices={'a': 'A', 'b': 'Bbb', 'c': 'Ccccc'}) 731 | 732 | def test_one(self): 733 | items = [Item(choice='a'), 734 | Item(choice='b'), 735 | Item(choice='c'), 736 | Item(choice='d')] 737 | self.assert_html_equivalent_from_file( 738 | 'opt_test', 'test_one', items) 739 | 740 | 741 | class OptNoChoicesTest(TableTest): 742 | 743 | class MyTable(Table): 744 | choice = OptCol('Choice Heading') 745 | 746 | def test_one(self): 747 | items = [Item(choice='a')] 748 | self.assert_html_equivalent_from_file( 749 | 'opt_test', 'test_one_no_choices', items) 750 | 751 | 752 | class OptTestDefaultKey(TableTest): 753 | 754 | class MyTable(Table): 755 | choice = OptCol( 756 | 'Choice Heading', 757 | choices={'a': 'A', 'b': 'Bbb', 'c': 'Ccccc'}, 758 | default_key='c') 759 | 760 | def test_one(self): 761 | items = [Item(choice='a'), 762 | Item(choice='b'), 763 | Item(choice='c'), 764 | Item(choice='d')] 765 | self.assert_html_equivalent_from_file( 766 | 'opt_test', 'test_one_default_key', items) 767 | 768 | 769 | class OptTestDefaultValue(TableTest): 770 | 771 | class MyTable(Table): 772 | choice = OptCol( 773 | 'Choice Heading', 774 | choices={'a': 'A', 'b': 'Bbb', 'c': 'Ccccc'}, 775 | default_value='Ddddddd') 776 | 777 | def test_one(self): 778 | items = [Item(choice='a'), 779 | Item(choice='b'), 780 | Item(choice='c'), 781 | Item(choice='d')] 782 | self.assert_html_equivalent_from_file( 783 | 'opt_test', 'test_one_default_value', items) 784 | 785 | 786 | class DateTest(TableTest): 787 | 788 | class MyTable(Table): 789 | date = DateCol('Date Heading') 790 | 791 | def test_one(self): 792 | items = [Item(date=date(2014, 1, 1)), Item(date=None)] 793 | self.assert_html_equivalent_from_file( 794 | 'date_test', 'test_one', items) 795 | 796 | 797 | class DateTestFormat(TableTest): 798 | 799 | class MyTable(Table): 800 | date = DateCol('Date Heading', date_format="YYYY-MM-dd") 801 | 802 | def test_one(self): 803 | items = [Item(date=date(2014, 2, 1)), Item(date=None)] 804 | self.assert_html_equivalent_from_file( 805 | 'date_test_format', 'test_one', items) 806 | 807 | 808 | class DatetimeTest(TableTest): 809 | 810 | class MyTable(Table): 811 | datetime = DatetimeCol('DateTime Heading') 812 | 813 | def test_one(self): 814 | items = [ 815 | Item(datetime=datetime(2014, 1, 1, 10, 20, 30)), 816 | Item(datetime=None)] 817 | self.assert_html_equivalent_from_file( 818 | 'datetime_test', 'test_one', items) 819 | 820 | 821 | class DatetimeTestFormat(TableTest): 822 | 823 | class MyTable(Table): 824 | datetime = DatetimeCol( 825 | 'DateTime Heading', 826 | datetime_format="YYYY-MM-dd hh:mm") 827 | 828 | def test_one(self): 829 | items = [ 830 | Item(datetime=datetime(2014, 1, 1, 10, 20, 30)), 831 | Item(datetime=None)] 832 | self.assert_html_equivalent_from_file( 833 | 'datetime_test_format', 'test_one', items) 834 | 835 | 836 | class EscapeTest(TableTest): 837 | 838 | class MyTable(Table): 839 | name = Col('Name') 840 | 841 | def test_one(self): 842 | items = [Item(name='<&"\'')] 843 | self.assert_html_equivalent_from_file( 844 | 'escape_test', 'test_one', items) 845 | 846 | 847 | class SortingTest(FlaskTableTest): 848 | 849 | class MyTable(Table): 850 | allow_sort = True 851 | name = Col('Name') 852 | 853 | def sort_url(self, col_key, reverse=False): 854 | kwargs = {'sort': col_key} 855 | if reverse: 856 | kwargs['direction'] = 'desc' 857 | return url_for('index', **kwargs) 858 | 859 | def test_start(self): 860 | items = [Item(name='name')] 861 | self.assert_html_equivalent_from_file( 862 | 'sorting_test', 'test_start', items) 863 | 864 | def test_sorted(self): 865 | items = [Item(name='name')] 866 | tab = self.table_cls(items, sort_by='name') 867 | self.assert_html_equivalent_from_file( 868 | 'sorting_test', 'test_sorted', items, table=tab) 869 | 870 | def test_sorted_reverse(self): 871 | items = [Item(name='name')] 872 | tab = self.table_cls(items, sort_by='name', sort_reverse=True) 873 | self.assert_html_equivalent_from_file( 874 | 'sorting_test', 'test_sorted_reverse', items, table=tab) 875 | 876 | 877 | class GeneratorTest(TableTest): 878 | 879 | class MyTable(Table): 880 | number = Col('Number') 881 | 882 | def gen_nums(self, upto): 883 | i = 1 884 | while True: 885 | if i > upto: 886 | return 887 | yield {'number': i} 888 | i += 1 889 | 890 | def test_one(self): 891 | items = self.gen_nums(1) 892 | self.assert_html_equivalent_from_file( 893 | 'generator_test', 'test_one', items) 894 | 895 | def test_empty(self): 896 | items = self.gen_nums(0) 897 | self.assert_html_equivalent_from_file( 898 | 'generator_test', 'test_empty', items) 899 | 900 | def test_ten(self): 901 | items = self.gen_nums(10) 902 | self.assert_html_equivalent_from_file( 903 | 'generator_test', 'test_ten', items) 904 | 905 | 906 | class NestedColTest(TableTest): 907 | def setUp(self): 908 | class MySubTable(Table): 909 | b = Col('b') 910 | c = Col('c') 911 | 912 | class MyMainTable(Table): 913 | a = Col('a') 914 | nest = NestedTableCol('Nested column', MySubTable) 915 | 916 | self.table_cls = MyMainTable 917 | 918 | def test_one(self): 919 | items = [Item(a='row1', nest=[Item(b='r1asc1', c='r1asc2'), 920 | Item(b='r1bsc1', c='r1bsc2')]), 921 | Item(a='row2', nest=[Item(b='r2asc1', c='r2asc2'), 922 | Item(b='r2bsc1', c='r2bsc2')])] 923 | self.assert_html_equivalent_from_file( 924 | 'nestedcol_test', 'test_one', items) 925 | 926 | 927 | class ColumnAttrsTest(TableTest): 928 | 929 | class MyTable(Table): 930 | name = Col('Name Heading', column_html_attrs={'class': 'myclass'}) 931 | 932 | def test_column_html_attrs(self): 933 | items = [Item(name='one')] 934 | self.assert_html_equivalent_from_file( 935 | 'column_html_attrs_test', 'test_column_html_attrs', items) 936 | 937 | 938 | class TDAttrsTest(TableTest): 939 | 940 | class MyTable(Table): 941 | name = Col('Name Heading', td_html_attrs={'class': 'myclass'}) 942 | 943 | def test_td_html_attrs(self): 944 | items = [Item(name='one')] 945 | self.assert_html_equivalent_from_file( 946 | 'column_html_attrs_test', 'test_td_html_attrs', items) 947 | 948 | 949 | class THAttrsTest(TableTest): 950 | 951 | class MyTable(Table): 952 | name = Col('Name Heading', th_html_attrs={'class': 'myclass'}) 953 | 954 | def test_th_html_attrs(self): 955 | items = [Item(name='one')] 956 | self.assert_html_equivalent_from_file( 957 | 'column_html_attrs_test', 'test_th_html_attrs', items) 958 | 959 | 960 | class BothAttrsTest(TableTest): 961 | 962 | class MyTable(Table): 963 | name = Col( 964 | 'Name Heading', 965 | th_html_attrs={'class': 'my-th-class'}, 966 | td_html_attrs={'class': 'my-td-class'}) 967 | 968 | def test_both_html_attrs(self): 969 | items = [Item(name='one')] 970 | self.assert_html_equivalent_from_file( 971 | 'column_html_attrs_test', 'test_both_html_attrs', items) 972 | 973 | 974 | class AttrsOverwriteTest(TableTest): 975 | 976 | class MyTable(Table): 977 | name = Col( 978 | 'Name Heading', 979 | column_html_attrs={'data-other': 'mydata', 'class': 'myclass'}, 980 | th_html_attrs={'class': 'my-th-class'}, 981 | td_html_attrs={'class': 'my-td-class'}) 982 | 983 | def test_overwrite_attrs(self): 984 | items = [Item(name='one')] 985 | self.assert_html_equivalent_from_file( 986 | 'column_html_attrs_test', 'test_overwrite_attrs', items) 987 | 988 | 989 | class TableHTMLAttrsTest(TableTest): 990 | 991 | class MyTable(Table): 992 | name = Col('Name Heading') 993 | 994 | def test_html_attrs(self): 995 | items = [Item(name='one')] 996 | html_attrs = { 997 | 'data-myattr': 'myval', 998 | } 999 | self.assert_html_equivalent_from_file( 1000 | 'html_attrs_test', 1001 | 'test_html_attrs', 1002 | items, 1003 | table_kwargs=dict(html_attrs=html_attrs)) 1004 | 1005 | 1006 | class TableHTMLAttrsOnClassTest(TableTest): 1007 | 1008 | class MyTable(Table): 1009 | html_attrs = { 1010 | 'data-myattr': 'myval', 1011 | } 1012 | name = Col('Name Heading') 1013 | 1014 | def test_html_attrs(self): 1015 | items = [Item(name='one')] 1016 | self.assert_html_equivalent_from_file( 1017 | 'html_attrs_test', 1018 | 'test_html_attrs', 1019 | items) 1020 | -------------------------------------------------------------------------------- /tests/html/attr_list_test/test_one.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Subitem Name Heading
one
13 | -------------------------------------------------------------------------------- /tests/html/attr_list_test/test_two_one_empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Subitem Name Heading
one
16 | -------------------------------------------------------------------------------- /tests/html/bool_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
YesNo Heading
Yes
Yes
No
11 | -------------------------------------------------------------------------------- /tests/html/bool_test/test_one_custom_display.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
YesNo Heading
Affirmative
Affirmative
Negatory
11 | -------------------------------------------------------------------------------- /tests/html/bool_test/test_one_na.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
YesNoNa Heading
Yes
Yes
No
N/A
12 | -------------------------------------------------------------------------------- /tests/html/border_test/table_bordered.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
NameDescription
Name1Description1
Name2Description2
Name3Description3
-------------------------------------------------------------------------------- /tests/html/button_attrs_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
NameDelete
one 12 |
13 | 14 |
15 |
19 | -------------------------------------------------------------------------------- /tests/html/button_form_attrs_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
NameDelete
one 12 |
13 | 14 |
15 |
19 | -------------------------------------------------------------------------------- /tests/html/button_hidden_fields_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 20 |
NameDelete
one 12 |
13 | 14 | 15 | 16 |
17 |
21 | -------------------------------------------------------------------------------- /tests/html/button_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
NameDelete
one 12 |
13 | 14 |
15 |
19 | -------------------------------------------------------------------------------- /tests/html/class_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
one
11 | -------------------------------------------------------------------------------- /tests/html/col_test/test_encoding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
äöüß
11 | 12 | -------------------------------------------------------------------------------- /tests/html/col_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
one
11 | -------------------------------------------------------------------------------- /tests/html/col_test/test_ten.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Name Heading
0
1
2
3
4
5
6
7
8
9
18 | -------------------------------------------------------------------------------- /tests/html/col_test/test_two.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
Name Heading
one
two
14 | -------------------------------------------------------------------------------- /tests/html/column_html_attrs_test/test_both_html_attrs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
one
11 | -------------------------------------------------------------------------------- /tests/html/column_html_attrs_test/test_column_html_attrs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
one
11 | -------------------------------------------------------------------------------- /tests/html/column_html_attrs_test/test_overwrite_attrs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
one
11 | -------------------------------------------------------------------------------- /tests/html/column_html_attrs_test/test_td_html_attrs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
one
11 | -------------------------------------------------------------------------------- /tests/html/column_html_attrs_test/test_th_html_attrs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
one
11 | -------------------------------------------------------------------------------- /tests/html/date_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Date Heading
01/01/2014
10 | -------------------------------------------------------------------------------- /tests/html/date_test_format/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Date Heading
2014-02-01
10 | 11 | -------------------------------------------------------------------------------- /tests/html/datetime_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
DateTime Heading
01/01/2014, 10:20
10 | -------------------------------------------------------------------------------- /tests/html/datetime_test_format/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
DateTime Heading
2014-01-01 10:20
10 | -------------------------------------------------------------------------------- /tests/html/dynamic_cols_inherit_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
NameNumber
TestName10
15 | -------------------------------------------------------------------------------- /tests/html/dynamic_cols_num_cols_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
012
012
17 | -------------------------------------------------------------------------------- /tests/html/dynamic_cols_num_cols_test/test_ten.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
012
012
012
012
012
012
012
012
012
012
012
62 | -------------------------------------------------------------------------------- /tests/html/dynamic_cols_options_test/test_none.html: -------------------------------------------------------------------------------- 1 |

Empty

2 | -------------------------------------------------------------------------------- /tests/html/dynamic_cols_options_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
one
11 | -------------------------------------------------------------------------------- /tests/html/empty_test/test_none.html: -------------------------------------------------------------------------------- 1 |

No Items

2 | -------------------------------------------------------------------------------- /tests/html/escape_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Name
<&"'
9 | -------------------------------------------------------------------------------- /tests/html/generator_test/test_empty.html: -------------------------------------------------------------------------------- 1 |

No Items

2 | -------------------------------------------------------------------------------- /tests/html/generator_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Number
1
9 | -------------------------------------------------------------------------------- /tests/html/generator_test/test_ten.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Number
1
2
3
4
5
6
7
8
9
10
18 | -------------------------------------------------------------------------------- /tests/html/html_attrs_test/test_html_attrs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
one
11 | -------------------------------------------------------------------------------- /tests/html/link_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
NameView
oneView
15 | -------------------------------------------------------------------------------- /tests/html/link_test/test_one_attrs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
NameView
oneView
15 | -------------------------------------------------------------------------------- /tests/html/link_test/test_one_custom_content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
View
one
11 | -------------------------------------------------------------------------------- /tests/html/link_test/test_one_extra_kwargs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
NameView
oneView
15 | -------------------------------------------------------------------------------- /tests/html/link_test/test_one_no_url_kwargs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
NameView
oneView
15 | -------------------------------------------------------------------------------- /tests/html/link_test/test_one_override_content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
View
MyText
11 | -------------------------------------------------------------------------------- /tests/html/nestedcol_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 18 | 19 |
aNested column
row1 5 | 6 | 7 | 8 | 9 | 10 |
bc
r1asc1r1asc2
r1bsc1r1bsc2
row2 12 | 13 | 14 | 15 | 16 | 17 |
bc
r2asc1r2asc2
r2bsc1r2bsc2
20 | -------------------------------------------------------------------------------- /tests/html/no_items_allow_empty/test_zero.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Name Heading
4 | -------------------------------------------------------------------------------- /tests/html/no_items_test/test_zero.html: -------------------------------------------------------------------------------- 1 |

There is nothing here

2 | -------------------------------------------------------------------------------- /tests/html/opt_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
Choice Heading
A
Bbb
Ccccc
12 | -------------------------------------------------------------------------------- /tests/html/opt_test/test_one_default_key.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
Choice Heading
A
Bbb
Ccccc
Ccccc
12 | -------------------------------------------------------------------------------- /tests/html/opt_test/test_one_default_value.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
Choice Heading
A
Bbb
Ccccc
Ddddddd
12 | -------------------------------------------------------------------------------- /tests/html/opt_test/test_one_no_choices.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Choice Heading
9 | -------------------------------------------------------------------------------- /tests/html/override_tr_test/test_ten.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Number
0
1
2
3
4
5
6
7
8
9
18 | -------------------------------------------------------------------------------- /tests/html/sorting_test/test_sorted.html: -------------------------------------------------------------------------------- 1 | 2 |
↓Name
name
3 | -------------------------------------------------------------------------------- /tests/html/sorting_test/test_sorted_reverse.html: -------------------------------------------------------------------------------- 1 | 2 |
↑Name
name
3 | -------------------------------------------------------------------------------- /tests/html/sorting_test/test_start.html: -------------------------------------------------------------------------------- 1 | 2 |
Name
name
3 | -------------------------------------------------------------------------------- /tests/html/tableid_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
Name Heading
one
7 | -------------------------------------------------------------------------------- /tests/html/thead_class_test/test_one.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Name Heading
one
11 | --------------------------------------------------------------------------------