from inspect import getargspec
from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
+from philo.models.fields import JSONField
from philo.utils import ContentTypeSubclassLimiter
from philo.validators import RedirectValidator
from philo.exceptions import ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths, AncestorDoesNotExist
from philo.signals import view_about_to_render, view_finished_rendering
+from mptt.templatetags.mptt_tags import cache_tree_children
_view_content_type_limiter = ContentTypeSubclassLimiter(None)
+DEFAULT_NAVIGATION_DEPTH = 3
class Node(TreeEntity):
except AncestorDoesNotExist, ViewDoesNotExist:
return None
+ def get_navigation(self, depth=DEFAULT_NAVIGATION_DEPTH):
+ max_depth = depth + self.get_level()
+ tree = cache_tree_children(self.get_descendants(include_self=True).filter(level__lte=max_depth))
+
+ def get_nav(parent, nodes):
+ node_overrides = dict([(override.child.pk, override) for override in NodeNavigationOverride.objects.filter(parent=parent, child__in=nodes).select_related('child')])
+
+ navigation_list = []
+ for node in nodes:
+ node._override = node_overrides.get(node.pk, None)
+
+ if node._override:
+ if node._override.hide:
+ continue
+ navigation = node._override.get_navigation(node, max_depth)
+ else:
+ navigation = node.view.get_navigation(node, max_depth)
+
+ if not node.is_leaf_node() and node.get_level() < max_depth:
+ children = navigation.get('children', [])
+ children += get_nav(node, node.get_children())
+ navigation['children'] = children
+
+ if 'children' in navigation:
+ navigation['children'].sort(cmp=lambda x,y: cmp(x['order'], y['order']))
+
+ navigation_list.append(navigation)
+
+ return navigation_list
+
+ navigation = get_nav(self.parent, tree)
+ root = navigation[0]
+ navigation = [root] + root['children']
+ del(root['children'])
+ return navigation
+
+ def save(self):
+ super(Node, self).save()
+
+
class Meta:
app_label = 'philo'
models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
+class NodeNavigationOverride(Entity):
+ parent = models.ForeignKey(Node, related_name="child_navigation_overrides", blank=True, null=True)
+ child = models.ForeignKey(Node, related_name="navigation_overrides")
+
+ title = models.CharField(max_length=100, blank=True)
+ url = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True)
+ order = models.PositiveSmallIntegerField(blank=True, null=True)
+ child_navigation = JSONField()
+ hide = models.BooleanField()
+
+ def get_navigation(self, node, max_depth):
+ default = node.view.get_navigation(node, max_depth)
+ if self.url:
+ default['url'] = self.url
+ if self.title:
+ default['title'] = self.title
+ if self.order:
+ default['order'] = self.order
+ if isinstance(self.child_navigation, list) and node.get_level() < max_depth:
+ child_navigation = self.child_navigation[:]
+
+ for child in child_navigation:
+ child['url'] = default['url'] + child['url']
+
+ if 'children' in default:
+ overridden = set([child['url'] for child in default['children']]) & set([child['url'] for child in self.child_navigation])
+ if overridden:
+ for child in default[:]:
+ if child['url'] in overridden:
+ default.remove(child)
+ default['children'] += self.child_navigation
+ else:
+ default['children'] = self.child_navigation
+ return default
+
+ class Meta:
+ ordering = ['order']
+ unique_together = ('parent', 'child',)
+ app_label = 'philo'
+
+
class View(Entity):
nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id')
def actually_render_to_response(self, request, extra_context=None):
raise NotImplementedError('View subclasses must implement render_to_response.')
+ def get_navigation(self, node, max_depth):
+ """
+ Subclasses should implement get_navigation to support auto-generated navigation.
+ max_depth is the deepest `level` that should be generated; node is the node that
+ is asking for the navigation. This method should return a dictionary of the form:
+ {
+ 'url': url,
+ 'title': title,
+ 'order': order, # None for no ordering.
+ 'children': [ # Optional
+ <similar child navigation dictionaries>
+ ]
+ }
+ """
+ raise NotImplementedError('View subclasses must implement get_navigation.')
+
class Meta:
abstract = True
kwargs['extra_context'] = extra_context
return view(request, *args, **kwargs)
+ def reverse(self, view_name, args=None, kwargs=None, node=None):
+ """Shortcut method to handle the common pattern of getting the absolute url for a multiview's
+ subpaths."""
+ subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {})
+ if node is not None:
+ return '/%s/%s/' % (node.get_absolute_url().strip('/'), subpath.strip('/'))
+ return subpath
+
class Meta:
abstract = True
subpath = reverse(view_name, urlconf=node.view, args=args, kwargs=kwargs)
except NoReverseMatch:
if self.as_var is None:
- raise
+ if settings.TEMPLATE_DEBUG:
+ raise
+ return settings.TEMPLATE_STRING_IF_INVALID
else:
if subpath[0] == '/':
subpath = subpath[1:]
@register.tag(name='node_url')
def do_node_url(parser, token):
"""
- {% node_url [for <node>] [as <var] %}
+ {% node_url [for <node>] [as <var>] %}
{% node_url with <obj> [for <node>] [as <var>] %}
{% node_url <view_name> [<arg1> [<arg2> ...] ] [for <node>] [as <var>] %}
{% node_url <view_name> [<key1>=<value1> [<key2>=<value2> ...] ] [for <node>] [as <var>]%}
args.append(parser.compile_filter(value))
return NodeURLNode(view_name=view_name, args=args, kwargs=kwargs, node=node, as_var=as_var)
- return NodeURLNode(node=node, as_var=as_var)
+ return NodeURLNode(node=node, as_var=as_var)
+
+
+class NavigationNode(template.Node):
+ def __init__(self, node=None, as_var=None):
+ self.as_var = as_var
+ self.node = node
+
+ def render(self, context):
+ if 'request' not in context:
+ return settings.TEMPLATE_STRING_IF_INVALID
+
+ if self.node:
+ node = self.node.resolve(context)
+ else:
+ node = context.get('node', None)
+
+ if not node:
+ return settings.TEMPLATE_STRING_IF_INVALID
+
+ try:
+ nav_root = node.attributes['navigation_root']
+ except KeyError:
+ if settings.TEMPLATE_DEBUG:
+ raise
+ return settings.TEMPLATE_STRING_IF_INVALID
+
+ # Should I get its override and check for a max depth override there?
+ navigation = nav_root.get_navigation()
+
+ if self.as_var:
+ context[self.as_var] = navigation
+ return ''
+
+ return self.compile(navigation, context['request'].path, nav_root.get_absolute_url(), nav_root.get_level(), nav_root.get_level() + 3)
+
+ def compile(self, navigation, active_path, root_url, current_depth, max_depth):
+ compiled = ""
+ for item in navigation:
+ if item['url'] in active_path and (item['url'] != root_url or root_url == active_path):
+ compiled += "<li class='active'>"
+ else:
+ compiled += "<li>"
+
+ if item['url']:
+ compiled += "<a href='%s'>" % item['url']
+
+ compiled += item['title']
+
+ if item['url']:
+ compiled += "</a>"
+
+ if 'children' in item and current_depth < max_depth:
+ compiled += "<ul>%s</ul>" % self.compile(item['children'], active_path, root_url, current_depth + 1, max_depth)
+
+ compiled += "</li>"
+ return compiled
+
+
+@register.tag(name='navigation')
+def do_navigation(parser, token):
+ """
+ {% navigation [for <node>] [as <var>] %}
+ """
+ bits = token.split_contents()
+ tag = bits[0]
+ bits = bits[1:]
+ node = None
+ as_var = None
+
+ if len(bits) >= 2 and bits[-2] == 'as':
+ as_var = bits[-1]
+ bits = bits[:-2]
+
+ if len(bits) >= 2 and bits[-2] == 'for':
+ node = parser.compile_filter(bits[-1])
+ bits = bits[-2]
+
+ if bits:
+ raise template.TemplateSyntaxError('`%s` template tag expects the syntax {%% %s [for <node>] [as <var>] %}' % (tag, tag))
+ return NavigationNode(node, as_var)