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',)
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",)}
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
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
inlines = [ContentletInline, ContentReferenceInline] + ViewAdmin.inlines
-class TemplateAdmin(admin.ModelAdmin):
+class TemplateAdmin(TreeAdmin):
prepopulated_fields = {'slug': ('name',)}
fieldsets = (
(None, {
[
- {
- "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"
}
},
--- /dev/null
+# 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']
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):
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.
# 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)
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):
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):
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:
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
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],
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)