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
14 class Tag(models.Model):
15 name = models.CharField(max_length=255)
16 slug = models.SlugField(max_length=255, unique=True)
18 def __unicode__(self):
25 class Titled(models.Model):
26 title = models.CharField(max_length=255)
27 slug = models.SlugField(max_length=255)
29 def __unicode__(self):
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.')
43 def __unicode__(self):
44 return u'"%s": %s' % (self.key, self.value)
48 unique_together = ('key', 'entity_content_type', 'entity_object_id')
51 value_content_type_limiter = ContentTypeRegistryLimiter()
54 def register_value_model(model):
55 value_content_type_limiter.register_class(model)
58 def unregister_value_model(model):
59 value_content_type_limiter.unregister_class(model)
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')
72 def __unicode__(self):
73 return u'"%s": %s' % (self.key, self.value)
77 unique_together = ('key', 'entity_content_type', 'entity_object_id')
80 class QuerySetMapper(object, DictMixin):
81 def __init__(self, queryset, passthrough=None):
82 self.queryset = queryset
83 self.passthrough = passthrough
85 def __getitem__(self, key):
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)
94 keys = set(self.queryset.values_list('key', flat=True).distinct())
95 if self.passthrough is not None:
96 keys |= set(self.passthrough.keys())
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 = []
108 def add_proxy_field(self, proxy_field):
109 self.proxy_fields.append(proxy_field)
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)
121 class Entity(models.Model):
122 __metaclass__ = EntityBase
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')
128 def attributes(self):
129 return QuerySetMapper(self.attribute_set)
132 def relationships(self):
133 return QuerySetMapper(self.relationship_set)
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
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
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
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
159 def save(self, *args, **kwargs):
160 super(Entity, self).save(*args, **kwargs)
162 for key in self._removed_attribute_registry:
163 self.attribute_set.filter(key__exact=key).delete()
164 del self._removed_attribute_registry[:]
166 for key, value in self._added_attribute_registry.items():
168 attribute = self.attribute_set.get(key__exact=key)
169 except Attribute.DoesNotExist:
170 attribute = Attribute()
171 attribute.entity = self
173 attribute.value = value
175 self._added_attribute_registry.clear()
177 for key in self._removed_relationship_registry:
178 self.relationship_set.filter(key__exact=key).delete()
179 del self._removed_relationship_registry[:]
181 for key, value in self._added_relationship_registry.items():
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
190 self._added_relationship_registry.clear()
196 class TreeManager(models.Manager):
197 use_for_related_fields = True
200 return self.filter(parent__isnull=True)
202 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
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).
209 slugs = path.split(pathsep)
211 remaining_slugs = list(slugs)
214 remaining_slugs.remove(slug)
215 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
217 obj = self.get(slug__exact=slug, parent__exact=obj)
218 except self.model.DoesNotExist:
221 remaining_slugs.insert(0, slug)
222 remainder = pathsep.join(remaining_slugs)
228 return (obj, remainder)
229 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
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)
237 def has_ancestor(self, ancestor):
240 if parent == ancestor:
242 parent = parent.parent
245 def get_path(self, root=None, pathsep='/', field='slug'):
247 if not self.has_ancestor(root):
248 raise AncestorDoesNotExist(root)
251 while parent and parent != root:
252 path = getattr(parent, field, '?') + pathsep + path
253 parent = parent.parent
256 path = getattr(self, field, '?')
258 while parent and parent != root:
259 path = getattr(parent, field, '?') + pathsep + path
260 parent = parent.parent
262 path = property(get_path)
264 def __unicode__(self):
268 unique_together = (('parent', 'slug'),)
272 class TreeEntity(Entity, TreeModel):
274 def attributes(self):
276 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
277 return super(TreeEntity, self).attributes
280 def relationships(self):
282 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
283 return super(TreeEntity, self).relationships