Improved blog and newsletter ModelAdmins. Set BlogEntries and NewsletterArticlesup...
[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 apply_data(self, data):
60                 raise NotImplementedError
61         
62         def value_formfield(self, **kwargs):
63                 raise NotImplementedError
64         
65         def __unicode__(self):
66                 return unicode(self.value)
67         
68         class Meta:
69                 abstract = True
70
71
72 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
73
74
75 class JSONValue(AttributeValue):
76         value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
77         
78         def __unicode__(self):
79                 return smart_str(self.value)
80         
81         def value_formfield(self, **kwargs):
82                 kwargs['initial'] = self.value_json
83                 return self._meta.get_field('value').formfield(**kwargs)
84         
85         def apply_data(self, cleaned_data):
86                 self.value = cleaned_data.get('value', None)
87         
88         class Meta:
89                 app_label = 'philo'
90
91
92 class ForeignKeyValue(AttributeValue):
93         content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
94         object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
95         value = generic.GenericForeignKey()
96         
97         def value_formfield(self, form_class=forms.ModelChoiceField, **kwargs):
98                 if self.content_type is None:
99                         return None
100                 kwargs.update({'initial': self.object_id, 'required': False})
101                 return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
102         
103         def apply_data(self, cleaned_data):
104                 if 'value' in cleaned_data and cleaned_data['value'] is not None:
105                         self.value = cleaned_data['value']
106                 else:
107                         self.content_type = cleaned_data.get('content_type', None)
108                         # If there is no value set in the cleaned data, clear the stored value.
109                         self.object_id = None
110         
111         class Meta:
112                 app_label = 'philo'
113
114
115 class ManyToManyValue(AttributeValue):
116         content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
117         values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
118         
119         def get_object_id_list(self):
120                 if not self.values.count():
121                         return []
122                 else:
123                         return self.values.values_list('object_id', flat=True)
124         
125         def get_value(self):
126                 if self.content_type is None:
127                         return None
128                 
129                 return self.content_type.model_class()._default_manager.filter(id__in=self.get_object_id_list())
130         
131         def set_value(self, value):
132                 # Value is probably a queryset - but allow any iterable.
133                 
134                 # These lines shouldn't be necessary; however, if value is an EmptyQuerySet,
135                 # the code (specifically the object_id__in query) won't work without them. Unclear why...
136                 if not value:
137                         value = []
138                 
139                 # Before we can fiddle with the many-to-many to foreignkeyvalues, we need
140                 # a pk.
141                 if self.pk is None:
142                         self.save()
143                 
144                 if isinstance(value, models.query.QuerySet):
145                         value = value.values_list('id', flat=True)
146                 
147                 self.values.filter(~models.Q(object_id__in=value)).delete()
148                 current = self.get_object_id_list()
149                 
150                 for v in value:
151                         if v in current:
152                                 continue
153                         self.values.create(content_type=self.content_type, object_id=v)
154         
155         value = property(get_value, set_value)
156         
157         def value_formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
158                 if self.content_type is None:
159                         return None
160                 kwargs.update({'initial': self.get_object_id_list(), 'required': False})
161                 return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
162         
163         def apply_data(self, cleaned_data):
164                 if 'value' in cleaned_data and cleaned_data['value'] is not None:
165                         self.value = cleaned_data['value']
166                 else:
167                         self.content_type = cleaned_data.get('content_type', None)
168                         # If there is no value set in the cleaned data, clear the stored value.
169                         self.value = []
170         
171         class Meta:
172                 app_label = 'philo'
173
174
175 class Attribute(models.Model):
176         entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
177         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
178         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
179         
180         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)
181         value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
182         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
183         
184         key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.")
185         
186         def __unicode__(self):
187                 return u'"%s": %s' % (self.key, self.value)
188         
189         class Meta:
190                 app_label = 'philo'
191                 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
192
193
194 class QuerySetMapper(object, DictMixin):
195         def __init__(self, queryset, passthrough=None):
196                 self.queryset = queryset
197                 self.passthrough = passthrough
198         
199         def __getitem__(self, key):
200                 try:
201                         value = self.queryset.get(key__exact=key).value
202                 except ObjectDoesNotExist:
203                         if self.passthrough is not None:
204                                 return self.passthrough.__getitem__(key)
205                         raise KeyError
206                 else:
207                         if value is not None:
208                                 return value.value
209                         return value
210         
211         def keys(self):
212                 keys = set(self.queryset.values_list('key', flat=True).distinct())
213                 if self.passthrough is not None:
214                         keys |= set(self.passthrough.keys())
215                 return list(keys)
216
217
218 class EntityOptions(object):
219         def __init__(self, options):
220                 if options is not None:
221                         for key, value in options.__dict__.items():
222                                 setattr(self, key, value)
223                 if not hasattr(self, 'proxy_fields'):
224                         self.proxy_fields = []
225         
226         def add_proxy_field(self, proxy_field):
227                 self.proxy_fields.append(proxy_field)
228
229
230 class EntityBase(models.base.ModelBase):
231         def __new__(cls, name, bases, attrs):
232                 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
233                 entity_options = attrs.pop('EntityMeta', None)
234                 setattr(new, '_entity_meta', EntityOptions(entity_options))
235                 entity_class_prepared.send(sender=new)
236                 return new
237
238
239 class Entity(models.Model):
240         __metaclass__ = EntityBase
241         
242         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
243         
244         @property
245         def attributes(self):
246                 return QuerySetMapper(self.attribute_set.all())
247         
248         @property
249         def _added_attribute_registry(self):
250                 if not hasattr(self, '_real_added_attribute_registry'):
251                         self._real_added_attribute_registry = {}
252                 return self._real_added_attribute_registry
253         
254         @property
255         def _removed_attribute_registry(self):
256                 if not hasattr(self, '_real_removed_attribute_registry'):
257                         self._real_removed_attribute_registry = []
258                 return self._real_removed_attribute_registry
259         
260         def save(self, *args, **kwargs):
261                 super(Entity, self).save(*args, **kwargs)
262                 
263                 for key in self._removed_attribute_registry:
264                         self.attribute_set.filter(key__exact=key).delete()
265                 del self._removed_attribute_registry[:]
266                 
267                 for field, value in self._added_attribute_registry.items():
268                         try:
269                                 attribute = self.attribute_set.get(key__exact=field.key)
270                         except Attribute.DoesNotExist:
271                                 attribute = Attribute()
272                                 attribute.entity = self
273                                 attribute.key = field.key
274                         
275                         field.set_attribute_value(attribute, value)
276                         attribute.save()
277                 self._added_attribute_registry.clear()
278         
279         class Meta:
280                 abstract = True
281
282
283 class TreeManager(models.Manager):
284         use_for_related_fields = True
285         
286         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
287                 """
288                 Returns the object with the path, unless absolute_result is set to False, in which
289                 case it returns a tuple containing the deepest object found along the path, and the
290                 remainder of the path after that object as a string (or None if there is no remaining
291                 path). Raises a DoesNotExist exception if no object is found with the given path.
292                 
293                 If the path you're searching for is known to exist, it is always faster to use
294                 absolute_result=True - unless the path depth is over ~40, in which case the high cost
295                 of the absolute query makes a binary search (i.e. non-absolute) faster.
296                 """
297                 # Note: SQLite allows max of 64 tables in one join. That means the binary search will
298                 # only work on paths with a max depth of 127 and the absolute fetch will only work
299                 # to a max depth of (surprise!) 63. Although this could be handled, chances are your
300                 # tree structure won't be that deep.
301                 segments = path.split(pathsep)
302                 
303                 # Check for a trailing pathsep so we can restore it later.
304                 trailing_pathsep = False
305                 if segments[-1] == '':
306                         trailing_pathsep = True
307                 
308                 # Clean out blank segments. Handles multiple consecutive pathseps.
309                 while True:
310                         try:
311                                 segments.remove('')
312                         except ValueError:
313                                 break
314                 
315                 # Special-case a lack of segments. No queries necessary.
316                 if not segments:
317                         if root is not None:
318                                 if absolute_result:
319                                         return root
320                                 return root, None
321                         else:
322                                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
323                 
324                 def make_query_kwargs(segments, root):
325                         kwargs = {}
326                         prefix = ""
327                         revsegs = list(segments)
328                         revsegs.reverse()
329                         
330                         for segment in revsegs:
331                                 kwargs["%s%s__exact" % (prefix, field)] = segment
332                                 prefix += "parent__"
333                         
334                         if prefix:
335                                 kwargs[prefix[:-2]] = root
336                         
337                         return kwargs
338                 
339                 def build_path(segments):
340                         path = pathsep.join(segments)
341                         if trailing_pathsep and segments and segments[-1] != '':
342                                 path += pathsep
343                         return path
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, build_path(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, build_path(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, build_path(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, build_path(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()
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 list(qs) + [self]])
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