Moved sobol's search registry into core as a generic registry utility. Added support...
[philo.git] / philo / models / base.py
index 5d8fdc2..0218261 100644 (file)
@@ -1,5 +1,3 @@
-from UserDict import DictMixin
-
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
@@ -14,9 +12,13 @@ from philo.exceptions import AncestorDoesNotExist
 from philo.models.fields import JSONField
 from philo.signals import entity_class_prepared
 from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
 from philo.models.fields import JSONField
 from philo.signals import entity_class_prepared
 from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
+from philo.utils.entities import AttributeMapper, TreeAttributeMapper
 from philo.validators import json_validator
 
 
 from philo.validators import json_validator
 
 
+__all__ = ('Tag', 'value_content_type_limiter', 'register_value_model', 'unregister_value_model', 'JSONValue', 'ForeignKeyValue', 'ManyToManyValue', 'Attribute', 'Entity', 'TreeEntity')
+
+
 class Tag(models.Model):
        """A simple, generic model for tagging."""
        #: A CharField (max length 255) which contains the name of the tag.
 class Tag(models.Model):
        """A simple, generic model for tagging."""
        #: A CharField (max length 255) which contains the name of the tag.
@@ -33,18 +35,7 @@ class Tag(models.Model):
                ordering = ('name',)
 
 
                ordering = ('name',)
 
 
-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
-
-
-#: An instance of :class:`ContentTypeRegistryLimiter` which is used to track the content types which can be related to by :class:`ForeignKeyValue`\ s and :class:`ManyToManyValue`\ s.
+#: An instance of :class:`.ContentTypeRegistryLimiter` which is used to track the content types which can be related to by :class:`ForeignKeyValue`\ s and :class:`ManyToManyValue`\ s.
 value_content_type_limiter = ContentTypeRegistryLimiter()
 
 
 value_content_type_limiter = ContentTypeRegistryLimiter()
 
 
@@ -97,7 +88,7 @@ class AttributeValue(models.Model):
                abstract = True
 
 
                abstract = True
 
 
-#: An instance of :class:`ContentTypeSubclassLimiter` which is used to track the content types which are considered valid value models for an :class:`Attribute`.
+#: An instance of :class:`.ContentTypeSubclassLimiter` which is used to track the content types which are considered valid value models for an :class:`Attribute`.
 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
 
 
 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
 
 
@@ -243,7 +234,12 @@ class ManyToManyValue(AttributeValue):
 
 
 class Attribute(models.Model):
 
 
 class Attribute(models.Model):
-       """Represents an arbitrary key/value pair on an arbitrary :class:`Model` where the key consists of word characters and the value is a subclass of :class:`AttributeValue`."""
+       """
+       :class:`Attribute`\ s exist primarily to let arbitrary data be attached to arbitrary model instances without altering the database schema and without guaranteeing that the data will be available on every instance of that model.
+       
+       Generally, :class:`Attribute`\ s will not be accessed as models; instead, they will be accessed through the :attr:`Entity.attributes` property, which allows direct dictionary getting and setting of the value of an :class:`Attribute` with its key.
+       
+       """
        entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
        entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
        
        entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
        entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
        
@@ -262,35 +258,26 @@ class Attribute(models.Model):
        def __unicode__(self):
                return u'"%s": %s' % (self.key, self.value)
        
        def __unicode__(self):
                return u'"%s": %s' % (self.key, self.value)
        
+       def set_value(self, value, value_class=JSONValue):
+               """Given a value and a value class, sets up self.value appropriately."""
+               if isinstance(self.value, value_class):
+                       val = self.value
+               else:
+                       if isinstance(self.value, models.Model):
+                               self.value.delete()
+                       val = value_class()
+               
+               val.set_value(value)
+               val.save()
+               
+               self.value = val
+               self.save()
+       
        class Meta:
                app_label = 'philo'
                unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
 
 
        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
-       
-       def __getitem__(self, key):
-               try:
-                       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())
-               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:
 class EntityOptions(object):
        def __init__(self, options):
                if options is not None:
@@ -312,58 +299,15 @@ class EntityBase(models.base.ModelBase):
                return new
 
 
                return new
 
 
-class EntityAttributeMapper(object, DictMixin):
-       def __init__(self, entity):
-               self.entity = entity
-       
-       def get_attributes(self):
-               return self.entity.attribute_set.all()
-       
-       def make_cache(self):
-               attributes = self.get_attributes()
-               value_lookups = {}
-               
-               for a in attributes:
-                       value_lookups.setdefault(a.value_content_type, []).append(a.value_object_id)
-               
-               values_bulk = {}
-               
-               for ct, pks in value_lookups.items():
-                       values_bulk[ct] = ct.model_class().objects.in_bulk(pks)
-               
-               self._cache = dict([(a.key, getattr(values_bulk[a.value_content_type].get(a.value_object_id), 'value', None)) for a in attributes])
-       
-       def __getitem__(self, key):
-               if not hasattr(self, '_cache'):
-                       self.make_cache()
-               return self._cache[key]
-       
-       def keys(self):
-               if not hasattr(self, '_cache'):
-                       self.make_cache()
-               return self._cache.keys()
-       
-       def items(self):
-               if not hasattr(self, '_cache'):
-                       self.make_cache()
-               return self._cache.items()
-       
-       def values(self):
-               if not hasattr(self, '_cache'):
-                       self.make_cache()
-               return self._cache.values()
-
-
 class Entity(models.Model):
        """An abstract class that simplifies access to related attributes. Most models provided by Philo subclass Entity."""
        __metaclass__ = EntityBase
        
        attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
        
 class Entity(models.Model):
        """An abstract class that simplifies access to related attributes. Most models provided by Philo subclass Entity."""
        __metaclass__ = EntityBase
        
        attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
        
-       @property
-       def attributes(self):
+       def get_attribute_mapper(self, mapper=AttributeMapper):
                """
                """
-               Property that returns a dictionary-like object which can be used to retrieve related :class:`Attribute`\ s' values directly.
+               Returns an :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly.
 
                Example::
 
 
                Example::
 
@@ -374,18 +318,27 @@ class Entity(models.Model):
                        u'eggs'
                
                """
                        u'eggs'
                
                """
-               return EntityAttributeMapper(self)
+               return mapper(self)
+       attributes = property(get_attribute_mapper)
        
        class Meta:
                abstract = True
 
 
        
        class Meta:
                abstract = True
 
 
-class TreeManager(models.Manager):
+class TreeEntityBase(MPTTModelBase, EntityBase):
+       def __new__(meta, name, bases, attrs):
+               attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
+               cls = EntityBase.__new__(meta, name, bases, attrs)
+               
+               return meta.register(cls)
+
+
+class TreeEntityManager(models.Manager):
        use_for_related_fields = True
        
        use_for_related_fields = True
        
-       def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
+       def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='pk'):
                """
                """
-               If ``absolute_result`` is ``True``, returns the object at ``path`` (starting at ``root``) or raises a :class:`DoesNotExist` exception. Otherwise, returns a tuple containing the deepest object found along ``path`` (or ``root`` if no deeper object is found) and the remainder of the path after that object as a string (or None if there is no remaining path).
+               If ``absolute_result`` is ``True``, returns the object at ``path`` (starting at ``root``) or raises an :class:`~django.core.exceptions.ObjectDoesNotExist` exception. Otherwise, returns a tuple containing the deepest object found along ``path`` (or ``root`` if no deeper object is found) and the remainder of the path after that object as a string (or None if there is no remaining path).
                
                .. note:: If you are looking for something with an exact path, it is faster to use absolute_result=True, unless the path depth is over ~40, in which case the high cost of the absolute query may make a binary search (i.e. non-absolute) faster.
                
                
                .. note:: If you are looking for something with an exact path, it is faster to use absolute_result=True, unless the path depth is over ~40, in which case the high cost of the absolute query may make a binary search (i.e. non-absolute) faster.
                
@@ -396,7 +349,8 @@ class TreeManager(models.Manager):
                :param absolute_result: Whether to return an absolute result or do a binary search
                :param pathsep: The path separator used in ``path``
                :param field: The field on the model which should be queried for ``path`` segment matching.
                :param absolute_result: Whether to return an absolute result or do a binary search
                :param pathsep: The path separator used in ``path``
                :param field: The field on the model which should be queried for ``path`` segment matching.
-               :returns: An instance if absolute_result is True or (instance, remaining_path) otherwise.
+               :returns: An instance if ``absolute_result`` is ``True`` or an (instance, remaining_path) tuple otherwise.
+               :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters.
                
                """
                
                
                """
                
@@ -492,10 +446,12 @@ class TreeManager(models.Manager):
                return find_obj(segments, len(segments)/2 or len(segments))
 
 
                return find_obj(segments, len(segments)/2 or len(segments))
 
 
-class TreeModel(MPTTModel):
-       objects = TreeManager()
+class TreeEntity(Entity, MPTTModel):
+       """An abstract subclass of Entity which represents a tree relationship."""
+       
+       __metaclass__ = TreeEntityBase
+       objects = TreeEntityManager()
        parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
        parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
-       slug = models.SlugField(max_length=255)
        
        def get_path(self, root=None, pathsep='/', field='slug'):
                """
        
        def get_path(self, root=None, pathsep='/', field='slug'):
                """
@@ -509,6 +465,9 @@ class TreeModel(MPTTModel):
                if root == self:
                        return ''
                
                if root == self:
                        return ''
                
+               if root is None and self.is_root_node():
+                       return getattr(self, field, '?')
+               
                if root is not None and not self.is_descendant_of(root):
                        raise AncestorDoesNotExist(root)
                
                if root is not None and not self.is_descendant_of(root):
                        raise AncestorDoesNotExist(root)
                
@@ -520,38 +479,9 @@ class TreeModel(MPTTModel):
                return pathsep.join([getattr(parent, field, '?') for parent in qs])
        path = property(get_path)
        
                return pathsep.join([getattr(parent, field, '?') for parent in qs])
        path = property(get_path)
        
-       def __unicode__(self):
-               return self.path
-       
-       class Meta:
-               unique_together = (('parent', 'slug'),)
-               abstract = True
-
-
-class TreeEntityBase(MPTTModelBase, EntityBase):
-       def __new__(meta, name, bases, attrs):
-               attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
-               cls = EntityBase.__new__(meta, name, bases, attrs)
-               
-               return meta.register(cls)
-
-
-class TreeEntityAttributeMapper(EntityAttributeMapper):
-       def get_attributes(self):
-               ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
-               ct = ContentType.objects.get_for_model(self.entity)
-               return sorted(Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys()), key=lambda x: ancestors[x.entity_object_id])
-
-
-class TreeEntity(Entity, TreeModel):
-       """An abstract subclass of Entity which represents a tree relationship."""
-       
-       __metaclass__ = TreeEntityBase
-       
-       @property
-       def attributes(self):
+       def get_attribute_mapper(self, mapper=None):
                """
                """
-               Property that returns a dictionary-like object which can be used to retrieve related :class:`Attribute`\ s' values directly. If an attribute with a given key is not related to the :class:`Entity`, then the object will check the parent's attributes.
+               Returns a :class:`.TreeAttributeMapper` or :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly. If an :class:`Attribute` with a given key is not related to the :class:`Entity`, then the mapper will check the parent's attributes.
 
                Example::
 
 
                Example::
 
@@ -564,10 +494,34 @@ class TreeEntity(Entity, TreeModel):
                        u'eggs'
                
                """
                        u'eggs'
                
                """
-               
-               if self.parent:
-                       return TreeEntityAttributeMapper(self)
-               return super(TreeEntity, self).attributes
+               if mapper is None:
+                       if self.parent:
+                               mapper = TreeAttributeMapper
+                       else:
+                               mapper = AttributeMapper
+               return super(TreeEntity, self).get_attribute_mapper(mapper)
+       attributes = property(get_attribute_mapper)
+       
+       def __unicode__(self):
+               return self.path
+       
+       class Meta:
+               abstract = True
+
+
+class SlugTreeEntityManager(TreeEntityManager):
+       def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
+               return super(SlugTreeEntityManager, self).get_with_path(path, root, absolute_result, pathsep, field)
+
+
+class SlugTreeEntity(TreeEntity):
+       objects = SlugTreeEntityManager()
+       slug = models.SlugField(max_length=255)
+       
+       def get_path(self, root=None, pathsep='/', field='slug'):
+               return super(SlugTreeEntity, self).get_path(root, pathsep, field)
+       path = property(get_path)
        
        class Meta:
        
        class Meta:
+               unique_together = ('parent', 'slug')
                abstract = True
\ No newline at end of file
                abstract = True
\ No newline at end of file