Add Page.specific_deferred (#6661)

pull/6676/head
Andy Babic 2020-12-13 22:21:17 +00:00 zatwierdzone przez Matt Westcott
rodzic 2da410b2e0
commit f905e705da
5 zmienionych plików z 154 dodań i 5 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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']