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