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 philo.signals import entity_class_prepared
8 from UserDict import DictMixin
11 class Tag(models.Model):
12 name = models.CharField(max_length=250)
13 slug = models.SlugField(unique=True)
15 def __unicode__(self):
22 class Titled(models.Model):
23 title = models.CharField(max_length=255)
24 slug = models.SlugField(max_length=255)
26 def __unicode__(self):
33 class Attribute(models.Model):
34 entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
35 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
36 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
37 key = models.CharField(max_length=255)
38 json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
41 return json.loads(self.json_value)
43 def set_value(self, value):
44 self.json_value = json.dumps(value)
46 def delete_value(self):
47 self.json_value = json.dumps(None)
49 value = property(get_value, set_value, delete_value)
51 def __unicode__(self):
52 return u'"%s": %s' % (self.key, self.value)
58 value_content_type_limiter = ContentTypeRegistryLimiter()
61 def register_value_model(model):
62 value_content_type_limiter.register_class(model)
65 def unregister_value_model(model):
66 value_content_type_limiter.unregister_class(model)
70 class Relationship(models.Model):
71 entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
72 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
73 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
74 key = models.CharField(max_length=255)
75 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)
76 value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
77 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
79 def __unicode__(self):
80 return u'"%s": %s' % (self.key, self.value)
86 class QuerySetMapper(object, DictMixin):
87 def __init__(self, queryset, passthrough=None):
88 self.queryset = queryset
89 self.passthrough = passthrough
91 def __getitem__(self, key):
93 return self.queryset.get(key__exact=key).value
94 except ObjectDoesNotExist:
95 if self.passthrough is not None:
96 return self.passthrough.__getitem__(key)
100 keys = set(self.queryset.values_list('key', flat=True).distinct())
101 if self.passthrough is not None:
102 keys |= set(self.passthrough.keys())
106 class EntityOptions(object):
107 def __init__(self, options):
108 if options is not None:
109 for key, value in options.__dict__.items():
110 setattr(self, key, value)
111 if not hasattr(self, 'proxy_fields'):
112 self.proxy_fields = []
114 def add_proxy_field(self, proxy_field):
115 self.proxy_fields.append(proxy_field)
118 class EntityBase(models.base.ModelBase):
119 def __new__(cls, name, bases, attrs):
120 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
121 entity_options = attrs.pop('EntityMeta', None)
122 setattr(new, '_entity_meta', EntityOptions(entity_options))
123 entity_class_prepared.send(sender=new)
127 class Entity(models.Model):
128 __metaclass__ = EntityBase
130 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
131 relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
134 def attributes(self):
135 return QuerySetMapper(self.attribute_set)
138 def relationships(self):
139 return QuerySetMapper(self.relationship_set)
142 def _added_attribute_registry(self):
143 if not hasattr(self, '_real_added_attribute_registry'):
144 self._real_added_attribute_registry = {}
145 return self._real_added_attribute_registry
148 def _removed_attribute_registry(self):
149 if not hasattr(self, '_real_removed_attribute_registry'):
150 self._real_removed_attribute_registry = []
151 return self._real_removed_attribute_registry
154 def _added_relationship_registry(self):
155 if not hasattr(self, '_real_added_relationship_registry'):
156 self._real_added_relationship_registry = {}
157 return self._real_added_relationship_registry
160 def _removed_relationship_registry(self):
161 if not hasattr(self, '_real_removed_relationship_registry'):
162 self._real_removed_relationship_registry = []
163 return self._real_removed_relationship_registry
165 def save(self, *args, **kwargs):
166 super(Entity, self).save(*args, **kwargs)
168 for key in self._removed_attribute_registry:
169 self.attribute_set.filter(key__exact=key).delete()
170 del self._removed_attribute_registry[:]
172 for key, value in self._added_attribute_registry.items():
174 attribute = self.attribute_set.get(key__exact=key)
175 except Attribute.DoesNotExist:
176 attribute = Attribute()
177 attribute.entity = self
179 attribute.value = value
181 self._added_attribute_registry.clear()
183 for key in self._removed_relationship_registry:
184 self.relationship_set.filter(key__exact=key).delete()
185 del self._removed_relationship_registry[:]
187 for key, value in self._added_relationship_registry.items():
189 relationship = self.relationship_set.get(key__exact=key)
190 except Relationship.DoesNotExist:
191 relationship = Relationship()
192 relationship.entity = self
193 relationship.key = key
194 relationship.value = value
196 self._added_relationship_registry.clear()
202 class TreeManager(models.Manager):
203 use_for_related_fields = True
206 return self.filter(parent__isnull=True)
208 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
210 Returns the object with the path, or None if there is no object with that path,
211 unless absolute_result is set to False, in which case it returns a tuple containing
212 the deepest object found along the path, and the remainder of the path after that
213 object as a string (or None in the case that there is no remaining path).
215 slugs = path.split(pathsep)
217 remaining_slugs = list(slugs)
220 remaining_slugs.remove(slug)
221 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
223 obj = self.get(slug__exact=slug, parent__exact=obj)
224 except self.model.DoesNotExist:
227 remaining_slugs.insert(0, slug)
228 remainder = pathsep.join(remaining_slugs)
234 return (obj, remainder)
235 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
238 class TreeModel(models.Model):
239 objects = TreeManager()
240 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
241 slug = models.SlugField()
243 def has_ancestor(self, ancestor):
246 if parent == ancestor:
248 parent = parent.parent
251 def get_path(self, root=None, pathsep='/', field='slug'):
252 if root is not None and self.has_ancestor(root):
255 while parent and parent != root:
256 path = getattr(parent, field, '?') + pathsep + path
257 parent = parent.parent
260 path = getattr(self, field, '?')
262 while parent and parent != root:
263 path = getattr(parent, field, '?') + pathsep + path
264 parent = parent.parent
266 path = property(get_path)
268 def __unicode__(self):
272 unique_together = (('parent', 'slug'),)
276 class TreeEntity(TreeModel, Entity):
278 def attributes(self):
280 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
281 return super(TreeEntity, self).attributes
284 def relationships(self):
286 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
287 return super(TreeEntity, self).relationships