1 from functools import partial
2 from UserDict import DictMixin
4 from django.db import models
5 from django.contrib.contenttypes.models import ContentType
7 from philo.utils.lazycompat import SimpleLazyObject
13 class AttributeMapper(object, DictMixin):
15 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.
17 :param entity: The :class:`~philo.models.base.Entity` subclass instance whose :class:`~philo.models.base.Attribute`\ s will be made accessible.
20 def __init__(self, entity):
24 def __getitem__(self, key):
25 """Returns the ultimate python value of the :class:`~philo.models.base.Attribute` with the given ``key`` from the cache, populating the cache if necessary."""
26 if not self._cache_filled:
28 return self._cache[key]
30 def __setitem__(self, key, value):
31 """Given a python value, sets the value of the :class:`~philo.models.base.Attribute` with the given ``key`` to that value."""
32 # Prevent circular import.
33 from philo.models.base import JSONValue, ForeignKeyValue, ManyToManyValue, Attribute
34 old_attr = self.get_attribute(key)
35 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:
38 attribute = Attribute(key=key)
39 attribute.entity = self.entity
40 attribute.full_clean()
42 if isinstance(value, models.query.QuerySet):
43 value_class = ManyToManyValue
44 elif isinstance(value, models.Model):
45 value_class = ForeignKeyValue
47 value_class = JSONValue
49 attribute.set_value(value=value, value_class=value_class)
50 self._cache[key] = attribute.value.value
51 self._attributes_cache[key] = attribute
53 def get_attributes(self):
54 """Returns an iterable of all of the :class:`~philo.models.base.Entity`'s :class:`~philo.models.base.Attribute`\ s."""
55 return self.entity.attribute_set.all()
57 def get_attribute(self, key, default=None):
58 """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."""
59 if not self._cache_filled:
61 return self._attributes_cache.get(key, default)
64 """Returns the keys from the cache, first populating the cache if necessary."""
65 if not self._cache_filled:
67 return self._cache.keys()
70 """Returns the items from the cache, first populating the cache if necessary."""
71 if not self._cache_filled:
73 return self._cache.items()
76 """Returns the values from the cache, first populating the cache if necessary."""
77 if not self._cache_filled:
79 return self._cache.values()
81 def _fill_cache(self):
82 if self._cache_filled:
85 attributes = self.get_attributes()
89 value_lookups.setdefault(a.value_content_type_id, []).append(a.value_object_id)
90 self._attributes_cache[a.key] = a
92 values_bulk = dict(((ct_pk, SimpleLazyObject(partial(ContentType.objects.get_for_id(ct_pk).model_class().objects.in_bulk, pks))) for ct_pk, pks in value_lookups.items()))
97 cache[a.key] = SimpleLazyObject(partial(self._lazy_value_from_bulk, values_bulk, a))
98 a._value_cache = cache[a.key]
100 self._cache.update(cache)
101 self._cache_filled = True
103 def _lazy_value_from_bulk(self, bulk, attribute):
104 v = bulk[attribute.value_content_type_id].get(attribute.value_object_id)
105 return getattr(v, 'value', None)
107 def clear_cache(self):
108 """Clears the cache."""
110 self._attributes_cache = {}
111 self._cache_filled = False
114 class LazyAttributeMapperMixin(object):
115 """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."""
116 def __getitem__(self, key):
117 if key not in self._cache and not self._cache_filled:
118 self._add_to_cache(key)
119 return self._cache[key]
121 def get_attribute(self, key, default=None):
122 if key not in self._attributes_cache and not self._cache_filled:
123 self._add_to_cache(key)
124 return self._attributes_cache.get(key, default)
126 def _raw_get_attribute(self, key):
127 return self.get_attributes().get(key=key)
129 def _add_to_cache(self, key):
130 from philo.models.base import Attribute
132 attr = self._raw_get_attribute(key)
133 except Attribute.DoesNotExist:
136 val = getattr(attr.value, 'value', None)
137 self._cache[key] = val
138 self._attributes_cache[key] = attr
141 class LazyAttributeMapper(LazyAttributeMapperMixin, AttributeMapper):
142 def get_attributes(self):
143 return super(LazyAttributeMapper, self).get_attributes().exclude(key__in=self._cache.keys())
146 class TreeAttributeMapper(AttributeMapper):
147 """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."""
148 def get_attributes(self):
149 """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."""
150 from philo.models import Attribute
151 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
152 ct = ContentType.objects.get_for_model(self.entity)
153 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys())
154 return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
157 class LazyTreeAttributeMapper(LazyAttributeMapperMixin, TreeAttributeMapper):
158 def get_attributes(self):
159 from philo.models import Attribute
160 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
161 ct = ContentType.objects.get_for_model(self.entity)
162 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys()).exclude(key__in=self._cache.keys())
163 return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
165 def _raw_get_attribute(self, key):
166 from philo.models import Attribute
167 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
168 ct = ContentType.objects.get_for_model(self.entity)
170 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys(), key=key)
171 sorted_attrs = sorted(attrs, key=lambda x: ancestors[x.entity_object_id], reverse=True)
172 return sorted_attrs[0]
174 raise Attribute.DoesNotExist
177 class PassthroughAttributeMapper(AttributeMapper):
179 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.
181 :param entities: An iterable of :class:`.Entity` subclass instances.
184 def __init__(self, entities):
185 self._attributes = [e.attributes for e in entities]
186 super(PassthroughAttributeMapper, self).__init__(self._attributes[0].entity)
188 def _fill_cache(self):
189 if self._cache_filled:
192 for a in reversed(self._attributes):
194 self._attributes_cache.update(a._attributes_cache)
195 self._cache.update(a._cache)
197 self._cache_filled = True
199 def get_attributes(self):
200 raise NotImplementedError
202 def clear_cache(self):
203 super(PassthroughAttributeMapper, self).clear_cache()
204 for a in self._attributes:
208 class LazyPassthroughAttributeMapper(LazyAttributeMapperMixin, PassthroughAttributeMapper):
209 """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."""
210 def _raw_get_attribute(self, key):
211 from philo.models import Attribute
212 for a in self._attributes:
213 attr = a.get_attribute(key)
216 raise Attribute.DoesNotExist