kopia lustrzana https://github.com/wagtail/wagtail
857 wiersze
28 KiB
Markdown
857 wiersze
28 KiB
Markdown
(form_builder_customization)=
|
|
|
|
# Form builder customization
|
|
|
|
For a basic usage example see [form builder usage](form_builder_usage).
|
|
|
|
## Custom `related_name` for form fields
|
|
|
|
If you want to change `related_name` for form fields
|
|
(by default `AbstractForm` and `AbstractEmailForm` expect `form_fields` to be defined),
|
|
you will need to override the `get_form_fields` method.
|
|
You can do this as shown below.
|
|
|
|
```python
|
|
from modelcluster.fields import ParentalKey
|
|
from wagtail.admin.panels import (
|
|
FieldPanel, FieldRowPanel,
|
|
InlinePanel, MultiFieldPanel
|
|
)
|
|
from wagtail.fields import RichTextField
|
|
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
|
|
|
|
|
|
class FormField(AbstractFormField):
|
|
page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='custom_form_fields')
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
intro = RichTextField(blank=True)
|
|
thank_you_text = RichTextField(blank=True)
|
|
|
|
content_panels = AbstractEmailForm.content_panels + [
|
|
FieldPanel('intro'),
|
|
InlinePanel('custom_form_fields', label="Form fields"),
|
|
FieldPanel('thank_you_text'),
|
|
MultiFieldPanel([
|
|
FieldRowPanel([
|
|
FieldPanel('from_address', classname="col6"),
|
|
FieldPanel('to_address', classname="col6"),
|
|
]),
|
|
FieldPanel('subject'),
|
|
], "Email"),
|
|
]
|
|
|
|
def get_form_fields(self):
|
|
return self.custom_form_fields.all()
|
|
```
|
|
|
|
## 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 `wagtail.contrib.forms.models.AbstractFormSubmission`.
|
|
- Override the `get_submission_class` and `process_form_submission` methods in your page model.
|
|
|
|
Example:
|
|
|
|
```python
|
|
import json
|
|
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from modelcluster.fields import ParentalKey
|
|
from wagtail.admin.panels import (
|
|
FieldPanel, FieldRowPanel,
|
|
InlinePanel, MultiFieldPanel
|
|
)
|
|
from wagtail.fields import RichTextField
|
|
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission
|
|
|
|
|
|
class FormField(AbstractFormField):
|
|
page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
intro = RichTextField(blank=True)
|
|
thank_you_text = RichTextField(blank=True)
|
|
|
|
content_panels = AbstractEmailForm.content_panels + [
|
|
FieldPanel('intro'),
|
|
InlinePanel('form_fields', label="Form fields"),
|
|
FieldPanel('thank_you_text'),
|
|
MultiFieldPanel([
|
|
FieldRowPanel([
|
|
FieldPanel('from_address', classname="col6"),
|
|
FieldPanel('to_address', classname="col6"),
|
|
]),
|
|
FieldPanel('subject'),
|
|
], "Email"),
|
|
]
|
|
|
|
def get_submission_class(self):
|
|
return CustomFormSubmission
|
|
|
|
def process_form_submission(self, form):
|
|
return self.get_submission_class().objects.create(
|
|
form_data=form.cleaned_data,
|
|
page=self, user=form.user
|
|
)
|
|
|
|
|
|
class CustomFormSubmission(AbstractFormSubmission):
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
```
|
|
|
|
## Add custom data to CSV export
|
|
|
|
If you want to add custom data to the CSV export, you will need to:
|
|
|
|
- Override the `get_data_fields` method in page model.
|
|
- Override `get_data` in the submission model.
|
|
|
|
The example below shows how to add a username to the CSV export.
|
|
Note that this code also changes the submissions list view.
|
|
|
|
```python
|
|
import json
|
|
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from modelcluster.fields import ParentalKey
|
|
from wagtail.admin.panels import (
|
|
FieldPanel, FieldRowPanel,
|
|
InlinePanel, MultiFieldPanel
|
|
)
|
|
from wagtail.fields import RichTextField
|
|
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission
|
|
|
|
|
|
class FormField(AbstractFormField):
|
|
page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
intro = RichTextField(blank=True)
|
|
thank_you_text = RichTextField(blank=True)
|
|
|
|
content_panels = AbstractEmailForm.content_panels + [
|
|
FieldPanel('intro'),
|
|
InlinePanel('form_fields', label="Form fields"),
|
|
FieldPanel('thank_you_text'),
|
|
MultiFieldPanel([
|
|
FieldRowPanel([
|
|
FieldPanel('from_address', classname="col6"),
|
|
FieldPanel('to_address', classname="col6"),
|
|
]),
|
|
FieldPanel('subject'),
|
|
], "Email"),
|
|
]
|
|
|
|
def get_data_fields(self):
|
|
data_fields = [
|
|
('username', 'Username'),
|
|
]
|
|
data_fields += super().get_data_fields()
|
|
|
|
return data_fields
|
|
|
|
def get_submission_class(self):
|
|
return CustomFormSubmission
|
|
|
|
def process_form_submission(self, form):
|
|
return self.get_submission_class().objects.create(
|
|
form_data=form.cleaned_data,
|
|
page=self, user=form.user
|
|
)
|
|
|
|
|
|
class CustomFormSubmission(AbstractFormSubmission):
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
|
|
def get_data(self):
|
|
form_data = super().get_data()
|
|
form_data.update({
|
|
'username': self.user.username,
|
|
})
|
|
|
|
return form_data
|
|
```
|
|
|
|
## Check that a submission already exists for a user
|
|
|
|
If you want to prevent users from filling in a form more than once,
|
|
you need to override the `serve` method in your page model.
|
|
|
|
Example:
|
|
|
|
```python
|
|
import json
|
|
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from django.shortcuts import render
|
|
from modelcluster.fields import ParentalKey
|
|
from wagtail.admin.panels import (
|
|
FieldPanel, FieldRowPanel,
|
|
InlinePanel, MultiFieldPanel
|
|
)
|
|
from wagtail.fields import RichTextField
|
|
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission
|
|
|
|
|
|
class FormField(AbstractFormField):
|
|
page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
intro = RichTextField(blank=True)
|
|
thank_you_text = RichTextField(blank=True)
|
|
|
|
content_panels = AbstractEmailForm.content_panels + [
|
|
FieldPanel('intro'),
|
|
InlinePanel('form_fields', label="Form fields"),
|
|
FieldPanel('thank_you_text'),
|
|
MultiFieldPanel([
|
|
FieldRowPanel([
|
|
FieldPanel('from_address', classname="col6"),
|
|
FieldPanel('to_address', classname="col6"),
|
|
]),
|
|
FieldPanel('subject'),
|
|
], "Email"),
|
|
]
|
|
|
|
def serve(self, request, *args, **kwargs):
|
|
if self.get_submission_class().objects.filter(page=self, user__pk=request.user.pk).exists():
|
|
return render(
|
|
request,
|
|
self.template,
|
|
self.get_context(request)
|
|
)
|
|
|
|
return super().serve(request, *args, **kwargs)
|
|
|
|
def get_submission_class(self):
|
|
return CustomFormSubmission
|
|
|
|
def process_form_submission(self, form):
|
|
return self.get_submission_class().objects.create(
|
|
form_data=form.cleaned_data,
|
|
page=self, user=form.user
|
|
)
|
|
|
|
|
|
class CustomFormSubmission(AbstractFormSubmission):
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
|
|
class Meta:
|
|
unique_together = ('page', 'user')
|
|
```
|
|
|
|
Your template should look like this:
|
|
|
|
```html+django
|
|
{% load wagtailcore_tags %}
|
|
<html>
|
|
<head>
|
|
<title>{{ page.title }}</title>
|
|
</head>
|
|
<body>
|
|
<h1>{{ page.title }}</h1>
|
|
|
|
{% if user.is_authenticated and user.is_active or request.is_preview %}
|
|
{% if form %}
|
|
<div>{{ page.intro|richtext }}</div>
|
|
<form action="{% pageurl page %}" method="POST">
|
|
{% csrf_token %}
|
|
{{ form.as_p }}
|
|
<input type="submit">
|
|
</form>
|
|
{% else %}
|
|
<div>You can fill in the form only one time.</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div>To fill in the form, you must log in.</div>
|
|
{% endif %}
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
## Multi-step form
|
|
|
|
The following example shows how to create a multi-step form.
|
|
|
|
```python
|
|
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
|
from django.shortcuts import render
|
|
from modelcluster.fields import ParentalKey
|
|
from wagtail.admin.panels import (
|
|
FieldPanel, FieldRowPanel,
|
|
InlinePanel, MultiFieldPanel
|
|
)
|
|
from wagtail.fields import RichTextField
|
|
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
|
|
|
|
|
|
class FormField(AbstractFormField):
|
|
page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
intro = RichTextField(blank=True)
|
|
thank_you_text = RichTextField(blank=True)
|
|
|
|
content_panels = AbstractEmailForm.content_panels + [
|
|
FieldPanel('intro'),
|
|
InlinePanel('form_fields', label="Form fields"),
|
|
FieldPanel('thank_you_text'),
|
|
MultiFieldPanel([
|
|
FieldRowPanel([
|
|
FieldPanel('from_address', classname="col6"),
|
|
FieldPanel('to_address', classname="col6"),
|
|
]),
|
|
FieldPanel('subject'),
|
|
], "Email"),
|
|
]
|
|
|
|
def get_form_class_for_step(self, step):
|
|
return self.form_builder(step.object_list).get_form_class()
|
|
|
|
def serve(self, request, *args, **kwargs):
|
|
"""
|
|
Implements a simple multi-step form.
|
|
|
|
Stores each step into a session.
|
|
When the last step is submitted correctly, saves the whole form into a DB.
|
|
"""
|
|
|
|
session_key_data = 'form_data-%s' % self.pk
|
|
is_last_step = False
|
|
step_number = request.GET.get('p', 1)
|
|
|
|
paginator = Paginator(self.get_form_fields(), per_page=1)
|
|
try:
|
|
step = paginator.page(step_number)
|
|
except PageNotAnInteger:
|
|
step = paginator.page(1)
|
|
except EmptyPage:
|
|
step = paginator.page(paginator.num_pages)
|
|
is_last_step = True
|
|
|
|
if request.method == 'POST':
|
|
# The first step will be submitted with step_number == 2,
|
|
# so we need to get a form from the previous step
|
|
# Edge case - submission of the last step
|
|
prev_step = step if is_last_step else paginator.page(step.previous_page_number())
|
|
|
|
# Create a form only for submitted step
|
|
prev_form_class = self.get_form_class_for_step(prev_step)
|
|
prev_form = prev_form_class(request.POST, page=self, user=request.user)
|
|
if prev_form.is_valid():
|
|
# If data for step is valid, update the session
|
|
form_data = request.session.get(session_key_data, {})
|
|
form_data.update(prev_form.cleaned_data)
|
|
request.session[session_key_data] = form_data
|
|
|
|
if prev_step.has_next():
|
|
# Create a new form for a following step, if the following step is present
|
|
form_class = self.get_form_class_for_step(step)
|
|
form = form_class(page=self, user=request.user)
|
|
else:
|
|
# If there is no next step, create form for all fields
|
|
form = self.get_form(
|
|
request.session[session_key_data],
|
|
page=self, user=request.user
|
|
)
|
|
|
|
if form.is_valid():
|
|
# Perform validation again for whole form.
|
|
# After successful validation, save data into DB,
|
|
# and remove from the session.
|
|
form_submission = self.process_form_submission(form)
|
|
del request.session[session_key_data]
|
|
# render the landing page
|
|
return self.render_landing_page(request, form_submission, *args, **kwargs)
|
|
else:
|
|
# If data for step is invalid
|
|
# we will need to display form again with errors,
|
|
# so restore previous state.
|
|
form = prev_form
|
|
step = prev_step
|
|
else:
|
|
# Create empty form for non-POST requests
|
|
form_class = self.get_form_class_for_step(step)
|
|
form = form_class(page=self, user=request.user)
|
|
|
|
context = self.get_context(request)
|
|
context['form'] = form
|
|
context['fields_step'] = step
|
|
return render(
|
|
request,
|
|
self.template,
|
|
context
|
|
)
|
|
```
|
|
|
|
Your template for this form page should look like this:
|
|
|
|
```html+django
|
|
{% load wagtailcore_tags %}
|
|
<html>
|
|
<head>
|
|
<title>{{ page.title }}</title>
|
|
</head>
|
|
<body>
|
|
<h1>{{ page.title }}</h1>
|
|
|
|
<div>{{ page.intro|richtext }}</div>
|
|
<form action="{% pageurl page %}?p={{ fields_step.number|add:"1" }}" method="POST">
|
|
{% csrf_token %}
|
|
{{ form.as_p }}
|
|
<input type="submit">
|
|
</form>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
Note that the example shown before allows the user to return to a previous step,
|
|
or to open a second step without submitting the first step.
|
|
Depending on your requirements, you may need to add extra checks.
|
|
|
|
## Show results
|
|
|
|
If you are implementing polls or surveys, you may want to show results after submission.
|
|
The following example demonstrates how to do this.
|
|
|
|
First, you need to collect results as shown below:
|
|
|
|
```python
|
|
from modelcluster.fields import ParentalKey
|
|
from wagtail.admin.panels import (
|
|
FieldPanel, FieldRowPanel,
|
|
InlinePanel, MultiFieldPanel
|
|
)
|
|
from wagtail.fields import RichTextField
|
|
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
|
|
|
|
|
|
class FormField(AbstractFormField):
|
|
page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
intro = RichTextField(blank=True)
|
|
thank_you_text = RichTextField(blank=True)
|
|
|
|
content_panels = AbstractEmailForm.content_panels + [
|
|
FieldPanel('intro'),
|
|
InlinePanel('form_fields', label="Form fields"),
|
|
FieldPanel('thank_you_text'),
|
|
MultiFieldPanel([
|
|
FieldRowPanel([
|
|
FieldPanel('from_address', classname="col6"),
|
|
FieldPanel('to_address', classname="col6"),
|
|
]),
|
|
FieldPanel('subject'),
|
|
], "Email"),
|
|
]
|
|
|
|
def get_context(self, request, *args, **kwargs):
|
|
context = super().get_context(request, *args, **kwargs)
|
|
|
|
# If you need to show results only on landing page,
|
|
# You may need to check request.method
|
|
|
|
results = dict()
|
|
# Get information about form fields
|
|
data_fields = [
|
|
(field.clean_name, field.label)
|
|
for field in self.get_form_fields()
|
|
]
|
|
|
|
# Get all submissions for current page
|
|
submissions = self.get_submission_class().objects.filter(page=self)
|
|
for submission in submissions:
|
|
data = submission.get_data()
|
|
|
|
# Count results for each question
|
|
for name, label in data_fields:
|
|
answer = data.get(name)
|
|
if answer is None:
|
|
# Something wrong with data.
|
|
# Probably you have changed questions
|
|
# and now we are receiving answers for old questions.
|
|
# Just skip them.
|
|
continue
|
|
|
|
if type(answer) is list:
|
|
# Answer is a list if the field type is 'Checkboxes'
|
|
answer = u', '.join(answer)
|
|
|
|
question_stats = results.get(label, {})
|
|
question_stats[answer] = question_stats.get(answer, 0) + 1
|
|
results[label] = question_stats
|
|
|
|
context.update({
|
|
'results': results,
|
|
})
|
|
return context
|
|
```
|
|
|
|
Next, you need to transform your template to display the results:
|
|
|
|
```html+django
|
|
{% load wagtailcore_tags %}
|
|
<html>
|
|
<head>
|
|
<title>{{ page.title }}</title>
|
|
</head>
|
|
<body>
|
|
<h1>{{ page.title }}</h1>
|
|
|
|
<h2>Results</h2>
|
|
{% for question, answers in results.items %}
|
|
<h3>{{ question }}</h3>
|
|
{% for answer, count in answers.items %}
|
|
<div>{{ answer }}: {{ count }}</div>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
|
|
<div>{{ page.intro|richtext }}</div>
|
|
<form action="{% pageurl page %}" method="POST">
|
|
{% csrf_token %}
|
|
{{ form.as_p }}
|
|
<input type="submit">
|
|
</form>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
You can also show the results on the landing page.
|
|
|
|
(form_builder_custom_landing_page_redirect)=
|
|
|
|
## Custom landing page redirect
|
|
|
|
You can override the `render_landing_page` method on your `FormPage` to change what is rendered when a form submits.
|
|
|
|
In the example below we have added a `thank_you_page` field that enables custom redirects after a form submits to the selected page.
|
|
|
|
When overriding the `render_landing_page` method, we check if there is a linked `thank_you_page` and then redirect to it if it exists.
|
|
|
|
Finally, we add a URL param of `id` based on the `form_submission` if it exists.
|
|
|
|
```python
|
|
from django.shortcuts import redirect
|
|
from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel
|
|
from wagtail.contrib.forms.models import AbstractEmailForm
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
|
|
# intro, thank_you_text, ...
|
|
|
|
thank_you_page = models.ForeignKey(
|
|
'wagtailcore.Page',
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='+',
|
|
)
|
|
|
|
def render_landing_page(self, request, form_submission=None, *args, **kwargs):
|
|
if self.thank_you_page:
|
|
url = self.thank_you_page.url
|
|
# if a form_submission instance is available, append the id to URL
|
|
# when previewing landing page, there will not be a form_submission instance
|
|
if form_submission:
|
|
url += '?id=%s' % form_submission.id
|
|
return redirect(url, permanent=False)
|
|
# if no thank_you_page is set, render default landing page
|
|
return super().render_landing_page(request, form_submission, *args, **kwargs)
|
|
|
|
content_panels = AbstractEmailForm.content_panels + [
|
|
FieldPanel('intro'),
|
|
InlinePanel('form_fields'),
|
|
FieldPanel('thank_you_text'),
|
|
FieldPanel('thank_you_page'),
|
|
MultiFieldPanel([
|
|
FieldRowPanel([
|
|
FieldPanel('from_address', classname='col6'),
|
|
FieldPanel('to_address', classname='col6'),
|
|
]),
|
|
FieldPanel('subject'),
|
|
], 'Email'),
|
|
]
|
|
```
|
|
|
|
(custom_form_submission_listing)=
|
|
|
|
## Customize form submissions listing in Wagtail Admin
|
|
|
|
The Admin listing of form submissions can be customized by setting the attribute `submissions_list_view_class` on your FormPage model.
|
|
|
|
The list view class must be a subclass of `SubmissionsListView` from `wagtail.contrib.forms.views`, which is a subclass of `wagtail.admin.views.generic.base.BaseListingView` and Django's class based {class}`~django.views.generic.list.ListView`.
|
|
|
|
Example:
|
|
|
|
```python
|
|
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
|
|
from wagtail.contrib.forms.views import SubmissionsListView
|
|
|
|
|
|
class CustomSubmissionsListView(SubmissionsListView):
|
|
paginate_by = 50 # show more submissions per page, default is 20
|
|
default_ordering = ('submit_time',) # order submissions by oldest first, normally newest first
|
|
ordering_csv = ('-submit_time',) # order csv export by newest first, normally oldest first
|
|
|
|
# override the method to generate csv filename
|
|
def get_csv_filename(self):
|
|
""" Returns the filename for CSV file with page slug at start"""
|
|
filename = super().get_csv_filename()
|
|
return self.form_page.slug + '-' + filename
|
|
|
|
|
|
class FormField(AbstractFormField):
|
|
page = ParentalKey('FormPage', related_name='form_fields')
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
"""Form Page with customized submissions listing view"""
|
|
|
|
# set custom view class as class attribute
|
|
submissions_list_view_class = CustomSubmissionsListView
|
|
|
|
intro = RichTextField(blank=True)
|
|
thank_you_text = RichTextField(blank=True)
|
|
|
|
# content_panels = ...
|
|
```
|
|
|
|
## Adding a custom field type
|
|
|
|
First, make the new field type available in the page editor by changing your `FormField` model.
|
|
|
|
- Create a new set of choices which includes the original `FORM_FIELD_CHOICES` along with new field types you want to make available.
|
|
- Each choice must contain a unique key and a human-readable name of the field, for example `('slug', 'URL Slug')`
|
|
- Override the `field_type` field in your `FormField` model with `choices` attribute using these choices.
|
|
- You will need to run `./manage.py makemigrations` and `./manage.py migrate` after this step.
|
|
|
|
Then, create and use a new form builder class.
|
|
|
|
- Define a new form builder class that extends the `FormBuilder` class.
|
|
- Add a method that will return a created Django form field for the new field type.
|
|
- Its name must be in the format: `create_<field_type_key>_field`, for example `create_slug_field`
|
|
- Override the `form_builder` attribute in your form page model to use your new form builder class.
|
|
|
|
Example:
|
|
|
|
```python
|
|
from django import forms
|
|
from django.db import models
|
|
from modelcluster.fields import ParentalKey
|
|
from wagtail.contrib.forms.forms import FormBuilder
|
|
from wagtail.contrib.forms.models import (
|
|
AbstractEmailForm, AbstractFormField, FORM_FIELD_CHOICES)
|
|
|
|
|
|
class FormField(AbstractFormField):
|
|
# extend the built-in field type choices
|
|
# our field type key will be 'ipaddress'
|
|
CHOICES = FORM_FIELD_CHOICES + (('ipaddress', 'IP Address'),)
|
|
|
|
page = ParentalKey('FormPage', related_name='form_fields')
|
|
# override the field_type field with extended choices
|
|
field_type = models.CharField(
|
|
verbose_name='field type',
|
|
max_length=16,
|
|
# use the choices tuple defined above
|
|
choices=CHOICES
|
|
)
|
|
|
|
|
|
class CustomFormBuilder(FormBuilder):
|
|
# create a function that returns an instanced Django form field
|
|
# function name must match create_<field_type_key>_field
|
|
def create_ipaddress_field(self, field, options):
|
|
# return `forms.GenericIPAddressField(**options)` not `forms.SlugField`
|
|
# returns created a form field with the options passed in
|
|
return forms.GenericIPAddressField(**options)
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
# intro, thank_you_text, edit_handlers, etc...
|
|
|
|
# use custom form builder defined above
|
|
form_builder = CustomFormBuilder
|
|
```
|
|
|
|
(form_builder_render_email)=
|
|
|
|
## Custom `render_email` method
|
|
|
|
If you want to change the content of the email that is sent when a form submits you can override the `render_email` method.
|
|
|
|
To do this, you need to:
|
|
|
|
- Ensure you have your form model defined that extends `wagtail.contrib.forms.models.AbstractEmailForm`.
|
|
- Override the `render_email` method in your page model.
|
|
|
|
Example:
|
|
|
|
```python
|
|
from datetime import date
|
|
# ... additional wagtail imports
|
|
from wagtail.contrib.forms.models import AbstractEmailForm
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
# ... fields, content_panels, etc
|
|
|
|
def render_email(self, form):
|
|
# Get the original content (string)
|
|
email_content = super().render_email(form)
|
|
|
|
# Add a title (not part of the original method)
|
|
title = '{}: {}'.format('Form', self.title)
|
|
|
|
content = [title, '', email_content, '']
|
|
|
|
# Add a link to the form page
|
|
content.append('{}: {}'.format('Submitted Via', self.full_url))
|
|
|
|
# Add the date the form was submitted
|
|
submitted_date_str = date.today().strftime('%x')
|
|
content.append('{}: {}'.format('Submitted on', submitted_date_str))
|
|
|
|
# Content is joined with a new line to separate each text line
|
|
content = '\n'.join(content)
|
|
|
|
return content
|
|
```
|
|
|
|
## Custom `send_mail` method
|
|
|
|
If you want to change the subject or some other part of how an email is sent when a form submits you can override the `send_mail` method.
|
|
|
|
To do this, you need to:
|
|
|
|
- Ensure you have your form model defined that extends `wagtail.contrib.forms.models.AbstractEmailForm`.
|
|
- In your models.py file, import the `wagtail.admin.mail.send_mail` function.
|
|
- Override the `send_mail` method in your page model.
|
|
|
|
Example:
|
|
|
|
```python
|
|
from datetime import date
|
|
# ... additional wagtail imports
|
|
from wagtail.admin.mail import send_mail
|
|
from wagtail.contrib.forms.models import AbstractEmailForm
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
# ... fields, content_panels, etc
|
|
|
|
def send_mail(self, form):
|
|
# `self` is the FormPage, `form` is the form's POST data on submit
|
|
|
|
# Email addresses are parsed from the FormPage's addresses field
|
|
addresses = [x.strip() for x in self.to_address.split(',')]
|
|
|
|
# Subject can be adjusted (adding submitted date), be sure to include the form's defined subject field
|
|
submitted_date_str = date.today().strftime('%x')
|
|
subject = f"{self.subject} - {submitted_date_str}"
|
|
|
|
send_mail(subject, self.render_email(form), addresses, self.from_address,)
|
|
```
|
|
|
|
## Custom `clean_name` generation
|
|
|
|
- Each time a new `FormField` is added a `clean_name` also gets generated based on the user-entered `label`.
|
|
- `AbstractFormField` has a method `get_field_clean_name` to convert the label into an HTML-valid `lower_snake_case` ASCII string using the [AnyAscii](https://pypi.org/project/anyascii/) library which can be overridden to generate a custom conversion.
|
|
- The resolved `clean_name` is also used as the form field name in rendered HTML forms.
|
|
- Ensure that any conversion will be unique enough to not create conflicts within your `FormPage` instance.
|
|
- This method gets called on the creation of new fields only and as such will not have access to its own `Page` or `pk`. This does not get called when labels are edited as modifying the `clean_name` after any form responses are submitted will mean those field responses will not be retrieved.
|
|
- This method gets called for form previews and also validation of duplicate labels.
|
|
|
|
```python
|
|
import uuid
|
|
|
|
from django.db import models
|
|
from modelcluster.fields import ParentalKey
|
|
|
|
# ... other field and edit_handler imports
|
|
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
|
|
|
|
|
|
class FormField(AbstractFormField):
|
|
page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')
|
|
|
|
def get_field_clean_name(self):
|
|
clean_name = super().get_field_clean_name()
|
|
id = str(uuid.uuid4())[:8] # short uuid
|
|
return f"{id}_{clean_name}"
|
|
|
|
|
|
class FormPage(AbstractEmailForm):
|
|
# ... page definitions
|
|
```
|
|
|
|
(form_builder_mixins)=
|
|
|
|
## Using `FormMixin` or `EmailFormMixin` to use with other `Page` subclasses
|
|
|
|
If you need to add form behavior while extending an additional class, you can use the base mixins instead of the abstract models.
|
|
|
|
```python
|
|
from wagtail.models import Page
|
|
from wagtail.contrib.forms.models import EmailFormMixin, FormMixin
|
|
|
|
|
|
class BasePage(Page):
|
|
"""
|
|
A shared base page used throughout the project.
|
|
"""
|
|
|
|
# ...
|
|
|
|
class FormPage(FormMixin, BasePage):
|
|
intro = RichTextField(blank=True)
|
|
# ...
|
|
|
|
class EmailFormPage(EmailFormMixin, FormMixin, BasePage):
|
|
intro = RichTextField(blank=True)
|
|
# ...
|
|
|
|
```
|
|
|
|
(form_builder_custom_admin_validation)=
|
|
|
|
## Custom validation for admin form pages
|
|
|
|
By default, pages that inherit from `FormMixin` will validate that each field added by an editor has a unique `clean_name`.
|
|
|
|
If you need to add custom validation, create a subclass of `WagtailAdminFormPageForm` and add your own `clean` definition and set the `base_form_class` on your `Page` model.
|
|
|
|
```{note}
|
|
Validation only applies when editors use the form builder to add fields in the Wagtail admin,
|
|
not when the form is submitted by end users.
|
|
```
|
|
|
|
```python
|
|
from wagtail.models import Page
|
|
from wagtail.contrib.forms.models import FormMixin, WagtailAdminFormPageForm
|
|
|
|
|
|
class CustomWagtailAdminFormPageForm(WagtailAdminFormPageForm):
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
# Insert custom validation here, see `WagtailAdminFormPageForm.clean` for an example
|
|
return cleaned_data
|
|
|
|
|
|
class FormPage(AbstractForm):
|
|
base_form_class = CustomWagtailAdminFormPageForm
|
|
```
|