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