Chapter 13. Photo Gallery


Photo Gallery is the second of three complete Rails applications in this book, each designed to demonstrate different real-world techniques for building Ajax applications in Rails, from start to finish.

In Example A, the Review Quiz application was primarily textual. So this time, the focus will be more graphical. We'll look at an implementation of Ajax file upload, in-place-editing, encapsulating client-side behavior in custom JavaScript objects, and of course, RJS.

The application is a simple photo gallery and is a simple way to organize and browse collections of images, as shown in Figure B-1. Ajax is used to make the uploading process smooth and to display full-size images inline with the thumbnails view.

Figure 13-1. Gallery home page


To download the source to this application, rails gallery, visit http://www.oreilly.com/catalog/9780596527440. Where files aren't listed they are the same as the Rails default skeleton. Once the files are in the correct place, you'll need to configure a database by editing config/database.yml. The default configuration as generated by rails gallery expects a MySQL database named gallery_development, accessible at localhost with the username root and no password. To get started, create a database for the application and change database.yml as needed, then run rake db:schema:load to create the application's database structure, as specified by schema.rb.

The database and model for the application are very simple: just two tables and two models, for albums and photos.

ActiveRecord::Schema.define(  ) do   create_table "albums", :force => true do |t|     t.column "name", :string, :limit => 50,        :default => "New Album", :null => false   end   create_table "photos", :force => true do |t|     t.column "album_id", :integer, :default => 0,  :null => false     t.column "position", :integer, :default => 1,  :null => false     t.column "file",     :binary,  :default => "", :null => false     t.column "width",    :integer, :default => 0,  :null => false,                          :limit => 50,     t.column "height",   :integer, :default => 0,  :null => false,                          :limit => 50,     t.column "name",     :string, :default => "Untitled",                           :limit => 50, :null => false   end   add_index "photos", ["album_id"], :name => "album_id"   add_index "photos", ["position"], :name => "position" end

The Album model consists of nothing more than an association to the Photo model.

class Album < ActiveRecord::Base   has_many :photos, :order => "position", :dependent => :destroy end

The first two methods in the Photo model handle saving an uploaded image (file=) and downloading it again (full). The next two methods (thumb and medium) generate scaled-down versions of the image using the RMagick library.

class Photo < ActiveRecord::Base   belongs_to :album   acts_as_list :scope => :album   def file= file     with_image file.read do |img|       self.width  = img.columns       self.height = img.rows       write_attribute'file', img.to_blob     end   end      def full(  ) file end   def thumb     with_image do |image|       geo = (1 > (height.to_f / width.to_f)) ? "x100" : "100"       image = image.change_geometry(geo) do |cols, rows, img|         img.resize!(cols, rows)       end       image = image.crop(Magick::CenterGravity, 100, 100)       image.profile!('*', nil)       return image.to_blob { self.format='JPG'; self.quality = 60 }     end   end   def medium     with_image do |img|       maxw, maxh = 640, 480       new = maxw.to_f / maxh.to_f       w, h = img.columns,  img.rows       old = w.to_f / h.to_f       scaleratio = old > new ? maxw.to_f / w : maxh.to_f / h       return img.resize(scaleratio).to_blob  do         self.format='JPG'; self.quality = 60       end     end   end   private     def with_image file=nil       data = Base64.b64encode(file || self.file)       img = Magick::Image::read_inline(data).first       yield img       img = nil       GC.start     end end

routes.rb starts with an interesting trick, in service of the DRY principle: the first block loops over the three possible image sizes (full, thumb, and medium), and creates a route for each.

The calls to map.resources set up RESTful routeseach one creating all of the needed routes to create, retrieve, update, and delete the given resources.

ActionController::Routing::Routes.draw do |map|   %w(full thumb medium).each do |size|     map.named_route "#{size}_photo",        "albums/:album_id/photos/:id.#{size}.jpg",        :controller =>'photos', :action => size   end   map.resources :sessions   map.resources :albums do |album|     album.resources :photos   end   map.home '', :controller =>'albums'   map.connect ':controller/:action/:id' end

In environment.rb, the RMagick library is loaded to handle image manipulation, and we add text/jpeg to Rails' collection of known media types so we can handle JPEG images. At the bottom, some constants are defined to identify the administrator's credentials and the name of the site.

RAILS_GEM_VERSION ='1.1.5' require File.join(File.dirname(__FILE_  _),'boot') Rails::Initializer.run do |config| end require'rmagick' require'base64' Mime::Type.register'image/jpeg', :jpg USERNAME, PASSWORD = "admin", "admin" SITE_TITLE = "Gallery"

This file defines a filter for controlling access and a helper method to determine whether a user is logged in. The application doesn't have any real accounts; just a master user defined in environment.rb.

class ApplicationController < ActionController::Base      private        # Before filter to protect administrator actions     def require_login       unless logged_in?         redirect_to home_url         return false       end     end     # Login information is set in environment.rb     def logged_in?       session[:username] == USERNAME and       session[:password] == PASSWORD     end     helper_method :logged_in?      end

application.rhtml is the master layout for the application. It provides sign-in and sign-out links; the call to yield lets the views insert their own content.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">   <head>     <title><%= SITE_TITLE %></title>     <%= stylesheet_link_tag "application" %>     <%= javascript_include_tag :defaults %>   </head>   <body>     <div >       <% if logged_in? %>         <%= link_to "Sign out", session_url(:id => session.session_id),                :method => :delete %>       <% else%>         <%= link_to_function "Sign in",                "$('signin').toggle();$('signin_link').toggle(  )",                :id => "signin_link" %>         <%= form_tag sessions_url, :id => "signin",                :style => "display: none" %>           Username <%= text_field_tag'username' %>           Password <%= text_field_tag'password' %>           <%= submit_tag "Sign in"%>         <%= end_form_tag %>       <% end %>     </div>     <h1><%= link_to SITE_TITLE, home_url %></h1>     <% if flash[:notice] %>       <div ><%= flash[:notice] %></div>     <% end %>     <%= yield %>   </body>    </html>

This file contains some helper methods. First, thumb_for takes a Photo instance and returns an HTML image tag with its thumbnail. Clicking the image triggers a JavaScript function defined with RJS syntax (page.photo.show). Even though RJS syntax is used, there's no client-server interactionit's just a way to use RJS to simplify your templates.

The next method, toggle_edit_photo, is an RJS helper; it takes a photo ID and toggles the visibility three page elements.

module ApplicationHelper   def thumb_for photo     url = thumb_photo_url(:album_id => photo.album_id, :id => photo)     image = image_tag(url, :class => "thumb", :alt => "")     link_to_function image, nil, :class => "show" do |page|       page.photo.show medium_photo_url(:album_id => photo.album_id,          :id => photo)     end   end      def toggle_edit_photo id     page.toggle "#{id}_name", "#{id}_edit", "#{id}_delete"   end end

The SessionsController provides actions for logging in (creating a session) and logging out (destroying a session).

class SessionsController < ApplicationController   def create     session[:username] = params[:username]     session[:password] = params[:password]     flash[:notice] = "Couldn't authenticate you." unless logged_in?     redirect_to :back   end   def destroy     reset_session     redirect_to :back   end end

The AlbumsController is a fairly typical Rails controller. The update action is the one Ajax part: it supports an in-place editing form by simply returning a piece of text to the browser (the new album name), rather than rendering a complete view.

class AlbumsController < ApplicationController      before_filter :require_login, :only => [:create,:update,:destroy ]   before_filter :find_album, :only => [ :show, :update, :destroy ]   def index     @albums = Album.find :all   end      def create     @album = Album.create params[:album]     redirect_to album_url(:id => @album)   end      def show   end      def update     @album.update_attributes params[:album]     render :text => @album.name   end      def destroy     @album.destroy     redirect_to albums_url   end      private        def find_album(  ) @album = Album.find params[:id] end end

The index.rhtml view loops through all the albums and displays each one, as was shown in Figure B-1. Only a user who is logged in can create, delete, or rename an album.

<% if logged_in? %>   <% form_for :album, Album.new, :url => albums_url, :html =>         { :id => "album_create" } do |f| %>     <%= image_tag "add", :class =>'icon' %>      <%= f.text_field :name %>      <%= submit_tag "Create" %>   <% end %> <% end %> <% if @albums.any? %>   <ul >     <% @albums.each do |album| %>       <li>         <%= link_to image_tag(thumb_photo_url(:album_id => album,                 :id => album.photos.first), :class => "thumb",                 :alt => ""), album_url(:id => album) %><br/>         <%= link_to album.name, album_url(:id => album) %>          <% if logged_in? %>           <%= link_to image_tag("delete", :class =>'icon'),                  album_url(:id => album), :method => :delete %>         <% end %><br/>         <%= pluralize album.photos_count,'photo' %>       </li>Figure B-     <% end %>   </ul> <% end %>

The show.rhtml view provides the meat of the photo gallery's UI. For regular users, it presents all the album's photos, as shown in Figure B-2.

Figure 13-2. Viewing an album


Logged-in users can edit albums in various ways. For example, the user can rename albums as shown in Figure B-3.

Figure 13-3. Renaming an album


The user can also edit an album by changing the photo's label, as shown in Figure B-4, and by adding a new photo.

Figure 13-4. Editing an album


If a user adds a new photo, show.rhtml provides the UI for selecting a photo to upload, as shown in Figure B-5, and also notifies the user that the upload is in progress, as shown in Figure B-6.

Figure 13-5. Choosing a photo for upload


Figure 13-6. Uploading a photo


Because XMLHttpRequest can't handle file uploads, the photo upload form targets a hidden frame with an ID of uploader. The action that handles the upload, PhotosController#create, then renders a bare-bones HTML document with a JavaScript snippet to handle updating the page with the new photo.

<h2 ><%= @album.name %></h2> <% if logged_in? %>   <div  style="display: none">     <%= javascript_tag "$('name').addClassName('rollover')" %>     <%= javascript_tag "$('name').onclick=function(  ){            $('name').toggle(); $('rename').toggle(  )          }"      %>     <% remote_form_for :album, @album,           :url => album_url,           :html => { :method => :put },           :update =>'name',           :before => "$('name').update('Saving...');                       $('name').toggle(  );                       $('rename').toggle(  )" do |f| %>       <%= f.text_field :name %>        <%= link_to_function "Cancel" do |page|              page.toggle :name, :rename           end       %>     <% end %>   </div>   <div >     <% form_for :photo, Photo.new,           :url => photos_url(:album_id => @album),           :html => { :multipart => true, :target => "uploader",                      :id => "photo_upload" } do |f| %>       <label for="photo_file">         <%= image_tag "add", :class =>'icon' %> Add a photo:       </label>        <%= f.file_field :file, :onchange => "Photo.upload(  );" %>     <% end %>     <div  style="display: none">Uploading...</div>     <iframe src="/books/4/386/1/html/2//404.html"  name="uploader"></iframe>   </div> <% end %> <div ><%= render :partial => "photos/index" %></div> <%= render :partial => "photos/show" %>

In this controller, the create action renders without a layout, because create.rhtml contains the necessary HTML boilerplate.

A loop defines three methods at once, one for each image size (full, thumb, and medium). Rails' send_data method handles sending the JPEG data for the appropriately sized image.

The update and destroy actions are fairly simple, but use RJS to send the results back to the page rather than do a full page update.

class PhotosController < ApplicationController      before_filter :require_login, :only => [:create,:update,:destroy ]   before_filter :find_album   before_filter :find_photo, :only => [ :update, :destroy ]      def index     render :partial => "index"   end   # Renders HTML containing a JavaScript callback to    # finish the upload   def create     @photo = @album.photos.create params[:photo]     render :layout =>'plain'   end      %w(full thumb medium).each do |size|     class_eval <<-END       def #{size}         find_photo         send_data @photo.#{size},             :filename => "\#{@photo.id}.#{size}.jpg",             :type =>'image/jpeg',             :disposition =>'inline'       end       caches_page :#{size}     END   end      def update     @photo.update_attributes :name => params[:name]     render :update do |page|       page["#{@photo.id}_name"].replace_html @photo.name     end   end   def destroy     @photo.destroy     render :update do |page|       page[:photos].update render(:partial => "index")     end   end      private        def find_album(  ) @album = Album.find params[:album_id] end     def find_photo(  ) @photo = @album.photos.find params[:id] end end

After uploading a new photo, create.rhtml is returned to the hidden frame containing a simple JavaScript instruction to add the new photo to the page. In order for the JavaScript to be evaluated by the frame, however, it must be wrapped in HTML boilerplate.

The JavaScript itself delegates to Photo.finish, as defined in application.js. In order to access the parent document from the child frame, we use parent.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">     <head><title>Gallery</title></head>     <body>       <% url = photos_url :album_id => @album %>       <%= javascript_tag "parent.Photo.finish('#{url}')" %>     </body> </html>

The _index.rhtml partial displays an unordered list of photos, one item per photo. Each entry in the list includes the thumbnail (using the thumb_for helper); if the user is logged in, the list also includes links to edit the image. For example, link_to_function photo.name displays the photo name as link; if you click, you get an inline form (defined by form_remote_tag). The toggle_edit_photo helper controls whether the photo or the form for editing is displayed.

<% if @album.photos.any? %>   <ul>     <% @album.photos.each do |photo| %>       <li >         <%= thumb_for photo %><br/>         <% if logged_in? %>           <%= link_to_function photo.name, nil,                  :class => "rollover",                  :id => "#{photo.id}_name" do |page|             page.toggle_edit_photo photo.id           end %>           <%= link_to_remote image_tag("delete", :class =>'icon',                  :id => "#{photo.id}_delete"),                  :url => photo_url(:album_id => @album, :id => photo),                  :method => :delete %>           <%= form_remote_tag                 :url => photo_url(:album_id => @album, :id => photo),                 :html => { :style => "display: none", :method => :put,                            :id => "#{photo.id}_edit" },                 :before => update_page { |page|                   page["#{photo.id}_name"].update'Saving...'                   page.toggle_edit_photo photo.id                 } %>             <%= text_field_tag :name, photo.name %>           <%= end_form_tag %>         <% else %>           <%= photo.name %>         <% end %>       </li>     <% end %>   </ul> <% end %>

This partial simply displays a photo and provides links that invoke functions in application.js for navigating to the next and previous photo, as shown in Figure B-7.

Figure 13-7. Viewing an individual photo


<div  style="display: none"></div> <div  style="display: none;">   <img  onclick="Photo.hide(  );" src="/books/4/386/1/html/2/" />   <div >     <%= link_to_function "#{image_tag'arrow_left'} Previous",            "Photo.prev(  )" %>     <%= link_to_function "Next #{image_tag'arrow_right'}",            "Photo.next(  )" %>   </div> </div>

This library of JavaScript functions encapsulates the job of working with photos on the client side. Photo.upload uploads a file, displaying a "loading" message; Photo.finish adds a newly created photo to the page and hides the "loading" message; Photo.show requests the display of a particular photo; and so on. By organizing these methods into the Photo object, they can more easily be called from RJS.

var Photo = {   upload: function(  ) {     $('loading').show(  );     $('photo_upload').submit(  );   },   finish: function(url) {     new Ajax.Updater('photos', url, {method:'get',        onComplete:function(  ){         $('loading').hide(  );         $('photo_upload').reset(  );       }     });   },   show: function(url) {     $('photo').src = url;     $('mask').show(  );     $('photo-wrapper').visualEffect('appear', {duration:0.5});   },   hide: function(  ) {     $('mask').hide(  );     $('photo-wrapper').visualEffect('fade', {duration:0.5});   },   currentIndex: function(  ) {     return this.urls(  ).indexOf($('photo').src);   },   prev: function(  ) {     if(this.urls()[this.currentIndex(  )-1]) {       this.show(this.urls()[this.currentIndex(  )-1])     }   },   next: function(  ) {     if(this.urls()[this.currentIndex(  )+1]) {       this.show(this.urls()[this.currentIndex(  )+1])     }   },   urls: function(  ) {     if (!this.cached_urls) {       this.cached_urls = $$('a.show').collect(function(el){         var onclick = el.onclick.toString(  );         return onclick.match(/".*"/g)[0].replace(/"/g,'');       });     }     return this.cached_urls;   } }

As with Example A, the CSS stylesheet is included for completeness. There's one interesting UI trick here. The img.thumb rule adds a background image with the text "Loading..." to every photo thumbnail. The reason is that newly created images take a few seconds to generate thumbnails, and this satisfies the user's need to see something happening. Of course, when the image has loaded, it covers the default image. There's no interaction with the server, but it makes the application feel more dynamic and responsive.

html {   border-top: 10px solid #000; } body {   background-color: #444;   color: #fff;   font-family: trebuchet ms;   padding-top: 0px;   padding-left: 50px; } h1 {   text-shadow: black 1px 1px 5px;   position: relative;   left: -20px;   width: 400px; } h2 {   text-shadow: black 1px 1px 5px; } h2.rollover:hover {   color: #ffc; } ul, ol, li {   margin: 0;   padding: 0;   text-indent: 0;   list-style-type: none; } li {   float: left;   margin-right: 20px; } a {   color: #abc;   text-decoration: none; } #utility {   float: right;   margin-right: 40px;   color: #ddd;   font-size: 0.8em; } #utility input {   width: 80px; } #notice {   background-color: #999;   width: 500px;   padding: 4px;   margin-bottom: 10px;   color: #900; } #album_create {   background-color: #555;   border: 1px solid #222;   padding: 8px 12px;   width: 300px;   height: 34px;   margin-bottom: 20px; } #album_create input {   font-size: 1.2em;   font-weight: bold; } #album_create input[type=text] {   width: 200px; } #rename input {   font-size: 1.5em;   width: 250px;   margin-left: -5px;   background-color: #ffc;   font-weight: bold;   margin-top: -3px; } #upload_container {   background-color: #555;   border: 1px solid #222;   padding: 0;   width: 520px;   height: 50px;   margin-bottom: 20px;   z-index: 1; } #uploader {     width: 0px;      height: 0px;      border: 0px; } #photo_upload {   position: relative;   top: 15px;   left: 20px;   z-index: 2; } #loading {   position: relative;   top: -37px;   left: 0;   margin: 0px;   padding-top: 10px;   padding-bottom: -10px;   font-size: 1.5em;   height: 40px;   width: 100%;   text-align: center;   background-color: #222;   z-index: 3;   opacity: .75;   filter: alpha(opacity=75); } img.icon {   position: relative;   top: 3px;   left: 2px; } img.thumb {   border: 1px solid black;   background: #C2C2C2 url(/images/loading.gif);   width: 100px;   height: 100px; } #mask {   position: absolute;   top: 0;   left: 0;   width: 100%;   height: 100%;   background-color: #222;   z-index: 1000;   opacity: .75;   filter: alpha(opacity=75); } #photo-wrapper {   position: absolute;   top: 0;   left: 0;   z-index: 1001;   position: absolute;   text-align: center;   width: 100%;   height: 100%; } #nav a {   margin: 7px;   color: #ccc;   text-transform: uppercase;   font-size: 0.7em; } #nav img {   position: relative;   top: 5px; } #photo {   float: center;   margin-top: 100px auto;   margin-bottom: auto;   border: 8px solid #222; } #albums li {   width: 100px;   text-align: center;   font-size: 0.8em;   color: #777; } #albums a {   color: #fff;   font-size: 1.2em; } #photos li .icon {   left: 3px; } #photos li {   width: 120px;   height: 140px;   text-align: center;   font-size: 0.8em;   xline-height: 0.8em;   color: #ccc;   margin-bottom: 10px; } #photos a {   color: #fff; } #photos li input {   width: 100px;   background-color: #ffc; } #photos a.rollover:hover {   color: #ffc; } 




Ajax on Rails
Ajax on Rails
ISBN: 0596527446
EAN: 2147483647
Year: 2006
Pages: 103
Authors: Scott Raymond

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net