Lazy-eval the values of AttributeValues instead of loading them all during AttributeM...
[philo.git] / philo / utils / entities.py
1 from functools import partial
2 from UserDict import DictMixin
3
4 from django.db import models
5 from django.contrib.contenttypes.models import ContentType
6
7 from philo.utils.lazycompat import SimpleLazyObject
8
9
10 ### AttributeMappers
11
12
13 class AttributeMapper(object, DictMixin):
14         """
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.
16         
17         :param entity: The :class:`~philo.models.base.Entity` subclass instance whose :class:`~philo.models.base.Attribute`\ s will be made accessible.
18         
19         """
20         def __init__(self, entity):
21                 self.entity = entity
22                 self.clear_cache()
23         
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:
27                         self._fill_cache()
28                 return self._cache[key]
29         
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:
36                         attribute = old_attr
37                 else:
38                         attribute = Attribute(key=key)
39                         attribute.entity = self.entity
40                         attribute.full_clean()
41                 
42                 if isinstance(value, models.query.QuerySet):
43                         value_class = ManyToManyValue
44                 elif isinstance(value, models.Model):
45                         value_class = ForeignKeyValue
46                 else:
47                         value_class = JSONValue
48                 
49                 attribute.set_value(value=value, value_class=value_class)
50                 self._cache[key] = attribute.value.value
51                 self._attributes_cache[key] = attribute
52         
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()
56         
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:
60                         self._fill_cache()
61                 return self._attributes_cache.get(key, default)
62         
63         def keys(self):
64                 """Returns the keys from the cache, first populating the cache if necessary."""
65                 if not self._cache_filled:
66                         self._fill_cache()
67                 return self._cache.keys()
68         
69         def items(self):
70                 """Returns the items from the cache, first populating the cache if necessary."""
71                 if not self._cache_filled:
72                         self._fill_cache()
73                 return self._cache.items()
74         
75         def values(self):
76                 """Returns the values from the cache, first populating the cache if necessary."""
77                 if not self._cache_filled:
78                         self._fill_cache()
79                 return self._cache.values()
80         
81         def _fill_cache(self):
82                 if self._cache_filled:
83                         return
84                 
85                 attributes = self.get_attributes()
86                 value_lookups = {}
87                 
88                 for a in attributes:
89                         value_lookups.setdefault(a.value_content_type_id, []).append(a.value_object_id)
90                         self._attributes_cache[a.key] = a
91                 
92                 values_bulk = {}
93                 
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)
96                 
97                 self._cache.update(dict([
98                         (
99                                 a.key,
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
103         
104         def clear_cache(self):
105                 """Clears the cache."""
106                 self._cache = {}
107                 self._attributes_cache = {}
108                 self._cache_filled = False
109
110
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]
117         
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)
122         
123         def _raw_get_attribute(self, key):
124                 return self.get_attributes().get(key=key)
125         
126         def _add_to_cache(self, key):
127                 from philo.models.base import Attribute
128                 try:
129                         attr = self._raw_get_attribute(key)
130                 except Attribute.DoesNotExist:
131                         raise KeyError
132                 else:
133                         val = getattr(attr.value, 'value', None)
134                         self._cache[key] = val
135                         self._attributes_cache[key] = attr
136
137
138 class LazyAttributeMapper(LazyAttributeMapperMixin, AttributeMapper):
139         def get_attributes(self):
140                 return super(LazyAttributeMapper, self).get_attributes().exclude(key__in=self._cache.keys())
141
142
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])
152
153
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])
161         
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)
166                 try:
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]
170                 except IndexError:
171                         raise Attribute.DoesNotExist
172
173
174 class PassthroughAttributeMapper(AttributeMapper):
175         """
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.
177         
178         :param entities: An iterable of :class:`.Entity` subclass instances.
179         
180         """
181         def __init__(self, entities):
182                 self._attributes = [e.attributes for e in entities]
183                 super(PassthroughAttributeMapper, self).__init__(self._attributes[0].entity)
184         
185         def _fill_cache(self):
186                 if self._cache_filled:
187                         return
188                 
189                 for a in reversed(self._attributes):
190                         a._fill_cache()
191                         self._attributes_cache.update(a._attributes_cache)
192                         self._cache.update(a._cache)
193                 
194                 self._cache_filled = True
195         
196         def get_attributes(self):
197                 raise NotImplementedError
198         
199         def clear_cache(self):
200                 super(PassthroughAttributeMapper, self).clear_cache()
201                 for a in self._attributes:
202                         a.clear_cache()
203
204
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)
211                         if attr is not None:
212                                 return attr
213                 raise Attribute.DoesNotExist