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