Wagtail Streamforms¶
Allows you to build forms in the CMS admin area and add them to any StreamField in your site. You can create your own types of forms meaning an endless array of possibilities. Templates can be created which will then appear as choices when you build your form, allowing you to display and submit a form however you want.
Backwards Compatibility¶
If you are using a version of wagtail 1.x, then the latest compatible version of this package is 1.6.3:
$ pip install wagtailstreamforms<2
Other wise you must install a version of this package from 2 onwards:
$ pip install wagtailstreamforms>=2
What else is included?¶
- Customise things like success and error messages, post submit redirects and more.
- Forms are processed via a
before_page_serve
hook. Meaning there is no fuss like remembering to include a page mixin. - The hook can easily be disabled to provide the ability to create your own.
- Forms are categorised by their class in the CMS admin for easier navigation.
- Form submissions are also listed by their form which you can filter by date and are ordered by newest first.
- You can add site wide regex validators for use in regex fields.
- A form and its fields can easily be copied to a new form.
- There is a template tag that can be used to render a form, in case you want it to appear outside a StreamField.
- Recaptcha can be added to a form.
Installation¶
Wagtail Streamform is available on PyPI - to install it, just run:
pip install wagtailstreamforms
Once thats done you need to add the following to your INSTALLED_APPS
settings:
INSTALLED_APPS = [
...
'wagtail.contrib.modeladmin',
'wagtail.contrib.forms',
'wagtailstreamforms'
...
]
Run migrations:
python manage.py migrate
Go to your cms admin area and you will see the Streamforms
section.
Basic Usage¶
Just add the wagtailstreamforms.blocks.WagtailFormBlock()
in any of your streamfields:
body = StreamField([
...
('form', WagtailFormBlock())
...
])
And you are ready to go.
Using the template tag¶
There is also a template tag you can use outside of a streamfield, within a page. All this is doing is rendering the form using the same block as in the streamfield.
The tag takes three parameters:
- slug (
string
) - The slug of the form instance. - reference (
string
) - This should be a unique string and needs to be persistent on refresh/reload. See note below. - action (
string
) optional - The form action url.
Note
The reference is used when the form is being validated.
Because you can have any number of the same form on a page there needs to be a way of uniquely identifying the form beyond its PK
.
This is so that when the form has validation errors and it is passed back through the pages context, We know what form it is.
This reference MUST be persistent on page refresh or you will never see the errors.
Usage:
{% load streamforms_tags %}
{% streamforms_form slug="form-slug" reference="some-very-unique-reference" action="." %}
Form Templates¶
You can create your own form templates to use against any form in the system, providing a vast array of ways to create, style and submit your forms.
The default template located at streamforms/form_block.html
can be seen below:
<h2>{{ value.form.name }}</h2>
<form action="{{ value.form_action }}" method="post" novalidate>
{% csrf_token %}
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
{% for field in form.visible_fields %}
{% include 'streamforms/partials/form_field.html' %}
{% endfor %}
<input type="submit" value="{{ value.form.submit_button_text }}">
</form>
Note
It is important here to keep the hidden fields as the form will have some in order to process correctly.
Once you have created you own you will need to add it to the list of available templates.
This is as simple as adding it to the WAGTAILSTREAMFORMS_FORM_TEMPLATES
in settings:
# this is the defaults
WAGTAILSTREAMFORMS_FORM_TEMPLATES = (
('streamforms/form_block.html', 'Default Form Template'),
)
Rendering your StreamField¶
It is important to ensure the request is in the context of your page to do this iterate over your StreamField block using
wagtails include_block
template tag.
{% load wagtailcore_tags %}
{% for block in page.body %}
{% include_block block %}
{% endfor %}
DO NOT use the short form method of {{ block }}
as described here
as you will get CSRF verification failures.
Deleted forms¶
In the event of a form being deleted which is still in use in a streamfield the following template will be rendered in its place:
streamforms/non_existent_form.html
<p>Sorry, this form has been deleted.</p>
You can override this by putting a copy of the template in you own project using the same
path under a templates directory ie app/templates/streamforms/non_existent_form.html
. As long as the app is before
wagtailstreamforms
in INSTALLED_APPS
it will use your template instead.
Messaging¶
When the success
or error
message options are completed in the form builder and upon submission of the form
a message is sent to django’s messaging framework.
You will need to add django.contrib.messages
to your INSTALLED_APPS
setting:
INSTALLED_APPS = [
...
'django.contrib.messages'
...
]
To display these in your site you will need to include somewhere in your page’s markup a snippet similar to the following:
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
Any message from the form will then be displayed.
Form Customisation¶
Currently we have defined two different types of forms, one which just enables saving the submission and one to additionally email the results of the submission.
Custom basic form¶
You can easily add your own all you have to do is create a model that
inherits from wagtailstreamforms.models.BaseForm
add any additional fields or properties and
this will be added to the cms admin area.
Example:
from wagtailstreamforms.models import BaseForm
class CustomForm(BaseForm):
def process_form_submission(self, form):
super().process_form_submission(form) # handles the submission saving
# do your own stuff here
Custom email form¶
If you want to inherit the additional email sending functionality then inherit from
wagtailstreamforms.models.AbstractEmailForm
. The saving of the submission and sending of the email
is handled in the process_form_submission
so be sure to call super
if overriding that method.
Example:
from wagtailstreamforms.models import AbstractEmailForm
class CustomEmailForm(AbstractEmailForm):
""" As above with email sending. """
def process_form_submission(self, form):
super().process_form_submission(form) # handles the submission saving and emailing
# do your own stuff here
Custom email form with content¶
Here is an example of an email form that has an additional RichTextField
rendered with the form.
This is especially useful if your form is being rendered from the template tag and you dont want to slot it in a streamfield.
Model:
from wagtail.admin.edit_handlers import TabbedInterface, ObjectList, FieldPanel
from wagtail.core.fields import RichTextField
from wagtailstreamforms.models import AbstractEmailForm, BaseForm
class EmailFormWithContent(AbstractEmailForm):
""" A form with content that sends and email. """
content = RichTextField(blank=True)
content_panels = [
FieldPanel('content', classname='full'),
]
edit_handler = TabbedInterface([
ObjectList(AbstractEmailForm.settings_panels, heading='General'),
ObjectList(AbstractEmailForm.field_panels, heading='Fields'),
ObjectList(AbstractEmailForm.email_panels, heading='Email Submission'),
ObjectList(content_panels, heading='Content'),
])
Template:
{% load wagtailcore_tags %}
<h2>{{ value.form.name }}</h2>
{% if value.form.content %}
<div class="form-content">{{ value.form.content|richtext }}</div>
{% endif %}
<form action="{{ value.form_action }}" method="post" novalidate>
{% csrf_token %}
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
{% for field in form.visible_fields %}
{% include 'streamforms/partials/form_field.html' %}
{% endfor %}
<input type="submit" value="{{ value.form.submit_button_text }}">
</form>
Custom form submission model¶
If you need to save additional data, you can use a custom form submission model. To do this, you need to:
- Define a model that extends
wagtailstreamforms.models.AbstractFormSubmission
. - Override the
get_submission_class
andprocess_form_submission
methods in your form model.
Example:
import json
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.utils.translation import ugettext_lazy as _
from wagtail.core.models import Page
from wagtailstreamforms.models import AbstractFormSubmission, BaseForm
class CustomForm(BaseForm):
""" A form that saves the current user and page. """
def get_data_fields(self):
data_fields = super().get_data_fields()
data_fields += [
('user', _('User')),
('page', _('Page'))
]
return data_fields
def get_submission_class(self):
return CustomFormSubmission
def process_form_submission(self, form):
if self.store_submission:
self.get_submission_class().objects.create(
form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
form=self,
page=form.page,
user=form.user if not form.user.is_anonymous() else None
)
class CustomFormSubmission(AbstractFormSubmission):
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True)
page = models.ForeignKey(Page)
def get_data(self):
form_data = super().get_data()
form_data.update({
'page': self.page,
'user': self.user
})
return form_data
Note
Its important to note here that the form.page
and form.user
seen above are passed in via the
before_serve_page
hook wagtailstreamforms.wagtail_hooks.process_form
.
If you want to use a different method of saving the form and you require these you will need to pass
them in yourself when adding request.POST
to the form.
Example usage can be seen in Providing your own submission method
Reference¶
-
class
wagtailstreamforms.models.
BaseForm
(*args, **kwargs)¶ A form base class, any form should inherit from this.
-
exception
DoesNotExist
¶
-
exception
MultipleObjectsReturned
¶
-
copy
()¶ Copy this form and its fields.
-
get_data_fields
()¶ Returns a list of tuples with (field_name, field_label).
-
get_form_fields
()¶ Form expects form_fields to be declared. If you want to change backwards relation name, you need to override this method.
-
get_submission_class
()¶ Returns submission class.
You can override this method to provide custom submission class. Your class must be inherited from AbstractFormSubmission.
-
process_form_submission
(form)¶ Accepts form instance with submitted data. Creates submission instance if self.store_submission = True.
You can override this method if you want to have custom creation logic. For example, you want to additionally send an email.
-
specific
¶ Return this form in its most specific subclassed form.
-
specific_class
¶ Return the class that this page would be if instantiated in its most specific form
-
exception
-
class
wagtailstreamforms.models.
AbstractEmailForm
(*args, **kwargs)¶ A form that sends and email.
You can create custom form model based on this abstract model. For example, if you need a form that will send an email.
-
process_form_submission
(form)¶ Process the form submission and send an email.
-
send_form_mail
(form)¶ Send an email.
-
-
class
wagtailstreamforms.models.
AbstractFormSubmission
(*args, **kwargs)¶ Data for a form submission.
You can create custom submission model based on this abstract model. For example, if you need to save additional data or a reference to a user.
-
get_data
()¶ Returns dict with form data.
You can override this method to add additional data.
-
Form Submission Methods¶
Form submissions are handled by the means of a wagtail before_serve_page
hook. The built in hook at
wagtailstreamforms.wagtail_hooks.process_form
looks for a form in the post request,
and either:
- processes it redirecting back to the current page or defined page in the form setup.
- or renders the current page with any validation error.
If no form was posted then the page serves in the usual manner.
Note
Currently the hook expects the form to be posting to the same page it exists on.
Providing your own submission method¶
If you do not want the current hook to be used you need to disable it by setting the
WAGTAILSTREAMFORMS_ENABLE_FORM_PROCESSING
to False
in your settings:
WAGTAILSTREAMFORMS_ENABLE_FORM_PROCESSING = False
With this set no forms will be processed of any kind and you are free to process them how you feel fit.
A basic hook example¶
from django.contrib import messages
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from wagtail.core import hooks
from wagtailstreamforms.utils import get_form_instance_from_request
@hooks.register('before_serve_page')
def process_form(page, request, *args, **kwargs):
""" Process the form if there is one, if not just continue. """
if request.method == 'POST':
form_def = get_form_instance_from_request(request)
if form_def:
form = form_def.get_form(request.POST, request.FILES, page=page, user=request.user)
context = page.get_context(request, *args, **kwargs)
if form.is_valid():
# process the form submission
form_def.process_form_submission(form)
# create success message
if form_def.success_message:
messages.success(request, form_def.success_message, fail_silently=True)
# redirect to the page defined in the form
# or the current page as a fallback - this will avoid refreshing and submitting again
redirect_page = form_def.post_redirect_page or page
return redirect(redirect_page.get_url(request), context=context)
else:
# update the context with the invalid form and serve the page
# IMPORTANT you must set these so that the when the form in the streamfield is
# rendered it knows that it is the form that is invalid
context.update({
'invalid_stream_form_reference': form.data.get('form_reference'),
'invalid_stream_form': form
})
# create error message
if form_def.error_message:
messages.error(request, form_def.error_message, fail_silently=True)
return TemplateResponse(
request,
page.get_template(request, *args, **kwargs),
context
)
Supporting ajax requests¶
The only addition here from the basic example is just the if request.is_ajax:
and the JsonResponse
parts.
We are just making it respond with this if the request was ajax.
from django.contrib import messages
from django.http import JsonResponse
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from wagtail.core import hooks
from wagtailstreamforms.utils import get_form_instance_from_request
@hooks.register('before_serve_page')
def process_form(page, request, *args, **kwargs):
""" Process the form if there is one, if not just continue. """
if request.method == 'POST':
form_def = get_form_instance_from_request(request)
if form_def:
form = form_def.get_form(request.POST, request.FILES, page=page, user=request.user)
context = page.get_context(request, *args, **kwargs)
if form.is_valid():
# process the form submission
form_def.process_form_submission(form)
# if the request is_ajax then just return a success message
if request.is_ajax():
return JsonResponse({'message': form_def.success_message or 'success'})
# create success message
if form_def.success_message:
messages.success(request, form_def.success_message, fail_silently=True)
# redirect to the page defined in the form
# or the current page as a fallback - this will avoid refreshing and submitting again
redirect_page = form_def.post_redirect_page or page
return redirect(redirect_page.get_url(request), context=context)
else:
# if the request is_ajax then return an error message and the form errors
if request.is_ajax():
return JsonResponse({
'message': form_def.error_message or 'error',
'errors': form.errors
})
# update the context with the invalid form and serve the page
# IMPORTANT you must set these so that the when the form in the streamfield is
# rendered it knows that it is the form that is invalid
context.update({
'invalid_stream_form_reference': form.data.get('form_reference'),
'invalid_stream_form': form
})
# create error message
if form_def.error_message:
messages.error(request, form_def.error_message, fail_silently=True)
return TemplateResponse(
request,
page.get_template(request, *args, **kwargs),
context
)
The template for the form might look like:
<h2>{{ value.form.name }}</h2>
<form action="{{ value.form_action }}" method="post" id="id_streamforms_{{ form.initial.form_id }}" novalidate>
{% csrf_token %}
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
{% for field in form.visible_fields %}
{% include 'streamforms/partials/form_field.html' %}
{% endfor %}
<input type="submit" value="{{ value.form.submit_button_text }}">
</form>
<script>
$("#id_streamforms_{{ form.initial.form_id }}").submit(function(e) {
$.ajax({
type: "POST",
url: ".",
data: $(this).serialize(),
success: function(data) {
// do something with data
console.log(data);
},
error: function(data) {
// do something with data
console.log(data);
}
});
e.preventDefault();
});
</script>
Permissions¶
Setting the level of access to administer your different types of forms is the same as it is for any page. Your types of forms will appear in the groups section of the wagtail admin > settings area.
Here you can assign the usual add, change and delete permissions.
Note
Its worth noting here that if you do delete a form it will also delete all submissions for that form.
Form submission permissions¶
Because the form submission models are not listed in the admin area the following statement applies.
Important
If you can either add, change or delete a form type then you can view all of its submissions. However to be able to delete the submissions, it requires that you can delete the form type.
Enabling reCAPTCHA¶
Has been enabled via the django-recaptcha package.
Once you have signed up for reCAPTCHA.
Follow the below and an option will be in the form setup fields
tab to add a reCAPTCHA.
Just add captcha
to your INSTALLED_APPS
settings:
INSTALLED_APPS = [
...
'captcha'
...
]
Add the required keys in your settings:
RECAPTCHA_PUBLIC_KEY = 'xxx'
RECAPTCHA_PRIVATE_KEY = 'xxx'
If you would like to use the new No Captcha reCaptcha add the setting NOCAPTCHA = True
. For example:
NOCAPTCHA = True
Settings¶
Any settings with their defaults are listed below for quick reference.
# the label of the forms area in the admin sidebar
WAGTAILSTREAMFORMS_ADMIN_MENU_LABEL = 'Streamforms'
# the order of the forms area in the admin sidebar
WAGTAILSTREAMFORMS_ADMIN_MENU_ORDER = None
# enable the built in hook to process form submissions
WAGTAILSTREAMFORMS_ENABLE_FORM_PROCESSING = True
# the default form template choices
WAGTAILSTREAMFORMS_FORM_TEMPLATES = (
('streamforms/form_block.html', 'Default Form Template'),
)
Contributors¶
People that have helped in any way shape or form to get to where we are, many thanks.
In our team¶
In the community¶
Changelog¶
master¶
- in development
2.0.1¶
- Fixed migration issue #70
2.0.0¶
- Added support for wagtail 2.
1.6.3¶
- Fix issue where js was not in final package
1.6.2¶
- Added javascript to auto populate the form slug from the name
1.6.1¶
- Small tidy up in form code
1.6.0¶
- Stable Release
1.5.2¶
- Added
AbstractEmailForm
to more easily allow creating additional form types.
1.5.1¶
- Fix migrations being regenerated when template choices change
1.5.0¶
- Removed all project dependencies except wagtail and recapcha
- The urls no longer need to be specified in your
urls.py
and can be removed.
1.4.4¶
- The template tag now has the full page context incase u need a reference to the user or page
1.4.3¶
- Fixed bug where messages are not available in the template tags context
1.4.2¶
- Removed label value from recapcha field
- Added setting to set order of menu item in cms admin
1.4.1¶
- Added an optional error message to display if the forms have errors
1.4.0¶
- Added a template tag that can be used to render a form. Incase you want it to appear outside a streamfield
1.3.0¶
- A form and it’s fields can easily be copied to a new form from within the admin area
1.2.3¶
- Fix paginator on submission list not remembering date filters
1.2.2¶
- Form submission viewing and deleting permissions have been implemented
1.2.1¶
- On the event that a form is deleted that is still referenced in a streamfield, we are rendering a generic template that can be overridden to warn the end user
1.2.0¶
- In the form builder you can now specify a page to redirect to upon successful submission of the form
- The page mixin StreamFormPageMixin that needed to be included in every page has now been replaced by a wagtail before_serve_page hook so you will need to remove this mixin
1.1.1¶
- Fixed bug where multiple forms of same type in a streamfield were both showing validation errors when one submitted