Switched Attribute ManyToManyValues to have a ManyToManyField to ForeignKeyValues...
[philo.git] / models / base.py
index 145c1ee..0077250 100644 (file)
@@ -1,3 +1,4 @@
+from django import forms
 from django.db import models
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
@@ -5,7 +6,7 @@ from django.utils import simplejson as json
 from django.core.exceptions import ObjectDoesNotExist
 from philo.exceptions import AncestorDoesNotExist
 from philo.models.fields import JSONField
-from philo.utils import ContentTypeRegistryLimiter
+from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
 from philo.signals import entity_class_prepared
 from philo.validators import json_validator
 from UserDict import DictMixin
@@ -44,56 +45,124 @@ def unregister_value_model(model):
        value_content_type_limiter.unregister_class(model)
 
 
-class JSONValue(models.Model):
+class AttributeValue(models.Model):
+       attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
+       
+       @property
+       def attribute(self):
+               return self.attribute_set.all()[0]
+       
+       def apply_data(self, data):
+               raise NotImplementedError
+       
+       def value_formfield(self, **kwargs):
+               raise NotImplementedError
+       
+       def __unicode__(self):
+               return unicode(self.value)
+       
+       class Meta:
+               abstract = True
+
+
+attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
+
+
+class JSONValue(AttributeValue):
        value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
        
        def __unicode__(self):
                return self.value_json
        
+       def value_formfield(self, **kwargs):
+               kwargs['initial'] = self.value_json
+               return self._meta.get_field('value').formfield(**kwargs)
+       
+       def apply_data(self, cleaned_data):
+               self.value = cleaned_data.get('value', None)
+       
        class Meta:
                app_label = 'philo'
 
 
-class ForeignKeyValue(models.Model):
-       content_type = models.ForeignKey(ContentType, related_name='foreign_key_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+class ForeignKeyValue(AttributeValue):
+       content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
        object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
        value = generic.GenericForeignKey()
        
-       def __unicode__(self):
-               return unicode(self.value)
+       def value_formfield(self, form_class=forms.ModelChoiceField, **kwargs):
+               if self.content_type is None:
+                       return None
+               kwargs.update({'initial': self.object_id, 'required': False})
+               return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
+       
+       def apply_data(self, cleaned_data):
+               if 'value' in cleaned_data and cleaned_data['value'] is not None:
+                       self.value = cleaned_data['value']
+               else:
+                       self.content_type = cleaned_data.get('content_type', None)
+                       # If there is no value set in the cleaned data, clear the stored value.
+                       self.object_id = None
        
        class Meta:
                app_label = 'philo'
 
 
-class ManyToManyValue(models.Model):
-       content_type = models.ForeignKey(ContentType, related_name='many_to_many_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
-       object_ids = models.CommaSeparatedIntegerField(max_length=300, verbose_name='Value IDs', null=True, blank=True)
+class ManyToManyValue(AttributeValue):
+       content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+       values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
+       
+       def get_object_id_list(self):
+               if not self.values.count():
+                       return []
+               else:
+                       return self.values.values_list('object_id', flat=True)
        
        def get_value(self):
-               return self.content_type.model_class()._default_manager.filter(id__in=self.object_ids)
+               if self.content_type is None:
+                       return None
+               
+               return self.content_type.model_class()._default_manager.filter(id__in=self.get_object_id_list())
        
        def set_value(self, value):
-               if not isinstance(value, models.query.QuerySet):
-                       raise TypeError("Value must be a QuerySet.")
-               self.content_type = ContentType.objects.get_for_model(value.model)
-               self.object_ids = ','.join(value.values_list('id', flat=True))
+               # Value is probably a queryset - but allow any iterable.
+               
+               # These lines shouldn't be necessary; however, if value is an EmptyQuerySet,
+               # the code won't work without them. Unclear why...
+               if not value:
+                       value = []
+               
+               if isinstance(value, models.query.QuerySet):
+                       value = value.values_list('id', flat=True)
+               
+               self.values.filter(~models.Q(object_id__in=value)).delete()
+               current = self.get_object_id_list()
+               
+               for v in value:
+                       if v in current:
+                               continue
+                       self.values.create(content_type=self.content_type, object_id=v)
        
        value = property(get_value, set_value)
        
-       def __unicode__(self):
-               return unicode(self.value)
+       def value_formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
+               if self.content_type is None:
+                       return None
+               kwargs.update({'initial': self.get_object_id_list(), 'required': False})
+               return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
+       
+       def apply_data(self, cleaned_data):
+               if 'value' in cleaned_data and cleaned_data['value'] is not None:
+                       self.value = cleaned_data['value']
+               else:
+                       self.content_type = cleaned_data.get('content_type', None)
+                       # If there is no value set in the cleaned data, clear the stored value.
+                       self.value = []
        
        class Meta:
                app_label = 'philo'
 
 
-attribute_value_limiter = ContentTypeRegistryLimiter()
-attribute_value_limiter.register_class(JSONValue)
-attribute_value_limiter.register_class(ForeignKeyValue)
-attribute_value_limiter.register_class(ManyToManyValue)
-
-
 class Attribute(models.Model):
        entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
        entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
@@ -105,28 +174,6 @@ class Attribute(models.Model):
        
        key = models.CharField(max_length=255)
        
-       def get_value_class(self, value):
-               if isinstance(value, models.query.QuerySet):
-                       return ManyToManyValue
-               elif isinstance(value, models.Model):
-                       return ForeignKeyValue
-               else:
-                       return JSONValue
-       
-       def set_value(self, value):
-               value_class = self.get_value_class(value)
-               
-               if self.value is None or value_class != self.value_content_type.model_class():
-                       if self.value is not None:
-                               self.value.delete()
-                       new_value = value_class()
-                       new_value.value = value
-                       new_value.save()
-                       self.value = new_value
-               else:
-                       self.value.value = value
-                       self.value.save()
-       
        def __unicode__(self):
                return u'"%s": %s' % (self.key, self.value)
        
@@ -142,11 +189,15 @@ class QuerySetMapper(object, DictMixin):
        
        def __getitem__(self, key):
                try:
-                       return self.queryset.get(key__exact=key).value
+                       value = self.queryset.get(key__exact=key).value
                except ObjectDoesNotExist:
                        if self.passthrough is not None:
                                return self.passthrough.__getitem__(key)
                        raise KeyError
+               else:
+                       if value is not None:
+                               return value.value
+                       return value
        
        def keys(self):
                keys = set(self.queryset.values_list('key', flat=True).distinct())
@@ -204,14 +255,15 @@ class Entity(models.Model):
                        self.attribute_set.filter(key__exact=key).delete()
                del self._removed_attribute_registry[:]
                
-               for key, value in self._added_attribute_registry.items():
+               for field, value in self._added_attribute_registry.items():
                        try:
-                               attribute = self.attribute_set.get(key__exact=key)
+                               attribute = self.attribute_set.get(key__exact=field.key)
                        except Attribute.DoesNotExist:
                                attribute = Attribute()
                                attribute.entity = self
-                               attribute.key = key
-                       attribute.set_value(value)
+                               attribute.key = field.key
+                       
+                       field.set_attribute_value(attribute, value)
                        attribute.save()
                self._added_attribute_registry.clear()
        
@@ -269,22 +321,15 @@ class TreeModel(models.Model):
                return False
        
        def get_path(self, root=None, pathsep='/', field='slug'):
-               if root is not None:
-                       if not self.has_ancestor(root):
-                               raise AncestorDoesNotExist(root)
-                       path = ''
-                       parent = self
-                       while parent and parent != root:
-                               path = getattr(parent, field, '?') + pathsep + path
-                               parent = parent.parent
-                       return path
-               else:
-                       path = getattr(self, field, '?')
-                       parent = self.parent
-                       while parent and parent != root:
-                               path = getattr(parent, field, '?') + pathsep + path
-                               parent = parent.parent
-                       return path
+               if root is not None and not self.has_ancestor(root):
+                       raise AncestorDoesNotExist(root)
+               
+               path = getattr(self, field, '?')
+               parent = self.parent
+               while parent and parent != root:
+                       path = getattr(parent, field, '?') + pathsep + path
+                       parent = parent.parent
+               return path
        path = property(get_path)
        
        def __unicode__(self):
@@ -302,11 +347,5 @@ class TreeEntity(Entity, TreeModel):
                        return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
                return super(TreeEntity, self).attributes
        
-       @property
-       def relationships(self):
-               if self.parent:
-                       return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
-               return super(TreeEntity, self).relationships
-       
        class Meta:
                abstract = True
\ No newline at end of file