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 UserDict import DictMixin
9 def register_value_model(model):
13 def unregister_value_model(model):
17 class Attribute(models.Model):
18 entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
19 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
20 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
21 key = models.CharField(max_length=255)
22 json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
25 return json.loads(self.json_value)
27 def set_value(self, value):
28 self.json_value = json.dumps(value)
30 def delete_value(self):
31 self.json_value = json.dumps(None)
33 value = property(get_value, set_value, delete_value)
35 def __unicode__(self):
36 return u'"%s": %s' % (self.key, self.value)
42 class Relationship(models.Model):
43 entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
44 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
45 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
46 key = models.CharField(max_length=255)
47 value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', verbose_name='Value type')
48 value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
49 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
51 def __unicode__(self):
52 return u'"%s": %s' % (self.key, self.value)
58 class QuerySetMapper(object, DictMixin):
59 def __init__(self, queryset, passthrough=None):
60 self.queryset = queryset
61 self.passthrough = passthrough
62 def __getitem__(self, key):
64 return self.queryset.get(key__exact=key).value
65 except ObjectDoesNotExist:
67 return self.passthrough.__getitem__(key)
70 keys = set(self.queryset.values_list('key', flat=True).distinct())
72 keys += set(self.passthrough.keys())
76 class Entity(models.Model):
77 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
78 relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
82 return QuerySetMapper(self.attribute_set)
85 def relationships(self):
86 return QuerySetMapper(self.relationship_set)
93 class TreeManager(models.Manager):
94 use_for_related_fields = True
97 return self.filter(parent__isnull=True)
99 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
101 Returns the object with the path, or None if there is no object with that path,
102 unless absolute_result is set to False, in which case it returns a tuple containing
103 the deepest object found along the path, and the remainder of the path after that
104 object as a string (or None in the case that there is no remaining path).
106 slugs = path.split(pathsep)
108 remaining_slugs = list(slugs)
111 remaining_slugs.remove(slug)
112 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
114 obj = self.get(slug__exact=slug, parent__exact=obj)
115 except self.model.DoesNotExist:
118 remaining_slugs.insert(0, slug)
119 remainder = pathsep.join(remaining_slugs)
125 return (obj, remainder)
126 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
129 class TreeModel(models.Model):
130 objects = TreeManager()
131 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
132 slug = models.SlugField()
134 def get_path(self, pathsep='/', field='slug'):
135 path = getattr(self, field, '?')
138 path = getattr(parent, field, '?') + pathsep + path
139 parent = parent.parent
141 path = property(get_path)
143 def __unicode__(self):
151 class TreeEntity(TreeModel, Entity):
153 def attributes(self):
155 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
156 return super(TreeEntity, self).attributes
159 def relationships(self):
161 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
162 return super(TreeEntity, self).relationships
169 class InheritableTreeEntity(TreeEntity):
170 instance_type = models.ForeignKey(ContentType, editable=False)
172 def save(self, force_insert=False, force_update=False):
173 if not hasattr(self, 'instance_type_ptr'):
174 self.instance_type = ContentType.objects.get_for_model(self.__class__)
175 super(InheritableTreeEntity, self).save(force_insert, force_update)
180 return self.instance_type.get_object_for_this_type(id=self.id)
184 def get_path(self, pathsep='/', field='slug'):
185 path = getattr(self.instance, field, getattr(self.instance, 'slug', '?'))
188 path = getattr(parent.instance, field, getattr(parent.instance, 'slug', '?')) + pathsep + path
189 parent = parent.parent
191 path = property(get_path)
194 def attributes(self):
196 return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
197 return QuerySetMapper(self.instance.attribute_set)
200 def relationships(self):
202 return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
203 return QuerySetMapper(self.instance.relationship_set)