Search code examples
djangofilepond

How to use FilePond in Django project


Background: I have two models: SellingItem and SellingItemImages. SellingItemImages has a custom FileField that can take multiple files. By putting two forms(itemform and imageform) under single element (enctype="multipart/form-data"), I was able to allow users to upload multiple images. Now, I want to incorporate client side image optimization and better UI. I tried out filepond but am facing some challenges. I organized this post by

  1. showing django code without filepond
  2. showing code with filepond what
  3. I accomplished with filepond so far
  4. questions on what to do next

** 1)django code without filepond.** models.py

# models.py
class SellingItem(models.Model):
    seller = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    name = models.CharField(max_length=200)
    description = models.CharField(max_length= 500, null=True, blank=True)
    price = models.IntegerField(default=0)


class SellingItemImages(models.Model):
    sellingitem = models.ForeignKey(SellingItem, default = None, on_delete=models.CASCADE, related_name='images')
    image = ContentTypeRestrictedFileField(content_types=['image/png', 'image/jpeg','image/jpg'],blank=True, 
                                   max_upload_size=5242880) 
    #ContentTypeRestrictedFileField is a custom FileField. 

Here is forms.py

class SellingItemForm(forms.ModelForm):

    class Meta:
        model = SellingItem
        fields = ('name', 'description', 'price')

class SellingItemImagesForm(forms.ModelForm):

    class Meta:
        model = SellingItemImages
        fields= ('image',)
        widgets = {
            'image': forms.FileInput(attrs={'multiple':True,}),
        }

Here is views.py

@login_required
def post_newitem(request):

    if request.method == 'POST':

        itemform = SellingItemForm(request.POST)
        imageform = SellingItemImagesForm(request.POST, request.FILES)

        if '_cancel' in request.POST:
            itemform = SellingItemForm()
            imageform = SellingItemImagesForm()
            return render(request, 'market/post_newitem.html',
                  {'itemform': itemform, 'imageform': imageform})

        else:
            if '_publish' in request.POST:  
                print('hit publish')  
                if itemform.is_valid() and imageform.is_valid():
                    print('two forms are valid')
                    sellingitem = itemform.save(commit=False)
                    sellingitem.seller = request.user
                    sellingitem.published_date = timezone.now()
                    sellingitem.save()

                    files = request.FILES.getlist('image')
                    for f in files:
                        photo = SellingItemImages(sellingitem=sellingitem, image=f)
                        photo.save()
                    return redirect('market_home')    
                else:
                    print(itemform.errors, imageform.errors)    

    else:
        itemform = SellingItemForm()
        imageform = SellingItemImagesForm(request.POST)
    return render(request, 'market/post_newitem.html',
                  {'itemform': itemform, 'imageform': imageform})

Here is template post_newitem.html. Here I put two forms under single element.

{% extends 'market/marketbase.html' %}
{% block content %}
    <form id="post_form" method="post" action="" enctype="multipart/form-data">

    {% csrf_token %}

    {% for hidden in itemform.hidden_fields %}
        {{ hidden }}
    {% endfor %}

    {% load widget_tweaks %}
        <table>
            <tr>
                <td>{{ itemform.name |add_class:"name_form_field"}}</td>
            </tr>
            <tr>
                <td>{{ itemform.description |add_class:"description_form_field" }}</td>
            </tr>
            <tr>
                <td>{{ itemform.price |add_class:"price_form_field" }}</td>
            </tr>

        {% for hidden in imageform.hidden_fields %}
            {{ hidden }}
        {% endfor %}
            <tr>
                <td>
                    {{ imageform.image |add_class:"image_form_field" }}
                </td>
            </tr>
        </table>

    <input class='edit-delete-buttons' type="submit" name="_publish">
    <input class='edit-delete-buttons' type="submit" name="_cancel">
    </form>
{% endblock %}

The above code works in allowing users to upload multiple images. As mentioned earlier, to get better UI and client-side image optimization, I turned to this nice javascript library, filepond.

2) code with filepond

<script>
document.addEventListener('DOMContentLoaded', function() {

    // Create FilePond object
    const inputElement = document.querySelector('input[type="file"]');
    const pond = FilePond.create(inputElement, {
        // track addfile event
        onaddfile: (err, fileItem) => {
        console.log(err, fileItem.getMetadata('resize'));
        },
        // to see whether all files are processed
        onprocessfiles: () => {
            console.log('process finished for all files');
        },
        // show when a file is reverted
        onprocessfilerevert: (fileItem) => {
            console.log(fileItem + 'is reverted');
        },

    });
});

    FilePond.registerPlugin(
        FilePondPluginImagePreview,
        FilePondPluginImageCrop,
        FilePondPluginImageTransform,
        FilePondPluginFileValidateType,
        FilePondPluginImageResize);

    var csrf_token="{{ csrf_token }}";

    FilePond.setOptions({
        imagePreviewHeight: 100,
        allowMultiple: true,
        imageCropAspectRatio: 1,
        imageResizeTargetWidth: 256,
        imageResizeMode: 'contain',
        imageTransformOutputQuality: 80,
        maxFiles: 4,

        server: {
            // url, none, because endpoints located on the same server
            process: {
                headers: {"X-CSRFToken":csrf_token,},
                url: '/media/',
                method: 'POST',
            },
            revert: {
                headers: {
                "X-CSRFToken":csrf_token,
                },
                url: '/media/',
                method: 'DELETE',
            },
            fetch: null,
            load: null,
        }
        });
    </script>

3) what I accomplished with filepond so far

With the above code, I was able to a. show filepond drop-drag area b. show image preview c. filepond showing upload completed as shown in the following image d. in Chrome Develop Tools console, showing "process finished for all files"

image showing filepond drop area after selecting two files

4) questions on what to do next

a: server related:I understand that the green highlight with "upload finished" is for users. This does not necessarily mean that the file is uploaded to server.

Were the files uploaded to the server? Is my server config. correct? How can one know if the files are uploaded to server (using console)?

b: django related: once the files are uploaded to server, how can one retrieve these files and point to the right django models (in my case, SellingItemsImages)?

I tried files=request.FILES.getlist('filepond') as shown in this post, but files returned empty list. I do not know if this is because this snippet does not work, or it is because I do not have any files uploaded to start with.

c: django form related: as mentioned in the background, I have two forms, one is regular form with name, price, etc; another one for uploading images. Without filepond, I was sending both forms with one submit button in one post_newitem view. With filepond, I guess I have a couple of options: - option 1: send regular form with submit button, while send filepond files asynchronously. - option 2: let filepond optimize images (via transformplugin), and send images and other form areas(name, price, etc) as FormData.

I hope to get some input on pros and cons about these two options, and how to proceed with these two options.


Solution

  • First, thanks for the detailed question.

    a) Green means the server has returned a 200 OK response to the upload request. So the file should be uploaded. You can check if the file was sent in your dev tools network tab by clicking on the POST request and inspecting its body (it should show as binary data / multipart form data).

    b) I'm not familiar with Django so I'm afraid I can't help with the Django side of things. FilePod does send two items per POST, one JSON object (the metadata of the file) and one file object, maybe that is causing some issues with Django? More on that here: https://pqina.nl/filepond/docs/patterns/api/server/

    There's also this Django server component that might be of use: https://github.com/ImperialCollegeLondon/django-drf-filepond

    c) Option 2 has the advantage of not having to store temp files, but the final form submit will take longer, and if large files are present it might be less stable on slow / mobile connections. With option 1 you have the ability to restore temp uploaded files for when the user leaves and returns to the page. I've written about the pros/cons of uploading client-side edited files here: https://pqina.nl/blog/the-trouble-with-editing-and-uploading-files-in-the-browser/