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 ValidationError
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__ = ('value_content_type_limiter', 'register_value_model', 'unregister_value_model', 'JSONValue', 'ForeignKeyValue', 'ManyToManyValue', 'Attribute', 'Entity', 'TreeEntity', 'SlugTreeEntity')
22 #: 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.
23 value_content_type_limiter = ContentTypeRegistryLimiter()
26 def register_value_model(model):
27 """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
28 value_content_type_limiter.register_class(model)
31 def unregister_value_model(model):
32 """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
33 value_content_type_limiter.unregister_class(model)
36 class AttributeValue(models.Model):
38 This is an abstract base class for models that can be used as values for :class:`Attribute`\ s.
40 AttributeValue subclasses are expected to supply access to a clean version of their value through an attribute called "value".
44 #: :class:`GenericRelation` to :class:`Attribute`
45 attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
47 def set_value(self, value):
48 """Given a ``value``, sets the appropriate fields so that it can be correctly stored in the database."""
49 raise NotImplementedError
51 def value_formfields(self, **kwargs):
53 Returns any formfields that would be used to construct an instance of this value.
55 :returns: A dictionary mapping field names to formfields.
59 raise NotImplementedError
61 def construct_instance(self, **kwargs):
62 """Applies cleaned data from the formfields generated by valid_formfields to oneself."""
63 raise NotImplementedError
65 def __unicode__(self):
66 return unicode(self.value)
72 #: An instance of :class:`.ContentTypeSubclassLimiter` which is used to track the content types which are considered valid value models for an :class:`Attribute`.
73 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
76 class JSONValue(AttributeValue):
77 """Stores a python object as a json string."""
78 value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null', db_index=True)
80 def __unicode__(self):
81 return force_unicode(self.value)
83 def value_formfields(self):
84 kwargs = {'initial': self.value_json}
85 field = self._meta.get_field('value')
86 return {field.name: field.formfield(**kwargs)}
88 def construct_instance(self, **kwargs):
89 field_name = self._meta.get_field('value').name
90 self.set_value(kwargs.pop(field_name, None))
92 def set_value(self, value):
99 class ForeignKeyValue(AttributeValue):
100 """Stores a generic relationship to an instance of any value content type (as defined by the :data:`value_content_type_limiter`)."""
101 content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
102 object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
103 value = generic.GenericForeignKey()
105 def value_formfields(self):
106 field = self._meta.get_field('content_type')
107 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
109 if self.content_type:
111 'initial': self.object_id,
113 'queryset': self.content_type.model_class()._default_manager.all()
115 fields['value'] = forms.ModelChoiceField(**kwargs)
118 def construct_instance(self, **kwargs):
119 field_name = self._meta.get_field('content_type').name
120 ct = kwargs.pop(field_name, None)
121 if ct is None or ct != self.content_type:
122 self.object_id = None
123 self.content_type = ct
125 value = kwargs.pop('value', None)
126 self.set_value(value)
128 self.content_type = ct
130 def set_value(self, value):
137 class ManyToManyValue(AttributeValue):
138 """Stores a generic relationship to many instances of any value content type (as defined by the :data:`value_content_type_limiter`)."""
139 content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
140 values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
142 def get_object_ids(self):
143 return self.values.values_list('object_id', flat=True)
144 object_ids = property(get_object_ids)
146 def set_value(self, value):
147 # Value must be a queryset. Watch out for ModelMultipleChoiceField;
148 # it returns its value as a list if empty.
150 self.content_type = ContentType.objects.get_for_model(value.model)
152 # Before we can fiddle with the many-to-many to foreignkeyvalues, we need
157 object_ids = value.values_list('id', flat=True)
159 # These lines shouldn't be necessary; however, if object_ids is an EmptyQuerySet,
160 # the code (specifically the object_id__in query) won't work without them. Unclear why...
161 # TODO: is this still the case?
163 self.values.all().delete()
165 self.values.exclude(object_id__in=object_ids, content_type=self.content_type).delete()
167 current_ids = self.object_ids
169 for object_id in object_ids:
170 if object_id in current_ids:
172 self.values.create(content_type=self.content_type, object_id=object_id)
175 if self.content_type is None:
178 # HACK to be safely explicit until http://code.djangoproject.com/ticket/15145 is resolved
179 object_ids = self.object_ids
180 manager = self.content_type.model_class()._default_manager
182 return manager.none()
183 return manager.filter(id__in=self.object_ids)
185 value = property(get_value, set_value)
187 def value_formfields(self):
188 field = self._meta.get_field('content_type')
189 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
191 if self.content_type:
193 'initial': self.object_ids,
195 'queryset': self.content_type.model_class()._default_manager.all()
197 fields['value'] = forms.ModelMultipleChoiceField(**kwargs)
200 def construct_instance(self, **kwargs):
201 field_name = self._meta.get_field('content_type').name
202 ct = kwargs.pop(field_name, None)
203 if ct is None or ct != self.content_type:
205 self.content_type = ct
207 value = kwargs.get('value', None)
209 value = self.content_type.model_class()._default_manager.none()
210 self.set_value(value)
211 construct_instance.alters_data = True
217 class Attribute(models.Model):
219 :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.
221 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.
224 entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
225 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
227 #: :class:`GenericForeignKey` to anything (generally an instance of an Entity subclass).
228 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
230 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)
231 value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
233 #: :class:`GenericForeignKey` to an instance of a subclass of :class:`AttributeValue` as determined by the :data:`attribute_value_limiter`.
234 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
236 #: :class:`CharField` containing a key (up to 255 characters) consisting of alphanumeric characters and underscores.
237 key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
239 def __unicode__(self):
240 return u'"%s": %s' % (self.key, self.value)
242 def set_value(self, value, value_class=JSONValue):
243 """Given a value and a value class, sets up self.value appropriately."""
244 if isinstance(self.value, value_class):
247 if isinstance(self.value, models.Model):
259 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
262 class EntityOptions(object):
263 def __init__(self, options):
264 if options is not None:
265 for key, value in options.__dict__.items():
266 setattr(self, key, value)
267 if not hasattr(self, 'proxy_fields'):
268 self.proxy_fields = []
270 def add_proxy_field(self, proxy_field):
271 self.proxy_fields.append(proxy_field)
274 class EntityBase(models.base.ModelBase):
275 def __new__(cls, name, bases, attrs):
276 entity_meta = attrs.pop('EntityMeta', None)
277 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
278 new.add_to_class('_entity_meta', EntityOptions(entity_meta))
279 entity_class_prepared.send(sender=new)
283 class Entity(models.Model):
284 """An abstract class that simplifies access to related attributes. Most models provided by Philo subclass Entity."""
285 __metaclass__ = EntityBase
287 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
289 def get_attribute_mapper(self, mapper=AttributeMapper):
291 Returns an :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly.
295 >>> attr = entity.attribute_set.get(key='spam')
298 >>> entity.attributes['spam']
305 def attributes(self):
306 if not hasattr(self, '_attributes'):
307 self._attributes = self.get_attribute_mapper()
308 return self._attributes
314 class TreeEntityBase(MPTTModelBase, EntityBase):
315 def __new__(meta, name, bases, attrs):
316 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
317 cls = EntityBase.__new__(meta, name, bases, attrs)
319 return meta.register(cls)
322 class TreeEntityManager(models.Manager):
323 use_for_related_fields = True
325 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='pk'):
327 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).
329 .. 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.
331 .. 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.
333 :param path: The path of the object
334 :param root: The object which will be considered the root of the search
335 :param absolute_result: Whether to return an absolute result or do a binary search
336 :param pathsep: The path separator used in ``path``
337 :param field: The field on the model which should be queried for ``path`` segment matching.
338 :returns: An instance if ``absolute_result`` is ``True`` or an (instance, remaining_path) tuple otherwise.
339 :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters.
343 segments = path.split(pathsep)
345 # Clean out blank segments. Handles multiple consecutive pathseps.
352 # Special-case a lack of segments. No queries necessary.
359 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
361 def make_query_kwargs(segments, root):
364 revsegs = list(segments)
367 for segment in revsegs:
368 kwargs["%s%s__exact" % (prefix, field)] = segment
372 kwargs[prefix[:-2]] = root
376 def find_obj(segments, depth, deepest_found=None):
377 if deepest_found is None:
380 deepest_level = deepest_found.get_level() + 1
382 deepest_level = deepest_found.get_level() - root.get_level()
384 obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found or root))
385 except self.model.DoesNotExist:
386 if not deepest_level and depth > 1:
387 # make sure there's a root node...
390 # Try finding one with half the path since the deepest find.
391 depth = (deepest_level + depth)/2
393 if deepest_level == depth:
394 # This should happen if nothing is found with any part of the given path.
395 if root is not None and deepest_found is None:
396 return root, pathsep.join(segments)
399 return find_obj(segments, depth, deepest_found)
403 deepest_level = obj.get_level() + 1
405 deepest_level = obj.get_level() - root.get_level()
407 # Could there be a deeper one?
408 if obj.is_leaf_node():
409 return obj, pathsep.join(segments[deepest_level:]) or None
411 depth += (len(segments) - depth)/2 or len(segments) - depth
413 if depth > deepest_level + obj.get_descendant_count():
414 depth = deepest_level + obj.get_descendant_count()
416 if deepest_level == depth:
417 return obj, pathsep.join(segments[deepest_level:]) or None
420 return find_obj(segments, depth, obj)
421 except self.model.DoesNotExist:
422 # Then this was the deepest.
423 return obj, pathsep.join(segments[deepest_level:])
426 return self.get(**make_query_kwargs(segments, root))
428 # Try a modified binary search algorithm. Feed the root in so that query complexity
429 # can be reduced. It might be possible to weight the search towards the beginning
430 # of the path, since short paths are more likely, but how far forward? It would
431 # need to shift depending on len(segments) - perhaps logarithmically?
432 return find_obj(segments, len(segments)/2 or len(segments))
435 class TreeEntity(Entity, MPTTModel):
436 """An abstract subclass of Entity which represents a tree relationship."""
438 __metaclass__ = TreeEntityBase
439 objects = TreeEntityManager()
440 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
442 def get_path(self, root=None, pathsep='/', field='pk', memoize=True):
444 :param root: Only return the path since this object.
445 :param pathsep: The path separator to use when constructing an instance's path
446 :param field: The field to pull path information from for each ancestor.
447 :param memoize: Whether to use memoized results. Since, in most cases, the ancestors of a TreeEntity will not change over the course of an instance's lifetime, this defaults to ``True``.
448 :returns: A string representation of an object's path.
455 parent_id = getattr(self, "%s_id" % self._mptt_meta.parent_attr)
456 if getattr(root, 'pk', None) == parent_id:
457 return getattr(self, field, '?')
459 if root is not None and not self.is_descendant_of(root):
460 raise AncestorDoesNotExist(root)
463 memo_args = (parent_id, getattr(root, 'pk', None), pathsep, getattr(self, field, '?'))
465 return self._path_memo[memo_args]
466 except AttributeError:
471 qs = self.get_ancestors(include_self=True)
474 qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
476 path = pathsep.join([getattr(parent, field, '?') for parent in qs])
479 self._path_memo[memo_args] = path
482 path = property(get_path)
484 def get_attribute_mapper(self, mapper=None):
486 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.
490 >>> attr = entity.attribute_set.get(key='spam')
491 DoesNotExist: Attribute matching query does not exist.
492 >>> attr = entity.parent.attribute_set.get(key='spam')
495 >>> entity.attributes['spam']
500 if getattr(self, "%s_id" % self._mptt_meta.parent_attr):
501 mapper = TreeAttributeMapper
503 mapper = AttributeMapper
504 return super(TreeEntity, self).get_attribute_mapper(mapper)
506 def __unicode__(self):
513 class SlugTreeEntityManager(TreeEntityManager):
514 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
515 return super(SlugTreeEntityManager, self).get_with_path(path, root, absolute_result, pathsep, field)
518 class SlugTreeEntity(TreeEntity):
519 objects = SlugTreeEntityManager()
520 slug = models.SlugField(max_length=255)
522 def get_path(self, root=None, pathsep='/', field='slug', memoize=True):
523 return super(SlugTreeEntity, self).get_path(root, pathsep, field, memoize)
524 path = property(get_path)
527 if getattr(self, "%s_id" % self._mptt_meta.parent_attr) is None:
529 self._default_manager.exclude(pk=self.pk).get(slug=self.slug, parent__isnull=True)
530 except self.DoesNotExist:
533 raise ValidationError(self.unique_error_message(self.__class__, ('parent', 'slug')))
536 unique_together = ('parent', 'slug')