2 from django.core.exceptions import ValidationError
3 from django.core.urlresolvers import NoReverseMatch
4 from django.core.validators import RegexValidator, MinValueValidator
5 from django.db import models
6 from django.forms.models import model_to_dict
7 from philo.models import TreeEntity, Node, TreeManager, Entity, TargetURLModel
8 from philo.validators import RedirectValidator
9 from UserDict import DictMixin
12 DEFAULT_NAVIGATION_DEPTH = 3
15 class NavigationQuerySetMapper(object, DictMixin):
16 """This class exists to prevent setting of items in the navigation cache through node.navigation."""
17 def __init__(self, node):
20 def __getitem__(self, key):
21 return Navigation.objects.get_cache_for(self.node)[key]['root_items']
24 return Navigation.objects.get_cache_for(self.node).keys()
28 if not hasattr(self, '_navigation'):
29 self._navigation = NavigationQuerySetMapper(self)
30 return self._navigation
33 Node.navigation = property(navigation)
36 class NavigationCacheQuerySet(models.query.QuerySet):
38 This subclass will trigger general cache clearing for Navigation.objects when a mass
39 update or deletion is performed. As there is no convenient way to iterate over the
40 changed or deleted instances, there's no way to be more precise about what gets cleared.
42 def update(self, *args, **kwargs):
43 super(NavigationCacheQuerySet, self).update(*args, **kwargs)
44 Navigation.objects.clear_cache()
46 def delete(self, *args, **kwargs):
47 super(NavigationCacheQuerySet, self).delete(*args, **kwargs)
48 Navigation.objects.clear_cache()
51 class NavigationManager(models.Manager):
52 # Since navigation is going to be hit frequently and changed
53 # relatively infrequently, cache it. Analogous to contenttypes.
54 use_for_related = True
57 def get_queryset(self):
58 return NavigationCacheQuerySet(self.model, using=self._db)
60 def get_cache_for(self, node, update_targets=True):
62 if not self.has_cache_for(node):
63 self.create_cache_for(node)
66 if update_targets and not created:
67 self.update_targets_for(node)
69 return self.__class__._cache[self.db][node]
71 def has_cache_for(self, node):
72 return self.db in self.__class__._cache and node in self.__class__._cache[self.db]
74 def create_cache_for(self, node):
75 "This method loops through the nodes ancestors and caches all unique navigation keys."
76 ancestors = node.get_ancestors(ascending=True, include_self=True)
80 for node in ancestors:
81 if self.has_cache_for(node):
82 cache = self.get_cache_for(node).copy()
85 nodes_to_cache.insert(0, node)
89 for node in nodes_to_cache:
91 cache.update(self._build_cache_for(node))
92 self.__class__._cache.setdefault(self.db, {})[node] = cache
94 def _build_cache_for(self, node):
96 tree_id_attr = NavigationItem._mptt_meta.tree_id_attr
97 level_attr = NavigationItem._mptt_meta.level_attr
99 for navigation in node.navigation_set.all():
100 tree_ids = navigation.roots.values_list(tree_id_attr)
101 items = list(NavigationItem.objects.filter(**{'%s__in' % tree_id_attr: tree_ids, '%s__lt' % level_attr: navigation.depth}).order_by('order', 'lft'))
106 item._is_cached = True
108 if not hasattr(item, '_cached_children'):
109 item._cached_children = []
112 # alternatively, if I don't want to force it to a list, I could keep track of
113 # instances where the parent hasn't yet been met and do this step later for them.
115 item.parent = items[items.index(item.parent)]
116 if not hasattr(item.parent, '_cached_children'):
117 item.parent._cached_children = []
118 item.parent._cached_children.append(item)
120 root_items.append(item)
122 cache[navigation.key] = {
123 'navigation': navigation,
124 'root_items': root_items,
130 def clear_cache_for(self, node):
131 # Clear the cache for this node and all its descendants. The
132 # navigation for this node has probably changed, and for now,
133 # it isn't worth it to only clear the descendants actually
135 if not self.has_cache_for(node):
139 descendants = node.get_descendants(include_self=True)
140 cache = self.__class__._cache[self.db]
141 for node in descendants:
142 cache.pop(node, None)
144 def update_targets_for(self, node):
145 # Manually update a cache's target nodes in case something's changed there.
146 # This should be a less complex operation than reloading the models each
147 # time. Not as good as selective updates... but not much to be done
148 # about that. TODO: Benchmark it.
149 caches = self.__class__._cache[self.db][node].values()
154 items += cache['items']
156 # A distinct query is not strictly necessary. TODO: benchmark the efficiency
157 # with/without distinct.
158 targets = list(Node.objects.filter(shipherd_navigationitem_related__in=items).distinct())
161 for item in cache['items']:
162 item.target_node = targets[targets.index(item.target_node)]
164 def clear_cache(self):
165 self.__class__._cache.pop(self.db, None)
168 class Navigation(Entity):
169 objects = NavigationManager()
171 node = models.ForeignKey(Node, related_name='navigation_set', help_text="Be available as navigation for this node.")
172 key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
173 depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.")
175 def __init__(self, *args, **kwargs):
176 super(Navigation, self).__init__(*args, **kwargs)
177 self._initial_data = model_to_dict(self)
179 def __unicode__(self):
180 return "%s[%s]" % (self.node, self.key)
182 def _has_changed(self):
183 return self._initial_data != model_to_dict(self)
185 def save(self, *args, **kwargs):
186 super(Navigation, self).save(*args, **kwargs)
188 if self._has_changed():
189 Navigation.objects.clear_cache_for(self.node)
190 self._initial_data = model_to_dict(self)
192 def delete(self, *args, **kwargs):
193 super(Navigation, self).delete(*args, **kwargs)
194 Navigation.objects.clear_cache_for(self.node)
197 unique_together = ('node', 'key')
200 class NavigationItemManager(TreeManager):
201 use_for_related = True
203 def get_queryset(self):
204 return NavigationCacheQuerySet(self.model, using=self._db)
207 class NavigationItem(TreeEntity, TargetURLModel):
208 objects = NavigationItemManager()
210 navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.")
211 text = models.CharField(max_length=50)
213 order = models.PositiveSmallIntegerField(default=0)
215 def __init__(self, *args, **kwargs):
216 super(NavigationItem, self).__init__(*args, **kwargs)
217 self._initial_data = model_to_dict(self)
218 self._is_cached = False
220 def __unicode__(self):
221 return self.get_path(field='text', pathsep=u' › ')
224 super(NavigationItem, self).clean()
225 if bool(self.parent) == bool(self.navigation):
226 raise ValidationError("Exactly one of `parent` and `navigation` must be defined.")
228 def is_active(self, request):
229 if self.target_url == request.path:
230 # Handle the `default` case where the target_url and requested path
234 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):
235 # If there's no target_node, double-check whether it's a full-url
239 if self.target_node and not self.url_or_subpath:
240 # If there is a target node and it's targeted simply, but the target URL is not
241 # the same as the request path, check whether the target node is an ancestor
242 # of the requested node. If so, this is active unless the target node
243 # is the same as the ``host node`` for this navigation structure.
245 host_node = self.get_root().navigation.node
246 except AttributeError:
249 if self.target_node != host_node and self.target_node.is_ancestor_of(request.node):
254 def has_active_descendants(self, request):
255 for child in self.get_children():
256 if child.is_active(request) or child.has_active_descendants(request):
260 def _has_changed(self):
261 if model_to_dict(self) == self._initial_data:
265 def _clear_cache(self):
267 root = self.get_root()
268 if self.get_level() < root.navigation.depth:
269 Navigation.objects.clear_cache_for(self.get_root().navigation.node)
270 except AttributeError:
273 def save(self, *args, **kwargs):
274 super(NavigationItem, self).save(*args, **kwargs)
276 if self._has_changed():
279 def delete(self, *args, **kwargs):
280 super(NavigationItem, self).delete(*args, **kwargs)