kopia lustrzana https://github.com/wagtail/wagtail
rodzic
4ea06426ab
commit
7f10938b57
|
@ -0,0 +1,554 @@
|
|||
Form builder customisation
|
||||
==========================
|
||||
|
||||
For basic usage example see :ref:`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.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel,
|
||||
InlinePanel, MultiFieldPanel
|
||||
)
|
||||
from wagtail.wagtailcore.fields import RichTextField
|
||||
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
|
||||
|
||||
|
||||
class FormField(AbstractFormField):
|
||||
page = ParentalKey('FormPage', 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', classname="full"),
|
||||
InlinePanel('custom_form_fields', label="Form fields"),
|
||||
FieldPanel('thank_you_text', classname="full"),
|
||||
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.wagtailforms.models.AbstractFormSubmission``.
|
||||
* Override the ``get_submission_class`` and ``process_form_submission`` methods in your page model.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel,
|
||||
InlinePanel, MultiFieldPanel
|
||||
)
|
||||
from wagtail.wagtailcore.fields import RichTextField
|
||||
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission
|
||||
|
||||
|
||||
class FormField(AbstractFormField):
|
||||
page = ParentalKey('FormPage', related_name='form_fields')
|
||||
|
||||
|
||||
class FormPage(AbstractEmailForm):
|
||||
intro = RichTextField(blank=True)
|
||||
thank_you_text = RichTextField(blank=True)
|
||||
|
||||
content_panels = AbstractEmailForm.content_panels + [
|
||||
FieldPanel('intro', classname="full"),
|
||||
InlinePanel('form_fields', label="Form fields"),
|
||||
FieldPanel('thank_you_text', classname="full"),
|
||||
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):
|
||||
self.get_submission_class().objects.create(
|
||||
form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
|
||||
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 following example shows how to add a username to the CSV export:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel,
|
||||
InlinePanel, MultiFieldPanel
|
||||
)
|
||||
from wagtail.wagtailcore.fields import RichTextField
|
||||
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission
|
||||
|
||||
|
||||
class FormField(AbstractFormField):
|
||||
page = ParentalKey('FormPage', related_name='form_fields')
|
||||
|
||||
|
||||
class FormPage(AbstractEmailForm):
|
||||
intro = RichTextField(blank=True)
|
||||
thank_you_text = RichTextField(blank=True)
|
||||
|
||||
content_panels = AbstractEmailForm.content_panels + [
|
||||
FieldPanel('intro', classname="full"),
|
||||
InlinePanel('form_fields', label="Form fields"),
|
||||
FieldPanel('thank_you_text', classname="full"),
|
||||
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(FormPage, self).get_data_fields()
|
||||
|
||||
return data_fields
|
||||
|
||||
def get_submission_class(self):
|
||||
return CustomFormSubmission
|
||||
|
||||
def process_form_submission(self, form):
|
||||
self.get_submission_class().objects.create(
|
||||
form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
|
||||
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(CustomFormSubmission, self).get_data()
|
||||
form_data.update({
|
||||
'username': self.user.username,
|
||||
})
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
Note that this code also changes the submissions list view.
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from django.shortcuts import render
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel,
|
||||
InlinePanel, MultiFieldPanel
|
||||
)
|
||||
from wagtail.wagtailcore.fields import RichTextField
|
||||
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission
|
||||
|
||||
|
||||
class FormField(AbstractFormField):
|
||||
page = ParentalKey('FormPage', related_name='form_fields')
|
||||
|
||||
|
||||
class FormPage(AbstractEmailForm):
|
||||
intro = RichTextField(blank=True)
|
||||
thank_you_text = RichTextField(blank=True)
|
||||
|
||||
content_panels = AbstractEmailForm.content_panels + [
|
||||
FieldPanel('intro', classname="full"),
|
||||
InlinePanel('form_fields', label="Form fields"),
|
||||
FieldPanel('thank_you_text', classname="full"),
|
||||
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(FormPage, self).serve(request, *args, **kwargs)
|
||||
|
||||
def get_submission_class(self):
|
||||
return CustomFormSubmission
|
||||
|
||||
def process_form_submission(self, form):
|
||||
self.get_submission_class().objects.create(
|
||||
form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
|
||||
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:
|
||||
|
||||
.. code-block:: 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 from only one time.</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div>To fill in the form, you must to log in.</div>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Multi-step form
|
||||
---------------
|
||||
|
||||
The following example shows how to create a multi-step form.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||
from django.shortcuts import render
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel,
|
||||
InlinePanel, MultiFieldPanel
|
||||
)
|
||||
from wagtail.wagtailcore.fields import RichTextField
|
||||
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
|
||||
|
||||
|
||||
class FormField(AbstractFormField):
|
||||
page = ParentalKey('FormPage', related_name='form_fields')
|
||||
|
||||
|
||||
class FormPage(AbstractEmailForm):
|
||||
intro = RichTextField(blank=True)
|
||||
thank_you_text = RichTextField(blank=True)
|
||||
|
||||
content_panels = AbstractEmailForm.content_panels + [
|
||||
FieldPanel('intro', classname="full"),
|
||||
InlinePanel('form_fields', label="Form fields"),
|
||||
FieldPanel('thank_you_text', classname="full"),
|
||||
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 simple a multi-step form.
|
||||
|
||||
Stores each step into a session.
|
||||
When the last step was submitted correctly, saves 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 from from 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 more steps, 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.
|
||||
self.process_form_submission(form)
|
||||
del request.session[session_key_data]
|
||||
|
||||
# Render the landing page
|
||||
return render(
|
||||
request,
|
||||
self.landing_page_template,
|
||||
self.get_context(request)
|
||||
)
|
||||
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:
|
||||
|
||||
.. code-block:: 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 are implementing polls or surveys, you may want to show results after submission. The following example demonstrates how to do this.
|
||||
|
||||
At first, you need to collect results as shown below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel,
|
||||
InlinePanel, MultiFieldPanel
|
||||
)
|
||||
from wagtail.wagtailcore.fields import RichTextField
|
||||
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
|
||||
|
||||
|
||||
class FormField(AbstractFormField):
|
||||
page = ParentalKey('FormPage', related_name='form_fields')
|
||||
|
||||
|
||||
class FormPage(AbstractEmailForm):
|
||||
intro = RichTextField(blank=True)
|
||||
thank_you_text = RichTextField(blank=True)
|
||||
|
||||
content_panels = AbstractEmailForm.content_panels + [
|
||||
FieldPanel('intro', classname="full"),
|
||||
InlinePanel('form_fields', label="Form fields"),
|
||||
FieldPanel('thank_you_text', classname="full"),
|
||||
MultiFieldPanel([
|
||||
FieldRowPanel([
|
||||
FieldPanel('from_address', classname="col6"),
|
||||
FieldPanel('to_address', classname="col6"),
|
||||
]),
|
||||
FieldPanel('subject'),
|
||||
], "Email"),
|
||||
]
|
||||
|
||||
def get_context(self, request, *args, **kwargs):
|
||||
context = super(FormPage, self).get_context(request, *args, **kwargs)
|
||||
|
||||
# If you need to show results only on landing page,
|
||||
# you may need 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:
|
||||
|
||||
.. code-block:: 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.
|
|
@ -9,6 +9,8 @@ The ``wagtailforms`` module allows you to set up single-page forms, such as a 'C
|
|||
.. note::
|
||||
**wagtailforms is not a replacement for** `Django's form support <https://docs.djangoproject.com/en/1.10/topics/forms/>`_. It is designed as a way for page authors to build general-purpose data collection forms without having to write code. If you intend to build a form that assigns specific behaviour to individual fields (such as creating user accounts), or needs a custom HTML layout, you will almost certainly be better served by a standard Django form, where the fields are fixed in code rather than defined on-the-fly by a page author. See the `wagtail-form-example project <https://github.com/gasman/wagtail-form-example/commits/master>`_ for an example of integrating a Django form into a Wagtail page.
|
||||
|
||||
.. _form_builder_usage:
|
||||
|
||||
Usage
|
||||
~~~~~
|
||||
|
||||
|
@ -27,14 +29,18 @@ Within the ``models.py`` of one of your apps, create a model that extends ``wagt
|
|||
.. code-block:: python
|
||||
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.wagtailadmin.edit_handlers import (FieldPanel, FieldRowPanel,
|
||||
InlinePanel, MultiFieldPanel)
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel,
|
||||
InlinePanel, MultiFieldPanel
|
||||
)
|
||||
from wagtail.wagtailcore.fields import RichTextField
|
||||
from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
|
||||
|
||||
|
||||
class FormField(AbstractFormField):
|
||||
page = ParentalKey('FormPage', related_name='form_fields')
|
||||
|
||||
|
||||
class FormPage(AbstractEmailForm):
|
||||
intro = RichTextField(blank=True)
|
||||
thank_you_text = RichTextField(blank=True)
|
||||
|
@ -98,3 +104,12 @@ Displaying form submission information
|
|||
FieldPanel('intro', classname="full"),
|
||||
# ...
|
||||
]
|
||||
|
||||
|
||||
Index
|
||||
~~~~~
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
customisation
|
|
@ -8,7 +8,7 @@ Wagtail ships with a variety of extra optional modules.
|
|||
:maxdepth: 2
|
||||
|
||||
settings
|
||||
forms
|
||||
forms/index
|
||||
staticsitegen
|
||||
sitemaps
|
||||
frontendcache
|
||||
|
@ -25,8 +25,8 @@ Wagtail ships with a variety of extra optional modules.
|
|||
Site-wide settings that are editable by administrators in the Wagtail admin.
|
||||
|
||||
|
||||
:doc:`forms`
|
||||
------------
|
||||
:doc:`forms/index`
|
||||
------------------
|
||||
|
||||
Allows forms to be created by admins and provides an interface for browsing form submissions.
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue