Moved efficient QuerySetMappers into philo.utils and replaced them with a more comple...
[philo.git] / philo / utils.py
1 from UserDict import DictMixin
2
3 from django.db import models
4 from django.contrib.contenttypes.models import ContentType
5 from django.core.paginator import Paginator, EmptyPage
6 from django.template import Context
7 from django.template.loader_tags import ExtendsNode, ConstantIncludeNode
8
9
10 def fattr(*args, **kwargs):
11         def wrapper(function):
12                 for key in kwargs:
13                         setattr(function, key, kwargs[key])
14                 return function
15         return wrapper
16
17
18 ### ContentTypeLimiters
19
20
21 class ContentTypeLimiter(object):
22         def q_object(self):
23                 return models.Q(pk__in=[])
24         
25         def add_to_query(self, query, *args, **kwargs):
26                 query.add_q(self.q_object(), *args, **kwargs)
27
28
29 class ContentTypeRegistryLimiter(ContentTypeLimiter):
30         def __init__(self):
31                 self.classes = []
32         
33         def register_class(self, cls):
34                 self.classes.append(cls)
35         
36         def unregister_class(self, cls):
37                 self.classes.remove(cls)
38         
39         def q_object(self):
40                 contenttype_pks = []
41                 for cls in self.classes:
42                         try:
43                                 if issubclass(cls, models.Model):
44                                         if not cls._meta.abstract:
45                                                 contenttype = ContentType.objects.get_for_model(cls)
46                                                 contenttype_pks.append(contenttype.pk)
47                         except:
48                                 pass
49                 return models.Q(pk__in=contenttype_pks)
50
51
52 class ContentTypeSubclassLimiter(ContentTypeLimiter):
53         def __init__(self, cls, inclusive=False):
54                 self.cls = cls
55                 self.inclusive = inclusive
56         
57         def q_object(self):
58                 contenttype_pks = []
59                 def handle_subclasses(cls):
60                         for subclass in cls.__subclasses__():
61                                 try:
62                                         if issubclass(subclass, models.Model):
63                                                 if not subclass._meta.abstract:
64                                                         if not self.inclusive and subclass is self.cls:
65                                                                 continue
66                                                         contenttype = ContentType.objects.get_for_model(subclass)
67                                                         contenttype_pks.append(contenttype.pk)
68                                         handle_subclasses(subclass)
69                                 except:
70                                         pass
71                 handle_subclasses(self.cls)
72                 return models.Q(pk__in=contenttype_pks)
73
74
75 ### Pagination
76
77
78 def paginate(objects, per_page=None, page_number=1):
79         """
80         Given a list of objects, return a (paginator, page, objects) tuple.
81         """
82         try:
83                 per_page = int(per_page)
84         except (TypeError, ValueError):
85                 # Then either it wasn't set or it was set to an invalid value
86                 paginator = page = None
87         else:
88                 # There also shouldn't be pagination if the list is too short. Try count()
89                 # first - good chance it's a queryset, where count is more efficient.
90                 try:
91                         if objects.count() <= per_page:
92                                 paginator = page = None
93                 except AttributeError:
94                         if len(objects) <= per_page:
95                                 paginator = page = None
96         
97         try:
98                 return paginator, page, objects
99         except NameError:
100                 pass
101         
102         paginator = Paginator(objects, per_page)
103         try:
104                 page_number = int(page_number)
105         except:
106                 page_number = 1
107         
108         try:
109                 page = paginator.page(page_number)
110         except EmptyPage:
111                 page = None
112         else:
113                 objects = page.object_list
114         
115         return paginator, page, objects
116
117
118 ### Facilitating template analysis.
119
120
121 LOADED_TEMPLATE_ATTR = '_philo_loaded_template'
122 BLANK_CONTEXT = Context()
123
124
125 def get_extended(self):
126         return self.get_parent(BLANK_CONTEXT)
127
128
129 def get_included(self):
130         return self.template
131
132
133 # We ignore the IncludeNode because it will never work in a blank context.
134 setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended))
135 setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
136
137
138 ### AttributeMappers
139
140
141 class AttributeMapper(object, DictMixin):
142         def __init__(self, entity):
143                 self.entity = entity
144                 self.clear_cache()
145         
146         def __getitem__(self, key):
147                 if not self._cache_populated:
148                         self._populate_cache()
149                 return self._cache[key]
150         
151         def __setitem__(self, key, value):
152                 # Prevent circular import.
153                 from philo.models.base import JSONValue, ForeignKeyValue, ManyToManyValue, Attribute
154                 old_attr = self.get_attribute(key)
155                 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:
156                         attribute = old_attr
157                 else:
158                         attribute = Attribute(key=key)
159                         attribute.entity = self.entity
160                         attribute.full_clean()
161                 
162                 if isinstance(value, models.query.QuerySet):
163                         value_class = ManyToManyValue
164                 elif isinstance(value, models.Model):
165                         value_class = ForeignKeyValue
166                 else:
167                         value_class = JSONValue
168                 
169                 attribute.set_value(value=value, value_class=value_class)
170                 self._cache[key] = attribute.value.value
171                 self._attributes_cache[key] = attribute
172         
173         def get_attributes(self):
174                 return self.entity.attribute_set.all()
175         
176         def get_attribute(self, key):
177                 if not self._cache_populated:
178                         self._populate_cache()
179                 return self._attributes_cache.get(key, None)
180         
181         def keys(self):
182                 if not self._cache_populated:
183                         self._populate_cache()
184                 return self._cache.keys()
185         
186         def items(self):
187                 if not self._cache_populated:
188                         self._populate_cache()
189                 return self._cache.items()
190         
191         def values(self):
192                 if not self._cache_populated:
193                         self._populate_cache()
194                 return self._cache.values()
195         
196         def _populate_cache(self):
197                 if self._cache_populated:
198                         return
199                 
200                 attributes = self.get_attributes()
201                 value_lookups = {}
202                 
203                 for a in attributes:
204                         value_lookups.setdefault(a.value_content_type, []).append(a.value_object_id)
205                         self._attributes_cache[a.key] = a
206                 
207                 values_bulk = {}
208                 
209                 for ct, pks in value_lookups.items():
210                         values_bulk[ct] = ct.model_class().objects.in_bulk(pks)
211                 
212                 self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type].get(a.value_object_id), 'value', None)) for a in attributes]))
213                 self._cache_populated = True
214         
215         def clear_cache(self):
216                 self._cache = {}
217                 self._attributes_cache = {}
218                 self._cache_populated = False
219
220
221 class LazyAttributeMapperMixin(object):
222         def __getitem__(self, key):
223                 if key not in self._cache and not self._cache_populated:
224                         self._add_to_cache(key)
225                 return self._cache[key]
226         
227         def get_attribute(self, key):
228                 if key not in self._attributes_cache and not self._cache_populated:
229                         self._add_to_cache(key)
230                 return self._attributes_cache[key]
231         
232         def _add_to_cache(self, key):
233                 try:
234                         attr = self.get_attributes().get(key=key)
235                 except Attribute.DoesNotExist:
236                         raise KeyError
237                 else:
238                         val = getattr(attr.value, 'value', None)
239                         self._cache[key] = val
240                         self._attributes_cache[key] = attr
241
242
243 class LazyAttributeMapper(LazyAttributeMapperMixin, AttributeMapper):
244         def get_attributes(self):
245                 return super(LazyAttributeMapper, self).get_attributes().exclude(key__in=self._cache.keys())
246
247
248 class TreeAttributeMapper(AttributeMapper):
249         def get_attributes(self):
250                 from philo.models import Attribute
251                 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
252                 ct = ContentType.objects.get_for_model(self.entity)
253                 return sorted(Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys()), key=lambda x: ancestors[x.entity_object_id])
254
255
256 class LazyTreeAttributeMapper(LazyAttributeMapperMixin, TreeAttributeMapper):
257         def get_attributes(self):
258                 return super(LazyTreeAttributeMapper, self).get_attributes().exclude(key__in=self._cache.keys())
259
260
261 class PassthroughAttributeMapper(AttributeMapper):
262         def __init__(self, entities):
263                 self._attributes = [e.attributes for e in entities]
264                 super(PassthroughAttributeMapper, self).__init__(self._attributes[0].entity)
265         
266         def _populate_cache(self):
267                 if self._cache_populated:
268                         return
269                 
270                 for a in reversed(self._attributes):
271                         a._populate_cache()
272                         self._attributes_cache.update(a._attributes_cache)
273                         self._cache.update(a._cache)
274                 
275                 self._cache_populated = True
276         
277         def get_attributes(self):
278                 raise NotImplementedError
279         
280         def clear_cache(self):
281                 super(PassthroughAttributeMapper, self).clear_cache()
282                 for a in self._attributes:
283                         a.clear_cache()
284
285
286 class LazyPassthroughAttributeMapper(LazyAttributeMapperMixin, PassthroughAttributeMapper):
287         def _add_to_cache(self, key):
288                 for a in self._attributes:
289                         try:
290                                 self._cache[key] = a[key]
291                                 self._attributes_cache[key] = a.get_attribute(key)
292                         except KeyError:
293                                 pass
294                         else:
295                                 break
296                 return self._cache[key]