1 from django import forms
2 from django.contrib.contenttypes.models import ContentType
3 from django.contrib.contenttypes import generic
4 from django.core.exceptions import ObjectDoesNotExist
5 from django.core.validators import RegexValidator
6 from django.db import models
7 from django.utils import simplejson as json
8 from django.utils.encoding import force_unicode
9 from mptt.models import MPTTModel, MPTTModelBase, MPTTOptions
11 from philo.exceptions import AncestorDoesNotExist
12 from philo.models.fields import JSONField
13 from philo.signals import entity_class_prepared
14 from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
15 from philo.utils.entities import AttributeMapper, TreeAttributeMapper
16 from philo.validators import json_validator
19 __all__ = ('Tag', 'value_content_type_limiter', 'register_value_model', 'unregister_value_model', 'JSONValue', 'ForeignKeyValue', 'ManyToManyValue', 'Attribute', 'Entity', 'TreeEntity')
22 class Tag(models.Model):
23 """A simple, generic model for tagging."""
24 #: A CharField (max length 255) which contains the name of the tag.
25 name = models.CharField(max_length=255)
26 #: A CharField (max length 255) which contains the tag's unique slug.
27 slug = models.SlugField(max_length=255, unique=True)
29 def __unicode__(self):
30 """Returns the value of the :attr:`name` field"""
38 #: An instance of :class:`.ContentTypeRegistryLimiter` which is used to track the content types which can be related to by :class:`ForeignKeyValue`\ s and :class:`ManyToManyValue`\ s.
39 value_content_type_limiter = ContentTypeRegistryLimiter()
42 def register_value_model(model):
43 """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
44 value_content_type_limiter.register_class(model)
47 register_value_model(Tag)
50 def unregister_value_model(model):
51 """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
52 value_content_type_limiter.unregister_class(model)
55 class AttributeValue(models.Model):
57 This is an abstract base class for models that can be used as values for :class:`Attribute`\ s.
59 AttributeValue subclasses are expected to supply access to a clean version of their value through an attribute called "value".
63 #: :class:`GenericRelation` to :class:`Attribute`
64 attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
66 def set_value(self, value):
67 """Given a ``value``, sets the appropriate fields so that it can be correctly stored in the database."""
68 raise NotImplementedError
70 def value_formfields(self, **kwargs):
72 Returns any formfields that would be used to construct an instance of this value.
74 :returns: A dictionary mapping field names to formfields.
78 raise NotImplementedError
80 def construct_instance(self, **kwargs):
81 """Applies cleaned data from the formfields generated by valid_formfields to oneself."""
82 raise NotImplementedError
84 def __unicode__(self):
85 return unicode(self.value)
91 #: An instance of :class:`.ContentTypeSubclassLimiter` which is used to track the content types which are considered valid value models for an :class:`Attribute`.
92 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
95 class JSONValue(AttributeValue):
96 """Stores a python object as a json string."""
97 value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null', db_index=True)
99 def __unicode__(self):
100 return force_unicode(self.value)
102 def value_formfields(self):
103 kwargs = {'initial': self.value_json}
104 field = self._meta.get_field('value')
105 return {field.name: field.formfield(**kwargs)}
107 def construct_instance(self, **kwargs):
108 field_name = self._meta.get_field('value').name
109 self.set_value(kwargs.pop(field_name, None))
111 def set_value(self, value):
118 class ForeignKeyValue(AttributeValue):
119 """Stores a generic relationship to an instance of any value content type (as defined by the :data:`value_content_type_limiter`)."""
120 content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
121 object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
122 value = generic.GenericForeignKey()
124 def value_formfields(self):
125 field = self._meta.get_field('content_type')
126 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
128 if self.content_type:
130 'initial': self.object_id,
132 'queryset': self.content_type.model_class()._default_manager.all()
134 fields['value'] = forms.ModelChoiceField(**kwargs)
137 def construct_instance(self, **kwargs):
138 field_name = self._meta.get_field('content_type').name
139 ct = kwargs.pop(field_name, None)
140 if ct is None or ct != self.content_type:
141 self.object_id = None
142 self.content_type = ct
144 value = kwargs.pop('value', None)
145 self.set_value(value)
147 self.content_type = ct
149 def set_value(self, value):
156 class ManyToManyValue(AttributeValue):
157 """Stores a generic relationship to many instances of any value content type (as defined by the :data:`value_content_type_limiter`)."""
158 content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
159 values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
161 def get_object_ids(self):
162 return self.values.values_list('object_id', flat=True)
163 object_ids = property(get_object_ids)
165 def set_value(self, value):
166 # Value must be a queryset. Watch out for ModelMultipleChoiceField;
167 # it returns its value as a list if empty.
169 self.content_type = ContentType.objects.get_for_model(value.model)
171 # Before we can fiddle with the many-to-many to foreignkeyvalues, we need
176 object_ids = value.values_list('id', flat=True)
178 # These lines shouldn't be necessary; however, if object_ids is an EmptyQuerySet,
179 # the code (specifically the object_id__in query) won't work without them. Unclear why...
180 # TODO: is this still the case?
182 self.values.all().delete()
184 self.values.exclude(object_id__in=object_ids, content_type=self.content_type).delete()
186 current_ids = self.object_ids
188 for object_id in object_ids:
189 if object_id in current_ids:
191 self.values.create(content_type=self.content_type, object_id=object_id)
194 if self.content_type is None:
197 # HACK to be safely explicit until http://code.djangoproject.com/ticket/15145 is resolved
198 object_ids = self.object_ids
199 manager = self.content_type.model_class()._default_manager
201 return manager.none()
202 return manager.filter(id__in=self.object_ids)
204 value = property(get_value, set_value)
206 def value_formfields(self):
207 field = self._meta.get_field('content_type')
208 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
210 if self.content_type:
212 'initial': self.object_ids,
214 'queryset': self.content_type.model_class()._default_manager.all()
216 fields['value'] = forms.ModelMultipleChoiceField(**kwargs)
219 def construct_instance(self, **kwargs):
220 field_name = self._meta.get_field('content_type').name
221 ct = kwargs.pop(field_name, None)
222 if ct is None or ct != self.content_type:
224 self.content_type = ct
226 value = kwargs.get('value', None)
228 value = self.content_type.model_class()._default_manager.none()
229 self.set_value(value)
230 construct_instance.alters_data = True
236 class Attribute(models.Model):
238 :class:`Attribute`\ s exist primarily to let arbitrary data be attached to arbitrary model instances without altering the database schema and without guaranteeing that the data will be available on every instance of that model.
240 Generally, :class:`Attribute`\ s will not be accessed as models; instead, they will be accessed through the :attr:`Entity.attributes` property, which allows direct dictionary getting and setting of the value of an :class:`Attribute` with its key.
243 entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
244 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
246 #: :class:`GenericForeignKey` to anything (generally an instance of an Entity subclass).
247 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
249 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)
250 value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
252 #: :class:`GenericForeignKey` to an instance of a subclass of :class:`AttributeValue` as determined by the :data:`attribute_value_limiter`.
253 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
255 #: :class:`CharField` containing a key (up to 255 characters) consisting of alphanumeric characters and underscores.
256 key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
258 def __unicode__(self):
259 return u'"%s": %s' % (self.key, self.value)
261 def set_value(self, value, value_class=JSONValue):
262 """Given a value and a value class, sets up self.value appropriately."""
263 if isinstance(self.value, value_class):
266 if isinstance(self.value, models.Model):
278 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
281 class EntityOptions(object):
282 def __init__(self, options):
283 if options is not None:
284 for key, value in options.__dict__.items():
285 setattr(self, key, value)
286 if not hasattr(self, 'proxy_fields'):
287 self.proxy_fields = []
289 def add_proxy_field(self, proxy_field):
290 self.proxy_fields.append(proxy_field)
293 class EntityBase(models.base.ModelBase):
294 def __new__(cls, name, bases, attrs):
295 entity_meta = attrs.pop('EntityMeta', None)
296 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
297 new.add_to_class('_entity_meta', EntityOptions(entity_meta))
298 entity_class_prepared.send(sender=new)
302 class Entity(models.Model):
303 """An abstract class that simplifies access to related attributes. Most models provided by Philo subclass Entity."""
304 __metaclass__ = EntityBase
306 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
308 def get_attribute_mapper(self, mapper=AttributeMapper):
310 Returns an :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly.
314 >>> attr = entity.attribute_set.get(key='spam')
317 >>> entity.attributes['spam']
322 attributes = property(get_attribute_mapper)
328 class TreeEntityBase(MPTTModelBase, EntityBase):
329 def __new__(meta, name, bases, attrs):
330 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
331 cls = EntityBase.__new__(meta, name, bases, attrs)
333 return meta.register(cls)
336 class TreeEntityManager(models.Manager):
337 use_for_related_fields = True
339 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='pk'):
341 If ``absolute_result`` is ``True``, returns the object at ``path`` (starting at ``root``) or raises an :class:`~django.core.exceptions.ObjectDoesNotExist` exception. Otherwise, returns a tuple containing the deepest object found along ``path`` (or ``root`` if no deeper object is found) and the remainder of the path after that object as a string (or None if there is no remaining path).
343 .. note:: If you are looking for something with an exact path, it is faster to use absolute_result=True, unless the path depth is over ~40, in which case the high cost of the absolute query may make a binary search (i.e. non-absolute) faster.
345 .. note:: SQLite allows max of 64 tables in one join. That means the binary search will only work on paths with a max depth of 127 and the absolute fetch will only work to a max depth of (surprise!) 63. Larger depths could be handled, but since the common use case will not have a tree structure that deep, they are not.
347 :param path: The path of the object
348 :param root: The object which will be considered the root of the search
349 :param absolute_result: Whether to return an absolute result or do a binary search
350 :param pathsep: The path separator used in ``path``
351 :param field: The field on the model which should be queried for ``path`` segment matching.
352 :returns: An instance if ``absolute_result`` is ``True`` or an (instance, remaining_path) tuple otherwise.
353 :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters.
357 segments = path.split(pathsep)
359 # Clean out blank segments. Handles multiple consecutive pathseps.
366 # Special-case a lack of segments. No queries necessary.
373 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
375 def make_query_kwargs(segments, root):
378 revsegs = list(segments)
381 for segment in revsegs:
382 kwargs["%s%s__exact" % (prefix, field)] = segment
386 kwargs[prefix[:-2]] = root
390 def find_obj(segments, depth, deepest_found=None):
391 if deepest_found is None:
394 deepest_level = deepest_found.get_level() + 1
396 deepest_level = deepest_found.get_level() - root.get_level()
398 obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found or root))
399 except self.model.DoesNotExist:
400 if not deepest_level and depth > 1:
401 # make sure there's a root node...
404 # Try finding one with half the path since the deepest find.
405 depth = (deepest_level + depth)/2
407 if deepest_level == depth:
408 # This should happen if nothing is found with any part of the given path.
409 if root is not None and deepest_found is None:
410 return root, pathsep.join(segments)
413 return find_obj(segments, depth, deepest_found)
417 deepest_level = obj.get_level() + 1
419 deepest_level = obj.get_level() - root.get_level()
421 # Could there be a deeper one?
422 if obj.is_leaf_node():
423 return obj, pathsep.join(segments[deepest_level:]) or None
425 depth += (len(segments) - depth)/2 or len(segments) - depth
427 if depth > deepest_level + obj.get_descendant_count():
428 depth = deepest_level + obj.get_descendant_count()
430 if deepest_level == depth:
431 return obj, pathsep.join(segments[deepest_level:]) or None
434 return find_obj(segments, depth, obj)
435 except self.model.DoesNotExist:
436 # Then this was the deepest.
437 return obj, pathsep.join(segments[deepest_level:])
440 return self.get(**make_query_kwargs(segments, root))
442 # Try a modified binary search algorithm. Feed the root in so that query complexity
443 # can be reduced. It might be possible to weight the search towards the beginning
444 # of the path, since short paths are more likely, but how far forward? It would
445 # need to shift depending on len(segments) - perhaps logarithmically?
446 return find_obj(segments, len(segments)/2 or len(segments))
449 class TreeEntity(Entity, MPTTModel):
450 """An abstract subclass of Entity which represents a tree relationship."""
452 __metaclass__ = TreeEntityBase
453 objects = TreeEntityManager()
454 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
456 def get_path(self, root=None, pathsep='/', field='slug'):
458 :param root: Only return the path since this object.
459 :param pathsep: The path separator to use when constructing an instance's path
460 :param field: The field to pull path information from for each ancestor.
461 :returns: A string representation of an object's path.
468 if root is None and self.is_root_node():
469 return getattr(self, field, '?')
471 if root is not None and not self.is_descendant_of(root):
472 raise AncestorDoesNotExist(root)
474 qs = self.get_ancestors(include_self=True)
477 qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
479 return pathsep.join([getattr(parent, field, '?') for parent in qs])
480 path = property(get_path)
482 def get_attribute_mapper(self, mapper=None):
484 Returns a :class:`.TreeAttributeMapper` or :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly. If an :class:`Attribute` with a given key is not related to the :class:`Entity`, then the mapper will check the parent's attributes.
488 >>> attr = entity.attribute_set.get(key='spam')
489 DoesNotExist: Attribute matching query does not exist.
490 >>> attr = entity.parent.attribute_set.get(key='spam')
493 >>> entity.attributes['spam']
499 mapper = TreeAttributeMapper
501 mapper = AttributeMapper
502 return super(TreeEntity, self).get_attribute_mapper(mapper)
503 attributes = property(get_attribute_mapper)
505 def __unicode__(self):
512 class SlugTreeEntityManager(TreeEntityManager):
513 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
514 return super(SlugTreeEntityManager, self).get_with_path(path, root, absolute_result, pathsep, field)
517 class SlugTreeEntity(TreeEntity):
518 objects = SlugTreeEntityManager()
519 slug = models.SlugField(max_length=255)
521 def get_path(self, root=None, pathsep='/', field='slug'):
522 return super(SlugTreeEntity, self).get_path(root, pathsep, field)
523 path = property(get_path)
526 unique_together = ('parent', 'slug')