Merge branch 'master' into navigation
authorStephen Burrows <stephen.r.burrows@gmail.com>
Tue, 21 Dec 2010 15:12:03 +0000 (10:12 -0500)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Tue, 21 Dec 2010 15:12:03 +0000 (10:12 -0500)
1  2 
models/nodes.py
templatetags/nodes.py

diff --combined models/nodes.py
@@@ -10,16 -10,13 +10,16 @@@ from django.template import add_to_buil
  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
  
@@@ -219,6 -119,14 +219,14 @@@ class MultiView(View)
                        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
  
diff --combined templatetags/nodes.py
@@@ -51,7 -51,9 +51,9 @@@ class NodeURLNode(template.Node)
                                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:]
@@@ -68,7 -70,7 +70,7 @@@
  @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)