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