diff --git a/CHANGES.rst b/CHANGES.rst index e49381f3..edfc3faa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,13 +11,17 @@ Release date: - - Combine ``class`` argument of ``render_field`` or ``field.render_kw.class`` with Bootstrap classes (`#159 `__). - Add initial support for Bootstrap 5 (`#161 `__): - - Add ``Bootstrap4`` class and deprecate ``Bootstrap``. - - Add ``Bootstrap5`` class for Bootstrap 5 support. - - Move Bootstrap 4-related files to ``bootstrap4`` subfolder, and deprecate template path ``bootstrap/``. - - Bootstrap 4 macros are in the ``bootstrap4/`` template folder, and Bootstrap 5 macros are in ``bootstrap5/``. - - Add seperate tests, templates, static files, and examples for Bootstrap 5. + - Add ``Bootstrap4`` class and deprecate ``Bootstrap``. + - Add ``Bootstrap5`` class for Bootstrap 5 support. + - Move Bootstrap 4-related files to ``bootstrap4`` subfolder, and deprecate template path ``bootstrap/``. + - Bootstrap 4 macros are in the ``bootstrap4/`` template folder, and Bootstrap 5 macros are in ``bootstrap5/``. + - Add separate tests, templates, static files, and examples for Bootstrap 5. - Remove the deprecated ``form_errors`` macro and the URL string variable support in ``render_table``. - Render boolean field as a Bootstrap switch with ``SwitchField`` class. +- Add ``BOOTSTRAP_FORM_GROUP_CLASSES`` config for Bootstrap 5, defaults to ``mb-3``. Also add a ``form_group_classes`` + parameter for ``render_form``, ``render_field``, and ``render_form_row``. +- Add ``BOOTSTRAP_FORM_INLINE_CLASSES`` config for Bootstrap 5, defaults to ``row row-cols-lg-auto g-3 align-items-center``. + Also add a ``form_inline_classes`` parameter for ``render_form``. 1.8.0 diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 7755bfe8..e6ff3cd0 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -1,13 +1,13 @@ -

Bootstrap-Flask

+

Bootstrap-Flask

Bootstrap helper for Flask.

Links

diff --git a/docs/basic.rst b/docs/basic.rst index 5794cb86..5d26f4dd 100644 --- a/docs/basic.rst +++ b/docs/basic.rst @@ -153,30 +153,34 @@ Go to the :doc:`macros` page to see the detailed usage for these macros. Configurations -------------- -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| Configuration Variable | Default Value | Description | -+=============================+================================+==============================================================================================+ -| BOOTSTRAP_SERVE_LOCAL | ``False`` | If set to ``True``, local resources will be used for ``load_*`` methods. | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| BOOTSTRAP_BTN_STYLE | ``'primary'`` | Default form button style, will change to ``primary`` in next major release | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| BOOTSTRAP_BTN_SIZE | ``'md'`` | Default form button size | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| BOOTSTRAP_ICON_SIZE | ``'1em'`` | Default icon size | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| BOOTSTRAP_ICON_COLOR | ``None`` | Default icon color, follow the context with ``currentColor`` if not set | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| BOOTSTRAP_BOOTSWATCH_THEME | ``None`` | Bootswatch theme to use, see available themes at :ref:`bootswatch_theme` | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| BOOTSTRAP_MSG_CATEGORY | ``'primary'`` | Default flash message category | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| BOOTSTRAP_TABLE_VIEW_TITLE | ``'View'`` | Default title for view icon of table actions | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| BOOTSTRAP_TABLE_EDIT_TITLE | ``'Edit'`` | Default title for edit icon of table actions | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| BOOTSTRAP_TABLE_DELETE_TITLE| ``'Delete'`` | Default title for delete icon of table actions | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ -| BOOTSTRAP_TABLE_NEW_TITLE | ``'New'`` | Default title for new icon of table actions | -+-----------------------------+--------------------------------+----------------------------------------------------------------------------------------------+ ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| Configuration Variable | Default Value | Description | ++=============================+===================================================+==============================================================================================+ +| BOOTSTRAP_SERVE_LOCAL | ``False`` | If set to ``True``, local resources will be used for ``load_*`` methods | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_BTN_STYLE | ``'primary'`` | Default form button style, will change to ``primary`` in next major release | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_BTN_SIZE | ``'md'`` | Default form button size | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_ICON_SIZE | ``'1em'`` | Default icon size | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_ICON_COLOR | ``None`` | Default icon color, follow the context with ``currentColor`` if not set | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_BOOTSWATCH_THEME | ``None`` | Bootswatch theme to use, see available themes at :ref:`bootswatch_theme` | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_MSG_CATEGORY | ``'primary'`` | Default flash message category | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_TABLE_VIEW_TITLE | ``'View'`` | Default title for view icon of table actions | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_TABLE_EDIT_TITLE | ``'Edit'`` | Default title for edit icon of table actions | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_TABLE_DELETE_TITLE| ``'Delete'`` | Default title for delete icon of table actions | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_TABLE_NEW_TITLE | ``'New'`` | Default title for new icon of table actions | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +| BOOTSTRAP_FORM_GROUP_CLASSES| ``'mb-3'`` | Default form group classes | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ +|BOOTSTRAP_FORM_INLINE_CLASSES| ``'row row-cols-lg-auto g-3 align-items-center'`` | Default form inline classes | ++-----------------------------+---------------------------------------------------+----------------------------------------------------------------------------------------------+ .. tip:: See :ref:`button_customization` to learn how to customize form buttons. diff --git a/docs/macros.rst b/docs/macros.rst index 69f762cb..8c90ea4b 100644 --- a/docs/macros.rst +++ b/docs/macros.rst @@ -108,7 +108,13 @@ the ``render_kw`` dict or the ``class`` keyword arguments with Bootstrap classes API ~~~~ -.. py:function:: render_field(field, form_type="basic", horizontal_columns=('lg', 2, 10), button_style="", button_size="", button_map={}) +.. py:function:: render_field(field,\ + form_type="basic",\ + horizontal_columns=('lg', 2, 10),\ + button_style="",\ + button_size="",\ + button_map={},\ + form_group_classes="") :param field: The form field (attribute) to render. :param form_type: One of ``basic``, ``inline`` or ``horizontal``. See the @@ -121,9 +127,13 @@ API overwrite config ``BOOTSTRAP_BTN_STYLE``. :param button_size: Set button size for ``SubmitField``. Accept Bootstrap button size name: sm, md, lg, block, 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``) .. tip:: See :ref:`button_customization` and :ref:`checkbox_customization` to learn more on customizations. + render_form() --------------- @@ -154,7 +164,9 @@ API button_map={},\ id="",\ novalidate=False,\ - render_kw={}) + render_kw={},\ + form_group_classes="",\ + form_inline_classes="",) :param form: The form to output. :param action: The URL to receive form data. @@ -180,6 +192,12 @@ API :param novalidate: Flag that decide whether add ``novalidate`` class in ``
``. :param render_kw: A dictionary, specifying custom attributes for the ```` 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``) + :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``). + .. tip:: See :ref:`button_customization` to learn how to customize form buttons. @@ -239,16 +257,17 @@ API ~~~~ .. py:function:: render_form_row(fields,\ - row_class='form-row',\ + row_class='row/form-row',\ col_class_default='col',\ col_map={},\ button_style="",\ button_size="",\ - button_map={}) + button_map={},\ + form_group_classes="") :param fields: An iterable of fields to render in a row. - :param row_class: Class to apply to the div intended to represent the row, like ``form-row`` - or ``row`` + :param row_class: Class to apply to the div intended to represent the row, like ``form-row`` (Bootstrap 4) + or ``row`` (Bootstrap 5). :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 @@ -260,6 +279,9 @@ API default to ``md``. This will overwrite config ``BOOTSTRAP_BTN_SIZE``. :param button_map: A dictionary, mapping button field name to Bootstrap button style names. For example, ``{'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``) .. tip:: See :ref:`button_customization` to learn how to customize form buttons. diff --git a/examples/bootstrap4/app.py b/examples/bootstrap4/app.py index a7d98a0a..68a14021 100644 --- a/examples/bootstrap4/app.py +++ b/examples/bootstrap4/app.py @@ -1,12 +1,8 @@ # -*- coding: utf-8 -*- -from flask import Flask, render_template, request, flash, Markup - +from flask import Flask, render_template, request, flash, Markup, redirect, url_for from flask_wtf import FlaskForm, CSRFProtect -from wtforms import StringField, SubmitField, BooleanField, PasswordField, IntegerField,\ - FormField, SelectField, FieldList from wtforms.validators import DataRequired, Length, Regexp from wtforms.fields import * - from flask_bootstrap import Bootstrap4, SwitchField from flask_sqlalchemy import SQLAlchemy @@ -126,7 +122,18 @@ def index(): @app.route('/form', methods=['GET', 'POST']) def test_form(): form = HelloForm() - return render_template('form.html', form=form, telephone_form=TelephoneForm(), contact_form=ContactForm(), im_form=IMForm(), button_form=ButtonForm(), example_form=ExampleForm()) + if form.validate_on_submit(): + flash('Form validated!') + return redirect(url_for('index')) + return render_template( + 'form.html', + form=form, + telephone_form=TelephoneForm(), + contact_form=ContactForm(), + im_form=IMForm(), + button_form=ButtonForm(), + example_form=ExampleForm() + ) @app.route('/nav', methods=['GET', 'POST']) diff --git a/examples/bootstrap5/app.py b/examples/bootstrap5/app.py index e226f20f..96aec6f6 100644 --- a/examples/bootstrap5/app.py +++ b/examples/bootstrap5/app.py @@ -1,12 +1,8 @@ # -*- coding: utf-8 -*- -from flask import Flask, render_template, request, flash, Markup - +from flask import Flask, render_template, request, flash, Markup, redirect, url_for from flask_wtf import FlaskForm, CSRFProtect -from wtforms import StringField, SubmitField, BooleanField, PasswordField, IntegerField,\ - FormField, SelectField, FieldList from wtforms.validators import DataRequired, Length, Regexp from wtforms.fields import * - from flask_bootstrap import Bootstrap5, SwitchField from flask_sqlalchemy import SQLAlchemy @@ -126,7 +122,18 @@ def index(): @app.route('/form', methods=['GET', 'POST']) def test_form(): form = HelloForm() - return render_template('form.html', form=form, telephone_form=TelephoneForm(), contact_form=ContactForm(), im_form=IMForm(), button_form=ButtonForm(), example_form=ExampleForm()) + if form.validate_on_submit(): + flash('Form validated!') + return redirect(url_for('index')) + return render_template( + 'form.html', + form=form, + telephone_form=TelephoneForm(), + contact_form=ContactForm(), + im_form=IMForm(), + button_form=ButtonForm(), + example_form=ExampleForm() + ) @app.route('/nav', methods=['GET', 'POST']) diff --git a/examples/bootstrap5/templates/form.html b/examples/bootstrap5/templates/form.html index e5143001..93d76099 100644 --- a/examples/bootstrap5/templates/form.html +++ b/examples/bootstrap5/templates/form.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% from 'bootstrap4/form.html' import render_form, render_field, render_form_row %} +{% from 'bootstrap5/form.html' import render_form, render_field, render_form_row %} {% block content %} diff --git a/flask_bootstrap/__init__.py b/flask_bootstrap/__init__.py index 0577155a..db7defdd 100644 --- a/flask_bootstrap/__init__.py +++ b/flask_bootstrap/__init__.py @@ -87,6 +87,11 @@ def init_app(self, app): app.config.setdefault('BOOTSTRAP_TABLE_EDIT_TITLE', 'Edit') app.config.setdefault('BOOTSTRAP_TABLE_DELETE_TITLE', 'Delete') app.config.setdefault('BOOTSTRAP_TABLE_NEW_TITLE', 'New') + app.config.setdefault('BOOTSTRAP_FORM_GROUP_CLASSES', 'mb-3') # Bootstrap 5 only + app.config.setdefault( + 'BOOTSTRAP_FORM_INLINE_CLASSES', + 'row row-cols-lg-auto g-3 align-items-center' + ) # Bootstrap 5 only def load_css(self, version=None, bootstrap_sri=None): """Load Bootstrap's css resources with given version. diff --git a/flask_bootstrap/templates/bootstrap4/form.html b/flask_bootstrap/templates/bootstrap4/form.html index 6351f8ca..b6691fed 100644 --- a/flask_bootstrap/templates/bootstrap4/form.html +++ b/flask_bootstrap/templates/bootstrap4/form.html @@ -57,7 +57,7 @@ {% set field_kwargs = kwargs %} {% call _hz_form_wrap(horizontal_columns, form_type, True, required=required) %} {% if field.type == 'SwitchField' %} -
+
{%- if field.errors %} {{ field(class_="custom-control-input is-invalid%s" % extra_classes)|safe }} {%- else -%} diff --git a/flask_bootstrap/templates/bootstrap5/form.html b/flask_bootstrap/templates/bootstrap5/form.html new file mode 100644 index 00000000..54d4bfc4 --- /dev/null +++ b/flask_bootstrap/templates/bootstrap5/form.html @@ -0,0 +1,293 @@ +{# This file was part of Flask-Bootstrap and was modified under the terms of + its BSD License. Copyright (c) 2013, Marc Brinkmann. All rights reserved. #} + +{% macro render_hidden_errors(form) %} + {%- if form.errors %} + {%- for fieldname, errors in form.errors.items() %} + {%- if bootstrap_is_hidden_field(form[fieldname]) %} + {%- for error in errors %} +
{{ error }}
+ {%- endfor %} + {%- endif %} + {%- endfor %} + {%- endif %} +{%- endmacro %} + +{% macro _hz_form_wrap(horizontal_columns, form_type, add_group=False, required=False) %} + {% if form_type == "horizontal" %} + {% if add_group %} +
{% endif %} +
+ {% endif %} +{{ caller() }} + +{% if form_type == "horizontal" %} + {% if add_group %}
{% endif %} +
+{% endif %} +{% endmacro %} + +{% macro render_field(field, + form_type="basic", + horizontal_columns=('lg', 2, 10), + button_map={}, + button_style='', + button_size='', + form_group_classes='') %} + + {# this is a workaround hack for the more straightforward-code of just passing required=required parameter. older versions of wtforms do not have +the necessary fix for required=False attributes, but will also not set the required flag in the first place. we skirt the issue using the code below #} + {% if field.flags.required and not required in kwargs %} + {% set kwargs = dict(required=True, **kwargs) %} + {% endif %} + + {% set form_group_classes = form_group_classes or config.BOOTSTRAP_FORM_GROUP_CLASSES %} + + {# combine render_kw class or class/class_ argument with Bootstrap classes #} + {% set render_kw_class = ' ' + field.render_kw.class if field.render_kw.class else '' %} + {% set class = kwargs.pop('class', '') or kwargs.pop('class_', '') %} + {% if class %} + {# override render_kw class when class/class_ presents as keyword argument #} + {% set render_kw_class = '' %} + {% set render_kw_class_ = '' %} + {% set class = ' ' + class %} + {% endif %} + {% set extra_classes = render_kw_class + class %} + + {% if field.widget.input_type == 'checkbox' %} + {% set field_kwargs = kwargs %} + {% call _hz_form_wrap(horizontal_columns, form_type, True, required=required) %} + {% if field.type == 'SwitchField' %} + {% do field_kwargs.update({'role': 'switch'}) %} + {% endif %} +
+ {%- if field.errors %} + {{ field(class="form-check-input is-invalid%s" % extra_classes, **field_kwargs)|safe }} + {%- else -%} + {{ field(class="form-check-input%s" % extra_classes, **field_kwargs)|safe }} + {%- endif %} + {{ field.label(class="form-check-label", for=field.id)|safe }} + {%- if field.errors %} + {%- for error in field.errors %} +
{{ error }}
+ {%- endfor %} + {%- elif field.description -%} + {% call _hz_form_wrap(horizontal_columns, form_type, required=required) %} + {{ field.description|safe }} + {% endcall %} + {%- endif %} +
+ {% endcall %} + {%- elif field.type == 'RadioField' -%} + {# note: A cleaner solution would be rendering depending on the widget, + this is just a hack for now, until I can think of something better #} +
+ {%- if form_type == "inline" %} + {{ field.label(class="visually-hidden")|safe }} + {% elif form_type == "horizontal" %} + {{ field.label(class="col-form-label" + ( + " col-%s-%s" % horizontal_columns[0:2]))|safe }} + {%- else -%} + {{ field.label(class="form-label")|safe }} + {% endif %} + {% if form_type == 'horizontal' %} +
+ {% endif %} + {#% call _hz_form_wrap(horizontal_columns, form_type, True, required=required) %#} + {% for item in field -%} +
+ {{ item(class="form-check-input")|safe }} + {{ item.label(class="form-check-label", for=item.id)|safe }} +
+ {% endfor %} + {#% endcall %#} + {% if form_type == 'horizontal' %} +
+ {% endif %} + {%- if field.errors %} + {%- for error in field.errors %} +
{{ error }}
+ {%- endfor %} + {%- elif field.description -%} + {{ field.description|safe }} + {%- endif %} +
+ {%- elif field.type == 'SubmitField' -%} + {# deal with jinja scoping issues? #} + {% set field_kwargs = kwargs %} + {# note: same issue as above - should check widget, not field type #} + {% call _hz_form_wrap(horizontal_columns, form_type, True, required=required) %} + {% set default_button_style = button_style or config.BOOTSTRAP_BTN_STYLE %} + {% set default_button_size = button_size or config.BOOTSTRAP_BTN_SIZE %} + {{ field(class='btn btn-%s btn-%s%s' % (button_map.get(field.name, default_button_style), default_button_size, extra_classes), **field_kwargs) }} + {% endcall %} + {%- elif field.type in ['CSRFTokenField', 'HiddenField'] -%} + {{ field()|safe }} + {%- elif field.type in ['FormField', 'FieldList'] -%} + {# note: FormFields are tricky to get right and complex setups requiring + these are probably beyond the scope of what this macro tries to do. + the code below ensures that things don't break horribly if we run into + one, but does not try too hard to get things pretty. #} +
+ {{ field.label }} + {%- for subfield in field %} + {% if not bootstrap_is_hidden_field(subfield) -%} + {{ render_field(subfield, + form_type=form_type, + horizontal_columns=horizontal_columns, + button_map=button_map) }} + {%- endif %} + {%- endfor %} +
+ {% else -%} +
+ {%- if form_type == "inline" %} + {{ field.label(class="visually-hidden")|safe }} + {% if field.errors %} + {{ field(class="form-control mb-2 mr-sm-2 mb-sm-0 is-invalid%s" % extra_classes, **kwargs)|safe }} + {% else %} + {{ field(class="form-control mb-2 mr-sm-2 mb-sm-0%s" % extra_classes, **kwargs)|safe }} + {% endif %} + {% elif form_type == "horizontal" %} + {{ field.label(class="col-form-label" + (" col-%s-%s" % horizontal_columns[0:2]))|safe }} +
+ {% if field.errors %} + {{ field(class="form-control is-invalid%s" % extra_classes, **kwargs)|safe }} + {% else %} + {{ field(class="form-control%s" % extra_classes, **kwargs)|safe }} + {% endif %} +
+ {%- if field.errors %} + {%- for error in field.errors %} + {% call _hz_form_wrap(horizontal_columns, form_type, required=required) %} +
{{ error }}
+ {% endcall %} + {%- endfor %} + {%- elif field.description -%} + {% call _hz_form_wrap(horizontal_columns, form_type, required=required) %} + {{ field.description|safe }} + {% endcall %} + {%- endif %} + {%- else -%} + {{ field.label(class="form-label")|safe }} + {% if field.errors %} + {{ field(class="form-control is-invalid%s" % extra_classes, **kwargs)|safe }} + {% else %} + {{ field(class="form-control%s" % extra_classes, **kwargs)|safe }} + {% endif %} + {%- if field.errors %} + {%- for error in field.errors %} +
{{ error }}
+ {%- endfor %} + {%- elif field.description -%} + {{ field.description|safe }} + {%- endif %} + {%- endif %} +
+ {% endif %} +{% endmacro %} + +{# valid form types are "basic", "inline" and "horizontal" #} +{% macro render_form(form, + action="", + method="post", + extra_classes=None, + role="form", + form_type="basic", + horizontal_columns=('lg', 2, 10), + enctype=None, + button_map={}, + button_style="", + button_size="", + id="", + novalidate=False, + render_kw={}, + form_group_classes='', + form_inline_classes='') %} + {#- +action="" is what we want, from http://www.ietf.org/rfc/rfc2396.txt: + +4.2. Same-document References + + A URI reference that does not contain a URI is a reference to the + current document. In other words, an empty URI reference within a + document is interpreted as a reference to the start of that document, + and a reference containing only a fragment identifier is a reference + to the identified fragment of that document. Traversal of such a + reference should not result in an additional retrieval action. + However, if the URI reference occurs in a context that is always + intended to result in a new request, as in the case of HTML's FORM + element, then an empty URI reference represents the base URI of the + current document and should be replaced by that URI when transformed + into a request. + + -#} + {#- if any file fields are inside the form and enctype is automatic, adjust + if file fields are found. could really use the equalto test of jinja2 + here, but latter is not available until 2.8 + + warning: the code below is guaranteed to make you cry =( +#} + {%- set form_inline_classes = form_inline_classes or config.BOOTSTRAP_FORM_INLINE_CLASSES %} + {%- set _enctype = [] %} + {%- if enctype is none -%} + {%- for field in form %} + {%- if field.type in ['FileField', 'MultipleFileField'] %} + {#- for loops come with a fairly watertight scope, so this list-hack is + used to be able to set values outside of it #} + {%- set _ = _enctype.append('multipart/form-data') -%} + {%- endif %} + {%- endfor %} + {%- else %} + {% set _ = _enctype.append(enctype) %} + {%- endif %} + {%- if form_type == "inline" %}{% set form_group_classes = 'col-12' %}{%- endif %} + + {{ form.hidden_tag() }} + {{ render_hidden_errors(form) }} + {%- for field in form %} + {% if not bootstrap_is_hidden_field(field) -%} + {{ render_field(field, + form_type=form_type, + horizontal_columns=horizontal_columns, + button_map=button_map, + button_style=button_style, + button_size=button_size, + form_group_classes=form_group_classes) }} + {%- endif %} + {%- endfor %} + +{%- endmacro %} + +{% macro render_form_row(fields, + row_class='row', + col_class_default='col', + col_map={}, + button_map={}, + button_style='', + button_size='', + form_group_classes='') %} +
+ {% for field in fields %} + {% if field.name in col_map %} + {% set col_class = col_map[field.name] %} + {% else %} + {% set col_class = col_class_default %} + {% endif %} +
+ {{ render_field(field, button_map=button_map, button_style=button_style, button_size=button_size, form_group_classes=form_group_classes) }} +
+ {% endfor %} +
+{% endmacro %} diff --git a/tests/test_bootstrap4/test_render_form.py b/tests/test_bootstrap4/test_render_form.py index 4d31732f..6d67f3db 100644 --- a/tests/test_bootstrap4/test_render_form.py +++ b/tests/test_bootstrap4/test_render_form.py @@ -41,21 +41,21 @@ def description(): assert 'Just check this' in data -# test WTForm field description for SwitchField -def test_form_description_for_switchfield(app, client): +# test render SwitchField +def test_switchfield(app, client): class TestForm(FlaskForm): remember = SwitchField('Remember me', description='Just check this') - @app.route('/description') - def description(): + @app.route('/switch') + def test_switch(): form = TestForm() return render_template_string(''' {% from 'bootstrap4/form.html' import render_form %} {{ render_form(form) }} ''', form=form) - response = client.get('/description') + response = client.get('/switch') data = response.get_data(as_text=True) assert 'Remember me' in data assert 'custom-switch' in data diff --git a/tests/test_bootstrap5/__init__.py b/tests/test_bootstrap5/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_bootstrap5/test_render_form.py b/tests/test_bootstrap5/test_render_form.py new file mode 100644 index 00000000..81010464 --- /dev/null +++ b/tests/test_bootstrap5/test_render_form.py @@ -0,0 +1,111 @@ +from flask import render_template_string +from flask_wtf import FlaskForm +from flask_bootstrap import SwitchField + + +def test_switchfield(app, client): + + class TestForm(FlaskForm): + remember = SwitchField('Remember me', description='Just check this') + + @app.route('/switch') + def test_switch(): + form = TestForm() + return render_template_string(''' + {% from 'bootstrap5/form.html' import render_form %} + {{ render_form(form) }} + ''', form=form) + + response = client.get('/switch') + data = response.get_data(as_text=True) + assert 'Remember me' in data + assert 'custom-control custom-switch' not in data + assert 'form-check form-switch' in data + assert 'role="switch"' in data + assert 'Just check this' in data + + +def test_form_group_class(app, client, hello_form): + @app.route('/default') + def test_default(): + form = hello_form() + return render_template_string(''' + {% from 'bootstrap5/form.html' import render_form %} + {{ render_form(form) }} + ''', form=form) + + @app.route('/custom') + def test_custom(): + form = hello_form() + return render_template_string(''' + {% from 'bootstrap5/form.html' import render_form %} + {{ render_form(form, form_group_classes='mb-2') }} + ''', form=form) + + response = client.get('/default') + data = response.get_data(as_text=True) + assert '
' not in data + assert '
' in data + assert '
' in data