Repository: wagtail/wagtail

Issue:
Wagtail 2.15 Bulk Publish for Inherited Page
Found a bug? Please fill out the sections below. 👍

### Issue Summary
Assume we have two classes:
class A(Page):
  name = models.CharField(max_length=255)

class B(A):
  nickname = models.CharField(max_length=255)

When do bulk action of publishing the pages with B, an error will occur:
![image](https://user-images.githubusercontent.com/81170508/140770606-161a016f-52c2-4bda-b45d-0cd78ba99310.png)

The existing solution I think is to customize bulk action according to https://docs.wagtail.io/en/stable/extending/custom_bulk_actions.html#custom-bulk-actions
But I would like to know if there is other solution?


Discussion:
Use case:

1. Simple page model

```python
class MyPage(Page):
    pass
```

2. create a draft using the above
3. add required field
```python
class MyPage(Page):
    subtitle = models.CharField(max_length=255)

    content_panels = Page.content_panels + [FieldPanel("subtitle")]
```
4. try to publish via bulk actions 💥 
<details>
<summary>Traceback</summary>

```
Traceback (most recent call last):
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/wagtail/admin/urls/__init__.py", line 125, in wrapper
    return view_func(request, *args, **kwargs)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/wagtail/admin/auth.py", line 174, in decorated_view
    response = view_func(request, *args, **kwargs)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/wagtail/admin/views/bulk_action/dispatcher.py", line 11, in index
    return action_class(request, model).dispatch(request)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/django/views/generic/base.py", line 97, in dispatch
    return handler(request, *args, **kwargs)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/django/views/generic/edit.py", line 142, in post
    return self.form_valid(form)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/wagtail/admin/views/bulk_action/base_bulk_action.py", line 135, in form_valid
    num_parent_objects, num_child_objects = self.execute_action(objects, **self.get_execution_context())
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/wagtail/admin/views/pages/bulk_actions/publish.py", line 38, in execute_action
    revision.publish(user=user)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/wagtail/core/models/__init__.py", line 2595, in publish
    page.save()
  File "/usr/lib/python3.6/contextlib.py", line 52, in inner
    return func(*args, **kwds)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/wagtail/core/models/__init__.py", line 482, in save
    self.full_clean()
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/wagtail/core/models/__init__.py", line 441, in full_clean
    super().full_clean(*args, **kwargs)
  File "/home/vagrant/.virtualenvs/bakerydemo/lib/python3.6/site-packages/django/db/models/base.py", line 1222, in full_clean
    raise ValidationError(errors)

Exception Type: ValidationError at /admin/bulk/wagtailcore/page/publish/
Exception Value: {'subtitle': ['This field cannot be blank.']}
```
</details>
If the steps to reproduce are indeed as @zerolab describes, then this is _arguably_ logically correct, and could happen anywhere that a page is published without going through the edit form (e.g. a moderator approving it from the dashboard or edit bird, and possibly also scheduled publishing). It arises because draft / submitted revisions are saved as JSON in wagtailcore_pagerevisions, where the schema change doesn't affect them. To be absolutely correct, the developer making the change would have to provide a data migration to update the revisions too (although, to be fair, we don't provide any documentation for that...) - since they don't, the revision no longer passes validation. And since this is developer error rather than one made by an editor, it's appropriate to throw a developer-facing error. Having said that, catching ValidationError and reporting a friendly error back to the editor ("the following pages could not be published because the validation requirements have changed since they were last saved") wouldn't hurt.

I'm not sure if that's the same bug that @aliceni81 is describing, though, since it's unrelated to page inheritance - I'd like to check whether the original bug can be replicated without any after-the-fact schema changes.
It seems the bug is more basic than either of the above cases - it doesn't require inheritance, or having a draft previously saved before adding the required field:

Starting from a new project with home/models.py edited to:
```
from django.db import models

from wagtail.core.models import Page
from wagtail.admin.edit_handlers import FieldPanel


class HomePage(Page):
    pass


class MyPage(Page):
    subtitle = models.CharField(max_length=255)
    content_panels = Page.content_panels + [FieldPanel("subtitle")]
```
run

    ./manage.py makemigrations
    ./manage.py migrate
    ./manage.py createsuperuser
    ./manage.py runserver

Log in, create a "My Page" as a child of the homepage, enter a title and subtitle, and save as draft.
Then, from the Home explorer listing, use the bulk action to publish that new page - the `{'subtitle': ['This field cannot be blank.']}` validation error occurs.

```
Environment:


Request Method: POST
Request URL: http://localhost:8000/admin/bulk/wagtailcore/page/publish/?next=%2Fadmin%2Fpages%2F3%2F&id=4

Django Version: 3.2.8
Python Version: 3.8.0
Installed Applications:
['home',
 'search',
 'wagtail.contrib.forms',
 'wagtail.contrib.redirects',
 'wagtail.embeds',
 'wagtail.sites',
 'wagtail.users',
 'wagtail.snippets',
 'wagtail.documents',
 'wagtail.images',
 'wagtail.search',
 'wagtail.admin',
 'wagtail.core',
 'modelcluster',
 'taggit',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles']
Installed Middleware:
['django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'django.middleware.security.SecurityMiddleware',
 'wagtail.contrib.redirects.middleware.RedirectMiddleware']



Traceback (most recent call last):
  File "/Users/matthew/.virtualenvs/wagtail/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/Users/matthew/.virtualenvs/wagtail/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/matthew/.virtualenvs/wagtail/lib/python3.8/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/Users/matthew/Development/tbx/wagtail/devscript/wagtail/wagtail/admin/urls/__init__.py", line 125, in wrapper
    return view_func(request, *args, **kwargs)
  File "/Users/matthew/Development/tbx/wagtail/devscript/wagtail/wagtail/admin/auth.py", line 195, in decorated_view
    return view_func(request, *args, **kwargs)
  File "/Users/matthew/Development/tbx/wagtail/devscript/wagtail/wagtail/admin/views/bulk_action/dispatcher.py", line 11, in index
    return action_class(request, model).dispatch(request)
  File "/Users/matthew/.virtualenvs/wagtail/lib/python3.8/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/Users/matthew/.virtualenvs/wagtail/lib/python3.8/site-packages/django/views/generic/edit.py", line 142, in post
    return self.form_valid(form)
  File "/Users/matthew/Development/tbx/wagtail/devscript/wagtail/wagtail/admin/views/bulk_action/base_bulk_action.py", line 135, in form_valid
    num_parent_objects, num_child_objects = self.execute_action(objects, **self.get_execution_context())
  File "/Users/matthew/Development/tbx/wagtail/devscript/wagtail/wagtail/admin/views/pages/bulk_actions/publish.py", line 38, in execute_action
    revision.publish(user=user)
  File "/Users/matthew/Development/tbx/wagtail/devscript/wagtail/wagtail/core/models/__init__.py", line 2595, in publish
    page.save()
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/contextlib.py", line 75, in inner
    return func(*args, **kwds)
  File "/Users/matthew/Development/tbx/wagtail/devscript/wagtail/wagtail/core/models/__init__.py", line 482, in save
    self.full_clean()
  File "/Users/matthew/Development/tbx/wagtail/devscript/wagtail/wagtail/core/models/__init__.py", line 441, in full_clean
    super().full_clean(*args, **kwargs)
  File "/Users/matthew/.virtualenvs/wagtail/lib/python3.8/site-packages/django/db/models/base.py", line 1238, in full_clean
    raise ValidationError(errors)

Exception Type: ValidationError at /admin/bulk/wagtailcore/page/publish/
Exception Value: {'subtitle': ['This field cannot be blank.']}
```
Even worse :-( If the specific page type _doesn't_ contain any required fields, then [this line](https://github.com/wagtail/wagtail/blob/33445188e76692419897cf3fd22dbbc64b029d90/wagtail/admin/views/pages/bulk_actions/publish.py#L37) causes it to save and publish a new revision derived from the basic `Page` object, meaning that all other fields are blanked. Looks like we're missing a `page.specific` here, and our tests are only testing basic Page instances.
Ugh, great sleuthing @gasman. Was just testing the previous case.

One option would be to use the specific qs in https://github.com/wagtail/wagtail/blob/33445188e76692419897cf3fd22dbbc64b029d90/wagtail/admin/views/pages/bulk_actions/page_bulk_action.py#L16 

Resolving patch:
diff --git a/wagtail/admin/views/pages/bulk_actions/publish.py b/wagtail/admin/views/pages/bulk_actions/publish.py
--- a/wagtail/admin/views/pages/bulk_actions/publish.py
+++ b/wagtail/admin/views/pages/bulk_actions/publish.py
@@ -34,6 +34,7 @@ def get_execution_context(self):
     def execute_action(cls, objects, include_descendants=False, user=None, **kwargs):
         num_parent_objects, num_child_objects = 0, 0
         for page in objects:
+            page = page.specific
             revision = page.save_revision(user=user)
             revision.publish(user=user)
             num_parent_objects += 1


Test patch:
diff --git a/wagtail/admin/tests/pages/test_bulk_actions/test_bulk_publish.py b/wagtail/admin/tests/pages/test_bulk_actions/test_bulk_publish.py
--- a/wagtail/admin/tests/pages/test_bulk_actions/test_bulk_publish.py
+++ b/wagtail/admin/tests/pages/test_bulk_actions/test_bulk_publish.py
@@ -9,6 +9,7 @@
 from wagtail.admin.views.pages.bulk_actions.page_bulk_action import PageBulkAction
 from wagtail.core.models import Page
 from wagtail.core.signals import page_published
+from wagtail.tests.testapp.models import SimplePage
 from wagtail.tests.utils import WagtailTestUtils
 
 
@@ -18,7 +19,7 @@ def setUp(self):
 
         # Add child pages
         self.child_pages = [
-            Page(title=f"Hello world!-{i}", slug=f"hello-world-{i}", live=False)
+            SimplePage(title=f"Hello world!-{i}", slug=f"hello-world-{i}", content=f"Hello world {i}!", live=False)
             for i in range(1, 5)
         ]
         self.pages_to_be_published = self.child_pages[:3]
@@ -168,7 +169,7 @@ def setUp(self):
 
         # Add child pages
         self.child_pages = [
-            Page(title=f"Hello world!-{i}", slug=f"hello-world-{i}", live=False)
+            SimplePage(title=f"Hello world!-{i}", slug=f"hello-world-{i}", content=f"Hello world {i}!", live=False)
             for i in range(1, 5)
         ]
         self.pages_to_be_published = self.child_pages[:3]
@@ -179,10 +180,11 @@ def setUp(self):
 
         # map of the form { page: [child_pages] } to be added
         self.grandchildren_pages = {
-            self.pages_to_be_published[0]: [Page(title="Hello world!-a", slug="hello-world-a", live=False)],
+            self.pages_to_be_published[0]: [
+                SimplePage(title="Hello world!-a", slug="hello-world-a", content="Hello world a!", live=False)],
             self.pages_to_be_published[1]: [
-                Page(title="Hello world!-b", slug="hello-world-b", live=False),
-                Page(title="Hello world!-c", slug="hello-world-c", live=False)
+                SimplePage(title="Hello world!-b", slug="hello-world-b", content="Hello world b!", live=False),
+                SimplePage(title="Hello world!-c", slug="hello-world-c", content="Hello world c!", live=False)
             ]
         }
         for child_page, grandchild_pages in self.grandchildren_pages.items():