Home > django > Creating User Profiles with Image Thumbnails in Django

Creating User Profiles with Image Thumbnails in Django

November 20th, 2008

I am currently working on a django application that will include a profile page for each user.  Like many web apps, it will have the ability to upload a profile picture, and have this picture shrunk to a suitable thumbnail size.

To complicate matters, I want the photo uploaded to a directory which will be named after the user.

I’m fairly new to Django, so it took some time to piece this all together, but someone out there may have had the same problems and I hope this post helps.

Prerequisites

You must have the Python Imaging Library installed. You can get this from the PIL homepage.

User Profiles

The first thing we need to do is create a model for our user profile.

from django.db import models
from django.contrib.auth.models import User

class UserProfile(models.Model):
    user = models.ForeignKey(User, unique=True)
    nickname = models.CharField(max_length=50, blank=True, null=True)
    photo = models.ImageField(upload_to='profile_pics', blank=True, null=True)
    thumbnail = models.ImageField(upload_to='profile_thumb', blank=True, null=True,
          editable=False)

I’ve made the upload_to attributes static for the time being, but we will be changing this later.

User Profile Setup

So that Django knows which model to use for you user profile, you have to add a setting to your settings file.

AUTH_PROFILE_MODULE = 'appname.UserProfile'

Make sure that this uses your app name, not your project name. I’ve used UserProfile for the model, but you can call it whatever you like.

Dynamic Upload Directory

To make the upload_to attribute dynamic based on the current user, we have to write a small function that returns the appropriately formed path. My thanks to Josh Ourisman and his post for this part.

Create a file called files.py and save it in your application directory.

def get_profile_path(instance, filename)
    dir = "%s/profile_pics/%s" % (instance.user, filename)
    return dir

This will make the profile picture save in the directory ‘username/profile_pics/filename’. You can move these around if you want a different directory structure. The MEDIA_ROOT value from your settings file will be prepended to this path automatically, so make sure you set that.

Amend the Model

Now back to our model, where we call our new function from the upload_to attribute.

from projname.appname.files import get_profile_path

class UserProfile(models.Model):
    user = models.ForeignKey(User, unique=True)
    nickname = models.CharField(max_length=50, blank=True, null=True)
    photo = models.ImageField(upload_to='get_profile_path', blank=True, null=True)
    thumbnail = models.ImageField(upload_to='profile_thumb', blank=True, null=True,
         editable=False)

We will leave the thumbnail upload_to field, as we will be dealing with that one manually.

Creating the View

I wanted to just create one view that would handle creating a new profile, and displaying and updating an existing profile. First I’ll create a form to display in the view.

from django import forms
from django.forms import ModelForm
from projname.appname.models import UserProfile

class UserProfileForm(ModelForm):
    class Meta:
        model = UserProfile
        exclude = ('user')

Now to the view

from django.shortcuts import render_to_response
from django.template import RequestContext
from famblog.blog.forms import UserProfileForm
from famblog.blog.models import UserProfile

from django.contrib.auth.decorators import login_required

@login_required
def profile(request):
    try:
        myprofile = request.user.get_profile()
    except:
        up = UserProfile(user=request.user)
        up.save()
        myprofile = request.user.get_profile()

    if request.method == 'POST':
        f = UserProfileForm(request.POST, request.FILES, instance=myprofile)
        if f.is_valid():
            f.save()
    else:
        f = UserProfileForm(instance=myprofile)

    return render_to_response('appname/profile.html', {'f':f, 'profile':myprofile},
        context_instance = RequestContext(request))

This view will try and retrieve the user profile based on the current logged in user. If the user profile doesn’t exist, then an exception is thrown. If this happens, we create a profile with just the username and call get_profile() again. We then deal either with a POST event, or we bind the retrieved profile instance to the form ready for updating by the user.

If you don’t want to create thumbnails of your images, you can leave it here. The images will have been loaded

Thumbnail Creation

To create the thumbnail, we now need to override the save def in our model.

def save(self, force_insert=False, force_update=False):
        #get mtime stats from file
        thumb_update = False

        if self.thumbnail:
            statinfo1 = os.stat(self.photo.path)
            statinfo2 = os.stat(self.thumbnail.path)
            if statinfo1 > statinfo2:
                thumb_update = True

        if self.photo and not self.thumbnail or thumb_update:
            from PIL import Image

            THUMB_SIZE = (150, 150)

            #self.thumbnail = self.photo

            image = Image.open(self.photo.path)

            if image.mode not in ('L', 'RGB'):
                image = image.convert('RGB')

            image.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
            (head, tail) = os.path.split(self.photo.path)
            (a, b) = os.path.split(self.photo.name)

            if not os.path.isdir(head + '/thumbs'):
                os.mkdir(head + '/thumbs')

            image.save(head + '\\thumbs\\' + tail)

            self.thumbnail = a + '/thumbs/' + b

        super(UserProfile, self).save()

First we set a thumb_update var to false. Then, if the thumbnail exists, we check to see if the timestamp on the original image is newer than the existing thumbnail. If we didn’t check for this, then when we changed the image, we wouldn’t know whether or not to create the thumbnail.

The rest of the code deals with using the Imaging Library to resize the image. It also creates a ‘thumbs’ directory if one doesn’t already exist, saves the image, and updates the ‘thumbnail’ field in the model.

Conclusion

I hope this has been helpful. Like I said, I haven’t been using Django or Python for more than a few weeks, so if anyone has any suggestions for improving this code, then please comment.

Enjoy!

django , , , ,

  1. November 20th, 2008 at 22:08 | #1

    Hi, i’m sure you’ve heard of sorl.thumbnail which brings the thumbnail functionality to your templates?

  2. tom
    November 20th, 2008 at 22:12 | #2

    Good article. It seem’s that creating thumbnails is a common task for many sites and this is certainly a solid example.

    I recommend people read up on the python image library (PIL), there is a good set of documentation on their site.

  3. November 20th, 2008 at 22:20 | #3

    @Israel
    I hadn’t heard of it until now, thanks, I’ll check it out.

  4. Jökull
    November 20th, 2008 at 22:33 | #4

    def save(self, force_insert=False, force_update=False):
    Should probably be
    def save(self, *args, **kwargs):

  5. Ahmad Al-Ibrahim
    November 22nd, 2008 at 01:16 | #5

    In linux you can only create 32K directories in a directory, so this code will only allow you to create 32K profiles, you need a way to overcome the OS file system limitation, by putting each group of users in a directory.

  6. ger
    December 19th, 2008 at 12:32 | #6

    #Ahmad Al-Ibrahim
    False, this limitation is only for a very very old linux, you can now create a bilions of directories in a directory.

  7. ger
    December 23rd, 2008 at 14:04 | #7

    @ger

    This limitation is for ext3, this limitation will override with ext4 (ready in the next kernel version: 2.6.28)
    Sorry, my fault.

  8. sean
    February 28th, 2009 at 18:38 | #8

    Looked through your code and noticed a gaping error.

    image = Image.open(self.photo.path)

    Here self.photo.path gives you the path where the photo will be saved, but of course since you don’t call the base method until the end, no photo exists there yet and PIL cannot open it!

  9. February 28th, 2009 at 19:28 | #9

    @sean
    I think you may need to re-read the post my friend!

    Take a look at the model. There are 2 images fields, one for the main image, the other for the thumbnail. The main image is already saved using upload_to.

    The code you refer to (image = Image.open(self.photo.path)) works because the image has already been saved, and upload_to takes care of creating directories if none exist.

    Later in the code, the resized image is saved to a thumb directory, which is created if it doesn’t exist.

  10. sean
    February 28th, 2009 at 20:47 | #10

    Thanks for the quick reply. Sorry I still don’t follow where it is saved - upload_to is just a variable with the path (relative to MEDIA_ROOT) where it will be saved. I’ve looked through the code and django source and can’t spot it, would you mind telling me which line of code leads to the file being saved?

  11. February 28th, 2009 at 22:09 | #11

    @sean
    The value for ‘photo’ in the model won’t actually be committed to the database until after the ’save’ method has run.

    However, the value of the current instance is available (self.photo), and the image has already been uploaded and saved prior to the save method running.

    The model field itself takes care of the upload and storage automatically, which is how the image can be opened prior to the data being committed.

    I just tried it on my system just to check. I deleted the appropriate folders in MEDIA_ROOT before trying it, and it works as described.

    Hope that makes sense.

    Stuart

  12. sean
    March 1st, 2009 at 14:10 | #12

    Hi Stuart,

    Thanks for the reply. I made a quick django project to test this and got the error I expected when the image is loaded from the path:
    [Errno 2] No such file or directory: u’/home/sean/dj/test/media/dsc202.jpg’

    Changing the code to “image = Image.open(self.photo)” - note the change from self.photo.path to self.photo, works however. This works because the ImageField acts as an in-memory file like object once binded with the form.

    Since the code is obviously working for you I’m confused. When you say the field has already been saved prior to save method been called, do you mean that the photo field is populated and the image has been saved (i.e. you already have UserProfile instance with just the thumbnail left to set) before the form submission? The only other thing I can think of is that you were uploading the image from MEDIA_ROOT when you tested this? Most likely is that I’m missing something obvious!

  13. sean
    March 1st, 2009 at 14:14 | #13

    Just had a thought…maybe an older version of Django used to save the photo to MEDIA_ROOT when the model is instantiated prior to it’s save method been called? Which version of Django are you using?

  14. sean
    March 1st, 2009 at 14:39 | #14

    Okay - turns out we were both right! Reverted back to revision 9000 of Django and your code does work with the earlier version. So in earlier versions it saves the image to upload_to path when a model is instantiated prior to save, but in some later version this only occurs in save(). You might want to change the code to self.photo and deal directly with the in-memory object anyway, even if your using an older version, otherwise you have two copies of the same image in memory whilst creating the thumb.

  15. sean
    March 1st, 2009 at 14:44 | #15

    Hmm, don’t do that if you use a later version of Django though, otherwise you’ll end up with two copies of the thumb - so later versions you’ll have to do something more complicated, like subclass ImageField if you wanted to avoid two copies in memory. Sorry for all the messages - feel free to delete them as necessary!

  16. March 2nd, 2009 at 19:34 | #16

    @sean
    Thanks for digging into this.

    I’m using the last stable release (1.0.2 I believe).

    To be honest, the behavior you’ve seen in the later revisions is the behavior I expected when I wrote this, but in my version it saved the file prior to the save method, so I wrote it this way. The new way, and probably a fix, is better as you can make changes to data prior to actual file save.

    I’ll have a tinker and update the code.

    Thanks again.

    Stuart

  17. JT
    April 19th, 2009 at 20:12 | #17

    @sean

    Thank you for solving the mystery Sean, that helped a lot.

  18. May 22nd, 2009 at 17:08 | #18

    Does this mean that only one directory ‘profile_pics’ can be associated to a user?

    What if we want something like
    /${username}/pics/${filename}
    /${username}/mp3/${filename}
    ?

  19. May 22nd, 2009 at 18:45 | #19

    @Stu
    The directory isn’t associated with the model or user. It’s associated with the field. If you want different directories, just have extra fields. You will need to create a different function to store that particular file in the correct place.

  20. June 18th, 2009 at 00:36 | #20

    great post but you didn t check the type of the file (image ) .it could be a malicious code ( file with a rm *) , what do you think , would love an answer :)

  1. January 27th, 2009 at 16:41 | #1