Merge branch 'release/0.9.2'
[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 ValidationError
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
15 from philo.utils.entities import AttributeMapper, TreeAttributeMapper
16 from philo.validators import json_validator
17
18
19 __all__ = ('value_content_type_limiter', 'register_value_model', 'unregister_value_model', 'JSONValue', 'ForeignKeyValue', 'ManyToManyValue', 'Attribute', 'Entity', 'TreeEntity', 'SlugTreeEntity')
20
21
22 #: 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.
23 value_content_type_limiter = ContentTypeRegistryLimiter()
24
25
26 def register_value_model(model):
27         """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
28         value_content_type_limiter.register_class(model)
29
30
31 def unregister_value_model(model):
32         """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
33         value_content_type_limiter.unregister_class(model)
34
35
36 class AttributeValue(models.Model):
37         """
38         This is an abstract base class for models that can be used as values for :class:`Attribute`\ s.
39         
40         AttributeValue subclasses are expected to supply access to a clean version of their value through an attribute called "value".
41         
42         """
43         
44         #: :class:`GenericRelation` to :class:`Attribute`
45         attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
46         
47         def set_value(self, value):
48                 """Given a ``value``, sets the appropriate fields so that it can be correctly stored in the database."""
49                 raise NotImplementedError
50         
51         def value_formfields(self, **kwargs):
52                 """
53                 Returns any formfields that would be used to construct an instance of this value.
54                 
55                 :returns: A dictionary mapping field names to formfields.
56                 
57                 """
58                 
59                 raise NotImplementedError
60         
61         def construct_instance(self, **kwargs):
62                 """Applies cleaned data from the formfields generated by valid_formfields to oneself."""
63                 raise NotImplementedError
64         
65         def __unicode__(self):
66                 return unicode(self.value)
67         
68         class Meta:
69                 abstract = True
70
71
72 #: An instance of :class:`.ContentTypeSubclassLimiter` which is used to track the content types which are considered valid value models for an :class:`Attribute`.
73 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
74
75
76 class JSONValue(AttributeValue):
77         """Stores a python object as a json string."""
78         value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null', db_index=True)
79         
80         def __unicode__(self):
81                 return force_unicode(self.value)
82         
83         def value_formfields(self):
84                 kwargs = {'initial': self.value_json}
85                 field = self._meta.get_field('value')
86                 return {field.name: field.formfield(**kwargs)}
87         
88         def construct_instance(self, **kwargs):
89                 field_name = self._meta.get_field('value').name
90                 self.set_value(kwargs.pop(field_name, None))
91         
92         def set_value(self, value):
93                 self.value = value
94         
95         class Meta:
96                 app_label = 'philo'
97
98
99 class ForeignKeyValue(AttributeValue):
100         """Stores a generic relationship to an instance of any value content type (as defined by the :data:`value_content_type_limiter`)."""
101         content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
102         object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
103         value = generic.GenericForeignKey()
104         
105         def value_formfields(self):
106                 field = self._meta.get_field('content_type')
107                 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
108                 
109                 if self.content_type:
110                         kwargs = {
111                                 'initial': self.object_id,
112                                 'required': False,
113                                 'queryset': self.content_type.model_class()._default_manager.all()
114                         }
115                         fields['value'] = forms.ModelChoiceField(**kwargs)
116                 return fields
117         
118         def construct_instance(self, **kwargs):
119                 field_name = self._meta.get_field('content_type').name
120                 ct = kwargs.pop(field_name, None)
121                 if ct is None or ct != self.content_type:
122                         self.object_id = None
123                         self.content_type = ct
124                 else:
125                         value = kwargs.pop('value', None)
126                         self.set_value(value)
127                         if value is None:
128                                 self.content_type = ct
129         
130         def set_value(self, value):
131                 self.value = value
132         
133         class Meta:
134                 app_label = 'philo'
135
136
137 class ManyToManyValue(AttributeValue):
138         """Stores a generic relationship to many instances of any value content type (as defined by the :data:`value_content_type_limiter`)."""
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         """
219         :class:`Attribute`\ s exist primarily to let arbitrary data be attached to arbitrary model instances without altering the database schema and without guaranteeing that the data will be available on every instance of that model.
220         
221         Generally, :class:`Attribute`\ s will not be accessed as models; instead, they will be accessed through the :attr:`Entity.attributes` property, which allows direct dictionary getting and setting of the value of an :class:`Attribute` with its key.
222         
223         """
224         entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
225         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
226         
227         #: :class:`GenericForeignKey` to anything (generally an instance of an Entity subclass).
228         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
229         
230         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)
231         value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
232         
233         #: :class:`GenericForeignKey` to an instance of a subclass of :class:`AttributeValue` as determined by the :data:`attribute_value_limiter`.
234         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
235         
236         #: :class:`CharField` containing a key (up to 255 characters) consisting of alphanumeric characters and underscores.
237         key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
238         
239         def __unicode__(self):
240                 return u'"%s": %s' % (self.key, self.value)
241         
242         def set_value(self, value, value_class=JSONValue):
243                 """Given a value and a value class, sets up self.value appropriately."""
244                 if isinstance(self.value, value_class):
245                         val = self.value
246                 else:
247                         if isinstance(self.value, models.Model):
248                                 self.value.delete()
249                         val = value_class()
250                 
251                 val.set_value(value)
252                 val.save()
253                 
254                 self.value = val
255                 self.save()
256         
257         class Meta:
258                 app_label = 'philo'
259                 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
260
261
262 class EntityOptions(object):
263         def __init__(self, options):
264                 if options is not None:
265                         for key, value in options.__dict__.items():
266                                 setattr(self, key, value)
267                 if not hasattr(self, 'proxy_fields'):
268                         self.proxy_fields = []
269         
270         def add_proxy_field(self, proxy_field):
271                 self.proxy_fields.append(proxy_field)
272
273
274 class EntityBase(models.base.ModelBase):
275         def __new__(cls, name, bases, attrs):
276                 entity_meta = attrs.pop('EntityMeta', None)
277                 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
278                 new.add_to_class('_entity_meta', EntityOptions(entity_meta))
279                 entity_class_prepared.send(sender=new)
280                 return new
281
282
283 class Entity(models.Model):
284         """An abstract class that simplifies access to related attributes. Most models provided by Philo subclass Entity."""
285         __metaclass__ = EntityBase
286         
287         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
288         
289         def get_attribute_mapper(self, mapper=AttributeMapper):
290                 """
291                 Returns an :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly.
292
293                 Example::
294
295                         >>> attr = entity.attribute_set.get(key='spam')
296                         >>> attr.value.value
297                         u'eggs'
298                         >>> entity.attributes['spam']
299                         u'eggs'
300                 
301                 """
302                 return mapper(self)
303         
304         @property
305         def attributes(self):
306                 if not hasattr(self, '_attributes'):
307                         self._attributes = self.get_attribute_mapper()
308                 return self._attributes
309         
310         class Meta:
311                 abstract = True
312
313
314 class TreeEntityBase(MPTTModelBase, EntityBase):
315         def __new__(meta, name, bases, attrs):
316                 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
317                 cls = EntityBase.__new__(meta, name, bases, attrs)
318                 
319                 return meta.register(cls)
320
321
322 class TreeEntityManager(models.Manager):
323         use_for_related_fields = True
324         
325         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='pk'):
326                 """
327                 If ``absolute_result`` is ``True``, returns the object at ``path`` (starting at ``root``) or raises an :class:`~django.core.exceptions.ObjectDoesNotExist` 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).
328                 
329                 .. 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.
330                 
331                 .. 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.
332                 
333                 :param path: The path of the object
334                 :param root: The object which will be considered the root of the search
335                 :param absolute_result: Whether to return an absolute result or do a binary search
336                 :param pathsep: The path separator used in ``path``
337                 :param field: The field on the model which should be queried for ``path`` segment matching.
338                 :returns: An instance if ``absolute_result`` is ``True`` or an (instance, remaining_path) tuple otherwise.
339                 :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters.
340                 
341                 """
342                 
343                 segments = path.split(pathsep)
344                 
345                 # Clean out blank segments. Handles multiple consecutive pathseps.
346                 while True:
347                         try:
348                                 segments.remove('')
349                         except ValueError:
350                                 break
351                 
352                 # Special-case a lack of segments. No queries necessary.
353                 if not segments:
354                         if root is not None:
355                                 if absolute_result:
356                                         return root
357                                 return root, None
358                         else:
359                                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
360                 
361                 def make_query_kwargs(segments, root):
362                         kwargs = {}
363                         prefix = ""
364                         revsegs = list(segments)
365                         revsegs.reverse()
366                         
367                         for segment in revsegs:
368                                 kwargs["%s%s__exact" % (prefix, field)] = segment
369                                 prefix += "parent__"
370                         
371                         if prefix:
372                                 kwargs[prefix[:-2]] = root
373                         
374                         return kwargs
375                 
376                 def find_obj(segments, depth, deepest_found=None):
377                         if deepest_found is None:
378                                 deepest_level = 0
379                         elif root is None:
380                                 deepest_level = deepest_found.get_level() + 1
381                         else:
382                                 deepest_level = deepest_found.get_level() - root.get_level()
383                         try:
384                                 obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found or root))
385                         except self.model.DoesNotExist:
386                                 if not deepest_level and depth > 1:
387                                         # make sure there's a root node...
388                                         depth = 1
389                                 else:
390                                         # Try finding one with half the path since the deepest find.
391                                         depth = (deepest_level + depth)/2
392                                 
393                                 if deepest_level == depth:
394                                         # This should happen if nothing is found with any part of the given path.
395                                         if root is not None and deepest_found is None:
396                                                 return root, pathsep.join(segments)
397                                         raise
398                                 
399                                 return find_obj(segments, depth, deepest_found)
400                         else:
401                                 # Yay! Found one!
402                                 if root is None:
403                                         deepest_level = obj.get_level() + 1
404                                 else:
405                                         deepest_level = obj.get_level() - root.get_level()
406                                 
407                                 # Could there be a deeper one?
408                                 if obj.is_leaf_node():
409                                         return obj, pathsep.join(segments[deepest_level:]) or None
410                                 
411                                 depth += (len(segments) - depth)/2 or len(segments) - depth
412                                 
413                                 if depth > deepest_level + obj.get_descendant_count():
414                                         depth = deepest_level + obj.get_descendant_count()
415                                 
416                                 if deepest_level == depth:
417                                         return obj, pathsep.join(segments[deepest_level:]) or None
418                                 
419                                 try:
420                                         return find_obj(segments, depth, obj)
421                                 except self.model.DoesNotExist:
422                                         # Then this was the deepest.
423                                         return obj, pathsep.join(segments[deepest_level:])
424                 
425                 if absolute_result:
426                         return self.get(**make_query_kwargs(segments, root))
427                 
428                 # Try a modified binary search algorithm. Feed the root in so that query complexity
429                 # can be reduced. It might be possible to weight the search towards the beginning
430                 # of the path, since short paths are more likely, but how far forward? It would
431                 # need to shift depending on len(segments) - perhaps logarithmically?
432                 return find_obj(segments, len(segments)/2 or len(segments))
433
434
435 class TreeEntity(Entity, MPTTModel):
436         """An abstract subclass of Entity which represents a tree relationship."""
437         
438         __metaclass__ = TreeEntityBase
439         objects = TreeEntityManager()
440         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
441         
442         def get_path(self, root=None, pathsep='/', field='pk', memoize=True):
443                 """
444                 :param root: Only return the path since this object.
445                 :param pathsep: The path separator to use when constructing an instance's path
446                 :param field: The field to pull path information from for each ancestor.
447                 :param memoize: Whether to use memoized results. Since, in most cases, the ancestors of a TreeEntity will not change over the course of an instance's lifetime, this defaults to ``True``.
448                 :returns: A string representation of an object's path.
449                 
450                 """
451                 
452                 if root == self:
453                         return ''
454                 
455                 parent_id = getattr(self, "%s_id" % self._mptt_meta.parent_attr)
456                 if getattr(root, 'pk', None) == parent_id:
457                         return getattr(self, field, '?')
458                 
459                 if root is not None and not self.is_descendant_of(root):
460                         raise AncestorDoesNotExist(root)
461                 
462                 if memoize:
463                         memo_args = (parent_id, getattr(root, 'pk', None), pathsep, getattr(self, field, '?'))
464                         try:
465                                 return self._path_memo[memo_args]
466                         except AttributeError:
467                                 self._path_memo = {}
468                         except KeyError:
469                                 pass
470                 
471                 qs = self.get_ancestors(include_self=True)
472                 
473                 if root is not None:
474                         qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
475                 
476                 path = pathsep.join([getattr(parent, field, '?') for parent in qs])
477                 
478                 if memoize:
479                         self._path_memo[memo_args] = path
480                 
481                 return path
482         path = property(get_path)
483         
484         def get_attribute_mapper(self, mapper=None):
485                 """
486                 Returns a :class:`.TreeAttributeMapper` or :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly. If an :class:`Attribute` with a given key is not related to the :class:`Entity`, then the mapper will check the parent's attributes.
487
488                 Example::
489
490                         >>> attr = entity.attribute_set.get(key='spam')
491                         DoesNotExist: Attribute matching query does not exist.
492                         >>> attr = entity.parent.attribute_set.get(key='spam')
493                         >>> attr.value.value
494                         u'eggs'
495                         >>> entity.attributes['spam']
496                         u'eggs'
497                 
498                 """
499                 if mapper is None:
500                         if getattr(self, "%s_id" % self._mptt_meta.parent_attr):
501                                 mapper = TreeAttributeMapper
502                         else:
503                                 mapper = AttributeMapper
504                 return super(TreeEntity, self).get_attribute_mapper(mapper)
505         
506         def __unicode__(self):
507                 return self.path
508         
509         class Meta:
510                 abstract = True
511
512
513 class SlugTreeEntityManager(TreeEntityManager):
514         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
515                 return super(SlugTreeEntityManager, self).get_with_path(path, root, absolute_result, pathsep, field)
516
517
518 class SlugTreeEntity(TreeEntity):
519         objects = SlugTreeEntityManager()
520         slug = models.SlugField(max_length=255)
521         
522         def get_path(self, root=None, pathsep='/', field='slug', memoize=True):
523                 return super(SlugTreeEntity, self).get_path(root, pathsep, field, memoize)
524         path = property(get_path)
525         
526         def clean(self):
527                 if getattr(self, "%s_id" % self._mptt_meta.parent_attr) is None:
528                         try:
529                                 self._default_manager.exclude(pk=self.pk).get(slug=self.slug, parent__isnull=True)
530                         except self.DoesNotExist:
531                                 pass
532                         else:
533                                 raise ValidationError(self.unique_error_message(self.__class__, ('parent', 'slug')))
534         
535         class Meta:
536                 unique_together = ('parent', 'slug')
537                 abstract = True