From: Stephen Burrows Date: Wed, 11 May 2011 21:23:41 +0000 (-0400) Subject: Merge branch 'attribute_access' into release X-Git-Tag: philo-0.9~12^2~18 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/7f6fa6595b4c558d7a97ed00cdb19469db4919df?hp=-c Merge branch 'attribute_access' into release Conflicts: philo/models/nodes.py --- 7f6fa6595b4c558d7a97ed00cdb19469db4919df diff --combined philo/models/base.py index 1726d19,0ab9b70..7d78383 --- a/philo/models/base.py +++ b/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:: @@@ -332,8 -320,8 +320,8 @@@ 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. @@@ -355,8 -343,7 +343,8 @@@ :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:: @@@ -517,10 -503,13 +504,13 @@@ 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 index a9b77fb,44a4d31..a225416 --- a/philo/models/nodes.py +++ b/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: @@@ -172,10 -176,10 +172,10 @@@ 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.")