A Django lookbook stores user photos in Cloudinary (not on your app disk) and uses AI-aware crops, overlays, and orientation rules per lookbook. This post tours models, views, templates, and transformation helpers for UGC and a Django + Cloudinary image gallery.
What you’ll see in the code
-
Models:
Lookbook,Profile, andCloudinaryFieldfor image columns. -
Transforms:
utils.pyhelpers (generate_transformation_options,create_transformed_url) driving overlays andg_face-style delivery URLs. -
Django ORM & filters: e.g.
all_lookbooksfiltering by user. -
Auth & profiles: ForeignKey / OneToOne to
User, signup/login routes, and profile data on cards.
This lookbook ( Django + Cloudinary ) stores user photos in Cloudinary, not on disk, and applies per-lookbook overlays, borders, and orientation. Below we walk through models, views, and transformation helpers.
These apps, also known as lookbooks, show up in fashion, real estate, travel, and anywhere you need a user-generated photo gallery.
I built a comprehensive lookbook with Django and Cloudinary. You can get a feel for it from a video walkthrough and the notes below, plus a fork of the repo. We’ll also cover features that matter if you’re wiring Django CloudinaryField and transformation URLs in production.
Tip: Fork the app from GitHub, sign up for a free Cloudinary account, and use the YouTube series that focuses on specific areas of the app.
App Description
Here’s what the app offers:
Homepage navigation
- View all lookbooks.
- Filter lookbooks by user.
- See user profile pictures on their lookbooks.
User account features
- Sign up and log in to create a profile with a bio and profile picture.
- Create, edit, and delete lookbooks.
- Upload and customize images using Cloudinary.
Features this repo demonstrates
This lookbook app integrates Cloudinary for UGC — managing and delivering images with transforms and overlays instead of piling raw binaries into your database or local storage.
Integrating Cloudinary for image management
One of the main pieces is using Cloudinary for lookbook and profile images: upload once, then deliver URL-based transformations (and overlays) from the CDN.
-
CloudinaryField usage:
Using
CloudinaryFieldin Django models keeps file bytes off your server. It plugs into the ORM and forms and stores delivery URLs in the fields you read in templates. (See Dynamic Form Handling.)
# models.py
from cloudinary.models import CloudinaryField
class Lookbook(models.Model):
overlay_image = CloudinaryField('overlay_images')
-
Transformations and overlays:
The app uses AI-style crops and generative fill to match the lookbook’s orientation and border options, and layered overlays when a user picks an asset. Utility functions in Utility Function for Image Processing centralize the transformation dict you pass to
build_url. For profile photos, face-gravity crops keep heads centered, for example:
Tip: Open the original and transformed URLs in a new tab to compare the string.
profile_url = CloudinaryImage(public_id).build_url(
quality='auto', width=600, height=600, crop='auto', gravity='face'
)
-
Retrieval in views and templates:
In views, you load the
Lookbookand related images; in the template, you render the stored transformation URL on eachimgtag.
views.py — display view:
# views.py
def display(request, lookbook_id):
lookbook = get_object_or_404(Lookbook, pk=lookbook_id)
images = lookbook.images.all()
return render(request, 'display.html', {'photos': images, 'lookbook': lookbook})
templates/display.html — loop over each photo’s transformation URL:
<!-- templates/display.html -->
{% for photo in photos %}
<div class="gallery-col">
<div class="gallery-image">
<img src="{{ photo.transformed_image }}" alt="Image" class="img-fluid">
<div class="gallery-overlay">
<a href="{{ photo.transformed_image }}" target="_blank"><h5>Click to View</h5></a>
</div>
</div>
</div>
{% endfor %}
-
What Cloudinary is doing for you here:
CloudinaryField(and the URLs you save) let the CDN resize, reformat, and cache derivatives so your Django app isn’t the bottleneck for heavy image work—that’s the practical upside behind “scalable” delivery.
User-generated content management
A big part of the product is that each user owns their lookbooks and profile.
-
User associations — ForeignKey to
User:
# models.py
from django.contrib.auth.models import User
class Lookbook(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
- Profiles — one profile per user, Cloudinary profile image, and many-to-many to lookbooks:
# models.py
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
profile_picture = CloudinaryField('profile_pictures')
profile_url = models.URLField(blank=True, null=True, default='https://res.cloudinary.com/yelenik/image/upload/avatar')
bio = models.TextField(blank=True)
my_lookbooks = models.ManyToManyField(Lookbook, blank=True)
def __str__(self):
return self.user.username
templates/all_lookbooks.html — cards list lookbooks and pull the user’s profile_url:
<!-- templates/all_lookbooks.html -->
<div class="row">
{% for lookbook in lookbooks %}
<div class="col-md-4">
<div class="card mb-4">
<div class="card-body all">
<div class="d-flex align-items-center mb-3">
<img src="{{ lookbook.user.profile.profile_url }}" alt="{{ lookbook.user.username }}" class="rounded-circle mr-2" style="width: 40px; height: 40px;">
<h5 class="mb-0">{{ lookbook.user.username }}</h5>
</div>
<h5 class="card-title">{{ lookbook.title }}</h5>
<p class="card-text">{{ lookbook.description }}</p>
<a href="{% url 'display' lookbook.id %}" class="btn btn-primary card-btn">View</a>
</div>
</div>
</div>
{% empty %}
<div class="col">
<p>No lookbooks found.</p>
</div>
{% endfor %}
</div>
Dynamic Form Handling
Forms drive lookbook and image create/edit flows, with ModelForm subclasses and save hooks so Cloudinary-backed fields persist correctly when a user submits.
# forms.py
class LookbookForm(forms.ModelForm):
class Meta:
model = Lookbook
fields = ['title', 'description', 'overlay_image']
def save(self, commit=True):
instance = super().save(commit=False)
if commit:
instance.save()
return instance
Utility Function for Image Processing
When users edit a lookbook, you still need a single place to compute the transformation chain. Helpers like generate_transformation_options, get_public_id_from_url, and create_transformed_url keep edit and create paths consistent and use AI-backed options (e.g. gen_fill) where you need them.
Here’s an example of an original and transformed lookbook image and the code that builds the transformation list:
Tip: Open those URLs in a browser to inspect the full transformation chain.
# utils.py
def generate_transformation_options(lookbook):
# Determine width and height based on orientation
if lookbook.orientation == 'portrait':
width, height = 400, 500
elif lookbook.orientation == 'landscape':
width, height = 800, 600
else: # square
width, height = 800, 800
# Initialize transformation list if not already present
transformation_options = { }
if 'transformation' not in transformation_options:
transformation_options['transformation'] = []
# Apply border style if border width is not '0px'
if lookbook.border_width != '0px':
transformation_options['border'] = f'{lookbook.border_width}_solid_{lookbook.border_color}'
# Define base transformation options
all_transformation = [
{'quality': 'auto',
'width': width,
'height': height,
'crop': 'pad',
'background': 'gen_fill:ignore-foreground_true'}
]
transformation_options['transformation'].insert(0, all_transformation)
# Apply overlay image if provided
if lookbook.overlay_image:
overlay_transformation = [
{'overlay': lookbook.overlay_image, 'gravity': 'north_east', 'width': 100, 'flags': 'layer_apply', 'x': 20, 'y': 20, 'opacity': 80}
]
transformation_options['transformation'].insert(1, overlay_transformation)
# Add scale transformation to make the width 800
transformation_options['transformation'].insert(2, {'width': 800, 'crop': 'scale'})
return transformation_options
def create_transformed_url(public_id, transformation_options):
return CloudinaryImage(public_id).build_url(**transformation_options)
Efficient data querying and filtering
The lookbook app uses the Django ORM to fetch and filter lookbooks by the selected user(s) — e.g. all_lookbooks:
# views.py
def all_lookbooks(request):
users = User.objects.all()
user_ids = request.GET.getlist('user_ids')
if not user_ids or 'all' in user_ids:
user_ids = [str(user.id) for user in users]
lookbooks = Lookbook.objects.filter(user__id__in=user_ids).order_by('title')
return render(request, 'all_lookbooks.html', {'lookbooks': lookbooks, 'users': users, 'selected_user_ids': user_ids})
Robust URL routing
urls.py maps paths to lookbook, profile, and auth views in one place.
# urls.py
from django.urls import path
from lookbook_app import views
from django.contrib.auth import views as auth_views
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
path('signup/', views.signup, name='signup'),
path('create_lookbook/', views.create_lookbook, name='create_lookbook'),
path('display/<int:lookbook_id>/', views.display, name='display'),
path('profile/', views.profile, name='profile'),
path('my_lookbooks/', views.my_lookbooks, name='my_lookbooks'),
path('all_lookbooks/', views.all_lookbooks, name='all_lookbooks'),
path('', views.all_lookbooks, name='all_lookbooks'),
path('edit_lookbook/<int:lookbook_id>/', views.edit_lookbook, name='edit_lookbook'),
]
Conclusion
This lookbook app shows how Django and Cloudinary can power user-generated portfolios with reusable transformation logic, ORM-friendly models, and a clear URL layout. Fork it on GitHub and go deeper in the YouTube walkthrough.
| Cloudinary ❤️ developers |
|---|
| Ready to level up your media workflow? Start using Cloudinary for free and build better visual experiences today. |
| 👉 Create your free account |




Top comments (0)