From 3a134160f8e7e663c5807cd44296f5f3a88b8a36 Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Thu, 24 Mar 2011 17:58:57 -0400 Subject: [PATCH] Resolves issue #71 for the admin interface by working around the use of modelform_factory. If any code uses modelform_factory elsewhere with an entity form, the same issue will arise. Also compatible with 1.2.X. --- admin/base.py | 9 +++++ forms/entities.py | 90 ++++++++++++++++++++++++++++++++++------------- models/base.py | 4 +-- 3 files changed, 77 insertions(+), 26 deletions(-) diff --git a/admin/base.py b/admin/base.py index 75fa336..b616290 100644 --- a/admin/base.py +++ b/admin/base.py @@ -133,6 +133,15 @@ class EntityAdmin(admin.ModelAdmin): # kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) return db_field.formfield(**kwargs) + + def get_form(self, request, obj=None, **kwargs): + """ + Ensures that the form's proxy fields are included in its base fields. + This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved + """ + form = super(EntityAdmin, self).get_form(request, obj, **kwargs) + form.base_fields.update(form.proxy_fields) + return form class TreeAdmin(MPTTModelAdmin): diff --git a/forms/entities.py b/forms/entities.py index b6259a3..43094b9 100644 --- a/forms/entities.py +++ b/forms/entities.py @@ -1,4 +1,4 @@ -from django.forms.models import ModelFormMetaclass, ModelForm +from django.forms.models import ModelFormMetaclass, ModelForm, ModelFormOptions from django.utils.datastructures import SortedDict from philo.utils import fattr @@ -6,7 +6,7 @@ from philo.utils import fattr __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=None): field_list = [] ignored = [] opts = entity_model._entity_meta @@ -21,7 +21,14 @@ def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widge kwargs = {'widget': widgets[f.name]} else: kwargs = {} - formfield = formfield_callback(f, **kwargs) + + if formfield_callback is None: + formfield = f.formfield(**kwargs) + elif not callable(formfield_callback): + raise TypeError('formfield_callback must be a function or callable') + else: + formfield = formfield_callback(f, **kwargs) + if formfield: field_list.append((f.name, formfield)) else: @@ -35,31 +42,66 @@ def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widge return field_dict -# BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved - -class EntityFormBase(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) - opts = new_class._meta - if issubclass(new_class, EntityFormBase) 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 +class EntityFormMetaclass(ModelFormMetaclass): + def __new__(cls, name, bases, attrs): + try: + parents = [b for b in bases if issubclass(b, EntityForm)] + except NameError: + # We are defining EntityForm itself + parents = None + sup = super(EntityFormMetaclass, cls) + + if not parents: + # Then there's no business trying to use proxy fields. + return sup.__new__(cls, name, bases, attrs) + + # Preemptively make a meta so we can screen out any proxy fields. + # After http://code.djangoproject.com/ticket/14082 has been resolved, + # perhaps switch to setting proxy fields as "declared"? + _opts = ModelFormOptions(attrs.get('Meta', None)) + + # Make another copy of opts to spoof the proxy fields not being there. + opts = ModelFormOptions(attrs.get('Meta', None)) + if opts.model: + formfield_callback = attrs.get('formfield_callback', None) + proxy_fields = proxy_fields_for_entity_model(opts.model, opts.fields, opts.exclude, opts.widgets, formfield_callback) + else: + proxy_fields = {} + + # Screen out all proxy fields from the meta + opts.fields = list(opts.fields or []) + opts.exclude = list(opts.exclude or []) + + for field in proxy_fields.keys(): + try: + opts.fields.remove(field) + except ValueError: + pass + try: + opts.exclude.remove(field) + except ValueError: + pass + opts.fields = opts.fields or None + opts.exclude = opts.exclude or None + + # Put in the new Meta. + attrs['Meta'] = opts + + new_class = sup.__new__(cls, name, bases, attrs) + + intersections = set(new_class.declared_fields.keys()) & set(proxy_fields.keys()) + for key in intersections: + proxy_fields.pop(key) + new_class.proxy_fields = proxy_fields + new_class._meta = _opts new_class.base_fields.update(proxy_fields) - return new_class - -ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new) + return new_class -# END HACK - -class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK +class EntityForm(ModelForm): + __metaclass__ = EntityFormMetaclass + def __init__(self, *args, **kwargs): initial = kwargs.pop('initial', None) instance = kwargs.get('instance', None) diff --git a/models/base.py b/models/base.py index faac89b..af1e880 100644 --- a/models/base.py +++ b/models/base.py @@ -271,9 +271,9 @@ class EntityOptions(object): class EntityBase(models.base.ModelBase): def __new__(cls, name, bases, attrs): + entity_meta = attrs.pop('EntityMeta', None) new = super(EntityBase, cls).__new__(cls, name, bases, attrs) - entity_options = attrs.pop('EntityMeta', None) - setattr(new, '_entity_meta', EntityOptions(entity_options)) + new.add_to_class('_entity_meta', EntityOptions(entity_meta)) entity_class_prepared.send(sender=new) return new -- 2.20.1