Merge branch 'master' of git://github.com/melinath/philo
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Mon, 29 Nov 2010 19:11:37 +0000 (14:11 -0500)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Mon, 29 Nov 2010 19:11:37 +0000 (14:11 -0500)
* 'master' of git://github.com/melinath/philo:
  Added {% include_string %} tag to allow passing of context from templates to template code from arbitrary sources (such as a NewsletterArticle).
  Added get_path tests back in. Special-cased get_path if root and self are equal to return '' instead of raising an exception.
  Corrected Node's get_path method to only fetch the path since the given root (if any)
  Brought tests in line with mptt branch. Added assertQueryLimit method as a rough measure of efficiency. Added get_with_path tests. Made corrections to get_with_path for cases where root != None.
  Added {% embed <instance> %} syntax for embedding. Switched to actually using the EmbedNode get_instance and get_template methods for greater flexibility.
  Added MPTTModelAdmin integration. Committed docstring changes for get_with_path.
  Initial mptt commit. Implements a much more efficient get_with_path method for TreeManager, optimized with mptt features. Also increases efficiency of TreeModel.get_path(). mptt admin features to come.

admin/base.py
admin/nodes.py
admin/pages.py
fixtures/test_fixtures.json
migrations/0009_auto__add_field_node_lft__add_field_node_rght__add_field_node_tree_id_.py [new file with mode: 0644]
models/base.py
templatetags/embed.py
templatetags/include_string.py [new file with mode: 0644]
tests.py

index 0413dde..0d35cf6 100644 (file)
@@ -7,6 +7,7 @@ from django.utils.html import escape
 from philo.models import Tag, Attribute
 from philo.forms import AttributeForm, AttributeInlineFormSet
 from philo.admin.widgets import TagFilteredSelectMultiple
+from mptt.admin import MPTTModelAdmin
 
 
 COLLAPSE_CLASSES = ('collapse', 'collapse-closed', 'closed',)
@@ -33,6 +34,14 @@ class EntityAdmin(admin.ModelAdmin):
        save_on_top = True
 
 
+class TreeAdmin(MPTTModelAdmin):
+       pass
+
+
+class TreeEntityAdmin(TreeAdmin, EntityAdmin):
+       pass
+
+
 class TagAdmin(admin.ModelAdmin):
        list_display = ('name', 'slug')
        prepopulated_fields = {"slug": ("name",)}
index 093537e..45a3172 100644 (file)
@@ -1,9 +1,9 @@
 from django.contrib import admin
-from philo.admin.base import EntityAdmin
+from philo.admin.base import EntityAdmin, TreeEntityAdmin
 from philo.models import Node, Redirect, File
 
 
-class NodeAdmin(EntityAdmin):
+class NodeAdmin(TreeEntityAdmin):
        pass
 
 
index 15b06d9..caeee05 100644 (file)
@@ -1,7 +1,7 @@
 from django.conf import settings
 from django.contrib import admin
 from django import forms
-from philo.admin.base import COLLAPSE_CLASSES
+from philo.admin.base import COLLAPSE_CLASSES, TreeAdmin
 from philo.admin.nodes import ViewAdmin
 from philo.models.pages import Page, Template, Contentlet, ContentReference
 from philo.forms import ContentletInlineFormSet, ContentReferenceInlineFormSet, ContentletForm, ContentReferenceForm
@@ -48,7 +48,7 @@ class PageAdmin(ViewAdmin):
        inlines = [ContentletInline, ContentReferenceInline] + ViewAdmin.inlines
 
 
-class TemplateAdmin(admin.ModelAdmin):
+class TemplateAdmin(TreeAdmin):
        prepopulated_fields = {'slug': ('name',)}
        fieldsets = (
                (None, {
index 14f5a27..4c55372 100644 (file)
 [
-    {
-        "pk": 1, 
-        "model": "philo.tag", 
-        "fields": {
-            "name": "Test tag", 
-            "slug": "test-tag"
-        }
-    }, 
     {
         "pk": 1, 
         "model": "philo.node", 
         "fields": {
+            "rght": 144, 
             "view_object_id": 1, 
-            "slug": "never", 
-            "parent": 3, 
             "view_content_type": [
                 "philo", 
-                "page"
-            ]
+                "redirect"
+            ], 
+            "parent": null, 
+            "level": 0, 
+            "lft": 1, 
+            "tree_id": 1, 
+            "slug": "root"
         }
     }, 
     {
         "pk": 2, 
         "model": "philo.node", 
         "fields": {
+            "rght": 9, 
             "view_object_id": 1, 
-            "slug": "blog", 
-            "parent": 3, 
             "view_content_type": [
-                "penfield", 
-                "blogview"
-            ]
+                "philo", 
+                "page"
+            ], 
+            "parent": 1, 
+            "level": 1, 
+            "lft": 2, 
+            "tree_id": 1, 
+            "slug": "second"
         }
     }, 
     {
         "pk": 3, 
         "model": "philo.node", 
         "fields": {
+            "rght": 8, 
             "view_object_id": 1, 
-            "slug": "root", 
-            "parent": null, 
             "view_content_type": [
                 "philo", 
-                "redirect"
-            ]
+                "page"
+            ], 
+            "parent": 2, 
+            "level": 2, 
+            "lft": 3, 
+            "tree_id": 1, 
+            "slug": "third"
         }
     }, 
     {
         "pk": 4, 
         "model": "philo.node", 
         "fields": {
+            "rght": 7, 
             "view_object_id": 1, 
-            "slug": "more", 
-            "parent": 1, 
             "view_content_type": [
                 "philo", 
                 "page"
-            ]
+            ], 
+            "parent": 3, 
+            "level": 3, 
+            "lft": 4, 
+            "tree_id": 1, 
+            "slug": "fourth"
         }
     }, 
     {
         "pk": 5, 
         "model": "philo.node", 
         "fields": {
+            "rght": 6, 
             "view_object_id": 1, 
-            "slug": "second", 
-            "parent": 4, 
             "view_content_type": [
                 "philo", 
                 "page"
-            ]
+            ], 
+            "parent": 4, 
+            "level": 4, 
+            "lft": 5, 
+            "tree_id": 1, 
+            "slug": "fifth"
         }
     }, 
     {
         "pk": 6, 
         "model": "philo.node", 
         "fields": {
+            "rght": 143, 
             "view_object_id": 1, 
-            "slug": "third", 
-            "parent": 5, 
             "view_content_type": [
-                "philo", 
-                "page"
-            ]
+                "penfield", 
+                "blogview"
+            ], 
+            "parent": 1, 
+            "level": 1, 
+            "lft": 10, 
+            "tree_id": 1, 
+            "slug": "second2"
         }
     }, 
     {
         "pk": 7, 
         "model": "philo.node", 
         "fields": {
+            "rght": 124, 
             "view_object_id": 1, 
-            "slug": "recursive1", 
-            "parent": 9, 
             "view_content_type": [
                 "philo", 
                 "page"
-            ]
+            ], 
+            "parent": 6, 
+            "level": 2, 
+            "lft": 11, 
+            "tree_id": 1, 
+            "slug": "third2"
         }
     }, 
     {
         "pk": 8, 
         "model": "philo.node", 
         "fields": {
+            "rght": 123, 
             "view_object_id": 1, 
-            "slug": "recursive2", 
-            "parent": 7, 
             "view_content_type": [
                 "philo", 
                 "page"
-            ]
+            ], 
+            "parent": 7, 
+            "level": 3, 
+            "lft": 12, 
+            "tree_id": 1, 
+            "slug": "fourth2"
         }
     }, 
     {
         "pk": 9, 
         "model": "philo.node", 
         "fields": {
+            "rght": 122, 
             "view_object_id": 1, 
-            "slug": "recursive3", 
-            "parent": 8, 
             "view_content_type": [
                 "philo", 
                 "page"
-            ]
+            ], 
+            "parent": 8, 
+            "level": 4, 
+            "lft": 13, 
+            "tree_id": 1, 
+            "slug": "fifth2"
         }
     }, 
     {
         "pk": 10, 
         "model": "philo.node", 
         "fields": {
+            "rght": 121, 
             "view_object_id": 1, 
-            "slug": "postrecursive1", 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
             "parent": 9, 
+            "level": 5, 
+            "lft": 14, 
+            "tree_id": 1, 
+            "slug": "0"
+        }
+    }, 
+    {
+        "pk": 11, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 120, 
+            "view_object_id": 1, 
             "view_content_type": [
                 "philo", 
                 "page"
-            ]
+            ], 
+            "parent": 10, 
+            "level": 6, 
+            "lft": 15, 
+            "tree_id": 1, 
+            "slug": "1"
+        }
+    }, 
+    {
+        "pk": 12, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 119, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 11, 
+            "level": 7, 
+            "lft": 16, 
+            "tree_id": 1, 
+            "slug": "2"
+        }
+    }, 
+    {
+        "pk": 13, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 118, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 12, 
+            "level": 8, 
+            "lft": 17, 
+            "tree_id": 1, 
+            "slug": "3"
+        }
+    }, 
+    {
+        "pk": 14, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 117, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 13, 
+            "level": 9, 
+            "lft": 18, 
+            "tree_id": 1, 
+            "slug": "4"
+        }
+    }, 
+    {
+        "pk": 15, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 116, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 14, 
+            "level": 10, 
+            "lft": 19, 
+            "tree_id": 1, 
+            "slug": "5"
+        }
+    }, 
+    {
+        "pk": 16, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 115, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 15, 
+            "level": 11, 
+            "lft": 20, 
+            "tree_id": 1, 
+            "slug": "6"
+        }
+    }, 
+    {
+        "pk": 17, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 114, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 16, 
+            "level": 12, 
+            "lft": 21, 
+            "tree_id": 1, 
+            "slug": "7"
+        }
+    }, 
+    {
+        "pk": 18, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 113, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 17, 
+            "level": 13, 
+            "lft": 22, 
+            "tree_id": 1, 
+            "slug": "8"
+        }
+    }, 
+    {
+        "pk": 19, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 112, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 18, 
+            "level": 14, 
+            "lft": 23, 
+            "tree_id": 1, 
+            "slug": "9"
+        }
+    }, 
+    {
+        "pk": 20, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 111, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 19, 
+            "level": 15, 
+            "lft": 24, 
+            "tree_id": 1, 
+            "slug": "10"
+        }
+    }, 
+    {
+        "pk": 21, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 110, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 20, 
+            "level": 16, 
+            "lft": 25, 
+            "tree_id": 1, 
+            "slug": "11"
+        }
+    }, 
+    {
+        "pk": 22, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 109, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 21, 
+            "level": 17, 
+            "lft": 26, 
+            "tree_id": 1, 
+            "slug": "12"
+        }
+    }, 
+    {
+        "pk": 23, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 108, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 22, 
+            "level": 18, 
+            "lft": 27, 
+            "tree_id": 1, 
+            "slug": "13"
+        }
+    }, 
+    {
+        "pk": 24, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 107, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 23, 
+            "level": 19, 
+            "lft": 28, 
+            "tree_id": 1, 
+            "slug": "14"
+        }
+    }, 
+    {
+        "pk": 25, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 106, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 24, 
+            "level": 20, 
+            "lft": 29, 
+            "tree_id": 1, 
+            "slug": "15"
+        }
+    }, 
+    {
+        "pk": 26, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 105, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 25, 
+            "level": 21, 
+            "lft": 30, 
+            "tree_id": 1, 
+            "slug": "16"
+        }
+    }, 
+    {
+        "pk": 27, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 104, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 26, 
+            "level": 22, 
+            "lft": 31, 
+            "tree_id": 1, 
+            "slug": "17"
+        }
+    }, 
+    {
+        "pk": 28, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 73, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 27, 
+            "level": 23, 
+            "lft": 32, 
+            "tree_id": 1, 
+            "slug": "18"
+        }
+    }, 
+    {
+        "pk": 29, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 72, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 28, 
+            "level": 24, 
+            "lft": 33, 
+            "tree_id": 1, 
+            "slug": "19"
+        }
+    }, 
+    {
+        "pk": 30, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 71, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 29, 
+            "level": 25, 
+            "lft": 34, 
+            "tree_id": 1, 
+            "slug": "20"
+        }
+    }, 
+    {
+        "pk": 31, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 70, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 30, 
+            "level": 26, 
+            "lft": 35, 
+            "tree_id": 1, 
+            "slug": "21"
+        }
+    }, 
+    {
+        "pk": 32, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 69, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 31, 
+            "level": 27, 
+            "lft": 36, 
+            "tree_id": 1, 
+            "slug": "22"
+        }
+    }, 
+    {
+        "pk": 33, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 68, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 32, 
+            "level": 28, 
+            "lft": 37, 
+            "tree_id": 1, 
+            "slug": "23"
+        }
+    }, 
+    {
+        "pk": 34, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 67, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 33, 
+            "level": 29, 
+            "lft": 38, 
+            "tree_id": 1, 
+            "slug": "24"
+        }
+    }, 
+    {
+        "pk": 35, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 66, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 34, 
+            "level": 30, 
+            "lft": 39, 
+            "tree_id": 1, 
+            "slug": "25"
+        }
+    }, 
+    {
+        "pk": 36, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 65, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 35, 
+            "level": 31, 
+            "lft": 40, 
+            "tree_id": 1, 
+            "slug": "26"
+        }
+    }, 
+    {
+        "pk": 37, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 64, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 36, 
+            "level": 32, 
+            "lft": 41, 
+            "tree_id": 1, 
+            "slug": "27"
+        }
+    }, 
+    {
+        "pk": 38, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 63, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 37, 
+            "level": 33, 
+            "lft": 42, 
+            "tree_id": 1, 
+            "slug": "28"
+        }
+    }, 
+    {
+        "pk": 39, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 62, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 38, 
+            "level": 34, 
+            "lft": 43, 
+            "tree_id": 1, 
+            "slug": "29"
+        }
+    }, 
+    {
+        "pk": 40, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 61, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 39, 
+            "level": 35, 
+            "lft": 44, 
+            "tree_id": 1, 
+            "slug": "30"
+        }
+    }, 
+    {
+        "pk": 41, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 60, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 40, 
+            "level": 36, 
+            "lft": 45, 
+            "tree_id": 1, 
+            "slug": "31"
+        }
+    }, 
+    {
+        "pk": 42, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 59, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 41, 
+            "level": 37, 
+            "lft": 46, 
+            "tree_id": 1, 
+            "slug": "32"
+        }
+    }, 
+    {
+        "pk": 43, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 58, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 42, 
+            "level": 38, 
+            "lft": 47, 
+            "tree_id": 1, 
+            "slug": "33"
+        }
+    }, 
+    {
+        "pk": 44, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 57, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 43, 
+            "level": 39, 
+            "lft": 48, 
+            "tree_id": 1, 
+            "slug": "34"
+        }
+    }, 
+    {
+        "pk": 45, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 56, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 44, 
+            "level": 40, 
+            "lft": 49, 
+            "tree_id": 1, 
+            "slug": "35"
+        }
+    }, 
+    {
+        "pk": 46, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 55, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 45, 
+            "level": 41, 
+            "lft": 50, 
+            "tree_id": 1, 
+            "slug": "36"
+        }
+    }, 
+    {
+        "pk": 47, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 54, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 46, 
+            "level": 42, 
+            "lft": 51, 
+            "tree_id": 1, 
+            "slug": "37"
+        }
+    }, 
+    {
+        "pk": 48, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 53, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 47, 
+            "level": 43, 
+            "lft": 52, 
+            "tree_id": 1, 
+            "slug": "38"
+        }
+    }, 
+    {
+        "pk": 49, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 103, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 27, 
+            "level": 23, 
+            "lft": 74, 
+            "tree_id": 1, 
+            "slug": "39"
+        }
+    }, 
+    {
+        "pk": 50, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 102, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 49, 
+            "level": 24, 
+            "lft": 75, 
+            "tree_id": 1, 
+            "slug": "40"
+        }
+    }, 
+    {
+        "pk": 51, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 101, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 50, 
+            "level": 25, 
+            "lft": 76, 
+            "tree_id": 1, 
+            "slug": "41"
+        }
+    }, 
+    {
+        "pk": 52, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 100, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 51, 
+            "level": 26, 
+            "lft": 77, 
+            "tree_id": 1, 
+            "slug": "42"
+        }
+    }, 
+    {
+        "pk": 53, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 99, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 52, 
+            "level": 27, 
+            "lft": 78, 
+            "tree_id": 1, 
+            "slug": "43"
+        }
+    }, 
+    {
+        "pk": 54, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 98, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 53, 
+            "level": 28, 
+            "lft": 79, 
+            "tree_id": 1, 
+            "slug": "44"
+        }
+    }, 
+    {
+        "pk": 55, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 97, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 54, 
+            "level": 29, 
+            "lft": 80, 
+            "tree_id": 1, 
+            "slug": "45"
+        }
+    }, 
+    {
+        "pk": 56, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 96, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 55, 
+            "level": 30, 
+            "lft": 81, 
+            "tree_id": 1, 
+            "slug": "46"
+        }
+    }, 
+    {
+        "pk": 57, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 95, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 56, 
+            "level": 31, 
+            "lft": 82, 
+            "tree_id": 1, 
+            "slug": "47"
+        }
+    }, 
+    {
+        "pk": 58, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 94, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 57, 
+            "level": 32, 
+            "lft": 83, 
+            "tree_id": 1, 
+            "slug": "48"
+        }
+    }, 
+    {
+        "pk": 59, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 93, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 58, 
+            "level": 33, 
+            "lft": 84, 
+            "tree_id": 1, 
+            "slug": "49"
+        }
+    }, 
+    {
+        "pk": 60, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 92, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 59, 
+            "level": 34, 
+            "lft": 85, 
+            "tree_id": 1, 
+            "slug": "50"
+        }
+    }, 
+    {
+        "pk": 61, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 91, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 60, 
+            "level": 35, 
+            "lft": 86, 
+            "tree_id": 1, 
+            "slug": "51"
+        }
+    }, 
+    {
+        "pk": 62, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 90, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 61, 
+            "level": 36, 
+            "lft": 87, 
+            "tree_id": 1, 
+            "slug": "52"
+        }
+    }, 
+    {
+        "pk": 63, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 89, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 62, 
+            "level": 37, 
+            "lft": 88, 
+            "tree_id": 1, 
+            "slug": "53"
+        }
+    }, 
+    {
+        "pk": 64, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 142, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 6, 
+            "level": 2, 
+            "lft": 125, 
+            "tree_id": 1, 
+            "slug": "54"
+        }
+    }, 
+    {
+        "pk": 65, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 141, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 64, 
+            "level": 3, 
+            "lft": 126, 
+            "tree_id": 1, 
+            "slug": "55"
+        }
+    }, 
+    {
+        "pk": 66, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 140, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 65, 
+            "level": 4, 
+            "lft": 127, 
+            "tree_id": 1, 
+            "slug": "56"
+        }
+    }, 
+    {
+        "pk": 67, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 139, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 66, 
+            "level": 5, 
+            "lft": 128, 
+            "tree_id": 1, 
+            "slug": "57"
+        }
+    }, 
+    {
+        "pk": 68, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 138, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 67, 
+            "level": 6, 
+            "lft": 129, 
+            "tree_id": 1, 
+            "slug": "58"
+        }
+    }, 
+    {
+        "pk": 69, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 137, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 68, 
+            "level": 7, 
+            "lft": 130, 
+            "tree_id": 1, 
+            "slug": "59"
+        }
+    }, 
+    {
+        "pk": 70, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 136, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 69, 
+            "level": 8, 
+            "lft": 131, 
+            "tree_id": 1, 
+            "slug": "60"
+        }
+    }, 
+    {
+        "pk": 71, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 135, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 70, 
+            "level": 9, 
+            "lft": 132, 
+            "tree_id": 1, 
+            "slug": "61"
+        }
+    }, 
+    {
+        "pk": 72, 
+        "model": "philo.node", 
+        "fields": {
+            "rght": 134, 
+            "view_object_id": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ], 
+            "parent": 71, 
+            "level": 10, 
+            "lft": 133, 
+            "tree_id": 1, 
+            "slug": "62"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "philo.tag", 
+        "fields": {
+            "name": "Test tag", 
+            "slug": "test-tag"
         }
     }, 
     {
         "model": "philo.redirect", 
         "fields": {
             "status_code": 302, 
-            "target": "never"
+            "target": "second"
         }
     }, 
     {
         "model": "philo.template", 
         "fields": {
             "mimetype": "text/html", 
+            "rght": 2, 
             "code": "Never is working!\r\n{% node_url %}", 
             "name": "Never", 
             "parent": null, 
+            "level": 0, 
             "documentation": "", 
+            "lft": 1, 
+            "tree_id": 1, 
             "slug": "never"
         }
     }, 
         "model": "philo.template", 
         "fields": {
             "mimetype": "text/html", 
-            "code": "An index page!\r\n{% node_url %}\r\n\r\n{% for entry in entries %}\r\n<h4><a href='{% node_url with entry %}'>{{ entry.title }}</a></h4>\r\n<div class='post content'>\r\n{{ entry.content }}\r\n</div>\r\n{% endfor %}", 
+            "rght": 2, 
+            "code": "An index page!\r\n{% node_url %}\r\n{% for entry in entries %}\r\n<h4><a href='{% node_url with entry %}'>{{ entry.title }}</a></h4>\r\n<div class='post content'>\r\n{{ entry.content }}\r\n</div>\r\n{% endfor %}", 
             "name": "Index", 
             "parent": null, 
+            "level": 0, 
             "documentation": "", 
+            "lft": 1, 
+            "tree_id": 2, 
             "slug": "index"
         }
     }, 
         "model": "philo.template", 
         "fields": {
             "mimetype": "text/html", 
+            "rght": 2, 
             "code": "Entry detail page.", 
-            "name": "Entry", 
+            "name": "Entry Detail Page", 
             "parent": null, 
+            "level": 0, 
             "documentation": "", 
+            "lft": 1, 
+            "tree_id": 3, 
             "slug": "entry"
         }
     }, 
         "model": "philo.template", 
         "fields": {
             "mimetype": "text/html", 
+            "rght": 2, 
             "code": "Tag page!", 
             "name": "Tag", 
             "parent": null, 
+            "level": 0, 
             "documentation": "", 
+            "lft": 1, 
+            "tree_id": 4, 
             "slug": "tag"
         }
     }, 
         "model": "philo.template", 
         "fields": {
             "mimetype": "text/html", 
+            "rght": 2, 
             "code": "Entry archive page!", 
             "name": "Entry Archives!", 
             "parent": null, 
+            "level": 0, 
             "documentation": "", 
+            "lft": 1, 
+            "tree_id": 5, 
             "slug": "entry-archives"
         }
     }, 
         "model": "philo.template", 
         "fields": {
             "mimetype": "text/html", 
+            "rght": 2, 
             "code": "tag archives...", 
             "name": "Tag Archives", 
             "parent": null, 
+            "level": 0, 
             "documentation": "", 
+            "lft": 1, 
+            "tree_id": 6, 
             "slug": "tag-archives"
         }
     }, 
diff --git a/migrations/0009_auto__add_field_node_lft__add_field_node_rght__add_field_node_tree_id_.py b/migrations/0009_auto__add_field_node_lft__add_field_node_rght__add_field_node_tree_id_.py
new file mode 100644 (file)
index 0000000..a6f58fd
--- /dev/null
@@ -0,0 +1,178 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding field 'Node.lft'
+        db.add_column('philo_node', 'lft', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True), keep_default=False)
+
+        # Adding field 'Node.rght'
+        db.add_column('philo_node', 'rght', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True), keep_default=False)
+
+        # Adding field 'Node.tree_id'
+        db.add_column('philo_node', 'tree_id', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True), keep_default=False)
+
+        # Adding field 'Node.level'
+        db.add_column('philo_node', 'level', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True), keep_default=False)
+
+        # Adding field 'Template.lft'
+        db.add_column('philo_template', 'lft', self.gf('django.db.models.fields.PositiveIntegerField')(default=0, db_index=True), keep_default=False)
+
+        # Adding field 'Template.rght'
+        db.add_column('philo_template', 'rght', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True), keep_default=False)
+
+        # Adding field 'Template.tree_id'
+        db.add_column('philo_template', 'tree_id', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True), keep_default=False)
+
+        # Adding field 'Template.level'
+        db.add_column('philo_template', 'level', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'Node.lft'
+        db.delete_column('philo_node', 'lft')
+
+        # Deleting field 'Node.rght'
+        db.delete_column('philo_node', 'rght')
+
+        # Deleting field 'Node.tree_id'
+        db.delete_column('philo_node', 'tree_id')
+
+        # Deleting field 'Node.level'
+        db.delete_column('philo_node', 'level')
+
+        # Deleting field 'Template.lft'
+        db.delete_column('philo_template', 'lft')
+
+        # Deleting field 'Template.rght'
+        db.delete_column('philo_template', 'rght')
+
+        # Deleting field 'Template.tree_id'
+        db.delete_column('philo_template', 'tree_id')
+
+        # Deleting field 'Template.level'
+        db.delete_column('philo_template', 'level')
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'philo.attribute': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.collection': {
+            'Meta': {'object_name': 'Collection'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.collectionmember': {
+            'Meta': {'object_name': 'CollectionMember'},
+            'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.contentlet': {
+            'Meta': {'object_name': 'Contentlet'},
+            'content': ('philo.models.fields.TemplateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+        },
+        'philo.contentreference': {
+            'Meta': {'object_name': 'ContentReference'},
+            'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+        },
+        'philo.file': {
+            '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'})
+        },
+        'philo.foreignkeyvalue': {
+            'Meta': {'object_name': 'ForeignKeyValue'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.jsonvalue': {
+            'Meta': {'object_name': 'JSONValue'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'value': ('philo.models.fields.JSONField', [], {})
+        },
+        'philo.manytomanyvalue': {
+            'Meta': {'object_name': 'ManyToManyValue'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
+        },
+        'philo.node': {
+            'Meta': {'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'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.page': {
+            'Meta': {'object_name': 'Page'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.redirect': {
+            'Meta': {'object_name': 'Redirect'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        'philo.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+        },
+        'philo.template': {
+            'Meta': {'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'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['philo']
index 03b9b54..202c2f3 100644 (file)
@@ -10,6 +10,7 @@ from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
 from philo.signals import entity_class_prepared
 from philo.validators import json_validator
 from UserDict import DictMixin
+from mptt.models import MPTTModel, MPTTModelBase, MPTTOptions
 
 
 class Tag(models.Model):
@@ -279,36 +280,21 @@ class Entity(models.Model):
 class TreeManager(models.Manager):
        use_for_related_fields = True
        
-       def roots(self):
-               return self.filter(parent__isnull=True)
-       
-       def get_branch_pks(self, root, depth=5, inclusive=True):
-               branch_pks = []
-               parent_pks = [root.pk]
-               
-               if inclusive:
-                       branch_pks.append(root.pk)
-               
-               for i in xrange(depth):
-                       child_pks = list(self.filter(parent__pk__in=parent_pks).exclude(pk__in=branch_pks).values_list('pk', flat=True))
-                       if not child_pks:
-                               break
-                       
-                       branch_pks += child_pks
-                       parent_pks = child_pks
-               
-               return branch_pks
-       
-       def get_branch(self, root, depth=5, inclusive=True):
-               return self.filter(pk__in=self.get_branch_pks(root, depth, inclusive))
-       
        def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
                """
                Returns the object with the path, unless absolute_result is set to False, in which
                case it returns a tuple containing the deepest object found along the path, and the
                remainder of the path after that object as a string (or None if there is no remaining
                path). Raises a DoesNotExist exception if no object is found with the given path.
+               
+               If the path you're searching for is known to exist, it is always faster to use
+               absolute_result=True - unless the path depth is over ~40, in which case the high cost
+               of the absolute query makes a binary search (i.e. non-absolute) faster.
                """
+               # Note: SQLite allows max of 64 tables in one join. That means the binary search will
+               # only work on paths with a max depth of 127 and the absolute fetch will only work
+               # to a max depth of (surprise!) 63. Although this could be handled, chances are your
+               # tree structure won't be that deep.
                segments = path.split(pathsep)
                
                # Check for a trailing pathsep so we can restore it later.
@@ -326,11 +312,13 @@ class TreeManager(models.Manager):
                # Special-case a lack of segments. No queries necessary.
                if not segments:
                        if root is not None:
+                               if absolute_result:
+                                       return root
                                return root, None
                        else:
                                raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
                
-               def make_query_kwargs(segments):
+               def make_query_kwargs(segments, root):
                        kwargs = {}
                        prefix = ""
                        revsegs = list(segments)
@@ -340,7 +328,9 @@ class TreeManager(models.Manager):
                                kwargs["%s%s__exact" % (prefix, field)] = segment
                                prefix += "parent__"
                        
-                       kwargs[prefix[:-2]] = root
+                       if prefix:
+                               kwargs[prefix[:-2]] = root
+                       
                        return kwargs
                
                def build_path(segments):
@@ -349,86 +339,81 @@ class TreeManager(models.Manager):
                                path += pathsep
                        return path
                
-               def find_obj(segments, depth, deepest_found):
+               def find_obj(segments, depth, deepest_found=None):
+                       if deepest_found is None:
+                               deepest_level = 0
+                       elif root is None:
+                               deepest_level = deepest_found.get_level() + 1
+                       else:
+                               deepest_level = deepest_found.get_level() - root.get_level()
                        try:
-                               obj = self.get(**make_query_kwargs(segments[:depth]))
+                               obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found or root))
                        except self.model.DoesNotExist:
-                               if absolute_result:
-                                       raise
+                               if not deepest_level and depth > 1:
+                                       # make sure there's a root node...
+                                       depth = 1
+                               else:
+                                       # Try finding one with half the path since the deepest find.
+                                       depth = (deepest_level + depth)/2
                                
-                               depth = (deepest_found + depth)/2
-                               if deepest_found == depth:
+                               if deepest_level == depth:
                                        # This should happen if nothing is found with any part of the given path.
                                        raise
                                
-                               # Try finding one with half the path since the deepest find.
                                return find_obj(segments, depth, deepest_found)
                        else:
-                               # Yay! Found one! Could there be a deeper one?
-                               if absolute_result:
-                                       return obj
+                               # Yay! Found one!
+                               if root is None:
+                                       deepest_level = obj.get_level() + 1
+                               else:
+                                       deepest_level = obj.get_level() - root.get_level()
+                               
+                               # Could there be a deeper one?
+                               if obj.is_leaf_node():
+                                       return obj, build_path(segments[deepest_level:]) or None
                                
-                               deepest_found = depth
-                               depth = (len(segments) + depth)/2
+                               depth += (len(segments) - depth)/2 or len(segments) - depth
                                
-                               if deepest_found == depth:
-                                       return obj, build_path(segments[deepest_found:]) or None
+                               if depth > deepest_level + obj.get_descendant_count():
+                                       depth = deepest_level + obj.get_descendant_count()
+                               
+                               if deepest_level == depth:
+                                       return obj, build_path(segments[deepest_level:]) or None
                                
                                try:
-                                       return find_obj(segments, depth, deepest_found)
+                                       return find_obj(segments, depth, obj)
                                except self.model.DoesNotExist:
-                                       # Then the deepest one was already found.
-                                       return obj, build_path(segments[deepest_found:])
+                                       # Then this was the deepest.
+                                       return obj, build_path(segments[deepest_level:])
+               
+               if absolute_result:
+                       return self.get(**make_query_kwargs(segments, root))
                
-               return find_obj(segments, len(segments), 0)
+               # Try a modified binary search algorithm. Feed the root in so that query complexity
+               # can be reduced. It might be possible to weight the search towards the beginning
+               # of the path, since short paths are more likely, but how far forward? It would
+               # need to shift depending on len(segments) - perhaps logarithmically?
+               return find_obj(segments, len(segments)/2 or len(segments))
 
 
-class TreeModel(models.Model):
+class TreeModel(MPTTModel):
        objects = TreeManager()
        parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
        slug = models.SlugField(max_length=255)
        
-       def has_ancestor(self, ancestor, inclusive=False):
-               if inclusive:
-                       parent = self
-               else:
-                       parent = self.parent
-               
-               parents = []
-               
-               while parent:
-                       if parent == ancestor:
-                               return True
-                       # If we've found this parent before, the path is recursive and ancestor wasn't on it.
-                       if parent in parents:
-                               return False
-                       parents.append(parent)
-                       parent = parent.parent
-               # If ancestor is None, catch it here.
-               if parent == ancestor:
-                       return True
-               return False
-       
        def get_path(self, root=None, pathsep='/', field='slug'):
-               parent = self.parent
-               parents = [self]
+               if root == self:
+                       return ''
                
-               def compile_path(parents):
-                       return pathsep.join([getattr(parent, field, '?') for parent in parents])
+               if root is not None and not self.is_descendant_of(root):
+                       raise AncestorDoesNotExist(root)
                
-               while parent and parent != root:
-                       if parent in parents:
-                               if root is not None:
-                                       raise AncestorDoesNotExist(root)
-                               parents.append(parent)
-                               return u"\u2026%s%s" % (pathsep, compile_path(parents[::-1]))
-                       parents.append(parent)
-                       parent = parent.parent
+               qs = self.get_ancestors()
                
-               if root is not None and parent is None:
-                       raise AncestorDoesNotExist(root)
+               if root is not None:
+                       qs = qs.filter(level__gt=root.level)
                
-               return compile_path(parents[::-1])
+               return pathsep.join([getattr(parent, field, '?') for parent in list(qs) + [self]])
        path = property(get_path)
        
        def __unicode__(self):
@@ -439,7 +424,17 @@ class TreeModel(models.Model):
                abstract = True
 
 
+class TreeEntityBase(MPTTModelBase, EntityBase):
+       def __new__(meta, name, bases, attrs):
+               attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
+               cls = EntityBase.__new__(meta, name, bases, attrs)
+               
+               return meta.register(cls)
+
+
 class TreeEntity(Entity, TreeModel):
+       __metaclass__ = TreeEntityBase
+       
        @property
        def attributes(self):
                if self.parent:
index d7a8466..ef2eeb2 100644 (file)
@@ -26,7 +26,8 @@ class EmbedContext(object):
                """To return a template for an embed node, find the node's position in the stack
                and then progress up the stack until a template-defining node is found
                """
-               embeds = self.embeds[embed.content_type]
+               ct = embed.get_content_type(context)
+               embeds = self.embeds[ct]
                embeds = embeds[:embeds.index(embed)][::-1]
                for e in embeds:
                        template = e.get_template(context)
@@ -46,7 +47,7 @@ class EmbedContext(object):
                        else:
                                embed_context = context_dict[EMBED_CONTEXT_KEY]
                                # We can tell where we are in the list of embeds by which have already been rendered.
-                               embeds = embed_context.embeds[embed.content_type][:len(embed_context.rendered)][::-1]
+                               embeds = embed_context.embeds[ct][:len(embed_context.rendered)][::-1]
                                for e in embeds:
                                        template = e.get_template(context)
                                        if template:
@@ -133,7 +134,7 @@ class ConstantEmbedNode(template.Node):
                else:
                        self.template = None
        
-       def compile_instance(self, object_pk, context=None):
+       def compile_instance(self, object_pk):
                self.object_pk = object_pk
                model = self.content_type.model_class()
                try:
@@ -147,7 +148,7 @@ class ConstantEmbedNode(template.Node):
        def get_instance(self, context):
                return self.instance
        
-       def compile_template(self, template_name, context=None):
+       def compile_template(self, template_name):
                try:
                        return template.loader.get_template(template_name)
                except template.TemplateDoesNotExist:
@@ -159,35 +160,40 @@ class ConstantEmbedNode(template.Node):
        def get_template(self, context):
                return self.template
        
+       def get_content_type(self, context):
+               return self.content_type
+       
        def check_context(self, context):
                if EMBED_CONTEXT_KEY not in context.render_context:
                        context.render_context[EMBED_CONTEXT_KEY] = EmbedContext()
                embed_context = context.render_context[EMBED_CONTEXT_KEY]
                
-               
-               if self.content_type not in embed_context.embeds:
-                       embed_context.embeds[self.content_type] = [self]
-               elif self not in embed_context.embeds[self.content_type]:
-                       embed_context.embeds[self.content_type].append(self)
+               ct = self.get_content_type(context)
+               if ct not in embed_context.embeds:
+                       embed_context.embeds[ct] = [self]
+               elif self not in embed_context.embeds[ct]:
+                       embed_context.embeds[ct].append(self)
        
-       def mark_rendered(self, context):
+       def mark_rendered_for(self, context):
                context.render_context[EMBED_CONTEXT_KEY].rendered.append(self)
        
        def render(self, context):
                self.check_context(context)
                
-               if self.template is not None:
-                       if self.template is False:
+               template = self.get_template(context)
+               if template is not None:
+                       self.mark_rendered_for(context)
+                       if template is False:
                                return settings.TEMPLATE_STRING_IF_INVALID
-                       self.mark_rendered(context)
                        return ''
                
-               # Otherwise self.instance should be set. Render the instance with the appropriate template!
-               if self.instance is None or self.instance is False:
-                       self.mark_rendered(context)
+               # Otherwise an instance should be available. Render the instance with the appropriate template!
+               instance = self.get_instance(context)
+               if instance is None or instance is False:
+                       self.mark_rendered_for(context)
                        return settings.TEMPLATE_STRING_IF_INVALID
                
-               return self.render_instance(context, self.instance)
+               return self.render_instance(context, instance)
        
        def render_instance(self, context, instance):
                try:
@@ -205,7 +211,7 @@ class ConstantEmbedNode(template.Node):
                context.update(kwargs)
                t_rendered = t.render(context)
                context.pop()
-               self.mark_rendered(context)
+               self.mark_rendered_for(context)
                return t_rendered
 
 
@@ -213,11 +219,7 @@ class EmbedNode(ConstantEmbedNode):
        def __init__(self, content_type, object_pk=None, template_name=None, kwargs=None):
                assert template_name is not None or object_pk is not None
                self.content_type = content_type
-               
-               kwargs = kwargs or {}
-               for k, v in kwargs.items():
-                       kwargs[k] = v
-               self.kwargs = kwargs
+               self.kwargs = kwargs or {}
                
                if object_pk is not None:
                        self.object_pk = object_pk
@@ -232,27 +234,29 @@ class EmbedNode(ConstantEmbedNode):
                        self.template = None
        
        def get_instance(self, context):
-               return self.compile_instance(self.object_pk, context)
+               if self.object_pk is None:
+                       return None
+               return self.compile_instance(self.object_pk.resolve(context))
        
        def get_template(self, context):
-               return self.compile_template(self.template_name, context)
+               if self.template_name is None:
+                       return None
+               return self.compile_template(self.template_name.resolve(context))
+
+
+class InstanceEmbedNode(EmbedNode):
+       def __init__(self, instance, kwargs=None):
+               self.instance = instance
+               self.kwargs = kwargs or {}
        
-       def render(self, context):
-               self.check_context(context)
-               
-               if self.template_name is not None:
-                       self.mark_rendered(context)
-                       return ''
-               
-               if self.object_pk is None:
-                       if settings.TEMPLATE_DEBUG:
-                               raise ValueError("NoneType is not a valid object_pk value")
-                       self.mark_rendered(context)
-                       return settings.TEMPLATE_STRING_IF_INVALID
-               
-               instance = self.compile_instance(self.object_pk.resolve(context))
-               
-               return self.render_instance(context, instance)
+       def get_template(self, context):
+               return None
+       
+       def get_instance(self, context):
+               return self.instance.resolve(context)
+       
+       def get_content_type(self, context):
+               return ContentType.objects.get_for_model(self.get_instance(context))
 
 
 def get_embedded(self):
@@ -262,51 +266,63 @@ def get_embedded(self):
 setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded))
 
 
+def get_content_type(bit):
+       try:
+               app_label, model = bit.split('.')
+       except ValueError:
+               raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tag)
+       try:
+               ct = ContentType.objects.get(app_label=app_label, model=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)' % tag)
+       return ct
+
+
 def do_embed(parser, token):
        """
        The {% embed %} tag can be used in two ways:
        {% embed <app_label>.<model_name> with <template> %} :: Sets which template will be used to render a particular model.
-       {% embed <app_label>.<model_name> <object_pk> [<argname>=<value> ...]%} :: Embeds the instance specified by the given parameters in the document with the previously-specified template. Any kwargs provided will be passed into the context of the template.
+       {% embed (<app_label>.<model_name> <object_pk> || <instance>) [<argname>=<value> ...] %} :: Embeds the instance specified by the given parameters in the document with the previously-specified template. Any kwargs provided will be passed into the context of the template.
        """
-       args = token.split_contents()
-       tag = args[0]
+       bits = token.split_contents()
+       tag = bits.pop(0)
        
-       if len(args) < 2:
-               raise template.TemplateSyntaxError('"%s" template tag must have at least three arguments.' % tag)
-       else:
-               if '.' not in args[1]:
-                       raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tag)
-               
-               app_label, model = args[1].split('.')
-               try:
-                       ct = ContentType.objects.get(app_label=app_label, model=model)
-               except ContentType.DoesNotExist:
-                       raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tag)
-               
-               if args[2] == "with":
-                       if len(args) > 4:
-                               raise template.TemplateSyntaxError('"%s" template tag may have no more than four arguments.' % tag)
-                       
-                       if args[3][0] in ['"', "'"] and args[3][0] == args[3][-1]:
-                               return ConstantEmbedNode(ct, template_name=args[3])
-                       
-                       return EmbedNode(ct, template_name=args[3])
+       if len(bits) < 1:
+               raise template.TemplateSyntaxError('"%s" template tag must have at least two arguments.' % tag)
+       
+       if len(bits) == 3 and bits[-2] == 'with':
+               ct = get_content_type(bits[0])
                
-               object_pk = args[2]
-               remaining_args = args[3:]
-               kwargs = {}
-               for arg in remaining_args:
-                       if '=' not in arg:
-                               raise template.TemplateSyntaxError("Invalid keyword argument for '%s' template tag: %s" % (tag, arg))
-                       k, v = arg.split('=')
+               if bits[2][0] in ['"', "'"] and bits[2][0] == bits[2][-1]:
+                       return ConstantEmbedNode(ct, template_name=bits[2])
+               return EmbedNode(ct, template_name=bits[2])
+       
+       # Otherwise they're trying to embed a certain instance.
+       kwargs = {}
+       try:
+               bit = bits.pop()
+               while '=' in bit:
+                       k, v = bit.split('=')
                        kwargs[k] = parser.compile_filter(v)
-               
-               try:
-                       int(object_pk)
-               except ValueError:
-                       return EmbedNode(ct, object_pk=parser.compile_filter(object_pk), kwargs=kwargs)
-               else:
-                       return ConstantEmbedNode(ct, object_pk=object_pk, kwargs=kwargs)
+                       bit = bits.pop()
+               bits.append(bit)
+       except IndexError:
+               raise template.TemplateSyntaxError('"%s" template tag expects at least one non-keyword argument when embedding instances.')
+       
+       if len(bits) == 1:
+               instance = parser.compile_filter(bits[0])
+               return InstanceEmbedNode(instance, kwargs)
+       elif len(bits) > 2:
+               raise template.TemplateSyntaxError('"%s" template tag expects at most 2 non-keyword arguments when embedding instances.')
+       ct = get_content_type(bits[0])
+       pk = bits[1]
+       
+       try:
+               int(pk)
+       except ValueError:
+               return EmbedNode(ct, object_pk=parser.compile_filter(pk), kwargs=kwargs)
+       else:
+               return ConstantEmbedNode(ct, object_pk=pk, kwargs=kwargs)
 
 
 register.tag('embed', do_embed)
\ No newline at end of file
diff --git a/templatetags/include_string.py b/templatetags/include_string.py
new file mode 100644 (file)
index 0000000..260dcff
--- /dev/null
@@ -0,0 +1,38 @@
+from django import template
+from django.conf import settings
+
+
+register = template.Library()
+
+
+class IncludeStringNode(template.Node):
+       """The passed variable is expected to be a string of template code to be rendered with
+       the current context."""
+       def __init__(self, string):
+               self.string = string
+       
+       def render(self, context):
+               try:
+                       t = template.Template(self.string.resolve(context))
+                       return t.render(context)
+               except template.TemplateSyntaxError:
+                       if settings.TEMPLATE_DEBUG:
+                               raise
+                       return settings.TEMPLATE_STRING_IF_INVALID
+               except:
+                       return settings.TEMPLATE_STRING_IF_INVALID
+
+
+def do_include_string(parser, token):
+       """
+       Include a flat string by interpreting it as a template.
+       {% include_string <template_code> %}
+       """
+       bits = token.split_contents()
+       if len(bits) != 2:
+               raise TemplateSyntaxError("%r tag takes one argument: the template string to be included" % bits[0])
+       string = parser.compile_filter(bits[1])
+       return IncludeStringNode(string)
+
+
+register.tag('include_string', do_include_string)
\ No newline at end of file
index b79534f..b0f40d5 100644 (file)
--- a/tests.py
+++ b/tests.py
@@ -1,6 +1,7 @@
 from django.test import TestCase
 from django import template
 from django.conf import settings
+from django.db import connection
 from django.template import loader
 from django.template.loaders import cached
 from philo.exceptions import AncestorDoesNotExist
@@ -78,6 +79,7 @@ class TemplateTestCase(TestCase):
                        'simple02': ('{% extends "simple01" %}', {}, '%sSimple' % blog.title),
                        'simple03': ('{% embed penfield.blog with "embed000" %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
                        'simple04': ('{% embed penfield.blog 1 %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
+                       'simple05': ('{% embed penfield.blog with "embed01" %}{% embed blog %}', {'blog': blog}, blog.title),
                        
                        # Kwargs
                        'kwargs01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1="hi" var2=lo %}', {'lo': 'lo'}, '%shilo' % blog.title),
@@ -109,25 +111,25 @@ class NodeURLTestCase(TestCase):
                        command.handle(all_apps=True)
                
                self.templates = [
-                               ("{% node_url %}", "/root/never/"),
-                               ("{% node_url for node2 %}", "/root/blog/"),
-                               ("{% node_url as hello %}<p>{{ hello|slice:'1:' }}</p>", "<p>root/never/</p>"),
-                               ("{% node_url for nodes|first %}", "/root/never/"),
+                               ("{% node_url %}", "/root/second/"),
+                               ("{% node_url for node2 %}", "/root/second2/"),
+                               ("{% node_url as hello %}<p>{{ hello|slice:'1:' }}</p>", "<p>root/second/</p>"),
+                               ("{% node_url for nodes|first %}", "/root/"),
                                ("{% node_url with entry %}", settings.TEMPLATE_STRING_IF_INVALID),
-                               ("{% node_url with entry for node2 %}", "/root/blog/2010/10/20/first-entry"),
-                               ("{% node_url with tag for node2 %}", "/root/blog/tags/test-tag/"),
-                               ("{% node_url with date for node2 %}", "/root/blog/2010/10/20"),
-                               ("{% node_url entries_by_day year=date|date:'Y' month=date|date:'m' day=date|date:'d' for node2 as goodbye %}<em>{{ goodbye|upper }}</em>", "<em>/ROOT/BLOG/2010/10/20</em>"),
-                               ("{% node_url entries_by_month year=date|date:'Y' month=date|date:'m' for node2 %}", "/root/blog/2010/10"),
-                               ("{% node_url entries_by_year year=date|date:'Y' for node2 %}", "/root/blog/2010/"),
+                               ("{% node_url with entry for node2 %}", "/root/second2/2010/10/20/first-entry"),
+                               ("{% node_url with tag for node2 %}", "/root/second2/tags/test-tag/"),
+                               ("{% node_url with date for node2 %}", "/root/second2/2010/10/20"),
+                               ("{% node_url entries_by_day year=date|date:'Y' month=date|date:'m' day=date|date:'d' for node2 as goodbye %}<em>{{ goodbye|upper }}</em>", "<em>/ROOT/SECOND2/2010/10/20</em>"),
+                               ("{% node_url entries_by_month year=date|date:'Y' month=date|date:'m' for node2 %}", "/root/second2/2010/10"),
+                               ("{% node_url entries_by_year year=date|date:'Y' for node2 %}", "/root/second2/2010/"),
                ]
                
                nodes = Node.objects.all()
                blog = Blog.objects.all()[0]
                
                self.context = template.Context({
-                       'node': nodes[0],
-                       'node2': nodes[1],
+                       'node': nodes.get(slug='second'),
+                       'node2': nodes.get(slug='second2'),
                        'nodes': nodes,
                        'entry': BlogEntry.objects.all()[0],
                        'tag': blog.entry_tags.all()[0],
@@ -148,52 +150,71 @@ class TreePathTestCase(TestCase):
                        command = Command()
                        command.handle(all_apps=True)
        
-       def test_has_ancestor(self):
+       def assertQueryLimit(self, max, expected_result, *args, **kwargs):
+               # As a rough measure of efficiency, limit the number of queries required for a given operation.
+               settings.DEBUG = True
+               call = kwargs.pop('callable', Node.objects.get_with_path)
+               try:
+                       queries = len(connection.queries)
+                       if isinstance(expected_result, type) and issubclass(expected_result, Exception):
+                               self.assertRaises(expected_result, call, *args, **kwargs)
+                       else:
+                               self.assertEqual(call(*args, **kwargs), expected_result)
+                       queries = len(connection.queries) - queries
+                       if queries > max:
+                               raise AssertionError('"%d" unexpectedly not less than or equal to "%s"' % (queries, max))
+               finally:
+                       settings.DEBUG = False
+       
+       def test_get_with_path(self):
                root = Node.objects.get(slug='root')
                third = Node.objects.get(slug='third')
-               r1 = Node.objects.get(slug='recursive1')
-               r2 = Node.objects.get(slug='recursive2')
-               pr1 = Node.objects.get(slug='postrecursive1')
-               
-               # Simple case: straight path
-               self.assertEqual(third.has_ancestor(root), True)
-               self.assertEqual(root.has_ancestor(root), False)
-               self.assertEqual(root.has_ancestor(None), True)
-               self.assertEqual(third.has_ancestor(None), True)
-               self.assertEqual(root.has_ancestor(root, inclusive=True), True)
-               
-               # Recursive case
-               self.assertEqual(r1.has_ancestor(r1), True)
-               self.assertEqual(r1.has_ancestor(r2), True)
-               self.assertEqual(r2.has_ancestor(r1), True)
-               self.assertEqual(r2.has_ancestor(None), False)
-               
-               # Post-recursive case
-               self.assertEqual(pr1.has_ancestor(r1), True)
-               self.assertEqual(pr1.has_ancestor(pr1), False)
-               self.assertEqual(pr1.has_ancestor(pr1, inclusive=True), True)
-               self.assertEqual(pr1.has_ancestor(None), False)
-               self.assertEqual(pr1.has_ancestor(root), False)
+               second2 = Node.objects.get(slug='second2')
+               fifth = Node.objects.get(slug='fifth')
+               e = Node.DoesNotExist
+               
+               # Empty segments
+               self.assertQueryLimit(0, root, '', root=root)
+               self.assertQueryLimit(0, e, '')
+               self.assertQueryLimit(0, (root, None), '', root=root, absolute_result=False)
+               
+               # Absolute result
+               self.assertQueryLimit(1, third, 'root/second/third')
+               self.assertQueryLimit(1, third, 'second/third', root=root)
+               self.assertQueryLimit(1, third, 'root//////second/third///')
+               
+               self.assertQueryLimit(1, e, 'root/secont/third')
+               self.assertQueryLimit(1, e, 'second/third')
+               
+               # Non-absolute result (binary search)
+               self.assertQueryLimit(2, (second2, 'sub/path/tail'), 'root/second2/sub/path/tail', absolute_result=False)
+               self.assertQueryLimit(3, (second2, 'sub/'), 'root/second2/sub/', absolute_result=False)
+               self.assertQueryLimit(2, e, 'invalid/path/1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/7/8/9/0', absolute_result=False)
+               self.assertQueryLimit(1, (root, None), 'root', absolute_result=False)
+               self.assertQueryLimit(2, (second2, None), 'root/second2', absolute_result=False)
+               self.assertQueryLimit(3, (third, None), 'root/second/third', absolute_result=False)
+               
+               # with root != None
+               self.assertQueryLimit(1, (second2, None), 'second2', root=root, absolute_result=False)
+               self.assertQueryLimit(2, (third, None), 'second/third', root=root, absolute_result=False)
+               
+               # Preserve trailing slash
+               self.assertQueryLimit(2, (second2, 'sub/path/tail/'), 'root/second2/sub/path/tail/', absolute_result=False)
+               
+               # Speed increase for leaf nodes - should this be tested?
+               self.assertQueryLimit(1, (fifth, 'sub/path/tail/len/five'), 'root/second/third/fourth/fifth/sub/path/tail/len/five', absolute_result=False)
        
        def test_get_path(self):
                root = Node.objects.get(slug='root')
+               root2 = Node.objects.get(slug='root')
                third = Node.objects.get(slug='third')
-               r1 = Node.objects.get(slug='recursive1')
-               r2 = Node.objects.get(slug='recursive2')
-               pr1 = Node.objects.get(slug='postrecursive1')
-               
-               # Simple case: straight path to None
-               self.assertEqual(root.get_path(), 'root')
-               self.assertEqual(third.get_path(), 'root/never/more/second/third')
-               
-               # Recursive case: Looped path to root None
-               self.assertEqual(r1.get_path(), u'\u2026/recursive1/recursive2/recursive3/recursive1')
-               self.assertEqual(pr1.get_path(), u'\u2026/recursive3/recursive1/recursive2/recursive3/postrecursive1')
-               
-               # Simple error case: straight invalid path
-               self.assertRaises(AncestorDoesNotExist, root.get_path, root=third)
-               self.assertRaises(AncestorDoesNotExist, third.get_path, root=pr1)
-               
-               # Recursive error case
-               self.assertRaises(AncestorDoesNotExist, r1.get_path, root=root)
-               self.assertRaises(AncestorDoesNotExist, pr1.get_path, root=third)
\ No newline at end of file
+               second2 = Node.objects.get(slug='second2')
+               fifth = Node.objects.get(slug='fifth')
+               e = AncestorDoesNotExist
+               
+               self.assertQueryLimit(0, 'root', callable=root.get_path)
+               self.assertQueryLimit(0, '', root2, callable=root.get_path)
+               self.assertQueryLimit(1, 'root/second/third', callable=third.get_path)
+               self.assertQueryLimit(1, 'second/third', root, callable=third.get_path)
+               self.assertQueryLimit(1, e, third, callable=second2.get_path)
+               self.assertQueryLimit(1, '? - ?', root, ' - ', 'title', callable=third.get_path)