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 target_pks |= set([item.target_node_id for item in cache['items']])
156 # A distinct query is not strictly necessary. TODO: benchmark the efficiency
157 # with/without distinct.
158 targets = list(Node.objects.filter(pk__in=target_pks).distinct())
161 for item in cache['items']:
162 if item.target_node_id:
163 item.target_node = targets[targets.index(item.target_node)]
165 def clear_cache(self):
166 self.__class__._cache.pop(self.db, None)
169 class Navigation(Entity):
170 objects = NavigationManager()
172 node = models.ForeignKey(Node, related_name='navigation_set', help_text="Be available as navigation for this node.")
173 key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
174 depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.")
176 def __init__(self, *args, **kwargs):
177 super(Navigation, self).__init__(*args, **kwargs)
178 self._initial_data = model_to_dict(self)
180 def __unicode__(self):
181 return "%s[%s]" % (self.node, self.key)
183 def _has_changed(self):
184 return self._initial_data != model_to_dict(self)
186 def save(self, *args, **kwargs):
187 super(Navigation, self).save(*args, **kwargs)
189 if self._has_changed():
190 Navigation.objects.clear_cache_for(self.node)
191 self._initial_data = model_to_dict(self)
193 def delete(self, *args, **kwargs):
194 super(Navigation, self).delete(*args, **kwargs)
195 Navigation.objects.clear_cache_for(self.node)
198 unique_together = ('node', 'key')
201 class NavigationItemManager(TreeManager):
202 use_for_related = True
204 def get_queryset(self):
205 return NavigationCacheQuerySet(self.model, using=self._db)
208 class NavigationItem(TreeEntity, TargetURLModel):
209 objects = NavigationItemManager()
211 navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.")
212 text = models.CharField(max_length=50)
214 order = models.PositiveSmallIntegerField(default=0)
216 def __init__(self, *args, **kwargs):
217 super(NavigationItem, self).__init__(*args, **kwargs)
218 self._initial_data = model_to_dict(self)
219 self._is_cached = False
221 def __unicode__(self):
222 return self.get_path(field='text', pathsep=u' › ')
225 super(NavigationItem, self).clean()
226 if bool(self.parent) == bool(self.navigation):
227 raise ValidationError("Exactly one of `parent` and `navigation` must be defined.")
229 def is_active(self, request):
230 if self.target_url == request.path:
231 # Handle the `default` case where the target_url and requested path
235 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):
236 # If there's no target_node, double-check whether it's a full-url
240 if self.target_node and not self.url_or_subpath:
241 # If there is a target node and it's targeted simply, but the target URL is not
242 # the same as the request path, check whether the target node is an ancestor
243 # of the requested node. If so, this is active unless the target node
244 # is the same as the ``host node`` for this navigation structure.
246 host_node = self.get_root().navigation.node
247 except AttributeError:
250 if self.target_node != host_node and self.target_node.is_ancestor_of(request.node):
255 def has_active_descendants(self, request):
256 for child in self.get_children():
257 if child.is_active(request) or child.has_active_descendants(request):
261 def _has_changed(self):
262 if model_to_dict(self) == self._initial_data:
266 def _clear_cache(self):
268 root = self.get_root()
269 if self.get_level() < root.navigation.depth:
270 Navigation.objects.clear_cache_for(self.get_root().navigation.node)
271 except AttributeError:
274 def save(self, *args, **kwargs):
275 super(NavigationItem, self).save(*args, **kwargs)
277 if self._has_changed():
280 def delete(self, *args, **kwargs):
281 super(NavigationItem, self).delete(*args, **kwargs)