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 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
 
 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.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):
 
 
 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 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')
 
 
 __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):
                sender._entity_meta.add_proxy_field(self)
        
        def contribute_to_class(self, cls, name):
-               from philo.models.base import Entity
                if issubclass(cls, 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('_', ' ')
                        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):
                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):
                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__:
                        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]
        
                
                return instance.__dict__[self.field.name]
        
@@ -98,13 +100,13 @@ class AttributeFieldDescriptor(object):
                
                registry = self.get_registry(instance)
                registry['added'].add(self.field)
                
                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)
        
        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)
 
 
                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()
                
                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:
                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
                        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:
                        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()
                        
                                        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
                        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.")
        
                "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):
 
 
 class JSONAttribute(AttributeField):
+       value_class = JSONValue
+       
        def __init__(self, field_template=None, **kwargs):
                super(JSONAttribute, self).__init__(**kwargs)
                if field_template is None:
        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)
                        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):
 
 
 class ForeignKeyAttribute(AttributeField):
+       value_class = ForeignKeyValue
+       
        def __init__(self, model, limit_choices_to=None, **kwargs):
                super(ForeignKeyAttribute, self).__init__(**kwargs)
                self.model = model
        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)
        
                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):
 
 
 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__))
        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 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 django.forms.models import ModelFormMetaclass, ModelForm
 from django.utils.datastructures import SortedDict
-from philo.models import Attribute
 from philo.utils import fattr
 
 
 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)):
 
 
 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.
                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)
        
        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):
        
        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()
                
                
                # 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)
        
        
        value = property(get_value, set_value)
        
@@ -184,7 +191,7 @@ class ManyToManyValue(AttributeValue):
                
                if self.content_type:
                        kwargs = {
                
                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()
                        }
                                '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:
                        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
        
                        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 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):
 
 
 class TemplateField(models.TextField):