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