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