Managing request.FILES in formset with ajax

Hey,

I have this form which includes document uploads. I used to just manage it via the default submit form from the browser, and the pass the files uploaded with request.FILES passed to the formset in the view, like so:

if self.request.POST: 
   form    = self.get_form()                           # manages POST, instance internally from self
   formset_document = DocumentFormSet(self.request.POST, self.request.FILES, instance=self.object)

And then the documents would get saved according this form/formest/model:


DocumentFormSet = inlineformset_factory(Produit, DocumentProduit,
                                        form=DocumentsProduitForm,
                                        extra=0,
                                        can_delete=True)
class DocumentProduit(models.Model):
	fk_produit = models.ForeignKey(Produit, on_delete=models.SET_DEFAULT,related_name="documents", null=False, blank=False, default=0)
	document = models.FileField(upload_to=docprod_uploadto, verbose_name="Document Produit", blank=True, null=True)
	filename = models.CharField(max_length=50, verbose_name="Nom du document", null=True, blank=True)
	notes	= models.TextField(verbose_name="Notes", blank=True, null=True)
class DocumentsProduitForm(ModelForm):
    class Meta:
        model = DocumentProduit
        fields = ["document", "notes"]
        widgets = {"document":FileInput(attrs={"class":"fileUploader"})}

Then I had to switch to making those request AJAX calls on the client side, using jquery, like so:

        let csrftoken = $('input[name="csrfmiddlewaretoken"]').val();
        $('form#mainform').append($input)
        let data = $('form#mainform').serialize();
        $.ajax({
            url: url,
            type: "POST",
            data: data,
            headers: {'X-CSRFToken':csrftoken},
            success: ....,
            error: .... })

And the form:

	<form id="mainform" action="" method="post" enctype='multipart/form-data'>
		{%csrf_token%}
                 ....

                    <tbody id="documents_body">
                    {{ formset_document.management_form }}
                    {{ formset_document.non_form_errors }}
                    {% for form in formset_document %}
                        <tr>
                            {% if form.document.value %}
                                <td><a href="{{ form.document.value.url }}"  class="download-on-click">{{ form.document.value.name}}</a> </td>
                            {% else %}
                                <td>{% field_error form.document.errors %}{{ form.document }}</td>
                            {% endif %}
{#                            <td>{% field_error form.filename.errors %}{{ form.filename }}</td>#}
                            <td>{% field_error form.notes.errors %}{{ form.notes }}</td>
                            <td>{{ form.DELETE }}</td>
                            <td style="display: none">{{ form.fk_produit }}</td>
                            <td style="display: none">{{ form.id }}</td>
                        </tr>
                    {% endfor %}
                    </tbody>
                    ....

However in this case, request.FILES is empty on the server-side. Apparently with that switch I have lost part of the handling of those files that django provides (or perhaps ajax.post() doesn’t handle files in a form the same way that a regular submit does)?).

Any convenient way to “re-add” my files to request.FILES for the server-side? I’ve been able to “find” them with jquery selectors, like so:


        $(".fileUploader").each(function(){
            console.log(this.files[0])
        })

But there has to be a more standard way to do this than messing around with jquery selectors?

Okay, nevermind. Found how to ACTUALLY do this.

The root cause of my trouble is a botched implementation of ajax.post() in the first place; FormData() is a much, much better way to do it than serializing the html element. It now looks like this in the javascript:

        let url = $(clickedBtn).attr("data-url");
        let action = $(clickedBtn).attr("value");
        let formData = new FormData($('form#mainform')[0])
        formData.append("action", action)
        $.ajax({
            url: url,
            type: "POST",
            data: formData,
            contentType: false,
            processData: false,

So basically, you pass your <form> element as a whole to FormData(). And then you have a proper post request, with everything handled naturally as it should without the need to build fake inputs to add stuff to the post (e.g. csfr inputs & action in my case).

I had initially found the .serialize() approach on SO after a quick search. So much for taking the first solution that works…

1 Like