Relax model validation when saving pages as draft

pull/12964/head
Matt Westcott 2025-02-22 00:57:51 +00:00 zatwierdzone przez Matt Westcott
rodzic 13dbf61323
commit 1f210831dd
5 zmienionych plików z 54 dodań i 18 usunięć

Wyświetl plik

@ -113,6 +113,7 @@ def clear_page_url_from_cache_on_move(sender, **kwargs):
pre_page_move.connect(clear_old_page_urls_from_cache)
```
(page_slug_changed)=
## `page_slug_changed`
This signal is emitted from a `Page` when a change to its slug is published.

Wyświetl plik

@ -54,6 +54,10 @@ depth: 1
## Upgrade considerations - changes affecting all projects
### `Page.save()` no longer automatically calls `full_clean` for draft pages
In previous releases, the `save()` method on page models called the `full_clean` method to apply [model-level validation rules](inv:django#validating-objects), regardless of whether the page was in a draft or live state, unless this was explicitly disabled by passing `clean=False`. As of this release, saving a page in a draft state (`live=False`) will only perform the minimum validation necessary to ensure data integrity: the title must be non-empty, and the slug must be unique within the parent page. Saving a page with `live=True` will apply full validation as before. If you have user code that creates draft pages and requires them to be validated, you must now call `full_clean` explicitly.
## Upgrade considerations - deprecation of old functionality
### `TAG_LIMIT` and `TAG_SPACES_ALLOWED` settings renamed to `WAGTAIL_TAG_LIMIT` and `WAGTAIL_TAG_SPACES_ALLOWED`

Wyświetl plik

@ -650,8 +650,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
self._set_core_field_defaults()
super().full_clean(*args, **kwargs)
def clean(self):
super().clean()
def _check_slug_is_unique(self):
parent_page = self.get_parent()
if not Page._slug_is_available(self.slug, parent_page, self):
raise ValidationError(
@ -663,6 +662,15 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
}
)
def clean(self):
super().clean()
self._check_slug_is_unique()
def minimal_clean(self):
self._set_core_field_defaults()
self.title = self._meta.get_field("title").clean(self.title, self)
self._check_slug_is_unique()
def is_site_root(self):
"""
Returns True if this page is the root of any site.
@ -682,25 +690,35 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
# ensure that changes are only committed when we have updated all descendant URL paths, to preserve consistency
def save(self, clean=True, user=None, log_action=False, **kwargs):
"""
Overrides default method behavior to make additional updates unique to pages,
such as updating the ``url_path`` value of descendant page to reflect changes
to this page's slug.
Writes the page to the database, performing additional housekeeping tasks to ensure data
integrity:
New pages should generally be saved via the :meth:`~treebeard.mp_tree.MP_Node.add_child`
or :meth:`~treebeard.mp_tree.MP_Node.add_sibling`
method of an existing page, which will correctly set the ``path`` and ``depth``
fields on the new page before saving it.
* ``locale``, ``draft_title`` and ``slug`` are set to default values if not provided, with ``slug``
being generated from the title with a suffix to ensure uniqueness within the parent page
where necessary
* The ``url_path`` field is set based on the ``slug`` and the parent page
* If the ``slug`` has changed, the ``url_path`` of this page and all descendants is updated and
a :ref:`page_slug_changed` signal is sent
By default, pages are validated using ``full_clean()`` before attempting to
save changes to the database, which helps to preserve validity when restoring
pages from historic revisions (which might not necessarily reflect the current
model state). This validation step can be bypassed by calling the method with
``clean=False``.
New pages should be saved by passing the unsaved page instance to the
:meth:`~treebeard.mp_tree.MP_Node.add_child`
or :meth:`~treebeard.mp_tree.MP_Node.add_sibling` method of an existing page, which will correctly update
the fields responsible for tracking the page's location in the tree.
If ``clean=False`` is passed, the page is saved without validation. This is appropriate for updates that only
change metadata such as `latest_revision` while keeping content and page location unchanged.
If ``clean=True`` is passed (the default), and the page has ``live=True`` set, the page is validated using
:meth:`~django.db.models.Model.full_clean` before saving.
If ``clean=True`` is passed, and the page has `live=False` set, only the title and slug fields are validated.
"""
if clean:
self.full_clean()
else:
self._set_core_field_defaults()
if self.live:
self.full_clean()
else:
# Saving as draft; only perform the minimal validation to satisfy data integrity
self.minimal_clean()
slug_changed = False
is_new = self.id is None

Wyświetl plik

@ -349,7 +349,7 @@ class TestPublishScheduledPagesCommand(WagtailTestUtils, TestCase):
.exists()
)
with self.assertNumQueries(47):
with self.assertNumQueries(42):
with self.captureOnCommitCallbacks(execute=True):
management.call_command("publish_scheduled_pages")

Wyświetl plik

@ -104,6 +104,19 @@ class TestValidation(TestCase):
with self.assertRaises(ValidationError):
homepage.add_child(instance=hello_page)
def test_title_is_required_for_draft_page(self):
homepage = Page.objects.get(url_path="/home/")
hello_page = SimplePage(slug="hello-world", content="hello", live=False)
with self.assertRaises(ValidationError):
homepage.add_child(instance=hello_page)
hello_page = SimplePage(
title="", slug="hello-world", content="hello", live=False
)
with self.assertRaises(ValidationError):
homepage.add_child(instance=hello_page)
def test_slug_is_autogenerated(self):
homepage = Page.objects.get(url_path="/home/")