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.
<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.
<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>
I will try to expand on some of the details, but that's the bulk of it. Post a comment if you have any questions.