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 value_content_type_limiter = ContentTypeRegistryLimiter()
39 def register_value_model(model):
40 value_content_type_limiter.register_class(model)
43 def unregister_value_model(model):
44 value_content_type_limiter.unregister_class(model)
47 class JSONValue(models.Model):
48 value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
50 def __unicode__(self):
51 return self.value_json
57 class ForeignKeyValue(models.Model):
58 content_type = models.ForeignKey(ContentType, related_name='foreign_key_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
59 object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
60 value = generic.GenericForeignKey()
62 def __unicode__(self):
63 return unicode(self.value)
69 class ManyToManyValue(models.Model):
70 content_type = models.ForeignKey(ContentType, related_name='many_to_many_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
71 object_ids = models.CommaSeparatedIntegerField(max_length=300, verbose_name='Value IDs', null=True, blank=True)
74 return self.content_type.model_class()._default_manager.filter(id__in=self.object_ids)
76 def set_value(self, value):
77 if not isinstance(value, models.query.QuerySet):
78 raise TypeError("Value must be a QuerySet.")
79 self.content_type = ContentType.objects.get_for_model(value.model)
80 self.object_ids = ','.join(value.values_list('id', flat=True))
82 value = property(get_value, set_value)
84 def __unicode__(self):
85 return unicode(self.value)
91 attribute_value_limiter = ContentTypeRegistryLimiter()
92 attribute_value_limiter.register_class(JSONValue)
93 attribute_value_limiter.register_class(ForeignKeyValue)
94 attribute_value_limiter.register_class(ManyToManyValue)
97 class Attribute(models.Model):
98 entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
99 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
100 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
102 value_content_type = models.ForeignKey(ContentType, related_name='attribute_value_set', limit_choices_to=attribute_value_limiter, verbose_name='Value type', null=True, blank=True)
103 value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
104 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
106 key = models.CharField(max_length=255)
108 def get_value_class(self, value):
109 if isinstance(value, models.query.QuerySet):
110 return ManyToManyValue
111 elif isinstance(value, models.Model):
112 return ForeignKeyValue
116 def set_value(self, value):
117 value_class = self.get_value_class(value)
119 if self.value is None or value_class != self.value_content_type.model_class():
120 if self.value is not None:
122 new_value = value_class()
123 new_value.value = value
125 self.value = new_value
127 self.value.value = value
130 def __unicode__(self):
131 return u'"%s": %s' % (self.key, self.value)
135 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
138 class QuerySetMapper(object, DictMixin):
139 def __init__(self, queryset, passthrough=None):
140 self.queryset = queryset
141 self.passthrough = passthrough
143 def __getitem__(self, key):
145 return self.queryset.get(key__exact=key).value
146 except ObjectDoesNotExist:
147 if self.passthrough is not None:
148 return self.passthrough.__getitem__(key)
152 keys = set(self.queryset.values_list('key', flat=True).distinct())
153 if self.passthrough is not None:
154 keys |= set(self.passthrough.keys())
158 class EntityOptions(object):
159 def __init__(self, options):
160 if options is not None:
161 for key, value in options.__dict__.items():
162 setattr(self, key, value)
163 if not hasattr(self, 'proxy_fields'):
164 self.proxy_fields = []
166 def add_proxy_field(self, proxy_field):
167 self.proxy_fields.append(proxy_field)
170 class EntityBase(models.base.ModelBase):
171 def __new__(cls, name, bases, attrs):
172 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
173 entity_options = attrs.pop('EntityMeta', None)
174 setattr(new, '_entity_meta', EntityOptions(entity_options))
175 entity_class_prepared.send(sender=new)
179 class Entity(models.Model):
180 __metaclass__ = EntityBase
182 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
185 def attributes(self):
186 return QuerySetMapper(self.attribute_set)
189 def _added_attribute_registry(self):
190 if not hasattr(self, '_real_added_attribute_registry'):
191 self._real_added_attribute_registry = {}
192 return self._real_added_attribute_registry
195 def _removed_attribute_registry(self):
196 if not hasattr(self, '_real_removed_attribute_registry'):
197 self._real_removed_attribute_registry = []
198 return self._real_removed_attribute_registry
200 def save(self, *args, **kwargs):
201 super(Entity, self).save(*args, **kwargs)
203 for key in self._removed_attribute_registry:
204 self.attribute_set.filter(key__exact=key).delete()
205 del self._removed_attribute_registry[:]
207 for key, value in self._added_attribute_registry.items():
209 attribute = self.attribute_set.get(key__exact=key)
210 except Attribute.DoesNotExist:
211 attribute = Attribute()
212 attribute.entity = self
214 attribute.set_value(value)
216 self._added_attribute_registry.clear()
222 class TreeManager(models.Manager):
223 use_for_related_fields = True
226 return self.filter(parent__isnull=True)
228 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
230 Returns the object with the path, or None if there is no object with that path,
231 unless absolute_result is set to False, in which case it returns a tuple containing
232 the deepest object found along the path, and the remainder of the path after that
233 object as a string (or None in the case that there is no remaining path).
235 slugs = path.split(pathsep)
237 remaining_slugs = list(slugs)
240 remaining_slugs.remove(slug)
241 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
243 obj = self.get(slug__exact=slug, parent__exact=obj)
244 except self.model.DoesNotExist:
247 remaining_slugs.insert(0, slug)
248 remainder = pathsep.join(remaining_slugs)
254 return (obj, remainder)
255 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
258 class TreeModel(models.Model):
259 objects = TreeManager()
260 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
261 slug = models.SlugField(max_length=255)
263 def has_ancestor(self, ancestor):
266 if parent == ancestor:
268 parent = parent.parent
271 def get_path(self, root=None, pathsep='/', field='slug'):
273 if not self.has_ancestor(root):
274 raise AncestorDoesNotExist(root)
277 while parent and parent != root:
278 path = getattr(parent, field, '?') + pathsep + path
279 parent = parent.parent
282 path = getattr(self, field, '?')
284 while parent and parent != root:
285 path = getattr(parent, field, '?') + pathsep + path
286 parent = parent.parent
288 path = property(get_path)
290 def __unicode__(self):
294 unique_together = (('parent', 'slug'),)
298 class TreeEntity(Entity, TreeModel):
300 def attributes(self):
302 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
303 return super(TreeEntity, self).attributes
306 def relationships(self):
308 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
309 return super(TreeEntity, self).relationships