Ruby on Rails, Part 2

LUG Programming Course, 17th March 2008
This week's lesson concludes the two part adventure building a web application using Ruby and the Ruby on Rails framework. Although we'll have a working application by the end of the lesson, good enough to play with at home, there will still be some work required to bring it to production environment standards.
The lesson went quite well, and although there was a lot to cover, we overran by only 15 minutes. We finished off the evening to celebrate the end of the course with a drink in a local bar.

Creating the Albums and Pictures

The next step will be to create another two classes that make up our data model, namely albums and pictures. But, if you remember, we have to do quite a lot of work modifying the migration, model, and views that were generated. Will we have to do all those modifications again? Well, the answer is - not entirely. I'm not too concerned about modifying the migration and model, after all there will only be four files, but we did do a lot of repetitive work on the views, and there'll be eight of them. Now since Rails is an open source project, and we've frozen it in our application, why not modify the templates that generate the code first? So we'll modify four files instead of eight – but we'll also be modifying less code. If we were going to create a dozen new classes, I'm sure you'd see the advantage. I won't show you the template code here (you'll be seeing enough code as it is), but you'll find the modified template files view_*.html.erb in the folder /vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates.
Now, in the console type:
ruby script/generate scaffold album caption:string description:text user:references
Then modify db/migrate/002_create_albums.rb:
class CreateAlbums < ActiveRecord::Migration
  def self.up
    create_table :albums do |t|
      t.string :caption, :limit => 40, :null => false
      t.text :description
      t.references :user, :null => false
      t.integer :pictures_count, :default => 0
      t.timestamps
    end
    add_index :albums, [:user_id], :name => :albums_user_index
  end

  def self.down
    remove_index :albums, :name => :albums_user_index
    drop_table :albums
  end
end
So albums will have a (required) caption, an optional, and possibly very lengthy description, and a reference to the user. The pictures_count will be explained in a moment, when we modify the user migration
Now type (all on one line):
ruby script/generate scaffold picture caption:string description:text user:references album:references content_type:string width:integer height:integer
Then modify db/migrate/003_create_pictures.rb:
class CreatePictures < ActiveRecord::Migration
  def self.up
    create_table :pictures do |t|
      t.string :caption, :limit => 40
      t.text :description
      t.references :user, :null => false
      t.references :album, :null => false
      t.string :content_type, :null => false
      t.integer :width, :null => false
      t.integer :height, :null => false
      t.timestamps
    end
    add_index :pictures, [:user_id], :name => :pictures_user_index
    add_index :pictures, [:album_id], :name => :pictures_album_index
  end

  def self.down
    remove_index :pictures, :name => :pictures_album_index
    remove_index :pictures, :name => :pictures_user_index
    drop_table :pictures
  end
end
Pictures will have an optional caption, an optional and possibly lengthy description, references to the user and album, and information about the uploaded image – the content_type, width and height.
Is that everything? Not quite. Our users are (hopefully) going to end up with many albums and many pictures. How many? Well, we could always run an SQL query on the two tables, but it will be more efficient to keep the count in the user table. In fact, this is one of Rails' specialities – using a simple convention, as you'd expect. We already did this in the albums migration, now you know why. So we'll modify the db/migrate/001_create_users.rb file too:
class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :name, :limit => 40, :null => false
      t.string :motto, :limit => 80
      t.string :salted_password, :limit => 40, :null => false
      t.string :salt, :limit => 40, :null => false
      t.integer :albums_count, :default => 0
      t.integer :pictures_count, :default => 0
      t.timestamps
    end
    add_index :users, [:name], :name => :users_name_index, :unique => true
  end

  def self.down
    remove_index :users, :name => :users_name_index
    drop_table :users
  end
end
However, we'll have to revert the database (and we'll loose the user data too – but there were only two users), and then update again. Type:
rake db:migrate VERSION=0
rake db:migrate
Which produces the results:
C:\Courses\LUGPC10\WebAlbum>rake db:migrate VERSION=0
(in C:/Courses/LUGPC10/WebAlbum)
== 1 CreateUsers: reverting ===================================================
-- remove_index(:users, {:name=>:users_name_index})
   -> 0.0620s
-- drop_table(:users)
   -> 0.0630s
== 1 CreateUsers: reverted (0.1250s) ==========================================

C:\Courses\LUGPC10\WebAlbum>rake db:migrate
(in C:/Courses/LUGPC10/WebAlbum)
== 1 CreateUsers: migrating ===================================================
-- create_table(:users)
   -> 0.1570s
-- add_index(:users, [:name], {:name=>:users_name_index, :unique=>true})
   -> 0.1250s
== 1 CreateUsers: migrated (0.2820s) ==========================================

== 2 CreateAlbums: migrating ==================================================
-- create_table(:albums)
   -> 0.0940s
-- add_index(:albums, [:user_id], {:name=>:albums_user_index, :unique=>false})
   -> 0.0780s
== 2 CreateAlbums: migrated (0.1720s) =========================================

== 3 CreatePictures: migrating ================================================
-- create_table(:pictures)
   -> 0.0780s
-- add_index(:pictures, [:user_id], {:name=>:pictures_user_index, :unique=>false})
   -> 0.0780s
-- add_index(:pictures, [:album_id], {:name=>:pictures_album_index, :unique=>false})
   -> 0.0780s
== 3 CreatePictures: migrated (0.2340s) =======================================
In the previous lesson, we started to get to know the Rails framework as we created the code for our users. We'll continue using this knowledge, and expand our understanding of Rails, as we first write the code for albums, and then for pictures.
The Album Controller
Users have albums, users and albums have pictures. The behaviour of our application changes dramatically depending on whether the visitor is logged in or not. When not logged in, we'll show all the albums and all the pictures. When logged in, we'll show only the albums and pictures belonging to the user. Additionally, we can then allow the user to create albums, and create pictures for an album.
We used the session object to keep track of the current user, and we'll do the same for the current album. Let's start with the helper methods we'll require. In app/controllers/application.rb add the following lines at the end of the class:
...
  def logout
    session[:user_id] = nil
    session[:album_id] = nil
    redirect_to '/users'
  end
...
  helper_method :find_album
  def find_album(id)
    Album.find(:first, :conditions => [ "id = ?", id ])
  end

  helper_method :current_album
  def current_album
    @current_album ||= (session[:album_id] ? find_album(session[:album_id]) : nil)
  end

  helper_method :current_album=
  def current_album=(id)
    if current_user
      session[:album_id] = id
    else
      session[:album_id] = nil
    end
  end

  helper_method :all_albums
  def all_albums
    if current_user
      return current_user.albums
    else
      return Album.find(:all, :order => "id DESC", :limit => 20 )
    end
  end

  helper_method :all_pictures
  def all_pictures
    if current_user
      return current_user.pictures
    else
      return Picture.find(:all, :order => "id DESC", :limit => 20 )
    end
  end

  helper_method :user_albums
  def user_albums
    if current_user
      return current_user.albums
    else
      return []
    end
  end

  helper_method :current_user_owns?
  def current_user_owns?(item)
    logged_user = current_user
    return true if logged_user && logged_user.id == item.user_id
    false
  end
end
We added a line to the logout method to remove the :album_id in the session object. Although we want to track the current album, we only want to do so when the user is logged in. Then we added a current_album helper method which works just like the current_user, and we also add an assignment helper method current_album=. We also added helper methods for all_albums and all_pictures, which will give back different results depending whether the visitor is logged in or not. We added a helper method to get the user_albums for the logged in user. Note that when the visitor is not logged in, we limit the results to the latest 20 albums or pictures. Finally, we added a current_user_owns? helper method to check if the currently logged in user owns the album or picture.
Now we'll move on and modify the newly generated controller. Again, as for the UsersController, the modifications are fairly superficial, so I won't show the code here.
The Album Views
We need to take some precautions when creating new albums - we must have a user identifier. So the 'new' page need additional protection – we'll tell the visitor to log on, instead of showing the form. The app/views/albums/new.html.erb file will look like:
<h1>New album</h1>

<% if current_user %>

  <%= error_messages_for :album %>

  <% form_for(@album) do |f| %>
    <div>
      <%= f.label :caption, 'Caption:' %>
      <%= f.text_field :caption, :size => 40, :maxlength => 40 %>
    </div>

    <div>
      <%= f.label :description, 'Description:' %>
      <%= f.text_area :description, :rows => 4, :cols => 38 %>
    </div>

    <div>
      <%= f.hidden_field :user_id, :value => current_user.id %>
      <%= f.submit "Create", :class => 'button' %>
    </div>
  <% end %>

<% else %>

  <p class='advise'>
    You must be logged in to create an album.<br />
    Why not <%= link_to 'register', new_user_path %> now?
  </p>

<% end %>

<%= link_to 'Back', albums_path %>
I've highlighted the differences from the original. Basically we've wrapped the form in a 'user logged in?' test, which shows the form if they're logged in, or tells the visitor what to do if they're not. Next is app/views/albums/edit.html.erb, which uses a similar technique, but checking if the current_user_owns? the @album, so I'll skip the source code.
The Album Model
Our migration code created references to the user for an album, and references to the user and album for a picture. Those references basically say 'I belong to ...'. This is known as an association, and is part of the database normalisation process. There are many types of associations; one-to-one, one-to-many, many-to-one, and many-to-many. In our data model one user can have many albums and many pictures, one album can belong to one user and have many pictures, and one picture can belong to one user and belong to one album. We need to update our models so that they understand these associations.
We'll add the following lines to app/models/user.rb:
...
class User < ActiveRecord::Base
  has_many :albums, :order => "id DESC", :dependent => :destroy
  has_many :pictures, :order => "id DESC", :dependent => :destroy
...
One this side of the association for albums and pictures, we also supply the ordering, and the dependency mechanism – this describes what should happen to the albums and pictures, if the user is destroyed.
The app/models/albums.rb file will look like:
class Album < ActiveRecord::Base
  belongs_to :user, :counter_cache => true
  has_many :pictures, :order => "id DESC", :dependent => :destroy

  validates_presence_of :user_id
  validates_presence_of :caption
  validates_length_of   :caption, :within => 1..40
end
As well as the associations, we've also added validation rules. The :counter_cache will make Rails update the albums_count in the User class.
The First (and Second) Album
That's enough coding for now. Start the server, then go to http://localhost:3000/albums, click on New album, log in and create a couple of albums. Remember that you'll have to create some users too.
LUGPC10Images/ror1.png
Oops, forgot to log in. So register or log in, then create the album.
LUGPC10Images/ror2.png

Let's Add Some Pictures

The next obvious step is to add enough code to allow our little web application add some pictures. Wouldn't be much of a WebAlbum without them, I suppose.
First things first, though. I'm getting a little tired of having to change the URL from /users to /albums (and /pictures, next), so we'll fix that right now. Since we're about it, we'll add the additional JavaScript and CSS links, too. Open app/views/layouts/web_album.html.erb, and add the following highlighted lines of code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
    <%= javascript_include_tag :defaults %>
    <%= javascript_include_tag 'lightbox' %>
    <% title = 'WebAlbum: ' +
         controller.controller_name.capitalize + ': ' +
         controller.action_name.capitalize %>
    <title><%= title %></title>
    <%= stylesheet_link_tag 'web_album' %>
    <%= stylesheet_link_tag 'lightbox' %>
  </head>
  <body>
<%= render 'layouts/partials/header' %>
    <div class='content'>
      <div class='navigation'>
        <%= link_to 'Users', users_path %>&#160;|&#160;
        <%= link_to 'Albums', albums_path %>&#160;|&#160;
        <%= link_to 'Pictures', pictures_path %>
      </div>
      <p style="color: green"><%= flash[:notice] %></p>
<%= yield  %>
    </div>
<%= render 'layouts/partials/footer' %>
  </body>
</html>
The javascript_include_tag and stylesheet_link_tag additions are for Lightbox, which we'll look at later on.
Ah yes, &#160; is the Unicode character for non breaking space. In HTML you can use &nbsp;, but this is XHTML, and named entities other than &amp;, &lt;, &gt;, &quot;, and &apos; are not known to the XHTML parser.
That's better, now we have three links Users, Albums and Pictures, that take us to the main list pages.
Uploading Image Files and RMagick
Settle down comfortably, we're going to be here for a little while – there's some heavy lifting to do. We'll have to take the binary data of the uploaded image file and store it somewhere. We'll also have to make smaller copies of the image, which we'll use in the other views – and that means getting to grips with RMagick, the Ruby image manipulation library, par excellence.
In fact, we'll need to save four images:
  • The 'original' image,
  • The 'large' image, scaled to 500 by 500 pixels,
  • The 'medium' image, scaled to 250 by 250 pixels,
  • The 'small' image, scaled to 100 by 100 pixels.
We'll keep the original image, which people can download via a link. The large image will be used to display the scaled picture 'full screen' in our web application, the medium image will be used for the picture view, and the small image for lists. The sizes were chosen to allow screenshots to be made for this article, with a maximum width of 600 pixels.
We'll keep these images in another database table, which we'll call images. Keeping binary image data in a database table may not be the best of solutions, especially if we have thousands or millions of images, but it does have the advantage of keeping all the data in one place. You could use some type of file caching mechanism to reduce the database load – but that's another story that we won't have time for in this lesson.
We'll generate the image model, this time using the model generator script, because we won't need any views or a controller, we'll be reusing the ones we've already got. Type the following all on one line:
ruby script/generate model image user:references picture:references size:integer data:binary content_type:string width:integer height:integer
Next we'll modify the image migration file db/migrate/004_create_images.rb:
class CreateImages < ActiveRecord::Migration
  def self.up
    create_table :images do |t|
      t.references :user, :null => false
      t.references :picture, :null => false
      t.integer :size, :null => false
      t.binary :data, :null => false
      t.string :content_type, :null => false
      t.integer :width, :null => false
      t.integer :height, :null => false
      t.timestamps
    end
    add_index :images, [:picture_id, :size], :name => :images_index
  end

  def self.down
    remove_index :images, :name => :images_index
    drop_table :images
  end
end
Now we can update the database again, type:
rake db:migrate
The next modification is to the Image model in app/models/image.rb:
class Image < ActiveRecord::Base
  belongs_to :user
  belongs_to :picture

  validates_presence_of :user_id
  validates_presence_of :picture_id
  validates_presence_of :size
  validates_presence_of :data
  validates_presence_of :content_type
  validates_presence_of :width
  validates_presence_of :height

  Original = 1
  Large = 2
  Medium = 3
  Small = 4

  Widths =  [ 0, 0, 500, 250, 100 ]
  Heights = [ 0, 0, 500, 250, 100 ]

  Names = [ "", "Original", "Large", "Medium", "Small" ]

  def filename
    return Image.filename(picture_id, size)
  end

  def self.filename(id, size)
    if size == Original
      return "#{id}-#{Names[size]}.jpg"
    else
      return "#{id}-#{Names[size]}.gif"
    end
  end
end
Apart from the usual validation rules, we provide constants for the images sizes, and a filename method which calculates the image filename, based on the picture identifier, size, and stored image format. We do not create an association to the album, since Picture already has this.
Next we'll look at the Picture class, as this handles the first part of the image uploading code, app/models/picture.rb:
require 'RMagick'

class Picture < ActiveRecord::Base
  belongs_to :user, :counter_cache => true
  belongs_to :album, :counter_cache => true
  has_many :images, :order => "size ASC", :dependent => :destroy

  attr_accessor :operation
  
  validates_presence_of :user_id
  validates_presence_of :album_id
  validates_length_of   :caption, :within => 0..40

  NoOp = 0
  RotateClockWise90 = 1
  RotateAntiClockWise90 = 2
  Rotate180 = 3
  Flip = 4
  Flop = 5

  def validate_on_create
    errors.add_to_base(@picture_error) if @picture_error
  end

  def picture_file
    @picture_filename
  end

  def picture_file=(field)
    extract_picture field
  end

  def picture_image
    @picture_image
  end

  private
    def extract_picture(field)
      @picture_error = @picture_image = nil
      if field.class.method_defined? :original_filename
        @picture_filename = field.original_filename
        data = field.read
        if data.length == 0
          @picture_error = "Empty picture file: #{@picture_filename}"
        else
          begin
            @picture_image = org = Magick::Image.from_blob(data).first
            org.format = "JPG"
            self.content_type = org.mime_type
            self.width = org.columns
            self.height = org.rows
          rescue Magick::ImageMagickError
            @picture_image = nil
            @picture_error = "Unknown picture format: #{@picture_filename}"
          end
        end
      else
        @picture_error = "No picture filename specified"
      end
    end
end
Everything should be pretty familiar by now – except that big private extract_picture method. Oh, and the constants – they're for image manipulation operations, which we'll see shortly. The extract_picture method takes care of converting the received binary image information into a Magick::Image class.
The first thing it does is to check that we've received a binary image, using the reflection method method_defined?. Since it is a class method, we use the field.class method to get the class. If a binary image was sent, the field object will contain an original_filename method (and a read method), otherwise it won't. This is an example of duck typing. We then read the binary data, and check its length. As long as the length is greater than 0, we'll have some binary data which we can convert into an image.
At this point we create a new Magick::Image from the data. However, if the data is not a valid image, RMagick will throw an exception. Exceptions are usually caused by rare or unusual events – such as the hard disk being full. In Ruby, we can capture, and act on, an exception by wrapping the method call in a begin ... rescue ... end code block.
If everything goes well, the code from the begin up to, but not including the rescue will be executed, then control will pass to the first statement after the end of the block. If, however, an exception occurs (and in our case, more specifically, a Magick::ImageMagickError exception), then the code between the rescue and end will be executed.
There is more RMagick code in the Picture controller, which we'll look at next.
The Picture Controller
Users and albums have pictures. We'll make pretty much the same superficial modifications to app/controllers/pictures_controller.rb, so I'll skip most of the code for the controller, however, there is a new action which needs a little explaining:
...
  # VIEW /image/:id/:size/:filename.:ext
  def view
    id = params[:id]
    size = params[:size].to_i
    case size
      when Image::Original, Image::Large, Image::Medium, Image::Small
      else
        size = Image::Small
    end
    image = find_image id, size
    if image
      headers['Last-Modified'] = image.updated_at.httpdate
      send_data(image.data, :filename => image.filename,
        :type => image.content_type, :disposition => 'inline', :status => 200)
    else
      render :file => "./public/404.html", :type => 'text/html; charset=utf-8',
        :status => 404
    end
  end

  include RmagickHelper
end
This action takes care of supplying the image data in binary format. A typical image URL would be /image/7/1/7-Original.jpg. In this case, we use the send_data method to send back the binary data, rather than as XHTML.
Picture Controller and RMagick
Now we'll look at that include method call in the controller. We need to add a pretty heavyweight set of helper methods, which can modify the received image and create the smaller images. The sort of modifications that we'll provide are the usual; rotating and flipping the image. Here's the code in app/controllers/rmagick_helper.rb: broken down into sections, with explanations:
require 'RMagick'

module RmagickHelper

  private
    def find_image(pic_id, size)
      Image.find(:first, :conditions => [ "picture_id = ? AND size = ?", pic_id, size ])
    end

    def find_images(pic_id)
      Image.find(:all, :order => "size ASC", :conditions => [ "picture_id = ?", pic_id ])
    end
The classic finders, the first for a specific image, the second for all (four) images, ordered by size.
    def save_all(picture)
      begin
        Picture.transaction do
          picture.save!
          org = picture.picture_image
          images = create_all picture.id, picture.user_id
          update_images images, org
          images.each { |image| image.save! }
        end
      rescue
        return false
      end
      true
    end
The save_all method has the job of saving the picture, and creating and saving all four images. It is called when a new picture is uploaded. It uses a few helper methods, which we'll see shortly, but more importantly, it also handles a database transaction to store the picture, and the images.
To consistently store the information in the database, we have to store one picture and four images. The transaction method guarantees that either all five records are stored, or that none of them are stored.
    def update_all(picture, params)
      begin
        Picture.transaction do
          picture.update_attributes!(params)
          operation = picture.operation
          if operation && operation.to_i != Picture::NoOp
            images = find_images picture.id
            org = Magick::Image.from_blob(images[0].data).first
            case operation.to_i
              when Picture::RotateClockWise90 then     changed = org.rotate(90)
              when Picture::RotateAntiClockWise90 then changed = org.rotate(-90)
              when Picture::Rotate180 then             changed = org.rotate(180)
              when Picture::Flip then                  changed = org.flip
              when Picture::Flop then                  changed = org.flop
              else                                     return true
            end
            update_images images, changed
            images.each { |image| image.save! }
            picture.width = changed.columns
            picture.height = changed.rows
            picture.save!
          end
        end
      rescue
        return false
      end
      true
    end
The update_all method has to use a database transaction as well, to guarantee consistency of the database. It is called when an existing picture is edited. Inside the case statement we handle the actual image manipulation, using some of the many methods available in RMagick. We need to save the picture again afterwards, because the rotate manipulation method can change the width and height of the image.
    def dimensions(size)
      [ Image::Widths[size], Image::Heights[size] ]
    end

    def create_all(pic_id, user_id)
      [ Image.new(:user_id => user_id, :picture_id => pic_id, :size => Image::Original),
        Image.new(:user_id => user_id, :picture_id => pic_id, :size => Image::Large),
        Image.new(:user_id => user_id, :picture_id => pic_id, :size => Image::Medium),
        Image.new(:user_id => user_id, :picture_id => pic_id, :size => Image::Small) ]
    end

    def update_images(images, original)
      update_image(images[0], original)
      large = resize_image(original, dimensions(Image::Large))
      update_image(images[1], large)
      update_image(images[2], resize_image(large, dimensions(Image::Medium), true))
      update_image(images[3], resize_image(large, dimensions(Image::Small), true))
    end

    def update_image(image, data)
      image.content_type = data.mime_type
      image.width = data.columns
      image.height = data.rows
      image.data = data.to_blob
    end
These are the simple helper methods. The update_images method converts the original image into the three smaller scale images. To reduce the workload (which still takes several seconds for large images) the method first reduces the original to the large (500 x 500 pixel) image, and then takes that image to create the smaller versions at 250 x 250, and 100 x 100 pixels, respectively.
    def resize_image(data, dims, reposition = false)
      width = dims[0]
      height = dims[1]
      geometry = Magick::Geometry.new(width, height, nil, nil, Magick::GreaterGeometry)
      data.change_geometry(geometry) do |cols, rows, img|
        img = img.resize(cols, rows)
        img.background_color = "transparent"
        img.format = "GIF"
        if reposition && (cols < width || rows < height)
          img = img.extent(width, height, -((width - cols) / 2), -((height - rows) / 2))
        end
        img
      end
    end
end
The resize_image does the real work of scaling down the image. When reposition is set to true, the reduced image is padded out to make it square.
Phew! That's the most complex part of the program out of the way. Now we'll look at the, er, views.
The Picture Views
Again we need to take precautions when creating new pictures - we must have a logged in user and that user must have at least one album. So the 'new' pages need protection – we'll tell the visitor to log on, or create an album, instead of showing the form. The app/views/pictures/new.html.erb file looks like:
<h1>New picture</h1>

<% if current_user %>

  <% if @albums.length > 0 %>

    <%= error_messages_for :picture %>

    <% form_for(@picture, :html => {
         :enctype => "multipart/form-data",
         :onsubmit => "return Lightbox.block('Uploading picture, please wait...');"
       } ) do |f| %>
      <div>
        <%= f.label :caption, 'Caption:' %>
        <%= f.text_field :caption, :size => 40, :maxlength => 40 %>
      </div>

      <div>
        <%= f.label :description, 'Description:' %>
        <%= f.text_area :description, :rows => 4, :cols => 38 %>
      </div>

      <div>
        <%= f.hidden_field :user_id, :value => current_user.id %>
        <%= f.label :album_id, 'Album:' %>
        <% current_album_id = current_album ? current_album.id : @albums[0].id
           options = @albums.collect { |album| [album.caption, album.id] } %>
        <%= f.select :album_id, options, :selected => current_album_id %>
      </div>

      <div>
        <%= f.label :picture_file, 'Picture file:' %>
        <%= f.file_field :picture_file %>
      </div>

      <div>
        <%= f.submit "Create", :class = 'button' %>
      </div>
    <% end %>

  <% else %>

    <p class='advise'>
      You must have an album to create a picture.<br />
      Why not <%= link_to 'create', new_album_path %> one now?
    </p>

  <% end %>

<% else %>

  <p class='advise'>
    You must be logged in to create a picture.<br />
    Why not <%= link_to 'register', new_user_path %> now?
  </p>

<% end %>

<%= link_to 'Back', pictures_path %>
We have two wrapper tests here, the outer 'logged in?' test, and the inner 'has albums?' test. Each sends back a different advisory message. The more interesting code lies inside the form. Firstly, we have to set the form encoding type attribute (enctype) to 'multipart/form-data', since the form will be posting an image in binary format, together with the other parameters. Secondly, we have added two new form input types; select and file_field. The file_field is pretty straightforward – it lets the user choose a file (hopefully an image file) to be uploaded to the server. The select input lets the user choose the album to add the picture to. This produces a drop down list, where one of the albums can be chosen. The two lines of code above the select input set two local variables; current_album_id, and options. The current_album_id will be either the session stored current_album identifier, or if there isn't one, the identifier of the first album in the list. The options variable is an array of album captions (what the user will see in the drop down list) and album identifiers (what our code requires to be able to identify the album). In this case we use the collect iterator, which puts the results in an array.
The onsubmit attribute uses Lightbox to provide visual feedback while the image is being uploaded, which can take several seconds for a large image file.
Next, let's look at the app/views/pictures/edit.html.erb file:
<h1>Editing picture</h1>

<% if current_user_owns? @picture %>

  <%= error_messages_for :picture %>

  <% form_for(@picture, :html => {
       :onsubmit => "return Lightbox.block('Modifying picture, please wait...');"
     } ) do |f| %>
    <div>
      <label>Image:</label>
      <span class='gallery'><%= picture_img @picture, Image::Medium %></span>
    </div>

    <div>
      <%= f.label :caption, 'Caption:' %>
      <%= f.text_field :caption, :size => 40, :maxlength => 40 %>
    </div>

    <div>
      <%= f.label :description, 'Description:' %>
      <%= f.text_area :description, :rows => 4, :cols => 38 %>
    </div>

    <div>
      <%= f.label :operation, 'Operation:' %>
      <% options = [
           [ "None",                             Picture::NoOp ],
           [ "Rotate clockwise 90 degrees",      Picture::RotateClockWise90 ],
           [ "Rotate anti-clockwise 90 degrees", Picture::RotateAntiClockWise90 ],
           [ "Rotate 180 degrees",               Picture::Rotate180 ],
           [ "Vertical mirror image",            Picture::Flip ],
           [ "Horizontal mirror image",          Picture::Flop ]
         ]
       %>
      <%= f.select :operation, options, :selected => Picture::NoOp %>
    </div>

    <div>
      <%= f.hidden_field :user_id, :value => current_user.id %>
      <%= f.label :album_id, 'Album:' %>
      <% options = @albums.collect { |album| [album.caption, album.id] } %>
      <%= f.select :album_id, options, :selected => @picture.album_id %>
    </div>

    <div>
      <%= f.submit "Update", :class => 'button'  %>
    </div>

  <% end %>

<% else %>

  <p class='advise'>You must be logged in as the owner to edit this picture.</p>

<% end %>

<%= link_to 'Show', @picture %> |
<%= link_to 'Back', pictures_path %>
This is very similar to the previous view, except that the :selected value is the current album_id for the picture. We also display the image, and provide an image manipulation selection box, with rotation and flipping options. You'd be surprised how many people send images that then need rotating. Of course, we've also protected the form, by checking that the current_user_owns? the @picture.
Finally, Some Configuration
We've done nothing up to now by way of configuration. We've just followed the conventions, and Rails took care of the rest. However, we need to make two small changes to /config/routes.rb:
ActionController::Routing::Routes.draw do |map|
  map.root :controller => 'users', :action => 'index'

  map.connect 'image/:id/:size/:filename.:ext', :controller => 'pictures', :action => 'view'
...
Setting map.root (and deleting /public/index.html) defines the controller and action to use for the root URL (/). The map.connect method call specifies the controller and action to send the binary images to the browser.

Adding Some Spice with Lightbox

We've done enough coding now to be able to upload and modify our pictures. We can also see them listed in the pictures and album views. We've added links so that we can see the original pictures. Seems that we're pretty much done – except that we're still missing a little showmanship. Enter Lightbox.
Excellence seems to inspire excellence. Developed using Prototype and script.aculo.us, Lightbox provides a simple mechanism together with some pleasant visual effects, to create a 'slide show' for a group of pictures.
We'll need to make a couple of small modifications to add Lightbox to our application. First we'll add a couple of helper methods in app/helpers/application_helper.rb, to produce the links:
...
  def picture_lightbox(picture, size, lightbox_size)
    caption = picture.caption
    caption = '...' if caption.nil? || caption.blank?
    link = h("<a href=\"#{picture_url(picture.id, Image::Original)}\" title=\"View Original\" target=\"_blank\">#{caption.gsub(/'/, '&#39;')}</a>")
    "<a href='#{picture_url(picture.id, lightbox_size)}' rel='lightbox[list]' title=\"#{link}\">#{picture_img(picture, size)}</a>"
  end

  def picture_a(picture, size, html_tag)
    html_tag = '...' if html_tag.nil? || html_tag.blank?
    "<a href='#{picture_url(picture.id, size)}' target='_blank'>#{html_tag}</a>"
  end

  def picture_img(picture, size)
    width = 0
    height = 0
    case size
      when Image::Original
        width = picture.width
        height = picture.height
      else
        width = Image::Widths[size]
        height = Image::Widths[size]
    end 
    "<img src='#{picture_url(picture.id, size)}' alt=\"#{h picture.caption}\" title=\"#{h picture.caption}\" width='#{width}' height='#{height}' />"
  end

  private
    def picture_url(id, size)
      "/image/#{id}/#{size}/#{Image.filename(id, size)}"
    end
end
The picture_lightbox method produces the link which Lightbox uses to do its magic. Since there might not be a caption for the picture, we use ... so that the anchor has some text. The remaining methods produce normal links to the pictures.
Finally, (and I mean it this time), we need to activate Lightbox in three views; app/views/albums/show.html.erb, app/views/pictures/index.html.erb, and app/views/pictures/show.html.erb. Just add the following JavaScript code snippet to the start of those pages:
<%= javascript_tag "document.observe('dom:loaded', Lightbox.onload);" %>
This ensures that Lightbox is activated when the Document Object Model has been loaded.

The Results

For those of you who can't be bothered to download the code, well thanks for reading this far! Here are a few snapshots of the running application, having logged in as jhl with password villafranca. First, the user list page, which is also now the home page:
LUGPC10Images/ror3.png
The albums list page:
LUGPC10Images/ror4.png
The album page, with associated pictures:
LUGPC10Images/ror5.jpg
Lightbox being used to show a larger version of the small images, the link opens a new window for the original picture:
LUGPC10Images/ror6.jpg

Source Files

All the source files for this lesson can be found in the LUGPC10.zip archived file. Be warned, though – this is a 4 MB file. The password for both users (jhl and lug) is villafranca. The source code is licensed identically to Ruby on Rails, using the MIT license.

What's Next?

Well, this is the end of the programming course, but perhaps not the end of the WebAlbum application. There are still a few small problems which need tidying up. The application still has a 'programmer' look. A few graphics, and better choice of colours would improve things a lot. It's probably a good idea to use the application for a while, you'll soon find where things are a little awkward, perhaps we should add a few links to improve navigation?
More importantly, the controllers need protection again accidental or malignant usage via URL construction, such as /users/1/delete.
I hope you've enjoyed this programming course. We've been through a lot of topics, and seen a lot of code. Hopefully, the results have justified the effort. Do feel free to use the code for you're own experimentation, the best way to learn is by getting your hands dirty!

Addendum

Wrapping the edit.html.erb templates using the current_user_owns? method is not by any means the best way to provide security in Rails. At the time I thought it would have taken too long to explain the intricacies of the filtering mechanism. I have since taken the WebAlbum application one step forward in an article I wrote on my company site, which provides such security. Feel free to take a look.