Merge branch 'attribute_access' into release
[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 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).
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 an (instance, remaining_path) tuple otherwise.
347                 :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters.
348                 
349                 """
350                 
351                 segments = path.split(pathsep)
352                 
353                 # Clean out blank segments. Handles multiple consecutive pathseps.
354                 while True:
355                         try:
356                                 segments.remove('')
357                         except ValueError:
358                                 break
359                 
360                 # Special-case a lack of segments. No queries necessary.
361                 if not segments:
362                         if root is not None:
363                                 if absolute_result:
364                                         return root
365                                 return root, None
366                         else:
367                                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
368                 
369                 def make_query_kwargs(segments, root):
370                         kwargs = {}
371                         prefix = ""
372                         revsegs = list(segments)
373                         revsegs.reverse()
374                         
375                         for segment in revsegs:
376                                 kwargs["%s%s__exact" % (prefix, field)] = segment
377                                 prefix += "parent__"
378                         
379                         if prefix:
380                                 kwargs[prefix[:-2]] = root
381                         
382                         return kwargs
383                 
384                 def find_obj(segments, depth, deepest_found=None):
385                         if deepest_found is None:
386                                 deepest_level = 0
387                         elif root is None:
388                                 deepest_level = deepest_found.get_level() + 1
389                         else:
390                                 deepest_level = deepest_found.get_level() - root.get_level()
391                         try:
392                                 obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found or root))
393                         except self.model.DoesNotExist:
394                                 if not deepest_level and depth > 1:
395                                         # make sure there's a root node...
396                                         depth = 1
397                                 else:
398                                         # Try finding one with half the path since the deepest find.
399                                         depth = (deepest_level + depth)/2
400                                 
401                                 if deepest_level == depth:
402                                         # This should happen if nothing is found with any part of the given path.
403                                         if root is not None and deepest_found is None:
404                                                 return root, pathsep.join(segments)
405                                         raise
406                                 
407                                 return find_obj(segments, depth, deepest_found)
408                         else:
409                                 # Yay! Found one!
410                                 if root is None:
411                                         deepest_level = obj.get_level() + 1
412                                 else:
413                                         deepest_level = obj.get_level() - root.get_level()
414                                 
415                                 # Could there be a deeper one?
416                                 if obj.is_leaf_node():
417                                         return obj, pathsep.join(segments[deepest_level:]) or None
418                                 
419                                 depth += (len(segments) - depth)/2 or len(segments) - depth
420                                 
421                                 if depth > deepest_level + obj.get_descendant_count():
422                                         depth = deepest_level + obj.get_descendant_count()
423                                 
424                                 if deepest_level == depth:
425                                         return obj, pathsep.join(segments[deepest_level:]) or None
426                                 
427                                 try:
428                                         return find_obj(segments, depth, obj)
429                                 except self.model.DoesNotExist:
430                                         # Then this was the deepest.
431                                         return obj, pathsep.join(segments[deepest_level:])
432                 
433                 if absolute_result:
434                         return self.get(**make_query_kwargs(segments, root))
435                 
436                 # Try a modified binary search algorithm. Feed the root in so that query complexity
437                 # can be reduced. It might be possible to weight the search towards the beginning
438                 # of the path, since short paths are more likely, but how far forward? It would
439                 # need to shift depending on len(segments) - perhaps logarithmically?
440                 return find_obj(segments, len(segments)/2 or len(segments))
441
442
443 class TreeModel(MPTTModel):
444         objects = TreeManager()
445         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
446         slug = models.SlugField(max_length=255)
447         
448         def get_path(self, root=None, pathsep='/', field='slug'):
449                 """
450                 :param root: Only return the path since this object.
451                 :param pathsep: The path separator to use when constructing an instance's path
452                 :param field: The field to pull path information from for each ancestor.
453                 :returns: A string representation of an object's path.
454                 
455                 """
456                 
457                 if root == self:
458                         return ''
459                 
460                 if root is not None and not self.is_descendant_of(root):
461                         raise AncestorDoesNotExist(root)
462                 
463                 qs = self.get_ancestors(include_self=True)
464                 
465                 if root is not None:
466                         qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
467                 
468                 return pathsep.join([getattr(parent, field, '?') for parent in qs])
469         path = property(get_path)
470         
471         def __unicode__(self):
472                 return self.path
473         
474         class Meta:
475                 unique_together = (('parent', 'slug'),)
476                 abstract = True
477
478
479 class TreeEntityBase(MPTTModelBase, EntityBase):
480         def __new__(meta, name, bases, attrs):
481                 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
482                 cls = EntityBase.__new__(meta, name, bases, attrs)
483                 
484                 return meta.register(cls)
485
486
487 class TreeEntity(Entity, TreeModel):
488         """An abstract subclass of Entity which represents a tree relationship."""
489         
490         __metaclass__ = TreeEntityBase
491         
492         def get_attribute_mapper(self, mapper=None):
493                 """
494                 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.
495
496                 Example::
497
498                         >>> attr = entity.attribute_set.get(key='spam')
499                         DoesNotExist: Attribute matching query does not exist.
500                         >>> attr = entity.parent.attribute_set.get(key='spam')
501                         >>> attr.value.value
502                         u'eggs'
503                         >>> entity.attributes['spam']
504                         u'eggs'
505                 
506                 """
507                 if mapper is None:
508                         if self.parent:
509                                 mapper = TreeAttributeMapper
510                         else:
511                                 mapper = AttributeMapper
512                 return super(TreeEntity, self).get_attribute_mapper(mapper)
513         attributes = property(get_attribute_mapper)
514         
515         class Meta:
516                 abstract = True