WTForms image picker widget voor Flask met Bootstrap 4 zonder extra Javascript en CSS
Door de WTforms RadioField ListWidget aan te passen en Bootstrap 4 knoppen te gebruiken kunnen we een mooie image picker bouwen.
Wanneer u zich aanmeldt voor deze website krijgt u een avatarafbeelding toegewezen. Natuurlijk kun je de avatar in 'je account' veranderen en dit gebeurt met een image picker. Veel image pickers voorbeelden zijn te vinden op het internet. Maar dit is een Flask site inclusief WTForms en ik wil dat de image picker wordt gegenereerd door de prachtige Jinja macro die ik gebruik, zie ook onderstaande link, ok, ik heb het een beetje aangepast. Met deze macro is het plaatsen van een formulier op het sjabloon een makkie en ziet het er uit als:
{% 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 %}
De site gebruikt ook Bootstrap 4 dus ik heb liever geen extra Javascript en/of CSS, we hebben al genoeg! Er zijn veel image pickers op het internet, maar bij het filteren van de resultaten met WTForms blijft er niet veel over.
Oplossing: maak een WTForms widget aan.
Als we de beschikbare middelen willen gebruiken is er geen andere keuze dan een WTForms image picker widget aan te maken. Het probleem is dat de documentatie hierover beperkt is en dat er niet veel voorbeelden zijn. Dus hoe gaan we nu verder? De image picker vorm is als een groep van radio buttons. Selecteer er één en verzend het formulier. Bij het invoeren van de code WTForms is de klasse RadioField als volgt:
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()
De ListWidget wordt gebruikt om de radio buttons HTML code uit te voeren. De code 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))
Wat we moeten doen is 1. een kopie maken van de ListWidget en 2. deze zo aanpassen dat deze ook de avatarafbeeldingen zal uitvoeren. Dan kunnen we dit als volgt gebruiken:
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'))
De avatar_id-keuzes worden in de weergavefunctie gegenereerd als lijst van tuples. De geselecteerde waarde wordt ook toegekend door bijvoorbeeld de weergavefunctie:
form.avatar_id.choices = [('default.png', 'default.png'), ('avatar1.png', 'avatar1.png'), ...]
form.avatar_id.data = 'default.png'
Het is niet echt moeilijk om te zien hoe de ListWidget de HTML genereert. De __call__() functie begint met de openingscode, in de 'html' lijst:
html = ['<%s %s>' % (self.html_tag, html_params(**kwargs))]
Vervolgens wordt één voor één de code HTML radio button toegevoegd aan de 'html'-lijst:
html.append('<li>%s %s</li>' % (subfield(), subfield.label))
Het sluitzegel is bijgevoegd:
html.append('</%s>' % self.html_tag)
En de HTML wordt geretourneerd door zich aan te sluiten bij de 'html' lijst:
return HTMLString(''.join(html))
Wat zit er in een subveld?
Als we onze aangepaste HTML willen bouwen hebben we alle informatie nodig, maar hoe kunnen we die krijgen? Het moet in het subveld zijn. Wat ik deed was wat ik gewoonlijk doe en dat is het afdrukken van de subveld attributen:
for subfield in field:
current_app.logger.debug(fname + ': subfield.__dict__ = {}'.format(subfield.__dict__))
Dit gaf de volgende informatie, die slechts voor één van de subvelden werd getoond:
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}
Let op het aangevinkte attribuut, het signaleert het geselecteerde item. We gebruiken dit om onze eigen HTML te bouwen.
Met behulp van Bootstrap 4 knoppen
Ik gebruik al enige tijd Bootstrap en dacht dat de Bootstrap knop een goede kandidaat zou kunnen zijn voor de image picker widget. De avatarafbeeldingen die op de site worden gebruikt hebben een transparante achtergrond, wat leuk is bij gebruik van de Bootstrap -knoppen. Een geselecteerde, zwevende, Bootstrap knop toont een donkerdere achtergrond en rand.
De truc is om de radio button en de avatar afbeelding in de knop te plaatsen. Ik verberg ook de radio button met behulp van de d-none klasse. Tenslotte sluit ik de knoppen in met een div. De ListImagesWidget ziet er nu uit:
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))
Dit werkt allemaal heel mooi. Het toont alle foto's en legt de knop eronder vast. De geselecteerde afbeelding is iets donkerder.
Samenvatting
Dit is slechts een van de vele manieren om dit te implementeren. Hier heb ik wat WTForms code gekopieerd en aangepast. Het mooie is dat we geen extra Javascript, jQuery en/of CSS hoeven toe te voegen. Als u dit in actie wilt zien, moet u zich bij deze site registreren en naar uw 'Account' gaan.
Links / credits
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
Recent
- Database UUID primaire sleutels van je webapplicatie verbergen
- Don't Repeat Yourself (DRY) met Jinja2
- SQLAlchemy, PostgreSQL, maximum aantal rijen per user
- Toon de waarden in SQLAlchemy dynamische filters
- Veilige gegevensoverdracht met Public Key versleuteling en pyNaCl
- rqlite: een alternatief voor SQLite met hoge beschikbaarheid en distributed
Meest bekeken
- Met behulp van Python's pyOpenSSL om SSL-certificaten die van een host zijn gedownload te controleren
- Gebruik van UUIDs in plaats van Integer Autoincrement Primary Keys met SQLAlchemy en MariaDb
- Maak verbinding met een dienst op een Docker host vanaf een Docker container
- PyInstaller en Cython gebruiken om een Python executable te maken
- SQLAlchemy: Gebruik van Cascade Deletes om verwante objecten te verwijderen
- Flask RESTful API verzoekparametervalidatie met Marshmallow-schema's