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
Post a Comment