Writing a Gallery App in Django, Part I

One thing that I see the need for on almost every site is a place to put images. Whether it's a band website, a personal homepage, or a school newspaper, there will be a need for a photo gallery. The easy way to do that is to use an open source package available already like Gallery2. But that's written in PHP and it won't integrate easily with the rest of your site--especially if you use Django as the framework for the rest of your site.

The solution: write a gallery application for use in your website. At first it may seem like a daunting task to create, but as I've found out, it can be quite easy. My implementation is not completely up and running yet, but that's due to design issues with the rest of the site, not the photo gallery app itself. With no further adieu, let's dive in and see how this can work.

class Album(models.Model):
    name = models.CharField(maxlength=128)
    slug = models.SlugField(prepopulate_from=("name",))
    summary = models.TextField()
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

As you can see, this is the Album object which contains information about a set of associated photos. But wait, we don't have photos created yet! Let's do that now.

class Photo(models.Model):
    title = models.CharField(maxlength=256)
    summary = models.TextField(blank=True, null=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    image = models.ImageField(upload_to='photos/%Y/%m')
    album = models.ForeignKey(Album)
    is_cover_photo = models.BooleanField()

Ok so now we have albums which have photos, and photos have a lot of information like a file which contains an image, a title, and a summary.

Now that we have these objects, we have some choices to make. The images need to be resized into medium and small images for display on the list and detail pages, respectively. This can be done several ways:

  1. Use the HTML width and height attributes to resize the images.
  2. Create a Django view with width and height parameters to both resize and serve the images. (Lazy computation)
  3. Resize the images on upload and store them to disk. (Upfront computation)

The first way is not optimal for two reasons. Firstly each picture must be downloaded in it's entirety. This is inefficient and could upset people with less bandwidth or bandwidth limits. Secondly, when most browsers resize images, they do so using poor quality filters, resulting in a low quality representation of an image.

The second way is the most flexible, since the height and the width can be changed in a template or in a view and the resized images will change accordingly. However, there is more on-the-fly computation with this way, possibly increasing page load times. Also, Django is not designed to be used to serve binary files directly, so there could be unforseen consequences with handling a large number of photos this way.

The third way is less flexible, but it has one key advantage: it's fast. Since the computation is done on upload, Apache or any other http media server can be used instead, completely removing the need to use the Django framework at all. Let's implement it this way.

First, we'll need to overload the save function of the Photo model:

def save(self):
    if self.is_cover_photo:
        other_cover_photo = Photo.objects.filter(album=self.album).filter(is_cover_photo = True)
        for photo in other_cover_photo:
            photo.is_cover_photo = False
            photo.save()
    filename = self.get_image_filename()
    if not filename == '':
        img = Image.open(filename)
        img.thumbnail((512,512), Image.ANTIALIAS)
        img.save(self.get_medium_filename())
        img.thumbnail((150,150), Image.ANTIALIAS)
        img.save(self.get_small_filename())
    super(Photo, self).save()

Before I talk about the thumbnailing aspect of this function, I'd like to briefly explain what's going on with the cover_photo aspect of Photo objects. Each Album has a cover photo, so if the Album needs to be represented, it can be represented by one special photo. This is just part of the way that I have designed my object, and can easily be removed. However, there is a small bit of obligatory boilerplate code in this save function which sets any other is_cover_photo attributes to False if necessary. (There can only be one cover photo per album, after all). I'll come back to dealing with cover photos later.

First off, the if not filename == '' statement is needed because save is sometimes called with no image data, and that can and will throw exceptions if PIL is used on a None object. Then, it resizes a medium-sized (512px by 512px) image and saves it to a location provided by get_medium_filename. I leave it to you to define your own get_medium_filename and get_small_filename with your own naming convention. I followed Flickr's example of an underscore followed by an argument (image001.jpg becomes image001_m.jpg for medium and image001_s.jpg for small). Finally, this method must call the it's parent save method so that all of the other attributes are saved correctly.

Now that we've overloaded the save functionality, we are going to have a problem with deletion. Django will automatically delete the original image, but the resized thumbnails will be left on the disk forever. This is not a huge problem, but we don't want that to happen anyways. So let's take care of that by also overloading the delete function of Photo's model:

def delete(self):
    filename = self.get_image_filename()
    try:
        os.remove(self.get_medium_filename())
        os.remove(self.get_small_filename())
    except:
        pass
    super(Photo, self).delete()

Simply put, it deletes the thumbnail files and then calls it's parent's delete, which will in turn delete it's original file.

Aside from creating some optional helper functions like get_small_image_url and/or get_medium_image_url, there's not much more to be done with the Photo model. What can be done still, however, is in Album. We now have zero or one cover photos for each Album, but it's going to be tricky to query for this each time, so let's create a function in Album to help us retrieve the associated cover_photo Photo object:

def get_cover_photo(self):
    if self.photo_set.filter(is_cover_photo=True).count() > 0:
        return self.photo_set.filter(is_cover_photo=True)[0]
    elif self.photo_set.all().count() > 0:
        return self.photo_set.all()[0]
    else:
        return None

That is, if the Album has a photo with is_cover_photo == True, then grab it, otherwise grab the first image. If there are no images in the album, return None. That's it for the models. Easy, huh? Just run manage.py syncdb, and let Django do the heavy lifting for you.

That's all for part one of this series on writing a gallery application with Django. Next up: writing the views, urlconfs, and putting it all together. Templating will be left up to you, since there are so many ways to display this information, but some examples will be given to point you in the right direction.

Note to purists: I know that some of the functionality that is being implemented as helper functions in the models would be better implemented as custom template tags, but I find it easier to take a less philosophical stance on the "right" way to do things and sometimes do what's more practical. In this case, writing model functions is a much easier solution than creating completely new template tags. In either case, moving to a new site will require a rewrite, so I'm not even convinced that it hurts reusability.

Free Icons!

If you have noticed, there are now icons next to each comments link. These are provided, free of cost, by famfamfam.com. If some 16x16 icons are needed, and you are not graphically or creatively inclined, this is your one-stop-shop.

In other news, a Speed graph has been added to the running page as well as a distance graph. In doing that, I ran into a problem which was hard to debug, since it only happens on the live site. In Django, trying to serialize a Numeric data type with simplejson will not work, so make sure to cast it to a python-native data type first.

Problem code:

i = 0
for run in runs:
    distance = run.distance()
    speed_data.append((i,distance/run.time))
    distance_data.append((i,distance))
    i = i + 1

Solution:

i = 0
for run in runs:
    distance = run.distance()
    speed_data.append((i,distance/float(run.time)))
    distance_data.append((i,distance))
    i = i + 1

I also noticed a problem with my Pygments function, due to the regular expression, where it only allowed me to post one code snippet. As you can see in this post, that has been fixed (although it was another particularly nasty bug to track down, and required a complete refactoring of my code in the end).

MythTV Box Down

Yesterday the MythTV computer that my roommate and I has died. That is to say that the hard drives which contained all of the shows stopped functioning. I think what we're going to do is wipe the hard drives and put only a small main hard drive in the MythTV box, and put the rest into a FreeNAS NAS server.

It'll have 2x200GB, 1x160GB, and 1x500GB hard drives by the time we're done, making for a total of 1060GB (just over a terabyte!) of total hard drive space.

Then, we'll have some HDs used for our respective media files, and probably the 500gb drive mapped from the MythTV box as the media storage for all the TV shows. That way, if something happens to the MythTV box, we don't lose everything like we did this time. The only problem that I can see is if the network just isn't fast enough for us to store our files on there and access it from our computers simultaneously.

I'm also trying to decide what case will be best to put the new MythTV components in. Right now I'm leaning towards the Lian Li PC-V880B, but any other suggestions are welcome.

Memcached and OpenWebLoad

There has been a somewhat minor update to the site. Now almost all of the site is cached. That is, there is a memcached backend running on the server, and this website can take advantage of that.

To test out the site's load capabilities, I used an open source program called OpenWebLoad. It's quite impressive in its simplicity and utility. With it, I was able to test a load of over 50 simultaneous users to the website. Surprisingly, the site handled it with flying colors.

On a side note, I went to Wil Wheaton's site after a long stint of not visiting it, and it's still as good as it's ever been. There's an almost tangible difference between a writer's website and a blogger's website. I think that it boils down to the fact that using only words, Wil can make you feel the emotion that he wants you to feel.

New Site Live!

Hello all! I have finally put my new website up for the world to see! I'm still not much for designing the look and feel of websites, but after learning all about Django, I felt like I just had to update my website to use Django.

Under the hood, there is low-level caching being done on both the Digg RSS feed and the Yahoo Flickr feed. There are two RSS feeds available for the site: new blog posts, and new run information. To show the graph under runs, I've used some AJAX to get the running data dynamically and create the graph with javascript. Finally, I've created a custom Django template tag to allow me to post syntax-highlighted code using Pygments.

Example of pygmentized code (the one from this site, actually, that gets my latest digg articles):

def get_digg_rss():
    d = feedparser.parse('http://www.digg.com/rss/floguy/index2.xml')
    dugg_dicts = cache.get('dugg_dicts')
    if dugg_dicts == None:
        print "Had to re-fetch digg links"
        dugg_dicts = []
        for entry in d.entries:
            dugg_dicts.append({'title':entry.title, 'link':entry.link})
        cache.set('dugg_dicts', dugg_dicts, 60*60)
    return dugg_dicts

I hope that you all enjoy the website, and I also hope that you'll begin to frequent it, because I'll be keeping it up-to-date with the goings-on of my professional life.

Search

Badges

  • django badge
  • apache badge
  • GeoURL
  • XFN Friendly
  • Valid HTML 4.01 Transitional