Merge branch 'master' of git://github.com/melinath/philo
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Wed, 24 Nov 2010 02:28:28 +0000 (21:28 -0500)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Wed, 24 Nov 2010 02:28:28 +0000 (21:28 -0500)
* 'master' of git://github.com/melinath/philo:
  Enabled Embed rule inheritance from parent templates and to included templates with a bit of overriding of EmbedNode methods and use of context.render_context similar to BlockNodes. Removed {% embed as <varname> %} syntax as obsolete. Added tests for {% embed %} tags in various situations.
  Switched template loading from Template.loader to philo.loaders.database.Loader in keeping with Django's recommended way of implementing loaders. Note that the origin of the template is considered to be the actual instance instead of a path, as this seems more strictly accurate. The instance is converted to unicode by the loader to more correctly fit the expected "display_name". Removed origin and django_template properties from templates. Corrected a minor error in the node_url templatetag for contexts not containing 'node'.
  Switched node_view conditions for raising MIDDLEWARE_NOT_CONFIGURED to actually check whether the middleware is configured. This allows errors in request.node evaluation to propogate and be more easily debugged.
  Added get_branch support to TreeManager.
  Added support for recursive trees - i.e. recursion checks to prevent infinite loops. Improved efficiency of TreeModel.get_path method by removing has_ancestor check. Shortened and clarified NodeURLTestCase; added test case for TreeModel paths and ancestor checks.
  Increased specificity of penfield get_article conditions for 404 raising. Corrected get_with_path handling of trailing pathseps to re-add them to the path's remainder.
  Customized FilterSelectMultiple widget for Tags to allow one-click adding of tags not found with normal filtering. Addresses feature #9 for the built-in admin interface.
  Improved general efficiency of TreeManager's get_with_path method in terms of number of queries by implementing a halving algorithm. Can require more queries than the previous algorithm in cases where the remainder following the deepest node is relatively long.
  Shunted responsibility for 404 and 500 error catching from node_view to RequestNodeMiddleware. Made minor corrections to ManyToMany values and descriptors.
  Minor correction to Waldo logout view to fall back to the node's absolute_url if HTTP_REFERER is not found in request.META
  Tweaked Entity and TreeEntity to pass an actual queryset to QuerySetMapper instead of a manager. The results are generally the same, and anyone accessing querysetmapper.queryset will be expecting to find a queryset.
  Switched Attribute ManyToManyValues to have a ManyToManyField to ForeignKeyValues instead of a CommaSeparatedIntegerField. This allows joined queries and referential integrity. In the long run, it also simplifies adding support for non-integer primary keys.
  Cleaned up ManyToManyValue set_value method. Also tweaked container template tag to recognize a contentlet as dynamic if it only contains a variable.

20 files changed:
__init__.py
admin/base.py
admin/widgets.py
contrib/penfield/admin.py
contrib/penfield/models.py
contrib/waldo/models.py
fixtures/test_fixtures.json
loaders/__init__.py [new file with mode: 0644]
loaders/database.py [new file with mode: 0644]
media/admin/js/TagCreation.js [new file with mode: 0644]
middleware.py
migrations/0008_auto__del_field_manytomanyvalue_object_ids.py [new file with mode: 0644]
models/base.py
models/fields.py
models/pages.py
templatetags/containers.py
templatetags/embed.py
templatetags/nodes.py
tests.py
views.py

index 52956f3..ba78dda 100644 (file)
@@ -1,4 +1,15 @@
-from philo.models.pages import Template
+from philo.loaders.database import Loader
 
 
-load_template_source = Template.loader
+_loader = Loader()
+
+
+def load_template_source(template_name, template_dirs=None):
+    # For backwards compatibility
+    import warnings
+    warnings.warn(
+        "'philo.load_template_source' is deprecated; use 'philo.loaders.database.Loader' instead.",
+        PendingDeprecationWarning
+    )
+    return _loader.load_template_source(template_name, template_dirs)
+load_template_source.is_usable = True
index cb814b7..0413dde 100644 (file)
@@ -1,8 +1,12 @@
 from django.conf import settings
 from django.contrib import admin
 from django.contrib.contenttypes import generic
+from django.http import HttpResponse
+from django.utils import simplejson as json
+from django.utils.html import escape
 from philo.models import Tag, Attribute
 from philo.forms import AttributeForm, AttributeInlineFormSet
+from philo.admin.widgets import TagFilteredSelectMultiple
 
 
 COLLAPSE_CLASSES = ('collapse', 'collapse-closed', 'closed',)
@@ -33,5 +37,31 @@ class TagAdmin(admin.ModelAdmin):
        list_display = ('name', 'slug')
        prepopulated_fields = {"slug": ("name",)}
        search_fields = ["name"]
+       
+       def response_add(self, request, obj, post_url_continue='../%s/'):
+               # If it's an ajax request, return a json response containing the necessary information.
+               if request.is_ajax():
+                       return HttpResponse(json.dumps({'pk': escape(obj._get_pk_val()), 'unicode': escape(obj)}))
+               return super(TagAdmin, self).response_add(request, obj, post_url_continue)
+
+
+class AddTagAdmin(admin.ModelAdmin):
+       def formfield_for_manytomany(self, db_field, request=None, **kwargs):
+               """
+               Get a form Field for a ManyToManyField.
+               """
+               # If it uses an intermediary model that isn't auto created, don't show
+               # a field in admin.
+               if not db_field.rel.through._meta.auto_created:
+                       return None
+               
+               if db_field.rel.to == Tag and db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
+                       opts = Tag._meta
+                       if request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()):
+                               kwargs['widget'] = TagFilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
+                               return db_field.formfield(**kwargs)
+               
+               return super(AddTagAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
+
 
 admin.site.register(Tag, TagAdmin)
\ No newline at end of file
index f8799fe..7a47c63 100644 (file)
@@ -1,5 +1,6 @@
 from django import forms
 from django.conf import settings
+from django.contrib.admin.widgets import FilteredSelectMultiple
 from django.utils.translation import ugettext as _
 from django.utils.safestring import mark_safe
 from django.utils.text import truncate_words
@@ -30,4 +31,35 @@ class ModelLookupWidget(forms.TextInput):
                                output += '&nbsp;<strong>%s</strong>' % escape(truncate_words(value_object, 14))
                        except value_class.DoesNotExist:
                                pass
-               return mark_safe(output)
\ No newline at end of file
+               return mark_safe(output)
+
+
+class TagFilteredSelectMultiple(FilteredSelectMultiple):
+       """
+       A SelectMultiple with a JavaScript filter interface.
+
+       Note that the resulting JavaScript assumes that the jsi18n
+       catalog has been loaded in the page
+       """
+       class Media:
+               js = (settings.ADMIN_MEDIA_PREFIX + "js/core.js",
+                         settings.ADMIN_MEDIA_PREFIX + "js/SelectBox.js",
+                         settings.ADMIN_MEDIA_PREFIX + "js/SelectFilter2.js")
+               
+               if 'staticmedia' in settings.INSTALLED_APPS:
+                       import staticmedia
+                       js += (staticmedia.url('admin/js/TagCreation.js'),)
+               else:
+                       js += (settings.ADMIN_MEDIA_PREFIX + "js/TagCreation.js",)
+
+       def render(self, name, value, attrs=None, choices=()):
+               if attrs is None: attrs = {}
+               attrs['class'] = 'selectfilter'
+               if self.is_stacked: attrs['class'] += 'stacked'
+               output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
+               output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
+               # TODO: "id_" is hard-coded here. This should instead use the correct
+               # API to determine the ID dynamically.
+               output.append(u'SelectFilter.init("id_%s", "%s", %s, "%s"); tagCreation.init("id_%s"); });</script>\n' % \
+                       (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX, name))
+               return mark_safe(u''.join(output))
\ No newline at end of file
index 85888aa..5faf4ef 100644 (file)
@@ -1,5 +1,5 @@
 from django.contrib import admin
-from philo.admin import EntityAdmin
+from philo.admin import EntityAdmin, AddTagAdmin
 from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView
 
 
@@ -12,7 +12,7 @@ class BlogAdmin(TitledAdmin):
        pass
 
 
-class BlogEntryAdmin(TitledAdmin):
+class BlogEntryAdmin(TitledAdmin, AddTagAdmin):
        filter_horizontal = ['tags']
 
 
@@ -24,8 +24,8 @@ class NewsletterAdmin(TitledAdmin):
        pass
 
 
-class NewsletterArticleAdmin(TitledAdmin):
-       pass
+class NewsletterArticleAdmin(TitledAdmin, AddTagAdmin):
+       filter_horizontal = TitledAdmin.filter_horizontal + ('tags', 'authors')
 
 
 class NewsletterIssueAdmin(TitledAdmin):
index 6550f8a..8248340 100644 (file)
@@ -387,7 +387,7 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
        def get_articles_by_issue(self, request, numbering, extra_context=None):
                try:
                        issue = self.newsletter.issues.get(numbering=numbering)
-               except:
+               except NewsletterIssue.DoesNotExist:
                        raise Http404
                context = extra_context or {}
                context.update({'issue': issue})
@@ -403,7 +403,7 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
                        articles = articles.filter(date__day=day)
                try:
                        article = articles.get(slug=slug)
-               except:
+               except NewsletterArticle.DoesNotExist:
                        raise Http404
                context = self.get_context()
                context.update(extra_context or {})
index e2d70ae..f1ee05a 100644 (file)
@@ -164,7 +164,7 @@ class LoginMultiView(MultiView):
        login = never_cache(login)
        
        def logout(self, request):
-               return auth_views.logout(request, request.META['HTTP_REFERER'])
+               return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
        
        def login_required(self, view):
                def inner(request, *args, **kwargs):
index 18f6962..14f5a27 100644 (file)
             ]
         }
     }, 
+    {
+        "pk": 4, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "more", 
+            "parent": 1, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ]
+        }
+    }, 
+    {
+        "pk": 5, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "second", 
+            "parent": 4, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ]
+        }
+    }, 
+    {
+        "pk": 6, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "third", 
+            "parent": 5, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ]
+        }
+    }, 
+    {
+        "pk": 7, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "recursive1", 
+            "parent": 9, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ]
+        }
+    }, 
+    {
+        "pk": 8, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "recursive2", 
+            "parent": 7, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ]
+        }
+    }, 
+    {
+        "pk": 9, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "recursive3", 
+            "parent": 8, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ]
+        }
+    }, 
+    {
+        "pk": 10, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "postrecursive1", 
+            "parent": 9, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ]
+        }
+    }, 
     {
         "pk": 1, 
         "model": "philo.redirect", 
diff --git a/loaders/__init__.py b/loaders/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/loaders/database.py b/loaders/database.py
new file mode 100644 (file)
index 0000000..141aedd
--- /dev/null
@@ -0,0 +1,15 @@
+from django.template import TemplateDoesNotExist
+from django.template.loader import BaseLoader
+from django.utils.encoding import smart_unicode
+from philo.models import Template
+
+
+class Loader(BaseLoader):
+       is_usable=True
+       
+       def load_template_source(self, template_name, template_dirs=None):
+               try:
+                       template = Template.objects.get_with_path(template_name)
+               except Template.DoesNotExist:
+                       raise TemplateDoesNotExist(template_name)
+               return (template.code, smart_unicode(template))
\ No newline at end of file
diff --git a/media/admin/js/TagCreation.js b/media/admin/js/TagCreation.js
new file mode 100644 (file)
index 0000000..31f2910
--- /dev/null
@@ -0,0 +1,78 @@
+var tagCreation = window.tagCreation;
+
+(function($) {
+       tagCreation = {
+               'cache': {},
+               'addTagFromSlug': function(triggeringLink) {
+                       var id = triggeringLink.id.replace(/^ajax_add_/, '') + '_input';
+                       var slug = document.getElementById(id).value;
+       
+                       var name = slug.split(' ');
+                       for(var i=0;i<name.length;i++) {
+                               name[i] = name[i].substr(0,1).toUpperCase() + name[i].substr(1);
+                       }
+                       name = name.join(' ');
+                       slug = name.toLowerCase().replace(/ /g, '-').replace(/[^\w-]/g, '');
+       
+                       var href = triggeringLink.href;
+                       var data = {
+                               'name': name,
+                               'slug': slug
+                       };
+                       $.post(href, data, function(data){
+                               newId = html_unescape(data.pk);
+                               newRepr = html_unescape(data.unicode);
+                               var toId = id.replace(/_input$/, '_to');
+                               elem = document.getElementById(toId);
+                               var o = new Option(newRepr, newId);
+                               SelectBox.add_to_cache(toId, o);
+                               SelectBox.redisplay(toId);
+                       }, "json")
+               },
+               'init': function(id) {
+                       tagCreation.cache[id] = {}
+                       var input = tagCreation.cache[id].input = document.getElementById(id + '_input');
+                       var select = tagCreation.cache[id].select = document.getElementById(id + '_from');
+                       var addLinkTemplate = document.getElementById('add_' + input.id.replace(/_input$/, '')).cloneNode(true);
+                       var addLink = tagCreation.cache[id].addLink = document.createElement('A');
+                       addLink.id = 'ajax_add_' + id;
+                       addLink.className = addLinkTemplate.className;
+                       addLink.href = addLinkTemplate.href;
+                       addLink.appendChild($(addLinkTemplate).children()[0].cloneNode(false));
+                       addLink.innerHTML += " <span style='vertical-align:text-top;'>Add this tag</span>"
+                       addLink.style.marginLeft = "20px";
+                       addLink.style.display = "block";
+                       addLink.style.backgroundPosition = "10px 5px";
+                       addLink.style.width = "120px";
+                       $(input).after(addLink);
+                       if (window.grappelli) {
+                               addLink.parentNode.style.backgroundPosition = "6px 8px";
+                       } else {
+                               addLink.style.marginTop = "5px";
+                       }
+                       tagCreation.toggleButton(id);
+                       addEvent(input, 'keyup', function() {
+                               tagCreation.toggleButton(id);
+                       })
+                       addEvent(addLink, 'click', function(e) {
+                               e.preventDefault();
+                               tagCreation.addTagFromSlug(addLink);
+                       })
+               },
+               'toggleButton': function(id) {
+                       var addLink = tagCreation.cache[id].addLink;
+                       var select = $(tagCreation.cache[id].select);
+                       if (select[0].options.length == 0) {
+                               if (addLink.style.display == 'none') {
+                                       addLink.style.display = 'block';
+                                       select.height(select.height() - $(addLink).outerHeight(false))
+                               }
+                       } else {
+                               if (addLink.style.display == 'block') {
+                                       select[0].style.height = null;
+                                       addLink.style.display = 'none';
+                               }
+                       }
+               }
+       }
+}(django.jQuery))
\ No newline at end of file
index 8c208e4..ad660ec 100644 (file)
@@ -1,5 +1,7 @@
+from django.conf import settings
 from django.contrib.sites.models import Site
-from philo.models import Node
+from django.http import Http404
+from philo.models import Node, View
 
 
 class LazyNode(object):
@@ -32,4 +34,20 @@ class RequestNodeMiddleware(object):
                request.__class__.node = LazyNode()
        
        def process_view(self, request, view_func, view_args, view_kwargs):
-               request._cached_node_path = view_kwargs.get('path', '/')
\ No newline at end of file
+               request._cached_node_path = view_kwargs.get('path', '/')
+       
+       def process_exception(self, request, exception):
+               if settings.DEBUG or not hasattr(request, 'node') or not request.node:
+                       return
+               
+               if isinstance(exception, Http404):
+                       error_view = request.node.attributes.get('Http404', None)
+               else:
+                       error_view = request.node.attributes.get('Http500', None)
+               
+               if error_view is None or not isinstance(error_view, View):
+                       # Should this be duck-typing? Perhaps even no testing?
+                       return
+               
+               extra_context = {'exception': exception}
+               return error_view.render_to_response(request, extra_context)
\ No newline at end of file
diff --git a/migrations/0008_auto__del_field_manytomanyvalue_object_ids.py b/migrations/0008_auto__del_field_manytomanyvalue_object_ids.py
new file mode 100644 (file)
index 0000000..db0451d
--- /dev/null
@@ -0,0 +1,139 @@
+# 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):
+        
+        # Deleting field 'ManyToManyValue.object_ids'
+        db.delete_column('philo_manytomanyvalue', 'object_ids')
+
+        # Adding M2M table for field values on 'ManyToManyValue'
+        db.create_table('philo_manytomanyvalue_values', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('manytomanyvalue', models.ForeignKey(orm['philo.manytomanyvalue'], null=False)),
+            ('foreignkeyvalue', models.ForeignKey(orm['philo.foreignkeyvalue'], null=False))
+        ))
+        db.create_unique('philo_manytomanyvalue_values', ['manytomanyvalue_id', 'foreignkeyvalue_id'])
+
+
+    def backwards(self, orm):
+        
+        # Adding field 'ManyToManyValue.object_ids'
+        db.add_column('philo_manytomanyvalue', 'object_ids', self.gf('django.db.models.fields.CommaSeparatedIntegerField')(max_length=300, null=True, blank=True), keep_default=False)
+
+        # Removing M2M table for field values on 'ManyToManyValue'
+        db.delete_table('philo_manytomanyvalue_values')
+
+
+    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', [], {'blank': 'True', 'related_name': "'foreign_key_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            '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', [], {'blank': 'True', 'related_name': "'many_to_many_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            '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'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', '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'}),
+            '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']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['philo']
index 6f881a8..03b9b54 100644 (file)
@@ -46,7 +46,12 @@ def unregister_value_model(model):
 
 
 class AttributeValue(models.Model):
-       attribute = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
+       attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
+       
+       @property
+       def attribute(self):
+               return self.attribute_set.all()[0]
+       
        def apply_data(self, data):
                raise NotImplementedError
        
@@ -81,7 +86,7 @@ class JSONValue(AttributeValue):
 
 
 class ForeignKeyValue(AttributeValue):
-       content_type = models.ForeignKey(ContentType, related_name='foreign_key_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+       content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
        object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
        value = generic.GenericForeignKey()
        
@@ -104,15 +109,14 @@ class ForeignKeyValue(AttributeValue):
 
 
 class ManyToManyValue(AttributeValue):
-       # TODO: Change object_ids to object_pks.
-       content_type = models.ForeignKey(ContentType, related_name='many_to_many_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
-       object_ids = models.CommaSeparatedIntegerField(max_length=300, verbose_name='Value IDs', null=True, blank=True)
+       content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+       values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
        
        def get_object_id_list(self):
-               if not self.object_ids:
+               if not self.values.count():
                        return []
                else:
-                       return self.object_ids.split(',')
+                       return self.values.values_list('object_id', flat=True)
        
        def get_value(self):
                if self.content_type is None:
@@ -121,13 +125,28 @@ class ManyToManyValue(AttributeValue):
                return self.content_type.model_class()._default_manager.filter(id__in=self.get_object_id_list())
        
        def set_value(self, value):
-               if value is None:
-                       self.object_ids = ""
-                       return
-               if not isinstance(value, models.query.QuerySet):
-                       raise TypeError("Value must be a QuerySet.")
-               self.content_type = ContentType.objects.get_for_model(value.model)
-               self.object_ids = ','.join([`value` for value in value.values_list('id', flat=True)])
+               # Value is probably a queryset - but allow any iterable.
+               
+               # These lines shouldn't be necessary; however, if value is an EmptyQuerySet,
+               # the code (specifically the object_id__in query) won't work without them. Unclear why...
+               if not value:
+                       value = []
+               
+               # Before we can fiddle with the many-to-many to foreignkeyvalues, we need
+               # a pk.
+               if self.pk is None:
+                       self.save()
+               
+               if isinstance(value, models.query.QuerySet):
+                       value = value.values_list('id', flat=True)
+               
+               self.values.filter(~models.Q(object_id__in=value)).delete()
+               current = self.get_object_id_list()
+               
+               for v in value:
+                       if v in current:
+                               continue
+                       self.values.create(content_type=self.content_type, object_id=v)
        
        value = property(get_value, set_value)
        
@@ -143,7 +162,7 @@ class ManyToManyValue(AttributeValue):
                else:
                        self.content_type = cleaned_data.get('content_type', None)
                        # If there is no value set in the cleaned data, clear the stored value.
-                       self.object_ids = ""
+                       self.value = []
        
        class Meta:
                app_label = 'philo'
@@ -220,7 +239,7 @@ class Entity(models.Model):
        
        @property
        def attributes(self):
-               return QuerySetMapper(self.attribute_set)
+               return QuerySetMapper(self.attribute_set.all())
        
        @property
        def _added_attribute_registry(self):
@@ -263,34 +282,105 @@ class TreeManager(models.Manager):
        def roots(self):
                return self.filter(parent__isnull=True)
        
-       def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
+       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, or None if there is no object with that 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 in the case that there is no remaining path).
+               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.
                """
-               slugs = path.split(pathsep)
-               obj = root
-               remaining_slugs = list(slugs)
-               remainder = None
-               for slug in slugs:
-                       remaining_slugs.remove(slug)
-                       if slug: # ignore blank slugs, handles for multiple consecutive pathseps
+               segments = path.split(pathsep)
+               
+               # Check for a trailing pathsep so we can restore it later.
+               trailing_pathsep = False
+               if segments[-1] == '':
+                       trailing_pathsep = True
+               
+               # Clean out blank segments. Handles multiple consecutive pathseps.
+               while True:
+                       try:
+                               segments.remove('')
+                       except ValueError:
+                               break
+               
+               # Special-case a lack of segments. No queries necessary.
+               if not segments:
+                       if root is not None:
+                               return root, None
+                       else:
+                               raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
+               
+               def make_query_kwargs(segments):
+                       kwargs = {}
+                       prefix = ""
+                       revsegs = list(segments)
+                       revsegs.reverse()
+                       
+                       for segment in revsegs:
+                               kwargs["%s%s__exact" % (prefix, field)] = segment
+                               prefix += "parent__"
+                       
+                       kwargs[prefix[:-2]] = root
+                       return kwargs
+               
+               def build_path(segments):
+                       path = pathsep.join(segments)
+                       if trailing_pathsep and segments and segments[-1] != '':
+                               path += pathsep
+                       return path
+               
+               def find_obj(segments, depth, deepest_found):
+                       try:
+                               obj = self.get(**make_query_kwargs(segments[:depth]))
+                       except self.model.DoesNotExist:
+                               if absolute_result:
+                                       raise
+                               
+                               depth = (deepest_found + depth)/2
+                               if deepest_found == 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
+                               
+                               deepest_found = depth
+                               depth = (len(segments) + depth)/2
+                               
+                               if deepest_found == depth:
+                                       return obj, build_path(segments[deepest_found:]) or None
+                               
                                try:
-                                       obj = self.get(slug__exact=slug, parent__exact=obj)
+                                       return find_obj(segments, depth, deepest_found)
                                except self.model.DoesNotExist:
-                                       if absolute_result:
-                                               obj = None
-                                       remaining_slugs.insert(0, slug)
-                                       remainder = pathsep.join(remaining_slugs)
-                                       break
-               if obj:
-                       if absolute_result:
-                               return obj
-                       else:
-                               return (obj, remainder)
-               raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
+                                       # Then the deepest one was already found.
+                                       return obj, build_path(segments[deepest_found:])
+               
+               return find_obj(segments, len(segments), 0)
 
 
 class TreeModel(models.Model):
@@ -298,24 +388,47 @@ class TreeModel(models.Model):
        parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
        slug = models.SlugField(max_length=255)
        
-       def has_ancestor(self, ancestor):
-               parent = self
+       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'):
-               if root is not None and not self.has_ancestor(root):
-                       raise AncestorDoesNotExist(root)
-               
-               path = getattr(self, field, '?')
                parent = self.parent
+               parents = [self]
+               
+               def compile_path(parents):
+                       return pathsep.join([getattr(parent, field, '?') for parent in parents])
+               
                while parent and parent != root:
-                       path = getattr(parent, field, '?') + pathsep + path
+                       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
-               return path
+               
+               if root is not None and parent is None:
+                       raise AncestorDoesNotExist(root)
+               
+               return compile_path(parents[::-1])
        path = property(get_path)
        
        def __unicode__(self):
@@ -330,7 +443,7 @@ class TreeEntity(Entity, TreeModel):
        @property
        def attributes(self):
                if self.parent:
-                       return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
+                       return QuerySetMapper(self.attribute_set.all(), passthrough=self.parent.attributes)
                return super(TreeEntity, self).attributes
        
        class Meta:
index 85c5583..3e43c0f 100644 (file)
@@ -92,7 +92,7 @@ class ForeignKeyAttributeDescriptor(AttributeFieldDescriptor):
 
 class ManyToManyAttributeDescriptor(AttributeFieldDescriptor):
        def __set__(self, instance, value):
-               if isinstance(value, models.QuerySet):
+               if isinstance(value, models.query.QuerySet):
                        if self.field in instance._removed_attribute_registry:
                                instance._removed_attribute_registry.remove(self.field)
                        instance._added_attribute_registry[self.field] = value
index fddb0ae..81b84c9 100644 (file)
@@ -21,14 +21,6 @@ class Template(TreeModel):
        mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
        code = TemplateField(secure=False, verbose_name='django template code')
        
-       @property
-       def origin(self):
-               return 'philo.models.Template: ' + self.path
-       
-       @property
-       def django_template(self):
-               return DjangoTemplate(self.code)
-       
        @property
        def containers(self):
                """
@@ -41,7 +33,7 @@ class Template(TreeModel):
                        if isinstance(node, ContainerNode):
                                nodes.append(node)
                
-               all_nodes = nodelist_crawl(self.django_template.nodelist, process_node)
+               all_nodes = nodelist_crawl(DjangoTemplate(self.code).nodelist, process_node)
                contentlet_node_names = set([node.name for node in all_nodes if not node.references])
                contentreference_node_names = []
                contentreference_node_specs = []
@@ -54,15 +46,6 @@ class Template(TreeModel):
        def __unicode__(self):
                return self.get_path(pathsep=u' â€º ', field='name')
        
-       @staticmethod
-       @fattr(is_usable=True)
-       def loader(template_name, template_dirs=None): # load_template_source
-               try:
-                       template = Template.objects.get_with_path(template_name)
-               except Template.DoesNotExist:
-                       raise TemplateDoesNotExist(template_name)
-               return (template.code, template.origin)
-       
        class Meta:
                app_label = 'philo'
 
@@ -84,13 +67,14 @@ class Page(View):
                context = {}
                context.update(extra_context or {})
                context.update({'page': self, 'attributes': self.attributes})
+               template = DjangoTemplate(self.template.code)
                if request:
                        context.update({'node': request.node, 'attributes': self.attributes_with_node(request.node)})
                        page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
-                       string = self.template.django_template.render(RequestContext(request, context))
+                       string = template.render(RequestContext(request, context))
                else:
                        page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
-                       string = self.template.django_template.render(Context(context))
+                       string = template.render(Context(context))
                page_finished_rendering_to_string.send(sender=self, string=string)
                return string
        
index 8bb0c6b..c5fd445 100644 (file)
@@ -43,7 +43,7 @@ class ContainerNode(template.Node):
                        # Otherwise it's a contentlet.
                        try:
                                contentlet = page.contentlets.get(name__exact=self.name)
-                               if '{%' in contentlet.content:
+                               if '{%' in contentlet.content or '{{' in contentlet.content:
                                        try:
                                                content = template.Template(contentlet.content, name=contentlet.name).render(context)
                                        except template.TemplateSyntaxError, error:
index 8fb240d..d7a8466 100644 (file)
 from django import template
 from django.contrib.contenttypes.models import ContentType
 from django.conf import settings
+from django.template.loader_tags import ExtendsNode, BlockContext, BLOCK_CONTEXT_KEY, TextNode, BlockNode
 from philo.utils import LOADED_TEMPLATE_ATTR
 
 
 register = template.Library()
+EMBED_CONTEXT_KEY = 'embed_context'
+
+
+class EmbedContext(object):
+       "Inspired by django.template.loader_tags.BlockContext."
+       def __init__(self):
+               self.embeds = {}
+               self.rendered = []
+       
+       def add_embeds(self, embeds):
+               for content_type, embed_list in embeds.iteritems():
+                       if content_type in self.embeds:
+                               self.embeds[content_type] = embed_list + self.embeds[content_type]
+                       else:
+                               self.embeds[content_type] = embed_list
+       
+       def get_embed_template(self, embed, context):
+               """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]
+               embeds = embeds[:embeds.index(embed)][::-1]
+               for e in embeds:
+                       template = e.get_template(context)
+                       if template:
+                               return template
+               
+               # No template was found in the current render_context - but perhaps one level up? Or more?
+               # We may be in an inclusion tag.
+               self_found = False
+               for context_dict in context.render_context.dicts[::-1]:
+                       if not self_found:
+                               if self in context_dict.values():
+                                       self_found = True
+                                       continue
+                       elif EMBED_CONTEXT_KEY not in context_dict:
+                               continue
+                       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]
+                               for e in embeds:
+                                       template = e.get_template(context)
+                                       if template:
+                                               return template
+               
+               raise IndexError
+
+
+# Override ExtendsNode render method to have it handle EmbedNodes
+# similarly to BlockNodes.
+old_extends_node_init = ExtendsNode.__init__
+
+
+def get_embed_dict(nodelist):
+       embeds = {}
+       for n in nodelist.get_nodes_by_type(ConstantEmbedNode):
+               if n.content_type not in embeds:
+                       embeds[n.content_type] = [n]
+               else:
+                       embeds[n.content_type].append(n)
+       return embeds
+
+
+def extends_node_init(self, nodelist, *args, **kwargs):
+       self.embeds = get_embed_dict(nodelist)
+       old_extends_node_init(self, nodelist, *args, **kwargs)
+
+
+def render_extends_node(self, context):
+       compiled_parent = self.get_parent(context)
+       
+       if BLOCK_CONTEXT_KEY not in context.render_context:
+               context.render_context[BLOCK_CONTEXT_KEY] = BlockContext()
+       block_context = context.render_context[BLOCK_CONTEXT_KEY]
+       
+       if EMBED_CONTEXT_KEY not in context.render_context:
+               context.render_context[EMBED_CONTEXT_KEY] = EmbedContext()
+       embed_context = context.render_context[EMBED_CONTEXT_KEY]
+       
+       # Add the block nodes from this node to the block context
+       # Do the equivalent for embed nodes
+       block_context.add_blocks(self.blocks)
+       embed_context.add_embeds(self.embeds)
+       
+       # If this block's parent doesn't have an extends node it is the root,
+       # and its block nodes also need to be added to the block context.
+       for node in compiled_parent.nodelist:
+               # The ExtendsNode has to be the first non-text node.
+               if not isinstance(node, TextNode):
+                       if not isinstance(node, ExtendsNode):
+                               blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)])
+                               block_context.add_blocks(blocks)
+                               embeds = get_embed_dict(compiled_parent.nodelist)
+                               embed_context.add_embeds(embeds)
+                       break
+
+       # Call Template._render explicitly so the parser context stays
+       # the same.
+       return compiled_parent._render(context)
+
+
+ExtendsNode.__init__ = extends_node_init
+ExtendsNode.render = render_extends_node
 
 
 class ConstantEmbedNode(template.Node):
        """Analogous to the ConstantIncludeNode, this node precompiles several variables necessary for correct rendering - namely the referenced instance or the included template."""
-       def __init__(self, content_type, varname, object_pk=None, template_name=None, kwargs=None):
+       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
-               self.varname = varname
                
                kwargs = kwargs or {}
                for k, v in kwargs.items():
-                       kwargs[k] = template.Variable(v)
+                       kwargs[k] = v
                self.kwargs = kwargs
                
                if object_pk is not None:
-                       self.compile_instance(object_pk)
+                       self.instance = self.compile_instance(object_pk)
                else:
                        self.instance = None
                
                if template_name is not None:
-                       self.compile_template(template_name[1:-1])
+                       self.template = self.compile_template(template_name[1:-1])
                else:
                        self.template = None
        
-       def compile_instance(self, object_pk):
+       def compile_instance(self, object_pk, context=None):
                self.object_pk = object_pk
                model = self.content_type.model_class()
                try:
-                       self.instance = model.objects.get(pk=object_pk)
+                       return model.objects.get(pk=object_pk)
                except model.DoesNotExist:
                        if not hasattr(self, 'object_pk') and settings.TEMPLATE_DEBUG:
                                # Then it's a constant node.
                                raise
-                       self.instance = False
+                       return False
+       
+       def get_instance(self, context):
+               return self.instance
        
-       def compile_template(self, template_name):
+       def compile_template(self, template_name, context=None):
                try:
-                       self.template = template.loader.get_template(template_name)
+                       return template.loader.get_template(template_name)
                except template.TemplateDoesNotExist:
                        if not hasattr(self, 'template_name') and settings.TEMPLATE_DEBUG:
                                # Then it's a constant node.
                                raise
-                       self.template = False
+                       return False
+       
+       def get_template(self, context):
+               return self.template
+       
+       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)
+       
+       def mark_rendered(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:
                                return settings.TEMPLATE_STRING_IF_INVALID
-                       
-                       if self.varname not in context:
-                               context[self.varname] = {}
-                       context[self.varname][self.content_type] = self.template
-                       
+                       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)
                        return settings.TEMPLATE_STRING_IF_INVALID
                
-               return self.render_template(context, self.instance)
+               return self.render_instance(context, self.instance)
        
-       def render_template(self, context, instance):
+       def render_instance(self, context, instance):
                try:
-                       t = context[self.varname][self.content_type]
-               except KeyError:
+                       t = context.render_context[EMBED_CONTEXT_KEY].get_embed_template(self, context)
+               except (KeyError, IndexError):
+                       if settings.TEMPLATE_DEBUG:
+                               raise
                        return settings.TEMPLATE_STRING_IF_INVALID
                
                context.push()
@@ -80,42 +205,54 @@ class ConstantEmbedNode(template.Node):
                context.update(kwargs)
                t_rendered = t.render(context)
                context.pop()
+               self.mark_rendered(context)
                return t_rendered
 
 
 class EmbedNode(ConstantEmbedNode):
-       def __init__(self, content_type, varname, object_pk=None, template_name=None, kwargs=None):
+       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
-               self.varname = varname
                
                kwargs = kwargs or {}
                for k, v in kwargs.items():
-                       kwargs[k] = template.Variable(v)
+                       kwargs[k] = v
                self.kwargs = kwargs
                
                if object_pk is not None:
-                       self.object_pk = template.Variable(object_pk)
+                       self.object_pk = object_pk
                else:
                        self.object_pk = None
                        self.instance = None
                
                if template_name is not None:
-                       self.template_name = template.Variable(template_name)
+                       self.template_name = template_name
                else:
                        self.template_name = None
                        self.template = None
        
+       def get_instance(self, context):
+               return self.compile_instance(self.object_pk, context)
+       
+       def get_template(self, context):
+               return self.compile_template(self.template_name, context)
+       
        def render(self, context):
+               self.check_context(context)
+               
                if self.template_name is not None:
-                       template_name = self.template_name.resolve(context)
-                       self.compile_template(template_name)
+                       self.mark_rendered(context)
+                       return ''
                
-               if self.object_pk is not None:
-                       object_pk = self.object_pk.resolve(context)
-                       self.compile_instance(object_pk)
+               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
                
-               return super(EmbedNode, self).render(context)
+               instance = self.compile_instance(self.object_pk.resolve(context))
+               
+               return self.render_instance(context, instance)
 
 
 def get_embedded(self):
@@ -127,8 +264,7 @@ setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded))
 
 def do_embed(parser, token):
        """
-       The {% embed %} tag can be used in three ways:
-       {% embed as <varname> %} :: This sets which variable will be used to track embedding template names for the current context. Default: "embed"
+       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.
        """
@@ -137,9 +273,6 @@ def do_embed(parser, token):
        
        if len(args) < 2:
                raise template.TemplateSyntaxError('"%s" template tag must have at least three arguments.' % tag)
-       elif len(args) == 3 and args[1] == "as":
-               parser._embedNodeVarName = args[2]
-               return template.defaulttags.CommentNode()
        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)
@@ -150,16 +283,14 @@ def do_embed(parser, token):
                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)
                
-               varname = getattr(parser, '_embedNodeVarName', 'embed')
-               
                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], varname=varname)
+                               return ConstantEmbedNode(ct, template_name=args[3])
                        
-                       return EmbedNode(ct, template_name=args[3], varname=varname)
+                       return EmbedNode(ct, template_name=args[3])
                
                object_pk = args[2]
                remaining_args = args[3:]
@@ -168,9 +299,14 @@ def do_embed(parser, token):
                        if '=' not in arg:
                                raise template.TemplateSyntaxError("Invalid keyword argument for '%s' template tag: %s" % (tag, arg))
                        k, v = arg.split('=')
-                       kwargs[k] = v
+                       kwargs[k] = parser.compile_filter(v)
                
-               return EmbedNode(ct, object_pk=object_pk, varname=varname, kwargs=kwargs)
+               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)
 
 
 register.tag('embed', do_embed)
\ No newline at end of file
index 8a98630..338ac2d 100644 (file)
@@ -25,7 +25,7 @@ class NodeURLNode(template.Node):
                if self.node:
                        node = self.node.resolve(context)
                else:
-                       node = context['node']
+                       node = context.get('node', None)
                
                if not node:
                        return settings.TEMPLATE_STRING_IF_INVALID
index d9f743f..b79534f 100644 (file)
--- a/tests.py
+++ b/tests.py
@@ -1,8 +1,100 @@
 from django.test import TestCase
 from django import template
 from django.conf import settings
+from django.template import loader
+from django.template.loaders import cached
+from philo.exceptions import AncestorDoesNotExist
 from philo.models import Node, Page, Template
 from philo.contrib.penfield.models import Blog, BlogView, BlogEntry
+import sys, traceback
+
+
+class TemplateTestCase(TestCase):
+       fixtures = ['test_fixtures.json']
+       
+       def test_templates(self):
+               "Tests to make sure that embed behaves with complex includes and extends"
+               template_tests = self.get_template_tests()
+               
+               # Register our custom template loader. Shamelessly cribbed from django core regressiontests.
+               def test_template_loader(template_name, template_dirs=None):
+                       "A custom template loader that loads the unit-test templates."
+                       try:
+                               return (template_tests[template_name][0] , "test:%s" % template_name)
+                       except KeyError:
+                               raise template.TemplateDoesNotExist, template_name
+               
+               cache_loader = cached.Loader(('test_template_loader',))
+               cache_loader._cached_loaders = (test_template_loader,)
+               
+               old_template_loaders = loader.template_source_loaders
+               loader.template_source_loaders = [cache_loader]
+               
+               # Turn TEMPLATE_DEBUG off, because tests assume that.
+               old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
+               
+               # Set TEMPLATE_STRING_IF_INVALID to a known string.
+               old_invalid = settings.TEMPLATE_STRING_IF_INVALID
+               expected_invalid_str = 'INVALID'
+               
+               failures = []
+               
+               # Run tests
+               for name, vals in template_tests.items():
+                       xx, context, result = vals
+                       try:
+                               test_template = loader.get_template(name)
+                               output = test_template.render(template.Context(context))
+                       except Exception:
+                               exc_type, exc_value, exc_tb = sys.exc_info()
+                               if exc_type != result:
+                                       tb = '\n'.join(traceback.format_exception(exc_type, exc_value, exc_tb))
+                                       failures.append("Template test %s -- FAILED. Got %s, exception: %s\n%s" % (name, exc_type, exc_value, tb))
+                               continue
+                       if output != result:
+                               failures.append("Template test %s -- FAILED. Expected %r, got %r" % (name, result, output))
+               
+               # Cleanup
+               settings.TEMPLATE_DEBUG = old_td
+               settings.TEMPLATE_STRING_IF_INVALID = old_invalid
+               loader.template_source_loaders = old_template_loaders
+               
+               self.assertEqual(failures, [], "Tests failed:\n%s\n%s" % ('-'*70, ("\n%s\n" % ('-'*70)).join(failures)))
+       
+       
+       def get_template_tests(self):
+               # SYNTAX --
+               # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class)
+               blog = Blog.objects.all()[0]
+               return {
+                       # EMBED INCLUSION HANDLING
+                       
+                       'embed01': ('{{ embedded.title|safe }}', {'embedded': blog}, blog.title),
+                       'embed02': ('{{ embedded.title|safe }}{{ var1 }}{{ var2 }}', {'embedded': blog}, blog.title),
+                       'embed03': ('{{ embedded.title|safe }} is a lie!', {'embedded': blog}, '%s is a lie!' % blog.title),
+                       
+                       # Simple template structure with embed
+                       'simple01': ('{% embed penfield.blog with "embed01" %}{% embed penfield.blog 1 %}Simple{% block one %}{% endblock %}', {'blog': blog}, '%sSimple' % blog.title),
+                       '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),
+                       
+                       # Kwargs
+                       'kwargs01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1="hi" var2=lo %}', {'lo': 'lo'}, '%shilo' % blog.title),
+                       
+                       # Filters/variables
+                       'filters01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1=hi|first var2=lo|slice:"3" %}', {'hi': ["These", "words"], 'lo': 'lower'}, '%sTheselow' % blog.title),
+                       'filters02': ('{% embed penfield.blog with "embed01" %}{% embed penfield.blog entry %}', {'entry': 1}, blog.title),
+                       
+                       # Blocky structure
+                       'block01': ('{% block one %}Hello{% endblock %}', {}, 'Hello'),
+                       'block02': ('{% extends "simple01" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s" % (blog.title, blog.title)),
+                       'block03': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s is a lie!" % (blog.title, blog.title)),
+                       
+                       # Blocks and includes
+                       'block-include01': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% include "simple01" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%sSimple%s is a lie!" % (blog.title, blog.title, blog.title)),
+                       'block-include02': ('{% extends "simple01" %}{% block one %}{% include "simple04" %}{% embed penfield.blog with "embed03" %}{% include "simple04" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s%s is a lie!%s is a lie!" % (blog.title, blog.title, blog.title, blog.title)),
+               }
 
 
 class NodeURLTestCase(TestCase):
@@ -16,20 +108,18 @@ class NodeURLTestCase(TestCase):
                        command = Command()
                        command.handle(all_apps=True)
                
-               self.templates = [template.Template(string) for string in
-                       [
-                               "{% node_url %}", # 0
-                               "{% node_url for node2 %}", # 1
-                               "{% node_url as hello %}<p>{{ hello|slice:'1:' }}</p>", # 2
-                               "{% node_url for nodes|first %}", # 3
-                               "{% node_url with entry %}", # 4
-                               "{% node_url with entry for node2 %}", # 5
-                               "{% node_url with tag for node2 %}", # 6
-                               "{% node_url with date for node2 %}", # 7
-                               "{% 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>", # 8
-                               "{% node_url entries_by_month year=date|date:'Y' month=date|date:'m' for node2 %}", # 9
-                               "{% node_url entries_by_year year=date|date:'Y' for node2 %}", # 10
-                       ]
+               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 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/"),
                ]
                
                nodes = Node.objects.all()
@@ -45,30 +135,65 @@ class NodeURLTestCase(TestCase):
                })
        
        def test_nodeurl(self):
-               for i, template in enumerate(self.templates):
-                       t = template.render(self.context)
-                       
-                       if i == 0:
-                               self.assertEqual(t, "/root/never/")
-                       elif i == 1:
-                               self.assertEqual(t, "/root/blog/")
-                       elif i == 2:
-                               self.assertEqual(t, "<p>root/never/</p>")
-                       elif i == 3:
-                               self.assertEqual(t, "/root/never/")
-                       elif i == 4:
-                               self.assertEqual(t, settings.TEMPLATE_STRING_IF_INVALID)
-                       elif i == 5:
-                               self.assertEqual(t, "/root/blog/2010/10/20/first-entry")
-                       elif i == 6:
-                               self.assertEqual(t, "/root/blog/tags/test-tag/")
-                       elif i == 7:
-                               self.assertEqual(t, "/root/blog/2010/10/20")
-                       elif i == 8:
-                               self.assertEqual(t, "<em>/ROOT/BLOG/2010/10/20</em>")
-                       elif i == 9:
-                               self.assertEqual(t, "/root/blog/2010/10")
-                       elif i == 10:
-                               self.assertEqual(t, "/root/blog/2010/")
-                       else:
-                               print "Rendered as:\n%s\n\n" % t
\ No newline at end of file
+               for string, result in self.templates:
+                       self.assertEqual(template.Template(string).render(self.context), result)
+
+class TreePathTestCase(TestCase):
+       urls = 'philo.urls'
+       fixtures = ['test_fixtures.json']
+       
+       def setUp(self):
+               if 'south' in settings.INSTALLED_APPS:
+                       from south.management.commands.migrate import Command
+                       command = Command()
+                       command.handle(all_apps=True)
+       
+       def test_has_ancestor(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)
+       
+       def test_get_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 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
index 7aac917..255e54e 100644 (file)
--- a/views.py
+++ b/views.py
@@ -1,15 +1,12 @@
-from django.contrib.sites.models import Site
 from django.conf import settings
-from django.http import Http404, HttpResponse
-from django.template import RequestContext
+from django.http import Http404
 from django.views.decorators.vary import vary_on_headers
 from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
-from philo.models import Node
 
 
 @vary_on_headers('Accept')
 def node_view(request, path=None, **kwargs):
-       if not hasattr(request, 'node'):
+       if "philo.middleware.RequestNodeMiddleware" not in settings.MIDDLEWARE_CLASSES:
                raise MIDDLEWARE_NOT_CONFIGURED
        
        if not request.node:
@@ -18,37 +15,6 @@ def node_view(request, path=None, **kwargs):
        node = request.node
        subpath = request.node.subpath
        
-       try:
-               if subpath and not node.accepts_subpath:
-                       raise Http404
-               return node.render_to_response(request, kwargs)
-       except Http404, e:
-               if settings.DEBUG:
-                       raise
-               
-               try:
-                       Http404View = node.attributes['Http404']
-               except KeyError:
-                       Http404View = None
-               
-               if not Http404View:
-                       raise e
-               
-               extra_context = {'exception': e}
-               
-               return Http404View.render_to_response(request, extra_context)
-       except Exception, e:
-               if settings.DEBUG:
-                       raise
-               
-               try:
-                       Http500View = node.attributes['Http500']
-                       
-                       if not Http500View:
-                               raise e
-                       
-                       extra_context = {'exception': e}
-                       
-                       return Http500View.render_to_response(request, extra_context)
-               except:
-                       raise e
\ No newline at end of file
+       if subpath and not node.accepts_subpath:
+               raise Http404
+       return node.render_to_response(request, kwargs)
\ No newline at end of file