Removed penfield's reliance on the Titled model and marked it as deprecated.
[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         """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`."""
250         entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
251         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
252         
253         #: :class:`GenericForeignKey` to anything (generally an instance of an Entity subclass).
254         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
255         
256         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)
257         value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
258         
259         #: :class:`GenericForeignKey` to an instance of a subclass of :class:`AttributeValue` as determined by the :data:`attribute_value_limiter`.
260         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
261         
262         #: :class:`CharField` containing a key (up to 255 characters) consisting of alphanumeric characters and underscores.
263         key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
264         
265         def __unicode__(self):
266                 return u'"%s": %s' % (self.key, self.value)
267         
268         def set_value(self, value, value_class=JSONValue):
269                 """Given a value and a value class, sets up self.value appropriately."""
270                 if isinstance(self.value, value_class):
271                         val = self.value
272                 else:
273                         if isinstance(self.value, models.Model):
274                                 self.value.delete()
275                         val = value_class()
276                 
277                 val.set_value(value)
278                 val.save()
279                 
280                 self.value = val
281                 self.save()
282         
283         class Meta:
284                 app_label = 'philo'
285                 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
286
287
288 class EntityOptions(object):
289         def __init__(self, options):
290                 if options is not None:
291                         for key, value in options.__dict__.items():
292                                 setattr(self, key, value)
293                 if not hasattr(self, 'proxy_fields'):
294                         self.proxy_fields = []
295         
296         def add_proxy_field(self, proxy_field):
297                 self.proxy_fields.append(proxy_field)
298
299
300 class EntityBase(models.base.ModelBase):
301         def __new__(cls, name, bases, attrs):
302                 entity_meta = attrs.pop('EntityMeta', None)
303                 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
304                 new.add_to_class('_entity_meta', EntityOptions(entity_meta))
305                 entity_class_prepared.send(sender=new)
306                 return new
307
308
309 class Entity(models.Model):
310         """An abstract class that simplifies access to related attributes. Most models provided by Philo subclass Entity."""
311         __metaclass__ = EntityBase
312         
313         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
314         
315         def get_attribute_mapper(self, mapper=AttributeMapper):
316                 """
317                 Returns an :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly.
318
319                 Example::
320
321                         >>> attr = entity.attribute_set.get(key='spam')
322                         >>> attr.value.value
323                         u'eggs'
324                         >>> entity.attributes['spam']
325                         u'eggs'
326                 
327                 """
328                 return mapper(self)
329         attributes = property(get_attribute_mapper)
330         
331         class Meta:
332                 abstract = True
333
334
335 class TreeManager(models.Manager):
336         use_for_related_fields = True
337         
338         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
339                 """
340                 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).
341                 
342                 .. 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.
343                 
344                 .. 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.
345                 
346                 :param path: The path of the object
347                 :param root: The object which will be considered the root of the search
348                 :param absolute_result: Whether to return an absolute result or do a binary search
349                 :param pathsep: The path separator used in ``path``
350                 :param field: The field on the model which should be queried for ``path`` segment matching.
351                 :returns: An instance if ``absolute_result`` is ``True`` or an (instance, remaining_path) tuple otherwise.
352                 :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters.
353                 
354                 """
355                 
356                 segments = path.split(pathsep)
357                 
358                 # Clean out blank segments. Handles multiple consecutive pathseps.
359                 while True:
360                         try:
361                                 segments.remove('')
362                         except ValueError:
363                                 break
364                 
365                 # Special-case a lack of segments. No queries necessary.
366                 if not segments:
367                         if root is not None:
368                                 if absolute_result:
369                                         return root
370                                 return root, None
371                         else:
372                                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
373                 
374                 def make_query_kwargs(segments, root):
375                         kwargs = {}
376                         prefix = ""
377                         revsegs = list(segments)
378                         revsegs.reverse()
379                         
380                         for segment in revsegs:
381                                 kwargs["%s%s__exact" % (prefix, field)] = segment
382                                 prefix += "parent__"
383                         
384                         if prefix:
385                                 kwargs[prefix[:-2]] = root
386                         
387                         return kwargs
388                 
389                 def find_obj(segments, depth, deepest_found=None):
390                         if deepest_found is None:
391                                 deepest_level = 0
392                         elif root is None:
393                                 deepest_level = deepest_found.get_level() + 1
394                         else:
395                                 deepest_level = deepest_found.get_level() - root.get_level()
396                         try:
397                                 obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found or root))
398                         except self.model.DoesNotExist:
399                                 if not deepest_level and depth > 1:
400                                         # make sure there's a root node...
401                                         depth = 1
402                                 else:
403                                         # Try finding one with half the path since the deepest find.
404                                         depth = (deepest_level + depth)/2
405                                 
406                                 if deepest_level == depth:
407                                         # This should happen if nothing is found with any part of the given path.
408                                         if root is not None and deepest_found is None:
409                                                 return root, pathsep.join(segments)
410                                         raise
411                                 
412                                 return find_obj(segments, depth, deepest_found)
413                         else:
414                                 # Yay! Found one!
415                                 if root is None:
416                                         deepest_level = obj.get_level() + 1
417                                 else:
418                                         deepest_level = obj.get_level() - root.get_level()
419                                 
420                                 # Could there be a deeper one?
421                                 if obj.is_leaf_node():
422                                         return obj, pathsep.join(segments[deepest_level:]) or None
423                                 
424                                 depth += (len(segments) - depth)/2 or len(segments) - depth
425                                 
426                                 if depth > deepest_level + obj.get_descendant_count():
427                                         depth = deepest_level + obj.get_descendant_count()
428                                 
429                                 if deepest_level == depth:
430                                         return obj, pathsep.join(segments[deepest_level:]) or None
431                                 
432                                 try:
433                                         return find_obj(segments, depth, obj)
434                                 except self.model.DoesNotExist:
435                                         # Then this was the deepest.
436                                         return obj, pathsep.join(segments[deepest_level:])
437                 
438                 if absolute_result:
439                         return self.get(**make_query_kwargs(segments, root))
440                 
441                 # Try a modified binary search algorithm. Feed the root in so that query complexity
442                 # can be reduced. It might be possible to weight the search towards the beginning
443                 # of the path, since short paths are more likely, but how far forward? It would
444                 # need to shift depending on len(segments) - perhaps logarithmically?
445                 return find_obj(segments, len(segments)/2 or len(segments))
446
447
448 class TreeModel(MPTTModel):
449         objects = TreeManager()
450         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
451         slug = models.SlugField(max_length=255)
452         
453         def get_path(self, root=None, pathsep='/', field='slug'):
454                 """
455                 :param root: Only return the path since this object.
456                 :param pathsep: The path separator to use when constructing an instance's path
457                 :param field: The field to pull path information from for each ancestor.
458                 :returns: A string representation of an object's path.
459                 
460                 """
461                 
462                 if root == self:
463                         return ''
464                 
465                 if root is not None and not self.is_descendant_of(root):
466                         raise AncestorDoesNotExist(root)
467                 
468                 qs = self.get_ancestors(include_self=True)
469                 
470                 if root is not None:
471                         qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
472                 
473                 return pathsep.join([getattr(parent, field, '?') for parent in qs])
474         path = property(get_path)
475         
476         def __unicode__(self):
477                 return self.path
478         
479         class Meta:
480                 unique_together = (('parent', 'slug'),)
481                 abstract = True
482
483
484 class TreeEntityBase(MPTTModelBase, EntityBase):
485         def __new__(meta, name, bases, attrs):
486                 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
487                 cls = EntityBase.__new__(meta, name, bases, attrs)
488                 
489                 return meta.register(cls)
490
491
492 class TreeEntity(Entity, TreeModel):
493         """An abstract subclass of Entity which represents a tree relationship."""
494         
495         __metaclass__ = TreeEntityBase
496         
497         def get_attribute_mapper(self, mapper=None):
498                 """
499                 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.
500
501                 Example::
502
503                         >>> attr = entity.attribute_set.get(key='spam')
504                         DoesNotExist: Attribute matching query does not exist.
505                         >>> attr = entity.parent.attribute_set.get(key='spam')
506                         >>> attr.value.value
507                         u'eggs'
508                         >>> entity.attributes['spam']
509                         u'eggs'
510                 
511                 """
512                 if mapper is None:
513                         if self.parent:
514                                 mapper = TreeAttributeMapper
515                         else:
516                                 mapper = AttributeMapper
517                 return super(TreeEntity, self).get_attribute_mapper(mapper)
518         attributes = property(get_attribute_mapper)
519         
520         class Meta:
521                 abstract = True