Back to index

plone3  3.1.7
navtree.py
Go to the documentation of this file.
00001 # This module contains a function to help build navigation-tree-like structures
00002 # from catalog queries.
00003 
00004 from zope.interface import implements
00005 
00006 from Products.CMFCore.utils import getToolByName
00007 from Products.CMFPlone import utils
00008 
00009 from types import StringType
00010 
00011 from plone.app.layout.navigation.interfaces import INavtreeStrategy
00012 
00013 class NavtreeStrategyBase(object):
00014     """Basic navigation tree strategy that does nothing.
00015     """
00016 
00017     implements(INavtreeStrategy)
00018 
00019     __allow_access_to_unprotected_subobjects__ = 1
00020     
00021     rootPath = None
00022     showAllParents = False
00023 
00024     def nodeFilter(self, node):
00025         return True
00026 
00027     def subtreeFilter(self, node):
00028         return True
00029 
00030     def decoratorFactory(self, node):
00031         return node
00032         
00033     def showChildrenOf(self, object):
00034         return True
00035 
00036 def buildFolderTree(context, obj=None, query={}, strategy=NavtreeStrategyBase()):
00037     """Create a tree structure representing a navigation tree. By default,
00038     it will create a full "sitemap" tree, rooted at the portal, ordered
00039     by explicit folder order. If the 'query' parameter contains a 'path'
00040     key, this can be used to override this. To create a navtree rooted
00041     at the portal root, set query['path'] to:
00042 
00043         {'query' : '/'.join(context.getPhysicalPath()),
00044          'navtree' : 1}
00045 
00046     to start this 1 level below the portal root, set query['path'] to:
00047 
00048         {'query' : '/'.join(obj.getPhysicalPath()),
00049          'navtree' : 1,
00050          'navtree_start' : 1}
00051 
00052     to create a sitemap with depth limit 3, rooted in the portal:
00053 
00054         {'query' : '/'.join(obj.getPhysicalPath()),
00055          'depth' : 3}
00056 
00057     The parameters:
00058 
00059     - 'context' is the acquisition context, from which tools will be acquired
00060     - 'obj' is the current object being displayed.
00061     - 'query' is a catalog query to apply to find nodes in the tree.
00062     - 'strategy' is an object that can affect how the generation works. It
00063         should be derived from NavtreeStrategyBase, if given, and contain:
00064 
00065             rootPath -- a string property; the physical path to the root node.
00066 
00067             If not given, it will default to any path set in the query, or the
00068             portal root. Note that in a navtree query, the root path will
00069             default to the portal only, possibly adjusted for any navtree_start
00070             set. If rootPath points to something not returned by the query by
00071             the query, a dummy node containing only an empty 'children' list
00072             will be returned.
00073 
00074             showAllParents -- a boolean property; if true and obj is given,
00075                 ensure that all parents of the object, including any that would
00076                 normally be filtered out are included in the tree.
00077 
00078             nodeFilter(node) -- a method returning a boolean; if this returns
00079                 False, the given node will not be inserted in the tree
00080 
00081             subtreeFilter(node) -- a method returning a boolean; if this returns
00082                 False, the given (folderish) node will not be expanded (its
00083                 children will be pruned off)
00084 
00085             decoratorFactory(node) -- a method returning a dict; this can inject
00086                 additional keys in a node being inserted.
00087                 
00088             showChildrenOf(object) -- a method returning True if children of
00089                 the given object (normally the root) should be returned
00090 
00091     Returns tree where each node is represented by a dict:
00092 
00093         item            -   A catalog brain of this item
00094         depth           -   The depth of this item, relative to the startAt level
00095         currentItem     -   True if this is the current item
00096         currentParent   -   True if this is a direct parent of the current item
00097         children        -   A list of children nodes of this node
00098 
00099     Note: Any 'decoratorFactory' specified may modify this list, but
00100     the 'children' property is guaranteed to be there.
00101 
00102     Note: If the query does not return the root node itself, the root
00103     element of the tree may contain *only* the 'children' list.
00104 
00105     Note: Folder default-pages are not included in the returned result.
00106     If the 'obj' passed in is a default-page, its parent folder will be
00107     used for the purposes of selecting the 'currentItem'.
00108     """
00109 
00110     portal_url = getToolByName(context, 'portal_url')
00111     portal_catalog = getToolByName(context, 'portal_catalog')
00112 
00113     showAllParents = strategy.showAllParents
00114     rootPath = strategy.rootPath
00115 
00116     request = getattr(context, 'REQUEST', {})
00117     
00118     # Find the object's path. Use parent folder if context is a default-page
00119 
00120     objPath = None
00121     objPhysicalPath = None
00122     if obj is not None:
00123         objPhysicalPath = obj.getPhysicalPath()
00124         if utils.isDefaultPage(obj, request):
00125             objPhysicalPath = objPhysicalPath[:-1]
00126         objPath = '/'.join(objPhysicalPath)
00127 
00128     portalPath = portal_url.getPortalPath()
00129     portalObject = portal_url.getPortalObject()
00130     
00131     # Calculate rootPath from the path query if not set.
00132 
00133     if 'path' not in query:
00134         if rootPath is None:
00135             rootPath = portalPath
00136         query['path'] = rootPath
00137     elif rootPath is None:
00138         pathQuery = query['path']
00139         if type(pathQuery) == StringType:
00140             rootPath = pathQuery
00141         else:
00142             # Adjust for the fact that in a 'navtree' query, the actual path
00143             # is the path of the current context
00144             if pathQuery.get('navtree', False):
00145                 navtreeLevel = pathQuery.get('navtree_start', 1)
00146                 if navtreeLevel > 1:
00147                     navtreeContextPath = pathQuery['query']
00148                     navtreeContextPathElements = navtreeContextPath[len(portalPath)+1:].split('/')
00149                     # Short-circuit if we won't be able to find this path
00150                     if len(navtreeContextPathElements) < (navtreeLevel - 1):
00151                         return {'children' : []}
00152                     rootPath = portalPath + '/' + '/'.join(navtreeContextPathElements[:navtreeLevel-1])
00153                 else:
00154                     rootPath = portalPath
00155             else:
00156                 rootPath = pathQuery['query']
00157 
00158     rootDepth = len(rootPath.split('/'))
00159 
00160     # Determine if we need to prune the root (but still force the path to)
00161     # the parent if necessary
00162     
00163     pruneRoot = False
00164     if strategy is not None:
00165         rootObject = portalObject.unrestrictedTraverse(rootPath, None)
00166         if rootObject is not None:
00167             pruneRoot = not strategy.showChildrenOf(rootObject)
00168 
00169     # Default sorting and threatment of default-pages
00170 
00171     if 'sort_on' not in query:
00172         query['sort_on'] = 'getObjPositionInParent'
00173 
00174     if 'is_default_page' not in query:
00175         query['is_default_page'] = False
00176 
00177     results = portal_catalog.searchResults(query)
00178 
00179     # We keep track of a dict of item path -> node, so that we can easily
00180     # find parents and attach children. If a child appears before its
00181     # parent, we stub the parent node.
00182 
00183     # This is necessary because whilst the sort_on parameter will ensure
00184     # that the objects in a folder are returned in the right order relative
00185     # to each other, we don't know the relative order of objects from
00186     # different folders. So, if /foo comes before /bar, and /foo/a comes
00187     # before /foo/b, we may get a list like (/bar/x, /foo/a, /foo/b, /foo,
00188     # /bar,).
00189 
00190     itemPaths = {}
00191     
00192     # Add an (initially empty) node for the root
00193     itemPaths[rootPath] = {'children' : []}
00194     
00195     # If we need to "prune" the parent (but still allow showAllParent to 
00196     # force some children), do so now
00197     if pruneRoot:
00198         itemPaths[rootPath]['_pruneSubtree'] = True
00199 
00200     def insertElement(itemPaths, item, forceInsert=False):
00201         """Insert the given 'item' brain into the tree, which is kept in
00202         'itemPaths'. If 'forceInsert' is True, ignore node- and subtree-
00203         filters, otherwise any node- or subtree-filter set will be allowed to
00204         block the insertion of a node.
00205         """
00206         itemPath = item.getPath()
00207         itemInserted = (itemPaths.get(itemPath, {}).get('item', None) is not None)
00208 
00209         # Short-circuit if we already added this item. Don't short-circuit
00210         # if we're forcing the insert, because we may have inserted but
00211         # later pruned off the node
00212         if not forceInsert and itemInserted:
00213             return
00214 
00215         itemPhysicalPath = itemPath.split('/')
00216         parentPath = '/'.join(itemPhysicalPath[:-1])
00217         parentPruned = (itemPaths.get(parentPath, {}).get('_pruneSubtree', False))
00218 
00219         # Short-circuit if we know we're pruning this item's parent
00220 
00221         # XXX: We could do this recursively, in case of parent of the
00222         # parent was being pruned, but this may not be a great trade-off
00223 
00224         # There is scope for more efficiency improvement here: If we knew we
00225         # were going to prune the subtree, we would short-circuit here each time.
00226         # In order to know that, we'd have to make sure we inserted each parent
00227         # before its children, by sorting the catalog result set (probably
00228         # manually) to get a breadth-first search.
00229 
00230         if not forceInsert and parentPruned:
00231             return
00232 
00233         isCurrent = isCurrentParent = False
00234         if objPath is not None:
00235             if objPath == itemPath:
00236                 isCurrent = True
00237             elif objPath.startswith(itemPath + '/') and len(objPhysicalPath) > len(itemPhysicalPath):
00238                 isCurrentParent = True
00239 
00240         relativeDepth = len(itemPhysicalPath) - rootDepth
00241 
00242         newNode = {'item'          : item,
00243                    'depth'         : relativeDepth,
00244                    'currentItem'   : isCurrent,
00245                    'currentParent' : isCurrentParent,}
00246 
00247         insert = True
00248         if not forceInsert and strategy is not None:
00249             insert = strategy.nodeFilter(newNode)
00250         if insert:
00251 
00252             if strategy is not None:
00253                 newNode = strategy.decoratorFactory(newNode)
00254 
00255             # Tell parent about this item, unless an earlier subtree filter
00256             # told us not to. If we're forcing the insert, ignore the
00257             # pruning, but avoid inserting the node twice
00258             if itemPaths.has_key(parentPath):
00259                 itemParent = itemPaths[parentPath]
00260                 if forceInsert:
00261                     nodeAlreadyInserted = False
00262                     for i in itemParent['children']:
00263                         if i['item'].getPath() == itemPath:
00264                             nodeAlreadyInserted = True
00265                             break
00266                     if not nodeAlreadyInserted:
00267                         itemParent['children'].append(newNode)
00268                 elif not itemParent.get('_pruneSubtree', False):
00269                     itemParent['children'].append(newNode)
00270             else:
00271                 itemPaths[parentPath] = {'children': [newNode]}
00272 
00273             # Ask the subtree filter (if any), if we should be expanding this node
00274             if strategy.showAllParents and isCurrentParent:
00275                 # If we will be expanding this later, we can't prune off children now
00276                 expand = True
00277             else:
00278                 expand = getattr(item, 'is_folderish', True)
00279             if expand and (not forceInsert and strategy is not None):
00280                 expand = strategy.subtreeFilter(newNode)
00281 
00282             children = newNode.setdefault('children',[])
00283             if expand:
00284                 # If we had some orphaned children for this node, attach
00285                 # them
00286                 if itemPaths.has_key(itemPath):
00287                     children.extend(itemPaths[itemPath]['children'])
00288             else:
00289                 newNode['_pruneSubtree'] = True
00290 
00291             itemPaths[itemPath] = newNode
00292 
00293     # Add the results of running the query
00294     for r in results:
00295         insertElement(itemPaths, r)
00296 
00297     # If needed, inject additional nodes for the direct parents of the
00298     # context. Note that we use an unrestricted query: things we don't normally
00299     # have permission to see will be included in the tree.
00300     if strategy.showAllParents and objPath is not None:
00301         objSubPathElements = objPath[len(rootPath)+1:].split('/')
00302         parentPaths = []
00303 
00304         haveNode = (itemPaths.get(rootPath, {}).get('item', None) is None)
00305         if not haveNode:
00306             parentPaths.append(rootPath)
00307 
00308         parentPath = rootPath
00309         for i in range(len(objSubPathElements)):
00310             nodePath = rootPath + '/' + '/'.join(objSubPathElements[:i+1])
00311             node = itemPaths.get(nodePath, None)
00312 
00313             # If we don't have this node, we'll have to get it, if we have it
00314             # but it wasn't connected, re-connect it
00315             if node is None or 'item' not in node:
00316                 parentPaths.append(nodePath)
00317             else:
00318                 nodeParent = itemPaths.get(parentPath, None)
00319                 if nodeParent is not None:
00320                     nodeAlreadyInserted = False
00321                     for i in nodeParent['children']:
00322                         if i['item'].getPath() == nodePath:
00323                             nodeAlreadyInserted = True
00324                             break
00325                     if not nodeAlreadyInserted:
00326                         nodeParent['children'].append(node)
00327 
00328             parentPath = nodePath
00329 
00330         # If we were outright missing some nodes, find them again
00331         if len(parentPaths) > 0:
00332             query = {'path' : {'query' : parentPaths, 'depth' : 0}}
00333             results = portal_catalog.unrestrictedSearchResults(query)
00334 
00335             for r in results:
00336                 insertElement(itemPaths, r, forceInsert=True)
00337 
00338     # Return the tree starting at rootPath as the root node.
00339     return itemPaths[rootPath]