External Field Plugin Mechanism

New in version 3.0.0.

We’ve included a mechanism to add your own fields to the collection of available fields in django-formidable.

It’ll be possible to:

  • define a new form using this new type of field,
  • store their definition and parameters in a Formidable object instance (and thus, in the database),
  • using this form definition, validate the end-user data when filling this form against your field business logic mechanism.

For the sake of the example, let’s say you want to add a “Color Picker” field in django-formidable. You’ll have to create a django library project that we’ll call django-formidable-color-picker. Let’s say that this module has its own setup.py with the appropriate scripts to be installed in dev mode using pip install -e ./.

Let’s also say that you have added it in your INSTALLED_APPS.

Tree structure

.
├── formidable_color_picker
│   ├── apps.py
│   ├── __init__.py
|   |── field_builder.py
│   ├── serializers.py
├── setup.cfg
└── setup.py

Loading the field for building time

The first file we’re going to browse is serializers.py. Here’s a minimal version of it:

from formidable.register import load_serializer, FieldSerializerRegister
from formidable.serializers.fields import FieldSerializer, BASE_FIELDS

field_register = FieldSerializerRegister.get_instance()


@load_serializer(field_register)
class ColorPickerFieldSerializer(FieldSerializer):

    type_id = 'color_picker'

    class Meta(FieldSerializer.Meta):
        fields = BASE_FIELDS

Then you’re going to need to make sure that Django would catch this file at startup, and thus load the Serializer. It’s done via the apps.py file.

from django.apps import AppConfig


class FormidableColorPickerConfig(AppConfig):
    """
    Formidable Color Picker configuration class.
    """
    name = 'formidable_color_picker'

    def ready(self):
        """
        Load external serializer when ready
        """
        from . import serializers  # noqa

As you’d do for any other Django application, you can now add this line to your __init__.py file at the root of the python module:

default_app_config = 'formidable_color_picker.apps.FormidableColorPickerConfig'

Check that it’s working

Loading the Django shell:

>>> from formidable.serializers import FormidableSerializer
>>> data = {
    "label": "Color picker test",
    "description": "May I help you pick your favorite color?",
    "fields": [{
        "slug": "color",
        "label": "What is your favorite color?",
        "type_id": "color_picker",
        "accesses": [],
    }]
}
>>> instance = FormidableSerializer(data=data)
>>> instance.is_valid()
True
>>> formidable_instance = instance.save()

This means that you can create a form with a field whose type is not in django-formidable code, but in your module’s.

Then you can also retrieve this instance JSON defintion

>>> import json
>>> print(json.dumps(formidable_instance.to_json(), indent=2))
{
  "label": "Color picker test",
  "description": "May I help you pick your favorite color?",
  "fields": [
    {
      "slug": "color",
      "label": "What is your favorite color?",
      "type_id": "color_picker",
      "placeholder": null,
      "description": null,
      "accesses": [],
      "validations": [],
      "defaults": [],
    }
  ],
  "id": 42,
  "conditions": [],
  "version": 5
}

Making your field a bit more clever

Let’s say that colors can be expressed in two ways: RGB tuple (rgb) or Hexadecimal expression (hex). This means your field has to be parametrized in order to store this information at the builder step. Let’s imagine your JSON payload would look like:

{
    "label": "Color picker test",
    "description": "May I help you pick your favorite color?",
    "fields": [{
        "slug": "color",
        "label": "What is your favorite color?",
        "type_id": "color_picker",
        "accesses": [],
        "color_format": "hex"
    }]
}

You want then to make sure that your user would not send a wrong parameter, as in these BAD examples:

"color_format": ""
"color_format": "foo"
"color_format": "wrong"

For this specific field, you only want one parameter and its key is format and its values are only hex or rgb

Let’s add some validation in your Serializer, then.

from rest_framework import serializers
from formidable.register import load_serializer, FieldSerializerRegister
from formidable.serializers.fields import FieldSerializer, BASE_FIELDS

field_register = FieldSerializerRegister.get_instance()


@load_serializer(field_register)
class ColorPickerFieldSerializer(FieldSerializer):

    type_id = 'color_picker'

    allowed_formats = ('rgb', 'hex')
    default_error_messages = {
        "missing_parameter": "You need a `format` parameter for this field",
        "invalid_format": "Invalid format: `{format}` is not one of {formats}."
    }

    class Meta(FieldSerializer.Meta):
        config_fields = ('color_format', )
        fields = BASE_FIELDS + ('parameters',)

    def to_internal_value(self, data):
        data = super().to_internal_value(data)
        # Check if the parameters are compliant
        format = data.get('color_format')
        if format is None:
            self.fail('missing_parameter')

        if format not in self.allowed_formats:
            self.fail("invalid_format",
                      format=format, formats=self.allowed_formats)

        return data

Load your field for the form filler

In your Django settings, add or update the settings.FORMIDABLE_EXTERNAL_FIELD_BUILDERS variable, like this:

FORMIDABLE_EXTERNAL_FIELD_BUILDERS = {
    "color_picker": 'formidable_color_picker.field_builder.ColorPickerFieldBuilder',
}

Then this namespace should point at your ColorPickerFieldBuilder class, which can be written as follows:

Important

The classes you’re pointing at in this settings must be subclasses of formidable.forms.field_builder.FieldBuilder.

import re
from formidable.forms.fields import ParametrizedFieldMixin, CharField
from formidable.forms.field_builder import FieldBuilder

COLOR_RE = re.compile('^#(?:[0-9a-fA-F]{3}){1,2}$')

class ColorPickerWidget(TextInput):
    """
    This widget class enables to use the :meth:`to_formidable()` helper.
    """
    type_id = 'color_picker'

class ColorPickerField(ParametrizedFieldMixin, CharField):
    """
    The ColorPickerField should inherit from a ``formidable.forms.fields``
    subclass.
    """
    widget = ColorPickerWidget

    def to_python(self, value):
        return value

    def validate(self, value):
        # Depending on the parent class, it might be a good idea to call
        # super() in order to use the parents validation.
        super().validate(value)
        params = getattr(self, '__formidable_field_parameters', {})
        color_format = params.get('color_format')
        if color_format == 'rgb':
            if value not in ('red', 'green', 'blue'):
                raise forms.ValidationError("Invalid color: {}".format(value))
        elif color_format == 'hex':
            if not COLOR_RE.match(value):
                raise forms.ValidationError("Invalid color: {}".format(value))
        else:
            raise forms.ValidationError("Invalid color format.")

class ColorPickerFieldBuilder(FieldBuilder):
    field_class = ColorPickerField

Important

  • The field should inherit from a formidable Field class, to enable to_formidable() and to_json() to be used
  • The widget associated with the Field should have the type_id property set to the same than the Serializer.

Note

Full example

You may browse this as a complete directly usable example in the following repository: “django-formidable-color-picker”