Added AttributeForm to elegantly handle all the fields that should be displayed in...
[philo.git] / models / base.py
index 81e557f..cd25357 100644 (file)
@@ -1,16 +1,20 @@
+from django import forms
 from django.db import models
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
 from django.utils import simplejson as json
 from django.core.exceptions import ObjectDoesNotExist
 from django.db import models
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
 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.signals import entity_class_prepared
 from philo.utils import ContentTypeRegistryLimiter
 from philo.signals import entity_class_prepared
+from philo.validators import json_validator
 from UserDict import DictMixin
 
 
 class Tag(models.Model):
 from UserDict import DictMixin
 
 
 class Tag(models.Model):
-       name = models.CharField(max_length=250)
-       slug = models.SlugField(unique=True)
+       name = models.CharField(max_length=255)
+       slug = models.SlugField(max_length=255, unique=True)
        
        def __unicode__(self):
                return self.name
        
        def __unicode__(self):
                return self.name
@@ -30,57 +34,116 @@ class Titled(models.Model):
                abstract = True
 
 
                abstract = True
 
 
-class Attribute(models.Model):
-       entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
-       entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
-       entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
-       key = models.CharField(max_length=255)
-       json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
-       
-       def get_value(self):
-               return json.loads(self.json_value)
+value_content_type_limiter = ContentTypeRegistryLimiter()
+
+
+def register_value_model(model):
+       value_content_type_limiter.register_class(model)
+
+
+def unregister_value_model(model):
+       value_content_type_limiter.unregister_class(model)
+
+
+class JSONValue(models.Model):
+       value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
        
        
-       def set_value(self, value):
-               self.json_value = json.dumps(value)
+       def __unicode__(self):
+               return self.value_json
        
        
-       def delete_value(self):
-               self.json_value = json.dumps(None)
+       def value_formfield(self, *args, **kwargs):
+               kwargs['initial'] = self.value_json
+               return self._meta.get_field('value').formfield(*args, **kwargs)
        
        
-       value = property(get_value, set_value, delete_value)
+       class Meta:
+               app_label = 'philo'
+
+
+class ForeignKeyValue(models.Model):
+       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):
        
        def __unicode__(self):
-               return u'"%s": %s' % (self.key, self.value)
+               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)
        
        class Meta:
                app_label = 'philo'
 
 
        
        class Meta:
                app_label = 'philo'
 
 
-value_content_type_limiter = ContentTypeRegistryLimiter()
-
-
-def register_value_model(model):
-       value_content_type_limiter.register_class(model)
-
+class ManyToManyValue(models.Model):
+       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_value(self):
+               return self.content_type.model_class()._default_manager.filter(id__in=self.object_ids)
+       
+       def set_value(self, value):
+               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))
+       
+       value = property(get_value, set_value)
+       
+       def __unicode__(self):
+               return unicode(self.value)
+       
+       class Meta:
+               app_label = 'philo'
 
 
-def unregister_value_model(model):
-       value_content_type_limiter.unregister_class(model)
 
 
+attribute_value_limiter = ContentTypeRegistryLimiter()
+attribute_value_limiter.register_class(JSONValue)
+attribute_value_limiter.register_class(ForeignKeyValue)
+attribute_value_limiter.register_class(ManyToManyValue)
 
 
 
 
-class Relationship(models.Model):
-       entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
+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')
        entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
        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', null=True, blank=True)
+       
+       value_content_type = models.ForeignKey(ContentType, related_name='attribute_value_set', limit_choices_to=attribute_value_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')
        
        value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
        value = generic.GenericForeignKey('value_content_type', 'value_object_id')
        
+       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) or (value is None and self.value_content_type.model_class() is ForeignKeyValue):
+                       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)
        
        class Meta:
                app_label = 'philo'
        def __unicode__(self):
                return u'"%s": %s' % (self.key, self.value)
        
        class Meta:
                app_label = 'philo'
+               unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
 
 
 class QuerySetMapper(object, DictMixin):
 
 
 class QuerySetMapper(object, DictMixin):
@@ -128,16 +191,11 @@ class Entity(models.Model):
        __metaclass__ = EntityBase
        
        attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
        __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')
        
        @property
        def attributes(self):
                return QuerySetMapper(self.attribute_set)
        
        
        @property
        def attributes(self):
                return QuerySetMapper(self.attribute_set)
        
-       @property
-       def relationships(self):
-               return QuerySetMapper(self.relationship_set)
-       
        @property
        def _added_attribute_registry(self):
                if not hasattr(self, '_real_added_attribute_registry'):
        @property
        def _added_attribute_registry(self):
                if not hasattr(self, '_real_added_attribute_registry'):
@@ -150,18 +208,6 @@ class Entity(models.Model):
                        self._real_removed_attribute_registry = []
                return 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)
                
        def save(self, *args, **kwargs):
                super(Entity, self).save(*args, **kwargs)
                
@@ -176,24 +222,9 @@ class Entity(models.Model):
                                attribute = Attribute()
                                attribute.entity = self
                                attribute.key = key
                                attribute = Attribute()
                                attribute.entity = self
                                attribute.key = key
-                       attribute.value = value
+                       attribute.set_value(value)
                        attribute.save()
                self._added_attribute_registry.clear()
                        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
        
        class Meta:
                abstract = True
@@ -238,7 +269,7 @@ class TreeManager(models.Manager):
 class TreeModel(models.Model):
        objects = TreeManager()
        parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
 class TreeModel(models.Model):
        objects = TreeManager()
        parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
-       slug = models.SlugField()
+       slug = models.SlugField(max_length=255)
        
        def has_ancestor(self, ancestor):
                parent = self
        
        def has_ancestor(self, ancestor):
                parent = self
@@ -249,7 +280,9 @@ class TreeModel(models.Model):
                return False
        
        def get_path(self, root=None, pathsep='/', field='slug'):
                return False
        
        def get_path(self, root=None, pathsep='/', field='slug'):
-               if root is not None and self.has_ancestor(root):
+               if root is not None:
+                       if not self.has_ancestor(root):
+                               raise AncestorDoesNotExist(root)
                        path = ''
                        parent = self
                        while parent and parent != root:
                        path = ''
                        parent = self
                        while parent and parent != root:
@@ -273,7 +306,7 @@ class TreeModel(models.Model):
                abstract = True
 
 
                abstract = True
 
 
-class TreeEntity(TreeModel, Entity):
+class TreeEntity(Entity, TreeModel):
        @property
        def attributes(self):
                if self.parent:
        @property
        def attributes(self):
                if self.parent: