Merge branch 'flexible_attributes'
authorStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 14 Oct 2010 16:43:56 +0000 (12:43 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 14 Oct 2010 16:43:56 +0000 (12:43 -0400)
admin/base.py
admin/pages.py
forms.py
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]
models/base.py
models/fields.py
models/nodes.py
models/pages.py
templates/admin/philo/edit_inline/tabular_attribute.html [moved from templates/admin/philo/edit_inline/tabular_collapse.html with 95% similarity]

index b7c93d7..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,23 +12,16 @@ 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
 
 
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
index ced29b2..48e1d7f 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
 
 
@@ -96,6 +97,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/migrations/0005_add_attribute_values.py b/migrations/0005_add_attribute_values.py
new file mode 100644 (file)
index 0000000..d8675ee
--- /dev/null
@@ -0,0 +1,182 @@
+# 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'])
+
+
+    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']
index 718429b..5d9d761 100644 (file)
@@ -1,3 +1,4 @@
+from django import forms
 from django.db import models
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
@@ -5,7 +6,7 @@ from django.utils import simplejson as json
 from django.core.exceptions import ObjectDoesNotExist
 from philo.exceptions import AncestorDoesNotExist
 from philo.models.fields import JSONField
-from philo.utils import ContentTypeRegistryLimiter
+from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
 from philo.signals import entity_class_prepared
 from philo.validators import json_validator
 from UserDict import DictMixin
@@ -33,48 +34,155 @@ 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)
-       value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
+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
+       
+       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):
+       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):
+               self.value = cleaned_data.get('value', None)
+       
+       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 get_value_class(self, value):
+               if isinstance(value, models.query.QuerySet):
+                       return ManyToManyValue
+               elif isinstance(value, models.Model) or (value is None and self.value_content_type.model_class() is ForeignKeyValue):
+                       return ForeignKeyValue
+               else:
+                       return JSONValue
+       
+       def set_value(self, value):
+               # is this useful? The best way of doing it?
+               value_class = self.get_value_class(value)
+               
+               if self.value is None or value_class != self.value_content_type.model_class():
+                       if self.value is not None:
+                               self.value.delete()
+                       new_value = value_class()
+                       new_value.value = value
+                       new_value.save()
+                       self.value = new_value
+               else:
+                       self.value.value = value
+                       self.value.save()
+       
        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):
@@ -122,16 +230,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'):
@@ -144,18 +247,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)
                
@@ -170,24 +261,9 @@ class Entity(models.Model):
                                attribute = Attribute()
                                attribute.entity = self
                                attribute.key = key
-                       attribute.value = value
+                       attribute.set_value(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 9483516..0c68858 100644 (file)
@@ -7,7 +7,7 @@ from philo.signals import entity_class_prepared
 from philo.validators import TemplateValidator, json_validator
 
 
-__all__ = ('AttributeField', 'RelationshipField')
+__all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
 
 
 class EntityProxyField(object):
@@ -59,9 +59,7 @@ class AttributeFieldDescriptor(object):
                        raise AttributeError('The \'%s\' attribute can only be accessed from %s instances.' % (self.field.name, owner.__name__))
        
        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:
@@ -69,8 +67,42 @@ class AttributeFieldDescriptor(object):
                instance._removed_attribute_registry.append(self.field.key)
 
 
+class JSONAttributeDescriptor(AttributeFieldDescriptor):
+       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
+
+
+class ForeignKeyAttributeDescriptor(AttributeFieldDescriptor):
+       def __set__(self, instance, value):
+               if isinstance(value, (models.Model, type(None))):
+                       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
+               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.key in instance._removed_attribute_registry:
+                               instance._removed_attribute_registry.remove(self.field.key)
+                       instance._added_attribute_registry[self.field.key] = 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
+
+
+class JSONAttribute(AttributeField):
+       descriptor_class = JSONAttributeDescriptor
        
        def __init__(self, field_template=None, key=None, **kwargs):
                super(AttributeField, self).__init__(**kwargs)
@@ -79,74 +111,41 @@ 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}
                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 __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 value_from_object(self, obj):
+               return getattr(obj, self.attname).value
 
 
-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}
                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)
+               relobj = super(ForeignKeyAttribute, self).value_from_object(obj).value
                return getattr(relobj, 'pk', None)
 
 
+class ManyToManyAttribute(AttributeField):
+       descriptor_class = ManyToManyAttributeDescriptor
+       #FIXME: Add __init__ and formfield methods
+
+
 class TemplateField(models.TextField):
        def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
                super(TemplateField, self).__init__(*args, **kwargs)
index 14f5063..2bda588 100644 (file)
@@ -57,9 +57,6 @@ 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):
                extra_context = extra_context or {}
                view_about_to_render.send(sender=self, node=node, request=request, path=path, subpath=subpath, extra_context=extra_context)
index 323aeb8..16098d1 100644 (file)
@@ -82,9 +82,9 @@ class Page(View):
        def render_to_string(self, node=None, request=None, path=None, subpath=None, extra_context=None):
                context = {}
                context.update(extra_context or {})
-               context.update({'page': self, 'attributes': self.attributes, 'relationships': self.relationships})
+               context.update({'page': self, 'attributes': self.attributes})
                if node and request:
-                       context.update({'node': node, 'attributes': self.attributes_with_node(node), 'relationships': self.relationships_with_node(node)})
+                       context.update({'node': node, 'attributes': self.attributes_with_node(node)})
                        page_about_to_render_to_string.send(sender=self, node=node, request=request, extra_context=context)
                        string = self.template.django_template.render(RequestContext(request, context))
                else:
@@ -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 %}