Merge branch 'master' into flexible_attributes
[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
14
15 class Tag(models.Model):
16         name = models.CharField(max_length=255)
17         slug = models.SlugField(max_length=255, unique=True)
18         
19         def __unicode__(self):
20                 return self.name
21         
22         class Meta:
23                 app_label = 'philo'
24
25
26 class Titled(models.Model):
27         title = models.CharField(max_length=255)
28         slug = models.SlugField(max_length=255)
29         
30         def __unicode__(self):
31                 return self.title
32         
33         class Meta:
34                 abstract = True
35
36
37 value_content_type_limiter = ContentTypeRegistryLimiter()
38
39
40 def register_value_model(model):
41         value_content_type_limiter.register_class(model)
42
43
44 def unregister_value_model(model):
45         value_content_type_limiter.unregister_class(model)
46
47
48 class AttributeValue(models.Model):
49         def apply_data(self, data):
50                 raise NotImplementedError
51         
52         def value_formfield(self, **kwargs):
53                 raise NotImplementedError
54         
55         def __unicode__(self):
56                 return unicode(self.value)
57         
58         class Meta:
59                 abstract = True
60
61
62 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
63
64
65 class JSONValue(AttributeValue):
66         value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
67         
68         def __unicode__(self):
69                 return self.value_json
70         
71         def value_formfield(self, **kwargs):
72                 kwargs['initial'] = self.value_json
73                 return self._meta.get_field('value').formfield(**kwargs)
74         
75         def apply_data(self, cleaned_data):
76                 self.value = cleaned_data.get('value', None)
77         
78         class Meta:
79                 app_label = 'philo'
80
81
82 class ForeignKeyValue(AttributeValue):
83         content_type = models.ForeignKey(ContentType, related_name='foreign_key_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
84         object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
85         value = generic.GenericForeignKey()
86         
87         def value_formfield(self, form_class=forms.ModelChoiceField, **kwargs):
88                 if self.content_type is None:
89                         return None
90                 kwargs.update({'initial': self.object_id, 'required': False})
91                 return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
92         
93         def apply_data(self, cleaned_data):
94                 if 'value' in cleaned_data and cleaned_data['value'] is not None:
95                         self.value = cleaned_data['value']
96                 else:
97                         self.content_type = cleaned_data.get('content_type', None)
98                         # If there is no value set in the cleaned data, clear the stored value.
99                         self.object_id = None
100         
101         class Meta:
102                 app_label = 'philo'
103
104
105 class ManyToManyValue(AttributeValue):
106         content_type = models.ForeignKey(ContentType, related_name='many_to_many_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
107         object_ids = models.CommaSeparatedIntegerField(max_length=300, verbose_name='Value IDs', null=True, blank=True)
108         
109         def get_object_id_list(self):
110                 if not self.object_ids:
111                         return []
112                 else:
113                         return self.object_ids.split(',')
114         
115         def get_value(self):
116                 if self.content_type is None:
117                         return None
118                 
119                 return self.content_type.model_class()._default_manager.filter(id__in=self.get_object_id_list())
120         
121         def set_value(self, value):
122                 if value is None:
123                         self.object_ids = ""
124                         return
125                 if not isinstance(value, models.query.QuerySet):
126                         raise TypeError("Value must be a QuerySet.")
127                 self.content_type = ContentType.objects.get_for_model(value.model)
128                 self.object_ids = ','.join([`value` for value in value.values_list('id', flat=True)])
129         
130         value = property(get_value, set_value)
131         
132         def value_formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
133                 if self.content_type is None:
134                         return None
135                 kwargs.update({'initial': self.get_object_id_list(), 'required': False})
136                 return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
137         
138         def apply_data(self, cleaned_data):
139                 self.value = cleaned_data.get('value', None)
140         
141         class Meta:
142                 app_label = 'philo'
143
144
145 class Attribute(models.Model):
146         entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
147         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
148         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
149         
150         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)
151         value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
152         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
153         
154         key = models.CharField(max_length=255)
155         
156         def get_value_class(self, value):
157                 if isinstance(value, models.query.QuerySet):
158                         return ManyToManyValue
159                 elif isinstance(value, models.Model) or (value is None and self.value_content_type.model_class() is ForeignKeyValue):
160                         return ForeignKeyValue
161                 else:
162                         return JSONValue
163         
164         def set_value(self, value):
165                 # is this useful? The best way of doing it?
166                 value_class = self.get_value_class(value)
167                 
168                 if self.value is None or value_class != self.value_content_type.model_class():
169                         if self.value is not None:
170                                 self.value.delete()
171                         new_value = value_class()
172                         new_value.value = value
173                         new_value.save()
174                         self.value = new_value
175                 else:
176                         self.value.value = value
177                         self.value.save()
178         
179         def __unicode__(self):
180                 return u'"%s": %s' % (self.key, self.value)
181         
182         class Meta:
183                 app_label = 'philo'
184                 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
185
186
187 class QuerySetMapper(object, DictMixin):
188         def __init__(self, queryset, passthrough=None):
189                 self.queryset = queryset
190                 self.passthrough = passthrough
191         
192         def __getitem__(self, key):
193                 try:
194                         return self.queryset.get(key__exact=key).value
195                 except ObjectDoesNotExist:
196                         if self.passthrough is not None:
197                                 return self.passthrough.__getitem__(key)
198                         raise KeyError
199         
200         def keys(self):
201                 keys = set(self.queryset.values_list('key', flat=True).distinct())
202                 if self.passthrough is not None:
203                         keys |= set(self.passthrough.keys())
204                 return list(keys)
205
206
207 class EntityOptions(object):
208         def __init__(self, options):
209                 if options is not None:
210                         for key, value in options.__dict__.items():
211                                 setattr(self, key, value)
212                 if not hasattr(self, 'proxy_fields'):
213                         self.proxy_fields = []
214         
215         def add_proxy_field(self, proxy_field):
216                 self.proxy_fields.append(proxy_field)
217
218
219 class EntityBase(models.base.ModelBase):
220         def __new__(cls, name, bases, attrs):
221                 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
222                 entity_options = attrs.pop('EntityMeta', None)
223                 setattr(new, '_entity_meta', EntityOptions(entity_options))
224                 entity_class_prepared.send(sender=new)
225                 return new
226
227
228 class Entity(models.Model):
229         __metaclass__ = EntityBase
230         
231         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
232         
233         @property
234         def attributes(self):
235                 return QuerySetMapper(self.attribute_set)
236         
237         @property
238         def _added_attribute_registry(self):
239                 if not hasattr(self, '_real_added_attribute_registry'):
240                         self._real_added_attribute_registry = {}
241                 return self._real_added_attribute_registry
242         
243         @property
244         def _removed_attribute_registry(self):
245                 if not hasattr(self, '_real_removed_attribute_registry'):
246                         self._real_removed_attribute_registry = []
247                 return self._real_removed_attribute_registry
248         
249         def save(self, *args, **kwargs):
250                 super(Entity, self).save(*args, **kwargs)
251                 
252                 for key in self._removed_attribute_registry:
253                         self.attribute_set.filter(key__exact=key).delete()
254                 del self._removed_attribute_registry[:]
255                 
256                 for key, value in self._added_attribute_registry.items():
257                         try:
258                                 attribute = self.attribute_set.get(key__exact=key)
259                         except Attribute.DoesNotExist:
260                                 attribute = Attribute()
261                                 attribute.entity = self
262                                 attribute.key = key
263                         attribute.set_value(value)
264                         attribute.save()
265                 self._added_attribute_registry.clear()
266         
267         class Meta:
268                 abstract = True
269
270
271 class TreeManager(models.Manager):
272         use_for_related_fields = True
273         
274         def roots(self):
275                 return self.filter(parent__isnull=True)
276         
277         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
278                 """
279                 Returns the object with the path, or None if there is no object with that path,
280                 unless absolute_result is set to False, in which case it returns a tuple containing
281                 the deepest object found along the path, and the remainder of the path after that
282                 object as a string (or None in the case that there is no remaining path).
283                 """
284                 slugs = path.split(pathsep)
285                 obj = root
286                 remaining_slugs = list(slugs)
287                 remainder = None
288                 for slug in slugs:
289                         remaining_slugs.remove(slug)
290                         if slug: # ignore blank slugs, handles for multiple consecutive pathseps
291                                 try:
292                                         obj = self.get(slug__exact=slug, parent__exact=obj)
293                                 except self.model.DoesNotExist:
294                                         if absolute_result:
295                                                 obj = None
296                                         remaining_slugs.insert(0, slug)
297                                         remainder = pathsep.join(remaining_slugs)
298                                         break
299                 if obj:
300                         if absolute_result:
301                                 return obj
302                         else:
303                                 return (obj, remainder)
304                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
305
306
307 class TreeModel(models.Model):
308         objects = TreeManager()
309         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
310         slug = models.SlugField(max_length=255)
311         
312         def has_ancestor(self, ancestor):
313                 parent = self
314                 while parent:
315                         if parent == ancestor:
316                                 return True
317                         parent = parent.parent
318                 return False
319         
320         def get_path(self, root=None, pathsep='/', field='slug'):
321                 if root is not None:
322                         if not self.has_ancestor(root):
323                                 raise AncestorDoesNotExist(root)
324                         path = ''
325                         parent = self
326                         while parent and parent != root:
327                                 path = getattr(parent, field, '?') + pathsep + path
328                                 parent = parent.parent
329                         return path
330                 else:
331                         path = getattr(self, field, '?')
332                         parent = self.parent
333                         while parent and parent != root:
334                                 path = getattr(parent, field, '?') + pathsep + path
335                                 parent = parent.parent
336                         return path
337         path = property(get_path)
338         
339         def __unicode__(self):
340                 return self.path
341         
342         class Meta:
343                 unique_together = (('parent', 'slug'),)
344                 abstract = True
345
346
347 class TreeEntity(Entity, TreeModel):
348         @property
349         def attributes(self):
350                 if self.parent:
351                         return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
352                 return super(TreeEntity, self).attributes
353         
354         @property
355         def relationships(self):
356                 if self.parent:
357                         return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
358                 return super(TreeEntity, self).relationships
359         
360         class Meta:
361                 abstract = True