Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #268. Allow table actions when data is provided as dicts #284

Merged
merged 3 commits into from Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Expand Up @@ -7,6 +7,8 @@ Changelog

Release date: -

- Support creating action URLs for dict data (`#268 <https://github.com/helloflask/bootstrap-flask/issues/268>`__).


2.2.0
-----
Expand Down
14 changes: 9 additions & 5 deletions docs/macros.rst
Expand Up @@ -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')
Expand All @@ -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:

Expand All @@ -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:

Expand Down
16 changes: 7 additions & 9 deletions 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('.') -%}
Expand All @@ -20,7 +19,6 @@
{%- do url_params.update({url_parameter: db_field}) -%}
{% endif %}
{% endfor %}
{% endwith -%}
{{ arg_url_for(endpoint, url_params) }}
{%- endwith %}
{%- endmacro %}
Expand Down Expand Up @@ -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) }}</a>
{% endfor %}
Expand All @@ -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') }}
Expand All @@ -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') }}
Expand All @@ -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 %}
Expand Down
50 changes: 40 additions & 10 deletions tests/test_bootstrap4/test_render_table.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down