kopia lustrzana https://github.com/wagtail/wagtail
Add Page.specific_deferred (#6661)
rodzic
2da410b2e0
commit
f905e705da
|
@ -13,6 +13,7 @@ Changelog
|
|||
* Support returning None from `register_page_action_menu_item` and `register_snippet_action_menu_item` to skip registering an item (Vadim Karpenko)
|
||||
* Fields on a custom image model can now be defined as required / `blank=False` (Matt Westcott)
|
||||
* Add combined index for Postgres search backend (Will Giddens)
|
||||
* Add `Page.specific_deferred` property for accessing specific page instance without up-front database queries (Andy Babic)
|
||||
* Fix: Stop menu icon overlapping the breadcrumb on small viewport widths in page editor (Karran Besen)
|
||||
* Fix: Make sure document chooser pagination preserves the selected collection when moving between pages (Alex Sa)
|
||||
* Fix: Gracefully handle oEmbed endpoints returning non-JSON responses (Matt Westcott)
|
||||
|
|
|
@ -158,8 +158,12 @@ In addition to the model fields provided, ``Page`` has many properties and metho
|
|||
.. class:: Page
|
||||
:noindex:
|
||||
|
||||
.. automethod:: get_specific
|
||||
|
||||
.. autoattribute:: specific
|
||||
|
||||
.. autoattribute:: specific_deferred
|
||||
|
||||
.. autoattribute:: specific_class
|
||||
|
||||
.. autoattribute:: cached_content_type
|
||||
|
|
|
@ -36,6 +36,7 @@ Other features
|
|||
* Support returning None from ``register_page_action_menu_item`` and ``register_snippet_action_menu_item`` to skip registering an item (Vadim Karpenko)
|
||||
* Fields on a custom image model can now be defined as required / ``blank=False`` (Matt Westcott)
|
||||
* Add combined index for Postgres search backend (Will Giddens)
|
||||
* Add ``Page.specific_deferred`` property for accessing specific page instance without up-front database queries (Andy Babic)
|
||||
|
||||
|
||||
Bug fixes
|
||||
|
|
|
@ -18,7 +18,7 @@ from django.core.exceptions import PermissionDenied, ValidationError
|
|||
from django.core.handlers.base import BaseHandler
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.db import migrations, models, transaction
|
||||
from django.db.models import Q, Value
|
||||
from django.db.models import DEFERRED, Q, Value
|
||||
from django.db.models.expressions import OuterRef, Subquery
|
||||
from django.db.models.functions import Concat, Lower, Substr
|
||||
from django.db.models.signals import pre_save
|
||||
|
@ -1165,11 +1165,24 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
|
|||
)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def specific(self):
|
||||
def get_specific(self, deferred=False, copy_attrs=None):
|
||||
"""
|
||||
.. versionadded:: 2.12
|
||||
|
||||
Return this page in its most specific subclassed form.
|
||||
|
||||
By default, a database query is made to fetch all field values for the
|
||||
specific object. If you only require access to custom methods or other
|
||||
non-field attributes on the specific object, you can use
|
||||
``deferred=True`` to avoid this query. However, any attempts to access
|
||||
specific field values from the returned object will trigger additional
|
||||
database queries.
|
||||
|
||||
If there are attribute values on this object that you wish to be copied
|
||||
over to the specific version (for example: evaluated relationship field
|
||||
values, annotations or cached properties), use `copy_attrs`` to pass an
|
||||
iterable of names of attributes you wish to be copied.
|
||||
|
||||
If called on a page object that is already an instance of the most
|
||||
specific class (e.g. an ``EventPage``), the object will be returned
|
||||
as is, and no database queries or other operations will be triggered.
|
||||
|
@ -1189,10 +1202,50 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
|
|||
# reverted before switching branches). So, the best we can do is
|
||||
# return the page in it's current form.
|
||||
return self
|
||||
|
||||
if isinstance(self, model_class):
|
||||
# self is already the an instance of the most specific class
|
||||
return self
|
||||
return self.cached_content_type.get_object_for_this_type(id=self.id)
|
||||
|
||||
if deferred:
|
||||
# Generate a tuple of values in the order expected by __init__(),
|
||||
# with missing values substituted with DEFERRED ()
|
||||
values = tuple(
|
||||
getattr(self, f.attname, self.pk if f.primary_key else DEFERRED)
|
||||
for f in model_class._meta.concrete_fields
|
||||
)
|
||||
# Create object from known attribute values
|
||||
specific_obj = model_class(*values)
|
||||
specific_obj._state.adding = self._state.adding
|
||||
else:
|
||||
# Fetch object from database
|
||||
specific_obj = model_class._default_manager.get(id=self.id)
|
||||
|
||||
# Copy additional attribute values
|
||||
for attr in copy_attrs or ():
|
||||
if attr in self.__dict__:
|
||||
setattr(specific_obj, attr, getattr(self, attr))
|
||||
|
||||
return specific_obj
|
||||
|
||||
@cached_property
|
||||
def specific(self):
|
||||
"""
|
||||
Returns this page in its most specific subclassed form with all field
|
||||
values fetched from the database. The result is cached in memory.
|
||||
"""
|
||||
return self.get_specific()
|
||||
|
||||
@cached_property
|
||||
def specific_deferred(self):
|
||||
"""
|
||||
.. versionadded:: 2.12
|
||||
|
||||
Returns this page in its most specific subclassed form without any
|
||||
additional field values being fetched from the database. The result
|
||||
is cached in memory.
|
||||
"""
|
||||
return self.get_specific(deferred=True)
|
||||
|
||||
@cached_property
|
||||
def specific_class(self):
|
||||
|
@ -1202,7 +1255,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
|
|||
|
||||
If the model class can no longer be found in the codebase, and the
|
||||
relevant ``ContentType`` has been removed by a database migration,
|
||||
the return value will be ``Page``.
|
||||
the return value will be ``None``.
|
||||
|
||||
If the model class can no longer be found in the codebase, but the
|
||||
relevant ``ContentType`` is still present in the database (usually a
|
||||
|
|
|
@ -944,6 +944,96 @@ class TestLiveRevision(TestCase):
|
|||
)
|
||||
|
||||
|
||||
class TestPageGetSpecific(TestCase):
|
||||
fixtures = ['test.json']
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.page = Page.objects.get(url_path="/home/about-us/")
|
||||
self.page.foo = 'ABC'
|
||||
self.page.bar = {'key': 'value'}
|
||||
self.page.baz = 999
|
||||
|
||||
def test_default(self):
|
||||
# Field values are fetched from the database, hence the query
|
||||
with self.assertNumQueries(1):
|
||||
result = self.page.get_specific(copy_attrs=['foo', 'bar'])
|
||||
|
||||
# The returned instance is the correct type
|
||||
self.assertIsInstance(result, SimplePage)
|
||||
|
||||
# Generic page field values can be accessed for free
|
||||
with self.assertNumQueries(0):
|
||||
self.assertEqual(result.id, self.page.id)
|
||||
self.assertEqual(result.title, self.page.title)
|
||||
|
||||
# Specific model fields values are available without additional queries
|
||||
with self.assertNumQueries(0):
|
||||
self.assertTrue(result.content)
|
||||
|
||||
# 'foo' and 'bar' attributes should have been copied over...
|
||||
self.assertIs(result.foo, self.page.foo)
|
||||
self.assertIs(result.bar, self.page.bar)
|
||||
|
||||
# ...but not 'baz'
|
||||
self.assertFalse(hasattr(result, 'baz'))
|
||||
|
||||
def test_deferred(self):
|
||||
# Field values are NOT fetched from the database, hence no query
|
||||
with self.assertNumQueries(0):
|
||||
result = self.page.get_specific(deferred=True, copy_attrs=['foo', 'bar'])
|
||||
|
||||
# The returned instance is the correct type
|
||||
self.assertIsInstance(result, SimplePage)
|
||||
|
||||
# Generic page field values can be accessed for free
|
||||
with self.assertNumQueries(0):
|
||||
self.assertEqual(result.id, self.page.id)
|
||||
self.assertEqual(result.title, self.page.title)
|
||||
|
||||
# But, specific model fields values are NOT available without additional queries
|
||||
with self.assertNumQueries(1):
|
||||
self.assertTrue(result.content)
|
||||
|
||||
# 'foo' and 'bar' attributes should have been copied over...
|
||||
self.assertIs(result.foo, self.page.foo)
|
||||
self.assertIs(result.bar, self.page.bar)
|
||||
|
||||
# ...but not 'baz'
|
||||
self.assertFalse(hasattr(result, 'baz'))
|
||||
|
||||
def test_specific_cached_property(self):
|
||||
# invoking several times to demonstrate that field values
|
||||
# are fetched only once from the database, and each time the
|
||||
# same object is returned
|
||||
with self.assertNumQueries(1):
|
||||
result = self.page.specific
|
||||
result_2 = self.page.specific
|
||||
result_3 = self.page.specific
|
||||
self.assertIs(result, result_2)
|
||||
self.assertIs(result, result_3)
|
||||
|
||||
self.assertIsInstance(result, SimplePage)
|
||||
# Specific model fields values are available without additional queries
|
||||
with self.assertNumQueries(0):
|
||||
self.assertTrue(result.content)
|
||||
|
||||
def test_specific_deferred_cached_property(self):
|
||||
# invoking several times to demonstrate that the property
|
||||
# returns the same object (without any queries)
|
||||
with self.assertNumQueries(0):
|
||||
result = self.page.specific_deferred
|
||||
result_2 = self.page.specific_deferred
|
||||
result_3 = self.page.specific_deferred
|
||||
self.assertIs(result, result_2)
|
||||
self.assertIs(result, result_3)
|
||||
|
||||
self.assertIsInstance(result, SimplePage)
|
||||
# Specific model fields values are not available without additional queries
|
||||
with self.assertNumQueries(1):
|
||||
self.assertTrue(result.content)
|
||||
|
||||
|
||||
class TestCopyPage(TestCase):
|
||||
fixtures = ['test.json']
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue