Added unittest for node_url templatetag. Fixed issues uncovered with unittests. Added...
[philo.git] / models / base.py
index f062e2c..ceeb81e 100644 (file)
@@ -3,16 +3,41 @@ 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.utils import ContentTypeRegistryLimiter
+from philo.signals import entity_class_prepared
+from philo.validators import json_validator
 from UserDict import DictMixin
 
 
+class Tag(models.Model):
+       name = models.CharField(max_length=255)
+       slug = models.SlugField(max_length=255, unique=True)
+       
+       def __unicode__(self):
+               return self.name
+       
+       class Meta:
+               app_label = 'philo'
+
+
+class Titled(models.Model):
+       title = models.CharField(max_length=255)
+       slug = models.SlugField(max_length=255)
+       
+       def __unicode__(self):
+               return self.title
+       
+       class Meta:
+               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.')
+       json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', validators=[json_validator])
        
        def get_value(self):
                return json.loads(self.json_value)
@@ -30,6 +55,7 @@ class Attribute(models.Model):
        
        class Meta:
                app_label = 'philo'
+               unique_together = ('key', 'entity_content_type', 'entity_object_id')
 
 
 value_content_type_limiter = ContentTypeRegistryLimiter()
@@ -49,8 +75,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):
@@ -58,6 +84,7 @@ class Relationship(models.Model):
        
        class Meta:
                app_label = 'philo'
+               unique_together = ('key', 'entity_content_type', 'entity_object_id')
 
 
 class QuerySetMapper(object, DictMixin):
@@ -80,7 +107,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')
        
@@ -92,9 +142,65 @@ 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
-               app_label = 'philo'
 
 
 class TreeManager(models.Manager):
@@ -136,15 +242,33 @@ class TreeManager(models.Manager):
 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 get_path(self, pathsep='/', field='slug'):
-               path = getattr(self, field, '?')
-               parent = self.parent
+       def has_ancestor(self, ancestor):
+               parent = self
                while parent:
-                       path = getattr(parent, field, '?') + pathsep + path
+                       if parent == ancestor:
+                               return True
                        parent = parent.parent
-               return path
+               return False
+       
+       def get_path(self, root=None, pathsep='/', field='slug'):
+               if root is not None:
+                       if not self.has_ancestor(root):
+                               raise AncestorDoesNotExist(root)
+                       path = ''
+                       parent = self
+                       while parent and parent != root:
+                               path = getattr(parent, field, '?') + pathsep + path
+                               parent = parent.parent
+                       return path
+               else:
+                       path = getattr(self, field, '?')
+                       parent = self.parent
+                       while parent and parent != root:
+                               path = getattr(parent, field, '?') + pathsep + path
+                               parent = parent.parent
+                       return path
        path = property(get_path)
        
        def __unicode__(self):
@@ -153,10 +277,9 @@ class TreeModel(models.Model):
        class Meta:
                unique_together = (('parent', 'slug'),)
                abstract = True
-               app_label = 'philo'
 
 
-class TreeEntity(TreeModel, Entity):
+class TreeEntity(Entity, TreeModel):
        @property
        def attributes(self):
                if self.parent:
@@ -170,5 +293,4 @@ class TreeEntity(TreeModel, Entity):
                return super(TreeEntity, self).relationships
        
        class Meta:
-               abstract = True
-               app_label = 'philo'
\ No newline at end of file
+               abstract = True
\ No newline at end of file