Merge branch 'attribute_access' into release
authorStephen Burrows <stephen.r.burrows@gmail.com>
Wed, 11 May 2011 21:23:41 +0000 (17:23 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Wed, 11 May 2011 21:23:41 +0000 (17:23 -0400)
Conflicts:
philo/models/nodes.py

1  2 
philo/models/base.py
philo/models/nodes.py

diff --combined philo/models/base.py
@@@ -1,5 -1,3 +1,3 @@@
- from UserDict import DictMixin
  from django import forms
  from django.contrib.contenttypes.models import ContentType
  from django.contrib.contenttypes import generic
@@@ -13,7 -11,7 +11,7 @@@ from mptt.models import MPTTModel, MPTT
  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.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter, AttributeMapper, TreeAttributeMapper
  from philo.validators import json_validator
  
  
@@@ -262,35 -260,26 +260,26 @@@ class Attribute(models.Model)
        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 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:
@@@ -318,10 -307,9 +307,9 @@@ class Entity(models.Model)
        
        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 a dictionary-like object which can be used to retrieve related :class:`Attribute`\ s' values directly.
  
                Example::
  
                        u'eggs'
                
                """
-               
-               return QuerySetMapper(self.attribute_set.all())
+               return mapper(self)
+       attributes = property(get_attribute_mapper)
        
        class Meta:
                abstract = True
@@@ -344,7 -332,7 +332,7 @@@ class TreeManager(models.Manager)
        
        def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
                """
 -              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.
                
                :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.
                
                """
                
@@@ -501,10 -488,9 +489,9 @@@ class TreeEntity(Entity, TreeModel)
        
        __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 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.
  
                Example::
  
                        u'eggs'
                
                """
-               
-               if self.parent:
-                       return QuerySetMapper(self.attribute_set.all(), passthrough=self.parent.attributes)
-               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)
        
        class Meta:
                abstract = True
diff --combined philo/models/nodes.py
@@@ -12,9 -12,10 +12,9 @@@ from django.template import add_to_buil
  from django.utils.encoding import smart_str
  
  from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED, ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths
- from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
+ from philo.models.base import TreeEntity, Entity, register_value_model
  from philo.models.fields import JSONField
- from philo.utils import ContentTypeSubclassLimiter
+ from philo.utils import ContentTypeSubclassLimiter, LazyPassthroughAttributeMapper
 -from philo.validators import RedirectValidator
  from philo.signals import view_about_to_render, view_finished_rendering
  
  
@@@ -132,14 -133,17 +132,14 @@@ class View(Entity)
                
                If ``obj`` is provided, :meth:`get_reverse_params` will be called and the results will be combined with any ``view_name``, ``args``, and ``kwargs`` that may have been passed in.
                
 -              This method will raise the following exceptions:
 -              
 -              - :class:`~philo.exceptions.ViewDoesNotProvideSubpaths` if :attr:`accepts_subpath` is False.
 -              - :class:`~philo.exceptions.ViewCanNotProvideSubpath` if a reversal is not possible.
 -              
                :param view_name: The name of the view to be reversed.
                :param args: Extra args for reversing the view.
                :param kwargs: A dictionary of arguments for reversing the view.
                :param node: The node whose subpath this is.
                :param obj: An object to be passed to :meth:`get_reverse_params` to generate a view_name, args, and kwargs for reversal.
                :returns: A subpath beyond the node that reverses the view, or an absolute url that reverses the view if a node was passed in.
 +              :except philo.exceptions.ViewDoesNotProvideSubpaths: if :attr:`accepts_subpath` is False
 +              :except philo.exceptions.ViewCanNotProvideSubpath: if a reversal is not possible.
                
                """
                if not self.accepts_subpath:
        
        def attributes_with_node(self, node):
                """
-               Returns a :class:`~philo.models.base.QuerySetMapper` using the :class:`Node`'s attributes as a passthrough.
+               Returns a dictionary-like object which can be used to directly retrieve the values of :class:`Attribute`\ s related to the :class:`View`, falling back on similar object which retrieves the values of the passed-in node and its ancestors.
                
                """
-               return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
+               return LazyPassthroughAttributeMapper((self, node))
        
        def render_to_response(self, request, extra_context=None):
                """
@@@ -290,8 -294,8 +290,8 @@@ class TargetURLModel(models.Model)
        """An abstract parent class for models which deal in targeting a url."""
        #: An optional :class:`ForeignKey` to a :class:`Node`. If provided, that node will be used as the basis for the redirect.
        target_node = models.ForeignKey(Node, blank=True, null=True, related_name="%(app_label)s_%(class)s_related")
 -      #: A :class:`CharField` which may contain an absolute or relative URL. This will be validated with :class:`philo.validators.RedirectValidator`.
 -      url_or_subpath = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.")
 +      #: A :class:`CharField` which may contain an absolute or relative URL, or the name of a node's subpath.
 +      url_or_subpath = models.CharField(max_length=200, blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.")
        #: A :class:`~philo.models.fields.JSONField` instance. If the value of :attr:`reversing_parameters` is not None, the :attr:`url_or_subpath` will be treated as the name of a view to be reversed. The value of :attr:`reversing_parameters` will be passed into the reversal as args if it is a list or as kwargs if it is a dictionary. Otherwise it will be ignored.
        reversing_parameters = JSONField(blank=True, help_text="If reversing parameters are defined, url_or_subpath will instead be interpreted as the view name to be reversed.")