Chapter 14. Intranet Workgroup Collaboration


This application is a workgroup tool that's appropriate for small teams, as shown in Figure C-1. It provides a lot of features for office communication and collaboration: facilities for managing projects, attaching comments and documents to projects, and so on. It's typical of many real-world applications: not flashy like Google Maps, but useful and necessary. The Ajax is also relatively low-key: it makes the application more powerful and usable, but doesn't call attention to itself. This application also shows where not to use Ajax.

Figure 14-1. Intranet home page


To download the source to this application, rails intranet, 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 intranet expects a MySQL database named intranet_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 application uses three tables: users, posts, and attachments. The users table is for managing users, as you'd expect. Attachments are binary file uploads (e.g., photos, spreadsheets, documents). Most of the application centers on posts; a post can be a document (contained in an attachment), a project plan, a message, a comment, or a contact.

ActiveRecord::Schema.define(  ) do   create_table "users", :force => true do |t|     t.column "email",        :string,   :limit => 100,                               :default => "",    :null => false     t.column "password",     :string,   :limit => 100,                               :default => "",    :null => false     t.column "name",         :string,   :limit => 40,                                :default => "",    :null => false     t.column "phone",        :string,   :limit => 50,                                :default => "",    :null => false     t.column "address",      :string,   :limit => 50,                                :default => "",    :null => false     t.column "city",         :string,   :limit => 50,                                :default => "",    :null => false     t.column "state",        :string,   :limit => 50,                                :default => "",    :null => false     t.column "zip",          :string,   :limit => 50,                                :default => "",    :null => false     t.column "picture_id",   :integer     t.column "created_at",   :datetime     t.column "updated_at",   :datetime     t.column "status",       :string,   :limit => 50,                                :default => "",    :null => false     t.column "last_active",  :datetime     t.column "admin",        :boolean,                                               :default => false, :null => false   end   add_index "users", ["email"], :name => "email", :unique => true   add_index "users", ["password"], :name => "password"   create_table "posts", :force => true do |t|     t.column "type",         :string,   :limit => 20     t.column "post_id",      :integer     t.column "created_at",   :datetime     t.column "updated_at",   :datetime     t.column "created_by",   :integer     t.column "updated_by",   :integer     t.column "name",         :string,   :limit => 128,                               :default => "Untitled", :null => false     t.column "body",         :text, :default => "",  :null => false     t.column "email",        :string,   :limit => 50,                                :default => "",         :null => false     t.column "phone",        :string,   :limit => 50,                                :default => "",        :null => false     t.column "start_date",   :date     t.column "end_date",     :date     t.column "attachment_id",           :integer     t.column "attachment_filename",     :string     t.column "attachment_content_type", :string,   :limit => 128     t.column "attachment_size",         :integer   end   add_index "posts", ["type"], :name => "type"   add_index "posts", ["created_at"], :name => "created_at"   add_index "posts", ["updated_at"], :name => "updated_at"   add_index "posts", ["post_id"], :name => "post_id"   create_table "attachments", :force => true do |t|     t.column "content",    :binary     t.column "updated_at", :datetime   end end

The User model is used to record the system's users. Each user is associated to the posts he created, and every user can have a user picture, stored in an Attachment model. The inactive? method tells whether the user is currently online (more precisely, has been active within the last minute).

class User < ActiveRecord::Base   has_many   :posts, :foreign_key => "created_by",      :dependent => :destroy   belongs_to :picture, :class_name =>'Attachment',      :foreign_key =>'picture_id', :dependent => :destroy   validates_length_of     :name, :password, :email, :within => 4..100   validates_uniqueness_of :email   validates_format_of     :email,      :with => /^(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?$/   def self.authenticate(email, password)     find_by_email_and_password(email, password)   end   def first_name; name.split.first; end   def last_name;  name.split.last; end   def short_name     name.blank? ? "" : "#{first_name} #{last_name[0,1]}."   end   # Makes an attachment from a thumbnail upload   def file= file     unless file.size == 0       picture = Attachment.new :content => file.read       picture.save       write_attribute'picture_id', picture.id     end   end      # n.b, the status heartbeat updates last_active every 30 seconds   def inactive?     last_active < 1.minute.ago rescue true   end end

Post is the superclass for Plan, Message, Document, and Comment. A post has a creator, which must be a user. A post can have attachment and comments. The file= method allows an attachment to be added to a post.

class Post < ActiveRecord::Base      has_many   :comments,  :order =>'id', :dependent => :destroy   belongs_to :creator, :class_name =>'User',      :foreign_key => "created_by"   belongs_to :attachment, :dependent => :destroy   validates_presence_of :name   # Creates an attachment from a file upload   def file=(file)     unless file.size == 0       attachment=Attachment.new :content => file.read       attachment.save       write_attribute('attachment_id', attachment.id)       write_attribute('attachment_filename', file.original_filename)       write_attribute('attachment_content_type', file.content_type)       write_attribute('attachment_size', file.size)     end   end    end

Contact (not to be confused with User) is a type of Post that stores information about a personfor example, a sales representative, a publicist, or a customer.

class Contact < Post   validates_format_of :name, :with => /^.+ .+$/,      :message => "must include full name"   validates_format_of :email,      :with => /^(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?$/   # Find by first letter of last name   def self.letter letter     Contact.find :all,        :conditions => [ "name like ?", '% '+letter+'%' ]   end      # Turns "Scott Douglas Raymond" into "Raymond, Scott Douglas"   def reversed_name     names = name.split     "#{names.pop}, #{names.join ' '}"   end end

Document is a subclass of Post that can represent almost any kind of content: a spreadsheet, PDF, Word document, etc.

class Document < Post end

Message is yet another subclass of Post that represents almost any kind of simple text message.

class Message < Post end

Plan is a post that represents a particular kind of event. The Plan model provides methods to get plans by certain date ranges.

class Plan < Post      def self.this_week     Plan.find :all, :conditions => "start_date >= now(  ) and          start_date < '#{Date.today + 7}'",        :order => "start_date asc"   end      def self.next_three_weeks     Plan.find :all, :conditions => "start_date >=          '#{Date.today + 7}' and start_date < '#{Date.today + 28}'",        :order => "start_date asc"   end      def self.beyond     Plan.find :all,        :conditions => "start_date >= '#{Date.today + 28}'",        :order => "start_date asc"   end end

Project is yet another kind of Post.

class Project < Post end

Comment is a simple kind of Post that can be attached to another post.

class Comment < Post   belongs_to :post   validates_presence_of :body    end

For efficiency reasons, binary files aren't stored directly in the posts table. Instead, Attachment manages them. Attachments are used to represent the binary data associated with a document, and for images attached to the system's users.

class Attachment < ActiveRecord::Base end

The routing for this application is fairly simple. The calls to map.resources set up RESTful access to the application.

ActionController::Routing::Routes.draw do |map|   # A resource for each post type   map.resources :messages, :plans, :documents, :projects, :contacts,      :member => { :download => :get }   # A comments resource under every post type; e.g.,   # /messages/comments and /documents/comments   map.resources :comments, :path_prefix => "/:post_type/:post_id"   # User and session resources   map.resources :sessions   map.resources :users, :collection => { :statuses => :get },                          :member => { :status => :any }   # Home and default routes   map.home '', :controller =>'messages', :action =>'home'   map.connect ':controller/:action/:id' end

The environment file requires the application to load lib/labeling_form_helper.rb.

RAILS_GEM_VERSION ='1.1.2' require File.join(File.dirname(__FILE_  _),'boot') Rails::Initializer.run do |config| end # Include a customized helper for building forms from the lib/ dir require'labeling_form_helper'

authentication.rb provides very simple authentication services.

# based on acts_as_authenticated  # http://svn.techno-weenie.net/projects/plugins/acts_as_authenticated module Authentication   protected     def logged_in?       return false unless session[:user_id]       begin         @current_user ||= User.find(session[:user_id])       rescue ActiveRecord::RecordNotFound         reset_session       end     end          def current_user       @current_user if logged_in?     end          def require_login       username, passwd = get_auth_data       if username && passwd         self.current_user ||=            User.authenticate(username, passwd) || :false       end       return true if logged_in?       respond_to do |format|         format.html do           session[:return_to] = request.request_uri           redirect_to new_session_url         end         format.xml do           headers["Status"]           = "Unauthorized"           headers["WWW-Authenticate"] = %(Basic realm="Web Password")           render :text => "Could't authenticate you",              :status =>'401 Unauthorized'         end       end       false     end          def access_denied       redirect_to new_session_url     end            def store_location       session[:return_to] = request.request_uri     end          def redirect_back_or_default(default)       session[:return_to] ?          redirect_to_url(session[:return_to]) :          redirect_to(default)       session[:return_to] = nil     end          def self.included(base)       base.send :helper_method, :current_user, :logged_in?     end   private     def get_auth_data       user, pass = nil, nil       if request.env.has_key?'X-HTTP_AUTHORIZATION'          authdata = request.env['X-HTTP_AUTHORIZATION'].to_s.split        elsif request.env.has_key?'HTTP_AUTHORIZATION'          authdata = request.env['HTTP_AUTHORIZATION'].to_s.split         end        if authdata && authdata[0] =='Basic'          user, pass = Base64.decode64(authdata[1]).split(':')[0..1]        end        return [user, pass]      end end

LabelingFormBuilder overrides some of the methods in form_for and remote_form_for, extending them so that they automatically handle field names. It's an admittedly tricky bit of code, partly because I use a loop to define several methods at once (e.g., text_field and password_field).

class LabelingFormBuilder < ActionView::Helpers::FormBuilder   # Overrides default field helpers, adding support for automatic    # <label> tags with inline validation messages.   (%w(text_field password_field text_area        date_select file_field)).each do |selector|     src = <<-end_src       def #{selector}(method, options = {})         text = options.delete(:label) || method.to_s.humanize         errors = @object.errors.on(method.to_s)         errors = errors.is_a?(Array) ? errors.first : errors.to_s         html = '<label for="' + @object_name.to_s +'_' +                  method.to_s + '">'         html << text         unless errors.blank?           html << ' <span >' + errors + '</span>'         end         html << '</label> '         #{selector=='date_select' ? "html << '<span id=\"' +                       @object_name.to_s +'_' + method.to_s +                       '\"></span>'" : ""}         html << super         html       end     end_src     class_eval src, __FILE__, __LINE_  _   end end

The application controller provides some before_filters (to make sure that the user has logged in, to make sure the user is valid, and to display a message of the day); it also provides helper methods for access control.

class ApplicationController < ActionController::Base   include Authentication   before_filter :require_login   before_filter :set_system_announcement   before_filter :check_for_valid_user      private     # Feel free to remove or change this announcement when      # customizing the application to your needs     def set_system_announcement       flash.now[:system_announcement] =          "This is the <strong>Ajax on Rails Intranet</strong>, <br/>          released as part of <a href=\"http://scottraymond.net/\">          <em>Ajax on Rails</em></a> from O&rsquo;Reilly Media."     end     # Helper method to determine whether the current user can      # modify +record+     def can_edit? record       # admins can edit anything       return true if current_user.admin?       case record.class.to_s         when'User'           # regular users can't edit other users           record.id == current_user.id         when'Message'           # messages can only be edited by their creators           record.created_by == current_user.id         else true # everyone can edit anything else       end     end     helper_method :can_edit?     # Helper method to determine whether the current user is      # an administrator     def admin?; current_user.admin?; end     helper_method :admin?          # Before filter to limit certain actions to administrators     def require_admin       unless admin?         flash[:warning] = "Sorry, only administrators can do that."         redirect_to messages_url       end     end     # Before filter that insists the current user model is      # valid  generally just used when the first user is created.     def check_for_valid_user       if logged_in? and !current_user.valid?         flash[:warning] = "Please create your administrator account"         redirect_to edit_user_url(:id => current_user)         return false       end     end end

application_helper.rb defines more helper methods, for returning information about content types. page_title tries to infer a page title if a title isn't given explicitly. standard_form uses the labeling_form_helper; it exists to simplify the view templates.

module ApplicationHelper      # Returns the name of an icon (in public/images) for the    # given content type   def icon_for content_type     case content_type.to_s.strip       when "image/jpeg"         "JPG"       when "application/vnd.ms-excel"         "XLS"       when "application/msword"         "DOC"       when "application/pdf"         "PDF"       else "Generic"     end   end   # Returns a textual description of the content type   def description_of content_type     case content_type.to_s.strip       when "image/jpeg"         "JPEG graphic"       when "application/vnd.ms-excel"         "Excel worksheet"       when "application/msword"         "Word document"       when "application/pdf"         "PDF file"       else ""     end   end      # Returns the name of the site (for the title and h1 elements)   def site_title    'Intranet'   end   # If a page title isn't explicitly set with @page_title, it's    # inferred from the post or user title   def page_title     return @page_title if @page_title     return @post.name if @post and !@post.new_record?     return @user.name if @user and !@user.new_record?     ''   end   # Returns a div for each key passed if there's a flash    # with that key   def flash_div *keys     divs = keys.select { |k| flash[k] }.collect do |k|       content_tag :div, flash[k], :class => "flash #{k}"     end     divs.join   end      # Returns a div with the user's thumbnail and name   def user_thumb user     img = tag("img",        :src => formatted_user_url(:id => user, :format =>'jpg'),        :class =>'user_picture', :alt => user.name)     img_link = link_to img, user_url(:id => user)     text_link = link_to user.short_name, user_url(:id => user)     content_tag :div, "#{img_link}<br/>#{text_link}",        :class =>'user'   end      # Returns a div   def clear_div     '<div ></div>'   end   # Renders the form used for all post and user creating/editing.   # Yields an instance of LabelingFormBuilder    # (see lib/labeling_form_helper.rb).   def standard_form name, object, &block     url  = { :action    => object.new_record? ? "index" : "show" }     html = { :class     => "standard",              :style     => (@edit_on ? '' : "display: none;"),              :multipart => true }     concat form_tag(url, html) + "<fieldset>", block.binding     unless object.new_record?       concat '<input name="_method" type="hidden" value="put" />',          block.binding     end     yield LabelingFormBuilder.new(name, object, self, {}, block)     concat "</fieldset>" + end_form_tag, block.binding   end      # Standard submit button and delete link for posts and users   def standard_submit name=nil, object=nil     name = post_type unless name     object = @post unless object     delete_link = link_to("Delete", { :action =>'show' },        :method => :delete,        :confirm => "Are you sure?",       :class => "delete")     submit_tag("Save #{name}") +        (object.new_record? ? "" : (" or " + delete_link))   end end

application.rhtml is a basic layout that includes links for navigating through the application. It includes links for signing in and out, plus CSS to create some tabbed navigation. The utility DIV is an Ajax sidebar that lists who is and who isn't logged in, shown in Figure C-2. This DIV uses Prototype's PeriodicalExecutor to send a "heartbeat" back to the server every 30 seconds. In response, the server sends of list of users who are logged in; this list is displayed by rendering the users/_statuses.rhtml partial.

Figure 14-2. Changing presence status


The utility DIV also creates a script.aculo.us Ajax InPlaceEditor to allow the user to modify his "away" message inline.

<!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 +            (page_title.blank? ? '' : " - #{page_title}") %>     </title>     <%= stylesheet_link_tag "application" %>     <%= javascript_include_tag :defaults %>   </head>   <body >     <%= flash_div :system_announcement %>     <div >       <h1><%= link_to site_title, home_url %></h1>       <% if logged_in? and current_user.valid? %>         <div >           Signed in as            <%= link_to current_user.first_name,                  user_url(:id => current_user),                  :class =>'strong stealth' %><br/>           <%= link_to'Settings',                        edit_user_url(:id => current_user),                        :class =>'small subtle' %>            <%= link_to'Sign Out',                        session_url(:id => session.session_id),                        :method => :delete,                        :class =>'small delete' %>         </div>         <ul >           <li >             <%= link_to "Messages", messages_url %>           </li>           <li >             <%= link_to "Event Plans", plans_url %>           </li>           <li >             <%= link_to "Documents", documents_url %>           </li>           <li >             <%= link_to "Projects", projects_url %>           </li>           <li >             <%= link_to "Contacts", contacts_url %>           </li>         </ul>       <% end %>     </div>     <div >       <%= flash_div :notice %>       <% if logged_in? and current_user.valid? %>         <div >           <ul>             <li>               <%= link_to current_user.short_name,                            user_url(:id => current_user) %>               <span >                 <%= current_user.status.blank? ?                        "(Click to set status)" :                        current_user.status %>               </span>             </li>           </ul>           <%= javascript_tag "new Ajax.InPlaceEditor('my_status',                  '#{user_url(current_user)}',                  {loadTextURL:'#{status_user_url(current_user)}',                   ajaxOptions:{method:'put'},                   callback:function(form, value){                      return'user[status]='+escape(value);                   }});" %>           <%= render :partial => "users/statuses" %>           <%= javascript_tag "new PeriodicalExecuter(function(  ){                  new Ajax.Updater('statuses', '#{statuses_users_url}',                     {method:'get'}); }, 30)" %>         </div>       <% end %>     </div>          <div >       <%= flash_div :warning %>       <%= content_tag :h2, page_title %>       <%= yield %>     </div>            </body> </html>

PostsController is the superclass for all the other controllers. It implements all the basic CRUD actions for posts.

class PostsController < ApplicationController      before_filter :find_post,      :only => [ :show, :download, :edit, :update, :destroy ]   before_filter :check_permissions, :only => [ :update, :destroy ]      def index     @page_title = post_type.pluralize     @post = model.new     @posts = model.find :all   end      def new     @page_title = "New #{post_type}"     @edit_on = true     @post = model.new   end   def create     @post = model.new params[:post]     @post.creator = current_user     @post.updated_by = @post.created_by     if @post.save        flash[:notice] ='Post successfully created.'       redirect_to :action =>'index'     else       @page_title = "New #{post_type}"       @edit_on = true       render :action =>'new'     end   end   def show   end   def download     filename = @post.attachment_filename.split(/\\/).last     send_data @post.attachment.content, :filename => filename,        :type => @post.attachment_content_type,        :disposition =>'attachment'   end      def edit     @edit_on = true     render :action =>'show'   end      def update     post = params[:post].merge(:updated_by => current_user)     if @post.update_attributes post       flash[:notice] ='Your changes were saved.'       redirect_to :action =>'show'     else       @edit_on = true       render :action =>'show'     end   end   def destroy     @post.destroy     flash[:notice] = "The post was deleted."     redirect_to :action =>'index'   end      private        # The name of the model associated with the controller.      # Expected to be overridden.     def model_name;'Post'; end     # The'human name' of the model, if different from the actual      # model name.     def post_type; model_name; end     helper_method :post_type     # The model class associated with the controller.     def model; eval model_name; end        def find_post       @post = model.find params[:id]     end          # Before filter to bail unless the user has permission to edit      # the post.     def check_permissions       unless can_edit? @post         flash[:warning] = "You can't edit that post."         redirect_to :action =>'show'         return false       end     end    end

The CommentsController is the first of many subclasses of the Posts controller. I'll move through the controllers quickly; they are fairly similar.

The create action creates a comment, as shown in Figure C-3; it uses respond_to, which allows the form to work correctly even if the browser has JavaScript disabled.

Figure 14-3. Adding a comment


class CommentsController < ApplicationController   before_filter :find_post   def index   end   # Handles both Ajax and regular form submissions   def create     @comment = Comment.new params[:comment]     @comment.post_id = @post.id     @comment.name = "Re: #{@post.name}"     @comment.creator = current_user     @comment.save     respond_to do |format|       format.html {         flash[:notice] = "Comment saved."         redirect_to :back       }       format.js {         render :update do |page|           page[:comments].reload         end       }     end   end      def show     @comment = @post.comments.find params[:id]   end      private        def find_post       @post = Post.find params[:post_id]     end    end

The _comment.rhtml partial displays a single comment that already exists.

<div >   <%= user_thumb comment.creator %>   <p ><%= time_ago_in_words comment.created_at %> ago</p>   <%= simple_format comment.body %>   <%= clear_div %> </div>

The _comments.rhtml partial loops through the existing comments, displaying the _comment.rhtml partial for each. After listing all the comments, it provides an Ajax-enabled form for inserting a new comment. A fallback is included in case JavaScript is disabled.

<div >   <h2>     <% if @post.comments.any? %>       <%= pluralize @post.comments.size,'Comment' %> so far     <% else %>       Be the first to post a comment     <% end %>   </h2>   <%= render :partial =>'comments/comment',          :collection => @post.comments %>   <%# Creates an Ajax-enabled form with a fallback to      # regular form submission %>   <% remote_form_for :comment,        :url => comments_url(:post_type => params[:controller],           :post_id => @post),        :html => { :action =>            comments_url(:post_type => params[:controller],              :post_id => @post)         },        :before => "$('spinner').show(  )",        :complete => "$('spinner').hide(  );           $('comment_body').value=''" do |c| %>     <fieldset>       <h3><%= current_user.name %> said...</h3>       <p><%= c.text_area :body %></p>       <p>          <%= submit_tag "Post New Comment" %>           <%= image_tag "spinner.gif", :style => "display: none;",               :id => "spinner" %>       </p>     </fieldset>   <% end %> </div>

<%= render :partial =>'comments' %>

<%= render :partial => "comment" %>

Another subclass of Posts, it is used for managing contact recordssuch as customers, vendors, or partners.

ContactsController inherits most of its behavior from PostsController.

class ContactsController < PostsController   # If params[:letter] is specified, only returns users whose    # last names start with it   def index     @page_title = post_type.pluralize     @post = model.new     @posts = params[:letter] ?        Contact.letter(params[:letter]) :        Contact.find(:all)   end   private     def model_name;'Contact'; end end

The _form.rhtml partial is a form for entering contact info, as shown in Figure C-4. standard_form wraps form_for, extending it to include field labels automatically.

Figure 14-4. Editing a contact


<%# See +standard_form+ in application_helper.rb %> <% standard_form :post, @post do |f| %>   <%= f.text_field :name %>   <%= f.date_select :start_date, :label => "First Call" %>   <%= f.date_select :end_date, :label => "Last Call" %>   <%= f.text_field :phone %>   <%= f.text_field :email %>   <%= f.text_area :body, :label => "Notes" %>   <%= standard_submit %> <% end %>

The _post.rhtml partial renders one contact.

<div >   <div >     <h4><%= link_to post.reversed_name, :action =>'show',                :id => post %></h4>     <p >       <span >         <%= link_to pluralize(post.comments_count,'comment'),                :action =>'show', :id => post %>       </span>       <% if post.end_date %> Last called           <%= time_ago_in_words post.end_date %> ago<% end %>     </p>   </div>   <%= clear_div %> </div>

This view renders all the contacts, as shown in Figure C-5, by using the _post.rhtml partial.

Figure 14-5. Contacts list


<div >   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >     <span>+ </span>     <%= link_to_function "New #{post_type}", "PostForm.toggle(  )",            :class =>'create' %>   </div>   <%= render :partial =>'form' %> </div> <div >   <div >     <%= link_to "All", { :letter => "" },            :class => (params[:letter].blank? ?'active' : '') %>     <% ('A'..'Z').each do |letter| %>       <%= link_to letter, { :letter => letter },              :class => (params[:letter]==letter ?'active' : '') %>     <% end %>   </div>   <%= render :partial => "post", :collection => @posts %> </div>

A template that holds a form (defined by the _form.rhtml partial) for creating new contacts.

<div  >   <%= render :partial =>'form' %> </div>

A detailed view of one contact. The edit link swaps the plain view with the form view so that you can edit a contact.

<div  <% if @edit_on %><% end %>>   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >     <% if can_edit? @post %>       <span>&nbsp;&nbsp;</span>       <%= link_to_function "Edit", "PostForm.toggle(  )",              :class =>'create' %>     <% end %>   </div>   <div >     Posted <%= distance_of_time_in_words_to_now(@post.created_at) %>      ago by <%= link_to @post.creator.name,                    user_url(@post.creator), {'class' =>'grey' } %>   </div>   <%= render :partial =>'form' %>   <div >     <p><strong>First call:</strong> <%= @post.start_date %></p>     <p><strong>Last call:</strong> <%= @post.end_date %>         (<%= time_ago_in_words @post.end_date %> ago)</p>     <% unless @post.phone.blank? %>       <p><strong>Phone:</strong> <%= @post.phone %></p>     <% end %>     <% unless @post.email.blank? %>       <p><strong>Email:</strong> <%= mail_to @post.email %></p>     <% end %>     <%= simple_format @post.body %>   </div> </div> <%= render :partial => "comments/comments",        :comments => @post.comments %>

Documents are simpler than contacts, and the controller behavior is essentially the same. Documents allow you to upload files, which are represented with the appropriate icons for their file type, as shown in Figure C-6.

Figure 14-6. Documents list with appropriate icons


class DocumentsController < PostsController   private     def model_name;'Document'; end end

This partial allows you to upload a file. (The attachment is the actual binary.)

<% standard_form :post, @post do |f| %>   <%= f.text_field :name %>   <% label = (@post.new_record? or !@post.attachment_id) ?                  "File to upload" :                  "File to upload (overwriting existing file)" %>   <%= f.file_field :file, :label => label %>   <%= f.text_area :body, :label => "Description" %>   <%= standard_submit %> <% end %>

<div >   <div >     <% icon = icon_for(post.attachment_content_type) %>     <img src="/books/4/386/1/html/2//images/icon_<%= icon %>_big.gif"  />     <h4>       <%= link_to post.name, :action =>           post.attachment_id.nil? ?'show' :'download',           :id => post %>     </h4>     <p >       <span >         <%= link_to pluralize(post.comments_count,'comment'),                :action =>'show', :id => post %>       </span>       <%= description_of post.attachment_content_type %>        <%= link_to'Edit', :action =>'edit', :id => post %>     </p>   </div>   <%= clear_div %> </div>

<div >   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >     <span>+ </span>     <%= link_to_function "New #{post_type}", "PostForm.toggle(  )",            :class =>'create' %>   </div>   <%= render :partial =>'form' %> </div> <div >   <%= render :partial => "post", :collection => @posts %> </div>

Simply renders the _form.rhtml partial to allow creating a new document, as shown in Figure C-7.

Figure 14-7. Creating a new document


<div  >   <%= render :partial =>'form' %> </div>

<div  <% if @edit_on %><% end %>>   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >     <% if can_edit? @post %>       <span>&nbsp;&nbsp;</span>       <%= link_to_function "Edit", "PostForm.toggle(  )",              :class =>'create' %>     <% end %>   </div>   <div >     Posted      <%= distance_of_time_in_words_to_now(@post.created_at) %> ago      by <%= link_to @post.creator.name, user_url(:id => @post.creator),              {'class' =>'grey' } %>   </div>   <%= render :partial =>'form' %>   <div >     <% if @post.attachment_id %>       <% icon = icon_for(@post.attachment_content_type) %>       <img src="/books/4/386/1/html/2//images/icon_<%= icon %>_big.gif"  />       <h4><%= link_to @post.name, :action =>'download' %></h4>       <p >         <%= description_of @post.attachment_content_type %>       </p>     <% end %>     <%= simple_format(@post.body) if @post.body.any? %>   </div> </div> <%= render :partial => "comments/comments",        :comments => @post.comments %>

Again, messages are similar to contacts and documents. The home action of the messages controller is the default home page for the application, as was shown in Figure C-1.

class MessagesController < PostsController      # Default action for the app; might be changed to show a    # dashboard-like view   def home     flash.keep     redirect_to messages_url   end   def index     super     @post_pages, @posts = paginate :messages,        :order_by =>'created_at desc', :per_page => 30   end   private     def model_name;'Message'; end end

<% standard_form :post, @post do |f| %>   <%= f.text_field :name, :label => "Subject" %>   <%= f.text_area :body, :label => "Message body" %>   <%= standard_submit %> <% end %>

<div >   <%= user_thumb post.creator %>   <div >     <h4>       <%= link_to post.name,           url_for(:action =>'show', :id => post) %>     </h4>     <p >       <span >         <%= link_to pluralize(post.comments_count,'comment'),                :action =>'show', :id => post %>       </span>       <%= time_ago_in_words post.updated_at %> ago     </p>     <%= simple_format post.body %>   </div>   <%= clear_div %> </div>

<div >   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >      <span>+ </span>      <%= link_to_function "New #{post_type}", "PostForm.toggle(  )",             :class =>'create' %>   </div>   <%= render :partial =>'form' %> </div> <div >   <%= render :partial => "post", :collection => @posts %>   <%= pagination_links @post_pages %> </div>

<div  >   <%= render :partial =>'form' %> </div>

<div  <% if @edit_on %><% end %>>   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >     <% if can_edit? @post %>       <span>&nbsp;&nbsp;</span>       <%= link_to_function "Edit", "PostForm.toggle(  )",              :class =>'create' %>     <% end %>   </div>   <div >     Posted <%= time_ago_in_words @post.created_at %> ago by      <%= link_to @post.creator.name, user_url(:id => @post.creator),           {'class' =>'grey' } %>   </div>   <%= render :partial =>'form' %>   <div >     <%= simple_format(@post.body) if @post.body.any? %>   </div> </div> <%= render :partial => "comments/comments",        :comments => @post.comments %>

Plans are also similar to documents, messages, and contacts. Ajax is used in the form for building a plan, adding comments to a plan, and toggling back and forth between the show view and the edit view. Figure C-8 shows a list of upcoming event plans.

Figure 14-8. Plans list


class PlansController < PostsController        def index     super     @page_title = "Upcoming Event Plans"     @this_week = Plan.this_week     @next_three_weeks = Plan.next_three_weeks     @beyond = Plan.beyond   end   private     def model_name;'Plan'; end     def post_type;'Event Plan'; end end

<% standard_form :post, @post do |f| %>   <%= f.text_field :name %>   <%= f.date_select :start_date, :label => "Date" %>   <%= f.text_area :body, :label => "Details" %>   <%= standard_submit %> <% end %>

<div >   <div >     <h4>       <%= link_to post.name, :action =>'show', :id => post %>     </h4>     <p >       <span >         <%= link_to pluralize(post.comments_count,'comment'),                :action =>'show', :id => post %>       </span>       <%= post.start_date.strftime "%a, %b %d" %>     </p>   </div>   <%= clear_div %> </div>

<div >   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >     <span>+ </span>     <%= link_to_function "New #{post_type}", "PostForm.toggle(  )",            :class =>'create' %>   </div>   <%= render :partial =>'form' %> </div> <div >   <h3>This week</h3>   <%= render :partial => "post", :collection => @this_week %>   <h3>Next three weeks</h3>   <%= render :partial => "post", :collection => @next_three_weeks %>   <h3>Beyond</h3>   <%= render :partial => "post", :collection => @beyond %> </div>

<div  >   <%= render :partial =>'form' %> </div>

<div  <% if @edit_on %><% end %>>   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >     <% if can_edit? @post %>       <span>&nbsp;&nbsp;</span>       <%= link_to_function "Edit", "PostForm.toggle(  )",              :class =>'create' %>     <% end %>   </div>   <div >     Posted      <%= distance_of_time_in_words_to_now(@post.created_at) %> ago by      <%= link_to @post.creator.name, user_url(:id => @post.creator),            {'class' =>'grey' } %>   </div>   <%= render :partial =>'form' %>   <div >     <p>       <strong>         Date: <%= @post.start_date.strftime "%a, %b %d" %>       </strong>     </p>     <%= simple_format(@post.body) if @post.body.any? %></div> </div> <%= render :partial => "comments/comments",        :comments => @post.comments %>

Projects is the last of the Posts subclasses. There's nothing happening here that you haven't seen already. Figure C-9 shows the form for adding a new project.

Figure 14-9. New project form


class ProjectsController < PostsController   private     def model_name;'Project'; end end

<% standard_form :post, @post do |f| %>   <%= f.text_field :name %>   <%= f.text_field :phone %>   <%= f.text_field :email %>   <%= f.text_area :body, :label => "Details" %>   <%= standard_submit %> <% end %>

<div >   <%= user_thumb post.creator %>   <div >     <h4><%= link_to post.name,                url_for(:action =>'show', :id => post) %></h4>     <p >       <span >         <%= link_to pluralize(post.comments_count,'comment'),                :action =>'show', :id => post %>       </span>       <%= time_ago_in_words post.updated_at %> ago     </p>     <%= simple_format post.body %>   </div>   <%= clear_div %> </div>

<div >   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >     <span>+ </span>     <%= link_to_function "New #{post_type}", "PostForm.toggle(  )",            :class =>'create' %>   </div>   <%= render :partial =>'form' %> </div> <div >   <%= render :partial => "post", :collection => @posts %> </div>

<div  >   <%= render :partial =>'form' %> </div>

<div  <% if @edit_on %><% end %>>   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >     <% if can_edit? @post %>       <span>&nbsp;&nbsp;</span>       <%= link_to_function "Edit", "PostForm.toggle(  )",              :class =>'create' %>     <% end %>   </div>   <div >     Posted <%= time_ago_in_words(@post.created_at) %> ago by      <%= link_to @post.creator.name, user_url(:id => @post.creator),            {'class' =>'grey' } %>   </div>   <%= render :partial =>'form' %>   <div >     <%= simple_format(@post.body) if @post.body.any? %>   </div> </div> <%= render :partial => "comments/comments",        :comments => @post.comments %>

Sessions handle login and logout. The new action displays the sign in form; the create action processes the form.

class SessionsController < ApplicationController   before_filter :create_first_user, :only => :new   skip_before_filter :require_login   filter_parameter_logging :password   def new     redirect_to home_url if logged_in?     @user = User.new   end      def create     if user = User.authenticate(params[:session][:email],                                  params[:session][:password])       reset_session       session[:user_id] = user.id       redirect_back_or_default home_url       flash[:notice] = "Signed in successfully"     else       flash[:warning] = "There was a problem signing you in.                           Please try again."       @user = User.new       render :action =>'new'     end   end   def destroy     reset_session     flash[:notice] = "You have been signed out."     redirect_to new_session_url   end      private        # Before filter that automatically creates a recordand signs      # in for the first user of the system     def create_first_user       return true unless User.count == 0       user = User.new :admin => 1       user.save_with_validation false       session[:user_id] = user.id       redirect_to home_url     end    end

Figure C-10 shows the sign-in form generated by new.rhtml. It posts to the create action, which does the processing.

Figure 14-10. Sign-in form


<% @page_title = "Sign In" %> <% form_for :session, @user, :url => sessions_url,       :html => { :class => "standard", :style => "width: 250px" },       :builder => LabelingFormBuilder do |f| %>   <fieldset>     <%= f.text_field :email %>     <%= f.password_field :password %>     <%= submit_tag'Sign in' %>   </fieldset> <% end %>

UsersController supports sign up (i.e., creation of a new user) and editing a user profile. The statuses action is key to the application's presence indicator. This action is invoked repeatedly by the application.rhtml layout. Whenever the action is invoked, we record that the user is online and render a partial that lists everyone else's status.

class UsersController < ApplicationController   before_filter :require_admin, :only => [ :new, :create ]   before_filter :find_user,      :only => [ :show, :status, :edit, :update, :destroy ]   before_filter :check_permissions,      :only => [ :edit, :update, :destroy ]   skip_before_filter :check_for_valid_user,      :only => [ :edit, :update ]   filter_parameter_logging :password      def index     @users = User.find :all     @page_title = "Users"     @user = User.new   end      def new     @page_title = "New User"     @user = User.new     @edit_on = true   end      def statuses     current_user.update_attributes :last_active => Time.now     render :partial =>'statuses'   end   def create     if @user = User.create(params[:user])       flash[:notice] ='User was successfully saved.'       redirect_to user_url(:id => @user)     else       render :action =>'index'     end   end   def show     if params[:format]=='jpg'       if @user.has_picture?         send_data @user.picture.content,            :filename    => "#{@user.id}.jpg",            :type        =>'image/jpeg',            :disposition =>'inline'       else         send_file RAILS_ROOT+'/public/images/default_user.jpg',            :filename    => "#{@user.id}.jpg",            :type        =>'image/jpeg',            :disposition =>'inline'       end       return     end   end      def status     render :text => @user.status   end      def edit     @edit_on = true     render :action =>'show'   end   def update     success = @user.update_attributes params[:user]     respond_to do |format|       format.html {         if success           flash[:notice] ='User was successfully updated.'           redirect_to user_url         else           @edit_on = true           render :action =>'show'         end       }       format.js {         render :text => @user.status.blank? ?                            "(none)" :                            @user.status       }     end   end   def destroy     @user.destroy     flash[:notice] = "User deleted."     redirect_to users_url   end   private        def post_type; "User"; end     helper_method :post_type     def find_user       @user = User.find params[:id]     end          def check_permissions       return false unless can_edit? @user     end end

The _form.rhtml partial lets you edit a profile, as shown in Figure C-11.

Figure 14-11. Editing a user's profile


<% standard_form :user, @user do |f| %>   <%= f.text_field :name %>   <%= f.password_field :password %>   <%= f.text_field :email %>   <%= f.text_field :phone %>   <%= f.text_field :address %>   <%= f.text_field :city %>   <%= f.text_field :state %>   <%= f.text_field :zip %>   <%= standard_submit "User", @user %> <% end %>

The _statuses.rhtml partial is rendered to application.rhtml's sidebar to show which users are online at any given time.

<%# This query is put here so that the partial    # can easily be included in any view %> <% users = User.find(:all, :conditions => ["id!=?",                                              current_user.id]) %> <% if users.any? %>   <ul >     <% users.each do |user| %>       <li <% if user.inactive? %><% end %>>         <%= link_to user.short_name, user_url(:id => user.id) %>         <span><%=h user.status %></span>       </li>     <% end %>   </ul> <% end %>

The index.rhtml view renders a list of users, shown in Figure C-12, and allows an administrator to add new users.

Figure 14-12. Users list


<div >   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <% if admin? %><div >     <span>+ </span>     <%= link_to_function "New #{post_type}", "PostForm.toggle(  )",            :class =>'create' %>   </div><% end %>   <%= render :partial =>'form' %> </div> <table  style="margin-top: 20px;">   <% for user in @users %>     <tr>       <td><strong>         <%= link_to user.name, user_url(:id => user) %>       </strong></td>       <td><%= user.email %></td>       <td><%= user.phone %></td>     </tr>   <% end %> </table>

This template renders the form for creating a new user, with the _form.rhtml partial.

<div  >   <%= render :partial =>'form' %> </div>

The show.rhtml template is used both to display a user's data, and to edit that data. If you're allowed to edit the user, you can switch to the form view and change the user's profile.

<div  <% if @edit_on %><% end %>>   <div >     <%= link_to_function "Cancel", "PostForm.toggle(  )",            :class =>'delete small' %>   </div>   <div >     <% if can_edit? @user %>       <span>&nbsp;&nbsp;</span>       <%= link_to_function "Edit", "PostForm.toggle(  )",              :class =>'create' %>     <% end %>   </div>   <%= render :partial =>'form' %>   <div >     <div >       <%= image_tag formatted_user_url(:id => @user,              :format =>'jpg'),              :class =>'user_picture',              :alt => @user.name %>       <p><%= link_to_function'Change Picture',                 "$('picture_form').toggle(  )" %></p>       <form  style="display: none;"              method="post" enctype="multipart/form-data"              action="<%= url_for(:action =>'show') %>">         <fieldset>           <input type="hidden" name="_method" value="put" />           <input type="file" name="user[file]" />            <input type="submit" value="Upload" />         </fieldset>       </form>     </div>     <p><strong>Email:</strong> <%= @user.email %></p>     <% unless @user.phone.blank? %>       <p><strong>Phone:</strong> <%= @user.phone %></p>     <% end %>     <p>       <%= @user.address %><br/>       <%= @user.city %>       <% unless @user.city.blank? %>, <% end %>       <%= @user.state %>        <%= @user.zip %>     </p>   </div> </div>

The PostForm class provides a visual effect when the user clicks an edit link: the form slides in over the content.

var PostForm = {   toggle: function(  ) {     var container = $('form_container');     var form = $$('#form_container form').first(  );     if(container.hasClassName('active')) {       form.visualEffect('blind_up', {          duration: 0.25,          afterFinish: function(  ){           container.removeClassName('active');         }       });     } else {       form.visualEffect('blind_down', {          duration: 0.5,          beforeStart: function(  ){           container.addClassName('active');         }       });     }   } }

The stylesheet is included for completeness.

/* Basics */ /* --------------------------------------------------------- */ */* */ {   color: inherit;   font: inherit;   margin: 0;   list-style: none;   padding: 0;   text-decoration: none; } body {     background-color: #fff;     background-repeat: repeat-y;     color: #333; } body, p, ol, ul, td {     font-family: verdana, arial, helvetica, sans-serif;     font-size:   11px;     line-height: 14px; } p { margin-bottom: 8px; } ul li { list-style-type: disc; } ul, ol { margin: .5em 0 .5em 2em; } ol li { list-style-type: decimal; } fieldset { border: none; } strong, b { font-weight: bold; } em { font-style: italic; } .strong { font-weight: bold; } .small { font-size: 10px; } #main {     float: left;     position: relative;     left: -2px;     top: 24px;     padding-right: 30px;     width: 575px;     padding-bottom: 50px; } #utility {     width: 170px;     padding: 45px 10px 20px 18px;     float: left;     height: 100%; } div.clear {     clear: both;     margin-top: 1px;     display: block; } /* Links */ /* ------------------------------------------------------------ */ a { color: #264764; text-decoration: underline; } a:visited { color: #264764; } a:hover { color: #fff; background-color: #264764;            text-decoration: none; } a.stealth { color: #000; text-decoration: none; } a:hover.stealth { background-color: #000; color: #fff; } a.subtle { color: #666; text-decoration: underline; } a:hover.subtle { background-color: #666; color: #fff; } a.delete { color: #c00; text-decoration: underline; } a:hover.delete { background-color: #c00; color: #fff; } a.create { color: #009900; text-decoration: underline; } a:hover.create { background-color: #009900; color: #fff; } /* Headers */ /* ---------------------------------------------------------- */ #header {     height: 92px;     background-color: #E0E6EF;     border-bottom: 1px solid #888; } #header h1 {     font-family: futura;     font-size: 30px;     float: left;     height: 92px;     width: 181px;     xbackground-image: url('/images/logo.gif');     xtext-indent: -1000px;     /* or */     height: 37px;     padding-top: 55px;     width: 136px;     padding-left: 45px;      } #header h1 a { text-decoration: none; } #header #account {     float: right;     text-align: right;     font-family: verdana;     font-size: 11px;     color: #333;     margin-right: 8px;     margin-top: 15px;     line-height: 14px; } #main h2 {     font-family: trebuchet ms;     font-size: 18px;     font-weight: normal;     color: #264764;     margin-bottom: 0px;     border-bottom: 1px solid #B8B8B8;     width: 569px;     padding-bottom: 8px;     clear: both; } h3 {     font-size: 12px;     font-weight: bold;     margin-top: 10px;     margin-bottom: 0;     background-color: #eee;     padding: 3px 0 3px 5px;     border-bottom: 1px solid #ddd; } h4 {     font-size: 11px;     font-weight: bold;     margin-top: 10px;     margin-bottom: 2px; } /* Warnings and notices */ /* ----------------------------------------------------------- */ .flash.notice {   background-color: #ffc;   padding: .5em;   border-top: 1px solid #dda;   border-bottom: 1px solid #dda;   margin: 0 30px 1.5em 0; } .flash.warning {   background-color: #c22;   padding: .5em;   border-top: 1px solid #600;   border-bottom: 1px solid #600;   margin: 0em 0 2em 0em;   color: #fff;   font-weight: bold; } .flash.warning a { color: #fff; } .flash.system_announcement {   padding: 5px;   background-color: #EFF3AB;   border-bottom: 1px solid #898989;   color: #444;   text-align: center;   height: 30px; } /* Navigation */ /* ------------------------------------------------------------- */ ul#nav { margin: 0; position: relative; left: 15px; top: 67px; } html>body ul#nav { top: 68px; } /* non-iewin */ ul#nav li {   display: inline;   height: 30px;   font-size: 12px;   line-height: 26px;   font-family: helvetica, arial;   margin-right: 5px;   padding: 3px 4px 5px 7px; } html>body ul#nav li { padding: 3px 7px 4px 7px; } /* non-iewin */ body.messages li#messages, body.plans li#plans,  body.documents li#documents, body.projects li#projects,  body.contacts li#contacts {     background-color: #fff; border: 1px solid #888;      border-bottom: 1px solid #fff; } ul#nav li a { text-decoration: none; color: #555; } ul#nav li a:hover { background-color: transparent;                      text-decoration: none; } ul#nav li:hover a { text-decoration: none; color: #000; } ul#nav li:hover { text-decoration: underline; } /* Statuses */ /* ------------------------------------------------------------ */ #status ul li {     list-style-type: none;     margin-bottom: 5px;     font-weight: bold; } #status ul li span {     font-weight: normal;     display: block;   font-style: italic; } #status ul { margin-left: 0; } #status ul li a { text-decoration: none; } #status ul li.inactive, #status ul li.inactive a { color: #777; } /* Post container */ /* ------------------------------------------------------------- */ #form_container {     padding: 6px 10px 15px 12px;     width: 545px;     margin-bottom: 0px; } #form_container.active {   background: #EEF8ED;   border-left: 1px solid #89B989;   border-right: 1px solid #89B989;   border-bottom: 1px solid #89B989; } #form_container #new_link {     float: left;     color: #009900;     font-weight: bold;     margin-left: -12px; } #form_container.active #new_link span { visibility: hidden; } #form_container #cancel_link { visibility: hidden; float: right; } #form_container.active #cancel_link { visibility: visible; } #form_container.active #new_link a { text-decoration: none; } #form_container #meta { float: left; margin-left: 10px; } #form_container #detail { clear: left; padding-top: 20px; } #form_container.active #detail { display: none; } /* Standard form */ /* ------------------------------------------------------------- */ form.standard {     clear: left;     margin-top: 10px;   margin-left: 15px;   padding-top: 10px;   width: 510px; } form.standard label {     font-weight: bold;     display: block;     margin-bottom: 3px;     font-size: 12px;     font-family: verdana; } form.standard input, form.standard textarea {     width: 100%;     display: block;     margin-bottom: 10px; } form.standard input#post_name, form.standard input#user_name {    font-size: 18px; font-weight: bold;  } form.standard .fieldWithErrors {    border-left: 4px solid #c00; padding-left: 3px;  } form.standard label span.error { color: #c00; } form.standard input[type='submit'] { width: 100px; display: inline; } form.standard textarea { height: 150px; } form.standard select { margin-bottom: 10px; } /* Body details */ /* ------------------------------------------------------------ */ .post_detail {   background-color: #eee;   border: 1px solid #ccc;   padding: 12px;   width: 508px; } #main div.post {     margin-top: 11px;     margin-bottom: 25px;     margin-left: 1px; } #main div.post div.user {     width: 60px;     float: left;     text-align: center;     margin-right: 10px;     margin-top: 4px;     font-size: 10px; } img.user_picture {     text-decoration: none;     background-color: #fff;     margin-bottom: -2px;     width: 60px;     height: 60px;     border: 1px solid #666;     padding: 2px; } #main div.post h3 {     font-weight: bold;     font-size: 11px;     padding-top: 2px;     padding-left: 1px; } #main div.post h3 span {     color: #666;     font-weight: normal;     margin-left: 8px; } #main div.post p.meta {     color: #666;     font-size: 10px;     margin-bottom: 5px; } #main img.icon {   float: left;   width: 32px;   height: 32px;   padding-right: 8px; } #main div.post h3 span a { color: #666; font-weight: normal; } #main div.post h3 span a:hover { background-color: #666;                                   color: #fff; } #main div.post div.body { margin-left: 70px; } #main div.post div.no_user { margin-left: 0px; } #main div.post p.meta span.comments {    float: right; font-size: 11px;  } #main p.meta { color: #666; margin-bottom: 10px; } #letter_links {     margin-top: 20px;     margin-bottom: 20px; } #letter_links a {     background-color: #ffa;     padding: 3px 4px;     margin: 0px 1px;     border: 1px solid #dd9;     text-decoration: none; } #letter_links a:hover, #letter_links a.active {     color: #000;     background-color: #dd9;     border: 1px solid #cc7; } /* Comments */ /* ------------------------------------------------------------- */ #comments {     background-color: #eee;     width: 500px;     margin: 3em 0 1em 0;     padding: 0 0 1em 0;     width: 100%; } #comments h2 {   background-color: #777;   width: 100%;   color: #fff;   font-size: 1em;   font-weight: bold;   padding: 3px 0px 3px 3px;   line-height: 1em;   border-bottom: 1px solid #555; } #comments form, #comments div.post {    margin: 1em 0 1em 1em; background-color: #eee;  } #comments form textarea { width: 90%; height: 80px; } #comments h3 { font-size: 1em; } #comments input { float: left; } #comments p img { margin-top: 1px; margin-left: 10px; } /* User list */ /* ----------------------------------------------------------- */ table#posts { width: 100%; } table#posts td { margin: 0; padding: 4px;                   border-bottom: 1px solid #ccc; } /* User#show */ /* ----------------------------------------------------------- */ body.user_show #main img {   float: left;   margin-right: 20px;   border: 1px solid #ccc;   padding: 3px;   width: 80px;   height: 80px;   margin-bottom: 100px; } #change_picture {     float: right;     width: 120px;     padding: 15px;     text-align: center;     border: 1px solid #ccc } body.user_show #main { width: 600px; } body.user_show #main h3 { clear: left; margin-bottom: 10px; } body.user_show #main ul#lookuplinks li {    display: inline; margin-right: 10px;  } #change_picture img { border: 1px solid black; } 




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

Similar book on Amazon

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