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