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 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
 
 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.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):
 
 
 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 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
        
                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):
                raise NotImplementedError
        
        def __unicode__(self):
@@ -73,17 +78,22 @@ attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
 
 
 class JSONValue(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 __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'
        
        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()
        
        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.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'
        
        class Meta:
                app_label = 'philo'
@@ -154,19 +178,29 @@ class ManyToManyValue(AttributeValue):
        
        value = property(get_value, set_value)
        
        
        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:
                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'
        
        class Meta:
                app_label = 'philo'
@@ -245,37 +279,6 @@ class Entity(models.Model):
        def attributes(self):
                return QuerySetMapper(self.attribute_set.all())
        
        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
 
        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