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