There is the common problem in web development where you need to have randomly generated people with faces, especially for mockups. I ran into this problem the other day when making a Django app. Knowing that https://thispersondoesnotexist.com/ is a thing, I decided to write a Person model and admin that fetches images from that site. Here are the features I wanted that I will demonstrate below:
- A way to preview the generated image in the Django admin interface.
- A way to generate a new image in the admin interface if I didn't like the first one that was generated.
- A way to handle saving and deleting image files on creation and deletion of the model.
With that said, let's dive into it!
Project Setup
Create a Django project with a persons app.
pip install Django Pillow requests
django-admin startproject project .
cd project
django-admin startapp persons
Make sure to add persons
to INSTALLED_APPS in project/settings.py
.
INSTALLED_APPS = [
...
'project.persons'
]
At the bottom of project/settings.py
add media settings because we will be saving images.
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
Create media directories in root of project. We will be saving images to media/persons/
.
media/
persons/
project/ # our Django project
persons/ # our Django app
...
...
Image Downloader
Create a view in persons/views.py
that uses the requests library to download an image from https://thispersondoesnotexist.com/image. We will be fetching this view from our Person admin later.
from django.http import JsonResponse
from django.conf.global_settings import MEDIA_URL
import requests
def generate_image(request):
# Use this site to generate an image file of a person
url = 'https://thispersondoesnotexist.com/image'
req = requests.get(url)
# Adjust the names and paths below to fit your project
filename = '/persons/tmp.jpg'
with open("media" + filename, "wb+") as f:
f.write(req.content)
return JsonResponse(data={
"path": MEDIA_URL + filename
})
Add the view and the media static server to urls.py
.
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path
from project.persons.views import generate_image
urlpatterns = [
path('admin/', admin.site.urls),
path('generate-image/', generate_image, name="generate-image")
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Visit or curl
http://127.0.0.1:8000/generate-image/ to make sure it works.
Models and Admin
Create a Person model in persons/models.py
. The save()
method is overwritten to make sure that the image saved by default
(our generated image) gets saved as its own file. A little image processing with the Pillow library resizes the image. The delete()
method is also overwritten to make sure the image file gets deleted along with the model. For more on overriding these methods, visit here.
from django.db import models
from shutil import copy2
from datetime import datetime
import os
from PIL import Image
MEDIA_LOCATION = "persons"
DEFAULT = "tmp.jpg"
class Person(models.Model):
name = models.CharField(max_length=100)
image = models.ImageField(upload_to=MEDIA_LOCATION, default=f"{MEDIA_LOCATION}/{DEFAULT}")
def save(self, *args, **kwargs):
if DEFAULT in self.image.url:
# Copy image to a new, unique file related to the person's name and the time of download
new_filename = f"{self.name[:10]}-{datetime.now().isoformat()}"
new_filename = "".join([c for c in new_filename if c.isalpha() or c.isdigit()]).rstrip() + ".jpg"
copy2(f"media/{MEDIA_LOCATION}/{DEFAULT}", f"media/{MEDIA_LOCATION}/{new_filename}")
self.image = f"{MEDIA_LOCATION}/{new_filename}"
# Save the image and resize it
super(Person, self).save(*args, **kwargs)
# The [1:] slice is to remove the / in front of /media/persons/...
im = Image.open(self.image.url[1:])
if im.width > 150:
im = im.resize((150, 150))
im.save(self.image.url[1:])
def delete(self, *args, **kwargs):
# Delete the image file along with the model
try:
os.remove(self.image.url[1:])
except:
pass
super(Person, self).delete(*args, **kwargs)
def __str__(self):
return f"Person {self.id}: {self.name}"
Register the model in persons/admin.py
. We're going to do a little admin customization using a custom widget so we can preview and generate images. For more on widgets, see here.
from django.contrib import admin
from django.db import models
from django.forms.widgets import ClearableFileInput
from project.persons.models import Person
class ImageWidget(ClearableFileInput):
template_name = "image_widget.html"
class PersonAdmin(admin.ModelAdmin):
formfield_overrides = {
models.ImageField: {'widget': ImageWidget},
}
admin.site.register(Person, PersonAdmin)
Here's the template to go along with it. Put it in persons/templates/image_widget.html
{% include "django/forms/widgets/clearable_file_input.html" %}
{% if widget.value %}
{% comment %} This is for when you are changing a person {% endcomment %}
<img src="{{ widget.value.url }}" id="currentImage" width="100" alt="current image">
{% else %}
{% comment %} This is for when you are adding a person {% endcomment %}
<div>
<h5>If no file above, this generated image will be used</h5>
<img id="generatedImage" width="100">
<button id="newImage">Get New Image</button>
<span id="loading" style="display: none;">Loading...</span>
</div>
<script>
function loadImage() {
// The date thingy at the end is prevent image caching
document.getElementById("generatedImage").setAttribute("src", "/media/persons/tmp.jpg?"+new Date().toISOString())
}
loadImage()
async function getNewImage(e) {
const loading = document.getElementById("loading")
loading.style.display = "inline"
e.preventDefault()
await fetch('{% url "generate-image" %}').then(() => {
loadImage()
loading.style.display = "none"
})
}
document.getElementById("newImage").onclick = getNewImage
</script>
{% endif %}
Be sure to migrate the database. I'm just using the default sqlite database from settings. Create a superuser too in order to log in to admin.
python3 manage.py makemigrations
python3 manage.py migrate
python3 manage.py createsuperuser
Once those things are set up, run the server and visit 127.0.0.1:8000/admin/
to try adding persons. You should see something like the following:
Add Person Admin Page
Click "Get New Image" to fetch another person that doesn't exist using the generate-image
view we wrote earlier.
Change Person Admin Page
Just shows a preview of the Person's image.
End
And that's it! The code for this post is available on GitHub. Good luck!
Top comments (3)
There's a library if you want to abstract away the logic.
pypi.org/project/thispersondoesnot...
Usually the chunk size is much smaller at 1024.
If you're going to use a massive chunk size like that you might as well leave out chunks and use less code.
jdhao.github.io/2020/06/17/downloa...
Using
with
block instead of open and close is also safer and more common.Would also love to see 4 space indention instead of 2 for Python code, to match the recommended style.
Fixed! Thanks. I didn't proofread much before upload :)