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