Generalized the 'single step' case in TreeEntity.get_path to include the case where...
[philo.git] / philo / models / base.py
index 46fa8d5..8df67c3 100644 (file)
@@ -1,7 +1,7 @@
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import ValidationError
 from django.core.validators import RegexValidator
 from django.db import models
 from django.utils import simplejson as json
@@ -16,7 +16,7 @@ from philo.utils.entities import AttributeMapper, TreeAttributeMapper
 from philo.validators import json_validator
 
 
-__all__ = ('Tag', 'value_content_type_limiter', 'register_value_model', 'unregister_value_model', 'JSONValue', 'ForeignKeyValue', 'ManyToManyValue', 'Attribute', 'Entity', 'TreeEntity')
+__all__ = ('Tag', 'value_content_type_limiter', 'register_value_model', 'unregister_value_model', 'JSONValue', 'ForeignKeyValue', 'ManyToManyValue', 'Attribute', 'Entity', 'TreeEntity', 'SlugTreeEntity')
 
 
 class Tag(models.Model):
@@ -35,17 +35,6 @@ class Tag(models.Model):
                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.
 value_content_type_limiter = ContentTypeRegistryLimiter()
 
@@ -99,7 +88,7 @@ class AttributeValue(models.Model):
                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)
 
 
@@ -245,7 +234,12 @@ class ManyToManyValue(AttributeValue):
 
 
 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)
        
@@ -325,16 +319,29 @@ class Entity(models.Model):
                
                """
                return mapper(self)
-       attributes = property(get_attribute_mapper)
+       
+       @property
+       def attributes(self):
+               if not hasattr(self, '_attributes'):
+                       self._attributes = self.get_attribute_mapper()
+               return self._attributes
        
        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
        
-       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 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).
                
@@ -444,16 +451,19 @@ class TreeManager(models.Manager):
                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)
-       slug = models.SlugField(max_length=255)
        
-       def get_path(self, root=None, pathsep='/', field='slug'):
+       def get_path(self, root=None, pathsep='/', field='pk', memoize=True):
                """
                :param root: Only return the path since this object.
                :param pathsep: The path separator to use when constructing an instance's path
                :param field: The field to pull path information from for each ancestor.
+               :param memoize: Whether to use memoized results. Since, in most cases, the ancestors of a TreeEntity will not change over the course of an instance's lifetime, this defaults to ``True``.
                :returns: A string representation of an object's path.
                
                """
@@ -461,37 +471,34 @@ class TreeModel(MPTTModel):
                if root == self:
                        return ''
                
+               parent_id = getattr(self, "%s_id" % self._mptt_meta.parent_attr)
+               if getattr(root, 'pk', None) == parent_id:
+                       return getattr(self, field, '?')
+               
                if root is not None and not self.is_descendant_of(root):
                        raise AncestorDoesNotExist(root)
                
+               if memoize:
+                       memo_args = (parent_id, getattr(root, 'pk', None), pathsep, getattr(self, field, '?'))
+                       try:
+                               return self._path_memo[memo_args]
+                       except AttributeError:
+                               self._path_memo = {}
+                       except KeyError:
+                               pass
+               
                qs = self.get_ancestors(include_self=True)
                
                if root is not None:
                        qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
                
-               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)
+               path = pathsep.join([getattr(parent, field, '?') for parent in qs])
                
-               return meta.register(cls)
-
-
-class TreeEntity(Entity, TreeModel):
-       """An abstract subclass of Entity which represents a tree relationship."""
-       
-       __metaclass__ = TreeEntityBase
+               if memoize:
+                       self._path_memo[memo_args] = path
+               
+               return path
+       path = property(get_path)
        
        def get_attribute_mapper(self, mapper=None):
                """
@@ -509,12 +516,41 @@ class TreeEntity(Entity, TreeModel):
                
                """
                if mapper is None:
-                       if self.parent:
+                       if getattr(self, "%s_id" % self._mptt_meta.parent_attr):
                                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', memoize=True):
+               return super(SlugTreeEntity, self).get_path(root, pathsep, field, memoize)
+       path = property(get_path)
+       
+       def clean(self):
+               if getattr(self, "%s_id" % self._mptt_meta.parent_attr) is None:
+                       try:
+                               self._default_manager.exclude(pk=self.pk).get(slug=self.slug, parent__isnull=True)
+                       except self.DoesNotExist:
+                               pass
+                       else:
+                               raise ValidationError(self.unique_error_message(self.__class__, ('parent', 'slug')))
        
        class Meta:
+               unique_together = ('parent', 'slug')
                abstract = True
\ No newline at end of file