bedrock/mozorg/hierarchy.py (76 lines of code) (raw):
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from bedrock.base.urlresolvers import reverse
from bedrock.mozorg.util import page
class PageNode:
"""
A utility for representing a hierarchical page structure.
A PageNode is associated with a static page and can have several child nodes
that themselves have pages and children, forming a tree structure. The root
of the tree can then be used to create a urlconf representing every page
within it.
Example:
hierarchy = PageRoot('Root', path='root', children=[
PageNode('Child1', path='child1', template='child1.html'),
PageNode('Child2', path='child2', template='child2.html')
])
urlpatterns = hierarchy.as_urlpatterns()
In the example above, the template `child1.html` will be available at the
url `/root/child1/`.
"""
def __init__(self, display_name, path=None, template=None, children=None):
"""
Create a new PageNode.
display_name is a user-facing name for this node that may be shown in
hierarchical navigation or breadcrumb navigation.
path is a url path component that will be appended to the front of any
child node paths, as well as the final path component for this node's
page.
template is the path to the template that this node's page will use. If
it is None, this node won't have a template.
children is a list of child nodes.
"""
self.display_name = display_name
self.path = path
self.template = template
self.parent = None
self.children = children or ()
for child in self.children:
child.parent = self
@property
def full_path(self):
"""
The full url path for this node, including the paths of its parent
nodes.
"""
return "/".join([node.path for node in self.breadcrumbs if node.path is not None])
@property
def page(self):
"""The page for this node, which is a RegexURLPattern."""
if self.template:
return page(self.full_path, self.template, node_root=self.root, node=self)
else:
return None
@property
def path_to_root(self):
"""
An iterable that contains the nodes that lead to the tree's root,
starting with the current node.
"""
node = self
while node:
yield node
node = node.parent
@property
def breadcrumbs(self):
"""
A list of nodes that form a path from the tree root to the current node.
"""
path = list(self.path_to_root)
path.reverse()
return path
@property
def root(self):
"""The root of the tree that this node is in."""
root = list(self.path_to_root)[-1]
if not isinstance(root, PageRoot):
raise ValueError("Root node is not a PageRoot object.")
return root
@property
def previous(self):
"""
The previous node with a page in a pre-order traversal of the tree.
"""
return self.root.get_previous_node(self)
@property
def next(self):
"""The next node with a page in a pre-order traversal of the tree."""
return self.root.get_next_node(self)
@property
def url(self):
"""
The url for this node's page.
If this node doesn't have a page, it will return the url of its first
child. If it has no children, it will return None.
"""
if self.page:
return reverse(self.page.name)
elif self.children:
return self.children[0].url
else:
return None
def __repr__(self):
return f'{self.__class__.__name__}(display_name="{self.display_name}", path="{self.full_path}", template="{self.template})"'
class PageRoot(PageNode):
"""
Root of a PageNode tree.
The root node of a PageNode tree MUST be a PageRoot. If it is not, any
reference to the root of the tree with throw a ValueError.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Buid a pre-order traversal of this tree's nodes.
self.preordered_nodes = []
nodes = [self]
while nodes:
node = nodes.pop()
self.preordered_nodes.append(node)
nodes.extend(reversed(node.children))
def get_previous_node(self, current_node):
end = self.preordered_nodes.index(current_node)
for node in reversed(self.preordered_nodes[0:end]):
if node.template:
return node
return None
def get_next_node(self, current_node):
start = self.preordered_nodes.index(current_node) + 1
for node in self.preordered_nodes[start:]:
if node.template:
return node
return None
def as_urlpatterns(self):
"""Return a urlconf for this PageRoot and its children."""
return [node.page for node in self.preordered_nodes if node.template]