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