Minor correction to embed template tag: updating the context adds an additional conte...
[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                                         if root is not None:
362                                                 return root, build_path(segments)
363                                         raise
364                                 
365                                 return find_obj(segments, depth, deepest_found)
366                         else:
367                                 # Yay! Found one!
368                                 if root is None:
369                                         deepest_level = obj.get_level() + 1
370                                 else:
371                                         deepest_level = obj.get_level() - root.get_level()
372                                 
373                                 # Could there be a deeper one?
374                                 if obj.is_leaf_node():
375                                         return obj, build_path(segments[deepest_level:]) or None
376                                 
377                                 depth += (len(segments) - depth)/2 or len(segments) - depth
378                                 
379                                 if depth > deepest_level + obj.get_descendant_count():
380                                         depth = deepest_level + obj.get_descendant_count()
381                                 
382                                 if deepest_level == depth:
383                                         return obj, build_path(segments[deepest_level:]) or None
384                                 
385                                 try:
386                                         return find_obj(segments, depth, obj)
387                                 except self.model.DoesNotExist:
388                                         # Then this was the deepest.
389                                         return obj, build_path(segments[deepest_level:])
390                 
391                 if absolute_result:
392                         return self.get(**make_query_kwargs(segments, root))
393                 
394                 # Try a modified binary search algorithm. Feed the root in so that query complexity
395                 # can be reduced. It might be possible to weight the search towards the beginning
396                 # of the path, since short paths are more likely, but how far forward? It would
397                 # need to shift depending on len(segments) - perhaps logarithmically?
398                 return find_obj(segments, len(segments)/2 or len(segments))
399
400
401 class TreeModel(MPTTModel):
402         objects = TreeManager()
403         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
404         slug = models.SlugField(max_length=255)
405         
406         def get_path(self, root=None, pathsep='/', field='slug'):
407                 if root == self:
408                         return ''
409                 
410                 if root is not None and not self.is_descendant_of(root):
411                         raise AncestorDoesNotExist(root)
412                 
413                 qs = self.get_ancestors()
414                 
415                 if root is not None:
416                         qs = qs.filter(level__gt=root.level)
417                 
418                 return pathsep.join([getattr(parent, field, '?') for parent in list(qs) + [self]])
419         path = property(get_path)
420         
421         def __unicode__(self):
422                 return self.path
423         
424         class Meta:
425                 unique_together = (('parent', 'slug'),)
426                 abstract = True
427
428
429 class TreeEntityBase(MPTTModelBase, EntityBase):
430         def __new__(meta, name, bases, attrs):
431                 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
432                 cls = EntityBase.__new__(meta, name, bases, attrs)
433                 
434                 return meta.register(cls)
435
436
437 class TreeEntity(Entity, TreeModel):
438         __metaclass__ = TreeEntityBase
439         
440         @property
441         def attributes(self):
442                 if self.parent:
443                         return QuerySetMapper(self.attribute_set.all(), passthrough=self.parent.attributes)
444                 return super(TreeEntity, self).attributes
445         
446         class Meta:
447                 abstract = True