Django Person Image Generator
Tags: django, tutorial, webdev, productivity
Published: 2021-01-30 18:51:50

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/](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.
```bash
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`.
```python
INSTALLED_APPS = [
    ...
    'project.persons'
]
```

At the bottom of `project/settings.py` add media settings because we will be saving images.
```python
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
```

Create media directories in root of project. We will be saving images to `media/persons/`.
```bash
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.
```python
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`.
```python
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](https://docs.djangoproject.com/en/3.1/topics/db/models/#overriding-predefined-model-methods).
```python
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](https://docs.djangoproject.com/en/3.1/ref/forms/widgets/).
```python
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`
```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.
```bash
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.
![Add Person](https://dev-to-uploads.s3.amazonaws.com/i/kxyiya1163vo4z8zgztn.PNG)

---
#### Change Person Admin Page
Just shows a preview of the Person's image.
![Change Person](https://dev-to-uploads.s3.amazonaws.com/i/vqm5nxpq9c5uh3gewl0l.PNG)

## End
And that's it! The code for this post is available on [GitHub](https://github.com/buckldav/django-person-generator/tree/659e089528714e593866904eed6dcacce18df2bc). Good luck!