Bewerkbare formulieren, waaronder reCaptcha, toevoegen aan uw Wagtail-site

Formulieren zijn een essentieel onderdeel van veel sites. In deze tutorial maken we een formulierbouwer met veel verschillende velden, waarmee je formulieren kunt maken op elke Wagtail-pagina.

13 juli 2020 10:10
Thema's: Wagtail Streamfield opties

Er zijn verschillende manieren om formulieren toe te voegen aan een Wagtail-site. Wagtail heeft zijn eigen ingebouwde formulierbouwer, wat in veel situaties geweldig is voor on-the-fly formulieren. Op een meertalige site met wagtailtrans (zoals we eerder hebben opgezet), worden alle pagina's echter afgeleid van TranslatablePage, terwijl de pagina in het Wagtail-formulier niet wordt afgeleid van het paginamodel maar van AbstractForm. Dit maakt het onmogelijk of althans niet triviaal om de ingebouwde formulierbouwer van Wagtail te gebruiken voor meertalige sites. Hard-coding van formulieren in pagina's, zoals we eerder hebben gedaan met een commentaarformulier, is een andere mogelijkheid, maar niet erg flexibel. Linken naar urls met formulieren die helemaal buiten Wagtail vallen, is misschien een geschikte oplossing voor inloggen / aanmelden, maar is ook niet ideaal. In deze tutorial zullen we Wagtail Streamforms implementeren, dat veel out-of-the-box velden levert, zoals singleline en multiline tekst, datum, e-mail, checkbox, bestand. We zullen hier ook een reCaptcha-veld aan toevoegen en een contactpagina maken met een contactformulier voor onze site. Er is echter een voorbehoud dat ik in het begin moet noemen: een klein compatibiliteitsprobleem met Django 3.0, dat ik aan het einde van dit artikel zal bespreken.

Installeer met:

pip3 install wagtailstreamforms

Voeg toe aan requirements.txt en voeg het volgende toe aan INSTALLED_APPS (wagtail.contrib.modeladmin is er misschien al):

'wagtail.contrib.modeladmin',
'wagtailstreamforms',

We kunnen nu een model maken voor een contactpagina in models.py (afgeleid van TranslatablePage als onze site meertalig is):

from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core import blocks
from wagtailtrans.models import TranslatablePage
from wagtailstreamforms.blocks import WagtailFormBlock

class ContactPage(TranslatablePage):
    intro = RichTextField(blank=True)
    body = StreamField([
        ('paragraph', blocks.RichTextBlock()),
        ('form', WagtailFormBlock()),
    ])
    content_panels = TranslatablePage.content_panels + [
        FieldPanel('intro'),
        StreamFieldPanel('body'),
    ]

Natuurlijk kunnen we meer velden aan de pagina toevoegen als we dat willen. Nu kunnen we de template maken in een bestand custom_form.html:

<form{% if form.is_multipart %} enctype="multipart/form-data"{% endif %} action="{{ value.form_action }}" method="post" class="needs-validation" novalidate>
    {{ form.media }}
    {% csrf_token %}
    {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
    {% for field in form.visible_fields %}
        <div class="form-group">
            {% include "account/form_field.html" %}
        </div>
    {% endfor %}
    <button type="submit" class="btn btn-outline-primary">{{ value.form.submit_button_text }}</button>
</form>

Het lijkt veel op de template in de documentatie, we hebben alleen onze eigen mooi vormgegeven form_field.html-template opgenomen die we in eerdere tutorials hebben gebruikt, en een andere knop. Zoals beschreven voegen we het aangepaste formulier samen met het standaardformulier toe aan onze instellingen:

WAGTAILSTREAMFORMS_FORM_TEMPLATES = (
    ('streamforms/form_block.html', _("Default Form Template")),  # default
    ('cms/custom_form.html', _("Custom Form Template")),
)

We hebben nog een template nodig voor de contactpagina. Dit is in wezen alleen de hoofdtekst van de contactpagina-klasse, wat een StreamField is. We gaan berichten van Wagtailstreamforms toevoegen, als die er zijn; dit wordt beschreven in de documentatie. De code voor de template is dan:

{% extends 'account/base_card.html' %}

{% load i18n wagtailcore_tags %}

{% block card-header %}
    <h3>{{ page.title }}</h3>
    <p>{{ page.intro|richtext }}</p>
{% endblock %}

{% block card-body %}
    {% if messages %}
        {% for message in messages %}
            <p{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</p>
        {% endfor %}
    {% endif %}

    {% if page.body %}
        {%  include "streamfield.html" %}
    {% endif %}

{% endblock %}

In principe hebben we nu een werkende formulierenbouwer en kunnen we de database migreren. We willen echter een paar dingen toevoegen. Laten we om te beginnen een e-mail sturen naar een specifiek adres wanneer een formulier wordt ingediend via de contactpagina. Dit wordt gedaan met een submission hook in een apart bestand wagtailstreamforms_hooks.py, zoals wordt uitgelegd in de documentatie van Wagtail Streamforms; de code is letterlijk uitgeschreven. Daarin staat het to-adres (waarnaar de e-mail wordt verzonden) vast; het is handiger om dat configureerbaar te maken in de editor. Dat wordt ook beschreven in de documentatie, dus laten we dat eerst doen. Maak in models.py het model:

from wagtailstreamforms.models.abstract import AbstractFormSetting

class AdvancedFormSetting(AbstractFormSetting):
    to_address = models.EmailField()

en zet in het instellingenbestand een parameter die naar deze klasse verwijst:

WAGTAILSTREAMFORMS_ADVANCED_SETTINGS_MODEL = 'cms.AdvancedFormSetting'

Nu zijn we klaar om een bestand wagtailstreamforms_hooks.py te maken en de code uit de documentatie daarin te plakken:

from django.conf import settings
from django.core.mail import EmailMessage
from django.template.defaultfilters import pluralize

from wagtailstreamforms.hooks import register

@register('process_form_submission')
def email_submission(instance, form):
    """ Send an email with the submission. """

    addresses = [instance.advanced_settings.to_address]
    content = ['Please see below submission\n', ]
    from_address = settings.DEFAULT_FROM_EMAIL
    subject = 'New Form Submission : %s' % instance.title

    # build up the email content
    for field, value in form.cleaned_data.items():
        if field in form.files:
            count = len(form.files.getlist(field))
            value = '{} file{}'.format(count, pluralize(count))
        elif isinstance(value, list):
            value = ', '.join(value)
        content.append('{}: {}'.format(field, value))
    content = '\n'.join(content)

    # create the email message
    email = EmailMessage(
        subject=subject,
        body=content,
        from_email=from_address,
        to=addresses
    )

    # attach any files submitted
    for field in form.files:
        for file in form.files.getlist(field):
            file.seek(0)
            email.attach(file.name, file.read(), file.content_type)

    # finally send the email
    email.send(fail_silently=True)

Het enige dat we hebben gewijzigd, is het to-adres in ons zojuist gemaakte instance.advanced_settings.to_address.

Een laatste stap is het toevoegen van reCaptcha, dat ook goed gedocumenteerd is. Eerst hebben we reCaptcha-sleutels nodig om toegang te krijgen. Ga naar https://www.google.com/u/1/recaptcha/admin/create terwijl je bent ingelogd met je Google-account en registreer je site; dit is vrij eenvoudig. We zullen reCaptcha v2 gebruiken, kijk hier voor de verschillende versies. Dit geeft je de openbare en privé-sleutel die we beide nodig hebben. Wanneer je dit test op je lokale computer (in mijn geval een Mac), krijg je mogelijk een foutmelding [SSL: CERTIFICATE_VERIFY_FAILED]; kijk hier voor een oplossing.

Vervolgens moeten we reCaptcha installeren. Daar zijn een aantal pakketten voor, we houden vast aan degene die wordt gebruikt in de documentatie van Wagtail Streamforms:

pip3 install django-recaptcha

Voeg het toe aan requirements.txt en voeg captcha toe aan INSTALLED_APPS. Plaats de openbare en privé-sleutel in instellingen en schakel noCAPTCHA in:

# reCaptcha settings
RECAPTCHA_PUBLIC_KEY = '<your public reCaptcha key>'
RECAPTCHA_PRIVATE_KEY = '<your private reCaptcha key>'
# enable no captcha
NOCAPTCHA = True

Aangezien je wilt voorkomen dat je privésleutel in je repository terechtkomt, wil je deze misschien in het niet-gesynchroniseerde deel van je instellingen plaatsen. Maak ten slotte een wagtailstreamforms_fields.py-bestand en plak de code in de documentatie erin; het is een letterlijke kopie, dus ik zal het hier niet herhalen. Het bevat de definitie van het reCaptcha-veld en registreert het als zodanig.

We zijn bijna klaar, maar er is één probleem: Wagtail Streamforms ondersteunt Django 3.0 nog niet. "Wat ?!", hoor ik je denken, "en dat vertel je me nu?". Het klinkt eigenlijk erger dan het is. Django 3.0 heeft het argument context verwijderd van de methode Field.from_db_value() en Wagtail Streamforms verwacht het nog steeds. Het probleem is geïdentificeerd door het Wagtail Streamforms-team en er is een eenvoudige oplossing voorgesteld: verander in de methode from_db_value het argument context in context=None. Het is alleen nog niet ge-merge-t bij het schrijven van dit artikel. Een mogelijkheid om hiermee om te gaan zou zijn om de repository van Wagtail Streamforms te forken, de verandering in de fork aan te brengen en die fork te gebruiken in plaats van de originele wagtailstreamforms. Voor ontwikkeling zullen we de quick and dirty oplossing nemen en de verandering aanbrengen in onze lokale omgeving. Niet aanbevolen voor productie! Laten we de oplossing van het probleem goed in de gaten houden.

Met dat in gedachten gaan we door. Migreer de database als je dat nog niet hebt gedaan en ga naar admin. Maak een formulier met alle gewenste velden en maak vervolgens een contactpagina met dat formulier erin. Klik op advanced om e-mailinzending en/of formulieropslag te selecteren. Probeer het: vul het formulier in en verstuur het, het zou in je mailbox moeten belanden.

Op een meertalige site willen we natuurlijk formulieren in verschillende talen. Daarvoor moeten ze dezelfde vorm in de andere taal opnieuw creëren; het is (nog) niet mogelijk om alle vertaalde vormen in één set te clusteren. Zolang het aantal formulieren beperkt is, is dit niet zo'n groot probleem. We kunnen het vertaalde formulier dan gewoon koppelen aan de respectievelijke vertaalde pagina.

Als je een navigatiemenu hebt gemaakt, is het eenvoudig om de contactpagina eraan toe te voegen: ga gewoon naar het menu in admin en voeg de link naar de pagina eraan toe. Bekijk de video als je wilt zien hoe.

We zijn voorlopig klaar met formulieren. Als je klaar bent om je applicatie te testen, lees dan verder.

Reageer op dit artikel (log eerst in of bevestig hieronder met naam en email)