Implementation of QuerySetMappers for Entities that improves the worst-case number...
[philo.git] / models / base.py
1 from django import forms
2 from django.db import models
3 from django.contrib.contenttypes.models import ContentType
4 from django.contrib.contenttypes import generic
5 from django.core.exceptions import ObjectDoesNotExist
6 from django.core.validators import RegexValidator
7 from django.utils import simplejson as json
8 from django.utils.encoding import force_unicode
9 from philo.exceptions import AncestorDoesNotExist
10 from philo.models.fields import JSONField
11 from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
12 from philo.signals import entity_class_prepared
13 from philo.validators import json_validator
14 from UserDict import DictMixin
15 from mptt.models import MPTTModel, MPTTModelBase, MPTTOptions
16
17
18 class Tag(models.Model):
19         name = models.CharField(max_length=255)
20         slug = models.SlugField(max_length=255, unique=True)
21         
22         def __unicode__(self):
23                 return self.name
24         
25         class Meta:
26                 app_label = 'philo'
27                 ordering = ('name',)
28
29
30 class Titled(models.Model):
31         title = models.CharField(max_length=255)
32         slug = models.SlugField(max_length=255)
33         
34         def __unicode__(self):
35                 return self.title
36         
37         class Meta:
38                 abstract = True
39
40
41 value_content_type_limiter = ContentTypeRegistryLimiter()
42
43
44 def register_value_model(model):
45         value_content_type_limiter.register_class(model)
46
47
48 register_value_model(Tag)
49
50
51 def unregister_value_model(model):
52         value_content_type_limiter.unregister_class(model)
53
54
55 class AttributeValue(models.Model):
56         attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
57         
58         def set_value(self, value):
59                 raise NotImplementedError
60         
61         def value_formfields(self, **kwargs):
62                 """Define any formfields that would be used to construct an instance of this value."""
63                 raise NotImplementedError
64         
65         def construct_instance(self, **kwargs):
66                 """Apply cleaned data from the formfields generated by valid_formfields to oneself."""
67                 raise NotImplementedError
68         
69         def __unicode__(self):
70                 return unicode(self.value)
71         
72         class Meta:
73                 abstract = True
74
75
76 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
77
78
79 class JSONValue(AttributeValue):
80         value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null', db_index=True)
81         
82         def __unicode__(self):
83                 return force_unicode(self.value)
84         
85         def value_formfields(self):
86                 kwargs = {'initial': self.value_json}
87                 field = self._meta.get_field('value')
88                 return {field.name: field.formfield(**kwargs)}
89         
90         def construct_instance(self, **kwargs):
91                 field_name = self._meta.get_field('value').name
92                 self.set_value(kwargs.pop(field_name, None))
93         
94         def set_value(self, value):
95                 self.value = value
96         
97         class Meta:
98                 app_label = 'philo'
99
100
101 class ForeignKeyValue(AttributeValue):
102         content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
103         object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
104         value = generic.GenericForeignKey()
105         
106         def value_formfields(self):
107                 field = self._meta.get_field('content_type')
108                 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
109                 
110                 if self.content_type:
111                         kwargs = {
112                                 'initial': self.object_id,
113                                 'required': False,
114                                 'queryset': self.content_type.model_class()._default_manager.all()
115                         }
116                         fields['value'] = forms.ModelChoiceField(**kwargs)
117                 return fields
118         
119         def construct_instance(self, **kwargs):
120                 field_name = self._meta.get_field('content_type').name
121                 ct = kwargs.pop(field_name, None)
122                 if ct is None or ct != self.content_type:
123                         self.object_id = None
124                         self.content_type = ct
125                 else:
126                         value = kwargs.pop('value', None)
127                         self.set_value(value)
128                         if value is None:
129                                 self.content_type = ct
130         
131         def set_value(self, value):
132                 self.value = value
133         
134         class Meta:
135                 app_label = 'philo'
136
137
138 class ManyToManyValue(AttributeValue):
139         content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
140         values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
141         
142         def get_object_ids(self):
143                 return self.values.values_list('object_id', flat=True)
144         object_ids = property(get_object_ids)
145         
146         def set_value(self, value):
147                 # Value must be a queryset. Watch out for ModelMultipleChoiceField;
148                 # it returns its value as a list if empty.
149                 
150                 self.content_type = ContentType.objects.get_for_model(value.model)
151                 
152                 # Before we can fiddle with the many-to-many to foreignkeyvalues, we need
153                 # a pk.
154                 if self.pk is None:
155                         self.save()
156                 
157                 object_ids = value.values_list('id', flat=True)
158                 
159                 # These lines shouldn't be necessary; however, if object_ids is an EmptyQuerySet,
160                 # the code (specifically the object_id__in query) won't work without them. Unclear why...
161                 # TODO: is this still the case?
162                 if not object_ids:
163                         self.values.all().delete()
164                 else:
165                         self.values.exclude(object_id__in=object_ids, content_type=self.content_type).delete()
166                         
167                         current_ids = self.object_ids
168                         
169                         for object_id in object_ids:
170                                 if object_id in current_ids:
171                                         continue
172                                 self.values.create(content_type=self.content_type, object_id=object_id)
173         
174         def get_value(self):
175                 if self.content_type is None:
176                         return None
177                 
178                 # HACK to be safely explicit until http://code.djangoproject.com/ticket/15145 is resolved
179                 object_ids = self.object_ids
180                 manager = self.content_type.model_class()._default_manager
181                 if not object_ids:
182                         return manager.none()
183                 return manager.filter(id__in=self.object_ids)
184         
185         value = property(get_value, set_value)
186         
187         def value_formfields(self):
188                 field = self._meta.get_field('content_type')
189                 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
190                 
191                 if self.content_type:
192                         kwargs = {
193                                 'initial': self.object_ids,
194                                 'required': False,
195                                 'queryset': self.content_type.model_class()._default_manager.all()
196                         }
197                         fields['value'] = forms.ModelMultipleChoiceField(**kwargs)
198                 return fields
199         
200         def construct_instance(self, **kwargs):
201                 field_name = self._meta.get_field('content_type').name
202                 ct = kwargs.pop(field_name, None)
203                 if ct is None or ct != self.content_type:
204                         self.values.clear()
205                         self.content_type = ct
206                 else:
207                         value = kwargs.get('value', None)
208                         if not value:
209                                 value = self.content_type.model_class()._default_manager.none()
210                         self.set_value(value)
211         construct_instance.alters_data = True
212         
213         class Meta:
214                 app_label = 'philo'
215
216
217 class Attribute(models.Model):
218         entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
219         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
220         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
221         
222         value_content_type = models.ForeignKey(ContentType, related_name='attribute_value_set', limit_choices_to=attribute_value_limiter, verbose_name='Value type', null=True, blank=True)
223         value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
224         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
225         
226         key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
227         
228         def __unicode__(self):
229                 return u'"%s": %s' % (self.key, self.value)
230         
231         class Meta:
232                 app_label = 'philo'
233                 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
234
235
236 class QuerySetMapper(object, DictMixin):
237         def __init__(self, queryset, passthrough=None):
238                 self.queryset = queryset
239                 self.passthrough = passthrough
240         
241         def __getitem__(self, key):
242                 try:
243                         value = self.queryset.get(key__exact=key).value
244                 except ObjectDoesNotExist:
245                         if self.passthrough is not None:
246                                 return self.passthrough.__getitem__(key)
247                         raise KeyError
248                 else:
249                         if value is not None:
250                                 return value.value
251                         return value
252         
253         def keys(self):
254                 keys = set(self.queryset.values_list('key', flat=True).distinct())
255                 if self.passthrough is not None:
256                         keys |= set(self.passthrough.keys())
257                 return list(keys)
258
259
260 class EntityOptions(object):
261         def __init__(self, options):
262                 if options is not None:
263                         for key, value in options.__dict__.items():
264                                 setattr(self, key, value)
265                 if not hasattr(self, 'proxy_fields'):
266                         self.proxy_fields = []
267         
268         def add_proxy_field(self, proxy_field):
269                 self.proxy_fields.append(proxy_field)
270
271
272 class EntityBase(models.base.ModelBase):
273         def __new__(cls, name, bases, attrs):
274                 entity_meta = attrs.pop('EntityMeta', None)
275                 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
276                 new.add_to_class('_entity_meta', EntityOptions(entity_meta))
277                 entity_class_prepared.send(sender=new)
278                 return new
279
280
281 class EntityAttributeMapper(object, DictMixin):
282         def __init__(self, entity):
283                 self.entity = entity
284         
285         def get_attributes(self):
286                 return self.entity.attribute_set.all()
287         
288         def make_cache(self):
289                 attributes = self.get_attributes()
290                 value_lookups = {}
291                 
292                 for a in attributes:
293                         value_lookups.setdefault(a.value_content_type, []).append(a.value_object_id)
294                 
295                 values_bulk = {}
296                 
297                 for ct, pks in value_lookups.items():
298                         values_bulk[ct] = ct.model_class().objects.in_bulk(pks)
299                 
300                 self._cache = dict([(a.key, getattr(values_bulk[a.value_content_type].get(a.value_object_id), 'value', None)) for a in attributes])
301         
302         def __getitem__(self, key):
303                 if not hasattr(self, '_cache'):
304                         self.make_cache()
305                 return self._cache[key]
306         
307         def keys(self):
308                 if not hasattr(self, '_cache'):
309                         self.make_cache()
310                 return self._cache.keys()
311         
312         def items(self):
313                 if not hasattr(self, '_cache'):
314                         self.make_cache()
315                 return self._cache.items()
316         
317         def values(self):
318                 if not hasattr(self, '_cache'):
319                         self.make_cache()
320                 return self._cache.values()
321
322
323 class Entity(models.Model):
324         __metaclass__ = EntityBase
325         
326         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
327         
328         @property
329         def attributes(self):
330                 return EntityAttributeMapper(self)
331         
332         class Meta:
333                 abstract = True
334
335
336 class TreeManager(models.Manager):
337         use_for_related_fields = True
338         
339         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
340                 """
341                 Returns the object with the path, unless absolute_result is set to False, in which
342                 case it returns a tuple containing the deepest object found along the path, and the
343                 remainder of the path after that object as a string (or None if there is no remaining
344                 path). Raises a DoesNotExist exception if no object is found with the given path.
345                 
346                 If the path you're searching for is known to exist, it is always faster to use
347                 absolute_result=True - unless the path depth is over ~40, in which case the high cost
348                 of the absolute query makes a binary search (i.e. non-absolute) faster.
349                 """
350                 # Note: SQLite allows max of 64 tables in one join. That means the binary search will
351                 # only work on paths with a max depth of 127 and the absolute fetch will only work
352                 # to a max depth of (surprise!) 63. Although this could be handled, chances are your
353                 # tree structure won't be that deep.
354                 segments = path.split(pathsep)
355                 
356                 # Clean out blank segments. Handles multiple consecutive pathseps.
357                 while True:
358                         try:
359                                 segments.remove('')
360                         except ValueError:
361                                 break
362                 
363                 # Special-case a lack of segments. No queries necessary.
364                 if not segments:
365                         if root is not None:
366                                 if absolute_result:
367                                         return root
368                                 return root, None
369                         else:
370                                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
371                 
372                 def make_query_kwargs(segments, root):
373                         kwargs = {}
374                         prefix = ""
375                         revsegs = list(segments)
376                         revsegs.reverse()
377                         
378                         for segment in revsegs:
379                                 kwargs["%s%s__exact" % (prefix, field)] = segment
380                                 prefix += "parent__"
381                         
382                         if prefix:
383                                 kwargs[prefix[:-2]] = root
384                         
385                         return kwargs
386                 
387                 def find_obj(segments, depth, deepest_found=None):
388                         if deepest_found is None:
389                                 deepest_level = 0
390                         elif root is None:
391                                 deepest_level = deepest_found.get_level() + 1
392                         else:
393                                 deepest_level = deepest_found.get_level() - root.get_level()
394                         try:
395                                 obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found or root))
396                         except self.model.DoesNotExist:
397                                 if not deepest_level and depth > 1:
398                                         # make sure there's a root node...
399                                         depth = 1
400                                 else:
401                                         # Try finding one with half the path since the deepest find.
402                                         depth = (deepest_level + depth)/2
403                                 
404                                 if deepest_level == depth:
405                                         # This should happen if nothing is found with any part of the given path.
406                                         if root is not None and deepest_found is None:
407                                                 return root, pathsep.join(segments)
408                                         raise
409                                 
410                                 return find_obj(segments, depth, deepest_found)
411                         else:
412                                 # Yay! Found one!
413                                 if root is None:
414                                         deepest_level = obj.get_level() + 1
415                                 else:
416                                         deepest_level = obj.get_level() - root.get_level()
417                                 
418                                 # Could there be a deeper one?
419                                 if obj.is_leaf_node():
420                                         return obj, pathsep.join(segments[deepest_level:]) or None
421                                 
422                                 depth += (len(segments) - depth)/2 or len(segments) - depth
423                                 
424                                 if depth > deepest_level + obj.get_descendant_count():
425                                         depth = deepest_level + obj.get_descendant_count()
426                                 
427                                 if deepest_level == depth:
428                                         return obj, pathsep.join(segments[deepest_level:]) or None
429                                 
430                                 try:
431                                         return find_obj(segments, depth, obj)
432                                 except self.model.DoesNotExist:
433                                         # Then this was the deepest.
434                                         return obj, pathsep.join(segments[deepest_level:])
435                 
436                 if absolute_result:
437                         return self.get(**make_query_kwargs(segments, root))
438                 
439                 # Try a modified binary search algorithm. Feed the root in so that query complexity
440                 # can be reduced. It might be possible to weight the search towards the beginning
441                 # of the path, since short paths are more likely, but how far forward? It would
442                 # need to shift depending on len(segments) - perhaps logarithmically?
443                 return find_obj(segments, len(segments)/2 or len(segments))
444
445
446 class TreeModel(MPTTModel):
447         objects = TreeManager()
448         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
449         slug = models.SlugField(max_length=255)
450         
451         def get_path(self, root=None, pathsep='/', field='slug'):
452                 if root == self:
453                         return ''
454                 
455                 if root is not None and not self.is_descendant_of(root):
456                         raise AncestorDoesNotExist(root)
457                 
458                 qs = self.get_ancestors(include_self=True)
459                 
460                 if root is not None:
461                         qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
462                 
463                 return pathsep.join([getattr(parent, field, '?') for parent in qs])
464         path = property(get_path)
465         
466         def __unicode__(self):
467                 return self.path
468         
469         class Meta:
470                 unique_together = (('parent', 'slug'),)
471                 abstract = True
472
473
474 class TreeEntityBase(MPTTModelBase, EntityBase):
475         def __new__(meta, name, bases, attrs):
476                 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
477                 cls = EntityBase.__new__(meta, name, bases, attrs)
478                 
479                 return meta.register(cls)
480
481
482 class TreeEntityAttributeMapper(EntityAttributeMapper):
483         def get_attributes(self):
484                 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
485                 ct = ContentType.objects.get_for_model(self.entity)
486                 return sorted(Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys()), key=lambda x: ancestors[x.entity_object_id])
487
488
489 class TreeEntity(Entity, TreeModel):
490         __metaclass__ = TreeEntityBase
491         
492         @property
493         def attributes(self):
494                 if self.parent:
495                         return TreeEntityAttributeMapper(self)
496                 return super(TreeEntity, self).attributes
497         
498         class Meta:
499                 abstract = True