Upgraded to Rails 2.1. This can have wide ranging consequences, so please help track down any issues introduced by the upgrade. Requires environment.rb modifications.

Changes you will need to make:

 * In your environment.rb, you will need to update references to a few files per environment.rb.tmpl
 * In your environment.rb, you will need to specify the local time zone of the computer that is running your Tracks install.

Other notes on my changes:

 * Modified our code to take advantage of Rails 2.1's slick time zone support.
 * Upgraded will_paginate for compatibility
 * Hacked the Selenium on Rails plugin, which has not been updated in some time and does not support Rails 2.1
 * Verified that all tests pass on my machine, including Selenium tests -- I'd like confirmation from others, too.
This commit is contained in:
Luke Melia 2008-06-17 01:13:25 -04:00
parent f3bae73868
commit 901a58f8a3
1086 changed files with 51452 additions and 19526 deletions

View file

@ -2,7 +2,7 @@
# Likewise will all the methods added be available for all controllers. # Likewise will all the methods added be available for all controllers.
require_dependency "login_system" require_dependency "login_system"
require_dependency "source_view" require_dependency "tracks/source_view"
require "redcloth" require "redcloth"
require 'date' require 'date'
@ -20,6 +20,7 @@ class ApplicationController < ActionController::Base
layout proc{ |controller| controller.mobile? ? "mobile" : "standard" } layout proc{ |controller| controller.mobile? ? "mobile" : "standard" }
before_filter :set_session_expiration before_filter :set_session_expiration
before_filter :set_time_zone
prepend_before_filter :login_required prepend_before_filter :login_required
prepend_before_filter :enable_mobile_content_negotiation prepend_before_filter :enable_mobile_content_negotiation
after_filter :restore_content_type_for_mobile after_filter :restore_content_type_for_mobile
@ -178,6 +179,14 @@ class ApplicationController < ActionController::Base
raise ArgumentError.new("invalid value for Boolean: \"#{s}\"") raise ArgumentError.new("invalid value for Boolean: \"#{s}\"")
end end
def self.openid_enabled?
Tracks::Config.openid_enabled?
end
def openid_enabled?
self.class.openid_enabled?
end
private private
def parse_date_per_user_prefs( s ) def parse_date_per_user_prefs( s )
@ -213,4 +222,8 @@ class ApplicationController < ActionController::Base
logger.error("ERROR: #{message}") if type == :error logger.error("ERROR: #{message}") if type == :error
end end
def set_time_zone
Time.zone = current_user.prefs.time_zone if logged_in?
end
end end

View file

@ -6,11 +6,11 @@ class LoginController < ApplicationController
skip_before_filter :login_required skip_before_filter :login_required
before_filter :login_optional before_filter :login_optional
before_filter :get_current_user before_filter :get_current_user
open_id_consumer if Tracks::Config.openid_enabled? open_id_consumer if openid_enabled?
def login def login
@page_title = "TRACKS::Login" @page_title = "TRACKS::Login"
@openid_url = cookies[:openid_url] if Tracks::Config.openid_enabled? @openid_url = cookies[:openid_url] if openid_enabled?
case request.method case request.method
when :post when :post
if @user = User.authenticate(params['user_login'], params['user_password']) if @user = User.authenticate(params['user_login'], params['user_password'])

View file

@ -415,7 +415,7 @@ class StatsController < ApplicationController
@max=0 @max=0
@actions_creation_hour_array = Array.new(24) { |i| 0} @actions_creation_hour_array = Array.new(24) { |i| 0}
@actions_creation_hour.each do |r| @actions_creation_hour.each do |r|
hour = current_user.prefs.tz.adjust(r.created_at).hour hour = r.created_at.hour
@actions_creation_hour_array[hour] += 1 @actions_creation_hour_array[hour] += 1
end end
0.upto(23) { |i| @max = @actions_creation_hour_array[i] if @actions_creation_hour_array[i] > @max} 0.upto(23) { |i| @max = @actions_creation_hour_array[i] if @actions_creation_hour_array[i] > @max}
@ -423,7 +423,7 @@ class StatsController < ApplicationController
# convert to hash to be able to fill in non-existing days # convert to hash to be able to fill in non-existing days
@actions_completion_hour_array = Array.new(24) { |i| 0} @actions_completion_hour_array = Array.new(24) { |i| 0}
@actions_completion_hour.each do |r| @actions_completion_hour.each do |r|
hour = current_user.prefs.tz.adjust(r.completed_at).hour hour = r.completed_at.hour
@actions_completion_hour_array[hour] += 1 @actions_completion_hour_array[hour] += 1
end end
0.upto(23) { |i| @max = @actions_completion_hour_array[i] if @actions_completion_hour_array[i] > @max} 0.upto(23) { |i| @max = @actions_completion_hour_array[i] if @actions_completion_hour_array[i] > @max}
@ -446,7 +446,7 @@ class StatsController < ApplicationController
@max=0 @max=0
@actions_creation_hour_array = Array.new(24) { |i| 0} @actions_creation_hour_array = Array.new(24) { |i| 0}
@actions_creation_hour.each do |r| @actions_creation_hour.each do |r|
hour = current_user.prefs.tz.adjust(r.created_at).hour hour = r.created_at.hour
@actions_creation_hour_array[hour] += 1 @actions_creation_hour_array[hour] += 1
end end
0.upto(23) { |i| @max = @actions_creation_hour_array[i] if @actions_creation_hour_array[i] > @max} 0.upto(23) { |i| @max = @actions_creation_hour_array[i] if @actions_creation_hour_array[i] > @max}
@ -454,7 +454,7 @@ class StatsController < ApplicationController
# convert to hash to be able to fill in non-existing days # convert to hash to be able to fill in non-existing days
@actions_completion_hour_array = Array.new(24) { |i| 0} @actions_completion_hour_array = Array.new(24) { |i| 0}
@actions_completion_hour.each do |r| @actions_completion_hour.each do |r|
hour = current_user.prefs.tz.adjust(r.completed_at).hour hour = r.completed_at.hour
@actions_completion_hour_array[hour] += 1 @actions_completion_hour_array[hour] += 1
end end
0.upto(23) { |i| @max = @actions_completion_hour_array[i] if @actions_completion_hour_array[i] > @max} 0.upto(23) { |i| @max = @actions_completion_hour_array[i] if @actions_completion_hour_array[i] > @max}

View file

@ -267,9 +267,9 @@ class TodosController < ApplicationController
def completed def completed
@page_title = "TRACKS::Completed tasks" @page_title = "TRACKS::Completed tasks"
@done = current_user.completed_todos @done = current_user.completed_todos
@done_today = @done.completed_within current_user.time - 1.day @done_today = @done.completed_within Time.zone.now - 1.day
@done_this_week = @done.completed_within current_user.time - 1.week @done_this_week = @done.completed_within Time.zone.now - 1.week
@done_this_month = @done.completed_within current_user.time - 4.week @done_this_month = @done.completed_within Time.zone.now - 4.week
@count = @done_today.size + @done_this_week.size + @done_this_month.size @count = @done_today.size + @done_this_week.size + @done_this_month.size
end end
@ -277,7 +277,7 @@ class TodosController < ApplicationController
@page_title = "TRACKS::Archived completed tasks" @page_title = "TRACKS::Archived completed tasks"
@done = current_user.completed_todos @done = current_user.completed_todos
@count = @done.size @count = @done.size
@done_archive = @done.completed_more_than current_user.time - 28.days @done_archive = @done.completed_more_than Time.zone.now - 28.days
end end
def list_deferred def list_deferred
@ -392,7 +392,7 @@ class TodosController < ApplicationController
if params.key?('due') if params.key?('due')
due_within = params['due'].to_i due_within = params['due'].to_i
due_within_when = current_user.time + due_within.days due_within_when = Time.zone.now + due_within.days
condition_builder.add('todos.due <= ?', due_within_when) condition_builder.add('todos.due <= ?', due_within_when)
due_within_date_s = due_within_when.strftime("%Y-%m-%d") due_within_date_s = due_within_when.strftime("%Y-%m-%d")
@title << " due today" if (due_within == 0) @title << " due today" if (due_within == 0)
@ -402,7 +402,7 @@ class TodosController < ApplicationController
if params.key?('done') if params.key?('done')
done_in_last = params['done'].to_i done_in_last = params['done'].to_i
condition_builder.add('todos.completed_at >= ?', current_user.time - done_in_last.days) condition_builder.add('todos.completed_at >= ?', Time.zone.now - done_in_last.days)
@title << " actions completed" @title << " actions completed"
@description << " in the last #{done_in_last.to_s} days" @description << " in the last #{done_in_last.to_s} days"
end end

View file

@ -1,6 +1,6 @@
class UsersController < ApplicationController class UsersController < ApplicationController
if Tracks::Config.openid_enabled? if openid_enabled?
open_id_consumer open_id_consumer
before_filter :begin_open_id_auth, :only => :update_auth_type before_filter :begin_open_id_auth, :only => :update_auth_type
end end
@ -153,7 +153,7 @@ class UsersController < ApplicationController
end end
def update_auth_type def update_auth_type
if (params[:user][:auth_type] == 'open_id') && Tracks::Config.openid_enabled? if (params[:user][:auth_type] == 'open_id') && openid_enabled?
case open_id_response.status case open_id_response.status
when OpenID::SUCCESS when OpenID::SUCCESS
# The URL was a valid identity URL. Now we just need to send a redirect # The URL was a valid identity URL. Now we just need to send a redirect
@ -179,7 +179,7 @@ class UsersController < ApplicationController
end end
def complete def complete
return unless Tracks::Config.openid_enabled? return unless openid_enabled?
openid_url = session['openid_url'] openid_url = session['openid_url']
if openid_url.blank? if openid_url.blank?
notify :error, "expected an openid_url" notify :error, "expected an openid_url"

View file

@ -3,7 +3,7 @@
module ApplicationHelper module ApplicationHelper
def user_time def user_time
current_user.time Time.zone.now
end end
# Replicates the link_to method but also checks request.request_uri to find # Replicates the link_to method but also checks request.request_uri to find

View file

@ -1,8 +1,5 @@
class Preference < ActiveRecord::Base class Preference < ActiveRecord::Base
belongs_to :user belongs_to :user
composed_of :tz,
:class_name => 'TimeZone',
:mapping => %w(time_zone name)
def self.due_styles def self.due_styles
{ :due_in_n_days => 0, :due_on => 1} { :due_in_n_days => 0, :due_on => 1}

View file

@ -1,7 +1,7 @@
class Project < ActiveRecord::Base class Project < ActiveRecord::Base
has_many :todos, :dependent => :delete_all, :include => :context has_many :todos, :dependent => :delete_all, :include => :context
has_many :notes, :dependent => :delete_all, :order => "created_at DESC" has_many :notes, :dependent => :delete_all, :order => "created_at DESC"
belongs_to :default_context, :dependent => :nullify, :class_name => "Context", :foreign_key => "default_context_id" belongs_to :default_context, :class_name => "Context", :foreign_key => "default_context_id"
belongs_to :user belongs_to :user
validates_presence_of :name, :message => "project must have a name" validates_presence_of :name, :message => "project must have a name"

View file

@ -1,6 +1,6 @@
class Todo < ActiveRecord::Base class Todo < ActiveRecord::Base
belongs_to :context, :order => 'name' belongs_to :context
belongs_to :project belongs_to :project
belongs_to :user belongs_to :user

View file

@ -158,7 +158,7 @@ class User < ActiveRecord::Base
end end
def time def time
prefs.tz.adjust(Time.now.utc) Time.now.in_time_zone(prefs.time_zone)
end end
def date def date

View file

@ -7,7 +7,7 @@
<li>Last name: <span class="highlight"><%= current_user.last_name %></span></li> <li>Last name: <span class="highlight"><%= current_user.last_name %></span></li>
<li>Date format: <span class="highlight"><%= prefs.date_format %></span> Your current date: <%= format_date(user_time) %></li> <li>Date format: <span class="highlight"><%= prefs.date_format %></span> Your current date: <%= format_date(user_time) %></li>
<li>Title date format: <span class="highlight"><%= prefs.title_date_format %></span> Your current title date: <%= user_time.strftime(prefs.title_date_format) %></li> <li>Title date format: <span class="highlight"><%= prefs.title_date_format %></span> Your current title date: <%= user_time.strftime(prefs.title_date_format) %></li>
<li>Time zone: <span class="highlight"><%= prefs.tz %></span> Your current time: <%= user_time.strftime('%I:%M %p') %></li> <li>Time zone: <span class="highlight"><%= prefs.time_zone %></span> Your current time: <%= user_time.strftime('%I:%M %p') %></li>
<li>Week starts on: <span class="highlight"><%= Preference.day_number_to_name_map[prefs.week_starts] %></span></li> <li>Week starts on: <span class="highlight"><%= Preference.day_number_to_name_map[prefs.week_starts] %></span></li>
<li>Show the last <span class="highlight"><%= prefs.show_number_completed %></span> completed items</li> <li>Show the last <span class="highlight"><%= prefs.show_number_completed %></span> completed items</li>
<li>Show completed projects in sidebar: <span class="highlight"><%= prefs.show_completed_projects_in_sidebar %></span></li> <li>Show completed projects in sidebar: <span class="highlight"><%= prefs.show_completed_projects_in_sidebar %></span></li>

View file

@ -24,9 +24,8 @@ module Rails
File.exist?("#{RAILS_ROOT}/vendor/rails") File.exist?("#{RAILS_ROOT}/vendor/rails")
end end
# FIXME : Ruby 1.9
def preinitialize def preinitialize
load(preinitializer_path) if File.exists?(preinitializer_path) load(preinitializer_path) if File.exist?(preinitializer_path)
end end
def preinitializer_path def preinitializer_path
@ -44,6 +43,7 @@ module Rails
class VendorBoot < Boot class VendorBoot < Boot
def load_initializer def load_initializer
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
Rails::Initializer.run(:install_gem_spec_stubs)
end end
end end

View file

@ -48,6 +48,10 @@ Rails::Initializer.run do |config|
# Make Active Record use UTC-base instead of local time # Make Active Record use UTC-base instead of local time
config.active_record.default_timezone = :utc config.active_record.default_timezone = :utc
# You''ll probably want to change this to the time zone of the computer where Tracks is running
# run rake time:zones:local have Rails suggest time zone names on your system
config.time_zone = 'UTC'
# Use Active Record's schema dumper instead of SQL when creating the test database # Use Active Record's schema dumper instead of SQL when creating the test database
# (enables use of different database adapters for development and test environments) # (enables use of different database adapters for development and test environments)
config.active_record.schema_format = :ruby config.active_record.schema_format = :ruby
@ -66,18 +70,13 @@ end
# Include your application configuration below # Include your application configuration below
# Time zone setting. Set your local time zone here. #
# You should be able to find a list of time zones in /usr/share/zoneinfo
# e.g. if you are in the Eastern time zone of the US, set the value below.
# ENV['TZ'] = 'US/Eastern'
# Leave this alone or set it to one or more of ['database', 'ldap', 'open_id']. # Leave this alone or set it to one or more of ['database', 'ldap', 'open_id'].
# If you choose ldap, see the additional configuration options further down. # If you choose ldap, see the additional configuration options further down.
AUTHENTICATION_SCHEMES = ['database'] AUTHENTICATION_SCHEMES = ['database']
require 'name_part_finder' require 'name_part_finder'
require 'todo_list' require 'tracks/todo_list'
require 'config' require 'tracks/config'
require 'activerecord_base_tag_extensions' # Needed for tagging-specific extensions require 'activerecord_base_tag_extensions' # Needed for tagging-specific extensions
require 'digest/sha1' #Needed to support 'rake db:fixtures:load' on some ruby installs: http://dev.rousette.org.uk/ticket/557 require 'digest/sha1' #Needed to support 'rake db:fixtures:load' on some ruby installs: http://dev.rousette.org.uk/ticket/557
require 'prototype_helper_extensions' require 'prototype_helper_extensions'

View file

@ -26,6 +26,8 @@ config.action_controller.allow_forgery_protection = false
# config.pre_loaded_fixtures = false # config.pre_loaded_fixtures = false
SALT = "change-me" unless defined?( SALT ).nil? SALT = "change-me" unless defined?( SALT ).nil?
config.time_zone = 'UTC'
config.after_initialize do config.after_initialize do
require File.expand_path(File.dirname(__FILE__) + "/../../test/selenium_helper") require File.expand_path(File.dirname(__FILE__) + "/../../test/selenium_helper")
end end

View file

@ -58,6 +58,10 @@ ActionController::Routing::Routes.draw do |map|
map.preferences 'preferences', :controller => 'preferences', :action => 'index' map.preferences 'preferences', :controller => 'preferences', :action => 'index'
map.integrations 'integrations', :controller => 'integrations', :action => 'index' map.integrations 'integrations', :controller => 'integrations', :action => 'index'
if Rails.env == 'test'
map.connect '/selenium_helper/login', :controller => 'selenium_helper', :action => 'login'
end
# Install the default route as the lowest priority. # Install the default route as the lowest priority.
map.connect ':controller/:action/:id' map.connect ':controller/:action/:id'

View file

@ -1,5 +1,5 @@
# This file is auto-generated from the current state of the database. Instead of editing this file, # This file is auto-generated from the current state of the database. Instead of editing this file,
# please use the migrations feature of ActiveRecord to incrementally modify your database, and # please use the migrations feature of Active Record to incrementally modify your database, and
# then regenerate this schema definition. # then regenerate this schema definition.
# #
# Note that this schema.rb definition is the authoritative source for your database schema. If you need # Note that this schema.rb definition is the authoritative source for your database schema. If you need
@ -9,43 +9,43 @@
# #
# It's strongly recommended to check this file into your version control system. # It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 38) do ActiveRecord::Schema.define(:version => 20080617044632) do
create_table "contexts", :force => true do |t| create_table "contexts", :force => true do |t|
t.string "name", :null => false t.string "name", :default => "", :null => false
t.integer "position" t.integer "position", :limit => 11
t.boolean "hide", :default => false t.boolean "hide", :default => false
t.integer "user_id", :default => 1 t.integer "user_id", :limit => 11, :default => 1
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
end end
add_index "contexts", ["user_id", "name"], :name => "index_contexts_on_user_id_and_name"
add_index "contexts", ["user_id"], :name => "index_contexts_on_user_id" add_index "contexts", ["user_id"], :name => "index_contexts_on_user_id"
add_index "contexts", ["user_id", "name"], :name => "index_contexts_on_user_id_and_name"
create_table "notes", :force => true do |t| create_table "notes", :force => true do |t|
t.integer "user_id", :null => false t.integer "user_id", :limit => 11, :null => false
t.integer "project_id", :null => false t.integer "project_id", :limit => 11, :null => false
t.text "body" t.text "body"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
end end
add_index "notes", ["user_id"], :name => "index_notes_on_user_id"
add_index "notes", ["project_id"], :name => "index_notes_on_project_id" add_index "notes", ["project_id"], :name => "index_notes_on_project_id"
add_index "notes", ["user_id"], :name => "index_notes_on_user_id"
create_table "open_id_associations", :force => true do |t| create_table "open_id_associations", :force => true do |t|
t.binary "server_url" t.binary "server_url"
t.string "handle" t.string "handle"
t.binary "secret" t.binary "secret"
t.integer "issued" t.integer "issued", :limit => 11
t.integer "lifetime" t.integer "lifetime", :limit => 11
t.string "assoc_type" t.string "assoc_type"
end end
create_table "open_id_nonces", :force => true do |t| create_table "open_id_nonces", :force => true do |t|
t.string "nonce" t.string "nonce"
t.integer "created" t.integer "created", :limit => 11
end end
create_table "open_id_settings", :force => true do |t| create_table "open_id_settings", :force => true do |t|
@ -54,40 +54,40 @@ ActiveRecord::Schema.define(:version => 38) do
end end
create_table "preferences", :force => true do |t| create_table "preferences", :force => true do |t|
t.integer "user_id", :null => false t.integer "user_id", :limit => 11, :null => false
t.string "date_format", :limit => 40, :default => "%d/%m/%Y", :null => false t.string "date_format", :limit => 40, :default => "%d/%m/%Y", :null => false
t.integer "week_starts", :default => 0, :null => false t.integer "week_starts", :limit => 11, :default => 0, :null => false
t.integer "show_number_completed", :default => 5, :null => false t.integer "show_number_completed", :limit => 11, :default => 5, :null => false
t.integer "staleness_starts", :default => 7, :null => false t.integer "staleness_starts", :limit => 11, :default => 7, :null => false
t.boolean "show_completed_projects_in_sidebar", :default => true, :null => false t.boolean "show_completed_projects_in_sidebar", :default => true, :null => false
t.boolean "show_hidden_contexts_in_sidebar", :default => true, :null => false t.boolean "show_hidden_contexts_in_sidebar", :default => true, :null => false
t.integer "due_style", :default => 0, :null => false t.integer "due_style", :limit => 11, :default => 0, :null => false
t.string "admin_email", :default => "butshesagirl@rousette.org.uk", :null => false t.string "admin_email", :default => "butshesagirl@rousette.org.uk", :null => false
t.integer "refresh", :default => 0, :null => false t.integer "refresh", :limit => 11, :default => 0, :null => false
t.boolean "verbose_action_descriptors", :default => false, :null => false t.boolean "verbose_action_descriptors", :default => false, :null => false
t.boolean "show_hidden_projects_in_sidebar", :default => true, :null => false t.boolean "show_hidden_projects_in_sidebar", :default => true, :null => false
t.string "time_zone", :default => "London", :null => false t.string "time_zone", :default => "London", :null => false
t.boolean "show_project_on_todo_done", :default => false, :null => false t.boolean "show_project_on_todo_done", :default => false, :null => false
t.string "title_date_format", :default => "%A, %d %B %Y", :null => false t.string "title_date_format", :default => "%A, %d %B %Y", :null => false
t.integer "mobile_todos_per_page", :default => 6, :null => false t.integer "mobile_todos_per_page", :limit => 11, :default => 6, :null => false
end end
add_index "preferences", ["user_id"], :name => "index_preferences_on_user_id" add_index "preferences", ["user_id"], :name => "index_preferences_on_user_id"
create_table "projects", :force => true do |t| create_table "projects", :force => true do |t|
t.string "name", :null => false t.string "name", :default => "", :null => false
t.integer "position" t.integer "position", :limit => 11
t.integer "user_id", :default => 1 t.integer "user_id", :limit => 11, :default => 1
t.text "description" t.text "description"
t.string "state", :limit => 20, :default => "active", :null => false t.string "state", :limit => 20, :default => "active", :null => false
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "default_context_id" t.integer "default_context_id", :limit => 11
t.datetime "completed_at" t.datetime "completed_at"
end end
add_index "projects", ["user_id", "name"], :name => "index_projects_on_user_id_and_name"
add_index "projects", ["user_id"], :name => "index_projects_on_user_id" add_index "projects", ["user_id"], :name => "index_projects_on_user_id"
add_index "projects", ["user_id", "name"], :name => "index_projects_on_user_id_and_name"
create_table "sessions", :force => true do |t| create_table "sessions", :force => true do |t|
t.string "session_id" t.string "session_id"
@ -98,10 +98,10 @@ ActiveRecord::Schema.define(:version => 38) do
add_index "sessions", ["session_id"], :name => "index_sessions_on_session_id" add_index "sessions", ["session_id"], :name => "index_sessions_on_session_id"
create_table "taggings", :force => true do |t| create_table "taggings", :force => true do |t|
t.integer "taggable_id" t.integer "taggable_id", :limit => 11
t.integer "tag_id" t.integer "tag_id", :limit => 11
t.string "taggable_type" t.string "taggable_type"
t.integer "user_id" t.integer "user_id", :limit => 11
end end
add_index "taggings", ["tag_id", "taggable_id", "taggable_type"], :name => "index_taggings_on_tag_id_and_taggable_id_and_taggable_type" add_index "taggings", ["tag_id", "taggable_id", "taggable_type"], :name => "index_taggings_on_tag_id_and_taggable_id_and_taggable_type"
@ -115,27 +115,27 @@ ActiveRecord::Schema.define(:version => 38) do
add_index "tags", ["name"], :name => "index_tags_on_name" add_index "tags", ["name"], :name => "index_tags_on_name"
create_table "todos", :force => true do |t| create_table "todos", :force => true do |t|
t.integer "context_id", :null => false t.integer "context_id", :limit => 11, :null => false
t.integer "project_id" t.integer "project_id", :limit => 11
t.string "description", :null => false t.string "description", :default => "", :null => false
t.text "notes" t.text "notes"
t.datetime "created_at" t.datetime "created_at"
t.date "due" t.date "due"
t.datetime "completed_at" t.datetime "completed_at"
t.integer "user_id", :default => 1 t.integer "user_id", :limit => 11, :default => 1
t.date "show_from" t.date "show_from"
t.string "state", :limit => 20, :default => "immediate", :null => false t.string "state", :limit => 20, :default => "immediate", :null => false
end end
add_index "todos", ["user_id", "context_id"], :name => "index_todos_on_user_id_and_context_id"
add_index "todos", ["context_id"], :name => "index_todos_on_context_id"
add_index "todos", ["project_id"], :name => "index_todos_on_project_id"
add_index "todos", ["user_id", "project_id"], :name => "index_todos_on_user_id_and_project_id"
add_index "todos", ["user_id", "state"], :name => "index_todos_on_user_id_and_state" add_index "todos", ["user_id", "state"], :name => "index_todos_on_user_id_and_state"
add_index "todos", ["user_id", "project_id"], :name => "index_todos_on_user_id_and_project_id"
add_index "todos", ["project_id"], :name => "index_todos_on_project_id"
add_index "todos", ["context_id"], :name => "index_todos_on_context_id"
add_index "todos", ["user_id", "context_id"], :name => "index_todos_on_user_id_and_context_id"
create_table "users", :force => true do |t| create_table "users", :force => true do |t|
t.string "login", :limit => 80, :null => false t.string "login", :limit => 80, :default => "", :null => false
t.string "crypted_password", :limit => 40, :null => false t.string "crypted_password", :limit => 40
t.string "token" t.string "token"
t.boolean "is_admin", :default => false, :null => false t.boolean "is_admin", :default => false, :null => false
t.string "first_name" t.string "first_name"

View file

@ -1,4 +1,4 @@
// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan) // (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com) // (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
// Contributors: // Contributors:

View file

@ -1,4 +1,4 @@
// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) // (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
// //
// script.aculo.us is freely distributable under the terms of an MIT-style license. // script.aculo.us is freely distributable under the terms of an MIT-style license.

View file

@ -1,4 +1,4 @@
// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors: // Contributors:
// Justin Palmer (http://encytemedia.com/) // Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/) // Mark Pilgrim (http://diveintomark.org/)

3
script/dbconsole Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/dbconsole'

View file

@ -1,7 +1,7 @@
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
<% <%
def today def today
Time.now.utc.beginning_of_day.to_s(:db) Time.zone.now.beginning_of_day.to_s(:db)
end end
%> %>

View file

@ -2,23 +2,23 @@
<% <%
def today def today
Time.now.utc.beginning_of_day.to_s(:db) Time.zone.now.beginning_of_day.to_s(:db)
end end
def next_week def next_week
1.week.from_now.beginning_of_day.utc.to_s(:db) 1.week.from_now.beginning_of_day.to_s(:db)
end end
def last_week def last_week
1.week.ago.utc.beginning_of_day.to_s(:db) 1.week.ago.beginning_of_day.to_s(:db)
end end
def two_weeks_ago def two_weeks_ago
2.weeks.ago.utc.beginning_of_day.to_s(:db) 2.weeks.ago.beginning_of_day.to_s(:db)
end end
def two_weeks_hence def two_weeks_hence
2.weeks.from_now.utc.beginning_of_day.to_s(:db) 2.weeks.from_now.beginning_of_day.to_s(:db)
end end
%> %>

View file

@ -103,7 +103,7 @@ class ContextsControllerTest < TodoContainerControllerTestBase
assert_select 'p', /\d+&nbsp;actions. Context is (Active|Hidden)./ assert_select 'p', /\d+&nbsp;actions. Context is (Active|Hidden)./
end end
end end
assert_select 'published', /(#{contexts(:agenda).created_at.xmlschema}|#{contexts(:library).created_at.xmlschema})/ assert_select 'published', /(#{Regexp.escape(contexts(:agenda).created_at.xmlschema)}|#{Regexp.escape(contexts(:library).created_at.xmlschema)})/
end end
end end
end end

View file

@ -160,7 +160,7 @@ class ProjectsControllerTest < TodoContainerControllerTestBase
assert_select 'p', /\d+&nbsp;actions. Project is (active|hidden|completed)./ assert_select 'p', /\d+&nbsp;actions. Project is (active|hidden|completed)./
end end
end end
assert_select 'published', /(#{projects(:timemachine).updated_at.xmlschema}|#{projects(:moremoney).updated_at.xmlschema})/ assert_select 'published', /(#{Regexp.escape(projects(:timemachine).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/
end end
end end
end end

View file

@ -245,7 +245,7 @@ class TodosControllerTest < Test::Rails::TestCase
assert_xml_select 'entry', 10 do assert_xml_select 'entry', 10 do
assert_xml_select 'title', /.+/ assert_xml_select 'title', /.+/
assert_xml_select 'content[type="html"]', /.*/ assert_xml_select 'content[type="html"]', /.*/
assert_xml_select 'published', /(#{projects(:timemachine).updated_at.xmlschema}|#{projects(:moremoney).updated_at.xmlschema})/ assert_xml_select 'published', /(#{Regexp.escape(projects(:timemachine).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/
end end
end end
end end

View file

@ -12,8 +12,6 @@ class SeleniumHelperController < ActionController::Base
end end
end end
ActionController::Routing::Routes.add_route '/selenium_helper/login', :controller => 'selenium_helper', :action => 'login'
module SeleniumOnRails::TestBuilderActions module SeleniumOnRails::TestBuilderActions
def login options = {} def login options = {}
options = {options => nil} unless options.is_a? Hash options = {options => nil} unless options.is_a? Hash

View file

@ -12,7 +12,6 @@ class PreferenceTest < Test::Rails::TestCase
def test_time_zone def test_time_zone
assert_equal 'London', @admin_user.preference.time_zone assert_equal 'London', @admin_user.preference.time_zone
assert_equal @admin_user.preference.tz, TimeZone['London']
end end
def test_show_project_on_todo_done def test_show_project_on_todo_done

View file

@ -19,8 +19,8 @@ class TodoTest < Test::Rails::TestCase
assert_equal "Call Bill Gates to find out how much he makes per day", @not_completed1.description assert_equal "Call Bill Gates to find out how much he makes per day", @not_completed1.description
assert_nil @not_completed1.notes assert_nil @not_completed1.notes
assert @not_completed1.completed? == false assert @not_completed1.completed? == false
assert_equal 1.week.ago.utc.beginning_of_day.strftime("%Y-%m-%d %H:%M"), @not_completed1.created_at.strftime("%Y-%m-%d %H:%M") assert_equal 1.week.ago.beginning_of_day.strftime("%Y-%m-%d %H:%M"), @not_completed1.created_at.strftime("%Y-%m-%d %H:%M")
assert_equal 2.week.from_now.utc.beginning_of_day.strftime("%Y-%m-%d"), @not_completed1.due.strftime("%Y-%m-%d") assert_equal 2.week.from_now.beginning_of_day.strftime("%Y-%m-%d"), @not_completed1.due.strftime("%Y-%m-%d")
assert_nil @not_completed1.completed_at assert_nil @not_completed1.completed_at
assert_equal 1, @not_completed1.user_id assert_equal 1, @not_completed1.user_id
end end

View file

@ -10,10 +10,6 @@ class TodosHelperTest < Test::Rails::HelperTestCase
include ApplicationHelper include ApplicationHelper
include TodosHelper include TodosHelper
def user_time
Time.now
end
def format_date(date) def format_date(date)
if date if date
date_format = "%d/%m/%Y" date_format = "%d/%m/%Y"
@ -31,7 +27,7 @@ class TodosHelperTest < Test::Rails::HelperTestCase
end end
def test_show_date_today def test_show_date_today
date = Time.now.to_date date = Time.zone.now.to_date
html = show_date(date) html = show_date(date)
formatted_date = format_date(date) formatted_date = format_date(date)
assert_equal %Q{<a title="#{formatted_date}"><span class="amber">Show Today</span></a> }, html assert_equal %Q{<a title="#{formatted_date}"><span class="amber">Show Today</span></a> }, html

View file

@ -7,6 +7,8 @@ if envs.include? RAILS_ENV
require 'selenium_controller' require 'selenium_controller'
require File.dirname(__FILE__) + '/routes' require File.dirname(__FILE__) + '/routes'
SeleniumController.prepend_view_path File.expand_path(File.dirname(__FILE__) + '/lib/views')
else else
#erase all traces #erase all traces
$LOAD_PATH.delete lib_path $LOAD_PATH.delete lib_path

View file

@ -13,13 +13,8 @@ module SeleniumOnRails
File.expand_path(File.dirname(__FILE__) + '/../views/' + view) File.expand_path(File.dirname(__FILE__) + '/../views/' + view)
end end
# Returns the path to the layout template. The path is relative in relation
# to the app/views/ directory since Rails doesn't support absolute paths
# to layout templates.
def layout_path def layout_path
rails_root = Pathname.new File.expand_path(File.join(RAILS_ROOT, 'app/views')) '/layout.rhtml'
view_path = Pathname.new view_path('layout')
view_path.relative_path_from(rails_root).to_s
end end
def fixtures_path def fixtures_path

View file

@ -8,7 +8,7 @@
# See SeleniumOnRails::TestBuilder for a list of available commands. # See SeleniumOnRails::TestBuilder for a list of available commands.
class SeleniumOnRails::RSelenese < SeleniumOnRails::TestBuilder class SeleniumOnRails::RSelenese < SeleniumOnRails::TestBuilder
end end
ActionView::Base.register_template_handler 'rsel', SeleniumOnRails::RSelenese ActionView::Template.register_template_handler 'rsel', SeleniumOnRails::RSelenese
class SeleniumOnRails::RSelenese < SeleniumOnRails::TestBuilder class SeleniumOnRails::RSelenese < SeleniumOnRails::TestBuilder
attr_accessor :view attr_accessor :view
@ -20,16 +20,16 @@ class SeleniumOnRails::RSelenese < SeleniumOnRails::TestBuilder
end end
# Render _template_ using _local_assigns_. # Render _template_ using _local_assigns_.
def render template, local_assigns def render template
title = (@view.assigns['page_title'] or local_assigns['page_title']) title = @view.assigns['page_title']
table(title) do table(title) do
test = self #to enable test.command test = self #to enable test.command
eval template.source
assign_locals_code = ''
local_assigns.each_key {|key| assign_locals_code << "#{key} = local_assigns[#{key.inspect}];"}
eval assign_locals_code + "\n" + template
end end
end end
def compilable?
false
end
end end

View file

@ -1,6 +1,6 @@
class SeleniumOnRails::Selenese class SeleniumOnRails::Selenese
end end
ActionView::Base.register_template_handler 'sel', SeleniumOnRails::Selenese ActionView::Template.register_template_handler 'sel', SeleniumOnRails::Selenese
class SeleniumOnRails::Selenese class SeleniumOnRails::Selenese
@ -8,8 +8,8 @@ class SeleniumOnRails::Selenese
@view = view @view = view
end end
def render template, local_assigns def render template
name = (@view.assigns['page_title'] or local_assigns['page_title']) name = @view.assigns['page_title']
lines = template.strip.split "\n" lines = template.strip.split "\n"
html = '' html = ''
html << extract_comments(lines) html << extract_comments(lines)
@ -19,6 +19,10 @@ class SeleniumOnRails::Selenese
html html
end end
def compilable?
false
end
private private
def next_line lines, expects def next_line lines, expects
while lines.any? while lines.any?

View file

@ -1,2 +1,4 @@
/pkg
/doc /doc
/rails
*.gem
/coverage

49
vendor/plugins/will_paginate/.manifest vendored Normal file
View file

@ -0,0 +1,49 @@
CHANGELOG
LICENSE
README.rdoc
Rakefile
examples
examples/apple-circle.gif
examples/index.haml
examples/index.html
examples/pagination.css
examples/pagination.sass
init.rb
lib
lib/will_paginate
lib/will_paginate.rb
lib/will_paginate/array.rb
lib/will_paginate/collection.rb
lib/will_paginate/core_ext.rb
lib/will_paginate/finder.rb
lib/will_paginate/named_scope.rb
lib/will_paginate/named_scope_patch.rb
lib/will_paginate/version.rb
lib/will_paginate/view_helpers.rb
test
test/boot.rb
test/collection_test.rb
test/console
test/database.yml
test/finder_test.rb
test/fixtures
test/fixtures/admin.rb
test/fixtures/developer.rb
test/fixtures/developers_projects.yml
test/fixtures/project.rb
test/fixtures/projects.yml
test/fixtures/replies.yml
test/fixtures/reply.rb
test/fixtures/schema.rb
test/fixtures/topic.rb
test/fixtures/topics.yml
test/fixtures/user.rb
test/fixtures/users.yml
test/helper.rb
test/lib
test/lib/activerecord_test_case.rb
test/lib/activerecord_test_connector.rb
test/lib/load_fixtures.rb
test/lib/view_test_process.rb
test/tasks.rake
test/view_test.rb

92
vendor/plugins/will_paginate/CHANGELOG vendored Normal file
View file

@ -0,0 +1,92 @@
== master
* ActiveRecord 2.1: remove :include from count query when tables are not
referenced in :conditions
== 2.3.2, released 2008-05-16
* Fixed LinkRenderer#stringified_merge by removing "return" from iterator block
* Ensure that 'href' values in pagination links are escaped URLs
== 2.3.1, released 2008-05-04
* Fixed page numbers not showing with custom routes and implicit first page
* Try to use Hanna for documentation (falls back to default RDoc template if not)
== 2.3.0, released 2008-04-29
* Changed LinkRenderer to receive collection, options and reference to view template NOT in
constructor, but with the #prepare method. This is a step towards supporting passing of
LinkRenderer (or subclass) instances that may be preconfigured in some way
* LinkRenderer now has #page_link and #page_span methods for easier customization of output in
subclasses
* Changed page_entries_info() method to adjust its output according to humanized class name of
collection items. Override this with :entry_name parameter (singular).
page_entries_info(@posts)
#-> "Displaying all 12 posts"
page_entries_info(@posts, :entry_name => 'item')
#-> "Displaying all 12 items"
== 2.2.3, released 2008-04-26
* will_paginate gem is no longer published on RubyForge, but on
gems.github.com:
gem sources -a http://gems.github.com/ (you only need to do this once)
gem install mislav-will_paginate
* extract reusable pagination testing stuff into WillPaginate::View
* rethink the page URL construction mechanizm to be more bulletproof when
combined with custom routing for page parameter
* test that anchor parameter can be used in pagination links
== 2.2.2, released 2008-04-21
* Add support for page parameter in custom routes like "/foo/page/2"
* Change output of "page_entries_info" on single-page collection and erraneous
output with empty collection as reported by Tim Chater
== 2.2.1, released 2008-04-08
* take less risky path when monkeypatching named_scope; fix that it no longer
requires ActiveRecord::VERSION
* use strings in "respond_to?" calls to work around a bug in acts_as_ferret
stable (ugh)
* add rake release task
== 2.2.0, released 2008-04-07
=== API changes
* Rename WillPaginate::Collection#page_count to "total_pages" for consistency.
If you implemented this interface, change your implementation accordingly.
* Remove old, deprecated style of calling Array#paginate as "paginate(page,
per_page)". If you want to specify :page, :per_page or :total_entries, use a
parameter hash.
* Rename LinkRenderer#url_options to "url_for" and drastically optimize it
=== View changes
* Added "prev_page" and "next_page" CSS classes on previous/next page buttons
* Add examples of pagination links styling in "examples/index.html"
* Change gap in pagination links from "..." to
"<span class="gap">&hellip;</span>".
* Add "paginated_section", a block helper that renders pagination both above and
below content in the block
* Add rel="prev|next|start" to page links
=== Other
* Add ability to opt-in for Rails 2.1 feature "named_scope" by calling
WillPaginate.enable_named_scope (tested in Rails 1.2.6 and 2.0.2)
* Support complex page parameters like "developers[page]"
* Move Array#paginate definition to will_paginate/array.rb. You can now easily
use pagination on arrays outside of Rails:
gem 'will_paginate'
require 'will_paginate/array'
* Add "paginated_each" method for iterating through every record by loading only
one page of records at the time
* Rails 2: Rescue from WillPaginate::InvalidPage error with 404 Not Found by
default

View file

@ -1,180 +0,0 @@
= WillPaginate
Pagination is just limiting the number of records displayed. Why should you let
it get in your way while developing, then? This plugin makes magic happen. Did
you ever want to be able to do just this on a model:
Post.paginate :page => 1, :order => 'created_at DESC'
... and then render the page links with a single view helper? Well, now you
can.
Ryan Bates made an awesome screencast[http://railscasts.com/episodes/51],
check it out.
Your mind reels with questions? Join our Google
group[http://groups.google.com/group/will_paginate].
== Installation
Will Paginate officially supports Rails versions 1.2.6 and 2.0.x.
Previously, the plugin was available on the following SVN location:
svn://errtheblog.com/svn/plugins/will_paginate
In February 2008, it moved to GitHub[http://github.com/mislav/will_paginate/tree]
to be tracked with git. The SVN repo continued to have updates, but not
forever. Therefore you should switch to using the gem:
gem install will_paginate --no-ri
After that, you can remove the plugin from your applications and add
a simple require to the end of config/environment.rb:
require 'will_paginate'
That's it, just remember to install the gem on all machines that
you are deploying to.
The second option is to download and extract the tarball from GitHub. Here is the
link for downloading the current state of the master branch:
http://github.com/mislav/will_paginate/tarball/master
Extract it to <tt>vendor/plugins</tt>. The directory will have a default name
like "mislav-will_paginate-master"; you can rename it to "will_paginate" for
simplicity.
== Example usage
Use a paginate finder in the controller:
@posts = Post.paginate_by_board_id @board.id, :page => params[:page], :order => 'updated_at DESC'
Yeah, +paginate+ works just like +find+ -- it just doesn't fetch all the
records. Don't forget to tell it which page you want, or it will complain!
Read more on WillPaginate::Finder::ClassMethods.
Render the posts in your view like you would normally do. When you need to render
pagination, just stick this in:
<%= will_paginate @posts %>
You're done. (Copy and paste the example fancy CSS styles from the bottom.) You
can find the option list at WillPaginate::ViewHelpers.
How does it know how much items to fetch per page? It asks your model by calling
its <tt>per_page</tt> class method. You can define it like this:
class Post < ActiveRecord::Base
cattr_reader :per_page
@@per_page = 50
end
... or like this:
class Post < ActiveRecord::Base
def self.per_page
50
end
end
... or don't worry about it at all. WillPaginate defines it to be <b>30</b> by default.
But you can always specify the count explicitly when calling +paginate+:
@posts = Post.paginate :page => params[:page], :per_page => 50
The +paginate+ finder wraps the original finder and returns your resultset that now has
some new properties. You can use the collection as you would with any ActiveRecord
resultset. WillPaginate view helpers also need that object to be able to render pagination:
<ol>
<% for post in @posts -%>
<li>Render `post` in some nice way.</li>
<% end -%>
</ol>
<p>Now let's render us some pagination!</p>
<%= will_paginate @posts %>
More detailed documentation:
* WillPaginate::Finder::ClassMethods for pagination on your models;
* WillPaginate::ViewHelpers for your views.
== Oh noes, a bug!
Tell us what happened so we can fix it, quick! Issues are filed on the Lighthouse project:
http://err.lighthouseapp.com/projects/466-plugins/tickets?q=tagged:will_paginate
Steps to make an awesome bug report:
1. Run <tt>rake test</tt> in the <i>will_paginate</i> directory. (You will need SQLite3.)
Copy the output if there are failing tests.
2. Register on Lighthouse to create a new ticket.
3. Write a descriptive, short title. Provide as much info as you can in the body.
Assign the ticket to Mislav and tag it with meaningful tags, <tt>"will_paginate"</tt>
being among them.
4. Yay! You will be notified on updates automatically.
Here is an example of a great bug report and patch:
http://err.lighthouseapp.com/projects/466/tickets/172-total_entries-ignored-in-paginate_by_sql
== Authors, credits, contact
Want to discuss, request features, ask questions? Join the Google group:
http://groups.google.com/group/will_paginate
Authors:: Mislav Marohnić, PJ Hyett
Original announcement:: http://errtheblog.com/post/929
Original PHP source:: http://www.strangerstudios.com/sandbox/pagination/diggstyle.php
All these people helped making will_paginate what it is now with their code
contributions or simply awesome ideas:
Chris Wanstrath, Dr. Nic Williams, K. Adam Christensen, Mike Garey, Bence
Golda, Matt Aimonetti, Charles Brian Quinn, Desi McAdam, James Coglan, Matijs
van Zuijlen, Maria, Brendan Ribera, Todd Willey, Bryan Helmkamp, Jan Berkel.
== Usable pagination in the UI
Copy the following CSS into your stylesheet for a good start:
.pagination {
padding: 3px;
margin: 3px;
}
.pagination a {
padding: 2px 5px 2px 5px;
margin: 2px;
border: 1px solid #aaaadd;
text-decoration: none;
color: #000099;
}
.pagination a:hover, .pagination a:active {
border: 1px solid #000099;
color: #000;
}
.pagination span.current {
padding: 2px 5px 2px 5px;
margin: 2px;
border: 1px solid #000099;
font-weight: bold;
background-color: #000099;
color: #FFF;
}
.pagination span.disabled {
padding: 2px 5px 2px 5px;
margin: 2px;
border: 1px solid #eee;
color: #ddd;
}
More reading about pagination as design pattern:
* Pagination 101:
http://kurafire.net/log/archive/2007/06/22/pagination-101
* Pagination gallery:
http://www.smashingmagazine.com/2007/11/16/pagination-gallery-examples-and-good-practices/
* Pagination on Yahoo Design Pattern Library:
http://developer.yahoo.com/ypatterns/parent.php?pattern=pagination

131
vendor/plugins/will_paginate/README.rdoc vendored Normal file
View file

@ -0,0 +1,131 @@
= WillPaginate
Pagination is just limiting the number of records displayed. Why should you let
it get in your way while developing, then? This plugin makes magic happen. Did
you ever want to be able to do just this on a model:
Post.paginate :page => 1, :order => 'created_at DESC'
... and then render the page links with a single view helper? Well, now you
can.
Some resources to get you started:
* Your mind reels with questions? Join our
{Google group}[http://groups.google.com/group/will_paginate].
* The will_paginate project page: http://github.com/mislav/will_paginate
* How to report bugs: http://github.com/mislav/will_paginate/wikis/report-bugs
* Ryan Bates made an awesome screencast[http://railscasts.com/episodes/51],
check it out.
== Installation
The recommended way is that you get the gem:
gem install mislav-will_paginate --source http://gems.github.com/
After that you don't need the will_paginate <i>plugin</i> in your Rails
application anymore. Just add a simple require to the end of
"config/environment.rb":
gem 'mislav-will_paginate', '~> 2.2'
require 'will_paginate'
That's it. Remember to install the gem on <b>all</b> machines that you are
deploying to.
<i>There are extensive
{installation instructions}[http://github.com/mislav/will_paginate/wikis/installation]
on {the wiki}[http://github.com/mislav/will_paginate/wikis].</i>
== Example usage
Use a paginate finder in the controller:
@posts = Post.paginate_by_board_id @board.id, :page => params[:page], :order => 'updated_at DESC'
Yeah, +paginate+ works just like +find+ -- it just doesn't fetch all the
records. Don't forget to tell it which page you want, or it will complain!
Read more on WillPaginate::Finder::ClassMethods.
Render the posts in your view like you would normally do. When you need to render
pagination, just stick this in:
<%= will_paginate @posts %>
You're done. (Copy and paste the example fancy CSS styles from the bottom.) You
can find the option list at WillPaginate::ViewHelpers.
How does it know how much items to fetch per page? It asks your model by calling
its <tt>per_page</tt> class method. You can define it like this:
class Post < ActiveRecord::Base
cattr_reader :per_page
@@per_page = 50
end
... or like this:
class Post < ActiveRecord::Base
def self.per_page
50
end
end
... or don't worry about it at all. WillPaginate defines it to be <b>30</b> by default.
But you can always specify the count explicitly when calling +paginate+:
@posts = Post.paginate :page => params[:page], :per_page => 50
The +paginate+ finder wraps the original finder and returns your resultset that now has
some new properties. You can use the collection as you would with any ActiveRecord
resultset. WillPaginate view helpers also need that object to be able to render pagination:
<ol>
<% for post in @posts -%>
<li>Render `post` in some nice way.</li>
<% end -%>
</ol>
<p>Now let's render us some pagination!</p>
<%= will_paginate @posts %>
More detailed documentation:
* WillPaginate::Finder::ClassMethods for pagination on your models;
* WillPaginate::ViewHelpers for your views.
== Authors and credits
Authors:: Mislav Marohnić, PJ Hyett
Original announcement:: http://errtheblog.com/post/929
Original PHP source:: http://www.strangerstudios.com/sandbox/pagination/diggstyle.php
All these people helped making will_paginate what it is now with their code
contributions or just simply awesome ideas:
Chris Wanstrath, Dr. Nic Williams, K. Adam Christensen, Mike Garey, Bence
Golda, Matt Aimonetti, Charles Brian Quinn, Desi McAdam, James Coglan, Matijs
van Zuijlen, Maria, Brendan Ribera, Todd Willey, Bryan Helmkamp, Jan Berkel,
Lourens Naudé, Rick Olson, Russell Norris, Piotr Usewicz, Chris Eppstein.
== Usable pagination in the UI
There are some CSS styles to get you started in the "examples/" directory. They
are showcased in the <b>"examples/index.html"</b> file.
More reading about pagination as design pattern:
* Pagination 101:
http://kurafire.net/log/archive/2007/06/22/pagination-101
* Pagination gallery:
http://www.smashingmagazine.com/2007/11/16/pagination-gallery-examples-and-good-practices/
* Pagination on Yahoo Design Pattern Library:
http://developer.yahoo.com/ypatterns/parent.php?pattern=pagination
Want to discuss, request features, ask questions? Join the
{Google group}[http://groups.google.com/group/will_paginate].

View file

@ -1,65 +1,62 @@
require 'rake' require 'rubygems'
require 'rake/testtask' begin
require 'rake/rdoctask' hanna_dir = '/home/mislav/projects/hanna/lib'
$:.unshift hanna_dir if File.exists? hanna_dir
require 'hanna/rdoctask'
rescue LoadError
require 'rake'
require 'rake/rdoctask'
end
load 'test/tasks.rake'
desc 'Default: run unit tests.' desc 'Default: run unit tests.'
task :default => :test task :default => :test
desc 'Test the will_paginate plugin.'
Rake::TestTask.new(:test) do |t|
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
# I want to specify environment variables at call time
class EnvTestTask < Rake::TestTask
attr_accessor :env
def ruby(*args)
env.each { |key, value| ENV[key] = value } if env
super
env.keys.each { |key| ENV.delete key } if env
end
end
for configuration in %w( sqlite3 mysql postgres )
EnvTestTask.new("test_#{configuration}") do |t|
t.pattern = 'test/finder_test.rb'
t.verbose = true
t.env = { 'DB' => configuration }
end
end
task :test_databases => %w(test_mysql test_sqlite3 test_postgres)
desc %{Test everything on SQLite3, MySQL and PostgreSQL}
task :test_full => %w(test test_mysql test_postgres)
desc %{Test everything with Rails 1.2.x and 2.0.x gems}
task :test_all do
all = Rake::Task['test_full']
ENV['RAILS_VERSION'] = '~>1.2.6'
all.invoke
# reset the invoked flag
%w( test_full test test_mysql test_postgres ).each do |name|
Rake::Task[name].instance_variable_set '@already_invoked', false
end
# do it again
ENV['RAILS_VERSION'] = '~>2.0.2'
all.invoke
end
desc 'Generate RDoc documentation for the will_paginate plugin.' desc 'Generate RDoc documentation for the will_paginate plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc| Rake::RDocTask.new(:rdoc) do |rdoc|
files = ['README', 'LICENSE', 'lib/**/*.rb'] rdoc.rdoc_files.include('README.rdoc', 'LICENSE', 'CHANGELOG').
rdoc.rdoc_files.add(files) include('lib/**/*.rb').
rdoc.main = "README" # page to start on exclude('lib/will_paginate/named_scope*').
rdoc.title = "will_paginate" exclude('lib/will_paginate/array.rb').
exclude('lib/will_paginate/version.rb')
templates = %w[/Users/chris/ruby/projects/err/rock/template.rb /var/www/rock/template.rb] rdoc.main = "README.rdoc" # page to start on
rdoc.template = templates.find { |t| File.exists? t } rdoc.title = "will_paginate documentation"
rdoc.rdoc_dir = 'doc' # rdoc output folder rdoc.rdoc_dir = 'doc' # rdoc output folder
rdoc.options << '--inline-source' rdoc.options << '--inline-source' << '--charset=UTF-8'
rdoc.options << '--charset=UTF-8' rdoc.options << '--webcvs=http://github.com/mislav/will_paginate/tree/master/'
end
desc %{Update ".manifest" with the latest list of project filenames. Respect\
.gitignore by excluding everything that git ignores. Update `files` and\
`test_files` arrays in "*.gemspec" file if it's present.}
task :manifest do
list = Dir['**/*'].sort
spec_file = Dir['*.gemspec'].first
list -= [spec_file] if spec_file
File.read('.gitignore').each_line do |glob|
glob = glob.chomp.sub(/^\//, '')
list -= Dir[glob]
list -= Dir["#{glob}/**/*"] if File.directory?(glob) and !File.symlink?(glob)
puts "excluding #{glob}"
end
if spec_file
spec = File.read spec_file
spec.gsub! /^(\s* s.(test_)?files \s* = \s* )( \[ [^\]]* \] | %w\( [^)]* \) )/mx do
assignment = $1
bunch = $2 ? list.grep(/^test\//) : list
'%s%%w(%s)' % [assignment, bunch.join(' ')]
end
File.open(spec_file, 'w') {|f| f << spec }
end
File.open('.manifest', 'w') {|f| f << list.join("\n") }
end
task :examples do
%x(haml examples/index.haml examples/index.html)
%x(sass examples/pagination.sass examples/pagination.css)
end end

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

View file

@ -0,0 +1,69 @@
!!!
%html
%head
%title Samples of pagination styling for will_paginate
%link{ :rel => 'stylesheet', :type => 'text/css', :href => 'pagination.css' }
%style{ :type => 'text/css' }
:sass
html
:margin 0
:padding 0
:background #999
:font normal 76% "Lucida Grande", Verdana, Helvetica, sans-serif
body
:margin 2em
:padding 2em
:border 2px solid gray
:background white
:color #222
h1
:font-size 2em
:font-weight normal
:margin 0 0 1em 0
h2
:font-size 1.4em
:margin 1em 0 .5em 0
pre
:font-size 13px
:font-family Monaco, "DejaVu Sans Mono", "Bitstream Vera Mono", "Courier New", monospace
- pagination = '<span class="disabled prev_page">&laquo; Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">&hellip;</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next &raquo;</a>'
- pagination_no_page_links = '<span class="disabled prev_page">&laquo; Previous</span> <a href="./?page=2" rel="next" class="next_page">Next &raquo;</a>'
%body
%h1 Samples of pagination styling for will_paginate
%p
Find these styles in <b>"examples/pagination.css"</b> of <i>will_paginate</i> library.
There is a Sass version of it for all you sassy people.
%p
Read about good rules for pagination:
%a{ :href => 'http://kurafire.net/log/archive/2007/06/22/pagination-101' } Pagination 101
%p
%em Warning:
page links below don't lead anywhere (so don't click on them).
%h2 Unstyled pagination <span style="font-weight:normal">(<i>ewww!</i>)</span>
%div= pagination
%h2 Digg.com
.digg_pagination= pagination
%h2 Digg-style, no page links
.digg_pagination= pagination_no_page_links
%p Code that renders this:
%pre= '<code>%s</code>' % %[<%= will_paginate @posts, :page_links => false %>].gsub('<', '&lt;').gsub('>', '&gt;')
%h2 Digg-style, extra content
.digg_pagination
.page_info Displaying entries <b>1&nbsp;-&nbsp;6</b> of <b>180</b> in total
= pagination
%p Code that renders this:
%pre= '<code>%s</code>' % %[<div class="digg_pagination">\n <div clas="page_info">\n <%= page_entries_info @posts %>\n </div>\n <%= will_paginate @posts, :container => false %>\n</div>].gsub('<', '&lt;').gsub('>', '&gt;')
%h2 Apple.com store
.apple_pagination= pagination
%h2 Flickr.com
.flickr_pagination
= pagination
.page_info (118 photos)

View file

@ -0,0 +1,92 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
</html>
<head>
<title>Samples of pagination styling for will_paginate</title>
<link href='pagination.css' rel='stylesheet' type='text/css' />
<style type='text/css'>
html {
margin: 0;
padding: 0;
background: #999;
font: normal 76% "Lucida Grande", Verdana, Helvetica, sans-serif; }
body {
margin: 2em;
padding: 2em;
border: 2px solid gray;
background: white;
color: #222; }
h1 {
font-size: 2em;
font-weight: normal;
margin: 0 0 1em 0; }
h2 {
font-size: 1.4em;
margin: 1em 0 .5em 0; }
pre {
font-size: 13px;
font-family: Monaco, "DejaVu Sans Mono", "Bitstream Vera Mono", "Courier New", monospace; }
</style>
</head>
<body>
<h1>Samples of pagination styling for will_paginate</h1>
<p>
Find these styles in <b>"examples/pagination.css"</b> of <i>will_paginate</i> library.
There is a Sass version of it for all you sassy people.
</p>
<p>
Read about good rules for pagination:
<a href='http://kurafire.net/log/archive/2007/06/22/pagination-101'>Pagination 101</a>
</p>
<p>
<em>Warning:</em>
page links below don't lead anywhere (so don't click on them).
</p>
<h2>
Unstyled pagination <span style="font-weight:normal">(<i>ewww!</i>)</span>
</h2>
<div>
<span class="disabled prev_page">&laquo; Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">&hellip;</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next &raquo;</a>
</div>
<h2>Digg.com</h2>
<div class='digg_pagination'>
<span class="disabled prev_page">&laquo; Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">&hellip;</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next &raquo;</a>
</div>
<h2>Digg-style, no page links</h2>
<div class='digg_pagination'>
<span class="disabled prev_page">&laquo; Previous</span> <a href="./?page=2" rel="next" class="next_page">Next &raquo;</a>
</div>
<p>Code that renders this:</p>
<pre>
<code>&lt;%= will_paginate @posts, :page_links =&gt; false %&gt;</code>
</pre>
<h2>Digg-style, extra content</h2>
<div class='digg_pagination'>
<div class='page_info'>
Displaying entries <b>1&nbsp;-&nbsp;6</b> of <b>180</b> in total
</div>
<span class="disabled prev_page">&laquo; Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">&hellip;</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next &raquo;</a>
</div>
<p>Code that renders this:</p>
<pre>
<code>&lt;div class="digg_pagination"&gt;
&lt;div clas="page_info"&gt;
&lt;%= page_entries_info @posts %&gt;
&lt;/div&gt;
&lt;%= will_paginate @posts, :container =&gt; false %&gt;
&lt;/div&gt;</code>
</pre>
<h2>Apple.com store</h2>
<div class='apple_pagination'>
<span class="disabled prev_page">&laquo; Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">&hellip;</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next &raquo;</a>
</div>
<h2>Flickr.com</h2>
<div class='flickr_pagination'>
<span class="disabled prev_page">&laquo; Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">&hellip;</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next &raquo;</a>
<div class='page_info'>(118 photos)</div>
</div>
</body>

View file

@ -0,0 +1,90 @@
.digg_pagination {
background: white;
/* self-clearing method: */ }
.digg_pagination a, .digg_pagination span {
padding: .2em .5em;
display: block;
float: left;
margin-right: 1px; }
.digg_pagination span.disabled {
color: #999;
border: 1px solid #DDD; }
.digg_pagination span.current {
font-weight: bold;
background: #2E6AB1;
color: white;
border: 1px solid #2E6AB1; }
.digg_pagination a {
text-decoration: none;
color: #105CB6;
border: 1px solid #9AAFE5; }
.digg_pagination a:hover, .digg_pagination a:focus {
color: #003;
border-color: #003; }
.digg_pagination .page_info {
background: #2E6AB1;
color: white;
padding: .4em .6em;
width: 22em;
margin-bottom: .3em;
text-align: center; }
.digg_pagination .page_info b {
color: #003;
background: #6aa6ed;
padding: .1em .25em; }
.digg_pagination:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden; }
* html .digg_pagination {
height: 1%; }
*:first-child+html .digg_pagination {
overflow: hidden; }
.apple_pagination {
background: #F1F1F1;
border: 1px solid #E5E5E5;
text-align: center;
padding: 1em; }
.apple_pagination a, .apple_pagination span {
padding: .2em .3em; }
.apple_pagination span.disabled {
color: #AAA; }
.apple_pagination span.current {
font-weight: bold;
background: transparent url(apple-circle.gif) no-repeat 50% 50%; }
.apple_pagination a {
text-decoration: none;
color: black; }
.apple_pagination a:hover, .apple_pagination a:focus {
text-decoration: underline; }
.flickr_pagination {
text-align: center;
padding: .3em; }
.flickr_pagination a, .flickr_pagination span {
padding: .2em .5em; }
.flickr_pagination span.disabled {
color: #AAA; }
.flickr_pagination span.current {
font-weight: bold;
color: #FF0084; }
.flickr_pagination a {
border: 1px solid #DDDDDD;
color: #0063DC;
text-decoration: none; }
.flickr_pagination a:hover, .flickr_pagination a:focus {
border-color: #003366;
background: #0063DC;
color: white; }
.flickr_pagination .page_info {
color: #aaa;
padding-top: .8em; }
.flickr_pagination .prev_page, .flickr_pagination .next_page {
border-width: 2px; }
.flickr_pagination .prev_page {
margin-right: 1em; }
.flickr_pagination .next_page {
margin-left: 1em; }

View file

@ -0,0 +1,91 @@
.digg_pagination
:background white
a, span
:padding .2em .5em
:display block
:float left
:margin-right 1px
span.disabled
:color #999
:border 1px solid #DDD
span.current
:font-weight bold
:background #2E6AB1
:color white
:border 1px solid #2E6AB1
a
:text-decoration none
:color #105CB6
:border 1px solid #9AAFE5
&:hover, &:focus
:color #003
:border-color #003
.page_info
:background #2E6AB1
:color white
:padding .4em .6em
:width 22em
:margin-bottom .3em
:text-align center
b
:color #003
:background = #2E6AB1 + 60
:padding .1em .25em
/* self-clearing method:
&:after
:content "."
:display block
:height 0
:clear both
:visibility hidden
* html &
:height 1%
*:first-child+html &
:overflow hidden
.apple_pagination
:background #F1F1F1
:border 1px solid #E5E5E5
:text-align center
:padding 1em
a, span
:padding .2em .3em
span.disabled
:color #AAA
span.current
:font-weight bold
:background transparent url(apple-circle.gif) no-repeat 50% 50%
a
:text-decoration none
:color black
&:hover, &:focus
:text-decoration underline
.flickr_pagination
:text-align center
:padding .3em
a, span
:padding .2em .5em
span.disabled
:color #AAA
span.current
:font-weight bold
:color #FF0084
a
:border 1px solid #DDDDDD
:color #0063DC
:text-decoration none
&:hover, &:focus
:border-color #003366
:background #0063DC
:color white
.page_info
:color #aaa
:padding-top .8em
.prev_page, .next_page
:border-width 2px
.prev_page
:margin-right 1em
.next_page
:margin-left 1em

View file

@ -1,2 +1 @@
require 'will_paginate' require 'will_paginate'
WillPaginate.enable

View file

@ -21,7 +21,7 @@ module WillPaginate
require 'will_paginate/view_helpers' require 'will_paginate/view_helpers'
ActionView::Base.class_eval { include ViewHelpers } ActionView::Base.class_eval { include ViewHelpers }
if ActionController::Base.respond_to? :rescue_responses if defined?(ActionController::Base) and ActionController::Base.respond_to? :rescue_responses
ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found
end end
end end
@ -33,28 +33,45 @@ module WillPaginate
require 'will_paginate/finder' require 'will_paginate/finder'
ActiveRecord::Base.class_eval { include Finder } ActiveRecord::Base.class_eval { include Finder }
associations = ActiveRecord::Associations # support pagination on associations
collection = associations::AssociationCollection a = ActiveRecord::Associations
returning([ a::AssociationCollection ]) { |classes|
# to support paginating finders on associations, we have to mix in the # detect http://dev.rubyonrails.org/changeset/9230
# method_missing magic from WillPaginate::Finder::ClassMethods to AssociationProxy unless a::HasManyThroughAssociation.superclass == a::HasManyAssociation
# subclasses, but in a different way for Rails 1.2.x and 2.0 classes << a::HasManyThroughAssociation
(collection.instance_methods.include?(:create!) ? end
collection : collection.subclasses.map(&:constantize) }.each do |klass|
).push(associations::HasManyThroughAssociation).each do |klass|
klass.class_eval do klass.class_eval do
include Finder::ClassMethods include Finder::ClassMethods
alias_method_chain :method_missing, :paginate alias_method_chain :method_missing, :paginate
end end
end end
end end
# Enable named_scope, a feature of Rails 2.1, even if you have older Rails
# (tested on Rails 2.0.2 and 1.2.6).
#
# You can pass +false+ for +patch+ parameter to skip monkeypatching
# *associations*. Use this if you feel that <tt>named_scope</tt> broke
# has_many, has_many :through or has_and_belongs_to_many associations in
# your app. By passing +false+, you can still use <tt>named_scope</tt> in
# your models, but not through associations.
def enable_named_scope(patch = true)
return if defined? ActiveRecord::NamedScope
require 'will_paginate/named_scope'
require 'will_paginate/named_scope_patch' if patch
ActiveRecord::Base.class_eval do
include WillPaginate::NamedScope
end
end
end end
module Deprecation #:nodoc: module Deprecation #:nodoc:
extend ActiveSupport::Deprecation extend ActiveSupport::Deprecation
def self.warn(message, callstack = caller) def self.warn(message, callstack = caller)
message = 'WillPaginate: ' + message.strip.gsub(/ {3,}/, ' ') message = 'WillPaginate: ' + message.strip.gsub(/\s+/, ' ')
behavior.call(message, callstack) if behavior && !silenced? behavior.call(message, callstack) if behavior && !silenced?
end end
@ -63,3 +80,7 @@ module WillPaginate
end end
end end
end end
if defined?(Rails) and defined?(ActiveRecord) and defined?(ActionController)
WillPaginate.enable
end

View file

@ -0,0 +1,16 @@
require 'will_paginate/collection'
# http://www.desimcadam.com/archives/8
Array.class_eval do
def paginate(options = {})
raise ArgumentError, "parameter hash expected (got #{options.inspect})" unless Hash === options
WillPaginate::Collection.create(
options[:page] || 1,
options[:per_page] || 30,
options[:total_entries] || self.length
) { |pager|
pager.replace self[pager.offset, pager.per_page].to_a
}
end
end

View file

@ -1,11 +1,18 @@
require 'will_paginate'
module WillPaginate module WillPaginate
# = OMG, invalid page number! # = Invalid page number error
# This is an ArgumentError raised in case a page was requested that is either # This is an ArgumentError raised in case a page was requested that is either
# zero or negative number. You should decide how do deal with such errors in # zero or negative number. You should decide how do deal with such errors in
# the controller. # the controller.
# #
# If you're using Rails 2, then this error will automatically get handled like
# 404 Not Found. The hook is in "will_paginate.rb":
#
# ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found
#
# If you don't like this, use your preffered method of rescuing exceptions in
# public from your controllers to handle this differently. The +rescue_from+
# method is a nice addition to Rails 2.
#
# This error is *not* raised when a page further than the last page is # This error is *not* raised when a page further than the last page is
# requested. Use <tt>WillPaginate::Collection#out_of_bounds?</tt> method to # requested. Use <tt>WillPaginate::Collection#out_of_bounds?</tt> method to
# check for those cases and manually deal with them as you see fit. # check for those cases and manually deal with them as you see fit.
@ -15,26 +22,34 @@ module WillPaginate
end end
end end
# Arrays returned from paginating finds are, in fact, instances of this. # = The key to pagination
# You may think of WillPaginate::Collection as an ordinary array with some # Arrays returned from paginating finds are, in fact, instances of this little
# extra properties. Those properties are used by view helpers to generate # class. You may think of WillPaginate::Collection as an ordinary array with
# some extra properties. Those properties are used by view helpers to generate
# correct page links. # correct page links.
# #
# WillPaginate::Collection also assists in rolling out your own pagination # WillPaginate::Collection also assists in rolling out your own pagination
# solutions: see +create+. # solutions: see +create+.
# #
# If you are writing a library that provides a collection which you would like
# to conform to this API, you don't have to copy these methods over; simply
# make your plugin/gem dependant on the "will_paginate" gem:
#
# gem 'will_paginate'
# require 'will_paginate/collection'
#
# # now use WillPaginate::Collection directly or subclass it
class Collection < Array class Collection < Array
attr_reader :current_page, :per_page, :total_entries attr_reader :current_page, :per_page, :total_entries, :total_pages
# Arguments to this constructor are the current page number, per-page limit # Arguments to the constructor are the current page number, per-page limit
# and the total number of entries. The last argument is optional because it # and the total number of entries. The last argument is optional because it
# is best to do lazy counting; in other words, count *conditionally* after # is best to do lazy counting; in other words, count *conditionally* after
# populating the collection using the +replace+ method. # populating the collection using the +replace+ method.
#
def initialize(page, per_page, total = nil) def initialize(page, per_page, total = nil)
@current_page = page.to_i @current_page = page.to_i
raise InvalidPage.new(page, @current_page) if @current_page < 1 raise InvalidPage.new(page, @current_page) if @current_page < 1
@per_page = per_page.to_i @per_page = per_page.to_i
raise ArgumentError, "`per_page` setting cannot be less than 1 (#{@per_page} given)" if @per_page < 1 raise ArgumentError, "`per_page` setting cannot be less than 1 (#{@per_page} given)" if @per_page < 1
self.total_entries = total if total self.total_entries = total if total
@ -65,29 +80,25 @@ module WillPaginate
# end # end
# end # end
# #
# The Array#paginate API has since then changed, but this still serves as a
# fine example of WillPaginate::Collection usage.
def self.create(page, per_page, total = nil, &block) def self.create(page, per_page, total = nil, &block)
pager = new(page, per_page, total) pager = new(page, per_page, total)
yield pager yield pager
pager pager
end end
# The total number of pages.
def page_count
@total_pages
end
# Helper method that is true when someone tries to fetch a page with a # Helper method that is true when someone tries to fetch a page with a
# larger number than the last page. Can be used in combination with flashes # larger number than the last page. Can be used in combination with flashes
# and redirecting. # and redirecting.
def out_of_bounds? def out_of_bounds?
current_page > page_count current_page > total_pages
end end
# Current offset of the paginated collection. If we're on the first page, # Current offset of the paginated collection. If we're on the first page,
# it is always 0. If we're on the 2nd page and there are 30 entries per page, # it is always 0. If we're on the 2nd page and there are 30 entries per page,
# the offset is 30. This property is useful if you want to render ordinals # the offset is 30. This property is useful if you want to render ordinals
# besides your records: simply start with offset + 1. # besides your records: simply start with offset + 1.
#
def offset def offset
(current_page - 1) * per_page (current_page - 1) * per_page
end end
@ -99,7 +110,7 @@ module WillPaginate
# current_page + 1 or nil if there is no next page # current_page + 1 or nil if there is no next page
def next_page def next_page
current_page < page_count ? (current_page + 1) : nil current_page < total_pages ? (current_page + 1) : nil
end end
def total_entries=(number) def total_entries=(number)
@ -120,13 +131,15 @@ module WillPaginate
# +total_entries+ and set it to a proper value if it's +nil+. See the example # +total_entries+ and set it to a proper value if it's +nil+. See the example
# in +create+. # in +create+.
def replace(array) def replace(array)
returning super do result = super
# The collection is shorter then page limit? Rejoice, because
# then we know that we are on the last page! # The collection is shorter then page limit? Rejoice, because
if total_entries.nil? and length > 0 and length < per_page # then we know that we are on the last page!
self.total_entries = offset + length if total_entries.nil? and length < per_page and (current_page == 1 or length > 0)
end self.total_entries = offset + length
end end
result
end end
end end
end end

View file

@ -1,5 +1,5 @@
require 'will_paginate'
require 'set' require 'set'
require 'will_paginate/array'
unless Hash.instance_methods.include? 'except' unless Hash.instance_methods.include? 'except'
Hash.class_eval do Hash.class_eval do
@ -30,51 +30,3 @@ unless Hash.instance_methods.include? 'slice'
end end
end end
end end
unless Hash.instance_methods.include? 'rec_merge!'
Hash.class_eval do
# Same as Hash#merge!, but recursively merges sub-hashes
# (stolen from Haml)
def rec_merge!(other)
other.each do |key, other_value|
value = self[key]
if value.is_a?(Hash) and other_value.is_a?(Hash)
value.rec_merge! other_value
else
self[key] = other_value
end
end
self
end
end
end
require 'will_paginate/collection'
unless Array.instance_methods.include? 'paginate'
# http://www.desimcadam.com/archives/8
Array.class_eval do
def paginate(options_or_page = {}, per_page = nil)
if options_or_page.nil? or Fixnum === options_or_page
if defined? WillPaginate::Deprecation
WillPaginate::Deprecation.warn <<-DEPR
Array#paginate now conforms to the main, ActiveRecord::Base#paginate API. You should \
call it with a parameters hash (:page, :per_page). The old API (numbers as arguments) \
has been deprecated and is going to be unsupported in future versions of will_paginate.
DEPR
end
page = options_or_page
options = {}
else
options = options_or_page
page = options[:page]
raise ArgumentError, "wrong number of arguments (1 hash or 2 Fixnums expected)" if per_page
per_page = options[:per_page]
end
WillPaginate::Collection.create(page || 1, per_page || 30, options[:total_entries] || size) do |pager|
pager.replace self[pager.offset, pager.per_page].to_a
end
end
end
end

View file

@ -2,7 +2,7 @@ require 'will_paginate/core_ext'
module WillPaginate module WillPaginate
# A mixin for ActiveRecord::Base. Provides +per_page+ class method # A mixin for ActiveRecord::Base. Provides +per_page+ class method
# and makes +paginate+ finders possible with some method_missing magic. # and hooks things up to provide paginating finders.
# #
# Find out more in WillPaginate::Finder::ClassMethods # Find out more in WillPaginate::Finder::ClassMethods
# #
@ -18,9 +18,9 @@ module WillPaginate
# = Paginating finders for ActiveRecord models # = Paginating finders for ActiveRecord models
# #
# WillPaginate adds +paginate+ and +per_page+ methods to ActiveRecord::Base # WillPaginate adds +paginate+, +per_page+ and other methods to
# class methods and associations. It also hooks into +method_missing+ to # ActiveRecord::Base class methods and associations. It also hooks into
# intercept pagination calls to dynamic finders such as # +method_missing+ to intercept pagination calls to dynamic finders such as
# +paginate_by_user_id+ and translate them to ordinary finders # +paginate_by_user_id+ and translate them to ordinary finders
# (+find_all_by_user_id+ in this case). # (+find_all_by_user_id+ in this case).
# #
@ -86,6 +86,31 @@ module WillPaginate
end end
end end
# Iterates through all records by loading one page at a time. This is useful
# for migrations or any other use case where you don't want to load all the
# records in memory at once.
#
# It uses +paginate+ internally; therefore it accepts all of its options.
# You can specify a starting page with <tt>:page</tt> (default is 1). Default
# <tt>:order</tt> is <tt>"id"</tt>, override if necessary.
#
# See http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord where
# Jamis Buck describes this and also uses a more efficient way for MySQL.
def paginated_each(options = {}, &block)
options = { :order => 'id', :page => 1 }.merge options
options[:page] = options[:page].to_i
options[:total_entries] = 0 # skip the individual count queries
total = 0
begin
collection = paginate(options)
total += collection.each(&block).size
options[:page] += 1
end until collection.size < collection.per_page
total
end
# Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
# based on the params otherwise used by paginating finds: +page+ and # based on the params otherwise used by paginating finds: +page+ and
# +per_page+. # +per_page+.
@ -159,6 +184,7 @@ module WillPaginate
unless options[:select] and options[:select] =~ /^\s*DISTINCT\b/i unless options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
excludees << :select # only exclude the select param if it doesn't begin with DISTINCT excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
end end
# count expects (almost) the same options as find # count expects (almost) the same options as find
count_options = options.except *excludees count_options = options.except *excludees
@ -166,12 +192,19 @@ module WillPaginate
# this allows you to specify :select, :order, or anything else just for the count query # this allows you to specify :select, :order, or anything else just for the count query
count_options.update options[:count] if options[:count] count_options.update options[:count] if options[:count]
# we may be in a model or an association proxy
klass = (@owner and @reflection) ? @reflection.klass : self
# forget about includes if they are irrelevant (Rails 2.1)
if count_options[:include] and
klass.private_methods.include?('references_eager_loaded_tables?') and
!klass.send(:references_eager_loaded_tables?, count_options)
count_options.delete :include
end
# we may have to scope ... # we may have to scope ...
counter = Proc.new { count(count_options) } counter = Proc.new { count(count_options) }
# we may be in a model or an association proxy!
klass = (@owner and @reflection) ? @reflection.klass : self
count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with')) count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
# scope_out adds a 'with_finder' method which acts like with_scope, if it's present # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
# then execute the count with the scoping provided by the with_finder # then execute the count with the scoping provided by the with_finder

View file

@ -0,0 +1,132 @@
## stolen from: http://dev.rubyonrails.org/browser/trunk/activerecord/lib/active_record/named_scope.rb?rev=9084
module WillPaginate
# This is a feature backported from Rails 2.1 because of its usefullness not only with will_paginate,
# but in other aspects when managing complex conditions that you want to be reusable.
module NamedScope
# All subclasses of ActiveRecord::Base have two named_scopes:
# * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
# * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly:
#
# Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)
#
# These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
# intermediate values (scopes) around as first-class objects is convenient.
def self.included(base)
base.class_eval do
extend ClassMethods
named_scope :all
named_scope :scoped, lambda { |scope| scope }
end
end
module ClassMethods
def scopes #:nodoc:
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
end
# Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
# such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
#
# class Shirt < ActiveRecord::Base
# named_scope :red, :conditions => {:color => 'red'}
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
# end
#
# The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
#
# Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
# constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
# <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
# as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
#
# These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
#
# All scopes are available as class methods on the ActiveRecord descendent upon which the scopes were defined. But they are also available to
# <tt>has_many</tt> associations. If,
#
# class Person < ActiveRecord::Base
# has_many :shirts
# end
#
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
# only shirts.
#
# Named scopes can also be procedural.
#
# class Shirt < ActiveRecord::Base
# named_scope :colored, lambda { |color|
# { :conditions => { :color => color } }
# }
# end
#
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
#
# Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
#
# class Shirt < ActiveRecord::Base
# named_scope :red, :conditions => {:color => 'red'} do
# def dom_id
# 'red_shirts'
# end
# end
# end
#
def named_scope(name, options = {}, &block)
scopes[name] = lambda do |parent_scope, *args|
Scope.new(parent_scope, case options
when Hash
options
when Proc
options.call(*args)
end, &block)
end
(class << self; self end).instance_eval do
define_method name do |*args|
scopes[name].call(self, *args)
end
end
end
end
class Scope #:nodoc:
attr_reader :proxy_scope, :proxy_options
[].methods.each { |m| delegate m, :to => :proxy_found unless m =~ /(^__|^nil\?|^send|class|extend|find|count|sum|average|maximum|minimum|paginate)/ }
delegate :scopes, :with_scope, :to => :proxy_scope
def initialize(proxy_scope, options, &block)
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
extend Module.new(&block) if block_given?
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
end
def reload
load_found; self
end
protected
def proxy_found
@found || load_found
end
private
def method_missing(method, *args, &block)
if scopes.include?(method)
scopes[method].call(self, *args)
else
with_scope :find => proxy_options do
proxy_scope.send(method, *args, &block)
end
end
end
def load_found
@found = find(:all)
end
end
end
end

View file

@ -0,0 +1,39 @@
## based on http://dev.rubyonrails.org/changeset/9084
ActiveRecord::Associations::AssociationProxy.class_eval do
protected
def with_scope(*args, &block)
@reflection.klass.send :with_scope, *args, &block
end
end
[ ActiveRecord::Associations::AssociationCollection,
ActiveRecord::Associations::HasManyThroughAssociation ].each do |klass|
klass.class_eval do
protected
alias :method_missing_without_scopes :method_missing_without_paginate
def method_missing_without_paginate(method, *args, &block)
if @reflection.klass.scopes.include?(method)
@reflection.klass.scopes[method].call(self, *args, &block)
else
method_missing_without_scopes(method, *args, &block)
end
end
end
end
# Rails 1.2.6
ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do
protected
def method_missing(method, *args, &block)
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
super
elsif @reflection.klass.scopes.include?(method)
@reflection.klass.scopes[method].call(self, *args)
else
@reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
@reflection.klass.send(method, *args, &block)
end
end
end
end if ActiveRecord::Base.respond_to? :find_first

View file

@ -0,0 +1,9 @@
module WillPaginate
module VERSION
MAJOR = 2
MINOR = 3
TINY = 3
STRING = [MAJOR, MINOR, TINY].join('.')
end
end

View file

@ -49,12 +49,13 @@ module WillPaginate
# * <tt>:param_name</tt> -- parameter name for page number in URLs (default: <tt>:page</tt>) # * <tt>:param_name</tt> -- parameter name for page number in URLs (default: <tt>:page</tt>)
# * <tt>:params</tt> -- additional parameters when generating pagination links # * <tt>:params</tt> -- additional parameters when generating pagination links
# (eg. <tt>:controller => "foo", :action => nil</tt>) # (eg. <tt>:controller => "foo", :action => nil</tt>)
# * <tt>:renderer</tt> -- class name of the link renderer (default: WillPaginate::LinkRenderer) # * <tt>:renderer</tt> -- class name, class or instance of a link renderer (default:
# <tt>WillPaginate::LinkRenderer</tt>)
# * <tt>:page_links</tt> -- when false, only previous/next links are rendered (default: true) # * <tt>:page_links</tt> -- when false, only previous/next links are rendered (default: true)
# * <tt>:container</tt> -- toggles rendering of the DIV container for pagination links, set to # * <tt>:container</tt> -- toggles rendering of the DIV container for pagination links, set to
# false only when you are rendering your own pagination markup (default: true) # false only when you are rendering your own pagination markup (default: true)
# * <tt>:id</tt> -- HTML ID for the container (default: nil). Pass +true+ to have the ID automatically # * <tt>:id</tt> -- HTML ID for the container (default: nil). Pass +true+ to have the ID
# generated from the class name of objects in collection: for example, paginating # automatically generated from the class name of objects in collection: for example, paginating
# ArticleComment models would yield an ID of "article_comments_pagination". # ArticleComment models would yield an ID of "article_comments_pagination".
# #
# All options beside listed ones are passed as HTML attributes to the container # All options beside listed ones are passed as HTML attributes to the container
@ -85,29 +86,99 @@ module WillPaginate
collection_name = "@#{controller.controller_name}" collection_name = "@#{controller.controller_name}"
collection = instance_variable_get(collection_name) collection = instance_variable_get(collection_name)
raise ArgumentError, "The #{collection_name} variable appears to be empty. Did you " + raise ArgumentError, "The #{collection_name} variable appears to be empty. Did you " +
"forget to specify the collection object for will_paginate?" unless collection "forget to pass the collection object for will_paginate?" unless collection
end end
# early exit if there is nothing to render # early exit if there is nothing to render
return nil unless collection.page_count > 1 return nil unless WillPaginate::ViewHelpers.total_pages_for_collection(collection) > 1
options = options.symbolize_keys.reverse_merge WillPaginate::ViewHelpers.pagination_options options = options.symbolize_keys.reverse_merge WillPaginate::ViewHelpers.pagination_options
# create the renderer instance
renderer_class = options[:renderer].to_s.constantize # get the renderer instance
renderer = renderer_class.new collection, options, self renderer = case options[:renderer]
when String
options[:renderer].to_s.constantize.new
when Class
options[:renderer].new
else
options[:renderer]
end
# render HTML for pagination # render HTML for pagination
renderer.prepare collection, options, self
renderer.to_html renderer.to_html
end end
# Wrapper for rendering pagination links at both top and bottom of a block
# of content.
#
# <% paginated_section @posts do %>
# <ol id="posts">
# <% for post in @posts %>
# <li> ... </li>
# <% end %>
# </ol>
# <% end %>
#
# will result in:
#
# <div class="pagination"> ... </div>
# <ol id="posts">
# ...
# </ol>
# <div class="pagination"> ... </div>
#
# Arguments are passed to a <tt>will_paginate</tt> call, so the same options
# apply. Don't use the <tt>:id</tt> option; otherwise you'll finish with two
# blocks of pagination links sharing the same ID (which is invalid HTML).
def paginated_section(*args, &block)
pagination = will_paginate(*args).to_s
content = pagination + capture(&block) + pagination
concat content, block.binding
end
# Renders a helpful message with numbers of displayed vs. total entries. # Renders a helpful message with numbers of displayed vs. total entries.
# You can use this as a blueprint for your own, similar helpers. # You can use this as a blueprint for your own, similar helpers.
# #
# <%= page_entries_info @posts %> # <%= page_entries_info @posts %>
# #-> Displaying entries 6 - 10 of 26 in total # #-> Displaying posts 6 - 10 of 26 in total
def page_entries_info(collection) #
%{Displaying entries <b>%d&nbsp;-&nbsp;%d</b> of <b>%d</b> in total} % [ # By default, the message will use the humanized class name of objects
collection.offset + 1, # in collection: for instance, "project types" for ProjectType models.
collection.offset + collection.length, # Override this to your liking with the <tt>:entry_name</tt> parameter:
collection.total_entries #
] # <%= page_entries_info @posts, :entry_name => 'item' %>
# #-> Displaying items 6 - 10 of 26 in total
def page_entries_info(collection, options = {})
entry_name = options[:entry_name] ||
(collection.empty?? 'entry' : collection.first.class.name.underscore.sub('_', ' '))
if collection.total_pages < 2
case collection.size
when 0; "No #{entry_name.pluralize} found"
when 1; "Displaying <b>1</b> #{entry_name}"
else; "Displaying <b>all #{collection.size}</b> #{entry_name.pluralize}"
end
else
%{Displaying #{entry_name.pluralize} <b>%d&nbsp;-&nbsp;%d</b> of <b>%d</b> in total} % [
collection.offset + 1,
collection.offset + collection.length,
collection.total_entries
]
end
end
def self.total_pages_for_collection(collection) #:nodoc:
if collection.respond_to?('page_count') and !collection.respond_to?('total_pages')
WillPaginate::Deprecation.warn <<-MSG
You are using a paginated collection of class #{collection.class.name}
which conforms to the old API of WillPaginate::Collection by using
`page_count`, while the current method name is `total_pages`. Please
upgrade yours or 3rd-party code that provides the paginated collection.
MSG
class << collection
def total_pages; page_count; end
end
end
collection.total_pages
end end
end end
@ -115,22 +186,43 @@ module WillPaginate
# links. It is used by +will_paginate+ helper internally. # links. It is used by +will_paginate+ helper internally.
class LinkRenderer class LinkRenderer
def initialize(collection, options, template) # The gap in page links is represented by:
#
# <span class="gap">&hellip;</span>
attr_accessor :gap_marker
def initialize
@gap_marker = '<span class="gap">&hellip;</span>'
end
# * +collection+ is a WillPaginate::Collection instance or any other object
# that conforms to that API
# * +options+ are forwarded from +will_paginate+ view helper
# * +template+ is the reference to the template being rendered
def prepare(collection, options, template)
@collection = collection @collection = collection
@options = options @options = options
@template = template @template = template
# reset values in case we're re-using this instance
@total_pages = @param_name = @url_string = nil
end end
# Process it! This method returns the complete HTML string which contains
# pagination links. Feel free to subclass LinkRenderer and change this
# method as you see fit.
def to_html def to_html
links = @options[:page_links] ? windowed_links : [] links = @options[:page_links] ? windowed_links : []
# previous/next buttons # previous/next buttons
links.unshift page_link_or_span(@collection.previous_page, 'disabled', @options[:prev_label]) links.unshift page_link_or_span(@collection.previous_page, 'disabled prev_page', @options[:prev_label])
links.push page_link_or_span(@collection.next_page, 'disabled', @options[:next_label]) links.push page_link_or_span(@collection.next_page, 'disabled next_page', @options[:next_label])
html = links.join(@options[:separator]) html = links.join(@options[:separator])
@options[:container] ? @template.content_tag(:div, html, html_attributes) : html @options[:container] ? @template.content_tag(:div, html, html_attributes) : html
end end
# Returns the subset of +options+ this instance was initialized with that
# represent HTML attributes for the container element of pagination links.
def html_attributes def html_attributes
return @html_attributes if @html_attributes return @html_attributes if @html_attributes
@html_attributes = @options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class]) @html_attributes = @options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class])
@ -143,20 +235,21 @@ module WillPaginate
protected protected
def gap_marker; '...'; end # Collects link items for visible page numbers.
def windowed_links def windowed_links
prev = nil prev = nil
visible_page_numbers.inject [] do |links, n| visible_page_numbers.inject [] do |links, n|
# detect gaps: # detect gaps:
links << gap_marker if prev and n > prev + 1 links << gap_marker if prev and n > prev + 1
links << page_link_or_span(n) links << page_link_or_span(n, 'current')
prev = n prev = n
links links
end end
end end
# Calculates visible page numbers using the <tt>:inner_window</tt> and
# <tt>:outer_window</tt> options.
def visible_page_numbers def visible_page_numbers
inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i
window_from = current_page - inner_window window_from = current_page - inner_window
@ -166,9 +259,11 @@ module WillPaginate
if window_to > total_pages if window_to > total_pages
window_from -= window_to - total_pages window_from -= window_to - total_pages
window_to = total_pages window_to = total_pages
elsif window_from < 1 end
if window_from < 1
window_to += 1 - window_from window_to += 1 - window_from
window_from = 1 window_from = 1
window_to = total_pages if window_to > total_pages
end end
visible = (1..total_pages).to_a visible = (1..total_pages).to_a
@ -180,21 +275,63 @@ module WillPaginate
visible visible
end end
def page_link_or_span(page, span_class = 'current', text = nil) def page_link_or_span(page, span_class, text = nil)
text ||= page.to_s text ||= page.to_s
if page and page != current_page if page and page != current_page
@template.link_to text, url_options(page), :rel => rel_value(page) classnames = span_class && span_class.index(' ') && span_class.split(' ', 2).last
page_link page, text, :rel => rel_value(page), :class => classnames
else else
@template.content_tag :span, text, :class => span_class page_span page, text, :class => span_class
end end
end end
def url_options(page) def page_link(page, text, attributes = {})
options = { param_name => page } @template.link_to text, url_for(page), attributes
# page links should preserve GET parameters end
options = params.merge(options) if @template.request.get?
options.rec_merge!(@options[:params]) if @options[:params] def page_span(page, text, attributes = {})
return options @template.content_tag :span, text, attributes
end
# Returns URL params for +page_link_or_span+, taking the current GET params
# and <tt>:params</tt> option into account.
def url_for(page)
page_one = page == 1
unless @url_string and !page_one
@url_params = {}
# page links should preserve GET parameters
stringified_merge @url_params, @template.params if @template.request.get?
stringified_merge @url_params, @options[:params] if @options[:params]
if complex = param_name.index(/[^\w-]/)
page_param = (defined?(CGIMethods) ? CGIMethods : ActionController::AbstractRequest).
parse_query_parameters("#{param_name}=#{page}")
stringified_merge @url_params, page_param
else
@url_params[param_name] = page_one ? 1 : 2
end
url = @template.url_for(@url_params)
return url if page_one
if complex
@url_string = url.sub(%r!((?:\?|&amp;)#{CGI.escape param_name}=)#{page}!, '\1@')
return url
else
@url_string = url
@url_params[param_name] = 3
@template.url_for(@url_params).split(//).each_with_index do |char, i|
if char == '3' and url[i, 1] == '2'
@url_string[i] = '@'
break
end
end
end
end
# finally!
@url_string.sub '@', page.to_s
end end
private private
@ -212,15 +349,25 @@ module WillPaginate
end end
def total_pages def total_pages
@collection.page_count @total_pages ||= WillPaginate::ViewHelpers.total_pages_for_collection(@collection)
end end
def param_name def param_name
@param_name ||= @options[:param_name].to_sym @param_name ||= @options[:param_name].to_s
end end
def params # Recursively merge into target hash by using stringified keys from the other one
@params ||= @template.params.to_hash.symbolize_keys def stringified_merge(target, other)
other.each do |key, value|
key = key.to_s # this line is what it's all about!
existing = target[key]
if value.is_a?(Hash) and (existing.is_a?(Hash) or existing.nil?)
stringified_merge(existing || (target[key] = {}), value)
else
target[key] = value
end
end
end end
end end
end end

View file

@ -19,5 +19,3 @@ else
gem 'activerecord' gem 'activerecord'
end end
end end
$:.unshift "#{plugin_root}/lib"

View file

@ -1,5 +1,5 @@
require File.dirname(__FILE__) + '/helper' require 'helper'
require 'will_paginate/core_ext' require 'will_paginate/array'
class ArrayPaginationTest < Test::Unit::TestCase class ArrayPaginationTest < Test::Unit::TestCase
def test_simple def test_simple
@ -11,7 +11,8 @@ class ArrayPaginationTest < Test::Unit::TestCase
{ :page => 3, :per_page => 5, :expected => [] }, { :page => 3, :per_page => 5, :expected => [] },
]. ].
each do |conditions| each do |conditions|
assert_equal conditions[:expected], collection.paginate(conditions.slice(:page, :per_page)) expected = conditions.delete :expected
assert_equal expected, collection.paginate(conditions)
end end
end end
@ -22,14 +23,8 @@ class ArrayPaginationTest < Test::Unit::TestCase
end end
def test_deprecated_api def test_deprecated_api
assert_deprecated 'paginate API' do assert_raise(ArgumentError) { [].paginate(2) }
result = (1..50).to_a.paginate(2, 10) assert_raise(ArgumentError) { [].paginate(2, 10) }
assert_equal 2, result.current_page
assert_equal (11..20).to_a, result
assert_equal 50, result.total_entries
end
assert_deprecated { [].paginate nil }
end end
def test_total_entries_has_precedence def test_total_entries_has_precedence
@ -50,14 +45,28 @@ class ArrayPaginationTest < Test::Unit::TestCase
end end
assert_equal entries, collection assert_equal entries, collection
assert_respond_to_all collection, %w(page_count each offset size current_page per_page total_entries) assert_respond_to_all collection, %w(total_pages each offset size current_page per_page total_entries)
assert_kind_of Array, collection assert_kind_of Array, collection
assert_instance_of Array, collection.entries assert_instance_of Array, collection.entries
assert_equal 3, collection.offset assert_equal 3, collection.offset
assert_equal 4, collection.page_count assert_equal 4, collection.total_pages
assert !collection.out_of_bounds? assert !collection.out_of_bounds?
end end
def test_previous_next_pages
collection = create(1, 1, 3)
assert_nil collection.previous_page
assert_equal 2, collection.next_page
collection = create(2, 1, 3)
assert_equal 1, collection.previous_page
assert_equal 3, collection.next_page
collection = create(3, 1, 3)
assert_equal 2, collection.previous_page
assert_nil collection.next_page
end
def test_out_of_bounds def test_out_of_bounds
entries = create(2, 3, 2){} entries = create(2, 3, 2){}
assert entries.out_of_bounds? assert entries.out_of_bounds?
@ -90,13 +99,20 @@ class ArrayPaginationTest < Test::Unit::TestCase
pager.replace array(0) pager.replace array(0)
end end
assert_equal nil, entries.total_entries assert_equal nil, entries.total_entries
entries = create(1) do |pager|
# collection is empty and we're on page 1,
# so the whole thing must be empty, too
pager.replace array(0)
end
assert_equal 0, entries.total_entries
end end
def test_invalid_page def test_invalid_page
bad_input = [0, -1, nil, '', 'Schnitzel'] bad_inputs = [0, -1, nil, '', 'Schnitzel']
bad_input.each do |bad| bad_inputs.each do |bad|
assert_raise(WillPaginate::InvalidPage) { create(bad) } assert_raise(WillPaginate::InvalidPage) { create bad }
end end
end end
@ -104,6 +120,11 @@ class ArrayPaginationTest < Test::Unit::TestCase
assert_raise(ArgumentError) { create(1, -1) } assert_raise(ArgumentError) { create(1, -1) }
end end
def test_page_count_was_removed
assert_raise(NoMethodError) { create.page_count }
# It's `total_pages` now.
end
private private
def create(page = 2, limit = 5, total = nil, &block) def create(page = 2, limit = 5, total = nil, &block)
if block_given? if block_given?
@ -116,16 +137,4 @@ class ArrayPaginationTest < Test::Unit::TestCase
def array(size = 3) def array(size = 3)
Array.new(size) Array.new(size)
end end
def collect_deprecations
old_behavior = WillPaginate::Deprecation.behavior
deprecations = []
WillPaginate::Deprecation.behavior = Proc.new do |message, callstack|
deprecations << message
end
result = yield
[result, deprecations]
ensure
WillPaginate::Deprecation.behavior = old_behavior
end
end end

View file

@ -1,9 +1,8 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
libs = [] libs = []
dirname = File.dirname(__FILE__)
libs << 'irb/completion' libs << 'irb/completion'
libs << File.join(dirname, 'lib', 'load_fixtures') libs << File.join('lib', 'load_fixtures')
exec "#{irb}#{libs.map{ |l| " -r #{l}" }.join} --simple-prompt" exec "#{irb} -Ilib:test#{libs.map{ |l| " -r #{l}" }.join} --simple-prompt"

View file

@ -1,8 +1,9 @@
require File.dirname(__FILE__) + '/helper' require 'helper'
require File.dirname(__FILE__) + '/lib/activerecord_test_case' require 'lib/activerecord_test_case'
require 'will_paginate' require 'will_paginate'
WillPaginate.enable_activerecord WillPaginate.enable_activerecord
WillPaginate.enable_named_scope
class FinderTest < ActiveRecordTestCase class FinderTest < ActiveRecordTestCase
fixtures :topics, :replies, :users, :projects, :developers_projects fixtures :topics, :replies, :users, :projects, :developers_projects
@ -12,18 +13,18 @@ class FinderTest < ActiveRecordTestCase
end end
def test_simple_paginate def test_simple_paginate
entries = Topic.paginate :page => nil assert_queries(1) do
assert_equal 1, entries.current_page entries = Topic.paginate :page => nil
assert_nil entries.previous_page assert_equal 1, entries.current_page
assert_nil entries.next_page assert_equal 1, entries.total_pages
assert_equal 1, entries.page_count assert_equal 4, entries.size
assert_equal 4, entries.size end
entries = Topic.paginate :page => 2 assert_queries(2) do
assert_equal 2, entries.current_page entries = Topic.paginate :page => 2
assert_equal 1, entries.previous_page assert_equal 1, entries.total_pages
assert_equal 1, entries.page_count assert entries.empty?
assert entries.empty? end
end end
def test_parameter_api def test_parameter_api
@ -41,31 +42,31 @@ class FinderTest < ActiveRecordTestCase
def test_paginate_with_per_page def test_paginate_with_per_page
entries = Topic.paginate :page => 1, :per_page => 1 entries = Topic.paginate :page => 1, :per_page => 1
assert_equal 1, entries.size assert_equal 1, entries.size
assert_equal 4, entries.page_count assert_equal 4, entries.total_pages
# Developer class has explicit per_page at 10 # Developer class has explicit per_page at 10
entries = Developer.paginate :page => 1 entries = Developer.paginate :page => 1
assert_equal 10, entries.size assert_equal 10, entries.size
assert_equal 2, entries.page_count assert_equal 2, entries.total_pages
entries = Developer.paginate :page => 1, :per_page => 5 entries = Developer.paginate :page => 1, :per_page => 5
assert_equal 11, entries.total_entries assert_equal 11, entries.total_entries
assert_equal 5, entries.size assert_equal 5, entries.size
assert_equal 3, entries.page_count assert_equal 3, entries.total_pages
end end
def test_paginate_with_order def test_paginate_with_order
entries = Topic.paginate :page => 1, :order => 'created_at desc' entries = Topic.paginate :page => 1, :order => 'created_at desc'
expected = [topics(:futurama), topics(:harvey_birdman), topics(:rails), topics(:ar)].reverse expected = [topics(:futurama), topics(:harvey_birdman), topics(:rails), topics(:ar)].reverse
assert_equal expected, entries.to_a assert_equal expected, entries.to_a
assert_equal 1, entries.page_count assert_equal 1, entries.total_pages
end end
def test_paginate_with_conditions def test_paginate_with_conditions
entries = Topic.paginate :page => 1, :conditions => ["created_at > ?", 30.minutes.ago] entries = Topic.paginate :page => 1, :conditions => ["created_at > ?", 30.minutes.ago]
expected = [topics(:rails), topics(:ar)] expected = [topics(:rails), topics(:ar)]
assert_equal expected, entries.to_a assert_equal expected, entries.to_a
assert_equal 1, entries.page_count assert_equal 1, entries.total_pages
end end
def test_paginate_with_include_and_conditions def test_paginate_with_include_and_conditions
@ -85,11 +86,14 @@ class FinderTest < ActiveRecordTestCase
end end
def test_paginate_with_include_and_order def test_paginate_with_include_and_order
entries = Topic.paginate \ entries = nil
:page => 1, assert_queries(2) do
:include => :replies, entries = Topic.paginate \
:order => 'replies.created_at asc, topics.created_at asc', :page => 1,
:per_page => 10 :include => :replies,
:order => 'replies.created_at asc, topics.created_at asc',
:per_page => 10
end
expected = Topic.find :all, expected = Topic.find :all,
:include => 'replies', :include => 'replies',
@ -104,7 +108,7 @@ class FinderTest < ActiveRecordTestCase
entries, project = nil, projects(:active_record) entries, project = nil, projects(:active_record)
assert_nothing_raised "THIS IS A BUG in Rails 1.2.3 that was fixed in [7326]. " + assert_nothing_raised "THIS IS A BUG in Rails 1.2.3 that was fixed in [7326]. " +
"Please upgrade to the 1-2-stable branch or edge Rails." do "Please upgrade to a newer version of Rails." do
entries = project.topics.paginate \ entries = project.topics.paginate \
:page => 1, :page => 1,
:include => :replies, :include => :replies,
@ -125,10 +129,12 @@ class FinderTest < ActiveRecordTestCase
expected_name_ordered = [projects(:action_controller), projects(:active_record)] expected_name_ordered = [projects(:action_controller), projects(:active_record)]
expected_id_ordered = [projects(:active_record), projects(:action_controller)] expected_id_ordered = [projects(:active_record), projects(:action_controller)]
# with association-specified order assert_queries(2) do
entries = dhh.projects.paginate(:page => 1) # with association-specified order
assert_equal expected_name_ordered, entries entries = dhh.projects.paginate(:page => 1)
assert_equal 2, entries.total_entries assert_equal expected_name_ordered, entries
assert_equal 2, entries.total_entries
end
# with explicit order # with explicit order
entries = dhh.projects.paginate(:page => 1, :order => 'projects.id') entries = dhh.projects.paginate(:page => 1, :order => 'projects.id')
@ -148,29 +154,43 @@ class FinderTest < ActiveRecordTestCase
def test_paginate_association_extension def test_paginate_association_extension
project = Project.find(:first) project = Project.find(:first)
entries = project.replies.paginate_recent :page => 1
assert_equal [replies(:brave)], entries assert_queries(2) do
entries = project.replies.paginate_recent :page => 1
assert_equal [replies(:brave)], entries
end
end end
def test_paginate_with_joins def test_paginate_with_joins
entries = Developer.paginate :page => 1, entries = nil
:joins => 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id',
:conditions => 'project_id = 1'
assert_equal 2, entries.size
developer_names = entries.map { |d| d.name }
assert developer_names.include?('David')
assert developer_names.include?('Jamis')
expected = entries.to_a assert_queries(1) do
entries = Developer.paginate :page => 1, entries = Developer.paginate :page => 1,
:joins => 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id', :joins => 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id',
:conditions => 'project_id = 1', :count => { :select => "users.id" } :conditions => 'project_id = 1'
assert_equal expected, entries.to_a assert_equal 2, entries.size
developer_names = entries.map &:name
assert developer_names.include?('David')
assert developer_names.include?('Jamis')
end
assert_queries(1) do
expected = entries.to_a
entries = Developer.paginate :page => 1,
:joins => 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id',
:conditions => 'project_id = 1', :count => { :select => "users.id" }
assert_equal expected, entries.to_a
assert_equal 2, entries.total_entries
end
end end
def test_paginate_with_group def test_paginate_with_group
entries = Developer.paginate :page => 1, :per_page => 10, entries = nil
:group => 'salary', :select => 'salary', :order => 'salary' assert_queries(1) do
entries = Developer.paginate :page => 1, :per_page => 10,
:group => 'salary', :select => 'salary', :order => 'salary'
end
expected = [ users(:david), users(:jamis), users(:dev_10), users(:poor_jamis) ].map(&:salary).sort expected = [ users(:david), users(:jamis), users(:dev_10), users(:poor_jamis) ].map(&:salary).sort
assert_equal expected, entries.map(&:salary) assert_equal expected, entries.map(&:salary)
end end
@ -201,6 +221,56 @@ class FinderTest < ActiveRecordTestCase
assert_equal 2, entries.total_entries assert_equal 2, entries.total_entries
end end
## named_scope ##
def test_paginate_in_named_scope
entries = Developer.poor.paginate :page => 1, :per_page => 1
assert_equal 1, entries.size
assert_equal 2, entries.total_entries
end
def test_paginate_in_named_scope_on_habtm_association
project = projects(:active_record)
assert_queries(2) do
entries = project.developers.poor.paginate :page => 1, :per_page => 1
assert_equal 1, entries.size, 'one developer should be found'
assert_equal 1, entries.total_entries, 'only one developer should be found'
end
end
def test_paginate_in_named_scope_on_hmt_association
project = projects(:active_record)
expected = [replies(:brave)]
assert_queries(2) do
entries = project.replies.recent.paginate :page => 1, :per_page => 1
assert_equal expected, entries
assert_equal 1, entries.total_entries, 'only one reply should be found'
end
end
def test_paginate_in_named_scope_on_has_many_association
project = projects(:active_record)
expected = [topics(:ar)]
assert_queries(2) do
entries = project.topics.mentions_activerecord.paginate :page => 1, :per_page => 1
assert_equal expected, entries
assert_equal 1, entries.total_entries, 'only one topic should be found'
end
end
## misc ##
def test_count_and_total_entries_options_are_mutually_exclusive
e = assert_raise ArgumentError do
Developer.paginate :page => 1, :count => {}, :total_entries => 1
end
assert_match /exclusive/, e.to_s
end
def test_readonly def test_readonly
assert_nothing_raised { Developer.paginate :readonly => true, :page => 1 } assert_nothing_raised { Developer.paginate :readonly => true, :page => 1 }
end end
@ -216,13 +286,15 @@ class FinderTest < ActiveRecordTestCase
end end
# Is this Rails 2.0? Find out by testing find_all which was removed in [6998] # Is this Rails 2.0? Find out by testing find_all which was removed in [6998]
unless Developer.respond_to? :find_all unless ActiveRecord::Base.respond_to? :find_all
def test_paginate_array_of_ids def test_paginate_array_of_ids
# AR finders also accept arrays of IDs # AR finders also accept arrays of IDs
# (this was broken in Rails before [6912]) # (this was broken in Rails before [6912])
entries = Developer.paginate((1..8).to_a, :per_page => 3, :page => 2, :order => 'id') assert_queries(1) do
assert_equal (4..6).to_a, entries.map(&:id) entries = Developer.paginate((1..8).to_a, :per_page => 3, :page => 2, :order => 'id')
assert_equal 8, entries.total_entries assert_equal (4..6).to_a, entries.map(&:id)
assert_equal 8, entries.total_entries
end
end end
end end
@ -230,7 +302,7 @@ class FinderTest < ActiveRecordTestCase
def test_implicit_all_with_dynamic_finders def test_implicit_all_with_dynamic_finders
Topic.expects(:find_all_by_foo).returns([]) Topic.expects(:find_all_by_foo).returns([])
Topic.expects(:count).returns(0) Topic.expects(:count).returns(0)
Topic.paginate_by_foo :page => 1 Topic.paginate_by_foo :page => 2
end end
def test_guessing_the_total_count def test_guessing_the_total_count
@ -241,6 +313,14 @@ class FinderTest < ActiveRecordTestCase
assert_equal 6, entries.total_entries assert_equal 6, entries.total_entries
end end
def test_guessing_that_there_are_no_records
Topic.expects(:find).returns([])
Topic.expects(:count).never
entries = Topic.paginate :page => 1, :per_page => 4
assert_equal 0, entries.total_entries
end
def test_extra_parameters_stay_untouched def test_extra_parameters_stay_untouched
Topic.expects(:find).with(:all, {:foo => 'bar', :limit => 4, :offset => 0 }).returns(Array.new(5)) Topic.expects(:find).with(:all, {:foo => 'bar', :limit => 4, :offset => 0 }).returns(Array.new(5))
Topic.expects(:count).with({:foo => 'bar'}).returns(1) Topic.expects(:count).with({:foo => 'bar'}).returns(1)
@ -251,13 +331,13 @@ class FinderTest < ActiveRecordTestCase
def test_count_skips_select def test_count_skips_select
Developer.stubs(:find).returns([]) Developer.stubs(:find).returns([])
Developer.expects(:count).with({}).returns(0) Developer.expects(:count).with({}).returns(0)
Developer.paginate :select => 'salary', :page => 1 Developer.paginate :select => 'salary', :page => 2
end end
def test_count_select_when_distinct def test_count_select_when_distinct
Developer.stubs(:find).returns([]) Developer.stubs(:find).returns([])
Developer.expects(:count).with(:select => 'DISTINCT salary').returns(0) Developer.expects(:count).with(:select => 'DISTINCT salary').returns(0)
Developer.paginate :select => 'DISTINCT salary', :page => 1 Developer.paginate :select => 'DISTINCT salary', :page => 2
end end
def test_should_use_scoped_finders_if_present def test_should_use_scoped_finders_if_present
@ -288,16 +368,16 @@ class FinderTest < ActiveRecordTestCase
Developer.expects(:find_by_sql).returns([]) Developer.expects(:find_by_sql).returns([])
Developer.expects(:count_by_sql).with("SELECT COUNT(*) FROM (sql\n ) AS count_table").returns(0) Developer.expects(:count_by_sql).with("SELECT COUNT(*) FROM (sql\n ) AS count_table").returns(0)
entries = Developer.paginate_by_sql "sql\n ORDER\nby foo, bar, `baz` ASC", :page => 1 Developer.paginate_by_sql "sql\n ORDER\nby foo, bar, `baz` ASC", :page => 2
end end
# TODO: counts are still wrong # TODO: counts are still wrong
def test_ability_to_use_with_custom_finders def test_ability_to_use_with_custom_finders
# acts_as_taggable defines find_tagged_with(tag, options) # acts_as_taggable defines find_tagged_with(tag, options)
Topic.expects(:find_tagged_with).with('will_paginate', :offset => 0, :limit => 5).returns([]) Topic.expects(:find_tagged_with).with('will_paginate', :offset => 5, :limit => 5).returns([])
Topic.expects(:count).with({}).returns(0) Topic.expects(:count).with({}).returns(0)
Topic.paginate_tagged_with 'will_paginate', :page => 1, :per_page => 5 Topic.paginate_tagged_with 'will_paginate', :page => 2, :per_page => 5
end end
def test_array_argument_doesnt_eliminate_count def test_array_argument_doesnt_eliminate_count
@ -310,7 +390,6 @@ class FinderTest < ActiveRecordTestCase
def test_paginating_finder_doesnt_mangle_options def test_paginating_finder_doesnt_mangle_options
Developer.expects(:find).returns([]) Developer.expects(:find).returns([])
Developer.expects(:count).returns(0)
options = { :page => 1 } options = { :page => 1 }
options.expects(:delete).never options.expects(:delete).never
options_before = options.dup options_before = options.dup
@ -318,5 +397,38 @@ class FinderTest < ActiveRecordTestCase
Developer.paginate(options) Developer.paginate(options)
assert_equal options, options_before assert_equal options, options_before
end end
def test_paginated_each
collection = stub('collection', :size => 5, :empty? => false, :per_page => 5)
collection.expects(:each).times(2).returns(collection)
last_collection = stub('collection', :size => 4, :empty? => false, :per_page => 5)
last_collection.expects(:each).returns(last_collection)
params = { :order => 'id', :total_entries => 0 }
Developer.expects(:paginate).with(params.merge(:page => 2)).returns(collection)
Developer.expects(:paginate).with(params.merge(:page => 3)).returns(collection)
Developer.expects(:paginate).with(params.merge(:page => 4)).returns(last_collection)
assert_equal 14, Developer.paginated_each(:page => '2') { }
end
# detect ActiveRecord 2.1
if ActiveRecord::Base.private_methods.include?('references_eager_loaded_tables?')
def test_removes_irrelevant_includes_in_count
Developer.expects(:find).returns([1])
Developer.expects(:count).with({}).returns(0)
Developer.paginate :page => 1, :per_page => 1, :include => :projects
end
def test_doesnt_remove_referenced_includes_in_count
Developer.expects(:find).returns([1])
Developer.expects(:count).with({ :include => :projects, :conditions => 'projects.id > 2' }).returns(0)
Developer.paginate :page => 1, :per_page => 1,
:include => :projects, :conditions => 'projects.id > 2'
end
end
end end
end end

View file

@ -7,5 +7,7 @@ class Developer < User
end end
end end
named_scope :poor, :conditions => ['salary <= ?', 80000], :order => 'salary'
def self.per_page() 10 end def self.per_page() 10 end
end end

View file

@ -1,7 +1,6 @@
action_controller:
id: 2
name: Active Controller
active_record: active_record:
id: 1 id: 1
name: Active Record name: Active Record
action_controller:
id: 2
name: Active Controller

View file

@ -1,5 +1,7 @@
class Reply < ActiveRecord::Base class Reply < ActiveRecord::Base
belongs_to :topic, :include => [:replies] belongs_to :topic, :include => [:replies]
named_scope :recent, :conditions => ['replies.created_at > ?', 15.minutes.ago]
validates_presence_of :content validates_presence_of :content
end end

View file

@ -1,4 +1,6 @@
class Topic < ActiveRecord::Base class Topic < ActiveRecord::Base
has_many :replies, :dependent => :destroy, :order => 'replies.created_at DESC' has_many :replies, :dependent => :destroy, :order => 'replies.created_at DESC'
belongs_to :project belongs_to :project
named_scope :mentions_activerecord, :conditions => ['topics.title LIKE ?', '%ActiveRecord%']
end end

View file

@ -4,7 +4,7 @@ require 'rubygems'
# gem install redgreen for colored test output # gem install redgreen for colored test output
begin require 'redgreen'; rescue LoadError; end begin require 'redgreen'; rescue LoadError; end
require File.join(File.dirname(__FILE__), 'boot') unless defined?(ActiveRecord) require 'boot' unless defined?(ActiveRecord)
class Test::Unit::TestCase class Test::Unit::TestCase
protected protected
@ -13,6 +13,18 @@ class Test::Unit::TestCase
[method.to_s, method.to_sym].each { |m| assert_respond_to object, m } [method.to_s, method.to_sym].each { |m| assert_respond_to object, m }
end end
end end
def collect_deprecations
old_behavior = WillPaginate::Deprecation.behavior
deprecations = []
WillPaginate::Deprecation.behavior = Proc.new do |message, callstack|
deprecations << message
end
result = yield
[result, deprecations]
ensure
WillPaginate::Deprecation.behavior = old_behavior
end
end end
# Wrap tests that use Mocha and skip if unavailable. # Wrap tests that use Mocha and skip if unavailable.

View file

@ -1,4 +1,4 @@
require File.join(File.dirname(__FILE__), 'activerecord_test_connector') require 'lib/activerecord_test_connector'
class ActiveRecordTestCase < Test::Unit::TestCase class ActiveRecordTestCase < Test::Unit::TestCase
# Set our fixture path # Set our fixture path
@ -18,6 +18,19 @@ class ActiveRecordTestCase < Test::Unit::TestCase
# Default so Test::Unit::TestCase doesn't complain # Default so Test::Unit::TestCase doesn't complain
def test_truth def test_truth
end end
protected
def assert_queries(num = 1)
$query_count = 0
yield
ensure
assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
end
def assert_no_queries(&block)
assert_queries(0, &block)
end
end end
ActiveRecordTestConnector.setup ActiveRecordTestConnector.setup

View file

@ -6,6 +6,8 @@ class ActiveRecordTestConnector
cattr_accessor :able_to_connect cattr_accessor :able_to_connect
cattr_accessor :connected cattr_accessor :connected
FIXTURES_PATH = File.join(File.dirname(__FILE__), '..', 'fixtures')
# Set our defaults # Set our defaults
self.connected = false self.connected = false
self.able_to_connect = true self.able_to_connect = true
@ -14,13 +16,12 @@ class ActiveRecordTestConnector
unless self.connected || !self.able_to_connect unless self.connected || !self.able_to_connect
setup_connection setup_connection
load_schema load_schema
# require_fixture_models Dependencies.load_paths.unshift FIXTURES_PATH
Dependencies.load_paths.unshift(File.dirname(__FILE__) + "/../fixtures")
self.connected = true self.connected = true
end end
rescue Exception => e # errors from ActiveRecord setup rescue Exception => e # errors from ActiveRecord setup
$stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" $stderr.puts "\nSkipping ActiveRecord tests: #{e}"
#$stderr.puts " #{e.backtrace.join("\n ")}\n" $stderr.puts "Install SQLite3 to run the full test suite for will_paginate.\n\n"
self.able_to_connect = false self.able_to_connect = false
end end
@ -38,7 +39,7 @@ class ActiveRecordTestConnector
ActiveRecord::Base.establish_connection(configuration) ActiveRecord::Base.establish_connection(configuration)
ActiveRecord::Base.configurations = { db => configuration } ActiveRecord::Base.configurations = { db => configuration }
ActiveRecord::Base.connection prepare ActiveRecord::Base.connection
unless Object.const_defined?(:QUOTED_TYPE) unless Object.const_defined?(:QUOTED_TYPE)
Object.send :const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type') Object.send :const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')
@ -48,13 +49,21 @@ class ActiveRecordTestConnector
def self.load_schema def self.load_schema
ActiveRecord::Base.silence do ActiveRecord::Base.silence do
ActiveRecord::Migration.verbose = false ActiveRecord::Migration.verbose = false
load File.dirname(__FILE__) + "/../fixtures/schema.rb" load File.join(FIXTURES_PATH, 'schema.rb')
end end
end end
def self.require_fixture_models def self.prepare(conn)
models = Dir.glob(File.dirname(__FILE__) + "/../fixtures/*.rb") class << conn
models = (models.grep(/user.rb/) + models).uniq IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SHOW FIELDS /]
models.each { |f| require f }
def execute_with_counting(sql, name = nil, &block)
$query_count ||= 0
$query_count += 1 unless IGNORED_SQL.any? { |r| sql =~ r }
execute_without_counting(sql, name, &block)
end
alias_method_chain :execute, :counting
end
end end
end end

View file

@ -1,21 +0,0 @@
require 'action_controller/test_process'
module HTML
class Node
def inner_text
children.map(&:inner_text).join('')
end
end
class Text
def inner_text
self.to_s
end
end
class Tag
def inner_text
childless?? '' : super
end
end
end

View file

@ -1,13 +1,11 @@
dirname = File.dirname(__FILE__) require 'boot'
require File.join(dirname, '..', 'boot') require 'lib/activerecord_test_connector'
require File.join(dirname, 'activerecord_test_connector')
# setup the connection # setup the connection
ActiveRecordTestConnector.setup ActiveRecordTestConnector.setup
# load all fixtures # load all fixtures
fixture_path = File.join(dirname, '..', 'fixtures') Fixtures.create_fixtures(ActiveRecordTestConnector::FIXTURES_PATH, ActiveRecord::Base.connection.tables)
Fixtures.create_fixtures(fixture_path, ActiveRecord::Base.connection.tables)
require 'will_paginate' require 'will_paginate'
WillPaginate.enable_activerecord WillPaginate.enable_activerecord

View file

@ -0,0 +1,165 @@
require 'action_controller'
require 'action_controller/test_process'
require 'will_paginate'
WillPaginate.enable_actionpack
ActionController::Routing::Routes.draw do |map|
map.connect 'dummy/page/:page', :controller => 'dummy'
map.connect 'dummy/dots/page.:page', :controller => 'dummy', :action => 'dots'
map.connect 'ibocorp/:page', :controller => 'ibocorp',
:requirements => { :page => /\d+/ },
:defaults => { :page => 1 }
map.connect ':controller/:action/:id'
end
ActionController::Base.perform_caching = false
class WillPaginate::ViewTestCase < Test::Unit::TestCase
def setup
super
@controller = DummyController.new
@request = @controller.request
@html_result = nil
@template = '<%= will_paginate collection, options %>'
@view = ActionView::Base.new
@view.assigns['controller'] = @controller
@view.assigns['_request'] = @request
@view.assigns['_params'] = @request.params
end
def test_no_complain; end
protected
def paginate(collection = {}, options = {}, &block)
if collection.instance_of? Hash
page_options = { :page => 1, :total_entries => 11, :per_page => 4 }.merge(collection)
collection = [1].paginate(page_options)
end
locals = { :collection => collection, :options => options }
if defined? ActionView::InlineTemplate
# Rails 2.1
args = [ ActionView::InlineTemplate.new(@view, @template, locals) ]
else
# older Rails versions
args = [nil, @template, nil, locals]
end
@html_result = @view.render_template(*args)
@html_document = HTML::Document.new(@html_result, true, false)
if block_given?
classname = options[:class] || WillPaginate::ViewHelpers.pagination_options[:class]
assert_select("div.#{classname}", 1, 'no main DIV', &block)
end
end
def response_from_page_or_rjs
@html_document.root
end
def validate_page_numbers expected, links, param_name = :page
param_pattern = /\W#{CGI.escape(param_name.to_s)}=([^&]*)/
assert_equal(expected, links.map { |e|
e['href'] =~ param_pattern
$1 ? $1.to_i : $1
})
end
def assert_links_match pattern, links = nil, numbers = nil
links ||= assert_select 'div.pagination a[href]' do |elements|
elements
end
pages = [] if numbers
links.each do |el|
assert_match pattern, el['href']
if numbers
el['href'] =~ pattern
pages << ($1.nil?? nil : $1.to_i)
end
end
assert_equal numbers, pages, "page numbers don't match" if numbers
end
def assert_no_links_match pattern
assert_select 'div.pagination a[href]' do |elements|
elements.each do |el|
assert_no_match pattern, el['href']
end
end
end
end
class DummyRequest
attr_accessor :symbolized_path_parameters
def initialize
@get = true
@params = {}
@symbolized_path_parameters = { :controller => 'foo', :action => 'bar' }
end
def get?
@get
end
def post
@get = false
end
def relative_url_root
''
end
def params(more = nil)
@params.update(more) if more
@params
end
end
class DummyController
attr_reader :request
attr_accessor :controller_name
def initialize
@request = DummyRequest.new
@url = ActionController::UrlRewriter.new(@request, @request.params)
end
def params
@request.params
end
def url_for(params)
@url.rewrite(params)
end
end
module HTML
Node.class_eval do
def inner_text
children.map(&:inner_text).join('')
end
end
Text.class_eval do
def inner_text
self.to_s
end
end
Tag.class_eval do
def inner_text
childless?? '' : super
end
end
end

View file

@ -1,272 +0,0 @@
require File.dirname(__FILE__) + '/helper'
require 'action_controller'
require File.dirname(__FILE__) + '/lib/html_inner_text'
ActionController::Routing::Routes.draw do |map|
map.connect ':controller/:action/:id'
end
ActionController::Base.perform_caching = false
require 'will_paginate'
WillPaginate.enable_actionpack
class PaginationTest < Test::Unit::TestCase
class DevelopersController < ActionController::Base
def list_developers
@options = session[:wp] || {}
@developers = (1..11).to_a.paginate(
:page => params[@options[:param_name] || :page] || 1,
:per_page => params[:per_page] || 4
)
render :inline => '<%= will_paginate @developers, @options %>'
end
def guess_collection_name
@developers = session[:wp]
@options = session[:wp_options]
render :inline => '<%= will_paginate @options %>'
end
protected
def rescue_errors(e) raise e end
def rescue_action(e) raise e end
end
def setup
@controller = DevelopersController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
super
end
def test_will_paginate
get :list_developers
entries = assigns :developers
assert entries
assert_equal 4, entries.size
assert_select 'div.pagination', 1, 'no main DIV' do |pagination|
assert_select 'a[href]', 3 do |elements|
validate_page_numbers [2,3,2], elements
assert_select elements.last, ':last-child', "Next &raquo;"
end
assert_select 'span', 2
assert_select 'span.disabled:first-child', "&laquo; Previous"
assert_select 'span.current', entries.current_page.to_s
assert_equal '&laquo; Previous 1 2 3 Next &raquo;', pagination.first.inner_text
end
end
def test_will_paginate_with_options
get :list_developers, { :page => 2 }, :wp => {
:class => 'will_paginate', :prev_label => 'Prev', :next_label => 'Next'
}
assert_response :success
entries = assigns :developers
assert entries
assert_equal 4, entries.size
assert_select 'div.will_paginate', 1, 'no main DIV' do
assert_select 'a[href]', 4 do |elements|
validate_page_numbers [1,1,3,3], elements
# test rel attribute values:
assert_select elements[1], 'a', '1' do |link|
assert_equal 'prev start', link.first['rel']
end
assert_select elements.first, 'a', "Prev" do |link|
assert_equal 'prev start', link.first['rel']
end
assert_select elements.last, 'a', "Next" do |link|
assert_equal 'next', link.first['rel']
end
end
assert_select 'span.current', entries.current_page.to_s
end
end
def test_will_paginate_without_container
get :list_developers, {}, :wp => { :container => false }
assert_select 'div.pagination', 0, 'no main DIV'
assert_select 'a[href]', 3
end
def test_will_paginate_without_page_links
get :list_developers, { :page => 2 }, :wp => { :page_links => false }
assert_select 'a[href]', 2 do |elements|
validate_page_numbers [1,3], elements
end
end
def test_will_paginate_preserves_parameters_on_get
get :list_developers, :foo => { :bar => 'baz' }
assert_links_match /foo%5Bbar%5D=baz/
end
def test_will_paginate_doesnt_preserve_parameters_on_post
post :list_developers, :foo => 'bar'
assert_no_links_match /foo=bar/
end
def test_adding_additional_parameters
get :list_developers, {}, :wp => { :params => { :foo => 'bar' } }
assert_links_match /foo=bar/
end
def test_removing_arbitrary_parameters
get :list_developers, { :foo => 'bar' }, :wp => { :params => { :foo => nil } }
assert_no_links_match /foo=bar/
end
def test_adding_additional_route_parameters
get :list_developers, {}, :wp => { :params => { :controller => 'baz' } }
assert_links_match %r{\Wbaz/list_developers\W}
end
def test_will_paginate_with_custom_page_param
get :list_developers, { :developers_page => 2 }, :wp => { :param_name => :developers_page }
assert_response :success
entries = assigns :developers
assert entries
assert_equal 4, entries.size
assert_select 'div.pagination', 1, 'no main DIV' do
assert_select 'a[href]', 4 do |elements|
validate_page_numbers [1,1,3,3], elements, :developers_page
end
assert_select 'span.current', entries.current_page.to_s
end
end
def test_will_paginate_windows
get :list_developers, { :page => 6, :per_page => 1 }, :wp => { :inner_window => 1 }
assert_response :success
entries = assigns :developers
assert entries
assert_equal 1, entries.size
assert_select 'div.pagination', 1, 'no main DIV' do |pagination|
assert_select 'a[href]', 8 do |elements|
validate_page_numbers [5,1,2,5,7,10,11,7], elements
assert_select elements.first, 'a', "&laquo; Previous"
assert_select elements.last, 'a', "Next &raquo;"
end
assert_select 'span.current', entries.current_page.to_s
assert_equal '&laquo; Previous 1 2 ... 5 6 7 ... 10 11 Next &raquo;', pagination.first.inner_text
end
end
def test_will_paginate_eliminates_small_gaps
get :list_developers, { :page => 6, :per_page => 1 }, :wp => { :inner_window => 2 }
assert_response :success
assert_select 'div.pagination', 1, 'no main DIV' do
assert_select 'a[href]', 12 do |elements|
validate_page_numbers [5,1,2,3,4,5,7,8,9,10,11,7], elements
end
end
end
def test_no_pagination
get :list_developers, :per_page => 12
entries = assigns :developers
assert_equal 1, entries.page_count
assert_equal 11, entries.size
assert_equal '', @response.body
end
def test_faulty_input_raises_error
assert_raise WillPaginate::InvalidPage do
get :list_developers, :page => 'foo'
end
end
uses_mocha 'helper internals' do
def test_collection_name_can_be_guessed
collection = mock
collection.expects(:page_count).returns(1)
get :guess_collection_name, {}, :wp => collection
end
end
def test_inferred_collection_name_raises_error_when_nil
ex = assert_raise ArgumentError do
get :guess_collection_name, {}, :wp => nil
end
assert ex.message.include?('@developers')
end
def test_setting_id_for_container
get :list_developers
assert_select 'div.pagination', 1 do |div|
assert_nil div.first['id']
end
# magic ID
get :list_developers, {}, :wp => { :id => true }
assert_select 'div.pagination', 1 do |div|
assert_equal 'fixnums_pagination', div.first['id']
end
# explicit ID
get :list_developers, {}, :wp => { :id => 'custom_id' }
assert_select 'div.pagination', 1 do |div|
assert_equal 'custom_id', div.first['id']
end
end
if ActionController::Base.respond_to? :rescue_responses
def test_rescue_response_hook_presence
assert_equal :not_found,
DevelopersController.rescue_responses['WillPaginate::InvalidPage']
end
end
protected
def validate_page_numbers expected, links, param_name = :page
param_pattern = /\W#{param_name}=([^&]*)/
assert_equal(expected, links.map { |e|
e['href'] =~ param_pattern
$1 ? $1.to_i : $1
})
end
def assert_links_match pattern
assert_select 'div.pagination a[href]' do |elements|
elements.each do |el|
assert_match pattern, el['href']
end
end
end
def assert_no_links_match pattern
assert_select 'div.pagination a[href]' do |elements|
elements.each do |el|
assert_no_match pattern, el['href']
end
end
end
end
class ViewHelpersTest < Test::Unit::TestCase
include WillPaginate::ViewHelpers
def test_page_entries_info
arr = ('a'..'z').to_a
collection = arr.paginate :page => 2, :per_page => 5
assert_equal %{Displaying entries <b>6&nbsp;-&nbsp;10</b> of <b>26</b> in total},
page_entries_info(collection)
collection = arr.paginate :page => 7, :per_page => 4
assert_equal %{Displaying entries <b>25&nbsp;-&nbsp;26</b> of <b>26</b> in total},
page_entries_info(collection)
end
end

View file

@ -0,0 +1,56 @@
require 'rake/testtask'
desc 'Test the will_paginate plugin.'
Rake::TestTask.new(:test) do |t|
t.pattern = 'test/**/*_test.rb'
t.verbose = true
t.libs << 'test'
end
# I want to specify environment variables at call time
class EnvTestTask < Rake::TestTask
attr_accessor :env
def ruby(*args)
env.each { |key, value| ENV[key] = value } if env
super
env.keys.each { |key| ENV.delete key } if env
end
end
for configuration in %w( sqlite3 mysql postgres )
EnvTestTask.new("test_#{configuration}") do |t|
t.pattern = 'test/finder_test.rb'
t.verbose = true
t.env = { 'DB' => configuration }
t.libs << 'test'
end
end
task :test_databases => %w(test_mysql test_sqlite3 test_postgres)
desc %{Test everything on SQLite3, MySQL and PostgreSQL}
task :test_full => %w(test test_mysql test_postgres)
desc %{Test everything with Rails 1.2.x and 2.0.x gems}
task :test_all do
all = Rake::Task['test_full']
ENV['RAILS_VERSION'] = '~>1.2.6'
all.invoke
# reset the invoked flag
%w( test_full test test_mysql test_postgres ).each do |name|
Rake::Task[name].instance_variable_set '@already_invoked', false
end
# do it again
ENV['RAILS_VERSION'] = '~>2.0.2'
all.invoke
end
task :rcov do
excludes = %w( lib/will_paginate/named_scope*
lib/will_paginate/core_ext.rb
lib/will_paginate.rb
rails* )
system %[rcov -Itest:lib test/*.rb -x #{excludes.join(',')}]
end

View file

@ -0,0 +1,355 @@
require 'helper'
require 'lib/view_test_process'
class AdditionalLinkAttributesRenderer < WillPaginate::LinkRenderer
def initialize(link_attributes = nil)
super()
@additional_link_attributes = link_attributes || { :default => 'true' }
end
def page_link(page, text, attributes = {})
@template.link_to text, url_for(page), attributes.merge(@additional_link_attributes)
end
end
class ViewTest < WillPaginate::ViewTestCase
## basic pagination ##
def test_will_paginate
paginate do |pagination|
assert_select 'a[href]', 3 do |elements|
validate_page_numbers [2,3,2], elements
assert_select elements.last, ':last-child', "Next &raquo;"
end
assert_select 'span', 2
assert_select 'span.disabled:first-child', '&laquo; Previous'
assert_select 'span.current', '1'
assert_equal '&laquo; Previous 1 2 3 Next &raquo;', pagination.first.inner_text
end
end
def test_no_pagination_when_page_count_is_one
paginate :per_page => 30
assert_equal '', @html_result
end
def test_will_paginate_with_options
paginate({ :page => 2 },
:class => 'will_paginate', :prev_label => 'Prev', :next_label => 'Next') do
assert_select 'a[href]', 4 do |elements|
validate_page_numbers [1,1,3,3], elements
# test rel attribute values:
assert_select elements[1], 'a', '1' do |link|
assert_equal 'prev start', link.first['rel']
end
assert_select elements.first, 'a', "Prev" do |link|
assert_equal 'prev start', link.first['rel']
end
assert_select elements.last, 'a', "Next" do |link|
assert_equal 'next', link.first['rel']
end
end
assert_select 'span.current', '2'
end
end
def test_will_paginate_using_renderer_class
paginate({}, :renderer => AdditionalLinkAttributesRenderer) do
assert_select 'a[default=true]', 3
end
end
def test_will_paginate_using_renderer_instance
renderer = WillPaginate::LinkRenderer.new
renderer.gap_marker = '<span class="my-gap">~~</span>'
paginate({ :per_page => 2 }, :inner_window => 0, :outer_window => 0, :renderer => renderer) do
assert_select 'span.my-gap', '~~'
end
renderer = AdditionalLinkAttributesRenderer.new(:title => 'rendered')
paginate({}, :renderer => renderer) do
assert_select 'a[title=rendered]', 3
end
end
def test_prev_next_links_have_classnames
paginate do |pagination|
assert_select 'span.disabled.prev_page:first-child'
assert_select 'a.next_page[href]:last-child'
end
end
def test_full_output
paginate
expected = <<-HTML
<div class="pagination"><span class="disabled prev_page">&laquo; Previous</span>
<span class="current">1</span>
<a href="/foo/bar?page=2" rel="next">2</a>
<a href="/foo/bar?page=3">3</a>
<a href="/foo/bar?page=2" class="next_page" rel="next">Next &raquo;</a></div>
HTML
expected.strip!.gsub!(/\s{2,}/, ' ')
assert_dom_equal expected, @html_result
end
def test_escaping_of_urls
paginate({:page => 1, :per_page => 1, :total_entries => 2},
:page_links => false, :params => { :tag => '<br>' })
assert_select 'a[href]', 1 do |links|
query = links.first['href'].split('?', 2)[1]
assert_equal %w(page=2 tag=%3Cbr%3E), query.split('&amp;').sort
end
end
## advanced options for pagination ##
def test_will_paginate_without_container
paginate({}, :container => false)
assert_select 'div.pagination', 0, 'main DIV present when it shouldn\'t'
assert_select 'a[href]', 3
end
def test_will_paginate_without_page_links
paginate({ :page => 2 }, :page_links => false) do
assert_select 'a[href]', 2 do |elements|
validate_page_numbers [1,3], elements
end
end
end
def test_will_paginate_windows
paginate({ :page => 6, :per_page => 1 }, :inner_window => 1) do |pagination|
assert_select 'a[href]', 8 do |elements|
validate_page_numbers [5,1,2,5,7,10,11,7], elements
assert_select elements.first, 'a', '&laquo; Previous'
assert_select elements.last, 'a', 'Next &raquo;'
end
assert_select 'span.current', '6'
assert_equal '&laquo; Previous 1 2 &hellip; 5 6 7 &hellip; 10 11 Next &raquo;', pagination.first.inner_text
end
end
def test_will_paginate_eliminates_small_gaps
paginate({ :page => 6, :per_page => 1 }, :inner_window => 2) do
assert_select 'a[href]', 12 do |elements|
validate_page_numbers [5,1,2,3,4,5,7,8,9,10,11,7], elements
end
end
end
def test_container_id
paginate do |div|
assert_nil div.first['id']
end
# magic ID
paginate({}, :id => true) do |div|
assert_equal 'fixnums_pagination', div.first['id']
end
# explicit ID
paginate({}, :id => 'custom_id') do |div|
assert_equal 'custom_id', div.first['id']
end
end
## other helpers ##
def test_paginated_section
@template = <<-ERB
<% paginated_section collection, options do %>
<%= content_tag :div, '', :id => "developers" %>
<% end %>
ERB
paginate
assert_select 'div.pagination', 2
assert_select 'div.pagination + div#developers', 1
end
def test_page_entries_info
@template = '<%= page_entries_info collection %>'
array = ('a'..'z').to_a
paginate array.paginate(:page => 2, :per_page => 5)
assert_equal %{Displaying strings <b>6&nbsp;-&nbsp;10</b> of <b>26</b> in total},
@html_result
paginate array.paginate(:page => 7, :per_page => 4)
assert_equal %{Displaying strings <b>25&nbsp;-&nbsp;26</b> of <b>26</b> in total},
@html_result
end
def test_page_entries_info_with_longer_class_name
@template = '<%= page_entries_info collection %>'
collection = ('a'..'z').to_a.paginate
collection.first.stubs(:class).returns(mock('class', :name => 'ProjectType'))
paginate collection
assert @html_result.index('project types'), "expected <#{@html_result.inspect}> to mention 'project types'"
end
def test_page_entries_info_with_single_page_collection
@template = '<%= page_entries_info collection %>'
paginate(('a'..'d').to_a.paginate(:page => 1, :per_page => 5))
assert_equal %{Displaying <b>all 4</b> strings}, @html_result
paginate(['a'].paginate(:page => 1, :per_page => 5))
assert_equal %{Displaying <b>1</b> string}, @html_result
paginate([].paginate(:page => 1, :per_page => 5))
assert_equal %{No entries found}, @html_result
end
def test_page_entries_info_with_custom_entry_name
@template = '<%= page_entries_info collection, :entry_name => "author" %>'
entries = (1..20).to_a
paginate(entries.paginate(:page => 1, :per_page => 5))
assert_equal %{Displaying authors <b>1&nbsp;-&nbsp;5</b> of <b>20</b> in total}, @html_result
paginate(entries.paginate(:page => 1, :per_page => 20))
assert_equal %{Displaying <b>all 20</b> authors}, @html_result
paginate(['a'].paginate(:page => 1, :per_page => 5))
assert_equal %{Displaying <b>1</b> author}, @html_result
paginate([].paginate(:page => 1, :per_page => 5))
assert_equal %{No authors found}, @html_result
end
## parameter handling in page links ##
def test_will_paginate_preserves_parameters_on_get
@request.params :foo => { :bar => 'baz' }
paginate
assert_links_match /foo%5Bbar%5D=baz/
end
def test_will_paginate_doesnt_preserve_parameters_on_post
@request.post
@request.params :foo => 'bar'
paginate
assert_no_links_match /foo=bar/
end
def test_adding_additional_parameters
paginate({}, :params => { :foo => 'bar' })
assert_links_match /foo=bar/
end
def test_adding_anchor_parameter
paginate({}, :params => { :anchor => 'anchor' })
assert_links_match /#anchor$/
end
def test_removing_arbitrary_parameters
@request.params :foo => 'bar'
paginate({}, :params => { :foo => nil })
assert_no_links_match /foo=bar/
end
def test_adding_additional_route_parameters
paginate({}, :params => { :controller => 'baz', :action => 'list' })
assert_links_match %r{\Wbaz/list\W}
end
def test_will_paginate_with_custom_page_param
paginate({ :page => 2 }, :param_name => :developers_page) do
assert_select 'a[href]', 4 do |elements|
validate_page_numbers [1,1,3,3], elements, :developers_page
end
end
end
def test_complex_custom_page_param
@request.params :developers => { :page => 2 }
paginate({ :page => 2 }, :param_name => 'developers[page]') do
assert_select 'a[href]', 4 do |links|
assert_links_match /\?developers%5Bpage%5D=\d+$/, links
validate_page_numbers [1,1,3,3], links, 'developers[page]'
end
end
end
def test_custom_routing_page_param
@request.symbolized_path_parameters.update :controller => 'dummy', :action => nil
paginate :per_page => 2 do
assert_select 'a[href]', 6 do |links|
assert_links_match %r{/page/(\d+)$}, links, [2, 3, 4, 5, 6, 2]
end
end
end
def test_custom_routing_page_param_with_dot_separator
@request.symbolized_path_parameters.update :controller => 'dummy', :action => 'dots'
paginate :per_page => 2 do
assert_select 'a[href]', 6 do |links|
assert_links_match %r{/page\.(\d+)$}, links, [2, 3, 4, 5, 6, 2]
end
end
end
def test_custom_routing_with_first_page_hidden
@request.symbolized_path_parameters.update :controller => 'ibocorp', :action => nil
paginate :page => 2, :per_page => 2 do
assert_select 'a[href]', 7 do |links|
assert_links_match %r{/ibocorp(?:/(\d+))?$}, links, [nil, nil, 3, 4, 5, 6, 3]
end
end
end
## internal hardcore stuff ##
class LegacyCollection < WillPaginate::Collection
alias :page_count :total_pages
undef :total_pages
end
def test_deprecation_notices_with_page_count
collection = LegacyCollection.new(1, 1, 2)
assert_deprecated collection.class.name do
paginate collection
end
end
uses_mocha 'view internals' do
def test_collection_name_can_be_guessed
collection = mock
collection.expects(:total_pages).returns(1)
@template = '<%= will_paginate options %>'
@controller.controller_name = 'developers'
@view.assigns['developers'] = collection
paginate(nil)
end
end
def test_inferred_collection_name_raises_error_when_nil
@template = '<%= will_paginate options %>'
@controller.controller_name = 'developers'
e = assert_raise ArgumentError do
paginate(nil)
end
assert e.message.include?('@developers')
end
if ActionController::Base.respond_to? :rescue_responses
# only on Rails 2
def test_rescue_response_hook_presence
assert_equal :not_found,
ActionController::Base.rescue_responses['WillPaginate::InvalidPage']
end
end
end

View file

@ -0,0 +1,21 @@
Gem::Specification.new do |s|
s.name = 'will_paginate'
s.version = '2.3.2'
s.date = '2008-05-16'
s.summary = "Most awesome pagination solution for Rails"
s.description = "The will_paginate library provides a simple, yet powerful and extensible API for ActiveRecord pagination and rendering of pagination links in ActionView templates."
s.authors = ['Mislav Marohnić', 'PJ Hyett']
s.email = 'mislav.marohnic@gmail.com'
s.homepage = 'http://github.com/mislav/will_paginate/wikis'
s.has_rdoc = true
s.rdoc_options = ['--main', 'README.rdoc']
s.rdoc_options << '--inline-source' << '--charset=UTF-8'
s.extra_rdoc_files = ['README.rdoc', 'LICENSE', 'CHANGELOG']
s.add_dependency 'activesupport', ['>= 1.4.4']
s.files = %w(CHANGELOG LICENSE README.rdoc Rakefile examples examples/apple-circle.gif examples/index.haml examples/index.html examples/pagination.css examples/pagination.sass init.rb lib lib/will_paginate lib/will_paginate.rb lib/will_paginate/array.rb lib/will_paginate/collection.rb lib/will_paginate/core_ext.rb lib/will_paginate/finder.rb lib/will_paginate/named_scope.rb lib/will_paginate/named_scope_patch.rb lib/will_paginate/version.rb lib/will_paginate/view_helpers.rb test test/boot.rb test/collection_test.rb test/console test/database.yml test/finder_test.rb test/fixtures test/fixtures/admin.rb test/fixtures/developer.rb test/fixtures/developers_projects.yml test/fixtures/project.rb test/fixtures/projects.yml test/fixtures/replies.yml test/fixtures/reply.rb test/fixtures/schema.rb test/fixtures/topic.rb test/fixtures/topics.yml test/fixtures/user.rb test/fixtures/users.yml test/helper.rb test/lib test/lib/activerecord_test_case.rb test/lib/activerecord_test_connector.rb test/lib/load_fixtures.rb test/lib/view_test_process.rb test/tasks.rake test/view_test.rb)
s.test_files = %w(test/boot.rb test/collection_test.rb test/console test/database.yml test/finder_test.rb test/fixtures test/fixtures/admin.rb test/fixtures/developer.rb test/fixtures/developers_projects.yml test/fixtures/project.rb test/fixtures/projects.yml test/fixtures/replies.yml test/fixtures/reply.rb test/fixtures/schema.rb test/fixtures/topic.rb test/fixtures/topics.yml test/fixtures/user.rb test/fixtures/users.yml test/helper.rb test/lib test/lib/activerecord_test_case.rb test/lib/activerecord_test_connector.rb test/lib/load_fixtures.rb test/lib/view_test_process.rb test/tasks.rake test/view_test.rb)
end

14
vendor/rails/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
debug.log
activeresource/doc
activerecord/doc
actionpack/doc
actionmailer/doc
activesupport/doc
railties/doc
activeresource/pkg
activerecord/pkg
actionpack/pkg
actionmailer/pkg
activesupport/pkg
railties/pkg
*.rbc

View file

@ -15,7 +15,7 @@ task :default => :test
desc "Run #{task_name} task for all projects" desc "Run #{task_name} task for all projects"
task task_name do task task_name do
PROJECTS.each do |project| PROJECTS.each do |project|
system %(cd #{project} && #{env} rake #{task_name}) system %(cd #{project} && #{env} #{$0} #{task_name})
end end
end end
end end

View file

@ -1,3 +1,14 @@
*2.1.0 (May 31st, 2008)*
* Fixed that a return-path header would be ignored #7572 [joost]
* Less verbose mail logging: just recipients for :info log level; the whole email for :debug only. #8000 [iaddict, Tarmo Tänav]
* Updated TMail to version 1.2.1 [raasdnil]
* Fixed that you don't have to call super in ActionMailer::TestCase#setup #10406 [jamesgolick]
*2.0.2* (December 16th, 2007) *2.0.2* (December 16th, 2007)
* Included in Rails 2.0.2 * Included in Rails 2.0.2

View file

@ -1,4 +1,4 @@
Copyright (c) 2004-2007 David Heinemeier Hansson Copyright (c) 2004-2008 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View file

@ -19,8 +19,7 @@ are all set up this way. An example of such a method:
recipients recipient recipients recipient
subject "[Signed up] Welcome #{recipient}" subject "[Signed up] Welcome #{recipient}"
from "system@loudthinking.com" from "system@loudthinking.com"
body :recipient => recipient
body(:recipient => recipient)
end end
The body of the email is created by using an Action View template (regular The body of the email is created by using an Action View template (regular
@ -78,21 +77,26 @@ Example:
end end
end end
This Mailman can be the target for Postfix. In Rails, you would use the runner like this: This Mailman can be the target for Postfix or other MTAs. In Rails, you would use the runner in the
trivial case like this:
./script/runner 'Mailman.receive(STDIN.read)' ./script/runner 'Mailman.receive(STDIN.read)'
However, invoking Rails in the runner for each mail to be received is very resource intensive. A single
instance of Rails should be run within a daemon if it is going to be utilized to process more than just
a limited number of email.
== Configuration == Configuration
The Base class has the full list of configuration options. Here's an example: The Base class has the full list of configuration options. Here's an example:
ActionMailer::Base.smtp_settings = { ActionMailer::Base.smtp_settings = {
:address=>'smtp.yourserver.com', # default: localhost :address => 'smtp.yourserver.com', # default: localhost
:port=>'25', # default: 25 :port => '25', # default: 25
:user_name=>'user', :user_name => 'user',
:password=>'pass', :password => 'pass',
:authentication=>:plain # :plain, :login or :cram_md5 :authentication => :plain # :plain, :login or :cram_md5
} }
== Dependencies == Dependencies

View file

@ -4,7 +4,7 @@ require 'rake/testtask'
require 'rake/rdoctask' require 'rake/rdoctask'
require 'rake/packagetask' require 'rake/packagetask'
require 'rake/gempackagetask' require 'rake/gempackagetask'
require 'rake/contrib/rubyforgepublisher' require 'rake/contrib/sshpublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version') require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@ -55,7 +55,7 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "actionmailer" s.rubyforge_project = "actionmailer"
s.homepage = "http://www.rubyonrails.org" s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 2.0.2' + PKG_BUILD) s.add_dependency('actionpack', '= 2.1.0' + PKG_BUILD)
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
@ -87,6 +87,7 @@ end
desc "Publish the release files to RubyForge." desc "Publish the release files to RubyForge."
task :release => [ :package ] do task :release => [ :package ] do
require 'rubyforge' require 'rubyforge'
require 'rake/contrib/rubyforgepublisher'
packages = %w( gem tgz zip ).collect{ |ext| "pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" } packages = %w( gem tgz zip ).collect{ |ext| "pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" }

View file

@ -1,5 +1,5 @@
#-- #--
# Copyright (c) 2004-2007 David Heinemeier Hansson # Copyright (c) 2004-2008 David Heinemeier Hansson
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the # a copy of this software and associated documentation files (the

View file

@ -16,7 +16,7 @@ module ActionMailer
define_method(name) do |*parameters| define_method(name) do |*parameters|
raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1 raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1
if parameters.empty? if parameters.empty?
if instance_variables.include?(ivar) if instance_variable_names.include?(ivar)
instance_variable_get(ivar) instance_variable_get(ivar)
end end
else else

View file

@ -5,12 +5,12 @@ require 'action_mailer/utils'
require 'tmail/net' require 'tmail/net'
module ActionMailer #:nodoc: module ActionMailer #:nodoc:
# ActionMailer allows you to send email from your application using a mailer model and views. # Action Mailer allows you to send email from your application using a mailer model and views.
# #
# #
# = Mailer Models # = Mailer Models
# #
# To use ActionMailer, you need to create a mailer model. # To use Action Mailer, you need to create a mailer model.
# #
# $ script/generate mailer Notifier # $ script/generate mailer Notifier
# #
@ -35,22 +35,27 @@ module ActionMailer #:nodoc:
# * <tt>subject</tt> - The subject of your email. Sets the <tt>Subject:</tt> header. # * <tt>subject</tt> - The subject of your email. Sets the <tt>Subject:</tt> header.
# * <tt>from</tt> - Who the email you are sending is from. Sets the <tt>From:</tt> header. # * <tt>from</tt> - Who the email you are sending is from. Sets the <tt>From:</tt> header.
# * <tt>cc</tt> - Takes one or more email addresses. These addresses will receive a carbon copy of your email. Sets the <tt>Cc:</tt> header. # * <tt>cc</tt> - Takes one or more email addresses. These addresses will receive a carbon copy of your email. Sets the <tt>Cc:</tt> header.
# * <tt>bcc</tt> - Takes one or more email address. These addresses will receive a blind carbon copy of your email. Sets the <tt>Bcc</tt> header. # * <tt>bcc</tt> - Takes one or more email addresses. These addresses will receive a blind carbon copy of your email. Sets the <tt>Bcc:</tt> header.
# * <tt>reply_to</tt> - Takes one or more email addresses. These addresses will be listed as the default recipients when replying to your email. Sets the <tt>Reply-To:</tt> header.
# * <tt>sent_on</tt> - The date on which the message was sent. If not set, the header wil be set by the delivery agent. # * <tt>sent_on</tt> - The date on which the message was sent. If not set, the header wil be set by the delivery agent.
# * <tt>content_type</tt> - Specify the content type of the message. Defaults to <tt>text/plain</tt>. # * <tt>content_type</tt> - Specify the content type of the message. Defaults to <tt>text/plain</tt>.
# * <tt>headers</tt> - Specify additional headers to be set for the message, e.g. <tt>headers 'X-Mail-Count' => 107370</tt>. # * <tt>headers</tt> - Specify additional headers to be set for the message, e.g. <tt>headers 'X-Mail-Count' => 107370</tt>.
# #
# When a <tt>headers 'return-path'</tt> is specified, that value will be used as the 'envelope from'
# address. Setting this is useful when you want delivery notifications sent to a different address than
# the one in <tt>from</tt>.
#
# The <tt>body</tt> method has special behavior. It takes a hash which generates an instance variable # The <tt>body</tt> method has special behavior. It takes a hash which generates an instance variable
# named after each key in the hash containing the value that that key points to. # named after each key in the hash containing the value that that key points to.
# #
# So, for example, <tt>body "account" => recipient</tt> would result # So, for example, <tt>body :account => recipient</tt> would result
# in an instance variable <tt>@account</tt> with the value of <tt>recipient</tt> being accessible in the # in an instance variable <tt>@account</tt> with the value of <tt>recipient</tt> being accessible in the
# view. # view.
# #
# #
# = Mailer views # = Mailer views
# #
# Like ActionController, each mailer class has a corresponding view directory # Like Action Controller, each mailer class has a corresponding view directory
# in which each method of the class looks for a template with its name. # in which each method of the class looks for a template with its name.
# To define a template to be used with a mailing, create an <tt>.erb</tt> file with the same name as the method # To define a template to be used with a mailing, create an <tt>.erb</tt> file with the same name as the method
# in your mailer model. For example, in the mailer defined above, the template at # in your mailer model. For example, in the mailer defined above, the template at
@ -69,21 +74,36 @@ module ActionMailer #:nodoc:
# <%= truncate(note.body, 25) %> # <%= truncate(note.body, 25) %>
# #
# #
# = Generating URLs for mailer views # = Generating URLs
# #
# If your view includes URLs from the application, you need to use url_for in the mailing method instead of the view. # URLs can be generated in mailer views using <tt>url_for</tt> or named routes.
# Unlike controllers from Action Pack, the mailer instance doesn't have any context about the incoming request. That's # Unlike controllers from Action Pack, the mailer instance doesn't have any context about the incoming request,
# why you need to jump this little hoop and supply all the details needed for the URL. Example: # so you'll need to provide all of the details needed to generate a URL.
# #
# def signup_notification(recipient) # When using <tt>url_for</tt> you'll need to provide the <tt>:host</tt>, <tt>:controller</tt>, and <tt>:action</tt>:
# recipients recipient.email_address_with_name
# from "system@example.com"
# subject "New account information"
# body :account => recipient,
# :home_page => url_for(:host => "example.com", :controller => "welcome", :action => "greeting")
# end
# #
# You can now access @home_page in the template and get http://example.com/welcome/greeting. # <%= url_for(:host => "example.com", :controller => "welcome", :action => "greeting") %>
#
# When using named routes you only need to supply the <tt>:host</tt>:
#
# <%= users_url(:host => "example.com") %>
#
# You will want to avoid using the <tt>name_of_route_path</tt> form of named routes because it doesn't make sense to
# generate relative URLs in email messages.
#
# It is also possible to set a default host that will be used in all mailers by setting the <tt>:host</tt> option in
# the <tt>ActionMailer::Base.default_url_options</tt> hash as follows:
#
# ActionMailer::Base.default_url_options[:host] = "example.com"
#
# This can also be set as a configuration option in <tt>config/environment.rb</tt>:
#
# config.action_mailer.default_url_options = { :host => "example.com" }
#
# If you do decide to set a default <tt>:host</tt> for your mailers you will want to use the
# <tt>:only_path => false</tt> option when using <tt>url_for</tt>. This will ensure that absolute URLs are generated because
# the <tt>url_for</tt> view helper will, by default, generate relative URLs when a <tt>:host</tt> option isn't
# explicitly provided.
# #
# = Sending mail # = Sending mail
# #
@ -108,11 +128,11 @@ module ActionMailer #:nodoc:
# #
# class MyMailer < ActionMailer::Base # class MyMailer < ActionMailer::Base
# def signup_notification(recipient) # def signup_notification(recipient)
# recipients recipient.email_address_with_name # recipients recipient.email_address_with_name
# subject "New account information" # subject "New account information"
# body "account" => recipient # from "system@example.com"
# from "system@example.com" # body :account => recipient
# content_type "text/html" # Here's where the magic happens # content_type "text/html"
# end # end
# end # end
# #
@ -126,6 +146,7 @@ module ActionMailer #:nodoc:
# recipients recipient.email_address_with_name # recipients recipient.email_address_with_name
# subject "New account information" # subject "New account information"
# from "system@example.com" # from "system@example.com"
# content_type "multipart/alternative"
# #
# part :content_type => "text/html", # part :content_type => "text/html",
# :body => render_message("signup-as-html", :account => recipient) # :body => render_message("signup-as-html", :account => recipient)
@ -137,7 +158,7 @@ module ActionMailer #:nodoc:
# end # end
# end # end
# #
# Multipart messages can also be used implicitly because ActionMailer will automatically # Multipart messages can also be used implicitly because Action Mailer will automatically
# detect and use multipart templates, where each template is named after the name of the action, followed # detect and use multipart templates, where each template is named after the name of the action, followed
# by the content type. Each such detected template will be added as separate part to the message. # by the content type. Each such detected template will be added as separate part to the message.
# #
@ -148,9 +169,14 @@ module ActionMailer #:nodoc:
# * signup_notification.text.x-yaml.erb # * signup_notification.text.x-yaml.erb
# #
# Each would be rendered and added as a separate part to the message, # Each would be rendered and added as a separate part to the message,
# with the corresponding content type. The same body hash is passed to # with the corresponding content type. The content type for the entire
# each template. # message is automatically set to <tt>multipart/alternative</tt>, which indicates
# that the email contains multiple different representations of the same email
# body. The same body hash is passed to each template.
# #
# Implicit template rendering is not performed if any attachments or parts have been added to the email.
# This means that you'll have to manually add each part to the email and set the content type of the email
# to <tt>multipart/alternative</tt>.
# #
# = Attachments # = Attachments
# #
@ -179,44 +205,45 @@ module ActionMailer #:nodoc:
# #
# These options are specified on the class level, like <tt>ActionMailer::Base.template_root = "/my/templates"</tt> # These options are specified on the class level, like <tt>ActionMailer::Base.template_root = "/my/templates"</tt>
# #
# * <tt>template_root</tt> - template root determines the base from which template references will be made. # * <tt>template_root</tt> - Determines the base from which template references will be made.
# #
# * <tt>logger</tt> - the logger is used for generating information on the mailing run if available. # * <tt>logger</tt> - the logger is used for generating information on the mailing run if available.
# Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers. # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
# #
# * <tt>smtp_settings</tt> - Allows detailed configuration for :smtp delivery method: # * <tt>smtp_settings</tt> - Allows detailed configuration for <tt>:smtp</tt> delivery method:
# * <tt>:address</tt> Allows you to use a remote mail server. Just change it from its default "localhost" setting. # * <tt>:address</tt> - Allows you to use a remote mail server. Just change it from its default "localhost" setting.
# * <tt>:port</tt> On the off chance that your mail server doesn't run on port 25, you can change it. # * <tt>:port</tt> - On the off chance that your mail server doesn't run on port 25, you can change it.
# * <tt>:domain</tt> If you need to specify a HELO domain, you can do it here. # * <tt>:domain</tt> - If you need to specify a HELO domain, you can do it here.
# * <tt>:user_name</tt> If your mail server requires authentication, set the username in this setting. # * <tt>:user_name</tt> - If your mail server requires authentication, set the username in this setting.
# * <tt>:password</tt> If your mail server requires authentication, set the password in this setting. # * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting.
# * <tt>:authentication</tt> If your mail server requires authentication, you need to specify the authentication type here. # * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the authentication type here.
# This is a symbol and one of :plain, :login, :cram_md5 # This is a symbol and one of <tt>:plain</tt>, <tt>:login</tt>, <tt>:cram_md5</tt>.
# #
# * <tt>sendmail_settings</tt> - Allows you to override options for the :sendmail delivery method # * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
# * <tt>:location</tt> The location of the sendmail executable, defaults to "/usr/sbin/sendmail" # * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
# * <tt>:arguments</tt> The command line arguments # * <tt>:arguments</tt> - The command line arguments. Defaults to <tt>-i -t</tt>.
# * <tt>raise_delivery_errors</tt> - whether or not errors should be raised if the email fails to be delivered.
# #
# * <tt>delivery_method</tt> - Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test. # * <tt>raise_delivery_errors</tt> - Whether or not errors should be raised if the email fails to be delivered.
# #
# * <tt>perform_deliveries</tt> - Determines whether deliver_* methods are actually carried out. By default they are, # * <tt>delivery_method</tt> - Defines a delivery method. Possible values are <tt>:smtp</tt> (default), <tt>:sendmail</tt>, and <tt>:test</tt>.
#
# * <tt>perform_deliveries</tt> - Determines whether <tt>deliver_*</tt> methods are actually carried out. By default they are,
# but this can be turned off to help functional testing. # but this can be turned off to help functional testing.
# #
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with <tt>delivery_method :test</tt>. Most useful
# for unit and functional testing. # for unit and functional testing.
# #
# * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also # * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also
# pick a different charset from inside a method with <tt>@charset</tt>. # pick a different charset from inside a method with +charset+.
# * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You # * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You
# can also pick a different content type from inside a method with <tt>@content_type</tt>. # can also pick a different content type from inside a method with +content_type+.
# * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to "1.0". You # * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to <tt>1.0</tt>. You
# can also pick a different value from inside a method with <tt>@mime_version</tt>. # can also pick a different value from inside a method with +mime_version+.
# * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates # * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates
# which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to # which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to
# ["text/html", "text/enriched", "text/plain"]. Items that appear first in the array have higher priority in the mail client # <tt>["text/html", "text/enriched", "text/plain"]</tt>. Items that appear first in the array have higher priority in the mail client
# and appear last in the mime encoded message. You can also pick a different order from inside a method with # and appear last in the mime encoded message. You can also pick a different order from inside a method with
# <tt>@implicit_parts_order</tt>. # +implicit_parts_order+.
class Base class Base
include AdvAttrAccessor, PartContainer include AdvAttrAccessor, PartContainer
include ActionController::UrlWriter if Object.const_defined?(:ActionController) include ActionController::UrlWriter if Object.const_defined?(:ActionController)
@ -291,6 +318,10 @@ module ActionMailer #:nodoc:
# Specify the from address for the message. # Specify the from address for the message.
adv_attr_accessor :from adv_attr_accessor :from
# Specify the address (if different than the "from" address) to direct
# replies to this message.
adv_attr_accessor :reply_to
# Specify additional headers to be added to the message. # Specify additional headers to be added to the message.
adv_attr_accessor :headers adv_attr_accessor :headers
@ -357,8 +388,8 @@ module ActionMailer #:nodoc:
# Receives a raw email, parses it into an email object, decodes it, # Receives a raw email, parses it into an email object, decodes it,
# instantiates a new mailer, and passes the email object to the mailer # instantiates a new mailer, and passes the email object to the mailer
# object's #receive method. If you want your mailer to be able to # object's +receive+ method. If you want your mailer to be able to
# process incoming messages, you'll need to implement a #receive # process incoming messages, you'll need to implement a +receive+
# method that accepts the email object as a parameter: # method that accepts the email object as a parameter:
# #
# class MyMailer < ActionMailer::Base # class MyMailer < ActionMailer::Base
@ -387,12 +418,17 @@ module ActionMailer #:nodoc:
# templating language other than rhtml or rxml are supported. # templating language other than rhtml or rxml are supported.
# To use this, include in your template-language plugin's init # To use this, include in your template-language plugin's init
# code or on a per-application basis, this can be invoked from # code or on a per-application basis, this can be invoked from
# config/environment.rb: # <tt>config/environment.rb</tt>:
# #
# ActionMailer::Base.register_template_extension('haml') # ActionMailer::Base.register_template_extension('haml')
def register_template_extension(extension) def register_template_extension(extension)
template_extensions << extension template_extensions << extension
end end
def template_root=(root)
write_inheritable_attribute(:template_root, root)
ActionView::TemplateFinder.process_view_paths(root)
end
end end
# Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
@ -459,11 +495,14 @@ module ActionMailer #:nodoc:
end end
# Delivers a TMail::Mail object. By default, it delivers the cached mail # Delivers a TMail::Mail object. By default, it delivers the cached mail
# object (from the #create! method). If no cached mail object exists, and # object (from the <tt>create!</tt> method). If no cached mail object exists, and
# no alternate has been given as the parameter, this will fail. # no alternate has been given as the parameter, this will fail.
def deliver!(mail = @mail) def deliver!(mail = @mail)
raise "no mail object available for delivery!" unless mail raise "no mail object available for delivery!" unless mail
logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil? unless logger.nil?
logger.info "Sent mail to #{Array(recipients).join(', ')}"
logger.debug "\n#{mail.encoded}"
end
begin begin
__send__("perform_delivery_#{delivery_method}", mail) if perform_deliveries __send__("perform_delivery_#{delivery_method}", mail) if perform_deliveries
@ -483,7 +522,7 @@ module ActionMailer #:nodoc:
@content_type ||= @@default_content_type.dup @content_type ||= @@default_content_type.dup
@implicit_parts_order ||= @@default_implicit_parts_order.dup @implicit_parts_order ||= @@default_implicit_parts_order.dup
@template ||= method_name @template ||= method_name
@mailer_name ||= Inflector.underscore(self.class.name) @mailer_name ||= self.class.name.underscore
@parts ||= [] @parts ||= []
@headers ||= {} @headers ||= {}
@body ||= {} @body ||= {}
@ -542,13 +581,14 @@ module ActionMailer #:nodoc:
def create_mail def create_mail
m = TMail::Mail.new m = TMail::Mail.new
m.subject, = quote_any_if_necessary(charset, subject) m.subject, = quote_any_if_necessary(charset, subject)
m.to, m.from = quote_any_address_if_necessary(charset, recipients, from) m.to, m.from = quote_any_address_if_necessary(charset, recipients, from)
m.bcc = quote_address_if_necessary(bcc, charset) unless bcc.nil? m.bcc = quote_address_if_necessary(bcc, charset) unless bcc.nil?
m.cc = quote_address_if_necessary(cc, charset) unless cc.nil? m.cc = quote_address_if_necessary(cc, charset) unless cc.nil?
m.reply_to = quote_address_if_necessary(reply_to, charset) unless reply_to.nil?
m.mime_version = mime_version unless mime_version.nil? m.mime_version = mime_version unless mime_version.nil?
m.date = sent_on.to_time rescue sent_on if sent_on m.date = sent_on.to_time rescue sent_on if sent_on
headers.each { |k, v| m[k] = v } headers.each { |k, v| m[k] = v }
real_content_type, ctype_attrs = parse_content_type real_content_type, ctype_attrs = parse_content_type
@ -582,15 +622,18 @@ module ActionMailer #:nodoc:
def perform_delivery_smtp(mail) def perform_delivery_smtp(mail)
destinations = mail.destinations destinations = mail.destinations
mail.ready_to_send mail.ready_to_send
sender = mail['return-path'] || mail.from
Net::SMTP.start(smtp_settings[:address], smtp_settings[:port], smtp_settings[:domain], Net::SMTP.start(smtp_settings[:address], smtp_settings[:port], smtp_settings[:domain],
smtp_settings[:user_name], smtp_settings[:password], smtp_settings[:authentication]) do |smtp| smtp_settings[:user_name], smtp_settings[:password], smtp_settings[:authentication]) do |smtp|
smtp.sendmail(mail.encoded, mail.from, destinations) smtp.sendmail(mail.encoded, sender, destinations)
end end
end end
def perform_delivery_sendmail(mail) def perform_delivery_sendmail(mail)
IO.popen("#{sendmail_settings[:location]} #{sendmail_settings[:arguments]}","w+") do |sm| sendmail_args = sendmail_settings[:arguments]
sendmail_args += " -f \"#{mail['return-path']}\"" if mail['return-path']
IO.popen("#{sendmail_settings[:location]} #{sendmail_args}","w+") do |sm|
sm.print(mail.encoded.gsub(/\r/, '')) sm.print(mail.encoded.gsub(/\r/, ''))
sm.flush sm.flush
end end

View file

@ -34,7 +34,7 @@ module ActionMailer
# helper FooHelper # helper FooHelper
# includes FooHelper in the template class. # includes FooHelper in the template class.
# helper { def foo() "#{bar} is the very best" end } # helper { def foo() "#{bar} is the very best" end }
# evaluates the block in the template class, adding method #foo. # evaluates the block in the template class, adding method +foo+.
# helper(:three, BlindHelper) { def mice() 'mice' end } # helper(:three, BlindHelper) { def mice() 'mice' end }
# does all three. # does all three.
def helper(*args, &block) def helper(*args, &block)
@ -93,9 +93,9 @@ module ActionMailer
begin begin
child.master_helper_module = Module.new child.master_helper_module = Module.new
child.master_helper_module.send! :include, master_helper_module child.master_helper_module.send! :include, master_helper_module
child.helper child.name.underscore child.helper child.name.to_s.underscore
rescue MissingSourceFile => e rescue MissingSourceFile => e
raise unless e.is_missing?("helpers/#{child.name.underscore}_helper") raise unless e.is_missing?("helpers/#{child.name.to_s.underscore}_helper")
end end
end end
end end

View file

@ -5,7 +5,7 @@ require 'action_mailer/utils'
module ActionMailer module ActionMailer
# Represents a subpart of an email message. It shares many similar # Represents a subpart of an email message. It shares many similar
# attributes of ActionMailer::Base. Although you can create parts manually # attributes of ActionMailer::Base. Although you can create parts manually
# and add them to the #parts list of the mailer, it is easier # and add them to the +parts+ list of the mailer, it is easier
# to use the helper methods in ActionMailer::PartContainer. # to use the helper methods in ActionMailer::PartContainer.
class Part class Part
include ActionMailer::AdvAttrAccessor include ActionMailer::AdvAttrAccessor
@ -13,7 +13,7 @@ module ActionMailer
# Represents the body of the part, as a string. This should not be a # Represents the body of the part, as a string. This should not be a
# Hash (like ActionMailer::Base), but if you want a template to be rendered # Hash (like ActionMailer::Base), but if you want a template to be rendered
# into the body of a subpart you can do it with the mailer's #render method # into the body of a subpart you can do it with the mailer's +render+ method
# and assign the result here. # and assign the result here.
adv_attr_accessor :body adv_attr_accessor :body

View file

@ -24,6 +24,8 @@ module ActionMailer
# Quote the given text if it contains any "illegal" characters # Quote the given text if it contains any "illegal" characters
def quote_if_necessary(text, charset) def quote_if_necessary(text, charset)
text = text.dup.force_encoding(Encoding::ASCII_8BIT) if text.respond_to?(:force_encoding)
(text =~ CHARS_NEEDING_QUOTING) ? (text =~ CHARS_NEEDING_QUOTING) ?
quoted_printable(text, charset) : quoted_printable(text, charset) :
text text
@ -38,7 +40,7 @@ module ActionMailer
# regular email address, or it can be a phrase followed by an address in # regular email address, or it can be a phrase followed by an address in
# brackets. The phrase is the only part that will be quoted, and only if # brackets. The phrase is the only part that will be quoted, and only if
# it needs to be. This allows extended characters to be used in the # it needs to be. This allows extended characters to be used in the
# "to", "from", "cc", and "bcc" headers. # "to", "from", "cc", "bcc" and "reply-to" headers.
def quote_address_if_necessary(address, charset) def quote_address_if_necessary(address, charset)
if Array === address if Array === address
address.map { |a| quote_address_if_necessary(a, charset) } address.map { |a| quote_address_if_necessary(a, charset) }

View file

@ -8,11 +8,13 @@ module ActionMailer
"test case definition" "test case definition"
end end
end end
# New Test Super class for forward compatibility.
# To override
class TestCase < ActiveSupport::TestCase class TestCase < ActiveSupport::TestCase
include ActionMailer::Quoting include ActionMailer::Quoting
setup :initialize_test_deliveries
setup :set_expected_mail
class << self class << self
def tests(mailer) def tests(mailer)
write_inheritable_attribute(:mailer_class, mailer) write_inheritable_attribute(:mailer_class, mailer)
@ -33,15 +35,18 @@ module ActionMailer
end end
end end
def setup protected
ActionMailer::Base.delivery_method = :test def initialize_test_deliveries
ActionMailer::Base.perform_deliveries = true ActionMailer::Base.delivery_method = :test
ActionMailer::Base.deliveries = [] ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
end
@expected = TMail::Mail.new def set_expected_mail
@expected.set_content_type "text", "plain", { "charset" => charset } @expected = TMail::Mail.new
@expected.mime_version = '1.0' @expected.set_content_type "text", "plain", { "charset" => charset }
end @expected.mime_version = '1.0'
end
private private
def charset def charset

View file

@ -2,9 +2,9 @@
require 'rubygems' require 'rubygems'
begin begin
gem 'tmail', '~> 1.1.0' gem 'tmail', '~> 1.2.3'
rescue Gem::LoadError rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/vendor/tmail-1.1.0" $:.unshift "#{File.dirname(__FILE__)}/vendor/tmail-1.2.3"
end end
begin begin

View file

@ -1,19 +0,0 @@
#
# lib/tmail/Makefile
#
debug:
rm -f parser.rb
make parser.rb DEBUG=true
parser.rb: parser.y
if [ "$(DEBUG)" = true ]; then \
racc -v -g -o$@ parser.y ;\
else \
racc -E -o$@ parser.y ;\
fi
clean:
rm -f parser.rb parser.output
distclean: clean

View file

@ -1,245 +0,0 @@
=begin rdoc
= Address handling class
=end
#
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
require 'tmail/encode'
require 'tmail/parser'
module TMail
class Address
include TextUtils
def Address.parse( str )
Parser.parse :ADDRESS, str
end
def address_group?
false
end
def initialize( local, domain )
if domain
domain.each do |s|
raise SyntaxError, 'empty word in domain' if s.empty?
end
end
@local = local
@domain = domain
@name = nil
@routes = []
end
attr_reader :name
def name=( str )
@name = str
@name = nil if str and str.empty?
end
alias phrase name
alias phrase= name=
attr_reader :routes
def inspect
"#<#{self.class} #{address()}>"
end
def local
return nil unless @local
return '""' if @local.size == 1 and @local[0].empty?
@local.map {|i| quote_atom(i) }.join('.')
end
def domain
return nil unless @domain
join_domain(@domain)
end
def spec
s = self.local
d = self.domain
if s and d
s + '@' + d
else
s
end
end
alias address spec
def ==( other )
other.respond_to? :spec and self.spec == other.spec
end
alias eql? ==
def hash
@local.hash ^ @domain.hash
end
def dup
obj = self.class.new(@local.dup, @domain.dup)
obj.name = @name.dup if @name
obj.routes.replace @routes
obj
end
include StrategyInterface
def accept( strategy, dummy1 = nil, dummy2 = nil )
unless @local
strategy.meta '<>' # empty return-path
return
end
spec_p = (not @name and @routes.empty?)
if @name
strategy.phrase @name
strategy.space
end
tmp = spec_p ? '' : '<'
unless @routes.empty?
tmp << @routes.map {|i| '@' + i }.join(',') << ':'
end
tmp << self.spec
tmp << '>' unless spec_p
strategy.meta tmp
strategy.lwsp ''
end
end
class AddressGroup
include Enumerable
def address_group?
true
end
def initialize( name, addrs )
@name = name
@addresses = addrs
end
attr_reader :name
def ==( other )
other.respond_to? :to_a and @addresses == other.to_a
end
alias eql? ==
def hash
map {|i| i.hash }.hash
end
def []( idx )
@addresses[idx]
end
def size
@addresses.size
end
def empty?
@addresses.empty?
end
def each( &block )
@addresses.each(&block)
end
def to_a
@addresses.dup
end
alias to_ary to_a
def include?( a )
@addresses.include? a
end
def flatten
set = []
@addresses.each do |a|
if a.respond_to? :flatten
set.concat a.flatten
else
set.push a
end
end
set
end
def each_address( &block )
flatten.each(&block)
end
def add( a )
@addresses.push a
end
alias push add
def delete( a )
@addresses.delete a
end
include StrategyInterface
def accept( strategy, dummy1 = nil, dummy2 = nil )
strategy.phrase @name
strategy.meta ':'
strategy.space
first = true
each do |mbox|
if first
first = false
else
strategy.meta ','
end
strategy.space
mbox.accept strategy
end
strategy.meta ';'
strategy.lwsp ''
end
end
end # module TMail

View file

@ -1,552 +0,0 @@
#
# facade.rb
#
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
require 'tmail/utils'
module TMail
class Mail
def header_string( name, default = nil )
h = @header[name.downcase] or return default
h.to_s
end
###
### attributes
###
include TextUtils
def set_string_array_attr( key, strs )
strs.flatten!
if strs.empty?
@header.delete key.downcase
else
store key, strs.join(', ')
end
strs
end
private :set_string_array_attr
def set_string_attr( key, str )
if str
store key, str
else
@header.delete key.downcase
end
str
end
private :set_string_attr
def set_addrfield( name, arg )
if arg
h = HeaderField.internal_new(name, @config)
h.addrs.replace [arg].flatten
@header[name] = h
else
@header.delete name
end
arg
end
private :set_addrfield
def addrs2specs( addrs )
return nil unless addrs
list = addrs.map {|addr|
if addr.address_group?
then addr.map {|a| a.spec }
else addr.spec
end
}.flatten
return nil if list.empty?
list
end
private :addrs2specs
#
# date time
#
def date( default = nil )
if h = @header['date']
h.date
else
default
end
end
def date=( time )
if time
store 'Date', time2str(time)
else
@header.delete 'date'
end
time
end
def strftime( fmt, default = nil )
if t = date
t.strftime(fmt)
else
default
end
end
#
# destination
#
def to_addrs( default = nil )
if h = @header['to']
h.addrs
else
default
end
end
def cc_addrs( default = nil )
if h = @header['cc']
h.addrs
else
default
end
end
def bcc_addrs( default = nil )
if h = @header['bcc']
h.addrs
else
default
end
end
def to_addrs=( arg )
set_addrfield 'to', arg
end
def cc_addrs=( arg )
set_addrfield 'cc', arg
end
def bcc_addrs=( arg )
set_addrfield 'bcc', arg
end
def to( default = nil )
addrs2specs(to_addrs(nil)) || default
end
def cc( default = nil )
addrs2specs(cc_addrs(nil)) || default
end
def bcc( default = nil )
addrs2specs(bcc_addrs(nil)) || default
end
def to=( *strs )
set_string_array_attr 'To', strs
end
def cc=( *strs )
set_string_array_attr 'Cc', strs
end
def bcc=( *strs )
set_string_array_attr 'Bcc', strs
end
#
# originator
#
def from_addrs( default = nil )
if h = @header['from']
h.addrs
else
default
end
end
def from_addrs=( arg )
set_addrfield 'from', arg
end
def from( default = nil )
addrs2specs(from_addrs(nil)) || default
end
def from=( *strs )
set_string_array_attr 'From', strs
end
def friendly_from( default = nil )
h = @header['from']
a, = h.addrs
return default unless a
return a.phrase if a.phrase
return h.comments.join(' ') unless h.comments.empty?
a.spec
end
def reply_to_addrs( default = nil )
if h = @header['reply-to']
h.addrs
else
default
end
end
def reply_to_addrs=( arg )
set_addrfield 'reply-to', arg
end
def reply_to( default = nil )
addrs2specs(reply_to_addrs(nil)) || default
end
def reply_to=( *strs )
set_string_array_attr 'Reply-To', strs
end
def sender_addr( default = nil )
f = @header['sender'] or return default
f.addr or return default
end
def sender_addr=( addr )
if addr
h = HeaderField.internal_new('sender', @config)
h.addr = addr
@header['sender'] = h
else
@header.delete 'sender'
end
addr
end
def sender( default )
f = @header['sender'] or return default
a = f.addr or return default
a.spec
end
def sender=( str )
set_string_attr 'Sender', str
end
#
# subject
#
def subject( default = nil )
if h = @header['subject']
h.body
else
default
end
end
alias quoted_subject subject
def subject=( str )
set_string_attr 'Subject', str
end
#
# identity & threading
#
def message_id( default = nil )
if h = @header['message-id']
h.id || default
else
default
end
end
def message_id=( str )
set_string_attr 'Message-Id', str
end
def in_reply_to( default = nil )
if h = @header['in-reply-to']
h.ids
else
default
end
end
def in_reply_to=( *idstrs )
set_string_array_attr 'In-Reply-To', idstrs
end
def references( default = nil )
if h = @header['references']
h.refs
else
default
end
end
def references=( *strs )
set_string_array_attr 'References', strs
end
#
# MIME headers
#
def mime_version( default = nil )
if h = @header['mime-version']
h.version || default
else
default
end
end
def mime_version=( m, opt = nil )
if opt
if h = @header['mime-version']
h.major = m
h.minor = opt
else
store 'Mime-Version', "#{m}.#{opt}"
end
else
store 'Mime-Version', m
end
m
end
def content_type( default = nil )
if h = @header['content-type']
h.content_type || default
else
default
end
end
def main_type( default = nil )
if h = @header['content-type']
h.main_type || default
else
default
end
end
def sub_type( default = nil )
if h = @header['content-type']
h.sub_type || default
else
default
end
end
def set_content_type( str, sub = nil, param = nil )
if sub
main, sub = str, sub
else
main, sub = str.split(%r</>, 2)
raise ArgumentError, "sub type missing: #{str.inspect}" unless sub
end
if h = @header['content-type']
h.main_type = main
h.sub_type = sub
h.params.clear
else
store 'Content-Type', "#{main}/#{sub}"
end
@header['content-type'].params.replace param if param
str
end
alias content_type= set_content_type
def type_param( name, default = nil )
if h = @header['content-type']
h[name] || default
else
default
end
end
def charset( default = nil )
if h = @header['content-type']
h['charset'] or default
else
default
end
end
def charset=( str )
if str
if h = @header[ 'content-type' ]
h['charset'] = str
else
store 'Content-Type', "text/plain; charset=#{str}"
end
end
str
end
def transfer_encoding( default = nil )
if h = @header['content-transfer-encoding']
h.encoding || default
else
default
end
end
def transfer_encoding=( str )
set_string_attr 'Content-Transfer-Encoding', str
end
alias encoding transfer_encoding
alias encoding= transfer_encoding=
alias content_transfer_encoding transfer_encoding
alias content_transfer_encoding= transfer_encoding=
def disposition( default = nil )
if h = @header['content-disposition']
h.disposition || default
else
default
end
end
alias content_disposition disposition
def set_disposition( str, params = nil )
if h = @header['content-disposition']
h.disposition = str
h.params.clear
else
store('Content-Disposition', str)
h = @header['content-disposition']
end
h.params.replace params if params
end
alias disposition= set_disposition
alias set_content_disposition set_disposition
alias content_disposition= set_disposition
def disposition_param( name, default = nil )
if h = @header['content-disposition']
h[name] || default
else
default
end
end
###
### utils
###
def create_reply
mail = TMail::Mail.parse('')
mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '')
mail.to_addrs = reply_addresses([])
mail.in_reply_to = [message_id(nil)].compact
mail.references = references([]) + [message_id(nil)].compact
mail.mime_version = '1.0'
mail
end
def base64_encode
store 'Content-Transfer-Encoding', 'Base64'
self.body = Base64.folding_encode(self.body)
end
def base64_decode
if /base64/i === self.transfer_encoding('')
store 'Content-Transfer-Encoding', '8bit'
self.body = Base64.decode(self.body, @config.strict_base64decode?)
end
end
def destinations( default = nil )
ret = []
%w( to cc bcc ).each do |nm|
if h = @header[nm]
h.addrs.each {|i| ret.push i.address }
end
end
ret.empty? ? default : ret
end
def each_destination( &block )
destinations([]).each do |i|
if Address === i
yield i
else
i.each(&block)
end
end
end
alias each_dest each_destination
def reply_addresses( default = nil )
reply_to_addrs(nil) or from_addrs(nil) or default
end
def error_reply_addresses( default = nil )
if s = sender(nil)
[s]
else
from_addrs(default)
end
end
def multipart?
main_type('').downcase == 'multipart'
end
end # class Mail
end # module TMail

View file

@ -1,35 +0,0 @@
#
# info.rb
#
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
module TMail
Version = '0.10.7'
Copyright = 'Copyright (c) 1998-2002 Minero Aoki'
end

View file

@ -1,540 +0,0 @@
=begin rdoc
= Facade.rb Provides an interface to the TMail object
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
require 'tmail/utils'
module TMail
class Mail
def header_string( name, default = nil )
h = @header[name.downcase] or return default
h.to_s
end
###
### attributes
###
include TextUtils
def set_string_array_attr( key, strs )
strs.flatten!
if strs.empty?
@header.delete key.downcase
else
store key, strs.join(', ')
end
strs
end
private :set_string_array_attr
def set_string_attr( key, str )
if str
store key, str
else
@header.delete key.downcase
end
str
end
private :set_string_attr
def set_addrfield( name, arg )
if arg
h = HeaderField.internal_new(name, @config)
h.addrs.replace [arg].flatten
@header[name] = h
else
@header.delete name
end
arg
end
private :set_addrfield
def addrs2specs( addrs )
return nil unless addrs
list = addrs.map {|addr|
if addr.address_group?
then addr.map {|a| a.spec }
else addr.spec
end
}.flatten
return nil if list.empty?
list
end
private :addrs2specs
#
# date time
#
def date( default = nil )
if h = @header['date']
h.date
else
default
end
end
def date=( time )
if time
store 'Date', time2str(time)
else
@header.delete 'date'
end
time
end
def strftime( fmt, default = nil )
if t = date
t.strftime(fmt)
else
default
end
end
#
# destination
#
def to_addrs( default = nil )
if h = @header['to']
h.addrs
else
default
end
end
def cc_addrs( default = nil )
if h = @header['cc']
h.addrs
else
default
end
end
def bcc_addrs( default = nil )
if h = @header['bcc']
h.addrs
else
default
end
end
def to_addrs=( arg )
set_addrfield 'to', arg
end
def cc_addrs=( arg )
set_addrfield 'cc', arg
end
def bcc_addrs=( arg )
set_addrfield 'bcc', arg
end
def to( default = nil )
addrs2specs(to_addrs(nil)) || default
end
def cc( default = nil )
addrs2specs(cc_addrs(nil)) || default
end
def bcc( default = nil )
addrs2specs(bcc_addrs(nil)) || default
end
def to=( *strs )
set_string_array_attr 'To', strs
end
def cc=( *strs )
set_string_array_attr 'Cc', strs
end
def bcc=( *strs )
set_string_array_attr 'Bcc', strs
end
#
# originator
#
def from_addrs( default = nil )
if h = @header['from']
h.addrs
else
default
end
end
def from_addrs=( arg )
set_addrfield 'from', arg
end
def from( default = nil )
addrs2specs(from_addrs(nil)) || default
end
def from=( *strs )
set_string_array_attr 'From', strs
end
def friendly_from( default = nil )
h = @header['from']
a, = h.addrs
return default unless a
return a.phrase if a.phrase
return h.comments.join(' ') unless h.comments.empty?
a.spec
end
def reply_to_addrs( default = nil )
if h = @header['reply-to']
h.addrs
else
default
end
end
def reply_to_addrs=( arg )
set_addrfield 'reply-to', arg
end
def reply_to( default = nil )
addrs2specs(reply_to_addrs(nil)) || default
end
def reply_to=( *strs )
set_string_array_attr 'Reply-To', strs
end
def sender_addr( default = nil )
f = @header['sender'] or return default
f.addr or return default
end
def sender_addr=( addr )
if addr
h = HeaderField.internal_new('sender', @config)
h.addr = addr
@header['sender'] = h
else
@header.delete 'sender'
end
addr
end
def sender( default )
f = @header['sender'] or return default
a = f.addr or return default
a.spec
end
def sender=( str )
set_string_attr 'Sender', str
end
#
# subject
#
def subject( default = nil )
if h = @header['subject']
h.body
else
default
end
end
alias quoted_subject subject
def subject=( str )
set_string_attr 'Subject', str
end
#
# identity & threading
#
def message_id( default = nil )
if h = @header['message-id']
h.id || default
else
default
end
end
def message_id=( str )
set_string_attr 'Message-Id', str
end
def in_reply_to( default = nil )
if h = @header['in-reply-to']
h.ids
else
default
end
end
def in_reply_to=( *idstrs )
set_string_array_attr 'In-Reply-To', idstrs
end
def references( default = nil )
if h = @header['references']
h.refs
else
default
end
end
def references=( *strs )
set_string_array_attr 'References', strs
end
#
# MIME headers
#
def mime_version( default = nil )
if h = @header['mime-version']
h.version || default
else
default
end
end
def mime_version=( m, opt = nil )
if opt
if h = @header['mime-version']
h.major = m
h.minor = opt
else
store 'Mime-Version', "#{m}.#{opt}"
end
else
store 'Mime-Version', m
end
m
end
def content_type( default = nil )
if h = @header['content-type']
h.content_type || default
else
default
end
end
def main_type( default = nil )
if h = @header['content-type']
h.main_type || default
else
default
end
end
def sub_type( default = nil )
if h = @header['content-type']
h.sub_type || default
else
default
end
end
def set_content_type( str, sub = nil, param = nil )
if sub
main, sub = str, sub
else
main, sub = str.split(%r</>, 2)
raise ArgumentError, "sub type missing: #{str.inspect}" unless sub
end
if h = @header['content-type']
h.main_type = main
h.sub_type = sub
h.params.clear
else
store 'Content-Type', "#{main}/#{sub}"
end
@header['content-type'].params.replace param if param
str
end
alias content_type= set_content_type
def type_param( name, default = nil )
if h = @header['content-type']
h[name] || default
else
default
end
end
def charset( default = nil )
if h = @header['content-type']
h['charset'] or default
else
default
end
end
def charset=( str )
if str
if h = @header[ 'content-type' ]
h['charset'] = str
else
store 'Content-Type', "text/plain; charset=#{str}"
end
end
str
end
def transfer_encoding( default = nil )
if h = @header['content-transfer-encoding']
h.encoding || default
else
default
end
end
def transfer_encoding=( str )
set_string_attr 'Content-Transfer-Encoding', str
end
alias encoding transfer_encoding
alias encoding= transfer_encoding=
alias content_transfer_encoding transfer_encoding
alias content_transfer_encoding= transfer_encoding=
def disposition( default = nil )
if h = @header['content-disposition']
h.disposition || default
else
default
end
end
alias content_disposition disposition
def set_disposition( str, params = nil )
if h = @header['content-disposition']
h.disposition = str
h.params.clear
else
store('Content-Disposition', str)
h = @header['content-disposition']
end
h.params.replace params if params
end
alias disposition= set_disposition
alias set_content_disposition set_disposition
alias content_disposition= set_disposition
def disposition_param( name, default = nil )
if h = @header['content-disposition']
h[name] || default
else
default
end
end
###
### utils
###
def create_reply
mail = TMail::Mail.parse('')
mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '')
mail.to_addrs = reply_addresses([])
mail.in_reply_to = [message_id(nil)].compact
mail.references = references([]) + [message_id(nil)].compact
mail.mime_version = '1.0'
mail
end
def base64_encode
store 'Content-Transfer-Encoding', 'Base64'
self.body = Base64.folding_encode(self.body)
end
def base64_decode
if /base64/i === self.transfer_encoding('')
store 'Content-Transfer-Encoding', '8bit'
self.body = Base64.decode(self.body, @config.strict_base64decode?)
end
end
def destinations( default = nil )
ret = []
%w( to cc bcc ).each do |nm|
if h = @header[nm]
h.addrs.each {|i| ret.push i.address }
end
end
ret.empty? ? default : ret
end
def each_destination( &block )
destinations([]).each do |i|
if Address === i
yield i
else
i.each(&block)
end
end
end
alias each_dest each_destination
def reply_addresses( default = nil )
reply_to_addrs(nil) or from_addrs(nil) or default
end
def error_reply_addresses( default = nil )
if s = sender(nil)
[s]
else
from_addrs(default)
end
end
def multipart?
main_type('').downcase == 'multipart'
end
end # class Mail
end # module TMail

View file

@ -1,381 +0,0 @@
#
# parser.y
#
# Copyright (c) 1998-2007 Minero Aoki
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2.1.
#
class TMail::Parser
options no_result_var
rule
content : DATETIME datetime { val[1] }
| RECEIVED received { val[1] }
| MADDRESS addrs_TOP { val[1] }
| RETPATH retpath { val[1] }
| KEYWORDS keys { val[1] }
| ENCRYPTED enc { val[1] }
| MIMEVERSION version { val[1] }
| CTYPE ctype { val[1] }
| CENCODING cencode { val[1] }
| CDISPOSITION cdisp { val[1] }
| ADDRESS addr_TOP { val[1] }
| MAILBOX mbox { val[1] }
datetime : day DIGIT ATOM DIGIT hour zone
# 0 1 2 3 4 5
# date month year
{
t = Time.gm(val[3].to_i, val[2], val[1].to_i, 0, 0, 0)
(t + val[4] - val[5]).localtime
}
day : /* none */
| ATOM ','
hour : DIGIT ':' DIGIT
{
(val[0].to_i * 60 * 60) +
(val[2].to_i * 60)
}
| DIGIT ':' DIGIT ':' DIGIT
{
(val[0].to_i * 60 * 60) +
(val[2].to_i * 60) +
(val[4].to_i)
}
zone : ATOM
{
timezone_string_to_unixtime(val[0])
}
received : from by via with id for received_datetime
{
val
}
from : /* none */
| FROM received_domain
{
val[1]
}
by : /* none */
| BY received_domain
{
val[1]
}
received_domain
: domain
{
join_domain(val[0])
}
| domain '@' domain
{
join_domain(val[2])
}
| domain DOMLIT
{
join_domain(val[0])
}
via : /* none */
| VIA ATOM
{
val[1]
}
with : /* none */
{
[]
}
| with WITH ATOM
{
val[0].push val[2]
val[0]
}
id : /* none */
| ID msgid
{
val[1]
}
| ID ATOM
{
val[1]
}
for : /* none */
| FOR received_addrspec
{
val[1]
}
received_addrspec
: routeaddr
{
val[0].spec
}
| spec
{
val[0].spec
}
received_datetime
: /* none */
| ';' datetime
{
val[1]
}
addrs_TOP : addrs
| group_bare
| addrs commas group_bare
addr_TOP : mbox
| group
| group_bare
retpath : addrs_TOP
| '<' '>' { [ Address.new(nil, nil) ] }
addrs : addr
{
val
}
| addrs commas addr
{
val[0].push val[2]
val[0]
}
addr : mbox
| group
mboxes : mbox
{
val
}
| mboxes commas mbox
{
val[0].push val[2]
val[0]
}
mbox : spec
| routeaddr
| addr_phrase routeaddr
{
val[1].phrase = Decoder.decode(val[0])
val[1]
}
group : group_bare ';'
group_bare: addr_phrase ':' mboxes
{
AddressGroup.new(val[0], val[2])
}
| addr_phrase ':' { AddressGroup.new(val[0], []) }
addr_phrase
: local_head { val[0].join('.') }
| addr_phrase local_head { val[0] << ' ' << val[1].join('.') }
routeaddr : '<' routes spec '>'
{
val[2].routes.replace val[1]
val[2]
}
| '<' spec '>'
{
val[1]
}
routes : at_domains ':'
at_domains: '@' domain { [ val[1].join('.') ] }
| at_domains ',' '@' domain { val[0].push val[3].join('.'); val[0] }
spec : local '@' domain { Address.new( val[0], val[2] ) }
| local { Address.new( val[0], nil ) }
local: local_head
| local_head '.' { val[0].push ''; val[0] }
local_head: word
{ val }
| local_head dots word
{
val[1].times do
val[0].push ''
end
val[0].push val[2]
val[0]
}
domain : domword
{ val }
| domain dots domword
{
val[1].times do
val[0].push ''
end
val[0].push val[2]
val[0]
}
dots : '.' { 0 }
| '.' '.' { 1 }
word : atom
| QUOTED
| DIGIT
domword : atom
| DOMLIT
| DIGIT
commas : ','
| commas ','
msgid : '<' spec '>'
{
val[1] = val[1].spec
val.join('')
}
keys : phrase { val }
| keys ',' phrase { val[0].push val[2]; val[0] }
phrase : word
| phrase word { val[0] << ' ' << val[1] }
enc : word
{
val.push nil
val
}
| word word
{
val
}
version : DIGIT '.' DIGIT
{
[ val[0].to_i, val[2].to_i ]
}
ctype : TOKEN '/' TOKEN params opt_semicolon
{
[ val[0].downcase, val[2].downcase, decode_params(val[3]) ]
}
| TOKEN params opt_semicolon
{
[ val[0].downcase, nil, decode_params(val[1]) ]
}
params : /* none */
{
{}
}
| params ';' TOKEN '=' QUOTED
{
val[0][ val[2].downcase ] = ('"' + val[4].to_s + '"')
val[0]
}
| params ';' TOKEN '=' TOKEN
{
val[0][ val[2].downcase ] = val[4]
val[0]
}
cencode : TOKEN
{
val[0].downcase
}
cdisp : TOKEN params opt_semicolon
{
[ val[0].downcase, decode_params(val[1]) ]
}
opt_semicolon
:
| ';'
atom : ATOM
| FROM
| BY
| VIA
| WITH
| ID
| FOR
end
---- header
#
# parser.rb
#
# Copyright (c) 1998-2007 Minero Aoki
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2.1.
#
require 'tmail/scanner'
require 'tmail/utils'
---- inner
include TextUtils
def self.parse( ident, str, cmt = nil )
new.parse(ident, str, cmt)
end
MAILP_DEBUG = false
def initialize
self.debug = MAILP_DEBUG
end
def debug=( flag )
@yydebug = flag && Racc_debug_parser
@scanner_debug = flag
end
def debug
@yydebug
end
def parse( ident, str, comments = nil )
@scanner = Scanner.new(str, ident, comments)
@scanner.debug = @scanner_debug
@first = [ident, ident]
result = yyparse(self, :parse_in)
comments.map! {|c| to_kcode(c) } if comments
result
end
private
def parse_in( &block )
yield @first
@scanner.scan(&block)
end
def on_error( t, val, vstack )
raise SyntaxError, "parse error on token #{racc_token2str t}"
end

View file

@ -1 +0,0 @@
require 'tmail'

View file

@ -2,3 +2,4 @@ require 'tmail/version'
require 'tmail/mail' require 'tmail/mail'
require 'tmail/mailbox' require 'tmail/mailbox'
require 'tmail/core_extensions' require 'tmail/core_extensions'
require 'tmail/net'

View file

@ -0,0 +1,426 @@
=begin rdoc
= Address handling class
=end
#--
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki.
#++
require 'tmail/encode'
require 'tmail/parser'
module TMail
# = Class Address
#
# Provides a complete handling library for email addresses. Can parse a string of an
# address directly or take in preformatted addresses themseleves. Allows you to add
# and remove phrases from the front of the address and provides a compare function for
# email addresses.
#
# == Parsing and Handling a Valid Address:
#
# Just pass the email address in as a string to Address.parse:
#
# email = TMail::Address.parse('Mikel Lindsaar <mikel@lindsaar.net>)
# #=> #<TMail::Address mikel@lindsaar.net>
# email.address
# #=> "mikel@lindsaar.net"
# email.local
# #=> "mikel"
# email.domain
# #=> "lindsaar.net"
# email.name # Aliased as phrase as well
# #=> "Mikel Lindsaar"
#
# == Detecting an Invalid Address
#
# If you want to check the syntactical validity of an email address, just pass it to
# Address.parse and catch any SyntaxError:
#
# begin
# TMail::Mail.parse("mikel 2@@@@@ me .com")
# rescue TMail::SyntaxError
# puts("Invalid Email Address Detected")
# else
# puts("Address is valid")
# end
# #=> "Invalid Email Address Detected"
class Address
include TextUtils #:nodoc:
# Sometimes you need to parse an address, TMail can do it for you and provide you with
# a fairly robust method of detecting a valid address.
#
# Takes in a string, returns a TMail::Address object.
#
# Raises a TMail::SyntaxError on invalid email format
def Address.parse( str )
Parser.parse :ADDRESS, special_quote_address(str)
end
def Address.special_quote_address(str) #:nodoc:
# Takes a string which is an address and adds quotation marks to special
# edge case methods that the RACC parser can not handle.
#
# Right now just handles two edge cases:
#
# Full stop as the last character of the display name:
# Mikel L. <mikel@me.com>
# Returns:
# "Mikel L." <mikel@me.com>
#
# Unquoted @ symbol in the display name:
# mikel@me.com <mikel@me.com>
# Returns:
# "mikel@me.com" <mikel@me.com>
#
# Any other address not matching these patterns just gets returned as is.
case
# This handles the missing "" in an older version of Apple Mail.app
# around the display name when the display name contains a '@'
# like 'mikel@me.com <mikel@me.com>'
# Just quotes it to: '"mikel@me.com" <mikel@me.com>'
when str =~ /\A([^"].+@.+[^"])\s(<.*?>)\Z/
return "\"#{$1}\" #{$2}"
# This handles cases where 'Mikel A. <mikel@me.com>' which is a trailing
# full stop before the address section. Just quotes it to
# '"Mikel A. <mikel@me.com>"
when str =~ /\A(.*?\.)\s(<.*?>)\Z/
return "\"#{$1}\" #{$2}"
else
str
end
end
def address_group? #:nodoc:
false
end
# Address.new(local, domain)
#
# Accepts:
#
# * local - Left of the at symbol
#
# * domain - Array of the domain split at the periods.
#
# For example:
#
# Address.new("mikel", ["lindsaar", "net"])
# #=> "#<TMail::Address mikel@lindsaar.net>"
def initialize( local, domain )
if domain
domain.each do |s|
raise SyntaxError, 'empty word in domain' if s.empty?
end
end
# This is to catch an unquoted "@" symbol in the local part of the
# address. Handles addresses like <"@"@me.com> and makes sure they
# stay like <"@"@me.com> (previously were becomming <@@me.com>)
if local && (local.join == '@' || local.join =~ /\A[^"].*?@.*?[^"]\Z/)
@local = "\"#{local.join}\""
else
@local = local
end
@domain = domain
@name = nil
@routes = []
end
# Provides the name or 'phrase' of the email address.
#
# For Example:
#
# email = TMail::Address.parse("Mikel Lindsaar <mikel@lindsaar.net>")
# email.name
# #=> "Mikel Lindsaar"
def name
@name
end
# Setter method for the name or phrase of the email
#
# For Example:
#
# email = TMail::Address.parse("mikel@lindsaar.net")
# email.name
# #=> nil
# email.name = "Mikel Lindsaar"
# email.to_s
# #=> "Mikel Lindsaar <mikel@me.com>"
def name=( str )
@name = str
@name = nil if str and str.empty?
end
#:stopdoc:
alias phrase name
alias phrase= name=
#:startdoc:
# This is still here from RFC 822, and is now obsolete per RFC2822 Section 4.
#
# "When interpreting addresses, the route portion SHOULD be ignored."
#
# It is still here, so you can access it.
#
# Routes return the route portion at the front of the email address, if any.
#
# For Example:
# email = TMail::Address.parse( "<@sa,@another:Mikel@me.com>")
# => #<TMail::Address Mikel@me.com>
# email.to_s
# => "<@sa,@another:Mikel@me.com>"
# email.routes
# => ["sa", "another"]
def routes
@routes
end
def inspect #:nodoc:
"#<#{self.class} #{address()}>"
end
# Returns the local part of the email address
#
# For Example:
#
# email = TMail::Address.parse("mikel@lindsaar.net")
# email.local
# #=> "mikel"
def local
return nil unless @local
return '""' if @local.size == 1 and @local[0].empty?
# Check to see if it is an array before trying to map it
if @local.respond_to?(:map)
@local.map {|i| quote_atom(i) }.join('.')
else
quote_atom(@local)
end
end
# Returns the domain part of the email address
#
# For Example:
#
# email = TMail::Address.parse("mikel@lindsaar.net")
# email.local
# #=> "lindsaar.net"
def domain
return nil unless @domain
join_domain(@domain)
end
# Returns the full specific address itself
#
# For Example:
#
# email = TMail::Address.parse("mikel@lindsaar.net")
# email.address
# #=> "mikel@lindsaar.net"
def spec
s = self.local
d = self.domain
if s and d
s + '@' + d
else
s
end
end
alias address spec
# Provides == function to the email. Only checks the actual address
# and ignores the name/phrase component
#
# For Example
#
# addr1 = TMail::Address.parse("My Address <mikel@lindsaar.net>")
# #=> "#<TMail::Address mikel@lindsaar.net>"
# addr2 = TMail::Address.parse("Another <mikel@lindsaar.net>")
# #=> "#<TMail::Address mikel@lindsaar.net>"
# addr1 == addr2
# #=> true
def ==( other )
other.respond_to? :spec and self.spec == other.spec
end
alias eql? ==
# Provides a unique hash value for this record against the local and domain
# parts, ignores the name/phrase value
#
# email = TMail::Address.parse("mikel@lindsaar.net")
# email.hash
# #=> 18767598
def hash
@local.hash ^ @domain.hash
end
# Duplicates a TMail::Address object returning the duplicate
#
# addr1 = TMail::Address.parse("mikel@lindsaar.net")
# addr2 = addr1.dup
# addr1.id == addr2.id
# #=> false
def dup
obj = self.class.new(@local.dup, @domain.dup)
obj.name = @name.dup if @name
obj.routes.replace @routes
obj
end
include StrategyInterface #:nodoc:
def accept( strategy, dummy1 = nil, dummy2 = nil ) #:nodoc:
unless @local
strategy.meta '<>' # empty return-path
return
end
spec_p = (not @name and @routes.empty?)
if @name
strategy.phrase @name
strategy.space
end
tmp = spec_p ? '' : '<'
unless @routes.empty?
tmp << @routes.map {|i| '@' + i }.join(',') << ':'
end
tmp << self.spec
tmp << '>' unless spec_p
strategy.meta tmp
strategy.lwsp ''
end
end
class AddressGroup
include Enumerable
def address_group?
true
end
def initialize( name, addrs )
@name = name
@addresses = addrs
end
attr_reader :name
def ==( other )
other.respond_to? :to_a and @addresses == other.to_a
end
alias eql? ==
def hash
map {|i| i.hash }.hash
end
def []( idx )
@addresses[idx]
end
def size
@addresses.size
end
def empty?
@addresses.empty?
end
def each( &block )
@addresses.each(&block)
end
def to_a
@addresses.dup
end
alias to_ary to_a
def include?( a )
@addresses.include? a
end
def flatten
set = []
@addresses.each do |a|
if a.respond_to? :flatten
set.concat a.flatten
else
set.push a
end
end
set
end
def each_address( &block )
flatten.each(&block)
end
def add( a )
@addresses.push a
end
alias push add
def delete( a )
@addresses.delete a
end
include StrategyInterface
def accept( strategy, dummy1 = nil, dummy2 = nil )
strategy.phrase @name
strategy.meta ':'
strategy.space
first = true
each do |mbox|
if first
first = false
else
strategy.meta ','
end
strategy.space
mbox.accept strategy
end
strategy.meta ';'
strategy.lwsp ''
end
end
end # module TMail

View file

@ -1,6 +1,6 @@
=begin rdoc =begin rdoc
= Attachment handling class = Attachment handling file
=end =end
@ -17,8 +17,7 @@ module TMail
end end
def attachment?(part) def attachment?(part)
(part['content-disposition'] && part['content-disposition'].disposition == "attachment") || part.disposition_is_attachment? || part.content_type_is_text?
part.header['content-type'].main_type != "text"
end end
def attachments def attachments

View file

@ -1,9 +1,4 @@
# = TITLE: #--
#
# Base64
#
# = COPYRIGHT:
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net> # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
@ -27,10 +22,9 @@
# #
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
# with permission of Minero Aoki. # with permission of Minero Aoki.
#++
# #:stopdoc:
module TMail module TMail
module Base64 module Base64
module_function module_function
@ -48,5 +42,5 @@ module TMail
end end
end end
end end
#:startdoc:

Some files were not shown because too many files have changed in this diff Show more