customisation.rst 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. Form builder customisation
  2. ==========================
  3. For a basic usage example see :ref:`form_builder_usage`.
  4. Custom ``related_name`` for form fields
  5. ---------------------------------------
  6. If you want to change ``related_name`` for form fields
  7. (by default ``AbstractForm`` and ``AbstractEmailForm`` expect ``form_fields`` to be defined),
  8. you will need to override the ``get_form_fields`` method.
  9. You can do this as shown below.
  10. .. code-block:: python
  11. from modelcluster.fields import ParentalKey
  12. from wagtail.wagtailadmin.edit_handlers import (
  13. FieldPanel, FieldRowPanel,
  14. InlinePanel, MultiFieldPanel
  15. )
  16. from wagtail.wagtailcore.fields import RichTextField
  17. from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
  18. class FormField(AbstractFormField):
  19. page = ParentalKey('FormPage', related_name='custom_form_fields')
  20. class FormPage(AbstractEmailForm):
  21. intro = RichTextField(blank=True)
  22. thank_you_text = RichTextField(blank=True)
  23. content_panels = AbstractEmailForm.content_panels + [
  24. FieldPanel('intro', classname="full"),
  25. InlinePanel('custom_form_fields', label="Form fields"),
  26. FieldPanel('thank_you_text', classname="full"),
  27. MultiFieldPanel([
  28. FieldRowPanel([
  29. FieldPanel('from_address', classname="col6"),
  30. FieldPanel('to_address', classname="col6"),
  31. ]),
  32. FieldPanel('subject'),
  33. ], "Email"),
  34. ]
  35. def get_form_fields(self):
  36. return self.custom_form_fields.all()
  37. Custom form submission model
  38. ----------------------------
  39. If you need to save additional data, you can use a custom form submission model.
  40. To do this, you need to:
  41. * Define a model that extends ``wagtail.wagtailforms.models.AbstractFormSubmission``.
  42. * Override the ``get_submission_class`` and ``process_form_submission`` methods in your page model.
  43. Example:
  44. .. code-block:: python
  45. import json
  46. from django.conf import settings
  47. from django.core.serializers.json import DjangoJSONEncoder
  48. from django.db import models
  49. from modelcluster.fields import ParentalKey
  50. from wagtail.wagtailadmin.edit_handlers import (
  51. FieldPanel, FieldRowPanel,
  52. InlinePanel, MultiFieldPanel
  53. )
  54. from wagtail.wagtailcore.fields import RichTextField
  55. from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission
  56. class FormField(AbstractFormField):
  57. page = ParentalKey('FormPage', related_name='form_fields')
  58. class FormPage(AbstractEmailForm):
  59. intro = RichTextField(blank=True)
  60. thank_you_text = RichTextField(blank=True)
  61. content_panels = AbstractEmailForm.content_panels + [
  62. FieldPanel('intro', classname="full"),
  63. InlinePanel('form_fields', label="Form fields"),
  64. FieldPanel('thank_you_text', classname="full"),
  65. MultiFieldPanel([
  66. FieldRowPanel([
  67. FieldPanel('from_address', classname="col6"),
  68. FieldPanel('to_address', classname="col6"),
  69. ]),
  70. FieldPanel('subject'),
  71. ], "Email"),
  72. ]
  73. def get_submission_class(self):
  74. return CustomFormSubmission
  75. def process_form_submission(self, form):
  76. self.get_submission_class().objects.create(
  77. form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
  78. page=self, user=form.user
  79. )
  80. class CustomFormSubmission(AbstractFormSubmission):
  81. user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
  82. Add custom data to CSV export
  83. -----------------------------
  84. If you want to add custom data to the CSV export, you will need to:
  85. * Override the ``get_data_fields`` method in page model.
  86. * Override ``get_data`` in the submission model.
  87. The following example shows how to add a username to the CSV export:
  88. .. code-block:: python
  89. import json
  90. from django.conf import settings
  91. from django.core.serializers.json import DjangoJSONEncoder
  92. from django.db import models
  93. from modelcluster.fields import ParentalKey
  94. from wagtail.wagtailadmin.edit_handlers import (
  95. FieldPanel, FieldRowPanel,
  96. InlinePanel, MultiFieldPanel
  97. )
  98. from wagtail.wagtailcore.fields import RichTextField
  99. from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission
  100. class FormField(AbstractFormField):
  101. page = ParentalKey('FormPage', related_name='form_fields')
  102. class FormPage(AbstractEmailForm):
  103. intro = RichTextField(blank=True)
  104. thank_you_text = RichTextField(blank=True)
  105. content_panels = AbstractEmailForm.content_panels + [
  106. FieldPanel('intro', classname="full"),
  107. InlinePanel('form_fields', label="Form fields"),
  108. FieldPanel('thank_you_text', classname="full"),
  109. MultiFieldPanel([
  110. FieldRowPanel([
  111. FieldPanel('from_address', classname="col6"),
  112. FieldPanel('to_address', classname="col6"),
  113. ]),
  114. FieldPanel('subject'),
  115. ], "Email"),
  116. ]
  117. def get_data_fields(self):
  118. data_fields = [
  119. ('username', 'Username'),
  120. ]
  121. data_fields += super(FormPage, self).get_data_fields()
  122. return data_fields
  123. def get_submission_class(self):
  124. return CustomFormSubmission
  125. def process_form_submission(self, form):
  126. self.get_submission_class().objects.create(
  127. form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
  128. page=self, user=form.user
  129. )
  130. class CustomFormSubmission(AbstractFormSubmission):
  131. user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
  132. def get_data(self):
  133. form_data = super(CustomFormSubmission, self).get_data()
  134. form_data.update({
  135. 'username': self.user.username,
  136. })
  137. return form_data
  138. Note that this code also changes the submissions list view.
  139. Check that a submission already exists for a user
  140. -------------------------------------------------
  141. If you want to prevent users from filling in a form more than once,
  142. you need to override the ``serve`` method in your page model.
  143. Example:
  144. .. code-block:: python
  145. import json
  146. from django.conf import settings
  147. from django.core.serializers.json import DjangoJSONEncoder
  148. from django.db import models
  149. from django.shortcuts import render
  150. from modelcluster.fields import ParentalKey
  151. from wagtail.wagtailadmin.edit_handlers import (
  152. FieldPanel, FieldRowPanel,
  153. InlinePanel, MultiFieldPanel
  154. )
  155. from wagtail.wagtailcore.fields import RichTextField
  156. from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission
  157. class FormField(AbstractFormField):
  158. page = ParentalKey('FormPage', related_name='form_fields')
  159. class FormPage(AbstractEmailForm):
  160. intro = RichTextField(blank=True)
  161. thank_you_text = RichTextField(blank=True)
  162. content_panels = AbstractEmailForm.content_panels + [
  163. FieldPanel('intro', classname="full"),
  164. InlinePanel('form_fields', label="Form fields"),
  165. FieldPanel('thank_you_text', classname="full"),
  166. MultiFieldPanel([
  167. FieldRowPanel([
  168. FieldPanel('from_address', classname="col6"),
  169. FieldPanel('to_address', classname="col6"),
  170. ]),
  171. FieldPanel('subject'),
  172. ], "Email"),
  173. ]
  174. def serve(self, request, *args, **kwargs):
  175. if self.get_submission_class().objects.filter(page=self, user__pk=request.user.pk).exists():
  176. return render(
  177. request,
  178. self.template,
  179. self.get_context(request)
  180. )
  181. return super(FormPage, self).serve(request, *args, **kwargs)
  182. def get_submission_class(self):
  183. return CustomFormSubmission
  184. def process_form_submission(self, form):
  185. self.get_submission_class().objects.create(
  186. form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
  187. page=self, user=form.user
  188. )
  189. class CustomFormSubmission(AbstractFormSubmission):
  190. user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
  191. class Meta:
  192. unique_together = ('page', 'user')
  193. Your template should look like this:
  194. .. code-block:: django
  195. {% load wagtailcore_tags %}
  196. <html>
  197. <head>
  198. <title>{{ page.title }}</title>
  199. </head>
  200. <body>
  201. <h1>{{ page.title }}</h1>
  202. {% if user.is_authenticated and user.is_active or request.is_preview %}
  203. {% if form %}
  204. <div>{{ page.intro|richtext }}</div>
  205. <form action="{% pageurl page %}" method="POST">
  206. {% csrf_token %}
  207. {{ form.as_p }}
  208. <input type="submit">
  209. </form>
  210. {% else %}
  211. <div>You can fill in the from only one time.</div>
  212. {% endif %}
  213. {% else %}
  214. <div>To fill in the form, you must to log in.</div>
  215. {% endif %}
  216. </body>
  217. </html>
  218. Multi-step form
  219. ---------------
  220. The following example shows how to create a multi-step form.
  221. .. code-block:: python
  222. from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
  223. from django.shortcuts import render
  224. from modelcluster.fields import ParentalKey
  225. from wagtail.wagtailadmin.edit_handlers import (
  226. FieldPanel, FieldRowPanel,
  227. InlinePanel, MultiFieldPanel
  228. )
  229. from wagtail.wagtailcore.fields import RichTextField
  230. from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
  231. class FormField(AbstractFormField):
  232. page = ParentalKey('FormPage', related_name='form_fields')
  233. class FormPage(AbstractEmailForm):
  234. intro = RichTextField(blank=True)
  235. thank_you_text = RichTextField(blank=True)
  236. content_panels = AbstractEmailForm.content_panels + [
  237. FieldPanel('intro', classname="full"),
  238. InlinePanel('form_fields', label="Form fields"),
  239. FieldPanel('thank_you_text', classname="full"),
  240. MultiFieldPanel([
  241. FieldRowPanel([
  242. FieldPanel('from_address', classname="col6"),
  243. FieldPanel('to_address', classname="col6"),
  244. ]),
  245. FieldPanel('subject'),
  246. ], "Email"),
  247. ]
  248. def get_form_class_for_step(self, step):
  249. return self.form_builder(step.object_list).get_form_class()
  250. def serve(self, request, *args, **kwargs):
  251. """
  252. Implements a simple multi-step form.
  253. Stores each step into a session.
  254. When the last step was submitted correctly, saves whole form into a DB.
  255. """
  256. session_key_data = 'form_data-%s' % self.pk
  257. is_last_step = False
  258. step_number = request.GET.get('p', 1)
  259. paginator = Paginator(self.get_form_fields(), per_page=1)
  260. try:
  261. step = paginator.page(step_number)
  262. except PageNotAnInteger:
  263. step = paginator.page(1)
  264. except EmptyPage:
  265. step = paginator.page(paginator.num_pages)
  266. is_last_step = True
  267. if request.method == 'POST':
  268. # The first step will be submitted with step_number == 2,
  269. # so we need to get a form from previous step
  270. # Edge case - submission of the last step
  271. prev_step = step if is_last_step else paginator.page(step.previous_page_number())
  272. # Create a form only for submitted step
  273. prev_form_class = self.get_form_class_for_step(prev_step)
  274. prev_form = prev_form_class(request.POST, page=self, user=request.user)
  275. if prev_form.is_valid():
  276. # If data for step is valid, update the session
  277. form_data = request.session.get(session_key_data, {})
  278. form_data.update(prev_form.cleaned_data)
  279. request.session[session_key_data] = form_data
  280. if prev_step.has_next():
  281. # Create a new form for a following step, if the following step is present
  282. form_class = self.get_form_class_for_step(step)
  283. form = form_class(page=self, user=request.user)
  284. else:
  285. # If there is no next step, create form for all fields
  286. form = self.get_form(
  287. request.session[session_key_data],
  288. page=self, user=request.user
  289. )
  290. if form.is_valid():
  291. # Perform validation again for whole form.
  292. # After successful validation, save data into DB,
  293. # and remove from the session.
  294. self.process_form_submission(form)
  295. del request.session[session_key_data]
  296. # Render the landing page
  297. return render(
  298. request,
  299. self.landing_page_template,
  300. self.get_context(request)
  301. )
  302. else:
  303. # If data for step is invalid
  304. # we will need to display form again with errors,
  305. # so restore previous state.
  306. form = prev_form
  307. step = prev_step
  308. else:
  309. # Create empty form for non-POST requests
  310. form_class = self.get_form_class_for_step(step)
  311. form = form_class(page=self, user=request.user)
  312. context = self.get_context(request)
  313. context['form'] = form
  314. context['fields_step'] = step
  315. return render(
  316. request,
  317. self.template,
  318. context
  319. )
  320. Your template for this form page should look like this:
  321. .. code-block:: django
  322. {% load wagtailcore_tags %}
  323. <html>
  324. <head>
  325. <title>{{ page.title }}</title>
  326. </head>
  327. <body>
  328. <h1>{{ page.title }}</h1>
  329. <div>{{ page.intro|richtext }}</div>
  330. <form action="{% pageurl page %}?p={{ fields_step.number|add:"1" }}" method="POST">
  331. {% csrf_token %}
  332. {{ form.as_p }}
  333. <input type="submit">
  334. </form>
  335. </body>
  336. </html>
  337. Note that the example shown before allows the user to return to a previous step,
  338. or to open a second step without submitting the first step.
  339. Depending on your requirements, you may need to add extra checks.
  340. Show results
  341. ------------
  342. If you are implementing polls or surveys, you may want to show results after submission.
  343. The following example demonstrates how to do this.
  344. First, you need to collect results as shown below:
  345. .. code-block:: python
  346. from modelcluster.fields import ParentalKey
  347. from wagtail.wagtailadmin.edit_handlers import (
  348. FieldPanel, FieldRowPanel,
  349. InlinePanel, MultiFieldPanel
  350. )
  351. from wagtail.wagtailcore.fields import RichTextField
  352. from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField
  353. class FormField(AbstractFormField):
  354. page = ParentalKey('FormPage', related_name='form_fields')
  355. class FormPage(AbstractEmailForm):
  356. intro = RichTextField(blank=True)
  357. thank_you_text = RichTextField(blank=True)
  358. content_panels = AbstractEmailForm.content_panels + [
  359. FieldPanel('intro', classname="full"),
  360. InlinePanel('form_fields', label="Form fields"),
  361. FieldPanel('thank_you_text', classname="full"),
  362. MultiFieldPanel([
  363. FieldRowPanel([
  364. FieldPanel('from_address', classname="col6"),
  365. FieldPanel('to_address', classname="col6"),
  366. ]),
  367. FieldPanel('subject'),
  368. ], "Email"),
  369. ]
  370. def get_context(self, request, *args, **kwargs):
  371. context = super(FormPage, self).get_context(request, *args, **kwargs)
  372. # If you need to show results only on landing page,
  373. # you may need check request.method
  374. results = dict()
  375. # Get information about form fields
  376. data_fields = [
  377. (field.clean_name, field.label)
  378. for field in self.get_form_fields()
  379. ]
  380. # Get all submissions for current page
  381. submissions = self.get_submission_class().objects.filter(page=self)
  382. for submission in submissions:
  383. data = submission.get_data()
  384. # Count results for each question
  385. for name, label in data_fields:
  386. answer = data.get(name)
  387. if answer is None:
  388. # Something wrong with data.
  389. # Probably you have changed questions
  390. # and now we are receiving answers for old questions.
  391. # Just skip them.
  392. continue
  393. if type(answer) is list:
  394. # Answer is a list if the field type is 'Checkboxes'
  395. answer = u', '.join(answer)
  396. question_stats = results.get(label, {})
  397. question_stats[answer] = question_stats.get(answer, 0) + 1
  398. results[label] = question_stats
  399. context.update({
  400. 'results': results,
  401. })
  402. return context
  403. Next, you need to transform your template to display the results:
  404. .. code-block:: django
  405. {% load wagtailcore_tags %}
  406. <html>
  407. <head>
  408. <title>{{ page.title }}</title>
  409. </head>
  410. <body>
  411. <h1>{{ page.title }}</h1>
  412. <h2>Results</h2>
  413. {% for question, answers in results.items %}
  414. <h3>{{ question }}</h3>
  415. {% for answer, count in answers.items %}
  416. <div>{{ answer }}: {{ count }}</div>
  417. {% endfor %}
  418. {% endfor %}
  419. <div>{{ page.intro|richtext }}</div>
  420. <form action="{% pageurl page %}" method="POST">
  421. {% csrf_token %}
  422. {{ form.as_p }}
  423. <input type="submit">
  424. </form>
  425. </body>
  426. </html>
  427. You can also show the results on the landing page.