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:
"""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)
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:
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:
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:
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:
context.update(kwargs)
t_rendered = t.render(context)
context.pop()
- self.mark_rendered(context)
+ self.mark_rendered_for(context)
return t_rendered
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
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):
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
--- /dev/null
+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
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
'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),
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)