├── .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 | [](https://travis-ci.org/plumdog/flask_table)
8 | [](https://coveralls.io/r/plumdog/flask_table?branch=master)
9 | [](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 | Name | Description |
58 |
59 | Name1 | Description1 |
60 | Name2 | Description2 |
61 | Name3 | Description3 |
62 |
63 |
64 | ```
65 |
66 | Or as HTML:
67 |
68 |
69 | Name | Description |
70 |
71 | Name1 | Description1 |
72 | Name2 | Description2 |
73 | Name3 | Description3 |
74 |
75 |
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 | Name |
44 | Description |
45 |
46 |
47 |
48 |
49 | Name1 |
50 | Description1 |
51 |
52 |
53 |
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 | Name |
69 | Description |
70 |
71 |
72 |
73 |
74 | Name1 |
75 | Description1 |
76 |
77 |
78 |
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 | Name |
60 |
63 | Description
64 | |
65 |
66 |
67 |
68 |
69 | Name1 |
70 |
72 | Description1
73 | |
74 |
75 |
76 | Name2 |
77 |
79 | Description2
80 | |
81 |
82 |
83 | Name3 |
84 |
86 | Description3
87 | |
88 |
89 |
90 |
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 | Name |
23 | Description |
24 |
25 |
26 |
27 |
28 | Name1 |
29 | Description1 |
30 |
31 |
32 | Name2 |
33 | Description2 |
34 |
35 |
36 | Name3 |
37 | Description3 |
38 |
39 |
40 |
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 | Name |
44 | Description |
45 |
46 |
47 |
48 |
49 | Name1 |
50 | Boring |
51 |
52 |
53 | Name2 |
54 | A very important item |
55 |
56 |
57 | Name3 |
58 | Boring |
59 |
60 |
61 |
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 | Name |
38 | Description |
39 |
40 |
41 |
42 |
43 | Name1 |
44 | Description1 |
45 |
46 |
47 | Name2 |
48 | Description2 |
49 |
50 |
51 | Name3 |
52 | Description3 |
53 |
54 |
55 |
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 | Name | Description | Subtable |
54 |
55 |
56 | Name1 | Description1 |
57 |
58 | Sub-column 1 | Sub-column 2 |
59 |
60 |
61 | r1sr1c1 | r1sr1c2 |
62 | r1sr2c1 | r1sr2c2 |
63 |
64 | |
65 | Name2 | Description2 |
66 |
67 | Sub-column 1 | Sub-column 2 |
68 |
69 |
70 | r2sr1c1 | r2sr1c2 |
71 | r2sr2c1 | r2sr2c2 |
72 |
73 | |
74 |
75 |
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}{element}>'.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 | Subitem Name Heading |
5 |
6 |
7 |
8 |
9 | one |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/html/attr_list_test/test_two_one_empty.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Subitem Name Heading |
5 |
6 |
7 |
8 |
9 | one |
10 |
11 |
12 | |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/html/bool_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | YesNo Heading |
4 |
5 |
6 | Yes |
7 | Yes |
8 | No |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/bool_test/test_one_custom_display.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | YesNo Heading |
4 |
5 |
6 | Affirmative |
7 | Affirmative |
8 | Negatory |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/bool_test/test_one_na.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | YesNoNa Heading |
4 |
5 |
6 | Yes |
7 | Yes |
8 | No |
9 | N/A |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/html/border_test/table_bordered.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | Description |
6 |
7 |
8 |
9 |
10 | Name1 |
11 | Description1 |
12 |
13 |
14 | Name2 |
15 | Description2 |
16 |
17 |
18 | Name3 |
19 | Description3 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tests/html/button_attrs_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | Delete |
6 |
7 |
8 |
9 |
10 | one |
11 |
12 |
15 | |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/html/button_form_attrs_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | Delete |
6 |
7 |
8 |
9 |
10 | one |
11 |
12 |
15 | |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/html/button_hidden_fields_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | Delete |
6 |
7 |
8 |
9 |
10 | one |
11 |
12 |
17 | |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/html/button_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | Delete |
6 |
7 |
8 |
9 |
10 | one |
11 |
12 |
15 | |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/html/class_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/col_test/test_encoding.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | äöüß |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/html/col_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/col_test/test_ten.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 | 0 |
7 | 1 |
8 | 2 |
9 | 3 |
10 | 4 |
11 | 5 |
12 | 6 |
13 | 7 |
14 | 8 |
15 | 9 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/html/col_test/test_two.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 | two |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/html/column_html_attrs_test/test_both_html_attrs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/column_html_attrs_test/test_column_html_attrs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/column_html_attrs_test/test_overwrite_attrs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/column_html_attrs_test/test_td_html_attrs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/column_html_attrs_test/test_th_html_attrs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/date_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Date Heading |
4 |
5 |
6 | 01/01/2014 |
7 | |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/html/date_test_format/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Date Heading |
4 |
5 |
6 | 2014-02-01 |
7 | |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/datetime_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | DateTime Heading |
4 |
5 |
6 | 01/01/2014, 10:20 |
7 | |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/html/datetime_test_format/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | DateTime Heading |
4 |
5 |
6 | 2014-01-01 10:20 |
7 | |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/html/dynamic_cols_inherit_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | Number |
6 |
7 |
8 |
9 |
10 | TestName |
11 | 10 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/html/dynamic_cols_num_cols_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 0 |
5 | 1 |
6 | 2 |
7 |
8 |
9 |
10 |
11 | 0 |
12 | 1 |
13 | 2 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/html/dynamic_cols_num_cols_test/test_ten.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 0 |
5 | 1 |
6 | 2 |
7 |
8 |
9 |
10 |
11 | 0 |
12 | 1 |
13 | 2 |
14 |
15 |
16 | 0 |
17 | 1 |
18 | 2 |
19 |
20 |
21 | 0 |
22 | 1 |
23 | 2 |
24 |
25 |
26 | 0 |
27 | 1 |
28 | 2 |
29 |
30 |
31 | 0 |
32 | 1 |
33 | 2 |
34 |
35 |
36 | 0 |
37 | 1 |
38 | 2 |
39 |
40 |
41 | 0 |
42 | 1 |
43 | 2 |
44 |
45 |
46 | 0 |
47 | 1 |
48 | 2 |
49 |
50 |
51 | 0 |
52 | 1 |
53 | 2 |
54 |
55 |
56 | 0 |
57 | 1 |
58 | 2 |
59 |
60 |
61 |
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 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/empty_test/test_none.html:
--------------------------------------------------------------------------------
1 | No Items
2 |
--------------------------------------------------------------------------------
/tests/html/escape_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name |
4 |
5 |
6 | <&"' |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/html/generator_test/test_empty.html:
--------------------------------------------------------------------------------
1 | No Items
2 |
--------------------------------------------------------------------------------
/tests/html/generator_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Number |
4 |
5 |
6 | 1 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/html/generator_test/test_ten.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Number |
4 |
5 |
6 | 1 |
7 | 2 |
8 | 3 |
9 | 4 |
10 | 5 |
11 | 6 |
12 | 7 |
13 | 8 |
14 | 9 |
15 | 10 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/html/html_attrs_test/test_html_attrs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/link_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | View |
6 |
7 |
8 |
9 |
10 | one |
11 | View |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/html/link_test/test_one_attrs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | View |
6 |
7 |
8 |
9 |
10 | one |
11 | View |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/html/link_test/test_one_custom_content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | View |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/link_test/test_one_extra_kwargs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | View |
6 |
7 |
8 |
9 |
10 | one |
11 | View |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/html/link_test/test_one_no_url_kwargs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Name |
5 | View |
6 |
7 |
8 |
9 |
10 | one |
11 | View |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/html/link_test/test_one_override_content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | View |
4 |
5 |
6 |
7 | MyText |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/html/nestedcol_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 | a | Nested column |
3 |
4 | row1 |
5 | b | c |
6 |
7 | r1asc1 | r1asc2 |
8 | r1bsc1 | r1bsc2 |
9 |
10 | |
11 | row2 |
12 | b | c |
13 |
14 | r2asc1 | r2asc2 |
15 | r2bsc1 | r2bsc2 |
16 |
17 | |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tests/html/no_items_allow_empty/test_zero.html:
--------------------------------------------------------------------------------
1 |
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 | Choice Heading |
4 |
5 |
6 | A |
7 | Bbb |
8 | Ccccc |
9 | |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/html/opt_test/test_one_default_key.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Choice Heading |
4 |
5 |
6 | A |
7 | Bbb |
8 | Ccccc |
9 | Ccccc |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/html/opt_test/test_one_default_value.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Choice Heading |
4 |
5 |
6 | A |
7 | Bbb |
8 | Ccccc |
9 | Ddddddd |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/html/opt_test/test_one_no_choices.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Choice Heading |
4 |
5 |
6 | |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/html/override_tr_test/test_ten.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Number |
4 |
5 |
6 | 0 |
7 | 1 |
8 | 2 |
9 | 3 |
10 | 4 |
11 | 5 |
12 | 6 |
13 | 7 |
14 | 8 |
15 | 9 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/html/sorting_test/test_sorted.html:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/tests/html/sorting_test/test_sorted_reverse.html:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/tests/html/sorting_test/test_start.html:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/tests/html/tableid_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 | Name Heading |
3 |
4 | one |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/html/thead_class_test/test_one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Name Heading |
4 |
5 |
6 |
7 | one |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------