Added recaptcha support to waldo registration form.
[philo.git] / models / base.py
index 5ca9d93..ceeb81e 100644 (file)
@@ -3,15 +3,33 @@ 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
 
 
-def register_value_model(model):
-       pass
+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'
 
 
-def unregister_value_model(model):
-       pass
+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):
@@ -19,7 +37,7 @@ class Attribute(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)
-       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)
@@ -37,6 +55,19 @@ class Attribute(models.Model):
        
        class Meta:
                app_label = 'philo'
+               unique_together = ('key', 'entity_content_type', 'entity_object_id')
+
+
+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 Relationship(models.Model):
@@ -44,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', 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):
@@ -53,27 +84,53 @@ class Relationship(models.Model):
        
        class Meta:
                app_label = 'philo'
+               unique_together = ('key', 'entity_content_type', 'entity_object_id')
 
 
 class QuerySetMapper(object, DictMixin):
        def __init__(self, queryset, passthrough=None):
                self.queryset = queryset
                self.passthrough = passthrough
+       
        def __getitem__(self, key):
                try:
                        return self.queryset.get(key__exact=key).value
                except ObjectDoesNotExist:
-                       if self.passthrough:
+                       if self.passthrough is not None:
                                return self.passthrough.__getitem__(key)
                        raise KeyError
+       
        def keys(self):
                keys = set(self.queryset.values_list('key', flat=True).distinct())
-               if self.passthrough:
-                       keys += set(self.passthrough.keys())
+               if self.passthrough is not None:
+                       keys |= set(self.passthrough.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):
+       __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')
        
@@ -85,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):
@@ -129,26 +242,44 @@ 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):
                return self.path
        
        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:
@@ -162,46 +293,4 @@ class TreeEntity(TreeModel, Entity):
                return super(TreeEntity, self).relationships
        
        class Meta:
-               abstract = True
-               app_label = 'philo'
-
-
-class InheritableTreeEntity(TreeEntity):
-       instance_type = models.ForeignKey(ContentType, editable=False)
-       
-       def save(self, force_insert=False, force_update=False):
-               if not hasattr(self, 'instance_type_ptr'):
-                       self.instance_type = ContentType.objects.get_for_model(self.__class__)
-               super(InheritableTreeEntity, self).save(force_insert, force_update)
-       
-       @property
-       def instance(self):
-               try:
-                       return self.instance_type.get_object_for_this_type(id=self.id)
-               except:
-                       return None
-       
-       def get_path(self, pathsep='/', field='slug'):
-               path = getattr(self.instance, field, getattr(self.instance, 'slug', '?'))
-               parent = self.parent
-               while parent:
-                       path = getattr(parent.instance, field, getattr(parent.instance, 'slug', '?')) + pathsep + path
-                       parent = parent.parent
-               return path
-       path = property(get_path)
-       
-       @property
-       def attributes(self):
-               if self.parent:
-                       return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
-               return QuerySetMapper(self.instance.attribute_set)
-
-       @property
-       def relationships(self):
-               if self.parent:
-                       return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
-               return QuerySetMapper(self.instance.relationship_set)
-       
-       class Meta:
-               abstract = True
-               app_label = 'philo'
\ No newline at end of file
+               abstract = True
\ No newline at end of file