Our next project will be an app similar to flickr. We’ll implement tags, ratings, albums, sharing, searching, filtering and sorting.
As with previous tutorials, we’ll start by defining a model (in photo/models.py):
from django.db import models from django.contrib.auth.models import User from django.contrib import admin class Album(models.Model): title = models.CharField(max_length=60) public = models.BooleanField(default=False) def __unicode__(self): return self.title class Tag(models.Model): tag = models.CharField(max_length=50) def __unicode__(self): return self.tag class Image(models.Model): title = models.CharField(max_length=60, blank=True, null=True) image = models.FileField(upload_to="images/") tags = models.ManyToManyField(Tag, blank=True) albums = models.ManyToManyField(Album, blank=True) created = models.DateTimeField(auto_now_add=True) rating = models.IntegerField(default=50) width = models.IntegerField(blank=True, null=True) height = models.IntegerField(blank=True, null=True) user = models.ForeignKey(User, null=True, blank=True) def __unicode__(self): return self.image.name class AlbumAdmin(admin.ModelAdmin): search_fields = ["title"] list_display = ["title"] class TagAdmin(admin.ModelAdmin): list_display = ["tag"] class ImageAdmin(admin.ModelAdmin): search_fields = ["title"] list_display = ["__unicode__", "title", "user", "rating", "created"] list_filter = ["tags", "albums"] admin.site.register(Album, AlbumAdmin) admin.site.register(Tag, TagAdmin) admin.site.register(Image, ImageAdmin)
... and running: manage.py syncdb; manage.py runserver
We also need to create a location for uploaded images and set up our settings.py to point to it:
MEDIA_ROOT = '/home/username/dbe/media/' MEDIA_URL = 'http://127.0.0.1:8000/media/'
At this point, you can go ahead and add a few images in the Admin so that you have something to play with.
I’m not yet sure if we’ll even need the Admin by the end of this tutorial, but adding a few useful features is just too easy (this will also be good as an illustration — it may be that what we’ll do with the Admin alone will be enough for your needs):
from string import join import os from PIL import Image as PImage from settings import MEDIA_ROOT class Image(models.Model): # ... def save(self, *args, **kwargs): """Save image dimensions.""" super(Image, self).save(*args, **kwargs) im = PImage.open(os.path.join(MEDIA_ROOT, self.image.name)) self.width, self.height = im.size super(Image, self).save(*args, ** kwargs) def size(self): """Image size.""" return "%s x %s" % (self.width, self.height) def __unicode__(self): return self.image.name def tags_(self): lst = [x for x in self.tags.values_list()] return str(join(lst, ', ')) def albums_(self): lst = [x for x in self.albums.values_list()] return str(join(lst, ', ')) def thumbnail(self): return """<a href="/media/%s"><img border="0" alt="" src="/media/%s" height="40" /></a>""" % ( (self.image.name, self.image.name)) thumbnail.allow_tags = True class ImageAdmin(admin.ModelAdmin): # search_fields = ["title"] list_display = ["__unicode__", "title", "user", "rating", "size", "tags_", "albums_", "thumbnail", "created"] list_filter = ["tags", "albums", "user"] def save_model(self, request, obj, form, change): obj.user = request.user obj.save()
If you want to save image dimensions, you’ll need to install PIL module (python-imaging module in Ubuntu/Debian). In save() we’re overriding the default save method to process the image and save dimensions. In tags_() and albums_() we’re getting a list of values from a Many-to-Many object, which has a method values_list() that returns tuples with primary keys and values — we are only interested in values in this case.
You can click the thumbnail to see full-sized image. We could easily add a view to resize the image and show a back button or open an image in a popup window.
Finally, we’re overriding the save_model() method to assign current user as the owner of the image. And that’s what we have so far:
We can’t do much more with the admin, except for one small thing: we’ll add a list of links to images in the Album listing (you’ll also have to add the images field to AlbumAdmin):
class Album(models.Model): # ... def images(self): lst = [x.image.name for x in self.image_set.all()] lst = ["<a href='/media/%s'>%s</a>" % (x, x.split('/')[-1]) for x in lst] return join(lst, ', ') images.allow_tags = True
The image_set object is automatically created as a part of Many-to-Many relationship between two models.
There is a pretty significant performance issue with the way thumbnails are handled right now. If you’re loading a hundred images in the Admin view, the page will load full-sized images and only then resize them for displaying. That’s not good! Thinking ahead, we know we’ll need thumbnails for other views, as well, and we also know we’ll probably want to have more than one size of thumbnails.
For the sake of simplicity, let’s say we want to store two different thumbnails for each image and to generate them when an image is added. PIL to the rescue, again!
from django.core.files import File from os.path import join as pjoin from tempfile import * class Image(models.Model): # ... thumbnail2 = models.ImageField(upload_to="images/", blank=True, null=True) def save(self, *args, **kwargs): """Save image dimensions.""" super(Image, self).save(*args, **kwargs) im = PImage.open(pjoin(MEDIA_ROOT, self.image.name)) self.width, self.height = im.size # large thumbnail fn, ext = os.path.splitext(self.image.name) im.thumbnail((128,128), PImage.ANTIALIAS) thumb_fn = fn + "-thumb2" + ext tf2 = NamedTemporaryFile() im.save(tf2.name, "JPEG") self.thumbnail2.save(thumb_fn, File(open(tf2.name)), save=False) tf2.close() # small thumbnail im.thumbnail((40,40), PImage.ANTIALIAS) thumb_fn = fn + "-thumb" + ext tf = NamedTemporaryFile() im.save(tf.name, "JPEG") self.thumbnail.save(thumb_fn, File(open(tf.name)), save=False) tf.close() super(Image, self).save(*args, ** kwargs)
There are a few fine points here I need to address: firstly, we have to save to a temporary filename using Python’s tempfile module — the reason is that if we save it to the proper location (which we already know), saving the whole Model will result in a duplicate file because ImageField will refuse to overwrite the old file. Secondly, we have to set save=False when saving thumbnails, otherwise we’ll get an infinite recursive loop (not exactly sure why) — instead, we’re letting both thumbnails be saved with the Model.
You also have to update the thumbnail image display in the listing, but that should be straightforward enough (if not, no worry — full sources will be linked at the end as always).
And that’s that for the Admin.