We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Preview error when using StreamField with EditHandler on per-request basis
### Issue Summary

We're using `EditHandler`s, which show/hide fields depending on the user's permission. This is based on the example in wagtail's tests (see https://github.com/wagtail/wagtail/blob/main/wagtail/tests/testapp/models.py#L1418).

**Problem:**
Unfortunately, there are multiple places in wagtail's codebase, where `BaseFormEditHandler.get_form_class()` is called, without binding the current request (and also instance etc). One example is the preview-mode (see [preview.py](https://github.com/wagtail/wagtail/blob/main/wagtail/admin/views/pages/preview.py#L51)).

As a result, a user which can only view a subset of the full admin form of a page for example, actually validates his POST-data (which only contains a subset of the fields) with the full form, since the logic that would hide some fields based on the request isn't called.

This probably works for simple cases like [wagtail's test](https://github.com/wagtail/wagtail/blob/main/wagtail/tests/testapp/models.py#L1442) and described in multiple tickets (e.g. https://github.com/wagtail/wagtail/issues/5546), but in our case it results in crashes when showing/hiding `StreamField`s.

**Solution:**
At least bind `instance` and `request` before `get_form_class()` is called, where possible, similar to how the normal edit-view does it: https://github.com/wagtail/wagtail/blob/main/wagtail/admin/views/pages/edit.py#L311

I will try to add those calls to all the places I can find and let the tests run and I hope this can be fixed quickly.


### Steps to Reproduce

I don't have a simple example available yet, but basically we have a custom `BaseCompositeEditHandler` which may skip some of its children, if the user is not a superuser. This is very similar to the [sample](https://github.com/wagtail/wagtail/blob/main/wagtail/tests/testapp/models.py#L1393) in wagtail's tests, but instead of switching between two panel-lists, we have one default list, and based on the field-keys we may exclude some of those fields.

So this all works for the superuser, since he can edit all of the fields. But if one of those excluded fields is a `StreamField` and the non-superuser clicks on the preview button, the server crashes with something like that:

    2021-09-24 11:43:13 ERROR django.request: Internal Server Error: /admin/content/pages/add/base/testpage/1337/preview/ (log.py:224)
    Traceback (most recent call last):
      File "/****/lib/python3.8/site-packages/django/utils/datastructures.py", line 76, in __getitem__
        list_ = super().__getitem__(key)
    KeyError: 'form_element_blocks-count'

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    ...****...
        return view_func(request, *args, **kwargs)
      File "/****/lib/python3.8/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
        response = view_func(request, *args, **kwargs)
      File "/****/lib/python3.8/site-packages/wagtail/admin/urls/__init__.py", line 121, in wrapper
        return view_func(request, *args, **kwargs)
      File "/****/lib/python3.8/site-packages/wagtail/admin/auth.py", line 193, in decorated_view
        return view_func(request, *args, **kwargs)
      File "/****/lib/python3.8/site-packages/django/views/generic/base.py", line 70, in view
        return self.dispatch(request, *args, **kwargs)
      File "/****/lib/python3.8/site-packages/django/views/generic/base.py", line 98, in dispatch
        return handler(request, *args, **kwargs)
      File "/****/lib/python3.8/site-packages/wagtail/admin/views/pages/preview.py", line 64, in post
        form = self.get_form(self.get_page(), request.POST)
      File "/****/lib/python3.8/site-packages/wagtail/admin/views/pages/preview.py", line 129, in get_form
        if form.is_valid():
      File "/****/lib/python3.8/site-packages/wagtail/admin/forms/pages.py", line 150, in is_valid
        return super().is_valid()
      File "/****/lib/python3.8/site-packages/modelcluster/forms.py", line 316, in is_valid
        form_is_valid = super(ClusterForm, self).is_valid()
      File "/****/lib/python3.8/site-packages/django/forms/forms.py", line 175, in is_valid
        return self.is_bound and not self.errors
      File "/****/lib/python3.8/site-packages/django/forms/forms.py", line 170, in errors
        self.full_clean()
      File "/****/lib/python3.8/site-packages/django/forms/forms.py", line 372, in full_clean
        self._clean_fields()
      File "/****/lib/python3.8/site-packages/django/forms/forms.py", line 384, in _clean_fields
        value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
      File "/****/lib/python3.8/site-packages/wagtail/core/blocks/base.py", line 535, in value_from_datadict
        return self.block_def.value_from_datadict(data, files, name)
      File "/****/lib/python3.8/site-packages/wagtail/core/blocks/stream_block.py", line 97, in value_from_datadict
        count = int(data['%s-count' % prefix])
      File "/****/lib/python3.8/site-packages/django/utils/datastructures.py", line 78, in __getitem__
        raise MultiValueDictKeyError(key)
    django.utils.datastructures.MultiValueDictKeyError: 'form_element_blocks-count'
    2021-09-24 11:43:13 ERROR django.channels.server: HTTP POST /admin/content/pages/add/base/testpage/1337/preview/ 500 [0.15, 127.0.0.1:44074] (runserver.py:160)

In the above stacktrace, the `form_element_blocks` (which is a `StreamField`) are not visible to the non-superuser user in the frontend. But the preview-request actually creates a form with the `StreamField` (because our code in `on_request_bound()` isn't called), which fails when validating against the POST-data, which doesn't contain any of the `StreamField`'s POST-data.


### Technical details

* Python version: 3.8.10
* Django version: 3.2.7
* Wagtail version: 2.14.1


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `wagtail/admin/views/pages/preview.py`
Content:
```
1 from time import time
2 
3 from django.contrib.contenttypes.models import ContentType
4 from django.core.exceptions import PermissionDenied
5 from django.http import Http404, JsonResponse
6 from django.http.request import QueryDict
7 from django.shortcuts import get_object_or_404
8 from django.template.response import TemplateResponse
9 from django.views.generic import View
10 
11 from wagtail.core.models import Page
12 
13 
14 def view_draft(request, page_id):
15     page = get_object_or_404(Page, id=page_id).get_latest_revision_as_page()
16     perms = page.permissions_for_user(request.user)
17     if not (perms.can_publish() or perms.can_edit()):
18         raise PermissionDenied
19 
20     try:
21         preview_mode = page.default_preview_mode
22     except IndexError:
23         raise PermissionDenied
24 
25     return page.make_preview_request(request, preview_mode)
26 
27 
28 class PreviewOnEdit(View):
29     http_method_names = ('post', 'get')
30     preview_expiration_timeout = 60 * 60 * 24  # seconds
31     session_key_prefix = 'wagtail-preview-'
32 
33     def remove_old_preview_data(self):
34         expiration = time() - self.preview_expiration_timeout
35         expired_keys = [
36             k for k, v in self.request.session.items()
37             if k.startswith(self.session_key_prefix) and v[1] < expiration]
38         # Removes the session key gracefully
39         for k in expired_keys:
40             self.request.session.pop(k)
41 
42     @property
43     def session_key(self):
44         return '{}{}'.format(self.session_key_prefix, self.kwargs['page_id'])
45 
46     def get_page(self):
47         return get_object_or_404(Page,
48                                  id=self.kwargs["page_id"]).get_latest_revision_as_page()
49 
50     def get_form(self, page, query_dict):
51         form_class = page.get_edit_handler().get_form_class()
52         parent_page = page.get_parent().specific
53 
54         if self.session_key not in self.request.session:
55             # Session key not in session, returning null form
56             return form_class(instance=page, parent_page=parent_page)
57 
58         return form_class(query_dict, instance=page, parent_page=parent_page)
59 
60     def post(self, request, *args, **kwargs):
61         # TODO: Handle request.FILES.
62         request.session[self.session_key] = request.POST.urlencode(), time()
63         self.remove_old_preview_data()
64         form = self.get_form(self.get_page(), request.POST)
65         return JsonResponse({'is_valid': form.is_valid()})
66 
67     def error_response(self, page):
68         return TemplateResponse(
69             self.request, 'wagtailadmin/pages/preview_error.html',
70             {'page': page}
71         )
72 
73     def get(self, request, *args, **kwargs):
74         page = self.get_page()
75 
76         post_data, timestamp = self.request.session.get(self.session_key,
77                                                         (None, None))
78         if not isinstance(post_data, str):
79             post_data = ''
80         form = self.get_form(page, QueryDict(post_data))
81 
82         if not form.is_valid():
83             return self.error_response(page)
84 
85         form.save(commit=False)
86 
87         try:
88             preview_mode = request.GET.get('mode', page.default_preview_mode)
89         except IndexError:
90             raise PermissionDenied
91 
92         return page.make_preview_request(request, preview_mode)
93 
94 
95 class PreviewOnCreate(PreviewOnEdit):
96     @property
97     def session_key(self):
98         return '{}{}-{}-{}'.format(
99             self.session_key_prefix,
100             self.kwargs['content_type_app_name'],
101             self.kwargs['content_type_model_name'],
102             self.kwargs['parent_page_id'],
103         )
104 
105     def get_page(self):
106         content_type_app_name = self.kwargs["content_type_app_name"]
107         content_type_model_name = self.kwargs["content_type_model_name"]
108         parent_page_id = self.kwargs["parent_page_id"]
109         try:
110             content_type = ContentType.objects.get_by_natural_key(
111                 content_type_app_name, content_type_model_name)
112         except ContentType.DoesNotExist:
113             raise Http404
114 
115         page = content_type.model_class()()
116         parent_page = get_object_or_404(Page, id=parent_page_id).specific
117         # We need to populate treebeard's path / depth fields in order to
118         # pass validation. We can't make these 100% consistent with the rest
119         # of the tree without making actual database changes (such as
120         # incrementing the parent's numchild field), but by calling treebeard's
121         # internal _get_path method, we can set a 'realistic' value that will
122         # hopefully enable tree traversal operations
123         # to at least partially work.
124         page.depth = parent_page.depth + 1
125         # Puts the page at the next available path
126         # for a child of `parent_page`.
127         if parent_page.is_leaf():
128             # set the path as the first child of parent_page
129             page.path = page._get_path(parent_page.path, page.depth, 1)
130         else:
131             # add the new page after the last child of parent_page
132             page.path = parent_page.get_last_child()._inc_path()
133 
134         return page
135 
136     def get_form(self, page, query_dict):
137         form = super().get_form(page, query_dict)
138         if form.is_valid():
139             # Ensures our unsaved page has a suitable url.
140             form.instance.set_url_path(form.parent_page)
141 
142             form.instance.full_clean()
143         return form
144
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/wagtail/admin/views/pages/preview.py b/wagtail/admin/views/pages/preview.py
--- a/wagtail/admin/views/pages/preview.py
+++ b/wagtail/admin/views/pages/preview.py
@@ -48,7 +48,7 @@
                                  id=self.kwargs["page_id"]).get_latest_revision_as_page()
 
     def get_form(self, page, query_dict):
-        form_class = page.get_edit_handler().get_form_class()
+        form_class = page.get_edit_handler().bind_to(instance=page, request=self.request).get_form_class()
         parent_page = page.get_parent().specific
 
         if self.session_key not in self.request.session:

{"golden_diff": "diff --git a/wagtail/admin/views/pages/preview.py b/wagtail/admin/views/pages/preview.py\n--- a/wagtail/admin/views/pages/preview.py\n+++ b/wagtail/admin/views/pages/preview.py\n@@ -48,7 +48,7 @@\n                                  id=self.kwargs[\"page_id\"]).get_latest_revision_as_page()\n \n     def get_form(self, page, query_dict):\n-        form_class = page.get_edit_handler().get_form_class()\n+        form_class = page.get_edit_handler().bind_to(instance=page, request=self.request).get_form_class()\n         parent_page = page.get_parent().specific\n \n         if self.session_key not in self.request.session:\n", "issue": "Preview error when using StreamField with EditHandler on per-request basis\n### Issue Summary\r\n\r\nWe're using `EditHandler`s, which show/hide fields depending on the user's permission. This is based on the example in wagtail's tests (see https://github.com/wagtail/wagtail/blob/main/wagtail/tests/testapp/models.py#L1418).\r\n\r\n**Problem:**\r\nUnfortunately, there are multiple places in wagtail's codebase, where `BaseFormEditHandler.get_form_class()` is called, without binding the current request (and also instance etc). One example is the preview-mode (see [preview.py](https://github.com/wagtail/wagtail/blob/main/wagtail/admin/views/pages/preview.py#L51)).\r\n\r\nAs a result, a user which can only view a subset of the full admin form of a page for example, actually validates his POST-data (which only contains a subset of the fields) with the full form, since the logic that would hide some fields based on the request isn't called.\r\n\r\nThis probably works for simple cases like [wagtail's test](https://github.com/wagtail/wagtail/blob/main/wagtail/tests/testapp/models.py#L1442) and described in multiple tickets (e.g. https://github.com/wagtail/wagtail/issues/5546), but in our case it results in crashes when showing/hiding `StreamField`s.\r\n\r\n**Solution:**\r\nAt least bind `instance` and `request` before `get_form_class()` is called, where possible, similar to how the normal edit-view does it: https://github.com/wagtail/wagtail/blob/main/wagtail/admin/views/pages/edit.py#L311\r\n\r\nI will try to add those calls to all the places I can find and let the tests run and I hope this can be fixed quickly.\r\n\r\n\r\n### Steps to Reproduce\r\n\r\nI don't have a simple example available yet, but basically we have a custom `BaseCompositeEditHandler` which may skip some of its children, if the user is not a superuser. This is very similar to the [sample](https://github.com/wagtail/wagtail/blob/main/wagtail/tests/testapp/models.py#L1393) in wagtail's tests, but instead of switching between two panel-lists, we have one default list, and based on the field-keys we may exclude some of those fields.\r\n\r\nSo this all works for the superuser, since he can edit all of the fields. But if one of those excluded fields is a `StreamField` and the non-superuser clicks on the preview button, the server crashes with something like that:\r\n\r\n    2021-09-24 11:43:13 ERROR django.request: Internal Server Error: /admin/content/pages/add/base/testpage/1337/preview/ (log.py:224)\r\n    Traceback (most recent call last):\r\n      File \"/****/lib/python3.8/site-packages/django/utils/datastructures.py\", line 76, in __getitem__\r\n        list_ = super().__getitem__(key)\r\n    KeyError: 'form_element_blocks-count'\r\n\r\n    During handling of the above exception, another exception occurred:\r\n\r\n    Traceback (most recent call last):\r\n    ...****...\r\n        return view_func(request, *args, **kwargs)\r\n      File \"/****/lib/python3.8/site-packages/django/views/decorators/cache.py\", line 44, in _wrapped_view_func\r\n        response = view_func(request, *args, **kwargs)\r\n      File \"/****/lib/python3.8/site-packages/wagtail/admin/urls/__init__.py\", line 121, in wrapper\r\n        return view_func(request, *args, **kwargs)\r\n      File \"/****/lib/python3.8/site-packages/wagtail/admin/auth.py\", line 193, in decorated_view\r\n        return view_func(request, *args, **kwargs)\r\n      File \"/****/lib/python3.8/site-packages/django/views/generic/base.py\", line 70, in view\r\n        return self.dispatch(request, *args, **kwargs)\r\n      File \"/****/lib/python3.8/site-packages/django/views/generic/base.py\", line 98, in dispatch\r\n        return handler(request, *args, **kwargs)\r\n      File \"/****/lib/python3.8/site-packages/wagtail/admin/views/pages/preview.py\", line 64, in post\r\n        form = self.get_form(self.get_page(), request.POST)\r\n      File \"/****/lib/python3.8/site-packages/wagtail/admin/views/pages/preview.py\", line 129, in get_form\r\n        if form.is_valid():\r\n      File \"/****/lib/python3.8/site-packages/wagtail/admin/forms/pages.py\", line 150, in is_valid\r\n        return super().is_valid()\r\n      File \"/****/lib/python3.8/site-packages/modelcluster/forms.py\", line 316, in is_valid\r\n        form_is_valid = super(ClusterForm, self).is_valid()\r\n      File \"/****/lib/python3.8/site-packages/django/forms/forms.py\", line 175, in is_valid\r\n        return self.is_bound and not self.errors\r\n      File \"/****/lib/python3.8/site-packages/django/forms/forms.py\", line 170, in errors\r\n        self.full_clean()\r\n      File \"/****/lib/python3.8/site-packages/django/forms/forms.py\", line 372, in full_clean\r\n        self._clean_fields()\r\n      File \"/****/lib/python3.8/site-packages/django/forms/forms.py\", line 384, in _clean_fields\r\n        value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))\r\n      File \"/****/lib/python3.8/site-packages/wagtail/core/blocks/base.py\", line 535, in value_from_datadict\r\n        return self.block_def.value_from_datadict(data, files, name)\r\n      File \"/****/lib/python3.8/site-packages/wagtail/core/blocks/stream_block.py\", line 97, in value_from_datadict\r\n        count = int(data['%s-count' % prefix])\r\n      File \"/****/lib/python3.8/site-packages/django/utils/datastructures.py\", line 78, in __getitem__\r\n        raise MultiValueDictKeyError(key)\r\n    django.utils.datastructures.MultiValueDictKeyError: 'form_element_blocks-count'\r\n    2021-09-24 11:43:13 ERROR django.channels.server: HTTP POST /admin/content/pages/add/base/testpage/1337/preview/ 500 [0.15, 127.0.0.1:44074] (runserver.py:160)\r\n\r\nIn the above stacktrace, the `form_element_blocks` (which is a `StreamField`) are not visible to the non-superuser user in the frontend. But the preview-request actually creates a form with the `StreamField` (because our code in `on_request_bound()` isn't called), which fails when validating against the POST-data, which doesn't contain any of the `StreamField`'s POST-data.\r\n\r\n\r\n### Technical details\r\n\r\n* Python version: 3.8.10\r\n* Django version: 3.2.7\r\n* Wagtail version: 2.14.1\r\n\n", "before_files": [{"content": "from time import time\n\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.core.exceptions import PermissionDenied\nfrom django.http import Http404, JsonResponse\nfrom django.http.request import QueryDict\nfrom django.shortcuts import get_object_or_404\nfrom django.template.response import TemplateResponse\nfrom django.views.generic import View\n\nfrom wagtail.core.models import Page\n\n\ndef view_draft(request, page_id):\n    page = get_object_or_404(Page, id=page_id).get_latest_revision_as_page()\n    perms = page.permissions_for_user(request.user)\n    if not (perms.can_publish() or perms.can_edit()):\n        raise PermissionDenied\n\n    try:\n        preview_mode = page.default_preview_mode\n    except IndexError:\n        raise PermissionDenied\n\n    return page.make_preview_request(request, preview_mode)\n\n\nclass PreviewOnEdit(View):\n    http_method_names = ('post', 'get')\n    preview_expiration_timeout = 60 * 60 * 24  # seconds\n    session_key_prefix = 'wagtail-preview-'\n\n    def remove_old_preview_data(self):\n        expiration = time() - self.preview_expiration_timeout\n        expired_keys = [\n            k for k, v in self.request.session.items()\n            if k.startswith(self.session_key_prefix) and v[1] < expiration]\n        # Removes the session key gracefully\n        for k in expired_keys:\n            self.request.session.pop(k)\n\n    @property\n    def session_key(self):\n        return '{}{}'.format(self.session_key_prefix, self.kwargs['page_id'])\n\n    def get_page(self):\n        return get_object_or_404(Page,\n                                 id=self.kwargs[\"page_id\"]).get_latest_revision_as_page()\n\n    def get_form(self, page, query_dict):\n        form_class = page.get_edit_handler().get_form_class()\n        parent_page = page.get_parent().specific\n\n        if self.session_key not in self.request.session:\n            # Session key not in session, returning null form\n            return form_class(instance=page, parent_page=parent_page)\n\n        return form_class(query_dict, instance=page, parent_page=parent_page)\n\n    def post(self, request, *args, **kwargs):\n        # TODO: Handle request.FILES.\n        request.session[self.session_key] = request.POST.urlencode(), time()\n        self.remove_old_preview_data()\n        form = self.get_form(self.get_page(), request.POST)\n        return JsonResponse({'is_valid': form.is_valid()})\n\n    def error_response(self, page):\n        return TemplateResponse(\n            self.request, 'wagtailadmin/pages/preview_error.html',\n            {'page': page}\n        )\n\n    def get(self, request, *args, **kwargs):\n        page = self.get_page()\n\n        post_data, timestamp = self.request.session.get(self.session_key,\n                                                        (None, None))\n        if not isinstance(post_data, str):\n            post_data = ''\n        form = self.get_form(page, QueryDict(post_data))\n\n        if not form.is_valid():\n            return self.error_response(page)\n\n        form.save(commit=False)\n\n        try:\n            preview_mode = request.GET.get('mode', page.default_preview_mode)\n        except IndexError:\n            raise PermissionDenied\n\n        return page.make_preview_request(request, preview_mode)\n\n\nclass PreviewOnCreate(PreviewOnEdit):\n    @property\n    def session_key(self):\n        return '{}{}-{}-{}'.format(\n            self.session_key_prefix,\n            self.kwargs['content_type_app_name'],\n            self.kwargs['content_type_model_name'],\n            self.kwargs['parent_page_id'],\n        )\n\n    def get_page(self):\n        content_type_app_name = self.kwargs[\"content_type_app_name\"]\n        content_type_model_name = self.kwargs[\"content_type_model_name\"]\n        parent_page_id = self.kwargs[\"parent_page_id\"]\n        try:\n            content_type = ContentType.objects.get_by_natural_key(\n                content_type_app_name, content_type_model_name)\n        except ContentType.DoesNotExist:\n            raise Http404\n\n        page = content_type.model_class()()\n        parent_page = get_object_or_404(Page, id=parent_page_id).specific\n        # We need to populate treebeard's path / depth fields in order to\n        # pass validation. We can't make these 100% consistent with the rest\n        # of the tree without making actual database changes (such as\n        # incrementing the parent's numchild field), but by calling treebeard's\n        # internal _get_path method, we can set a 'realistic' value that will\n        # hopefully enable tree traversal operations\n        # to at least partially work.\n        page.depth = parent_page.depth + 1\n        # Puts the page at the next available path\n        # for a child of `parent_page`.\n        if parent_page.is_leaf():\n            # set the path as the first child of parent_page\n            page.path = page._get_path(parent_page.path, page.depth, 1)\n        else:\n            # add the new page after the last child of parent_page\n            page.path = parent_page.get_last_child()._inc_path()\n\n        return page\n\n    def get_form(self, page, query_dict):\n        form = super().get_form(page, query_dict)\n        if form.is_valid():\n            # Ensures our unsaved page has a suitable url.\n            form.instance.set_url_path(form.parent_page)\n\n            form.instance.full_clean()\n        return form\n", "path": "wagtail/admin/views/pages/preview.py"}], "after_files": [{"content": "from time import time\n\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.core.exceptions import PermissionDenied\nfrom django.http import Http404, JsonResponse\nfrom django.http.request import QueryDict\nfrom django.shortcuts import get_object_or_404\nfrom django.template.response import TemplateResponse\nfrom django.views.generic import View\n\nfrom wagtail.core.models import Page\n\n\ndef view_draft(request, page_id):\n    page = get_object_or_404(Page, id=page_id).get_latest_revision_as_page()\n    perms = page.permissions_for_user(request.user)\n    if not (perms.can_publish() or perms.can_edit()):\n        raise PermissionDenied\n\n    try:\n        preview_mode = page.default_preview_mode\n    except IndexError:\n        raise PermissionDenied\n\n    return page.make_preview_request(request, preview_mode)\n\n\nclass PreviewOnEdit(View):\n    http_method_names = ('post', 'get')\n    preview_expiration_timeout = 60 * 60 * 24  # seconds\n    session_key_prefix = 'wagtail-preview-'\n\n    def remove_old_preview_data(self):\n        expiration = time() - self.preview_expiration_timeout\n        expired_keys = [\n            k for k, v in self.request.session.items()\n            if k.startswith(self.session_key_prefix) and v[1] < expiration]\n        # Removes the session key gracefully\n        for k in expired_keys:\n            self.request.session.pop(k)\n\n    @property\n    def session_key(self):\n        return '{}{}'.format(self.session_key_prefix, self.kwargs['page_id'])\n\n    def get_page(self):\n        return get_object_or_404(Page,\n                                 id=self.kwargs[\"page_id\"]).get_latest_revision_as_page()\n\n    def get_form(self, page, query_dict):\n        form_class = page.get_edit_handler().bind_to(instance=page, request=self.request).get_form_class()\n        parent_page = page.get_parent().specific\n\n        if self.session_key not in self.request.session:\n            # Session key not in session, returning null form\n            return form_class(instance=page, parent_page=parent_page)\n\n        return form_class(query_dict, instance=page, parent_page=parent_page)\n\n    def post(self, request, *args, **kwargs):\n        # TODO: Handle request.FILES.\n        request.session[self.session_key] = request.POST.urlencode(), time()\n        self.remove_old_preview_data()\n        form = self.get_form(self.get_page(), request.POST)\n        return JsonResponse({'is_valid': form.is_valid()})\n\n    def error_response(self, page):\n        return TemplateResponse(\n            self.request, 'wagtailadmin/pages/preview_error.html',\n            {'page': page}\n        )\n\n    def get(self, request, *args, **kwargs):\n        page = self.get_page()\n\n        post_data, timestamp = self.request.session.get(self.session_key,\n                                                        (None, None))\n        if not isinstance(post_data, str):\n            post_data = ''\n        form = self.get_form(page, QueryDict(post_data))\n\n        if not form.is_valid():\n            return self.error_response(page)\n\n        form.save(commit=False)\n\n        try:\n            preview_mode = request.GET.get('mode', page.default_preview_mode)\n        except IndexError:\n            raise PermissionDenied\n\n        return page.make_preview_request(request, preview_mode)\n\n\nclass PreviewOnCreate(PreviewOnEdit):\n    @property\n    def session_key(self):\n        return '{}{}-{}-{}'.format(\n            self.session_key_prefix,\n            self.kwargs['content_type_app_name'],\n            self.kwargs['content_type_model_name'],\n            self.kwargs['parent_page_id'],\n        )\n\n    def get_page(self):\n        content_type_app_name = self.kwargs[\"content_type_app_name\"]\n        content_type_model_name = self.kwargs[\"content_type_model_name\"]\n        parent_page_id = self.kwargs[\"parent_page_id\"]\n        try:\n            content_type = ContentType.objects.get_by_natural_key(\n                content_type_app_name, content_type_model_name)\n        except ContentType.DoesNotExist:\n            raise Http404\n\n        page = content_type.model_class()()\n        parent_page = get_object_or_404(Page, id=parent_page_id).specific\n        # We need to populate treebeard's path / depth fields in order to\n        # pass validation. We can't make these 100% consistent with the rest\n        # of the tree without making actual database changes (such as\n        # incrementing the parent's numchild field), but by calling treebeard's\n        # internal _get_path method, we can set a 'realistic' value that will\n        # hopefully enable tree traversal operations\n        # to at least partially work.\n        page.depth = parent_page.depth + 1\n        # Puts the page at the next available path\n        # for a child of `parent_page`.\n        if parent_page.is_leaf():\n            # set the path as the first child of parent_page\n            page.path = page._get_path(parent_page.path, page.depth, 1)\n        else:\n            # add the new page after the last child of parent_page\n            page.path = parent_page.get_last_child()._inc_path()\n\n        return page\n\n    def get_form(self, page, query_dict):\n        form = super().get_form(page, query_dict)\n        if form.is_valid():\n            # Ensures our unsaved page has a suitable url.\n            form.instance.set_url_path(form.parent_page)\n\n            form.instance.full_clean()\n        return form\n", "path": "wagtail/admin/views/pages/preview.py"}]}