Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

root_level in show_sub_menu #1344

Merged
merged 20 commits into from

6 participants

@andrewschoen

I end up creating a menu like this in just about every project. What I always need to do is print a sub_menu that starts after the root_level specified and not at the selected node. This will include all ancestors, descendants of the selected node (up to root_level), plus every sibling of the selected node and all their descendants.

@superdmp and I discussed a menu like this before. Does anybody else ever need to create menus this way?

@evildmp
Owner

Nearly - mine need to be up to the nearest soft root (rather than some level).

And we don't print descendants of siblings (i.e. nephews and nieces). How about a nephews argument: 0 means no nephews, 1 means show immediate nephews, 2 means show nephews and great-nephews, and so on?

@travisbot

This pull request passes (merged 8151be6 into dc00224).

@andrewschoen

@evildmp good idea, not sure if I would use it much but I guess having the option is good. How about an argument for showing the descendants of ancestors (uncles, aunts?) as well?

I wonder if anybody else will get any use out of this, or is it just us and our crazy university menus?

I wouldn't want to get too argument crazy, but if we think it'll be useful I can spend a bit of time on it.

@travisbot

This pull request passes (merged 89cfdb0 into dc00224).

@digi604
Collaborator

So what is the difference from

{% show_sub_menu 100 0 %}

to

{% show_menu 1 100 0 100 %}

?

@andrewschoen

Here is an example of show_sub_menu 100 0

sub_menu

And here is the show_menu 1 100 0 100

show_menu

They are pretty similar but my version is showing more child pages. I've attempted to pull in more children pages with show_menu using the third parameter, but then I end up getting some of my apphook menus include, which I don't want.

Here's an example of show_menu 1 100 100 100 (the last two nav items are from the Polls apphook)

show_menu

@andrewschoen

Honestly, if there is a way to get this to work without this patch that's great. I just have never been able to get my menus to work as designed using show_menu. I could very well just be missing something though.

This is a good site as an example of when I needed to use this patch. I needed to show all the nav from the active main navigation, including all descendants without showing anything from another main navigation node (Admissions, Athletics, etc.). I was never able to get that to work with show_menu alone.

http://www.rockhurst.edu/academics/international/study-abroad/france/

@andrewschoen

Maybe these are a better example. I added Content Page 2 with it's own set of descendants underneath it.

With show_sub_menu 100 0, the menu under Content Page stays the same.

sub_menu

And here is show_menu 1 100 100 100 with Content Page 2 added. Subpage 5, 6 and child 4 should not show up as it's in the Content Page 2 tree.

show_menu

@digi604
Collaborator

Ok... confirmed this. I would like an other change though:

{% show_sub_menu 100 0 %} should start at level 0... so level 0 is displayed.

{% show_sub_menu 100 1 %} should display what you currently have.

@andrewschoen

Alright, that makes sense. What's your thought on the nephews argument @evildmp suggested? I've started work on that as well, just about finished really.

@digi604
Collaborator

let me see code or the API

@andrewschoen

ok, I pushed up my latest code. Still a work in progress but all the tests I wrote for it pass. This is the commit you'll want to look at.

andrewschoen@f6e0b47

@travisbot

This pull request passes (merged fdf8462 into c3aa982).

@andrewschoen

This commit really isn't related to the PR, but I noticed this when I was working on it. Why go back to the db when create_page returns the page object anyway? This speed up the test suite by a couple seconds for me.

@travisbot

This pull request passes (merged 7577ab5 into c3aa982).

@travisbot

This pull request passes (merged f58a6cf into c3aa982).

@andrewschoen

Ok, so the last commit actually doesn't pass the '{% show_sub_menu 100 0 %} should start at level 0... so level 0 is displayed.' requirement. I'll work on that.

The more I think about it though, do we need to let them specify a level? It actually creates weird situations where no menu is displayed at all. For example, I pass 3 to it but I'm at level 1, I get no menu.

As an alternative we could just make root_level (named something else) just a boolean argument. If True, the menu would root at the root node of that tree. So, show every node after the root node for the active tree.

@andrewschoen

ok, show_sub_menu 100 0 works as expected now. Disregard my previous suggestion, this way is better.

@travisbot

This pull request passes (merged a4b8d68 into c3aa982).

@digi604
Collaborator

please remerge so we can get this bugger in for 2.4

@digi604
Collaborator

and please update release notes :)

@digi604 digi604 merged commit a4b8d68 into divio:develop

1 check passed

Details default The Travis build passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 16, 2012
  1. @mitar

    `copy_plugins_to` should return copied plugins for additional

    mitar authored
    post-processing to be possible.
Commits on Jun 22, 2012
  1. @piquadrat

    cache the result of the global page permission query on the request o…

    piquadrat authored
    …bject
    
    this saves a lot of duplicate queries (1 per page in the tree if my calculations are correct)
Commits on Jul 12, 2012
  1. @andrewschoen
  2. @andrewschoen
  3. @andrewschoen
  4. @andrewschoen

    updated docs

    andrewschoen authored
Commits on Jul 31, 2012
  1. @piquadrat
Commits on Aug 2, 2012
  1. @piquadrat
Commits on Aug 10, 2012
  1. @digi604

    Merge pull request #1380 from digi604/asTag

    digi604 authored
    page_attribute with "as"
  2. @digi604

    Merge pull request #1300 from mitar/returnfromcopyplugins

    digi604 authored
    `copy_plugins_to` should return copied plugins for additional post-processing to be possible
  3. @digi604

    Merge pull request #1319 from piquadrat/feature/global-perms-caching

    digi604 authored
    cache the result of the global page permission query on the request object
Commits on Aug 11, 2012
  1. @andrewschoen
  2. @andrewschoen
  3. @andrewschoen
  4. @andrewschoen

    updated docs

    andrewschoen authored
Commits on Aug 13, 2012
  1. @andrewschoen
Commits on Aug 15, 2012
  1. @andrewschoen
  2. @andrewschoen

    speed up of menu fixtures for tests. why go back to the db when creat…

    andrewschoen authored
    …e_page returns the page object.
Commits on Aug 16, 2012
  1. @andrewschoen
Commits on Aug 17, 2012
  1. @andrewschoen
This page is out of date. Refresh to see the latest.
View
64 cms/test_utils/fixtures/menus.py
@@ -27,15 +27,46 @@ def create_fixtures(self):
p1 = create_page('P1', published=True, in_navigation=True, **defaults)
p4 = create_page('P4', published=True, in_navigation=True, **defaults)
p6 = create_page('P6', published=True, in_navigation=False, **defaults)
- p1 = Page.objects.get(pk=p1.pk)
p2 = create_page('P2', published=True, in_navigation=True, parent=p1, **defaults)
- create_page('P3', published=True, in_navigation=True, parent=p2, **defaults)
- p4 = Page.objects.get(pk=p4.pk)
- create_page('P5', published=True, in_navigation=True, parent=p4, **defaults)
- p6 = Page.objects.get(pk=p6.pk)
- create_page('P7', published=True, in_navigation=True, parent=p6, **defaults)
- p6 = Page.objects.get(pk=p6.pk)
- create_page('P8', published=True, in_navigation=True, parent=p6, **defaults)
+ p3 = create_page('P3', published=True, in_navigation=True, parent=p2, **defaults)
+ p5 = create_page('P5', published=True, in_navigation=True, parent=p4, **defaults)
+ p7 = create_page('P7', published=True, in_navigation=True, parent=p6, **defaults)
+ p8 = create_page('P8', published=True, in_navigation=True, parent=p6, **defaults)
+
+
+class ExtendedMenusFixture(object):
+ def create_fixtures(self):
+ """
+ Tree from fixture:
+
+ + P1
+ | + P2
+ | + P3
+ | + P9
+ | + P10
+ | + P11
+ + P4
+ | + P5
+ + P6 (not in menu)
+ + P7
+ + P8
+ """
+ defaults = {
+ 'template': 'nav_playground.html',
+ 'language': 'en',
+ }
+ with SettingsOverride(CMS_MODERATOR=False, CMS_PERMISSION=False):
+ p1 = create_page('P1', published=True, in_navigation=True, **defaults)
+ p4 = create_page('P4', published=True, in_navigation=True, **defaults)
+ p6 = create_page('P6', published=True, in_navigation=False, **defaults)
+ p2 = create_page('P2', published=True, in_navigation=True, parent=p1, **defaults)
+ p3 = create_page('P3', published=True, in_navigation=True, parent=p2, **defaults)
+ p5 = create_page('P5', published=True, in_navigation=True, parent=p4, **defaults)
+ p7 = create_page('P7', published=True, in_navigation=True, parent=p6, **defaults)
+ p6 = create_page('P8', published=True, in_navigation=True, parent=p6, **defaults)
+ p9 = create_page('P9', published=True, in_navigation=True, parent=p1, **defaults)
+ p10 = create_page('P10', published=True, in_navigation=True, parent=p9, **defaults)
+ p11 = create_page('P11', published=True, in_navigation=True, parent=p10, **defaults)
class SubMenusFixture(object):
@@ -60,15 +91,11 @@ def create_fixtures(self):
p1 = create_page('P1', published=True, in_navigation=True, **defaults)
p4 = create_page('P4', published=True, in_navigation=True, **defaults)
p6 = create_page('P6', published=True, in_navigation=True, **defaults)
- p1 = Page.objects.get(pk=p1.pk)
p2 = create_page('P2', published=True, in_navigation=True, parent=p1, **defaults)
- create_page('P3', published=True, in_navigation=True, parent=p2, **defaults)
- p4 = Page.objects.get(pk=p4.pk)
- create_page('P5', published=True, in_navigation=True, parent=p4, **defaults)
- p6 = Page.objects.get(pk=p6.pk)
- create_page('P7', published=True, in_navigation=False, parent=p6, **defaults)
- p6 = Page.objects.get(pk=p6.pk)
- create_page('P8', published=True, in_navigation=True, parent=p6, **defaults)
+ p3 = create_page('P3', published=True, in_navigation=True, parent=p2, **defaults)
+ p5 = create_page('P5', published=True, in_navigation=True, parent=p4, **defaults)
+ p7 = create_page('P7', published=True, in_navigation=False, parent=p6, **defaults)
+ p8 = create_page('P8', published=True, in_navigation=True, parent=p6, **defaults)
class SoftrootFixture(object):
@@ -99,11 +126,8 @@ def create_fixtures(self):
aaa = create_page('aaa', parent=root, **defaults)
_111 = create_page('111', parent=aaa, **defaults)
ccc = create_page('ccc', parent=_111, **defaults)
- create_page('ddd', parent=ccc, **defaults)
- aaa = Page.objects.get(pk=aaa.pk)
+ ddd = create_page('ddd', parent=ccc, **defaults)
create_page('222', parent=aaa, **defaults)
- root = Page.objects.get(pk=root.pk)
bbb = create_page('bbb', parent=root, **defaults)
create_page('333', parent=bbb, **defaults)
- bbb = Page.objects.get(pk=bbb.pk)
create_page('444', parent=bbb, **defaults)
View
78 cms/tests/menu.py
@@ -5,7 +5,7 @@
from cms.models import Page
from cms.models.permissionmodels import GlobalPagePermission, PagePermission
from cms.test_utils.fixtures.menus import (MenusFixture, SubMenusFixture,
- SoftrootFixture)
+ SoftrootFixture, ExtendedMenusFixture)
from cms.test_utils.testcases import SettingsOverrideTestCase
from cms.test_utils.util.context_managers import (SettingsOverride,
LanguageOverride)
@@ -51,6 +51,58 @@ def tearDown(self):
super(BaseMenuTest, self).tearDown()
+class ExtendedFixturesMenuTests(ExtendedMenusFixture, BaseMenuTest):
+ """
+ Tree from fixture:
+
+ + P1
+ | + P2
+ | + P3
+ | + P9
+ | + P10
+ | + P11
+ + P4
+ | + P5
+ + P6 (not in menu)
+ + P7
+ + P8
+ """
+ def get_page(self, num):
+ return Page.objects.get(title_set__title='P%s' % num)
+
+ def get_level(self, num):
+ return Page.objects.filter(level=num)
+
+ def get_all_pages(self):
+ return Page.objects.all()
+
+ def test_menu_failfast_on_invalid_usage(self):
+ context = self.get_context()
+ context['child'] = self.get_page(1)
+ # test standard show_menu
+ with SettingsOverride(DEBUG=True, TEMPLATE_DEBUG=True):
+ tpl = Template("{% load menu_tags %}{% show_menu 0 0 0 0 'menu/menu.html' child %}")
+ self.assertRaises(TemplateSyntaxError, tpl.render, context)
+
+ def test_show_submenu_nephews(self):
+ context = self.get_context(path=self.get_page(2).get_absolute_url())
+ tpl = Template("{% load menu_tags %}{% show_sub_menu 100 1 1 %}")
+ tpl.render(context)
+ nodes = context["children"]
+ # P2 is the selected node
+ self.assertTrue(nodes[0].selected)
+ # Should include P10 but not P11
+ self.assertEqual(len(nodes[1].children), 1)
+ self.assertFalse(nodes[1].children[0].children)
+
+ tpl = Template("{% load menu_tags %}{% show_sub_menu 100 1 %}")
+ tpl.render(context)
+ nodes = context["children"]
+ # should now include both P10 and P11
+ self.assertEqual(len(nodes[1].children), 1)
+ self.assertEqual(len(nodes[1].children[0].children), 1)
+
+
class FixturesMenuTests(MenusFixture, BaseMenuTest):
"""
Tree from fixture:
@@ -228,6 +280,30 @@ def test_show_submenu(self):
nodes = context['children']
self.assertEqual(len(nodes), 1)
self.assertEqual(len(nodes[0].children), 0)
+
+ context = self.get_context(path=self.get_page(3).get_absolute_url())
+ tpl = Template("{% load menu_tags %}{% show_sub_menu 100 1 %}")
+ tpl.render(context)
+ nodes = context["children"]
+ # P3 is the selected node
+ self.assertFalse(nodes[0].selected)
+ self.assertTrue(nodes[0].children[0].selected)
+ # top level node should be P2
+ self.assertEqual(nodes[0].get_absolute_url(), self.get_page(2).get_absolute_url())
+ # should include P3 as well
+ self.assertEqual(len(nodes[0].children), 1)
+ # but not P1 as it's at the root_level
+ self.assertEqual(nodes[0].parent, None)
+
+ context = self.get_context(path=self.get_page(2).get_absolute_url())
+ tpl = Template("{% load menu_tags %}{% show_sub_menu 100 0 %}")
+ tpl.render(context)
+ nodes = context["children"]
+ # P1 should be in the nav
+ self.assertEqual(nodes[0].get_absolute_url(), self.get_page(1).get_absolute_url())
+ # P2 is selected
+ self.assertTrue(nodes[0].children[0].selected)
+
def test_show_breadcrumb(self):
context = self.get_context(path=self.get_page(3).get_absolute_url())
View
10 cms/utils/admin.py
@@ -6,9 +6,9 @@
from django.contrib.sites.models import Site
-from cms.models.pagemodel import Page
-from cms.models.permissionmodels import GlobalPagePermission
+from cms.models import Page
from cms.utils import permissions, moderator, get_language_from_request
+from cms.utils.permissions import has_global_page_permission
NOT_FOUND_RESPONSE = "NotFound"
@@ -42,9 +42,9 @@ def get_admin_menu_item_context(request, page, filtered=False):
has_add_on_same_level_permission = False
opts = Page._meta
if settings.CMS_PERMISSION:
- if (request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()) and
- GlobalPagePermission.objects.with_user(request.user).filter(can_add=True, sites__in=[page.site_id])):
- has_add_on_same_level_permission = True
+ perms = has_global_page_permission(request, page.site_id, can_add=True)
+ if (request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()) and perms):
+ has_add_on_same_level_permission = True
if not has_add_on_same_level_permission and page.parent_id:
has_add_on_same_level_permission = permissions.has_generic_permission(page.parent_id, request.user, "add", page.site)
View
4 cms/utils/copy_plugins.py
@@ -18,4 +18,6 @@ def copy_plugins_to(plugin_list, to_placeholder, to_language = None):
for new_plugin, old_plugin in plugins_ziplist:
new_instance = new_plugin.get_plugin_instance()[0]
if new_instance:
- new_instance.post_copy(old_plugin, plugins_ziplist)
+ new_instance.post_copy(old_plugin, plugins_ziplist)
+ # returns information about originals and copies
+ return plugins_ziplist
View
34 cms/utils/permissions.py
@@ -56,7 +56,7 @@ def has_page_add_permission(request):
except Page.DoesNotExist:
return False
if (request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()) and
- GlobalPagePermission.objects.with_user(request.user).filter(can_add=True, sites__in=[page.site_id])):
+ has_global_page_permission(request, page.site_id, can_add=True)):
return True
if position in ("first-child", "last-child"):
return page.has_add_permission(request)
@@ -68,7 +68,7 @@ def has_page_add_permission(request):
from cms.utils.plugins import current_site
site = current_site(request)
if (request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()) and
- GlobalPagePermission.objects.with_user(request.user).filter(can_add=True, sites__in=[site])):
+ has_global_page_permission(request, site, can_add=True)):
return True
return False
@@ -91,12 +91,31 @@ def has_page_change_permission(request):
opts = Page._meta
if request.user.is_superuser or (
request.user.has_perm(opts.app_label + '.' + opts.get_change_permission()) and (
- GlobalPagePermission.objects.with_user(request.user).filter(
- can_change=True, sites__in=[current_site(request)]
- ).exists()) or has_any_page_change_permissions(request)):
+ has_global_page_permission(request, current_site(request), can_change=True))
+ or has_any_page_change_permissions(request)):
return True
return False
+
+def has_global_page_permission(request, site, **filters):
+ """
+ A helper function to check for global page permissions for the current user
+ and site. Caches the result on a request basis, so multiple calls to this
+ funtion inside of one request/response cycle only generate one query.
+
+ :param request: the Request object
+ :param site: the Site object or ID
+ :param filters: queryset filters, e.g. ``can_add = True``
+ :return: ``True`` or ``False``
+ """
+ if not hasattr(request, '_cms_global_perms'):
+ request._cms_global_perms = {}
+ key = (site.pk if hasattr(site, 'pk') else int(site),) + tuple((k, v) for k, v in filters.iteritems())
+ if key not in request._cms_global_perms:
+ request._cms_global_perms[key] = GlobalPagePermission.objects.with_user(request.user).filter(sites__in=[site], **filters).exists()
+ return request._cms_global_perms[key]
+
+
def get_any_page_view_permissions(request, page):
"""
Used by the admin template tag is_restricted
@@ -104,7 +123,6 @@ def get_any_page_view_permissions(request, page):
return PagePermission.objects.for_page(page=page).filter(can_view=True)
-
def get_user_permission_level(user):
"""
Returns highest user level from the page/permission hierarchy on which
@@ -224,7 +242,7 @@ def has_global_change_permissions_permission(user):
opts = GlobalPagePermission._meta
if user.is_superuser or (
user.has_perm(opts.app_label + '.' + opts.get_change_permission()) and
- GlobalPagePermission.objects.with_user(user).filter(can_change=True)):
+ GlobalPagePermission.objects.with_user(user).filter(can_change=True).exists()):
return True
return False
@@ -264,7 +282,7 @@ def get_user_sites_queryset(user):
# so he haves access to all sites
return qs
- # add some pages if he haves permission to add / change them
+ # add some pages if he has permission to add / change them
q |= Q(Q(page__pagepermission__user=user) | Q(page__pagepermission__group__user=user)) & \
(Q(Q(page__pagepermission__can_add=True) | Q(page__pagepermission__can_change=True)))
return qs.filter(q).distinct()
View
17 docs/advanced/templatetags.rst
@@ -293,13 +293,26 @@ show_sub_menu
*************
Displays the sub menu of the current page (as a nested list).
-Takes one argument that specifies how many levels deep the submenu should be
-displayed. The template can be found at ``cms/sub_menu.html``::
+
+The first argument, ``levels`` (default=100), specifies how many levels deep the submenu should be
+displayed
+
+The second argument, ``root_level`` (default=None), specifies at what level, if any, the menu should root at. For example, if root_level is 0 the menu will start at that level regardless of what level the current page is on.
+
+The third argumemnt, ``nephews`` (default=100), specifices how many levels of nephews (children of siblings) are show.
+
+The template can be found at ``cms/sub_menu.html``::
<ul>
{% show_sub_menu 1 %}
</ul>
+Rooted at level 0::
+
+ <ul>
+ {% show_sub_menu 1 0 %}
+ </ul>
+
Or with a custom template::
<ul>
View
19 docs/getting_started/navigation.rst
@@ -101,14 +101,27 @@ You can give it the same optional parameters as :ttag:`show_menu`::
show_sub_menu
*************
-Display the sub menu of the current page (as a nested list).
-Takes one argument that specifies how many levels deep the submenu should be
-displayed. The template can be found at ``menu/sub_menu.html``::
+Displays the sub menu of the current page (as a nested list).
+
+The first argument, ``levels`` (default=100), specifies how many levels deep the submenu should be
+displayed
+
+The second argument, ``root_level`` (default=None), specifies at what level, if any, the menu should root at. For example, if root_level is 0 the menu will start at that level regardless of what level the current page is on.
+
+The third argumemnt, ``nephews`` (default=100), specifices how many levels of nephews (children of siblings) are show.
+
+The template can be found at ``cms/sub_menu.html``::
<ul>
{% show_sub_menu 1 %}
</ul>
+Rooted at level 0::
+
+ <ul>
+ {% show_sub_menu 1 0 %}
+ </ul>
+
Or with a custom template::
<ul>
View
31 menus/templatetags/menu_tags.py
@@ -171,6 +171,8 @@ class ShowSubMenu(InclusionTag):
"""
show the sub menu of the current nav-node.
-levels: how many levels deep
+ -root_level: the level to start the menu at
+ -nephews: the level of descendants of siblings (nephews) to show
-temlplate: template used to render the navigation
"""
name = 'show_sub_menu'
@@ -178,10 +180,12 @@ class ShowSubMenu(InclusionTag):
options = Options(
IntegerArgument('levels', default=100, required=False),
+ IntegerArgument('root_level', default=None, required=False),
+ IntegerArgument('nephews', default=100, required=False),
Argument('template', default='menu/sub_menu.html', required=False),
)
- def get_context(self, context, levels, template):
+ def get_context(self, context, levels, root_level, nephews, template):
try:
# If there's an exception (500), default context_processors may not be called.
request = context['request']
@@ -189,13 +193,34 @@ def get_context(self, context, levels, template):
return {'template': 'menu/empty.html'}
nodes = menu_pool.get_nodes(request)
children = []
+ # adjust root_level so we cut before the specified level, not after
+ include_root = False
+ if root_level > 0:
+ root_level = root_level - 1
+ elif root_level == 0:
+ include_root = True
for node in nodes:
- if node.selected:
+ if root_level is None:
+ if node.selected:
+ # if no root_level specified, set it to the selected nodes level
+ root_level = node.level
+ # is this the ancestor of current selected node at the root level?
+ is_root_ancestor = (node.ancestor and node.level == root_level)
+ # is a node selected on the root_level specified
+ root_selected = (node.selected and node.level == root_level)
+ if is_root_ancestor or root_selected:
cut_after(node, levels, [])
children = node.children
for child in children:
child.parent = None
- children = menu_pool.apply_modifiers(children, request, post_cut=True)
+ if child.sibling:
+ cut_after(child, nephews, [])
+ # if root_level was 0 we need to give the menu the entire tree
+ # not just the children
+ if include_root:
+ children = menu_pool.apply_modifiers([node], request, post_cut=True)
+ else:
+ children = menu_pool.apply_modifiers(children, request, post_cut=True)
context.update({
'children':children,
'template':template,
Something went wrong with that request. Please try again.