Grosse MàJ

This commit is contained in:
olivier
2008-11-25 22:11:16 +01:00
parent 53195fdfcd
commit 3e719157ea
2980 changed files with 343846 additions and 0 deletions

View File

@ -0,0 +1,18 @@
Copyright (c) 2007 PJ Hyett and Mislav Marohnić
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.

View File

@ -0,0 +1,119 @@
= WillPaginate
Pagination is just limiting the number of records displayed. Why should you let
it get in your way while doing more important tasks on your project? This
plugin makes magic happen. Ever wanted to be able to do just this:
Post.paginate :page => 1
... and then render the page links with a single call to a view helper? Well,
now you can. Simply:
script/plugin install svn://errtheblog.com/svn/plugins/will_paginate
Ryan Bates made an awesome screencast[http://railscasts.com/episodes/51], check
it out.
== Example usage:
Use a paginate finder in the controller:
@posts = Post.paginate_by_board_id @board.id, :page => params[:page]
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
+Post.per_page+. 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 30 if missing.)
You can also 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, but 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 %>
== Authors, credits, contact!
REPORT BUGS on Lighthouse: http://err.lighthouseapp.com/projects/466-plugins/overview
BROWSE SOURCE on Warehouse: http://plugins.require.errtheblog.com/browser/will_paginate
Want to discuss, request features, ask questions? Join the Google group:
http://groups.google.com/group/will_paginate
Ruby port by: PJ Hyett, Mislav Marohnić (Sulien)
Original announcement: http://errtheblog.com/post/929
Original PHP source: http://www.strangerstudios.com/sandbox/pagination/diggstyle.php
Contributors: Chris Wanstrath, Dr. Nic Williams, K. Adam Christensen,
Mike Garey, Bence Golda, Matt Aimonetti, Charles Brian Quinn,
Desi McAdam, James Coglan, Matijs van Zuijlen
== Want Digg style?
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;
}

View File

@ -0,0 +1,26 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the will_paginate plugin.'
Rake::TestTask.new(:test) do |t|
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate RDoc documentation for the will_paginate plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
files = ['README', 'LICENSE', 'lib/**/*.rb']
rdoc.rdoc_files.add(files)
rdoc.main = "README" # page to start on
rdoc.title = "will_paginate"
templates = %w[/Users/chris/ruby/projects/err/rock/template.rb /var/www/rock/template.rb]
rdoc.template = templates.find { |t| File.exists? t }
rdoc.rdoc_dir = 'doc' # rdoc output folder
rdoc.options << '--inline-source'
end

View File

@ -0,0 +1,4 @@
unless ActiveRecord::Base.respond_to? :paginate
require 'will_paginate'
WillPaginate.enable
end

View File

@ -0,0 +1,57 @@
require 'active_support'
# = You *will* paginate!
#
# First read about WillPaginate::Finder::ClassMethods, then see
# WillPaginate::ViewHelpers. The magical array you're handling in-between is
# WillPaginate::Collection.
#
# Happy paginating!
module WillPaginate
class << self
def enable
enable_actionpack
enable_activerecord
end
def enable_actionpack
return if ActionView::Base.instance_methods.include? 'will_paginate'
require 'will_paginate/view_helpers'
ActionView::Base.class_eval { include ViewHelpers }
end
def enable_activerecord
return if ActiveRecord::Base.respond_to? :paginate
require 'will_paginate/finder'
ActiveRecord::Base.class_eval { include Finder }
associations = ActiveRecord::Associations
collection = associations::AssociationCollection
# to support paginating finders on associations, we have to mix in the
# method_missing magic from WillPaginate::Finder::ClassMethods to AssociationProxy
# subclasses, but in a different way for Rails 1.2.x and 2.0
(collection.instance_methods.include?(:create!) ?
collection : collection.subclasses.map(&:constantize)
).push(associations::HasManyThroughAssociation).each do |klass|
klass.class_eval do
include Finder::ClassMethods
alias_method_chain :method_missing, :paginate
end
end
end
end
module Deprecation
extend ActiveSupport::Deprecation
def self.warn(message, callstack = caller)
message = 'WillPaginate: ' + message.strip.gsub(/ {3,}/, ' ')
behavior.call(message, callstack) if behavior && !silenced?
end
def self.silenced?
ActiveSupport::Deprecation.silenced?
end
end
end

View File

@ -0,0 +1,113 @@
module WillPaginate
# Arrays returned from paginating finds are, in fact, instances of this.
# 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.
#
# WillPaginate::Collection also assists in rolling out your own pagination
# solutions: see +create+.
#
class Collection < Array
attr_reader :current_page, :per_page, :total_entries
# Arguments to this constructor are the current page number, per-page limit
# 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
# populating the collection using the +replace+ method.
#
def initialize(page, per_page, total = nil)
@current_page = page.to_i
@per_page = per_page.to_i
self.total_entries = total if total
end
# Just like +new+, but yields the object after instantiation and returns it
# afterwards. This is very useful for manual pagination:
#
# @entries = WillPaginate::Collection.create(1, 10) do |pager|
# result = Post.find(:all, :limit => pager.per_page, :offset => pager.offset)
# # inject the result array into the paginated collection:
# pager.replace(result)
#
# unless pager.total_entries
# # the pager didn't manage to guess the total count, do it manually
# pager.total_entries = Post.count
# end
# end
#
# The possibilities with this are endless. For another example, here is how
# WillPaginate defines pagination on Array instances:
#
# Array.class_eval do
# def paginate(page = 1, per_page = 15)
# WillPaginate::Collection.create(page, per_page, size) do |pager|
# pager.replace self[pager.offset, pager.per_page].to_a
# end
# end
# end
#
def self.create(page, per_page, total = nil, &block)
pager = new(page, per_page, total)
yield pager
pager
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 larger
# number than the last page or with a number smaller than 1
def out_of_bounds?
current_page > page_count or current_page < 1
end
# 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,
# the offset is 30. This property is useful if you want to render ordinals
# besides your records: simply start with offset + 1.
#
def offset
(current_page - 1) * per_page
end
# current_page - 1 or nil if there is no previous page
def previous_page
current_page > 1 ? (current_page - 1) : nil
end
# current_page + 1 or nil if there is no next page
def next_page
current_page < page_count ? (current_page + 1) : nil
end
def total_entries=(number)
@total_entries = number.to_i
@total_pages = (@total_entries / per_page.to_f).ceil
end
# This is a magic wrapper for the original Array#replace method. It serves
# for populating the paginated collection after initialization.
#
# Why magic? Because it tries to guess the total number of entries judging
# by the size of given array. If it is shorter than +per_page+ limit, then we
# know we're on the last page. This trick is very useful for avoiding
# unnecessary hits to the database to do the counting after we fetched the
# data for the current page.
#
# However, after using +replace+ you should always test the value of
# +total_entries+ and set it to a proper value if it's +nil+. See the example
# in +create+.
def replace(array)
returning super do
# The collection is shorter then page limit? Rejoice, because
# then we know that we are on the last page!
if total_entries.nil? and length > 0 and length < per_page
self.total_entries = offset + length
end
end
end
end
end

View File

@ -0,0 +1,61 @@
require 'set'
unless Hash.instance_methods.include? 'except'
Hash.class_eval do
# Returns a new hash without the given keys.
def except(*keys)
rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
reject { |key,| rejected.include?(key) }
end
# Replaces the hash without only the given keys.
def except!(*keys)
replace(except(*keys))
end
end
end
unless Hash.instance_methods.include? 'slice'
Hash.class_eval do
# Returns a new hash with only the given keys.
def slice(*keys)
allowed = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
reject { |key,| !allowed.include?(key) }
end
# Replaces the hash with only the given keys.
def slice!(*keys)
replace(slice(*keys))
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] || 1
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

@ -0,0 +1,174 @@
require 'will_paginate/core_ext'
module WillPaginate
# A mixin for ActiveRecord::Base. Provides +per_page+ class method
# and makes +paginate+ finders possible with some method_missing magic.
#
# Find out more in WillPaginate::Finder::ClassMethods
#
module Finder
def self.included(base)
base.extend ClassMethods
class << base
alias_method_chain :method_missing, :paginate
define_method(:per_page) { 30 } unless respond_to?(:per_page)
end
end
# = Paginating finders for ActiveRecord models
#
# WillPaginate doesn't really add extra methods to your ActiveRecord models (except +per_page+
# unless it's already available). It simply intercepts
# the calls to paginating finders such as +paginate+, +paginate_by_user_id+ (and so on) and
# translates them to ordinary finders: +find+, +find_by_user_id+, etc. It does so with some
# method_missing magic, but you don't need to care for that. You simply use paginating finders
# same way you used ordinary ones. You only need to tell them what page you want in options.
#
# @topics = Topic.paginate :all, :page => params[:page]
#
# In paginating finders, "all" is implicit. No sense in paginating a single record, right? So:
#
# Post.paginate => Post.find :all
# Post.paginate_all_by_something => Post.find_all_by_something
# Post.paginate_by_something => Post.find_all_by_something
#
# Knowing that, the above example can be written simply as:
#
# @topics = Topic.paginate :page => params[:page]
#
# Don't forget to pass the +page+ parameter! Without it, paginating finders will raise an error.
#
# == Options
# Options for paginating finders are:
#
# page REQUIRED, but defaults to 1 if false or nil
# per_page (default is read from the model, which is 30 if not overridden)
# total entries not needed unless you want to count the records yourself somehow
# count hash of options that are used only for the call to count
#
module ClassMethods
# This methods 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 +per_page+.
#
# Example:
#
# @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000],
# :page => params[:page], :per_page => 3
#
def paginate_by_sql(sql, options)
options, page, per_page = wp_parse_options!(options)
WillPaginate::Collection.create(page, per_page) do |pager|
query = sanitize_sql(sql)
count_query = "SELECT COUNT(*) FROM (#{query}) AS count_table" unless options[:total_entries]
options.update :offset => pager.offset, :limit => pager.per_page
add_limit! query, options
pager.replace find_by_sql(query)
pager.total_entries = options[:total_entries] || count_by_sql(count_query) unless pager.total_entries
end
end
def respond_to?(method, include_priv = false)
case method.to_sym
when :paginate, :paginate_by_sql
true
else
super(method.to_s.sub(/^paginate/, 'find'), include_priv)
end
end
protected
def method_missing_with_paginate(method, *args, &block)
# did somebody tried to paginate? if not, let them be
unless method.to_s.index('paginate') == 0
return method_missing_without_paginate(method, *args, &block)
end
options, page, per_page, total_entries = wp_parse_options!(args.pop)
# an array of IDs may have been given:
total_entries ||= (Array === args.first and args.first.size)
# paginate finders are really just find_* with limit and offset
finder = method.to_s.sub /^paginate/, 'find'
# :all is implicit
if finder == 'find'
args.unshift(:all) if args.empty?
elsif finder.index('find_by_') == 0
finder.sub! /^find/, 'find_all'
end
WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
args << options.except(:count).merge(:offset => pager.offset, :limit => pager.per_page)
pager.replace send(finder, *args)
# magic counting for user convenience:
pager.total_entries = wp_count!(options, args, finder) unless pager.total_entries
end
end
def wp_count!(options, args, finder)
excludees = [:count, :order, :limit, :offset]
unless options[:select] and options[:select] =~ /^\s*DISTINCT/i
excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
end
# count expects (almost) the same options as find
count_options = options.except *excludees
# merge the hash found in :count
# this allows you to specify :select, :order, or anything else just for the count query
count_options.update(options.delete(:count) || {}) if options.key? :count
# we may have to scope ...
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 =~ /^find_/ 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
# then execute the count with the scoping provided by the with_finder
send(scoper, &counter)
elsif conditions = wp_extract_finder_conditions(finder, args)
# extracted the conditions from calls like "paginate_by_foo_and_bar"
with_scope(:find => { :conditions => conditions }, &counter)
else
counter.call
end
count.respond_to?(:length) ? count.length : count
end
def wp_parse_options!(options)
raise ArgumentError, 'hash parameters expected' unless options.respond_to? :symbolize_keys!
options.symbolize_keys!
raise ArgumentError, ':page parameter required' unless options.key? :page
if options[:count] and options[:total_entries]
raise ArgumentError, ':count and :total_entries are mutually exclusive parameters'
end
page = options.delete(:page) || 1
per_page = options.delete(:per_page) || self.per_page
total = options.delete(:total_entries)
[options, page, per_page, total]
end
private
# thanks to active record for making us duplicate this code
def wp_extract_finder_conditions(finder, arguments)
return unless match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(finder.to_s)
attribute_names = extract_attribute_names_from_match(match)
unless all_attributes_exists?(attribute_names)
raise "I can't make sense of `#{finder}`. Try doing the count manually"
end
construct_attributes_from_arguments(attribute_names, arguments)
end
end
end
end

View File

@ -0,0 +1,136 @@
require 'will_paginate/core_ext'
module WillPaginate
# = Global options for pagination helpers
#
# Options for pagination helpers are optional and get their default values from the
# WillPaginate::ViewHelpers.pagination_options hash. You can write to this hash to
# override default options on the global level:
#
# WillPaginate::ViewHelpers.pagination_options[:prev_label] = 'Previous page'
#
# By putting this into your environment.rb you can easily translate link texts to previous
# and next pages, as well as override some other defaults to your liking.
module ViewHelpers
# default options that can be overridden on the global level
@@pagination_options = { :class => 'pagination',
:prev_label => '&laquo; Previous',
:next_label => 'Next &raquo;',
:inner_window => 4, # links around the current page
:outer_window => 1, # links around beginning and end
:separator => ' ', # single space is friendly to spiders and non-graphic browsers
:param_name => :page
}
mattr_reader :pagination_options
# Renders Digg-style pagination. (We know you wanna!)
# Returns nil if there is only one page in total (can't paginate that).
#
# Options for will_paginate view helper:
#
# class: CSS class name for the generated DIV (default "pagination")
# prev_label: default '&laquo; Previous',
# next_label: default 'Next &raquo;',
# inner_window: how many links are shown around the current page, defaults to 4
# outer_window: how many links are around the first and the last page, defaults to 1
# separator: string separator for page HTML elements, default " " (single space)
# param_name: parameter name for page number in URLs, defaults to "page"
#
# All extra options are passed to the generated container DIV, so eventually
# they become its HTML attributes.
#
def will_paginate(entries = @entries, options = {})
total_pages =
if entries.page_count > 1
renderer = WillPaginate::LinkRenderer.new entries, options, self
links = renderer.items
content_tag :div, links, renderer.html_options
end
end
end
# This class does the heavy lifting of actually building the pagination
# links. It is used by +will_paginate+ helper internally, but avoid using it
# directly (for now) because its API is not set in stone yet.
class LinkRenderer
def initialize(collection, options, template)
@collection = collection
@options = options.symbolize_keys.reverse_merge WillPaginate::ViewHelpers.pagination_options
@template = template
end
def items
returning windowed_paginator do |links|
# next and previous buttons
links.unshift page_link_or_span(@collection.previous_page, 'disabled', @options[:prev_label])
links.push page_link_or_span(@collection.next_page, 'disabled', @options[:next_label])
end.join(@options[:separator])
end
def html_options
@options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class])
end
protected
def windowed_paginator
inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i
min = page - inner_window
max = page + inner_window
# adjust lower or upper limit if other is out of bounds
if max > total_pages then min -= max - total_pages
elsif min < 1 then max += 1 - min
end
current = min..max
beginning = 1..(1 + outer_window)
tail = (total_pages - outer_window)..total_pages
visible = [beginning, current, tail].map(&:to_a).flatten.sort.uniq
links, prev = [], 0
visible.each do |n|
next if n < 1
break if n > total_pages
unless n - prev > 1
prev = n
links << page_link_or_span((n != page ? n : nil), 'current', n)
else
# ellipsis represents the gap between windows
prev = n - 1
links << '...'
redo
end
end
links
end
def page_link_or_span(page, span_class, text)
unless page
@template.content_tag :span, text, :class => span_class
else
# page links should preserve GET/POST parameters
@template.link_to text, @template.params.merge(param => page != 1 ? page : nil)
end
end
private
def page
@collection.current_page
end
def total_pages
@collection.page_count
end
def param
@options[:param_name].to_sym
end
end
end

View File

@ -0,0 +1,121 @@
require File.dirname(__FILE__) + '/helper'
require 'will_paginate'
require 'will_paginate/core_ext'
class ArrayPaginationTest < Test::Unit::TestCase
def test_simple
collection = ('a'..'e').to_a
[{ :page => 1, :per_page => 3, :expected => %w( a b c ) },
{ :page => 2, :per_page => 3, :expected => %w( d e ) },
{ :page => 1, :per_page => 5, :expected => %w( a b c d e ) },
{ :page => 3, :per_page => 5, :expected => [] },
{ :page => -1, :per_page => 5, :expected => [] },
{ :page => 1, :per_page => -5, :expected => [] },
].
each do |conditions|
assert_equal conditions[:expected], collection.paginate(conditions.slice(:page, :per_page))
end
end
def test_defaults
result = (1..50).to_a.paginate
assert_equal 1, result.current_page
assert_equal 30, result.size
end
def test_deprecated_api
assert_deprecated 'paginate API' do
result = (1..50).to_a.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
def test_total_entries_has_precedence
result = %w(a b c).paginate :total_entries => 5
assert_equal 5, result.total_entries
end
def test_argument_error_with_params_and_another_argument
assert_raise ArgumentError do
[].paginate({}, 5)
end
end
def test_paginated_collection
entries = %w(a b c)
collection = create(2, 3, 10) do |pager|
assert_equal entries, pager.replace(entries)
end
assert_equal entries, collection
assert_respond_to_all collection, %w(page_count each offset size current_page per_page total_entries)
assert_kind_of Array, collection
assert_instance_of Array, collection.entries
assert_equal 3, collection.offset
assert_equal 4, collection.page_count
assert !collection.out_of_bounds?
end
def test_out_of_bounds
entries = create(2, 3, 2){}
assert entries.out_of_bounds?
entries = create(0, 3, 2){}
assert entries.out_of_bounds?
entries = create(1, 3, 2){}
assert !entries.out_of_bounds?
end
def test_guessing_total_count
entries = create do |pager|
# collection is shorter than limit
pager.replace array
end
assert_equal 8, entries.total_entries
entries = create(2, 5, 10) do |pager|
# collection is shorter than limit, but we have an explicit count
pager.replace array
end
assert_equal 10, entries.total_entries
entries = create do |pager|
# collection is the same as limit; we can't guess
pager.replace array(5)
end
assert_equal nil, entries.total_entries
entries = create do |pager|
# collection is empty; we can't guess
pager.replace array(0)
end
assert_equal nil, entries.total_entries
end
private
def create(page = 2, limit = 5, total = nil, &block)
WillPaginate::Collection.create(page, limit, total, &block)
end
def array(size = 3)
Array.new(size)
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

View File

@ -0,0 +1,24 @@
plugin_root = File.join(File.dirname(__FILE__), '..')
# first look for a symlink to a copy of the framework
if framework_root = ["#{plugin_root}/rails", "#{plugin_root}/../../rails"].find { |p| File.directory? p }
puts "found framework root: #{framework_root}"
# this allows for a plugin to be tested outside an app
$:.unshift "#{framework_root}/activesupport/lib", "#{framework_root}/activerecord/lib", "#{framework_root}/actionpack/lib"
else
# is the plugin installed in an application?
app_root = plugin_root + '/../../..'
if File.directory? app_root + '/config'
puts 'using config/boot.rb'
ENV['RAILS_ENV'] = 'test'
require File.expand_path(app_root + '/config/boot')
else
# simply use installed gems if available
puts 'using rubygems'
require 'rubygems'
gem 'actionpack'; gem 'activerecord'
end
end
$:.unshift "#{plugin_root}/lib"

View File

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

View File

@ -0,0 +1,285 @@
require File.dirname(__FILE__) + '/helper'
require File.dirname(__FILE__) + '/lib/activerecord_test_case'
require 'will_paginate'
WillPaginate.enable_activerecord
class FinderTest < ActiveRecordTestCase
fixtures :topics, :replies, :users, :projects, :developers_projects
def test_new_methods_presence
assert_respond_to_all Topic, %w(per_page paginate paginate_by_sql)
end
def test_simple_paginate
entries = Topic.paginate :page => nil
assert_equal 1, entries.current_page
assert_nil entries.previous_page
assert_nil entries.next_page
assert_equal 1, entries.page_count
assert_equal 4, entries.size
entries = Topic.paginate :page => 2
assert_equal 2, entries.current_page
assert_equal 1, entries.previous_page
assert_equal 1, entries.page_count
assert entries.empty?
end
def test_parameter_api
# :page parameter in options is required!
assert_raise(ArgumentError){ Topic.paginate }
assert_raise(ArgumentError){ Topic.paginate({}) }
# explicit :all should not break anything
assert_equal Topic.paginate(:page => nil), Topic.paginate(:all, :page => 1)
# :count could be nil and we should still not cry
assert_nothing_raised { Topic.paginate :page => 1, :count => nil }
end
def test_paginate_with_per_page
entries = Topic.paginate :page => 1, :per_page => 1
assert_equal 1, entries.size
assert_equal 4, entries.page_count
# Developer class has explicit per_page at 10
entries = Developer.paginate :page => 1
assert_equal 10, entries.size
assert_equal 2, entries.page_count
entries = Developer.paginate :page => 1, :per_page => 5
assert_equal 11, entries.total_entries
assert_equal 5, entries.size
assert_equal 3, entries.page_count
end
def test_paginate_with_order
entries = Topic.paginate :page => 1, :order => 'created_at desc'
expected = [topics(:futurama), topics(:harvey_birdman), topics(:rails), topics(:ar)].reverse
assert_equal expected, entries.to_a
assert_equal 1, entries.page_count
end
def test_paginate_with_conditions
entries = Topic.paginate :page => 1, :conditions => ["created_at > ?", 30.minutes.ago]
expected = [topics(:rails), topics(:ar)]
assert_equal expected, entries.to_a
assert_equal 1, entries.page_count
end
def test_paginate_with_include_and_conditions
entries = Topic.paginate \
:page => 1,
:include => :replies,
:conditions => "replies.content LIKE 'Bird%' ",
:per_page => 10
expected = Topic.find :all,
:include => 'replies',
:conditions => "replies.content LIKE 'Bird%' ",
:limit => 10
assert_equal expected, entries.to_a
assert_equal 1, entries.total_entries
end
def test_paginate_with_include_and_order
entries = Topic.paginate \
:page => 1,
:include => :replies,
:order => 'replies.created_at asc, topics.created_at asc',
:per_page => 10
expected = Topic.find :all,
:include => 'replies',
:order => 'replies.created_at asc, topics.created_at asc',
:limit => 10
assert_equal expected, entries.to_a
assert_equal 4, entries.total_entries
end
def test_paginate_associations_with_include
entries, project = nil, projects(:active_record)
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
entries = project.topics.paginate \
:page => 1,
:include => :replies,
:conditions => "replies.content LIKE 'Nice%' ",
:per_page => 10
end
expected = Topic.find :all,
:include => 'replies',
:conditions => "project_id = #{project.id} AND replies.content LIKE 'Nice%' ",
:limit => 10
assert_equal expected, entries.to_a
end
def test_paginate_associations
dhh = users :david
expected_name_ordered = [projects(:action_controller), projects(:active_record)]
expected_id_ordered = [projects(:active_record), projects(:action_controller)]
# with association-specified order
entries = dhh.projects.paginate(:page => 1)
assert_equal expected_name_ordered, entries
assert_equal 2, entries.total_entries
# with explicit order
entries = dhh.projects.paginate(:page => 1, :order => 'projects.id')
assert_equal expected_id_ordered, entries
assert_equal 2, entries.total_entries
assert_nothing_raised { dhh.projects.find(:all, :order => 'projects.id', :limit => 4) }
entries = dhh.projects.paginate(:page => 1, :order => 'projects.id', :per_page => 4)
assert_equal expected_id_ordered, entries
# has_many with implicit order
topic = Topic.find(1)
expected = [replies(:spam), replies(:witty_retort)]
assert_equal expected.map(&:id).sort, topic.replies.paginate(:page => 1).map(&:id).sort
assert_equal expected.reverse, topic.replies.paginate(:page => 1, :order => 'replies.id ASC')
end
def test_paginate_association_extension
project = Project.find(:first)
entries = project.replies.paginate_recent :page => 1
assert_equal [replies(:brave)], entries
end
def test_paginate_with_joins
entries = Developer.paginate :page => 1,
: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
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
end
def test_paginate_with_group
entries = Developer.paginate :page => 1, :per_page => 10, :group => 'salary'
expected = [ users(:david), users(:jamis), users(:dev_10), users(:poor_jamis) ].map(&:salary).sort
assert_equal expected, entries.map(&:salary).sort
end
def test_paginate_with_dynamic_finder
expected = [replies(:witty_retort), replies(:spam)]
assert_equal expected, Reply.paginate_by_topic_id(1, :page => 1)
entries = Developer.paginate :conditions => { :salary => 100000 }, :page => 1, :per_page => 5
assert_equal 8, entries.total_entries
assert_equal entries, Developer.paginate_by_salary(100000, :page => 1, :per_page => 5)
# dynamic finder + conditions
entries = Developer.paginate_by_salary(100000, :page => 1,
:conditions => ['id > ?', 6])
assert_equal 4, entries.total_entries
assert_equal (7..10).to_a, entries.map(&:id)
assert_raises NoMethodError do
Developer.paginate_by_inexistent_attribute 100000, :page => 1
end
end
def test_paginate_by_sql
assert_respond_to Developer, :paginate_by_sql
entries = Developer.paginate_by_sql ['select * from users where salary > ?', 80000],
:page => 2, :per_page => 3, :total_entries => 9
assert_equal (5..7).to_a, entries.map(&:id)
assert_equal 9, entries.total_entries
end
def test_count_by_sql
entries = Developer.paginate_by_sql ['select * from users where salary > ?', 60000],
:page => 2, :per_page => 3
assert_equal 12, entries.total_entries
end
def test_count_distinct
entries = Developer.paginate :select => 'DISTINCT salary', :page => 1, :per_page => 4
assert_equal 4, entries.size
assert_equal 4, entries.total_entries
end
def test_scoped_paginate
entries =
Developer.with_poor_ones do
Developer.paginate :page => 1
end
assert_equal 2, entries.size
assert_equal 2, entries.total_entries
end
# Are we on edge? Find out by testing find_all which was removed in [6998]
unless Developer.respond_to? :find_all
def test_paginate_array_of_ids
# AR finders also accept arrays of IDs
# (this was broken in Rails before [6912])
entries = Developer.paginate((1..8).to_a, :per_page => 3, :page => 2)
assert_equal (4..6).to_a, entries.map(&:id)
assert_equal 8, entries.total_entries
end
end
uses_mocha 'parameter' do
def test_implicit_all_with_dynamic_finders
Topic.expects(:find_all_by_foo).returns([])
Topic.expects(:wp_extract_finder_conditions)
Topic.expects(:count)
Topic.paginate_by_foo :page => 1
end
def test_guessing_the_total_count
Topic.expects(:find).returns(Array.new(2))
Topic.expects(:count).never
entries = Topic.paginate :page => 2, :per_page => 4
assert_equal 6, entries.total_entries
end
def test_extra_parameters_stay_untouched
Topic.expects(:find).with() { |*args| args.last.key? :foo }.returns(Array.new(5))
Topic.expects(:count).with(){ |*args| args.last.key? :foo }.returns(1)
Topic.paginate :foo => 'bar', :page => 1, :per_page => 4
end
def test_count_doesnt_use_select_options
Developer.expects(:find).with() { |*args| args.last.key? :select }.returns(Array.new(5))
Developer.expects(:count).with(){ |*args| !args.last.key?(:select) }.returns(1)
Developer.paginate :select => 'users.*', :page => 1, :per_page => 4
end
def test_should_use_scoped_finders_if_present
# scope-out compatibility
Topic.expects(:find_best).returns(Array.new(5))
Topic.expects(:with_best).returns(1)
Topic.paginate_best :page => 1, :per_page => 4
end
def test_ability_to_use_with_custom_finders
# acts_as_taggable defines `find_tagged_with(tag, options)`
Topic.expects(:find_tagged_with).with('will_paginate', :offset => 0, :limit => 5).returns([])
Topic.expects(:count).with({}).returns(0)
Topic.paginate_tagged_with 'will_paginate', :page => 1, :per_page => 5
end
end
end

View File

@ -0,0 +1,3 @@
class Admin < User
has_many :companies, :finder_sql => 'SELECT * FROM companies'
end

View File

@ -0,0 +1,11 @@
class Developer < User
has_and_belongs_to_many :projects, :include => :topics, :order => 'projects.name'
def self.with_poor_ones(&block)
with_scope :find => { :conditions => ['salary <= ?', 80000], :order => 'salary' } do
yield
end
end
def self.per_page() 10 end
end

View File

@ -0,0 +1,13 @@
david_action_controller:
developer_id: 1
project_id: 2
joined_on: 2004-10-10
david_active_record:
developer_id: 1
project_id: 1
joined_on: 2004-10-10
jamis_active_record:
developer_id: 2
project_id: 1

View File

@ -0,0 +1,15 @@
class Project < ActiveRecord::Base
has_and_belongs_to_many :developers, :uniq => true
has_many :topics
# :finder_sql => 'SELECT * FROM topics WHERE (topics.project_id = #{id})',
# :counter_sql => 'SELECT COUNT(*) FROM topics WHERE (topics.project_id = #{id})'
has_many :replies, :through => :topics do
def find_recent(params = {})
with_scope :find => { :conditions => ['replies.created_at > ?', 15.minutes.ago] } do
find :all, params
end
end
end
end

View File

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

View File

@ -0,0 +1,34 @@
witty_retort:
id: 1
topic_id: 1
content: Birdman is better!
created_at: <%= 6.hours.ago.to_s(:db) %>
updated_at: nil
another:
id: 2
topic_id: 2
content: Nuh uh!
created_at: <%= 1.hour.ago.to_s(:db) %>
updated_at: nil
spam:
id: 3
topic_id: 1
content: Nice site!
created_at: <%= 1.hour.ago.to_s(:db) %>
updated_at: nil
decisive:
id: 4
topic_id: 4
content: "I'm getting to the bottom of this"
created_at: <%= 30.minutes.ago.to_s(:db) %>
updated_at: nil
brave:
id: 5
topic_id: 4
content: "AR doesn't scare me a bit"
created_at: <%= 10.minutes.ago.to_s(:db) %>
updated_at: nil

View File

@ -0,0 +1,5 @@
class Reply < ActiveRecord::Base
belongs_to :topic, :include => [:replies]
validates_presence_of :content
end

View File

@ -0,0 +1,38 @@
ActiveRecord::Schema.define do
create_table "developers_projects", :id => false, :force => true do |t|
t.column "developer_id", :integer, :null => false
t.column "project_id", :integer, :null => false
t.column "joined_on", :date
t.column "access_level", :integer, :default => 1
end
create_table "projects", :force => true do |t|
t.column "name", :text
end
create_table "replies", :force => true do |t|
t.column "content", :text
t.column "created_at", :datetime
t.column "updated_at", :datetime
t.column "topic_id", :integer
end
create_table "topics", :force => true do |t|
t.column "project_id", :integer
t.column "title", :string
t.column "subtitle", :string
t.column "content", :text
t.column "created_at", :datetime
t.column "updated_at", :datetime
end
create_table "users", :force => true do |t|
t.column "name", :text
t.column "salary", :integer, :default => 70000
t.column "created_at", :datetime
t.column "updated_at", :datetime
t.column "type", :text
end
end

View File

@ -0,0 +1,4 @@
class Topic < ActiveRecord::Base
has_many :replies, :dependent => :destroy, :order => 'replies.created_at DESC'
belongs_to :project
end

View File

@ -0,0 +1,30 @@
futurama:
id: 1
title: Isnt futurama awesome?
subtitle: It really is, isnt it.
content: I like futurama
created_at: <%= 1.day.ago.to_s(:db) %>
updated_at:
harvey_birdman:
id: 2
title: Harvey Birdman is the king of all men
subtitle: yup
content: He really is
created_at: <%= 2.hours.ago.to_s(:db) %>
updated_at:
rails:
id: 3
project_id: 1
title: Rails is nice
subtitle: It makes me happy
content: except when I have to hack internals to fix pagination. even then really.
created_at: <%= 20.minutes.ago.to_s(:db) %>
ar:
id: 4
project_id: 1
title: ActiveRecord sometimes freaks me out
content: "I mean, what's the deal with eager loading?"
created_at: <%= 15.minutes.ago.to_s(:db) %>

View File

@ -0,0 +1,2 @@
class User < ActiveRecord::Base
end

View File

@ -0,0 +1,35 @@
david:
id: 1
name: David
salary: 80000
type: Developer
jamis:
id: 2
name: Jamis
salary: 150000
type: Developer
<% for digit in 3..10 %>
dev_<%= digit %>:
id: <%= digit %>
name: fixture_<%= digit %>
salary: 100000
type: Developer
<% end %>
poor_jamis:
id: 11
name: Jamis
salary: 9000
type: Developer
admin:
id: 12
name: admin
type: Admin
goofy:
id: 13
name: Goofy
type: Admin

View File

@ -0,0 +1,25 @@
require 'test/unit'
require 'rubygems'
# gem install redgreen for colored test output
begin require 'redgreen'; rescue LoadError; end
require File.join(File.dirname(__FILE__), 'boot') unless defined?(ActiveRecord)
class Test::Unit::TestCase
protected
def assert_respond_to_all object, methods
methods.each do |method|
[method.to_s, method.to_sym].each { |m| assert_respond_to object, m }
end
end
end
# Wrap tests that use Mocha and skip if unavailable.
def uses_mocha(test_name)
require 'mocha' unless Object.const_defined?(:Mocha)
yield
rescue LoadError => load_error
raise unless load_error.message =~ /mocha/i
$stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again."
end

View File

@ -0,0 +1,23 @@
require File.join(File.dirname(__FILE__), 'activerecord_test_connector')
class ActiveRecordTestCase < Test::Unit::TestCase
# Set our fixture path
if ActiveRecordTestConnector.able_to_connect
self.fixture_path = File.join(File.dirname(__FILE__), '..', 'fixtures')
self.use_transactional_fixtures = false
end
def self.fixtures(*args)
super if ActiveRecordTestConnector.connected
end
def run(*args)
super if ActiveRecordTestConnector.connected
end
# Default so Test::Unit::TestCase doesn't complain
def test_truth
end
end
ActiveRecordTestConnector.setup

View File

@ -0,0 +1,67 @@
require 'active_record'
require 'active_record/version'
require 'active_record/fixtures'
class ActiveRecordTestConnector
cattr_accessor :able_to_connect
cattr_accessor :connected
# Set our defaults
self.connected = false
self.able_to_connect = true
def self.setup
unless self.connected || !self.able_to_connect
setup_connection
load_schema
# require_fixture_models
Dependencies.load_paths.unshift(File.dirname(__FILE__) + "/../fixtures")
self.connected = true
end
rescue Exception => e # errors from ActiveRecord setup
$stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
#$stderr.puts " #{e.backtrace.join("\n ")}\n"
self.able_to_connect = false
end
private
def self.setup_connection
if Object.const_defined?(:ActiveRecord)
defaults = { :database => ':memory:' }
ActiveRecord::Base.logger = Logger.new STDOUT if $0 == 'irb'
begin
options = defaults.merge :adapter => 'sqlite3', :timeout => 500
ActiveRecord::Base.establish_connection(options)
ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options }
ActiveRecord::Base.connection
rescue Exception # errors from establishing a connection
$stderr.puts 'SQLite 3 unavailable; trying SQLite 2.'
options = defaults.merge :adapter => 'sqlite'
ActiveRecord::Base.establish_connection(options)
ActiveRecord::Base.configurations = { 'sqlite2_ar_integration' => options }
ActiveRecord::Base.connection
end
unless Object.const_defined?(:QUOTED_TYPE)
Object.send :const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')
end
else
raise "Can't setup connection since ActiveRecord isn't loaded."
end
end
def self.load_schema
ActiveRecord::Base.silence do
ActiveRecord::Migration.verbose = false
load File.dirname(__FILE__) + "/../fixtures/schema.rb"
end
end
def self.require_fixture_models
models = Dir.glob(File.dirname(__FILE__) + "/../fixtures/*.rb")
models = (models.grep(/user.rb/) + models).uniq
models.each { |f| require f }
end
end

View File

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

View File

@ -0,0 +1,146 @@
require File.dirname(__FILE__) + '/helper'
require 'action_controller'
require 'action_controller/test_process'
ActionController::Routing::Routes.reload rescue nil
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 PaginationController < ActionController::Base
def list_developers
@options = params.delete(:options) || {}
@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
protected
def rescue_errors(e) raise e end
def rescue_action(e) raise e end
end
def setup
@controller = PaginationController.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 |el|
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
end
end
def test_will_paginate_with_options
get :list_developers, :page => 2, :options => {
: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 [nil,nil,3,3], elements
assert_select elements.first, 'a', "Prev"
assert_select elements.last, 'a', "Next"
end
assert_select 'span.current', entries.current_page.to_s
end
end
def test_will_paginate_preserves_parameters
get :list_developers, :foo => { :bar => 'baz' }
assert_response :success
assert_select 'div.pagination', 1, 'no main DIV' do
assert_select 'a[href]', 3 do |elements|
elements.each do |el|
assert_match /foo%5Bbar%5D=baz/, el['href'], "THIS IS A BUG in Rails 1.2 which " +
"has been fixed in Rails 2.0."
# there is no need to worry *unless* you too are using hashes in parameters which
# need to be preserved over pages
end
end
end
end
def test_will_paginate_with_custom_page_param
get :list_developers, :developers_page => 2, :options => { :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 [nil,nil,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, :options => { :inner_window => 2 }
assert_response :success
entries = assigns :developers
assert entries
assert_equal 1, entries.size
assert_select 'div.pagination', 1, 'no main DIV' do
assert_select 'a[href]', 10 do |elements|
validate_page_numbers [5,nil,2,4,5,7,8,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
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
protected
def validate_page_numbers expected, links, param_name = :page
assert_equal(expected, links.map { |e|
e['href'] =~ /\W#{param_name}=([^&]*)/
$1 ? $1.to_i : $1
})
end
end