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__ = ('Tag', 'value_content_type_limiter', 'register_value_model', 'unregister_value_model', 'JSONValue', 'ForeignKeyValue', 'ManyToManyValue', 'Attribute', 'Entity', 'TreeEntity', 'SlugTreeEntity')
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']
324 def attributes(self):
325 if not hasattr(self, '_attributes'):
326 self._attributes = self.get_attribute_mapper()
327 return self._attributes
333 class TreeEntityBase(MPTTModelBase, EntityBase):
334 def __new__(meta, name, bases, attrs):
335 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
336 cls = EntityBase.__new__(meta, name, bases, attrs)
338 return meta.register(cls)
341 class TreeEntityManager(models.Manager):
342 use_for_related_fields = True
344 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='pk'):
346 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).
348 .. 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.
350 .. 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.
352 :param path: The path of the object
353 :param root: The object which will be considered the root of the search
354 :param absolute_result: Whether to return an absolute result or do a binary search
355 :param pathsep: The path separator used in ``path``
356 :param field: The field on the model which should be queried for ``path`` segment matching.
357 :returns: An instance if ``absolute_result`` is ``True`` or an (instance, remaining_path) tuple otherwise.
358 :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters.
362 segments = path.split(pathsep)
364 # Clean out blank segments. Handles multiple consecutive pathseps.
371 # Special-case a lack of segments. No queries necessary.
378 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
380 def make_query_kwargs(segments, root):
383 revsegs = list(segments)
386 for segment in revsegs:
387 kwargs["%s%s__exact" % (prefix, field)] = segment
391 kwargs[prefix[:-2]] = root
395 def find_obj(segments, depth, deepest_found=None):
396 if deepest_found is None:
399 deepest_level = deepest_found.get_level() + 1
401 deepest_level = deepest_found.get_level() - root.get_level()
403 obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found or root))
404 except self.model.DoesNotExist:
405 if not deepest_level and depth > 1:
406 # make sure there's a root node...
409 # Try finding one with half the path since the deepest find.
410 depth = (deepest_level + depth)/2
412 if deepest_level == depth:
413 # This should happen if nothing is found with any part of the given path.
414 if root is not None and deepest_found is None:
415 return root, pathsep.join(segments)
418 return find_obj(segments, depth, deepest_found)
422 deepest_level = obj.get_level() + 1
424 deepest_level = obj.get_level() - root.get_level()
426 # Could there be a deeper one?
427 if obj.is_leaf_node():
428 return obj, pathsep.join(segments[deepest_level:]) or None
430 depth += (len(segments) - depth)/2 or len(segments) - depth
432 if depth > deepest_level + obj.get_descendant_count():
433 depth = deepest_level + obj.get_descendant_count()
435 if deepest_level == depth:
436 return obj, pathsep.join(segments[deepest_level:]) or None
439 return find_obj(segments, depth, obj)
440 except self.model.DoesNotExist:
441 # Then this was the deepest.
442 return obj, pathsep.join(segments[deepest_level:])
445 return self.get(**make_query_kwargs(segments, root))
447 # Try a modified binary search algorithm. Feed the root in so that query complexity
448 # can be reduced. It might be possible to weight the search towards the beginning
449 # of the path, since short paths are more likely, but how far forward? It would
450 # need to shift depending on len(segments) - perhaps logarithmically?
451 return find_obj(segments, len(segments)/2 or len(segments))
454 class TreeEntity(Entity, MPTTModel):
455 """An abstract subclass of Entity which represents a tree relationship."""
457 __metaclass__ = TreeEntityBase
458 objects = TreeEntityManager()
459 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
461 def get_path(self, root=None, pathsep='/', field='pk', memoize=True):
463 :param root: Only return the path since this object.
464 :param pathsep: The path separator to use when constructing an instance's path
465 :param field: The field to pull path information from for each ancestor.
466 :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``.
467 :returns: A string representation of an object's path.
474 parent_id = getattr(self, "%s_id" % self._mptt_meta.parent_attr)
475 if getattr(root, 'pk', None) == parent_id:
476 return getattr(self, field, '?')
478 if root is not None and not self.is_descendant_of(root):
479 raise AncestorDoesNotExist(root)
482 memo_args = (parent_id, getattr(root, 'pk', None), pathsep, getattr(self, field, '?'))
484 return self._path_memo[memo_args]
485 except AttributeError:
490 qs = self.get_ancestors(include_self=True)
493 qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
495 path = pathsep.join([getattr(parent, field, '?') for parent in qs])
498 self._path_memo[memo_args] = path
501 path = property(get_path)
503 def get_attribute_mapper(self, mapper=None):
505 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.
509 >>> attr = entity.attribute_set.get(key='spam')
510 DoesNotExist: Attribute matching query does not exist.
511 >>> attr = entity.parent.attribute_set.get(key='spam')
514 >>> entity.attributes['spam']
519 if getattr(self, "%s_id" % self._mptt_meta.parent_attr):
520 mapper = TreeAttributeMapper
522 mapper = AttributeMapper
523 return super(TreeEntity, self).get_attribute_mapper(mapper)
525 def __unicode__(self):
532 class SlugTreeEntityManager(TreeEntityManager):
533 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
534 return super(SlugTreeEntityManager, self).get_with_path(path, root, absolute_result, pathsep, field)
537 class SlugTreeEntity(TreeEntity):
538 objects = SlugTreeEntityManager()
539 slug = models.SlugField(max_length=255)
541 def get_path(self, root=None, pathsep='/', field='slug', memoize=True):
542 return super(SlugTreeEntity, self).get_path(root, pathsep, field, memoize)
543 path = property(get_path)
546 if getattr(self, "%s_id" % self._mptt_meta.parent_attr) is None:
548 self._default_manager.exclude(pk=self.pk).get(slug=self.slug, parent__isnull=True)
549 except self.DoesNotExist:
552 raise ValidationError(self.unique_error_message(self.__class__, ('parent', 'slug')))
555 unique_together = ('parent', 'slug')