Настройка конструктора форм
Для примера базового использования см. использование конструктора форм.
Настройка модели отправки формы
Если вам нужно сохранить дополнительные данные, вы можете использовать пользовательскую модель отправки формы. Для этого нужно:
- Определить модель, которая расширяет
wagtail.contrib.forms.models.AbstractFormSubmission. - Переопределить методы
get_submission_classиprocess_form_submissionв вашей модели страницы.
Пример:
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):
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)
Добавление пользовательских данных в экспорт CSV
Если вы хотите добавить пользовательские данные в экспорт CSV, вам нужно:
- Переопределить метод
get_data_fieldsв модели страницы. - Переопределить
get_dataв модели отправки.
В примере ниже показано, как добавить имя пользователя в экспорт CSV. Обратите внимание, что этот код также изменяет представление списка отправленных данных.
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):
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
Проверка существования отправки для пользователя
Если вы хотите предотвратить заполнение формы пользователем более одного раза, вам нужно переопределить метод serve в вашей модели страницы.
Пример:
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):
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')
Ваш шаблон должен выглядеть так:
{% 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>
Многоэтапная форма
Следующий пример демонстрирует создание многоэтапной формы.
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 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 form 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 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
)
Ваш шаблон для этой страницы формы должен выглядеть так:
{% 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>
Обратите внимание, что показанный пример позволяет пользователю вернуться к предыдущему шагу или открыть второй шаг без отправки первого. В зависимости от ваших потребностей, вам может потребоваться добавить дополнительные проверки.
Отображение результатов
Если вы реализуете опросы или анкеты, вы можете отобразить результаты после отправки. Следующий пример демонстрирует, как это сделать.
Сначала вам нужно собрать результаты, как показано ниже:
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 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
Далее, вам нужно преобразовать ваш шаблон для отображения результатов:
{% 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>
Вы также можете отобразить результаты на стартовой странице.
Настройка перенаправления на целевую страницу
Вы можете переопределить метод render_landing_page в вашей FormPage, чтобы изменить вывод при отправке формы.
В этом примере мы добавили поле thank_you_page, которое позволяет настраивать перенаправления после отправки формы на выбранную страницу.
При переопределении метода render_landing_page мы проверяем, есть ли связанная thank_you_page, и затем перенаправляем на неё, если она существует.
Наконец, мы добавляем параметр URL id на основе form_submission, если он существует.
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'),
]
Настройка списка отправленных данных в Wagtail Admin
Список отправленных данных в админке Wagtail можно настроить, установив атрибут submissions_list_view_class в вашей модели FormPage.
Класс представления списка должен быть подклассом SubmissionsListView из wagtail.contrib.forms.views, который является подклассом базированного на классах представления Django ListView.
Пример:
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
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 customised 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 = ...
Добавление пользовательского типа поля
Сначала сделайте новый тип поля доступным в редакторе страницы, изменив свою модель FormField.
- Создайте новый набор вариантов, который включает исходный
FORM_FIELD_CHOICESвместе с новыми типами полей, которые вы хотите сделать доступными. - Каждый вариант должен содержать уникальный ключ и удобочитаемое имя поля, например
('slug', 'URL Slug') - Переопределите поле
field_typeв своей моделиFormFieldс атрибутомchoices, используя эти варианты. - Вам нужно будет выполнить
./manage.py makemigrationsи./manage.py migrateпосле этого шага.
Затем создайте и используйте новый класс конструктора форм.
- Определите новый класс конструктора форм, который расширяет класс
FormBuilder. - Добавьте метод, который вернёт созданное поле Django для нового типа поля.
- Его имя должно быть в формате:
create_<field_type_key>_field, напримерcreate_slug_field - Переопределите атрибут
form_builderв вашей модели страницы для использования вашего нового класса конструктора форм.
Пример:
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
Настройка метода render_email
Если вы хотите изменить содержимое письма, которое отправляется при отправке формы, вы можете переопределить метод render_email.
Для этого нужно:
- Убедитесь, что у вас определена модель формы, которая расширяет
wagtail.contrib.forms.models.AbstractEmailForm. - Переопределите метод
render_emailв вашей модели страницы.
Пример:
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 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
Настройка метода send_mail
Если вы хотите изменить тему или другую часть того, как отправляется электронное письмо при отправке формы, вы можете переопределить метод send_mail.
Для этого нужно:
- Убедитесь, что у вас определена модель формы, которая расширяет
wagtail.contrib.forms.models.AbstractEmailForm. - В файле models.py импортируйте функцию
wagtail.admin.mail.send_mail. - Переопределите метод
send_mailв вашей модели страницы.
Пример:
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,)
Настройка генерации clean_name
- Каждый раз, когда добавляется новая
FormField, генерируется такжеclean_name, основанная на введённом пользователемlabel. -
AbstractFormFieldимеет методget_field_clean_nameдля преобразования метки в допустимую HTML строкуlower_snake_caseASCII с использованием библиотеки AnyAscii, который можно переопределить для генерации пользовательского преобразования. - Полученное значение
clean_nameтакже используется в качестве имени поля формы в рендерном HTML. - Убедитесь, что любое преобразование достаточно уникально, чтобы не создавать конфликтов в вашей
FormPageэкземпляре. - Этот метод вызывается только при создании новых полей и, таким образом, не имеет доступа к собственным
Pageилиpk. Он не вызывается при редактировании меток, так как изменениеclean_nameпосле отправки любых ответов на форму означает, что эти ответы на поля не будут получены. - Этот метод вызывается для предварительного просмотра формы и также для проверки наличия дублирующих меток.
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
Использование FormMixin или EmailFormMixin для использования с другими подклассами Page
Если вам нужно добавить поведение формы, расширяя дополнительный класс, вы можете использовать базовые миксины вместо абстрактных моделей.
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)
# ...
© 2014-present Torchbox Ltd and individual contributors.
All rights are reserved.
Licensed under the BSD License.
https://docs.wagtail.org/en/stable/reference/contrib/forms/customisation.html