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
6 from philo.validators import RedirectValidator
8 #from mptt.templatetags.mptt_tags import cache_tree_children
11 DEFAULT_NAVIGATION_DEPTH = 3
14 class NavigationManager(models.Manager):
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?
31 hosted_navigation = self.__class__._cache[self.db][key]
33 # Find the most recent host!
34 ancestors = node.get_ancestors(ascending=True, include_self=True).annotate(num_navigation=models.Count("hosted_navigation"))
36 # Iterate down the ancestors until you find one that:
38 # b) has hosted navigation.
41 for ancestor in ancestors:
42 if ancestor.pk in self.__class__._cache[self.db] or ancestor.num_navigation > 0:
46 pks_to_cache.append(ancestor.pk)
51 if ancestor.pk not in self.__class__._cache[self.db]:
52 self.__class__._cache[self.db][ancestor.pk] = host_node.hosted_navigation.select_related('target_node')
54 hosted_navigation = self.__class__._cache[self.db][ancestor.pk]
56 # Cache the queryset instance for every pk that was passed over, as well.
57 for pk in pks_to_cache:
58 self.__class__._cache[self.db][pk] = hosted_navigation
60 return hosted_navigation
62 def clear_cache(self, navigation=None):
64 Clear out the navigation cache. This needs to happen during database flushes
65 or if a navigation entry is changed to prevent caching of outdated navigation information.
67 TODO: call this method from update() and delete()!
69 if navigation is None:
70 self.__class__._cache.clear()
72 cache = self.__class__._cache[self.db]
73 for pk in cache.keys():
80 if navigation.is_descendant(instance):
88 class Navigation(TreeEntity):
89 text = models.CharField(max_length=50)
91 hosting_node = models.ForeignKey(Node, blank=True, null=True, related_name='hosted_navigation', help_text="Be part of this node's root navigation.")
93 target_node = models.ForeignKey(Node, blank=True, null=True, related_name='targeting_navigation', help_text="Point to this node's url.")
94 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.")
95 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.")
97 order = models.PositiveSmallIntegerField(blank=True, null=True)
98 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.")
101 # Should this be enforced? Not enforcing it would allow creation of "headers" in the navbar.
102 if not self.target_node and not self.url_or_subpath:
103 raise ValidationError("Either a target node or a url must be defined.")
105 if self.reversing_parameters and (not self.url_or_subpath or not self.target_node):
106 raise ValidationError("Reversing parameters require a view name and a target node.")
109 self.get_target_url()
110 except NoReverseMatch, e:
111 raise ValidationError(e.message)
113 def get_target_url(self):
114 node = self.target_node
115 if node is not None and node.accepts_subpath and self.url_or_subpath:
116 if self.reversing_parameters is not None:
117 view_name = self.url_or_subpath
118 params = self.reversing_parameters
119 args = isinstance(params, list) and params or None
120 kwargs = isinstance(params, dict) and params or None
121 return node.view.reverse(view_name, args=args, kwargs=kwargs, node=node)
123 subpath = self.url_or_subpath
124 while subpath and subpath[0] == '/':
125 subpath = subpath[1:]
126 return '%s%s' % (node.get_absolute_url(), subpath)
127 elif node is not None:
128 return node.get_absolute_url()
130 return self.url_or_subpath
131 target_url = property(get_target_url)
133 def __unicode__(self):
134 return self.get_path(field='text', pathsep=u' › ')
136 # TODO: Add delete and save methods to handle cache clearing.
140 verbose_name_plural = 'navigation'