From 7f10938b57f369adf77128a1f6894b1a1a3d91ad Mon Sep 17 00:00:00 2001 From: Mikalai Radchuk Date: Wed, 21 Dec 2016 15:51:58 +0300 Subject: [PATCH] Add docs about wagtailforms customisation Fixes #2945 --- .../reference/contrib/forms/customisation.rst | 554 ++++++++++++++++++ .../contrib/{forms.rst => forms/index.rst} | 19 +- docs/reference/contrib/index.rst | 6 +- 3 files changed, 574 insertions(+), 5 deletions(-) create mode 100644 docs/reference/contrib/forms/customisation.rst rename docs/reference/contrib/{forms.rst => forms/index.rst} (95%) diff --git a/docs/reference/contrib/forms/customisation.rst b/docs/reference/contrib/forms/customisation.rst new file mode 100644 index 0000000000..df7aeda6f1 --- /dev/null +++ b/docs/reference/contrib/forms/customisation.rst @@ -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 %} + + + {{ page.title }} + + +

{{ page.title }}

+ + {% if user.is_authenticated and user.is_active or request.is_preview %} + {% if form %} +
{{ page.intro|richtext }}
+
+ {% csrf_token %} + {{ form.as_p }} + +
+ {% else %} +
You can fill in the from only one time.
+ {% endif %} + {% else %} +
To fill in the form, you must to log in.
+ {% endif %} + + + + +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 %} + + + {{ page.title }} + + +

{{ page.title }}

+ +
{{ page.intro|richtext }}
+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + + + +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 %} + + + {{ page.title }} + + +

{{ page.title }}

+ +

Results

+ {% for question, answers in results.items %} +

{{ question }}

+ {% for answer, count in answers.items %} +
{{ answer }}: {{ count }}
+ {% endfor %} + {% endfor %} + +
{{ page.intro|richtext }}
+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + + + +You can also show the results on the landing page. diff --git a/docs/reference/contrib/forms.rst b/docs/reference/contrib/forms/index.rst similarity index 95% rename from docs/reference/contrib/forms.rst rename to docs/reference/contrib/forms/index.rst index e56d6a4279..93cd2b1738 100644 --- a/docs/reference/contrib/forms.rst +++ b/docs/reference/contrib/forms/index.rst @@ -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 `_. 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 `_ 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 diff --git a/docs/reference/contrib/index.rst b/docs/reference/contrib/index.rst index ded622b18f..840f1409ef 100644 --- a/docs/reference/contrib/index.rst +++ b/docs/reference/contrib/index.rst @@ -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.