DEV Community

Sharon Yelenik for Cloudinary

Posted on • Edited on

Django Lookbook with CloudinaryField, AI Crops, and UGC (Photo Collection App)

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, and CloudinaryField for image columns.
  • Transforms: utils.py helpers (generate_transformation_options, create_transformed_url) driving overlays and g_face-style delivery URLs.
  • Django ORM & filters: e.g. all_lookbooks filtering 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.

Homepage navigation features

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.

User account features

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 CloudinaryField in 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')
Enter fullscreen mode Exit fullscreen mode
  • 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'
)
Enter fullscreen mode Exit fullscreen mode

Profile image

  • Retrieval in views and templates: In views, you load the Lookbook and related images; in the template, you render the stored transformation URL on each img tag.

views.pydisplay 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})
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode
  • 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)
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Lookbook image

# 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)
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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'),
]
Enter fullscreen mode Exit fullscreen mode

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)