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, AttributeMapper, TreeAttributeMapper
15 from philo.validators import json_validator
18 class Tag(models.Model):
19 """A simple, generic model for tagging."""
20 #: A CharField (max length 255) which contains the name of the tag.
21 name = models.CharField(max_length=255)
22 #: A CharField (max length 255) which contains the tag's unique slug.
23 slug = models.SlugField(max_length=255, unique=True)
25 def __unicode__(self):
26 """Returns the value of the :attr:`name` field"""
34 class Titled(models.Model):
35 title = models.CharField(max_length=255)
36 slug = models.SlugField(max_length=255)
38 def __unicode__(self):
45 #: 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.
46 value_content_type_limiter = ContentTypeRegistryLimiter()
49 def register_value_model(model):
50 """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
51 value_content_type_limiter.register_class(model)
54 register_value_model(Tag)
57 def unregister_value_model(model):
58 """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
59 value_content_type_limiter.unregister_class(model)
62 class AttributeValue(models.Model):
64 This is an abstract base class for models that can be used as values for :class:`Attribute`\ s.
66 AttributeValue subclasses are expected to supply access to a clean version of their value through an attribute called "value".
70 #: :class:`GenericRelation` to :class:`Attribute`
71 attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
73 def set_value(self, value):
74 """Given a ``value``, sets the appropriate fields so that it can be correctly stored in the database."""
75 raise NotImplementedError
77 def value_formfields(self, **kwargs):
79 Returns any formfields that would be used to construct an instance of this value.
81 :returns: A dictionary mapping field names to formfields.
85 raise NotImplementedError
87 def construct_instance(self, **kwargs):
88 """Applies cleaned data from the formfields generated by valid_formfields to oneself."""
89 raise NotImplementedError
91 def __unicode__(self):
92 return unicode(self.value)
98 #: An instance of :class:`ContentTypeSubclassLimiter` which is used to track the content types which are considered valid value models for an :class:`Attribute`.
99 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
102 class JSONValue(AttributeValue):
103 """Stores a python object as a json string."""
104 value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null', db_index=True)
106 def __unicode__(self):
107 return force_unicode(self.value)
109 def value_formfields(self):
110 kwargs = {'initial': self.value_json}
111 field = self._meta.get_field('value')
112 return {field.name: field.formfield(**kwargs)}
114 def construct_instance(self, **kwargs):
115 field_name = self._meta.get_field('value').name
116 self.set_value(kwargs.pop(field_name, None))
118 def set_value(self, value):
125 class ForeignKeyValue(AttributeValue):
126 """Stores a generic relationship to an instance of any value content type (as defined by the :data:`value_content_type_limiter`)."""
127 content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
128 object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
129 value = generic.GenericForeignKey()
131 def value_formfields(self):
132 field = self._meta.get_field('content_type')
133 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
135 if self.content_type:
137 'initial': self.object_id,
139 'queryset': self.content_type.model_class()._default_manager.all()
141 fields['value'] = forms.ModelChoiceField(**kwargs)
144 def construct_instance(self, **kwargs):
145 field_name = self._meta.get_field('content_type').name
146 ct = kwargs.pop(field_name, None)
147 if ct is None or ct != self.content_type:
148 self.object_id = None
149 self.content_type = ct
151 value = kwargs.pop('value', None)
152 self.set_value(value)
154 self.content_type = ct
156 def set_value(self, value):
163 class ManyToManyValue(AttributeValue):
164 """Stores a generic relationship to many instances of any value content type (as defined by the :data:`value_content_type_limiter`)."""
165 content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
166 values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
168 def get_object_ids(self):
169 return self.values.values_list('object_id', flat=True)
170 object_ids = property(get_object_ids)
172 def set_value(self, value):
173 # Value must be a queryset. Watch out for ModelMultipleChoiceField;
174 # it returns its value as a list if empty.
176 self.content_type = ContentType.objects.get_for_model(value.model)
178 # Before we can fiddle with the many-to-many to foreignkeyvalues, we need
183 object_ids = value.values_list('id', flat=True)
185 # These lines shouldn't be necessary; however, if object_ids is an EmptyQuerySet,
186 # the code (specifically the object_id__in query) won't work without them. Unclear why...
187 # TODO: is this still the case?
189 self.values.all().delete()
191 self.values.exclude(object_id__in=object_ids, content_type=self.content_type).delete()
193 current_ids = self.object_ids
195 for object_id in object_ids:
196 if object_id in current_ids:
198 self.values.create(content_type=self.content_type, object_id=object_id)
201 if self.content_type is None:
204 # HACK to be safely explicit until http://code.djangoproject.com/ticket/15145 is resolved
205 object_ids = self.object_ids
206 manager = self.content_type.model_class()._default_manager
208 return manager.none()
209 return manager.filter(id__in=self.object_ids)
211 value = property(get_value, set_value)
213 def value_formfields(self):
214 field = self._meta.get_field('content_type')
215 fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))}
217 if self.content_type:
219 'initial': self.object_ids,
221 'queryset': self.content_type.model_class()._default_manager.all()
223 fields['value'] = forms.ModelMultipleChoiceField(**kwargs)
226 def construct_instance(self, **kwargs):
227 field_name = self._meta.get_field('content_type').name
228 ct = kwargs.pop(field_name, None)
229 if ct is None or ct != self.content_type:
231 self.content_type = ct
233 value = kwargs.get('value', None)
235 value = self.content_type.model_class()._default_manager.none()
236 self.set_value(value)
237 construct_instance.alters_data = True
243 class Attribute(models.Model):
244 """Represents an arbitrary key/value pair on an arbitrary :class:`Model` where the key consists of word characters and the value is a subclass of :class:`AttributeValue`."""
245 entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
246 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
248 #: :class:`GenericForeignKey` to anything (generally an instance of an Entity subclass).
249 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
251 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)
252 value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
254 #: :class:`GenericForeignKey` to an instance of a subclass of :class:`AttributeValue` as determined by the :data:`attribute_value_limiter`.
255 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
257 #: :class:`CharField` containing a key (up to 255 characters) consisting of alphanumeric characters and underscores.
258 key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
260 def __unicode__(self):
261 return u'"%s": %s' % (self.key, self.value)
263 def set_value(self, value, value_class=JSONValue):
264 """Given a value and a value class, sets up self.value appropriately."""
265 if isinstance(self.value, value_class):
268 if isinstance(self.value, models.Model):
280 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
283 class EntityOptions(object):
284 def __init__(self, options):
285 if options is not None:
286 for key, value in options.__dict__.items():
287 setattr(self, key, value)
288 if not hasattr(self, 'proxy_fields'):
289 self.proxy_fields = []
291 def add_proxy_field(self, proxy_field):
292 self.proxy_fields.append(proxy_field)
295 class EntityBase(models.base.ModelBase):
296 def __new__(cls, name, bases, attrs):
297 entity_meta = attrs.pop('EntityMeta', None)
298 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
299 new.add_to_class('_entity_meta', EntityOptions(entity_meta))
300 entity_class_prepared.send(sender=new)
304 class Entity(models.Model):
305 """An abstract class that simplifies access to related attributes. Most models provided by Philo subclass Entity."""
306 __metaclass__ = EntityBase
308 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
310 def get_attribute_mapper(self, mapper=AttributeMapper):
312 Returns a dictionary-like object which can be used to retrieve related :class:`Attribute`\ s' values directly.
316 >>> attr = entity.attribute_set.get(key='spam')
319 >>> entity.attributes['spam']
324 attributes = property(get_attribute_mapper)
330 class TreeManager(models.Manager):
331 use_for_related_fields = True
333 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
335 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).
337 .. 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.
339 .. 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.
341 :param path: The path of the object
342 :param root: The object which will be considered the root of the search
343 :param absolute_result: Whether to return an absolute result or do a binary search
344 :param pathsep: The path separator used in ``path``
345 :param field: The field on the model which should be queried for ``path`` segment matching.
346 :returns: An instance if ``absolute_result`` is ``True`` or an (instance, remaining_path) tuple otherwise.
347 :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters.
351 segments = path.split(pathsep)
353 # Clean out blank segments. Handles multiple consecutive pathseps.
360 # Special-case a lack of segments. No queries necessary.
367 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
369 def make_query_kwargs(segments, root):
372 revsegs = list(segments)
375 for segment in revsegs:
376 kwargs["%s%s__exact" % (prefix, field)] = segment
380 kwargs[prefix[:-2]] = root
384 def find_obj(segments, depth, deepest_found=None):
385 if deepest_found is None:
388 deepest_level = deepest_found.get_level() + 1
390 deepest_level = deepest_found.get_level() - root.get_level()
392 obj = self.get(**make_query_kwargs(segments[deepest_level:depth], deepest_found or root))
393 except self.model.DoesNotExist:
394 if not deepest_level and depth > 1:
395 # make sure there's a root node...
398 # Try finding one with half the path since the deepest find.
399 depth = (deepest_level + depth)/2
401 if deepest_level == depth:
402 # This should happen if nothing is found with any part of the given path.
403 if root is not None and deepest_found is None:
404 return root, pathsep.join(segments)
407 return find_obj(segments, depth, deepest_found)
411 deepest_level = obj.get_level() + 1
413 deepest_level = obj.get_level() - root.get_level()
415 # Could there be a deeper one?
416 if obj.is_leaf_node():
417 return obj, pathsep.join(segments[deepest_level:]) or None
419 depth += (len(segments) - depth)/2 or len(segments) - depth
421 if depth > deepest_level + obj.get_descendant_count():
422 depth = deepest_level + obj.get_descendant_count()
424 if deepest_level == depth:
425 return obj, pathsep.join(segments[deepest_level:]) or None
428 return find_obj(segments, depth, obj)
429 except self.model.DoesNotExist:
430 # Then this was the deepest.
431 return obj, pathsep.join(segments[deepest_level:])
434 return self.get(**make_query_kwargs(segments, root))
436 # Try a modified binary search algorithm. Feed the root in so that query complexity
437 # can be reduced. It might be possible to weight the search towards the beginning
438 # of the path, since short paths are more likely, but how far forward? It would
439 # need to shift depending on len(segments) - perhaps logarithmically?
440 return find_obj(segments, len(segments)/2 or len(segments))
443 class TreeModel(MPTTModel):
444 objects = TreeManager()
445 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
446 slug = models.SlugField(max_length=255)
448 def get_path(self, root=None, pathsep='/', field='slug'):
450 :param root: Only return the path since this object.
451 :param pathsep: The path separator to use when constructing an instance's path
452 :param field: The field to pull path information from for each ancestor.
453 :returns: A string representation of an object's path.
460 if root is not None and not self.is_descendant_of(root):
461 raise AncestorDoesNotExist(root)
463 qs = self.get_ancestors(include_self=True)
466 qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
468 return pathsep.join([getattr(parent, field, '?') for parent in qs])
469 path = property(get_path)
471 def __unicode__(self):
475 unique_together = (('parent', 'slug'),)
479 class TreeEntityBase(MPTTModelBase, EntityBase):
480 def __new__(meta, name, bases, attrs):
481 attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
482 cls = EntityBase.__new__(meta, name, bases, attrs)
484 return meta.register(cls)
487 class TreeEntity(Entity, TreeModel):
488 """An abstract subclass of Entity which represents a tree relationship."""
490 __metaclass__ = TreeEntityBase
492 def get_attribute_mapper(self, mapper=None):
494 Returns a dictionary-like object which can be used to retrieve related :class:`Attribute`\ s' values directly. If an attribute with a given key is not related to the :class:`Entity`, then the object will check the parent's attributes.
498 >>> attr = entity.attribute_set.get(key='spam')
499 DoesNotExist: Attribute matching query does not exist.
500 >>> attr = entity.parent.attribute_set.get(key='spam')
503 >>> entity.attributes['spam']
509 mapper = TreeAttributeMapper
511 mapper = AttributeMapper
512 return super(TreeEntity, self).get_attribute_mapper(mapper)
513 attributes = property(get_attribute_mapper)