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