Minimalistic Multiupload in Django Admin
Django makes it very easy have a file upload field on a model in the admin. However, the admin doesn’t have a built-in facility for uploading multiple files at once, at the time of writing.
For one of my projects I wanted to allow the site admins to upload photos in the admin and have django handle creating the related database objects for the files automatically. Additionally, I wanted to display thumbnails of the photos as inlines.
Packages like django-filer or django-filebrowser support multiupload, but they don’t allow attaching a collection of files to an object in one step. As such, neither of them provided the workflow I wanted. And while the django-multiupload project was very close to what I needed, their pypi package was 3 years behind the git repo, and I didn’t want to depend on it in my project.
Here I present a basic solution to this use case. Please note that this is not a full-fledged gallery UI with AJAX uploads and progress bars. It also doesn’t manage reusing already uploaded files, yet. However, integrating it with django-filer may be possible.
Step 0: accept and save multiple uploads
It turns out you don’t really need much to handle uploading multiple files in the admin, and there is a hint in the django documentation.
Let’s say we have two models:
# models.py
from django.db import models
class Show(models.Model):
title = models.CharField(max_length=1024)
slug = models.SlugField(max_length=1024)
class ShowPhoto(models.Model):
show = models.ForeignKey(
Show, on_delete=models.CASCADE, related_name="photos"
)
photo = models.ImageField()
We add an additional form field to the admin page of the Show
object. The field is going to accept multiple files at once from the
user thanks to the multiple
html5 attribute on the input
element. In our form class we will handle creating the associated
photo instances.
# forms.py
from django import forms
from django.core.validators import validate_image_file_extension
from django.utils.translation import gettext as _
from .models import Show, ShowPhoto
class ShowAdminForm(forms.ModelForm):
class Meta:
model = Show
fields = (
"title",
"slug",
)
photos = forms.FileField(
widget=forms.ClearableFileInput(attrs={"multiple": True}),
label=_("Add photos"),
required=False,
)
def clean_photos(self):
"""Make sure only images can be uploaded."""
for upload in self.files.getlist("photos"):
validate_image_file_extension(upload)
def save_photos(self, show):
"""Process each uploaded image."""
for upload in self.files.getlist("photos"):
photo = ShowPhoto(show=show, photo=upload)
photo.save()
Now, we need to trigger the save_photos
method from our ModelAdmin
instance after the main object has been saved1:
# admin.py
from django.contrib import admin
from .models import Show, ShowPhoto
from .forms import ShowAdminForm
class ShowPhotoInline(admin.TabularInline):
model = ShowPhoto
@admin.register(Show)
class ShowAdmin(admin.ModelAdmin):
form = ShowAdminForm
inlines = [ShowPhotoInline]
def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
form.save_photos(form.instance)
At this point we have the multiupload working and our ShowPhoto
objects are being created automatically for the user. Next, we add a
few touches to make the UI slightly more tidy.
Step 1: display thumbnails in the admin
By default, admin would display the image field as a file name with an upload widget. In our photos admin inline, we would like to display a thumbnail instead. Since in my project I was already using easy-thumbnails, I decided to employ it in the admin too.
First, we configure an image size preset in settings.py
:
THUMBNAIL_ALIASES = {
"": {
"small": {"size": (150, 150)}
},
}
Then we add a method on our ShowPhotoInline
class that would act as
a dynamic “pseudofield” in the admin:
# admin.py
from django.contrib import admin
+from django.template.loader import get_template
+from django.utils.translation import gettext as _
from .models import Show, ShowPhoto
from .forms import ShowAdminForm
class ShowPhotoInline(admin.TabularInline):
model = ShowPhoto
+ fields = ("showphoto_thumbnail",)
+ readonly_fields = ("showphoto_thumbnail",)
+ max_num = 0
+
+ def showphoto_thumbnail(self, instance):
+ """A (pseudo)field that returns an image thumbnail for a show photo."""
+ tpl = get_template("shows/admin/show_thumbnail.html")
+ return tpl.render({"photo": instance.photo})
+
+ showphoto_thumbnail.short_description = _("Thumbnail")
We have also limited the field list of the inline to only include the
thumbnail and set it as readonly. We also disable the UI for adding
new inlines, by setting max_num
to 0
, since we already handle
adding new photos via our photo multiupload field.
Finally, we create a little template to render the thumbnail at
shows/templates/shows/admin/show_thumbnail.html
where shows
is the
name of our app:
{% load thumbnail %}
<a href="{{ photo.url }}">
<img src="{{ photo|thumbnail_url:'small' }}" />
</a>
And… that’s it! Your admins can now attach multiple photos to models in one go.
Conclusion
This solution is very basic, but it does the job. There are important aspects that I have skipped in this tutorial in order to keep it simple - for instance, depending on the nature of uploaded images (decorative vs. informative) you might want to require the admins to describe each one with an appropriate alternative text. Or it may be important for you to allow the user to reorder the images; or let them upload via drag-and-drop, etc. I leave these details as an exercise for the reader.
-
Handling it in the form’s
save()
method won’t work, since ModelAdmin handles saving the object separately and only calls form’ssave()
method withcommit=False
. ↩