abidibo.net

Django & Dropzone js

javascript dropzone django ui

Hi everybody, this one is just an example of how you can integrate Dropzone.js in your Django application. It's just a basic example, but probably a good starting point.

My scenario is a sort of two steps form: I have a Sale model which can have many images associated.

class Sale(models.Model):
    title = models.CharField(_("titolo"), max_length=255)
    # ...


class SaleImage(models.Model):
    sale = models.ForeignKey(
        Sale, verbose_name="vendita", related_name="images", on_delete=models.CASCADE
    )
    image = models.ImageField(_("immagine"), upload_to=sale_image_file_name)
    # ...

The problem with Dropzone is that it tries to do everything automagically and that files are uploaded by ajax. That means that images are not uploaded when the user presses a submit button transmitting also other data (the ones we need to create the Sale object). Of course, we need a Sale instance before inserting images associated to it.

So I decided to split the form in two: first, the user inserts the Sale information, then after saving he can edit it and add images.

Another thing to manage is the presence of already uploaded images. I mean, this is an update view, so maybe we already have some images associated and I want them to be graphically displayed as the new inserted ones! For this reason, we'll customize a bit the Dropzone behaviour, attaching some callbacks to its managed events.

Let's start from the template.

{% block dropzone %}
<div class="dropzone mt-4 mb-4" id="dropzone">
    <div class="images-preview" id="dropzone-images-preview">
        {% for image in object.images.all %}
            {% thumbnail image.image "120x120" crop="center" as im %}
            <div class="thumb" data-id="{{ image.id }}">
                    <i class="fa fa-remove"></i>
                    <img class="img-thumbnail" src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}">
                </div>
            {% endthumbnail %}
        {% endfor %}
    </div>
    <div class="spinner" id="dropzone-spinner"><i class="fa fa-spinner fa-spin"></i></div>
</div>
<script charset="utf-8">
    (function ($) {
        var thumbs = [];
        var csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
        Dropzone.autoDiscover = false;

        var deleteImage = function () {
            var thumb = $(this).parent('.thumb');
            var id = thumb.attr('data-id');
            $.get('{% url 'market:update-sale-images' object.id %}?image_id=' + id, function (data) {
                if (data.status) {
                    thumb.remove();
                }
            })
        }

        $('#dropzone-images-preview .thumb i').on('click', deleteImage);

        var myDropzone = new Dropzone(
            "div#dropzone",
            {
                url: "{% url 'market:update-sale-images' object.id %}",
                params: {'csrfmiddlewaretoken': csrftoken},
                acceptedFiles: 'image/*',
                addedfile: function (file) {
                    $('#dropzone-spinner').addClass('active');
                },
                },
                complete: function () {
                    $('#dropzone-spinner').removeClass('active');
                },
                success: function (klass, data) {
                    thumbs.forEach(function (thumb) {
                        $('#dropzone-images-preview').append($('<div />', { class: 'thumb' })
                        .attr('data-id', data.id)
                        .append(
                            $('<img />', { class: 'img-thumbnail', src: thumb}),
                            $('<i />', { class: 'fa fa-remove' }).on('click', deleteImage)
                        ))
                    })
                    thumbs = []
                },
                thumbnail: function (klass, dataUrl) {
                    thumbs.push(dataUrl);
                    return null;
                }
            }
        );
    })(jQuery)
</script>
{% endblock dropzone %}

Some considerations:

  • Lines 2-14: this is the dropzone area. I've added an images preview div including all already associated images (thumbs with remove icon), and a spinner I'll use as undetermined progress feedback while uploading an image (I'll disable the default Dropzone behaviour because I need to manage images preview myself). There is room for improvements here: you can implement your custom progress bar to show upload progress.
  • Lines 17-19: I create a thumbs array that will keep all updates images until they're not shown as preview images. I read the CSRF token (which is present in another part of the template) and I tell Dropzone to avoid auto discovering feature.
  • Line 21-29: this is the dele image function, which performs an ajax request and removes the image thumb on success image deletion
  • Line 31: I attach the previous function to the remove icon click events. 
  • Lines 33-63: my Dropzone configuration. When a file is added I activate the spinner overlay, which will be deactivated on complete. When the thumbnail is ready (L58) I push the dataUrl into the thumbs array. When the upload is successful I create a thumb for each item in the thumbs array and then I clean it. I also attach the deleteImage callback to the newly generated thumb.

Now let's see a bit of SCSS:

#dropzone {
    align-items: center;
    border: 4px dashed #eee !important;
    display: flex;
    flex-direction: column;
    justify-content: center;
    position: relative;

    .dz-message {
        margin: 2rem 0 0;
    }
}

.spinner {
    align-items: center;
    background: rgba(255, 255, 255, .8);
    bottom: 0;
    display: none;
    color: #999;
    flex-direction: row;
    font-size: 2rem;
    left: 0;
    justify-content: center;
    position: absolute;
    right: 0;
    top: 0;


    &.active {
        display: flex;
        padding: 1rem 0;
    }

}

.images-preview {
    align-items: center;
    display: flex;
    flex-direction: row;
    justify-content: center;

    .thumb {
        margin: 0 .5rem;
        position: relative;

        img {
            border-radius: 20px;
        }

        .fa {
            align-items: center;
            background: #000;
            border-radius: 50%;
            color: #fff;
            cursor: pointer;
            display: flex;
            height: 25px;
            justify-content: center;
            opacity: .3;
            position: absolute;
            right: 15px;
            top: 15px;
            width: 25px;

            &:hover {
                opacity: 1;
            }
        }
    }
}

And finally the django view:

class SaleImagesUpdateView(View):
    # GET to delete an image
    def get(self, request, pk):
        try:
            id = request.GET.get('image_id')
            sale_image = get_object_or_404(SaleImage, id=id)
            sale_image.delete()

            data = {'status': True}
            return JsonResponse(data)
        except:
            data = {'status': False}
            return JsonResponse(data)

    # POST to add an image
    def post(self, request, pk):
        sale = get_object_or_404(Sale, pk=pk)
        try:
            files = request.FILES.getlist('file')
            for filename in files:
                save_image = SaleImage(sale=sale, image=filename)
                save_image.save()

                data = {'status': True, 'id': save_image.id}
                return JsonResponse(data)
        except KeyError:
            pass

        data = {'status': False}
        return JsonResponse(data)

That's all, bye!

Subscribe to abidibo.net!

If you want to stay up to date with new contents published on this blog, then just enter your email address, and you will receive blog updates! You can set you preferences and decide to receive emails only when articles are posted regarding a precise topic.

I promise, you'll never receive spam or advertising of any kind from this subscription, just content updates.

Subscribe to this blog

Comments are welcome!

blog comments powered by Disqus

Your Smartwatch Loves Tasker!

Your Smartwatch Loves Tasker!

Now available for purchase!

Featured