Merge branch 'develop' of git://github.com/melinath/philo into develop
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Sat, 25 Jun 2011 02:41:43 +0000 (22:41 -0400)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Sat, 25 Jun 2011 02:41:43 +0000 (22:41 -0400)
* 'develop' of git://github.com/melinath/philo:
  Added catch to NavigationManager.get_for_node for cases where items do not have a target node.
  Added a local cache to NavigationMapper - avoids some unpickling costs.
  Tweaked NavigationItem.is_active to better leverage the caching.
  Generalized the 'single step' case in TreeEntity.get_path to include the case where the root is the direct parent of the current instance.
  Forced path memoization prior to node caching for better performance.
  Added django cache backend use on top of cacheless shipherd.
  Corrections to has_navigation and navigation_host to use the new NavigationMapper correctly. Also added select_related calls to the NavigationManager.get_for_node method.
  Removed shipherd cache to see if it was really helping at all.
  Removed extraneous evaluations of TreeEntity.parent.
  Set handles_subpath to be a class method rather than trying to resolve the url based on instance data. Added a select_related depth of 1 to a node's view when trying to render it.
  Corrected a feed_patterns call in NewsletterView.urlpatterns.
  Minor optimization: only call patterns at the end of winer.FeedView.feed_patterns.
  Minor optimization to AttributeForm. Very slightly reduces the number of queries in the admin for Entities.
  Added memoization (optional but enabled by default) to TreeEntity.get_path.
  Eliminated (Generic)ForeignKey evaluations in shipherd's NavigationManager.update_cache_for, Node.accepts_subpath, and templatetages.nodes.NodeURLNode.render.
  Corrections to frozen models in migration 18 so that south knows what happened in 15-17.
  Eliminated unnecessary ContentType queries that used applabel/model-based lookups.
  Eliminated unnecessary ContentType queries from AttributeMapper._fill_cache.

14 files changed:
philo/admin/forms/attributes.py
philo/contrib/julian/models.py
philo/contrib/penfield/models.py
philo/contrib/shipherd/models.py
philo/contrib/shipherd/templatetags/shipherd.py
philo/contrib/winer/models.py
philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py
philo/models/base.py
philo/models/nodes.py
philo/templatetags/collections.py
philo/templatetags/containers.py
philo/templatetags/embed.py
philo/templatetags/nodes.py
philo/utils/entities.py

index 5372ab3..4a6dd67 100644 (file)
@@ -21,7 +21,7 @@ class AttributeForm(ModelForm):
                # This is necessary because model forms store changes to self.instance in their clean method.
                # Mutter mutter.
                value = self.instance.value
-               self._cached_value_ct = self.instance.value_content_type
+               self._cached_value_ct_id = self.instance.value_content_type_id
                self._cached_value = value
                
                # If there is a value, pull in its fields.
@@ -32,7 +32,7 @@ class AttributeForm(ModelForm):
        def save(self, *args, **kwargs):
                # At this point, the cleaned_data has already been stored on self.instance.
                
-               if self.instance.value_content_type != self._cached_value_ct:
+               if self.instance.value_content_type_id != self._cached_value_ct_id:
                        # The value content type has changed. Clear the old value, if there was one.
                        if self._cached_value:
                                self._cached_value.delete()
@@ -42,8 +42,8 @@ class AttributeForm(ModelForm):
                        
                        # Now create a new value instance so that on next instantiation, the form will
                        # know what fields to add.
-                       if self.instance.value_content_type is not None:
-                               self.instance.value = self.instance.value_content_type.model_class().objects.create()
+                       if self.instance.value_content_type_id is not None:
+                               self.instance.value = ContentType.objects.get_for_id(self.instance.value_content_type_id).model_class().objects.create()
                elif self.instance.value is not None:
                        # The value content type is the same, but one of the value fields has changed.
                        
index e4e78ba..fd0a7c5 100644 (file)
@@ -334,7 +334,7 @@ class CalendarView(FeedView):
        
        def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
                try:
-                       ct = ContentType.objects.get(app_label=app_label, model=model)
+                       ct = ContentType.objects.get_by_natural_key(app_label, model)
                        location = ct.model_class()._default_manager.get(pk=pk)
                except ObjectDoesNotExist:
                        raise Http404
index 3a8e82f..c2edd8d 100644 (file)
@@ -486,9 +486,7 @@ class NewsletterView(FeedView):
                                url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
                        )
                if self.article_archive_page:
-                       urlpatterns += patterns('',
-                               url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
-                       )
+                       urlpatterns += self.feed_patterns(r'^%s' % self.article_permalink_base, 'get_all_articles', 'article_archive_page', 'articles')
                        if self.article_permalink_style in 'DMY':
                                urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
                                if self.article_permalink_style in 'DM':
index 429faaa..95be501 100644 (file)
@@ -1,6 +1,9 @@
 #encoding: utf-8
 from UserDict import DictMixin
+from hashlib import sha1
 
+from django.contrib.sites.models import Site
+from django.core.cache import cache
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import NoReverseMatch
 from django.core.validators import RegexValidator, MinValueValidator
@@ -16,17 +19,20 @@ DEFAULT_NAVIGATION_DEPTH = 3
 
 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. The fetching goes through the :class:`NavigationManager` and can thus take advantage of the navigation cache. 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`
+       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):
@@ -38,141 +44,68 @@ def navigation(self):
 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 on a site will be hit frequently, is relatively costly to compute, and is changed relatively infrequently, the NavigationManager maintains a cache which maps nodes to navigations.
-       
-       """
        use_for_related = True
-       _cache = {}
        
-       def get_query_set(self):
-               """
-               Returns a :class:`NavigationCacheQuerySet` instance.
-               
-               """
-               return NavigationCacheQuerySet(self.model, using=self._db)
-       
-       def get_cache_for(self, node, update_targets=True):
-               """Returns the navigation cache for a given :class:`.Node`. If update_targets is ``True``, then :meth:`update_targets_for` will be run with the :class:`.Node`."""
-               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):
-               """Returns ``True`` if a cache exists for the :class:`.Node` and ``False`` otherwise."""
-               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 :class:`.Node`\ s 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'))
+       def get_for_node(self, node, key):
+               cache_key = self._get_cache_key(node, key)
+               cached = cache.get(cache_key)
+               
+               if cached is None:
+                       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').order_by('order')
+                       item_opts = NavigationItem._mptt_meta
+                       by_pk = {}
+                       tree_ids = []
                        
-                       root_items = []
+                       site_root_node = Site.objects.get_current().root_node
                        
-                       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)
+                       for root in roots:
+                               by_pk[root.pk] = root
+                               tree_ids.append(getattr(root, item_opts.tree_id_attr))
+                               root._cached_children = []
+                               if root.target_node:
+                                       root.target_node.get_path(root=site_root_node)
+                               root.navigation = nav
                        
-                       cache[navigation.key] = {
-                               'navigation': navigation,
-                               'root_items': root_items,
-                               'items': items
+                       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)
+                               if item.target_node:
+                                       item.target_node.get_path(root=site_root_node)
+                       
+                       cached = roots
+                       cache.set(cache_key, cached)
                
-               return cache
-       
-       def clear_cache_for(self, node):
-               """Clear the cache for the :class:`.Node` and all its descendants. The navigation for this node has probably changed, and it isn't worth it to figure out which descendants were 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)
+               return cached
        
-       def update_targets_for(self, node):
-               """Manually updates the target nodes for the :class:`.Node`'s cache in case something's changed there. This is a less complex operation than rebuilding the :class:`.Node`'s cache."""
-               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())
+       def _get_cache_key(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)
+               parent_id = getattr(node, "%s_id" % opts.parent_attr)
                
-               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):
-               """Clears the manager's entire navigation cache."""
-               self.__class__._cache.pop(self.db, None)
+               return sha1(unicode(left) + unicode(right) + unicode(tree_id) + unicode(parent_id) + unicode(node.pk) + unicode(key)).hexdigest()
 
 
 class Navigation(Entity):
@@ -199,43 +132,14 @@ class Navigation(Entity):
        #: 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(TreeEntityManager):
-       use_for_related = True
-       
-       def get_query_set(self):
-               """Returns a :class:`NavigationCacheQuerySet` instance."""
-               return NavigationCacheQuerySet(self.model, using=self._db)
-
-
 class NavigationItem(TreeEntity, TargetURLModel):
-       #: A :class:`NavigationItemManager` instance
-       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.
@@ -244,11 +148,6 @@ class NavigationItem(TreeEntity, TargetURLModel):
        #: 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 get_path(self, root=None, pathsep=u' â€º ', field='text'):
                return super(NavigationItem, self).get_path(root, pathsep, field)
        path = property(get_path)
@@ -275,13 +174,15 @@ class NavigationItem(TreeEntity, TargetURLModel):
                        # the same as the request path, check whether the target node is an ancestor
                        # of the requested node. If so, this is active unless the target node
                        # is the same as the ``host node`` for this navigation structure.
-                       try:
-                               host_node = self.get_root().navigation.node
-                       except AttributeError:
-                               pass
-                       else:
-                               if self.target_node != host_node and self.target_node.is_ancestor_of(request.node):
-                                       return True
+                       root = self
+                       
+                       # The common case will be cached items, whose parents are cached with them.
+                       while root.parent is not None:
+                               root = root.parent
+                       
+                       host_node_id = root.navigation.node_id
+                       if self.target_node.pk != host_node_id and self.target_node.is_ancestor_of(request.node):
+                               return True
                
                return False
        
@@ -290,27 +191,4 @@ class NavigationItem(TreeEntity, TargetURLModel):
                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
index 1031d73..833a995 100644 (file)
@@ -173,13 +173,7 @@ def recursenavigation(parser, token):
 def has_navigation(node, key=None):
        """Returns ``True`` if the node has a :class:`.Navigation` with the given key and ``False`` otherwise. If ``key`` is ``None``, returns whether the node has any :class:`.Navigation`\ s at all."""
        try:
-               nav = node.navigation
-               if key is not None:
-                       if key in nav and bool(node.navigation[key]):
-                               return True
-                       elif key not in node.navigation:
-                               return False
-               return bool(node.navigation)
+               return bool(node.navigation[key])
        except:
                return False
 
@@ -188,6 +182,6 @@ def has_navigation(node, key=None):
 def navigation_host(node, key):
        """Returns the :class:`.Node` which hosts the :class:`.Navigation` which ``node`` has inherited for ``key``. Returns ``node`` if any exceptions are encountered."""
        try:
-               return Navigation.objects.filter(node__in=node.get_ancestors(include_self=True), key=key).order_by('-node__level')[0].node
+               return node.navigation[key].node
        except:
                return node
\ No newline at end of file
index 5e85cc3..98d559e 100644 (file)
@@ -87,19 +87,15 @@ class FeedView(MultiView):
                .. seealso:: :meth:`get_feed_type`
                
                """
-               urlpatterns = patterns('')
+               feed_patterns = ()
                if self.feeds_enabled:
                        suffixes = [(self.feed_suffix, None)] + [(slug, slug) for slug in registry]
                        for suffix, feed_type in suffixes:
                                feed_view = http_not_acceptable(self.feed_view(get_items_attr, reverse_name, feed_type))
                                feed_pattern = r'%s%s%s$' % (base, "/" if base and base[-1] != "^" else "", suffix)
-                               urlpatterns += patterns('',
-                                       url(feed_pattern, feed_view, name="%s_%s" % (reverse_name, suffix)),
-                               )
-               urlpatterns += patterns('',
-                       url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
-               )
-               return urlpatterns
+                               feed_patterns += (url(feed_pattern, feed_view, name="%s_%s" % (reverse_name, suffix)),)
+               feed_patterns += (url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name),)
+               return patterns('', *feed_patterns)
        
        def get_object(self, request, **kwargs):
                """By default, returns the object stored in the attribute named by :attr:`object_attr`. This can be overridden for subclasses that publish different data for different URL parameters. It is part of the :class:`django.contrib.syndication.views.Feed` API."""
index b2e6a5e..75a3dee 100644 (file)
@@ -74,7 +74,8 @@ class Migration(SchemaMigration):
             'Meta': {'object_name': 'File'},
             'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+            'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
         },
         'philo.foreignkeyvalue': {
             'Meta': {'object_name': 'ForeignKeyValue'},
@@ -94,7 +95,7 @@ class Migration(SchemaMigration):
             'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
         },
         'philo.node': {
-            'Meta': {'object_name': 'Node'},
+            'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'},
             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
             'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
             'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
@@ -126,7 +127,7 @@ class Migration(SchemaMigration):
             'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
         },
         'philo.template': {
-            'Meta': {'object_name': 'Template'},
+            'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
             'code': ('philo.models.fields.TemplateField', [], {}),
             'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
index 2f798ae..8df67c3 100644 (file)
@@ -458,11 +458,12 @@ class TreeEntity(Entity, MPTTModel):
        objects = TreeEntityManager()
        parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
        
-       def get_path(self, root=None, pathsep='/', field='slug'):
+       def get_path(self, root=None, pathsep='/', field='pk', memoize=True):
                """
                :param root: Only return the path since this object.
                :param pathsep: The path separator to use when constructing an instance's path
                :param field: The field to pull path information from for each ancestor.
+               :param memoize: Whether to use memoized results. Since, in most cases, the ancestors of a TreeEntity will not change over the course of an instance's lifetime, this defaults to ``True``.
                :returns: A string representation of an object's path.
                
                """
@@ -470,18 +471,33 @@ class TreeEntity(Entity, MPTTModel):
                if root == self:
                        return ''
                
-               if root is None and self.is_root_node():
+               parent_id = getattr(self, "%s_id" % self._mptt_meta.parent_attr)
+               if getattr(root, 'pk', None) == parent_id:
                        return getattr(self, field, '?')
                
                if root is not None and not self.is_descendant_of(root):
                        raise AncestorDoesNotExist(root)
                
+               if memoize:
+                       memo_args = (parent_id, getattr(root, 'pk', None), pathsep, getattr(self, field, '?'))
+                       try:
+                               return self._path_memo[memo_args]
+                       except AttributeError:
+                               self._path_memo = {}
+                       except KeyError:
+                               pass
+               
                qs = self.get_ancestors(include_self=True)
                
                if root is not None:
                        qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
                
-               return pathsep.join([getattr(parent, field, '?') for parent in qs])
+               path = pathsep.join([getattr(parent, field, '?') for parent in qs])
+               
+               if memoize:
+                       self._path_memo[memo_args] = path
+               
+               return path
        path = property(get_path)
        
        def get_attribute_mapper(self, mapper=None):
@@ -500,7 +516,7 @@ class TreeEntity(Entity, MPTTModel):
                
                """
                if mapper is None:
-                       if self.parent:
+                       if getattr(self, "%s_id" % self._mptt_meta.parent_attr):
                                mapper = TreeAttributeMapper
                        else:
                                mapper = AttributeMapper
@@ -522,12 +538,12 @@ class SlugTreeEntity(TreeEntity):
        objects = SlugTreeEntityManager()
        slug = models.SlugField(max_length=255)
        
-       def get_path(self, root=None, pathsep='/', field='slug'):
-               return super(SlugTreeEntity, self).get_path(root, pathsep, field)
+       def get_path(self, root=None, pathsep='/', field='slug', memoize=True):
+               return super(SlugTreeEntity, self).get_path(root, pathsep, field, memoize)
        path = property(get_path)
        
        def clean(self):
-               if self.parent is None:
+               if getattr(self, "%s_id" % self._mptt_meta.parent_attr) is None:
                        try:
                                self._default_manager.exclude(pk=self.pk).get(slug=self.slug, parent__isnull=True)
                        except self.DoesNotExist:
index 5b8b8ed..830f94a 100644 (file)
@@ -39,18 +39,20 @@ class Node(SlugTreeEntity):
        @property
        def accepts_subpath(self):
                """A property shortcut for :attr:`self.view.accepts_subpath <View.accepts_subpath>`"""
-               if self.view:
-                       return self.view.accepts_subpath
+               if self.view_object_id and self.view_content_type_id:
+                       return ContentType.objects.get_for_id(self.view_content_type_id).model_class().accepts_subpath
                return False
        
        def handles_subpath(self, subpath):
-               if self.view:
-                       return self.view.handles_subpath(subpath)
+               if self.view_object_id and self.view_content_type_id:
+                       return ContentType.objects.get_for_id(self.view_content_type_id).model_class().handles_subpath(subpath)
                return False
        
        def render_to_response(self, request, extra_context=None):
                """This is a shortcut method for :meth:`View.render_to_response`"""
-               if self.view:
+               if self.view_object_id and self.view_content_type_id:
+                       view_model = ContentType.objects.get_for_id(self.view_content_type_id).model_class()
+                       self.view = view_model._default_manager.select_related(depth=1).get(pk=self.view_object_id)
                        return self.view.render_to_response(request, extra_context)
                raise Http404
        
@@ -126,12 +128,13 @@ class View(Entity):
        #: A generic relation back to nodes.
        nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id')
        
-       #: Property or attribute which defines whether this :class:`View` can handle subpaths. Default: ``False``
+       #: An attribute on the class which defines whether this :class:`View` can handle subpaths. Default: ``False``
        accepts_subpath = False
        
-       def handles_subpath(self, subpath):
+       @classmethod
+       def handles_subpath(cls, subpath):
                """Returns True if the :class:`View` handles the given subpath, and False otherwise."""
-               if not self.accepts_subpath and subpath != "/":
+               if not cls.accepts_subpath and subpath != "/":
                        return False
                return True
        
@@ -226,15 +229,6 @@ class MultiView(View):
                """Returns urlpatterns that point to views (generally methods on the class). :class:`MultiView`\ s can be thought of as "managing" these subpaths."""
                raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
        
-       def handles_subpath(self, subpath):
-               if not super(MultiView, self).handles_subpath(subpath):
-                       return False
-               try:
-                       resolve(subpath, urlconf=self)
-               except Http404:
-                       return False
-               return True
-       
        def actually_render_to_response(self, request, extra_context=None):
                """
                Resolves the remaining subpath left after finding this :class:`View`'s node using :attr:`self.urlpatterns <urlpatterns>` and renders the view function (or method) found with the appropriate args and kwargs.
index 414a742..e9db2bd 100644 (file)
@@ -47,7 +47,7 @@ def membersof(parser, token):
        
        try:
                app_label, model = params[3].strip('"').split('.')
-               ct = ContentType.objects.get(app_label=app_label, model=model)
+               ct = ContentType.objects.get_by_natural_key(app_label, model)
        except ValueError:
                raise template.TemplateSyntaxError('"%s" template tag option "with" requires an argument of the form app_label.model (see django.contrib.contenttypes)' % tag)
        except ContentType.DoesNotExist:
index e280e60..bc04b9f 100644 (file)
@@ -87,7 +87,7 @@ def container(parser, token):
                                if option_token == 'references':
                                        try:
                                                app_label, model = remaining_tokens.pop(0).strip('"').split('.')
-                                               references = ContentType.objects.get(app_label=app_label, model=model)
+                                               references = ContentType.objects.get_by_natural_key(app_label, model)
                                        except IndexError:
                                                raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument specifying a content type' % tag)
                                        except ValueError:
index 9599240..16f8092 100644 (file)
@@ -285,7 +285,7 @@ def parse_content_type(bit, tagname):
        except ValueError:
                raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tagname)
        try:
-               ct = ContentType.objects.get(app_label=app_label, model=model)
+               ct = ContentType.objects.get_by_natural_key(app_label, model)
        except ContentType.DoesNotExist:
                raise template.TemplateSyntaxError('"%s" template tag requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tagname)
        return ct
index 189fdd5..52da236 100644 (file)
@@ -39,7 +39,7 @@ class NodeURLNode(template.Node):
                if self.with_obj is None and self.view_name is None:
                        url = node.get_absolute_url()
                else:
-                       if not node.view.accepts_subpath:
+                       if not node.accepts_subpath:
                                return settings.TEMPLATE_STRING_IF_INVALID
                        
                        if self.with_obj is not None:
index 1ddff05..754a5dc 100644 (file)
@@ -83,15 +83,15 @@ class AttributeMapper(object, DictMixin):
                value_lookups = {}
                
                for a in attributes:
-                       value_lookups.setdefault(a.value_content_type, []).append(a.value_object_id)
+                       value_lookups.setdefault(a.value_content_type_id, []).append(a.value_object_id)
                        self._attributes_cache[a.key] = a
                
                values_bulk = {}
                
-               for ct, pks in value_lookups.items():
-                       values_bulk[ct] = ct.model_class().objects.in_bulk(pks)
+               for ct_pk, pks in value_lookups.items():
+                       values_bulk[ct_pk] = ContentType.objects.get_for_id(ct_pk).model_class().objects.in_bulk(pks)
                
-               self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type].get(a.value_object_id), 'value', None)) for a in attributes]))
+               self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type_id].get(a.value_object_id), 'value', None)) for a in attributes]))
                self._cache_filled = True
        
        def clear_cache(self):