Merge branch 'master' of git://github.com/melinath/philo
[philo.git] / philo / utils / entities.py
1 from UserDict import DictMixin
2
3 from django.db import models
4 from django.contrib.contenttypes.models import ContentType
5
6
7 ### AttributeMappers
8
9
10 class AttributeMapper(object, DictMixin):
11         """
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.
13         
14         :param entity: The :class:`~philo.models.base.Entity` subclass instance whose :class:`~philo.models.base.Attribute`\ s will be made accessible.
15         
16         """
17         def __init__(self, entity):
18                 self.entity = entity
19                 self.clear_cache()
20         
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:
24                         self._fill_cache()
25                 return self._cache[key]
26         
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:
33                         attribute = old_attr
34                 else:
35                         attribute = Attribute(key=key)
36                         attribute.entity = self.entity
37                         attribute.full_clean()
38                 
39                 if isinstance(value, models.query.QuerySet):
40                         value_class = ManyToManyValue
41                 elif isinstance(value, models.Model):
42                         value_class = ForeignKeyValue
43                 else:
44                         value_class = JSONValue
45                 
46                 attribute.set_value(value=value, value_class=value_class)
47                 self._cache[key] = attribute.value.value
48                 self._attributes_cache[key] = attribute
49         
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()
53         
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:
57                         self._fill_cache()
58                 return self._attributes_cache.get(key, default)
59         
60         def keys(self):
61                 """Returns the keys from the cache, first populating the cache if necessary."""
62                 if not self._cache_filled:
63                         self._fill_cache()
64                 return self._cache.keys()
65         
66         def items(self):
67                 """Returns the items from the cache, first populating the cache if necessary."""
68                 if not self._cache_filled:
69                         self._fill_cache()
70                 return self._cache.items()
71         
72         def values(self):
73                 """Returns the values from the cache, first populating the cache if necessary."""
74                 if not self._cache_filled:
75                         self._fill_cache()
76                 return self._cache.values()
77         
78         def _fill_cache(self):
79                 if self._cache_filled:
80                         return
81                 
82                 attributes = self.get_attributes()
83                 value_lookups = {}
84                 
85                 for a in attributes:
86                         value_lookups.setdefault(a.value_content_type, []).append(a.value_object_id)
87                         self._attributes_cache[a.key] = a
88                 
89                 values_bulk = {}
90                 
91                 for ct, pks in value_lookups.items():
92                         values_bulk[ct] = ct.model_class().objects.in_bulk(pks)
93                 
94                 self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type].get(a.value_object_id), 'value', None)) for a in attributes]))
95                 self._cache_filled = True
96         
97         def clear_cache(self):
98                 """Clears the cache."""
99                 self._cache = {}
100                 self._attributes_cache = {}
101                 self._cache_filled = False
102
103
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]
110         
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)
115         
116         def _raw_get_attribute(self, key):
117                 return self.get_attributes().get(key=key)
118         
119         def _add_to_cache(self, key):
120                 from philo.models.base import Attribute
121                 try:
122                         attr = self._raw_get_attribute(key)
123                 except Attribute.DoesNotExist:
124                         raise KeyError
125                 else:
126                         val = getattr(attr.value, 'value', None)
127                         self._cache[key] = val
128                         self._attributes_cache[key] = attr
129
130
131 class LazyAttributeMapper(LazyAttributeMapperMixin, AttributeMapper):
132         def get_attributes(self):
133                 return super(LazyAttributeMapper, self).get_attributes().exclude(key__in=self._cache.keys())
134
135
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])
145
146
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])
154         
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)
159                 try:
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]
163                 except IndexError:
164                         raise Attribute.DoesNotExist
165
166
167 class PassthroughAttributeMapper(AttributeMapper):
168         """
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.
170         
171         :param entities: An iterable of :class:`.Entity` subclass instances.
172         
173         """
174         def __init__(self, entities):
175                 self._attributes = [e.attributes for e in entities]
176                 super(PassthroughAttributeMapper, self).__init__(self._attributes[0].entity)
177         
178         def _fill_cache(self):
179                 if self._cache_filled:
180                         return
181                 
182                 for a in reversed(self._attributes):
183                         a._fill_cache()
184                         self._attributes_cache.update(a._attributes_cache)
185                         self._cache.update(a._cache)
186                 
187                 self._cache_filled = True
188         
189         def get_attributes(self):
190                 raise NotImplementedError
191         
192         def clear_cache(self):
193                 super(PassthroughAttributeMapper, self).clear_cache()
194                 for a in self._attributes:
195                         a.clear_cache()
196
197
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)
204                         if attr is not None:
205                                 return attr
206                 raise Attribute.DoesNotExist