Moved container forms and attribute forms into admin. Moved AttributeFields and Entit...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Fri, 21 Jan 2011 22:09:26 +0000 (17:09 -0500)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Fri, 21 Jan 2011 22:09:26 +0000 (17:09 -0500)
admin/base.py
admin/forms/__init__.py [new file with mode: 0644]
admin/forms/attributes.py [new file with mode: 0644]
admin/forms/containers.py [moved from forms/containers.py with 100% similarity]
admin/pages.py
contrib/cowell/__init__.py [new file with mode: 0644]
contrib/cowell/fields.py [moved from models/fields/attributes.py with 84% similarity]
contrib/cowell/forms.py [moved from forms/entities.py with 56% similarity]
forms/__init__.py
models/base.py
models/fields.py [moved from models/fields/__init__.py with 97% similarity]

index 100eb31..acba9c3 100644 (file)
@@ -5,7 +5,7 @@ from django.http import HttpResponse
 from django.utils import simplejson as json
 from django.utils.html import escape
 from philo.models import Tag, Attribute
-from philo.forms.entities import AttributeForm, AttributeInlineFormSet
+from philo.admin.forms.attributes import AttributeForm, AttributeInlineFormSet
 from philo.admin.widgets import TagFilteredSelectMultiple
 from mptt.admin import MPTTModelAdmin
 
diff --git a/admin/forms/__init__.py b/admin/forms/__init__.py
new file mode 100644 (file)
index 0000000..1906380
--- /dev/null
@@ -0,0 +1,2 @@
+from philo.admin.forms.attributes import *
+from philo.admin.forms.containers import *
\ No newline at end of file
diff --git a/admin/forms/attributes.py b/admin/forms/attributes.py
new file mode 100644 (file)
index 0000000..fc77d0f
--- /dev/null
@@ -0,0 +1,66 @@
+from django.contrib.contenttypes.generic import BaseGenericInlineFormSet
+from django.contrib.contenttypes.models import ContentType
+from django.forms.models import ModelForm
+from philo.models import Attribute
+
+
+__all__ = ('AttributeForm', 'AttributeInlineFormSet')
+
+
+class AttributeForm(ModelForm):
+       """
+       This class handles an attribute's fields as well as the fields for its value (if there is one.)
+       The fields defined will vary depending on the value type, but the fields for defining the value
+       (i.e. value_content_type and value_object_id) will always be defined. Except that value_object_id
+       will never be defined. BLARGH!
+       """
+       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.
+               value = self.instance.value
+               self._cached_value_ct = self.instance.value_content_type
+               self._cached_value = value
+               
+               # If there is a value, pull in its fields.
+               if value is not None:
+                       self.value_fields = value.value_formfields()
+                       self.fields.update(self.value_fields)
+       
+       def save(self, *args, **kwargs):
+               # At this point, the cleaned_data has already been stored on self.instance.
+               
+               if self.instance.value_content_type != self._cached_value_ct:
+                       # The value content type has changed. Clear the old value, if there was one.
+                       if self._cached_value:
+                               self._cached_value.delete()
+                       
+                       # Clear the submitted value, if any.
+                       self.cleaned_data.pop('value', None)
+                       
+                       # Now create a new value instance so that on next instantiation, the form will
+                       # know what fields to add.
+                       if self.instance.value_content_type is not None:
+                               self.instance.value = self.instance.value_content_type.model_class().objects.create()
+               elif self.instance.value is not None:
+                       # The value content type is the same, but one of the value fields has changed.
+                       
+                       # Use construct_instance to apply the changes from the cleaned_data to the value instance.
+                       fields = self.value_fields.keys()
+                       if set(fields) & set(self.changed_data):
+                               self.instance.value.construct_instance(**dict([(key, self.cleaned_data[key]) for key in fields]))
+                               self.instance.value.save()
+               
+               return super(AttributeForm, self).save(*args, **kwargs)
+       
+       class Meta:
+               model = Attribute
+
+
+class AttributeInlineFormSet(BaseGenericInlineFormSet):
+       "Necessary to force the GenericInlineFormset to use the form's save method for new objects."
+       def save_new(self, form, commit):
+               setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk)
+               setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk)
+               return form.save()
\ No newline at end of file
index 0a09c03..13d4098 100644 (file)
@@ -4,7 +4,7 @@ from django import forms
 from philo.admin.base import COLLAPSE_CLASSES, TreeAdmin
 from philo.admin.nodes import ViewAdmin
 from philo.models.pages import Page, Template, Contentlet, ContentReference
-from philo.forms.containers import *
+from philo.admin.forms.containers import *
 
 
 class ContentletInline(admin.StackedInline):
diff --git a/contrib/cowell/__init__.py b/contrib/cowell/__init__.py
new file mode 100644 (file)
index 0000000..710d164
--- /dev/null
@@ -0,0 +1,5 @@
+"""
+Cowell handles the code necessary for creating AttributeFields on Entities. This can be used
+to give the appearance of fields added to models without the needing to change the schema or
+to define multiple models due to minor differences in requirements.
+"""
\ No newline at end of file
similarity index 84%
rename from models/fields/attributes.py
rename to contrib/cowell/fields.py
index f85fb32..d9f3c8a 100644 (file)
@@ -24,6 +24,7 @@ from django.db import models
 from django.db.models.fields import NOT_PROVIDED
 from django.utils.text import capfirst
 from philo.signals import entity_class_prepared
+from philo.models import ManyToManyValue, JSONValue, ForeignKeyValue, Attribute, Entity
 
 
 __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
@@ -43,9 +44,8 @@ class EntityProxyField(object):
                sender._entity_meta.add_proxy_field(self)
        
        def contribute_to_class(self, cls, name):
-               from philo.models.base import Entity
                if issubclass(cls, Entity):
-                       self.name = name
+                       self.name = self.attname = name
                        self.model = cls
                        if self.verbose_name is None and name:
                                self.verbose_name = name.replace('_', ' ')
@@ -65,6 +65,8 @@ class EntityProxyField(object):
                return form_class(**defaults)
        
        def value_from_object(self, obj):
+               """The return value of this method will be used by the EntityForm as
+               this field's initial value."""
                return getattr(obj, self.name)
        
        def has_default(self):
@@ -85,7 +87,7 @@ class AttributeFieldDescriptor(object):
                        return self
                
                if self.field.name not in instance.__dict__:
-                       instance.__dict__[self.field.name] = instance.attributes[self.field.attribute_key]
+                       instance.__dict__[self.field.name] = instance.attributes.get(self.field.attribute_key, None)
                
                return instance.__dict__[self.field.name]
        
@@ -98,13 +100,13 @@ class AttributeFieldDescriptor(object):
                
                registry = self.get_registry(instance)
                registry['added'].add(self.field)
-               registry['removed'].remove(self.field)
+               registry['removed'].discard(self.field)
        
        def __delete__(self, instance):
                del instance.__dict__[self.field.name]
                
                registry = self.get_registry(instance)
-               registry['added'].remove(self.field)
+               registry['added'].discard(self.field)
                registry['removed'].add(self.field)
 
 
@@ -113,16 +115,15 @@ def process_attribute_fields(sender, instance, created, **kwargs):
                registry = instance.__dict__[ATTRIBUTE_REGISTRY]
                instance.attribute_set.filter(key__in=[field.attribute_key for field in registry['removed']]).delete()
                
-               from philo.models import Attribute
                for field in registry['added']:
                        try:
-                               attribute = self.attribute_set.get(key=field.key)
+                               attribute = instance.attribute_set.get(key=field.attribute_key)
                        except Attribute.DoesNotExist:
                                attribute = Attribute()
                                attribute.entity = instance
-                               attribute.key = field.key
+                               attribute.key = field.attribute_key
                        
-                       value_class = field.get_value_class()
+                       value_class = field.value_class
                        if isinstance(attribute.value, value_class):
                                value = attribute.value
                        else:
@@ -130,7 +131,7 @@ def process_attribute_fields(sender, instance, created, **kwargs):
                                        attribute.value.delete()
                                value = value_class()
                        
-                       value.set_value(field.value_from_object(instance))
+                       value.set_value(getattr(instance, field.name, None))
                        value.save()
                        
                        attribute.value = value
@@ -161,11 +162,14 @@ class AttributeField(EntityProxyField):
                "Confirm that the value is valid or raise an appropriate error."
                raise NotImplementedError("validate_value must be implemented by AttributeField subclasses.")
        
-       def get_value_class(self):
-               raise NotImplementedError("get_value_class must be implemented by AttributeField subclasses.")
+       @property
+       def value_class(self):
+               raise AttributeError("value_class must be defined on AttributeField subclasses.")
 
 
 class JSONAttribute(AttributeField):
+       value_class = JSONValue
+       
        def __init__(self, field_template=None, **kwargs):
                super(JSONAttribute, self).__init__(**kwargs)
                if field_template is None:
@@ -185,20 +189,11 @@ class JSONAttribute(AttributeField):
                        defaults['initial'] = self.default
                defaults.update(kwargs)
                return self.field_template.formfield(**defaults)
-       
-       def get_value_class(self):
-               from philo.models import JSONValue
-               return JSONValue
-       
-       # Not sure what this is doing - keep eyes open!
-       #def value_from_object(self, obj):
-       #       try:
-       #               return getattr(obj, self.name)
-       #       except AttributeError:
-       #               return None
 
 
 class ForeignKeyAttribute(AttributeField):
+       value_class = ForeignKeyValue
+       
        def __init__(self, model, limit_choices_to=None, **kwargs):
                super(ForeignKeyAttribute, self).__init__(**kwargs)
                self.model = model
@@ -217,19 +212,14 @@ class ForeignKeyAttribute(AttributeField):
                defaults.update(kwargs)
                return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
        
-       def get_value_class(self):
-               from philo.models import ForeignKeyValue
-               return ForeignKeyValue
-       
-       #def value_from_object(self, obj):
-       #       try:
-       #               relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
-       #       except AttributeError:
-       #               return None
-       #       return getattr(relobj, 'pk', None)
+       def value_from_object(self, obj):
+               relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
+               return getattr(relobj, 'pk', None)
 
 
 class ManyToManyAttribute(ForeignKeyAttribute):
+       value_class = ManyToManyValue
+       
        def validate_value(self, value):
                if not isinstance(value, models.query.QuerySet) or value.model != self.model:
                        raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.model.__name__))
@@ -237,6 +227,9 @@ class ManyToManyAttribute(ForeignKeyAttribute):
        def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
                return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
        
-       def get_value_class(self):
-               from philo.models import ManyToManyValue
-               return ManyToManyValue
\ No newline at end of file
+       def value_from_object(self, obj):
+               qs = super(ForeignKeyAttribute, self).value_from_object(obj)
+               try:
+                       return qs.values_list('pk', flat=True)
+               except:
+                       return []
\ No newline at end of file
similarity index 56%
rename from forms/entities.py
rename to contrib/cowell/forms.py
index a392e45..c4b573e 100644 (file)
@@ -1,13 +1,9 @@
-from django import forms
-from django.contrib.contenttypes.generic import BaseGenericInlineFormSet
-from django.contrib.contenttypes.models import ContentType
 from django.forms.models import ModelFormMetaclass, ModelForm
 from django.utils.datastructures import SortedDict
-from philo.models import Attribute
 from philo.utils import fattr
 
 
-__all__ = ('EntityForm', 'AttributeForm', 'AttributeInlineFormSet')
+__all__ = ('EntityForm',)
 
 
 def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
@@ -114,62 +110,4 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it
                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 AttributeForm(ModelForm):
-       """
-       This class handles an attribute's fields as well as the fields for its value (if there is one.)
-       The fields defined will vary depending on the value type, but the fields for defining the value
-       (i.e. value_content_type and value_object_id) will always be defined. Except that value_object_id
-       will never be defined. BLARGH!
-       """
-       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.
-               value = self.instance.value
-               self._cached_value_ct = self.instance.value_content_type
-               self._cached_value = value
-               
-               # If there is a value, pull in its fields.
-               if value is not None:
-                       self.value_fields = value.value_formfields()
-                       self.fields.update(self.value_fields)
-       
-       def save(self, *args, **kwargs):
-               # At this point, the cleaned_data has already been stored on self.instance.
-               
-               if self.instance.value_content_type != self._cached_value_ct:
-                       # The value content type has changed. Clear the old value, if there was one.
-                       if self._cached_value:
-                               self._cached_value.delete()
-                       
-                       # Clear the submitted value, if any.
-                       self.cleaned_data.pop('value', None)
-                       
-                       # Now create a new value instance so that on next instantiation, the form will
-                       # know what fields to add.
-                       if self.instance.value_content_type is not None:
-                               self.instance.value = self.instance.value_content_type.model_class().objects.create()
-               elif self.instance.value is not None:
-                       # The value content type is the same, but one of the value fields has changed.
-                       
-                       # Use construct_instance to apply the changes from the cleaned_data to the value instance.
-                       fields = self.value_fields.keys()
-                       if set(fields) & set(self.changed_data):
-                               self.instance.value.construct_instance(**dict([(key, self.cleaned_data[key]) for key in fields]))
-                               self.instance.value.save()
-               
-               return super(AttributeForm, self).save(*args, **kwargs)
-       
-       class Meta:
-               model = Attribute
-
-
-class AttributeInlineFormSet(BaseGenericInlineFormSet):
-       "Necessary to force the GenericInlineFormset to use the form's save method for new objects."
-       def save_new(self, form, commit):
-               setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk)
-               setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk)
-               return form.save()
\ No newline at end of file
+                       self.value = []
\ No newline at end of file
index e69de29..9e60966 100644 (file)
@@ -0,0 +1 @@
+from philo.forms.fields import *
\ No newline at end of file
index 20693b7..3bcf394 100644 (file)
@@ -140,41 +140,48 @@ 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):
-               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 get_object_ids(self):
+               return self.values.values_list('object_id', flat=True)
+       object_ids = property(get_object_ids)
        
        def set_value(self, value):
-               # Value is probably a queryset - but allow any iterable.
+               # Value must be a queryset. Watch out for ModelMultipleChoiceField;
+               # it returns its value as a list if empty.
                
-               # These lines shouldn't be necessary; however, if value is an EmptyQuerySet,
-               # the code (specifically the object_id__in query) won't work without them. Unclear why...
-               if not value:
-                       value = []
+               self.content_type = ContentType.objects.get_for_model(value.model)
                
                # Before we can fiddle with the many-to-many to foreignkeyvalues, we need
                # a pk.
                if self.pk is None:
                        self.save()
                
-               if isinstance(value, models.query.QuerySet):
-                       value = value.values_list('id', flat=True)
+               object_ids = value.values_list('id', flat=True)
                
-               self.values.filter(~models.Q(object_id__in=value)).delete()
-               current = self.get_object_id_list()
+               # These lines shouldn't be necessary; however, if object_ids is an EmptyQuerySet,
+               # the code (specifically the object_id__in query) won't work without them. Unclear why...
+               # TODO: is this still the case?
+               if not object_ids:
+                       self.values.all().delete()
+               else:
+                       self.values.exclude(object_id__in=object_ids, content_type=self.content_type).delete()
+                       
+                       current_ids = self.object_ids
+                       
+                       for object_id in object_ids:
+                               if object_id in current_ids:
+                                       continue
+                               self.values.create(content_type=self.content_type, object_id=object_id)
+       
+       def get_value(self):
+               if self.content_type is None:
+                       return None
                
-               for v in value:
-                       if v in current:
-                               continue
-                       self.values.create(content_type=self.content_type, object_id=v)
+               # HACK to be safely explicit until http://code.djangoproject.com/ticket/15145 is resolved
+               object_ids = self.object_ids
+               manager = self.content_type.model_class()._default_manager
+               if not object_ids:
+                       return manager.none()
+               return manager.filter(id__in=self.object_ids)
        
        value = property(get_value, set_value)
        
@@ -184,7 +191,7 @@ class ManyToManyValue(AttributeValue):
                
                if self.content_type:
                        kwargs = {
-                               'initial': self.get_object_id_list(),
+                               'initial': self.object_ids,
                                'required': False,
                                'queryset': self.content_type.model_class()._default_manager.all()
                        }
@@ -198,7 +205,9 @@ class ManyToManyValue(AttributeValue):
                        self.values.clear()
                        self.content_type = ct
                else:
-                       value = kwargs.get('value', self.content_type.model_class()._default_manager.none())
+                       value = kwargs.get('value', None)
+                       if not value:
+                               value = self.content_type.model_class()._default_manager.none()
                        self.set_value(value)
        construct_instance.alters_data = True
        
similarity index 97%
rename from models/fields/__init__.py
rename to models/fields.py
index a22e161..0289e57 100644 (file)
@@ -3,7 +3,6 @@ from django.db import models
 from django.utils import simplejson as json
 from philo.forms.fields import JSONFormField
 from philo.validators import TemplateValidator, json_validator
-from philo.models.fields.attributes import *
 
 
 class TemplateField(models.TextField):