c2c12ad00a9f5ccf510f92caab70f12f30285e61
[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                 Given a <pathsep>-separated path, fetch the matching model of this type. If the path
286                 you're searching for is known to exist, it is always faster to use absolute_result=True.
287                 Unless the path depth is over ~40, in which case the high cost of the absolute query
288                 makes a binary search (i.e. non-absolute) faster.
289                 """
290                 # Note: SQLite allows max of 64 tables in one join. That means the binary search will
291                 # only work on paths with a max depth of 127 and the absolute fetch will only work
292                 # to a max depth of (surprise!) 63. Although this could be handled, chances are your
293                 # tree structure won't be that deep.
294                 segments = path.split(pathsep)
295                 
296                 # Check for a trailing pathsep so we can restore it later.
297                 trailing_pathsep = False
298                 if segments[-1] == '':
299                         trailing_pathsep = True
300                 
301                 # Clean out blank segments. Handles multiple consecutive pathseps.
302                 while True:
303                         try:
304                                 segments.remove('')
305                         except ValueError:
306                                 break
307                 
308                 # Special-case a lack of segments. No queries necessary.
309                 if not segments:
310                         if root is not None:
311                                 if absolute_result:
312                                         return root
313                                 return root, None
314                         else:
315                                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
316                 
317                 def make_query_kwargs(segments, root):
318                         kwargs = {}
319                         prefix = ""
320                         revsegs = list(segments)
321                         revsegs.reverse()
322                         
323                         for segment in revsegs:
324                                 kwargs["%s%s__exact" % (prefix, field)] = segment
325                                 prefix += "parent__"
326                         
327                         if not prefix and root is not None:
328                                 prefix = "parent__"
329                         
330                         if prefix:
331                                 kwargs[prefix[:-2]] = root
332                         
333                         return kwargs
334                 
335                 def build_path(segments):
336                         path = pathsep.join(segments)
337                         if trailing_pathsep and segments and segments[-1] != '':
338                                 path += pathsep
339                         return path
340                 
341                 def find_obj(segments, depth, deepest_found):
342                         if deepest_found is None:
343                                 deepest_level = 0
344                         else:
345                                 deepest_level = deepest_found.get_level() + 1
346                         try:
347                                 obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found))
348                         except self.model.DoesNotExist:
349                                 if not deepest_level and depth > 1:
350                                         # make sure there's a root node...
351                                         depth = 1
352                                 else:
353                                         # Try finding one with half the path since the deepest find.
354                                         depth = (deepest_level + depth)/2
355                                 
356                                 if deepest_level == depth:
357                                         # This should happen if nothing is found with any part of the given path.
358                                         raise
359                                 
360                                 return find_obj(segments, depth, deepest_found)
361                         else:
362                                 # Yay! Found one!
363                                 deepest_level = obj.get_level() + 1
364                                 
365                                 # Could there be a deeper one?
366                                 if obj.is_leaf_node():
367                                         return obj, build_path(segments[deepest_level:]) or None
368                                 
369                                 depth += (len(segments) - depth)/2 or len(segments) - depth
370                                 
371                                 if depth > deepest_level + obj.get_descendant_count():
372                                         depth = deepest_level + obj.get_descendant_count()
373                                 
374                                 if deepest_level == depth:
375                                         return obj, build_path(segments[deepest_level:]) or None
376                                 
377                                 try:
378                                         return find_obj(segments, depth, obj)
379                                 except self.model.DoesNotExist:
380                                         # Then this was the deepest.
381                                         return obj, build_path(segments[deepest_level:])
382                 
383                 if absolute_result:
384                         return self.get(**make_query_kwargs(segments, root))
385                 
386                 # Try a modified binary search algorithm. Feed the root in so that query complexity
387                 # can be reduced. It might be possible to weight the search towards the beginning
388                 # of the path, since short paths are more likely, but how far forward? It would
389                 # need to shift depending on len(segments) - perhaps logarithmically?
390                 return find_obj(segments, len(segments)/2 or len(segments), root)
391
392
393 class TreeModel(MPTTModel):
394         objects = TreeManager()
395         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
396         slug = models.SlugField(max_length=255)
397         
398         def get_path(self, root=None, pathsep='/', field='slug'):
399                 if root is not None and not self.is_descendant_of(root):
400                         raise AncestorDoesNotExist(root)
401                 
402                 return pathsep.join([getattr(parent, field, '?') for parent in list(self.get_ancestors()) + [self]])
403         path = property(get_path)
404         
405         def __unicode__(self):
406                 return self.path
407         
408         class Meta:
409                 unique_together = (('parent', 'slug'),)
410                 abstract = True
411
412
413 class TreeEntityBase(MPTTModelBase, EntityBase):
414         def __new__(meta, name, bases, attrs):
415                 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
416                 cls = EntityBase.__new__(meta, name, bases, attrs)
417                 
418                 return meta.register(cls)
419
420
421 class TreeEntity(Entity, TreeModel):
422         __metaclass__ = TreeEntityBase
423         
424         @property
425         def attributes(self):
426                 if self.parent:
427                         return QuerySetMapper(self.attribute_set.all(), passthrough=self.parent.attributes)
428                 return super(TreeEntity, self).attributes
429         
430         class Meta:
431                 abstract = True