X-Git-Url: http://git.ithinksw.org/philo.git/blobdiff_plain/7f6fa6595b4c558d7a97ed00cdb19469db4919df..52d3a0f1ca43820d59447c256126aaba7a726dcd:/philo/contrib/shipherd/models.py diff --git a/philo/contrib/shipherd/models.py b/philo/contrib/shipherd/models.py index a09f385..9888378 100644 --- a/philo/contrib/shipherd/models.py +++ b/philo/contrib/shipherd/models.py @@ -7,220 +7,124 @@ from django.core.validators import RegexValidator, MinValueValidator from django.db import models from django.forms.models import model_to_dict -from philo.models import TreeEntity, Node, TreeManager, Entity, TargetURLModel +from philo.models.base import TreeEntity, TreeEntityManager, Entity +from philo.models.nodes import Node, TargetURLModel DEFAULT_NAVIGATION_DEPTH = 3 -class NavigationQuerySetMapper(object, DictMixin): - """This class exists to prevent setting of items in the navigation cache through node.navigation.""" +class NavigationMapper(object, DictMixin): + """ + The :class:`NavigationMapper` is a dictionary-like object which allows easy fetching of the root items of a navigation for a node according to a key. A :class:`NavigationMapper` instance will be available on each node instance as :attr:`Node.navigation` if :mod:`~philo.contrib.shipherd` is in the :setting:`INSTALLED_APPS` + + """ def __init__(self, node): self.node = node + self._cache = {} def __getitem__(self, key): - return Navigation.objects.get_cache_for(self.node)[key]['root_items'] - - def keys(self): - return Navigation.objects.get_cache_for(self.node).keys() + if key not in self._cache: + try: + self._cache[key] = Navigation.objects.get_for_node(self.node, key) + except Navigation.DoesNotExist: + self._cache[key] = None + return self._cache[key] def navigation(self): if not hasattr(self, '_navigation'): - self._navigation = NavigationQuerySetMapper(self) + self._navigation = NavigationMapper(self) return self._navigation Node.navigation = property(navigation) -class NavigationCacheQuerySet(models.query.QuerySet): - """ - This subclass will trigger general cache clearing for Navigation.objects when a mass - update or deletion is performed. As there is no convenient way to iterate over the - changed or deleted instances, there's no way to be more precise about what gets cleared. - """ - def update(self, *args, **kwargs): - super(NavigationCacheQuerySet, self).update(*args, **kwargs) - Navigation.objects.clear_cache() - - def delete(self, *args, **kwargs): - super(NavigationCacheQuerySet, self).delete(*args, **kwargs) - Navigation.objects.clear_cache() - - class NavigationManager(models.Manager): - # Since navigation is going to be hit frequently and changed - # relatively infrequently, cache it. Analogous to contenttypes. use_for_related = True - _cache = {} - - def get_queryset(self): - return NavigationCacheQuerySet(self.model, using=self._db) - - def get_cache_for(self, node, update_targets=True): - created = False - if not self.has_cache_for(node): - self.create_cache_for(node) - created = True - - if update_targets and not created: - self.update_targets_for(node) - - return self.__class__._cache[self.db][node] - def has_cache_for(self, node): - return self.db in self.__class__._cache and node in self.__class__._cache[self.db] - - def create_cache_for(self, node): - "This method loops through the nodes ancestors and caches all unique navigation keys." - ancestors = node.get_ancestors(ascending=True, include_self=True) - - nodes_to_cache = [] - - for node in ancestors: - if self.has_cache_for(node): - cache = self.get_cache_for(node).copy() - break - else: - nodes_to_cache.insert(0, node) - else: - cache = {} - - for node in nodes_to_cache: - cache = cache.copy() - cache.update(self._build_cache_for(node)) - self.__class__._cache.setdefault(self.db, {})[node] = cache - - def _build_cache_for(self, node): - cache = {} - tree_id_attr = NavigationItem._mptt_meta.tree_id_attr - level_attr = NavigationItem._mptt_meta.level_attr - - for navigation in node.navigation_set.all(): - tree_ids = navigation.roots.values_list(tree_id_attr) - items = list(NavigationItem.objects.filter(**{'%s__in' % tree_id_attr: tree_ids, '%s__lt' % level_attr: navigation.depth}).order_by('order', 'lft')) - - root_items = [] - - for item in items: - item._is_cached = True - - if not hasattr(item, '_cached_children'): - item._cached_children = [] - - if item.parent: - # alternatively, if I don't want to force it to a list, I could keep track of - # instances where the parent hasn't yet been met and do this step later for them. - # delayed action. - item.parent = items[items.index(item.parent)] - if not hasattr(item.parent, '_cached_children'): - item.parent._cached_children = [] - item.parent._cached_children.append(item) - else: - root_items.append(item) - - cache[navigation.key] = { - 'navigation': navigation, - 'root_items': root_items, - 'items': items - } - - return cache - - def clear_cache_for(self, node): - # Clear the cache for this node and all its descendants. The - # navigation for this node has probably changed, and for now, - # it isn't worth it to only clear the descendants actually - # affected by this. - if not self.has_cache_for(node): - # Already cleared. - return - - descendants = node.get_descendants(include_self=True) - cache = self.__class__._cache[self.db] - for node in descendants: - cache.pop(node, None) - - def update_targets_for(self, node): - # Manually update a cache's target nodes in case something's changed there. - # This should be a less complex operation than reloading the models each - # time. Not as good as selective updates... but not much to be done - # about that. TODO: Benchmark it. - caches = self.__class__._cache[self.db][node].values() - - target_pks = set() - - for cache in caches: - target_pks |= set([item.target_node_id for item in cache['items']]) - - # A distinct query is not strictly necessary. TODO: benchmark the efficiency - # with/without distinct. - targets = list(Node.objects.filter(pk__in=target_pks).distinct()) - - for cache in caches: - for item in cache['items']: - if item.target_node_id: - item.target_node = targets[targets.index(item.target_node)] - - def clear_cache(self): - self.__class__._cache.pop(self.db, None) + def get_for_node(self, node, key): + opts = Node._mptt_meta + left = getattr(node, opts.left_attr) + right = getattr(node, opts.right_attr) + tree_id = getattr(node, opts.tree_id_attr) + kwargs = { + "node__%s__lte" % opts.left_attr: left, + "node__%s__gte" % opts.right_attr: right, + "node__%s" % opts.tree_id_attr: tree_id + } + navs = self.filter(key=key, **kwargs).select_related('node').order_by('-node__%s' % opts.level_attr) + nav = navs[0] + roots = nav.roots.all().select_related('target_node') + item_opts = NavigationItem._mptt_meta + by_pk = {} + tree_ids = [] + + for root in roots: + by_pk[root.pk] = root + tree_ids.append(getattr(root, item_opts.tree_id_attr)) + root._cached_children = [] + + kwargs = { + '%s__in' % item_opts.tree_id_attr: tree_ids, + '%s__lt' % item_opts.level_attr: nav.depth, + '%s__gt' % item_opts.level_attr: 0 + } + items = NavigationItem.objects.filter(**kwargs).select_related('target_node').order_by('level', 'order') + for item in items: + by_pk[item.pk] = item + item._cached_children = [] + parent_pk = getattr(item, '%s_id' % item_opts.parent_attr) + item.parent = by_pk[parent_pk] + item.parent._cached_children.append(item) + + return roots class Navigation(Entity): + """ + :class:`Navigation` represents a group of :class:`NavigationItem`\ s that have an intrinsic relationship in terms of navigating a website. For example, a ``main`` navigation versus a ``side`` navigation, or a ``authenticated`` navigation versus an ``anonymous`` navigation. + + A :class:`Navigation`'s :class:`NavigationItem`\ s will be accessible from its related :class:`.Node` and that :class:`.Node`'s descendants through a :class:`NavigationMapper` instance at :attr:`Node.navigation`. Example:: + + >>> node.navigation_set.all() + [] + >>> parent = node.parent + >>> items = parent.navigation_set.get(key='main').roots.all() + >>> parent.navigation["main"] == node.navigation["main"] == list(items) + True + + """ + #: A :class:`NavigationManager` instance. objects = NavigationManager() + #: The :class:`.Node` which the :class:`Navigation` is attached to. The :class:`Navigation` will also be available to all the :class:`.Node`'s descendants and will override any :class:`Navigation` with the same key on any of the :class:`.Node`'s ancestors. node = models.ForeignKey(Node, related_name='navigation_set', help_text="Be available as navigation for this node.") + #: Each :class:`Navigation` has a ``key`` which consists of one or more word characters so that it can easily be accessed in a template as ``{{ node.navigation.this_key }}``. key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True) + #: There is no limit to the depth of a tree of :class:`NavigationItem`\ s, but ``depth`` will limit how much of the tree will be displayed. depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.") - def __init__(self, *args, **kwargs): - super(Navigation, self).__init__(*args, **kwargs) - self._initial_data = model_to_dict(self) - def __unicode__(self): return "%s[%s]" % (self.node, self.key) - def _has_changed(self): - return self._initial_data != model_to_dict(self) - - def save(self, *args, **kwargs): - super(Navigation, self).save(*args, **kwargs) - - if self._has_changed(): - Navigation.objects.clear_cache_for(self.node) - self._initial_data = model_to_dict(self) - - def delete(self, *args, **kwargs): - super(Navigation, self).delete(*args, **kwargs) - Navigation.objects.clear_cache_for(self.node) - class Meta: unique_together = ('node', 'key') -class NavigationItemManager(TreeManager): - use_for_related = True - - def get_queryset(self): - return NavigationCacheQuerySet(self.model, using=self._db) - - class NavigationItem(TreeEntity, TargetURLModel): - objects = NavigationItemManager() - + #: A :class:`ForeignKey` to a :class:`Navigation` instance. If this is not null, then the :class:`NavigationItem` will be a root node of the :class:`Navigation` instance. navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.") + #: The text which will be displayed in the navigation. This is a :class:`CharField` instance with max length 50. text = models.CharField(max_length=50) + #: The order in which the :class:`NavigationItem` will be displayed. order = models.PositiveSmallIntegerField(default=0) - def __init__(self, *args, **kwargs): - super(NavigationItem, self).__init__(*args, **kwargs) - self._initial_data = model_to_dict(self) - self._is_cached = False - - def __unicode__(self): - return self.get_path(field='text', pathsep=u' › ') + def get_path(self, root=None, pathsep=u' › ', field='text'): + return super(NavigationItem, self).get_path(root, pathsep, field) + path = property(get_path) def clean(self): super(NavigationItem, self).clean() @@ -228,6 +132,7 @@ class NavigationItem(TreeEntity, TargetURLModel): raise ValidationError("Exactly one of `parent` and `navigation` must be defined.") def is_active(self, request): + """Returns ``True`` if the :class:`NavigationItem` is considered active for a given request and ``False`` otherwise.""" if self.target_url == request.path: # Handle the `default` case where the target_url and requested path # are identical. @@ -254,30 +159,8 @@ class NavigationItem(TreeEntity, TargetURLModel): return False def has_active_descendants(self, request): + """Returns ``True`` if the :class:`NavigationItem` has active descendants and ``False`` otherwise.""" for child in self.get_children(): if child.is_active(request) or child.has_active_descendants(request): return True - return False - - def _has_changed(self): - if model_to_dict(self) == self._initial_data: - return False - return True - - def _clear_cache(self): - try: - root = self.get_root() - if self.get_level() < root.navigation.depth: - Navigation.objects.clear_cache_for(self.get_root().navigation.node) - except AttributeError: - pass - - def save(self, *args, **kwargs): - super(NavigationItem, self).save(*args, **kwargs) - - if self._has_changed(): - self._clear_cache() - - def delete(self, *args, **kwargs): - super(NavigationItem, self).delete(*args, **kwargs) - self._clear_cache() \ No newline at end of file + return False \ No newline at end of file