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:

 
 

Jan 19, 2012

How to use django_extensions' runscript command

The django_extensions app provides many helpful additions for the Django developer, which are worth checking out. I highly recommend it to anyone working on a Django project. One such addition is runscript, which allows you to run any script in the Django context, without having to manually import and configure your paths and Django settings. Unfortunately, there is very little documentation on how to correctly use it. This blog post here just quickly sums it all up:

1. Running a script

This is done like this:

    ./manage.py runscript <scriptname>

You cannot specify the name of a specific Python file, though. Scripts have to be within modules in a particular location, which is explained next.

2. The location of scripts

The runscript command looks for modules called scripts. These modules may be located in your project root directory, but may also reside in any of your apps directories. This allows you to maintain app-specific scripts. So, create the scripts directory (either at the root or within an apps directory), along with an __init__.py file:

    mkdir scripts
    touch scripts/__init__.py

3. The script itself

Scripts are simple Python files, but they need to contain a run() function. For example like this:

    def run():
        print "I am a script"

Within such a script, you could import and use any of your models or other parts of your Django project. When you store the above code in a file called scripts/testscript.py then you can run it like so (note that this is without the .py extension):

    ./manage.py runscript testscript

4. Run multiple scripts at once

A nice feature of runscript  is that you can execute multiple scripts at the same time, if those scripts have the same name. Assume that you have different applications and wish to initialize data for them by running a script. Rather than putting all of this into a single script, you could have a scripts directory in your various apps directories, and within each of those have - for example - an init_data.py script, thus keeping each script smaller and easier to understand and maintain.

You can see this in action if you specify more verbose output via the "-v 2" option. 

    ./manage.py runscript -v 2 init_data

The output will look something like this:

    Check for django_extensions.scripts.init_data
    Check for django.contrib.auth.scripts.init_data
    ...
    Check for one_of_my_apps.scripts.init_data
    Found script 'one_of_my_apps.scripts.init_data' ...
    Running script 'one_of_my_apps.scripts.init_data' ...
    I am a script for one of my apps.
    Check for some_other_app.scripts.init_data
    Found script 'some_other_app.scripts.init_data' ...
    Running script 'some_other_app.scripts.init_data' ...
    I am a script for some other app.
    Check for scripts.init_data
    Found script 'scripts.init_data' ...
    Running script 'scripts.init_data' ...
    I am a script at the root level.

As you can see, runscript checks for a scripts module for every installed application, plus on the root level. If it finds a match, it runs the script and then keeps looking for more matches.


Labels: