Implemented EntityForm, which knows how to deal with AttributeFields and Relationship...
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Mon, 9 Aug 2010 06:30:10 +0000 (02:30 -0400)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Mon, 9 Aug 2010 06:30:10 +0000 (02:30 -0400)
To be consistent with Attributes, Relationships now support null values. This changes the schema.

forms.py [new file with mode: 0644]
models/base.py
models/fields.py
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 37710b8..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 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
 
 
 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)
        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):
        value = generic.GenericForeignKey('value_content_type', 'value_object_id')
        
        def __unicode__(self):
@@ -102,7 +103,30 @@ class QuerySetMapper(object, DictMixin):
                return list(keys)
 
 
                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):
 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')
        
        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')
        
index 467befb..9fc2dfb 100644 (file)
@@ -1,12 +1,39 @@
 from django.db import models
 from django.db import models
-from django.db.models import signals
+from django import forms
 from django.core.exceptions import FieldError
 from philo.models.base import Entity
 from django.core.exceptions import FieldError
 from philo.models.base import Entity
+from philo.signals import entity_class_prepared
 
 
 __all__ = ('AttributeField', 'RelationshipField')
 
 
 
 
 __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
 class AttributeFieldDescriptor(object):
        def __init__(self, field):
                self.field = field
@@ -35,19 +62,19 @@ class AttributeFieldDescriptor(object):
                instance._removed_attribute_registry.append(self.field.key)
 
 
                instance._removed_attribute_registry.append(self.field.key)
 
 
-class AttributeField(object):
-       def __init__(self, key):
-               self.key = key
+class AttributeField(EntityProxyField):
+       descriptor_class = AttributeFieldDescriptor
        
        
-       def actually_contribute_to_class(self, sender, **kwargs):
-               setattr(sender, self.name, AttributeFieldDescriptor(self))
+       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 contribute_to_class(self, cls, name):
-               if issubclass(cls, Entity):
-                       self.name = name
-                       signals.class_prepared.connect(self.actually_contribute_to_class, sender=cls)
-               else:
-                       raise FieldError('AttributeFields can only be declared on Entity subclasses.')
+       def formfield(self, *args, **kwargs):
+               field = self.field_template.formfield(*args, **kwargs)
+               field.required = False
+               return field
 
 
 class RelationshipFieldDescriptor(object):
 
 
 class RelationshipFieldDescriptor(object):
@@ -81,16 +108,21 @@ class RelationshipFieldDescriptor(object):
                instance._removed_relationship_registry.append(self.field.key)
 
 
                instance._removed_relationship_registry.append(self.field.key)
 
 
-class RelationshipField(object):
-       def __init__(self, key):
+class RelationshipField(EntityProxyField):
+       descriptor_class = RelationshipFieldDescriptor
+       
+       def __init__(self, key, model, limit_choices_to=None):
                self.key = key
                self.key = key
+               self.model = model
+               if limit_choices_to is None:
+                       limit_choices_to = {}
+               self.limit_choices_to = limit_choices_to
        
        
-       def actually_contribute_to_class(self, sender, **kwargs):
-               setattr(sender, self.name, RelationshipFieldDescriptor(self))
+       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 contribute_to_class(self, cls, name):
-               if issubclass(cls, Entity):
-                       self.name = name
-                       signals.class_prepared.connect(self.actually_contribute_to_class, sender=cls)
-               else:
-                       raise FieldError('RelationshipFields can only be declared on Entity subclasses.')
\ No newline at end of file
+       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