Skip to content


Subversion checkout URL

You can clone with
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

522 lines (458 sloc) 20.733 kB
# -*- coding: utf-8 -*-
from collections import defaultdict
from cms.apphook_pool import apphook_pool
from cms.models.moderatormodels import (ACCESS_DESCENDANTS,
from cms.models.permissionmodels import PagePermission, GlobalPagePermission
from cms.models.titlemodels import Title
from cms.utils import get_language_from_request
from cms.utils.i18n import get_fallback_languages
from cms.utils.moderator import get_page_queryset, get_title_queryset
from cms.utils.plugins import current_site
from menus.base import Menu, NavigationNode, Modifier
from menus.menu_pool import menu_pool
from django.conf import settings
from django.contrib.sites.models import Site
from django.db.models.query_utils import Q
from django.contrib.auth.models import Permission
def get_visible_pages(request, pages, site=None):
This code is basically a many-pages-at-once version of
pages contains all published pages
check if there is ANY restriction
that needs a permission page visibility calculation
is_setting_public_all = settings.CMS_PUBLIC_FOR == 'all'
is_setting_public_staff = settings.CMS_PUBLIC_FOR == 'staff'
is_auth_user = request.user.is_authenticated()
visible_page_ids = []
restricted_pages = defaultdict(list)
pages_perms_q = Q()
for page in pages:
# taken from for_page as multiple at once version
page_q = Q(page__tree_id=page.tree_id) & (
| (Q(page__level__lt=page.level) & (Q(grant_on=ACCESS_DESCENDANTS) | Q(grant_on=ACCESS_PAGE_AND_DESCENDANTS)))
| (Q(page__level=page.level - 1) & (Q(grant_on=ACCESS_CHILDREN) | Q(grant_on=ACCESS_PAGE_AND_CHILDREN)))
pages_perms_q |= page_q
pages_perms_q &= Q(can_view=True)
page_permissions = PagePermission.objects.filter(pages_perms_q).select_related('page', 'group__users')
for perm in page_permissions:
# collect the pages that are affected by permissions
if perm is not None and perm not in restricted_pages[]:
# affective restricted pages gathering
# using mptt functions
# add the page with the perm itself
# add children
child_ids ='id', flat=True)
for id in child_ids:
# add descendants
child_ids ='id', flat=True)
for id in child_ids:
# anonymous
# no restriction applied at all
if (not is_auth_user and
is_setting_public_all and
not restricted_pages):
return [ for page in pages]
if site is None:
site = current_site(request)
# authenticated user and global permission
if is_auth_user:
global_page_perm_q = Q(
Q(user=request.user) | Q(group__user=request.user)
) & Q(can_view=True) & Q(Q(sites__in=[]) | Q(sites__isnull=True))
global_view_perms = GlobalPagePermission.objects.filter(global_page_perm_q).exists()
#no page perms edgcase - all visible
if ((is_setting_public_all or (
is_setting_public_staff and request.user.is_staff))and
not restricted_pages and
not global_view_perms):
return [ for page in pages]
#no page perms edgcase - none visible
elif (is_setting_public_staff and
not request.user.is_staff and
not restricted_pages and
not global_view_perms):
return []
def has_global_perm():
if has_global_perm.cache < 0:
has_global_perm.cache = 1 if request.user.has_perm('cms.view_page') else 0
return bool(has_global_perm.cache)
has_global_perm.cache = -1
def has_permission_membership(page):
PagePermission user group membership tests
user_pk =
page_pk =
has_perm = False
for perm in restricted_pages[page_pk]:
if perm.user_id == user_pk:
has_perm = True
if not perm.group_id:
group_user_ids ='pk', flat=True)
if user_pk in group_user_ids:
has_perm = True
return has_perm
for page in pages:
to_add = False
# default to false, showing a restricted page is bad
# explicitly check all the conditions
# of settings and permissions
is_restricted = in restricted_pages
# restricted_pages contains as key any that is
# affected by a permission grant_on
if is_auth_user:
# a global permission was given to the request's user
if global_view_perms:
to_add = True
# setting based handling of unrestricted pages
elif not is_restricted and (
is_setting_public_all or (
is_setting_public_staff and request.user.is_staff)
# authenticated user, no restriction and public for all
# or
# authenticated staff user, no restriction and public for staff
to_add = True
# check group and user memberships to restricted pages
elif is_restricted and has_permission_membership(page):
to_add = True
elif has_global_perm():
to_add = True
# anonymous user, no restriction
elif not is_restricted and is_setting_public_all:
to_add = True
# store it
if to_add:
return visible_page_ids
def page_to_node(page, home, cut):
Transform a CMS page into a navigation node.
page: the page you wish to transform
home: a reference to the "home" page (the page with tree_id=1)
cut: Should we cut page from it's parent pages? This means the node will not
have a parent anymore.
# Theses are simple to port over, since they are not calculated.
# Other attributes will be added conditionnally later.
attr = {'soft_root':page.soft_root,
parent_id = page.parent_id
# Should we cut the Node from its parents?
if home and page.parent_id == and cut:
parent_id = None
# possible fix for a possible problem
#if parent_id and not page.parent.get_calculated_status():
# parent_id = None # ????
if page.limit_visibility_in_menu == None:
attr['visible_for_authenticated'] = True
attr['visible_for_anonymous'] = True
attr['visible_for_authenticated'] = page.limit_visibility_in_menu == 1
attr['visible_for_anonymous'] = page.limit_visibility_in_menu == 2
if ==
attr['is_home'] = True
# Extenders can be either navigation extenders or from apphooks.
extenders = []
if page.navigation_extenders:
# Is this page an apphook? If so, we need to handle the apphooks's nodes
app_name = page.get_application_urls(fallback=False)
except Title.DoesNotExist:
app_name = None
if app_name: # it means it is an apphook
app = apphook_pool.get_apphook(app_name)
for menu in app.menus:
if extenders:
attr['navigation_extenders'] = extenders
# Do we have a redirectURL?
attr['redirect_url'] = page.get_redirect() # save redirect URL if any
# Now finally, build the NavigationNode object and return it.
ret_node = NavigationNode(
return ret_node
class CMSMenu(Menu):
def get_nodes(self, request):
page_queryset = get_page_queryset(request)
site = Site.objects.get_current()
lang = get_language_from_request(request)
filters = {
filters['title_set__language'] = lang
pages = page_queryset.published().filter(**filters).order_by("tree_id", "lft")
# ------------- additional code to speed up menus -----------
# pages now contains every single page in the site
# it was quick to get, but some other operations are slow,
# especially get_visible_pages() and titles
# so why not be smarter? Instead of getting every page in the site, let's
# just get relevant ones, instead of ones that will never appear in the menu
# such as pages related only through distant ancestors. We'll get:
# * the current page
# * all of its ancestors
# * all of their children
# in order to do this we'll also need an amendment to menus/menu_pool - it's now longer
# enough to do:
# key = "%smenu_nodes_%s_%s" % (prefix, lang, site_id)
# we also need to cache each request_path:
# key = "%smenu_nodes_%s_%s_%s" % (prefix, lang, site_id, hash(request.path))
current_page = request.current_page
# get the current page as a queryset and make sure it's unique
current_page_queryset = pages.filter(
# get all the ancestors - a chain of ancestry back to the root
ancestry = current_page.get_ancestors(ascending = True)
# create a queryset of relevant pages - we use the intersection of
# pages & ancestry to eliminate unpublished ancestors
relevant_pages = pages & ancestry
# get all of:
# pages whose parents are in relevant pages
# relevant pages
# pages whose parents are the current page
# current page
pages = \
pages.filter(parent__in=relevant_pages).distinct() | \
relevant_pages.distinct() | \
pages.filter(parent__in=current_page_queryset).distinct() | \
current_page_queryset | \
# do they need to be ordered here? I am not sure this is necessary
pages = pages.order_by("tree_id", "lft")
# ------------- end of additional code to speed up menus -----------
ids = []
nodes = []
first = True
home_cut = False
home_children = []
home = None
actual_pages = []
# cache view perms
visible_pages = get_visible_pages(request, pages, site)
for page in pages:
# Pages are ordered by tree_id, therefore the first page is the root
# of the page tree (a.k.a "home")
if not in visible_pages:
# Don't include pages the user doesn't have access to
if not home:
home = page
page.home_pk_cache =
if first and !=
home_cut = True
if (page.parent_id == or page.parent_id in home_children) and home_cut:
if ( == and home.in_navigation) or !=
first = False
titles = list(get_title_queryset(request).filter(page__in=ids, language=lang))
for page in actual_pages: # add the title and slugs and some meta data
for title in titles:
if title.page_id ==
if not hasattr(page, "title_cache"):
page.title_cache = {}
page.title_cache[title.language] = title
nodes.append(page_to_node(page, home, home_cut))
if ids: # get fallback languages
fallbacks = get_fallback_languages(lang)
for l in fallbacks:
titles = list(get_title_queryset(request).filter(page__in=ids, language=l))
for title in titles:
for page in actual_pages: # add the title and slugs and some meta data
if title.page_id ==
if not hasattr(page, "title_cache"):
page.title_cache = {}
page.title_cache[title.language] = title
nodes.append(page_to_node(page, home, home_cut))
if not ids:
return nodes
class NavExtender(Modifier):
def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
if post_cut:
return nodes
exts = []
# rearrange the parent relations
home = None
for node in nodes:
if node.attr.get("is_home", False):
home = node
extenders = node.attr.get("navigation_extenders", None)
if extenders:
for ext in extenders:
if not ext in exts:
for n in nodes:
if n.namespace == ext and not n.parent_id:# if home has nav extenders but home is not visible
if node.attr.get("is_home", False) and not node.visible:
n.parent_id = None
n.parent_namespace = None
n.parent = None
n.parent_id =
n.parent_namespace = node.namespace
n.parent = node
removed = []
# find all not assigned nodes
for menu in menu_pool.menus.items():
if hasattr(menu[1], 'cms_enabled') and menu[1].cms_enabled and not menu[0] in exts:
for node in nodes:
if node.namespace == menu[0]:
if breadcrumb:
# if breadcrumb and home not in navigation add node
if breadcrumb and home and not home.visible:
home.visible = True
if request.path == home.get_absolute_url():
home.selected = True
home.selected = False
# remove all nodes that are nav_extenders and not assigned
for node in removed:
return nodes
class SoftRootCutter(Modifier):
Ask evildmp/superdmp if you don't understand softroots!
Softroot description from the docs:
A soft root is a page that acts as the root for a menu navigation tree.
Typically, this will be a page that is the root of a significant new
section on your site.
When the soft root feature is enabled, the navigation menu for any page
will start at the nearest soft root, rather than at the real root of
the site’s page hierarchy.
This feature is useful when your site has deep page hierarchies (and
therefore multiple levels in its navigation trees). In such a case, you
usually don’t want to present site visitors with deep menus of nested
For example, you’re on the page -Introduction to Bleeding-?, so the menu
might look like this:
School of Medicine
Medical Education
Department of Lorem Ipsum
Department of Donec Imperdiet
Department of Cras Eros
Department of Mediaeval Surgery
Introduction to Bleeding <this is the current page>
Bleeding - the scientific evidence
Cleaning up the mess
Department of Curabitur a Purus
Department of Sed Accumsan
Department of Etiam
Contact us
which is frankly overwhelming.
By making -Department of Mediaeval Surgery-? a soft root, the menu
becomes much more manageable:
Department of Mediaeval Surgery
Introduction to Bleeding <current page>
Bleeding - the scientific evidence
Cleaning up the mess
def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
# only apply this modifier if we're pre-cut (since what we do is cut)
if post_cut or not settings.CMS_SOFTROOT:
return nodes
selected = None
root_nodes = []
# find the selected node as well as all the root nodes
for node in nodes:
if node.selected:
selected = node
if not node.parent:
# if we found a selected ...
if selected:
# and the selected is a softroot
if selected.attr.get("soft_root", False):
# get it's descendants
nodes = selected.get_descendants()
# remove the link to parent
selected.parent = None
# make the selected page the root in the menu
nodes = [selected] + nodes
# if it's not a soft root, walk ancestors (upwards!)
nodes = self.find_ancestors_and_remove_children(selected, nodes)
return nodes
def find_and_remove_children(self, node, nodes):
for n in node.children:
if n.attr.get("soft_root", False):
self.remove_children(n, nodes)
return nodes
def remove_children(self, node, nodes):
for n in node.children:
self.remove_children(n, nodes)
node.children = []
def find_ancestors_and_remove_children(self, node, nodes):
Check ancestors of node for soft roots
if node.parent:
if node.parent.attr.get("soft_root", False):
nodes = node.parent.get_descendants()
node.parent.parent = None
nodes = [node.parent] + nodes
nodes = self.find_ancestors_and_remove_children(node.parent, nodes)
for n in nodes:
if n != node and not n.parent:
self.find_and_remove_children(n, nodes)
for n in node.children:
if n != node:
self.find_and_remove_children(n, nodes)
return nodes
Jump to Line
Something went wrong with that request. Please try again.