-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
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',)
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
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
output += ' <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
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
pass
-class BlogEntryAdmin(TitledAdmin):
+class BlogEntryAdmin(TitledAdmin, AddTagAdmin):
filter_horizontal = ['tags']
pass
-class NewsletterArticleAdmin(TitledAdmin):
- pass
+class NewsletterArticleAdmin(TitledAdmin, AddTagAdmin):
+ filter_horizontal = TitledAdmin.filter_horizontal + ('tags', 'authors')
class NewsletterIssueAdmin(TitledAdmin):
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})
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 {})
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):
]
}
},
+ {
+ "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",
--- /dev/null
+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
--- /dev/null
+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
+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):
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
--- /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):
+
+ # 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']
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
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()
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:
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)
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'
@property
def attributes(self):
- return QuerySetMapper(self.attribute_set)
+ return QuerySetMapper(self.attribute_set.all())
@property
def _added_attribute_registry(self):
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):
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):
@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:
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
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):
"""
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 = []
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'
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
# 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:
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()
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):
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.
"""
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)
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:]
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
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
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):
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()
})
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
-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:
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