X-Git-Url: http://git.ithinksw.org/philo.git/blobdiff_plain/7838f389cf5ebed7c23b764581cd563d1865828c..26c548c5fe8c7cc7227b5f38cd27e0278442dd76:/contrib/shipherd/models.py diff --git a/contrib/shipherd/models.py b/contrib/shipherd/models.py index 72bd775..0643c3e 100644 --- a/contrib/shipherd/models.py +++ b/contrib/shipherd/models.py @@ -41,11 +41,9 @@ class NavigationManager(TreeManager): will be the most recent set of defined hosted navigation among the node's ancestors. Lookups are cached so that subsequent lookups for the same node don't hit the database. - - TODO: Should this create the auto-generated navigation in "physical" form? """ try: - return self._get_from_cache(self.db, node) + return self._get_cache_for(self.db, node) except KeyError: # Find the most recent host! ancestors = node.get_ancestors(ascending=True, include_self=True).annotate(num_navigation=models.Count("hosted_navigation")) @@ -56,54 +54,75 @@ class NavigationManager(TreeManager): nodes_to_cache = [] host_node = None for ancestor in ancestors: - if self._is_cached(self.db, ancestor) or ancestor.num_navigation > 0: + if self.has_cache_for(ancestor) or ancestor.num_navigation > 0: host_node = ancestor break else: nodes_to_cache.append(ancestor) - if not self._is_cached(self.db, host_node): + if not self.has_cache_for(host_node): self._add_to_cache(self.db, host_node) # Cache the queryset instance for every node that was passed over, as well. - hosted_navigation = self._get_from_cache(self.db, host_node) + hosted_navigation = self._get_cache_for(self.db, host_node) for node in nodes_to_cache: self._add_to_cache(self.db, node, hosted_navigation) return hosted_navigation def _add_to_cache(self, using, node, qs=None): - key = getattr(node, 'pk', None) - if qs is None: - if key is None: - roots = self.none() - else: + try: roots = node.hosted_navigation.select_related('target_node') + except AttributeError: + roots = [] + qs = self.none() for root in roots: root_qs = root.get_descendants(include_self=True).complex_filter({'%s__lte' % root._mptt_meta.level_attr: root.get_level() + root.depth}).exclude(depth__isnull=True) - if qs is None: - qs = root_qs - else: - qs |= root_qs - - if qs is None: - qs = self.none() + qs |= root_qs - self.__class__._cache.setdefault(using, {})[key] = qs + self.__class__._cache.setdefault(using, {})[node] = qs + + def has_cache(self): + return self.db in self.__class__._cache and self.__class__._cache[self.db] + + def _get_cache_for(self, using, node): + return self.__class__._cache[self.db][node] + + def is_cached(self, navigation): + return self._is_cached(self.db, navigation) + + def _is_cached(self, using, navigation): + cache = self.__class__._cache[using] + for qs in cache.values(): + if navigation in qs: + return True + return False - def _get_from_cache(self, using, node): - key = getattr(node, 'pk', None) - return self.__class__._cache[self.db][key] + def has_cache_for(self, node): + return self._has_cache_for(self.db, node) - def _is_cached(self, using, node): + def _has_cache_for(self, using, node): try: - self._get_from_cache(using, node) + self._get_cache_for(using, node) except KeyError: return False return True + def clear_cache_for(self, node): + """Clear the cache for a node and all its descendants""" + self._clear_cache_for(self.db, node) + + def _clear_cache_for(self, using, node): + # Clear the cache for all descendants of the node. Ideally we would + # only clear up to another hosting node, but the complexity is not + # necessary and may not be possible. + descendants = node.get_descendants(include_self=True) + cache = self.__class__._cache[using] + for node in descendants: + cache.pop(node, None) + def clear_cache(self, navigation=None): """ Clear out the navigation cache. This needs to happen during database flushes @@ -113,9 +132,9 @@ class NavigationManager(TreeManager): self.__class__._cache.clear() elif self.db in self.__class__._cache: cache = self.__class__._cache[self.db] - for pk, qs in cache.items(): + for node, qs in cache.items(): if navigation in qs: - cache.pop(pk) + cache.pop(node) class Navigation(TreeEntity): @@ -172,26 +191,35 @@ class Navigation(TreeEntity): target_url = property(get_target_url) def is_active(self, request): - # First check if this particular navigation is active. It is considered active if: - # - the requested node is this instance's target node and its subpath matches the requested path. - # - the requested node is a descendant of this instance's target node and this instance's target - # node is not the hosting node of this navigation structure. - # - this instance has no target node and the url matches either the request path or the full url. - # - any of this instance's children are active. node = request.node - if self.target_node == node: - if self.target_url == request.path: - return True - elif self.target_node is None: - if self.url_or_subpath == request.path or self.url_or_subpath == "http%s://%s%s" % (request.is_secure() and 's' or '', request.get_host(), request.path): - return True - elif self.target_node.is_ancestor_of(node) and self.target_node != self.hosting_node: + if self.target_url == request.path: + # Handle the `default` case where the target_url and requested path + # are identical. + return True + + if self.target_node is None and self.url_or_subpath == "http%s://%s%s" % (request.is_secure() and 's' or '', request.get_host(), request.path): + # If there's no target_node, double-check whether it's a full-url + # match. return True + if self.target_node: + ancestors = node.get_ancestors(ascending=True, include_self=True).annotate(num_navigation=models.Count("hosted_navigation")).filter(num_navigation__gt=0) + if ancestors: + # If the target node is an ancestor of the requested node, this is + # active - unless the target node is the `home` node for this set of + # navigation or this navigation points to some other url. + host_node = ancestors[0] + if self.target_node.is_ancestor_of(node) and self.target_node != host_node and not self.url_or_subpath: + return True + # Always fall back to whether the node has active children. return self.has_active_children(request) + def is_cached(self): + """Shortcut method for Navigation.objects.is_cached""" + return Navigation.objects.is_cached(self) + def has_active_children(self, request): for child in self.get_children(): if child.is_active(request): @@ -205,14 +233,25 @@ class Navigation(TreeEntity): def save(self, *args, **kwargs): super(Navigation, self).save(*args, **kwargs) + if self._has_changed(): self._initial_data = model_to_dict(self) - Navigation.objects.clear_cache(self) + if Navigation.objects.has_cache(): + if self.is_cached(): + Navigation.objects.clear_cache(self) + else: + for navigation in self.get_ancestors(): + if navigation.hosting_node and navigation.is_cached() and self.get_level() <= (navigation.get_level() + navigation.depth): + Navigation.objects.clear_cache(navigation) + + if self.hosting_node and Navigation.objects.has_cache_for(self.hosting_node): + Navigation.objects.clear_cache_for(self.hosting_node) def delete(self, *args, **kwargs): super(Navigation, self).delete(*args, **kwargs) Navigation.objects.clear_cache(self) class Meta: - ordering = ['order'] + # Should I even try ordering? + ordering = ['order', 'lft'] verbose_name_plural = 'navigation' \ No newline at end of file