2 from django.core.exceptions import ValidationError
3 from django.core.urlresolvers import NoReverseMatch
4 from django.db import models
5 from philo.models import TreeEntity, JSONField, Node, TreeManager
6 from philo.validators import RedirectValidator
8 #from mptt.templatetags.mptt_tags import cache_tree_children
11 DEFAULT_NAVIGATION_DEPTH = 3
14 class NavigationManager(TreeManager):
16 # Analagous to contenttypes, cache Navigation to avoid repeated lookups all over the place.
17 # Navigation will probably be used frequently.
20 def for_node(self, node):
22 Returns the set of Navigation objects for a given node's navigation. This
23 will be the most recent set of defined hosted navigation among the node's
24 ancestors. Lookups are cached so that subsequent lookups for the same node
25 don't hit the database.
27 TODO: Should this create the auto-generated navigation in "physical" form?
30 return self._get_from_cache(self.db, node)
32 # Find the most recent host!
33 ancestors = node.get_ancestors(ascending=True, include_self=True).annotate(num_navigation=models.Count("hosted_navigation"))
35 # Iterate down the ancestors until you find one that:
37 # b) has hosted navigation.
40 for ancestor in ancestors:
41 if self._is_cached(self.db, ancestor) or ancestor.num_navigation > 0:
45 nodes_to_cache.append(ancestor)
47 if not self._is_cached(self.db, host_node):
48 self._add_to_cache(self.db, host_node)
50 # Cache the queryset instance for every node that was passed over, as well.
51 hosted_navigation = self._get_from_cache(self.db, host_node)
52 for node in nodes_to_cache:
53 self._add_to_cache(self.db, node, hosted_navigation)
55 return hosted_navigation
57 def _add_to_cache(self, using, node, qs=None):
58 if node is None or node.pk is None:
65 qs = node.hosted_navigation.select_related('target_node')
67 self.__class__._cache.setdefault(using, {})[key] = qs
69 def _get_from_cache(self, using, node):
71 return self.__class__._cache[self.db][key]
73 def _is_cached(self, using, node):
75 self._get_from_cache(using, node)
80 def clear_cache(self, navigation=None):
82 Clear out the navigation cache. This needs to happen during database flushes
83 or if a navigation entry is changed to prevent caching of outdated navigation information.
85 TODO: call this method from update() and delete()!
87 if navigation is None:
88 self.__class__._cache.clear()
90 cache = self.__class__._cache[self.db]
91 for pk in cache.keys():
98 if navigation.is_descendant(instance):
106 class Navigation(TreeEntity):
107 objects = NavigationManager()
108 text = models.CharField(max_length=50)
110 hosting_node = models.ForeignKey(Node, blank=True, null=True, related_name='hosted_navigation', help_text="Be part of this node's root navigation.")
112 target_node = models.ForeignKey(Node, blank=True, null=True, related_name='targeting_navigation', help_text="Point to this node's url.")
113 url_or_subpath = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.")
114 reversing_parameters = JSONField(blank=True, help_text="If reversing parameters are defined, url_or_subpath will instead be interpreted as the view name to be reversed.")
116 order = models.PositiveSmallIntegerField(blank=True, null=True)
117 depth = models.PositiveSmallIntegerField(blank=True, null=True, default=DEFAULT_NAVIGATION_DEPTH, help_text="For the root of a hosted tree, defines the depth of the tree. A blank depth will hide this section of navigation. Otherwise, depth is ignored.")
120 # Should this be enforced? Not enforcing it would allow creation of "headers" in the navbar.
121 if not self.target_node and not self.url_or_subpath:
122 raise ValidationError("Either a target node or a url must be defined.")
124 if self.reversing_parameters and (not self.url_or_subpath or not self.target_node):
125 raise ValidationError("Reversing parameters require a view name and a target node.")
128 self.get_target_url()
129 except NoReverseMatch, e:
130 raise ValidationError(e.message)
132 def get_target_url(self):
133 node = self.target_node
134 if node is not None and node.accepts_subpath and self.url_or_subpath:
135 if self.reversing_parameters is not None:
136 view_name = self.url_or_subpath
137 params = self.reversing_parameters
138 args = isinstance(params, list) and params or None
139 kwargs = isinstance(params, dict) and params or None
140 return node.view.reverse(view_name, args=args, kwargs=kwargs, node=node)
142 subpath = self.url_or_subpath
143 while subpath and subpath[0] == '/':
144 subpath = subpath[1:]
145 return '%s%s' % (node.get_absolute_url(), subpath)
146 elif node is not None:
147 return node.get_absolute_url()
149 return self.url_or_subpath
150 target_url = property(get_target_url)
152 def __unicode__(self):
153 return self.get_path(field='text', pathsep=u' › ')
155 # TODO: Add delete and save methods to handle cache clearing.
159 verbose_name_plural = 'navigation'