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)
56 unique_together = ('key', 'entity_content_type', 'entity_object_id')
59 value_content_type_limiter = ContentTypeRegistryLimiter()
62 def register_value_model(model):
63 value_content_type_limiter.register_class(model)
66 def unregister_value_model(model):
67 value_content_type_limiter.unregister_class(model)
71 class Relationship(models.Model):
72 entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
73 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
74 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
75 key = models.CharField(max_length=255)
76 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)
77 value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
78 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
80 def __unicode__(self):
81 return u'"%s": %s' % (self.key, self.value)
85 unique_together = ('key', 'entity_content_type', 'entity_object_id')
88 class QuerySetMapper(object, DictMixin):
89 def __init__(self, queryset, passthrough=None):
90 self.queryset = queryset
91 self.passthrough = passthrough
93 def __getitem__(self, key):
95 return self.queryset.get(key__exact=key).value
96 except ObjectDoesNotExist:
97 if self.passthrough is not None:
98 return self.passthrough.__getitem__(key)
102 keys = set(self.queryset.values_list('key', flat=True).distinct())
103 if self.passthrough is not None:
104 keys |= set(self.passthrough.keys())
108 class EntityOptions(object):
109 def __init__(self, options):
110 if options is not None:
111 for key, value in options.__dict__.items():
112 setattr(self, key, value)
113 if not hasattr(self, 'proxy_fields'):
114 self.proxy_fields = []
116 def add_proxy_field(self, proxy_field):
117 self.proxy_fields.append(proxy_field)
120 class EntityBase(models.base.ModelBase):
121 def __new__(cls, name, bases, attrs):
122 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
123 entity_options = attrs.pop('EntityMeta', None)
124 setattr(new, '_entity_meta', EntityOptions(entity_options))
125 entity_class_prepared.send(sender=new)
129 class Entity(models.Model):
130 __metaclass__ = EntityBase
132 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
133 relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
136 def attributes(self):
137 return QuerySetMapper(self.attribute_set)
140 def relationships(self):
141 return QuerySetMapper(self.relationship_set)
144 def _added_attribute_registry(self):
145 if not hasattr(self, '_real_added_attribute_registry'):
146 self._real_added_attribute_registry = {}
147 return self._real_added_attribute_registry
150 def _removed_attribute_registry(self):
151 if not hasattr(self, '_real_removed_attribute_registry'):
152 self._real_removed_attribute_registry = []
153 return self._real_removed_attribute_registry
156 def _added_relationship_registry(self):
157 if not hasattr(self, '_real_added_relationship_registry'):
158 self._real_added_relationship_registry = {}
159 return self._real_added_relationship_registry
162 def _removed_relationship_registry(self):
163 if not hasattr(self, '_real_removed_relationship_registry'):
164 self._real_removed_relationship_registry = []
165 return self._real_removed_relationship_registry
167 def save(self, *args, **kwargs):
168 super(Entity, self).save(*args, **kwargs)
170 for key in self._removed_attribute_registry:
171 self.attribute_set.filter(key__exact=key).delete()
172 del self._removed_attribute_registry[:]
174 for key, value in self._added_attribute_registry.items():
176 attribute = self.attribute_set.get(key__exact=key)
177 except Attribute.DoesNotExist:
178 attribute = Attribute()
179 attribute.entity = self
181 attribute.value = value
183 self._added_attribute_registry.clear()
185 for key in self._removed_relationship_registry:
186 self.relationship_set.filter(key__exact=key).delete()
187 del self._removed_relationship_registry[:]
189 for key, value in self._added_relationship_registry.items():
191 relationship = self.relationship_set.get(key__exact=key)
192 except Relationship.DoesNotExist:
193 relationship = Relationship()
194 relationship.entity = self
195 relationship.key = key
196 relationship.value = value
198 self._added_relationship_registry.clear()
204 class TreeManager(models.Manager):
205 use_for_related_fields = True
208 return self.filter(parent__isnull=True)
210 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
212 Returns the object with the path, or None if there is no object with that path,
213 unless absolute_result is set to False, in which case it returns a tuple containing
214 the deepest object found along the path, and the remainder of the path after that
215 object as a string (or None in the case that there is no remaining path).
217 slugs = path.split(pathsep)
219 remaining_slugs = list(slugs)
222 remaining_slugs.remove(slug)
223 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
225 obj = self.get(slug__exact=slug, parent__exact=obj)
226 except self.model.DoesNotExist:
229 remaining_slugs.insert(0, slug)
230 remainder = pathsep.join(remaining_slugs)
236 return (obj, remainder)
237 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
240 class TreeModel(models.Model):
241 objects = TreeManager()
242 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
243 slug = models.SlugField()
245 def has_ancestor(self, ancestor):
248 if parent == ancestor:
250 parent = parent.parent
253 def get_path(self, root=None, pathsep='/', field='slug'):
254 if root is not None and self.has_ancestor(root):
257 while parent and parent != root:
258 path = getattr(parent, field, '?') + pathsep + path
259 parent = parent.parent
262 path = getattr(self, field, '?')
264 while parent and parent != root:
265 path = getattr(parent, field, '?') + pathsep + path
266 parent = parent.parent
268 path = property(get_path)
270 def __unicode__(self):
274 unique_together = (('parent', 'slug'),)
278 class TreeEntity(Entity, TreeModel):
280 def attributes(self):
282 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
283 return super(TreeEntity, self).attributes
286 def relationships(self):
288 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
289 return super(TreeEntity, self).relationships