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

show links in table text #204

Merged
merged 5 commits into from
May 22, 2022
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Changelog

Release date: --

- Add ``safe_columns`` and ``urlize_columns`` parameters to ``render_table`` macro
to support rendering table column as HTML/URL (`#204 <https://github.com/helloflask/bootstrap-flask/pull/204>`__).


2.0.1
-----
Expand Down
19 changes: 13 additions & 6 deletions docs/macros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ API
default to ``md``. This will overwrite config ``BOOTSTRAP_BTN_SIZE``.
:param form_group_classes: Bootstrap 5 only (``bootstrap5/form.html``). You can use this parameter to change the
form group classes, it will read the config ``BOOTSTRAP_FORM_GROUP_CLASSES`` first
(the default value is ``mb-3``)
(the default value is ``mb-3``).

.. tip:: See :ref:`button_customization` and :ref:`checkbox_customization` to learn more on customizations.

Expand Down Expand Up @@ -193,7 +193,7 @@ API
:param render_kw: A dictionary, specifying custom attributes for the
``<form>`` tag.
:param form_group_classes: Bootstrap 5 only (``bootstrap5/form.html``). You can use this parameter to change the form group classes, it will
read the config ``BOOTSTRAP_FORM_GROUP_CLASSES`` first (the default value is ``mb-3``)
read the config ``BOOTSTRAP_FORM_GROUP_CLASSES`` first (the default value is ``mb-3``).
:param form_inline_classes: Bootstrap 5 only (``bootstrap5/form.html``). You can use this parameter to change the form inline classes,
it will read the config ``BOOTSTRAP_FORM_INLINE_CLASSES`` first (the default value is
``row row-cols-lg-auto g-3 align-items-center``).
Expand Down Expand Up @@ -273,7 +273,7 @@ API
:param col_class_default: The default class to apply to the div that represents a column
if nothing more specific is said for the div column of the rendered field.
:param col_map: A dictionary, mapping field.name to a class definition that should be applied to
the div column that contains the field. For example: ``col_map={'username': 'col-md-2'})``
the div column that contains the field. For example: ``col_map={'username': 'col-md-2'})``.
:param button_style: Set button style for ``SubmitField``. Accept Bootstrap button style name (i.e. primary,
secondary, outline-success, etc.), default to ``primary`` (e.g. ``btn-primary``). This will
overwrite config ``BOOTSTRAP_BTN_STYLE``.
Expand All @@ -283,7 +283,7 @@ API
``{'submit': 'success'}``. This will overwrite ``button_style`` and ``BOOTSTRAP_BTN_STYLE``.
:param form_group_classes: Bootstrap 5 only (``bootstrap5/form.html``). You can use this parameter to change the
form group classes, it will read the config ``BOOTSTRAP_FORM_GROUP_CLASSES`` first
(the default value is ``mb-3``)
(the default value is ``mb-3``).
:param form_type: One of ``basic``, ``inline`` or ``horizontal``. See the Bootstrap docs for details on different
form layouts.
:param horizontal_columns: When using the horizontal layout, layout forms like this. Must be a 3-tuple of
Expand Down Expand Up @@ -366,7 +366,7 @@ API
:param size: Can be 'sm' or 'lg' for smaller/larger pagination.
:param args: Additional arguments passed to :func:`~flask.url_for`. If
``endpoint`` is ``None``, uses :attr:`~flask.Request.args` and
:attr:`~flask.Request.view_args`
:attr:`~flask.Request.view_args`.
:param fragment: Add URL fragment into link, such as ``#comment``.
:param align: The align of the pagination. Can be 'left', 'center' or 'right', default to 'left'.
:param kwargs: Extra attributes for the ``<ul>``-element.
Expand Down Expand Up @@ -442,7 +442,7 @@ API
:param dismissible: If true, will output a button to close an alert. For fully functioning dismissible alerts, you must use the alerts JavaScript plugin.
:param dismiss_animate: If true, will enable dismiss animate when click the dismiss button.

When you call ``flash('message', 'category')``, there are 8 category options available, mapping to Bootstrap 4's alerts type:
When you call ``flash('message', 'category')``, there are 8 category options available, mapping to Bootstrap's alerts type:

primary, secondary, success, danger, warning, info, light, dark.

Expand Down Expand Up @@ -491,6 +491,8 @@ API
header_classes=None,\
responsive=False,\
responsive_class='table-responsive',\
safe_columns=None,\
urlize_columns=None,\
show_actions=False,\
actions_title='Actions',\
model=None,\
Expand All @@ -510,6 +512,11 @@ 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 safe_columns: Tuple with columns names to render HTML safe using ``|safe``.
Has priority over ``urlize_columns`` parameter. Default is ``None``.
:param urlize_columns: Tuple with column names to render with HTML link on each URL
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 actions_title: Title for the actions column header. Default is ``'Actions'``.
Expand Down
15 changes: 14 additions & 1 deletion examples/bootstrap4/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,21 @@ def before_first_request_func():
db.drop_all()
db.create_all()
for i in range(20):
url = 'mailto:x@t.me'
if i % 7 == 0:
url = 'www.t.me'
elif i % 7 == 1:
url = 'https://t.me'
elif i % 7 == 2:
url = 'http://t.me'
elif i % 7 == 3:
url = 'http://t'
elif i % 7 == 4:
url = 'http://'
elif i % 7 == 5:
url = 'x@t.me'
m = Message(
text=f'Test message {i+1}',
text=f'Message {i+1} {url}',
author=f'Author {i+1}',
category=f'Category {i+1}',
create_time=4321*(i+1)
Expand Down
4 changes: 2 additions & 2 deletions examples/bootstrap4/templates/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ <h2>Responsive Table</h2>
{{ render_table(messages, responsive=True, responsive_class='table-responsive-sm') }}

<h2>Table with actions</h2>
<pre>{% raw %}{{ render_table(messages, show_actions=True, model=Message,
<pre>{% raw %}{{ render_table(messages, urlize_columns=('text'), show_actions=True, model=Message,
view_url=('view_message', [('message_id', ':id')]),
edit_url=('edit_message', [('message_id', ':id')]),
delete_url=('delete_message', [('message_id', ':id')]),
new_url=url_for('new_message')) }}{% endraw %}</pre>
{{ render_table(messages, show_actions=True, model=Message,
{{ render_table(messages, urlize_columns=('text'), show_actions=True, model=Message,
view_url=('view_message', [('message_id', ':id')]),
edit_url=('edit_message', [('message_id', ':id')]),
delete_url=('delete_message', [('message_id', ':id')]),
Expand Down
15 changes: 14 additions & 1 deletion examples/bootstrap5/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,21 @@ def before_first_request_func():
db.drop_all()
db.create_all()
for i in range(20):
url = 'mailto:x@t.me'
if i % 7 == 0:
url = 'www.t.me'
elif i % 7 == 1:
url = 'https://t.me'
elif i % 7 == 2:
url = 'http://t.me'
elif i % 7 == 3:
url = 'http://t'
elif i % 7 == 4:
url = 'http://'
elif i % 7 == 5:
url = 'x@t.me'
m = Message(
text=f'Test message {i+1}',
text=f'Message {i+1} {url}',
author=f'Author {i+1}',
category=f'Category {i+1}',
create_time=4321*(i+1)
Expand Down
4 changes: 2 additions & 2 deletions examples/bootstrap5/templates/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ <h2>Responsive Table</h2>
{{ render_table(messages, responsive=True, responsive_class='table-responsive-sm') }}

<h2>Table with actions</h2>
<pre>{% raw %}{{ render_table(messages, show_actions=True, model=Message,
<pre>{% raw %}{{ render_table(messages, urlize_columns=('text'), show_actions=True, model=Message,
view_url=('view_message', [('message_id', ':id')]),
edit_url=('edit_message', [('message_id', ':id')]),
delete_url=('delete_message', [('message_id', ':id')]),
new_url=url_for('new_message')) }}{% endraw %}</pre>
{{ render_table(messages, show_actions=True, model=Message,
{{ render_table(messages, urlize_columns=('text'), show_actions=True, model=Message,
view_url=('view_message', [('message_id', ':id')]),
edit_url=('edit_message', [('message_id', ':id')]),
delete_url=('delete_message', [('message_id', ':id')]),
Expand Down
32 changes: 27 additions & 5 deletions flask_bootstrap/templates/base/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
header_classes=None,
responsive=False,
responsive_class='table-responsive',
safe_columns=None,
urlize_columns=None,
model=None,
show_actions=False,
actions_title='Actions',
Expand Down Expand Up @@ -79,11 +81,31 @@
{% for row in data %}
<tr>
{% for title in titles %}
{% if title[0] == primary_key %}
<th scope="row">{{ row[title[0]] }}</th>
{% else %}
<td>{{ row[title[0]] }}</td>
{% endif %}
{% set key = title[0] %}
{% set value = row[key] %}
{%- if key == primary_key -%}
<th scope="row">
{%- else -%}
<td>
{%- endif -%}
{%- if value is string -%}
{%- if safe_columns and key in safe_columns -%}
{{ value|safe }}
{%- else -%}
{%- if urlize_columns and key in urlize_columns -%}
{{ value|urlize }}
{%- else -%}
{{ value }}
{%- endif -%}
{%- endif -%}
{%- else -%}
{{ value }}
{%- endif -%}
{%- if key == primary_key -%}
</th>
{%- else -%}
</td>
{%- endif -%}
{% endfor %}
{% if show_actions %}
<td>
Expand Down
68 changes: 67 additions & 1 deletion tests/test_bootstrap4/test_render_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,77 @@ def test():
assert '<table class="table">' in data
assert '<th scope="col">#</th>' in data
assert '<th scope="col">Message</th>' in data
assert '<th scope="col">Message</th>' in data
assert '<th scope="row">1</th>' in data
assert '<td>Test message 1</td>' in data


def test_render_safe_table(app, client):
db = SQLAlchemy(app)

class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
text = db.Column(db.Text)

@app.route('/table')
def test():
db.drop_all()
db.create_all()
for i in range(10):
msg = Message(text=f'Test <em>message</em> {i+1}')
db.session.add(msg)
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 'bootstrap4/table.html' import render_table %}
{{ render_table(messages, titles, safe_columns=('text'), urlize_columns=('text')) }}
''', titles=titles, messages=messages)

response = client.get('/table')
data = response.get_data(as_text=True)
assert '<table class="table">' in data
print(data)
assert '<th scope="col">#</th>' in data
assert '<th scope="col">Message</th>' in data
assert '<th scope="row">1</th>' in data
assert '<td>Test <em>message</em> 1</td>' in data


def test_render_urlize_table(app, client):
db = SQLAlchemy(app)

class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
text = db.Column(db.Text)

@app.route('/table')
def test():
db.drop_all()
db.create_all()
for i in range(10):
msg = Message(text=f'Test https://t.me {i+1}')
db.session.add(msg)
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 'bootstrap4/table.html' import render_table %}
{{ render_table(messages, titles, urlize_columns=('text')) }}
''', titles=titles, messages=messages)

response = client.get('/table')
data = response.get_data(as_text=True)
assert '<table class="table">' in data
assert '<th scope="col">#</th>' in data
assert '<th scope="col">Message</th>' in data
assert '<th scope="row">1</th>' in data
assert '<td>Test <a href="https://t.me" rel="noopener">https://t.me</a> 1</td>' in data


def test_render_customized_table(app, client):
db = SQLAlchemy(app)

Expand Down