Editable Form, from JSON

Ok, sorry to have to drag this up again, i’ve been on holidays so just coming back to this little project! Just hitting another stumbling block.

When trying to work with the form once submitted, I’m not sure my workflow is correct. is_valid() and is_bound are returning false. I can get a value from the form ‘manually’, it print to console.

How do I make the form is_bound when submitting it?

This is the view (with some debugs) handling the submission which is via HTMX.

elif request.htmx.trigger == "save-data":
        if request.method == "POST":
            
            form = ImageMetaDataForm(field_list=list(request.POST.keys()), json_data=request.POST)
            print(form.is_bound)
            print(form.is_valid())
            print(form['XMP:CreateDate'].value())
            return HttpResponse("testing")

#To save scrolling up here is the custom form which renders a editable dynamic form perfectly.

class ImageMetaDataForm(forms.Form):
    def __init__(self, field_list, json_data=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if json_data:
            for field_name in field_list:
                initial_value = json_data.get(field_name, '') 
                self.fields[field_name] = forms.CharField(initial=initial_value, required=False)```

The basic issue here is that you are assigning the values being submitted to the initial attribute of the field. Data isn’t “bound” to the initial values, so by not supplying post data in the initializer, Django doesn’t see where any data has been entered.

The is_bound attribute is actually set in the __init__ method of Form, so when you call super().__init__(...) without passing data, is_bound will be false.

Now, believe it or not, but you can supply the data to the form before creating the fields!

In other words, you can do something like this in your __init__ method:

super().__init__(*args, **kwargs)
for field_name in self.data:
    self.fields[field_name] = forms.CharField(required=False)

and then create the instance of the form as:
my_form = ImageMetaDataForm(json_data)

Mmmmm right thanks Ken, this gives me something to think about, it’s pretty confusing but I just want the correct thought path to go down with this.

So, can I go ahead and ‘do stuff’ with the data without .is_valid?? I mean the submitted data is the in the form obj but I’m not following the normal workflow (eg…if form.is_valid, data = form.cleaned_data…)

Also,

When instantiating the form with request.POST, like form = ImageMetaDataForm(request.POST) it’s empty. I’m having to do ImageMetaDataForm(field_list=list(request.POST.keys()), json_data=request.POST)

Correct.

I’m not telling you to instantiate it with request.POST.

The data is being submitted in what you’re calling json_data, so that is what you want to instantiate the form with. (Your calling sequence in there is unnecessary.)

No, you still want to do the is_valid call in your view. Other than creating the instance, which does happen in the view, these changes only affect the definition of the form itself.

Right OK, thanks Ken! I think i’m starting to understand, is_valid is returning False though (this could be a number of things I assume), form.errors is not returning anything.

I’d need to see the current form definition and the view to help you at this point. The most likely possibility that I can see is that there might be one or more keys in the data that aren’t valid python field names. You may need to sanitize those keys to be something valid internally.

1 Like

Yea understand, thanks again Ken

Hi Ken, if you could have one last look at why possible is_valis() is returning False that would be so much appreciated. I’ve spent hours trying to get my head around why it could be, and to my eye it should work, obviously i’m missing something.

Here is the custom form, i’m using the data structure you suggested to create it, in the testing phase i’m just using two fields.

This ‘works’ it generates and editable form with the initial values.

    'Description': {
        'type': forms.CharField,
        'params': {
            'max_length': 5000,
            'help_text': 'Image descritpion'
        }
    },

     'FileName': {
        'type': forms.CharField,
        'params': {
            'max_length': 200,
            'help_text': 'Image filename'
        }
    },
}


class ImageMetaDataForm(forms.Form):
    def __init__(self, field_list, json_data=None, *args, **kwargs):
       super().__init__(*args, **kwargs)
       for field_name in field_list:
            try:
                self.fields[field_name] = camera_fields[field_name]['type'](**camera_fields[field_name]['params'])
                if json_data and field_name in json_data:
                    self.fields[field_name].initial = json_data[field_name]
            except KeyError:
                print("Key does not exist")

The relevant code from the view. So when a user click save-data the form is submitted, but is_valid() is always False. reques.POST contains the key/values of the submitted form.

 elif request.htmx.trigger == "edit-data":
        form = ImageMetaDataForm(field_list=list(data.keys()), json_data=data)
        return render(request, "partials/edit_data.html", {'form':form, 'image':image})
    elif request.htmx.trigger == "save-data":
        print(request.POST)
        form = ImageMetaDataForm(field_list=list(request.POST.keys()), json_data=request.POST)
        print(form.is_valid())

Kinda of hitting my head against the wall with this.

What are you seeing as the output from the print(request.POST)?

You’re still trying to assign data to the initial attribute.

And, you’re still doing more work than necessary here.

In your view:

can be replaced by:
form = ImageMetaDataForm(request.POST)

allowing your form to be defined as:

request.POST is giving me

<QueryDict: {'csrfmiddlewaretoken': ['foo'], 'FileName': ['foo.JPG'], 'Description': ['bar]>

Applying your changes, self.data is empty

Please show the current form and view.

Form

    def __init__(self, *args, **kwargs):
       super().__init__(*args, **kwargs)
       for field_name in self.data:
            try:
                self.fields[field_name] = camera_fields[field_name]['type'](**camera_fields[field_name]['params'])
            except KeyError:
                print("no such key")

View, HTMX is handling request (iv’e debug the HTMX side of things and all good) but i’m thinking I need hx-vals to actually send the data on “edit-data” request.

if request.htmx.trigger == "get-data-button":
        return render(request, "partials/data.html", {'data': data})
    elif request.htmx.trigger == "edit-data":
        form = ImageMetaDataForm(request.POST)
        return render(request, "partials/edit_data.html", {'form':form, 'image':image})
    elif request.htmx.trigger == "save-data":
        print(request.POST)
        form = ImageMetaDataForm(request.POST)
        print(form.is_valid())```

If you’re printing request.POST in edit-data, and you’re seeing the form contents in request.POST, then htmx is submitting the data.

However, I’m now confused by what all you’re trying to do with this view. Can you please post the complete view? (And possibly outline what all you’re expecting this view to handle?)

Yea sure, sorry for the confusion on this and for the amount of help I’m asking, I just hate not being able to work stuff out! The aim of the view is to handle the viewing of the metadata of the image passed to it (image data is extracted in a function and save to a JSON field in the image model). Then, HTMX is handling the different option on the image.

The metadata can be viewed, via get-data-button which just passes the data as context and I just process the dictionary on the template. This displays the data.

The metadata can then also be edited via a edit-data button with the aim of returning a editable form initialised with the metadata fields/values.

The edited metadata can by saved via a save-data button once the form is edited.

def get_data(request, image=None):
    image = get_object_or_404(Uploaded_Image, pk=image)
    data = image.exif_data
    if request.htmx.trigger == "get-data-button":
        return render(request, "partials/data.html", {'data': data})
    elif request.htmx.trigger == "edit-data":
        form = ImageMetaDataForm(request.POST)
        return render(request, "partials/edit_data.html", {'form':form, 'image':image})
    elif request.htmx.trigger == "save-data":
        form = ImageMetaDataForm(request.POST)
        return render(request, "partials/edit_data.html", {'form':form, 'image':image})

About the best I can tell you at the moment is that this is fundamentally working for me.

In other words, I create this form:

class ImageMetaDataForm(forms.Form):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        print(self.data)
        for field_name in self.data:
            try:
                self.fields[field_name] = cf[field_name]['type'](**cf[field_name]['params'])
            except KeyError:
                print("Key does not exist")

Then I create some faked post data:
qd = {'csrfmiddlewaretoken':'foo', 'FileName':'foo.JPG', 'Description':'bar'}

Create an instance of the form:
image_data = ImageMetaDataForm(qd)

then:

In [13]: image_data.fields
Out[13]: 
{'FileName': <django.forms.fields.CharField at 0x7f89c09a2d20>,
 'Description': <django.forms.fields.CharField at 0x7f89bbf1b380>}

In [14]: image_data.is_valid()
Out[14]: True

In [15]: image_data.cleaned_data
Out[15]: {'FileName': 'foo.JPG', 'Description': 'bar'}

What I suggest then is tracking more closely what’s going on.

Look at exactly what htmx is sending, add more prints in the view to see what you’re getting and how it’s being handled.

Yea ok. Thanks Ken for putting in the effort to answer very much appreciated I’ll have a detailed look later when I have more time. Thanks again.

Ok well, well the form(s) are working, is_valid returning True and cleaned_data giving me a nice dictionary of submitted data.

After thinking about it, I actually passed data (a simple dict of k/v image data) into my form, form = ImageMetaDataForm(data) on it’s first instantiation.

Anyway, thanks for all the help Ken, defiantly learnt a lot about custom forms doing this.