Polished AttributeForm to better handle changes to an attribute instance. Introduced...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Tue, 12 Oct 2010 21:44:43 +0000 (17:44 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Tue, 12 Oct 2010 21:49:06 +0000 (17:49 -0400)
forms.py
models/base.py
templates/admin/philo/edit_inline/tabular_attribute.html

index c864f31..69273f0 100644 (file)
--- a/forms.py
+++ b/forms.py
@@ -98,6 +98,12 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it
 class AttributeForm(ModelForm):
        def __init__(self, *args, **kwargs):
                super(AttributeForm, self).__init__(*args, **kwargs)
+               
+               # This is necessary because model forms store changes to self.instance in their clean method.
+               # Mutter mutter.
+               self._cached_value_ct = self.instance.value_content_type
+               self._cached_value = self.instance.value
+               
                if self.instance.value is not None:
                        value_field = self.instance.value.value_formfield()
                        if value_field:
@@ -106,19 +112,39 @@ class AttributeForm(ModelForm):
                                self.fields['content_type'] = self.instance.value._meta.get_field('content_type').formfield(initial=getattr(self.instance.value.content_type, 'pk', None))
        
        def save(self, *args, **kwargs):
-               instance = super(AttributeForm, self).save(*args, **kwargs)
-               
-               if self.cleaned_data['value_content_type'] != self.instance.value_content_type:
+               # At this point, the cleaned_data has already been stored on self.instance.
+               if self.instance.value_content_type != self._cached_value_ct:
                        if self.instance.value is not None:
-                               self.instance.value.delete()
-                               del(self.cleaned_data['value'])
-               elif 'content_type' in self.cleaned_data and self.cleaned_data['content_type'] != self.instance.value.content_type:
-                       self.instance.value.content_type = self.cleaned_data['content_type']
+                               self._cached_value.delete()
+                               if 'value' in self.cleaned_data:
+                                       del(self.cleaned_data['value'])
+                       
+                       if self.instance.value_content_type is not None:
+                               # Make a blank value of the new type! Run special code for content_type attributes.
+                               if hasattr(self.instance.value_content_type.model_class(), 'content_type'):
+                                       if self._cached_value and hasattr(self._cached_value, 'content_type'):
+                                               new_ct = self._cached_value.content_type
+                                       else:
+                                               new_ct = None
+                                       new_value = self.instance.value_content_type.model_class().objects.create(content_type=new_ct)
+                               else:
+                                       new_value = self.instance.value_content_type.model_class().objects.create()
+                               
+                               new_value.apply_data(self.cleaned_data)
+                               new_value.save()
+                               self.instance.value = new_value
+               else:
+                       # The value type is the same, but one of the fields has changed.
+                       # Check to see if the changed value was the content type. We have to check the
+                       # cleaned_data because self.instance.value.content_type was overridden.
+                       if hasattr(self.instance.value, 'content_type') and 'content_type' in self.cleaned_data and 'value' in self.cleaned_data and (not hasattr(self._cached_value, 'content_type') or self._cached_value.content_type != self.cleaned_data['content_type']):
+                               self.cleaned_data['value'] = None
+                       
+                       self.instance.value.apply_data(self.cleaned_data)
                        self.instance.value.save()
-               elif 'value' in self.cleaned_data:
-                       self.instance.set_value(self.cleaned_data['value'])
                
-               return instance
+               super(AttributeForm, self).save(*args, **kwargs)
+               return self.instance
        
        class Meta:
                model = Attribute
index cd25357..5ca782a 100644 (file)
@@ -6,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
@@ -45,66 +45,103 @@ def unregister_value_model(model):
        value_content_type_limiter.unregister_class(model)
 
 
-class JSONValue(models.Model):
+class AttributeValue(models.Model):
+       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, *args, **kwargs):
+       def value_formfield(self, **kwargs):
                kwargs['initial'] = self.value_json
-               return self._meta.get_field('value').formfield(*args, **kwargs)
+               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):
+class ForeignKeyValue(AttributeValue):
        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)
        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):
+class ManyToManyValue(AttributeValue):
        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)
        
+       def get_object_id_list(self):
+               if not self.object_ids:
+                       return []
+               else:
+                       return self.object_ids.split(',')
+       
        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 value is None:
+                       self.object_ids = ""
+                       return
                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))
+               self.object_ids = ','.join([`value` for value in value.values_list('id', flat=True)])
        
        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):
+               self.value = cleaned_data.get('value', None)
        
        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')
@@ -125,6 +162,7 @@ class Attribute(models.Model):
                        return JSONValue
        
        def set_value(self, value):
+               # is this useful? The best way of doing it?
                value_class = self.get_value_class(value)
                
                if self.value is None or value_class != self.value_content_type.model_class():
index defdffb..22a4ee3 100644 (file)
@@ -55,8 +55,8 @@
             {% endfor %}
           {% endfor %}
         {% endfor %}
-        <td>{{ inline_admin_form.form.content_type }}</td>
-        <td>{{ inline_admin_form.form.value }}</td>
+        <td>{% with inline_admin_form.form.content_type as field %}{{ field.errors.as_ul }}{{ field }}{% endwith %}</td>
+        <td>{% with inline_admin_form.form.value as field %}{{ field.errors.as_ul }}{{ field }}{% endwith %}</td>
         {% if inline_admin_formset.formset.can_delete %}
           <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
         {% endif %}