From 39319f2191f3103653e5d89bf4f6e2c596fca5d6 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 26 Jul 2016 14:00:17 +0100 Subject: [PATCH] Limit explorer menu nav to the subtree the user has permission over Partially addresses #2401; adapted from #2463. Updates the explorer-nav logic to take the user's permissions into account. The menu now begins at the closest common ancestor node of all pages they have add/edit/publish/lock permission for - as a result, users with permission over a specific deep section of the tree don't have to redundantly drill down to it, and we're a step closer to true 'multi-homed' installations where the user is not made aware of tree structure that exists outside of their own remit. --- .../fixtures/test_explorable_pages.json | 512 ++++++++++++++++++ .../templatetags/wagtailadmin_tags.py | 6 +- .../wagtailadmin/tests/test_explorer_nav.py | 155 ++++++ wagtail/wagtailadmin/tests/tests.py | 14 - wagtail/wagtailadmin/views/pages.py | 2 +- wagtail/wagtailcore/models.py | 83 ++- 6 files changed, 741 insertions(+), 31 deletions(-) create mode 100644 wagtail/tests/testapp/fixtures/test_explorable_pages.json create mode 100644 wagtail/wagtailadmin/tests/test_explorer_nav.py diff --git a/wagtail/tests/testapp/fixtures/test_explorable_pages.json b/wagtail/tests/testapp/fixtures/test_explorable_pages.json new file mode 100644 index 0000000000..fe74d1969b --- /dev/null +++ b/wagtail/tests/testapp/fixtures/test_explorable_pages.json @@ -0,0 +1,512 @@ +[ +{ + "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 testserver!", + "numchild": 1, + "show_in_menus": false, + "live": true, + "depth": 2, + "content_type": ["tests", "eventpage"], + "path": "00010001", + "url_path": "/home/", + "slug": "home" + } +}, +{ + "pk": 2, + "model": "tests.eventpage", + "fields": { + "date_from": "2014-12-25", + "audience": "public", + "location": "The North Pole", + "body": "

Welcome!

", + "cost": "Free" + } +}, + +{ + "pk": 3, + "model": "wagtailcore.page", + "fields": { + "title": "About us", + "numchild": 0, + "show_in_menus": true, + "live": true, + "depth": 3, + "content_type": ["tests", "eventpage"], + "path": "000100010001", + "url_path": "/home/about-us/", + "slug": "about-us" + } +}, +{ + "pk": 3, + "model": "tests.eventpage", + "fields": { + "date_from": "2014-12-25", + "audience": "public", + "location": "The North Pole", + "body": "

Welcome!

", + "cost": "Free" + } +}, + +{ + "pk": 4, + "model": "wagtailcore.page", + "fields": { + "title": "Welcome to example.com!", + "numchild": 1, + "show_in_menus": false, + "live": true, + "depth": 2, + "content_type": ["tests", "eventpage"], + "path": "00010002", + "url_path": "/example-home/", + "slug": "example-home" + } +}, +{ + "pk": 4, + "model": "tests.eventpage", + "fields": { + "date_from": "2014-12-25", + "audience": "public", + "location": "The North Pole", + "body": "

Welcome!

", + "cost": "Free" + } +}, + +{ + "pk": 5, + "model": "wagtailcore.page", + "fields": { + "title": "Content", + "numchild": 2, + "show_in_menus": true, + "live": true, + "depth": 3, + "content_type": ["tests", "eventpage"], + "path": "000100020001", + "url_path": "/example-home/content/", + "slug": "content", + "owner": 1 + } +}, +{ + "pk": 5, + "model": "tests.eventpage", + "fields": { + "date_from": "2014-12-25", + "audience": "public", + "location": "The North Pole", + "body": "

Welcome!

", + "cost": "Free" + } +}, + +{ + "pk": 6, + "model": "wagtailcore.page", + "fields": { + "title": "Page 1", + "numchild": 0, + "show_in_menus": true, + "live": true, + "depth": 4, + "content_type": ["tests", "eventpage"], + "path": "0001000200010001", + "url_path": "/example-home/content/page-1/", + "slug": "page-1", + "owner": 1 + } +}, +{ + "pk": 6, + "model": "tests.eventpage", + "fields": { + "date_from": "2014-12-25", + "audience": "public", + "location": "The North Pole", + "body": "

Welcome!

", + "cost": "Free" + } +}, + +{ + "pk": 7, + "model": "wagtailcore.page", + "fields": { + "title": "Page 2", + "numchild": 1, + "show_in_menus": true, + "live": true, + "depth": 4, + "content_type": ["tests", "eventpage"], + "path": "0001000200010002", + "url_path": "/example-home/content/page-2/", + "slug": "page-2", + "owner": 1 + } +}, +{ + "pk": 7, + "model": "tests.eventpage", + "fields": { + "date_from": "2014-12-25", + "audience": "public", + "location": "The North Pole", + "body": "

Welcome!

", + "cost": "Free" + } +}, + +{ + "pk": 8, + "model": "wagtailcore.page", + "fields": { + "title": "Other Content", + "numchild": 0, + "show_in_menus": true, + "live": true, + "depth": 3, + "content_type": ["tests", "eventpage"], + "path": "000100020002", + "url_path": "/example-home/other-content/", + "slug": "other-content", + "owner": 1 + } +}, +{ + "pk": 8, + "model": "tests.eventpage", + "fields": { + "date_from": "2014-12-25", + "audience": "public", + "location": "The North Pole", + "body": "

Welcome!

", + "cost": "Free" + } +}, + +{ + "pk": 9, + "model": "wagtailcore.page", + "fields": { + "title": "Child 1 of Page 2", + "numchild": 0, + "show_in_menus": true, + "live": true, + "depth": 5, + "content_type": ["tests", "eventpage"], + "path": "00010002000100020001", + "url_path": "/example-home/content/page-2/child-1/", + "slug": "child-1", + "owner": 1 + } +}, +{ + "pk": 9, + "model": "tests.eventpage", + "fields": { + "date_from": "2014-12-25", + "audience": "public", + "location": "The North Pole", + "body": "

Welcome!

", + "cost": "Free" + } +}, + +{ + "pk": 10, + "model": "wagtailcore.page", + "fields": { + "title": "Welcome to example2.com!", + "numchild": 0, + "show_in_menus": false, + "live": true, + "depth": 2, + "content_type": ["tests", "eventpage"], + "path": "00010003", + "url_path": "/home-2/", + "slug": "home-2" + } +}, +{ + "pk": 10, + "model": "tests.eventpage", + "fields": { + "date_from": "2014-12-25", + "audience": "private", + "location": "The North Pole", + "body": "

Welcome!

", + "cost": "Free" + } +}, + +{ + "pk": 1, + "model": "wagtailcore.site", + "fields": { + "root_page": 2, + "hostname": "testserver", + "port": 80, + "is_default_site": true + } +}, +{ + "pk": 2, + "model": "wagtailcore.site", + "fields": { + "root_page": 4, + "hostname": "example.com", + "port": 80, + "is_default_site": false + } +}, +{ + "pk": 3, + "model": "wagtailcore.site", + "fields": { + "root_page": 10, + "hostname": "example2.com", + "port": 80, + "is_default_site": false + } +}, + +{ + "pk": 3, + "model": "auth.group", + "fields": { + "name": "Group 1", + "permissions": [ + ["access_admin", "wagtailadmin", "admin"] + ] + } +}, +{ + "pk": 4, + "model": "auth.group", + "fields": { + "name": "Group 2", + "permissions": [ + ["access_admin", "wagtailadmin", "admin"] + ] + } +}, +{ + "pk": 5, + "model": "auth.group", + "fields": { + "name": "Group 3", + "permissions": [ + ["access_admin", "wagtailadmin", "admin"] + ] + } +}, + +{ + "pk": 1, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Group 1"], + "page": 2, + "permission_type": "add" + } +}, +{ + "pk": 2, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Group 1"], + "page": 2, + "permission_type": "edit" + } +}, +{ + "pk": 3, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Group 1"], + "page": 2, + "permission_type": "publish" + } +}, +{ + "pk": 3, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Group 1"], + "page": 2, + "permission_type": "choose" + } +}, +{ + "pk": 5, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Group 2"], + "page": 6, + "permission_type": "edit" + } +}, +{ + "pk": 6, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Group 2"], + "page": 6, + "permission_type": "choose" + } +}, +{ + "pk": 7, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Group 3"], + "page": 8, + "permission_type": "edit" + } +}, +{ + "pk": 8, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Group 3"], + "page": 8, + "permission_type": "choose" + } +}, + +{ + "pk": 1, + "model": "customuser.customuser", + "fields": { + "username": "superman", + "first_name": "Clark", + "last_name": "Kent", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "groups": [ + ], + "user_permissions": [], + "password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22", + "email": "ckent@dailyplanet.com" + } +}, +{ + "pk": 2, + "model": "customuser.customuser", + "fields": { + "username": "jane", + "first_name": "Jane", + "last_name": "Smith", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "groups": [ + ["Group 1"] + ], + "user_permissions": [], + "password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22", + "email": "jane@example.com" + } +}, +{ + "pk": 3, + "model": "customuser.customuser", + "fields": { + "username": "bob", + "first_name": "Bob", + "last_name": "Smith", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "groups": [ + ["Group 2"] + ], + "user_permissions": [], + "password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22", + "email": "bob@example.com" + } +}, +{ + "pk": 4, + "model": "customuser.customuser", + "fields": { + "username": "sam", + "first_name": "Sam", + "last_name": "Smith", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "groups": [ + ["Group 1"], + ["Group 2"] + ], + "user_permissions": [], + "password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22", + "email": "sam@example.com" + } +}, +{ + "pk": 5, + "model": "customuser.customuser", + "fields": { + "username": "mary", + "first_name": "Mary", + "last_name": "Smith", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "groups": [ + ], + "user_permissions": [ + ["access_admin", "wagtailadmin", "admin"] + ], + "password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22", + "email": "mary@example.com" + } +}, +{ + "pk": 6, + "model": "customuser.customuser", + "fields": { + "username": "josh", + "first_name": "Josh", + "last_name": "Smith", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "groups": [ + ["Group 2"], + ["Group 3"] + ], + "user_permissions": [], + "password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22", + "email": "josh@example.com" + } +} + +] diff --git a/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py b/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py index dc039adcf6..640b24019c 100644 --- a/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py +++ b/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py @@ -29,10 +29,10 @@ else: assignment_tag = register.assignment_tag -@register.inclusion_tag('wagtailadmin/shared/explorer_nav.html') -def explorer_nav(): +@register.inclusion_tag('wagtailadmin/shared/explorer_nav.html', takes_context=True) +def explorer_nav(context): return { - 'nodes': get_navigation_menu_items() + 'nodes': get_navigation_menu_items(context['request'].user) } diff --git a/wagtail/wagtailadmin/tests/test_explorer_nav.py b/wagtail/wagtailadmin/tests/test_explorer_nav.py new file mode 100644 index 0000000000..c869e3f9f4 --- /dev/null +++ b/wagtail/wagtailadmin/tests/test_explorer_nav.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, unicode_literals + +from django.core.urlresolvers import reverse +from django.test import TestCase + +from wagtail.tests.utils import WagtailTestUtils +from wagtail.wagtailcore.models import Page + + +class TestExplorerNavView(TestCase, WagtailTestUtils): + """ + Test the way that the explorer nav menu behaves for users with different permissions. + + This is isolated in its own test case because it requires a custom page tree and custom set of + users and groups. + The fixture sets up this page tree: + ======================================================== + ID Site Path + ======================================================== + 1 / + 2 testserver /home/ + 3 testserver /home/about-us/ + 4 example.com /home/ + 5 example.com /home/content/ + 6 example.com /home/content/page-1/ + 7 example.com /home/content/page-2/ + 9 example.com /home/content/page-2/child-1 + 8 example.com /home/other-content/ + 10 example.com /home-2/ + ======================================================== + Group 1 has explore and choose permissions rooted at testserver's homepage. + Group 2 has explore and choose permissions rooted at exammple.com's page-1. + Group 3 has explore and choose permissions rooted at exammple.com's other-content. + User "jane" is in Group 1. + User "bob" is in Group 2. + User "sam" is in Groups 1 and 2. + User "josh" is in Groups 2 and 3. + User "mary" is is no Groups, but she has the "access wagtail admin" permission. + User "superman" is an admin. + + Note that the Explorer Nav does not display leaf nodes. + """ + + fixtures = ['test_explorable_pages.json'] + + def test_admins_see_all_pages(self): + self.assertTrue(self.client.login(username='superman', password='password')) + response = self.client.get(reverse('wagtailadmin_explorer_nav')) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('wagtailadmin/shared/explorer_nav.html') + self.assertEqual(len(response.context['nodes']), 3) + self.assertEqual(response.context['nodes'][0][0], Page.objects.get(id=2)) + self.assertEqual(response.context['nodes'][1][0], Page.objects.get(id=4)) + self.assertEqual(response.context['nodes'][1][1][0][0], Page.objects.get(id=5)) + self.assertEqual(response.context['nodes'][1][1][0][1][0][0], Page.objects.get(id=7)) + # Even though example.com's Home 2 has no children, it's still displayed because it's at + # the top menu level for this user + self.assertEqual(response.context['nodes'][2][0], Page.objects.get(id=10)) + + def test_nav_root_for_nonadmin_is_closest_common_ancestor(self): + self.assertTrue(self.client.login(username='jane', password='password')) + response = self.client.get(reverse('wagtailadmin_explorer_nav')) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('wagtailadmin/shared/explorer_nav.html') + self.assertEqual(len(response.context['nodes']), 1) + self.assertEqual(response.context['nodes'][0][0], Page.objects.get(id=2)) + self.client.logout() + + self.assertTrue(self.client.login(username='sam', password='password')) + response = self.client.get(reverse('wagtailadmin_explorer_nav')) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('wagtailadmin/shared/explorer_nav.html') + self.assertEqual(len(response.context['nodes']), 2) + self.assertEqual(response.context['nodes'][0][0], Page.objects.get(id=2)) + self.assertEqual(response.context['nodes'][1][0], Page.objects.get(id=4)) + + def test_nonadmin_sees_leaf_pages_at_root_level(self): + self.assertTrue(self.client.login(username='bob', password='password')) + response = self.client.get(reverse('wagtailadmin_explorer_nav')) + + # Bob's group's CCA is a leaf node, so by the naive "don't show childless pages" rule + # he would not be shown any nodes. This would be bad, so we make an exception whereby + # childless pages at the user's top level are shown + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('wagtailadmin/shared/explorer_nav.html') + self.assertEqual(len(response.context['nodes']), 1) + self.assertEqual(response.context['nodes'][0][0], Page.objects.get(id=6)) + self.assertEqual(len(response.context['nodes'][0][1]), 0) + + def test_nonadmin_sees_pages_below_closest_common_ancestor(self): + self.assertTrue(self.client.login(username='josh', password='password')) + response = self.client.get(reverse('wagtailadmin_explorer_nav')) + + # Josh has permissions for /example-home/content/page-1 and /example-home/other-content , + # of which the closest common ancestor is /example-home . However, since he doesn't need + # access to example-home itself, the menu begins at its children ('content' and + # 'other-content') instead + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('wagtailadmin/shared/explorer_nav.html') + + self.assertEqual(len(response.context['nodes']), 2) + self.assertEqual(response.context['nodes'][0][0], Page.objects.get(id=5)) + # page-1 is childless, but user has direct permission on it, so it should be shown + self.assertEqual(len(response.context['nodes'][0][1]), 1) + self.assertEqual(response.context['nodes'][0][1][0][0], Page.objects.get(id=6)) + self.assertEqual(len(response.context['nodes'][0][1][0][1]), 0) + + self.assertEqual(response.context['nodes'][1][0], Page.objects.get(id=8)) + self.assertEqual(len(response.context['nodes'][1][1]), 0) + + def test_nonadmin_sees_only_explorable_pages(self): + self.assertTrue(self.client.login(username='sam', password='password')) + response = self.client.get(reverse('wagtailadmin_explorer_nav')) + + # Sam has permissions for /home and /example-home/content/page-1 , of which the closest + # common ancestor is root; we don't show root in the menu, so the top level will consist + # of 'home' and 'example-home' (but not the sibling 'home-2', which Sam doesn't have + # permission on) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('wagtailadmin/shared/explorer_nav.html') + + self.assertEqual(len(response.context['nodes']), 2) + # Sam should see the testserver homepage, the example.com homepage, and the Content page, + # but should not see Page 2. + self.assertEqual(response.context['nodes'][0][0], Page.objects.get(id=2)) + self.assertEqual(response.context['nodes'][1][0], Page.objects.get(id=4)) + self.assertEqual(response.context['nodes'][1][1][0][0], Page.objects.get(id=5)) + self.assertEqual(len(response.context['nodes'][1][1][0][1]), 1) + # page-1 is included in the menu, despite being a leaf node, because Sam has direct + # permission on it + self.assertEqual(response.context['nodes'][1][1][0][1][0][0], Page.objects.get(id=6)) + self.client.logout() + + self.assertTrue(self.client.login(username='jane', password='password')) + response = self.client.get(reverse('wagtailadmin_explorer_nav')) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('wagtailadmin/shared/explorer_nav.html') + self.assertEqual(len(response.context['nodes']), 1) + self.assertEqual(response.context['nodes'][0][0], Page.objects.get(id=2)) + self.assertEqual(len(response.context['nodes'][0][1]), 0) + + def test_nonadmin_with_no_page_perms_sees_nothing_in_nav(self): + self.assertTrue(self.client.login(username='mary', password='password')) + response = self.client.get(reverse('wagtailadmin_explorer_nav')) + + self.assertEqual(response.status_code, 200) + # Being in no Groups, Mary should ot be shown any nodes. + self.assertEqual(len(response.context['nodes']), 0) diff --git a/wagtail/wagtailadmin/tests/tests.py b/wagtail/wagtailadmin/tests/tests.py index c744964695..4c6e927350 100644 --- a/wagtail/wagtailadmin/tests/tests.py +++ b/wagtail/wagtailadmin/tests/tests.py @@ -171,20 +171,6 @@ class TestSendMail(TestCase): self.assertEqual(mail.outbox[0].from_email, "webmaster@localhost") -class TestExplorerNavView(TestCase, WagtailTestUtils): - def setUp(self): - self.homepage = Page.objects.get(id=2).specific - self.login() - - def test_explorer_nav_view(self): - response = self.client.get(reverse('wagtailadmin_explorer_nav')) - - # Check response - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('wagtailadmin/shared/explorer_nav.html') - self.assertEqual(response.context['nodes'][0][0], self.homepage) - - class TestTagsAutocomplete(TestCase, WagtailTestUtils): def setUp(self): self.login() diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 11ae42831d..b07de39673 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -31,7 +31,7 @@ def get_valid_next_url_from_request(request): def explorer_nav(request): return render(request, 'wagtailadmin/shared/explorer_nav.html', { - 'nodes': get_navigation_menu_items(), + 'nodes': get_navigation_menu_items(request.user), }) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index a58ec41204..a8704dd0b8 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -1344,10 +1344,72 @@ class Page(six.with_metaclass(PageBase, MP_Node, index.Indexed, ClusterableModel verbose_name_plural = _('pages') -def get_navigation_menu_items(): - # Get all pages that appear in the navigation menu: ones which have children, - # or are at the top-level (this rule required so that an empty site out-of-the-box has a working menu) - pages = Page.objects.filter(Q(depth=2) | Q(numchild__gt=0)).order_by('path') +def get_navigation_menu_items(user): + # Get all pages that the user has direct add/edit/publish/lock permission on + if user.is_superuser: + # superuser has implicit permission on the root node + pages_with_direct_permission = Page.objects.filter(depth=1) + else: + pages_with_direct_permission = Page.objects.filter( + group_permissions__group__in=user.groups.all(), + group_permissions__permission_type__in=['add', 'edit', 'publish', 'lock'] + ) + + if not(pages_with_direct_permission): + return [] + + # Find the closest common ancestor of the pages the user has permission for; + # this (or its children) will be the root menu level for this user. + cca_path = pages_with_direct_permission[0].path + for page in pages_with_direct_permission[1:]: + # repeatedly try shorter prefixes of page.path until we find one that cca_path starts with; + # this becomes the new cca_path + for path_len in range(len(page.path), 0, -Page.steplen): + path_to_test = page.path[0:path_len] + if cca_path.startswith(path_to_test): + cca_path = path_to_test + break + + # Determine the depth (within the overall page tree) at which this user's menu starts: + # * if CCA is the root node, start at depth 2 (immediate children of root - because we + # never want to show root in the navigation); + # * else if CCA is a node they have direct permission for, start at that depth + # (so that they can edit that root node) + # * else start one level deeper (because the root node is only needed to provide navigation + # to deeper levels, and one level deeper is the first point where there's a choice to make) + + if len(cca_path) == Page.steplen: + # CCA is the root node + menu_root_depth = 2 + elif any(page.path == cca_path for page in pages_with_direct_permission): + # user has direct permission on the CCA node + menu_root_depth = int(len(cca_path) / Page.steplen) + else: + menu_root_depth = int(len(cca_path) / Page.steplen) + 1 + + # Run the query to fetch all pages to be shown in the navigation. This consists of the following + # set of pages, for each page in pages_with_direct_permission: + # * all ancestors (plus self) from menu_root_depth down + # * all descendants that have children + # * all descendants at menu_root_depth, regardless of whether they have children. + # (this ensures that a freshly built site with no child pages won't result in an empty menu) + + ancestor_paths = [ + page.path[0:path_len] + for page in pages_with_direct_permission + for path_len in range(menu_root_depth * Page.steplen, len(page.path) + Page.steplen, Page.steplen) + ] + + criteria = Q(path__in=ancestor_paths) + + for page in pages_with_direct_permission: + criteria = criteria | ( + Q(path__startswith=page.path) & ( + Q(depth=menu_root_depth) | Q(numchild__gt=0) + ) + ) + + pages = Page.objects.filter(criteria).order_by('path') # Turn this into a tree structure: # tree_node = (page, children) @@ -1358,7 +1420,9 @@ def get_navigation_menu_items(): # at depth d, its parent must be the last page we saw at depth (d-1), and so we can # find it in that list. - depth_list = [(None, [])] # a dummy node for depth=0, since one doesn't exist in the DB + # create dummy entries for pages at a lower depth than menu_root_depth + # as these won't be covered by the 'pages' queryset + depth_list = [(None, []) for i in range(0, menu_root_depth)] for page in pages: # create a node for this page @@ -1375,14 +1439,7 @@ def get_navigation_menu_items(): # an exception here means that this node is one level deeper than any we've seen so far depth_list.append(node) - # in Wagtail, the convention is to have one root node in the db (depth=1); the menu proper - # begins with the children of that node (depth=2). - try: - root, root_children = depth_list[1] - return root_children - except IndexError: - # what, we don't even have a root node? Fine, just return an empty list... - return [] + return depth_list[menu_root_depth - 1][1] @receiver(pre_delete, sender=Page)