Merge branch 'master' of git://github.com/melinath/philo
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Thu, 3 Mar 2011 20:38:44 +0000 (15:38 -0500)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Thu, 3 Mar 2011 20:38:44 +0000 (15:38 -0500)
* 'master' of git://github.com/melinath/philo:
  Added feed length limit to FeedView. Implements feature #111.
  Corrected LazyNavigationRecurser to mark its return value as safe.
  Switched back to setting a {{ children }} variable, but set it to a lazy recurser instead of a rendered result. Switched item and children to be set in the context directly for easy access. Improved/updated recursenavigation docstring.
  Refactored the RecurseNavigationNode to have less repetition. Switched from {{ children }} to {% recurse %} because it makes more sense to collect recursion only if needed.
  Added a number of counting variables to the context when rendering shipherd navigation. Designed to mirror the variables generated when using a for loop.
  Minor corrections to shipherd recursenavigation docstring.
  Added CSRF cookie js to TagCreation.js... apparently it isn't in the admin by default. Resolves issue 83.
  Added an admin action to NewsletterArticleAdmin to handle creating a NewsletterIssue from a selection of articles. Resolves issue 82.
  Minor LazyContainerFinder cleanup. It's not really that lazy in practice...
  Removed nodelist_crawl since nothing is using it and I'm starting to question its usefulness in general.
  Built a clearer algorithm for finding a template's containers; added support for template block overrides cancelling out containers in that block, which resolves issue #90.
  Corrected FeedView handling of incorrect Accept headers. Will now actually return 406 errors.
  Genericized nodelist_crawl to just pass each node to a callback function. Overloaded the Template.containers callback to create a blockcontext for handling containers in overridden blocks as per issue 90 and to handle contentreference/contentlet generation in a single sweep.
  Refactored ContainerForms to reflect a more suitable structure by storing the containers internally as a SortedDict. By handling containers more consistently, this commit resolves issue #89.
  Removed apparently-vestigial Entity.attribute property.

13 files changed:
admin/forms/containers.py
contrib/penfield/admin.py
contrib/penfield/exceptions.py [new file with mode: 0644]
contrib/penfield/middleware.py [new file with mode: 0644]
contrib/penfield/migrations/0004_auto__add_field_newsletterview_feed_length__add_field_blogview_feed_le.py [new file with mode: 0644]
contrib/penfield/models.py
contrib/shipherd/templatetags/shipherd.py
media/admin/js/TagCreation.js
models/base.py
models/pages.py
templates/admin/philo/edit_inline/grappelli_tabular_container.html
templates/admin/philo/edit_inline/tabular_container.html
utils.py

index 5991dfa..420ba17 100644 (file)
@@ -2,8 +2,9 @@ from django import forms
 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
 
@@ -20,17 +21,19 @@ class ContainerForm(ModelForm):
        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):
@@ -43,72 +46,92 @@ 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):
@@ -127,64 +150,41 @@ class ContainerInlineFormSet(BaseInlineFormSet):
 
 
 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
index 950539d..5aee6f8 100644 (file)
@@ -1,5 +1,7 @@
-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
 
@@ -91,10 +93,18 @@ class NewsletterArticleAdmin(TitledAdmin, AddTagAdmin):
                        '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):
@@ -117,7 +127,7 @@ class NewsletterViewAdmin(EntityAdmin):
                        '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
                })
        )
diff --git a/contrib/penfield/exceptions.py b/contrib/penfield/exceptions.py
new file mode 100644 (file)
index 0000000..96b96ed
--- /dev/null
@@ -0,0 +1,3 @@
+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
diff --git a/contrib/penfield/middleware.py b/contrib/penfield/middleware.py
new file mode 100644 (file)
index 0000000..b25a28b
--- /dev/null
@@ -0,0 +1,14 @@
+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
diff --git a/contrib/penfield/migrations/0004_auto__add_field_newsletterview_feed_length__add_field_blogview_feed_le.py b/contrib/penfield/migrations/0004_auto__add_field_newsletterview_feed_length__add_field_blogview_feed_le.py
new file mode 100644 (file)
index 0000000..9b9ffa7
--- /dev/null
@@ -0,0 +1,204 @@
+# 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']
index 98dcdd5..a03bed8 100644 (file)
@@ -10,6 +10,8 @@ from django.utils.datastructures import SortedDict
 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
@@ -44,6 +46,7 @@ class FeedView(MultiView):
        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")
@@ -62,7 +65,7 @@ class FeedView(MultiView):
                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),
@@ -139,8 +142,7 @@ class FeedView(MultiView):
                                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):
@@ -194,6 +196,9 @@ class FeedView(MultiView):
                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}))
index fa4ec3e..57fb020 100644 (file)
@@ -3,32 +3,75 @@ from django.conf import settings
 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:
@@ -39,28 +82,46 @@ class RecurseNavigationNode(RecurseTreeNode):
                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>
@@ -76,8 +137,13 @@ def recursenavigation(parser, token):
        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)
 
index 31f2910..d08d41e 100644 (file)
@@ -1,6 +1,29 @@
 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) {
index 836fe4a..faac89b 100644 (file)
@@ -55,10 +55,6 @@ def unregister_value_model(model):
 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
        
index ef68b5f..39125ef 100644 (file)
@@ -5,16 +5,71 @@ from django.contrib.contenttypes import generic
 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)
@@ -29,19 +84,51 @@ class Template(TreeModel):
                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')
index 5602a38..59aba8f 100644 (file)
@@ -5,7 +5,7 @@
 {% 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 %}
index f93e52f..77d5e23 100644 (file)
@@ -7,15 +7,6 @@
    <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>
index deb009c..57f949e 100644 (file)
--- a/utils.py
+++ b/utils.py
@@ -121,30 +121,4 @@ def get_included(self):
 
 # 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