From 768587dc5f4cbdb564a0a389126f5b9e7e3a66d8 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Fri, 3 Sep 2021 10:49:12 +0800 Subject: [PATCH 1/8] Deprecate URL string for render_table URL arguments --- flask_bootstrap/templates/bootstrap/table.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/flask_bootstrap/templates/bootstrap/table.html b/flask_bootstrap/templates/bootstrap/table.html index 2a3d1932..7748fdec 100644 --- a/flask_bootstrap/templates/bootstrap/table.html +++ b/flask_bootstrap/templates/bootstrap/table.html @@ -1,8 +1,11 @@ {% from 'bootstrap/utils.html' import render_icon, arg_url_for %} {% macro deprecate_old_pk_placeholder() %} - {{ warn('The default action primary key placeholder has changed to ":id", please update. - The support to the old value (":primary_key") will be removed in version 2.0.') }} + {{ warn('The default action primary key placeholder has changed to ":id", please update. The support to the old value (":primary_key") will be removed in version 2.0.') }} +{% endmacro %} + +{% macro deprecate_action_url_string() %} + {{ warn('Passing an string as action URL is deprecated. You will need to pass an URL tuple to URL arguments, see the API docs of render_table for more details. The support to URL string will be removed in version 2.0.') }} {% endmacro %} {% macro build_url(endpoint, model, pk, kwargs) %} @@ -90,6 +93,7 @@ {% endif %} Date: Fri, 3 Sep 2021 11:10:52 +0800 Subject: [PATCH 2/8] Fix URL string support for view_url, edit_url and delete_url --- flask_bootstrap/templates/bootstrap/table.html | 18 +++++++++--------- tests/test_render_table.py | 6 ++++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/flask_bootstrap/templates/bootstrap/table.html b/flask_bootstrap/templates/bootstrap/table.html index 7748fdec..dcf26fdb 100644 --- a/flask_bootstrap/templates/bootstrap/table.html +++ b/flask_bootstrap/templates/bootstrap/table.html @@ -102,48 +102,48 @@ {% endfor %} {% endif %} {% if view_url %} - {% if ':primary_key' in view_url|join('') %} + {% if ':primary_key' in view_url | join('') %} {% set action_pk_placeholder = ':primary_key' %} {% set w = deprecate_old_pk_placeholder() %} {% endif %} {{ render_icon('eye-fill') }} {% endif %} {% if edit_url -%} - {% if ':primary_key' in edit_url|join('') %} + {% if ':primary_key' in edit_url | join('') %} {% set action_pk_placeholder = ':primary_key' %} {% set w = deprecate_old_pk_placeholder() %} {% endif %} {{ render_icon('pencil-fill') }} {%- endif %} {% if delete_url %} - {% if ':primary_key' in delete_url|join('') %} + {% if ':primary_key' in delete_url | join('') %} {% set action_pk_placeholder = ':primary_key' %} {% set w = deprecate_old_pk_placeholder() %} {% endif %}
diff --git a/tests/test_render_table.py b/tests/test_render_table.py index 2606f5c2..24659a11 100644 --- a/tests/test_render_table.py +++ b/tests/test_render_table.py @@ -163,6 +163,10 @@ def test_view_message(sender, message_id): def test_create_message(): return 'New message' + @app.route('/messages//edit') + def edit_message(id): + return 'Editing message {}'.format(id) + @app.route('/table') def test(): db.drop_all() @@ -190,6 +194,7 @@ def test(): ) ], view_url=('test_view_message', [('sender', ':sender'), ('message_id', ':id')]), + edit_url=url_for('edit_message', id=':id'), new_url=url_for('test_create_message')) }} ''', titles=titles, model=Message, messages=messages) @@ -199,6 +204,7 @@ def test(): assert 'href="/table/john_doe/1/resend"' in data assert 'title="Resend">' in data assert 'href="/table/me/1/view"' in data + assert 'href="/messages/1/edit"' in data assert 'href="/table/new-message"' in data def test_customize_icon_title_of_table_actions(self, app, client): From 52d2982a85d026fb364c1f626a7dfd146f8f329b Mon Sep 17 00:00:00 2001 From: Grey Li Date: Fri, 3 Sep 2021 11:32:45 +0800 Subject: [PATCH 3/8] Raise RuntimeError if model is None when setting table action URLs --- flask_bootstrap/__init__.py | 5 +++++ flask_bootstrap/templates/bootstrap/table.html | 3 +++ 2 files changed, 8 insertions(+) diff --git a/flask_bootstrap/__init__.py b/flask_bootstrap/__init__.py index c11e2c8a..c3ae29f0 100644 --- a/flask_bootstrap/__init__.py +++ b/flask_bootstrap/__init__.py @@ -24,6 +24,10 @@ def is_hidden_field_filter(field): VERSION_POPPER = '1.14.0' +def raise_helper(message): + raise RuntimeError(message) + + def get_table_titles(data, primary_key, primary_key_title): """Detect and build the table titles tuple from ORM object, currently only support SQLAlchemy. @@ -58,6 +62,7 @@ def init_app(self, app): app.jinja_env.globals['bootstrap_is_hidden_field'] = is_hidden_field_filter app.jinja_env.globals['get_table_titles'] = get_table_titles app.jinja_env.globals['warn'] = warnings.warn + app.jinja_env.globals['raise'] = raise_helper app.jinja_env.add_extension('jinja2.ext.do') # default settings app.config.setdefault('BOOTSTRAP_SERVE_LOCAL', False) diff --git a/flask_bootstrap/templates/bootstrap/table.html b/flask_bootstrap/templates/bootstrap/table.html index dcf26fdb..7fe66378 100644 --- a/flask_bootstrap/templates/bootstrap/table.html +++ b/flask_bootstrap/templates/bootstrap/table.html @@ -9,6 +9,9 @@ {% endmacro %} {% macro build_url(endpoint, model, pk, kwargs) %} + {% if model == None %} + {{ raise("The model argument can't be None when setting action URLs.") }} + {% endif %} {% with url_params = {} -%} {%- do url_params.update(request.view_args if not endpoint else {}), url_params.update(request.args if not endpoint else {}) -%} From d22f6e49be469dd862baaa05dcc7e07639c051fb Mon Sep 17 00:00:00 2001 From: Grey Li Date: Fri, 3 Sep 2021 12:10:05 +0800 Subject: [PATCH 4/8] Support passing URL tuple to render_table new_url --- flask_bootstrap/templates/bootstrap/table.html | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/flask_bootstrap/templates/bootstrap/table.html b/flask_bootstrap/templates/bootstrap/table.html index 7fe66378..32427e97 100644 --- a/flask_bootstrap/templates/bootstrap/table.html +++ b/flask_bootstrap/templates/bootstrap/table.html @@ -5,7 +5,7 @@ {% endmacro %} {% macro deprecate_action_url_string() %} - {{ warn('Passing an string as action URL is deprecated. You will need to pass an URL tuple to URL arguments, see the API docs of render_table for more details. The support to URL string will be removed in version 2.0.') }} + {{ warn('Passing an string as action URL for view_url/edit_url/delete_url/custom_actions is deprecated. You will need to pass an URL tuple to URL arguments, see the API docs of render_table for more details. The support to URL string will be removed in version 2.0.') }} {% endmacro %} {% macro build_url(endpoint, model, pk, kwargs) %} @@ -68,8 +68,14 @@ {% if show_actions %} {{ actions_title }} {% if new_url %} - - {{ render_icon('plus-circle-fill') }} + + {{ render_icon('plus-circle-fill') }} {% endif %} From b054ae1122dae7345e13f47986d29016c4f237e3 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Fri, 3 Sep 2021 12:10:33 +0800 Subject: [PATCH 5/8] Improve tests for table action URLs --- tests/test_render_table.py | 61 ++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/tests/test_render_table.py b/tests/test_render_table.py index 24659a11..db36712c 100644 --- a/tests/test_render_table.py +++ b/tests/test_render_table.py @@ -143,6 +143,8 @@ def test(): assert response.status_code == 200 def test_render_table_with_actions(self, app, client): + app.jinja_env.globals['csrf_token'] = lambda: '' + db = SQLAlchemy(app) class Message(db.Model): @@ -159,13 +161,37 @@ def test_resend_message(recipient, message_id): def test_view_message(sender, message_id): return 'Viewing {} from {}'.format(message_id, sender) + @app.route('/table///edit') + def test_edit_message(sender, message_id): + return 'Editing {} from {}'.format(message_id, sender) + + @app.route('/table///delete') + def test_delete_message(sender, message_id): + return 'Deleting {} from {}'.format(message_id, sender) + @app.route('/table/new-message') def test_create_message(): return 'New message' - @app.route('/messages//edit') - def edit_message(id): - return 'Editing message {}'.format(id) + @app.route('/new-message') + def new_message(): + return 'Create message' + + @app.route('/messages//edit') + def edit_message(message_id): + return 'Editing message {}'.format(message_id) + + @app.route('/messages//view') + def view_message(message_id): + return 'Viewing message {}'.format(message_id) + + @app.route('/messages//delete') + def delete_message(message_id): + return 'Deleting message {}'.format(message_id) + + @app.route('/messages//resend') + def resend_message(message_id): + return 'Re-sending message'.format(message_id) @app.route('/table') def test(): @@ -185,6 +211,7 @@ def test(): titles = [('id', '#'), ('text', 'Message')] return render_template_string(''' {% from 'bootstrap/table.html' import render_table %} + # URL arguments with URL tuple {{ render_table(messages, titles, model=model, show_actions=True, custom_actions=[ ( @@ -194,8 +221,25 @@ def test(): ) ], view_url=('test_view_message', [('sender', ':sender'), ('message_id', ':id')]), - edit_url=url_for('edit_message', id=':id'), - new_url=url_for('test_create_message')) }} + edit_url=('test_edit_message', [('sender', ':sender'), ('message_id', ':id')]), + delete_url=('test_delete_message', [('sender', ':sender'), ('message_id', ':id')]), + new_url=('test_create_message') + ) }} + + # URL arguments with URL string (deprecated (except new_url), will be removed in 2.0) + {{ render_table(messages, titles, show_actions=True, + custom_actions=[ + ( + 'Resend', + 'bootstrap-reboot', + url_for('resend_message', message_id=':id') + ) + ], + view_url=url_for('view_message', message_id=':id'), + delete_url=url_for('delete_message', message_id=':id'), + edit_url=url_for('edit_message', message_id=':id'), + new_url=url_for('new_message') + ) }} ''', titles=titles, model=Message, messages=messages) response = client.get('/table') @@ -204,8 +248,13 @@ def test(): assert 'href="/table/john_doe/1/resend"' in data assert 'title="Resend">' in data assert 'href="/table/me/1/view"' in data - assert 'href="/messages/1/edit"' in data + assert 'action="/table/me/1/delete"' in data + assert 'href="/table/me/1/edit"' in data assert 'href="/table/new-message"' in data + assert 'href="/messages/1/edit"' in data + assert 'href="/messages/1/view"' in data + assert 'action="/messages/1/delete"' in data + assert 'href="/new-message"' in data def test_customize_icon_title_of_table_actions(self, app, client): From b7ca463b43412a73d5aad39dcc6f537d24369398 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Fri, 3 Sep 2021 12:37:35 +0800 Subject: [PATCH 6/8] Refactor tests for table URLs --- tests/test_render_table.py | 117 ++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 41 deletions(-) diff --git a/tests/test_render_table.py b/tests/test_render_table.py index db36712c..41e67627 100644 --- a/tests/test_render_table.py +++ b/tests/test_render_table.py @@ -3,7 +3,7 @@ from flask_wtf import CSRFProtect -class TestPagination: +class TestRenderTable: def test_render_simple_table(self, app, client): db = SQLAlchemy(app) @@ -142,7 +142,7 @@ def test(): response = client.get('/table') assert response.status_code == 200 - def test_render_table_with_actions(self, app, client): + def test_render_table_with_actions(self, app, client): # noqa: C901 app.jinja_env.globals['csrf_token'] = lambda: '' db = SQLAlchemy(app) @@ -153,26 +153,6 @@ class Message(db.Model): recipient = db.Column(db.String(20)) text = db.Column(db.Text) - @app.route('/table///resend') - def test_resend_message(recipient, message_id): - return 'Re-sending {} to {}'.format(message_id, recipient) - - @app.route('/table///view') - def test_view_message(sender, message_id): - return 'Viewing {} from {}'.format(message_id, sender) - - @app.route('/table///edit') - def test_edit_message(sender, message_id): - return 'Editing {} from {}'.format(message_id, sender) - - @app.route('/table///delete') - def test_delete_message(sender, message_id): - return 'Deleting {} from {}'.format(message_id, sender) - - @app.route('/table/new-message') - def test_create_message(): - return 'New message' - @app.route('/new-message') def new_message(): return 'Create message' @@ -191,7 +171,7 @@ def delete_message(message_id): @app.route('/messages//resend') def resend_message(message_id): - return 'Re-sending message'.format(message_id) + return 'Re-sending message {}'.format(message_id) @app.route('/table') def test(): @@ -211,34 +191,93 @@ def test(): titles = [('id', '#'), ('text', 'Message')] return render_template_string(''' {% from 'bootstrap/table.html' import render_table %} - # URL arguments with URL tuple - {{ render_table(messages, titles, model=model, show_actions=True, + # URL arguments with URL string (deprecated (except new_url), will be removed in 2.0) + {{ render_table(messages, titles, show_actions=True, custom_actions=[ ( 'Resend', 'bootstrap-reboot', - ('test_resend_message', [('recipient', ':recipient'), ('message_id', ':id')]) + url_for('resend_message', message_id=':id') ) ], - view_url=('test_view_message', [('sender', ':sender'), ('message_id', ':id')]), - edit_url=('test_edit_message', [('sender', ':sender'), ('message_id', ':id')]), - delete_url=('test_delete_message', [('sender', ':sender'), ('message_id', ':id')]), - new_url=('test_create_message') + view_url=url_for('view_message', message_id=':id'), + delete_url=url_for('delete_message', message_id=':id'), + edit_url=url_for('edit_message', message_id=':id'), + new_url=url_for('new_message') ) }} + ''', titles=titles, model=Message, messages=messages) - # URL arguments with URL string (deprecated (except new_url), will be removed in 2.0) - {{ render_table(messages, titles, show_actions=True, + response = client.get('/table') + data = response.get_data(as_text=True) + assert 'icons/bootstrap-icons.svg#bootstrap-reboot' in data + assert 'title="Resend">' in data + assert 'href="/messages/1/edit"' in data + assert 'href="/messages/1/view"' in data + assert 'action="/messages/1/delete"' in data + assert 'href="/new-message"' in data + + def test_render_table_with_actions_and_url_tuple(self, app, client): # noqa: C901 + app.jinja_env.globals['csrf_token'] = lambda: '' + + db = SQLAlchemy(app) + + class Message(db.Model): + id = db.Column(db.Integer, primary_key=True) + sender = db.Column(db.String(20)) + recipient = db.Column(db.String(20)) + text = db.Column(db.Text) + + @app.route('/table///resend') + def test_resend_message(recipient, message_id): + return 'Re-sending {} to {}'.format(message_id, recipient) + + @app.route('/table///view') + def test_view_message(sender, message_id): + return 'Viewing {} from {}'.format(message_id, sender) + + @app.route('/table///edit') + def test_edit_message(sender, message_id): + return 'Editing {} from {}'.format(message_id, sender) + + @app.route('/table///delete') + def test_delete_message(sender, message_id): + return 'Deleting {} from {}'.format(message_id, sender) + + @app.route('/table/new-message') + def test_create_message(): + return 'New message' + + @app.route('/table') + def test(): + db.drop_all() + db.create_all() + for i in range(10): + m = Message( + text='Test message {}'.format(i+1), + sender='me', + recipient='john_doe' + ) + db.session.add(m) + db.session.commit() + page = request.args.get('page', 1, type=int) + pagination = Message.query.paginate(page, per_page=10) + messages = pagination.items + titles = [('id', '#'), ('text', 'Message')] + return render_template_string(''' + {% from 'bootstrap/table.html' import render_table %} + # URL arguments with URL tuple + {{ render_table(messages, titles, model=model, show_actions=True, custom_actions=[ ( 'Resend', 'bootstrap-reboot', - url_for('resend_message', message_id=':id') + ('test_resend_message', [('recipient', ':recipient'), ('message_id', ':id')]) ) ], - view_url=url_for('view_message', message_id=':id'), - delete_url=url_for('delete_message', message_id=':id'), - edit_url=url_for('edit_message', message_id=':id'), - new_url=url_for('new_message') + view_url=('test_view_message', [('sender', ':sender'), ('message_id', ':id')]), + edit_url=('test_edit_message', [('sender', ':sender'), ('message_id', ':id')]), + delete_url=('test_delete_message', [('sender', ':sender'), ('message_id', ':id')]), + new_url=('test_create_message') ) }} ''', titles=titles, model=Message, messages=messages) @@ -251,10 +290,6 @@ def test(): assert 'action="/table/me/1/delete"' in data assert 'href="/table/me/1/edit"' in data assert 'href="/table/new-message"' in data - assert 'href="/messages/1/edit"' in data - assert 'href="/messages/1/view"' in data - assert 'action="/messages/1/delete"' in data - assert 'href="/new-message"' in data def test_customize_icon_title_of_table_actions(self, app, client): From be35c46176212f476a460cfc44242198e770f5c0 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sat, 4 Sep 2021 11:31:21 +0800 Subject: [PATCH 7/8] Improve build_url APIs and merge warnings --- .../templates/bootstrap/table.html | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/flask_bootstrap/templates/bootstrap/table.html b/flask_bootstrap/templates/bootstrap/table.html index 32427e97..f70bb92b 100644 --- a/flask_bootstrap/templates/bootstrap/table.html +++ b/flask_bootstrap/templates/bootstrap/table.html @@ -1,14 +1,10 @@ {% from 'bootstrap/utils.html' import render_icon, arg_url_for %} -{% macro deprecate_old_pk_placeholder() %} - {{ warn('The default action primary key placeholder has changed to ":id", please update. The support to the old value (":primary_key") will be removed in version 2.0.') }} +{% macro deprecate_placeholder_url() %} + {{ warn('Passing an URL with primary key palceholder for view_url/edit_url/delete_url/custom_actions is deprecated. You will need to pass an fixed URL or an URL tuple to URL arguments, see the API docs of render_table for more details. The support to URL string will be removed in version 2.0.') }} {% endmacro %} -{% macro deprecate_action_url_string() %} - {{ warn('Passing an string as action URL for view_url/edit_url/delete_url/custom_actions is deprecated. You will need to pass an URL tuple to URL arguments, see the API docs of render_table for more details. The support to URL string will be removed in version 2.0.') }} -{% endmacro %} - -{% macro build_url(endpoint, model, pk, kwargs) %} +{% macro build_url(endpoint, model, pk, url_tuples) %} {% if model == None %} {{ raise("The model argument can't be None when setting action URLs.") }} {% endif %} @@ -16,15 +12,15 @@ {%- do url_params.update(request.view_args if not endpoint else {}), url_params.update(request.args if not endpoint else {}) -%} {% with record = model.query.get(pk) %} - {% for kwarg, value in kwargs %} - {% if value.startswith(':') and '.' in value %} - {%- set value = value[1:].split('.') -%} - {%- do url_params.update({kwarg: record[value[0]][value[1]]}) -%} - {% elif value.startswith(':') %} - {%- set value = value[1:] -%} - {%- do url_params.update({kwarg: record[value]}) -%} + {% for url_parameter, db_field in url_tuples %} + {% if db_field.startswith(':') and '.' in db_field %} + {%- set db_field = db_field[1:].split('.') -%} + {%- do url_params.update({url_parameter: record[db_field[0]][db_field[1]]}) -%} + {% elif db_field.startswith(':') %} + {%- set db_field = db_field[1:] -%} + {%- do url_params.update({url_parameter: record[db_field]}) -%} {% else %} - {%- do url_params.update({kwarg: value}) -%} + {%- do url_params.update({url_parameter: db_field}) -%} {% endif %} {% endfor %} {% endwith -%} @@ -98,11 +94,10 @@ {% for (action_name, action_icon, action_url) in custom_actions %} {% if ':primary_key' in action_url | join('') %} {% set action_pk_placeholder = ':primary_key' %} - {% set w = deprecate_old_pk_placeholder() %} + {% set _ = deprecate_placeholder_url() %} {% endif %} Date: Sat, 4 Sep 2021 11:43:53 +0800 Subject: [PATCH 8/8] Update docs and changelog for table URLs --- docs/macros.rst | 88 +++++++++++++++++++++++++++++-------- flask_bootstrap/__init__.py | 2 +- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/docs/macros.rst b/docs/macros.rst index 486f5944..3530d1cb 100644 --- a/docs/macros.rst +++ b/docs/macros.rst @@ -413,6 +413,13 @@ Render a Bootstrap table with given data. Example ~~~~~~~ +.. code-block:: python + + @app.route('/test') + def test(): + data = Message.query.all() + return render_template('test.html', data=data) + .. code-block:: jinja {% from 'bootstrap/table.html' import render_table %} @@ -431,15 +438,14 @@ API header_classes=None,\ responsive=False,\ responsive_class='table-responsive',\ - model=None,\ show_actions=False,\ actions_title='Actions',\ + model=None,\ custom_actions=None,\ view_url=None,\ edit_url=None,\ delete_url=None,\ - new_url=None,\ - action_pk_placeholder=':id') + new_url=None) :param data: An iterable of data objects to render. Can be dicts or class objects. :param titles: An iterable of tuples of the format (prop, label) e.g ``[('id', '#')]``, if not provided, @@ -451,26 +457,70 @@ API :param header_classes: A string of classes to apply to the table header (e.g ``'thead-dark'``). :param responsive: Whether to enable/disable table responsiveness. :param responsive_class: The responsive class to apply to the table. Default is ``'table-responsive'``. - :param model: The model used to build custom_action, view, edit, delete, and new urls. This allows for proper - usage of Flask's default path converter types (i.e. ``str``, ``int``) (e.g. ``Model``). When using - this tuples are accepted in place of view, edit, delete, new, and custom_actions urls. Defaults to - ``None`` to allow for backwards-compatibility. Tuple format: - ``('route_name', [('db_model_fieldname', ':url_parameter_name')])``. ``db_model_fieldname`` may also - contain dots to access relationships and their fields (e.g. ``db_model_relationship_field.name``). :param show_actions: Whether to display the actions column. Default is ``False``. + :param model: The model used to build custom_action, view, edit, delete URLs. :param actions_title: Title for the actions column header. Default is ``'Actions'``. :param custom_actions: A list of tuples for creating custom action buttons, where each tuple contains - ('Title Text displayed on hover', 'bootstrap icon name', 'url_for()') + ('Title Text displayed on hover', 'bootstrap icon name', 'URL tuple') (e.g. ``[('Run', 'play-fill', ('run_report', [('report_id', ':id')]))]``). - :param view_url: URL or tuple (see :param:`model`) to use for the view action. - :param edit_url: URL or tuple (see :param:`model`) to use for the edit action. - :param delete_url: URL or tuple (see :param:`model`) to use for the delete action. - :param new_url: URL or tuple (see :param:`model`) to use for the create action (new in version 1.6.0). - :param action_pk_placeholder: The placeholder which replaced by the primary key when build the action URLs. Default is ``':id'``. - -.. tip:: The default value of ``action_pk_placeholder`` changed to ``:id`` in version 1.7.0. - The old value (``:primary_key``) will be removed in version 2.0. Currently, you can't - use ``int`` converter on the URL variable of primary key. + :param view_url: URL string or URL tuple in ``('endpoint', [('url_parameter_name', ':db_model_fieldname')])`` + to use for the view action. + :param edit_url: URL string or URL tuple in ``('endpoint', [('url_parameter_name', ':db_model_fieldname')])`` + to use for the edit action. + :param delete_url: URL string or URL tuple in ``('endpoint', [('url_parameter_name', ':db_model_fieldname')])`` + to use for the delete action. + :param new_url: URL string or endpoint to use for the create action (new in version 1.6.0). + +To set the URLs for table actions, you will need to pass an URL tuple in the form of +``('endpoint', [('url_parameter_name', ':db_model_fieldname')])``: + +- ``endpoint``: endpoint of the view, normally the name of the view function +- ``[('url_parameter_name', ':db_model_fieldname')]``: a list of two-element tuples, the tuple should contain the + URL parameter name and the corresponding field name in the database model (starts with a ``:`` mark to indicate + it's a variable, otherwise it will becomes a fixed value). `db_model_fieldname`` may also contain dots to access + relationships and their fields (e.g. ``user.name``). + +Remember to set the ``model`` when setting this URLs, so that Bootstrap-Flask will know where to get the actual value +when building the URL. + +For example, for the view below: + +.. code-block:: python + + class Message(Model): + id = Column(primary_key=True) + + @app.route('/messages/') + def view_message(message_id): + pass + +To pass the URL point to this view for ``view_url``, the value will be: ``view_url=('view_message', [('message_id', ':id')])``. +Here is the full example: + +.. code-block:: python + + @app.route('/test') + def test(): + data = Message.query.all() + return render_template('test.html', data=data, Message=Message) + +.. code-block:: jinja + + {% from 'bootstrap/table.html' import render_table %} + + {{ render_table(data, model=Message, view_url=('view_message', [('message_id', ':id')])) }} + +The following arguments are expect to accpet an URL tuple: + +- ``custom_actions`` +- ``view_url`` +- ``edit_url`` +- ``delete_url`` + +You can also pass a fiexd URL string, but use a primary key placeholder in the URL is deprecated and will be removed +in version 2.0. + +The ``new_url`` expects a fixed URL string or an endpoint. render_icon() diff --git a/flask_bootstrap/__init__.py b/flask_bootstrap/__init__.py index c3ae29f0..8ccf5d10 100644 --- a/flask_bootstrap/__init__.py +++ b/flask_bootstrap/__init__.py @@ -24,7 +24,7 @@ def is_hidden_field_filter(field): VERSION_POPPER = '1.14.0' -def raise_helper(message): +def raise_helper(message): # pragma: no cover raise RuntimeError(message)