Merge branch 'master' of git://github.com/melinath/philo
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Thu, 21 Oct 2010 19:59:40 +0000 (15:59 -0400)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Thu, 21 Oct 2010 19:59:40 +0000 (15:59 -0400)
* 'master' of git://github.com/melinath/philo:
  Added support for 'editable' to ProxyFields. Tweaked AttributeFieldDescriptor base class to not raise an AttributeError on access. This allows the descriptor to be detected by Django admin validation, allowing the proxy fields to be used (for example) in a ModelAdmin's readonly_fields and list_display fields. This may address the remaining issues in feature #16.
  Tweaked philo migrations to force contenttype creation after migration 0005 so the contenttypes are available in migration 0006. Ordinarily contenttypes are only synced after a complete set of migrations. Also implemented default values on proxy fields; partially addresses feature #16.
  Added unittest for node_url templatetag. Fixed issues uncovered with unittests. Added test fixture. Seems bugfree - as such, addresses feature #12.
  Initial node_url_refactor commit. Revises node_url templatetag to accept args/kwargs like urlnode, using the current or given node's view as the urlconf. with <obj> is retained as a convenient shortcut.
  Initial node_middleware commit. Implements an attribute on the HttpRequest class which handles lazy node evaluation - i.e. request.node is available for all requests, but is only evaluated on first use. Removed all references I could find to node, path, and subpath in Views, which may make for a simpler API. Path and subpath were generally unused and can be extrapolated in other ways; however, should it come to light that they are necessary, they can be added back in.
  Added model validation to Pages; for a page to be valid, its related template must also be valid. Ordinarily, the template would need to be valid in order to exist; however, this should help catch cases where, for example, changes on a filesystem template affect the validity of a database template. Addresses the remaining issues with bug #14.
  Genericized penfield's migrations to allow for variations in the PHILO_PERSON_MODULE setting by adding a central frozen reference to the person module appropriate for use in a South Migration.
  Tweaked QuerySetMapper to return the value's value, in line with the changes to flexible attributes. Adjusted attribute fields to compensate.
  Shunted the responsibility in Entity proxy models for setting an Attribute's value onto the field that manages the model's relationship to that attribute.
  Added migrations to handle transfer of structure and information to and from flexible attributes.
  Added AttributeInlineFormSet to force new Attribute instances to be run through the form's save method for extra processing.
  Added generic relation from attribute values back to Attribute.
  Added migrations from ithinksw/master to melinath/master
  Added search field to tags; added db_index=True to newsletter article full_text to expediate searching.
  Polished AttributeForm to better handle changes to an attribute instance. Introduced apply_data to AttributeValue models to further allow customization. Made an AttributeValue class which must be subclassed to create new attribute values.
  Added AttributeForm to elegantly handle all the fields that should be displayed in the admin related to the instance's value. Tweaked the tabular_collapse template (which was only used for attributes) to display the extra fields. Added the "value_formfield" method to Attribute Value models to allow for the possible future addition of value types.
  Initial flexible attributes commit. Removed Relationship model in favor of a flexible Attribute model with generic foreign keys to an entity and a value type. This should allow for easy addition of attribute types: for example, an attribute with a ManyToMany value.
  Created JSON field, descriptor, and formfield to handle json storage on models uniformly. Addresses feature #26.

30 files changed:
admin/base.py
admin/pages.py
contrib/penfield/migrations/0001_initial.py [new file with mode: 0644]
contrib/penfield/migrations/0002_auto.py [new file with mode: 0644]
contrib/penfield/migrations/__init__.py [new file with mode: 0644]
contrib/penfield/models.py
contrib/penfield/utils.py
contrib/waldo/models.py
exceptions.py
fixtures/test_fixtures.json [new file with mode: 0644]
forms.py
middleware.py [new file with mode: 0644]
migrations/0001_initial.py [new file with mode: 0644]
migrations/0002_auto__add_field_attribute_value.py [new file with mode: 0644]
migrations/0003_move_json.py [new file with mode: 0644]
migrations/0004_auto__del_field_attribute_json_value.py [new file with mode: 0644]
migrations/0005_add_attribute_values.py [new file with mode: 0644]
migrations/0006_move_attribute_and_relationship_values.py [new file with mode: 0644]
migrations/0007_auto__del_relationship__del_field_attribute_value.py [new file with mode: 0644]
migrations/__init__.py [new file with mode: 0644]
models/base.py
models/fields.py
models/nodes.py
models/pages.py
signals.py
templates/admin/philo/edit_inline/tabular_attribute.html [moved from templates/admin/philo/edit_inline/tabular_collapse.html with 95% similarity]
templatetags/nodes.py
tests.py [new file with mode: 0644]
validators.py
views.py

index 46c7c22..f4a5f2f 100644 (file)
@@ -1,6 +1,7 @@
 from django.contrib import admin
 from django.contrib.contenttypes import generic
-from philo.models import Tag, Attribute, Relationship
+from philo.models import Tag, Attribute
+from philo.forms import AttributeForm, AttributeInlineFormSet
 
 
 COLLAPSE_CLASSES = ('collapse', 'collapse-closed', 'closed',)
@@ -11,28 +12,22 @@ class AttributeInline(generic.GenericTabularInline):
        ct_fk_field = 'entity_object_id'
        model = Attribute
        extra = 1
-       template = 'admin/philo/edit_inline/tabular_collapse.html'
-       allow_add = True
-       classes = COLLAPSE_CLASSES
-
-
-class RelationshipInline(generic.GenericTabularInline):
-       ct_field = 'entity_content_type'
-       ct_fk_field = 'entity_object_id'
-       model = Relationship
-       extra = 1
-       template = 'admin/philo/edit_inline/tabular_collapse.html'
+       template = 'admin/philo/edit_inline/tabular_attribute.html'
        allow_add = True
        classes = COLLAPSE_CLASSES
+       form = AttributeForm
+       formset = AttributeInlineFormSet
+       exclude = ['value_object_id']
 
 
 class EntityAdmin(admin.ModelAdmin):
-       inlines = [AttributeInline, RelationshipInline]
+       inlines = [AttributeInline]
        save_on_top = True
 
 
 class TagAdmin(admin.ModelAdmin):
        list_display = ('name', 'slug')
        prepopulated_fields = {"slug": ("name",)}
+       search_fields = ["name"]
 
 admin.site.register(Tag, TagAdmin)
\ No newline at end of file
index 4810e0f..234b9d8 100644 (file)
@@ -1,6 +1,5 @@
 from django.contrib import admin
 from django import forms
-from philo.admin import widgets
 from philo.admin.base import COLLAPSE_CLASSES
 from philo.admin.nodes import ViewAdmin
 from philo.models.pages import Page, Template, Contentlet, ContentReference
diff --git a/contrib/penfield/migrations/0001_initial.py b/contrib/penfield/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..20148e6
--- /dev/null
@@ -0,0 +1,342 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+from philo.migrations import person_model, frozen_person
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding model 'Blog'
+        db.create_table('penfield_blog', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
+        ))
+        db.send_create_signal('penfield', ['Blog'])
+
+        # Adding model 'BlogEntry'
+        db.create_table('penfield_blogentry', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
+            ('blog', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='entries', null=True, to=orm['penfield.Blog'])),
+            ('author', self.gf('django.db.models.fields.related.ForeignKey')(related_name='blogentries', to=orm[person_model])),
+            ('date', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
+            ('content', self.gf('django.db.models.fields.TextField')()),
+            ('excerpt', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+        ))
+        db.send_create_signal('penfield', ['BlogEntry'])
+
+        # Adding M2M table for field tags on 'BlogEntry'
+        db.create_table('penfield_blogentry_tags', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('blogentry', models.ForeignKey(orm['penfield.blogentry'], null=False)),
+            ('tag', models.ForeignKey(orm['philo.tag'], null=False))
+        ))
+        db.create_unique('penfield_blogentry_tags', ['blogentry_id', 'tag_id'])
+
+        # Adding model 'BlogView'
+        db.create_table('penfield_blogview', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('blog', self.gf('django.db.models.fields.related.ForeignKey')(related_name='blogviews', to=orm['penfield.Blog'])),
+            ('index_page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='blog_index_related', to=orm['philo.Page'])),
+            ('entry_page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='blog_entry_related', to=orm['philo.Page'])),
+            ('entry_archive_page', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='blog_entry_archive_related', null=True, to=orm['philo.Page'])),
+            ('tag_page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='blog_tag_related', to=orm['philo.Page'])),
+            ('tag_archive_page', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='blog_tag_archive_related', null=True, to=orm['philo.Page'])),
+            ('entries_per_page', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)),
+            ('entry_permalink_style', self.gf('django.db.models.fields.CharField')(max_length=1)),
+            ('entry_permalink_base', self.gf('django.db.models.fields.CharField')(default='entries', max_length=255)),
+            ('tag_permalink_base', self.gf('django.db.models.fields.CharField')(default='tags', max_length=255)),
+            ('feed_suffix', self.gf('django.db.models.fields.CharField')(default='feed', max_length=255)),
+            ('feeds_enabled', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)),
+        ))
+        db.send_create_signal('penfield', ['BlogView'])
+
+        # Adding model 'Newsletter'
+        db.create_table('penfield_newsletter', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
+        ))
+        db.send_create_signal('penfield', ['Newsletter'])
+
+        # Adding model 'NewsletterArticle'
+        db.create_table('penfield_newsletterarticle', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
+            ('newsletter', self.gf('django.db.models.fields.related.ForeignKey')(related_name='articles', to=orm['penfield.Newsletter'])),
+            ('date', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
+            ('lede', self.gf('philo.models.fields.TemplateField')(null=True, blank=True)),
+            ('full_text', self.gf('philo.models.fields.TemplateField')()),
+        ))
+        db.send_create_signal('penfield', ['NewsletterArticle'])
+
+        # Adding unique constraint on 'NewsletterArticle', fields ['newsletter', 'slug']
+        db.create_unique('penfield_newsletterarticle', ['newsletter_id', 'slug'])
+
+        # Adding M2M table for field authors on 'NewsletterArticle'
+        db.create_table('penfield_newsletterarticle_authors', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('newsletterarticle', models.ForeignKey(orm['penfield.newsletterarticle'], null=False)),
+            ('person', models.ForeignKey(orm[person_model], null=False))
+        ))
+        db.create_unique('penfield_newsletterarticle_authors', ['newsletterarticle_id', 'person_id'])
+
+        # Adding M2M table for field tags on 'NewsletterArticle'
+        db.create_table('penfield_newsletterarticle_tags', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('newsletterarticle', models.ForeignKey(orm['penfield.newsletterarticle'], null=False)),
+            ('tag', models.ForeignKey(orm['philo.tag'], null=False))
+        ))
+        db.create_unique('penfield_newsletterarticle_tags', ['newsletterarticle_id', 'tag_id'])
+
+        # Adding model 'NewsletterIssue'
+        db.create_table('penfield_newsletterissue', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
+            ('newsletter', self.gf('django.db.models.fields.related.ForeignKey')(related_name='issues', to=orm['penfield.Newsletter'])),
+            ('numbering', self.gf('django.db.models.fields.CharField')(max_length=50)),
+        ))
+        db.send_create_signal('penfield', ['NewsletterIssue'])
+
+        # Adding unique constraint on 'NewsletterIssue', fields ['newsletter', 'numbering']
+        db.create_unique('penfield_newsletterissue', ['newsletter_id', 'numbering'])
+
+        # Adding M2M table for field articles on 'NewsletterIssue'
+        db.create_table('penfield_newsletterissue_articles', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('newsletterissue', models.ForeignKey(orm['penfield.newsletterissue'], null=False)),
+            ('newsletterarticle', models.ForeignKey(orm['penfield.newsletterarticle'], null=False))
+        ))
+        db.create_unique('penfield_newsletterissue_articles', ['newsletterissue_id', 'newsletterarticle_id'])
+
+        # Adding model 'NewsletterView'
+        db.create_table('penfield_newsletterview', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('newsletter', self.gf('django.db.models.fields.related.ForeignKey')(related_name='newsletterviews', to=orm['penfield.Newsletter'])),
+            ('index_page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='newsletter_index_related', to=orm['philo.Page'])),
+            ('article_page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='newsletter_article_related', to=orm['philo.Page'])),
+            ('article_archive_page', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='newsletter_article_archive_related', null=True, to=orm['philo.Page'])),
+            ('issue_page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='newsletter_issue_related', to=orm['philo.Page'])),
+            ('issue_archive_page', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='newsletter_issue_archive_related', null=True, to=orm['philo.Page'])),
+            ('article_permalink_style', self.gf('django.db.models.fields.CharField')(max_length=1)),
+            ('article_permalink_base', self.gf('django.db.models.fields.CharField')(default='articles', max_length=255)),
+            ('issue_permalink_base', self.gf('django.db.models.fields.CharField')(default='issues', max_length=255)),
+            ('feed_suffix', self.gf('django.db.models.fields.CharField')(default='feed', max_length=255)),
+            ('feeds_enabled', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)),
+        ))
+        db.send_create_signal('penfield', ['NewsletterView'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'Blog'
+        db.delete_table('penfield_blog')
+
+        # Deleting model 'BlogEntry'
+        db.delete_table('penfield_blogentry')
+
+        # Removing M2M table for field tags on 'BlogEntry'
+        db.delete_table('penfield_blogentry_tags')
+
+        # Deleting model 'BlogView'
+        db.delete_table('penfield_blogview')
+
+        # Deleting model 'Newsletter'
+        db.delete_table('penfield_newsletter')
+
+        # Deleting model 'NewsletterArticle'
+        db.delete_table('penfield_newsletterarticle')
+
+        # Removing unique constraint on 'NewsletterArticle', fields ['newsletter', 'slug']
+        db.delete_unique('penfield_newsletterarticle', ['newsletter_id', 'slug'])
+
+        # Removing M2M table for field authors on 'NewsletterArticle'
+        db.delete_table('penfield_newsletterarticle_authors')
+
+        # Removing M2M table for field tags on 'NewsletterArticle'
+        db.delete_table('penfield_newsletterarticle_tags')
+
+        # Deleting model 'NewsletterIssue'
+        db.delete_table('penfield_newsletterissue')
+
+        # Removing unique constraint on 'NewsletterIssue', fields ['newsletter', 'numbering']
+        db.delete_unique('penfield_newsletterissue', ['newsletter_id', 'numbering'])
+
+        # Removing M2M table for field articles on 'NewsletterIssue'
+        db.delete_table('penfield_newsletterissue_articles')
+
+        # Deleting model 'NewsletterView'
+        db.delete_table('penfield_newsletterview')
+
+
+    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'})
+        },
+        person_model: frozen_person,
+        '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['%s']" % person_model}),
+            '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': 'datetime.datetime.now'}),
+            '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_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+            'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False', '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']"}),
+            '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['%s']" % person_model}),
+            'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'full_text': ('philo.models.fields.TemplateField', [], {}),
+            '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_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+            'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False', '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'}),
+            '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'),)", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'json_value': ('django.db.models.fields.TextField', [], {}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.node': {
+            'Meta': {'object_name': 'Node'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.page': {
+            'Meta': {'object_name': 'Page'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.relationship': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Relationship'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'relationship_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'relationship_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+        },
+        'philo.template': {
+            'Meta': {'object_name': 'Template'},
+            'code': ('philo.models.fields.TemplateField', [], {}),
+            'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['penfield']
diff --git a/contrib/penfield/migrations/0002_auto.py b/contrib/penfield/migrations/0002_auto.py
new file mode 100644 (file)
index 0000000..a9ca974
--- /dev/null
@@ -0,0 +1,185 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+from philo.migrations import person_model, frozen_person
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding index on 'NewsletterArticle', fields ['full_text']
+        db.create_index('penfield_newsletterarticle', ['full_text'])
+
+
+    def backwards(self, orm):
+        
+        # Removing index on 'NewsletterArticle', fields ['full_text']
+        db.delete_index('penfield_newsletterarticle', ['full_text'])
+
+
+    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'})
+        },
+        person_model: frozen_person,
+        '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['%s']" % person_model}),
+            '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': 'datetime.datetime.now'}),
+            '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_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+            'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False', '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']"}),
+            '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['%s']" % person_model}),
+            'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            '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_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+            'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False', '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'}),
+            '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'),)", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value': ('philo.models.fields.JSONField', [], {})
+        },
+        'philo.node': {
+            'Meta': {'object_name': 'Node'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.page': {
+            'Meta': {'object_name': 'Page'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.relationship': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Relationship'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'relationship_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'relationship_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+        },
+        'philo.template': {
+            'Meta': {'object_name': 'Template'},
+            'code': ('philo.models.fields.TemplateField', [], {}),
+            'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['penfield']
diff --git a/contrib/penfield/migrations/__init__.py b/contrib/penfield/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
index c9c024c..6da87a6 100644 (file)
@@ -3,9 +3,8 @@ from django.conf import settings
 from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField
 from philo.exceptions import ViewCanNotProvideSubpath
 from django.conf.urls.defaults import url, patterns, include
-from django.core.urlresolvers import reverse
 from django.http import Http404
-from datetime import datetime
+from datetime import date, datetime
 from philo.utils import paginate
 from philo.contrib.penfield.validators import validate_pagination_count
 from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
@@ -75,7 +74,7 @@ class BlogView(MultiView, FeedMultiViewMixin):
        def per_page(self):
                return self.entries_per_page
        
-       def get_subpath(self, obj):
+       def get_reverse_params(self, obj):
                if isinstance(obj, BlogEntry):
                        if obj.blog == self.blog:
                                kwargs = {'slug': obj.slug}
@@ -85,25 +84,17 @@ class BlogView(MultiView, FeedMultiViewMixin):
                                                kwargs.update({'month': str(obj.date.month).zfill(2)})
                                                if self.entry_permalink_style == 'D':
                                                        kwargs.update({'day': str(obj.date.day).zfill(2)})
-                               return reverse(self.entry_view, urlconf=self, kwargs=kwargs)
+                               return self.entry_view, [], kwargs
                elif isinstance(obj, Tag):
                        if obj in self.blog.entry_tags:
-                               return reverse('entries_by_tag', urlconf=self, kwargs={'tag_slugs': obj.slug})
-               elif isinstance(obj, (str, unicode)):
-                       split_obj = obj.split(':')
-                       if len(split_obj) > 1:
-                               kwargs = {}
-                               try:
-                                       kwargs.update({'year': str(int(split_obj[1])).zfill(4)})
-                                       if len(split_obj) > 2:
-                                               kwargs.update({'month': str(int(split_obj[2])).zfill(2)})
-                                               if len(split_obj) > 3:
-                                                       kwargs.update({'day': str(int(split_obj[3])).zfill(2)})
-                                                       return reverse('entries_by_day', urlconf=self, kwargs=kwargs)
-                                               return reverse('entries_by_month', urlconf=self, kwargs=kwargs)
-                                       return reverse('entries_by_year', urlconf=self, kwargs=kwargs)
-                               except:
-                                       pass
+                               return 'entries_by_tag', [], {'tag_slugs': obj.slug}
+               elif isinstance(obj, (date, datetime)):
+                       kwargs = {
+                               'year': str(obj.year).zfill(4),
+                               'month': str(obj.month).zfill(2),
+                               'day': str(obj.day).zfill(2)
+                       }
+                       return 'entries_by_day', [], kwargs
                raise ViewCanNotProvideSubpath
        
        def get_context(self):
@@ -157,10 +148,10 @@ class BlogView(MultiView, FeedMultiViewMixin):
                        )
                return urlpatterns
        
-       def get_all_entries(self, request, node=None, extra_context=None):
+       def get_all_entries(self, request, extra_context=None):
                return self.blog.entries.all(), extra_context
        
-       def get_entries_by_ymd(self, request, year=None, month=None, day=None, node=None, extra_context=None):
+       def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
                if not self.entry_archive_page:
                        raise Http404
                entries = self.blog.entries.all()
@@ -175,7 +166,7 @@ class BlogView(MultiView, FeedMultiViewMixin):
                context.update({'year': year, 'month': month, 'day': day})
                return entries, context
        
-       def get_entries_by_tag(self, request, tag_slugs, node=None, extra_context=None):
+       def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
                tags = []
                for tag_slug in tag_slugs.replace('+', '/').split('/'):
                        if tag_slug: # ignore blank slugs, handles for multiple consecutive separators (+ or /)
@@ -220,7 +211,7 @@ class BlogView(MultiView, FeedMultiViewMixin):
                defaults.update(kwargs or {})
                return super(BlogView, self).get_feed(feed_type, extra_context, defaults)
        
-       def entry_view(self, request, slug, year=None, month=None, day=None, node=None, extra_context=None):
+       def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
                entries = self.blog.entries.all()
                if year:
                        entries = entries.filter(date__year=year)
@@ -235,15 +226,15 @@ class BlogView(MultiView, FeedMultiViewMixin):
                context = self.get_context()
                context.update(extra_context or {})
                context.update({'entry': entry})
-               return self.entry_page.render_to_response(node, request, extra_context=context)
+               return self.entry_page.render_to_response(request, extra_context=context)
        
-       def tag_archive_view(self, request, node=None, extra_context=None):
+       def tag_archive_view(self, request, extra_context=None):
                if not self.tag_archive_page:
                        raise Http404
                context = {}
                context.update(extra_context or {})
                context.update({'blog': self.blog})
-               return self.tag_archive_page.render_to_response(node, request, extra_context=context)
+               return self.tag_archive_page.render_to_response(request, extra_context=context)
 
 
 class Newsletter(Entity, Titled):
@@ -258,7 +249,7 @@ class NewsletterArticle(Entity, Titled):
        authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
        date = models.DateTimeField(default=datetime.now)
        lede = TemplateField(null=True, blank=True, verbose_name='Summary')
-       full_text = TemplateField()
+       full_text = TemplateField(db_index=True)
        tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True)
        
        class Meta:
@@ -310,7 +301,7 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
        def __unicode__(self):
                return self.newsletter.__unicode__()
        
-       def get_subpath(self, obj):
+       def get_reverse_params(self, obj):
                if isinstance(obj, NewsletterArticle):
                        if obj.newsletter == self.newsletter:
                                kwargs = {'slug': obj.slug}
@@ -320,10 +311,17 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
                                                kwargs.update({'month': str(obj.date.month).zfill(2)})
                                                if self.article_permalink_style == 'D':
                                                        kwargs.update({'day': str(obj.date.day).zfill(2)})
-                               return reverse(self.article_view, urlconf=self, kwargs=kwargs)
+                               return self.article_view, [], kwargs
                elif isinstance(obj, NewsletterIssue):
                        if obj.newsletter == self.newsletter:
-                               return reverse('issue', urlconf=self, kwargs={'numbering': obj.numbering})
+                               return 'issue', [], {'numbering': obj.numbering}
+               elif isinstance(obj, (date, datetime)):
+                       kwargs = {
+                               'year': str(obj.year).zfill(4),
+                               'month': str(obj.month).zfill(2),
+                               'day': str(obj.day).zfill(2)
+                       }
+                       return 'articles_by_day', [], kwargs
                raise ViewCanNotProvideSubpath
        
        @property
@@ -375,10 +373,10 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
        def get_context(self):
                return {'newsletter': self.newsletter}
        
-       def get_all_articles(self, request, node, extra_context=None):
+       def get_all_articles(self, request, extra_context=None):
                return self.newsletter.articles.all(), extra_context
        
-       def get_articles_by_ymd(self, request, year, month=None, day=None, node=None, extra_context=None):
+       def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
                articles = self.newsletter.articles.filter(dat__year=year)
                if month:
                        articles = articles.filter(date__month=month)
@@ -386,7 +384,7 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
                        articles = articles.filter(date__day=day)
                return articles
        
-       def get_articles_by_issue(self, request, numbering, node=None, extra_context=None):
+       def get_articles_by_issue(self, request, numbering, extra_context=None):
                try:
                        issue = self.newsletter.issues.get(numbering=numbering)
                except:
@@ -395,7 +393,7 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
                context.update({'issue': issue})
                return issue.articles.all(), context
        
-       def article_view(self, request, slug, year=None, month=None, day=None, node=None, extra_context=None):
+       def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
                articles = self.newsletter.articles.all()
                if year:
                        articles = articles.filter(date__year=year)
@@ -410,15 +408,15 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
                context = self.get_context()
                context.update(extra_context or {})
                context.update({'article': article})
-               return self.article_page.render_to_response(node, request, extra_context=context)
+               return self.article_page.render_to_response(request, extra_context=context)
        
-       def issue_archive_view(self, request, node=None, extra_context=None):
+       def issue_archive_view(self, request, extra_context=None):
                if not self.issue_archive_page:
                        raise Http404
                context = {}
                context.update(extra_context or {})
                context.update({'newsletter': self.newsletter})
-               return self.issue_archive_page.render_to_response(node, request, extra_context=context)
+               return self.issue_archive_page.render_to_response(request, extra_context=context)
        
        def add_item(self, feed, obj, kwargs=None):
                defaults = {
index 8103d9d..0386d38 100644 (file)
@@ -24,8 +24,8 @@ class FeedMultiViewMixin(object):
                """
                Wraps an object-fetching function and renders the results as a page.
                """
-               def inner(request, node=None, extra_context=None, **kwargs):
-                       objects, extra_context = func(request=request, node=node, extra_context=extra_context, **kwargs)
+               def inner(request, extra_context=None, **kwargs):
+                       objects, extra_context = func(request=request, extra_context=extra_context, **kwargs)
 
                        context = self.get_context()
                        context.update(extra_context or {})
@@ -37,7 +37,7 @@ class FeedMultiViewMixin(object):
                        else:
                                context.update({self.list_var: objects})
 
-                       return page.render_to_response(node, request, extra_context=context)
+                       return page.render_to_response(request, extra_context=context)
 
                return inner
        
@@ -45,8 +45,8 @@ class FeedMultiViewMixin(object):
                """
                Wraps an object-fetching function and renders the results as a rss or atom feed.
                """
-               def inner(request, node=None, extra_context=None, **kwargs):
-                       objects, extra_context = func(request=request, node=node, extra_context=extra_context, **kwargs)
+               def inner(request, extra_context=None, **kwargs):
+                       objects, extra_context = func(request=request, extra_context=extra_context, **kwargs)
        
                        if 'HTTP_ACCEPT' in request.META and 'rss' in request.META['HTTP_ACCEPT'] and 'atom' not in request.META['HTTP_ACCEPT']:
                                feed_type = 'rss'
@@ -54,15 +54,15 @@ class FeedMultiViewMixin(object):
                                feed_type = 'atom'
                        
                        current_site = Site.objects.get_current()
-                       
+                       #Could this be done with request.path instead somehow?
                        feed_kwargs = {
-                               'link': 'http://%s/%s/%s/' % (current_site.domain, node.get_absolute_url().strip('/'), reverse(reverse_name, urlconf=self, kwargs=kwargs).strip('/'))
+                               'link': 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse(reverse_name, urlconf=self, kwargs=kwargs).strip('/'))
                        }
                        feed = self.get_feed(feed_type, extra_context, feed_kwargs)
                        
                        for obj in objects:
                                kwargs = {
-                                       'link': 'http://%s/%s/%s/' % (current_site.domain, node.get_absolute_url().strip('/'), self.get_subpath(obj).strip('/'))
+                                       'link': 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), self.get_subpath(obj).strip('/'))
                                }
                                self.add_item(feed, obj, kwargs=kwargs)
        
index efb6504..e2d70ae 100644 (file)
@@ -71,7 +71,7 @@ class LoginMultiView(MultiView):
                context.update(extra_dict or {})
                return context
        
-       def display_login_page(self, request, message, node=None, extra_context=None):
+       def display_login_page(self, request, message, extra_context=None):
                request.session.set_test_cookie()
                
                referrer = request.META.get('HTTP_REFERER', None)
@@ -85,7 +85,7 @@ class LoginMultiView(MultiView):
                                redirect = '%s?%s' % (referrer[2], referrer[4])
                
                if referrer is None:
-                       redirect = node.get_absolute_url()
+                       redirect = request.node.get_absolute_url()
                
                path = request.get_full_path()
                if redirect != path:
@@ -102,14 +102,14 @@ class LoginMultiView(MultiView):
                        'form': form
                })
                context.update(extra_context or {})
-               return self.login_page.render_to_response(node, request, extra_context=context)
+               return self.login_page.render_to_response(request, extra_context=context)
        
-       def login(self, request, node=None, extra_context=None):
+       def login(self, request, extra_context=None):
                """
                Displays the login form for the given HttpRequest.
                """
                if request.user.is_authenticated():
-                       return HttpResponseRedirect(node.get_absolute_url())
+                       return HttpResponseRedirect(request.node.get_absolute_url())
                
                context = self.get_context(extra_context)
                
@@ -121,12 +121,12 @@ class LoginMultiView(MultiView):
                                message = _("Please log in again, because your session has expired.")
                        else:
                                message = ""
-                       return self.display_login_page(request, message, node, context)
+                       return self.display_login_page(request, message, context)
 
                # Check that the user accepts cookies.
                if not request.session.test_cookie_worked():
                        message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
-                       return self.display_login_page(request, message, node, context)
+                       return self.display_login_page(request, message, context)
                else:
                        request.session.delete_test_cookie()
                
@@ -148,7 +148,7 @@ class LoginMultiView(MultiView):
                                                                        " Try '%s' instead.") % user.username
                                        else:
                                                message = _("Usernames cannot contain the '@' character.")
-                       return self.display_login_page(request, message, node, context)
+                       return self.display_login_page(request, message, context)
 
                # The user data is correct; log in the user in and continue.
                else:
@@ -157,21 +157,21 @@ class LoginMultiView(MultiView):
                                try:
                                        redirect = request.session.pop('redirect')
                                except KeyError:
-                                       redirect = node.get_absolute_url()
+                                       redirect = request.node.get_absolute_url()
                                return HttpResponseRedirect(redirect)
                        else:
-                               return self.display_login_page(request, ERROR_MESSAGE, node, context)
+                               return self.display_login_page(request, ERROR_MESSAGE, context)
        login = never_cache(login)
        
        def logout(self, request):
                return auth_views.logout(request, request.META['HTTP_REFERER'])
        
        def login_required(self, view):
-               def inner(request, node=None, *args, **kwargs):
+               def inner(request, *args, **kwargs):
                        if not request.user.is_authenticated():
                                login_url = reverse('login', urlconf=self).strip('/')
-                               return HttpResponseRedirect('%s%s/' % (node.get_absolute_url(), login_url))
-                       return view(request, node=node, *args, **kwargs)
+                               return HttpResponseRedirect('%s%s/' % (request.node.get_absolute_url(), login_url))
+                       return view(request, *args, **kwargs)
                
                return inner
        
@@ -186,9 +186,9 @@ class LoginMultiView(MultiView):
                else:
                        send_mail(subject, text_content, from_email, [email])
        
-       def password_reset(self, request, node=None, extra_context=None, token_generator=password_token_generator):
+       def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
                if request.user.is_authenticated():
-                       return HttpResponseRedirect(node.get_absolute_url())
+                       return HttpResponseRedirect(request.node.get_absolute_url())
                
                if request.method == 'POST':
                        form = PasswordResetForm(request.POST)
@@ -196,7 +196,7 @@ class LoginMultiView(MultiView):
                                current_site = Site.objects.get_current()
                                for user in form.users_cache:
                                        token = token_generator.make_token(user)
-                                       link = 'http://%s/%s/%s/' % (current_site.domain, node.get_absolute_url().strip('/'), reverse('password_reset_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
+                                       link = 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse('password_reset_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
                                        context = {
                                                'link': link,
                                                'username': user.username
@@ -209,9 +209,9 @@ class LoginMultiView(MultiView):
                
                context = self.get_context({'form': form})
                context.update(extra_context or {})
-               return self.password_reset_page.render_to_response(node, request, extra_context=context)
+               return self.password_reset_page.render_to_response(request, extra_context=context)
        
-       def password_reset_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
+       def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
                """
                Checks that a given hash in a password reset link is valid. If so,
                displays the password set form.
@@ -231,16 +231,16 @@ class LoginMultiView(MultiView):
                                if form.is_valid():
                                        form.save()
                                        messages.add_message(request, messages.SUCCESS, "Password reset successful.")
-                                       return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('login', urlconf=self).strip('/')))
+                                       return HttpResponseRedirect('/%s/%s/' % (request.node.get_absolute_url().strip('/'), reverse('login', urlconf=self).strip('/')))
                        else:
                                form = SetPasswordForm(user)
                        
                        context = self.get_context({'form': form})
-                       return self.password_set_page.render_to_response(node, request, extra_context=context)
+                       return self.password_set_page.render_to_response(request, extra_context=context)
                
                raise Http404
        
-       def password_change(self, request, node=None, extra_context=None):
+       def password_change(self, request, extra_context=None):
                if request.method == 'POST':
                        form = PasswordChangeForm(request.user, request.POST)
                        if form.is_valid():
@@ -252,11 +252,11 @@ class LoginMultiView(MultiView):
                
                context = self.get_context({'form': form})
                context.update(extra_context or {})
-               return self.password_change_page.render_to_response(node, request, extra_context=context)
+               return self.password_change_page.render_to_response(request, extra_context=context)
        
-       def register(self, request, node=None, extra_context=None, token_generator=registration_token_generator):
+       def register(self, request, extra_context=None, token_generator=registration_token_generator):
                if request.user.is_authenticated():
-                       return HttpResponseRedirect(node.get_absolute_url())
+                       return HttpResponseRedirect(request.node.get_absolute_url())
                
                if request.method == 'POST':
                        form = RegistrationForm(request.POST)
@@ -264,21 +264,21 @@ class LoginMultiView(MultiView):
                                user = form.save()
                                current_site = Site.objects.get_current()
                                token = token_generator.make_token(user)
-                               link = 'http://%s/%s/%s/' % (current_site.domain, node.get_absolute_url().strip('/'), reverse('register_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
+                               link = 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse('register_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
                                context = {
                                        'link': link
                                }
                                self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
                                messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
-                               return HttpResponseRedirect(node.get_absolute_url())
+                               return HttpResponseRedirect(request.node.get_absolute_url())
                else:
                        form = RegistrationForm()
                
                context = self.get_context({'form': form})
                context.update(extra_context or {})
-               return self.register_page.render_to_response(node, request, extra_context=context)
+               return self.register_page.render_to_response(request, extra_context=context)
        
-       def register_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
+       def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
                """
                Checks that a given hash in a registration link is valid and activates
                the given account. If so, log them in and redirect to
@@ -303,12 +303,12 @@ class LoginMultiView(MultiView):
                                # if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
                                user.password = true_password
                                user.save()
-                       return self.post_register_confirm_redirect(request, node)
+                       return self.post_register_confirm_redirect(request)
                
                raise Http404
        
-       def post_register_confirm_redirect(self, request, node):
-               return HttpResponseRedirect(node.get_absolute_url())
+       def post_register_confirm_redirect(self, request):
+               return HttpResponseRedirect(request.node.get_absolute_url())
        
        class Meta:
                abstract = True
@@ -363,7 +363,7 @@ class AccountMultiView(LoginMultiView):
                
                return form_instances
        
-       def account_view(self, request, node=None, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
+       def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
                if request.method == 'POST':
                        form_instances = self.get_account_form_instances(request.user, request.POST)
                        current_email = request.user.email
@@ -383,7 +383,7 @@ class AccountMultiView(LoginMultiView):
                                        
                                        current_site = Site.objects.get_current()
                                        token = token_generator.make_token(request.user, email)
-                                       link = 'http://%s/%s/%s/' % (current_site.domain, node.get_absolute_url().strip('/'), reverse('email_change_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(request.user.id), 'email': email.replace('@', '+'), 'token': token}).strip('/'))
+                                       link = 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse('email_change_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(request.user.id), 'email': email.replace('@', '+'), 'token': token}).strip('/'))
                                        context = {
                                                'link': link
                                        }
@@ -401,7 +401,7 @@ class AccountMultiView(LoginMultiView):
                        'forms': form_instances
                })
                context.update(extra_context or {})
-               return self.manage_account_page.render_to_response(node, request, extra_context=context)
+               return self.manage_account_page.render_to_response(request, extra_context=context)
        
        def has_valid_account(self, user):
                user_form, profile_form = self.get_account_forms()
@@ -428,11 +428,11 @@ class AccountMultiView(LoginMultiView):
                inner = self.login_required(inner)
                return inner
        
-       def post_register_confirm_redirect(self, request, node):
+       def post_register_confirm_redirect(self, request):
                messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
-               return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
+               return HttpResponseRedirect('/%s/%s/' % (request.node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
        
-       def email_change_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
+       def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
                """
                Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
                """
@@ -455,7 +455,7 @@ class AccountMultiView(LoginMultiView):
                        user.email = email
                        user.save()
                        messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
-                       return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
+                       return HttpResponseRedirect('/%s/%s/' % (request.node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
                
                raise Http404
        
index 4759bae..1e4b9d9 100644 (file)
@@ -1,3 +1,9 @@
+from django.core.exceptions import ImproperlyConfigured
+
+
+MIDDLEWARE_NOT_CONFIGURED = ImproperlyConfigured("""Philo requires the RequestNode middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'philo.middleware.RequestNodeMiddleware'.""")
+
+
 class ViewDoesNotProvideSubpaths(Exception):
        """ Raised by get_subpath when the View does not provide subpaths (the default). """
        silent_variable_failure = True
diff --git a/fixtures/test_fixtures.json b/fixtures/test_fixtures.json
new file mode 100644 (file)
index 0000000..18f6962
--- /dev/null
@@ -0,0 +1,219 @@
+[
+    {
+        "pk": 1, 
+        "model": "philo.tag", 
+        "fields": {
+            "name": "Test tag", 
+            "slug": "test-tag"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "never", 
+            "parent": 3, 
+            "view_content_type": [
+                "philo", 
+                "page"
+            ]
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "blog", 
+            "parent": 3, 
+            "view_content_type": [
+                "penfield", 
+                "blogview"
+            ]
+        }
+    }, 
+    {
+        "pk": 3, 
+        "model": "philo.node", 
+        "fields": {
+            "view_object_id": 1, 
+            "slug": "root", 
+            "parent": null, 
+            "view_content_type": [
+                "philo", 
+                "redirect"
+            ]
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "philo.redirect", 
+        "fields": {
+            "status_code": 302, 
+            "target": "never"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "Never is working!\r\n{% node_url %}", 
+            "name": "Never", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "never"
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "An index page!\r\n{% node_url %}\r\n\r\n{% for entry in entries %}\r\n<h4><a href='{% node_url with entry %}'>{{ entry.title }}</a></h4>\r\n<div class='post content'>\r\n{{ entry.content }}\r\n</div>\r\n{% endfor %}", 
+            "name": "Index", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "index"
+        }
+    }, 
+    {
+        "pk": 3, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "Entry detail page.", 
+            "name": "Entry", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "entry"
+        }
+    }, 
+    {
+        "pk": 4, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "Tag page!", 
+            "name": "Tag", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "tag"
+        }
+    }, 
+    {
+        "pk": 5, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "Entry archive page!", 
+            "name": "Entry Archives!", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "entry-archives"
+        }
+    }, 
+    {
+        "pk": 6, 
+        "model": "philo.template", 
+        "fields": {
+            "mimetype": "text/html", 
+            "code": "tag archives...", 
+            "name": "Tag Archives", 
+            "parent": null, 
+            "documentation": "", 
+            "slug": "tag-archives"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 1, 
+            "title": "Never"
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 2, 
+            "title": "Index"
+        }
+    }, 
+    {
+        "pk": 3, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 3, 
+            "title": "Entry"
+        }
+    }, 
+    {
+        "pk": 4, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 4, 
+            "title": "Tag"
+        }
+    }, 
+    {
+        "pk": 5, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 5, 
+            "title": "Entry Archive"
+        }
+    }, 
+    {
+        "pk": 6, 
+        "model": "philo.page", 
+        "fields": {
+            "template": 6, 
+            "title": "Tag Archive Page"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "penfield.blog", 
+        "fields": {
+            "slug": "free-lovin", 
+            "title": "Free lovin'"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "penfield.blogentry", 
+        "fields": {
+            "content": "Lorem ipsum.\r\n\r\nDolor sit amet.", 
+            "author": 1, 
+            "title": "First Entry", 
+            "excerpt": "", 
+            "blog": 1, 
+            "date": "2010-10-20 10:38:58", 
+            "slug": "first-entry", 
+            "tags": [
+                1
+            ]
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "penfield.blogview", 
+        "fields": {
+            "entry_archive_page": 5, 
+            "tag_page": 4, 
+            "feed_suffix": "feed", 
+            "entry_permalink_style": "D", 
+            "tag_permalink_base": "tags", 
+            "feeds_enabled": true, 
+            "entries_per_page": null, 
+            "tag_archive_page": 6, 
+            "blog": 1, 
+            "entry_permalink_base": "entries", 
+            "index_page": 2, 
+            "entry_page": 3
+        }
+    }
+]
index ced29b2..a1785fb 100644 (file)
--- a/forms.py
+++ b/forms.py
@@ -1,5 +1,7 @@
 from django import forms
 from django.contrib.admin.widgets import AdminTextareaWidget
+from django.contrib.contenttypes.generic import BaseGenericInlineFormSet
+from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError, ObjectDoesNotExist
 from django.db.models import Q
 from django.forms.models import model_to_dict, fields_for_model, ModelFormMetaclass, ModelForm, BaseInlineFormSet
@@ -7,8 +9,7 @@ from django.forms.formsets import TOTAL_FORM_COUNT
 from django.template import loader, loader_tags, TemplateDoesNotExist, Context, Template as DjangoTemplate
 from django.utils.datastructures import SortedDict
 from philo.admin.widgets import ModelLookupWidget
-from philo.models import Entity, Template, Contentlet, ContentReference
-from philo.models.fields import RelationshipField
+from philo.models import Entity, Template, Contentlet, ContentReference, Attribute
 from philo.utils import fattr
 
 
@@ -20,6 +21,8 @@ def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widge
        ignored = []
        opts = entity_model._entity_meta
        for f in opts.proxy_fields:
+               if not f.editable:
+                       continue
                if fields and not f.name in fields:
                        continue
                if exclude and f.name in exclude:
@@ -85,8 +88,12 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it
                instance = super(EntityForm, self).save(commit=False)
                
                for f in instance._entity_meta.proxy_fields:
+                       if not f.editable or not f.name in cleaned_data:
+                               continue
                        if self._meta.fields and f.name not in self._meta.fields:
                                continue
+                       if self._meta.exclude and f.name in self._meta.exclude:
+                               continue
                        setattr(instance, f.attname, cleaned_data[f.name])
                
                if commit:
@@ -96,6 +103,69 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it
                return instance
 
 
+class AttributeForm(ModelForm):
+       def __init__(self, *args, **kwargs):
+               super(AttributeForm, self).__init__(*args, **kwargs)
+               
+               # This is necessary because model forms store changes to self.instance in their clean method.
+               # Mutter mutter.
+               self._cached_value_ct = self.instance.value_content_type
+               self._cached_value = self.instance.value
+               
+               if self.instance.value is not None:
+                       value_field = self.instance.value.value_formfield()
+                       if value_field:
+                               self.fields['value'] = value_field
+                       if hasattr(self.instance.value, 'content_type'):
+                               self.fields['content_type'] = self.instance.value._meta.get_field('content_type').formfield(initial=getattr(self.instance.value.content_type, 'pk', None))
+       
+       def save(self, *args, **kwargs):
+               # At this point, the cleaned_data has already been stored on self.instance.
+               if self.instance.value_content_type != self._cached_value_ct:
+                       if self.instance.value is not None:
+                               self._cached_value.delete()
+                               if 'value' in self.cleaned_data:
+                                       del(self.cleaned_data['value'])
+                       
+                       if self.instance.value_content_type is not None:
+                               # Make a blank value of the new type! Run special code for content_type attributes.
+                               if hasattr(self.instance.value_content_type.model_class(), 'content_type'):
+                                       if self._cached_value and hasattr(self._cached_value, 'content_type'):
+                                               new_ct = self._cached_value.content_type
+                                       else:
+                                               new_ct = None
+                                       new_value = self.instance.value_content_type.model_class().objects.create(content_type=new_ct)
+                               else:
+                                       new_value = self.instance.value_content_type.model_class().objects.create()
+                               
+                               new_value.apply_data(self.cleaned_data)
+                               new_value.save()
+                               self.instance.value = new_value
+               else:
+                       # The value type is the same, but one of the fields has changed.
+                       # Check to see if the changed value was the content type. We have to check the
+                       # cleaned_data because self.instance.value.content_type was overridden.
+                       if hasattr(self.instance.value, 'content_type') and 'content_type' in self.cleaned_data and 'value' in self.cleaned_data and (not hasattr(self._cached_value, 'content_type') or self._cached_value.content_type != self.cleaned_data['content_type']):
+                               self.cleaned_data['value'] = None
+                       
+                       self.instance.value.apply_data(self.cleaned_data)
+                       self.instance.value.save()
+               
+               super(AttributeForm, self).save(*args, **kwargs)
+               return self.instance
+       
+       class Meta:
+               model = Attribute
+
+
+class AttributeInlineFormSet(BaseGenericInlineFormSet):
+       "Necessary to force the GenericInlineFormset to use the form's save method for new objects."
+       def save_new(self, form, commit):
+               setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk)
+               setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk)
+               return form.save()
+
+
 class ContainerForm(ModelForm):
        def __init__(self, *args, **kwargs):
                super(ContainerForm, self).__init__(*args, **kwargs)
diff --git a/middleware.py b/middleware.py
new file mode 100644 (file)
index 0000000..8c208e4
--- /dev/null
@@ -0,0 +1,35 @@
+from django.contrib.sites.models import Site
+from philo.models import Node
+
+
+class LazyNode(object):
+       def __get__(self, request, obj_type=None):
+               if not hasattr(request, '_cached_node_path'):
+                       return None
+               
+               if not hasattr(request, '_found_node'):
+                       try:
+                               current_site = Site.objects.get_current()
+                       except Site.DoesNotExist:
+                               current_site = None
+                       
+                       try:
+                               node, subpath = Node.objects.get_with_path(request._cached_node_path, root=getattr(current_site, 'root_node', None), absolute_result=False)
+                       except Node.DoesNotExist:
+                               node = None
+                       
+                       if node:
+                               node.subpath = subpath
+                       
+                       request._found_node = node
+               
+               return request._found_node
+
+
+class RequestNodeMiddleware(object):
+       """Middleware to process the request's path and attach the closest ancestor node."""
+       def process_request(self, request):
+               request.__class__.node = LazyNode()
+       
+       def process_view(self, request, view_func, view_args, view_kwargs):
+               request._cached_node_path = view_kwargs.get('path', '/')
\ No newline at end of file
diff --git a/migrations/0001_initial.py b/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..178a7d3
--- /dev/null
@@ -0,0 +1,273 @@
+# 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 model 'Tag'
+        db.create_table('philo_tag', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=255, db_index=True)),
+        ))
+        db.send_create_signal('philo', ['Tag'])
+
+        # Adding model 'Attribute'
+        db.create_table('philo_attribute', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('entity_content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
+            ('entity_object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('key', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('json_value', self.gf('django.db.models.fields.TextField')()),
+        ))
+        db.send_create_signal('philo', ['Attribute'])
+
+        # Adding unique constraint on 'Attribute', fields ['key', 'entity_content_type', 'entity_object_id']
+        db.create_unique('philo_attribute', ['key', 'entity_content_type_id', 'entity_object_id'])
+
+        # Adding model 'Relationship'
+        db.create_table('philo_relationship', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('entity_content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='relationship_entity_set', to=orm['contenttypes.ContentType'])),
+            ('entity_object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('key', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('value_content_type', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='relationship_value_set', null=True, to=orm['contenttypes.ContentType'])),
+            ('value_object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)),
+        ))
+        db.send_create_signal('philo', ['Relationship'])
+
+        # Adding unique constraint on 'Relationship', fields ['key', 'entity_content_type', 'entity_object_id']
+        db.create_unique('philo_relationship', ['key', 'entity_content_type_id', 'entity_object_id'])
+
+        # Adding model 'Collection'
+        db.create_table('philo_collection', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+        ))
+        db.send_create_signal('philo', ['Collection'])
+
+        # Adding model 'CollectionMember'
+        db.create_table('philo_collectionmember', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('collection', self.gf('django.db.models.fields.related.ForeignKey')(related_name='members', to=orm['philo.Collection'])),
+            ('index', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)),
+            ('member_content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
+            ('member_object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
+        ))
+        db.send_create_signal('philo', ['CollectionMember'])
+
+        # Adding model 'Node'
+        db.create_table('philo_node', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['philo.Node'])),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
+            ('view_content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='node_view_set', to=orm['contenttypes.ContentType'])),
+            ('view_object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
+        ))
+        db.send_create_signal('philo', ['Node'])
+
+        # Adding model 'Redirect'
+        db.create_table('philo_redirect', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('target', self.gf('django.db.models.fields.CharField')(max_length=200)),
+            ('status_code', self.gf('django.db.models.fields.IntegerField')(default=302)),
+        ))
+        db.send_create_signal('philo', ['Redirect'])
+
+        # Adding model 'File'
+        db.create_table('philo_file', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('mimetype', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('file', self.gf('django.db.models.fields.files.FileField')(max_length=100)),
+        ))
+        db.send_create_signal('philo', ['File'])
+
+        # Adding model 'Template'
+        db.create_table('philo_template', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['philo.Template'])),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, db_index=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('documentation', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+            ('mimetype', self.gf('django.db.models.fields.CharField')(default='text/html', max_length=255)),
+            ('code', self.gf('philo.models.fields.TemplateField')()),
+        ))
+        db.send_create_signal('philo', ['Template'])
+
+        # Adding model 'Page'
+        db.create_table('philo_page', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('template', self.gf('django.db.models.fields.related.ForeignKey')(related_name='pages', to=orm['philo.Template'])),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+        ))
+        db.send_create_signal('philo', ['Page'])
+
+        # Adding model 'Contentlet'
+        db.create_table('philo_contentlet', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='contentlets', to=orm['philo.Page'])),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('content', self.gf('philo.models.fields.TemplateField')()),
+        ))
+        db.send_create_signal('philo', ['Contentlet'])
+
+        # Adding model 'ContentReference'
+        db.create_table('philo_contentreference', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='contentreferences', to=orm['philo.Page'])),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
+            ('content_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)),
+        ))
+        db.send_create_signal('philo', ['ContentReference'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'Tag'
+        db.delete_table('philo_tag')
+
+        # Deleting model 'Attribute'
+        db.delete_table('philo_attribute')
+
+        # Removing unique constraint on 'Attribute', fields ['key', 'entity_content_type', 'entity_object_id']
+        db.delete_unique('philo_attribute', ['key', 'entity_content_type_id', 'entity_object_id'])
+
+        # Deleting model 'Relationship'
+        db.delete_table('philo_relationship')
+
+        # Removing unique constraint on 'Relationship', fields ['key', 'entity_content_type', 'entity_object_id']
+        db.delete_unique('philo_relationship', ['key', 'entity_content_type_id', 'entity_object_id'])
+
+        # Deleting model 'Collection'
+        db.delete_table('philo_collection')
+
+        # Deleting model 'CollectionMember'
+        db.delete_table('philo_collectionmember')
+
+        # Deleting model 'Node'
+        db.delete_table('philo_node')
+
+        # Deleting model 'Redirect'
+        db.delete_table('philo_redirect')
+
+        # Deleting model 'File'
+        db.delete_table('philo_file')
+
+        # Deleting model 'Template'
+        db.delete_table('philo_template')
+
+        # Deleting model 'Page'
+        db.delete_table('philo_page')
+
+        # Deleting model 'Contentlet'
+        db.delete_table('philo_contentlet')
+
+        # Deleting model 'ContentReference'
+        db.delete_table('philo_contentreference')
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'philo.attribute': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'json_value': ('django.db.models.fields.TextField', [], {}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.collection': {
+            'Meta': {'object_name': 'Collection'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.collectionmember': {
+            'Meta': {'object_name': 'CollectionMember'},
+            'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.contentlet': {
+            'Meta': {'object_name': 'Contentlet'},
+            'content': ('philo.models.fields.TemplateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+        },
+        'philo.contentreference': {
+            'Meta': {'object_name': 'ContentReference'},
+            'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+        },
+        'philo.file': {
+            'Meta': {'object_name': 'File'},
+            'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.node': {
+            'Meta': {'object_name': 'Node'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.page': {
+            'Meta': {'object_name': 'Page'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.redirect': {
+            'Meta': {'object_name': 'Redirect'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        'philo.relationship': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Relationship'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'relationship_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'relationship_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+        },
+        'philo.template': {
+            'Meta': {'object_name': 'Template'},
+            'code': ('philo.models.fields.TemplateField', [], {}),
+            'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['philo']
diff --git a/migrations/0002_auto__add_field_attribute_value.py b/migrations/0002_auto__add_field_attribute_value.py
new file mode 100644 (file)
index 0000000..df3aaba
--- /dev/null
@@ -0,0 +1,120 @@
+# 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 'Attribute.value'
+        db.add_column('philo_attribute', 'value', self.gf('philo.models.fields.JSONField')(default=''), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'Attribute.value'
+        db.delete_column('philo_attribute', 'value_json')
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'philo.attribute': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'json_value': ('django.db.models.fields.TextField', [], {}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value': ('philo.models.fields.JSONField', [], {})
+        },
+        'philo.collection': {
+            'Meta': {'object_name': 'Collection'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.collectionmember': {
+            'Meta': {'object_name': 'CollectionMember'},
+            'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.contentlet': {
+            'Meta': {'object_name': 'Contentlet'},
+            'content': ('philo.models.fields.TemplateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+        },
+        'philo.contentreference': {
+            'Meta': {'object_name': 'ContentReference'},
+            'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+        },
+        'philo.file': {
+            'Meta': {'object_name': 'File'},
+            'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.node': {
+            'Meta': {'object_name': 'Node'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.page': {
+            'Meta': {'object_name': 'Page'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.redirect': {
+            'Meta': {'object_name': 'Redirect'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        'philo.relationship': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Relationship'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'relationship_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'relationship_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+        },
+        'philo.template': {
+            'Meta': {'object_name': 'Template'},
+            'code': ('philo.models.fields.TemplateField', [], {}),
+            'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['philo']
diff --git a/migrations/0003_move_json.py b/migrations/0003_move_json.py
new file mode 100644 (file)
index 0000000..70bcd60
--- /dev/null
@@ -0,0 +1,122 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+        "Write your forwards methods here."
+        for attribute in orm.Attribute.objects.all():
+            attribute.value_json = attribute.json_value
+            attribute.save()
+
+
+    def backwards(self, orm):
+        "Write your backwards methods here."
+        for attribute in orm.Attribute.objects.all():
+            attribute.json_value = attribute.value_json
+            attribute.save()
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'philo.attribute': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'json_value': ('django.db.models.fields.TextField', [], {}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value': ('philo.models.fields.JSONField', [], {})
+        },
+        'philo.collection': {
+            'Meta': {'object_name': 'Collection'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.collectionmember': {
+            'Meta': {'object_name': 'CollectionMember'},
+            'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.contentlet': {
+            'Meta': {'object_name': 'Contentlet'},
+            'content': ('philo.models.fields.TemplateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+        },
+        'philo.contentreference': {
+            'Meta': {'object_name': 'ContentReference'},
+            'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+        },
+        'philo.file': {
+            'Meta': {'object_name': 'File'},
+            'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.node': {
+            'Meta': {'object_name': 'Node'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.page': {
+            'Meta': {'object_name': 'Page'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.redirect': {
+            'Meta': {'object_name': 'Redirect'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        'philo.relationship': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Relationship'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'relationship_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'relationship_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+        },
+        'philo.template': {
+            'Meta': {'object_name': 'Template'},
+            'code': ('philo.models.fields.TemplateField', [], {}),
+            'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['philo']
diff --git a/migrations/0004_auto__del_field_attribute_json_value.py b/migrations/0004_auto__del_field_attribute_json_value.py
new file mode 100644 (file)
index 0000000..2cfc222
--- /dev/null
@@ -0,0 +1,119 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Deleting field 'Attribute.json_value'
+        db.delete_column('philo_attribute', 'json_value')
+
+
+    def backwards(self, orm):
+        
+        # Adding field 'Attribute.json_value'
+        db.add_column('philo_attribute', 'json_value', self.gf('django.db.models.fields.TextField')(default=''), keep_default=False)
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'philo.attribute': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value': ('philo.models.fields.JSONField', [], {})
+        },
+        'philo.collection': {
+            'Meta': {'object_name': 'Collection'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.collectionmember': {
+            'Meta': {'object_name': 'CollectionMember'},
+            'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.contentlet': {
+            'Meta': {'object_name': 'Contentlet'},
+            'content': ('philo.models.fields.TemplateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+        },
+        'philo.contentreference': {
+            'Meta': {'object_name': 'ContentReference'},
+            'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+        },
+        'philo.file': {
+            'Meta': {'object_name': 'File'},
+            'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.node': {
+            'Meta': {'object_name': 'Node'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.page': {
+            'Meta': {'object_name': 'Page'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.redirect': {
+            'Meta': {'object_name': 'Redirect'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        'philo.relationship': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Relationship'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'relationship_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'relationship_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+        },
+        'philo.template': {
+            'Meta': {'object_name': 'Template'},
+            'code': ('philo.models.fields.TemplateField', [], {}),
+            'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['philo']
diff --git a/migrations/0005_add_attribute_values.py b/migrations/0005_add_attribute_values.py
new file mode 100644 (file)
index 0000000..9bea73f
--- /dev/null
@@ -0,0 +1,186 @@
+# 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 model 'ManyToManyValue'
+        db.create_table('philo_manytomanyvalue', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='many_to_many_value_set', null=True, to=orm['contenttypes.ContentType'])),
+            ('object_ids', self.gf('django.db.models.fields.CommaSeparatedIntegerField')(max_length=300, null=True, blank=True)),
+        ))
+        db.send_create_signal('philo', ['ManyToManyValue'])
+
+        # Adding model 'JSONValue'
+        db.create_table('philo_jsonvalue', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('value', self.gf('philo.models.fields.JSONField')()),
+        ))
+        db.send_create_signal('philo', ['JSONValue'])
+
+        # Adding model 'ForeignKeyValue'
+        db.create_table('philo_foreignkeyvalue', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='foreign_key_value_set', null=True, to=orm['contenttypes.ContentType'])),
+            ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)),
+        ))
+        db.send_create_signal('philo', ['ForeignKeyValue'])
+
+        # Adding field 'Attribute.value_content_type'
+        db.add_column('philo_attribute', 'value_content_type', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='attribute_value_set', null=True, to=orm['contenttypes.ContentType']), keep_default=False)
+
+        # Adding field 'Attribute.value_object_id'
+        db.add_column('philo_attribute', 'value_object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True), keep_default=False)
+
+        # Adding unique constraint on 'Attribute', fields ['value_object_id', 'value_content_type']
+        db.create_unique('philo_attribute', ['value_object_id', 'value_content_type_id'])
+
+        # Manual addition! This is necessary to immediately cause contenttype creation.
+        # (needed for the next migration)
+        db.send_pending_create_signals()
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'ManyToManyValue'
+        db.delete_table('philo_manytomanyvalue')
+
+        # Deleting model 'JSONValue'
+        db.delete_table('philo_jsonvalue')
+
+        # Deleting model 'ForeignKeyValue'
+        db.delete_table('philo_foreignkeyvalue')
+
+        # Deleting field 'Attribute.value_content_type'
+        db.delete_column('philo_attribute', 'value_content_type_id')
+
+        # Deleting field 'Attribute.value_object_id'
+        db.delete_column('philo_attribute', 'value_object_id')
+
+        # Removing unique constraint on 'Attribute', fields ['value_object_id', 'value_content_type']
+        db.delete_unique('philo_attribute', ['value_object_id', 'value_content_type_id'])
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'philo.attribute': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('philo.models.fields.JSONField', [], {})
+        },
+        'philo.collection': {
+            'Meta': {'object_name': 'Collection'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.collectionmember': {
+            'Meta': {'object_name': 'CollectionMember'},
+            'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.contentlet': {
+            'Meta': {'object_name': 'Contentlet'},
+            'content': ('philo.models.fields.TemplateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+        },
+        'philo.contentreference': {
+            'Meta': {'object_name': 'ContentReference'},
+            'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+        },
+        'philo.file': {
+            'Meta': {'object_name': 'File'},
+            'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.foreignkeyvalue': {
+            'Meta': {'object_name': 'ForeignKeyValue'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'foreign_key_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.jsonvalue': {
+            'Meta': {'object_name': 'JSONValue'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'value': ('philo.models.fields.JSONField', [], {})
+        },
+        'philo.manytomanyvalue': {
+            'Meta': {'object_name': 'ManyToManyValue'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'many_to_many_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_ids': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'max_length': '300', 'null': 'True', 'blank': 'True'})
+        },
+        'philo.node': {
+            'Meta': {'object_name': 'Node'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.page': {
+            'Meta': {'object_name': 'Page'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.redirect': {
+            'Meta': {'object_name': 'Redirect'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        'philo.relationship': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Relationship'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'relationship_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'relationship_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+        },
+        'philo.template': {
+            'Meta': {'object_name': 'Template'},
+            'code': ('philo.models.fields.TemplateField', [], {}),
+            'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['philo']
diff --git a/migrations/0006_move_attribute_and_relationship_values.py b/migrations/0006_move_attribute_and_relationship_values.py
new file mode 100644 (file)
index 0000000..1cb0be9
--- /dev/null
@@ -0,0 +1,173 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+       def forwards(self, orm):
+               "Write your forwards methods here."
+               ContentType = orm['contenttypes.ContentType']
+               json_ct = ContentType.objects.get(app_label='philo', model='jsonvalue')
+               fk_ct = ContentType.objects.get(app_label='philo', model='foreignkeyvalue')
+               
+               for attribute in orm.Attribute.objects.all():
+                       value = orm.JSONValue(value_json=attribute.value_json)
+                       value.save()
+                       attribute.value_content_type = json_ct
+                       attribute.value_object_id = value.id
+                       attribute.save()
+               
+               for relationship in orm.Relationship.objects.all():
+                       attribute = orm.Attribute(entity_content_type=relationship.entity_content_type, entity_object_id=relationship.entity_object_id, key=relationship.key)
+                       value = orm.ForeignKeyValue(content_type=relationship.value_content_type, object_id=relationship.value_object_id)
+                       value.save()
+                       attribute.value_content_type = fk_ct
+                       attribute.value_object_id = value.id
+                       attribute.save()
+                       relationship.delete()
+
+
+       def backwards(self, orm):
+               "Write your backwards methods here."
+               if orm.ManyToManyValue.objects.count():
+                       raise NotImplementedError("ManyToManyValue objects cannot be unmigrated.")
+               
+               ContentType = orm['contenttypes.ContentType']
+               json_ct = ContentType.objects.get(app_label='philo', model='jsonvalue')
+               fk_ct = ContentType.objects.get(app_label='philo', model='foreignkeyvalue')
+               
+               for attribute in orm.Attribute.objects.all():
+                       if attribute.value_content_type == json_ct:
+                               value = orm.JSONValue.objects.get(pk=attribute.value_object_id)
+                               attribute.value_json = value.value_json
+                               attribute.save()
+                       elif attribute.value_content_type == fk_ct:
+                               value = orm.ForeignKeyValue.objects.get(pk=attribute.value_object_id)
+                               relationship = orm.Relationship(value_content_type=value.content_type, value_object_id=value.object_id, key=attribute.key, entity_content_type=attribute.entity_content_type, entity_object_id=attribute.entity_object_id)
+                               relationship.save()
+                               attribute.delete()
+                       else:
+                               ct = attribute.value_content_type
+                               raise NotImplementedError("Class cannot be converted: %s.%s" % (ct.app_label, ct.model))
+
+
+       models = {
+               'contenttypes.contenttype': {
+                       'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+                       'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+                       'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+               },
+               'philo.attribute': {
+                       'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+                       'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+                       'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+                       'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+                       'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('philo.models.fields.JSONField', [], {})
+               },
+               'philo.collection': {
+                       'Meta': {'object_name': 'Collection'},
+                       'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+               },
+               'philo.collectionmember': {
+                       'Meta': {'object_name': 'CollectionMember'},
+                       'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+                       'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+                       'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+               },
+               'philo.contentlet': {
+                       'Meta': {'object_name': 'Contentlet'},
+                       'content': ('philo.models.fields.TemplateField', [], {}),
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+                       'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+               },
+               'philo.contentreference': {
+                       'Meta': {'object_name': 'ContentReference'},
+                       'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+                       'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+                       'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+               },
+               'philo.file': {
+                       'Meta': {'object_name': 'File'},
+                       'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+               },
+               'philo.foreignkeyvalue': {
+                       'Meta': {'object_name': 'ForeignKeyValue'},
+                       'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'foreign_key_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+               },
+               'philo.jsonvalue': {
+                       'Meta': {'object_name': 'JSONValue'},
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'value': ('philo.models.fields.JSONField', [], {})
+               },
+               'philo.manytomanyvalue': {
+                       'Meta': {'object_name': 'ManyToManyValue'},
+                       'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'many_to_many_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'object_ids': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'max_length': '300', 'null': 'True', 'blank': 'True'})
+               },
+               'philo.node': {
+                       'Meta': {'object_name': 'Node'},
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+                       'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+                       'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+                       'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+               },
+               'philo.page': {
+                       'Meta': {'object_name': 'Page'},
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+                       'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+               },
+               'philo.redirect': {
+                       'Meta': {'object_name': 'Redirect'},
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+                       'target': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+               },
+        'philo.relationship': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'),)", 'object_name': 'Relationship'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'relationship_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'relationship_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+               'philo.tag': {
+                       'Meta': {'object_name': 'Tag'},
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+                       'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+               },
+               'philo.template': {
+                       'Meta': {'object_name': 'Template'},
+                       'code': ('philo.models.fields.TemplateField', [], {}),
+                       'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+                       'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                       'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+                       'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+                       'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+                       'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
+               }
+       }
+
+       complete_apps = ['philo']
diff --git a/migrations/0007_auto__del_relationship__del_field_attribute_value.py b/migrations/0007_auto__del_relationship__del_field_attribute_value.py
new file mode 100644 (file)
index 0000000..f94d316
--- /dev/null
@@ -0,0 +1,142 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Deleting model 'Relationship'
+        db.delete_table('philo_relationship')
+
+        # Deleting field 'Attribute.value'
+        db.delete_column('philo_attribute', 'value_json')
+
+
+    def backwards(self, orm):
+        
+        # Adding model 'Relationship'
+        db.create_table('philo_relationship', (
+            ('key', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('entity_content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='relationship_entity_set', to=orm['contenttypes.ContentType'])),
+            ('entity_object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
+            ('value_object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)),
+            ('value_content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='relationship_value_set', null=True, to=orm['contenttypes.ContentType'], blank=True)),
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+        ))
+        db.send_create_signal('philo', ['Relationship'])
+
+        # Adding field 'Attribute.value'
+        db.add_column('philo_attribute', 'value', self.gf('philo.models.fields.JSONField')(default='null'), keep_default=False)
+
+
+    models = {
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'philo.attribute': {
+            'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+            'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.collection': {
+            'Meta': {'object_name': 'Collection'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.collectionmember': {
+            'Meta': {'object_name': 'CollectionMember'},
+            'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.contentlet': {
+            'Meta': {'object_name': 'Contentlet'},
+            'content': ('philo.models.fields.TemplateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+        },
+        'philo.contentreference': {
+            'Meta': {'object_name': 'ContentReference'},
+            'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+        },
+        'philo.file': {
+            'Meta': {'object_name': 'File'},
+            'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.foreignkeyvalue': {
+            'Meta': {'object_name': 'ForeignKeyValue'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'foreign_key_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'philo.jsonvalue': {
+            'Meta': {'object_name': 'JSONValue'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'value': ('philo.models.fields.JSONField', [], {})
+        },
+        'philo.manytomanyvalue': {
+            'Meta': {'object_name': 'ManyToManyValue'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'many_to_many_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_ids': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'max_length': '300', 'null': 'True', 'blank': 'True'})
+        },
+        'philo.node': {
+            'Meta': {'object_name': 'Node'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+            'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+            'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+        },
+        'philo.page': {
+            'Meta': {'object_name': 'Page'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'philo.redirect': {
+            'Meta': {'object_name': 'Redirect'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        'philo.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+        },
+        'philo.template': {
+            'Meta': {'object_name': 'Template'},
+            'code': ('philo.models.fields.TemplateField', [], {}),
+            'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['philo']
diff --git a/migrations/__init__.py b/migrations/__init__.py
new file mode 100644 (file)
index 0000000..8904fd2
--- /dev/null
@@ -0,0 +1,23 @@
+from south.creator.freezer import prep_for_freeze
+from django.conf import settings
+from django.db import models
+
+
+person_model = getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User')
+
+
+def freeze_person_model():
+       try:
+               app_label, model = person_model.split('.')
+       except ValueError:
+               raise ValueError("Invalid PHILO_PERSON_MODULE definition: %s" % person_model)
+       
+       model = models.get_model(app_label, model.lower())
+       
+       if model is None:
+               raise ValueError("PHILO_PERSON_MODULE not found: %s" % person_model)
+       
+       return prep_for_freeze(model)
+
+
+frozen_person = freeze_person_model()
\ No newline at end of file
index ceeb81e..5d09f37 100644 (file)
@@ -1,10 +1,12 @@
+from django import forms
 from django.db import models
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
 from django.utils import simplejson as json
 from django.core.exceptions import ObjectDoesNotExist
 from philo.exceptions import AncestorDoesNotExist
-from philo.utils import ContentTypeRegistryLimiter
+from philo.models.fields import JSONField
+from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
 from philo.signals import entity_class_prepared
 from philo.validators import json_validator
 from UserDict import DictMixin
@@ -32,59 +34,138 @@ class Titled(models.Model):
                abstract = True
 
 
-class Attribute(models.Model):
-       entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
-       entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
-       entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
-       key = models.CharField(max_length=255)
-       json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', validators=[json_validator])
-       
-       def get_value(self):
-               return json.loads(self.json_value)
-       
-       def set_value(self, value):
-               self.json_value = json.dumps(value)
-       
-       def delete_value(self):
-               self.json_value = json.dumps(None)
+value_content_type_limiter = ContentTypeRegistryLimiter()
+
+
+def register_value_model(model):
+       value_content_type_limiter.register_class(model)
+
+
+def unregister_value_model(model):
+       value_content_type_limiter.unregister_class(model)
+
+
+class AttributeValue(models.Model):
+       attribute = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
+       def apply_data(self, data):
+               raise NotImplementedError
        
-       value = property(get_value, set_value, delete_value)
+       def value_formfield(self, **kwargs):
+               raise NotImplementedError
        
        def __unicode__(self):
-               return u'"%s": %s' % (self.key, self.value)
+               return unicode(self.value)
        
        class Meta:
-               app_label = 'philo'
-               unique_together = ('key', 'entity_content_type', 'entity_object_id')
+               abstract = True
 
 
-value_content_type_limiter = ContentTypeRegistryLimiter()
+attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
 
 
-def register_value_model(model):
-       value_content_type_limiter.register_class(model)
+class JSONValue(AttributeValue):
+       value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
+       
+       def __unicode__(self):
+               return self.value_json
+       
+       def value_formfield(self, **kwargs):
+               kwargs['initial'] = self.value_json
+               return self._meta.get_field('value').formfield(**kwargs)
+       
+       def apply_data(self, cleaned_data):
+               self.value = cleaned_data.get('value', None)
+       
+       class Meta:
+               app_label = 'philo'
 
 
-def unregister_value_model(model):
-       value_content_type_limiter.unregister_class(model)
+class ForeignKeyValue(AttributeValue):
+       content_type = models.ForeignKey(ContentType, related_name='foreign_key_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+       object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
+       value = generic.GenericForeignKey()
+       
+       def value_formfield(self, form_class=forms.ModelChoiceField, **kwargs):
+               if self.content_type is None:
+                       return None
+               kwargs.update({'initial': self.object_id, 'required': False})
+               return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
+       
+       def apply_data(self, cleaned_data):
+               if 'value' in cleaned_data and cleaned_data['value'] is not None:
+                       self.value = cleaned_data['value']
+               else:
+                       self.content_type = cleaned_data.get('content_type', None)
+                       # If there is no value set in the cleaned data, clear the stored value.
+                       self.object_id = None
+       
+       class Meta:
+               app_label = 'philo'
+
 
+class ManyToManyValue(AttributeValue):
+       # TODO: Change object_ids to object_pks.
+       content_type = models.ForeignKey(ContentType, related_name='many_to_many_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+       object_ids = models.CommaSeparatedIntegerField(max_length=300, verbose_name='Value IDs', null=True, blank=True)
+       
+       def get_object_id_list(self):
+               if not self.object_ids:
+                       return []
+               else:
+                       return self.object_ids.split(',')
+       
+       def get_value(self):
+               if self.content_type is None:
+                       return None
+               
+               return self.content_type.model_class()._default_manager.filter(id__in=self.get_object_id_list())
+       
+       def set_value(self, value):
+               if value is None:
+                       self.object_ids = ""
+                       return
+               if not isinstance(value, models.query.QuerySet):
+                       raise TypeError("Value must be a QuerySet.")
+               self.content_type = ContentType.objects.get_for_model(value.model)
+               self.object_ids = ','.join([`value` for value in value.values_list('id', flat=True)])
+       
+       value = property(get_value, set_value)
+       
+       def value_formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
+               if self.content_type is None:
+                       return None
+               kwargs.update({'initial': self.get_object_id_list(), 'required': False})
+               return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
+       
+       def apply_data(self, cleaned_data):
+               if 'value' in cleaned_data and cleaned_data['value'] is not None:
+                       self.value = cleaned_data['value']
+               else:
+                       self.content_type = cleaned_data.get('content_type', None)
+                       # If there is no value set in the cleaned data, clear the stored value.
+                       self.object_ids = ""
+       
+       class Meta:
+               app_label = 'philo'
 
 
-class Relationship(models.Model):
-       entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
+class Attribute(models.Model):
+       entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
        entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
        entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
-       key = models.CharField(max_length=255)
-       value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+       
+       value_content_type = models.ForeignKey(ContentType, related_name='attribute_value_set', limit_choices_to=attribute_value_limiter, verbose_name='Value type', null=True, blank=True)
        value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
        value = generic.GenericForeignKey('value_content_type', 'value_object_id')
        
+       key = models.CharField(max_length=255)
+       
        def __unicode__(self):
                return u'"%s": %s' % (self.key, self.value)
        
        class Meta:
                app_label = 'philo'
-               unique_together = ('key', 'entity_content_type', 'entity_object_id')
+               unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
 
 
 class QuerySetMapper(object, DictMixin):
@@ -94,11 +175,15 @@ class QuerySetMapper(object, DictMixin):
        
        def __getitem__(self, key):
                try:
-                       return self.queryset.get(key__exact=key).value
+                       value = self.queryset.get(key__exact=key).value
                except ObjectDoesNotExist:
                        if self.passthrough is not None:
                                return self.passthrough.__getitem__(key)
                        raise KeyError
+               else:
+                       if value is not None:
+                               return value.value
+                       return value
        
        def keys(self):
                keys = set(self.queryset.values_list('key', flat=True).distinct())
@@ -132,16 +217,11 @@ class Entity(models.Model):
        __metaclass__ = EntityBase
        
        attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
-       relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
        
        @property
        def attributes(self):
                return QuerySetMapper(self.attribute_set)
        
-       @property
-       def relationships(self):
-               return QuerySetMapper(self.relationship_set)
-       
        @property
        def _added_attribute_registry(self):
                if not hasattr(self, '_real_added_attribute_registry'):
@@ -154,18 +234,6 @@ class Entity(models.Model):
                        self._real_removed_attribute_registry = []
                return self._real_removed_attribute_registry
        
-       @property
-       def _added_relationship_registry(self):
-               if not hasattr(self, '_real_added_relationship_registry'):
-                       self._real_added_relationship_registry = {}
-               return self._real_added_relationship_registry
-       
-       @property
-       def _removed_relationship_registry(self):
-               if not hasattr(self, '_real_removed_relationship_registry'):
-                       self._real_removed_relationship_registry = []
-               return self._real_removed_relationship_registry
-       
        def save(self, *args, **kwargs):
                super(Entity, self).save(*args, **kwargs)
                
@@ -173,31 +241,17 @@ class Entity(models.Model):
                        self.attribute_set.filter(key__exact=key).delete()
                del self._removed_attribute_registry[:]
                
-               for key, value in self._added_attribute_registry.items():
+               for field, value in self._added_attribute_registry.items():
                        try:
-                               attribute = self.attribute_set.get(key__exact=key)
+                               attribute = self.attribute_set.get(key__exact=field.key)
                        except Attribute.DoesNotExist:
                                attribute = Attribute()
                                attribute.entity = self
-                               attribute.key = key
-                       attribute.value = value
+                               attribute.key = field.key
+                       
+                       field.set_attribute_value(attribute, value)
                        attribute.save()
                self._added_attribute_registry.clear()
-               
-               for key in self._removed_relationship_registry:
-                       self.relationship_set.filter(key__exact=key).delete()
-               del self._removed_relationship_registry[:]
-               
-               for key, value in self._added_relationship_registry.items():
-                       try:
-                               relationship = self.relationship_set.get(key__exact=key)
-                       except Relationship.DoesNotExist:
-                               relationship = Relationship()
-                               relationship.entity = self
-                               relationship.key = key
-                       relationship.value = value
-                       relationship.save()
-               self._added_relationship_registry.clear()
        
        class Meta:
                abstract = True
index dbf1886..85c5583 100644 (file)
@@ -1,29 +1,33 @@
-from django.db import models
 from django import forms
-from django.core.exceptions import FieldError
+from django.core.exceptions import FieldError, ValidationError
+from django.db import models
+from django.db.models.fields import NOT_PROVIDED
+from django.utils import simplejson as json
 from django.utils.text import capfirst
-from philo.models.base import Entity
 from philo.signals import entity_class_prepared
-from philo.validators import TemplateValidator
+from philo.validators import TemplateValidator, json_validator
 
 
-__all__ = ('AttributeField', 'RelationshipField')
+__all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
 
 
 class EntityProxyField(object):
        descriptor_class = None
        
-       def __init__(self, *args, **kwargs):
+       def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, *args, **kwargs):
                if self.descriptor_class is None:
                        raise NotImplementedError('EntityProxyField subclasses must specify a descriptor_class.')
-               self.verbose_name = kwargs.get('verbose_name', None)
-               self.help_text = kwargs.get('help_text', None)
+               self.verbose_name = verbose_name
+               self.help_text = help_text
+               self.default = default
+               self.editable = editable
        
        def actually_contribute_to_class(self, sender, **kwargs):
                sender._entity_meta.add_proxy_field(self)
                setattr(sender, self.attname, self.descriptor_class(self))
        
        def contribute_to_class(self, cls, name):
+               from philo.models.base import Entity
                if issubclass(cls, Entity):
                        self.name = name
                        self.attname = name
@@ -38,6 +42,9 @@ class EntityProxyField(object):
        
        def value_from_object(self, obj):
                return getattr(obj, self.attname)
+       
+       def has_default(self):
+               return self.default is not NOT_PROVIDED
 
 
 class AttributeFieldDescriptor(object):
@@ -46,30 +53,73 @@ class AttributeFieldDescriptor(object):
        
        def __get__(self, instance, owner):
                if instance:
-                       if self.field.key in instance._added_attribute_registry:
-                               return instance._added_attribute_registry[self.field.key]
-                       if self.field.key in instance._removed_attribute_registry:
+                       if self.field in instance._added_attribute_registry:
+                               return instance._added_attribute_registry[self.field]
+                       if self.field in instance._removed_attribute_registry:
                                return None
                        try:
                                return instance.attributes[self.field.key]
                        except KeyError:
                                return None
                else:
-                       raise AttributeError('The \'%s\' attribute can only be accessed from %s instances.' % (self.field.name, owner.__name__))
+                       return None
        
        def __set__(self, instance, value):
-               if self.field.key in instance._removed_attribute_registry:
-                       instance._removed_attribute_registry.remove(self.field.key)
-               instance._added_attribute_registry[self.field.key] = value
+               raise NotImplementedError('AttributeFieldDescriptor subclasses must implement a __set__ method.')
        
        def __delete__(self, instance):
-               if self.field.key in instance._added_attribute_registry:
-                       del instance._added_attribute_registry[self.field.key]
-               instance._removed_attribute_registry.append(self.field.key)
+               if self.field in instance._added_attribute_registry:
+                       del instance._added_attribute_registry[self.field]
+               instance._removed_attribute_registry.append(self.field)
+
+
+class JSONAttributeDescriptor(AttributeFieldDescriptor):
+       def __set__(self, instance, value):
+               if self.field in instance._removed_attribute_registry:
+                       instance._removed_attribute_registry.remove(self.field)
+               instance._added_attribute_registry[self.field] = value
+
+
+class ForeignKeyAttributeDescriptor(AttributeFieldDescriptor):
+       def __set__(self, instance, value):
+               if isinstance(value, (models.Model, type(None))):
+                       if self.field in instance._removed_attribute_registry:
+                               instance._removed_attribute_registry.remove(self.field)
+                       instance._added_attribute_registry[self.field] = value
+               else:
+                       raise AttributeError('The \'%s\' attribute can only be set using existing Model objects.' % self.field.name)
+
+
+class ManyToManyAttributeDescriptor(AttributeFieldDescriptor):
+       def __set__(self, instance, value):
+               if isinstance(value, models.QuerySet):
+                       if self.field in instance._removed_attribute_registry:
+                               instance._removed_attribute_registry.remove(self.field)
+                       instance._added_attribute_registry[self.field] = value
+               else:
+                       raise AttributeError('The \'%s\' attribute can only be set to a QuerySet.' % self.field.name)
 
 
 class AttributeField(EntityProxyField):
-       descriptor_class = AttributeFieldDescriptor
+       def contribute_to_class(self, cls, name):
+               super(AttributeField, self).contribute_to_class(cls, name)
+               if self.key is None:
+                       self.key = name
+       
+       def set_attribute_value(self, attribute, value, value_class):
+               if not isinstance(attribute.value, value_class):
+                       if isinstance(attribute.value, models.Model):
+                               attribute.value.delete()
+                       new_value = value_class()
+               else:
+                       new_value = attribute.value
+               new_value.value = value
+               new_value.save()
+               attribute.value = new_value
+
+
+class JSONAttribute(AttributeField):
+       descriptor_class = JSONAttributeDescriptor
        
        def __init__(self, field_template=None, key=None, **kwargs):
                super(AttributeField, self).__init__(**kwargs)
@@ -78,72 +128,69 @@ class AttributeField(EntityProxyField):
                        field_template = models.CharField(max_length=255)
                self.field_template = field_template
        
-       def contribute_to_class(self, cls, name):
-               super(AttributeField, self).contribute_to_class(cls, name)
-               if self.key is None:
-                       self.key = name
-       
        def formfield(self, **kwargs):
                defaults = {'required': False, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
+               if self.has_default():
+                       defaults['initial'] = self.default
                defaults.update(kwargs)
                return self.field_template.formfield(**defaults)
-
-
-class RelationshipFieldDescriptor(object):
-       def __init__(self, field):
-               self.field = field
        
-       def __get__(self, instance, owner):
-               if instance:
-                       if self.field.key in instance._added_relationship_registry:
-                               return instance._added_relationship_registry[self.field.key]
-                       if self.field.key in instance._removed_relationship_registry:
-                               return None
-                       try:
-                               return instance.relationships[self.field.key]
-                       except KeyError:
-                               return None
-               else:
-                       raise AttributeError('The \'%s\' attribute can only be accessed from %s instances.' % (self.field.name, owner.__name__))
-       
-       def __set__(self, instance, value):
-               if isinstance(value, (models.Model, type(None))):
-                       if self.field.key in instance._removed_relationship_registry:
-                               instance._removed_relationship_registry.remove(self.field.key)
-                       instance._added_relationship_registry[self.field.key] = value
-               else:
-                       raise AttributeError('The \'%s\' attribute can only be set using existing Model objects.' % self.field.name)
+       def value_from_object(self, obj):
+               try:
+                       return getattr(obj, self.attname)
+               except AttributeError:
+                       return None
        
-       def __delete__(self, instance):
-               if self.field.key in instance._added_relationship_registry:
-                       del instance._added_relationship_registry[self.field.key]
-               instance._removed_relationship_registry.append(self.field.key)
+       def set_attribute_value(self, attribute, value, value_class=None):
+               if value_class is None:
+                       from philo.models.base import JSONValue
+                       value_class = JSONValue
+               super(JSONAttribute, self).set_attribute_value(attribute, value, value_class)
 
 
-class RelationshipField(EntityProxyField):
-       descriptor_class = RelationshipFieldDescriptor
+class ForeignKeyAttribute(AttributeField):
+       descriptor_class = ForeignKeyAttributeDescriptor
        
        def __init__(self, model, limit_choices_to=None, key=None, **kwargs):
-               super(RelationshipField, self).__init__(**kwargs)
+               super(ForeignKeyAttribute, self).__init__(**kwargs)
                self.key = key
                self.model = model
                if limit_choices_to is None:
                        limit_choices_to = {}
                self.limit_choices_to = limit_choices_to
        
-       def contribute_to_class(self, cls, name):
-               super(RelationshipField, self).contribute_to_class(cls, name)
-               if self.key is None:
-                       self.key = name
-       
        def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
                defaults = {'required': False, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
+               if self.has_default():
+                       defaults['initial'] = self.default
                defaults.update(kwargs)
                return form_class(self.model._default_manager.complex_filter(self.limit_choices_to), **defaults)
        
        def value_from_object(self, obj):
-               relobj = super(RelationshipField, self).value_from_object(obj)
+               try:
+                       relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
+               except AttributeError:
+                       return None
                return getattr(relobj, 'pk', None)
+       
+       def set_attribute_value(self, attribute, value, value_class=None):
+               if value_class is None:
+                       from philo.models.base import ForeignKeyValue
+                       value_class = ForeignKeyValue
+               super(ForeignKeyAttribute, self).set_attribute_value(attribute, value, value_class)
+
+
+class ManyToManyAttribute(ForeignKeyAttribute):
+       descriptor_class = ManyToManyAttributeDescriptor
+       
+       def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
+               return super(ManyToManyAttribute, self).formfield(form_class, **kwargs)
+       
+       def set_attribute_value(self, attribute, value, value_class=None):
+               if value_class is None:
+                       from philo.models.base import ManyToManyValue
+                       value_class = ManyToManyValue
+               super(ManyToManyAttribute, self).set_attribute_value(attribute, value, value_class)
 
 
 class TemplateField(models.TextField):
@@ -152,9 +199,58 @@ class TemplateField(models.TextField):
                self.validators.append(TemplateValidator(allow, disallow, secure))
 
 
+class JSONFormField(forms.Field):
+       def clean(self, value):
+               try:
+                       return json.loads(value)
+               except Exception, e:
+                       raise ValidationError(u'JSON decode error: %s' % e)
+
+
+class JSONDescriptor(object):
+       def __init__(self, field):
+               self.field = field
+       
+       def __get__(self, instance, owner):
+               if instance is None:
+                       raise AttributeError # ?
+               
+               if self.field.name not in instance.__dict__:
+                       json_string = getattr(instance, self.field.attname)
+                       instance.__dict__[self.field.name] = json.loads(json_string)
+               
+               return instance.__dict__[self.field.name]
+       
+       def __set__(self, instance, value):
+               instance.__dict__[self.field.name] = value
+               setattr(instance, self.field.attname, json.dumps(value))
+       
+       def __delete__(self, instance):
+               del(instance.__dict__[self.field.name])
+               setattr(instance, self.field.attname, json.dumps(None))
+
+
+class JSONField(models.TextField):
+       def __init__(self, *args, **kwargs):
+               super(JSONField, self).__init__(*args, **kwargs)
+               self.validators.append(json_validator)
+       
+       def get_attname(self):
+               return "%s_json" % self.name
+       
+       def contribute_to_class(self, cls, name):
+               super(JSONField, self).contribute_to_class(cls, name)
+               setattr(cls, name, JSONDescriptor(self))
+       
+       def formfield(self, *args, **kwargs):
+               kwargs["form_class"] = JSONFormField
+               return super(JSONField, self).formfield(*args, **kwargs)
+
+
 try:
        from south.modelsinspector import add_introspection_rules
 except ImportError:
        pass
 else:
-       add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"])
\ No newline at end of file
+       add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"])
+       add_introspection_rules([], ["^philo\.models\.fields\.JSONField"])
\ No newline at end of file
index 14f5063..b16521b 100644 (file)
@@ -3,10 +3,12 @@ from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
 from django.contrib.sites.models import Site
 from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect
+from django.core.exceptions import ViewDoesNotExist
 from django.core.servers.basehttp import FileWrapper
-from django.core.urlresolvers import resolve, clear_url_caches
+from django.core.urlresolvers import resolve, clear_url_caches, reverse
 from django.template import add_to_builtins as register_templatetags
 from inspect import getargspec
+from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
 from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
 from philo.utils import ContentTypeSubclassLimiter
 from philo.validators import RedirectValidator
@@ -28,14 +30,22 @@ class Node(TreeEntity):
                        return self.view.accepts_subpath
                return False
        
-       def render_to_response(self, request, path=None, subpath=None, extra_context=None):
-               return self.view.render_to_response(self, request, path, subpath, extra_context)
+       def render_to_response(self, request, extra_context=None):
+               return self.view.render_to_response(request, extra_context)
        
        def get_absolute_url(self):
-               root = Site.objects.get_current().root_node
                try:
-                       return '/%s' % self.get_path(root=root)
-               except AncestorDoesNotExist:
+                       root = Site.objects.get_current().root_node
+               except Site.DoesNotExist:
+                       root = None
+               
+               try:
+                       path = self.get_path(root=root)
+                       if path:
+                               path += '/'
+                       root_url = reverse('philo-root')
+                       return '%s%s' % (root_url, path)
+               except AncestorDoesNotExist, ViewDoesNotExist:
                        return None
        
        class Meta:
@@ -57,17 +67,17 @@ class View(Entity):
        def attributes_with_node(self, node):
                return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
        
-       def relationships_with_node(self, node):
-               return QuerySetMapper(self.relationship_set, passthrough=node.relationships)
-       
-       def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+       def render_to_response(self, request, extra_context=None):
+               if not hasattr(request, 'node'):
+                       raise MIDDLEWARE_NOT_CONFIGURED
+               
                extra_context = extra_context or {}
-               view_about_to_render.send(sender=self, node=node, request=request, path=path, subpath=subpath, extra_context=extra_context)
-               response = self.actually_render_to_response(node, request, path, subpath, extra_context)
+               view_about_to_render.send(sender=self, request=request, extra_context=extra_context)
+               response = self.actually_render_to_response(request, extra_context)
                view_finished_rendering.send(sender=self, response=response)
                return response
        
-       def actually_render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+       def actually_render_to_response(self, request, extra_context=None):
                raise NotImplementedError('View subclasses must implement render_to_response.')
        
        class Meta:
@@ -80,10 +90,17 @@ _view_content_type_limiter.cls = View
 class MultiView(View):
        accepts_subpath = True
        
-       urlpatterns = []
+       @property
+       def urlpatterns(self, obj):
+               raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
+       
+       def get_reverse_params(self, obj):
+               """This method should return a view_name, args, kwargs tuple suitable for reversing a url for the given obj using self as the urlconf."""
+               raise NotImplementedError("MultiView subclasses must implement get_subpath.")
        
-       def actually_render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+       def actually_render_to_response(self, request, extra_context=None):
                clear_url_caches()
+               subpath = request.node.subpath
                if not subpath:
                        subpath = ""
                subpath = "/" + subpath
@@ -93,8 +110,6 @@ class MultiView(View):
                        if 'extra_context' in kwargs:
                                extra_context.update(kwargs['extra_context'])
                        kwargs['extra_context'] = extra_context
-               if 'node' in view_args[0] or view_args[2] is not None:
-                       kwargs['node'] = node
                return view(request, *args, **kwargs)
        
        class Meta:
@@ -109,7 +124,7 @@ class Redirect(View):
        target = models.CharField(max_length=200, validators=[RedirectValidator()])
        status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
        
-       def actually_render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+       def actually_render_to_response(self, request, extra_context=None):
                response = HttpResponseRedirect(self.target)
                response.status_code = self.status_code
                return response
@@ -118,13 +133,14 @@ class Redirect(View):
                app_label = 'philo'
 
 
+# Why does this exist?
 class File(View):
        """ For storing arbitrary files """
        
        mimetype = models.CharField(max_length=255)
        file = models.FileField(upload_to='philo/files/%Y/%m/%d')
        
-       def actually_render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
+       def actually_render_to_response(self, request, extra_context=None):
                wrapper = FileWrapper(self.file)
                response = HttpResponse(wrapper, content_type=self.mimetype)
                response['Content-Length'] = self.file.size
index 323aeb8..fddb0ae 100644 (file)
@@ -2,6 +2,7 @@
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 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
@@ -79,26 +80,44 @@ class Page(View):
                return self._containers
        containers = property(get_containers)
        
-       def render_to_string(self, node=None, request=None, path=None, subpath=None, extra_context=None):
+       def render_to_string(self, request=None, extra_context=None):
                context = {}
                context.update(extra_context or {})
-               context.update({'page': self, 'attributes': self.attributes, 'relationships': self.relationships})
-               if node and request:
-                       context.update({'node': node, 'attributes': self.attributes_with_node(node), 'relationships': self.relationships_with_node(node)})
-                       page_about_to_render_to_string.send(sender=self, node=node, request=request, extra_context=context)
+               context.update({'page': self, 'attributes': self.attributes})
+               if request:
+                       context.update({'node': request.node, 'attributes': self.attributes_with_node(request.node)})
+                       page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
                        string = self.template.django_template.render(RequestContext(request, context))
                else:
-                       page_about_to_render_to_string.send(sender=self, node=node, request=request, extra_context=context)
+                       page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
                        string = self.template.django_template.render(Context(context))
                page_finished_rendering_to_string.send(sender=self, string=string)
                return string
        
-       def actually_render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
-               return HttpResponse(self.render_to_string(node, request, path, subpath, extra_context), mimetype=self.template.mimetype)
+       def actually_render_to_response(self, request, extra_context=None):
+               return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
        
        def __unicode__(self):
                return self.title
        
+       def clean_fields(self, exclude=None):
+               try:
+                       super(Page, self).clean_fields(exclude)
+               except ValidationError, e:
+                       errors = e.message_dict
+               else:
+                       errors = {}
+               
+               if 'template' not in errors and 'template' not in exclude:
+                       try:
+                               self.template.clean_fields()
+                               self.template.clean()
+                       except ValidationError, e:
+                               errors['template'] = e.messages
+               
+               if errors:
+                       raise ValidationError(errors)
+       
        class Meta:
                app_label = 'philo'
 
index 37ea3b1..875039d 100644 (file)
@@ -2,9 +2,9 @@ from django.dispatch import Signal
 
 
 entity_class_prepared = Signal(providing_args=['class'])
-view_about_to_render = Signal(providing_args=['node', 'request', 'path', 'subpath', 'extra_context'])
+view_about_to_render = Signal(providing_args=['request', 'extra_context'])
 view_finished_rendering = Signal(providing_args=['response'])
-page_about_to_render_to_string = Signal(providing_args=['node', 'request', 'path', 'subpath', 'extra_context'])
+page_about_to_render_to_string = Signal(providing_args=['request', 'extra_context'])
 page_finished_rendering_to_string = Signal(providing_args=['string'])
 
 
@@ -12,6 +12,8 @@
          <th{% if forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}</th>
        {% endif %}
      {% endfor %}
+         <th>Content Type</th>
+         <th>Value</th>
      {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
      </tr></thead>
 
@@ -53,6 +55,8 @@
             {% endfor %}
           {% endfor %}
         {% endfor %}
+        <td>{% with inline_admin_form.form.content_type as field %}{{ field.errors.as_ul }}{{ field }}{% endwith %}</td>
+        <td>{% with inline_admin_form.form.value as field %}{{ field.errors.as_ul }}{{ field }}{% endwith %}</td>
         {% if inline_admin_formset.formset.can_delete %}
           <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
         {% endif %}
index 0cb2289..8a98630 100644 (file)
 from django import template
 from django.conf import settings
 from django.contrib.sites.models import Site
+from django.core.urlresolvers import reverse, NoReverseMatch
+from django.template.defaulttags import kwarg_re
+from django.utils.encoding import smart_str
+from philo.exceptions import ViewCanNotProvideSubpath
 
 
 register = template.Library()
 
 
 class NodeURLNode(template.Node):
-       def __init__(self, node, with_obj, as_var):
-               if node is not None:
-                       self.node = template.Variable(node)
-               else:
-                       self.node = None
-               
-               if with_obj is not None:
-                       self.with_obj = template.Variable(with_obj)
-               else:
-                       self.with_obj = None
-               
+       def __init__(self, node, as_var, with_obj=None, view_name=None, args=None, kwargs=None):
                self.as_var = as_var
+               self.view_name = view_name
+               
+               # Because the following variables have already been compiled as filters if they exist, they don't need to be re-scanned as template variables.
+               self.node = node
+               self.with_obj = with_obj
+               self.args = args
+               self.kwargs = kwargs
        
        def render(self, context):
-               try:
-                       if self.node:
-                               node = self.node.resolve(context)
-                       else:
-                               node = context['node']
-                       current_site = Site.objects.get_current()
-                       if node.has_ancestor(current_site.root_node):
-                               url = '/' + node.get_path(root=current_site.root_node)
-                               if self.with_obj:
-                                       with_obj = self.with_obj.resolve(context)
-                                       subpath = node.view.get_subpath(with_obj)
-                                       if subpath[0] is '/':
-                                               subpath = subpath[1:]
-                                       url += subpath
-                       else:
+               if self.node:
+                       node = self.node.resolve(context)
+               else:
+                       node = context['node']
+               
+               if not node:
+                       return settings.TEMPLATE_STRING_IF_INVALID
+               
+               if self.with_obj is None and self.view_name is None:
+                       url = node.get_absolute_url()
+               else:
+                       if not node.view.accepts_subpath:
                                return settings.TEMPLATE_STRING_IF_INVALID
                        
-                       if self.as_var:
-                               context[self.as_var] = url
-                               return settings.TEMPLATE_STRING_IF_INVALID
+                       if self.with_obj is not None:
+                               try:
+                                       view_name, args, kwargs = node.view.get_reverse_params(self.with_obj.resolve(context))
+                               except ViewCanNotProvideSubpath:
+                                       return settings.TEMPLATE_STRING_IF_INVALID
+                       else: # self.view_name is not None
+                               view_name = self.view_name
+                               args = [arg.resolve(context) for arg in self.args]
+                               kwargs = dict([(smart_str(k, 'ascii'), v.resolve(context)) for k, v in self.kwargs.items()])
+                       
+                       url = ''
+                       try:
+                               subpath = reverse(view_name, urlconf=node.view, args=args, kwargs=kwargs)
+                       except NoReverseMatch:
+                               if self.as_var is None:
+                                       raise
                        else:
-                               return url
-               except:
-                       return settings.TEMPLATE_STRING_IF_INVALID
+                               if subpath[0] == '/':
+                                       subpath = subpath[1:]
+                               
+                               url = node.get_absolute_url() + subpath
+               
+               if self.as_var:
+                       context[self.as_var] = url
+                       return ''
+               else:
+                       return url
 
 
 @register.tag(name='node_url')
 def do_node_url(parser, token):
        """
-       {% node_url [<node>] [with <obj>] [as <var>] %}
+       {% node_url [for <node>] [as <var] %}
+       {% node_url with <obj> [for <node>] [as <var>] %}
+       {% node_url <view_name> [<arg1> [<arg2> ...] ] [for <node>] [as <var>] %}
+       {% node_url <view_name> [<key1>=<value1> [<key2>=<value2> ...] ] [for <node>] [as <var>]%}
        """
        params = token.split_contents()
        tag = params[0]
+       as_var = None
+       with_obj = None
+       node = None
+       params = params[1:]
        
-       if len(params) <= 6:
-               node = None
-               with_obj = None
-               as_var = None
-               remaining_tokens = params[1:]
-               while remaining_tokens:
-                       option_token = remaining_tokens.pop(0)
-                       if option_token == 'with':
-                               try:
-                                       with_obj = remaining_tokens.pop(0)
-                               except IndexError:
-                                       raise template.TemplateSyntaxError('"%s" template tag option "with" requires an argument specifying an object handled by the view on the node' % tag)
-                       elif option_token == 'as':
-                               try:
-                                       as_var = remaining_tokens.pop(0)
-                               except IndexError:
-                                       raise template.TemplateSyntaxError('"%s" template tag option "as" requires an argument specifying a variable name' % tag)
-                       else: # node
-                               node = option_token
-               return NodeURLNode(node=node, with_obj=with_obj, as_var=as_var)
-       else:
-               raise template.TemplateSyntaxError('"%s" template tag cannot accept more than five arguments' % tag)
\ No newline at end of file
+       if len(params) >= 2 and params[-2] == 'as':
+               as_var = params[-1]
+               params = params[:-2]
+       
+       if len(params) >= 2 and params[-2] == 'for':
+               node = parser.compile_filter(params[-1])
+               params = params[:-2]
+       
+       if len(params) >= 2 and params[-2] == 'with':
+               with_obj = parser.compile_filter(params[-1])
+               params = params[:-2]
+       
+       if with_obj is not None:
+               if params:
+                       raise template.TemplateSyntaxError('`%s` template tag accepts no arguments or keyword arguments if with <obj> is specified.' % tag)
+               return NodeURLNode(with_obj=with_obj, node=node, as_var=as_var)
+       
+       if params:
+               args = []
+               kwargs = {}
+               view_name = params.pop(0)
+               for param in params:
+                       match = kwarg_re.match(param)
+                       if not match:
+                               raise TemplateSyntaxError("Malformed arguments to `%s` tag" % tag)
+                       name, value = match.groups()
+                       if name:
+                               kwargs[name] = parser.compile_filter(value)
+                       else:
+                               args.append(parser.compile_filter(value))
+               return NodeURLNode(view_name=view_name, args=args, kwargs=kwargs, node=node, as_var=as_var)
+       
+       return NodeURLNode(node=node, as_var=as_var)
\ No newline at end of file
diff --git a/tests.py b/tests.py
new file mode 100644 (file)
index 0000000..d9f743f
--- /dev/null
+++ b/tests.py
@@ -0,0 +1,74 @@
+from django.test import TestCase
+from django import template
+from django.conf import settings
+from philo.models import Node, Page, Template
+from philo.contrib.penfield.models import Blog, BlogView, BlogEntry
+
+
+class NodeURLTestCase(TestCase):
+       """Tests the features of the node_url template tag."""
+       urls = 'philo.urls'
+       fixtures = ['test_fixtures.json']
+       
+       def setUp(self):
+               if 'south' in settings.INSTALLED_APPS:
+                       from south.management.commands.migrate import Command
+                       command = Command()
+                       command.handle(all_apps=True)
+               
+               self.templates = [template.Template(string) for string in
+                       [
+                               "{% node_url %}", # 0
+                               "{% node_url for node2 %}", # 1
+                               "{% node_url as hello %}<p>{{ hello|slice:'1:' }}</p>", # 2
+                               "{% node_url for nodes|first %}", # 3
+                               "{% node_url with entry %}", # 4
+                               "{% node_url with entry for node2 %}", # 5
+                               "{% node_url with tag for node2 %}", # 6
+                               "{% node_url with date for node2 %}", # 7
+                               "{% node_url entries_by_day year=date|date:'Y' month=date|date:'m' day=date|date:'d' for node2 as goodbye %}<em>{{ goodbye|upper }}</em>", # 8
+                               "{% node_url entries_by_month year=date|date:'Y' month=date|date:'m' for node2 %}", # 9
+                               "{% node_url entries_by_year year=date|date:'Y' for node2 %}", # 10
+                       ]
+               ]
+               
+               nodes = Node.objects.all()
+               blog = Blog.objects.all()[0]
+               
+               self.context = template.Context({
+                       'node': nodes[0],
+                       'node2': nodes[1],
+                       'nodes': nodes,
+                       'entry': BlogEntry.objects.all()[0],
+                       'tag': blog.entry_tags.all()[0],
+                       'date': blog.entry_dates['day'][0]
+               })
+       
+       def test_nodeurl(self):
+               for i, template in enumerate(self.templates):
+                       t = template.render(self.context)
+                       
+                       if i == 0:
+                               self.assertEqual(t, "/root/never/")
+                       elif i == 1:
+                               self.assertEqual(t, "/root/blog/")
+                       elif i == 2:
+                               self.assertEqual(t, "<p>root/never/</p>")
+                       elif i == 3:
+                               self.assertEqual(t, "/root/never/")
+                       elif i == 4:
+                               self.assertEqual(t, settings.TEMPLATE_STRING_IF_INVALID)
+                       elif i == 5:
+                               self.assertEqual(t, "/root/blog/2010/10/20/first-entry")
+                       elif i == 6:
+                               self.assertEqual(t, "/root/blog/tags/test-tag/")
+                       elif i == 7:
+                               self.assertEqual(t, "/root/blog/2010/10/20")
+                       elif i == 8:
+                               self.assertEqual(t, "<em>/ROOT/BLOG/2010/10/20</em>")
+                       elif i == 9:
+                               self.assertEqual(t, "/root/blog/2010/10")
+                       elif i == 10:
+                               self.assertEqual(t, "/root/blog/2010/")
+                       else:
+                               print "Rendered as:\n%s\n\n" % t
\ No newline at end of file
index 1305afb..8b39abd 100644 (file)
@@ -45,8 +45,8 @@ class URLLinkValidator(RegexValidator):
 def json_validator(value):
        try:
                json.loads(value)
-       except:
-               raise ValidationError(u'\'%s\' is not valid JSON' % value)
+       except Exception, e:
+               raise ValidationError(u'JSON decode error: %s' % e)
 
 
 class TemplateValidationParser(Parser):
index b3e2a74..fd8e662 100644 (file)
--- a/views.py
+++ b/views.py
@@ -3,28 +3,25 @@ from django.conf import settings
 from django.http import Http404, HttpResponse
 from django.template import RequestContext
 from django.views.decorators.vary import vary_on_headers
+from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
 from philo.models import Node
 
 
 @vary_on_headers('Accept')
 def node_view(request, path=None, **kwargs):
-       node = None
-       subpath = None
-       if path is None:
-               path = '/'
-       current_site = Site.objects.get_current()
-       try:
-               node, subpath = Node.objects.get_with_path(path, root=current_site.root_node, absolute_result=False)
-       except Node.DoesNotExist:
-               raise Http404
+       if not hasattr(request, 'node'):
+               raise MIDDLEWARE_NOT_CONFIGURED
        
-       if not node:
+       if not request.node:
                raise Http404
        
+       node = request.node
+       subpath = request.node.subpath
+       
        try:
                if subpath and not node.accepts_subpath:
                        raise Http404
-               return node.render_to_response(request, path=path, subpath=subpath)
+               return node.render_to_response(request, kwargs)
        except Http404, e:
                if settings.DEBUG:
                        raise
@@ -39,7 +36,7 @@ def node_view(request, path=None, **kwargs):
                
                extra_context = {'exception': e}
                
-               return Http404View.render_to_response(node, request, path, subpath, extra_context)
+               return Http404View.render_to_response(request, extra_context)
        except Exception, e:
                if settings.DEBUG:
                        raise
@@ -52,6 +49,6 @@ def node_view(request, path=None, **kwargs):
                        
                        extra_context = {'exception': e}
                        
-                       return Http500View.render_to_response(node, request, path, subpath, extra_context)
+                       return Http500View.render_to_response(request, extra_context)
                except:
                        raise e
\ No newline at end of file