Tweaked Entity and TreeEntity to pass an actual queryset to QuerySetMapper instead...
[philo.git] / models / base.py
1 from django import forms
2 from django.db import models
3 from django.contrib.contenttypes.models import ContentType
4 from django.contrib.contenttypes import generic
5 from django.utils import simplejson as json
6 from django.core.exceptions import ObjectDoesNotExist
7 from philo.exceptions import AncestorDoesNotExist
8 from philo.models.fields import JSONField
9 from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
10 from philo.signals import entity_class_prepared
11 from philo.validators import json_validator
12 from UserDict import DictMixin
13
14
15 class Tag(models.Model):
16         name = models.CharField(max_length=255)
17         slug = models.SlugField(max_length=255, unique=True)
18         
19         def __unicode__(self):
20                 return self.name
21         
22         class Meta:
23                 app_label = 'philo'
24
25
26 class Titled(models.Model):
27         title = models.CharField(max_length=255)
28         slug = models.SlugField(max_length=255)
29         
30         def __unicode__(self):
31                 return self.title
32         
33         class Meta:
34                 abstract = True
35
36
37 value_content_type_limiter = ContentTypeRegistryLimiter()
38
39
40 def register_value_model(model):
41         value_content_type_limiter.register_class(model)
42
43
44 def unregister_value_model(model):
45         value_content_type_limiter.unregister_class(model)
46
47
48 class AttributeValue(models.Model):
49         attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
50         
51         @property
52         def attribute(self):
53                 return self.attribute_set.all()[0]
54         
55         def apply_data(self, data):
56                 raise NotImplementedError
57         
58         def value_formfield(self, **kwargs):
59                 raise NotImplementedError
60         
61         def __unicode__(self):
62                 return unicode(self.value)
63         
64         class Meta:
65                 abstract = True
66
67
68 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
69
70
71 class JSONValue(AttributeValue):
72         value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
73         
74         def __unicode__(self):
75                 return self.value_json
76         
77         def value_formfield(self, **kwargs):
78                 kwargs['initial'] = self.value_json
79                 return self._meta.get_field('value').formfield(**kwargs)
80         
81         def apply_data(self, cleaned_data):
82                 self.value = cleaned_data.get('value', None)
83         
84         class Meta:
85                 app_label = 'philo'
86
87
88 class ForeignKeyValue(AttributeValue):
89         content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
90         object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
91         value = generic.GenericForeignKey()
92         
93         def value_formfield(self, form_class=forms.ModelChoiceField, **kwargs):
94                 if self.content_type is None:
95                         return None
96                 kwargs.update({'initial': self.object_id, 'required': False})
97                 return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
98         
99         def apply_data(self, cleaned_data):
100                 if 'value' in cleaned_data and cleaned_data['value'] is not None:
101                         self.value = cleaned_data['value']
102                 else:
103                         self.content_type = cleaned_data.get('content_type', None)
104                         # If there is no value set in the cleaned data, clear the stored value.
105                         self.object_id = None
106         
107         class Meta:
108                 app_label = 'philo'
109
110
111 class ManyToManyValue(AttributeValue):
112         content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
113         values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
114         
115         def get_object_id_list(self):
116                 if not self.values.count():
117                         return []
118                 else:
119                         return self.values.values_list('object_id', flat=True)
120         
121         def get_value(self):
122                 if self.content_type is None:
123                         return None
124                 
125                 return self.content_type.model_class()._default_manager.filter(id__in=self.get_object_id_list())
126         
127         def set_value(self, value):
128                 # Value is probably a queryset - but allow any iterable.
129                 
130                 # These lines shouldn't be necessary; however, if value is an EmptyQuerySet,
131                 # the code won't work without them. Unclear why...
132                 if not value:
133                         value = []
134                 
135                 if isinstance(value, models.query.QuerySet):
136                         value = value.values_list('id', flat=True)
137                 
138                 self.values.filter(~models.Q(object_id__in=value)).delete()
139                 current = self.get_object_id_list()
140                 
141                 for v in value:
142                         if v in current:
143                                 continue
144                         self.values.create(content_type=self.content_type, object_id=v)
145         
146         value = property(get_value, set_value)
147         
148         def value_formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
149                 if self.content_type is None:
150                         return None
151                 kwargs.update({'initial': self.get_object_id_list(), 'required': False})
152                 return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
153         
154         def apply_data(self, cleaned_data):
155                 if 'value' in cleaned_data and cleaned_data['value'] is not None:
156                         self.value = cleaned_data['value']
157                 else:
158                         self.content_type = cleaned_data.get('content_type', None)
159                         # If there is no value set in the cleaned data, clear the stored value.
160                         self.value = []
161         
162         class Meta:
163                 app_label = 'philo'
164
165
166 class Attribute(models.Model):
167         entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
168         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
169         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
170         
171         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)
172         value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
173         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
174         
175         key = models.CharField(max_length=255)
176         
177         def __unicode__(self):
178                 return u'"%s": %s' % (self.key, self.value)
179         
180         class Meta:
181                 app_label = 'philo'
182                 unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
183
184
185 class QuerySetMapper(object, DictMixin):
186         def __init__(self, queryset, passthrough=None):
187                 self.queryset = queryset
188                 self.passthrough = passthrough
189         
190         def __getitem__(self, key):
191                 try:
192                         value = self.queryset.get(key__exact=key).value
193                 except ObjectDoesNotExist:
194                         if self.passthrough is not None:
195                                 return self.passthrough.__getitem__(key)
196                         raise KeyError
197                 else:
198                         if value is not None:
199                                 return value.value
200                         return value
201         
202         def keys(self):
203                 keys = set(self.queryset.values_list('key', flat=True).distinct())
204                 if self.passthrough is not None:
205                         keys |= set(self.passthrough.keys())
206                 return list(keys)
207
208
209 class EntityOptions(object):
210         def __init__(self, options):
211                 if options is not None:
212                         for key, value in options.__dict__.items():
213                                 setattr(self, key, value)
214                 if not hasattr(self, 'proxy_fields'):
215                         self.proxy_fields = []
216         
217         def add_proxy_field(self, proxy_field):
218                 self.proxy_fields.append(proxy_field)
219
220
221 class EntityBase(models.base.ModelBase):
222         def __new__(cls, name, bases, attrs):
223                 new = super(EntityBase, cls).__new__(cls, name, bases, attrs)
224                 entity_options = attrs.pop('EntityMeta', None)
225                 setattr(new, '_entity_meta', EntityOptions(entity_options))
226                 entity_class_prepared.send(sender=new)
227                 return new
228
229
230 class Entity(models.Model):
231         __metaclass__ = EntityBase
232         
233         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
234         
235         @property
236         def attributes(self):
237                 return QuerySetMapper(self.attribute_set.all())
238         
239         @property
240         def _added_attribute_registry(self):
241                 if not hasattr(self, '_real_added_attribute_registry'):
242                         self._real_added_attribute_registry = {}
243                 return self._real_added_attribute_registry
244         
245         @property
246         def _removed_attribute_registry(self):
247                 if not hasattr(self, '_real_removed_attribute_registry'):
248                         self._real_removed_attribute_registry = []
249                 return self._real_removed_attribute_registry
250         
251         def save(self, *args, **kwargs):
252                 super(Entity, self).save(*args, **kwargs)
253                 
254                 for key in self._removed_attribute_registry:
255                         self.attribute_set.filter(key__exact=key).delete()
256                 del self._removed_attribute_registry[:]
257                 
258                 for field, value in self._added_attribute_registry.items():
259                         try:
260                                 attribute = self.attribute_set.get(key__exact=field.key)
261                         except Attribute.DoesNotExist:
262                                 attribute = Attribute()
263                                 attribute.entity = self
264                                 attribute.key = field.key
265                         
266                         field.set_attribute_value(attribute, value)
267                         attribute.save()
268                 self._added_attribute_registry.clear()
269         
270         class Meta:
271                 abstract = True
272
273
274 class TreeManager(models.Manager):
275         use_for_related_fields = True
276         
277         def roots(self):
278                 return self.filter(parent__isnull=True)
279         
280         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
281                 """
282                 Returns the object with the path, or None if there is no object with that path,
283                 unless absolute_result is set to False, in which case it returns a tuple containing
284                 the deepest object found along the path, and the remainder of the path after that
285                 object as a string (or None in the case that there is no remaining path).
286                 """
287                 slugs = path.split(pathsep)
288                 obj = root
289                 remaining_slugs = list(slugs)
290                 remainder = None
291                 for slug in slugs:
292                         remaining_slugs.remove(slug)
293                         if slug: # ignore blank slugs, handles for multiple consecutive pathseps
294                                 try:
295                                         obj = self.get(slug__exact=slug, parent__exact=obj)
296                                 except self.model.DoesNotExist:
297                                         if absolute_result:
298                                                 obj = None
299                                         remaining_slugs.insert(0, slug)
300                                         remainder = pathsep.join(remaining_slugs)
301                                         break
302                 if obj:
303                         if absolute_result:
304                                 return obj
305                         else:
306                                 return (obj, remainder)
307                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
308
309
310 class TreeModel(models.Model):
311         objects = TreeManager()
312         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
313         slug = models.SlugField(max_length=255)
314         
315         def has_ancestor(self, ancestor):
316                 parent = self
317                 while parent:
318                         if parent == ancestor:
319                                 return True
320                         parent = parent.parent
321                 return False
322         
323         def get_path(self, root=None, pathsep='/', field='slug'):
324                 if root is not None and not self.has_ancestor(root):
325                         raise AncestorDoesNotExist(root)
326                 
327                 path = getattr(self, field, '?')
328                 parent = self.parent
329                 while parent and parent != root:
330                         path = getattr(parent, field, '?') + pathsep + path
331                         parent = parent.parent
332                 return path
333         path = property(get_path)
334         
335         def __unicode__(self):
336                 return self.path
337         
338         class Meta:
339                 unique_together = (('parent', 'slug'),)
340                 abstract = True
341
342
343 class TreeEntity(Entity, TreeModel):
344         @property
345         def attributes(self):
346                 if self.parent:
347                         return QuerySetMapper(self.attribute_set.all(), passthrough=self.parent.attributes)
348                 return super(TreeEntity, self).attributes
349         
350         class Meta:
351                 abstract = True