mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-05 22:14:45 +00:00
Add .specific() page queryset method
This commit is contained in:
parent
bbd4d6d3d1
commit
7d7eece0d1
5 changed files with 420 additions and 1 deletions
|
|
@ -196,3 +196,15 @@ Reference
|
||||||
|
|
||||||
# Unpublish current_page and all of its children
|
# Unpublish current_page and all of its children
|
||||||
Page.objects.descendant_of(current_page, inclusive=True).unpublish()
|
Page.objects.descendant_of(current_page, inclusive=True).unpublish()
|
||||||
|
|
||||||
|
.. automethod:: specific
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Get the specific instance of all children of the hompage,
|
||||||
|
# in a minimum number of database queries.
|
||||||
|
homepage.get_children().specific()
|
||||||
|
|
||||||
|
See also: :py:attr:`Page.specific <wagtail.wagtailcore.models.Page.specific>`
|
||||||
|
|
|
||||||
256
wagtail/tests/testapp/fixtures/test_specific.json
Normal file
256
wagtail/tests/testapp/fixtures/test_specific.json
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"pk": 1,
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"fields": {
|
||||||
|
"title": "Root",
|
||||||
|
"numchild": 1,
|
||||||
|
"show_in_menus": false,
|
||||||
|
"live": true,
|
||||||
|
"depth": 1,
|
||||||
|
"content_type": ["wagtailcore", "page"],
|
||||||
|
"path": "0001",
|
||||||
|
"url_path": "/",
|
||||||
|
"slug": "root"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 2,
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"fields": {
|
||||||
|
"title": "Welcome to the Wagtail test site!",
|
||||||
|
"numchild": 5,
|
||||||
|
"show_in_menus": false,
|
||||||
|
"live": true,
|
||||||
|
"depth": 2,
|
||||||
|
"content_type": ["wagtailcore", "page"],
|
||||||
|
"path": "00010001",
|
||||||
|
"url_path": "/home/",
|
||||||
|
"slug": "home"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 3,
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"fields": {
|
||||||
|
"title": "Events",
|
||||||
|
"numchild": 4,
|
||||||
|
"show_in_menus": true,
|
||||||
|
"live": true,
|
||||||
|
"depth": 3,
|
||||||
|
"content_type": ["tests", "eventindex"],
|
||||||
|
"path": "000100010001",
|
||||||
|
"url_path": "/home/events/",
|
||||||
|
"slug": "events"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 3,
|
||||||
|
"model": "tests.eventindex",
|
||||||
|
"fields": {
|
||||||
|
"intro": "Look at our lovely events."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 4,
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"fields": {
|
||||||
|
"title": "Christmas",
|
||||||
|
"numchild": 0,
|
||||||
|
"show_in_menus": true,
|
||||||
|
"live": true,
|
||||||
|
"depth": 4,
|
||||||
|
"content_type": ["tests", "eventpage"],
|
||||||
|
"path": "0001000100010001",
|
||||||
|
"url_path": "/home/events/christmas/",
|
||||||
|
"slug": "christmas",
|
||||||
|
"owner": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 4,
|
||||||
|
"model": "tests.eventpage",
|
||||||
|
"fields": {
|
||||||
|
"date_from": "2014-12-25",
|
||||||
|
"audience": "public",
|
||||||
|
"location": "The North Pole",
|
||||||
|
"body": "<p>Chestnuts roasting on an open fire</p>",
|
||||||
|
"cost": "Free",
|
||||||
|
"feed_image": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 1,
|
||||||
|
"model": "wagtailimages.image",
|
||||||
|
"fields": {
|
||||||
|
"title": "A missing image",
|
||||||
|
"file": "original_images/missing.jpg",
|
||||||
|
"width": 1000,
|
||||||
|
"height": 1000,
|
||||||
|
"created_at": "2014-01-01T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 5,
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"fields": {
|
||||||
|
"title": "Tentative Unpublished Event",
|
||||||
|
"numchild": 0,
|
||||||
|
"show_in_menus": true,
|
||||||
|
"live": false,
|
||||||
|
"depth": 4,
|
||||||
|
"content_type": ["tests", "eventpage"],
|
||||||
|
"path": "0001000100010002",
|
||||||
|
"url_path": "/home/events/tentative-unpublished-event/",
|
||||||
|
"slug": "tentative-unpublished-event",
|
||||||
|
"owner": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 5,
|
||||||
|
"model": "tests.eventpage",
|
||||||
|
"fields": {
|
||||||
|
"date_from": "2015-07-04",
|
||||||
|
"audience": "public",
|
||||||
|
"location": "The moon",
|
||||||
|
"body": "<p>I haven't worked out the details yet, but it's going to have cake and ponies</p>",
|
||||||
|
"cost": "Free"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 6,
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"fields": {
|
||||||
|
"title": "Someone Else's Event",
|
||||||
|
"numchild": 0,
|
||||||
|
"show_in_menus": true,
|
||||||
|
"live": false,
|
||||||
|
"depth": 4,
|
||||||
|
"content_type": ["tests", "eventpage"],
|
||||||
|
"path": "0001000100010003",
|
||||||
|
"url_path": "/home/events/someone-elses-event/",
|
||||||
|
"slug": "someone-elses-event",
|
||||||
|
"owner": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 6,
|
||||||
|
"model": "tests.eventpage",
|
||||||
|
"fields": {
|
||||||
|
"date_from": "2015-07-04",
|
||||||
|
"audience": "private",
|
||||||
|
"location": "The moon",
|
||||||
|
"body": "<p>your name's not down, you're not coming in</p>",
|
||||||
|
"cost": "Free (but not for you)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 7,
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"fields": {
|
||||||
|
"title": "About us",
|
||||||
|
"numchild": 0,
|
||||||
|
"show_in_menus": true,
|
||||||
|
"live": true,
|
||||||
|
"depth": 3,
|
||||||
|
"content_type": ["tests", "simplepage"],
|
||||||
|
"path": "000100010002",
|
||||||
|
"url_path": "/home/about-us/",
|
||||||
|
"slug": "about-us"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 7,
|
||||||
|
"model": "tests.simplepage",
|
||||||
|
"fields": {
|
||||||
|
"content": "<p>We are really good.</p>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 11,
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"fields": {
|
||||||
|
"title": "Other events",
|
||||||
|
"numchild": 1,
|
||||||
|
"show_in_menus": true,
|
||||||
|
"live": true,
|
||||||
|
"depth": 3,
|
||||||
|
"content_type": ["tests", "simplepage"],
|
||||||
|
"path": "000100010005",
|
||||||
|
"url_path": "/home/other/",
|
||||||
|
"slug": "other"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 11,
|
||||||
|
"model": "tests.simplepage",
|
||||||
|
"fields": {
|
||||||
|
"content": "<p>Other events</p>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 12,
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"fields": {
|
||||||
|
"title": "Special event",
|
||||||
|
"numchild": 0,
|
||||||
|
"show_in_menus": false,
|
||||||
|
"live": true,
|
||||||
|
"depth": 4,
|
||||||
|
"content_type": ["tests", "eventpage"],
|
||||||
|
"path": "0001000100050001",
|
||||||
|
"url_path": "/home/other/special-event/",
|
||||||
|
"slug": "special-event"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 12,
|
||||||
|
"model": "tests.eventpage",
|
||||||
|
"fields": {
|
||||||
|
"date_from": "2015-07-04",
|
||||||
|
"audience": "public",
|
||||||
|
"location": "Hobart",
|
||||||
|
"body": "<p>Party time</p>",
|
||||||
|
"cost": "free"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 1,
|
||||||
|
"model": "wagtailcore.site",
|
||||||
|
"fields": {
|
||||||
|
"root_page": 2,
|
||||||
|
"hostname": "localhost",
|
||||||
|
"port": 80,
|
||||||
|
"is_default_site": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"pk": 1,
|
||||||
|
"model": "customuser.customuser",
|
||||||
|
"fields": {
|
||||||
|
"username": "superuser",
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": true,
|
||||||
|
"is_staff": true,
|
||||||
|
"groups": [
|
||||||
|
],
|
||||||
|
"user_permissions": [],
|
||||||
|
"password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22",
|
||||||
|
"email": "superuser@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
|
@ -232,6 +232,9 @@ class PageManager(models.Manager):
|
||||||
def search(self, query_string, fields=None, backend='default'):
|
def search(self, query_string, fields=None, backend='default'):
|
||||||
return self.get_queryset().search(query_string, fields=fields, backend=backend)
|
return self.get_queryset().search(query_string, fields=fields, backend=backend)
|
||||||
|
|
||||||
|
def specific(self):
|
||||||
|
return self.get_queryset().specific()
|
||||||
|
|
||||||
|
|
||||||
class PageBase(models.base.ModelBase):
|
class PageBase(models.base.ModelBase):
|
||||||
"""Metaclass for Page"""
|
"""Metaclass for Page"""
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django import VERSION as DJANGO_VERSION
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
|
@ -206,3 +209,59 @@ class PageQuerySet(MP_NodeQuerySet):
|
||||||
This unpublishes all pages in the QuerySet
|
This unpublishes all pages in the QuerySet
|
||||||
"""
|
"""
|
||||||
self.update(live=False, has_unpublished_changes=True)
|
self.update(live=False, has_unpublished_changes=True)
|
||||||
|
|
||||||
|
def specific(self):
|
||||||
|
"""
|
||||||
|
This efficiently gets all the specific pages for the queryset, using
|
||||||
|
the minimum number of queries.
|
||||||
|
"""
|
||||||
|
if DJANGO_VERSION >= (1, 9):
|
||||||
|
clone = self._clone()
|
||||||
|
clone._iterator_class = SpecificIterator
|
||||||
|
return clone
|
||||||
|
else:
|
||||||
|
return self._clone(klass=SpecificQuerySet)
|
||||||
|
|
||||||
|
|
||||||
|
def specific_iterator(qs):
|
||||||
|
"""
|
||||||
|
This efficiently iterates all the specific pages in a queryset, using
|
||||||
|
the minimum number of queries.
|
||||||
|
|
||||||
|
This should be called from ``PageQuerySet.specific``
|
||||||
|
"""
|
||||||
|
pks_and_types = qs.values_list('pk', 'content_type')
|
||||||
|
pks_by_type = defaultdict(list)
|
||||||
|
for pk, content_type in pks_and_types:
|
||||||
|
pks_by_type[content_type].append(pk)
|
||||||
|
|
||||||
|
# Content types are cached by ID, so this will not run any queries.
|
||||||
|
content_types = {pk: ContentType.objects.get_for_id(pk)
|
||||||
|
for _, pk in pks_and_types}
|
||||||
|
|
||||||
|
# Get the specific instances of all pages, one model class at a time.
|
||||||
|
pages_by_type = {}
|
||||||
|
for content_type, pks in pks_by_type.items():
|
||||||
|
model = content_types[content_type].model_class()
|
||||||
|
pages = model.objects.filter(pk__in=pks)
|
||||||
|
pages_by_type[content_type] = {page.pk: page for page in pages}
|
||||||
|
|
||||||
|
# Yield all of the pages, in the order they occurred in the original query.
|
||||||
|
for pk, content_type in pks_and_types:
|
||||||
|
yield pages_by_type[content_type][pk]
|
||||||
|
|
||||||
|
|
||||||
|
# Django 1.9 changed how extending QuerySets with different iterators behaved
|
||||||
|
# considerably, in a way that is not easily compatible between the two versions
|
||||||
|
if DJANGO_VERSION >= (1, 9):
|
||||||
|
# TODO Test this once Wagtail runs under Django 1.9.
|
||||||
|
from django.db.models.query import BaseIterator
|
||||||
|
|
||||||
|
class SpecificIterator(BaseIterator):
|
||||||
|
__iter__ = specific_iterator
|
||||||
|
|
||||||
|
else:
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
|
class SpecificQuerySet(QuerySet):
|
||||||
|
iterator = specific_iterator
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,6 @@ class TestPageQuerySet(TestCase):
|
||||||
contact_us = Page.objects.get(url_path='/home/contact-us/')
|
contact_us = Page.objects.get(url_path='/home/contact-us/')
|
||||||
self.assertTrue(pages.filter(id=contact_us.id).exists())
|
self.assertTrue(pages.filter(id=contact_us.id).exists())
|
||||||
|
|
||||||
|
|
||||||
def test_not_type(self):
|
def test_not_type(self):
|
||||||
pages = Page.objects.not_type(EventPage)
|
pages = Page.objects.not_type(EventPage)
|
||||||
|
|
||||||
|
|
@ -321,3 +320,93 @@ class TestPageQuerySet(TestCase):
|
||||||
|
|
||||||
# Check that the event is in the results
|
# Check that the event is in the results
|
||||||
self.assertTrue(pages.filter(id=event.id).exists())
|
self.assertTrue(pages.filter(id=event.id).exists())
|
||||||
|
|
||||||
|
|
||||||
|
class TestSpecificQuery(TestCase):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
tests.
|
||||||
|
|
||||||
|
The fixture sets up a page structure like:
|
||||||
|
|
||||||
|
=========== =========================================
|
||||||
|
Type Path
|
||||||
|
=========== =========================================
|
||||||
|
Page /
|
||||||
|
Page /home/
|
||||||
|
SimplePage /home/about-us/
|
||||||
|
EventIndex /home/events/
|
||||||
|
EventPage /home/events/christmas/
|
||||||
|
EventPage /home/events/someone-elses-event/
|
||||||
|
EventPage /home/events/tentative-unpublished-event/
|
||||||
|
SimplePage /home/other/
|
||||||
|
EventPage /home/other/special-event/
|
||||||
|
=========== =========================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
fixtures = ['test_specific.json']
|
||||||
|
|
||||||
|
def test_specific(self):
|
||||||
|
root = Page.objects.get(url_path='/home/')
|
||||||
|
|
||||||
|
with self.assertNumQueries(0):
|
||||||
|
# The query should be lazy.
|
||||||
|
qs = root.get_descendants().specific()
|
||||||
|
|
||||||
|
with self.assertNumQueries(4):
|
||||||
|
# One query to get page type and ID, one query per page type:
|
||||||
|
# EventIndex, EventPage, SimplePage
|
||||||
|
pages = list(qs)
|
||||||
|
|
||||||
|
self.assertIsInstance(pages, list)
|
||||||
|
self.assertEqual(len(pages), 7)
|
||||||
|
|
||||||
|
for page in pages:
|
||||||
|
# An instance of the specific page type should be returned,
|
||||||
|
# not wagtailcore.Page.
|
||||||
|
content_type = page.content_type
|
||||||
|
model = content_type.model_class()
|
||||||
|
self.assertIsInstance(page, model)
|
||||||
|
|
||||||
|
# The page should already be the specific type, so this should not
|
||||||
|
# need another database query.
|
||||||
|
with self.assertNumQueries(0):
|
||||||
|
self.assertIs(page, page.specific)
|
||||||
|
|
||||||
|
def test_filtering_before_specific(self):
|
||||||
|
# This will get the other events, and then christmas
|
||||||
|
# 'someone-elses-event' and the tentative event are unpublished.
|
||||||
|
|
||||||
|
with self.assertNumQueries(0):
|
||||||
|
qs = Page.objects.live().order_by('-url_path')[:3].specific()
|
||||||
|
|
||||||
|
with self.assertNumQueries(3):
|
||||||
|
# Metadata, EventIndex and EventPage
|
||||||
|
pages = list(qs)
|
||||||
|
|
||||||
|
self.assertEqual(len(pages), 3)
|
||||||
|
|
||||||
|
self.assertEqual(pages, [
|
||||||
|
Page.objects.get(url_path='/home/other/special-event/').specific,
|
||||||
|
Page.objects.get(url_path='/home/other/').specific,
|
||||||
|
Page.objects.get(url_path='/home/events/christmas/').specific])
|
||||||
|
|
||||||
|
def test_filtering_after_specific(self):
|
||||||
|
# This will get the other events, and then christmas
|
||||||
|
# 'someone-elses-event' and the tentative event are unpublished.
|
||||||
|
|
||||||
|
with self.assertNumQueries(0):
|
||||||
|
qs = Page.objects.specific().live().in_menu().order_by('-url_path')[:4]
|
||||||
|
|
||||||
|
with self.assertNumQueries(4):
|
||||||
|
# Metadata, EventIndex, EventPage, SimplePage.
|
||||||
|
pages = list(qs)
|
||||||
|
|
||||||
|
self.assertEqual(len(pages), 4)
|
||||||
|
|
||||||
|
self.assertEqual(pages, [
|
||||||
|
Page.objects.get(url_path='/home/other/').specific,
|
||||||
|
Page.objects.get(url_path='/home/events/christmas/').specific,
|
||||||
|
Page.objects.get(url_path='/home/events/').specific,
|
||||||
|
Page.objects.get(url_path='/home/about-us/').specific])
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue