1 from UserDict import DictMixin
3 from django.db import models
4 from django.contrib.contenttypes.models import ContentType
10 class AttributeMapper(object, DictMixin):
12 Given an :class:`~philo.models.base.Entity` subclass instance, this class allows dictionary-style access to the :class:`~philo.models.base.Entity`'s :class:`~philo.models.base.Attribute`\ s. In order to prevent unnecessary queries, the :class:`AttributeMapper` will cache all :class:`~philo.models.base.Attribute`\ s and the associated python values when it is first accessed.
14 :param entity: The :class:`~philo.models.base.Entity` subclass instance whose :class:`~philo.models.base.Attribute`\ s will be made accessible.
17 def __init__(self, entity):
21 def __getitem__(self, key):
22 """Returns the ultimate python value of the :class:`~philo.models.base.Attribute` with the given ``key`` from the cache, populating the cache if necessary."""
23 if not self._cache_filled:
25 return self._cache[key]
27 def __setitem__(self, key, value):
28 """Given a python value, sets the value of the :class:`~philo.models.base.Attribute` with the given ``key`` to that value."""
29 # Prevent circular import.
30 from philo.models.base import JSONValue, ForeignKeyValue, ManyToManyValue, Attribute
31 old_attr = self.get_attribute(key)
32 if old_attr and old_attr.entity_content_type == ContentType.objects.get_for_model(self.entity) and old_attr.entity_object_id == self.entity.pk:
35 attribute = Attribute(key=key)
36 attribute.entity = self.entity
37 attribute.full_clean()
39 if isinstance(value, models.query.QuerySet):
40 value_class = ManyToManyValue
41 elif isinstance(value, models.Model):
42 value_class = ForeignKeyValue
44 value_class = JSONValue
46 attribute.set_value(value=value, value_class=value_class)
47 self._cache[key] = attribute.value.value
48 self._attributes_cache[key] = attribute
50 def get_attributes(self):
51 """Returns an iterable of all of the :class:`~philo.models.base.Entity`'s :class:`~philo.models.base.Attribute`\ s."""
52 return self.entity.attribute_set.all()
54 def get_attribute(self, key, default=None):
55 """Returns the :class:`~philo.models.base.Attribute` instance with the given ``key`` from the cache, populating the cache if necessary, or ``default`` if no such attribute is found."""
56 if not self._cache_filled:
58 return self._attributes_cache.get(key, default)
61 """Returns the keys from the cache, first populating the cache if necessary."""
62 if not self._cache_filled:
64 return self._cache.keys()
67 """Returns the items from the cache, first populating the cache if necessary."""
68 if not self._cache_filled:
70 return self._cache.items()
73 """Returns the values from the cache, first populating the cache if necessary."""
74 if not self._cache_filled:
76 return self._cache.values()
78 def _fill_cache(self):
79 if self._cache_filled:
82 attributes = self.get_attributes()
86 value_lookups.setdefault(a.value_content_type_id, []).append(a.value_object_id)
87 self._attributes_cache[a.key] = a
91 for ct_pk, pks in value_lookups.items():
92 values_bulk[ct_pk] = ContentType.objects.get_for_id(ct_pk).model_class().objects.in_bulk(pks)
94 self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type_id].get(a.value_object_id), 'value', None)) for a in attributes]))
95 self._cache_filled = True
97 def clear_cache(self):
98 """Clears the cache."""
100 self._attributes_cache = {}
101 self._cache_filled = False
104 class LazyAttributeMapperMixin(object):
105 """In some cases, it may be that only one attribute value needs to be fetched. In this case, it is more efficient to avoid populating the cache whenever possible. This mixin overrides the :meth:`__getitem__` and :meth:`get_attribute` methods to prevent their populating the cache. If the cache has been populated (i.e. through :meth:`keys`, :meth:`values`, etc.), then the value or attribute will simply be returned from the cache."""
106 def __getitem__(self, key):
107 if key not in self._cache and not self._cache_filled:
108 self._add_to_cache(key)
109 return self._cache[key]
111 def get_attribute(self, key, default=None):
112 if key not in self._attributes_cache and not self._cache_filled:
113 self._add_to_cache(key)
114 return self._attributes_cache.get(key, default)
116 def _raw_get_attribute(self, key):
117 return self.get_attributes().get(key=key)
119 def _add_to_cache(self, key):
120 from philo.models.base import Attribute
122 attr = self._raw_get_attribute(key)
123 except Attribute.DoesNotExist:
126 val = getattr(attr.value, 'value', None)
127 self._cache[key] = val
128 self._attributes_cache[key] = attr
131 class LazyAttributeMapper(LazyAttributeMapperMixin, AttributeMapper):
132 def get_attributes(self):
133 return super(LazyAttributeMapper, self).get_attributes().exclude(key__in=self._cache.keys())
136 class TreeAttributeMapper(AttributeMapper):
137 """The :class:`~philo.models.base.TreeEntity` class allows the inheritance of :class:`~philo.models.base.Attribute`\ s down the tree. This mapper will return the most recently declared :class:`~philo.models.base.Attribute` among the :class:`~philo.models.base.TreeEntity`'s ancestors or set an attribute on the :class:`~philo.models.base.Entity` it is attached to."""
138 def get_attributes(self):
139 """Returns a list of :class:`~philo.models.base.Attribute`\ s sorted by increasing parent level. When used to populate the cache, this will cause :class:`~philo.models.base.Attribute`\ s on the root to be overwritten by those on its children, etc."""
140 from philo.models import Attribute
141 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
142 ct = ContentType.objects.get_for_model(self.entity)
143 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys())
144 return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
147 class LazyTreeAttributeMapper(LazyAttributeMapperMixin, TreeAttributeMapper):
148 def get_attributes(self):
149 from philo.models import Attribute
150 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
151 ct = ContentType.objects.get_for_model(self.entity)
152 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys()).exclude(key__in=self._cache.keys())
153 return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
155 def _raw_get_attribute(self, key):
156 from philo.models import Attribute
157 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
158 ct = ContentType.objects.get_for_model(self.entity)
160 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys(), key=key)
161 sorted_attrs = sorted(attrs, key=lambda x: ancestors[x.entity_object_id], reverse=True)
162 return sorted_attrs[0]
164 raise Attribute.DoesNotExist
167 class PassthroughAttributeMapper(AttributeMapper):
169 Given an iterable of :class:`Entities <philo.models.base.Entity>`, this mapper will fetch an :class:`AttributeMapper` for each one. Lookups will return the value from the first :class:`AttributeMapper` which has an entry for a given key. Assignments will be made to the first :class:`.Entity` in the iterable.
171 :param entities: An iterable of :class:`.Entity` subclass instances.
174 def __init__(self, entities):
175 self._attributes = [e.attributes for e in entities]
176 super(PassthroughAttributeMapper, self).__init__(self._attributes[0].entity)
178 def _fill_cache(self):
179 if self._cache_filled:
182 for a in reversed(self._attributes):
184 self._attributes_cache.update(a._attributes_cache)
185 self._cache.update(a._cache)
187 self._cache_filled = True
189 def get_attributes(self):
190 raise NotImplementedError
192 def clear_cache(self):
193 super(PassthroughAttributeMapper, self).clear_cache()
194 for a in self._attributes:
198 class LazyPassthroughAttributeMapper(LazyAttributeMapperMixin, PassthroughAttributeMapper):
199 """The :class:`LazyPassthroughAttributeMapper` is lazy in that it tries to avoid accessing the :class:`AttributeMapper`\ s that it uses for lookups. However, those :class:`AttributeMapper`\ s may or may not be lazy themselves."""
200 def _raw_get_attribute(self, key):
201 from philo.models import Attribute
202 for a in self._attributes:
203 attr = a.get_attribute(key)
206 raise Attribute.DoesNotExist