Brendel Consulting logo

Brendel Consulting

composing beautiful software solutions

Jan 24, 2012

Django ModelForms: Setting any field attributes via Meta class

Django's ModelForms are a convenient way to create forms from already existing models. However, if any customization on the form definition is desired then we often are forced to manually re-define the field in the ModelForm definition. For example, consider this discussion on Stack Overflow. This is of course very unfortunate.

Some things about the form fields already can be modified via the Meta class of the ModelForm, for example the widgets used to render the form fields. Unfortunately, setting of custom error messages via the Meta class does not seem to be possible.

So I implemented a small change to the ModelForm class, which allows you to set arbitrary field attributes via the Meta class. For that, I introduced a new Meta class field, called "field_args". It is used like this (note that we are deriving from a new base class, called ExtendedMetaModelForm):

class AuthorForm(ExtendedMetaModelForm):
    class Meta:
        model = Author
        field_args = {
            "first_name" : {
                "error_messages" : {
                    "required" : "Please let us know what to call you!"
                }
            },
            "notes" : {
                "+error_messages" : {
                    "required" : "Please also enter some notes.",
                    "invalid"  : "This is not a valid note."
                },
                "widget" : forms.Textarea(attrs={'cols': 70, 'rows': 15}),
            }
        }

As you can see, field_args is a dictionary of dictionaries. Each dictionary within field_args specifies the field attributes you wish to set for a given field. As you can see, you are not limited to the error messages. For instance, the "notes" field here receives a custom widget. Please note that the Meta class already allows you to set custom widgets. This example here is is just to show that you are free to use field_args to set any field attribute.

Another interesting feature is illustrated with the error messages for the "notes" field. As you can see we have a plus sign at the start of  "+error_messages". This merely is used as an indicator that we wish to merge the specified attributes to an already existing attribute, rather than fully replace it. This only is supported if both the newly defined value and the already existing value for the named field attribute are dictionary types. The advantage of using the "+" notation is that you do not have to fully specify all error messages - for example - if you only wish to customize a few of them. In our example above, the error messages for the first_name field are fully replaced, while for the notes field they are merely modified or appended to.

The new ExtendedMetaModelForm can be used anywhere you traditionally would have used forms.ModelForm. The source code for this class is as follows:

class ExtendedMetaModelForm(forms.ModelForm):
    """
    Allow the setting of any field attributes via the Meta class.
    """
    def __init__(self, *args, **kwargs):
        """
        Iterate over fields, set attributes from Meta.field_args.
        """
        super(ExtendedMetaModelForm, self).__init__(*args, **kwargs)
        if hasattr(self.Meta, "field_args"):
            # Look at the field_args Meta class attribute to get
            # any (additional) attributes we should set for a field.
            field_args = self.Meta.field_args
            # Iterate over all fields...
            for fname, field in self.fields.items():
                # Check if we have something for that field in field_args
                fargs = field_args.get(fname)
                if fargs:
                    # Iterate over all attributes for a field that we
                    # have specified in field_args
                    for attr_name, attr_val in fargs.items():
                        if attr_name.startswith("+"):
                            merge_attempt = True
                            attr_name = attr_name[1:]
                        else:
                            merge_attempt = False
                        orig_attr_val = getattr(field, attr_name, None)
                        if orig_attr_val and merge_attempt and \
                                    type(orig_attr_val) == dict and \
                                    type(attr_val) == dict:
                            # Merge dictionaries together
                            orig_attr_val.update(attr_val)
                        else:
                            # Replace existing attribute
                            setattr(field, attr_name, attr_val)

I hope this little code snippet here helps you to make your work with Django's model forms easier.

Labels:

 
 

1 Comments:

  • I have added your solution for the non-inheritance of error_messages to the modelforms into the django bugtrack (https://code.djangoproject.com/ticket/13693)

    That works nice for me.
    Thank you for your solution.

    By Blogger Ivang, At August 1, 2012 at 10:26 AM  

Post a Comment

Subscribe to Post Comments [Atom]



<< Home