Browse Source

Use wagtail_flexible_forms 2.0 (#675)

Vince Salvino 1 month ago
parent
commit
cb12da945a

+ 1 - 1
coderedcms/blocks/stream_form_blocks.py

@@ -1,5 +1,6 @@
 from django.utils.translation import gettext_lazy as _
 from wagtail import blocks
+from wagtail_flexible_forms import blocks as form_blocks
 
 from coderedcms.blocks.base_blocks import BaseBlock
 from coderedcms.blocks.base_blocks import CoderedAdvSettings
@@ -10,7 +11,6 @@ from coderedcms.forms import CoderedDateTimeInput
 from coderedcms.forms import CoderedTimeField
 from coderedcms.forms import CoderedTimeInput
 from coderedcms.forms import SecureFileField
-from coderedcms.wagtail_flexible_forms import blocks as form_blocks
 
 
 class CoderedFormAdvSettings(CoderedAdvSettings):

+ 16 - 0
coderedcms/migrations/0042_remove_coderedsessionformsubmission_thumbnails_by_path.py

@@ -0,0 +1,16 @@
+# Generated by Django 4.2.16 on 2025-02-06 19:36
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("coderedcms", "0041_remove_layoutsettings_frontend_theme"),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name="coderedsessionformsubmission",
+            name="thumbnails_by_path",
+        ),
+    ]

+ 25 - 151
coderedcms/models/page_models.py

@@ -35,9 +35,6 @@ from django.core.serializers.json import DjangoJSONEncoder
 from django.core.validators import MaxValueValidator
 from django.core.validators import MinValueValidator
 from django.db import models
-from django.db.models.signals import post_delete
-from django.db.models.signals import post_save
-from django.dispatch import receiver
 from django.http import HttpResponseRedirect
 from django.http import JsonResponse
 from django.shortcuts import redirect
@@ -76,6 +73,10 @@ from wagtail.models import PageBase
 from wagtail.models import Site
 from wagtail.search import index
 from wagtail.utils.decorators import cached_classmethod
+from wagtail_flexible_forms.models import AbstractSessionFormSubmission
+from wagtail_flexible_forms.models import AbstractSubmissionRevision
+from wagtail_flexible_forms.models import StreamFormJSONEncoder
+from wagtail_flexible_forms.models import StreamFormMixin
 from wagtailcache.cache import WagtailCacheMixin
 from wagtailseo.models import SeoMixin
 from wagtailseo.models import TwitterCard
@@ -94,14 +95,6 @@ from coderedcms.forms import CoderedSubmissionsListView
 from coderedcms.models.snippet_models import ClassifierTerm
 from coderedcms.models.wagtailsettings_models import LayoutSettings
 from coderedcms.settings import crx_settings
-from coderedcms.wagtail_flexible_forms.blocks import FormFieldBlock
-from coderedcms.wagtail_flexible_forms.blocks import FormStepBlock
-from coderedcms.wagtail_flexible_forms.models import SessionFormSubmission
-from coderedcms.wagtail_flexible_forms.models import Step
-from coderedcms.wagtail_flexible_forms.models import Steps
-from coderedcms.wagtail_flexible_forms.models import StreamFormJSONEncoder
-from coderedcms.wagtail_flexible_forms.models import StreamFormMixin
-from coderedcms.wagtail_flexible_forms.models import SubmissionRevision
 from coderedcms.widgets import ClassifierSelectWidget
 
 
@@ -1793,46 +1786,18 @@ class CoderedFormPage(CoderedFormMixin, CoderedWebPage):
         return FormSubmission
 
 
-class CoderedSubmissionRevision(SubmissionRevision, models.Model):
+class CoderedSubmissionRevision(AbstractSubmissionRevision):
     pass
 
 
-class CoderedSessionFormSubmission(SessionFormSubmission):
-    INCOMPLETE = "incomplete"
-    COMPLETE = "complete"
-    REVIEWED = "reviewed"
-    APPROVED = "approved"
-    REJECTED = "rejected"
-    STATUSES = (
-        (INCOMPLETE, _("Not submitted")),
-        (COMPLETE, _("Complete")),
-        (REVIEWED, _("Under consideration")),
-        (APPROVED, _("Approved")),
-        (REJECTED, _("Rejected")),
-    )
-    status = models.CharField(
-        max_length=10, choices=STATUSES, default=INCOMPLETE
-    )
-
-    def create_normal_submission(self, delete_self=True):
-        submission_data = self.get_data()
-        if "user" in submission_data:
-            submission_data["user"] = str(submission_data["user"])
-        submission = FormSubmission.objects.create(
-            form_data=submission_data,
-            page=self.page,
-        )
-
-        if delete_self:
-            CoderedSubmissionRevision.objects.filter(
-                submission_id=self.id
-            ).delete()
-            self.delete()
-
-        return submission
+class CoderedSessionFormSubmission(AbstractSessionFormSubmission):
+    """
+    Customize how certain fields are rendered.
+    """
 
-    def render_email(self, value):
-        return value
+    @staticmethod
+    def get_revision_class():
+        return CoderedSubmissionRevision
 
     def render_link(self, value):
         return "{0}{1}".format(crx_settings.CRX_PROTECTED_MEDIA_URL, value)
@@ -1844,75 +1809,10 @@ class CoderedSessionFormSubmission(SessionFormSubmission):
         return "{0}{1}".format(crx_settings.CRX_PROTECTED_MEDIA_URL, value)
 
 
-@receiver(post_save)
-def create_submission_changed_revision(sender, **kwargs):
-    if not issubclass(sender, SessionFormSubmission):
-        return
-    submission = kwargs["instance"]
-    created = kwargs["created"]
-    CoderedSubmissionRevision.create_from_submission(
-        submission,
-        (
-            CoderedSubmissionRevision.CREATED
-            if created
-            else CoderedSubmissionRevision.CHANGED
-        ),
-    )
-
-
-@receiver(post_delete)
-def create_submission_deleted_revision(sender, **kwargs):
-    if not issubclass(sender, CoderedSessionFormSubmission):
-        return
-    submission = kwargs["instance"]
-    CoderedSubmissionRevision.create_from_submission(
-        submission, SubmissionRevision.DELETED
-    )
-
-
-class CoderedStep(Step):
-    def get_markups_and_bound_fields(self, form):
-        for struct_child in self.form_fields:
-            block = struct_child.block
-            if isinstance(block, FormFieldBlock):
-                struct_value = struct_child.value
-                field_name = block.get_slug(struct_value)
-                yield form[field_name], "field", struct_child
-            else:
-                yield mark_safe(struct_child), "markup"
-
-
-class CoderedSteps(Steps):
-    def __init__(self, page, request=None):
-        self.page = page
-        # TODO: Make it possible to change the `form_fields` attribute.
-        self.form_fields = page.form_fields
-        self.request = request
-        has_steps = any(
-            isinstance(struct_child.block, FormStepBlock)
-            for struct_child in self.form_fields
-        )
-        if has_steps:
-            steps = [
-                CoderedStep(self, i, form_field)
-                for i, form_field in enumerate(self.form_fields)
-            ]
-        else:
-            steps = [CoderedStep(self, 0, self.form_fields)]
-        super(Steps, self).__init__(steps)
-
-
 class CoderedStreamFormMixin(StreamFormMixin):
-    class Meta:
-        abstract = True
-
-    def get_steps(self, request=None):
-        if not hasattr(self, "steps"):
-            steps = CoderedSteps(self, request=request)
-            if request is None:
-                return steps
-            self.steps = steps
-        return self.steps
+    """
+    Customize the classes used to store submissions.
+    """
 
     @staticmethod
     def get_submission_class():
@@ -1922,37 +1822,6 @@ class CoderedStreamFormMixin(StreamFormMixin):
     def get_session_submission_class():
         return CoderedSessionFormSubmission
 
-    def get_submission(self, request):
-        Submission = self.get_session_submission_class()
-        if request.user.is_authenticated:
-            user_submission = (
-                Submission.objects.filter(user=request.user, page=self)
-                .order_by("-pk")
-                .first()
-            )
-            if user_submission is None:
-                return Submission(user=request.user, page=self, form_data="[]")
-            return user_submission
-
-        # Custom code to ensure that anonymous users get a session key.
-        if not request.session.session_key:
-            request.session.create()
-
-        user_submission = (
-            Submission.objects.filter(
-                session_key=request.session.session_key, page=self
-            )
-            .order_by("-pk")
-            .first()
-        )
-        if user_submission is None:
-            return Submission(
-                session_key=request.session.session_key,
-                page=self,
-                form_data="[]",
-            )
-        return user_submission
-
 
 class CoderedStreamFormPage(
     CoderedFormMixin, CoderedStreamFormMixin, CoderedWebPage
@@ -1981,15 +1850,20 @@ class CoderedStreamFormPage(
         if form.is_valid():
             is_complete = self.steps.update_data()
             if is_complete:
-                submission = self.get_submission(request)
+                # NOTE: normally this happens in
+                # ``wagtail_flexible_forms.StreamFormMixin.serve()``
+                # but we are overriding it in ``CoderedFormMixin``.
+                # This is a potentially confusing architecture.
+                form_submission = self.create_final_submission(
+                    request, delete_session=True
+                )
                 self.process_form_submission(
                     request=request,
                     form=form,
-                    form_submission=submission,
-                    processed_data=submission.get_data(),
+                    form_submission=form_submission,
+                    processed_data=form_submission.get_data(),
                 )
-                normal_submission = submission.create_normal_submission()
-                return self.render_landing_page(request, normal_submission)
+                return self.render_landing_page(request, form_submission)
             return HttpResponseRedirect(self.url)
         return self.process_form_get(form, request)
 

+ 5 - 5
coderedcms/templates/coderedcms/pages/stream_form_page.html

@@ -19,15 +19,15 @@
   {% endblock %}
 
   {% block stream_form %}
-  <form class="stream-form {{ page.form_css_class }}" id="{{ page.form_id }}" action="{% pageurl self %}" method="POST" {% if form|is_file_form %}enctype="multipart/form-data"{% endif %}>
+  <form class="stream-form {{ page.form_css_class }}" id="{{ page.form_id }}" action="{% pageurl self %}" method="POST" enctype="{{ form_enctype }}">
     {% csrf_token %}
 
     {% block stream_form_fields %}
     {% for item in markups_and_bound_fields %}
-    {% if item.1 == 'markup' %}
-    {% include_block item.0 %}
-    {% else %}
-    {% include "coderedcms/includes/stream_forms/render_field.html" with block=item.2 field=item.0 %}
+    {% if item.type == "markup" %}
+    {% include_block item.block %}
+    {% elif item.type == "field" %}
+    {% include "coderedcms/includes/stream_forms/render_field.html" with block=item.block field=item.field %}
     {% endif %}
     {% endfor %}
     {% endblock %}

+ 0 - 29
coderedcms/wagtail_flexible_forms/LICENSE

@@ -1,29 +0,0 @@
-BSD 3-Clause License
-
-Copyright (c) 2018, NoriPyt
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-* Neither the name of the copyright holder nor the names of its
-  contributors may be used to endorse or promote products derived from
-  this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 0 - 285
coderedcms/wagtail_flexible_forms/blocks.py

@@ -1,285 +0,0 @@
-from anyascii import anyascii
-from django import forms
-from django.db.models import BLANK_CHOICE_DASH
-from django.utils.dateparse import parse_datetime
-from django.utils.text import slugify
-from django.utils.translation import gettext_lazy as _
-from wagtail.blocks import BooleanBlock
-from wagtail.blocks import CharBlock
-from wagtail.blocks import ChoiceBlock
-from wagtail.blocks import DateBlock
-from wagtail.blocks import DateTimeBlock
-from wagtail.blocks import ListBlock
-from wagtail.blocks import RichTextBlock
-from wagtail.blocks import StreamBlock
-from wagtail.blocks import StructBlock
-from wagtail.blocks import TextBlock
-from wagtail.blocks import TimeBlock
-
-
-class FormFieldBlock(StructBlock):
-    field_label = CharBlock(label=_("Label"))
-    help_text = TextBlock(required=False, label=_("Help text"))
-
-    field_class = forms.CharField
-    widget = None
-
-    def get_slug(self, struct_value):
-        return slugify(anyascii(struct_value["field_label"]))
-
-    def get_field_class(self, struct_value):
-        return self.field_class
-
-    def get_widget(self, struct_value):
-        return self.widget
-
-    def get_field_kwargs(self, struct_value):
-        kwargs = {
-            "label": struct_value["field_label"],
-            "help_text": struct_value["help_text"],
-            "required": struct_value.get("required", False),
-        }
-        if "default_value" in struct_value:
-            kwargs["initial"] = struct_value["default_value"]
-        form_widget = self.get_widget(struct_value)
-        if form_widget is not None:
-            kwargs["widget"] = form_widget
-        return kwargs
-
-    def get_field(self, struct_value):
-        return self.get_field_class(struct_value)(
-            **self.get_field_kwargs(struct_value)
-        )
-
-
-class OptionalFormFieldBlock(FormFieldBlock):
-    required = BooleanBlock(label=_("Required"), required=False)
-
-
-CHARFIELD_FORMATS = [
-    ("email", _("Email")),
-    ("url", _("URL")),
-]
-try:
-    from phonenumber_field.formfields import PhoneNumberField
-except ImportError:
-    pass
-else:
-    CHARFIELD_FORMATS.append(("phone", _("Phone")))
-
-
-class CharFieldBlock(OptionalFormFieldBlock):
-    format = ChoiceBlock(
-        choices=CHARFIELD_FORMATS, required=False, label=_("Format")
-    )
-    default_value = CharBlock(required=False, label=_("Default value"))
-
-    class Meta:
-        label = _("Text field (single line)")
-
-    def get_field_class(self, struct_value):
-        text_format = struct_value["format"]
-        if text_format == "url":
-            return forms.URLField
-        if text_format == "email":
-            return forms.EmailField
-        if text_format == "phone":
-            return PhoneNumberField
-        return super().get_field_class(struct_value)
-
-
-class TextFieldBlock(OptionalFormFieldBlock):
-    default_value = TextBlock(required=False, label=_("Default value"))
-
-    widget = forms.Textarea(attrs={"rows": 5})
-
-    class Meta:
-        label = _("Text field (multi line)")
-
-
-class NumberFieldBlock(OptionalFormFieldBlock):
-    default_value = CharBlock(required=False, label=_("Default value"))
-
-    widget = forms.NumberInput
-
-    class Meta:
-        label = _("Number field")
-
-
-class CheckboxFieldBlock(FormFieldBlock):
-    default_value = BooleanBlock(required=False)
-
-    field_class = forms.BooleanField
-
-    class Meta:
-        label = _("Checkbox field")
-        icon = "tick-inverse"
-
-
-class RadioButtonsFieldBlock(OptionalFormFieldBlock):
-    choices = ListBlock(CharBlock(label=_("Choice")))
-
-    field_class = forms.ChoiceField
-    widget = forms.RadioSelect
-
-    class Meta:
-        label = _("Radio buttons")
-        icon = "radio-empty"
-
-    def get_field_kwargs(self, struct_value):
-        kwargs = super().get_field_kwargs(struct_value)
-        kwargs["choices"] = [
-            (choice, choice) for choice in struct_value["choices"]
-        ]
-        return kwargs
-
-
-class DropdownFieldBlock(RadioButtonsFieldBlock):
-    widget = forms.Select
-
-    class Meta:
-        label = _("Dropdown field")
-        icon = "arrow-down-big"
-
-    def get_field_kwargs(self, struct_value):
-        kwargs = super(DropdownFieldBlock, self).get_field_kwargs(struct_value)
-        kwargs["choices"].insert(0, BLANK_CHOICE_DASH[0])
-        return kwargs
-
-
-class CheckboxesFieldBlock(OptionalFormFieldBlock):
-    checkboxes = ListBlock(CharBlock(label=_("Checkbox")))
-
-    field_class = forms.MultipleChoiceField
-    widget = forms.CheckboxSelectMultiple
-
-    class Meta:
-        label = _("Multiple checkboxes field")
-        icon = "list-ul"
-
-    def get_field_kwargs(self, struct_value):
-        kwargs = super(CheckboxesFieldBlock, self).get_field_kwargs(
-            struct_value
-        )
-        kwargs["choices"] = [
-            (choice, choice) for choice in struct_value["checkboxes"]
-        ]
-        return kwargs
-
-
-class DatePickerInput(forms.DateInput):
-    def __init__(self, *args, **kwargs):
-        attrs = kwargs.get("attrs")
-        if attrs is None:
-            attrs = {}
-        attrs.update(
-            {
-                "data-provide": "datepicker",
-                "data-date-format": "yyyy-mm-dd",
-            }
-        )
-        kwargs["attrs"] = attrs
-        super().__init__(*args, **kwargs)
-
-
-class DateFieldBlock(OptionalFormFieldBlock):
-    default_value = DateBlock(required=False)
-
-    field_class = forms.DateField
-    widget = DatePickerInput
-
-    class Meta:
-        label = _("Date field")
-        icon = "date"
-
-
-class HTML5TimeInput(forms.TimeInput):
-    input_type = "time"
-
-
-class TimeFieldBlock(OptionalFormFieldBlock):
-    default_value = TimeBlock(required=False)
-
-    field_class = forms.TimeField
-    widget = HTML5TimeInput
-
-    class Meta:
-        label = _("Time field")
-        icon = "time"
-
-
-class DateTimePickerInput(forms.SplitDateTimeWidget):
-    def __init__(self, attrs=None, date_format=None, time_format=None):
-        super().__init__(
-            attrs=attrs, date_format=date_format, time_format=time_format
-        )
-        self.widgets = (
-            DatePickerInput(attrs=attrs, format=date_format),
-            HTML5TimeInput(attrs=attrs, format=time_format),
-        )
-
-    def decompress(self, value):
-        if isinstance(value, str):
-            value = parse_datetime(value)
-        return super().decompress(value)
-
-
-class DateTimeFieldBlock(OptionalFormFieldBlock):
-    default_value = DateTimeBlock(required=False)
-
-    field_class = forms.SplitDateTimeField
-    widget = DateTimePickerInput
-
-    class Meta:
-        label = _("Date+time field")
-        icon = "date"
-
-
-class ImageFieldBlock(OptionalFormFieldBlock):
-    field_class = forms.ImageField
-
-    class Meta:
-        label = _("Image field")
-        icon = "image"
-
-
-class FileFieldBlock(OptionalFormFieldBlock):
-    field_class = forms.FileField
-
-    class Meta:
-        label = _("File field")
-        icon = "download"
-
-
-class FormFieldsBlock(StreamBlock):
-    char = CharFieldBlock(group=_("Fields"))
-    text = TextFieldBlock(group=_("Fields"))
-    number = NumberFieldBlock(group=_("Fields"))
-    checkbox = CheckboxFieldBlock(group=_("Fields"))
-    radios = RadioButtonsFieldBlock(group=_("Fields"))
-    dropdown = DropdownFieldBlock(group=_("Fields"))
-    checkboxes = CheckboxesFieldBlock(group=_("Fields"))
-    date = DateFieldBlock(group=_("Fields"))
-    time = TimeFieldBlock(group=_("Fields"))
-    datetime = DateTimeFieldBlock(group=_("Fields"))
-    image = ImageFieldBlock(group=_("Fields"))
-    file = FileFieldBlock(group=_("Fields"))
-    text_markup = RichTextBlock(group=_("Other"))
-
-    class Meta:
-        label = _("Form fields")
-
-
-class FormStepBlock(StructBlock):
-    name = CharBlock(label=_("Name"), required=False)
-    form_fields = FormFieldsBlock()
-
-    class Meta:
-        label = _("Form step")
-
-
-class FormStepsBlock(StreamBlock):
-    step = FormStepBlock()
-
-    class Meta:
-        label = _("Form steps")

+ 0 - 35
coderedcms/wagtail_flexible_forms/edit_handlers.py

@@ -1,35 +0,0 @@
-from django.template.loader import render_to_string
-from django.utils.safestring import mark_safe
-from django.utils.translation import gettext as _
-from wagtail.admin.panels import EditHandler
-
-
-class FormSubmissionsPanel(EditHandler):
-    template = "wagtailforms/edit_handlers/form_responses_panel.html"
-
-    def bind_to(self, model=None, instance=None, request=None, form=None):
-        new = super().bind_to(model=model)
-        if self.heading is None:
-            new.heading = _("{} submissions").format(model.get_verbose_name())
-        return new
-
-    def render(self):
-        Submission = self.model.get_submission_class()
-        submissions = Submission.objects.filter(page=self.instance)
-        submission_count = submissions.count()
-
-        if not submission_count:
-            return ""
-
-        return mark_safe(
-            render_to_string(
-                self.template,
-                {
-                    "self": self,
-                    "submission_count": submission_count,
-                    "last_submit_time": (
-                        submissions.order_by("submit_time").last().submit_time
-                    ),
-                },
-            )
-        )

+ 0 - 925
coderedcms/wagtail_flexible_forms/models.py

@@ -1,925 +0,0 @@
-import datetime
-import json
-import os
-from collections import OrderedDict
-from importlib import import_module
-from itertools import zip_longest
-from pathlib import Path
-
-from django.apps import apps
-from django.conf import settings
-from django.contrib import messages
-from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
-from django.core.files.storage import default_storage
-from django.core.serializers.json import DjangoJSONEncoder
-from django.db.models import CASCADE
-from django.db.models import PROTECT
-from django.db.models import CharField
-from django.db.models import DateTimeField
-from django.db.models import ForeignKey
-from django.db.models import Model
-from django.db.models import QuerySet
-from django.db.models import TextField
-from django.db.models.fields.files import FieldFile
-from django.db.models.signals import post_delete
-from django.dispatch import receiver
-from django.forms import EmailField
-from django.forms import FileField
-from django.forms import Form
-from django.forms import ImageField
-from django.forms import URLField
-from django.http import HttpResponseRedirect
-from django.template.response import TemplateResponse
-from django.utils.safestring import SafeData
-from django.utils.safestring import mark_safe
-from django.utils.timezone import now
-from django.utils.translation import gettext_lazy as _
-from PIL import Image
-from wagtail.contrib.forms.models import AbstractEmailForm
-from wagtail.contrib.forms.models import AbstractForm
-from wagtail.contrib.forms.models import AbstractFormSubmission
-
-from .blocks import FormFieldBlock
-from .blocks import FormStepBlock
-
-
-class Step:
-    def __init__(self, steps, index, struct_child):
-        self.steps = steps
-        self.index = index
-        block = getattr(struct_child, "block", None)
-        if block is None:
-            struct_child = []
-        if isinstance(block, FormStepBlock):
-            self.name = struct_child.value["name"]
-            self.form_fields = struct_child.value["form_fields"]
-        else:
-            self.name = ""
-            self.form_fields = struct_child
-
-    @property
-    def index1(self):
-        return self.index + 1
-
-    @property
-    def url(self):
-        return "%s?step=%s" % (self.steps.page.url, self.index1)
-
-    def get_form_fields(self):
-        form_fields = OrderedDict()
-        field_blocks = self.form_fields
-        for struct_child in field_blocks:
-            block = struct_child.block
-            if isinstance(block, FormFieldBlock):
-                struct_value = struct_child.value
-                field_name = block.get_slug(struct_value)
-                form_fields[field_name] = block.get_field(struct_value)
-        return form_fields
-
-    def get_form_class(self):
-        return type(
-            "WagtailForm",
-            self.steps.page.get_form_class_bases(),
-            self.get_form_fields(),
-        )
-
-    def get_markups_and_bound_fields(self, form):
-        for struct_child in self.form_fields:
-            block = struct_child.block
-            if isinstance(block, FormFieldBlock):
-                struct_value = struct_child.value
-                field_name = block.get_slug(struct_value)
-                yield form[field_name], "field"
-            else:
-                yield mark_safe(struct_child), "markup"
-
-    def __str__(self):
-        if self.name:
-            return self.name
-        return _("Step %s") % self.index1
-
-    @property
-    def badge(self):
-        return mark_safe('<span class="badge">%s/%s</span>') % (
-            self.index1,
-            len(self.steps),
-        )
-
-    def __html__(self):
-        return "%s %s" % (self, self.badge)
-
-    @property
-    def is_active(self):
-        return self.index == self.steps.current_index
-
-    @property
-    def is_last(self):
-        return self.index1 == len(self.steps)
-
-    @property
-    def has_prev(self):
-        return self.index > 0
-
-    @property
-    def has_next(self):
-        return self.index1 < len(self.steps)
-
-    @property
-    def prev(self):
-        if self.has_prev:
-            return self.steps[self.index - 1]
-
-    @property
-    def next(self):
-        if self.has_next:
-            return self.steps[self.index + 1]
-
-    def get_existing_data(self, raw=False):
-        data = self.steps.get_existing_data()[self.index]
-        fields = self.get_form_fields()
-        if not raw:
-
-            class FakeField:
-                storage = self.steps.get_storage()
-
-            for field_name, value in data.items():
-                if field_name in fields and isinstance(
-                    fields[field_name], FileField
-                ):
-                    data[field_name] = FieldFile(None, FakeField, value)
-        return data
-
-    @property
-    def is_available(self):
-        return self.prev is None or self.prev.get_existing_data(raw=True)
-
-
-class StreamFormJSONEncoder(DjangoJSONEncoder):
-    def default(self, o):
-        try:
-            from phonenumber_field.phonenumber import PhoneNumber
-        except ImportError:
-            pass
-        else:
-            if isinstance(o, PhoneNumber):
-                return str(o)
-
-        return super().default(o)
-
-
-class Steps(list):
-    def __init__(self, page, request=None):
-        self.page = page
-        # TODO: Make it possible to change the `form_fields` attribute.
-        self.form_fields = page.form_fields
-        self.request = request
-        has_steps = any(
-            isinstance(struct_child.block, FormStepBlock)
-            for struct_child in self.form_fields
-        )
-        if has_steps:
-            steps = [
-                Step(self, i, form_field)
-                for i, form_field in enumerate(self.form_fields)
-            ]
-        else:
-            steps = [Step(self, 0, self.form_fields)]
-        super().__init__(steps)
-
-    def clamp_index(self, index: int):
-        if index < 0:
-            index = 0
-        if index >= len(self):
-            index = len(self) - 1
-        while not self[index].is_available:
-            index -= 1
-        return index
-
-    @property
-    def current_index(self):
-        return self.request.session.get(self.page.current_step_session_key, 0)
-
-    @property
-    def current(self):
-        return self[self.current_index]
-
-    @current.setter
-    def current(self, new_index: int):
-        if not isinstance(new_index, int):
-            raise TypeError("Use an integer to set the new current step.")
-        self.request.session[self.page.current_step_session_key] = (
-            self.clamp_index(new_index)
-        )
-
-    def forward(self, increment: int = 1):
-        self.current = self.current_index + increment
-
-    def backward(self, increment: int = 1):
-        self.current = self.current_index - increment
-
-    def get_submission(self):
-        return self.page.get_submission(self.request)
-
-    def get_existing_data(self):
-        submission = self.get_submission()
-        data = [] if submission is None else json.loads(submission.form_data)
-        length_difference = len(self) - len(data)
-        if length_difference > 0:
-            data.extend([{}] * length_difference)
-        return data
-
-    def get_current_form(self):
-        request = self.request
-        if request.method == "POST":
-            step_value = request.POST.get("step", "next")
-            if step_value == "prev":
-                self.backward()
-            else:
-                return self.current.get_form_class()(
-                    request.POST,
-                    request.FILES,
-                    initial=self.current.get_existing_data(),
-                )
-        return self.current.get_form_class()(
-            initial=self.current.get_existing_data()
-        )
-
-    def get_storage(self):
-        return self.page.get_storage()
-
-    def save_files(self, form):
-        submission = self.get_submission()
-        for name, field in form.fields.items():
-            if isinstance(field, FileField):
-                file = form.cleaned_data[name]
-                if file == form.initial.get(name, ""):  # Nothing submitted.
-                    form.cleaned_data[name] = file.name
-                    continue
-                if submission is not None:
-                    submission.delete_file(name)
-                if not file:  # 'Clear' was checked.
-                    form.cleaned_data[name] = ""
-                    continue
-                directory = self.request.session.session_key
-                storage = self.get_storage()
-                Path(storage.path(directory)).mkdir(parents=True, exist_ok=True)
-                path = storage.get_available_name(
-                    str(Path(directory) / file.name)
-                )
-                with storage.open(path, "wb+") as destination:
-                    for chunk in file.chunks():
-                        destination.write(chunk)
-                form.cleaned_data[name] = path
-
-    def update_data(self):
-        form = self.get_current_form()
-        if form.is_valid():
-            form_data = self.get_existing_data()
-            self.save_files(form)
-            form_data[self.current_index] = form.cleaned_data
-            form_data = json.dumps(form_data, cls=StreamFormJSONEncoder)
-            is_complete = self.current.is_last
-            submission = self.get_submission()
-            submission.form_data = form_data
-            if not submission.is_complete and is_complete:
-                submission.status = submission.COMPLETE
-            submission.save()
-            if is_complete:
-                self.current = 0
-            else:
-                self.forward()
-            return is_complete
-        return False
-
-
-class SessionFormSubmission(AbstractFormSubmission):
-    session_key = CharField(max_length=40, null=True, default=None)
-    user = ForeignKey(
-        settings.AUTH_USER_MODEL,
-        null=True,
-        blank=True,
-        related_name="+",
-        on_delete=PROTECT,
-    )
-    thumbnails_by_path = TextField(default=json.dumps({}))
-    last_modification = DateTimeField(_("last modification"), auto_now=True)
-    INCOMPLETE = "incomplete"
-    COMPLETE = "complete"
-    REVIEWED = "reviewed"
-    APPROVED = "approved"
-    REJECTED = "rejected"
-    STATUSES = (
-        (INCOMPLETE, _("Not submitted")),
-        (COMPLETE, _("In progress")),
-        (REVIEWED, _("Under consideration")),
-        (APPROVED, _("Approved")),
-        (REJECTED, _("Rejected")),
-    )
-    status = CharField(max_length=10, choices=STATUSES, default=INCOMPLETE)
-
-    class Meta:
-        verbose_name = _("form submission")
-        verbose_name_plural = _("form submissions")
-        unique_together = (("page", "session_key"), ("page", "user"))
-        abstract = True
-
-    @property
-    def is_complete(self):
-        return self.status != self.INCOMPLETE
-
-    @property
-    def form_page(self):
-        return self.page.specific
-
-    def get_session(self):
-        return import_module(settings.SESSION_ENGINE).SessionStore(
-            session_key=self.session_key
-        )
-
-    def reset_step(self):
-        session = self.get_session()
-        try:
-            del session[self.form_page.current_step_session_key]
-        except KeyError:
-            pass
-        else:
-            session.save()
-
-    def get_storage(self):
-        return self.form_page.get_storage()
-
-    def get_thumbnail_path(self, path, width=64, height=64):
-        if not path:
-            return ""
-        variant = "%s×%s" % (width, height)
-        thumbnails_by_path = json.loads(self.thumbnails_by_path)
-        thumbnails_paths = thumbnails_by_path.get(path)
-        if thumbnails_paths is None:
-            thumbnails_by_path[path] = {}
-        else:
-            thumbnail_path = thumbnails_paths.get(variant)
-            if thumbnail_path is not None:
-                return thumbnail_path
-
-        path = Path(path)
-        thumbnail_path = str(path.with_suffix(".%s%s" % (variant, path.suffix)))
-        storage = self.get_storage()
-        thumbnail_path = storage.get_available_name(thumbnail_path)
-
-        thumbnail = Image.open(storage.path(path))
-        thumbnail.thumbnail((width, height))
-        thumbnail.save(storage.path(thumbnail_path))
-
-        thumbnails_by_path[str(path)][variant] = thumbnail_path
-        self.thumbnails_by_path = json.dumps(
-            thumbnails_by_path, cls=StreamFormJSONEncoder
-        )
-        self.save()
-        return thumbnail_path
-
-    def get_fields(self, by_step=False):
-        return self.form_page.get_form_fields(by_step=by_step)
-
-    def get_existing_thumbnails(self, path):
-        thumbnails_paths = json.loads(self.thumbnails_by_path).get(path, {})
-        for thumbnail_path in thumbnails_paths.values():
-            yield thumbnail_path
-
-    def get_files_by_field(self):
-        data = self.get_data(raw=True)
-        files = {}
-        for name, field in self.get_fields().items():
-            if isinstance(field, FileField):
-                path = data.get(name)
-                if path:
-                    files[name] = [path] + list(
-                        self.get_existing_thumbnails(path)
-                    )
-        return files
-
-    def get_all_files(self):
-        for paths in self.get_files_by_field().values():
-            for path in paths:
-                yield path
-
-    def delete_file(self, field_name):
-        thumbnails_by_path = json.loads(self.thumbnails_by_path)
-        for path in self.get_files_by_field().get(field_name, ()):
-            self.get_storage().delete(path)
-            if path in thumbnails_by_path:
-                del thumbnails_by_path[path]
-        self.thumbnails_by_path = json.dumps(
-            thumbnails_by_path, cls=StreamFormJSONEncoder
-        )
-        self.save()
-
-    def render_email(self, value):
-        return mark_safe('<a href="mailto:%s" target="_blank">%s</a>') % (
-            value,
-            value,
-        )
-
-    def render_link(self, value):
-        return mark_safe('<a href="%s" target="_blank">%s</a>') % (value, value)
-
-    def render_image(self, value):
-        storage = self.get_storage()
-        return mark_safe(
-            '<a href="%s" target="_blank"><img src="%s" /></a>'
-        ) % (storage.url(value), storage.url(self.get_thumbnail_path(value)))
-
-    def render_file(self, value):
-        return mark_safe('<a href="%s" target="_blank">%s</a>') % (
-            self.get_storage().url(value),
-            Path(value).name,
-        )
-
-    def format_value(self, field, value):
-        if value is None or value == "":
-            return "-"
-        new_value = self.form_page.format_value(field, value)
-        if new_value != value:
-            return new_value
-        if value is True:
-            return "Yes"
-        if value is False:
-            return "No"
-        if isinstance(value, (list, tuple)):
-            return ", ".join([self.format_value(field, item) for item in value])
-        if isinstance(value, datetime.date):
-            return value
-        if isinstance(field, EmailField):
-            return self.render_email(value)
-        if isinstance(field, URLField):
-            return self.render_link(value)
-        if isinstance(field, ImageField):
-            return self.render_image(value)
-        if isinstance(field, FileField):
-            return self.render_file(value)
-        if isinstance(value, SafeData) or hasattr(value, "__html__"):
-            return value
-        return str(value)
-
-    def format_db_field(self, field_name, raw=False):
-        method = getattr(self, "get_%s_display" % field_name, None)
-        if method is not None:
-            return method()
-        value = getattr(self, field_name)
-        if raw:
-            return value
-        return self.format_value(
-            self._meta.get_field(field_name).formfield(), value
-        )
-
-    def get_steps_data(self, raw=False):
-        steps_data = json.loads(self.form_data)
-        if raw:
-            return steps_data
-        fields_and_data_iterator = zip_longest(
-            self.get_fields(by_step=True), steps_data, fillvalue={}
-        )
-        return [
-            OrderedDict(
-                [
-                    (name, self.format_value(field, step_data.get(name)))
-                    for name, field in step_fields.items()
-                ]
-            )
-            for step_fields, step_data in fields_and_data_iterator
-        ]
-
-    def get_extra_data(self, raw=False):
-        return self.form_page.get_extra_data(self, raw=raw)
-
-    def get_data(self, raw=False, add_metadata=True):
-        steps_data = self.get_steps_data(raw=raw)
-        form_data = {}
-        form_data.update(self.get_extra_data(raw=raw))
-        for step_data in steps_data:
-            form_data.update(step_data)
-        if add_metadata:
-            form_data.update(
-                status=self.format_db_field("status", raw=raw),
-                user=self.format_db_field("user", raw=raw),
-                submit_time=self.format_db_field("submit_time", raw=raw),
-                last_modification=self.format_db_field(
-                    "last_modification", raw=raw
-                ),
-            )
-        return form_data
-
-    def steps_with_data_iterator(self, raw=False):
-        for step, step_data_fields, step_data in zip(
-            self.form_page.get_steps(),
-            self.form_page.get_data_fields(by_step=True),
-            self.get_steps_data(raw=raw),
-        ):
-            yield (
-                step,
-                [
-                    (field_name, field_label, step_data[field_name])
-                    for field_name, field_label in step_data_fields
-                ],
-            )
-
-
-@receiver(post_delete, sender=SessionFormSubmission)
-def delete_files(sender, **kwargs):
-    instance = kwargs["instance"]
-    instance.reset_step()
-    storage = instance.get_storage()
-    for path in instance.get_all_files():
-        storage.delete(path)
-
-        # Automatically deletes ancestor folders if empty.
-        directory = Path(path)
-        while directory.parent != Path(directory.root):
-            directory = directory.parent
-            try:
-                subdirectories, files = storage.listdir(directory)
-            except FileNotFoundError:
-                continue
-            if not subdirectories and not files:
-                Path(storage.path(directory)).rmdir()
-
-
-class SubmissionRevisionQuerySet(QuerySet):
-    def for_submission(self, submission):
-        return self.filter(**self.model.get_filters_for(submission))
-
-    def created(self):
-        return self.filter(type=self.model.CREATED)
-
-    def changed(self):
-        return self.filter(type=self.model.CHANGED)
-
-    def deleted(self):
-        return self.filter(type=self.model.DELETED)
-
-
-class SubmissionRevision(Model):
-    CREATED = "created"
-    CHANGED = "changed"
-    DELETED = "deleted"
-    TYPES = (
-        (CREATED, _("Created")),
-        (CHANGED, _("Changed")),
-        (DELETED, _("Deleted")),
-    )
-    type = CharField(max_length=7, choices=TYPES)
-    created_at = DateTimeField(auto_now_add=True)
-    submission_ct = ForeignKey("contenttypes.ContentType", on_delete=CASCADE)
-    submission_id = TextField()
-    submission = GenericForeignKey("submission_ct", "submission_id")
-    data = TextField()
-    summary = TextField()
-
-    objects = SubmissionRevisionQuerySet.as_manager()
-
-    class Meta:
-        ordering = ("-created_at",)
-        abstract = True
-
-    @staticmethod
-    def get_filters_for(submission):
-        return {
-            "submission_ct": ContentType.objects.get_for_model(
-                submission._meta.model
-            ),
-            "submission_id": str(submission.pk),
-        }
-
-    @classmethod
-    def diff_summary(cls, page, data1, data2):
-        diff = []
-        data_fields = page.get_data_fields()
-        hidden_types = (tuple, list, dict)
-        for k, label in data_fields:
-            value1 = data1.get(k)
-            value2 = data2.get(k)
-            if value2 == value1 or not value1 and not value2:
-                continue
-            is_hidden = isinstance(value1, hidden_types) or isinstance(
-                value2, hidden_types
-            )
-
-            # Escapes newlines as they are used as separator inside summaries.
-            if isinstance(value1, str):
-                value1 = value1.replace("\n", r"\n")
-            if isinstance(value2, str):
-                value2 = value2.replace("\n", r"\n")
-
-            if value2 and not value1:
-                diff.append(
-                    (
-                        (_("“%s” set.") % label)
-                        if is_hidden
-                        else (_("“%s” set to “%s”.")) % (label, value2)
-                    )
-                )
-            elif value1 and not value2:
-                diff.append(_("“%s” unset.") % label)
-            else:
-                diff.append(
-                    (
-                        (_("“%s” changed.") % label)
-                        if is_hidden
-                        else (
-                            _("“%s” changed from “%s” to “%s”.")
-                            % (label, value1, value2)
-                        )
-                    )
-                )
-        return "\n".join(diff)
-
-    @classmethod
-    def create_from_submission(cls, submission, revision_type):
-        page = submission.form_page
-        try:
-            previous = cls.objects.for_submission(submission).latest(
-                "created_at"
-            )
-        except cls.DoesNotExist:
-            previous_data = {}
-        else:
-            previous_data = previous.get_data()
-        filters = cls.get_filters_for(submission)
-        data = submission.get_data(raw=True, add_metadata=False)
-        data["status"] = submission.status
-        if revision_type == cls.CREATED:
-            summary = _("Submission created.")
-        elif revision_type == cls.DELETED:
-            summary = _("Submission deleted.")
-        else:
-            summary = cls.diff_summary(page, previous_data, data)
-        if not summary:  # Nothing changed.
-            return
-        filters.update(
-            type=revision_type,
-            data=json.dumps(data, cls=StreamFormJSONEncoder),
-            summary=summary,
-        )
-        return cls.objects.create(**filters)
-
-    def get_data(self):
-        return json.loads(self.data)
-
-
-# ORIGINAL NORIPYT CODE.
-# We don't want these receivers triggering.
-
-# @receiver(post_save)
-# def create_submission_changed_revision(sender, **kwargs):
-#     if not issubclass(sender, SessionFormSubmission):
-#         return
-#     submission = kwargs['instance']
-#     created = kwargs['created']
-#     SubmissionRevision.create_from_submission(
-#         submission, (SubmissionRevision.CREATED if created
-#                      else SubmissionRevision.CHANGED))
-
-
-# @receiver(post_delete)
-# def create_submission_deleted_revision(sender, **kwargs):
-#     if not issubclass(sender, SessionFormSubmission):
-#         return
-#     submission = kwargs['instance']
-#     SubmissionRevision.create_from_submission(submission,
-#                                               SubmissionRevision.DELETED)
-
-
-class StreamFormMixin:
-    @property
-    def current_step_session_key(self):
-        return "%s:step" % self.pk
-
-    def get_steps(self, request=None):
-        if not hasattr(self, "steps"):
-            steps = Steps(self, request=request)
-            if request is None:
-                return steps
-            self.steps = steps
-        return self.steps
-
-    def get_form_fields(self, by_step=False):
-        if by_step:
-            return [step.get_form_fields() for step in self.get_steps()]
-        form_fields = OrderedDict()
-        for step_fields in self.get_form_fields(by_step=True):
-            form_fields.update(step_fields)
-        return form_fields
-
-    def get_context(self, request, *args, **kwargs):
-        context = super().get_context(request, *args, **kwargs)
-        self.steps = self.get_steps(request)
-        step_value = request.GET.get("step")
-        if step_value is not None and step_value.isdigit():
-            self.steps.current = int(step_value) - 1
-        form = self.steps.get_current_form()
-        context.update(
-            steps=self.steps,
-            step=self.steps.current,
-            form=form,
-            markups_and_bound_fields=list(
-                self.steps.current.get_markups_and_bound_fields(form)
-            ),
-        )
-        return context
-
-    def get_storage(self):
-        return default_storage
-
-    @staticmethod
-    def get_form_class_bases():
-        return (Form,)
-
-    @staticmethod
-    def get_submission_class():
-        return SessionFormSubmission
-
-    def get_submission(self, request):
-        Submission = self.get_submission_class()
-        if request.user.is_authenticated:
-            user_submission = (
-                Submission.objects.filter(user=request.user, page=self)
-                .order_by("-pk")
-                .first()
-            )
-            if user_submission is None:
-                return Submission(user=request.user, page=self, form_data="[]")
-            return user_submission
-
-        user_submission = (
-            Submission.objects.filter(
-                session_key=request.session.session_key, page=self
-            )
-            .order_by("-pk")
-            .first()
-        )
-        if user_submission is None:
-            return Submission(
-                session_key=request.session.session_key,
-                page=self,
-                form_data="[]",
-            )
-        return user_submission
-
-    def get_success_url(self):
-        form_complete_models = [
-            model
-            for model in apps.get_models()
-            if issubclass(model, FormCompleteMixin)
-        ]
-        cts = ContentType.objects.get_for_models(*form_complete_models).values()
-        first_child = self.get_children().filter(content_type__in=cts).first()
-        if first_child is None:
-            return self.url
-        return first_child.url
-
-    def serve_success(self, request, *args, **kwargs):
-        url = self.get_success_url()
-        if url == self.url:
-            messages.success(request, _("Successfully submitted the form."))
-        return HttpResponseRedirect(url)
-
-    def serve(self, request, *args, **kwargs):
-        context = self.get_context(request)
-        form = context["form"]
-        if request.method == "POST" and form.is_valid():
-            is_complete = self.steps.update_data()
-            if is_complete:
-                return self.serve_success(request, *args, **kwargs)
-            return HttpResponseRedirect(self.url)
-        return super().serve(self, request, *args, **kwargs)
-
-    def get_data_fields(self, by_step=False, add_metadata=True):
-        if by_step:
-            return [
-                [
-                    (field_name, field.label)
-                    for field_name, field in step_fields.items()
-                ]
-                for step_fields in self.get_form_fields(by_step=True)
-            ]
-
-        data_fields = []
-        data_fields.extend(self.get_extra_data_fields())
-        if add_metadata:
-            data_fields.extend(
-                (
-                    ("status", _("Status")),
-                    ("user", _("User")),
-                    ("submit_time", _("First modification")),
-                    ("last_modification", _("Last modification")),
-                )
-            )
-        data_fields.extend(
-            [
-                (field_name, field_label)
-                for step_data_fields in self.get_data_fields(by_step=True)
-                for field_name, field_label in step_data_fields
-            ]
-        )
-        return data_fields
-
-    def get_extra_data_fields(self):
-        return ()
-
-    def get_extra_data(self, submission, raw=False):
-        return {}
-
-    def format_value(self, field, value):
-        return value
-
-
-class ClosingFormMixin(Model):
-    closing_at = DateTimeField()
-
-    closed_template = None
-
-    class Meta:
-        abstract = True
-
-    @property
-    def is_closed(self):
-        return now() > self.closing_at
-
-    def get_closed_template(self, request, *args, **kwargs):
-        if self.closed_template is None:
-            template = self.get_template(request, *args, **kwargs)
-            base, ext = os.path.splitext(template)
-            return "%s_closed%s" % (base, ext)
-        return self.closed_template
-
-    def serve_closed(self, request, *args, **kwargs):
-        return TemplateResponse(
-            request,
-            self.get_closed_template(request, *args, **kwargs),
-            self.get_context(request, *args, **kwargs),
-        )
-
-    def serve(self, request, *args, **kwargs):
-        if self.is_closed:
-            return self.serve_closed(request, *args, **kwargs)
-        return super().serve(request, *args, **kwargs)
-
-
-class FormCompleteMixin:
-    def get_form_page(self):
-        return self.get_parent().specific
-
-    def serve(self, request, *args, **kwargs):
-        form_page = self.get_form_page()
-        if (
-            isinstance(form_page, LoginRequiredMixin)
-            and not request.user.is_authenticated()
-        ):
-            return HttpResponseRedirect(form_page.url)
-        self.submission = form_page.get_submission(request)
-        if (
-            self.submission is not None
-            and self.submission.is_complete
-            or getattr(request, "is_preview", False)
-        ):
-            return super().serve(request, *args, **kwargs)
-        return HttpResponseRedirect(form_page.url)
-
-    def get_context(self, *args, **kwargs):
-        context = super().get_context(*args, **kwargs)
-        if hasattr(self, "submission"):
-            context["submission"] = self.submission
-        return context
-
-
-class LoginRequiredMixin:
-    login_required_template = None
-
-    def get_login_required_template(self, request, *args, **kwargs):
-        if self.login_required_template is None:
-            template = self.get_template(request, *args, **kwargs)
-            base, ext = os.path.splitext(template)
-            return "%s_login_required%s" % (base, ext)
-        return self.login_required_template
-
-    def serve_login_required(self, request, *args, **kwargs):
-        return TemplateResponse(
-            request,
-            self.get_login_required_template(request, *args, **kwargs),
-            self.get_context(request, *args, **kwargs),
-        )
-
-    def serve(self, request, *args, **kwargs):
-        if not request.user.is_authenticated():
-            return self.serve_login_required(request, *args, **kwargs)
-        return super().serve(request, *args, **kwargs)
-
-
-class AbstractStreamForm(StreamFormMixin, AbstractForm):
-    class Meta:
-        abstract = True
-
-
-class AbstractEmailStreamForm(StreamFormMixin, AbstractEmailForm):
-    class Meta:
-        abstract = True

+ 17 - 7
coderedcms/wagtail_hooks.py

@@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
 from wagtail import hooks
 from wagtail.admin.menu import MenuItem
 from wagtail.models import get_page_models
-from wagtail.permission_policies.pages import PagePermissionPolicy
+from wagtail.permissions import page_permission_policy
 from wagtailcache.cache import clear_cache
 
 from coderedcms import __version__
@@ -84,22 +84,32 @@ def crx_forms(user, editable_forms):
     of its existence. Essentially this is a fork of wagtail.contrib.forms.get_forms_for_user()
     and wagtail.contrib.forms.get_form_types()
     """
-    from wagtail.contrib.forms.models import FormMixin
 
     from coderedcms.models import CoderedFormMixin
 
+    # Get content types of pages that inherit from CRX mixins.
     form_models = [
         model
         for model in get_page_models()
-        if issubclass(model, (FormMixin, CoderedFormMixin))
+        if issubclass(model, (CoderedFormMixin,))
     ]
     form_types = list(ContentType.objects.get_for_models(*form_models).values())
-    editable_forms = PagePermissionPolicy().instances_user_has_permission_for(
-        user, "change"
+
+    # Get all pages this user can access.
+    all_editable_pages = (
+        page_permission_policy.instances_user_has_permission_for(user, "change")
+    )
+    crx_editable_forms = all_editable_pages.filter(content_type__in=form_types)
+
+    # Combine the previous hook's ``editable_forms`` with our ``editable_forms``.
+    combined_forms_pks = list(
+        crx_editable_forms.values_list("pk", flat=True)
+    ) + list(editable_forms.values_list("pk", flat=True))
+    combined_editable_forms = all_editable_pages.filter(
+        pk__in=combined_forms_pks
     )
-    editable_forms = editable_forms.filter(content_type__in=form_types)
 
-    return editable_forms
+    return combined_editable_forms
 
 
 class ImportExportMenuItem(MenuItem):

+ 1 - 6
docs/features/page_types/stream_forms.rst

@@ -1,15 +1,10 @@
 Stream Forms
 ============
 
-Wagtail CRX integrates with ``wagtail_flexible_forms`` (https://github.com/noripyt/wagtail-flexible-forms).
+Wagtail CRX integrates with `wagtail-flexible-forms <https://github.com/coderedcorp/wagtail-flexible-forms>`_.
 A Stream Form enables forms built from StreamFields for advanced functionality such as multi-step forms,
 mixed content and form fields, and conditional logic.
 
-.. note::
-    Underlying functionality of Stream Forms may change in future versions as ``wagtail_flexible_forms``
-    is planned to be merged directly into Wagtail. We recommend using the simpler ``CoderedFormPage``
-    for forms that do not require advanced functionality (such as contact forms, etc.).
-
 
 Usage
 -----

+ 1 - 0
pyproject.toml

@@ -40,6 +40,7 @@ dependencies = [
     "icalendar==6.0.*",
     "wagtail>=6.3,<7.0",
     "wagtail-cache>=2.4,<3",
+    "wagtail-flexible-forms==2.*",
     "wagtail-seo>=2.5,<3",
 ]
 description = "Wagtail + CodeRed Extensions enabling rapid development of marketing-focused websites."