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.
@@ -14,10 +14,8 @@ Example::
        
        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
@@ -34,11 +32,12 @@ ATTRIBUTE_REGISTRY = '_attribute_registry'
 
 
 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._choices = choices or []
        
        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 _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):
@@ -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)
+       
+       @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):
index c4b573e..0b5a0c6 100644 (file)
@@ -3,7 +3,7 @@ from django.utils.datastructures import SortedDict
 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)):
@@ -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
 
-class EntityFormBase(ModelForm):
+class ProxyFieldFormBase(ModelForm):
        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)
-       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)
@@ -53,7 +59,7 @@ ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
 # 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)
@@ -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
-               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
-               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:
@@ -90,24 +96,4 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it
                        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