Merge branch '404_relationship_bugfix' of git://github.com/melinath/philo
[philo.git] / models / base.py
index 5ca9d93..05074c3 100644 (file)
+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, ContentTypeSubclassLimiter
+from philo.signals import entity_class_prepared
+from philo.validators import json_validator
 from UserDict import DictMixin
 
 
 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
+
+
+value_content_type_limiter = ContentTypeRegistryLimiter()
+
+
 def register_value_model(model):
 def register_value_model(model):
-       pass
+       value_content_type_limiter.register_class(model)
 
 
 def unregister_value_model(model):
 
 
 def unregister_value_model(model):
-       pass
+       value_content_type_limiter.unregister_class(model)
 
 
 
 
-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.')
+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(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 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(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):
        
        def get_value(self):
-               return json.loads(self.json_value)
+               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):
        
        def set_value(self, value):
-               self.json_value = json.dumps(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` for value in value.values_list('id', flat=True)])
        
        
-       def delete_value(self):
-               self.json_value = json.dumps(None)
+       value = property(get_value, set_value)
        
        
-       value = property(get_value, set_value, delete_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 __unicode__(self):
-               return u'"%s": %s' % (self.key, self.value)
+       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'
 
 
        
        class Meta:
                app_label = 'philo'
 
 
-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', verbose_name='Value type')
-       value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
+       
+       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 = generic.GenericForeignKey('value_content_type', 'value_object_id')
        
+       key = models.CharField(max_length=255)
+       
        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):
        def __init__(self, queryset, passthrough=None):
                self.queryset = queryset
                self.passthrough = passthrough
 
 
 class QuerySetMapper(object, DictMixin):
        def __init__(self, queryset, passthrough=None):
                self.queryset = queryset
                self.passthrough = passthrough
+       
        def __getitem__(self, key):
                try:
        def __getitem__(self, key):
                try:
-                       return self.queryset.get(key__exact=key).value
+                       value = self.queryset.get(key__exact=key).value
                except ObjectDoesNotExist:
                except ObjectDoesNotExist:
-                       if self.passthrough:
+                       if self.passthrough is not None:
                                return self.passthrough.__getitem__(key)
                        raise KeyError
                                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())
        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)
 
 
                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')
        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
        
        @property
        def attributes(self):
                return QuerySetMapper(self.attribute_set)
        
        @property
-       def relationships(self):
-               return QuerySetMapper(self.relationship_set)
+       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
+       
+       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 field, value in self._added_attribute_registry.items():
+                       try:
+                               attribute = self.attribute_set.get(key__exact=field.key)
+                       except Attribute.DoesNotExist:
+                               attribute = Attribute()
+                               attribute.entity = self
+                               attribute.key = field.key
+                       
+                       field.set_attribute_value(attribute, value)
+                       attribute.save()
+               self._added_attribute_registry.clear()
        
        class Meta:
                abstract = True
        
        class Meta:
                abstract = True
-               app_label = 'philo'
 
 
 class TreeManager(models.Manager):
 
 
 class TreeManager(models.Manager):
@@ -129,79 +296,49 @@ 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 get_path(self, pathsep='/', field='slug'):
-               path = getattr(self, field, '?')
-               parent = self.parent
+       def has_ancestor(self, ancestor):
+               parent = self
                while parent:
                while parent:
-                       path = getattr(parent, field, '?') + pathsep + path
+                       if parent == ancestor:
+                               return True
                        parent = parent.parent
                        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:
        path = property(get_path)
        
        def __unicode__(self):
                return self.path
        
        class Meta:
+               unique_together = (('parent', 'slug'),)
                abstract = True
                abstract = True
-               app_label = 'philo'
 
 
 
 
-class TreeEntity(TreeModel, Entity):
+class TreeEntity(Entity, TreeModel):
        @property
        def attributes(self):
                if self.parent:
                        return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
                return super(TreeEntity, self).attributes
        
        @property
        def attributes(self):
                if self.parent:
                        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
-               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:
        class Meta:
-               abstract = True
-               app_label = 'philo'
\ No newline at end of file
+               abstract = True
\ No newline at end of file