WTForms image picker widget para Flask con Bootstrap 4 sin Javascript extra y CSS
Modificando los WTforms RadioField ListWidget y usando los botones Bootstrap 4 podemos construir un bonito image picker.
Cuando te registras en este sitio web se te asigna una imagen de avatar. Por supuesto que puedes cambiar el avatar en 'tu cuenta' y esto se hace usando un image picker. Muchos ejemplos de image pickers se pueden encontrar en Internet. Pero este es un sitio Flask incluyendo WTForms y quiero que el image picker sea generado por la maravillosa macro Jinja que estoy usando, ver también el enlace de abajo, ok, lo modifiqué un poco. Con este macro poner un formulario en la plantilla es pan comido y parece:
{% from "macros/wtf_bootstrap.html" import bootstrap_form %}
{% extends "content_full_width.html" %}
{% block content %}
{{ bootstrap_form(form) }}
{% if back_button %}
<a href="{{ back_button.url }}" class="btn btn-outline-dark btn-lg mt-4" role="button">
{{ back_button.name }}
</a>
{% endif %}
{% endblock %}
El sitio también usa Bootstrap 4 por lo que prefiero no tener Javascript y/o CSS extra, ya tenemos suficiente! Hay muchos image pickers en Internet, pero cuando se filtran los resultados con WTForms no queda mucho.
Solución: crear un WTForms widget
Si queremos utilizar los recursos disponibles no hay otra opción que crear un WTForms image picker widget. El problema es que la documentación sobre esto es limitada y no hay muchos ejemplos. Entonces, ¿cómo proceder? El formulario image picker es como un grupo de radio buttons. Seleccione uno y envíe el formulario. Cuando se profundiza en el código WTForms , la clase RadioField es la siguiente:
lib64/python3.6/site-packages/wtforms/fields/core.py
class RadioField(SelectField):
"""
Like a SelectField, except displays a list of radio buttons.
Iterating the field will produce subfields (each containing a label as
well) in order to allow custom rendering of the individual radio fields.
"""
widget = widgets.ListWidget(prefix_label=False)
option_widget = widgets.RadioInput()
El ListWidget se utiliza para emitir el código radio buttons HTML . El código WTForms :
lib64/python3.6/site-packages/wtforms/widgets/core.py
class ListWidget(object):
"""
Renders a list of fields as a `ul` or `ol` list.
This is used for fields which encapsulate many inner fields as subfields.
The widget will try to iterate the field to get access to the subfields and
call them to render them.
If `prefix_label` is set, the subfield's label is printed before the field,
otherwise afterwards. The latter is useful for iterating radios or
checkboxes.
"""
def __init__(self, html_tag='ul', prefix_label=True):
assert html_tag in ('ol', 'ul')
self.html_tag = html_tag
self.prefix_label = prefix_label
def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)
html = ['<%s %s>' % (self.html_tag, html_params(**kwargs))]
for subfield in field:
if self.prefix_label:
html.append('<li>%s %s</li>' % (subfield.label, subfield()))
else:
html.append('<li>%s %s</li>' % (subfield(), subfield.label))
html.append('</%s>' % self.html_tag)
return HTMLString(''.join(html))
Lo que debemos hacer es 1. hacer una copia del ListWidget y 2. modificarlo para que también emita las imágenes de los avatares. Entonces podemos usar esto de la siguiente manera:
class ListImagesWidget(object):
...
our modified ListWidget code
...
class SelectImageField(SelectField):
widget = ListImagesWidget(prefix_label=False)
option_widget = RadioInput()
class AccountAvatarEditForm(FlaskForm):
avatar_id = SelectImageField(_('Select your avatar'))
submit = SubmitField(_l('Update'))
Las opciones de avatar_id se generan en la función de vista como lista de tuples. El valor seleccionado también es asignado por la función de vista, por ejemplo:
form.avatar_id.choices = [('default.png', 'default.png'), ('avatar1.png', 'avatar1.png'), ...]
form.avatar_id.data = 'default.png'
No es realmente difícil ver cómo el ListWidget genera el HTML. La función __call__() comienza con el código de apertura, en la lista 'html':
html = ['<%s %s>' % (self.html_tag, html_params(**kwargs))]
Luego, uno por uno, el código HTML radio button se agrega a la lista 'html':
html.append('<li>%s %s</li>' % (subfield(), subfield.label))
Se adjunta la etiqueta de cierre:
html.append('</%s>' % self.html_tag)
Y el HTML es devuelto al unirse a la lista 'html':
return HTMLString(''.join(html))
¿Qué hay en un subfield?
Si queremos construir nuestro HTML personalizado necesitamos toda la información, pero ¿cómo podemos obtenerla? Debe estar en subfield. Lo que hice fue lo que suelo hacer y es imprimir los atributos subfield :
for subfield in field:
current_app.logger.debug(fname + ': subfield.__dict__ = {}'.format(subfield.__dict__))
Esto dio la siguiente información, que se muestra sólo para uno de los subfields:
subfield.__dict__ = {
'meta': <wtforms.form.Meta object at 0x7ff388eb6750>,
'default': None,
'description': '',
'render_kw': None,
'filters': (),
'flags': <wtforms.fields.Flags: {}>,
'name': 'avatar_id',
'short_name': 'avatar_id',
'type': '_Option',
'validators': [],
'id': 'avatar_id-0',
'label': Label('avatar_id-0', 'default.png'),
'widget': <wtforms.widgets.core.RadioInput object at 0x7ff38989fb10>,
'process_errors': [],
'object_data': 'default.png',
'data': 'default.png',
'checked': True}
Observe el atributo marcado, que señala el elemento seleccionado. Usamos esto para construir nuestro propio HTML.
Utilizando los botones Bootstrap 4
He estado usando Bootstrap desde hace algún tiempo y pensé que el botón Bootstrap podría ser un buen candidato para el image picker widget. Las imágenes de avatar utilizadas en el sitio tienen un fondo transparente, lo cual es agradable cuando se utilizan los botones Bootstrap . Un botón seleccionado, que se encuentra en el aire, Bootstrap muestra un fondo y un borde más oscuros.
El truco está en poner el radio button y la imagen del avatar dentro del botón. También oculto el radio button usando la clase d-none. Finalmente encierro los botones por un div. El ListImagesWidget ahora se ve como:
class ListImagesWidget(object):
def __init__(self, html_tag='ul', prefix_label=True):
assert html_tag in ('ol', 'ul')
self.html_tag = html_tag
self.prefix_label = prefix_label
def __call__(self, field, **kwargs):
fname = 'ListImagesWidget - __call__'
kwargs.setdefault('id', field.id)
html = ['<div data-toggle="buttons">']
for subfield in field:
if self.prefix_label:
# never used, see caller: prefix_label=False
else:
checked = ''
active = ''
if subfield.checked:
checked = 'checked="checked"'
active = 'active'
avatar_img = '<img src="' + avatars_images_url() + '/' + str(subfield.label.text) + '" class="img-fluid rounded-circle w-75" alt="">'
button_html = '''
<button class="btn btn-light border-secondary mt-2 mr-2 mb-2 {active}">
<input type="radio" class="d-none" name="{subfield_name}" id="{subfield_id}" autocomplete="off" value="{subfield_data}" {checked}>
{avatar_img}
</button>
'''.format( active=active,
subfield_name=subfield.name,
subfield_id=subfield.id,
subfield_data=subfield.data,
checked=checked,
avatar_img=avatar_img,
)
html.append(button_html)
html.append('</div>')
return HTMLString(''.join(html))
Todo esto funciona muy bien. Muestra todas las imágenes y el botón de envío debajo de ellas. La imagen seleccionada es un poco más oscura.
Resumen
Esta es sólo una de las muchas maneras de implementar esto. Aquí copié algo de código WTForms y lo modifiqué. Lo bueno es que no tenemos que añadir Javascript, jQuery y/o CSS adicionales. Si quieres ver esto en acción debes registrarte en este sitio e ir a tu 'Cuenta'.
Enlaces / créditos
bootstrap-wtf
https://github.com/carlnewton/bootstrap-wtf
Buttons - Bootstrap
https://getbootstrap.com/docs/4.4/components/buttons/
WTForms - widgets
https://wtforms.readthedocs.io/en/stable/widgets.html
Recientes
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando PyInstaller y Cython para crear un ejecutable de Python
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow