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:
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:
pip install django cloudinary python-dotenv
django-admin startproject core .
python manage.py startapp sim
INSTALLED_APPS
in settings.py
.# settings.py
INSTALLED_APPS = [
...
'sim',
...
]
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
.env
file at core/.env
.env
fileCLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
settings.py
file to load your environment variables when your Django app starts.from dotenv import load_dotenv
load_dotenv()
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)
python manage.py makemigrations
python manage.py migrate
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.
templates
folder in sim/templates
index.html
in templates
folderindex.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>
services.py
in sim
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",
}
views.py
in sim
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)
urls.py
in sim
folderurls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
urls.py
in core
folder to have:from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('sim.urls')),
]
python manage.py runserver
http://localhost:8000/
and upload an image 🚀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. 💡