Merge branch 'entity-proxy-fields'
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Mon, 9 Aug 2010 06:35:54 +0000 (02:35 -0400)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Mon, 9 Aug 2010 06:39:22 +0000 (02:39 -0400)
* entity-proxy-fields:
  Implemented EntityForm, which knows how to deal with AttributeFields and RelationshipFields on the underlying model. To be consistent with Attributes, Relationships now support null values. This changes the schema.
  AttributeFields and RelationshipFields now store their changes and do not commit them until their model has been saved.
  Initial implementation of AttributeField and RelationshipField. These objects are not actual Fields, but rather they simply add descriptors to Entity subclasses which provide transparent access to specific attributes and relationships.

forms.py [new file with mode: 0644]
models/__init__.py
models/base.py
models/fields.py [new file with mode: 0644]
signals.py [new file with mode: 0644]

diff --git a/forms.py b/forms.py
new file mode 100644 (file)
index 0000000..b428d28
--- /dev/null
+++ b/forms.py
@@ -0,0 +1,89 @@
+from django.forms.models import model_to_dict, fields_for_model, ModelFormMetaclass, ModelForm
+from django.utils.datastructures import SortedDict
+from philo.models import Entity
+from philo.models.fields import RelationshipField
+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 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)]
+               )
+       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 self._meta.fields and f.name not in self._meta.fields:
+                               continue
+                       setattr(instance, f.attname, cleaned_data[f.name])
+               
+               if commit:
+                       instance.save()
+                       self.save_m2m()
+               
+               return instance
\ No newline at end of file
index b9ea3ac..5d39ac6 100644 (file)
@@ -2,6 +2,7 @@ from philo.models.base import *
 from philo.models.collections import *
 from philo.models.nodes import *
 from philo.models.pages import *
+from philo.models.fields import *
 from django.contrib.auth.models import User, Group
 from django.contrib.sites.models import Site
 
index 6f23191..81e557f 100644 (file)
@@ -4,6 +4,7 @@ from django.contrib.contenttypes import generic
 from django.utils import simplejson as json
 from django.core.exceptions import ObjectDoesNotExist
 from philo.utils import ContentTypeRegistryLimiter
+from philo.signals import entity_class_prepared
 from UserDict import DictMixin
 
 
@@ -71,8 +72,8 @@ class Relationship(models.Model):
        entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
        entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
        key = models.CharField(max_length=255)
-       value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type')
-       value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
+       value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+       value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
        value = generic.GenericForeignKey('value_content_type', 'value_object_id')
        
        def __unicode__(self):
@@ -102,7 +103,30 @@ class QuerySetMapper(object, DictMixin):
                return list(keys)
 
 
+class EntityOptions(object):
+       def __init__(self, options):
+               if options is not None:
+                       for key, value in options.__dict__.items():
+                               setattr(self, key, value)
+               if not hasattr(self, 'proxy_fields'):
+                       self.proxy_fields = []
+       
+       def add_proxy_field(self, proxy_field):
+               self.proxy_fields.append(proxy_field)
+
+
+class EntityBase(models.base.ModelBase):
+       def __new__(cls, name, bases, attrs):
+               new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
+               entity_options = attrs.pop('EntityMeta', None)
+               setattr(new, '_entity_meta', EntityOptions(entity_options))
+               entity_class_prepared.send(sender=new)
+               return new
+
+
 class Entity(models.Model):
+       __metaclass__ = EntityBase
+       
        attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
        relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
        
@@ -114,6 +138,63 @@ class Entity(models.Model):
        def relationships(self):
                return QuerySetMapper(self.relationship_set)
        
+       @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
+       
+       @property
+       def _added_relationship_registry(self):
+               if not hasattr(self, '_real_added_relationship_registry'):
+                       self._real_added_relationship_registry = {}
+               return self._real_added_relationship_registry
+       
+       @property
+       def _removed_relationship_registry(self):
+               if not hasattr(self, '_real_removed_relationship_registry'):
+                       self._real_removed_relationship_registry = []
+               return self._real_removed_relationship_registry
+       
+       def save(self, *args, **kwargs):
+               super(Entity, self).save(*args, **kwargs)
+               
+               for key in self._removed_attribute_registry:
+                       self.attribute_set.filter(key__exact=key).delete()
+               del self._removed_attribute_registry[:]
+               
+               for key, value in self._added_attribute_registry.items():
+                       try:
+                               attribute = self.attribute_set.get(key__exact=key)
+                       except Attribute.DoesNotExist:
+                               attribute = Attribute()
+                               attribute.entity = self
+                               attribute.key = key
+                       attribute.value = value
+                       attribute.save()
+               self._added_attribute_registry.clear()
+               
+               for key in self._removed_relationship_registry:
+                       self.relationship_set.filter(key__exact=key).delete()
+               del self._removed_relationship_registry[:]
+               
+               for key, value in self._added_relationship_registry.items():
+                       try:
+                               relationship = self.relationship_set.get(key__exact=key)
+                       except Relationship.DoesNotExist:
+                               relationship = Relationship()
+                               relationship.entity = self
+                               relationship.key = key
+                       relationship.value = value
+                       relationship.save()
+               self._added_relationship_registry.clear()
+       
        class Meta:
                abstract = True
 
diff --git a/models/fields.py b/models/fields.py
new file mode 100644 (file)
index 0000000..9fc2dfb
--- /dev/null
@@ -0,0 +1,128 @@
+from django.db import models
+from django import forms
+from django.core.exceptions import FieldError
+from philo.models.base import Entity
+from philo.signals import entity_class_prepared
+
+
+__all__ = ('AttributeField', 'RelationshipField')
+
+
+class EntityProxyField(object):
+       descriptor_class = None
+       
+       def __init__(self, *args, **kwargs):
+               if self.descriptor_class is None:
+                       raise NotImplementedError('EntityProxyField subclasses must specify a descriptor_class.')
+       
+       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):
+               if issubclass(cls, Entity):
+                       self.name = name
+                       self.attname = name
+                       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)
+
+
+class AttributeFieldDescriptor(object):
+       def __init__(self, field):
+               self.field = field
+       
+       def __get__(self, instance, owner):
+               if instance:
+                       if self.field.key in instance._added_attribute_registry:
+                               return instance._added_attribute_registry[self.field.key]
+                       if self.field.key in instance._removed_attribute_registry:
+                               return None
+                       try:
+                               return instance.attributes[self.field.key]
+                       except KeyError:
+                               return None
+               else:
+                       raise AttributeError('The \'%s\' attribute can only be accessed from %s instances.' % (self.field.name, owner.__name__))
+       
+       def __set__(self, instance, value):
+               if self.field.key in instance._removed_attribute_registry:
+                       instance._removed_attribute_registry.remove(self.field.key)
+               instance._added_attribute_registry[self.field.key] = value
+       
+       def __delete__(self, instance):
+               if self.field.key in instance._added_attribute_registry:
+                       del instance._added_attribute_registry[self.field.key]
+               instance._removed_attribute_registry.append(self.field.key)
+
+
+class AttributeField(EntityProxyField):
+       descriptor_class = AttributeFieldDescriptor
+       
+       def __init__(self, key, field_template=None):
+               self.key = key
+               if field_template is None:
+                       field_template = models.CharField(max_length=255)
+               self.field_template = field_template
+       
+       def formfield(self, *args, **kwargs):
+               field = self.field_template.formfield(*args, **kwargs)
+               field.required = False
+               return field
+
+
+class RelationshipFieldDescriptor(object):
+       def __init__(self, field):
+               self.field = field
+       
+       def __get__(self, instance, owner):
+               if instance:
+                       if self.field.key in instance._added_relationship_registry:
+                               return instance._added_relationship_registry[self.field.key]
+                       if self.field.key in instance._removed_relationship_registry:
+                               return None
+                       try:
+                               return instance.relationships[self.field.key]
+                       except KeyError:
+                               return None
+               else:
+                       raise AttributeError('The \'%s\' attribute can only be accessed from %s instances.' % (self.field.name, owner.__name__))
+       
+       def __set__(self, instance, value):
+               if isinstance(value, (models.Model, type(None))):
+                       if self.field.key in instance._removed_relationship_registry:
+                               instance._removed_relationship_registry.remove(self.field.key)
+                       instance._added_relationship_registry[self.field.key] = value
+               else:
+                       raise AttributeError('The \'%s\' attribute can only be set using existing Model objects.' % self.field.name)
+       
+       def __delete__(self, instance):
+               if self.field.key in instance._added_relationship_registry:
+                       del instance._added_relationship_registry[self.field.key]
+               instance._removed_relationship_registry.append(self.field.key)
+
+
+class RelationshipField(EntityProxyField):
+       descriptor_class = RelationshipFieldDescriptor
+       
+       def __init__(self, key, model, limit_choices_to=None):
+               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):
+               field = form_class(self.model._default_manager.complex_filter(self.limit_choices_to), **kwargs)
+               field.required = False
+               return field
+       
+       def value_from_object(self, obj):
+               relobj = super(RelationshipField, self).value_from_object(obj)
+               return getattr(relobj, 'pk', None)
\ No newline at end of file
diff --git a/signals.py b/signals.py
new file mode 100644 (file)
index 0000000..e76f319
--- /dev/null
@@ -0,0 +1,3 @@
+from django.dispatch import Signal
+
+entity_class_prepared = Signal(providing_args=['class'])
\ No newline at end of file