Django Multiple Image Upload with Dropzone.js

The easiest solution I have found to upload multiple images has been Dropzone.js. This is the second time I have implemented it in a Django app and I always end up customizing it a bit because I like the idea of only uploading when the user hits "submit." It's relatively straightforward, but required a bit of work in Django, JavaScript (jQuery here), HTML, and CSS.


The Photo model:

class Photo(ObjUserRelation):
    photo = models.ImageField(upload_to=saved_image_path)
    photo_compressed = models.ImageField(upload_to=saved_thumb_path, editable=False)
    thumbnail = models.ImageField(upload_to=saved_thumb_path, editable=False)

    def save(self, *args, **kwargs):

        if not self.make_thumbnail():
            # set to a default thumbnail
            raise Exception('Could not create thumbnail - is the file type valid?')

        if not self.make_thumbnail(small=True):
            # set to a default thumbnail
            raise Exception('Could not create thumbnail - is the file type valid?')

        super(Photo, self).save(*args, **kwargs)

    def make_thumbnail(self, small=False):
        return make_thumbnail(self, small)

I am setting custom locations for the files with:

def saved_directory_path(instance, filename, root):
    now_time = datetime.now()
    current_day = now_time.day
    current_month = now_time.month
    current_year = now_time.year
    return '{root}/{year}/{month}/{day}/{user}/{random}/{filename}'.format(root=root,
                                                                           year=current_year,
                                                                           month=current_month,
                                                                           day=current_day,
                                                                           user=instance.user.username,
                                                                           random=get_random_string(),
                                                                           filename=filename, )


def saved_image_path(instance, filename):
    return saved_directory_path(instance, filename, 'profile/images')


def saved_thumb_path(instance, filename):
    return saved_directory_path(instance, filename, 'profile/thumbs')

To create thumbnails (for quicker loading), I am using Pillow (pip install Pillow) and call make_thumbnail in Photo's save method:

def make_thumbnail(self, small=False):
    thumb_name, thumb_extension = os.path.splitext(self.photo.name)
    thumb_extension = thumb_extension.lower()

    image = Image.open(self.photo)

    if small:
        image.thumbnail(settings.THUMB_SIZE, Image.ANTIALIAS)
        thumb_filename = thumb_name + '_thumb' + thumb_extension

    else:
        image.thumbnail((700, 700), Image.ANTIALIAS)
        thumb_filename = thumb_name + '_compressed' + thumb_extension

    if thumb_extension in ['.jpg', '.jpeg']:
        FTYPE = 'JPEG'
    elif thumb_extension == '.gif':
        FTYPE = 'GIF'
    elif thumb_extension == '.png':
        FTYPE = 'PNG'
    else:
        return False  # Unrecognized file type

    # Save thumbnail to in-memory file as StringIO
    temp_thumb = BytesIO()
    image.save(temp_thumb, FTYPE, quality=70)
    temp_thumb.seek(0)

    if small:
        # set save=False, otherwise it will run in an infinite loop
        self.thumbnail.save(thumb_filename, ContentFile(temp_thumb.read()), save=False)
    else:
        self.photo_compressed.save(thumb_filename, ContentFile(temp_thumb.read()), save=False)
    temp_thumb.close()

    return True

The View:

The website I created is basically a social media site like Facebook, so I am uploading the images in the post request when the user creates a post:

class Posts(LoginRequiredMixin, views.APIView):
    http_method_names = ['get', 'post', 'delete']

    def post(self, request):
        body = request.POST.get('body', "")
        images = request.FILES.get('file[0]', None)

        if body or images:
            author = request.user
            post = SocialMediaPost()
            post.body = body
            post.author = author
            post.datetime_created = timezone.now()
            post.save()

            files = [request.FILES.get('file[%d]' % i) for i in range(0, len(request.FILES))]

            for image in files:
                photo = Photo(photo=image, user=request.user, obj_type='post', obj_id=post.pk)
                photo.save()

        return JsonResponse({'message': 'Post created'}, status=200)

If files = [request.FILES.get('file[%d]' % i) for i in range(0, len(request.FILES))] looks a bit strange, that's because of the way Dropzone.js submits the form data. it took me some trial, error, and a lot of debugging to figure out the format.

The template:

My post form is relatively simple; a textarea, the dropzone button, and the submit button. Now that I think of it, I'm not sure if drag and drop works, but you can ignore my customization with the font awesome icon and use the default if you want that.

def saved_directory_path(instance, filename, root):
       <div class="container post-container pt-1">

        <div class="card shadow bg-white mt-3 mb-3">
            <div class="card-body">
                <form action="{% url 'posts' %}" method="POST" class="post-form" enctype="multipart/form-data"
                      id="post-form">{% csrf_token %}
                    <textarea class="form-control" placeholder="What's going on?" id="post-form-body"
                              name="body"></textarea>
                    <div class="row pt-3">
                        <div class="col align-middle">
                            <div class="dropzone dropzone-file-area" id="fileUpload">
                                <div class="dz-default dz-message">
                                    <span id="images" class="far fa-images mb-3" data-toggle="tooltip"
                                          title="Add Photos"></span>
                                </div>
                            </div>
                            <input id="images" name="file" type="file" multiple hidden="hidden">
                        </div>
                        <div class="col-1 ml-auto">
                            <button id="submit-all" type="submit" class="save btn btn-primary float-right">Submit
                            </button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>

The Script:

Dropzone by default uploads asynchronously as the user adds photos. That's cool, but harder to deal with (what if they don't create the post, how do I keep track of the relationship to the post?), so I decided to upload the images with the post. That unfortunately means I need to prevent default action (.preventDefault()) when the submit button is pressed and build the form data in javascript.

def saved_directory_path(instance, filename, root):
   <script>
    Dropzone.options.fileUpload = {
        url: '{% url 'posts' %}',
        thumbnailWidth: 80,
        thumbnailHeight: 80,
        dictRemoveFile: "Remove",
        autoProcessQueue: false,
        uploadMultiple: true,
        parallelUploads: 20,
        maxFiles: 20,
        maxFilesize: 20,
        acceptedFiles: ".jpeg,.jpg,.png,.gif",
        addRemoveLinks: true,
        init: function () {
            dzClosure = this; // Makes sure that 'this' is understood inside the functions below.

            // for Dropzone to process the queue (instead of default form behavior):
            document.getElementById("submit-all").addEventListener("click", function (e) {
                // Make sure that the form isn't actually being sent.
                e.preventDefault();
                e.stopPropagation();
                if (dzClosure.getQueuedFiles().length > 0) {
                    dzClosure.processQueue();
                } else {
                    console.log('ajax')
                    $.ajax({
                        url: {% url 'posts' %},
                        type: 'POST',
                        dataType: 'json',
                        data: {
                            'body': jQuery("#post-form-body").val(),
                            'csrfmiddlewaretoken': '{{csrf_token}}',
                        },
                        beforeSend: function (xhr) {
                            xhr.setRequestHeader("X-CSRFToken", '{{ csrf_token }}');
                        },
                        success: function (result) {
                            window.location.replace("{% url 'posts' %}");
                        }
                    });
                }
            });

            //send all the form data along with the files:
            this.on("sendingmultiple", function (data, xhr, formData) {
                formData.append("body", jQuery("#post-form-body").val());
                formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
            });

            // On success refresh
            this.on("success", function (file) {
                window.location.replace("{% url 'posts' %}");
            });
        }
    }
</script>

Comments

Popular posts from this blog

How to Use Django-Markdownx for Your Blog

Django Pagination with Bootstrap