Replace `PageRevision` with generic `Revision` model (#8441)

pull/8560/head
sag᠎e 2022-05-17 17:46:30 +07:00 zatwierdzone przez GitHub
rodzic 501b28c62b
commit 52e5abfe62
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
47 zmienionych plików z 617 dodań i 256 usunięć

Wyświetl plik

@ -16,6 +16,7 @@ Changelog
* Fix: Typo in `ResumeWorkflowActionFormatter` message (Stefan Hammer)
* Fix: Throw a meaningful error when saving an image to an unrecognised image format (Christian Franke)
* Fix: Remove extra padding for headers with breadcrumbs on mobile viewport (Steven Steinwand)
* Replace `PageRevision` with generic `Revision` model (Sage Abdullah)
3.0 (16.05.2022)

Wyświetl plik

@ -484,25 +484,43 @@ The ``locale`` and ``translation_key`` fields have a unique key constraint to pr
.. autoattribute:: localized
.. _page-revision-model-ref:
.. _revision-model-ref:
``PageRevision``
================
``Revision``
============
Every time a page is edited a new ``PageRevision`` is created and saved to the database. It can be used to find the full history of all changes that have been made to a page and it also provides a place for new changes to be kept before going live.
Every time a page is edited a new ``Revision`` is created and saved to the database. It can be used to find the full history of all changes that have been made to a page and it also provides a place for new changes to be kept before going live.
- Revisions can be created from any :class:`~wagtail.models.Page` object by calling its :meth:`~Page.save_revision` method
- The content of the page is JSON-serialisable and stored in the :attr:`~PageRevision.content` field
- You can retrieve a ``PageRevision`` as a :class:`~wagtail.models.Page` object by calling the :meth:`~PageRevision.as_page_object` method
- The content of the page is JSON-serialisable and stored in the :attr:`~Revision.content` field
- You can retrieve a ``Revision`` as a :class:`~wagtail.models.Page` object by calling the :meth:`~Revision.as_object` method
.. versionchanged:: 4.0
The model has been renamed from ``PageRevision`` to ``Revision`` and it now references the ``Page`` model using a combination of an ``object_id`` :class:`~django.db.models.CharField` and foreign keys to :class:`~django.contrib.contenttypes.models.ContentType`.
Database fields
~~~~~~~~~~~~~~~
.. class:: PageRevision
.. class:: Revision
.. attribute:: page
.. attribute:: content_type
(foreign key to :class:`~wagtail.models.Page`)
(foreign key to :class:`~django.contrib.contenttypes.models.ContentType`)
This is the content type of the object this revision belongs to. For page revisions, this means the content type of the specific page type.
.. attribute:: base_content_type
(foreign key to :class:`~django.contrib.contenttypes.models.ContentType`)
This is the base content type of the object this revision belongs to. For page revisions, this means the content type of the :class:`~wagtail.models.Page` model.
.. attribute:: object_id
(string)
This represents the primary key of the object this revision belongs to.
.. attribute:: submitted_for_moderation
@ -536,38 +554,56 @@ Database fields
Managers
~~~~~~~~
.. class:: PageRevision
.. class:: Revision
:noindex:
.. attribute:: objects
This manager is used to retrieve all of the ``PageRevision`` objects in the database
This manager is used to retrieve all of the ``Revision`` objects in the database.
Example:
.. code-block:: python
PageRevision.objects.all()
Revision.objects.all()
.. attribute:: page_revisions
This manager is used to retrieve all of the ``Revision`` objects that belong to pages.
Example:
.. code-block:: python
Revision.page_revisions.all()
.. versionadded:: 4.0
This manager is added as a shorthand to retrieve page revisions.
.. attribute:: submitted_revisions
This manager is used to retrieve all of the ``PageRevision`` objects that are awaiting moderator approval
This manager is used to retrieve all of the ``Revision`` objects that are awaiting moderator approval.
Example:
.. code-block:: python
PageRevision.submitted_revisions.all()
Revision.submitted_revisions.all()
Methods and properties
~~~~~~~~~~~~~~~~~~~~~~
.. class:: PageRevision
.. class:: Revision
:noindex:
.. automethod:: as_page_object
.. automethod:: as_object
This method retrieves this revision as an instance of its :class:`~wagtail.models.Page` subclass.
This method retrieves this revision as an instance of its object's specific class. If the revision belongs to a page, it will be an instance of the :class:`~wagtail.models.Page`'s specific subclass.
.. versionadded:: 4.0
This method has been renamed from ``as_page_object()`` to ``as_object()``.
.. automethod:: approve_moderation
@ -579,11 +615,19 @@ Methods and properties
.. automethod:: is_latest_revision
Returns ``True`` if this revision is its page's latest revision
Returns ``True`` if this revision is the object's latest revision
.. automethod:: publish
Calling this will copy the content of this revision into the live page object. If the page is in draft, it will be published.
Calling this will copy the content of this revision into the live object. If the object is in draft, it will be published.
.. autoattribute:: content_object
This property returns the object this revision belongs to as an instance of the base class.
.. autoattribute:: specific_content_object
This property returns the object this revision belongs to as an instance of the specific class.
``GroupPagePermission``
=======================
@ -837,7 +881,7 @@ Database fields
.. attribute:: page_revision
(foreign key to ``PageRevision``)
(foreign key to ``Revision``)
The page revision this task state was created on.
@ -1049,7 +1093,7 @@ Database fields
.. attribute:: revision
(foreign key to :class:`PageRevision`)
(foreign key to :class:`Revision`)
A foreign key to the current page revision.
@ -1111,7 +1155,7 @@ Database fields
.. attribute:: revision_created
(foreign key to :class:`PageRevision`)
(foreign key to :class:`Revision`)
A foreign key to the revision on which the comment was created.

Wyświetl plik

@ -3,7 +3,7 @@
Signals
=======
Wagtail's :ref:`page-revision-model-ref` and :ref:`page-model-ref` implement
Wagtail's :ref:`revision-model-ref` and :ref:`page-model-ref` implement
:doc:`Signals <topics/signals>` from ``django.dispatch``.
Signals are useful for creating side-effects from page publish/unpublish events.
@ -13,11 +13,11 @@ For example, you could use signals to send publish notifications to a messaging
``page_published``
------------------
This signal is emitted from a ``PageRevision`` when a revision is set to `published`.
This signal is emitted from a ``Revision`` when a page revision is set to `published`.
:sender: The page ``class``.
:instance: The specific ``Page`` instance.
:revision: The ``PageRevision`` that was published.
:revision: The ``Revision`` that was published.
:kwargs: Any other arguments passed to ``page_published.send()``.
To listen to a signal, implement ``page_published.connect(receiver, sender, **kwargs)``. Here's a simple

Wyświetl plik

@ -26,6 +26,7 @@ depth: 1
* Fix typo in `ResumeWorkflowActionFormatter` message (Stefan Hammer)
* Throw a meaningful error when saving an image to an unrecognised image format (Christian Franke)
* Remove extra padding for headers with breadcrumbs on mobile viewport (Steven Steinwand)
* Replace `PageRevision` with generic `Revision` model (Sage Abdullah)
## Upgrade considerations
@ -34,8 +35,14 @@ depth: 1
The `wagtail.contrib.modeladmin.helpers.AdminURLHelper` class now accepts a `base_url_path` keyword argument on its constructor. Custom subclasses of this class should be updated to accept this keyword argument.
### Dropped support for Safari 13
Safari 13 will no longer be officially supported as of this release, this deviates the current support for the last 3 version of Safari by a few months and was required to add better support for RTL languages.
### `PageRevision` replaced with `Revision`
The `PageRevision` model has been replaced with a generic `Revision` model. If you use the `PageRevision` model in your code, make sure that:
* Creation of `PageRevision` objects should be updated to create `Revision` objects using the page's `id` as the `object_id`, the default `Page` model's content type as the `base_content_type`, and the page's specific content type as the `content_type`.
* Queries that use the `PageRevision.objects` manager should be updated to use the `Revision.page_revisions` manager.
* Queries for `Page` objects that use `PageRevision.page_id` should be updated to cast the `Revision.object_id` to an integer before using it in the query.

Wyświetl plik

@ -190,7 +190,7 @@ class CopyPageAction:
revision.pk = None
revision.submitted_for_moderation = False
revision.approved_go_live_at = None
revision.page = page_copy
revision.object_id = page_copy.id
# Update ID fields in content
revision_content = revision.content

Wyświetl plik

@ -37,7 +37,7 @@ class PublishPageRevisionAction:
self, revision, user=None, changed=True, log_action=True, previous_revision=None
):
self.revision = revision
self.page = self.revision.as_page_object()
self.page = self.revision.as_object()
self.user = user
self.changed = changed
self.log_action = log_action
@ -73,7 +73,7 @@ class PublishPageRevisionAction:
def _publish_page_revision(
self, revision, page, user, changed, log_action, previous_revision
):
from wagtail.models import COMMENTS_RELATION_NAME, PageRevision
from wagtail.models import COMMENTS_RELATION_NAME, Revision
if page.go_live_at and page.go_live_at > timezone.now():
page.has_unpublished_changes = True
@ -110,7 +110,7 @@ class PublishPageRevisionAction:
page.first_published_at = now
if previous_revision:
previous_revision_page = previous_revision.as_page_object()
previous_revision_page = previous_revision.as_object()
old_page_title = (
previous_revision_page.title
if page.title != previous_revision_page.title
@ -119,11 +119,11 @@ class PublishPageRevisionAction:
else:
try:
previous = revision.get_previous()
except PageRevision.DoesNotExist:
except Revision.DoesNotExist:
previous = None
old_page_title = (
previous.page.title
if previous and page.title != previous.page.title
previous.content_object.title
if previous and page.title != previous.content_object.title
else None
)
else:

Wyświetl plik

@ -57,7 +57,7 @@ class RevertToPageRevisionAction:
def execute(self, skip_permission_checks=False):
self.check(skip_permission_checks=skip_permission_checks)
return self.revision.as_page_object().save_revision(
return self.revision.as_object().save_revision(
previous_revision=self.revision,
user=self.user,
log_action=self.log_action,

Wyświetl plik

@ -38,5 +38,5 @@ class RevertToPageRevisionAPIAction(APIAction):
except RevertToPageRevisionError as e:
raise BadRequestError(e.args[0])
serializer = self.view.get_serializer(new_revision.as_page_object())
serializer = self.view.get_serializer(new_revision.as_object())
return Response(serializer.data, status=status.HTTP_200_OK)

Wyświetl plik

@ -73,7 +73,7 @@ def send_moderation_notification(revision, notification, excluded_user=None):
settings, "WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS", True
)
recipient_users = users_with_page_permission(
revision.page, "publish", include_superusers
revision.content_object, "publish", include_superusers
)
elif notification in ["rejected", "approved"]:
# Get submitter

Wyświetl plik

@ -18,25 +18,25 @@
</thead>
<tbody>
{% for revision in page_revisions_for_moderation %}
{% page_permissions revision.page as page_perms %}
{% page_permissions revision.content_object as page_perms %}
<tr>
<td class="title" valign="top">
<div class="title-wrapper">
{% if page_perms.can_edit %}
<a href="{% url 'wagtailadmin_pages:edit' revision.page.id %}" title="{% trans 'Edit this page' %}">{{ revision.page.specific_deferred.get_admin_display_title }}</a>
{% elif revision.page.is_previewable %}
<a href="{% url 'wagtailadmin_pages:preview_for_moderation' revision.id %}" title="{% trans 'Preview this page' %}">{{ revision.page.specific_deferred.get_admin_display_title }}</a>
<a href="{% url 'wagtailadmin_pages:edit' revision.object_id %}" title="{% trans 'Edit this page' %}">{{ revision.content_object.specific_deferred.get_admin_display_title }}</a>
{% elif revision.content_object.is_previewable %}
<a href="{% url 'wagtailadmin_pages:preview_for_moderation' revision.id %}" title="{% trans 'Preview this page' %}">{{ revision.content_object.specific_deferred.get_admin_display_title }}</a>
{% else %}
{{ revision.page.specific_deferred.get_admin_display_title }}
{{ revision.content_object.specific_deferred.get_admin_display_title }}
{% endif %}
{% i18n_enabled as show_locale_labels %}
{% if show_locale_labels and revision.page.locale_id %}
{% locale_label_from_id revision.page.locale_id as locale_label %}
{% if show_locale_labels and revision.content_object.locale_id %}
{% locale_label_from_id revision.content_object.locale_id as locale_label %}
<span class="status-tag status-tag--label">{{ locale_label }}</span>
{% endif %}
{% include "wagtailadmin/pages/listing/_privacy_indicator.html" with page=revision.page %}
{% include "wagtailadmin/pages/listing/_locked_indicator.html" with page=revision.page %}
{% include "wagtailadmin/pages/listing/_privacy_indicator.html" with page=revision.content_object %}
{% include "wagtailadmin/pages/listing/_locked_indicator.html" with page=revision.content_object %}
</div>
<ul class="actions">
<li>
@ -52,18 +52,18 @@
</form>
</li>
{% if page_perms.can_edit %}
<li><a href="{% url 'wagtailadmin_pages:edit' revision.page.id %}" class="button button-small button-secondary">{% trans 'Edit' %}</a></li>
<li><a href="{% url 'wagtailadmin_pages:edit' revision.object_id %}" class="button button-small button-secondary">{% trans 'Edit' %}</a></li>
{% endif %}
{% if revision.page.is_previewable %}
{% if revision.content_object.is_previewable %}
<li><a href="{% url 'wagtailadmin_pages:preview_for_moderation' revision.id %}" class="button button-small button-secondary" target="_blank" rel="noreferrer">{% trans 'Preview' %}</a></li>
{% endif %}
</ul>
</td>
<td valign="top">
<a href="{% url 'wagtailadmin_explore' revision.page.get_parent.id %}">{{ revision.page.get_parent.specific_deferred.get_admin_display_title }}</a>
<a href="{% url 'wagtailadmin_explore' revision.content_object.get_parent.id %}">{{ revision.content_object.get_parent.specific_deferred.get_admin_display_title }}</a>
</td>
<td class="type" valign="top">
{{ revision.page.content_type.model_class.get_verbose_name }}
{{ revision.content_object.content_type.model_class.get_verbose_name }}
</td>
<td valign="top">
<div class="human-readable-date" title="{{ revision.created_at|date:"DATETIME_FORMAT" }}">{% blocktrans trimmed with time_period=revision.created_at|timesince_simple %}{{ time_period }}{% endblocktrans %} </div>

Wyświetl plik

@ -19,39 +19,39 @@
<tbody>
{% for task_state, actions, workflow_tasks in states %}
{% with task_state.page_revision as revision %}
{% page_permissions revision.page as page_perms %}
{% page_permissions revision.content_object as page_perms %}
<tr>
<td class="title" valign="top">
<div class="title-wrapper">
{% if page_perms.can_edit %}
<a href="{% url 'wagtailadmin_pages:edit' revision.page.id %}" title="{% trans 'Edit this page' %}">{{ revision.page.specific_deferred.get_admin_display_title }}</a>
<a href="{% url 'wagtailadmin_pages:edit' revision.object_id %}" title="{% trans 'Edit this page' %}">{{ revision.content_object.specific_deferred.get_admin_display_title }}</a>
{% else %}
{{ revision.page.specific_deferred.get_admin_display_title }}
{{ revision.content_object.specific_deferred.get_admin_display_title }}
{% endif %}
{% i18n_enabled as show_locale_labels %}
{% if show_locale_labels and revision.page.locale_id %}
{% locale_label_from_id revision.page.locale_id as locale_label %}
{% if show_locale_labels and revision.content_object.locale_id %}
{% locale_label_from_id revision.content_object.locale_id as locale_label %}
<span class="status-tag status-tag--label">{{ locale_label }}</span>
{% endif %}
{% include "wagtailadmin/pages/listing/_privacy_indicator.html" with page=revision.page %}
{% include "wagtailadmin/pages/listing/_locked_indicator.html" with page=revision.page %}
{% include "wagtailadmin/pages/listing/_privacy_indicator.html" with page=revision.content_object %}
{% include "wagtailadmin/pages/listing/_locked_indicator.html" with page=revision.content_object %}
</div>
{% if actions %}
<ul class="actions">
{% for action_name, action_label, modal in actions %}
<li>
<button class="button button-small button-secondary" data-workflow-action-url="{% url 'wagtailadmin_pages:workflow_action' revision.page.id action_name task_state.id %}" {% if modal %}data-launch-modal{% endif %}>{{ action_label }}</button>
<button class="button button-small button-secondary" data-workflow-action-url="{% url 'wagtailadmin_pages:workflow_action' revision.object_id action_name task_state.id %}" {% if modal %}data-launch-modal{% endif %}>{{ action_label }}</button>
</li>
{% endfor %}
{% if page_perms.can_edit %}
<li>
<a href="{% url 'wagtailadmin_pages:edit' revision.page.id %}" class="button button-small button-secondary">{% trans 'Edit' %}</a>
<a href="{% url 'wagtailadmin_pages:edit' revision.object_id %}" class="button button-small button-secondary">{% trans 'Edit' %}</a>
</li>
{% endif %}
{% if revision.page.is_previewable %}
{% if revision.content_object.is_previewable %}
<li>
<a href="{% url 'wagtailadmin_pages:workflow_preview' revision.page.id task_state.task.id %}" class="button button-small button-secondary" target="_blank" rel="noreferrer">{% trans 'Preview' %}</a>
<a href="{% url 'wagtailadmin_pages:workflow_preview' revision.object_id task_state.task.id %}" class="button button-small button-secondary" target="_blank" rel="noreferrer">{% trans 'Preview' %}</a>
</li>
{% endif %}
</ul>

Wyświetl plik

@ -2,6 +2,6 @@
{% load i18n %}
{% block content %}
<p>{% blocktrans trimmed with title=revision.page.get_admin_display_title|safe %}The page "{{ title }}" has been approved.{% endblocktrans %}</p>
<p>{% trans "You can view the page here:" %} <a href="{{ revision.page.full_url }}">{{ revision.page.full_url }}</a></p>
<p>{% blocktrans trimmed with title=revision.content_object.get_admin_display_title|safe %}The page "{{ title }}" has been approved.{% endblocktrans %}</p>
<p>{% trans "You can view the page here:" %} <a href="{{ revision.content_object.full_url }}">{{ revision.content_object.full_url }}</a></p>
{% endblock %}

Wyświetl plik

@ -2,7 +2,7 @@
{% load i18n %}
{% block content %}
{% blocktrans trimmed with title=revision.page.get_admin_display_title|safe %}The page "{{ title }}" has been approved.{% endblocktrans %}
{% blocktrans trimmed with title=revision.content_object.get_admin_display_title|safe %}The page "{{ title }}" has been approved.{% endblocktrans %}
{% trans "You can view the page here:" %} {{ revision.page.full_url }}
{% trans "You can view the page here:" %} {{ revision.content_object.full_url }}
{% endblock %}

Wyświetl plik

@ -1,3 +1,3 @@
{% load i18n %}
{% blocktrans trimmed with title=revision.page.get_admin_display_title|safe %}The page "{{ title }}" has been approved{% endblocktrans %}
{% blocktrans trimmed with title=revision.content_object.get_admin_display_title|safe %}The page "{{ title }}" has been approved{% endblocktrans %}

Wyświetl plik

@ -3,6 +3,6 @@
{% base_url_setting as base_url %}
{% block content %}
<p>{% blocktrans trimmed with title=revision.page.get_admin_display_title|safe %}The page "{{ title }}" has been rejected.{% endblocktrans %}</p>
<p>{% trans "You can edit the page here:"%} <a href="{{ base_url }}{% url 'wagtailadmin_pages:edit' revision.page.id %}">{{ base_url }}{% url 'wagtailadmin_pages:edit' revision.page.id %}</a></p>
<p>{% blocktrans trimmed with title=revision.content_object.get_admin_display_title|safe %}The page "{{ title }}" has been rejected.{% endblocktrans %}</p>
<p>{% trans "You can edit the page here:"%} <a href="{{ base_url }}{% url 'wagtailadmin_pages:edit' revision.object_id %}">{{ base_url }}{% url 'wagtailadmin_pages:edit' revision.object_id %}</a></p>
{% endblock %}

Wyświetl plik

@ -3,7 +3,7 @@
{% base_url_setting as base_url %}
{% block content %}
{% blocktrans trimmed with title=revision.page.get_admin_display_title|safe %}The page "{{ title }}" has been rejected.{% endblocktrans %}
{% blocktrans trimmed with title=revision.content_object.get_admin_display_title|safe %}The page "{{ title }}" has been rejected.{% endblocktrans %}
{% trans "You can edit the page here:"%} {{ base_url }}{% url 'wagtailadmin_pages:edit' revision.page.id %}
{% trans "You can edit the page here:"%} {{ base_url }}{% url 'wagtailadmin_pages:edit' revision.object_id %}
{% endblock %}

Wyświetl plik

@ -1,3 +1,3 @@
{% load i18n %}
{% blocktrans trimmed with title=revision.page.get_admin_display_title|safe %}The page "{{ title }}" has been rejected{% endblocktrans %}
{% blocktrans trimmed with title=revision.content_object.get_admin_display_title|safe %}The page "{{ title }}" has been rejected{% endblocktrans %}

Wyświetl plik

@ -3,12 +3,12 @@
{% base_url_setting as base_url %}
{% block content %}
<p>{% blocktrans trimmed with page=revision.page|safe editor=revision.user|user_display_name %}The page "{{ page }}" has been submitted for moderation by {{ editor }}.{% endblocktrans %}</p>
<p>{% blocktrans trimmed with page=revision.content_object|safe editor=revision.user|user_display_name %}The page "{{ page }}" has been submitted for moderation by {{ editor }}.{% endblocktrans %}</p>
<p>
{% if revision.page.is_previewable %}
{% if revision.content_object.is_previewable %}
{% trans "You can preview the page here:" %} <a href="{{ base_url }}{% url 'wagtailadmin_pages:preview_for_moderation' revision.id %}">{{ base_url }}{% url 'wagtailadmin_pages:preview_for_moderation' revision.id %}</a><br/>
{% endif %}
{% trans "You can edit the page here:" %} <a href="{{ base_url }}{% url 'wagtailadmin_pages:edit' revision.page.id %}">{{ base_url }}{% url 'wagtailadmin_pages:edit' revision.page.id %}</a>
{% trans "You can edit the page here:" %} <a href="{{ base_url }}{% url 'wagtailadmin_pages:edit' revision.object_id %}">{{ base_url }}{% url 'wagtailadmin_pages:edit' revision.object_id %}</a>
</p>
{% endblock %}

Wyświetl plik

@ -3,8 +3,8 @@
{% base_url_setting as base_url %}
{% block content %}
{% blocktrans trimmed with page=revision.page|safe editor=revision.user|user_display_name %}The page "{{ page }}" has been submitted for moderation by {{ editor }}.{% endblocktrans %}
{% blocktrans trimmed with page=revision.content_object|safe editor=revision.user|user_display_name %}The page "{{ page }}" has been submitted for moderation by {{ editor }}.{% endblocktrans %}
{% if revision.page.is_previewable %}{% trans "You can preview the page here:" %} {{ base_url }}{% url 'wagtailadmin_pages:preview_for_moderation' revision.id %}{% endif %}
{% trans "You can edit the page here:" %} {{ base_url }}{% url 'wagtailadmin_pages:edit' revision.page.id %}
{% if revision.content_object.is_previewable %}{% trans "You can preview the page here:" %} {{ base_url }}{% url 'wagtailadmin_pages:preview_for_moderation' revision.id %}{% endif %}
{% trans "You can edit the page here:" %} {{ base_url }}{% url 'wagtailadmin_pages:edit' revision.object_id %}
{% endblock %}

Wyświetl plik

@ -1,3 +1,3 @@
{% load i18n %}
{% blocktrans trimmed with page=revision.page|safe %}The page "{{ page }}" has been submitted for moderation{% endblocktrans %}
{% blocktrans trimmed with page=revision.content_object|safe %}The page "{{ page }}" has been submitted for moderation{% endblocktrans %}

Wyświetl plik

@ -11,7 +11,7 @@ from wagtail.admin.userbar import (
ExplorePageItem,
RejectModerationEditPageItem,
)
from wagtail.models import PAGE_TEMPLATE_VAR, Page, PageRevision
from wagtail.models import PAGE_TEMPLATE_VAR, Page, Revision
from wagtail.users.models import UserProfile
register = template.Library()
@ -62,13 +62,17 @@ def wagtailuserbar(context, position="bottom-right"):
if revision_id:
items = [
AdminItem(),
ExplorePageItem(PageRevision.objects.get(id=revision_id).page),
EditPageItem(PageRevision.objects.get(id=revision_id).page),
ExplorePageItem(
Revision.page_revisions.get(id=revision_id).content_object
),
EditPageItem(
Revision.page_revisions.get(id=revision_id).content_object
),
ApproveModerationEditPageItem(
PageRevision.objects.get(id=revision_id)
Revision.page_revisions.get(id=revision_id)
),
RejectModerationEditPageItem(
PageRevision.objects.get(id=revision_id)
Revision.page_revisions.get(id=revision_id)
),
]
else:

Wyświetl plik

@ -1858,9 +1858,7 @@ class TestRevertToPageRevisionAction(AdminAPITestCase):
self.assertEqual(response.status_code, 404)
content = json.loads(response.content.decode("utf-8"))
self.assertEqual(
content, {"message": "No PageRevision matches the given query."}
)
self.assertEqual(content, {"message": "No Revision matches the given query."})
# Overwrite imported test cases do Django doesn't run them

Wyświetl plik

@ -766,7 +766,7 @@ class TestPageCopy(TestCase, WagtailTestUtils):
new_page = EventPage.objects.last()
# Hack: get the page instance from the edit form which assembles it from the
# latest revision. While we could do new_page.get_latest_revision().as_page_object()
# latest revision. While we could do new_page.get_latest_revision().as_object()
# this follow the edit view closer and should it change the test is less
# prone to continue working because we're skipping some step
response = self.client.get(

Wyświetl plik

@ -11,7 +11,7 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from wagtail.admin.tests.pages.timestamps import submittable_timestamp
from wagtail.models import GroupPagePermission, Locale, Page, PageRevision
from wagtail.models import GroupPagePermission, Locale, Page, Revision
from wagtail.signals import page_published
from wagtail.test.testapp.models import (
BusinessChild,
@ -458,7 +458,7 @@ class TestPageCreation(TestCase, WagtailTestUtils):
# No revisions with approved_go_live_at
self.assertFalse(
PageRevision.objects.filter(page=page)
Revision.page_revisions.filter(object_id=page.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -610,7 +610,7 @@ class TestPageCreation(TestCase, WagtailTestUtils):
# A revision with approved_go_live_at should exist now
self.assertTrue(
PageRevision.objects.filter(page=page)
Revision.page_revisions.filter(object_id=page.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)

Wyświetl plik

@ -94,7 +94,8 @@ class TestRecentEditsPanel(TestCase, WagtailTestUtils):
# check if the revision is the revision of edited Page
self.assertEqual(
ctx["last_edits"][0][0].page, Page.objects.get(pk=self.child_page.id)
ctx["last_edits"][0][0].content_object,
Page.objects.get(pk=self.child_page.id),
)
# check if the page in this list is the specific page of this revision
self.assertEqual(

Wyświetl plik

@ -22,8 +22,8 @@ from wagtail.models import (
Locale,
Page,
PageLogEntry,
PageRevision,
PageSubscription,
Revision,
Site,
)
from wagtail.signals import page_published
@ -346,21 +346,21 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# A revision with approved_go_live_at should not exist
self.assertFalse(
PageRevision.objects.filter(page=child_page_new)
Revision.page_revisions.filter(object_id=child_page_new.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
# But a revision with go_live_at and expire_at in their content json *should* exist
self.assertTrue(
PageRevision.objects.filter(
page=child_page_new,
Revision.page_revisions.filter(
object_id=child_page_new.id,
content__go_live_at__startswith=str(go_live_at.date()),
).exists()
)
self.assertTrue(
PageRevision.objects.filter(
page=child_page_new,
Revision.page_revisions.filter(
object_id=child_page_new.id,
content__expire_at__startswith=str(expire_at.date()),
).exists()
)
@ -551,7 +551,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# Instead a revision with approved_go_live_at should now exist
self.assertTrue(
PageRevision.objects.filter(page=child_page_new)
Revision.page_revisions.filter(object_id=child_page_new.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -597,7 +597,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# Instead a revision with approved_go_live_at should now exist
self.assertTrue(
PageRevision.objects.filter(page=child_page_new)
Revision.page_revisions.filter(object_id=child_page_new.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -625,7 +625,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# And a revision with approved_go_live_at should not exist
self.assertFalse(
PageRevision.objects.filter(page=child_page_new)
Revision.page_revisions.filter(object_id=child_page_new.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -664,7 +664,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# Instead a revision with approved_go_live_at should now exist
self.assertTrue(
PageRevision.objects.filter(page=child_page_new)
Revision.page_revisions.filter(object_id=child_page_new.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -719,7 +719,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# Instead a revision with approved_go_live_at should now exist
self.assertTrue(
PageRevision.objects.filter(page=child_page_new)
Revision.page_revisions.filter(object_id=child_page_new.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -753,7 +753,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# And a revision with approved_go_live_at should not exist
self.assertFalse(
PageRevision.objects.filter(page=child_page_new)
Revision.page_revisions.filter(object_id=child_page_new.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -1244,7 +1244,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# (which is not a valid content language under the current configuration)
Locale.objects.update(language_code="de")
PageRevision.objects.filter(page_id=self.child_page.id).delete()
Revision.page_revisions.filter(object_id=self.child_page.id).delete()
# Tests that the edit page loads
response = self.client.get(
@ -1772,7 +1772,7 @@ class TestIssue3982(TestCase, WagtailTestUtils):
)
page = SimplePage.objects.get()
self.assertFalse(page.live)
revision = PageRevision.objects.get(page=page)
revision = Revision.page_revisions.get(object_id=page.id)
revision.submitted_for_moderation = True
revision.save()
response = self.client.post(

Wyświetl plik

@ -9,7 +9,7 @@ from django.core.mail import EmailMultiAlternatives
from django.test import TestCase, override_settings
from django.urls import reverse
from wagtail.models import Page, PageRevision
from wagtail.models import Page, Revision
from wagtail.signals import page_published
from wagtail.test.testapp.models import SimplePage
from wagtail.test.utils import WagtailTestUtils
@ -145,7 +145,7 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
# Revision must no longer be submitted for moderation
self.assertFalse(
PageRevision.objects.get(id=self.revision.id).submitted_for_moderation
Revision.page_revisions.get(id=self.revision.id).submitted_for_moderation
)
def test_reject_moderation_view_bad_revision_id(self):

Wyświetl plik

@ -37,7 +37,7 @@ class TestModerationList(TestCase, WagtailTestUtils):
self.revision = self.page.get_latest_revision()
self.edit_page_url = reverse(
"wagtailadmin_pages:edit", args=(self.revision.page.id,)
"wagtailadmin_pages:edit", args=(self.revision.object_id,)
)
self.preview_page_url = reverse(
"wagtailadmin_pages:preview_for_moderation", args=(self.revision.id,)

Wyświetl plik

@ -110,7 +110,9 @@ class ModeratePageItem(BaseItem):
if not request.user.has_perm("wagtailadmin.access_admin"):
return ""
if not self.revision.page.permissions_for_user(request.user).can_publish():
if not self.revision.content_object.permissions_for_user(
request.user
).can_publish():
return ""
return super().render(request)

Wyświetl plik

@ -16,10 +16,11 @@ from wagtail.admin.site_summary import SiteSummaryPanel
from wagtail.admin.ui.components import Component
from wagtail.models import (
Page,
PageRevision,
Revision,
TaskState,
UserPagePermissionsProxy,
WorkflowState,
get_default_page_content_type,
)
User = get_user_model()
@ -53,7 +54,7 @@ class PagesForModerationPanel(Component):
user_perms = UserPagePermissionsProxy(request.user)
context["page_revisions_for_moderation"] = (
user_perms.revisions_for_moderation()
.select_related("page", "user")
.select_related("user")
.order_by("-created_at")
)
context["request"] = request
@ -102,7 +103,6 @@ class WorkflowPagesToModeratePanel(Component):
.select_related(
"page_revision",
"task",
"page_revision__page",
"page_revision__user",
)
.order_by("-started_at")
@ -111,7 +111,7 @@ class WorkflowPagesToModeratePanel(Component):
(
state,
state.task.specific.get_actions(
page=state.page_revision.page, user=request.user
page=state.page_revision.content_object, user=request.user
),
state.workflow_state.all_tasks_with_status(),
)
@ -162,35 +162,37 @@ class RecentEditsPanel(Component):
if connection.vendor == "mysql":
# MySQL can't handle the subselect created by the ORM version -
# it fails with "This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'"
last_edits = PageRevision.objects.raw(
last_edits = Revision.objects.raw(
"""
SELECT wp.* FROM
wagtailcore_pagerevision wp JOIN (
SELECT max(created_at) AS max_created_at, page_id FROM
wagtailcore_pagerevision WHERE user_id = %s GROUP BY page_id ORDER BY max_created_at DESC LIMIT %s
) AS max_rev ON max_rev.max_created_at = wp.created_at ORDER BY wp.created_at DESC
SELECT wr.* FROM
wagtailcore_revision wr JOIN (
SELECT max(created_at) AS max_created_at, object_id FROM
wagtailcore_revision WHERE user_id = %s AND base_content_type_id = %s GROUP BY object_id ORDER BY max_created_at DESC LIMIT %s
) AS max_rev ON max_rev.max_created_at = wr.created_at ORDER BY wr.created_at DESC
""",
[
User._meta.pk.get_db_prep_value(request.user.pk, connection),
get_default_page_content_type().id,
edit_count,
],
)
else:
last_edits_dates = (
PageRevision.objects.filter(user=request.user)
.values("page_id")
Revision.page_revisions.filter(user=request.user)
.values("object_id")
.annotate(latest_date=Max("created_at"))
.order_by("-latest_date")
.values("latest_date")[:edit_count]
)
last_edits = PageRevision.objects.filter(
last_edits = Revision.page_revisions.filter(
created_at__in=last_edits_dates
).order_by("-created_at")
page_keys = [pr.page_id for pr in last_edits]
# The revision's object_id is a string, so cast it to int first.
page_keys = [int(pr.object_id) for pr in last_edits]
pages = Page.objects.specific().in_bulk(page_keys)
context["last_edits"] = [
[revision, pages.get(revision.page_id)] for revision in last_edits
[revision, pages.get(int(revision.object_id))] for revision in last_edits
]
context["request"] = request
return context

Wyświetl plik

@ -16,7 +16,7 @@ from wagtail.log_actions import registry as log_action_registry
from wagtail.models import (
Page,
PageLogEntry,
PageRevision,
Revision,
TaskState,
UserPagePermissionsProxy,
WorkflowState,
@ -83,8 +83,8 @@ def workflow_history_detail(request, page_id, workflow_state_id):
# It's possible that the page is edited while the workflow is running, so some
# tasks may be repeated. All tasks that have been completed no matter what
# revision needs to be displayed on this page.
page_revisions = PageRevision.objects.filter(
page=page,
page_revisions = Revision.page_revisions.filter(
object_id=page.id,
id__in=TaskState.objects.filter(workflow_state=workflow_state).values_list(
"page_revision_id", flat=True
),

Wyświetl plik

@ -6,19 +6,19 @@ from django.views.decorators.http import require_GET
from wagtail.admin import messages
from wagtail.admin.mail import send_moderation_notification
from wagtail.models import PageRevision
from wagtail.models import Revision
def approve_moderation(request, revision_id):
revision = get_object_or_404(PageRevision, id=revision_id)
if not revision.page.permissions_for_user(request.user).can_publish():
revision = get_object_or_404(Revision.page_revisions, id=revision_id)
if not revision.content_object.permissions_for_user(request.user).can_publish():
raise PermissionDenied
if not revision.submitted_for_moderation:
messages.error(
request,
_("The page '{0}' is not currently awaiting moderation.").format(
revision.page.specific_deferred.get_admin_display_title()
revision.content_object.specific_deferred.get_admin_display_title()
),
)
return redirect("wagtailadmin_home")
@ -27,16 +27,19 @@ def approve_moderation(request, revision_id):
revision.approve_moderation(user=request.user)
message = _("Page '{0}' published.").format(
revision.page.specific_deferred.get_admin_display_title()
revision.content_object.specific_deferred.get_admin_display_title()
)
buttons = []
if revision.page.url is not None:
if revision.content_object.url is not None:
buttons.append(
messages.button(revision.page.url, _("View live"), new_window=False)
messages.button(
revision.content_object.url, _("View live"), new_window=False
)
)
buttons.append(
messages.button(
reverse("wagtailadmin_pages:edit", args=(revision.page.id,)), _("Edit")
reverse("wagtailadmin_pages:edit", args=(revision.content_object.id,)),
_("Edit"),
)
)
messages.success(request, message, buttons=buttons)
@ -48,15 +51,15 @@ def approve_moderation(request, revision_id):
def reject_moderation(request, revision_id):
revision = get_object_or_404(PageRevision, id=revision_id)
if not revision.page.permissions_for_user(request.user).can_publish():
revision = get_object_or_404(Revision.page_revisions, id=revision_id)
if not revision.content_object.permissions_for_user(request.user).can_publish():
raise PermissionDenied
if not revision.submitted_for_moderation:
messages.error(
request,
_("The page '{0}' is not currently awaiting moderation.").format(
revision.page.specific_deferred.get_admin_display_title()
revision.content_object.specific_deferred.get_admin_display_title()
),
)
return redirect("wagtailadmin_home")
@ -67,11 +70,13 @@ def reject_moderation(request, revision_id):
messages.success(
request,
_("Page '{0}' rejected for publication.").format(
revision.page.specific_deferred.get_admin_display_title()
revision.content_object.specific_deferred.get_admin_display_title()
),
buttons=[
messages.button(
reverse("wagtailadmin_pages:edit", args=(revision.page.id,)),
reverse(
"wagtailadmin_pages:edit", args=(revision.content_object.id,)
),
_("Edit"),
)
],
@ -85,20 +90,20 @@ def reject_moderation(request, revision_id):
@require_GET
def preview_for_moderation(request, revision_id):
revision = get_object_or_404(PageRevision, id=revision_id)
if not revision.page.permissions_for_user(request.user).can_publish():
revision = get_object_or_404(Revision.page_revisions, id=revision_id)
if not revision.content_object.permissions_for_user(request.user).can_publish():
raise PermissionDenied
if not revision.submitted_for_moderation:
messages.error(
request,
_("The page '{0}' is not currently awaiting moderation.").format(
revision.page.specific_deferred.get_admin_display_title()
revision.content_object.specific_deferred.get_admin_display_title()
),
)
return redirect("wagtailadmin_home")
page = revision.as_page_object()
page = revision.as_object()
try:
preview_mode = page.default_preview_mode

Wyświetl plik

@ -27,7 +27,7 @@ def revisions_revert(request, page_id, revision_id):
raise PermissionDenied
revision = get_object_or_404(page.revisions, id=revision_id)
revision_page = revision.as_page_object()
revision_page = revision.as_object()
content_type = ContentType.objects.get_for_model(page)
page_class = content_type.model_class()
@ -95,7 +95,7 @@ def revisions_view(request, page_id, revision_id):
raise PermissionDenied
revision = get_object_or_404(page.revisions, id=revision_id)
revision_page = revision.as_page_object()
revision_page = revision.as_object()
try:
preview_mode = page.default_preview_mode
@ -119,14 +119,12 @@ def revisions_compare(request, page_id, revision_id_a, revision_id_b):
elif revision_id_a == "earliest":
revision_a = page.revisions.order_by("created_at", "id").first()
if revision_a:
revision_a = revision_a.as_page_object()
revision_a = revision_a.as_object()
revision_a_heading = _("Earliest")
else:
raise Http404
else:
revision_a = get_object_or_404(
page.revisions, id=revision_id_a
).as_page_object()
revision_a = get_object_or_404(page.revisions, id=revision_id_a).as_object()
revision_a_heading = str(
get_object_or_404(page.revisions, id=revision_id_a).created_at
)
@ -141,14 +139,12 @@ def revisions_compare(request, page_id, revision_id_a, revision_id_b):
elif revision_id_b == "latest":
revision_b = page.revisions.order_by("created_at", "id").last()
if revision_b:
revision_b = revision_b.as_page_object()
revision_b = revision_b.as_object()
revision_b_heading = _("Latest")
else:
raise Http404
else:
revision_b = get_object_or_404(
page.revisions, id=revision_id_b
).as_page_object()
revision_b = get_object_or_404(page.revisions, id=revision_id_b).as_object()
revision_b_heading = str(
get_object_or_404(page.revisions, id=revision_id_b).created_at
)

Wyświetl plik

@ -10,7 +10,13 @@ from django.views.generic import View
from wagtail.admin import messages
from wagtail.admin.auth import user_has_any_page_permission, user_passes_test
from wagtail.admin.modal_workflow import render_modal_workflow
from wagtail.models import Page, Task, TaskState, WorkflowState
from wagtail.models import (
Page,
Task,
TaskState,
WorkflowState,
get_default_page_content_type,
)
class BaseWorkflowFormView(View):
@ -235,7 +241,10 @@ def preview_revision_for_task(request, page_id, task_id):
task = get_object_or_404(Task, id=task_id).specific
try:
task_state = TaskState.objects.get(
page_revision__page=page, task=task, status=TaskState.STATUS_IN_PROGRESS
page_revision__base_content_type=get_default_page_content_type(),
page_revision__object_id=page.id,
task=task,
status=TaskState.STATUS_IN_PROGRESS,
)
except TaskState.DoesNotExist:
messages.error(
@ -251,7 +260,7 @@ def preview_revision_for_task(request, page_id, task_id):
if not task.get_actions(page, request.user):
raise PermissionDenied
page_to_view = revision.as_page_object()
page_to_view = revision.as_object()
# TODO: provide workflow actions within this view

Wyświetl plik

@ -8,7 +8,7 @@ from wagtail.admin.userbar import (
EditPageItem,
RejectModerationEditPageItem,
)
from wagtail.models import Page, PageRevision
from wagtail.models import Page, Revision
@permission_required("wagtailadmin.access_admin", raise_exception=True)
@ -40,10 +40,10 @@ def for_frontend(request, page_id):
@permission_required("wagtailadmin.access_admin", raise_exception=True)
def for_moderation(request, revision_id):
items = [
EditPageItem(PageRevision.objects.get(id=revision_id).page),
AddPageItem(PageRevision.objects.get(id=revision_id).page),
ApproveModerationEditPageItem(PageRevision.objects.get(id=revision_id)),
RejectModerationEditPageItem(PageRevision.objects.get(id=revision_id)),
EditPageItem(Revision.page_revisions.get(id=revision_id).content_object),
AddPageItem(Revision.page_revisions.get(id=revision_id).content_object),
ApproveModerationEditPageItem(Revision.page_revisions.get(id=revision_id)),
RejectModerationEditPageItem(Revision.page_revisions.get(id=revision_id)),
]
for fn in hooks.get_hooks("construct_wagtail_userbar"):

Wyświetl plik

@ -18,9 +18,9 @@ from wagtail.models import ( # noqa
PageLogEntryQuerySet,
PageManager,
PagePermissionTester,
PageRevision,
PageSubscription,
PageViewRestriction,
Revision,
SubmittedRevisionsManager,
Task,
TaskManager,

Wyświetl plik

@ -1,6 +1,6 @@
from django.core.management.base import BaseCommand
from wagtail.models import PageLogEntry, PageRevision
from wagtail.models import PageLogEntry, Revision
def get_comparison(page, revision_a, revision_b):
@ -19,29 +19,32 @@ class Command(BaseCommand):
def handle(self, *args, **options):
current_page_id = None
missing_models_content_type_ids = set()
for revision in (
PageRevision.objects.order_by("page_id", "created_at")
.select_related("page")
.iterator()
):
for revision in Revision.page_revisions.order_by(
"object_id", "created_at"
).iterator():
# This revision is for a page type that is no longer in the database. Bail out early.
if revision.page.content_type_id in missing_models_content_type_ids:
if (
revision.content_object.content_type_id
in missing_models_content_type_ids
):
continue
if not revision.page.specific_class:
missing_models_content_type_ids.add(revision.page.content_type_id)
if not revision.content_object.specific_class:
missing_models_content_type_ids.add(
revision.content_object.content_type_id
)
continue
is_new_page = revision.page_id != current_page_id
is_new_page = revision.object_id != current_page_id
if is_new_page:
# reset previous revision when encountering a new page.
previous_revision = None
has_content_changes = False
current_page_id = revision.page_id
current_page_id = revision.object_id
if not PageLogEntry.objects.filter(revision=revision).exists():
try:
current_revision_as_page = revision.as_page_object()
current_revision_as_page = revision.as_object()
except Exception:
# restoring old revisions may fail if e.g. they have an on_delete=PROTECT foreign key
# to a no-longer-existing model instance. We cannot compare changes between two
@ -49,11 +52,11 @@ class Command(BaseCommand):
# change at the point that it went from restorable to non-restorable or vice versa.
current_revision_as_page = None
published = revision.id == revision.page.live_revision_id
published = revision.id == revision.content_object.live_revision_id
if previous_revision is not None:
try:
previous_revision_as_page = previous_revision.as_page_object()
previous_revision_as_page = previous_revision.as_object()
except Exception:
previous_revision_as_page = None
@ -72,7 +75,7 @@ class Command(BaseCommand):
else:
# Must use .specific so the comparison picks up all fields, not just base Page ones.
comparison = get_comparison(
revision.page.specific,
revision.content_object.specific,
previous_revision_as_page,
current_revision_as_page,
)
@ -106,7 +109,7 @@ class Command(BaseCommand):
def log_page_action(self, action, revision, has_content_changes):
PageLogEntry.objects.log_action(
instance=revision.page.specific,
instance=revision.content_object.specific,
action=action,
data={},
revision=None if action == "wagtail.create" else revision,

Wyświetl plik

@ -1,7 +1,7 @@
from django.core.management.base import BaseCommand
from django.utils import dateparse, timezone
from wagtail.models import Page, PageRevision
from wagtail.models import Page, Revision
def revision_date_expired(r):
@ -58,7 +58,7 @@ class Command(BaseCommand):
# 2. get all page revisions for moderation that have been expired
expired_revs = [
r
for r in PageRevision.objects.filter(submitted_for_moderation=True)
for r in Revision.page_revisions.filter(submitted_for_moderation=True)
if revision_date_expired(r)
]
if dryrun:
@ -88,7 +88,7 @@ class Command(BaseCommand):
er.save()
# 3. get all revisions that need to be published
revs_for_publishing = PageRevision.objects.filter(
revs_for_publishing = Revision.page_revisions.filter(
approved_go_live_at__lt=timezone.now()
)
if dryrun:

Wyświetl plik

@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand
from django.db.models import Q
from django.utils import timezone
from wagtail.models import PageRevision
from wagtail.models import Revision
try:
from wagtail.models import WorkflowState
@ -39,7 +39,7 @@ class Command(BaseCommand):
def purge_revisions(days=None):
# exclude revisions which have been submitted for moderation in the old system
purgeable_revisions = PageRevision.objects.exclude(
purgeable_revisions = Revision.page_revisions.exclude(
submitted_for_moderation=True
).exclude(
# and exclude revisions with an approved_go_live_at date

Wyświetl plik

@ -0,0 +1,67 @@
# Generated by Django 4.0.3 on 2022-04-26 12:31
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("wagtailcore", "0069_log_entry_jsonfield"),
]
operations = [
migrations.RenameModel(
old_name="PageRevision",
new_name="Revision",
),
migrations.AlterModelOptions(
name="revision",
options={"verbose_name": "revision", "verbose_name_plural": "revisions"},
),
migrations.AlterField(
model_name="revision",
name="page",
field=models.CharField(max_length=255, verbose_name="object id"),
),
migrations.RenameField(
model_name="revision",
old_name="page",
new_name="object_id",
),
migrations.AddField(
model_name="revision",
name="content_type",
field=models.ForeignKey(
null=True,
on_delete=models.CASCADE,
related_name="+",
to="contenttypes.contenttype",
),
),
migrations.AddField(
model_name="revision",
name="base_content_type",
field=models.ForeignKey(
null=True,
on_delete=models.CASCADE,
related_name="+",
to="contenttypes.contenttype",
),
),
migrations.AddIndex(
model_name="revision",
index=models.Index(
fields=["content_type", "object_id"],
name="content_object_idx",
),
),
migrations.AddIndex(
model_name="revision",
index=models.Index(
fields=["base_content_type", "object_id"],
name="base_content_object_idx",
),
),
]

Wyświetl plik

@ -0,0 +1,37 @@
# Generated by Django 4.0.3 on 2022-04-26 13:18
from django.db import migrations, models
from django.db.models.fields.json import KeyTextTransform
from django.db.models.functions import Cast
def populate_revision_content_type(apps, schema_editor):
ContentType = apps.get_model("contenttypes.ContentType")
Revision = apps.get_model("wagtailcore.Revision")
page_type = ContentType.objects.get(app_label="wagtailcore", model="page")
Revision.objects.all().update(
base_content_type=page_type,
content_type_id=Cast(
KeyTextTransform("content_type", models.F("content")),
output_field=models.PositiveIntegerField(),
),
)
def empty_revision_content_type(apps, schema_editor):
Revision = apps.get_model("wagtailcore.Revision")
Revision.objects.all().update(base_content_type=None, content_type=None)
class Migration(migrations.Migration):
dependencies = [
("wagtailcore", "0070_rename_pagerevision_revision"),
]
operations = [
migrations.RunPython(
populate_revision_content_type,
empty_revision_content_type,
)
]

Wyświetl plik

@ -0,0 +1,33 @@
# Generated by Django 4.0.3 on 2022-04-26 13:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("wagtailcore", "0071_populate_revision_content_type"),
]
operations = [
migrations.AlterField(
model_name="revision",
name="content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
related_name="+",
),
),
migrations.AlterField(
model_name="revision",
name="base_content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
related_name="+",
),
),
]

Wyświetl plik

@ -12,6 +12,7 @@ as Page.
import functools
import logging
import uuid
import warnings
from io import StringIO
from urllib.parse import urlparse
@ -28,7 +29,7 @@ from django.core.serializers.json import DjangoJSONEncoder
from django.db import models, transaction
from django.db.models import DEFERRED, Q, Value
from django.db.models.expressions import OuterRef, Subquery
from django.db.models.functions import Concat, Substr
from django.db.models.functions import Cast, Concat, Substr
from django.dispatch import receiver
from django.http import Http404
from django.template.response import TemplateResponse
@ -78,6 +79,7 @@ from wagtail.signals import (
)
from wagtail.treebeard import TreebeardPathFixMixin
from wagtail.url_routing import RouteResult
from wagtail.utils.deprecation import RemovedInWagtail50Warning
from .audit_log import ( # noqa
BaseLogEntry,
@ -315,7 +317,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
verbose_name=_("latest revision created at"), null=True, editable=False
)
live_revision = models.ForeignKey(
"PageRevision",
"Revision",
related_name="+",
verbose_name=_("live revision"),
on_delete=models.SET_NULL,
@ -400,6 +402,12 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
def __str__(self):
return self.title
@property
def revisions(self):
# This acts as a replacement for Django's related manager since we don't
# use a GenericRelation/GenericForeignKey.
return Revision.page_revisions.filter(object_id=self.id)
@classmethod
def get_streamfield_names(cls):
return get_streamfield_names(cls)
@ -899,12 +907,22 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
comment.save()
# Create revision
revision = self.revisions.create(
content=self.serializable_data(),
user=user,
# We want to always use the default Page model's ContentType as the
# base_content_type so that we can query for page revisions without
# having to know the specific Page type.
revision = Revision.objects.create(
content_type_id=self.content_type_id,
base_content_type_id=get_default_page_content_type().id,
object_id=self.id,
submitted_for_moderation=submitted_for_moderation,
user=user,
approved_go_live_at=approved_go_live_at,
content=self.serializable_data(),
)
# This is necessary to prevent fetching a fresh page instance when
# changing first_published_at.
# Ref: https://github.com/wagtail/wagtail/pull/3498
revision.content_object = self
for comment in new_comments:
comment.revision_created = revision
@ -985,7 +1003,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
latest_revision = self.get_latest_revision()
if latest_revision:
return latest_revision.as_page_object()
return latest_revision.as_object()
else:
return self.specific
@ -998,7 +1016,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
This is called by Wagtail whenever a page with aliases is published.
:param revision: The revision of the original page that we are updating to (used for logging purposes)
:type revision: PageRevision, optional
:type revision: Revision, optional
:param user: The user who is publishing (used for logging purposes)
:type user: User, optional
"""
@ -2144,17 +2162,59 @@ class Orderable(models.Model):
ordering = ["sort_order"]
class RevisionQuerySet(models.QuerySet):
def page_revisions(self):
return self.filter(base_content_type=get_default_page_content_type())
def submitted(self):
return self.filter(submitted_for_moderation=True)
def for_instance(self, instance):
return self.filter(
content_type=ContentType.objects.get_for_model(
instance, for_concrete_model=False
),
object_id=str(instance.pk),
)
def filter(self, *args, **kwargs):
# Make sure the object_id is a string, so queries that use the target model's
# id still works even when its id is not a string
# (e.g. Revision.objects.filter(object_id=some_page.id)).
if "object_id" in kwargs:
kwargs["object_id"] = str(kwargs["object_id"])
return super().filter(*args, **kwargs)
class PageRevisionsManager(models.Manager):
def get_queryset(self):
return RevisionQuerySet(self.model, using=self._db).page_revisions()
def for_instance(self, instance):
return self.get_queryset().for_instance(instance)
def submitted(self):
return self.get_queryset().submitted()
class SubmittedRevisionsManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(submitted_for_moderation=True)
return RevisionQuerySet(self.model, using=self._db).submitted()
def for_instance(self, instance):
return self.get_queryset().for_instance(instance)
class PageRevision(models.Model):
page = models.ForeignKey(
"Page",
verbose_name=_("page"),
related_name="revisions",
on_delete=models.CASCADE,
class Revision(models.Model):
content_type = models.ForeignKey(
ContentType, on_delete=models.CASCADE, related_name="+"
)
base_content_type = models.ForeignKey(
ContentType, on_delete=models.CASCADE, related_name="+"
)
object_id = models.CharField(
max_length=255,
verbose_name=_("object id"),
)
submitted_for_moderation = models.BooleanField(
verbose_name=_("submitted for moderation"), default=False, db_index=True
@ -2175,8 +2235,40 @@ class PageRevision(models.Model):
)
objects = models.Manager()
page_revisions = PageRevisionsManager()
submitted_revisions = SubmittedRevisionsManager()
@cached_property
def content_object(self):
return self.base_content_type.get_object_for_this_type(pk=self.object_id)
@cached_property
def specific_content_object(self):
if isinstance(self.content_object, Page):
return self.content_object.specific
return self.content_type.get_object_for_this_type(pk=self.object_id)
@property
def page(self):
warnings.warn(
"Revisions should access .content_object instead of .page "
"to retrieve the object.",
category=RemovedInWagtail50Warning,
stacklevel=2,
)
return self.content_object
@property
def page_id(self):
warnings.warn(
"Revisions should access .object_id instead of .page_id "
"to retrieve the object's primary key. For page revisions, "
"you may need to cast the object_id to integer first.",
category=RemovedInWagtail50Warning,
stacklevel=2,
)
return int(self.object_id)
def save(self, user=None, *args, **kwargs):
# Set default value for created_at to now
# We cannot use auto_now_add as that will override
@ -2184,12 +2276,19 @@ class PageRevision(models.Model):
if self.created_at is None:
self.created_at = timezone.now()
# Set default value for base_content_type to the content_type.
# Page revisions should set this to the default Page model's content type,
# but the distinction may not be necessary for models that do not use inheritance.
if self.base_content_type_id is None:
self.base_content_type_id = self.content_type_id
super().save(*args, **kwargs)
if self.submitted_for_moderation:
# ensure that all other revisions of this page have the 'submitted for moderation' flag unset
self.page.revisions.exclude(id=self.id).update(
submitted_for_moderation=False
)
# ensure that all other revisions of this object have the 'submitted for moderation' flag unset
Revision.objects.filter(
base_content_type_id=self.base_content_type_id,
object_id=self.object_id,
).exclude(id=self.id).update(submitted_for_moderation=False)
if (
self.approved_go_live_at is None
@ -2197,7 +2296,7 @@ class PageRevision(models.Model):
and "approved_go_live_at" in kwargs["update_fields"]
):
# Log scheduled revision publish cancellation
page = self.as_page_object()
page = self.as_object()
# go_live_at = kwargs['update_fields'][]
log(
instance=page,
@ -2215,19 +2314,28 @@ class PageRevision(models.Model):
revision=self,
)
def as_object(self):
return self.specific_content_object.with_content_json(self.content)
def as_page_object(self):
return self.page.specific.with_content_json(self.content)
warnings.warn(
"Revisions should use .as_object() instead of .as_page_object() "
"to create the object.",
category=RemovedInWagtail50Warning,
stacklevel=2,
)
return self.as_object()
def approve_moderation(self, user=None):
if self.submitted_for_moderation:
logger.info(
'Page moderation approved: "%s" id=%d revision_id=%d',
self.page.title,
self.page.id,
self.content_object.title,
self.content_object.id,
self.id,
)
log(
instance=self.as_page_object(),
instance=self.as_object(),
action="wagtail.moderation.approve",
user=user,
revision=self,
@ -2238,12 +2346,12 @@ class PageRevision(models.Model):
if self.submitted_for_moderation:
logger.info(
'Page moderation rejected: "%s" id=%d revision_id=%d',
self.page.title,
self.page.id,
self.content_object.title,
self.content_object.id,
self.id,
)
log(
instance=self.as_page_object(),
instance=self.as_object(),
action="wagtail.moderation.reject",
user=user,
revision=self,
@ -2257,7 +2365,10 @@ class PageRevision(models.Model):
# newer than any revision that might exist in the database
return True
latest_revision = (
PageRevision.objects.filter(page_id=self.page_id)
Revision.objects.filter(
base_content_type_id=self.base_content_type_id,
object_id=self.object_id,
)
.order_by("-created_at", "-id")
.first()
)
@ -2268,7 +2379,7 @@ class PageRevision(models.Model):
try:
next_revision = self.get_next()
except PageRevision.DoesNotExist:
except Revision.DoesNotExist:
next_revision = None
if next_revision:
@ -2287,17 +2398,33 @@ class PageRevision(models.Model):
).execute()
def get_previous(self):
return self.get_previous_by_created_at(page=self.page)
return self.get_previous_by_created_at(
base_content_type_id=self.base_content_type_id,
object_id=self.object_id,
)
def get_next(self):
return self.get_next_by_created_at(page=self.page)
return self.get_next_by_created_at(
base_content_type_id=self.base_content_type_id,
object_id=self.object_id,
)
def __str__(self):
return '"' + str(self.page) + '" at ' + str(self.created_at)
return '"' + str(self.content_object) + '" at ' + str(self.created_at)
class Meta:
verbose_name = _("page revision")
verbose_name_plural = _("page revisions")
verbose_name = _("revision")
verbose_name_plural = _("revisions")
indexes = [
models.Index(
fields=["content_type", "object_id"],
name="content_object_idx",
),
models.Index(
fields=["base_content_type", "object_id"],
name="base_content_object_idx",
),
]
PAGE_PERMISSION_TYPES = [
@ -2366,9 +2493,9 @@ class UserPagePermissionsProxy:
# Deal with the trivial cases first...
if not self.user.is_active:
return PageRevision.objects.none()
return Revision.objects.none()
if self.user.is_superuser:
return PageRevision.submitted_revisions.all()
return Revision.page_revisions.submitted()
# get the list of pages for which they have direct publish permission
# (i.e. they can publish any page within this subtree)
@ -2378,16 +2505,20 @@ class UserPagePermissionsProxy:
.distinct()
)
if not publishable_pages_paths:
return PageRevision.objects.none()
return Revision.objects.none()
# compile a filter expression to apply to the PageRevision.submitted_revisions manager:
# compile a filter expression to apply to the Revision.page_revisions.submitted() queryset:
# return only those pages whose paths start with one of the publishable_pages paths
only_my_sections = Q(page__path__startswith=publishable_pages_paths[0])
only_my_sections = Q(path__startswith=publishable_pages_paths[0])
for page_path in publishable_pages_paths[1:]:
only_my_sections = only_my_sections | Q(page__path__startswith=page_path)
only_my_sections = only_my_sections | Q(path__startswith=page_path)
# return the filtered queryset
return PageRevision.submitted_revisions.filter(only_my_sections)
return Revision.page_revisions.submitted().filter(
object_id__in=Page.objects.filter(only_my_sections).values_list(
Cast("pk", output_field=models.CharField()), flat=True
)
)
def for_page(self, page):
"""Return a PagePermissionTester object that can be used to query whether this user has
@ -3480,8 +3611,8 @@ class WorkflowState(models.Model):
def revisions(self):
"""Returns all page revisions associated with task states linked to the current workflow state"""
return PageRevision.objects.filter(
page_id=self.page_id,
return Revision.page_revisions.filter(
object_id=self.page_id,
id__in=self.task_states.values_list("page_revision_id", flat=True),
).defer("content")
@ -3623,7 +3754,7 @@ class TaskState(models.Model):
related_name="task_states",
)
page_revision = models.ForeignKey(
"PageRevision",
"Revision",
on_delete=models.CASCADE,
verbose_name=_("page revision"),
related_name="task_states",
@ -3798,7 +3929,7 @@ class TaskState(models.Model):
def log_state_change_action(self, user, action):
"""Log the approval/rejection action"""
page = self.page_revision.as_page_object()
page = self.page_revision.as_object()
next_task = self.workflow_state.get_next_task()
next_task_data = None
if next_task:
@ -3888,7 +4019,7 @@ class PageLogEntry(BaseLogEntry):
)
# Pointer to a specific page revision
revision = models.ForeignKey(
"wagtailcore.PageRevision",
"wagtailcore.Revision",
null=True,
blank=True,
on_delete=models.DO_NOTHING,
@ -3950,7 +4081,7 @@ class Comment(ClusterableModel):
updated_at = models.DateTimeField(auto_now=True)
revision_created = models.ForeignKey(
PageRevision,
Revision,
on_delete=models.CASCADE,
related_name="created_comments",
null=True,

Wyświetl plik

@ -456,13 +456,13 @@ class PageQuerySet(SearchableQuerySetMixin, TreeQuerySet):
Annotates each page with the existence of an approved go live time.
Used by `approved_schedule` property on `wagtailcore.models.Page`.
"""
from .models import PageRevision
from .models import Revision
return self.annotate(
_approved_schedule=Exists(
PageRevision.objects.exclude(approved_go_live_at__isnull=True).filter(
page__pk=OuterRef("pk")
)
Revision.page_revisions.exclude(
approved_go_live_at__isnull=True
).filter(object_id=OuterRef("pk"))
)
)

Wyświetl plik

@ -8,7 +8,7 @@ from django.db import models
from django.test import TestCase
from django.utils import timezone
from wagtail.models import Collection, Page, PageLogEntry, PageRevision
from wagtail.models import Collection, Page, PageLogEntry, Revision
from wagtail.signals import page_published, page_unpublished
from wagtail.test.testapp.models import EventPage, SecretPage, SimplePage
@ -191,7 +191,7 @@ class TestPublishScheduledPagesCommand(TestCase):
p = Page.objects.get(slug="hello-world")
self.assertFalse(p.live)
self.assertTrue(
PageRevision.objects.filter(page=p)
Revision.page_revisions.filter(object_id=p.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -203,7 +203,7 @@ class TestPublishScheduledPagesCommand(TestCase):
self.assertTrue(p.first_published_at)
self.assertFalse(p.has_unpublished_changes)
self.assertFalse(
PageRevision.objects.filter(page=p)
Revision.page_revisions.filter(object_id=p.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -251,7 +251,7 @@ class TestPublishScheduledPagesCommand(TestCase):
p = Page.objects.get(slug="hello-world")
self.assertFalse(p.live)
self.assertTrue(
PageRevision.objects.filter(page=p)
Revision.page_revisions.filter(object_id=p.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -261,7 +261,7 @@ class TestPublishScheduledPagesCommand(TestCase):
p = Page.objects.get(slug="hello-world")
self.assertFalse(p.live)
self.assertTrue(
PageRevision.objects.filter(page=p)
Revision.page_revisions.filter(object_id=p.id)
.exclude(approved_go_live_at__isnull=True)
.exists()
)
@ -336,14 +336,18 @@ class TestPublishScheduledPagesCommand(TestCase):
p = Page.objects.get(slug="hello-world")
self.assertFalse(p.live)
self.assertTrue(
PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists()
Revision.page_revisions.filter(
object_id=p.id, submitted_for_moderation=True
).exists()
)
management.call_command("publish_scheduled_pages")
p = Page.objects.get(slug="hello-world")
self.assertFalse(
PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists()
Revision.page_revisions.filter(
object_id=p.id, submitted_for_moderation=True
).exists()
)
@ -379,8 +383,12 @@ class TestPurgeRevisionsCommand(TestCase):
self.run_command()
# revision 1 should be deleted, revision 2 should not be
self.assertNotIn(revision_1, PageRevision.objects.filter(page=self.page))
self.assertIn(revision_2, PageRevision.objects.filter(page=self.page))
self.assertNotIn(
revision_1, Revision.page_revisions.filter(object_id=self.page.id)
)
self.assertIn(
revision_2, Revision.page_revisions.filter(object_id=self.page.id)
)
def test_revisions_in_moderation_not_purged(self):
@ -391,8 +399,8 @@ class TestPurgeRevisionsCommand(TestCase):
self.run_command()
self.assertTrue(
PageRevision.objects.filter(
page=self.page, submitted_for_moderation=True
Revision.page_revisions.filter(
object_id=self.page.id, submitted_for_moderation=True
).exists()
)
@ -408,7 +416,9 @@ class TestPurgeRevisionsCommand(TestCase):
self.run_command()
# even though no longer the latest revision, the old revision should stay as it is
# attached to an in progress workflow
self.assertIn(revision, PageRevision.objects.filter(page=self.page))
self.assertIn(
revision, Revision.page_revisions.filter(object_id=self.page.id)
)
except ImportError:
pass
@ -422,7 +432,9 @@ class TestPurgeRevisionsCommand(TestCase):
self.run_command()
self.assertIn(approved_revision, PageRevision.objects.filter(page=self.page))
self.assertIn(
approved_revision, Revision.page_revisions.filter(object_id=self.page.id)
)
def test_purge_revisions_with_date_cutoff(self):
@ -433,7 +445,9 @@ class TestPurgeRevisionsCommand(TestCase):
self.run_command(days=30)
# revision should not be deleted, as it is younger than 30 days
self.assertIn(old_revision, PageRevision.objects.filter(page=self.page))
self.assertIn(
old_revision, Revision.page_revisions.filter(object_id=self.page.id)
)
old_revision.created_at = timezone.now() - timedelta(days=31)
old_revision.save()
@ -441,7 +455,9 @@ class TestPurgeRevisionsCommand(TestCase):
self.run_command(days=30)
# revision is now older than 30 days, so should be deleted
self.assertNotIn(old_revision, PageRevision.objects.filter(page=self.page))
self.assertNotIn(
old_revision, Revision.page_revisions.filter(object_id=self.page.id)
)
class TestCreateLogEntriesFromRevisionsCommand(TestCase):
@ -509,8 +525,9 @@ class TestCreateLogEntriesFromRevisionsCommand(TestCase):
def test_command_doesnt_crash_for_revisions_without_page_model(self):
with mock.patch(
"wagtail.models.ContentType.model_class",
"wagtail.models.Page.specific_class",
return_value=None,
new_callable=mock.PropertyMock,
):
management.call_command("create_log_entries_from_revisions")
self.assertEqual(PageLogEntry.objects.count(), 0)

Wyświetl plik

@ -1489,7 +1489,7 @@ class TestCopyPage(TestCase):
# get_latest_revision_as_page might bypass the revisions table if it determines
# that there are no draft edits since publish - so retrieve it explicitly from the
# revision data, to ensure it's been updated there too
latest_revision = new_christmas_event.get_latest_revision().as_page_object()
latest_revision = new_christmas_event.get_latest_revision().as_object()
self.assertEqual(latest_revision.title, "New christmas event")
self.assertEqual(latest_revision.slug, "new-christmas-event")

Wyświetl plik

@ -13,6 +13,7 @@ from wagtail.test.testapp.models import (
SingleEventPage,
StreamPage,
)
from wagtail.test.utils import WagtailTestUtils
class TestPageQuerySet(TestCase):
@ -697,7 +698,7 @@ class TestPageQuerySetSearch(TestCase):
self.assertNotIn((EventPage, unpublished_event), unpublish_signals_fired)
class TestSpecificQuery(TestCase):
class TestSpecificQuery(TestCase, WagtailTestUtils):
"""
Test the .specific() queryset method. This is isolated in its own test case
because it is sensitive to database changes that might happen for other
@ -816,15 +817,18 @@ class TestSpecificQuery(TestCase):
# Ensure annotations are reapplied to specific() page queries
pages = Page.objects.live()
pages.first().save_revision()
pages.last().save_revision()
user = self.create_test_user()
pages.first().subscribers.create(user=user, comment_notifications=False)
pages.last().subscribers.create(user=user, comment_notifications=False)
results = (
Page.objects.live().specific().annotate(revision_count=Count("revisions"))
Page.objects.live()
.specific()
.annotate(subscribers_count=Count("subscribers"))
)
self.assertEqual(results.first().revision_count, 1)
self.assertEqual(results.last().revision_count, 1)
self.assertEqual(results.first().subscribers_count, 1)
self.assertEqual(results.last().subscribers_count, 1)
def test_specific_query_with_search_and_annotation(self):
# Ensure annotations are reapplied to specific() page queries