diff --git a/CHANGES.rst b/CHANGES.rst index dd1141d2..af3ed75c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,8 @@ Changelog Release date: - +- Support creating action URLs for dict data (`#268 `__). + 2.2.0 ----- diff --git a/docs/macros.rst b/docs/macros.rst index 0d8c578a..4b0184f5 100644 --- a/docs/macros.rst +++ b/docs/macros.rst @@ -519,7 +519,9 @@ API using ``|urlize``. Is overruled by ``safe_columns`` parameter. Default is ``None``. WARNING: Only use this for sanitized user data to prevent XSS attacks. :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 model: An optional model used to build custom_action, view, edit, + delete URLs. Set this if you need to pull the URL arguments from + a different SQLAlchemy class indexed with the same primary key. :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 tuple or fixed URL string') @@ -541,8 +543,10 @@ an URL tuple in the form of ``('endpoint', [('url_parameter_name', ':db_model_fi 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. +By default, Bootstrap-Flask will take the fields from the row data provided. +Alternatively, you may set the ``model``, in which case a record from that +model, indexed with the same primary key, will be used to get the actual +value when building the URL. For example, for the view below: @@ -563,13 +567,13 @@ Here is the full example: @app.route('/test') def test(): data = Message.query.all() - return render_template('test.html', data=data, Message=Message) + return render_template('test.html', data=data) .. code-block:: jinja {% from 'bootstrap4/table.html' import render_table %} - {{ render_table(data, model=Message, view_url=('view_message', [('message_id', ':id')])) }} + {{ render_table(data, view_url=('view_message', [('message_id', ':id')])) }} The following arguments are expect to accpet an URL tuple: diff --git a/flask_bootstrap/templates/base/table.html b/flask_bootstrap/templates/base/table.html index bf561a76..40751b31 100644 --- a/flask_bootstrap/templates/base/table.html +++ b/flask_bootstrap/templates/base/table.html @@ -1,14 +1,13 @@ {% from 'base/utils.html' import render_icon, arg_url_for %} -{% macro build_url(endpoint, model, pk, url_tuples) %} - {% if model == None %} - {{ raise("The model argument can't be None when setting action URLs.") }} +{% macro build_url(record, endpoint, url_tuples, model, pk_field) %} + {% if model != None %} + {% set record = model.query.get(record[pk_field]) %} {% endif %} {% with url_params = {} -%} {%- 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 url_parameter, db_field in url_tuples %} {% if db_field.startswith(':') and '.' in db_field %} {%- set db_field = db_field[1:].split('.') -%} @@ -20,7 +19,6 @@ {%- do url_params.update({url_parameter: db_field}) -%} {% endif %} {% endfor %} - {% endwith -%} {{ arg_url_for(endpoint, url_params) }} {%- endwith %} {%- endmacro %} @@ -115,7 +113,7 @@ {% if action_url is string %} href="{{ action_url }}" {% else %} - href="{{ build_url(action_url[0], model, row[primary_key], action_url[1]) | trim }}" + href="{{ build_url(row, action_url[0], action_url[1], model, primary_key) | trim }}" {% endif %} title="{{ action_name }}">{{ render_icon(action_icon) }} {% endfor %} @@ -125,7 +123,7 @@ {% if view_url is string %} href="{{ view_url }}" {% else %} - href="{{ build_url(view_url[0], model, row[primary_key], view_url[1]) | trim }}" + href="{{ build_url(row, view_url[0], view_url[1], model, primary_key) | trim }}" {% endif %} title="{{ config['BOOTSTRAP_TABLE_VIEW_TITLE'] }}"> {{ render_icon('eye-fill') }} @@ -136,7 +134,7 @@ {% if edit_url is string %} href="{{ edit_url }}" {% else %} - href="{{ build_url(edit_url[0], model, row[primary_key], edit_url[1]) | trim }}" + href="{{ build_url(row, edit_url[0], edit_url[1], model, primary_key) | trim }}" {% endif %} title="{{ config['BOOTSTRAP_TABLE_EDIT_TITLE'] }}"> {{ render_icon('pencil-fill') }} @@ -147,7 +145,7 @@ {% if delete_url is string %} action="{{ delete_url }}" {% else %} - action="{{ build_url(delete_url[0], model, row[primary_key], delete_url[1]) | trim }}" + action="{{ build_url(row, delete_url[0], delete_url[1], model, primary_key) | trim }}" {% endif %} method="post"> {% if csrf_token is undefined %} diff --git a/tests/test_bootstrap4/test_render_table.py b/tests/test_bootstrap4/test_render_table.py index 2efc87dd..2211dd1a 100644 --- a/tests/test_bootstrap4/test_render_table.py +++ b/tests/test_bootstrap4/test_render_table.py @@ -244,7 +244,7 @@ def test_create_message(): return 'New message' @app.route('/table') - def test(): + def table(): db.drop_all() db.create_all() for i in range(10): @@ -277,15 +277,45 @@ def test(): ) }} ''', titles=titles, model=Message, messages=messages) - response = client.get('/table') - data = response.get_data(as_text=True) - assert 'icons/bootstrap-icons.svg#bootstrap-reboot' in data - assert 'href="/table/john_doe/1/resend"' in data - assert 'title="Resend">' in data - assert 'href="/table/me/1/view"' 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 + @app.route('/table-with-dict-data') + def dict_data_table(): + row_dicts = [{ + "id": i+1, + "text": f'Test message {i + 1}', + "sender": 'me', + "recipient": 'john_doe' + } for i in range(10)] + + messages = row_dicts + titles = [('id', '#'), ('text', 'Message')] + return render_template_string(''' + {% from 'bootstrap4/table.html' import render_table %} + # URL arguments with URL tuple + {{ render_table(messages, titles, show_actions=True, + custom_actions=[ + ( + 'Resend', + 'bootstrap-reboot', + ('test_resend_message', [('recipient', ':recipient'), ('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') + ) }} + ''', titles=titles, messages=messages) + + for url in ['/table', '/table-with-dict-data']: + response = client.get(url) + data = response.get_data(as_text=True) + assert 'icons/bootstrap-icons.svg#bootstrap-reboot' in data + assert 'href="/table/john_doe/1/resend"' in data + assert 'title="Resend">' in data + assert 'href="/table/me/1/view"' 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 def test_customize_icon_title_of_table_actions(app, client):