Ana Balica

Hi, I'm Ana.

I'm a software developer. I mostly do Python. This blog is about my adventures with code, travel experiences and relevant life events.

Here's what I'm doing now.

Occasionally I give talks.

Please don't take my words for granted, because Internet is full of bad advice, and I might be part of it inadvertently.

The tale of DRY with django-crispy-forms | Part II

When you write a blog post, people of Internet will point out any mistakes they find. It’s a great opportunity to improve existing code and look into other solutions. After posting The tale of DRY with django-crispy-forms I found out about formulation - a template-based solution for rendering forms, in contrast to crispy-forms that builds the form structure in code.

Also @maraujop – crispy-forms developer – made several very helpful suggestions in the comments area, which brings us to Part II.

Mistakes of the past

The story of Foo people can be found in this archive. Their end was tragic. They knew truth was somewhere close, but they were unable to grasp it to its fullest and failed to survive this cruel software world.

FooForms had their __init__ cluttered with the initialization of a FormHelper object and the attachment of the SubmitCancelFormActions. Nothing bad with SubmitCancelFormActions itself, but as they grew in number overriding the initialization started to look silly.

Decline of Foo marked the birth of powerful Bar people.

Laputa: Castle in the Sky (Ghibli Studio) - How I imagine the fairy tale kingdoms
Laputa: Castle in the Sky (Ghibli Studio) - How I imagine the fairy tale kingdoms

Reforms

Assume that Bar model, view and template are similar to Foo. Bar people knew they should start with the API, and so they shaped their dreams first.

# fairytale/characters/forms.py
from characters.models import Bar
from common.forms import ModelFormWithHelper
from common.helpers import SubmitCancelFormHelper


class BarForm(ModelFormWithHelper):
    """Model Bar form"""
    class Meta:
        model = Bar
        helper_class = SubmitCancelFormHelper
        helper_cancel_href = "{% url 'some_cancel_url' %}"

What Bar wants is to declare an alternative FormHelper, which is responsible for customizing the form. Also it wants to provide helper attributes, that are different for each form - the cancel URL, for example.

There are 2 unknown creatures here - SubmitCancelFormHelper and ModelFormWithHelper.

SubmitCancelFormHelper as you might have guessed is of FormHelper breed. It tells you right to the face that it appends to the layout submit and cancel buttons. See for yourself.

# fairytale/common/helpers.py
from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper, Layout
from crispy_forms.layout import Submit, HTML


class SubmitCancelFormHelper(FormHelper):
    """Custom FormHelper that appends to the layout cancel and submit
    buttons (works only with bootstrap crispy-forms pack). It expects a
    `cancel_href` attribute to be used as cancel button URL.

    Example::

        SubmitCancelFormHelper(self, cancel_href="/some/url/")
    """
    def __init__(self, *args, **kwargs):
        cancel_href = kwargs.pop('cancel_href', '#')
        super(SubmitCancelFormHelper, self).__init__(*args, **kwargs)
        self.layout.append(
            Layout(
                FormActions(
                    HTML("""<a role="button" class="btn btn-default mr4"
                            href="{0}">Cancel</a>""".format(cancel_href)),
                    Submit('save', 'Submit'),
                )
            )
        )

This class expects a cancel_href named parameter. Afterwards it takes advantage of the power it was given by FormHelper and modifies the layout by appending a button that we represent using HTML widget and a Submit widget. SubmitCancelFormHelper serves us well.

What about ModelFormWithHelper mystery. This base class is a bit more tricky, but still nothing out of this world.

# fairytale/common/helpers.py
from django.core.exceptions import ImproperlyConfigured
from django.forms import ModelForm


class ModelFormWithHelper(ModelForm):
    """Custom ModelForm that allows to attach a crispy-forms FormHelper class,
    that will modify in some way the rendering of the layout.

    Example::

        FooForm(ModelFormWithHelper):
            class Meta:
                model = FooModel
                helper_class = FooFormHelper
    """
    def __init__(self, *args, **kwargs):
        super(ModelFormWithHelper, self).__init__(*args, **kwargs)

        if hasattr(self.Meta, "helper_class"):
            helper_class = getattr(self.Meta, "helper_class")
            kwargs = self.get_helper_kwargs()
            self.helper = helper_class(self, **kwargs)
        else:
            raise ImproperlyConfigured(
                "{0} is missing a 'helper_class' meta attribute.".format(
                    self.__class__.__name__))

    def get_helper_kwargs(self):
        """Get all helper attributes from class Meta by stripping them of
        `helper_` part of attribute string

        :return: dict with helper kwargs
        """
        kwargs = {}
        for attr, value in self.Meta.__dict__.items():
            if attr.startswith("helper_") and attr != "helper_class":
                new_attr = attr.split("_", 1)[1]
                kwargs[new_attr] = value
        return kwargs

Ohh, that’s a lot of code. Well, docstrings make it 1/3 of the listing. Let’s see what ModelFormWithHelper is trying to tell us.

In the initialization phase it checks if the class has a helper_class attribute. It’s mandatory to be there, otherwise there’s no point in using this class at all. If there isn’t, ModelFormWithHelper will throw an exception to remind you, Hey, you forgot that attribute - helper_class! Otherwise, it will initialize the form helper class and attach it to self.helper. But before that it extracts the helper kwargs.

Look at the dream BarForm class. Bar people wanted to have a custom cancel URL and used a meta attribute helper_cancel_href. Method get_helper_kwargs() deals with all the helper_ attributes and strips them of that prefix to ultimately feed them to the helper.

The class is not tied to any specific form helper, so you can create as many custom form helpers as your kingdom needs.

That’s it. Now forms will become more compact and readable due to the new meta attributes.