Split forms into containers, entities, and fields. Split attribute fields out from...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Fri, 21 Jan 2011 18:09:58 +0000 (13:09 -0500)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Fri, 21 Jan 2011 18:09:58 +0000 (13:09 -0500)
admin/base.py
admin/pages.py
forms.py [deleted file]
forms/__init__.py [new file with mode: 0644]
forms/containers.py [new file with mode: 0644]
forms/entities.py [new file with mode: 0644]
forms/fields.py [new file with mode: 0644]
models/base.py
models/fields.py [deleted file]
models/fields/__init__.py [new file with mode: 0644]
models/fields/attributes.py [new file with mode: 0644]

index 0d35cf6..100eb31 100644 (file)
@@ -5,7 +5,7 @@ from django.http import HttpResponse
 from django.utils import simplejson as json
 from django.utils.html import escape
 from philo.models import Tag, Attribute
-from philo.forms import AttributeForm, AttributeInlineFormSet
+from philo.forms.entities import AttributeForm, AttributeInlineFormSet
 from philo.admin.widgets import TagFilteredSelectMultiple
 from mptt.admin import MPTTModelAdmin
 
index caeee05..0a09c03 100644 (file)
@@ -4,7 +4,7 @@ from django import forms
 from philo.admin.base import COLLAPSE_CLASSES, TreeAdmin
 from philo.admin.nodes import ViewAdmin
 from philo.models.pages import Page, Template, Contentlet, ContentReference
-from philo.forms import ContentletInlineFormSet, ContentReferenceInlineFormSet, ContentletForm, ContentReferenceForm
+from philo.forms.containers import *
 
 
 class ContentletInline(admin.StackedInline):
diff --git a/forms.py b/forms.py
deleted file mode 100644 (file)
index a1785fb..0000000
--- a/forms.py
+++ /dev/null
@@ -1,340 +0,0 @@
-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
-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, Attribute
-from philo.utils import fattr
-
-
-__all__ = ('EntityForm', )
-
-
-def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
-       field_list = []
-       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:
-                       continue
-               if widgets and f.name in widgets:
-                       kwargs = {'widget': widgets[f.name]}
-               else:
-                       kwargs = {}
-               formfield = formfield_callback(f, **kwargs)
-               if formfield:
-                       field_list.append((f.name, formfield))
-               else:
-                       ignored.append(f.name)
-       field_dict = SortedDict(field_list)
-       if fields:
-               field_dict = SortedDict(
-                       [(f, field_dict.get(f)) for f in fields
-                               if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)]
-               )
-       return field_dict
-
-
-# BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved
-
-class EntityFormBase(ModelForm):
-       pass
-
-_old_metaclass_new = ModelFormMetaclass.__new__
-
-def _new_metaclass_new(cls, name, bases, attrs):
-       new_class = _old_metaclass_new(cls, name, bases, attrs)
-       if issubclass(new_class, EntityFormBase) and new_class._meta.model:
-               new_class.base_fields.update(proxy_fields_for_entity_model(new_class._meta.model, new_class._meta.fields, new_class._meta.exclude, new_class._meta.widgets)) # don't pass in formfield_callback
-       return new_class
-
-ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
-
-# END HACK
-
-
-class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK
-       def __init__(self, *args, **kwargs):
-               initial = kwargs.pop('initial', None)
-               instance = kwargs.get('instance', None)
-               if instance is not None:
-                       new_initial = {}
-                       for f in instance._entity_meta.proxy_fields:
-                               if self._meta.fields and not f.name in self._meta.fields:
-                                       continue
-                               if self._meta.exclude and f.name in self._meta.exclude:
-                                       continue
-                               new_initial[f.name] = f.value_from_object(instance)
-               else:
-                       new_initial = {}
-               if initial is not None:
-                       new_initial.update(initial)
-               kwargs['initial'] = new_initial
-               super(EntityForm, self).__init__(*args, **kwargs)
-       
-       @fattr(alters_data=True)
-       def save(self, commit=True):
-               cleaned_data = self.cleaned_data
-               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:
-                       instance.save()
-                       self.save_m2m()
-               
-               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)
-               self.verbose_name = self.instance.name.replace('_', ' ')
-
-
-class ContentletForm(ContainerForm):
-       content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content')
-       
-       def should_delete(self):
-               return not bool(self.cleaned_data['content'])
-       
-       class Meta:
-               model = Contentlet
-               fields = ['name', 'content']
-
-
-class ContentReferenceForm(ContainerForm):
-       def __init__(self, *args, **kwargs):
-               super(ContentReferenceForm, self).__init__(*args, **kwargs)
-               try:
-                       self.fields['content_id'].widget = ModelLookupWidget(self.instance.content_type)
-               except ObjectDoesNotExist:
-                       # This will happen when an empty form (which we will never use) gets instantiated.
-                       pass
-       
-       def should_delete(self):
-               return (self.cleaned_data['content_id'] is None)
-       
-       class Meta:
-               model = ContentReference
-               fields = ['name', 'content_id']
-
-
-class ContainerInlineFormSet(BaseInlineFormSet):
-       def __init__(self, containers, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
-               # Unfortunately, I need to add some things to BaseInline between its __init__ and its
-               # super call, so a lot of this is repetition.
-               
-               # Start cribbed from BaseInline
-               from django.db.models.fields.related import RelatedObject
-               self.save_as_new = save_as_new
-               # is there a better way to get the object descriptor?
-               self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
-               if self.fk.rel.field_name == self.fk.rel.to._meta.pk.name:
-                       backlink_value = self.instance
-               else:
-                       backlink_value = getattr(self.instance, self.fk.rel.field_name)
-               if queryset is None:
-                       queryset = self.model._default_manager
-               qs = queryset.filter(**{self.fk.name: backlink_value})
-               # End cribbed from BaseInline
-               
-               self.container_instances, qs = self.get_container_instances(containers, qs)
-               self.extra_containers = containers
-               self.extra = len(self.extra_containers)
-               super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs)
-       
-       def get_container_instances(self, containers, qs):
-               raise NotImplementedError
-       
-       def total_form_count(self):
-               if self.data or self.files:
-                       return self.management_form.cleaned_data[TOTAL_FORM_COUNT]
-               else:
-                       return self.initial_form_count() + self.extra
-       
-       def save_existing_objects(self, commit=True):
-               self.changed_objects = []
-               self.deleted_objects = []
-               if not self.get_queryset():
-                       return []
-
-               saved_instances = []
-               for form in self.initial_forms:
-                       pk_name = self._pk_field.name
-                       raw_pk_value = form._raw_value(pk_name)
-
-                       # clean() for different types of PK fields can sometimes return
-                       # the model instance, and sometimes the PK. Handle either.
-                       pk_value = form.fields[pk_name].clean(raw_pk_value)
-                       pk_value = getattr(pk_value, 'pk', pk_value)
-
-                       obj = self._existing_object(pk_value)
-                       if form.should_delete():
-                               self.deleted_objects.append(obj)
-                               obj.delete()
-                               continue
-                       if form.has_changed():
-                               self.changed_objects.append((obj, form.changed_data))
-                               saved_instances.append(self.save_existing(form, obj, commit=commit))
-                               if not commit:
-                                       self.saved_forms.append(form)
-               return saved_instances
-
-       def save_new_objects(self, commit=True):
-               self.new_objects = []
-               for form in self.extra_forms:
-                       if not form.has_changed():
-                               continue
-                       # If someone has marked an add form for deletion, don't save the
-                       # object.
-                       if form.should_delete():
-                               continue
-                       self.new_objects.append(self.save_new(form, commit=commit))
-                       if not commit:
-                               self.saved_forms.append(form)
-               return self.new_objects
-
-
-class ContentletInlineFormSet(ContainerInlineFormSet):
-       def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
-               if instance is None:
-                       self.instance = self.fk.rel.to()
-               else:
-                       self.instance = instance
-               
-               try:
-                       containers = list(self.instance.containers[0])
-               except ObjectDoesNotExist:
-                       containers = []
-       
-               super(ContentletInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
-       
-       def get_container_instances(self, containers, qs):
-               qs = qs.filter(name__in=containers)
-               container_instances = []
-               for container in qs:
-                       container_instances.append(container)
-                       containers.remove(container.name)
-               return container_instances, qs
-       
-       def _construct_form(self, i, **kwargs):
-               if i >= self.initial_form_count(): # and not kwargs.get('instance'):
-                       kwargs['instance'] = self.model(name=self.extra_containers[i - self.initial_form_count() - 1])
-               
-               return super(ContentletInlineFormSet, self)._construct_form(i, **kwargs)
-
-
-class ContentReferenceInlineFormSet(ContainerInlineFormSet):
-       def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
-               if instance is None:
-                       self.instance = self.fk.rel.to()
-               else:
-                       self.instance = instance
-               
-               try:
-                       containers = list(self.instance.containers[1])
-               except ObjectDoesNotExist:
-                       containers = []
-       
-               super(ContentReferenceInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
-       
-       def get_container_instances(self, containers, qs):
-               filter = Q()
-               
-               for name, ct in containers:
-                       filter |= Q(name=name, content_type=ct)
-               
-               qs = qs.filter(filter)
-               container_instances = []
-               for container in qs:
-                       container_instances.append(container)
-                       containers.remove((container.name, container.content_type))
-               return container_instances, qs
-
-       def _construct_form(self, i, **kwargs):
-               if i >= self.initial_form_count(): # and not kwargs.get('instance'):
-                       name, content_type = self.extra_containers[i - self.initial_form_count() - 1]
-                       kwargs['instance'] = self.model(name=name, content_type=content_type)
-
-               return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs)
\ No newline at end of file
diff --git a/forms/__init__.py b/forms/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/forms/containers.py b/forms/containers.py
new file mode 100644 (file)
index 0000000..5991dfa
--- /dev/null
@@ -0,0 +1,190 @@
+from django import forms
+from django.contrib.admin.widgets import AdminTextareaWidget
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Q
+from django.forms.models import ModelForm, BaseInlineFormSet
+from django.forms.formsets import TOTAL_FORM_COUNT
+from philo.admin.widgets import ModelLookupWidget
+from philo.models import Contentlet, ContentReference
+
+
+__all__ = (
+       'ContentletForm',
+       'ContentletInlineFormSet',
+       'ContentReferenceForm',
+       'ContentReferenceInlineFormSet'
+)
+
+
+class ContainerForm(ModelForm):
+       def __init__(self, *args, **kwargs):
+               super(ContainerForm, self).__init__(*args, **kwargs)
+               self.verbose_name = self.instance.name.replace('_', ' ')
+
+
+class ContentletForm(ContainerForm):
+       content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content')
+       
+       def should_delete(self):
+               return not bool(self.cleaned_data['content'])
+       
+       class Meta:
+               model = Contentlet
+               fields = ['name', 'content']
+
+
+class ContentReferenceForm(ContainerForm):
+       def __init__(self, *args, **kwargs):
+               super(ContentReferenceForm, self).__init__(*args, **kwargs)
+               try:
+                       self.fields['content_id'].widget = ModelLookupWidget(self.instance.content_type)
+               except ObjectDoesNotExist:
+                       # This will happen when an empty form (which we will never use) gets instantiated.
+                       pass
+       
+       def should_delete(self):
+               return (self.cleaned_data['content_id'] is None)
+       
+       class Meta:
+               model = ContentReference
+               fields = ['name', 'content_id']
+
+
+class ContainerInlineFormSet(BaseInlineFormSet):
+       def __init__(self, containers, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
+               # Unfortunately, I need to add some things to BaseInline between its __init__ and its
+               # super call, so a lot of this is repetition.
+               
+               # Start cribbed from BaseInline
+               from django.db.models.fields.related import RelatedObject
+               self.save_as_new = save_as_new
+               # is there a better way to get the object descriptor?
+               self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
+               if self.fk.rel.field_name == self.fk.rel.to._meta.pk.name:
+                       backlink_value = self.instance
+               else:
+                       backlink_value = getattr(self.instance, self.fk.rel.field_name)
+               if queryset is None:
+                       queryset = self.model._default_manager
+               qs = queryset.filter(**{self.fk.name: backlink_value})
+               # End cribbed from BaseInline
+               
+               self.container_instances, qs = self.get_container_instances(containers, qs)
+               self.extra_containers = containers
+               self.extra = len(self.extra_containers)
+               super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs)
+       
+       def get_container_instances(self, containers, qs):
+               raise NotImplementedError
+       
+       def total_form_count(self):
+               if self.data or self.files:
+                       return self.management_form.cleaned_data[TOTAL_FORM_COUNT]
+               else:
+                       return self.initial_form_count() + self.extra
+       
+       def save_existing_objects(self, commit=True):
+               self.changed_objects = []
+               self.deleted_objects = []
+               if not self.get_queryset():
+                       return []
+
+               saved_instances = []
+               for form in self.initial_forms:
+                       pk_name = self._pk_field.name
+                       raw_pk_value = form._raw_value(pk_name)
+
+                       # clean() for different types of PK fields can sometimes return
+                       # the model instance, and sometimes the PK. Handle either.
+                       pk_value = form.fields[pk_name].clean(raw_pk_value)
+                       pk_value = getattr(pk_value, 'pk', pk_value)
+
+                       obj = self._existing_object(pk_value)
+                       if form.should_delete():
+                               self.deleted_objects.append(obj)
+                               obj.delete()
+                               continue
+                       if form.has_changed():
+                               self.changed_objects.append((obj, form.changed_data))
+                               saved_instances.append(self.save_existing(form, obj, commit=commit))
+                               if not commit:
+                                       self.saved_forms.append(form)
+               return saved_instances
+
+       def save_new_objects(self, commit=True):
+               self.new_objects = []
+               for form in self.extra_forms:
+                       if not form.has_changed():
+                               continue
+                       # If someone has marked an add form for deletion, don't save the
+                       # object.
+                       if form.should_delete():
+                               continue
+                       self.new_objects.append(self.save_new(form, commit=commit))
+                       if not commit:
+                               self.saved_forms.append(form)
+               return self.new_objects
+
+
+class ContentletInlineFormSet(ContainerInlineFormSet):
+       def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
+               if instance is None:
+                       self.instance = self.fk.rel.to()
+               else:
+                       self.instance = instance
+               
+               try:
+                       containers = list(self.instance.containers[0])
+               except ObjectDoesNotExist:
+                       containers = []
+       
+               super(ContentletInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
+       
+       def get_container_instances(self, containers, qs):
+               qs = qs.filter(name__in=containers)
+               container_instances = []
+               for container in qs:
+                       container_instances.append(container)
+                       containers.remove(container.name)
+               return container_instances, qs
+       
+       def _construct_form(self, i, **kwargs):
+               if i >= self.initial_form_count(): # and not kwargs.get('instance'):
+                       kwargs['instance'] = self.model(name=self.extra_containers[i - self.initial_form_count() - 1])
+               
+               return super(ContentletInlineFormSet, self)._construct_form(i, **kwargs)
+
+
+class ContentReferenceInlineFormSet(ContainerInlineFormSet):
+       def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
+               if instance is None:
+                       self.instance = self.fk.rel.to()
+               else:
+                       self.instance = instance
+               
+               try:
+                       containers = list(self.instance.containers[1])
+               except ObjectDoesNotExist:
+                       containers = []
+       
+               super(ContentReferenceInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
+       
+       def get_container_instances(self, containers, qs):
+               filter = Q()
+               
+               for name, ct in containers:
+                       filter |= Q(name=name, content_type=ct)
+               
+               qs = qs.filter(filter)
+               container_instances = []
+               for container in qs:
+                       container_instances.append(container)
+                       containers.remove((container.name, container.content_type))
+               return container_instances, qs
+
+       def _construct_form(self, i, **kwargs):
+               if i >= self.initial_form_count(): # and not kwargs.get('instance'):
+                       name, content_type = self.extra_containers[i - self.initial_form_count() - 1]
+                       kwargs['instance'] = self.model(name=name, content_type=content_type)
+
+               return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs)
\ No newline at end of file
diff --git a/forms/entities.py b/forms/entities.py
new file mode 100644 (file)
index 0000000..a392e45
--- /dev/null
@@ -0,0 +1,175 @@
+from django import forms
+from django.contrib.contenttypes.generic import BaseGenericInlineFormSet
+from django.contrib.contenttypes.models import ContentType
+from django.forms.models import ModelFormMetaclass, ModelForm
+from django.utils.datastructures import SortedDict
+from philo.models import Attribute
+from philo.utils import fattr
+
+
+__all__ = ('EntityForm', 'AttributeForm', 'AttributeInlineFormSet')
+
+
+def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
+       field_list = []
+       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:
+                       continue
+               if widgets and f.name in widgets:
+                       kwargs = {'widget': widgets[f.name]}
+               else:
+                       kwargs = {}
+               formfield = formfield_callback(f, **kwargs)
+               if formfield:
+                       field_list.append((f.name, formfield))
+               else:
+                       ignored.append(f.name)
+       field_dict = SortedDict(field_list)
+       if fields:
+               field_dict = SortedDict(
+                       [(f, field_dict.get(f)) for f in fields
+                               if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)]
+               )
+       return field_dict
+
+
+# BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved
+
+class EntityFormBase(ModelForm):
+       pass
+
+_old_metaclass_new = ModelFormMetaclass.__new__
+
+def _new_metaclass_new(cls, name, bases, attrs):
+       new_class = _old_metaclass_new(cls, name, bases, attrs)
+       if issubclass(new_class, EntityFormBase) and new_class._meta.model:
+               new_class.base_fields.update(proxy_fields_for_entity_model(new_class._meta.model, new_class._meta.fields, new_class._meta.exclude, new_class._meta.widgets)) # don't pass in formfield_callback
+       return new_class
+
+ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
+
+# END HACK
+
+
+class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK
+       def __init__(self, *args, **kwargs):
+               initial = kwargs.pop('initial', None)
+               instance = kwargs.get('instance', None)
+               if instance is not None:
+                       new_initial = {}
+                       for f in instance._entity_meta.proxy_fields:
+                               if self._meta.fields and not f.name in self._meta.fields:
+                                       continue
+                               if self._meta.exclude and f.name in self._meta.exclude:
+                                       continue
+                               new_initial[f.name] = f.value_from_object(instance)
+               else:
+                       new_initial = {}
+               if initial is not None:
+                       new_initial.update(initial)
+               kwargs['initial'] = new_initial
+               super(EntityForm, self).__init__(*args, **kwargs)
+       
+       @fattr(alters_data=True)
+       def save(self, commit=True):
+               cleaned_data = self.cleaned_data
+               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:
+                       instance.save()
+                       self.save_m2m()
+               
+               return instance
+
+       
+       def apply_data(self, cleaned_data):
+               self.value = cleaned_data.get('value', None)
+       
+       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
+       
+       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.value = []
+
+class AttributeForm(ModelForm):
+       """
+       This class handles an attribute's fields as well as the fields for its value (if there is one.)
+       The fields defined will vary depending on the value type, but the fields for defining the value
+       (i.e. value_content_type and value_object_id) will always be defined. Except that value_object_id
+       will never be defined. BLARGH!
+       """
+       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.
+               value = self.instance.value
+               self._cached_value_ct = self.instance.value_content_type
+               self._cached_value = value
+               
+               # If there is a value, pull in its fields.
+               if value is not None:
+                       self.value_fields = value.value_formfields()
+                       self.fields.update(self.value_fields)
+       
+       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:
+                       # The value content type has changed. Clear the old value, if there was one.
+                       if self._cached_value:
+                               self._cached_value.delete()
+                       
+                       # Clear the submitted value, if any.
+                       self.cleaned_data.pop('value', None)
+                       
+                       # Now create a new value instance so that on next instantiation, the form will
+                       # know what fields to add.
+                       if self.instance.value_content_type is not None:
+                               self.instance.value = self.instance.value_content_type.model_class().objects.create()
+               elif self.instance.value is not None:
+                       # The value content type is the same, but one of the value fields has changed.
+                       
+                       # Use construct_instance to apply the changes from the cleaned_data to the value instance.
+                       fields = self.value_fields.keys()
+                       if set(fields) & set(self.changed_data):
+                               self.instance.value.construct_instance(**dict([(key, self.cleaned_data[key]) for key in fields]))
+                               self.instance.value.save()
+               
+               return super(AttributeForm, self).save(*args, **kwargs)
+       
+       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()
\ No newline at end of file
diff --git a/forms/fields.py b/forms/fields.py
new file mode 100644 (file)
index 0000000..9da6552
--- /dev/null
@@ -0,0 +1,18 @@
+from django import forms
+from django.utils import simplejson as json
+from philo.validators import json_validator
+
+
+__all__ = ('JSONFormField',)
+
+
+class JSONFormField(forms.Field):
+       default_validators = [json_validator]
+       
+       def clean(self, value):
+               if value == '' and not self.required:
+                       return None
+               try:
+                       return json.loads(value)
+               except Exception, e:
+                       raise ValidationError(u'JSON decode error: %s' % e)
\ No newline at end of file
index 8140520..20693b7 100644 (file)
@@ -56,10 +56,15 @@ class AttributeValue(models.Model):
        def attribute(self):
                return self.attribute_set.all()[0]
        
-       def apply_data(self, data):
+       def set_value(self, value):
+               raise NotImplementedError
+       
+       def value_formfields(self, **kwargs):
+               """Define any formfields that would be used to construct an instance of this value."""
                raise NotImplementedError
        
-       def value_formfield(self, **kwargs):
+       def construct_instance(self, **kwargs):
+               """Apply cleaned data from the formfields generated by valid_formfields to oneself."""
                raise NotImplementedError
        
        def __unicode__(self):
@@ -73,17 +78,22 @@ attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
 
 
 class JSONValue(AttributeValue):
-       value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
+       value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null')
        
        def __unicode__(self):
                return smart_str(self.value)
        
-       def value_formfield(self, **kwargs):
-               kwargs['initial'] = self.value_json
-               return self._meta.get_field('value').formfield(**kwargs)
+       def value_formfields(self):
+               kwargs = {'initial': self.value_json}
+               field = self._meta.get_field('value')
+               return {field.name: field.formfield(**kwargs)}
        
-       def apply_data(self, cleaned_data):
-               self.value = cleaned_data.get('value', None)
+       def construct_instance(self, **kwargs):
+               field_name = self._meta.get_field('value').name
+               self.set_value(kwargs.pop(field_name, None))
+       
+       def set_value(self, value):
+               self.value = value
        
        class Meta:
                app_label = 'philo'
@@ -94,19 +104,33 @@ class ForeignKeyValue(AttributeValue):
        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.
+       def value_formfields(self):
+               field = self._meta.get_field('content_type')
+               fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
+               
+               if self.content_type:
+                       kwargs = {
+                               'initial': self.object_id,
+                               'required': False,
+                               'queryset': self.content_type.model_class()._default_manager.all()
+                       }
+                       fields['value'] = forms.ModelChoiceField(**kwargs)
+               return fields
+       
+       def construct_instance(self, **kwargs):
+               field_name = self._meta.get_field('content_type').name
+               ct = kwargs.pop(field_name, None)
+               if ct is None or ct != self.content_type:
                        self.object_id = None
+                       self.content_type = ct
+               else:
+                       value = kwargs.pop('value', None)
+                       self.set_value(value)
+                       if value is None:
+                               self.content_type = ct
+       
+       def set_value(self, value):
+               self.value = value
        
        class Meta:
                app_label = 'philo'
@@ -154,19 +178,29 @@ class ManyToManyValue(AttributeValue):
        
        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']
+       def value_formfields(self):
+               field = self._meta.get_field('content_type')
+               fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
+               
+               if self.content_type:
+                       kwargs = {
+                               'initial': self.get_object_id_list(),
+                               'required': False,
+                               'queryset': self.content_type.model_class()._default_manager.all()
+                       }
+                       fields['value'] = forms.ModelMultipleChoiceField(**kwargs)
+               return fields
+       
+       def construct_instance(self, **kwargs):
+               field_name = self._meta.get_field('content_type').name
+               ct = kwargs.pop(field_name, None)
+               if ct is None or ct != self.content_type:
+                       self.values.clear()
+                       self.content_type = ct
                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.value = []
+                       value = kwargs.get('value', self.content_type.model_class()._default_manager.none())
+                       self.set_value(value)
+       construct_instance.alters_data = True
        
        class Meta:
                app_label = 'philo'
@@ -245,37 +279,6 @@ class Entity(models.Model):
        def attributes(self):
                return QuerySetMapper(self.attribute_set.all())
        
-       @property
-       def _added_attribute_registry(self):
-               if not hasattr(self, '_real_added_attribute_registry'):
-                       self._real_added_attribute_registry = {}
-               return self._real_added_attribute_registry
-       
-       @property
-       def _removed_attribute_registry(self):
-               if not hasattr(self, '_real_removed_attribute_registry'):
-                       self._real_removed_attribute_registry = []
-               return self._real_removed_attribute_registry
-       
-       def save(self, *args, **kwargs):
-               super(Entity, self).save(*args, **kwargs)
-               
-               for key in self._removed_attribute_registry:
-                       self.attribute_set.filter(key__exact=key).delete()
-               del self._removed_attribute_registry[:]
-               
-               for field, value in self._added_attribute_registry.items():
-                       try:
-                               attribute = self.attribute_set.get(key__exact=field.key)
-                       except Attribute.DoesNotExist:
-                               attribute = Attribute()
-                               attribute.entity = self
-                               attribute.key = field.key
-                       
-                       field.set_attribute_value(attribute, value)
-                       attribute.save()
-               self._added_attribute_registry.clear()
-       
        class Meta:
                abstract = True
 
diff --git a/models/fields.py b/models/fields.py
deleted file mode 100644 (file)
index 83798c4..0000000
+++ /dev/null
@@ -1,263 +0,0 @@
-from django import forms
-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.signals import entity_class_prepared
-from philo.validators import TemplateValidator, json_validator
-
-
-__all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
-
-
-class EntityProxyField(object):
-       descriptor_class = None
-       
-       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 = 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
-                       if self.verbose_name is None and name:
-                               self.verbose_name = name.replace('_', ' ')
-                       entity_class_prepared.connect(self.actually_contribute_to_class, sender=cls)
-               else:
-                       raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__)
-       
-       def formfield(self, *args, **kwargs):
-               raise NotImplementedError('EntityProxyField subclasses must implement a formfield method.')
-       
-       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):
-       def __init__(self, field):
-               self.field = field
-       
-       def __get__(self, instance, owner):
-               if instance:
-                       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:
-                       return None
-       
-       def __set__(self, instance, value):
-               raise NotImplementedError('AttributeFieldDescriptor subclasses must implement a __set__ method.')
-       
-       def __delete__(self, instance):
-               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.query.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):
-       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)
-               self.key = key
-               if field_template is None:
-                       field_template = models.CharField(max_length=255)
-               self.field_template = field_template
-       
-       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)
-       
-       def value_from_object(self, obj):
-               try:
-                       return getattr(obj, self.attname)
-               except AttributeError:
-                       return None
-       
-       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 ForeignKeyAttribute(AttributeField):
-       descriptor_class = ForeignKeyAttributeDescriptor
-       
-       def __init__(self, model, limit_choices_to=None, key=None, **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 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):
-               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):
-       def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
-               super(TemplateField, self).__init__(*args, **kwargs)
-               self.validators.append(TemplateValidator(allow, disallow, secure))
-
-
-class JSONFormField(forms.Field):
-       default_validators = [json_validator]
-       
-       def clean(self, value):
-               if value == '' and not self.required:
-                       return None
-               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):
-       default_validators = [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))
-               models.signals.pre_init.connect(self.fix_init_kwarg, sender=cls)
-       
-       def fix_init_kwarg(self, sender, args, kwargs, **signal_kwargs):
-               if self.name in kwargs:
-                       kwargs[self.attname] = json.dumps(kwargs.pop(self.name))
-       
-       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"])
-       add_introspection_rules([], ["^philo\.models\.fields\.JSONField"])
\ No newline at end of file
diff --git a/models/fields/__init__.py b/models/fields/__init__.py
new file mode 100644 (file)
index 0000000..a22e161
--- /dev/null
@@ -0,0 +1,64 @@
+from django import forms
+from django.db import models
+from django.utils import simplejson as json
+from philo.forms.fields import JSONFormField
+from philo.validators import TemplateValidator, json_validator
+from philo.models.fields.attributes import *
+
+
+class TemplateField(models.TextField):
+       def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
+               super(TemplateField, self).__init__(*args, **kwargs)
+               self.validators.append(TemplateValidator(allow, disallow, secure))
+
+
+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):
+       default_validators = [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))
+               models.signals.pre_init.connect(self.fix_init_kwarg, sender=cls)
+       
+       def fix_init_kwarg(self, sender, args, kwargs, **signal_kwargs):
+               if self.name in kwargs:
+                       kwargs[self.attname] = json.dumps(kwargs.pop(self.name))
+       
+       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"])
+       add_introspection_rules([], ["^philo\.models\.fields\.JSONField"])
\ No newline at end of file
diff --git a/models/fields/attributes.py b/models/fields/attributes.py
new file mode 100644 (file)
index 0000000..f85fb32
--- /dev/null
@@ -0,0 +1,242 @@
+"""
+The Attributes defined in this file can be assigned as fields on a proxy of
+a subclass of philo.models.Entity. They act like any other model fields,
+but instead of saving their data to the database, they save it to
+attributes related to a model instance. Additionally, a new attribute will
+be created for an instance if and only if the field's value has been set.
+This is relevant i.e. for passthroughs, where the value of the field may
+be defined by some other instance's attributes.
+
+Example::
+
+       class Thing(Entity):
+               numbers = models.PositiveIntegerField()
+       
+       class ThingProxy(Thing):
+               improvised = JSONAttribute(models.BooleanField)
+               
+               class Meta:
+                       proxy = True
+"""
+from django import forms
+from django.core.exceptions import FieldError
+from django.db import models
+from django.db.models.fields import NOT_PROVIDED
+from django.utils.text import capfirst
+from philo.signals import entity_class_prepared
+
+
+__all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
+
+
+ATTRIBUTE_REGISTRY = '_attribute_registry'
+
+
+class EntityProxyField(object):
+       def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, *args, **kwargs):
+               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)
+       
+       def contribute_to_class(self, cls, name):
+               from philo.models.base import Entity
+               if issubclass(cls, Entity):
+                       self.name = name
+                       self.model = cls
+                       if self.verbose_name is None and name:
+                               self.verbose_name = name.replace('_', ' ')
+                       entity_class_prepared.connect(self.actually_contribute_to_class, sender=cls)
+               else:
+                       raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__)
+       
+       def formfield(self, form_class=forms.CharField, **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(**defaults)
+       
+       def value_from_object(self, obj):
+               return getattr(obj, self.name)
+       
+       def has_default(self):
+               return self.default is not NOT_PROVIDED
+
+
+class AttributeFieldDescriptor(object):
+       def __init__(self, field):
+               self.field = field
+       
+       def get_registry(self, instance):
+               if ATTRIBUTE_REGISTRY not in instance.__dict__:
+                       instance.__dict__[ATTRIBUTE_REGISTRY] = {'added': set(), 'removed': set()}
+               return instance.__dict__[ATTRIBUTE_REGISTRY]
+       
+       def __get__(self, instance, owner):
+               if instance is None:
+                       return self
+               
+               if self.field.name not in instance.__dict__:
+                       instance.__dict__[self.field.name] = instance.attributes[self.field.attribute_key]
+               
+               return instance.__dict__[self.field.name]
+       
+       def __set__(self, instance, value):
+               if instance is None:
+                       raise AttributeError("%s must be accessed via instance" % self.field.name)
+               
+               self.field.validate_value(value)
+               instance.__dict__[self.field.name] = value
+               
+               registry = self.get_registry(instance)
+               registry['added'].add(self.field)
+               registry['removed'].remove(self.field)
+       
+       def __delete__(self, instance):
+               del instance.__dict__[self.field.name]
+               
+               registry = self.get_registry(instance)
+               registry['added'].remove(self.field)
+               registry['removed'].add(self.field)
+
+
+def process_attribute_fields(sender, instance, created, **kwargs):
+       if ATTRIBUTE_REGISTRY in instance.__dict__:
+               registry = instance.__dict__[ATTRIBUTE_REGISTRY]
+               instance.attribute_set.filter(key__in=[field.attribute_key for field in registry['removed']]).delete()
+               
+               from philo.models import Attribute
+               for field in registry['added']:
+                       try:
+                               attribute = self.attribute_set.get(key=field.key)
+                       except Attribute.DoesNotExist:
+                               attribute = Attribute()
+                               attribute.entity = instance
+                               attribute.key = field.key
+                       
+                       value_class = field.get_value_class()
+                       if isinstance(attribute.value, value_class):
+                               value = attribute.value
+                       else:
+                               if isinstance(attribute.value, models.Model):
+                                       attribute.value.delete()
+                               value = value_class()
+                       
+                       value.set_value(field.value_from_object(instance))
+                       value.save()
+                       
+                       attribute.value = value
+                       attribute.save()
+               del instance.__dict__[ATTRIBUTE_REGISTRY]
+
+
+class AttributeField(EntityProxyField):
+       def __init__(self, attribute_key=None, **kwargs):
+               self.attribute_key = attribute_key
+               super(AttributeField, self).__init__(**kwargs)
+       
+       def actually_contribute_to_class(self, sender, **kwargs):
+               super(AttributeField, self).actually_contribute_to_class(sender, **kwargs)
+               setattr(sender, self.name, AttributeFieldDescriptor(self))
+               opts = sender._entity_meta
+               if not hasattr(opts, '_has_attribute_fields'):
+                       opts._has_attribute_fields = True
+                       models.signals.post_save.connect(process_attribute_fields, sender=sender)
+               
+       
+       def contribute_to_class(self, cls, name):
+               if self.attribute_key is None:
+                       self.attribute_key = name
+               super(AttributeField, self).contribute_to_class(cls, name)
+       
+       def validate_value(self, value):
+               "Confirm that the value is valid or raise an appropriate error."
+               raise NotImplementedError("validate_value must be implemented by AttributeField subclasses.")
+       
+       def get_value_class(self):
+               raise NotImplementedError("get_value_class must be implemented by AttributeField subclasses.")
+
+
+class JSONAttribute(AttributeField):
+       def __init__(self, field_template=None, **kwargs):
+               super(JSONAttribute, self).__init__(**kwargs)
+               if field_template is None:
+                       field_template = models.CharField(max_length=255)
+               self.field_template = field_template
+       
+       def validate_value(self, value):
+               pass
+       
+       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)
+       
+       def get_value_class(self):
+               from philo.models import JSONValue
+               return JSONValue
+       
+       # Not sure what this is doing - keep eyes open!
+       #def value_from_object(self, obj):
+       #       try:
+       #               return getattr(obj, self.name)
+       #       except AttributeError:
+       #               return None
+
+
+class ForeignKeyAttribute(AttributeField):
+       def __init__(self, model, limit_choices_to=None, **kwargs):
+               super(ForeignKeyAttribute, self).__init__(**kwargs)
+               self.model = model
+               if limit_choices_to is None:
+                       limit_choices_to = {}
+               self.limit_choices_to = limit_choices_to
+       
+       def validate_value(self, value):
+               if value is not None and not isinstance(value, self.model) :
+                       raise TypeError("The '%s' attribute can only be set to an instance of %s or None." % (self.name, self.model.__name__))
+       
+       def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
+               defaults = {
+                       'queryset': self.model._default_manager.complex_filter(self.limit_choices_to)
+               }
+               defaults.update(kwargs)
+               return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
+       
+       def get_value_class(self):
+               from philo.models import ForeignKeyValue
+               return ForeignKeyValue
+       
+       #def value_from_object(self, obj):
+       #       try:
+       #               relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
+       #       except AttributeError:
+       #               return None
+       #       return getattr(relobj, 'pk', None)
+
+
+class ManyToManyAttribute(ForeignKeyAttribute):
+       def validate_value(self, value):
+               if not isinstance(value, models.query.QuerySet) or value.model != self.model:
+                       raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.model.__name__))
+       
+       def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
+               return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
+       
+       def get_value_class(self):
+               from philo.models import ManyToManyValue
+               return ManyToManyValue
\ No newline at end of file