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