开发者

Django: Reuse form fields without inheriting?

开发者 https://www.devze.com 2023-02-14 11:00 出处:网络
If I have two forms, based on different base classes (say, Form and ModelForm), but I want to use a few fields in both, can I reuse them in a DRY way?

If I have two forms, based on different base classes (say, Form and ModelForm), but I want to use a few fields in both, can I reuse them in a DRY way?

Consider the following scenario:

class AfricanSwallowForm(forms.ModelForm):
    airspeed_velocity = forms.IntegerField(some_important_details_here)
    is_migratory = forms.BooleanField(more_important_details)

    class Meta:
        model = AfricanBird

class EuropeanSwallowForm(forms.Form):
    airspeed_velocity = forms.IntegerField(some_important_details_here)
    is_migratory = forms.BooleanField(more_important_details)

....is t开发者_如何学Chere a way I can just reuse the fields airspeed_velocity and is_migratory? Imagine I have a couple dozen of these type of forms. The code will be soaking if I write these over and over again.

(Assume, for the purposes of this question, that I can't or won't turn airspeed_velocity and is_migratory into fields of the model AfricanBird.)


You could use multiple inheritance aka mixins, to factor out the fields that are used in both Form and ModelForm.

class SwallowFormFields:
    airspeed_velocity = forms.IntegerField( ... )
    is_migratory = forms.BooleanField( ... )

class AfricanSwallowForm(forms.ModelForm, SwallowFormFields):
    class Meta:
        model = AfricanBird

class EuropeanSwallowForm(forms.Form, SwallowFormFields):
    pass

UPDATE:

Since this does not work with Django metaprogramming, you either need to create a custom __init__ constructor that adds the inherited fields to the object's fields list or you can add the references explicitly inside the class definition:

class SwallowFormFields:
    airspeed_velocity = forms.IntegerField()
    is_migratory = forms.BooleanField()

class AfricanSwallowForm(forms.ModelForm):
    airspeed_velocity = SwallowFormFields.airspeed_velocity
    is_migratory = SwallowFormFields.is_migratory
    class Meta:
        model = AfricanSwallow

class EuropeanSwallowForm(forms.Form):
    airspeed_velocity = SwallowFormFields.airspeed_velocity
    is_migratory = SwallowFormFields.is_migratory

UPDATE:

Of course you don't have to nest your shared fields into a class -- you could also simply define them as globals ...

airspeed_velocity = forms.IntegerField()
is_migratory = forms.BooleanField()

class AfricanSwallowForm(forms.ModelForm):
    airspeed_velocity = airspeed_velocity
    is_migratory = is_migratory
    class Meta:
        model = AfricanSwallow

class EuropeanSwallowForm(forms.Form):
    airspeed_velocity = airspeed_velocity
    is_migratory = is_migratory

UPDATE:

Okay, if you really want to DRY to the max, you have to go with the metaclasses.

So here is how you may do it:

from django.forms.models import ModelForm, ModelFormMetaclass
from django.forms.forms import get_declared_fields, DeclarativeFieldsMetaclass
from django.utils.copycompat import deepcopy

class MixinFormMetaclass(ModelFormMetaclass, DeclarativeFieldsMetaclass):
    def __new__(cls, name, bases, attrs):

        # default __init__ that calls all base classes
        def init_all(self, *args, **kwargs):
            for base in bases:
                super(base, self).__init__(*args, **kwargs)
        attrs.setdefault('__init__', init_all)

        # collect declared fields
        attrs['declared_fields'] = get_declared_fields(bases, attrs, False)

        # create the class
        new_cls = super(MixinFormMetaclass, cls).__new__(cls, name, bases, attrs)
        return new_cls

class MixinForm(object):
    __metaclass__ = MixinFormMetaclass
    def __init__(self, *args, **kwargs):
        self.fields = deepcopy(self.declared_fields)

You can now derive your collections of formfields from MixinForm like this:

class SwallowFormFields(MixinForm):
    airspeed_velocity = forms.IntegerField()
    is_migratory = forms.BooleanField()

class MoreFormFields(MixinForm):
    is_endangered = forms.BooleanField()

Then add them to the list of base classes like this:

class EuropeanSwallowForm(forms.Form, SwallowFormFields, MoreFormFields):
    pass

class AfricanSwallowForm(forms.ModelForm, SwallowFormFields):
    class Meta:
        model = AfricanSwallow

So what does it do?

  • The metaclass collects all the fields declared in your MixinForm
  • It then adds custom __init__ constructors, to make sure that the __init__ method of the MixinForm gets magically called. (Otherwise you would have to call it explicitly.)
  • MixinForm.__init__ copies the declared fields int the field attribute

Please note that I am neither a Python guru nor a django developer, and that metaclasses are dangerous. So if you encounter weird behaviour better stick with the more verbose approach above :)

Good Luck!


How about a factory-style approach?

def form_factory(class_name, base, field_dict):
    always_has = {
        'airspeed_velocity': forms.IntegerField(some_important_details_here),
        'is_migratory': forms.BooleanField(more_important_details)
    }
    always_has.update(field_dict)
    return type(class_name, (base,), always_has)

def meta_factory(form_model):
    class Meta:
        model = form_model
    return Meta

AfricanSwallowForm = form_factory('AfricanSwallowForm', forms.ModelForm, {
        'other' = forms.IntegerField(some_important_details_here),
        'Meta': meta_factory(AfricanBird),
    })

EuropeanSwallowForm = form_factory('EuropeanSwallowForm', forms.Form, {
        'and_a_different' = forms.IntegerField(some_important_details_here),
    })

For that matter, you could modify the factory function here to look into an existing form class and pick out the attributes you want, so that you don't lose the declarative syntax...


class SwallowForm(forms.Form):
    airspeed_velocity = forms.IntegerField()
    is_migratory = forms.BooleanField()

class AfricanSwallowForm(forms.ModelForm, SwallowForm):
    class Meta:
        model = AfricanSwallow

class EuropeanSwallowForm(forms.Form, SwallowForm):
    ...

Should work.

I have some long running code that works and has the fields attr that looks like this.

languages_field = forms.ModelMultipleChoiceField(
        queryset=Language.objects.all(),
        widget=forms.CheckboxSelectMultiple,
        required=False
)

class FooLanguagesForm(forms.ModelForm):
    languages = languages_field

    class Meta:
        model = Foo
        fields = ('languages', )

Notice that I still use the fields tuple in Meta. The field shows up in the fields dict on the form instance whether or not it is in the Meta.fields.

I have a regular form that uses this pattern as well:

genres_field = forms.ModelMultipleChoiceField(
        queryset=blah,
        widget=forms.CheckboxSelectMultiple,
        #required=False,
)

class FooGenresForm(forms.Form):
    genres = genres_field

I see that my fields dict is working.

In [6]: f = FooLanguagesForm()

In [7]: f.fields
Out[7]: {'languages': <django.forms.models.ModelMultipleChoiceField object at 0x1024be450>}

In [8]: f2 = FooGenresForm()

In [9]: f2.fields
Out[9]: {'genres': <django.forms.models.ModelMultipleChoiceField object at 0x1024be3d0>}


Create a subclass of the IntegerField

class AirspeedField(forms.IntegerField):
    def __init__():
        super(AirspeedField, self).__init__(some_important_details_here)


I have just made a snippet that resolves this issue in a DRY way:

https://djangosnippets.org/snippets/10523/

It uses crispy-form, but the same idea can be used without crispy-forms. The idea is to use multiple forms under the same form tag.

0

精彩评论

暂无评论...
验证码 换一张
取 消