ModelForm with JSON field of list of dictionaries

Hi All! I have a variation of a scenario featured in previous posts, but can’t seem to make the final mental jump to my specific scenario.

I have a model Assessment that has some basic properties as well as a JSON model field definition_json that stores json data describing the details of the assessment.

For example, if the Assessment model is type MULTIPLE_CHOICE, the definition_json might have a dictionary of data with answers, like this:

{
    "choices": [
        {
            "text": "Answer #1. This one is correct.",
            "choice_key": "a",
            "is_correct": True
        },
        {
            "text": "Answer #2. This one is also correct.",
            "choice_key": "b",
            "is_correct": True
        },
        {
            "text": "Answer #3. This one is not correct",
            "choice_key": "c",
            "is_correct": False
        }
    ]
}

So I want to provide an “author” user with a form (not a Django admin form, just part of the regular UI) to configure this kind of Assessment.

The form has to show the regular Assessment fields (like ‘name’) and then a list of each existing answer as an editable line item: text field for choice_key, text field for answer text, checkbox for whether is_correct or not)…as well as a delete button to remove any answer. Finally, a button beneath the answer list to add a new row for each new answer the author wants to add.

All the user’s work should then submit as one POST to be processed by the form.

I’m starting with a ModelForm for the Assessment model, but not sure how to enable editing/adding/deleting answers, all of which would be operating on the json data from the definition_json JSON field.

I’m also not sure how to read in, parse and store that data once the main form is submitted.

As far as I can tell, I should use a formset factory, but I’m not sure how to integrate it into my form.

( From what I gather inlineformset_factory is only for parent <> child model relationships and can’t be used in this situation. )

This is my guess so far…



class MultipleChoiceAnswerMultiWidget(forms.widgets.MultiWidget):
    widgets = {
        'text': forms.TextInput(),
        'choice_key': forms.TextInput(),
        'correct': forms.CheckboxInput(),
    }

    def decompress(self, value):
        if value:
            return [value['text'], value['choice_key'], value['correct']]
        else:
            return [None, None, None]


class MultipleChoiceAnswerMultiValueField(forms.MultiValueField):

    def __init__(self, *args, **kwargs):
        fields = (
            forms.CharField(),
            forms.CharField(),
            forms.BooleanField(),
        )
        super().__init__(fields, *args, **kwargs)

    def compress(self, data_list):
        return {
            'text': data_list[0],
            'choice_key': data_list[1],
            'correct': data_list[2],
        }

class MultipleChoiceAnswerForm(forms.Form):

    question_definition = MultipleChoiceAnswerMultiValueField(
        required=False,
    )


MultipleChoiceAnswerFormSet = formset_factory(MultipleChoiceAnswerForm, extra=0)


class MultipleChoiceAssessmentPanelForm(ModelForm):
    """ Form for editing an Assessment of type MULTIPLE_CHOICE """
    
    name = forms.TextField()

    choices = MultipleChoiceAnswerFormSet()

    class Meta:
        model = Assessment
        fields = []

    ....

   def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        choices.initial = self.instance.definition_json

        ....


… and the template …

  {% load crispy_forms_tags %}
  {% csrf_token %}
  {% crispy form %}

…but not sure how or where to introduce template code and javascript to handle deleting existing rows or adding a new, empty row?

Or could it be the better approach is just to make a single widget that takes in the complete definition_json and manually creates input fields, delete buttons and add button?
I have a feeling Django already has the machinery for managing a CRUD list like this within one model, but I’m not sure how it relates here (I think it’s meant for parent <> child model relationships).

Sorry this is a bit disjointed. Thanks for any suggestions!

My initial reaction to this is to wonder why you’re trying to specify this as a JSON data structure. If the content for the choices list is always those three attributes, you’d be better off making them a separate model with a ForeignKey back to the Assessment model (if that’s the actual name).

I don’t see any value in using a MultiWidget for this, I’d make this a form with three fields - and since you’re going to have multiple instances of this form, I would use a formset. (And, if the data were normalized, it would be an inline formset relating back to Assessment.)

However, as far as adding or deleting elements within the formset, it doesn’t matter what the underlying representation or data is for the form - these are both JavaScript functions being performed on the page.

When trying to get your feet under you for formsets, I’ve been referring people to this thread: Django formsets tutorials

One thing to keep in mind is that forms and models are two different types of objects with two separate and independent behaviors. Yes, they are designed to work together in specific ways, especially when you’re talking about model forms and formsets, but there’s nothing that requires they be used together in that way.

Also keep in mind that a form (or formset) is a Python object that has the behavior of being able to generate HTML. However, an HTML form is not a Django Form. You can create one HTML form from multiple Django forms. This means that a formset (or inline formset) does not need to be part of another Django Form for it to be displayed on the same page.

Hi Ken,

Thanks for the thoughtful response.

Yes using a foreign key relationship to “answers” would probably be more normalized, but the kinds of assessments can be numerous and quite varied in data shape, and that variation will probably increase over time, and I didn’t want an explosion of tables to support that. So I went with JSON paired with schemas to validate each expected structure. Does that sound reasonable or would you have still gone with a table for each variant?

Thanks for the suggestion: I’ll go back to experimenting with just a simple form with three fields, packaged in a formset.

You wrote " You can create one HTML form from multiple Django forms. This means that a formset (or inline formset) does not need to be part of another Django Form for it to be displayed on the same page."

… understood, but it would be nice to group the two forms involved in allowing a user to define an “Assessment” – in this case both top-level properties like “name” and then more variable data that goes into json, like the “answers”, – in one form class.

However, I’m still not sure there’s a way to do that, such that when the form is rendered it knows how to write the “name” field, write the “answers” formset into a special template with accompanying javascript (for add/edit/delete interactivity), and then have one clean() and save() method where both parts are handled.

I’ll read the tutorial you sent, thanks! I might also have a go at using HTMx just for the answers editing, which would be a bit snappier UX but then makes the code a bit harder to reason about (with parts involving traditional forms and parts involving the HTMx approach).

Understood. (It’s that latter part that wasn’t clear to me from your original post.) However, that still makes me wonder how you’re planning to handle that aspect of it from what you’ve described so far.

Certainly you’ve got some idea of what some of these shapes are? And perhaps an upper bound regarding number? (We may have different ideas as to what constitutes “numerous and quite varied”.)

With the way you’ve described your current design, you’re still going to have a number of classes for the different shapes in terms of the forms being handled.

There are ways of managing issues like this to reduce or minimize this “explosion” of possibilities.

There’s no value in trying to do this. It provides no benefit. There’s nothing to be gained by this approach.

You would never have this under any reasonable circumstance anyway. A formset is a collection of a form. That form is going to have its own clean and save methods, because those methods are going to be different from the form existing outside the formset. (If for no other reason, the clean methods for the forms in the formset need to be executed once for each instance of that form, where the other form only needs to be validated once.)

That’s not to say that you can’t create your own function for your view that is responsible for calling both. Or, in the case of an inline formset, that you couldn’t override the clean and save methods of the parent form to also call the corresponding methods on the formset - but either way, this is something for you to manage, not Django.

Again, don’t confuse or conflate Django forms with the HTML representation generated by those forms. It doesn’t matter whether you’re working with 1 form or 100 forms - you still have complete control via the templates as to how those forms appear on a page. You can define a context containing any number of forms, and render them all in a template. There’s nothing requiring that the form be named “form” in the context and template.

Side note: I’m not sure that HTMX is going to make things any “snappier”. Easier, perhaps, but it’s not magic. It’s just a library allowing you to issue AJAX calls and inject the responses into the DOM without having to write your own JavaScript.

There’s also zero difference on the Django side whether you use raw JavaScript, jQuery, or HTMX.