3482a1a1558a54d2b91625602fbcfd70bf0da1e3
[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.utils import ContentTypeRegistryLimiter
7 from UserDict import DictMixin
8
9
10 class Tag(models.Model):
11         name = models.CharField(max_length=250)
12         slug = models.SlugField()
13         
14         def __unicode__(self):
15                 return self.name
16
17
18 class Titled(models.Model):
19         title = models.CharField(max_length=255)
20         slug = models.SlugField()
21         
22         def __unicode__(self):
23                 return self.title
24         
25         class Meta:
26                 abstract = True
27
28
29 class Attribute(models.Model):
30         entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
31         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
32         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
33         key = models.CharField(max_length=255)
34         json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
35         
36         def get_value(self):
37                 return json.loads(self.json_value)
38         
39         def set_value(self, value):
40                 self.json_value = json.dumps(value)
41         
42         def delete_value(self):
43                 self.json_value = json.dumps(None)
44         
45         value = property(get_value, set_value, delete_value)
46         
47         def __unicode__(self):
48                 return u'"%s": %s' % (self.key, self.value)
49         
50         class Meta:
51                 app_label = 'philo'
52
53
54 value_content_type_limiter = ContentTypeRegistryLimiter()
55
56
57 def register_value_model(model):
58         value_content_type_limiter.register_class(model)
59
60
61 def unregister_value_model(model):
62         value_content_type_limiter.unregister_class(model)
63
64
65
66 class Relationship(models.Model):
67         entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
68         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
69         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
70         key = models.CharField(max_length=255)
71         value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type')
72         value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
73         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
74         
75         def __unicode__(self):
76                 return u'"%s": %s' % (self.key, self.value)
77         
78         class Meta:
79                 app_label = 'philo'
80
81
82 class QuerySetMapper(object, DictMixin):
83         def __init__(self, queryset, passthrough=None):
84                 self.queryset = queryset
85                 self.passthrough = passthrough
86         
87         def __getitem__(self, key):
88                 try:
89                         return self.queryset.get(key__exact=key).value
90                 except ObjectDoesNotExist:
91                         if self.passthrough is not None:
92                                 return self.passthrough.__getitem__(key)
93                         raise KeyError
94         
95         def keys(self):
96                 keys = set(self.queryset.values_list('key', flat=True).distinct())
97                 if self.passthrough is not None:
98                         keys |= set(self.passthrough.keys())
99                 return list(keys)
100
101
102 class Entity(models.Model):
103         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
104         relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
105         
106         @property
107         def attributes(self):
108                 return QuerySetMapper(self.attribute_set)
109         
110         @property
111         def relationships(self):
112                 return QuerySetMapper(self.relationship_set)
113         
114         class Meta:
115                 abstract = True
116                 app_label = 'philo'
117
118
119 class TreeManager(models.Manager):
120         use_for_related_fields = True
121         
122         def roots(self):
123                 return self.filter(parent__isnull=True)
124         
125         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
126                 """
127                 Returns the object with the path, or None if there is no object with that path,
128                 unless absolute_result is set to False, in which case it returns a tuple containing
129                 the deepest object found along the path, and the remainder of the path after that
130                 object as a string (or None in the case that there is no remaining path).
131                 """
132                 slugs = path.split(pathsep)
133                 obj = root
134                 remaining_slugs = list(slugs)
135                 remainder = None
136                 for slug in slugs:
137                         remaining_slugs.remove(slug)
138                         if slug: # ignore blank slugs, handles for multiple consecutive pathseps
139                                 try:
140                                         obj = self.get(slug__exact=slug, parent__exact=obj)
141                                 except self.model.DoesNotExist:
142                                         if absolute_result:
143                                                 obj = None
144                                         remaining_slugs.insert(0, slug)
145                                         remainder = pathsep.join(remaining_slugs)
146                                         break
147                 if obj:
148                         if absolute_result:
149                                 return obj
150                         else:
151                                 return (obj, remainder)
152                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
153
154
155 class TreeModel(models.Model):
156         objects = TreeManager()
157         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
158         slug = models.SlugField()
159         
160         def get_path(self, pathsep='/', field='slug'):
161                 path = getattr(self, field, '?')
162                 parent = self.parent
163                 while parent:
164                         path = getattr(parent, field, '?') + pathsep + path
165                         parent = parent.parent
166                 return path
167         path = property(get_path)
168         
169         def __unicode__(self):
170                 return self.path
171         
172         class Meta:
173                 unique_together = (('parent', 'slug'),)
174                 abstract = True
175                 app_label = 'philo'
176
177
178 class TreeEntity(TreeModel, Entity):
179         @property
180         def attributes(self):
181                 if self.parent:
182                         return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
183                 return super(TreeEntity, self).attributes
184         
185         @property
186         def relationships(self):
187                 if self.parent:
188                         return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
189                 return super(TreeEntity, self).relationships
190         
191         class Meta:
192                 abstract = True
193                 app_label = 'philo'