Initial raw_id_fields support for proxy fields. Involves hacks to bypass model valida...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Mon, 24 Jan 2011 18:37:48 +0000 (13:37 -0500)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Tue, 25 Jan 2011 21:43:38 +0000 (16:43 -0500)
contrib/cowell/admin.py [new file with mode: 0644]
contrib/cowell/fields.py
contrib/cowell/forms.py
contrib/cowell/widgets.py [new file with mode: 0644]

diff --git a/contrib/cowell/admin.py b/contrib/cowell/admin.py
new file mode 100644 (file)
index 0000000..94df7cb
--- /dev/null
@@ -0,0 +1,107 @@
+from django import forms
+from django.contrib import admin
+from philo.contrib.cowell.fields import ForeignKeyAttribute, ManyToManyAttribute
+from philo.contrib.cowell.forms import ProxyFieldForm, proxy_fields_for_entity_model
+from philo.contrib.cowell.widgets import ForeignKeyAttributeRawIdWidget, ManyToManyAttributeRawIdWidget
+from philo.admin import EntityAdmin
+
+
+def hide_proxy_fields(hidden, attrs, attname, attvalue, proxy_fields):
+       attvalue = set(attvalue)
+       proxy_fields = set(proxy_fields)
+       if proxy_fields & attvalue:
+               hidden[attname] = list(attvalue)
+               attrs[attname] = list(attvalue - proxy_fields)
+
+
+class ProxyFieldAdminMetaclass(EntityAdmin.__metaclass__):
+       def __new__(cls, name, bases, attrs):
+               # HACK to bypass model validation for proxy fields by masking them as readonly fields
+               form = attrs.get('form')
+               if form:
+                       opts = form._meta
+                       if issubclass(form, ProxyFieldForm) and opts.model:
+                               proxy_fields = proxy_fields_for_entity_model(opts.model).keys()
+                               readonly_fields = attrs.pop('readonly_fields', ())
+                               cls._real_readonly_fields = readonly_fields
+                               attrs['readonly_fields'] = list(readonly_fields) + proxy_fields
+                               
+                               # Additional HACKS to handle raw_id_fields and other attributes that the admin
+                               # uses model._meta.get_field to validate.
+                               hidden_attributes = {}
+                               hide_proxy_fields(hidden_attributes, attrs, 'raw_id_fields', attrs.pop('raw_id_fields', ()), proxy_fields)
+                               attrs['_hidden_attributes'] = hidden_attributes
+               #END HACK
+               return EntityAdmin.__metaclass__.__new__(cls, name, bases, attrs)
+
+
+class ProxyFieldAdmin(EntityAdmin):
+       __metaclass__ = ProxyFieldAdminMetaclass
+       #form = ProxyFieldForm
+       
+       def __init__(self, *args, **kwargs):
+               # HACK PART 2 restores the actual readonly fields etc. on __init__.
+               self.readonly_fields = self.__class__._real_readonly_fields
+               if hasattr(self, '_hidden_attributes'):
+                       for name, value in self._hidden_attributes.items():
+                               setattr(self, name, value)
+               # END HACK
+               super(ProxyFieldAdmin, self).__init__(*args, **kwargs)
+       
+       def formfield_for_dbfield(self, db_field, **kwargs):
+               """
+               Override the default behavior to provide special formfields for EntityProxyFields.
+               Essentially clones the ForeignKey/ManyToManyField special behavior for the Attribute versions.
+               """
+               if not db_field.choices and isinstance(db_field, (ForeignKeyAttribute, ManyToManyAttribute)):
+                       request = kwargs.pop("request", None)
+                       # Combine the field kwargs with any options for formfield_overrides.
+                       # Make sure the passed in **kwargs override anything in
+                       # formfield_overrides because **kwargs is more specific, and should
+                       # always win.
+                       if db_field.__class__ in self.formfield_overrides:
+                               kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
+                       
+                       # Get the correct formfield.
+                       if isinstance(db_field, ManyToManyAttribute):
+                               formfield = self.formfield_for_manytomanyattribute(db_field, request, **kwargs)
+                       elif isinstance(db_field, ForeignKeyAttribute):
+                               formfield = self.formfield_for_foreignkeyattribute(db_field, request, **kwargs)
+                       
+                       # For non-raw_id fields, wrap the widget with a wrapper that adds
+                       # extra HTML -- the "add other" interface -- to the end of the
+                       # rendered output. formfield can be None if it came from a
+                       # OneToOneField with parent_link=True or a M2M intermediary.
+                       # TODO: Implement this.
+                       #if formfield and db_field.name not in self.raw_id_fields:
+                       #       formfield.widget = admin.widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field, self.admin_site)
+                       
+                       return formfield
+               return super(ProxyFieldAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+       
+       def formfield_for_foreignkeyattribute(self, db_field, request=None, **kwargs):
+               """Get a form field for a ForeignKeyAttribute field."""
+               db = kwargs.get('using')
+               if db_field.name in self.raw_id_fields:
+                       kwargs['widget'] = ForeignKeyAttributeRawIdWidget(db_field, db)
+               #TODO: Add support for radio fields
+               #elif db_field.name in self.radio_fields:
+               #       kwargs['widget'] = widgets.AdminRadioSelect(attrs={
+               #               'class': get_ul_class(self.radio_fields[db_field.name]),
+               #       })
+               #       kwargs['empty_label'] = db_field.blank and _('None') or None
+               
+               return db_field.formfield(**kwargs)
+       
+       def formfield_for_manytomanyattribute(self, db_field, request=None, **kwargs):
+               """Get a form field for a ManyToManyAttribute field."""
+               db = kwargs.get('using')
+               
+               if db_field.name in self.raw_id_fields:
+                       kwargs['widget'] = ManyToManyAttributeRawIdWidget(db_field, using=db)
+                       kwargs['help_text'] = ''
+               #TODO: Add support for filtered fields.
+               #elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
+               #       kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
+               
+               return db_field.formfield(**kwargs)
\ No newline at end of file
index d9f3c8a..c5db56d 100644 (file)
@@ -1,6 +1,6 @@
 """
 """
-The Attributes defined in this file can be assigned as fields on a proxy of
-subclass of philo.models.Entity. They act like any other model fields,
+The Attributes defined in this file can be assigned as fields on a
+subclass of philo.models.Entity. They act like any other model fields,
 but instead of saving their data to the database, they save it to
 attributes related to a model instance. Additionally, a new attribute will
 be created for an instance if and only if the field's value has been set.
 but instead of saving their data to the database, they save it to
 attributes related to a model instance. Additionally, a new attribute will
 be created for an instance if and only if the field's value has been set.
@@ -14,10 +14,8 @@ Example::
        
        class ThingProxy(Thing):
                improvised = JSONAttribute(models.BooleanField)
        
        class ThingProxy(Thing):
                improvised = JSONAttribute(models.BooleanField)
-               
-               class Meta:
-                       proxy = True
 """
 """
+from itertools import tee
 from django import forms
 from django.core.exceptions import FieldError
 from django.db import models
 from django import forms
 from django.core.exceptions import FieldError
 from django.db import models
@@ -34,11 +32,12 @@ ATTRIBUTE_REGISTRY = '_attribute_registry'
 
 
 class EntityProxyField(object):
 
 
 class EntityProxyField(object):
-       def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, *args, **kwargs):
+       def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs):
                self.verbose_name = verbose_name
                self.help_text = help_text
                self.default = default
                self.editable = editable
                self.verbose_name = verbose_name
                self.help_text = help_text
                self.default = default
                self.editable = editable
+               self._choices = choices or []
        
        def actually_contribute_to_class(self, sender, **kwargs):
                sender._entity_meta.add_proxy_field(self)
        
        def actually_contribute_to_class(self, sender, **kwargs):
                sender._entity_meta.add_proxy_field(self)
@@ -71,6 +70,14 @@ class EntityProxyField(object):
        
        def has_default(self):
                return self.default is not NOT_PROVIDED
        
        def has_default(self):
                return self.default is not NOT_PROVIDED
+       
+       def _get_choices(self):
+               if hasattr(self._choices, 'next'):
+                       choices, self._choices = tee(self._choices)
+                       return choices
+               else:
+                       return self._choices
+       choices = property(_get_choices)
 
 
 class AttributeFieldDescriptor(object):
 
 
 class AttributeFieldDescriptor(object):
@@ -215,6 +222,15 @@ class ForeignKeyAttribute(AttributeField):
        def value_from_object(self, obj):
                relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
                return getattr(relobj, 'pk', None)
        def value_from_object(self, obj):
                relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
                return getattr(relobj, 'pk', None)
+       
+       @property
+       def to(self):
+               """Spoof being a rel from a ForeignKey."""
+               return self.model
+       
+       def get_related_field(self):
+               """Again, spoof being a rel from a ForeignKey."""
+               return self.model._meta.pk
 
 
 class ManyToManyAttribute(ForeignKeyAttribute):
 
 
 class ManyToManyAttribute(ForeignKeyAttribute):
index c4b573e..0b5a0c6 100644 (file)
@@ -3,7 +3,7 @@ from django.utils.datastructures import SortedDict
 from philo.utils import fattr
 
 
 from philo.utils import fattr
 
 
-__all__ = ('EntityForm',)
+__all__ = ('ProxyFieldForm',)
 
 
 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)):
@@ -37,15 +37,21 @@ def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widge
 
 # BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved
 
 
 # BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved
 
-class EntityFormBase(ModelForm):
+class ProxyFieldFormBase(ModelForm):
        pass
 
 _old_metaclass_new = ModelFormMetaclass.__new__
 
 def _new_metaclass_new(cls, name, bases, attrs):
        pass
 
 _old_metaclass_new = ModelFormMetaclass.__new__
 
 def _new_metaclass_new(cls, name, bases, attrs):
+       formfield_callback = attrs.get('formfield_callback', lambda f, **kwargs: f.formfield(**kwargs))
        new_class = _old_metaclass_new(cls, name, bases, attrs)
        new_class = _old_metaclass_new(cls, name, bases, attrs)
-       if issubclass(new_class, EntityFormBase) and new_class._meta.model:
-               new_class.base_fields.update(proxy_fields_for_entity_model(new_class._meta.model, new_class._meta.fields, new_class._meta.exclude, new_class._meta.widgets)) # don't pass in formfield_callback
+       opts = new_class._meta
+       if issubclass(new_class, ProxyFieldFormBase) and opts.model:
+               # "override" proxy fields with declared fields by excluding them if there's a name conflict.
+               exclude = (list(opts.exclude or []) + new_class.declared_fields.keys()) or None
+               proxy_fields = proxy_fields_for_entity_model(opts.model, opts.fields, exclude, opts.widgets, formfield_callback) # don't pass in formfield_callback
+               new_class.proxy_fields = proxy_fields
+               new_class.base_fields.update(proxy_fields)
        return new_class
 
 ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
        return new_class
 
 ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
@@ -53,7 +59,7 @@ ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
 # END HACK
 
 
 # END HACK
 
 
-class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK
+class ProxyFieldForm(ProxyFieldFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK
        def __init__(self, *args, **kwargs):
                initial = kwargs.pop('initial', None)
                instance = kwargs.get('instance', None)
        def __init__(self, *args, **kwargs):
                initial = kwargs.pop('initial', None)
                instance = kwargs.get('instance', None)
@@ -70,12 +76,12 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it
                if initial is not None:
                        new_initial.update(initial)
                kwargs['initial'] = new_initial
                if initial is not None:
                        new_initial.update(initial)
                kwargs['initial'] = new_initial
-               super(EntityForm, self).__init__(*args, **kwargs)
+               super(ProxyFieldForm, self).__init__(*args, **kwargs)
        
        @fattr(alters_data=True)
        def save(self, commit=True):
                cleaned_data = self.cleaned_data
        
        @fattr(alters_data=True)
        def save(self, commit=True):
                cleaned_data = self.cleaned_data
-               instance = super(EntityForm, self).save(commit=False)
+               instance = super(ProxyFieldForm, self).save(commit=False)
                
                for f in instance._entity_meta.proxy_fields:
                        if not f.editable or not f.name in cleaned_data:
                
                for f in instance._entity_meta.proxy_fields:
                        if not f.editable or not f.name in cleaned_data:
@@ -90,24 +96,4 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it
                        instance.save()
                        self.save_m2m()
                
                        instance.save()
                        self.save_m2m()
                
-               return instance
-
-       
-       def apply_data(self, cleaned_data):
-               self.value = cleaned_data.get('value', None)
-       
-       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
-       
-       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 = []
\ No newline at end of file
+               return instance
\ No newline at end of file
diff --git a/contrib/cowell/widgets.py b/contrib/cowell/widgets.py
new file mode 100644 (file)
index 0000000..cff09f4
--- /dev/null
@@ -0,0 +1,9 @@
+from django.contrib.admin.widgets import ForeignKeyRawIdWidget, ManyToManyRawIdWidget
+
+
+class ForeignKeyAttributeRawIdWidget(ForeignKeyRawIdWidget):
+       pass
+
+
+class ManyToManyAttributeRawIdWidget(ManyToManyRawIdWidget):
+       pass
\ No newline at end of file