Original Post — Wagtail 1.12
The Problem — Your team are loving the custom form builder in Wagtail CMS and want to let people submit an image along with the form.
The Solution — Define a new form field type that is selectable when editing fields in the CMS Admin, this field type will be called ‘Upload Image’. This field should show up in the view as a normal upload field with restrictions on file type and size, just like the Wagtail Images system.
Wagtail, Images and Forms?
Skip ahead if you know the basics here.
Wagtail is a Content Management System CMS that is built on top of the Django Web Framework . What I love about Wagtail is that it embraces the Django ecosystem and way of doing things. It also has a really nice admin interface that makes it easy for users to interact with the content.
Wagtail has a built in interface and framework for uploading, storing and serving images. This is aptly named Wagtail Images, you can review the docs about Using Images in Templates or Advanced Image Usage for more information.
Wagtail comes with a great Form Builder module, it lets users build their own forms in the admin interface. These forms can have a series of fields such as Text, Multi-line Text, Email, URL, Checkbox, and others that build up a form page that can be viewed on the front end of the website. Users can customise the default value, whether the field is required and also some help text that relates to the field.
Before We Start
Before we start changing (breaking) things, it is important that you have the following items completed.
- Wagtail v1.12.x up and running as per the main documentation .
- Wagtailforms module is installed, running and you have forms working.
Adding Image Upload Fields to Forms in Wagtail
Planning our Changes
We want to enable the following user interaction:
- The admin interface should provide the ability to edit an existing form and create a new form as normal.
- When editing a form page, there should be a new dropdown option on the ‘Field Type’ field called ‘Upload Image’.
- The form page view should have one file upload field for every ‘Upload Image’ field that was defined in the admin.
- The form page view should accept images with the same restrictions as Wagtail Images (< 10mb, only PNG/JPG/GIF*).
- The form page view should require the image if the field is defined as ‘required’ in admin.
- When an image is valid, it should save this image into the Wagtail Images area.
- A link to the the image should be saved to the form submission (aka form response), this will ensure it appears on emails or reports.
* Default GIF support is quite basic in Wagtail, if you want to support animated GIFs you should read these docs regarding Animated GIFs .
1. Extend the AbstractFormField Class
In your models file that contains your FormPage class definition, you
should also have a definition for a FormField class. In the original
definition, the
AbstractFormField class
uses a fixed tuple of
FORM_FIELD_CHOICES
. We need to override the
field_type
with an appended set of choices.
In the above code you can see that we imported the original
FORM_FIELD_CHOICES
from wagtail.wagtailforms.models. We then converted it to a list, added
our new field type and then this is used in the choices argument of the
field_type field.
When you do this, you will need to make a migration, and run that migration. Test it out, the form in admin will now let you select this type, but it will not do much else yet.
2. Extend the FormBuilder Class
In your models file you will now need to create an extended form builder
class. In the original definition the
FormBuilder class
builds a form based on the
field_type
list that is stored in each FormPage instance. This building process
generates a set of Django field classes. This is done by taking each field
type and using a dictionary to find a dedicated function that returns a
Django field, these functions are stored in a dictionary called
FIELD_TYPES
.
In the above code, we have imported FormBuilder and WagtailImageField,
then created our own extended FormBuilder with a new class. The first
thing we do is define a function that returns a created WagtailImageField.
Then we update the dictionary of FIELD_TYPES with our new function, mapped
to the ‘image’ field type. Remember above we added the choice
(‘image’, ‘Upload Image’)
where the key is
image
.
3. Set our FormPage class to use ExtendedFormBuilder
This step is pretty easy, we want to override the
form_builder
definition in our FormPage model. This is a very nifty way that Wagtail
enables you to override the form_builder you use.
4. FormPage serve method to accept uploaded files
Uploaded files in Django are located in the request.FILES object, not in
the request.POST object. In the original definition of the
serve method inside AbstractForm
the request.FILES object is not give to the get_form method. This means we
will have to override the whole serve method. This step is also small but
requires copying and pasting a lot of code from the
serve
method on FormPage just to change one small part.
The only difference between this
serve
method and the one we are extending is in the line
form = self.getform(…
. We are adding
request.FILES
to the arguments between
request.POST
and
page=self
.
The detailed reason for this is that Django handles files sent with the request differently, see the Django documentation about file uploads for more information. We need to ensure that our form instance gets any request.FILES along with the data stored in request.POST.
Important: In future versions of Wagtail you should not have to do this, I put through a pull request that has been approved which stops the need for this whole step.
5. Ensure our form view can accept File Data
The form page view should have a <form> tag in it, the the implementation suggested by Wagtail does not allow files data to be submitted in the form.
The only difference to the basic form is that we have added
enctype=”multipart/form-data”
to our form attributes. If you do not do this you will never get any files
sent through the request and no errors to advise you why. For more
information about why we need to do this, you can view the
Django Docs File Uploads
page.
6. Process the Image (file) Data after Validation
We will now override the
process_form_submission
on our FormPage class. The original definition of the
process_form_submission method
has no notion of processing anything other than the request.POST data. It
will simply convert the cleaned data to JSON for storing on the form
submission instance. We will iterate through each field and find any
instances of
WagtailImageField
then get the data, create a new Wagtail Image with that file data, finally
we will store a link to the image in the response.
A few items of note here:
-
cleaned_data
contains the File Data (for any files), the Django form module does this for us. File Data cannot be parsed by the JSON parser, hence us having to process into a URL for these cases. -
filename_to_title
can look like whatever you want, I stripped out dashes and made the file title case. You do not have to do this but you do have to ensure there is some title when inserting a WagtailImage.
def filename_to_title(filename):
from os.path import splitext
if filename:
result = splitext(filename)[0]
result = result.replace('-', ' ').replace('_', ' ')
return result.title()
-
image.get_rendition
is a very useful function detailed in the Wagtail Documentation , it mimics the template helper but can be used in Python. By default the URL will be relative (it will not contain the http/https, or the domain), this will mean links sent to email will not work. It is up to you to work out how to best solve this if it is an issue. -
You must use
cleaned_data.update
to save a json seralizable reference to your image, hence the file data will not work. You could save the image ID or any other type of string, an Image URL is just what works for this use case. -
The images will be added to the default collection, which should be the
root
collection, you can also customise this if you want. -
Images require
uploaded_by_user
to be defined, if the form is public (ie. on a website that does not require being signed in) you will need to work around this. Maybe by just saying it was uploaded by the user that created the form page or the first user. -
Using
get_image_model
is the best practice way to get the Image Model that Wagtail is using.
Finishing Up
Your Form models.py file will now look something like the following:
Forms can now have one or more Image Upload fields that are defined by the CMS editors. These images will be available in Admin in the Images section and can be used throughout the rest of Wagtail. You also get all the benefits that come with Wagtail Images like search indexing, usage in templates and URLS for images of various compressed sizes.
Let me know if you run into issues or find some typos/bugs in this article. Thank you to the amazing team at Torchbox and all the developers of Wagtail for making this amazing tool. Show your support of Wagtail by starring the Wagtail repo on Github .
Thanks to my friend Adam for helping me proof this.