In 5 small steps, we'll build a Django app that uploads images to the cloud and stores the links in our database. This allows us to show the uploaded images on our app.
We'll:
- get a signed upload url from Cloudinary;
- upload the image; and
- save the image link to our database with Django
The images will then be available for future processing - e.g., detecting faces, creating captions, or training AI generation models (Personal example 🙂).
Let's begin 🐎
Here's an optional video tutorial (featuring me 🏇🏿) that follows the written guide below:
Edit: Thanks to Alex Goulielmos (from Youtube) for carefully reading this guide and correcting an error in the original version 👍
Setup
Setup your Django app
- Create a new Django and install requirements
pip install django cloudinary python-dotenv
django-admin startproject core .
python manage.py startapp sim
- Register your new app by adding it to your
INSTALLED_APPS
insettings.py
.
# settings.py
INSTALLED_APPS = [
...
'sim',
...
]
Setup Cloudinary (for storing your images in the cloud)
You can use any cloud storage service you want, but I'm using Cloudinary. It has a free tier and is easy to setup. (Here's a guide I wrote using AWS S3 instead of Cloudinary: https://www.photondesigner.com/articles/upload-files-properly-django-htmx?ref=upload-images-cloud-django)1
- Create an account on Cloudinary
- Go to "Programmable Media" > "Product Environment Credentials" where you'll see your "Cloud name", "API Key", and "API Secret".
Setup your environment variables
- Create a
.env
file atcore/.env
- Add the following variables to your
.env
file
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
- Add the following code to the top of your
settings.py
file to load your environment variables when your Django app starts.
from dotenv import load_dotenv
load_dotenv()
1. Add your models
- Add this to
sim/models.py
from django.db import models
class Image(models.Model):
key = models.CharField(help_text="The public id of the uploaded file", max_length=100)
url = models.CharField(max_length=100)
name = models.CharField(max_length=100, help_text='The original name of the uploaded image')
width = models.IntegerField(help_text='Width in pixels')
height = models.IntegerField(help_text='Height in pixels')
format = models.CharField(max_length=10)
created_at = models.DateTimeField(auto_now_add=True)
- Create your database by make migrations and migrating
python manage.py makemigrations
python manage.py migrate
2. Create your templates
Now we'll add the HTML and the JavaScript that will upload images client-side.
This will upload the image to Cloudinary, and return the link to our Django app to save to our database. We'll use Alpine.js (very lightweight and easy to use) to simplify the JavaScript.
- Create
templates
folder insim/templates
- Create
index.html
intemplates
folder - Add the following code to
index.html
<!DOCTYPE html>
<html lang="en" xmlns:x-on="http://www.w3.org/1999/xhtml" xmlns:x-bind="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Upload</title>
<script src="//unpkg.com/alpinejs" defer></script>
</head>
<body>
<section class="h-screen flex bg-gray-100">
<div class="m-auto grid image-uploader p-4 rounded-lg shadow-lg bg-white">
<div class="" x-data="{ file: null, message: '', imagePreview: ''}" x-cloak>
<div class="preview-container" x-on:click="$refs.fileInput.click()">
<img class="preview-image" x-show="imagePreview" :src="imagePreview" alt="Placeholder image">
<div x-show="!imagePreview" class="text-gray-400 preview-text" >
Click to choose an image
</div>
</div>
<input id="file-upload" type="file" style="display: none;" x-on:click="message = ''" x-ref="fileInput" x-on:change="showPreview">
<button class="button" x-on:click="upload" x-bind:disabled="!file">Upload</button>
<div x-text="message" class="message"></div>
</div>
<div class="image-grid-wrapper">
<div class="image-grid">
{% for image in images %}
<div class="image-wrapper">
<a class="image-link" href="{{ image.url }}" target="_blank">
<img src="{{ image.url }}" alt="{{ image.name }}">
</a>
</div>
{% endfor %}
</div>
</div>
</div>
</section>
<script>
async function upload() {
/*
Upload a file to S3 using the presigned URL.
*/
const formData = new FormData();
formData.append('signature', "{{ signature }}");
formData.append('api_key', "{{ api_key }}");
formData.append('timestamp', "{{ timestamp }}");
formData.append('file', this.file);
const response = await fetch("{{ upload_url }}", { method: 'POST', body: formData });
if (response.ok){
this.message = '✅ Upload successful'
const data = await response.json()
await saveLink(data)
}
else{
this.message = `❌ Upload failed: The error message is ${response.statusText}`
}
}
async function saveLink(uploadedData){
console.log("uploadedData = ", uploadedData)
const response = await fetch("", {
method: 'POST',
body: JSON.stringify(uploadedData),
headers: {
'X-CSRFToken': "{{ csrf_token }}"
}
}
);
if (response.ok) {
location.reload();
}
else {
const data = await response.json();
console.error('Error:', data.error);
}
}
function showPreview() {
const file = this.$refs.fileInput.files[0];
this.file = file;
if (file) {
this.imagePreview = URL.createObjectURL(file);
} else {
this.imagePreview = '';
}
}
</script>
</body>
</html>
<style>
:root {
--border-radius: 10px;
}
.h-screen {
height: 100vh;
}
.flex {
display: flex;
}
.bg-gray-100 {
background-color: #f7fafc;
}
.m-auto {
margin: auto;
}
.p-4 {
padding: 1rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.shadow-lg {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.bg-white {
background-color: #ffffff;
}
.text-gray-400 {
color: #cbd5e0;
}
.image-uploader {
place-content: center;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
border-radius: 15px;
}
.image-grid-wrapper {
height: 240px;
overflow-y: scroll;
}
.image-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
overflow-y: auto;
}
.image-wrapper {
position: relative;
overflow: hidden;
display: flex;
justify-content: space-evenly;
}
.image-wrapper img {
width: 100px;
object-fit: cover;
display: block;
border-radius: var(--border-radius);
transition: transform 0.3s ease-in-out;
}
.image-wrapper:hover img {
transform: scale(1.1);
cursor: pointer;
}
.image-link {
display: flex;
}
.preview-container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
width: 100%;
border: 1px dashed #ddd;
background: #fafafa;
border-radius: var(--border-radius);
}
.preview-text {
font-family: Arial, Helvetica, sans-serif;
cursor: pointer;
color: #cbd5e0; /* Tailwind text-gray-400 */
}
.preview-image {
max-width: 200px;
max-height: 200px;
border: 1px dotted #ddd;
object-fit: cover;
display: block;
background: #ddd;
}
.button {
padding: 8px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
border-radius: var(--border-radius);
transition: 0.3s ease-out;
width: 100%;
}
.button:not(:disabled){
border: 1px solid slategray;
color: #4f5a65;
}
.button:not(:disabled):hover {
cursor: pointer;
background-color: slategray;
color: white;
}
.message {
text-align: center;
}
[x-cloak] {
display: none !important;
}
</style>
3. Generate the signed upload url
- Create
services.py
insim
folder containing:
from datetime import datetime
import cloudinary
import os
cloudinary.config(
cloud_name=os.getenv('CLOUDINARY_CLOUD_NAME'),
api_key=os.getenv('CLOUDINARY_API_KEY'),
api_secret=os.getenv('CLOUDINARY_API_SECRET'),
)
def generate_signature() -> dict:
"""
Generate a signed Cloudinary upload url.
"""
timestamp = datetime.now().timestamp()
params_to_sign = {"timestamp": timestamp}
signature = cloudinary.utils.api_sign_request(params_to_sign, cloudinary.config().api_secret)
return {
'signature': signature,
'api_key': cloudinary.config().api_key,
'timestamp': timestamp,
'upload_url': f"https://api.cloudinary.com/v1_1/{cloudinary.config().cloud_name}/image/upload",
}
4. Create your views
- Create or update
views.py
insim
folder to have:
import json
from django.http import HttpResponse
from django.shortcuts import render
from .models import Image
from .services import generate_signature
def index(request) -> HttpResponse:
if request.method == 'GET':
images = Image.objects.all().order_by('-created_at')
return render(
request, 'index.html', context={
'images': images, **generate_signature()
}
)
elif request.method == 'POST':
body = request.body.decode('utf-8')
data = json.loads(body)
Image.objects.create(
key=data['public_id'], url=data['secure_url'],
width=data['width'], height=data['height'],
format=data['format'], name=data['original_filename'],
)
return HttpResponse(status=201)
5. Create your urls
- Create
urls.py
insim
folder - Add the following code to
urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
- Update
urls.py
incore
folder to have:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('sim.urls')),
]
Upload! 🌤️
- Run your Django app
python manage.py runserver
- Go to
http://localhost:8000/
and upload an image 🚀
P.S Photon Designer - This is how I build Django apps
Even for this small guide, styling the frontend took me way too much time (over an hour, and I'm a pro). Probably like you, I want to go from idea to elegant Django frontend as fast as possible (preferably instantly).
So, that's why I'm building Photon Designer.
Photon Designer lets me produce Django frontend visually and extremely quickly - like a painter sweeping his brush across the page 🖌️. You drag and drop elements, style them visually in any way you'd like, and export clean code to your Django app. It's like Photoshop but for Django apps. And much, much faster. 💡