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 class Titled(models.Model):
39 # Use of this model is deprecated.
40 title = models.CharField(max_length=255)
41 slug = models.SlugField(max_length=255)
43 def __unicode__(self):
50 #: 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.
51 value_content_type_limiter = ContentTypeRegistryLimiter()
54 def register_value_model(model):
55 """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
56 value_content_type_limiter.register_class(model)
59 register_value_model(Tag)
62 def unregister_value_model(model):
63 """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
64 value_content_type_limiter.unregister_class(model)
67 class AttributeValue(models.Model):
69 This is an abstract base class for models that can be used as values for :class:`Attribute`\ s.
71 AttributeValue subclasses are expected to supply access to a clean version of their value through an attribute called "value".
75 #: :class:`GenericRelation` to :class:`Attribute`
76 attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
78 def set_value(self, value):
79 """Given a ``value``, sets the appropriate fields so that it can be correctly stored in the database."""
80 raise NotImplementedError
82 def value_formfields(self, **kwargs):
84 Returns any formfields that would be used to construct an instance of this value.
86 :returns: A dictionary mapping field names to formfields.
90 raise NotImplementedError
92 def construct_instance(self, **kwargs):
93 """Applies cleaned data from the formfields generated by valid_formfields to oneself."""
94 raise NotImplementedError
96 def __unicode__(self):
97 return unicode(self.value)
103 #: An instance of :class:`.ContentTypeSubclassLimiter` which is used to track the content types which are considered valid value models for an :class:`Attribute`.
104 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
107 class JSONValue(AttributeValue):
108 """Stores a python object as a json string."""
109 value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null', db_index=True)
111 def __unicode__(self):
112 return force_unicode(self.value)
114 def value_formfields(self):
115 kwargs = {'initial': self.value_json}
116 field = self._meta.get_field('value')
117 return {field.name: field.formfield(**kwargs)}
119 def construct_instance(self, **kwargs):
120 field_name = self._meta.get_field('value').name
121 self.set_value(kwargs.pop(field_name, None))
123 def set_value(self, value):
130 class ForeignKeyValue(AttributeValue):
131 """Stores a generic relationship to an instance of any value content type (as defined by the :data:`value_content_type_limiter`)."""
132 content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
133 object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
134 value = generic.GenericForeignKey()
136 def value_formfields(self):
137 field = self._meta.get_field('content_type')
138 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
140 if self.content_type:
142 'initial': self.object_id,
144 'queryset': self.content_type.model_class()._default_manager.all()
146 fields['value'] = forms.ModelChoiceField(**kwargs)
149 def construct_instance(self, **kwargs):
150 field_name = self._meta.get_field('content_type').name
151 ct = kwargs.pop(field_name, None)
152 if ct is None or ct != self.content_type:
153 self.object_id = None
154 self.content_type = ct
156 value = kwargs.pop('value', None)
157 self.set_value(value)
159 self.content_type = ct
161 def set_value(self, value):
168 class ManyToManyValue(AttributeValue):
169 """Stores a generic relationship to many instances of any value content type (as defined by the :data:`value_content_type_limiter`)."""
170 content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
171 values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
173 def get_object_ids(self):
174 return self.values.values_list('object_id', flat=True)
175 object_ids = property(get_object_ids)
177 def set_value(self, value):
178 # Value must be a queryset. Watch out for ModelMultipleChoiceField;
179 # it returns its value as a list if empty.
181 self.content_type = ContentType.objects.get_for_model(value.model)
183 # Before we can fiddle with the many-to-many to foreignkeyvalues, we need
188 object_ids = value.values_list('id', flat=True)
190 # These lines shouldn't be necessary; however, if object_ids is an EmptyQuerySet,
191 # the code (specifically the object_id__in query) won't work without them. Unclear why...
192 # TODO: is this still the case?
194 self.values.all().delete()
196 self.values.exclude(object_id__in=object_ids, content_type=self.content_type).delete()
198 current_ids = self.object_ids
200 for object_id in object_ids:
201 if object_id in current_ids:
203 self.values.create(content_type=self.content_type, object_id=object_id)
206 if self.content_type is None:
209 # HACK to be safely explicit until http://code.djangoproject.com/ticket/15145 is resolved
210 object_ids = self.object_ids
211 manager = self.content_type.model_class()._default_manager
213 return manager.none()
214 return manager.filter(id__in=self.object_ids)
216 value = property(get_value, set_value)
218 def value_formfields(self):
219 field = self._meta.get_field('content_type')
220 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
222 if self.content_type:
224 'initial': self.object_ids,
226 'queryset': self.content_type.model_class()._default_manager.all()
228 fields['value'] = forms.ModelMultipleChoiceField(**kwargs)
231 def construct_instance(self, **kwargs):
232 field_name = self._meta.get_field('content_type').name
233 ct = kwargs.pop(field_name, None)
234 if ct is None or ct != self.content_type:
236 self.content_type = ct
238 value = kwargs.get('value', None)
240 value = self.content_type.model_class()._default_manager.none()
241 self.set_value(value)
242 construct_instance.alters_data = True
248 class Attribute(models.Model):
250 :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.
252 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.
255 entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
256 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
258 #: :class:`GenericForeignKey` to anything (generally an instance of an Entity subclass).
259 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
261 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)
262 value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
264 #: :class:`GenericForeignKey` to an instance of a subclass of :class:`AttributeValue` as determined by the :data:`attribute_value_limiter`.
265 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
267 #: :class:`CharField` containing a key (up to 255 characters) consisting of alphanumeric characters and underscores.
268 key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
270 def __unicode__(self):
271 return u'"%s": %s' % (self.key, self.value)
273 def set_value(self, value, value_class=JSONValue):
274 """Given a value and a value class, sets up self.value appropriately."""
275 if isinstance(self.value, value_class):
278 if isinstance(self.value, models.Model):
290 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
293 class EntityOptions(object):
294 def __init__(self, options):
295 if options is not None:
296 for key, value in options.__dict__.items():
297 setattr(self, key, value)
298 if not hasattr(self, 'proxy_fields'):
299 self.proxy_fields = []
301 def add_proxy_field(self, proxy_field):
302 self.proxy_fields.append(proxy_field)
305 class EntityBase(models.base.ModelBase):
306 def __new__(cls, name, bases, attrs):
307 entity_meta = attrs.pop('EntityMeta', None)
308 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
309 new.add_to_class('_entity_meta', EntityOptions(entity_meta))
310 entity_class_prepared.send(sender=new)
314 class Entity(models.Model):
315 """An abstract class that simplifies access to related attributes. Most models provided by Philo subclass Entity."""
316 __metaclass__ = EntityBase
318 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
320 def get_attribute_mapper(self, mapper=AttributeMapper):
322 Returns an :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly.
326 >>> attr = entity.attribute_set.get(key='spam')
329 >>> entity.attributes['spam']
334 attributes = property(get_attribute_mapper)
340 class TreeManager(models.Manager):
341 use_for_related_fields = True
343 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
345 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).
347 .. 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.
349 .. 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.
351 :param path: The path of the object
352 :param root: The object which will be considered the root of the search
353 :param absolute_result: Whether to return an absolute result or do a binary search
354 :param pathsep: The path separator used in ``path``
355 :param field: The field on the model which should be queried for ``path`` segment matching.
356 :returns: An instance if ``absolute_result`` is ``True`` or an (instance, remaining_path) tuple otherwise.
357 :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters.
361 segments = path.split(pathsep)
363 # Clean out blank segments. Handles multiple consecutive pathseps.
370 # Special-case a lack of segments. No queries necessary.
377 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
379 def make_query_kwargs(segments, root):
382 revsegs = list(segments)
385 for segment in revsegs:
386 kwargs["%s%s__exact" % (prefix, field)] = segment
390 kwargs[prefix[:-2]] = root
394 def find_obj(segments, depth, deepest_found=None):
395 if deepest_found is None:
398 deepest_level = deepest_found.get_level() + 1
400 deepest_level = deepest_found.get_level() - root.get_level()
402 obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found or root))
403 except self.model.DoesNotExist:
404 if not deepest_level and depth > 1:
405 # make sure there's a root node...
408 # Try finding one with half the path since the deepest find.
409 depth = (deepest_level + depth)/2
411 if deepest_level == depth:
412 # This should happen if nothing is found with any part of the given path.
413 if root is not None and deepest_found is None:
414 return root, pathsep.join(segments)
417 return find_obj(segments, depth, deepest_found)
421 deepest_level = obj.get_level() + 1
423 deepest_level = obj.get_level() - root.get_level()
425 # Could there be a deeper one?
426 if obj.is_leaf_node():
427 return obj, pathsep.join(segments[deepest_level:]) or None
429 depth += (len(segments) - depth)/2 or len(segments) - depth
431 if depth > deepest_level + obj.get_descendant_count():
432 depth = deepest_level + obj.get_descendant_count()
434 if deepest_level == depth:
435 return obj, pathsep.join(segments[deepest_level:]) or None
438 return find_obj(segments, depth, obj)
439 except self.model.DoesNotExist:
440 # Then this was the deepest.
441 return obj, pathsep.join(segments[deepest_level:])
444 return self.get(**make_query_kwargs(segments, root))
446 # Try a modified binary search algorithm. Feed the root in so that query complexity
447 # can be reduced. It might be possible to weight the search towards the beginning
448 # of the path, since short paths are more likely, but how far forward? It would
449 # need to shift depending on len(segments) - perhaps logarithmically?
450 return find_obj(segments, len(segments)/2 or len(segments))
453 class TreeModel(MPTTModel):
454 objects = TreeManager()
455 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
456 slug = models.SlugField(max_length=255)
458 def get_path(self, root=None, pathsep='/', field='slug'):
460 :param root: Only return the path since this object.
461 :param pathsep: The path separator to use when constructing an instance's path
462 :param field: The field to pull path information from for each ancestor.
463 :returns: A string representation of an object's path.
470 if root is not None and not self.is_descendant_of(root):
471 raise AncestorDoesNotExist(root)
473 qs = self.get_ancestors(include_self=True)
476 qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
478 return pathsep.join([getattr(parent, field, '?') for parent in qs])
479 path = property(get_path)
481 def __unicode__(self):
485 unique_together = (('parent', 'slug'),)
489 class TreeEntityBase(MPTTModelBase, EntityBase):
490 def __new__(meta, name, bases, attrs):
491 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
492 cls = EntityBase.__new__(meta, name, bases, attrs)
494 return meta.register(cls)
497 class TreeEntity(Entity, TreeModel):
498 """An abstract subclass of Entity which represents a tree relationship."""
500 __metaclass__ = TreeEntityBase
502 def get_attribute_mapper(self, mapper=None):
504 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.
508 >>> attr = entity.attribute_set.get(key='spam')
509 DoesNotExist: Attribute matching query does not exist.
510 >>> attr = entity.parent.attribute_set.get(key='spam')
513 >>> entity.attributes['spam']
519 mapper = TreeAttributeMapper
521 mapper = AttributeMapper
522 return super(TreeEntity, self).get_attribute_mapper(mapper)
523 attributes = property(get_attribute_mapper)