Merge branch '404_relationship_bugfix' of git://github.com/melinath/philo
[philo.git] / models / base.py
index 145c1ee..05074c3 100644 (file)
@@ -1,3 +1,4 @@
+from django import forms
 from django.db import models
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
@@ -5,7 +6,7 @@ from django.utils import simplejson as json
 from django.core.exceptions import ObjectDoesNotExist
 from philo.exceptions import AncestorDoesNotExist
 from philo.models.fields import JSONField
-from philo.utils import ContentTypeRegistryLimiter
+from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
 from philo.signals import entity_class_prepared
 from philo.validators import json_validator
 from UserDict import DictMixin
@@ -44,56 +45,110 @@ def unregister_value_model(model):
        value_content_type_limiter.unregister_class(model)
 
 
-class JSONValue(models.Model):
+class AttributeValue(models.Model):
+       attribute = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
+       def apply_data(self, data):
+               raise NotImplementedError
+       
+       def value_formfield(self, **kwargs):
+               raise NotImplementedError
+       
+       def __unicode__(self):
+               return unicode(self.value)
+       
+       class Meta:
+               abstract = True
+
+
+attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
+
+
+class JSONValue(AttributeValue):
        value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
        
        def __unicode__(self):
                return self.value_json
        
+       def value_formfield(self, **kwargs):
+               kwargs['initial'] = self.value_json
+               return self._meta.get_field('value').formfield(**kwargs)
+       
+       def apply_data(self, cleaned_data):
+               self.value = cleaned_data.get('value', None)
+       
        class Meta:
                app_label = 'philo'
 
 
-class ForeignKeyValue(models.Model):
+class ForeignKeyValue(AttributeValue):
        content_type = models.ForeignKey(ContentType, related_name='foreign_key_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
        object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
        value = generic.GenericForeignKey()
        
-       def __unicode__(self):
-               return unicode(self.value)
+       def value_formfield(self, form_class=forms.ModelChoiceField, **kwargs):
+               if self.content_type is None:
+                       return None
+               kwargs.update({'initial': self.object_id, 'required': False})
+               return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
+       
+       def apply_data(self, cleaned_data):
+               if 'value' in cleaned_data and cleaned_data['value'] is not None:
+                       self.value = cleaned_data['value']
+               else:
+                       self.content_type = cleaned_data.get('content_type', None)
+                       # If there is no value set in the cleaned data, clear the stored value.
+                       self.object_id = None
        
        class Meta:
                app_label = 'philo'
 
 
-class ManyToManyValue(models.Model):
+class ManyToManyValue(AttributeValue):
+       # TODO: Change object_ids to object_pks.
        content_type = models.ForeignKey(ContentType, related_name='many_to_many_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
        object_ids = models.CommaSeparatedIntegerField(max_length=300, verbose_name='Value IDs', null=True, blank=True)
        
+       def get_object_id_list(self):
+               if not self.object_ids:
+                       return []
+               else:
+                       return self.object_ids.split(',')
+       
        def get_value(self):
-               return self.content_type.model_class()._default_manager.filter(id__in=self.object_ids)
+               if self.content_type is None:
+                       return None
+               
+               return self.content_type.model_class()._default_manager.filter(id__in=self.get_object_id_list())
        
        def set_value(self, value):
+               if value is None:
+                       self.object_ids = ""
+                       return
                if not isinstance(value, models.query.QuerySet):
                        raise TypeError("Value must be a QuerySet.")
                self.content_type = ContentType.objects.get_for_model(value.model)
-               self.object_ids = ','.join(value.values_list('id', flat=True))
+               self.object_ids = ','.join([`value` for value in value.values_list('id', flat=True)])
        
        value = property(get_value, set_value)
        
-       def __unicode__(self):
-               return unicode(self.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']
+               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_ids = ""
        
        class Meta:
                app_label = 'philo'
 
 
-attribute_value_limiter = ContentTypeRegistryLimiter()
-attribute_value_limiter.register_class(JSONValue)
-attribute_value_limiter.register_class(ForeignKeyValue)
-attribute_value_limiter.register_class(ManyToManyValue)
-
-
 class Attribute(models.Model):
        entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
        entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
@@ -105,28 +160,6 @@ class Attribute(models.Model):
        
        key = models.CharField(max_length=255)
        
-       def get_value_class(self, value):
-               if isinstance(value, models.query.QuerySet):
-                       return ManyToManyValue
-               elif isinstance(value, models.Model):
-                       return ForeignKeyValue
-               else:
-                       return JSONValue
-       
-       def set_value(self, value):
-               value_class = self.get_value_class(value)
-               
-               if self.value is None or value_class != self.value_content_type.model_class():
-                       if self.value is not None:
-                               self.value.delete()
-                       new_value = value_class()
-                       new_value.value = value
-                       new_value.save()
-                       self.value = new_value
-               else:
-                       self.value.value = value
-                       self.value.save()
-       
        def __unicode__(self):
                return u'"%s": %s' % (self.key, self.value)
        
@@ -142,11 +175,15 @@ class QuerySetMapper(object, DictMixin):
        
        def __getitem__(self, key):
                try:
-                       return self.queryset.get(key__exact=key).value
+                       value = self.queryset.get(key__exact=key).value
                except ObjectDoesNotExist:
                        if self.passthrough is not None:
                                return self.passthrough.__getitem__(key)
                        raise KeyError
+               else:
+                       if value is not None:
+                               return value.value
+                       return value
        
        def keys(self):
                keys = set(self.queryset.values_list('key', flat=True).distinct())
@@ -204,14 +241,15 @@ class Entity(models.Model):
                        self.attribute_set.filter(key__exact=key).delete()
                del self._removed_attribute_registry[:]
                
-               for key, value in self._added_attribute_registry.items():
+               for field, value in self._added_attribute_registry.items():
                        try:
-                               attribute = self.attribute_set.get(key__exact=key)
+                               attribute = self.attribute_set.get(key__exact=field.key)
                        except Attribute.DoesNotExist:
                                attribute = Attribute()
                                attribute.entity = self
-                               attribute.key = key
-                       attribute.set_value(value)
+                               attribute.key = field.key
+                       
+                       field.set_attribute_value(attribute, value)
                        attribute.save()
                self._added_attribute_registry.clear()
        
@@ -302,11 +340,5 @@ class TreeEntity(Entity, TreeModel):
                        return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
                return super(TreeEntity, self).attributes
        
-       @property
-       def relationships(self):
-               if self.parent:
-                       return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
-               return super(TreeEntity, self).relationships
-       
        class Meta:
                abstract = True
\ No newline at end of file