from django.contrib.admin.widgets import AdminTextareaWidget
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
-from django.forms.models import ModelForm, BaseInlineFormSet
+from django.forms.models import ModelForm, BaseInlineFormSet, BaseModelFormSet
from django.forms.formsets import TOTAL_FORM_COUNT
+from django.utils.datastructures import SortedDict
from philo.admin.widgets import ModelLookupWidget
from philo.models import Contentlet, ContentReference
def __init__(self, *args, **kwargs):
super(ContainerForm, self).__init__(*args, **kwargs)
self.verbose_name = self.instance.name.replace('_', ' ')
+ self.prefix = self.instance.name
class ContentletForm(ContainerForm):
content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content')
def should_delete(self):
- return not bool(self.cleaned_data['content'])
+ # Delete iff: the data has changed and is now empty.
+ return self.has_changed() and not bool(self.cleaned_data['content'])
class Meta:
model = Contentlet
- fields = ['name', 'content']
+ fields = ['content']
class ContentReferenceForm(ContainerForm):
pass
def should_delete(self):
- return (self.cleaned_data['content_id'] is None)
+ return self.has_changed() and (self.cleaned_data['content_id'] is None)
class Meta:
model = ContentReference
- fields = ['name', 'content_id']
+ fields = ['content_id']
class ContainerInlineFormSet(BaseInlineFormSet):
- def __init__(self, containers, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
- # Unfortunately, I need to add some things to BaseInline between its __init__ and its
- # super call, so a lot of this is repetition.
+ @property
+ def containers(self):
+ if not hasattr(self, '_containers'):
+ self._containers = self.get_containers()
+ return self._containers
+
+ def total_form_count(self):
+ # This ignores the posted management form data... but that doesn't
+ # seem to have any ill side effects.
+ return len(self.containers.keys())
+
+ def _get_initial_forms(self):
+ return [form for form in self.forms if form.instance.pk is not None]
+ initial_forms = property(_get_initial_forms)
+
+ def _get_extra_forms(self):
+ return [form for form in self.forms if form.instance.pk is None]
+ extra_forms = property(_get_extra_forms)
+
+ def _construct_form(self, i, **kwargs):
+ if 'instance' not in kwargs:
+ kwargs['instance'] = self.containers.values()[i]
- # Start cribbed from BaseInline
- from django.db.models.fields.related import RelatedObject
- self.save_as_new = save_as_new
- # is there a better way to get the object descriptor?
- self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
- if self.fk.rel.field_name == self.fk.rel.to._meta.pk.name:
- backlink_value = self.instance
- else:
- backlink_value = getattr(self.instance, self.fk.rel.field_name)
- if queryset is None:
- queryset = self.model._default_manager
- qs = queryset.filter(**{self.fk.name: backlink_value})
- # End cribbed from BaseInline
+ # Skip over the BaseModelFormSet. We have our own way of doing things!
+ form = super(BaseModelFormSet, self)._construct_form(i, **kwargs)
- self.container_instances, qs = self.get_container_instances(containers, qs)
- self.extra_containers = containers
- self.extra = len(self.extra_containers)
- super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs)
-
- def get_container_instances(self, containers, qs):
- raise NotImplementedError
+ # Since we skipped over BaseModelFormSet, we need to duplicate what BaseInlineFormSet would do.
+ if self.save_as_new:
+ # Remove the primary key from the form's data, we are only
+ # creating new instances
+ form.data[form.add_prefix(self._pk_field.name)] = None
+
+ # Remove the foreign key from the form's data
+ form.data[form.add_prefix(self.fk.name)] = None
+
+ # Set the fk value here so that the form can do it's validation.
+ setattr(form.instance, self.fk.get_attname(), self.instance.pk)
+ return form
- def total_form_count(self):
- if self.data or self.files:
- return self.management_form.cleaned_data[TOTAL_FORM_COUNT]
+ def add_fields(self, form, index):
+ """Override the pk field's initial value with a real one."""
+ super(ContainerInlineFormSet, self).add_fields(form, index)
+ if index is not None:
+ pk_value = self.containers.values()[index].pk
else:
- return self.initial_form_count() + self.extra
+ pk_value = None
+ form.fields[self._pk_field.name].initial = pk_value
def save_existing_objects(self, commit=True):
self.changed_objects = []
self.deleted_objects = []
if not self.get_queryset():
return []
-
+
saved_instances = []
for form in self.initial_forms:
pk_name = self._pk_field.name
raw_pk_value = form._raw_value(pk_name)
-
+
# clean() for different types of PK fields can sometimes return
# the model instance, and sometimes the PK. Handle either.
pk_value = form.fields[pk_name].clean(raw_pk_value)
pk_value = getattr(pk_value, 'pk', pk_value)
-
- obj = self._existing_object(pk_value)
- if form.should_delete():
- self.deleted_objects.append(obj)
- obj.delete()
- continue
- if form.has_changed():
- self.changed_objects.append((obj, form.changed_data))
- saved_instances.append(self.save_existing(form, obj, commit=commit))
- if not commit:
- self.saved_forms.append(form)
+
+ # if the pk_value is None, they have just switched to a
+ # template which didn't contain data about this container.
+ # Skip!
+ if pk_value is not None:
+ obj = self._existing_object(pk_value)
+ if form.should_delete():
+ self.deleted_objects.append(obj)
+ obj.delete()
+ continue
+ if form.has_changed():
+ self.changed_objects.append((obj, form.changed_data))
+ saved_instances.append(self.save_existing(form, obj, commit=commit))
+ if not commit:
+ self.saved_forms.append(form)
return saved_instances
def save_new_objects(self, commit=True):
class ContentletInlineFormSet(ContainerInlineFormSet):
- def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
- if instance is None:
- self.instance = self.fk.rel.to()
- else:
- self.instance = instance
-
+ def get_containers(self):
try:
containers = list(self.instance.containers[0])
except ObjectDoesNotExist:
containers = []
-
- super(ContentletInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
-
- def get_container_instances(self, containers, qs):
- qs = qs.filter(name__in=containers)
- container_instances = []
- for container in qs:
- container_instances.append(container)
- containers.remove(container.name)
- return container_instances, qs
-
- def _construct_form(self, i, **kwargs):
- if i >= self.initial_form_count(): # and not kwargs.get('instance'):
- kwargs['instance'] = self.model(name=self.extra_containers[i - self.initial_form_count() - 1])
- return super(ContentletInlineFormSet, self)._construct_form(i, **kwargs)
+ qs = self.get_queryset().filter(name__in=containers)
+ container_dict = SortedDict([(container.name, container) for container in qs])
+ for name in containers:
+ if name not in container_dict:
+ container_dict[name] = self.model(name=name)
+
+ container_dict.keyOrder = containers
+ return container_dict
class ContentReferenceInlineFormSet(ContainerInlineFormSet):
- def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
- if instance is None:
- self.instance = self.fk.rel.to()
- else:
- self.instance = instance
-
+ def get_containers(self):
try:
- containers = list(self.instance.containers[1])
+ containers = self.instance.containers[1]
except ObjectDoesNotExist:
- containers = []
-
- super(ContentReferenceInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
-
- def get_container_instances(self, containers, qs):
- filter = Q()
+ containers = {}
- for name, ct in containers:
+ filter = Q()
+ for name, ct in containers.items():
filter |= Q(name=name, content_type=ct)
+ qs = self.get_queryset().filter(filter)
- qs = qs.filter(filter)
- container_instances = []
- for container in qs:
- container_instances.append(container)
- containers.remove((container.name, container.content_type))
- return container_instances, qs
-
- def _construct_form(self, i, **kwargs):
- if i >= self.initial_form_count(): # and not kwargs.get('instance'):
- name, content_type = self.extra_containers[i - self.initial_form_count() - 1]
- kwargs['instance'] = self.model(name=name, content_type=content_type)
-
- return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs)
\ No newline at end of file
+ container_dict = SortedDict([(container.name, container) for container in qs])
+
+ keyOrder = []
+ for name, ct in containers.items():
+ keyOrder.append(name)
+ if name not in container_dict:
+ container_dict[name] = self.model(name=name, content_type=ct)
+
+ container_dict.keyOrder = keyOrder
+ return container_dict
\ No newline at end of file
-from django.contrib import admin
from django import forms
+from django.contrib import admin
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect, QueryDict
from philo.admin import EntityAdmin, AddTagAdmin, COLLAPSE_CLASSES
from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView
'classes': COLLAPSE_CLASSES
})
)
+ actions = ['make_issue']
def author_names(self, obj):
return ', '.join([author.get_full_name() for author in obj.authors.all()])
author_names.short_description = "Authors"
+
+ def make_issue(self, request, queryset):
+ opts = NewsletterIssue._meta
+ info = opts.app_label, opts.module_name
+ url = reverse("admin:%s_%s_add" % info)
+ return HttpResponseRedirect("%s?articles=%s" % (url, ",".join([str(a.pk) for a in queryset])))
+ make_issue.short_description = u"Create issue from selected %(verbose_name_plural)s"
class NewsletterIssueAdmin(TitledAdmin):
'classes': COLLAPSE_CLASSES
}),
('Feeds', {
- 'fields': ( 'feeds_enabled', 'feed_suffix', 'feed_type', 'item_title_template', 'item_description_template',),
+ 'fields': ( 'feeds_enabled', 'feed_suffix', 'feed_type', 'feed_length', 'item_title_template', 'item_description_template',),
'classes': COLLAPSE_CLASSES
})
)
--- /dev/null
+class HttpNotAcceptable(Exception):
+ """This will be raised if an Http-Accept header will not accept the feed content types that are available."""
+ pass
\ No newline at end of file
--- /dev/null
+from django.http import HttpResponse
+from django.utils.decorators import decorator_from_middleware
+from philo.contrib.penfield.exceptions import HttpNotAcceptable
+
+
+class HttpNotAcceptableMiddleware(object):
+ """Middleware to catch HttpNotAcceptable errors and return an Http406 response.
+ See RFC 2616."""
+ def process_exception(self, request, exception):
+ if isinstance(exception, HttpNotAcceptable):
+ return HttpResponse(status=406)
+
+
+http_not_acceptable = decorator_from_middleware(HttpNotAcceptableMiddleware)
\ 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):
+
+ # Adding field 'NewsletterView.feed_length'
+ db.add_column('penfield_newsletterview', 'feed_length', self.gf('django.db.models.fields.PositiveIntegerField')(default=15, null=True, blank=True), keep_default=False)
+
+ # Adding field 'BlogView.feed_length'
+ db.add_column('penfield_blogview', 'feed_length', self.gf('django.db.models.fields.PositiveIntegerField')(default=15, null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'NewsletterView.feed_length'
+ db.delete_column('penfield_newsletterview', 'feed_length')
+
+ # Deleting field 'BlogView.feed_length'
+ db.delete_column('penfield_blogview', 'feed_length')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ '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': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ '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'})
+ },
+ 'oberlin.person': {
+ 'Meta': {'object_name': 'Person'},
+ 'bio': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '70', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'penfield.blog': {
+ 'Meta': {'object_name': 'Blog'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.blogentry': {
+ 'Meta': {'object_name': 'BlogEntry'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['oberlin.Person']"}),
+ 'blog': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'entries'", 'null': 'True', 'to': "orm['penfield.Blog']"}),
+ 'content': ('django.db.models.fields.TextField', [], {}),
+ 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
+ 'excerpt': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'blogentries'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['philo.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.blogview': {
+ 'Meta': {'object_name': 'BlogView'},
+ 'blog': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogviews'", 'to': "orm['penfield.Blog']"}),
+ 'entries_per_page': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'entry_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_entry_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'entry_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_entry_related'", 'to': "orm['philo.Page']"}),
+ 'entry_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'entries'", 'max_length': '255'}),
+ 'entry_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
+ 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}),
+ 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'application/atom+xml; charset=utf8'", 'max_length': '50'}),
+ 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_index_related'", 'to': "orm['philo.Page']"}),
+ 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'tag_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_tag_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'tag_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_tag_related'", 'to': "orm['philo.Page']"}),
+ 'tag_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'tags'", 'max_length': '255'})
+ },
+ 'penfield.newsletter': {
+ 'Meta': {'object_name': 'Newsletter'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterarticle': {
+ 'Meta': {'unique_together': "(('newsletter', 'slug'),)", 'object_name': 'NewsletterArticle'},
+ 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['oberlin.Person']"}),
+ 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
+ 'full_text': ('philo.models.fields.TemplateField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'lede': ('philo.models.fields.TemplateField', [], {'null': 'True', 'blank': 'True'}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'articles'", 'to': "orm['penfield.Newsletter']"}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'newsletterarticles'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['philo.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterissue': {
+ 'Meta': {'unique_together': "(('newsletter', 'numbering'),)", 'object_name': 'NewsletterIssue'},
+ 'articles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'issues'", 'symmetrical': 'False', 'to': "orm['penfield.NewsletterArticle']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issues'", 'to': "orm['penfield.Newsletter']"}),
+ 'numbering': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterview': {
+ 'Meta': {'object_name': 'NewsletterView'},
+ 'article_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_article_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'article_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_article_related'", 'to': "orm['philo.Page']"}),
+ 'article_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'articles'", 'max_length': '255'}),
+ 'article_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
+ 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}),
+ 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'application/atom+xml; charset=utf8'", 'max_length': '50'}),
+ 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_index_related'", 'to': "orm['philo.Page']"}),
+ 'issue_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_issue_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'issue_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_issue_related'", 'to': "orm['philo.Page']"}),
+ 'issue_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'issues'", 'max_length': '255'}),
+ 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletterviews'", 'to': "orm['penfield.Newsletter']"})
+ },
+ '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', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ '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', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.node': {
+ 'Meta': {'object_name': 'Node'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+ },
+ 'philo.template': {
+ 'Meta': {'object_name': 'Template'},
+ 'code': ('philo.models.fields.TemplateField', [], {}),
+ 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+ }
+ }
+
+ complete_apps = ['penfield']
from django.utils.encoding import smart_unicode, force_unicode
from django.utils.html import escape
from datetime import date, datetime
+from philo.contrib.penfield.exceptions import HttpNotAcceptable
+from philo.contrib.penfield.middleware import http_not_acceptable
from philo.contrib.penfield.validators import validate_pagination_count
from philo.exceptions import ViewCanNotProvideSubpath
from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField, Template
feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM)
feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
feeds_enabled = models.BooleanField(default=True)
+ feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.")
item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
urlpatterns = patterns('')
if self.feeds_enabled:
feed_reverse_name = "%s_feed" % reverse_name
- feed_view = self.feed_view(get_items_attr, feed_reverse_name)
+ feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name))
feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
urlpatterns += patterns('',
url(feed_pattern, feed_view, name=feed_reverse_name),
else:
feed_type = None
if not feed_type:
- # See RFC 2616
- return HttpResponse(status=406)
+ raise HttpNotAcceptable
return FEEDS[feed_type]
def get_feed(self, obj, request, reverse_name):
except Site.DoesNotExist:
current_site = RequestSite(request)
+ if self.feed_length is not None:
+ items = items[:self.feed_length]
+
for item in items:
if title_template is not None:
title = title_template.render(RequestContext(request, {'obj': item}))
from django.utils.safestring import mark_safe
from philo.contrib.shipherd.models import Navigation
from philo.models import Node
-from mptt.templatetags.mptt_tags import RecurseTreeNode, cache_tree_children
+from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
register = template.Library()
-class RecurseNavigationNode(RecurseTreeNode):
- def __init__(self, template_nodes, instance_var, key):
+class LazyNavigationRecurser(object):
+ def __init__(self, template_nodes, items, context, request):
self.template_nodes = template_nodes
- self.instance_var = instance_var
- self.key = key
+ self.items = items
+ self.context = context
+ self.request = request
- def _render_node(self, context, item, request):
- bits = []
+ def __call__(self):
+ items = self.items
+ context = self.context
+ request = self.request
+
+ if not items:
+ return ''
+
+ if 'navloop' in context:
+ parentloop = context['navloop']
+ else:
+ parentloop = {}
context.push()
- for child in item.get_children():
- context['item'] = child
- bits.append(self._render_node(context, child, request))
- context['item'] = item
- context['children'] = mark_safe(u''.join(bits))
- context['active'] = item.is_active(request)
- context['active_descendants'] = item.has_active_descendants(request)
- rendered = self.template_nodes.render(context)
+
+ depth = items[0].get_level()
+ len_items = len(items)
+
+ loop_dict = context['navloop'] = {
+ 'parentloop': parentloop,
+ 'depth': depth + 1,
+ 'depth0': depth
+ }
+
+ bits = []
+
+ for i, item in enumerate(items):
+ # First set context variables.
+ loop_dict['counter0'] = i
+ loop_dict['counter'] = i + 1
+ loop_dict['revcounter'] = len_items - i
+ loop_dict['revcounter0'] = len_items - i - 1
+ loop_dict['first'] = (i == 0)
+ loop_dict['last'] = (i == len_items - 1)
+
+ # Set on loop_dict and context for backwards-compatibility.
+ # Eventually only allow access through the loop_dict.
+ loop_dict['active'] = context['active'] = item.is_active(request)
+ loop_dict['active_descendants'] = context['active_descendants'] = item.has_active_descendants(request)
+
+ # Set these directly in the context for easy access.
+ context['item'] = item
+ context['children'] = self.__class__(self.template_nodes, item.get_children(), context, request)
+
+ # Then render the nodelist bit by bit.
+ for node in self.template_nodes:
+ bits.append(node.render(context))
context.pop()
- return rendered
+ return mark_safe(''.join(bits))
+
+
+class RecurseNavigationNode(template.Node):
+ def __init__(self, template_nodes, instance_var, key):
+ self.template_nodes = template_nodes
+ self.instance_var = instance_var
+ self.key = key
def render(self, context):
try:
instance = self.instance_var.resolve(context)
try:
- navigation = instance.navigation[self.key]
+ items = instance.navigation[self.key]
except:
return settings.TEMPLATE_STRING_IF_INVALID
- bits = [self._render_node(context, item, request) for item in navigation]
- return ''.join(bits)
+ return LazyNavigationRecurser(self.template_nodes, items, context, request)()
@register.tag
def recursenavigation(parser, token):
"""
- Based on django-mptt's recursetree templatetag. In addition to {{ item }} and {{ children }},
- sets {{ active }} and {{ active_descendants }} in the context.
+ The recursenavigation templatetag takes two arguments:
+ - the node for which the navigation should be found
+ - the navigation's key.
- Note that the tag takes one variable, which is a Node instance.
+ It will then recursively loop over each item in the navigation and render the template
+ chunk within the block. recursenavigation sets the following variables in the context:
- Usage:
+ ============================== ================================================
+ Variable Description
+ ============================== ================================================
+ ``navloop.depth`` The current depth of the loop (1 is the top level)
+ ``navloop.depth0`` The current depth of the loop (0 is the top level)
+ ``navloop.counter`` The current iteration of the current level(1-indexed)
+ ``navloop.counter0`` The current iteration of the current level(0-indexed)
+ ``navloop.first`` True if this is the first time through the current level
+ ``navloop.last`` True if this is the last time through the current level
+ ``navloop.parentloop`` This is the loop one level "above" the current one
+ ============================== ================================================
+ ``item`` The current item in the loop (a NavigationItem instance)
+ ``children`` If accessed, performs the next level of recursion.
+ ``navloop.active`` True if the item is active for this request
+ ``navloop.active_descendants`` True if the item has active descendants for this request
+ ============================== ================================================
+
+ Example:
<ul>
{% recursenavigation node main %}
- <li{% if active %} class='active'{% endif %}>
- {{ navigation.text }}
- {% if navigation.get_children %}
+ <li{% if navloop.active %} class='active'{% endif %}>
+ {{ navloop.item.text }}
+ {% if item.get_children %}
<ul>
{{ children }}
</ul>
instance_var = parser.compile_filter(bits[1])
key = bits[2]
- template_nodes = parser.parse(('endrecursenavigation',))
- parser.delete_first_token()
+ template_nodes = parser.parse(('recurse', 'endrecursenavigation',))
+
+ token = parser.next_token()
+ if token.contents == 'recurse':
+ template_nodes.append(RecurseNavigationMarker())
+ template_nodes.extend(parser.parse(('endrecursenavigation')))
+ parser.delete_first_token()
return RecurseNavigationNode(template_nodes, instance_var, key)
var tagCreation = window.tagCreation;
(function($) {
+ location_re = new RegExp("^https?:\/\/" + window.location.host + "/")
+
+ $('html').ajaxSend(function(event, xhr, settings) {
+ function getCookie(name) {
+ var cookieValue = null;
+ if (document.cookie && document.cookie != '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = $.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) == (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ }
+ if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url)) || location_re.test(settings.url)) {
+ // Only send the token to relative URLs i.e. locally.
+ xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
+ }
+ });
tagCreation = {
'cache': {},
'addTagFromSlug': function(triggeringLink) {
class AttributeValue(models.Model):
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 set_value(self, value):
raise NotImplementedError
from django.core.exceptions import ValidationError
from django.db import models
from django.http import HttpResponse
-from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, add_to_builtins as register_templatetags
+from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, add_to_builtins as register_templatetags, TextNode, VariableNode
+from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext
+from django.utils.datastructures import SortedDict
from philo.models.base import TreeModel, register_value_model
from philo.models.fields import TemplateField
from philo.models.nodes import View
from philo.templatetags.containers import ContainerNode
-from philo.utils import fattr, nodelist_crawl
+from philo.utils import fattr
from philo.validators import LOADED_TEMPLATE_ATTR
from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
+class LazyContainerFinder(object):
+ def __init__(self, nodes):
+ self.nodes = nodes
+ self.initialized = False
+ self.contentlet_specs = set()
+ self.contentreference_specs = SortedDict()
+ self.blocks = {}
+ self.block_super = False
+
+ def process(self, nodelist):
+ for node in nodelist:
+ if isinstance(node, ContainerNode):
+ if not node.references:
+ self.contentlet_specs.add(node.name)
+ else:
+ if node.name not in self.contentreference_specs.keys():
+ self.contentreference_specs[node.name] = node.references
+ continue
+
+ if isinstance(node, BlockNode):
+ self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
+ block.initialize()
+ self.blocks.update(block.blocks)
+ continue
+
+ if isinstance(node, ExtendsNode):
+ continue
+
+ if isinstance(node, VariableNode):
+ if node.filter_expression.var.lookups == (u'block', u'super'):
+ self.block_super = True
+
+ if hasattr(node, 'child_nodelists'):
+ for nodelist_name in node.child_nodelists:
+ if hasattr(node, nodelist_name):
+ nodelist = getattr(node, nodelist_name)
+ self.process(nodelist)
+
+ # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
+ # node as rendering an additional template. Philo monkeypatches the attribute onto
+ # the relevant default nodes and declares it on any native nodes.
+ if hasattr(node, LOADED_TEMPLATE_ATTR):
+ loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
+ if loaded_template:
+ nodelist = loaded_template.nodelist
+ self.process(nodelist)
+
+ def initialize(self):
+ if not self.initialized:
+ self.process(self.nodes)
+ self.initialized = True
+
+
class Template(TreeModel):
name = models.CharField(max_length=255)
documentation = models.TextField(null=True, blank=True)
This will break if there is a recursive extends or includes in the template code.
Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
"""
- def process_node(node, nodes):
- if isinstance(node, ContainerNode):
- nodes.append(node)
+ template = DjangoTemplate(self.code)
+
+ def build_extension_tree(nodelist):
+ nodelists = []
+ extends = None
+ for node in nodelist:
+ if not isinstance(node, TextNode):
+ if isinstance(node, ExtendsNode):
+ extends = node
+ break
+
+ if extends:
+ if extends.nodelist:
+ nodelists.append(LazyContainerFinder(extends.nodelist))
+ loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
+ nodelists.extend(build_extension_tree(loaded_template.nodelist))
+ else:
+ # Base case: root.
+ nodelists.append(LazyContainerFinder(nodelist))
+ return nodelists
+
+ # Build a tree of the templates we're using, placing the root template first.
+ levels = build_extension_tree(template.nodelist)[::-1]
+
+ contentlet_specs = set()
+ contentreference_specs = SortedDict()
+ blocks = {}
+
+ for level in levels:
+ level.initialize()
+ contentlet_specs |= level.contentlet_specs
+ contentreference_specs.update(level.contentreference_specs)
+ for name, block in level.blocks.items():
+ if block.block_super:
+ blocks.setdefault(name, []).append(block)
+ else:
+ blocks[name] = [block]
+
+ for block_list in blocks.values():
+ for block in block_list:
+ block.initialize()
+ contentlet_specs |= block.contentlet_specs
+ contentreference_specs.update(block.contentreference_specs)
- 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 = []
- for node in all_nodes:
- if node.references and node.name not in contentreference_node_names:
- contentreference_node_specs.append((node.name, node.references))
- contentreference_node_names.append(node.name)
- return contentlet_node_names, contentreference_node_specs
+ return contentlet_specs, contentreference_specs
def __unicode__(self):
return self.get_path(pathsep=u' › ', field='name')
{% comment %}Don't render the formset at all if there aren't any forms.{% endcomment %}
{% if inline_admin_formset.formset.forms %}
<fieldset class="module{% if inline_admin_formset.opts.classes %} {{ inline_admin_formset.opts.classes|join:" " }}{% endif %}">
- <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
+ <h2{% if "collapse" in inline_admin_formset.opts.classes %} class="collapse-handler"{% endif %}>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.non_form_errors }}
{% for inline_admin_form in inline_admin_formset %}
{% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}{% endspaceless %}
- <div class="row cells-{{ inline_admin_form.fields|length }}{% if not inline_admin_form.fields|length_is:"2" %} cells{% endif %}{% if inline_admin_form.errors %} errors{% endif %} {% for field in inline_admin_form %}{{ field.field.name }} {% endfor %}{% if forloop.last %} empty-form{% endif %}">
- <div{% if not inline_admin_form.fields|length_is:"2" %} class="cell"{% endif %}>
- <div class="column span-4"><label class='required' for="{{ inline_admin_form.form.content.auto_id }}{{ inline_admin_form.form.content_id.auto_id }}">{{ inline_admin_form.form.verbose_name|capfirst }}:</label>{{ inline_admin_form.form.name.as_hidden }}</div>
- {% for fieldset in inline_admin_form %}{% for line in fieldset %}{% for field in line %}
- {% if field.field.name != 'name' %}
+ {% endfor %}
+ {% for form in inline_admin_formset.formset.forms %}
+ <div class="row cells-{{ form.fields.keys|length }}{% if not form.fields.keys|length_is:"2" %} cells{% endif %}{% if form.errors %} errors{% endif %} {% for field in form %}{{ field.field.name }} {% endfor %}{% comment %} {% if forloop.last %} empty-form{% endif %}{% endcomment %}">
+ {{ form.non_field_errors }}
+ <div{% if not form.fields.keys|length_is:"2" %} class="cell"{% endif %}>
+ <div class="column span-4"><label class='required' for="{{ form.content.auto_id }}{{ form.content_id.auto_id }}">{{ form.verbose_name|capfirst }}:</label></div>
+ {% for field in form %}
+ {% if not field.is_hidden %}
<div class="column span-flexible">
- {% if field.is_readonly %}
- <p class="readonly">{{ field.contents }}</p>
- {% else %}
- {{ field.field }}
- {% endif %}
- {{ inline_admin_form.errors }}
- {% if field.field.field.help_text %}
- <p class="help">{{ field.field.field.help_text|safe }}</p>
+ {{ field }}
+ {{ field.errors }}
+ {% if field.field.help_text %}
+ <p class="help">{{ field.field.help_text|safe }}</p>
{% endif %}
</div>
{% endif %}
- {% endfor %}{% endfor %}{% endfor %}
+ {% endfor %}
</div>
</div>
{% endfor %}
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.non_form_errors }}
<table>
- <thead><tr>
- {% for field in inline_admin_formset.fields %}
- {% if not field.widget.is_hidden %}
- <th{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}</th>
- {% endif %}
- {% endfor %}
- {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
- </tr></thead>
-
<tbody>
{% for inline_admin_form in inline_admin_formset %}
{% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
- {{ inline_admin_form.form.name.as_hidden }}
{% endspaceless %}
- {% if inline_admin_form.form.non_field_errors %}
- <tr><td colspan="{{ inline_admin_form.field_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
+ {% endfor %}
+ {% for form in inline_admin_formset.formset.forms %}
+ {% if form.non_field_errors %}
+ <tr><td colspan="2">{{ form.non_field_errors }}</td></tr>
{% endif %}
- <tr class="{% cycle "row1" "row2" %} {% if forloop.last %} empty-form{% endif %}"
- id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
- <th>{{ inline_admin_form.form.verbose_name|capfirst }}:</th>
- {% for fieldset in inline_admin_form %}
- {% for line in fieldset %}
- {% for field in line %}
- {% if field.field.name != 'name' %}
+ <tr class="{% cycle "row1" "row2" %}{% comment %} {% if forloop.last %} empty-form{% endif %}{% endcomment %}"
+ id="{{ formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
+ <th>{{ form.verbose_name|capfirst }}:</th>
+ {% for field in form %}
+ {% if not field.is_hidden %}
<td class="{{ field.field.name }}">
- {% if field.is_readonly %}
- <p>{{ field.contents }}</p>
- {% else %}
{{ field.field.errors.as_ul }}
- {{ field.field }}
- {% endif %}
+ {{ field }}
+ {% if field.field.help_text %}
+ <p class="help">{{ field.field.help_text|safe }}</p>
+ {% endif %}
</td>
{% endif %}
{% endfor %}
- {% endfor %}
- {% endfor %}
- {% if inline_admin_formset.formset.can_delete %}
- <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
- {% endif %}
</tr>
{% endfor %}
</tbody>
# We ignore the IncludeNode because it will never work in a blank context.
setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended))
-setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
-
-
-def nodelist_crawl(nodelist, callback):
- """This function crawls through a template's nodelist and the nodelists of any included or extended
- templates, as determined by the presence and value of <LOADED_TEMPLATE_ATTR> on a node. Each node
- will also be passed to a callback function for additional processing."""
- results = []
- for node in nodelist:
- try:
- if hasattr(node, 'child_nodelists'):
- for nodelist_name in node.child_nodelists:
- if hasattr(node, nodelist_name):
- results.extend(nodelist_crawl(getattr(node, nodelist_name), callback))
-
- # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
- # node as rendering an additional template. Philo monkeypatches the attribute onto
- # the relevant default nodes and declares it on any native nodes.
- if hasattr(node, LOADED_TEMPLATE_ATTR):
- loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
- if loaded_template:
- results.extend(nodelist_crawl(loaded_template.nodelist, callback))
-
- callback(node, results)
- except:
- raise # fail for this node
- return results
\ No newline at end of file
+setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
\ No newline at end of file