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_populated:
24 self._populate_cache()
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_populated:
57 self._populate_cache()
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_populated:
63 self._populate_cache()
64 return self._cache.keys()
67 """Returns the items from the cache, first populating the cache if necessary."""
68 if not self._cache_populated:
69 self._populate_cache()
70 return self._cache.items()
73 """Returns the values from the cache, first populating the cache if necessary."""
74 if not self._cache_populated:
75 self._populate_cache()
76 return self._cache.values()
78 def _populate_cache(self):
79 if self._cache_populated:
82 attributes = self.get_attributes()
86 value_lookups.setdefault(a.value_content_type, []).append(a.value_object_id)
87 self._attributes_cache[a.key] = a
91 for ct, pks in value_lookups.items():
92 values_bulk[ct] = ct.model_class().objects.in_bulk(pks)
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_populated = True
97 def clear_cache(self):
98 """Clears the cache."""
100 self._attributes_cache = {}
101 self._cache_populated = 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_populated:
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_populated:
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):
121 attr = self._raw_get_attribute(key)
122 except Attribute.DoesNotExist:
125 val = getattr(attr.value, 'value', None)
126 self._cache[key] = val
127 self._attributes_cache[key] = attr
130 class LazyAttributeMapper(LazyAttributeMapperMixin, AttributeMapper):
131 def get_attributes(self):
132 return super(LazyAttributeMapper, self).get_attributes().exclude(key__in=self._cache.keys())
135 class TreeAttributeMapper(AttributeMapper):
136 """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."""
137 def get_attributes(self):
138 """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."""
139 from philo.models import Attribute
140 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
141 ct = ContentType.objects.get_for_model(self.entity)
142 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys())
143 return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
146 class LazyTreeAttributeMapper(LazyAttributeMapperMixin, TreeAttributeMapper):
147 def get_attributes(self):
148 from philo.models import Attribute
149 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
150 ct = ContentType.objects.get_for_model(self.entity)
151 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys()).exclude(key__in=self._cache.keys())
152 return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
154 def _raw_get_attribute(self, key):
155 from philo.models import Attribute
156 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
157 ct = ContentType.objects.get_for_model(self.entity)
159 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys(), key=key)
160 sorted_attrs = sorted(attrs, key=lambda x: ancestors[x.entity_object_id], reverse=True)
161 return sorted_attrs[0]
163 raise Attribute.DoesNotExist
166 class PassthroughAttributeMapper(AttributeMapper):
167 """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."""
168 def __init__(self, entities):
169 self._attributes = [e.attributes for e in entities]
170 super(PassthroughAttributeMapper, self).__init__(self._attributes[0].entity)
172 def _populate_cache(self):
173 if self._cache_populated:
176 for a in reversed(self._attributes):
178 self._attributes_cache.update(a._attributes_cache)
179 self._cache.update(a._cache)
181 self._cache_populated = True
183 def get_attributes(self):
184 raise NotImplementedError
186 def clear_cache(self):
187 super(PassthroughAttributeMapper, self).clear_cache()
188 for a in self._attributes:
192 class LazyPassthroughAttributeMapper(LazyAttributeMapperMixin, PassthroughAttributeMapper):
193 """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."""
194 def _raw_get_attribute(self, key):
195 from philo.models import Attribute
196 for a in self._attributes:
197 attr = a.get_attribute(key)
200 raise Attribute.DoesNotExist