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
94 for ct_pk, pks in value_lookups.items():
95 values_bulk[ct_pk] = ContentType.objects.get_for_id(ct_pk).model_class().objects.in_bulk(pks)
97 self._cache.update(dict([
100 SimpleLazyObject(partial(getattr, values_bulk[a.value_content_type_id].get(a.value_object_id), 'value', None))
101 ) for a in attributes]))
102 self._cache_filled = True
104 def clear_cache(self):
105 """Clears the cache."""
107 self._attributes_cache = {}
108 self._cache_filled = False
111 class LazyAttributeMapperMixin(object):
112 """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."""
113 def __getitem__(self, key):
114 if key not in self._cache and not self._cache_filled:
115 self._add_to_cache(key)
116 return self._cache[key]
118 def get_attribute(self, key, default=None):
119 if key not in self._attributes_cache and not self._cache_filled:
120 self._add_to_cache(key)
121 return self._attributes_cache.get(key, default)
123 def _raw_get_attribute(self, key):
124 return self.get_attributes().get(key=key)
126 def _add_to_cache(self, key):
127 from philo.models.base import Attribute
129 attr = self._raw_get_attribute(key)
130 except Attribute.DoesNotExist:
133 val = getattr(attr.value, 'value', None)
134 self._cache[key] = val
135 self._attributes_cache[key] = attr
138 class LazyAttributeMapper(LazyAttributeMapperMixin, AttributeMapper):
139 def get_attributes(self):
140 return super(LazyAttributeMapper, self).get_attributes().exclude(key__in=self._cache.keys())
143 class TreeAttributeMapper(AttributeMapper):
144 """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."""
145 def get_attributes(self):
146 """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."""
147 from philo.models import Attribute
148 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
149 ct = ContentType.objects.get_for_model(self.entity)
150 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys())
151 return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
154 class LazyTreeAttributeMapper(LazyAttributeMapperMixin, TreeAttributeMapper):
155 def get_attributes(self):
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)
159 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys()).exclude(key__in=self._cache.keys())
160 return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
162 def _raw_get_attribute(self, key):
163 from philo.models import Attribute
164 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
165 ct = ContentType.objects.get_for_model(self.entity)
167 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys(), key=key)
168 sorted_attrs = sorted(attrs, key=lambda x: ancestors[x.entity_object_id], reverse=True)
169 return sorted_attrs[0]
171 raise Attribute.DoesNotExist
174 class PassthroughAttributeMapper(AttributeMapper):
176 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.
178 :param entities: An iterable of :class:`.Entity` subclass instances.
181 def __init__(self, entities):
182 self._attributes = [e.attributes for e in entities]
183 super(PassthroughAttributeMapper, self).__init__(self._attributes[0].entity)
185 def _fill_cache(self):
186 if self._cache_filled:
189 for a in reversed(self._attributes):
191 self._attributes_cache.update(a._attributes_cache)
192 self._cache.update(a._cache)
194 self._cache_filled = True
196 def get_attributes(self):
197 raise NotImplementedError
199 def clear_cache(self):
200 super(PassthroughAttributeMapper, self).clear_cache()
201 for a in self._attributes:
205 class LazyPassthroughAttributeMapper(LazyAttributeMapperMixin, PassthroughAttributeMapper):
206 """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."""
207 def _raw_get_attribute(self, key):
208 from philo.models import Attribute
209 for a in self._attributes:
210 attr = a.get_attribute(key)
213 raise Attribute.DoesNotExist