Disclaimer: This is my first post in English I apologize in advance for any errors, feel free to correct me.

In short django-form-field provides a model field wich allows to associate custom forms to model instances. It uses a small DSL (Domain specific language) to define a form, and then generates standard Django forms from that. The DSL is based in the one described in this presentation and the syntax is almost identical, you can define a form like this:

width: Decimal-> min: 30.0 max:300.0
height: Decimal-> min: 50.0 max: 700.0
engraving: Text-> required: False

which is equivalent to this django form:

from django import forms

class CustomForm(forms.Form):
    width = forms.DecimalField(min_value=30, max_value=300)
    height = forms.DecimalField(min_value=50, max_value=700)
    engraving = forms.TextField(required=False)

Usage

As I allways learn most and easiest by example, let's write one

# models.py
from django.db import models
from param_field import ParamField, ParamDict
from jsonfield import JSONField

class CustomProduct(models.Model):
    name = models.CharField(max_length=44)
    description = models.CharField(max_length=500)
    ....
    params = ParamField(blank=True, max_length=3000)

class CustomProductRequest(models.Model):
    """To store client submited forms as a dictionary in a JSON Field"""
    product = models.ForeignKey(CustomProduct)
    user_params = JSONField()
    ...

Those could be some of the models from an e-commerce site, where all the products can be customized and each one requires a different form. Custom_product has a ParamField that contains the form, and CustomProductRequest has a JSONField that is used to store the valid returned form.

We can now add a new product to the DB with:

>> custom_params ="""
    width: Decimal-> min: 15.0 max: 35.0
    depth: Decimal-> min: 25.0 max: 20.0
    height: Decimal-> min: 10.0 max:20.0
    lacquer: Bool"""

>> CustomProduct.objects.create(
        name = "Wooden Box",
        description = "Not any box, A CUSTOM wooden box!",
        params = custom_params)

The view serving the product details along with it's form should be similar to this:

# views.py
from django.shortcuts import render, get_object_or_404
from django.views.generic import FormView
from django import forms
from .models import CustomProduct

class CustomProductFormView(FormView):
    template_name = 'product_form.html'
    form_class = forms.Form
    success_url = 'cart/'

    def dispatch(self, request, *args, **kwargs):
        """Find requested CustomProduct needed to generate the form"""
        pk = self.kwargs['pk']
        self.product = get_object_or_404(CustomProduct, pk=pk)
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        """Send product info to template"""
        context = super().get_context_data(**kwargs)
        context['product'] = self.product
        return context

    def get_form(self, form_class=None):
        """Generate form from param_field"""
        # NOTE: params.form(...) will return None when it doesn't
        # containt any field.
        return self.product.params.form(**self.get_form_kwargs())

    def form_valid(self, form):
        """At this point the form is a valid django form like any other"""
        # Just store the user request
        prod_request = CustomProductRequest.objects.create(
            user = self.request.user,
            product = self.product,
            user_params = form.cleaned_data.copy())

        # Do more things
        ...

        return super().form_valid(form)

It just generates the form, and store the valid responses in a CustomProductRequest model.

File support

Of course all of the above only works for simple fields, but if we enable support to File and Image parameters in the field:

...
params = ParamField(blank=True, file_support=True)
...

We can now define forms that accept files:

requested_image: Image-> required:True
scale: Decimal-> default: 1.0

To save those files we will need an extra model to store them, and modify form_valid() to handle them separately.

So we add another model called UserUploadedFile to store the file:

# models.py
...
class UserUploadedFile(models.Model):
    """To store client submited files"""
    product = models.ForeignKey(CustomProductRequest)
    name = models.CharField(max_length=80)
    user_file = models.FileField(upload_to="uploads/")

And lastly we modify form_valid() to separate File fields from the others:

# views.py
from django.core.files.uploadedfile import UploadedFile

def form_valid(self, form):

    fields = {}
    file_fields = {}

    # Separate File fields from the rest and dicard empty ones
    for key, value  in form.cleaned_data.items():
        if isinstance(value, UploadedFile):
            file_fields[key] = value
        elif value is not None:
            fields[key] = value

    # Store all fields except File and Image into JSONfield
    product_request = CustomProductRequest.objects.create(
            user = self.request.user,
            product = self.product,
            user_params = fields)

    # Link all uploaded files to product_request
    for key, value in file_fields.items():
        UserUploadedFile.objects.create(
            product = product_request,
            name = key,
            user_file=value)

    return super().form_valid(form)

That's all, and yes I know some of you DB guys are shaking your heads and saying that this is not how it is done, I understand. But sometimes you need something simple and/or fast.

Ver comentarios


En casi cualquier aplicación que use AJAX existe algún formulario con un campo que acepta JSON, para que la aplicación sea mínimamente robusta además es necesario verificar que todo el código JSON recibido está correctamente formado antes de procesarlo, y la mejor manera de hacer esto es usar un validador para que el formulario lo detecte y genere los errores. En este pequeño post trato de explicar como puedes hacerlo con algunos ejemplos.

Aunque Django no incluye un campo especifico JSON (por ahora), si que existe una app llamada django-json-field que nos proporciona el campo JSONFormField:

from django import forms
from json_field.fields import JSONFormField

class UserRequest(forms.Form):
    request = JSONFormField()

Desafortunadamente JSONFormField tiene problemas con los validadores, en lugar de aplicarlos tras convertir el valor JSON a objetos python como hacen los campos nativos, los aplica antes de la conversión cuando el valor es aún una cadena. Por eso a continuación describo un par de soluciones a este problema, si estas usando un campo distinto puedes saltar al final del post.

Custom Form Field

La primera y mi favorita es modificar la método clean del campo para que pueda aplicar validadores extra tras la conversión de JSON:

from json_field.fields import JSONFormField
from django.core.exceptions import ValidationError
from django import forms

class JSONValidatedFormField(JSONFormField):

    def __init__(self, *args, **kwargs):
        self.json_validators = kwargs.pop('json_validators', [])
        super(JSONValidatedFormField).__init__(*args, **kwargs)

    def clean(self, value):
        value=super(JSONValidatedFormField, self).clean(value)
        original_validators = self.validators
        self.validators = self.json_validators

        try:
            self.run_validators(value)
            self.validators = original_validators
        except Exception as e:
            self.validators = original_validators
            raise

        return value

JSONValidatedFormField acepta un nuevo parámetro extra llamado json_validators que es una lista de validadores que se aplican DESPUES de la conversión del valor a python. Por ejemplo:

def validar_peticion(value):
    """ Valida que el formato JSON es:
        {"request": "NEW",
        "data": "Texto del nuevo post"}
    """
    if not isinstance(value, dict):
        raise ValidationError("Se esperaba un diccionario.")
    if not all (k in foo for k in ("request","data")):
        raise ValidationError("formato de peticion erroneo.")


class RequestForm(forms.Form):
    request = JSONValidatedFormField(json_validators=[validar_peticion])

Si usas el campo de forma extensiva dentro de la aplicación también puedes crear un campo customizado:

class JSONRequestFormField(JSONValidatedFormField):

    def __init__(self, *args, **kwargs):
        super(JSONRequestFormField, self).__init__(*args, **kwargs)
        self.json_validators.append(validar_peticion)

Validador

La segunda opción es crear un validador que aplique su propio parser JSON a los datos antes de validarlos.

from django.core.exceptions import ValidationError
import json

class JSONRequestValidator(object):

    def __init__(self, opcion=None):
        self.opcion=opcion

    def __call__(self, value):
        try:
            request = json.loads(value)
        except Exception:
            raise ValidationError("Peticion mal construida")

        validar_peticion(request)

El problema es que inserta duplicidades de código e inconsistencias entre el parser JSON del campo y del validador, esto lo puedes mejorar usando el método clean del propio campo como parser:

from json_field.fields import JSONFormField
from django.core.exceptions import ValidationError

class JSONRequestValidator(object):

    def __init__(self, opcion=None):
        self.opcion = opcion

    def __call__(self, value):
        try:
            json_field = JSONFormField()
            request = json_field.clean(value)
        except ValidationError:
            raise
        except Exception:
            raise ValidationError("Peticion mal construida")

        validar_peticion(request)

Esta solución es parcial puesto que para algunos valores de incialización del campo en el formulario el parser puede diferir.

JSON Schema

Una vez tengas funcionando JSONFormField o el campo de tu elección, el siguiente paso es crear el validador para el formato que estés usando, en casos simples puedes usar un sistema similar al de los ejemplos anteriores comprobando manualmente los valores, para casos más complejos no es práctico y te recomiendo la biblioteca jsonschema. Con ella puedes definir un esquema que describe el formato JSON que es aceptable y luego usarlo para validar los datos recibidos. Su sintaxis es potente y flexible:

from django.core.exceptions import ValidationError
from jsonschema import validate

class JSONSchemaValidator(object):

    def __init__(self, schema=None, message="Invalid JSON object"):
        self.schema = schema
        self.message = message

    def __call__(self, value):
        try:
            validate(value, self.schema)
        except Exception:
            raise ValidationError(self.message, code=self.code, params={'value':value})


class JSONRequestValidator(JSONSchemaValidator):

    def __init__(self, schema=None, message=):
        self.schema = {
            "title": "User Request Schema",
            "type": "object",
            "properties": {
                "request": {
                    "type": "string"
                },
                "data": {
                    "type": "string"
                }
            },
            "required": ["request", "data"]
        }
        self.message="Invalid JSON Request"

En la documentación tienes ejemplos más avanzados de lo que se puede llegar a hacer:

{
    "title": "Example Schema",
    "type": "object",
    "properties": {
        "firstName": {
            "type": "string"
        },
        "lastName": {
            "type": "string"
        },
        "age": {
            "description": "Age in years",
            "type": "integer",
            "minimum": 0
        }
    },
    "required": ["firstName", "lastName"]
}

O con expresiones regulares:

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type": "object",
    "properties": {
        "/": {}
    },
    "patternProperties": {
        "^(/[^/]+)+$": {}
    },
    "additionalProperties": false,
    "required": [ "/" ]
}

Seguro que encuentras alguna manera de adaptarlo a tu problema.

Ver comentarios