Making a Flickr Killer With TurboGears – Part 2: A Flickr Clone in 37 Minutes Flat

This is the second installment of the lecture I gave at the Israeli Pythoneers Meeting. In case that you missed it, it is recommended that you read the first part of it.

At this point, I closed OpenOffice Impress and said that I would demonstrate how quickly you could get a functional Web application up and running with TurboGears.

Setting Things Up

I’ve quick-started a new project called TurboGallery…

$ tg-admin quickstart
Enter project name: TurboGallery
Enter package name [turbogallery]: <Enter>
Do you need Identity (usernames/passwords) in this project? [no] <Enter>

…and gone inside its directory to run it:

$ cd TurboGallery
$ python start-turbogallery.py


I started Firefox and browsed to http://localhost:8080. “There is already something to see on the site,” I said. The TurboGears’ default welcome page was showing up.

“The next thing I always do is to delete the entire body of the welcome template.” So, I opened welcome.kid in my favorite text editor and did just that. I then refreshed the page in Firefox. All the contents were gone except for the TurboGears’ header and the footer, which said, “TurboGears under the hood.” Someone asked if there was any way to remove those two remaining items. “Absolutely,” I said. It was time to mention master.kid. I opened the file and explained that it is used to render the layout that is common to all pages. As such, you do not have to duplicate it in all your templates. I next removed the TurboGears’ footer but left the header in its place.

I then opened model.py and added a class that would represent a single photo:

from datetime import datetime

class Photo(SQLObject):
    title = UnicodeCol()
    date = DateTimeCol(default=datetime.now)
    image = BLOBCol()
    thumbnail = BLOBCol()

A photo will have a title and a date field, which indicates the time it was uploaded. The field’s default argument makes the date of a newly created photo object be the current date. That way, you don’t need to specify this information every time you create a new photo. In the above definition, we don’t give the date field the current time. Rather, we give it a function that returns the current time. Whenever a photo is created, this function will be called to determine the value for this field. We store the images inside the database together with a thumbnail. BLOB stands for binary large object. The thumbnail part was not actually used in the tutorial.

Next, I created a database with

$ tg-admin sql create

I was then asked how TurboGears knows where and how to access the database. I replied that the default TurboGears’ setting uses SQLite, which creates a database-in-a-file. It is very convenient to have your database readily available as a normal file while your project is in its development and testing phases.

To save time, I have prepared in advance a database that contains four photos. I replaced the database file that was just created with my copy. The next thing to do will be to display the photos on the main page. I modified the index() method into…

@expose(template="turbogallery.templates.welcome")
def index(self):
    photos = Photo.select()
    return dict(photos=photos)

…and uncommented the “from model import *” line. The index() method is called whenever the front page of the Web site is viewed. I explained the expose() decorator, which makes a method available to the outside world. Without it, the method could not be accessed from the Web. The template argument specifies which template should be used to render the page.

The index() method gets a list of all photos from the database and returns it inside a dictionary. TurboGears passes this dictionary to the template. I then moved back to welcome.kid and added the following to the body:

<ul>
     <li py:for="photo in photos">${photo.title}</li>
</ul>
<hr/>

I refreshed the page, and the list of photos was displayed. It is educational to view the source of the resulting page, but the people who were listening to my lecture wanted to see the photos straightaway.

Seeing Some Photos

To see the photos, we’ll have to add an IMG element to the list. The IMG element will get the photo file from another URL. I’ve changed the welcome.kid list into…

<ul class="photo_list">
    <li py:for="photo in photos">
        <img src="/images/${photo.id}"
              id="image${photo.id}" width="160"/>
<br/>
        <a href="/photo_info/${photo.id}">${photo.title}</a>
    </li>
</ul>
<hr/>

…and then added the following photo_info() method to the controller:

@expose()
def images(self, photo_id):
    return "Hello "+photo_id

I next browsed to /images/world, and the browser displayed “Hello world.” The goal of this step was to demonstrate to the audience how TurboGears (CherryPy) maps URLs to methods. It was also intended to illustrate how easily a part of the URL can be transformed into positional arguments.
The right thing to put in this function would be a query that would obtain the photo from the database and return the associated jpeg file. Here is the code:

    @expose(content_type=‘image/jpeg’)
    def images(self, photo_id):
        photo = Photo.get(photo_id)
        return photo.image

I refreshed the page, and the photos I’d prepared were now showing. (I also changed the CSS file when no one was looking to have this layout.)
Evangalizing Firefox in Thailand
In the second picture, you can see me evangelizing for the use of Firefox in Thailand.
Since this is just an example application to make things simple, the image is sent in full size and is resized by the browser.
Clicking on the caption below the images sends us to a page we haven’t yet created. This page will display the photo in full size together with some statistics on it. I saved welcome.kid as photo_info.kid and changed the body contents to:

    <h1>Photo Details for "${photo.title}"

    <img id="image${photo.id}" src="/images/${photo.id}"/>

    <ul>
        <li>Image size: ${len(photo.image)} bytes</li>
        <li>Uploaded at: ${photo.date}</li>
    </ul>

The corresponding method in controllers.py() would be:

    @expose(template="turbogallery.templates.photo_info")
    def photo_info(self, photo_id):
        photo = Photo.get(photo_id)
        return dict(photo=photo)

Share Your Photos

An online gallery application is quite useless if its users can’t upload photos from their own computers. As such, we need to create an image upload form. In TurboGears, creating forms and validating user input is very easy. I’ve added a form definition to the top of controllers.py. The first part defines the fields:

from turbogears.widgets import *
from turbogears import *

class AddPhotoFields(WidgetsList):
    title = TextField(label=‘Title:’)
    image = FileField(label=‘Photo:’)

The form will have a text input field for the title of the photo as well as a field that enables the selection of an image file from the user’s computer. The second part of the definition is the validation schema for this form:

class AddPhotoSchema(validators.Schema):
    title = validators.String(not_empty=True, max=16)

“The touchiest issue with any Web application is its users,” I said. “Without them, there is no need to worry about bugs or invalid input.” The validation makes sure the input your Web application receives is a sound one. In many cases, further validation is needed. The above validator makes sure the title is not empty and is no longer than 16 letters. One audience member asked if the validation can be specified inside the fields definition. Yes, it is possible to specify a validator as a keyword argument to a field definition, but making the validation schema exterior to the fields definition renders it possible to define a more complex schema that involves field dependency or logical operators. A common instance where such a schema is useful is in the validation of a registration form, where you have to check whether or not the entered password field text and the “reenter password field” match.

The last part of the form definition ties the previous two parts together, with text for the submit button and a URL that will handle the form data:

add_photo_form = TableForm(fields=AddPhotoFields(),
        validator=AddPhotoSchema(),
        submit_text=‘Upload!’,
        action=‘/upload’,
        )

I’ve created a template named add.kid that will display just the form:

   <h1>Add New Photo</h1>
    ${form()}

I’ve also added a controller method to make this page accessible…

    @expose(template="turbogallery.templates.add")
    def add(self):
        return dict(form=add_photo_form)

…and I’ve linked to it from welcome.kid.

Here is a screenshot of this page:
Add photo form
As you can see, TurboGears gives us a nice form without us having to type in any HTML at all. Clicking the Upload! Button posts the data to the /upload URL. To test the form, I’ve added the corresponding method to the controller:

    @expose()
    @validate(form=add_photo_form)
    @error_handler(add)
    def upload(self, title, image):
        return "hi"

The decorators that are attached to this method make TurboGears validate its input using the add_photo_form. If a validation error occurs, the response is handled by the add() method, which just displays the form again along with the validation errors. So if, for example, we type in a too-long title, we will get the following:
TurboGears form validation errors
I would now really like to make the method save the image in a new photo object. The following code will do:

    @expose()
    @validate(form=add_photo_form)
    @error_handler(add)
    def upload(self, title, image):
        image = image.file.read()
        Photo(title=title, image=image, thumbnail=None)
        flash("Image successfully added!")
        raise redirect(‘/’)

Yes, handling a file upload in TurboGears is just a matter of reading from a file-like object. It is that simple. The next line creates a photo object with the title and the image data. The flash() method makes the given message appear on the next page, and we redirect to the main page. I filled out the form to upload an image that was downloaded from my camera. Here’s what I got:
Rotate photos in TurboGears
Damn! I hate it when my camera decides to rotate an image that has already been uploaded. So, let’s add an AJAX image-rotation tool. A click on the rotate link will rotate the image in place without requiring the entire page to be reloaded. We first add a method to rotate the image to our controller:

    @expose(‘json’)
    def rotate(self, photo_id):
        import Image
        from cStringIO import StringIO
        photo = Photo.get(photo_id)
        image = Image.open(StringIO(photo.image))
        rotated = image.rotate(90)
        photo.image = rotated.tostring(‘jpeg’, ‘RGB’)
        return dict(photo_id=photo_id, size=rotated.size)

The method uses PIL – Python Imaging Library. It loads the image from the database, rotates it, and then stores it again in the database. The method returns a dictionary containing the photo_id and the new photo dimensions. It is set to return the data in JSON format, which makes it extremely easy to use in Javascript. I directly entered http://localhost:8080/rotate/2 to show what the JSON object looks like. I then refreshed the main page to verify that the photo had been rotated. Next, I rotated it three more times until it was straight again.

I then went back to welcome.kid and added a rotate link for each photo:

<ul class="photo_list">
    <li py:for="photo in photos">
        <img src="/images/${photo.id}"
              id="image${photo.id}" width="160"/>
<br/>
        <a href="/photo_info/${photo.id}">${photo.title}</a>
        <a href="#" onclick="rotate_photo(${photo.id}); return false;">(rotate)</a>
    </li>
</ul>

Clicking on the “rotate” text located next to a photo will call a Javascript function that receives the corresponding photo ID. I’ve added the implementation of rotate_photo() to the top of the welcome.kid template. It uses MochiKit, which must be enabled in config/app.cfg (it is explained how to do so in that file).

<script type="text/javascript">
    function rotate_photo(photo_id) {
        d = loadJSONDoc(‘/rotate/’+photo_id);
        d.addCallback(update);
    }

    function update(r) {
       $(‘image’+r.photo_id).src=‘/images/’+(r.photo_id)+‘?random=’+Math.random();
    }
</script>

The function makes an asynchronous call to the rotate URL, providing it with the photo ID. Once the response arrives, update() is called and is provided with the JSON object we returned from the controller’s rotate() method. In Javascript, you can use dot notation to access the keys in that dictionary. When update() is called, the image has already been rotated at the server. Therefore it is the right time for the browser to fetch it again. To perform this re-fetching, update() sets the image src attribute to the image URL, but in order to prevent the browser from displaying the cached old image, we add a random argument to the URL. You can see a related post describing how to prevent browsers from caching. To make the image() controller method accept this argument and ignore it, its definition becomes:

def images(self, photo_id, random=None):

I then refreshed the main page and rotated the photos a few times to demonstrate how slick this functionality is (especially when you’re working on the server). You can see it in the video below:

Next, I uploaded another image I had prepared in advance, this one called questions.jpg, using the upload form. Here it is:

It says “Questions?” in Hebrew, signifying that the lecture was over and it was time to ask questions.

Download the full source code of TurboGallery.

This entry was posted in python, turbogears. Bookmark the permalink.

81 Responses to Making a Flickr Killer With TurboGears – Part 2: A Flickr Clone in 37 Minutes Flat

  1. This is really attention-grabbing, You’re an overly professional blogger.

    I’ve joined your rss feed and look ahead to in quest
    of more of your great post. Additionally, I have shared your web
    site in my social networks

  2. That is a great tip particularly to those new to the blogosphere.
    Brief but very accurate information… Thanks for sharing this one.

    A must read article!

  3. Hi there, You have done a fantastic job. I’ll certainly digg it and personally
    recommend to my friends. I’m confident they’ll
    be benefited from this site.

  4. Great blog! Do you have any suggestions for aspiring writers?

    I’m hoping to start my own site soon but I’m a little
    lost on everything. Would you propose starting
    with a free platform like WordPress or go for a paid option? There
    are so many options out there that I’m completely confused ..

    Any tips? Cheers!

  5. My partner and I stumbled over here by a different web address
    and thought I might as well check things out. I like what I see so i
    am just following you. Look forward to looking
    at your web page for a second time.

  6. Thanks for finally writing about > Making a Flickr Killer With TurboGears – Part 2: A Flickr Clone
    in 37 Minutes Flat | Nadav Samet’s Blog < Liked it!

  7. Write more, thats all I have to say. Literally, it
    seems as though you relied on the video to make your point.
    You obviously know what youre talking about, why waste your intelligence on just posting videos to your weblog when you could be giving us something informative
    to read?

  8. Simply wish to say your article is as astonishing. The clearness
    in your post is simply nice and i can assume you are an expert on this subject.
    Fine with your permission allow me to grab your RSS feed to keep updated with forthcoming post.
    Thanks a million and please keep up the gratifying work.

  9. I blog quite often and I really appreciate your content.
    This article has truly peaked my interest. I will bookmark
    your blog and keep checking for new details about once per week.
    I opted in for your Feed as well.

  10. Hello to all, how is all, I think every one is getting more from this
    site, and your views are pleasant designed for new users.

  11. whoah this blog is magnificent i like reading your posts.
    Stay up the great work! You recognize, a lot of persons
    are looking around for this information, you could aid them greatly.

  12. I’m truly enjoying the design and layout of your website. It’s a very easy on the eyes which makes it much more enjoyable for me to come here and visit more often. Did you
    hire out a designer to create your theme? Excellent work!

  13. My family every time say that I am wasting my time here at net, except
    I know I am getting know-how all the time by reading such pleasant articles or reviews.

  14. Asking questions are actually pleasant thing if you are not understanding something entirely,
    but this article gives nice understanding even.

  15. I’m amazed, I must say. Seldom do I come across a blog that’s
    both educative and interesting, and let me tell you, you have hit the nail on the head.
    The problem is something that too few people are speaking intelligently about.

    I am very happy that I found this during my hunt for
    something regarding this.

  16. I was curious if you ever thought of changing the layout of your site?
    Its very well written; I love what youve got to say.
    But maybe you could a little more in the way of content so people could connect with it better.
    Youve got an awful lot of text for only having 1 or
    two images. Maybe you could space it out better?

  17. Do you mind if I quote a couple of your articles as long as I provide
    credit and sources back to your weblog? My website is in the very same
    area of interest as yours and my users would certainly benefit
    from some of the information you provide here. Please let me know if this
    ok with you. Thank you!

  18. This design is spectacular! You most certainly know how to keep a reader entertained.
    Between your wit and your videos, I was almost moved to start
    my own blog (well, almost…HaHa!) Excellent job.
    I really loved what you had to say, and more than that, how you presented it.

    Too cool!

  19. Great goods from you, man. I’ve understand your stuff previous to and you are just too magnificent.

    I really like what you have acquired here, certainly like what you’re saying
    and the way in which you say it. You make it entertaining and you still
    care for to keep it wise. I can not wait to read much more from you.
    This is actually a terrific web site.

  20. Our secrets get shield security programs, which will make hacks and them the safest tips.

  21. Hmm is anyone else encountering problems with the pictures on this blog loading?
    I’m trying to figure out if its a problem on my end or if
    it’s the blog. Any responses would be greatly appreciated.

  22. Hi fantastic website! Does running a blog similar to this require a lot of work?
    I have no understanding of computer programming but I had been hoping to start my own blog soon.
    Anyhow, should you have any suggestions or techniques for new
    blog owners please share. I understand this is off subject however I just had to ask.
    Thank you!

  23. Today, I went to the beach with my kids. I found a sea shell and gave it to my
    4 year old daughter and said “You can hear the ocean if you put this to your ear.” She placed
    the shell to her ear and screamed. There was a hermit crab inside and it pinched her
    ear. She never wants to go back! LoL I know this is completely off topic but I had to tell someone!

  24. Undeniably consider that which you stated.
    Your favourite reason seemed to be at the internet the easiest thing to be aware of.
    I say to you, I certainly get annoyed while other people
    consider issues that they just do not recognize about.
    You managed to hit the nail upon the highest and also defined
    out the whole thing with no need side effect , other people
    could take a signal. Will probably be again to get more.
    Thank you

  25. Hey there I am so glad I found your webpage, I really found
    you by accident, while I was researching on Google for
    something else, Anyhow I am here now and
    would just like to say cheers for a tremendous post and a all round exciting blog (I also love the theme/design), I don’t have time to look
    over it all at the minute but I have bookmarked it and also added in your RSS feeds, so when I have time I will be back to read much more, Please do keep up the great work.

  26. Aw, this was an exceptionally nice post. Spending some time and actual effort to make a superb
    article… but what can I say… I put things off a lot and never seem to get nearly anything done.

  27. magnificent publish, very informative. I’m wondering why the other specialists of this sector don’t realize this.
    You must proceed your writing. I’m sure, you have a huge readers’ base already!

  28. Fantastic site. A lot of useful info here. I’m sending it to some friends ans also sharing in delicious.
    And certainly, thanks on your sweat!

  29. It’s in reality a great and helpful piece of info.

    I am glad that you shared this helpful information with us.

    Please stay us informed like this. Thanks for sharing.

  30. This design is wicked! You definitely know how to keep a reader entertained.
    Between your wit and your videos, I was almost
    moved to start my own blog (well, almost…HaHa!) Excellent job.
    I really loved what you had to say, and more than that, how you presented it.

    Too cool!

  31. I do not know if it’s just me or if everyone else
    encountering problems with your site. It looks like some of the
    written text on your posts are running off the screen. Can someone else please provide feedback and let me know if this is happening to them
    as well? This might be a problem with my internet browser because I’ve had this happen before.
    Kudos

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>