Django Problem inheriting formfield_callback in Mo

2020-07-08 07:11发布

问题:

I've only been using Django for a couple of weeks now, so I may be approaching this all kinds of wrong, but:

I have a base ModelForm that I put some boilerplate stuff in to keep things as DRY as possible, and all of my actual ModelForms just subclass that base form. This is working great for error_css_class = 'error' and required_css_class = 'required' but formfield_callback = add_css_classes isn't working like I would expect it to.

forms.py

# snippet I found
def add_css_classes(f, **kwargs):
    field = f.formfield(**kwargs)
    if field and 'class' not in field.widget.attrs:
        field.widget.attrs['class'] = '%s' % field.__class__.__name__.lower()
    return field

class BaseForm(forms.ModelForm):
    formfield_callback = add_css_classes  # not working

    error_css_class = 'error'
    required_css_class = 'required'
    class Meta:
        pass

class TimeLogForm(BaseForm):
    # I want the next line to be in the parent class
    # formfield_callback = add_css_classes
    class Meta(BaseForm.Meta):
        model = TimeLog

The end goal is to slap some jquery datetime pickers on forms with a class of datefield/timefield/datetimefield. I want all of the date time fields within the app to use the same widget, so I opted to do it this way than explicitly doing it for each field in every model. Adding an extra line to each form class isn't that big of a deal, but it just bugged me that I couldn't figure it out. Digging around in the django source showed this is probably doing something I'm not understanding:

django.forms.models

class ModelFormMetaclass(type):
    def __new__(cls, name, bases, attrs):
        formfield_callback = attrs.pop('formfield_callback', None)

But I don't know how __init__ and __new__ are all intermangled. In BaseForm I tried overriding __init__ and setting formfield_callback before and after the call to super, but I'm guessing it needs to be somewhere in args or kwargs.

回答1:

__new__ is called before object construction. Actually this is a factory method that returns the instance of a newly constructed object.

So there there are 3 key lines in ModelFormMetaclass:

formfield_callback = attrs.pop('formfield_callback', None) #1
fields = fields_for_model(opts.model, opts.fields, 
                                      opts.exclude, opts.widgets, formfield_callback) #2
new_class.base_fields = fields #3

In the class we attach base_fields to our form.

Now let's look to ModelForm class:

class ModelForm(BaseModelForm):
    __metaclass__ = ModelFormMetaclass

This means that ModelFormMetaclass.__new__(...) will be called when we create a ModelForm instance to change the structure of the future instance. And attrs of __new__ (def __new__(cls, name, bases, attrs)) in ModelFormMetaclass is a dict of all attributes of ModelForm class.

So decision is to create new InheritedFormMetaclass for our case (inheriting it from ModelFormMetaclass). Don't forget to call new of the parent in InheritedFormMetaclass. Then create our BaseForm class and say:

__metaclass__ = InheritedFormMetaclass

In __new__(...) implementation of InheritedFormMetaclass we could do all we want.

If my answer is not detailed enough please let me know with help of comments.



回答2:

You may set widgets class like this:

class TimeLogForm(BaseForm):
# I want the next line to be in the parent class
# formfield_callback = add_css_classes
class Meta(BaseForm.Meta):
    model = TimeLog
    widgets = {
            'some_fields' : SomeWidgets(attrs={'class' : 'myclass'})
    }


回答3:

For what you're trying to accomplish, I think you're better off just looping through the fields on form init. For example,

class BaseForm(forms.ModelForm):
  def __init__(self, *args, **kwargs):
    super(BaseForm, self).__init__(*args, **kwargs)
    for name, field in self.fields.items():
      field.widget.attrs['class'] = 'error'

Clearly you'll need a little more logic for your specific case. If you want to use the approach that sergzach suggested (overkill for your particular problem I think), here's some code for you that will call formfield_callback on the base class in the case the subclass doesn't define it.

baseform_formfield_callback(field):
    # do some stuff
    return field.formfield()

class BaseModelFormMetaclass(forms.models.ModelFormMetaclass):
    def __new__(cls, name, bases, attrs):
        if not attrs.has_key('formfield_callback'):
            attrs['formfield_callback'] = baseform_formfield_callback
        new_class = super(BaseModelFormMetaclass, cls).__new__(
            cls, name, bases, attrs)
        return new_class

class BaseModelForm(forms.ModelForm):
    __metaclass__ = OrganizationModelFormMetaclass
    # other form stuff

Finally, you might wanna look into crispy forms: https://github.com/maraujop/django-crispy-forms



回答4:

sergzach is correct that you have to use metaclasses; overriding __init__ is not enough. The reason is that the metaclass for ModelForm (which will be called for all ModelForm subclasses unless you specify another metaclass in a subclass) takes the class definition, and using the values in the class definition creates a class with class attributes. For example, both META.fields and our formfield_callback is used to create form Fields with various option (like which widget).

That means AFAIU formfield_callback is a parameter to the metaclass used when creating your custom model form class, not some value used at runtime when actual form instances are created. That makes placing formfield_callback in __init__ useless.

I solved a similiar problem with a custom metaclass like

from django.forms.models import ModelFormMetaclass

class MyModelFormMetaclass(ModelFormMetaclass):
    def __new__(cls,name,bases,attrs):
    attrs['formfield_callback']=my_callback_function
    return super(MyModelFormMetaclass,cls).__new__(cls,name,bases,attrs)

and in the base class for all my model forms setting the metaclass

class MyBaseModelForm(ModelForm):
    __metaclass__=MyModelFormMetaclass
    ...

which can be used like (at least in Django 1.6)

class MyConcreteModelForm(MyBaseModelForm):
    # no need setting formfield_callback here
    ...