diff --git a/.gitignore b/.gitignore
index 2b437b91c..068b713e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-/doc
-/rails
+.bundle
+doc
*.gem
-/coverage
+coverage
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..7fd13f5fc
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "website"]
+ path = website
+ url = git://github.com/mislav/will_paginate.git
diff --git a/.manifest b/.manifest
deleted file mode 100644
index c8074e6a7..000000000
--- a/.manifest
+++ /dev/null
@@ -1,43 +0,0 @@
-CHANGELOG.rdoc
-LICENSE
-README.rdoc
-Rakefile
-examples/apple-circle.gif
-examples/index.haml
-examples/index.html
-examples/pagination.css
-examples/pagination.sass
-init.rb
-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/boot.rb
-test/collection_test.rb
-test/console
-test/database.yml
-test/finder_test.rb
-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/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
\ No newline at end of file
diff --git a/.rspec b/.rspec
new file mode 100644
index 000000000..4e1e0d2f7
--- /dev/null
+++ b/.rspec
@@ -0,0 +1 @@
+--color
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..394e7b49b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,11 @@
+rvm:
+ - 1.8.7
+ - 1.9.2
+gemfile:
+ - Gemfile
+ - Gemfile.rails3.0
+env:
+ - DB=sqlite3
+ - DB=mysql
+ - DB=mysql2
+ - DB=postgres
diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc
index 8c4d2c90c..554a60fd3 100644
--- a/CHANGELOG.rdoc
+++ b/CHANGELOG.rdoc
@@ -1,3 +1,26 @@
+== "rails3" branch
+
+* added Sequel support
+* added an initialization hook for Merb
+* refactored URL generation
+* BACKWARDS INCOMPATIBLE: refactored LinkRenderer; also markup changes
+ 1 is now 1
+ a.prev_page -> a.previous_page (for consistency)
+* "prev_label" -> "previous_label"
+* ported view tests to specs
+* setup Autotest
+* added per_page=(limit) attribute writer to set default per_page
+* Remove :include option from count_all query when possible (Rails 2.1)
+* added WP::ViewHelpers::ActionView and LinkRenderer
+* specs for ViewHelpers::Base and LinkRendererBase
+* created LinkRendererBase that implements windowed visible page numbers logic
+* created WP::ViewHelpers::Base abstract module that implements generic view helpers
+* ported finder tests to specs
+* added WP::Finders::DataMapper
+* added WP::Finders::ActiveRecord mixin for ActiveRecord::Base
+* created WP::Finders::Base abstract module that implements generic pagination logic
+* removed dependency to ActiveSupport
+
= 2.3.12, released 2009-12-01
* make view helpers "HTML safe" for Rails 2.3.5 with rails_xss plugin
@@ -84,14 +107,14 @@
gem install mislav-will_paginate
* extract reusable pagination testing stuff into WillPaginate::View
-* rethink the page URL construction mechanizm to be more bulletproof when
+* rethink the page URL construction mechanism 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
+* Change output of "page_entries_info" on single-page collection and erroneous
output with empty collection as reported by Tim Chater
== 2.2.1, released 2008-04-08
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 000000000..1334eed2b
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,29 @@
+source 'http://rubygems.org'
+
+rails_version = '~> 3.1.0.rc'
+
+gem 'activerecord', rails_version
+gem 'activeresource', rails_version
+gem 'actionpack', rails_version
+
+gem 'rake'
+gem 'rspec', '~> 2.6.0'
+gem 'mocha', '~> 0.9.8'
+
+gem 'sequel', '~> 3.8'
+gem 'sqlite3', '~> 1.3.3'
+gem 'dm-core'
+gem 'dm-aggregates'
+gem 'dm-migrations'
+gem 'dm-sqlite-adapter'
+
+group :mysql do
+ gem 'mysql', '~> 2.8.1'
+ gem 'mysql2', '>= 0.3.6'
+end
+gem 'pg', '~> 0.11', :group => :pg
+
+group :debug do
+ gem 'ruby-debug', :platforms => :mri_18
+ gem 'ruby-debug19', :platforms => :mri_19
+end unless ENV['TRAVIS']
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 000000000..dd1fc829b
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,126 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ actionpack (3.1.0.rc5)
+ activemodel (= 3.1.0.rc5)
+ activesupport (= 3.1.0.rc5)
+ builder (~> 3.0.0)
+ erubis (~> 2.7.0)
+ i18n (~> 0.6)
+ rack (~> 1.3.1)
+ rack-cache (~> 1.0.2)
+ rack-mount (~> 0.8.1)
+ rack-test (~> 0.6.0)
+ sprockets (~> 2.0.0.beta.12)
+ activemodel (3.1.0.rc5)
+ activesupport (= 3.1.0.rc5)
+ bcrypt-ruby (~> 2.1.4)
+ builder (~> 3.0.0)
+ i18n (~> 0.6)
+ activerecord (3.1.0.rc5)
+ activemodel (= 3.1.0.rc5)
+ activesupport (= 3.1.0.rc5)
+ arel (~> 2.1.4)
+ tzinfo (~> 0.3.29)
+ activeresource (3.1.0.rc5)
+ activemodel (= 3.1.0.rc5)
+ activesupport (= 3.1.0.rc5)
+ activesupport (3.1.0.rc5)
+ multi_json (~> 1.0)
+ addressable (2.2.6)
+ archive-tar-minitar (0.5.2)
+ arel (2.1.4)
+ bcrypt-ruby (2.1.4)
+ builder (3.0.0)
+ columnize (0.3.4)
+ data_objects (0.10.6)
+ addressable (~> 2.1)
+ diff-lcs (1.1.2)
+ dm-aggregates (1.1.0)
+ dm-core (~> 1.1.0)
+ dm-core (1.1.0)
+ addressable (~> 2.2.4)
+ dm-do-adapter (1.1.0)
+ data_objects (~> 0.10.2)
+ dm-core (~> 1.1.0)
+ dm-migrations (1.1.0)
+ dm-core (~> 1.1.0)
+ dm-sqlite-adapter (1.1.0)
+ dm-do-adapter (~> 1.1.0)
+ do_sqlite3 (~> 0.10.2)
+ do_sqlite3 (0.10.6)
+ data_objects (= 0.10.6)
+ erubis (2.7.0)
+ hike (1.2.0)
+ i18n (0.6.0)
+ linecache (0.46)
+ rbx-require-relative (> 0.0.4)
+ linecache19 (0.5.12)
+ ruby_core_source (>= 0.1.4)
+ mocha (0.9.12)
+ multi_json (1.0.3)
+ mysql (2.8.1)
+ mysql2 (0.3.6)
+ pg (0.11.0)
+ rack (1.3.2)
+ rack-cache (1.0.2)
+ rack (>= 0.4)
+ rack-mount (0.8.1)
+ rack (>= 1.0.0)
+ rack-test (0.6.1)
+ rack (>= 1.0)
+ rake (0.9.2)
+ rbx-require-relative (0.0.5)
+ rspec (2.6.0)
+ rspec-core (~> 2.6.0)
+ rspec-expectations (~> 2.6.0)
+ rspec-mocks (~> 2.6.0)
+ rspec-core (2.6.4)
+ rspec-expectations (2.6.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.6.0)
+ ruby-debug (0.10.4)
+ columnize (>= 0.1)
+ ruby-debug-base (~> 0.10.4.0)
+ ruby-debug-base (0.10.4)
+ linecache (>= 0.3)
+ ruby-debug-base19 (0.11.25)
+ columnize (>= 0.3.1)
+ linecache19 (>= 0.5.11)
+ ruby_core_source (>= 0.1.4)
+ ruby-debug19 (0.11.6)
+ columnize (>= 0.3.1)
+ linecache19 (>= 0.5.11)
+ ruby-debug-base19 (>= 0.11.19)
+ ruby_core_source (0.1.5)
+ archive-tar-minitar (>= 0.5.2)
+ sequel (3.25.0)
+ sprockets (2.0.0.beta.12)
+ hike (~> 1.2)
+ rack (~> 1.0)
+ tilt (!= 1.3.0, ~> 1.1)
+ sqlite3 (1.3.3)
+ tilt (1.3.2)
+ tzinfo (0.3.29)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ actionpack (~> 3.1.0.rc)
+ activerecord (~> 3.1.0.rc)
+ activeresource (~> 3.1.0.rc)
+ dm-aggregates
+ dm-core
+ dm-migrations
+ dm-sqlite-adapter
+ mocha (~> 0.9.8)
+ mysql (~> 2.8.1)
+ mysql2 (>= 0.3.6)
+ pg (~> 0.11)
+ rake
+ rspec (~> 2.6.0)
+ ruby-debug
+ ruby-debug19
+ sequel (~> 3.8)
+ sqlite3 (~> 1.3.3)
diff --git a/Gemfile.rails3.0 b/Gemfile.rails3.0
new file mode 100644
index 000000000..249646d9a
--- /dev/null
+++ b/Gemfile.rails3.0
@@ -0,0 +1,24 @@
+source 'http://rubygems.org'
+
+rails_version = '~> 3.0.0'
+
+gem 'activerecord', rails_version
+gem 'activeresource', rails_version
+gem 'actionpack', rails_version
+
+gem 'rake'
+gem 'rspec', '~> 2.6.0'
+gem 'mocha', '~> 0.9.8'
+
+gem 'sqlite3', '~> 1.3.3'
+
+group :mysql do
+ gem 'mysql', '~> 2.8.1'
+ gem 'mysql2', '>= 0.3.6'
+end
+gem 'pg', '~> 0.11', :group => :pg
+
+group :debug do
+ gem 'ruby-debug', :platforms => :mri_18
+ gem 'ruby-debug19', :platforms => :mri_19
+end unless ENV['TRAVIS']
diff --git a/Gemfile.rails3.0.lock b/Gemfile.rails3.0.lock
new file mode 100644
index 000000000..7a3db5bb7
--- /dev/null
+++ b/Gemfile.rails3.0.lock
@@ -0,0 +1,92 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ abstract (1.0.0)
+ actionpack (3.0.9)
+ activemodel (= 3.0.9)
+ activesupport (= 3.0.9)
+ builder (~> 2.1.2)
+ erubis (~> 2.6.6)
+ i18n (~> 0.5.0)
+ rack (~> 1.2.1)
+ rack-mount (~> 0.6.14)
+ rack-test (~> 0.5.7)
+ tzinfo (~> 0.3.23)
+ activemodel (3.0.9)
+ activesupport (= 3.0.9)
+ builder (~> 2.1.2)
+ i18n (~> 0.5.0)
+ activerecord (3.0.9)
+ activemodel (= 3.0.9)
+ activesupport (= 3.0.9)
+ arel (~> 2.0.10)
+ tzinfo (~> 0.3.23)
+ activeresource (3.0.9)
+ activemodel (= 3.0.9)
+ activesupport (= 3.0.9)
+ activesupport (3.0.9)
+ archive-tar-minitar (0.5.2)
+ arel (2.0.10)
+ builder (2.1.2)
+ columnize (0.3.4)
+ diff-lcs (1.1.2)
+ erubis (2.6.6)
+ abstract (>= 1.0.0)
+ i18n (0.5.0)
+ linecache (0.46)
+ rbx-require-relative (> 0.0.4)
+ linecache19 (0.5.12)
+ ruby_core_source (>= 0.1.4)
+ mocha (0.9.12)
+ mysql (2.8.1)
+ mysql2 (0.3.6)
+ pg (0.11.0)
+ rack (1.2.3)
+ rack-mount (0.6.14)
+ rack (>= 1.0.0)
+ rack-test (0.5.7)
+ rack (>= 1.0)
+ rake (0.9.2)
+ rbx-require-relative (0.0.5)
+ rspec (2.6.0)
+ rspec-core (~> 2.6.0)
+ rspec-expectations (~> 2.6.0)
+ rspec-mocks (~> 2.6.0)
+ rspec-core (2.6.4)
+ rspec-expectations (2.6.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.6.0)
+ ruby-debug (0.10.4)
+ columnize (>= 0.1)
+ ruby-debug-base (~> 0.10.4.0)
+ ruby-debug-base (0.10.4)
+ linecache (>= 0.3)
+ ruby-debug-base19 (0.11.25)
+ columnize (>= 0.3.1)
+ linecache19 (>= 0.5.11)
+ ruby_core_source (>= 0.1.4)
+ ruby-debug19 (0.11.6)
+ columnize (>= 0.3.1)
+ linecache19 (>= 0.5.11)
+ ruby-debug-base19 (>= 0.11.19)
+ ruby_core_source (0.1.5)
+ archive-tar-minitar (>= 0.5.2)
+ sqlite3 (1.3.4)
+ tzinfo (0.3.29)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ actionpack (~> 3.0.0)
+ activerecord (~> 3.0.0)
+ activeresource (~> 3.0.0)
+ mocha (~> 0.9.8)
+ mysql (~> 2.8.1)
+ mysql2 (>= 0.3.6)
+ pg (~> 0.11)
+ rake
+ rspec (~> 2.6.0)
+ ruby-debug
+ ruby-debug19
+ sqlite3 (~> 1.3.3)
diff --git a/LICENSE b/LICENSE
index 96a48cb3f..44c52a6a0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2007 PJ Hyett and Mislav Marohnić
+Copyright (c) 2009 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
diff --git a/README.rdoc b/README.rdoc
index 320961a9e..ebc451755 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -1,64 +1,76 @@
-= WillPaginate
+= The will_paginate Ruby library
-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:
+Pagination is just limiting the number of records loaded and displayed. Why should you let it get in
+your way while developing?
+
+This is how you paginate on an ActiveRecord model:
Post.paginate :page => 1, :order => 'created_at DESC'
-... and then render the page links with a single view helper? Well, now you
-can.
+Most of the time it's as simple as replacing "find" with "paginate" and specifying the page you want.
Some resources to get you started:
-* {Installation instructions}[http://github.com/mislav/will_paginate/wikis/installation]
- on {the wiki}[http://github.com/mislav/will_paginate/wikis]
-* Your mind reels with questions? Join our
- {Google group}[http://groups.google.com/group/will_paginate].
-* {How to report bugs}[http://github.com/mislav/will_paginate/wikis/report-bugs]
+* The {will_paginate project page}[http://mislav.github.com/will_paginate/];
+* Your mind reels with questions? Join our {Google group}[http://groups.google.com/group/will_paginate];
+* {How to report bugs}[http://github.com/mislav/will_paginate/wikis/report-bugs];
+* {Watch the will_paginate screencast}[http://railscasts.com/episodes/51] by Ryan Bates.
+== I'm not using Rails; can I still use will_paginate?
-== Example usage
+Absolutely -- although will_paginate started off as a Rails plugin, now it is a completely
+framework-agnostic library with support for Rails and Merb built-in. The core library doesn't
+have any dependences and you can safely use it in any Ruby code.
-Use a paginate finder in the controller:
+When will_paginate is loaded in an environment where ActiveRecord and ActionView are present, it
+automatically hooks into these frameworks to provide easy pagination on your models and in your
+views. The same mechanism works for Merb applications, too. But, if no known framework is present
+then you have absolute control over what parts of will_paginate do you want to load and where you want
+them mixed in.
- @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.
+== Installation
-Render the posts in your view like you would normally do. When you need to render
-pagination, just stick this in:
+In your Gemfile (if using Bundler):
- <%= will_paginate @posts %>
+ gem 'will_paginate', '~> 3.0.beta'
-You're done. (You can find the option list at WillPaginate::ViewHelpers.)
+There are extensive {installation
+instructions}[http://github.com/mislav/will_paginate/wikis/installation] on {the
+wiki}[http://github.com/mislav/will_paginate/wikis].
-How does it know how much items to fetch per page? It asks your model by calling
-its per_page class method. You can define it like this:
- class Post < ActiveRecord::Base
- cattr_reader :per_page
- @@per_page = 50
- end
+== Example usage
+
+Typical usage involves a paginating find in the controller:
+
+ @posts = Post.paginate :page => params[:page], :order => 'updated_at DESC'
+
+It's true: +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 in WillPaginate::Finders.
-... or like this:
+Render the posts in your view like you would normally do, and when you need to render pagination,
+just stick this in:
+
+ <%= will_paginate @posts %>
+
+You're done. Read more in WillPaginate::ViewHelpers::Base.
+
+How does it know how much items to fetch per page? It asks your model by calling its
++per_page+ class method. You can define it like this:
class Post < ActiveRecord::Base
- def self.per_page
- 50
- end
+ self.per_page = 50
end
-... or don't worry about it at all. WillPaginate defines it to be 30 by default.
-But you can always specify the count explicitly when calling +paginate+:
+... or don't worry about it at all. WillPaginate defines it to be 30 by default. You can
+always specify the count explicitly when calling +paginate+:
- @posts = Post.paginate :page => params[:page], :per_page => 50
+ 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:
+The +paginate+ finder wraps the original finder and returns your result set that now has some new
+properties. You can use the collection as you would use any other array. WillPaginate view helpers
+also need that collection object to be able to render pagination:
<% for post in @posts -%>
@@ -69,39 +81,31 @@ resultset. WillPaginate view helpers also need that object to be able to render
Now let's render us some pagination!
<%= 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
+The original author of will_paginate was PJ Hyett, who later handed over development to Mislav
+Marohnić. (The library was completely rewritten since then.)
-All these people helped making will_paginate what it is now with their code
-contributions or just simply awesome ideas:
+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,
-Denis Barushev, Ben Pickles.
+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, Brandon Arbini, Denis Barushev, Paul Barry, Ben Pickles, Ken Collins, Lida Tang and Pieter
+Noordhuis.
== Usable pagination in the UI
-There are some CSS styles to get you started in the "examples/" directory. They
-are {showcased online here}[http://mislav.uniqpath.com/will_paginate/].
+There are example CSS styles to get you started on the will_paginate project page.
More reading about pagination as design pattern:
-* {Pagination 101}[https://gist.github.com/622561]
-* {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].
+* {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/] featured on Smashing Magazine;
+* {Pagination design pattern}[http://developer.yahoo.com/ypatterns/parent.php?pattern=pagination] on Yahoo Design Pattern Library.
+Want to discuss, request features, ask questions? Join the {Google
+group}[http://groups.google.com/group/will_paginate].
diff --git a/Rakefile b/Rakefile
index 6226b1b70..749576bd4 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,53 +1,37 @@
-require 'rubygems'
begin
- hanna_dir = '/Users/mislav/Projects/Hanna/lib'
- $:.unshift hanna_dir if File.exists? hanna_dir
- require 'hanna/rdoctask'
+ require 'rspec/core/rake_task'
rescue LoadError
- require 'rake'
- require 'rake/rdoctask'
-end
-load 'test/tasks.rake'
+ # no spec tasks
+else
+ task :default => [:create_database, :spec]
-desc 'Default: run unit tests.'
-task :default => :test
+ desc 'Run ALL OF the specs'
+ RSpec::Core::RakeTask.new(:spec) do |t|
+ # t.ruby_opts = '-w'
+ t.pattern = 'spec/finders/active_record_spec.rb' if ENV['DB'] and ENV['DB'] != 'sqlite3'
+ end
-desc 'Generate RDoc documentation for the will_paginate plugin.'
-Rake::RDocTask.new(:rdoc) do |rdoc|
- rdoc.rdoc_files.include('README.rdoc', 'LICENSE', 'CHANGELOG.rdoc').
- include('lib/**/*.rb').
- exclude('lib/will_paginate/named_scope*').
- exclude('lib/will_paginate/array.rb').
- exclude('lib/will_paginate/version.rb')
-
- rdoc.main = "README.rdoc" # page to start on
- rdoc.title = "will_paginate documentation"
-
- rdoc.rdoc_dir = 'doc' # rdoc output folder
- rdoc.options << '--inline-source' << '--charset=UTF-8'
- rdoc.options << '--webcvs=http://github.com/mislav/will_paginate/tree/master/'
+ namespace :spec do
+ desc "Run Rails specs"
+ RSpec::Core::RakeTask.new(:rails) do |t|
+ t.pattern = %w'spec/finders/active_record_spec.rb spec/view_helpers/action_view_spec.rb'
+ end
+ end
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 = `git ls-files --full-name --exclude=*.gemspec --exclude=.*`.chomp.split("\n")
-
- if spec_file = Dir['*.gemspec'].first
- 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 }
+desc 'Create necessary databases'
+task :create_database do |variable|
+ case ENV['DB']
+ when 'mysql', 'mysql2'
+ `mysql -e 'create database will_paginate;'`
+ abort "failed to create mysql database" unless $?.success?
+ when 'postgres'
+ `psql -c 'create database will_paginate;' -U postgres`
+ abort "failed to create postgres database" unless $?.success?
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)
+desc 'Run specs against both Rails 3.1 and Rails 3.0'
+task :rails3 do |variable|
+ system 'bundle exec rake spec && BUNDLE_GEMFILE=Gemfile.rails3.0 bundle exec rake spec:rails'
end
diff --git a/examples/apple-circle.gif b/examples/apple-circle.gif
deleted file mode 100644
index df8cbf7c8..000000000
Binary files a/examples/apple-circle.gif and /dev/null differ
diff --git a/examples/index.haml b/examples/index.haml
deleted file mode 100644
index 026b2711a..000000000
--- a/examples/index.haml
+++ /dev/null
@@ -1,69 +0,0 @@
-!!!
-%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 = '« Previous123456789…2930Next »'
- - pagination_no_page_links = '« PreviousNext »'
-
- %body
- %h1 Samples of pagination styling for will_paginate
- %p
- Find these styles in "examples/pagination.css" of will_paginate library.
- There is a Sass version of it for all you sassy people.
- %p
- Read about good rules for pagination:
- %a{ :href => 'https://gist.github.com/622561' } Pagination 101
- %p
- %em Warning:
- page links below don't lead anywhere (so don't click on them).
-
- %h2 Unstyled pagination (ewww!)
- %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= '%s' % %[<%= will_paginate @posts, :page_links => false %>].gsub('<', '<').gsub('>', '>')
-
- %h2 Digg-style, extra content
- .digg_pagination
- .page_info Displaying entries 1 - 6 of 180 in total
- = pagination
- %p Code that renders this:
- %pre= '%s' % %[
-
-
diff --git a/examples/pagination.css b/examples/pagination.css
deleted file mode 100644
index 889cd25b4..000000000
--- a/examples/pagination.css
+++ /dev/null
@@ -1,91 +0,0 @@
-.digg_pagination {
- background: white;
- /* self-clearing method: */ }
- .digg_pagination a, .digg_pagination span, .digg_pagination em {
- padding: .2em .5em;
- display: block;
- float: left;
- margin-right: 1px; }
- .digg_pagination span.disabled {
- color: #999;
- border: 1px solid #DDD; }
- .digg_pagination em {
- 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, .digg_pagination em {
- padding: .2em .3em; }
- .apple_pagination span.disabled {
- color: #AAA; }
- .apple_pagination em {
- 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, .digg_pagination em {
- padding: .2em .5em; }
- .flickr_pagination span.disabled {
- color: #AAA; }
- .flickr_pagination em {
- 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; }
-
diff --git a/examples/pagination.sass b/examples/pagination.sass
deleted file mode 100644
index 800f30d84..000000000
--- a/examples/pagination.sass
+++ /dev/null
@@ -1,91 +0,0 @@
-.digg_pagination
- :background white
- a, span, em
- :padding .2em .5em
- :display block
- :float left
- :margin-right 1px
- span.disabled
- :color #999
- :border 1px solid #DDD
- em
- :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, em
- :padding .2em .3em
- span.disabled
- :color #AAA
- em
- :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, em
- :padding .2em .5em
- span.disabled
- :color #AAA
- em
- :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
diff --git a/lib/will_paginate.rb b/lib/will_paginate.rb
index ac30b6586..09055c2ed 100644
--- a/lib/will_paginate.rb
+++ b/lib/will_paginate.rb
@@ -1,90 +1,15 @@
-require 'active_support'
-require 'will_paginate/core_ext'
-
-# = 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!
+# You will paginate!
module WillPaginate
- class << self
- # shortcut for enable_actionpack and enable_activerecord combined
- def enable
- enable_actionpack
- enable_activerecord
- end
-
- # hooks WillPaginate::ViewHelpers into ActionView::Base
- def enable_actionpack
- return if ActionView::Base.instance_methods.include_method? :will_paginate
- require 'will_paginate/view_helpers'
- ActionView::Base.send :include, ViewHelpers
-
- if defined?(ActionController::Base) and ActionController::Base.respond_to? :rescue_responses
- ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found
- end
- end
-
- # hooks WillPaginate::Finder into ActiveRecord::Base and classes that deal
- # with associations
- def enable_activerecord
- return if ActiveRecord::Base.respond_to? :paginate
- require 'will_paginate/finder'
- ActiveRecord::Base.send :include, Finder
-
- # support pagination on associations
- a = ActiveRecord::Associations
- [ a::AssociationCollection ].tap { |classes|
- # detect http://dev.rubyonrails.org/changeset/9230
- unless a::HasManyThroughAssociation.superclass == a::HasManyAssociation
- classes << a::HasManyThroughAssociation
- end
- }.each do |klass|
- klass.send :include, Finder::ClassMethods
- klass.class_eval { alias_method_chain :method_missing, :paginate }
- end
-
- # monkeypatch Rails ticket #2189: "count breaks has_many :through"
- ActiveRecord::Base.class_eval do
- protected
- def self.construct_count_options_from_args(*args)
- result = super
- result[0] = '*' if result[0].is_a?(String) and result[0] =~ /\.\*$/
- result
- 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 named_scope broke
- # has_many, has_many :through or has_and_belongs_to_many associations in
- # your app. By passing +false+, you can still use named_scope 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.send :include, WillPaginate::NamedScope
- end
- end
-
- module Deprecation # :nodoc:
- extend ActiveSupport::Deprecation
+end
- def self.warn(message, callstack = caller)
- message = 'WillPaginate: ' + message.strip.gsub(/\s+/, ' ')
- ActiveSupport::Deprecation.warn(message, callstack)
- end
- end
+if defined?(::Rails::Railtie)
+ require 'will_paginate/railtie'
end
-if defined? Rails
- WillPaginate.enable_activerecord if defined? ActiveRecord
- WillPaginate.enable_actionpack if defined? ActionController
+if defined?(::Merb::Plugins)
+ require 'will_paginate/view_helpers/merb'
+ # auto-load the right ORM adapter
+ if adapter = { :datamapper => 'data_mapper', :activerecord => 'active_record', :sequel => 'sequel' }[Merb.orm]
+ require "will_paginate/#{adapter}"
+ end
end
diff --git a/lib/will_paginate/active_record.rb b/lib/will_paginate/active_record.rb
new file mode 100644
index 000000000..914f457ed
--- /dev/null
+++ b/lib/will_paginate/active_record.rb
@@ -0,0 +1,177 @@
+require 'will_paginate/per_page'
+require 'will_paginate/collection'
+require 'active_record'
+
+module WillPaginate
+ # = Paginating finders for ActiveRecord models
+ #
+ # WillPaginate adds +paginate+, +per_page+ and other methods to
+ # ActiveRecord::Base class methods and associations.
+ #
+ # In short, paginating finders are equivalent to ActiveRecord finders; the
+ # only difference is that we start with "paginate" instead of "find" and
+ # that :page is required parameter:
+ #
+ # @posts = Post.paginate :all, :page => params[:page], :order => 'created_at DESC'
+ #
+ module ActiveRecord
+ # In Rails, this is automatically called to mix-in pagination functionality to ActiveRecord.
+ def self.setup
+ ::ActiveRecord::Base.extend PerPage
+ ::ActiveRecord::Base.extend ActiveRecord::Pagination
+ ::ActiveRecord::Base.extend ActiveRecord::BaseMethods
+
+ klasses = [::ActiveRecord::Relation]
+ if defined? ::ActiveRecord::Associations::CollectionProxy
+ klasses << ::ActiveRecord::Associations::CollectionProxy
+ else
+ klasses << ::ActiveRecord::Associations::AssociationCollection
+ end
+
+ # support pagination on associations and scopes
+ klasses.each { |klass| klass.send(:include, ActiveRecord::Pagination) }
+ end
+
+ # makes a Relation look like WillPaginate::Collection
+ module RelationMethods
+ attr_accessor :current_page
+ attr_writer :total_entries, :wp_count_options
+
+ def per_page(value = nil)
+ if value.nil? then limit_value
+ else limit(value)
+ end
+ end
+
+ # TODO: solve with less relation clones and code dups
+ def limit(num)
+ rel = super
+ if rel.current_page
+ rel.offset((rel.current_page-1) * rel.limit_value)
+ else
+ rel
+ end
+ end
+
+ def offset(value = nil)
+ if value.nil? then offset_value
+ else super(value)
+ end
+ end
+
+ def total_entries
+ @total_entries ||= begin
+ if loaded? and size < limit_value and (current_page == 1 or size > 0)
+ offset_value + size
+ else
+ excluded = [:order, :limit, :offset]
+ excluded << :includes unless eager_loading?
+ rel = self.except(*excluded)
+ # TODO: hack. decide whether to keep
+ rel = rel.apply_finder_options(@wp_count_options) if defined? @wp_count_options
+ rel.count
+ end
+ end
+ end
+
+ # workaround for Active Record 3.0
+ def size
+ if !loaded? and limit_value
+ [super, limit_value].min
+ else
+ super
+ end
+ end
+
+ def total_pages
+ (total_entries / limit_value.to_f).ceil
+ end
+
+ def clone
+ other = super
+ other.current_page = current_page unless other.current_page
+ other.total_entries = nil
+ other
+ end
+
+ def to_a
+ if current_page.nil? then super # workaround for Active Record 3.0
+ else
+ ::WillPaginate::Collection.create(current_page, limit_value) do |col|
+ col.replace super
+ col.total_entries ||= total_entries
+ end
+ end
+ end
+ end
+
+ module Pagination
+ def paginate(options)
+ options = options.dup
+ pagenum = options.fetch(:page) { raise ArgumentError, ":page parameter required" }
+ per_page = options.delete(:per_page) || self.per_page
+ total = options.delete(:total_entries)
+
+ count_options = options.delete(:count)
+ options.delete(:page)
+
+ rel = limit(per_page).page(pagenum)
+ rel = rel.apply_finder_options(options) if options.any?
+ rel.wp_count_options = count_options if count_options
+ rel
+ end
+
+ def page(num)
+ pagenum = num.nil? ? 1 : num.to_i
+ raise ::WillPaginate::InvalidPage, num, pagenum if pagenum < 1
+ rel = scoped.extending(RelationMethods)
+ rel = rel.offset((pagenum-1) * (rel.limit_value || per_page))
+ rel = rel.limit(per_page) unless rel.limit_value
+ rel.current_page = pagenum
+ rel
+ end
+ end
+
+ module BaseMethods
+ # 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
+ #
+ # A query for counting rows will automatically be generated if you don't
+ # supply :total_entries. If you experience problems with this
+ # generated SQL, you might want to perform the count manually in your
+ # application.
+ #
+ def paginate_by_sql(sql, options)
+ pagenum = options.fetch(:page) { raise ArgumentError, ":page parameter required" }
+ per_page = options[:per_page] || self.per_page
+ total = options[:total_entries]
+
+ WillPaginate::Collection.create(pagenum, per_page, total) do |pager|
+ query = sanitize_sql(sql.dup)
+ original_query = query.dup
+ # add limit, offset
+ query << " LIMIT #{pager.per_page} OFFSET #{pager.offset}"
+ # perfom the find
+ pager.replace find_by_sql(query)
+
+ unless pager.total_entries
+ count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
+ count_query = "SELECT COUNT(*) FROM (#{count_query})"
+
+ unless self.connection.adapter_name =~ /^(oracle|oci$)/i
+ count_query << ' AS count_table'
+ end
+ # perform the count query
+ pager.total_entries = count_by_sql(count_query)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/will_paginate/array.rb b/lib/will_paginate/array.rb
index d061d2be9..cd7f3ebca 100644
--- a/lib/will_paginate/array.rb
+++ b/lib/will_paginate/array.rb
@@ -1,16 +1,33 @@
require 'will_paginate/collection'
-# http://www.desimcadam.com/archives/8
-Array.class_eval do
+class Array
+ # Paginates a static array (extracting a subset of it). The result is a
+ # WillPaginate::Collection instance, which is an array with a few more
+ # properties about its paginated state.
+ #
+ # Parameters:
+ # * :page - current page, defaults to 1
+ # * :per_page - limit of items per page, defaults to 30
+ # * :total_entries - total number of items in the array, defaults to
+ # array.length (obviously)
+ #
+ # Example:
+ # arr = ['a', 'b', 'c', 'd', 'e']
+ # paged = arr.paginate(:per_page => 2) #-> ['a', 'b']
+ # paged.total_entries #-> 5
+ # arr.paginate(:page => 2, :per_page => 2) #-> ['c', 'd']
+ # arr.paginate(:page => 3, :per_page => 2) #-> ['e']
+ #
+ # This method was originally {suggested by Desi
+ # McAdam}[http://www.desimcadam.com/archives/8] and later proved to be the
+ # most useful method of will_paginate library.
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|
+ WillPaginate::Collection.create options[:page] || 1,
+ options[:per_page] || 30,
+ options[:total_entries] || self.length do |pager|
pager.replace self[pager.offset, pager.per_page].to_a
- }
+ end
end
end
diff --git a/lib/will_paginate/collection.rb b/lib/will_paginate/collection.rb
index 3ccb4a6aa..846c6d1a3 100644
--- a/lib/will_paginate/collection.rb
+++ b/lib/will_paginate/collection.rb
@@ -17,7 +17,7 @@ module WillPaginate
# requested. Use WillPaginate::Collection#out_of_bounds? method to
# check for those cases and manually deal with them as you see fit.
class InvalidPage < ArgumentError
- def initialize(page, page_num)
+ def initialize(page, page_num) #:nodoc:
super "#{page.inspect} given as value, which translates to '#{page_num}' as page number"
end
end
diff --git a/lib/will_paginate/core_ext.rb b/lib/will_paginate/core_ext.rb
index 3397736b3..7de066c68 100644
--- a/lib/will_paginate/core_ext.rb
+++ b/lib/will_paginate/core_ext.rb
@@ -1,18 +1,8 @@
require 'set'
-require 'will_paginate/array'
-# helper to check for method existance in ruby 1.8- and 1.9-compatible way
-# because `methods`, `instance_methods` and others return strings in 1.8 and symbols in 1.9
-#
-# ['foo', 'bar'].include_method?(:foo) # => true
-class Array
- def include_method?(name)
- name = name.to_sym
- !!(find { |item| item.to_sym == name })
- end
-end
+# copied from ActiveSupport so we don't depend on it
-unless Hash.instance_methods.include_method? :except
+unless Hash.method_defined? :except
Hash.class_eval do
# Returns a new hash without the given keys.
def except(*keys)
@@ -27,17 +17,14 @@ def except!(*keys)
end
end
-unless Hash.instance_methods.include_method? :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))
+unless String.method_defined? :underscore
+ String.class_eval do
+ def underscore
+ self.to_s.gsub(/::/, '/').
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
+ tr("-", "_").
+ downcase
end
end
end
diff --git a/lib/will_paginate/data_mapper.rb b/lib/will_paginate/data_mapper.rb
new file mode 100644
index 000000000..54e3de399
--- /dev/null
+++ b/lib/will_paginate/data_mapper.rb
@@ -0,0 +1,90 @@
+require 'dm-core'
+require 'dm-aggregates'
+require 'will_paginate/per_page'
+require 'will_paginate/collection'
+
+module WillPaginate
+ module DataMapper
+ module Pagination
+ def page(num)
+ pagenum = num.nil? ? 1 : num.to_i
+ raise ::WillPaginate::InvalidPage, num, pagenum if pagenum < 1
+ options = {:offset => (pagenum-1) * (query.limit || per_page)}
+ options[:limit] = per_page unless query.limit
+ col = new_collection(query.merge(options))
+ col.current_page = pagenum
+ col
+ end
+
+ def paginate(options)
+ options = options.dup
+ pagenum = options.fetch(:page) { raise ArgumentError, ":page parameter required" }
+ per_page = options.delete(:per_page) || self.per_page
+
+ options.delete(:page)
+ options[:limit] = per_page
+
+ all(options).page(pagenum)
+ end
+ end
+
+ module CollectionMethods
+ attr_accessor :current_page
+
+ def paginated?
+ !current_page.nil?
+ end
+
+ def per_page
+ query.limit || model.per_page
+ end
+
+ def offset
+ query.offset
+ end
+
+ def total_entries
+ @total_entries ||= begin
+ if loaded? and @array.size < per_page and (current_page == 1 or @array.size > 0)
+ offset + @array.size
+ else
+ clean_query = query.merge(:order => [])
+ # seems like the only way
+ clean_query.instance_variable_set('@limit', nil)
+ clean_query.instance_variable_set('@offset', 0)
+ new_collection(clean_query).count
+ end
+ end
+ end
+
+ def total_pages
+ (total_entries / per_page.to_f).ceil
+ end
+
+ def to_a
+ ::WillPaginate::Collection.create(current_page, per_page) do |col|
+ col.replace super
+ col.total_entries ||= total_entries
+ end
+ end
+
+ private
+
+ def new_collection(query, resources = nil)
+ col = super
+ col.current_page = self.current_page
+ col
+ end
+
+ def initialize_copy(original)
+ super
+ @total_entries = nil
+ end
+ end
+
+ ::DataMapper::Model.append_extensions PerPage
+ ::DataMapper::Model.append_extensions Pagination
+ ::DataMapper::Collection.send(:include, Pagination)
+ ::DataMapper::Collection.send(:include, CollectionMethods)
+ end
+end
diff --git a/lib/will_paginate/deprecation.rb b/lib/will_paginate/deprecation.rb
new file mode 100644
index 000000000..d74b09a04
--- /dev/null
+++ b/lib/will_paginate/deprecation.rb
@@ -0,0 +1,50 @@
+# borrowed from ActiveSupport::Deprecation
+module WillPaginate
+ module Deprecation
+ def self.debug() @debug; end
+ def self.debug=(value) @debug = value; end
+ self.debug = false
+
+ # Choose the default warn behavior according to Rails.env.
+ # Ignore deprecation warnings in production.
+ BEHAVIORS = {
+ 'test' => Proc.new { |message, callstack|
+ $stderr.puts(message)
+ $stderr.puts callstack.join("\n ") if debug
+ },
+ 'development' => Proc.new { |message, callstack|
+ logger = defined?(::RAILS_DEFAULT_LOGGER) ? ::RAILS_DEFAULT_LOGGER : Logger.new($stderr)
+ logger.warn message
+ logger.debug callstack.join("\n ") if debug
+ }
+ }
+
+ def self.warn(message, callstack = caller)
+ if behavior
+ message = 'WillPaginate: ' + message.strip.gsub(/\s+/, ' ')
+ behavior.call(message, callstack)
+ end
+ end
+
+ def self.default_behavior
+ if defined?(::Rails)
+ BEHAVIORS[::Rails.env.to_s]
+ else
+ BEHAVIORS['test']
+ end
+ end
+
+ # Behavior is a block that takes a message argument.
+ def self.behavior() @behavior; end
+ def self.behavior=(value) @behavior = value; end
+ self.behavior = default_behavior
+
+ def self.silence
+ old_behavior = self.behavior
+ self.behavior = nil
+ yield
+ ensure
+ self.behavior = old_behavior
+ end
+ end
+end
diff --git a/lib/will_paginate/finder.rb b/lib/will_paginate/finder.rb
deleted file mode 100644
index e121c5ff3..000000000
--- a/lib/will_paginate/finder.rb
+++ /dev/null
@@ -1,264 +0,0 @@
-require 'will_paginate/core_ext'
-
-module WillPaginate
- # A mixin for ActiveRecord::Base. Provides +per_page+ class method
- # and hooks things up to provide paginating finders.
- #
- # Find out more in WillPaginate::Finder::ClassMethods
- #
- module Finder
- def self.included(base)
- base.extend ClassMethods
- class << base
- alias_method_chain :method_missing, :paginate
- # alias_method_chain :find_every, :paginate
- define_method(:per_page) { 30 } unless respond_to?(:per_page)
- end
- end
-
- # = Paginating finders for ActiveRecord models
- #
- # WillPaginate adds +paginate+, +per_page+ and other methods to
- # ActiveRecord::Base class methods and associations. It also hooks into
- # +method_missing+ to intercept pagination calls to dynamic finders such as
- # +paginate_by_user_id+ and translate them to ordinary finders
- # (+find_all_by_user_id+ in this case).
- #
- # In short, paginating finders are equivalent to ActiveRecord finders; the
- # only difference is that we start with "paginate" instead of "find" and
- # that :page is required parameter:
- #
- # @posts = Post.paginate :all, :page => params[:page], :order => 'created_at DESC'
- #
- # In paginating finders, "all" is implicit. There is no sense in paginating
- # a single record, right? So, you can drop the :all argument:
- #
- # Post.paginate(...) => Post.find :all
- # Post.paginate_all_by_something => Post.find_all_by_something
- # Post.paginate_by_something => Post.find_all_by_something
- #
- # == The importance of the :order parameter
- #
- # In ActiveRecord finders, :order parameter specifies columns for
- # the ORDER BY clause in SQL. It is important to have it, since
- # pagination only makes sense with ordered sets. Without the ORDER
- # BY clause, databases aren't required to do consistent ordering when
- # performing SELECT queries; this is especially true for
- # PostgreSQL.
- #
- # Therefore, make sure you are doing ordering on a column that makes the
- # most sense in the current context. Make that obvious to the user, also.
- # For perfomance reasons you will also want to add an index to that column.
- module ClassMethods
- # This is the main paginating finder.
- #
- # == Special parameters for paginating finders
- # * :page -- REQUIRED, but defaults to 1 if false or nil
- # * :per_page -- defaults to CurrentModel.per_page (which is 30 if not overridden)
- # * :total_entries -- use only if you manually count total entries
- # * :count -- additional options that are passed on to +count+
- # * :finder -- name of the ActiveRecord finder used (default: "find")
- #
- # All other options (+conditions+, +order+, ...) are forwarded to +find+
- # and +count+ calls.
- def paginate(*args)
- options = args.pop
- page, per_page, total_entries = wp_parse_options(options)
- finder = (options[:finder] || 'find').to_s
-
- if finder == 'find'
- # an array of IDs may have been given:
- total_entries ||= (Array === args.first and args.first.size)
- # :all is implicit
- args.unshift(:all) if args.empty?
- end
-
- WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
- count_options = options.except :page, :per_page, :total_entries, :finder
- find_options = count_options.except(:count).update(:offset => pager.offset, :limit => pager.per_page)
-
- args << find_options
- # @options_from_last_find = nil
- pager.replace(send(finder, *args) { |*a| yield(*a) if block_given? })
-
- # magic counting for user convenience:
- pager.total_entries = wp_count(count_options, args, finder) unless pager.total_entries
- 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 :page (default is 1). Default
- # :order is "id", override if necessary.
- #
- # See {Faking Cursors in ActiveRecord}[http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord]
- # where Jamis Buck describes this and a more efficient way for MySQL.
- def paginated_each(options = {})
- 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)
- with_exclusive_scope(:find => {}) do
- # using exclusive scope so that the block is yielded in scope-free context
- total += collection.each { |item| yield item }.size
- end
- 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
- # 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
- #
- # A query for counting rows will automatically be generated if you don't
- # supply :total_entries. If you experience problems with this
- # generated SQL, you might want to perform the count manually in your
- # application.
- #
- def paginate_by_sql(sql, options)
- WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
- query = sanitize_sql(sql.dup)
- original_query = query.dup
- # add limit, offset
- add_limit! query, :offset => pager.offset, :limit => pager.per_page
- # perfom the find
- pager.replace find_by_sql(query)
-
- unless pager.total_entries
- count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
- count_query = "SELECT COUNT(*) FROM (#{count_query})"
-
- unless self.connection.adapter_name =~ /^(oracle|oci$)/i
- count_query << ' AS count_table'
- end
- # perform the count query
- pager.total_entries = count_by_sql(count_query)
- end
- end
- end
-
- def respond_to?(method, include_priv = false) #:nodoc:
- case method.to_sym
- when :paginate, :paginate_by_sql
- true
- else
- super || super(method.to_s.sub(/^paginate/, 'find'), include_priv)
- end
- end
-
- protected
-
- def method_missing_with_paginate(method, *args) #:nodoc:
- # did somebody tried to paginate? if not, let them be
- unless method.to_s.index('paginate') == 0
- if block_given?
- return method_missing_without_paginate(method, *args) { |*a| yield(*a) }
- else
- return method_missing_without_paginate(method, *args)
- end
- end
-
- # paginate finders are really just find_* with limit and offset
- finder = method.to_s.sub('paginate', 'find')
- finder.sub!('find', 'find_all') if finder.index('find_by_') == 0
-
- options = args.pop
- raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
- options = options.dup
- options[:finder] = finder
- args << options
-
- paginate(*args) { |*a| yield(*a) if block_given? }
- end
-
- # Does the not-so-trivial job of finding out the total number of entries
- # in the database. It relies on the ActiveRecord +count+ method.
- def wp_count(options, args, finder)
- excludees = [:count, :order, :limit, :offset, :readonly]
- excludees << :from unless ActiveRecord::Calculations::CALCULATIONS_OPTIONS.include?(:from)
-
- # we may be in a model or an association proxy
- klass = (@owner and @reflection) ? @reflection.klass : self
-
- # Use :select from scope if it isn't already present.
- options[:select] = scope(:find, :select) unless options[:select]
-
- if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
- # Remove quoting and check for table_name.*-like statement.
- if options[:select].gsub(/[`"]/, '') =~ /\w+\.\*/
- options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
- end
- else
- 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[:count] if options[:count]
-
- # forget about includes if they are irrelevant (Rails 2.1)
- if count_options[:include] and
- klass.private_methods.include_method?(:references_eager_loaded_tables?) and
- !klass.send(:references_eager_loaded_tables?, count_options)
- count_options.delete :include
- end
-
- # we may have to scope ...
- counter = Proc.new { count(count_options) }
-
- 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
- # then execute the count with the scoping provided by the with_finder
- send(scoper, &counter)
- elsif finder =~ /^find_(all_by|by)_([_a-zA-Z]\w*)$/
- # extract conditions from calls like "paginate_by_foo_and_bar"
- attribute_names = $2.split('_and_')
- conditions = construct_attributes_from_arguments(attribute_names, args)
- with_scope(:find => { :conditions => conditions }, &counter)
- else
- counter.call
- end
-
- (!count.is_a?(Integer) && count.respond_to?(:length)) ? count.length : count
- end
-
- def wp_parse_options(options) #:nodoc:
- raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
- options = 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'
- end
-
- page = options[:page] || 1
- per_page = options[:per_page] || self.per_page
- total = options[:total_entries]
- [page, per_page, total]
- end
-
- private
-
- # def find_every_with_paginate(options)
- # @options_from_last_find = options
- # find_every_without_paginate(options)
- # end
- end
- end
-end
diff --git a/lib/will_paginate/locale/en.yml b/lib/will_paginate/locale/en.yml
new file mode 100644
index 000000000..1fde2d17c
--- /dev/null
+++ b/lib/will_paginate/locale/en.yml
@@ -0,0 +1,19 @@
+en:
+ views:
+ will_paginate:
+ previous_label: "← Previous"
+ next_label: "Next →"
+ page_gap: "…"
+
+ page_entries_info:
+ single_page:
+ zero: "No %{plural} found"
+ one: "Displaying 1 %{name}"
+ other: "Displaying all %{count} %{plural}"
+ single_page_html:
+ zero: "No %{plural} found"
+ one: "Displaying 1 %{name}"
+ other: "Displaying all %{count} %{plural}"
+
+ multi_page: "Displaying %{plural} %{from} - %{to} of %{total} in total"
+ multi_page_html: "Displaying %{plural} %{from} - %{to} of %{total} in total"
diff --git a/lib/will_paginate/named_scope.rb b/lib/will_paginate/named_scope.rb
deleted file mode 100644
index 5a743d7fb..000000000
--- a/lib/will_paginate/named_scope.rb
+++ /dev/null
@@ -1,170 +0,0 @@
-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:
- # * all, which is similar to a find(:all) query, and
- # * scoped, 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 :scoped, lambda { |scope| scope }
- end
- end
-
- module ClassMethods
- def scopes
- 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 :conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions.
- #
- # 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 named_scope define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
- # in effect, represents the query Shirt.find(:all, :conditions => {:color => 'red'}).
- #
- # Unlike Shirt.find(...), however, the object returned by Shirt.red is not an Array; it resembles the association object
- # constructed by a has_many declaration. For instance, you can invoke Shirt.red.find(:first), Shirt.red.count,
- # Shirt.red.find(:all, :conditions => {:size => 'small'}). Also, just
- # as with the association objects, name scopes acts like an Array, implementing Enumerable; Shirt.red.each(&block),
- # Shirt.red.first, and Shirt.red.inject(memo, &block) all behave as if Shirt.red really were an Array.
- #
- # These named scopes are composable. For instance, Shirt.red.dry_clean_only will produce all shirts that are both red and dry clean only.
- # Nested finds and calculations also work with these compositions: Shirt.red.dry_clean_only.count returns the number of garments
- # for which these criteria obtain. Similarly with Shirt.red.dry_clean_only.average(:thread_count).
- #
- # All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
- # has_many associations. If,
- #
- # class Person < ActiveRecord::Base
- # has_many :shirts
- # end
- #
- # then elton.shirts.red.dry_clean_only 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, Shirt.colored('puce') finds all puce shirts.
- #
- # Named scopes can also have extensions, just as with has_many declarations:
- #
- # class Shirt < ActiveRecord::Base
- # named_scope :red, :conditions => {:color => 'red'} do
- # def dom_id
- # 'red_shirts'
- # end
- # end
- # end
- #
- #
- # For testing complex named scopes, you can examine the scoping options using the
- # proxy_options method on the proxy itself.
- #
- # class Shirt < ActiveRecord::Base
- # named_scope :colored, lambda { |color|
- # { :conditions => { :color => color } }
- # }
- # end
- #
- # expected_options = { :conditions => { :colored => 'red' } }
- # assert_equal expected_options, Shirt.colored('red').proxy_options
- def named_scope(name, options = {})
- name = name.to_sym
- scopes[name] = lambda do |parent_scope, *args|
- Scope.new(parent_scope, case options
- when Hash
- options
- when Proc
- options.call(*args)
- end) { |*a| yield(*a) if block_given? }
- end
- (class << self; self end).instance_eval do
- define_method name do |*args|
- scopes[name].call(self, *args)
- end
- end
- end
- end
-
- class Scope
- attr_reader :proxy_scope, :proxy_options
-
- [].methods.each do |m|
- unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
- delegate m, :to => :proxy_found
- end
- end
-
- delegate :scopes, :with_scope, :to => :proxy_scope
-
- def initialize(proxy_scope, options)
- [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
- extend Module.new { |*args| yield(*args) } if block_given?
- @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
- end
-
- def reload
- load_found; self
- end
-
- def first(*args)
- if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
- proxy_found.first(*args)
- else
- find(:first, *args)
- end
- end
-
- def last(*args)
- if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
- proxy_found.last(*args)
- else
- find(:last, *args)
- end
- end
-
- def empty?
- @found ? @found.empty? : count.zero?
- end
-
- def respond_to?(method, include_private = false)
- super || @proxy_scope.respond_to?(method, include_private)
- end
-
- protected
- def proxy_found
- @found || load_found
- end
-
- private
- def method_missing(method, *args)
- if scopes.include?(method)
- scopes[method].call(self, *args)
- else
- with_scope :find => proxy_options do
- proxy_scope.send(method, *args) { |*a| yield(*a) if block_given? }
- end
- end
- end
-
- def load_found
- @found = find(:all)
- end
- end
- end
-end
diff --git a/lib/will_paginate/named_scope_patch.rb b/lib/will_paginate/named_scope_patch.rb
deleted file mode 100644
index 7daff5922..000000000
--- a/lib/will_paginate/named_scope_patch.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-ActiveRecord::Associations::AssociationProxy.class_eval do
- protected
- def with_scope(*args)
- @reflection.klass.send(:with_scope, *args) { |*a| yield(*a) if block_given? }
- 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)
- if @reflection.klass.scopes.include?(method)
- @reflection.klass.scopes[method].call(self, *args) { |*a| yield(*a) if block_given? }
- else
- method_missing_without_scopes(method, *args) { |*a| yield(*a) if block_given? }
- end
- end
- end
-end
-
-# Rails 1.2.6
-ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do
- protected
- def method_missing(method, *args)
- 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) { |*a| yield(*a) if block_given? }
- end
- end
- end
-end if ActiveRecord::Base.respond_to? :find_first
diff --git a/lib/will_paginate/per_page.rb b/lib/will_paginate/per_page.rb
new file mode 100644
index 000000000..29303e1dd
--- /dev/null
+++ b/lib/will_paginate/per_page.rb
@@ -0,0 +1,27 @@
+module WillPaginate
+ module PerPage
+ def per_page
+ defined?(@per_page) ? @per_page : WillPaginate.per_page
+ end
+
+ def per_page=(limit)
+ @per_page = limit.to_i
+ end
+
+ def self.extended(base)
+ base.extend Inheritance if base.is_a? Class
+ end
+
+ module Inheritance
+ def inherited(subclass)
+ super
+ subclass.per_page = self.per_page
+ end
+ end
+ end
+
+ extend PerPage
+
+ # default number of items per page
+ self.per_page = 30
+end
diff --git a/lib/will_paginate/railtie.rb b/lib/will_paginate/railtie.rb
new file mode 100644
index 000000000..6b2b4773e
--- /dev/null
+++ b/lib/will_paginate/railtie.rb
@@ -0,0 +1,25 @@
+require 'will_paginate'
+require 'will_paginate/collection'
+
+module WillPaginate
+ class Railtie < Rails::Railtie
+ initializer "will_paginate" do |app|
+ ActiveSupport.on_load :active_record do
+ require 'will_paginate/active_record'
+ WillPaginate::ActiveRecord.setup
+ end
+
+ ActiveSupport.on_load :action_controller do
+ ActionDispatch::ShowExceptions.rescue_responses['WillPaginate::InvalidPage'] = :not_found
+ end
+
+ ActiveSupport.on_load :action_view do
+ require 'will_paginate/view_helpers/action_view'
+ include WillPaginate::ActionView
+ end
+
+ locale_path = File.expand_path('../locale', __FILE__)
+ config.i18n.railties_load_path.concat Dir["#{locale_path}/*.{rb,yml}"]
+ end
+ end
+end
diff --git a/lib/will_paginate/sequel.rb b/lib/will_paginate/sequel.rb
new file mode 100644
index 000000000..5826229f5
--- /dev/null
+++ b/lib/will_paginate/sequel.rb
@@ -0,0 +1,29 @@
+require 'sequel'
+require 'sequel/extensions/pagination'
+
+module WillPaginate
+ module SequelMethods
+ def total_pages
+ page_count
+ end
+
+ def per_page
+ page_size
+ end
+
+ def total_entries
+ pagination_record_count
+ end
+
+ def out_of_bounds?
+ current_page > total_pages
+ end
+
+ # Current offset of the paginated collection
+ def offset
+ (current_page - 1) * per_page
+ end
+ end
+
+ Sequel::Dataset::Pagination.send(:include, SequelMethods)
+end
diff --git a/lib/will_paginate/version.rb b/lib/will_paginate/version.rb
index b7de1ad36..e42476c47 100644
--- a/lib/will_paginate/version.rb
+++ b/lib/will_paginate/version.rb
@@ -1,8 +1,8 @@
-module WillPaginate
- module VERSION
- MAJOR = 2
- MINOR = 3
- TINY = 15
+module WillPaginate #:nodoc:
+ module VERSION #:nodoc:
+ MAJOR = 3
+ MINOR = 0
+ TINY = 'pre4'
STRING = [MAJOR, MINOR, TINY].join('.')
end
diff --git a/lib/will_paginate/view_helpers.rb b/lib/will_paginate/view_helpers.rb
index 48972b6e4..637c2ff84 100644
--- a/lib/will_paginate/view_helpers.rb
+++ b/lib/will_paginate/view_helpers.rb
@@ -1,156 +1,92 @@
+# encoding: utf-8
require 'will_paginate/core_ext'
module WillPaginate
# = Will Paginate view helpers
#
- # The main view helper, #will_paginate, renders
- # pagination links for the given collection. The helper itself is lightweight
- # and serves only as a wrapper around LinkRenderer instantiation; the
- # renderer then does all the hard work of generating the HTML.
- #
- # == Global options for 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[:previous_label] = 'Previous page'
- #
- # By putting this into "config/initializers/will_paginate.rb" (or simply environment.rb in
- # older versions of Rails) you can easily translate link texts to previous
- # and next pages, as well as override some other defaults to your liking.
+ # The main view helper is +will_paginate+. It renders the pagination links
+ # for the given collection. The helper itself is lightweight and serves only
+ # as a wrapper around LinkRenderer instantiation; the renderer then does
+ # all the hard work of generating the HTML.
module ViewHelpers
- # default options that can be overridden on the global level
- @@pagination_options = {
+ class << self
+ # Write to this hash to override default options on the global level:
+ #
+ # WillPaginate::ViewHelpers.pagination_options[:page_links] = false
+ #
+ attr_accessor :pagination_options
+ end
+
+ # default view options
+ self.pagination_options = {
:class => 'pagination',
- :previous_label => '« Previous',
- :next_label => 'Next »',
+ :previous_label => nil,
+ :next_label => nil,
: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
+ :link_separator => ' ', # single space is friendly to spiders and non-graphic browsers
:param_name => :page,
:params => nil,
- :renderer => 'WillPaginate::LinkRenderer',
+ :renderer => nil,
:page_links => true,
:container => true
}
- mattr_reader :pagination_options
- # Renders Digg/Flickr-style pagination for a WillPaginate::Collection
- # object. Nil is returned if there is only one page in total; no point in
- # rendering the pagination in that case...
+ # Returns HTML representing page links for a WillPaginate::Collection-like object.
+ # In case there is no more than one page in total, nil is returned.
#
# ==== Options
- # Display options:
- # * :previous_label -- default: "« Previous" (this parameter is called :prev_label in versions 2.3.2 and older!)
+ # * :class -- CSS class name for the generated DIV (default: "pagination")
+ # * :previous_label -- default: "« Previous"
# * :next_label -- default: "Next »"
# * :page_links -- when false, only previous/next links are rendered (default: true)
# * :inner_window -- how many links are shown around the current page (default: 4)
# * :outer_window -- how many links are around the first and the last page (default: 1)
- # * :separator -- string separator for page HTML elements (default: single space)
- #
- # HTML options:
- # * :class -- CSS class name for the generated DIV (default: "pagination")
- # * :container -- toggles rendering of the DIV container for pagination links, set to
- # false only when you are rendering your own pagination markup (default: true)
- # * :id -- HTML ID for the container (default: nil). Pass +true+ to have the ID
- # automatically generated from the class name of objects in collection: for example, paginating
- # ArticleComment models would yield an ID of "article_comments_pagination".
- #
- # Advanced options:
+ # * :link_separator -- string separator for page HTML elements (default: single space)
# * :param_name -- parameter name for page number in URLs (default: :page)
# * :params -- additional parameters when generating pagination links
# (eg. :controller => "foo", :action => nil)
# * :renderer -- class name, class or instance of a link renderer (default:
# WillPaginate::LinkRenderer)
+ # * :page_links -- when false, only previous/next links are rendered (default: true)
+ # * :container -- toggles rendering of the DIV container for pagination links, set to
+ # false only when you are rendering your own pagination markup (default: true)
#
# All options not recognized by will_paginate will become HTML attributes on the container
# element for pagination links (the DIV). For example:
#
- # <%= will_paginate @posts, :style => 'font-size: small' %>
- #
- # ... will result in:
- #
- #
...
+ # <%= will_paginate @posts, :style => 'color:blue' %>
#
- # ==== Using the helper without arguments
- # If the helper is called without passing in the collection object, it will
- # try to read from the instance variable inferred by the controller name.
- # For example, calling +will_paginate+ while the current controller is
- # PostsController will result in trying to read from the @posts
- # variable. Example:
- #
- # <%= will_paginate :id => true %>
- #
- # ... will result in @post collection getting paginated:
+ # will result in:
#
- #
...
+ #
...
#
- def will_paginate(collection = nil, options = {})
- options, collection = collection, nil if collection.is_a? Hash
- unless collection or !controller
- collection_name = "@#{controller.controller_name}"
- collection = instance_variable_get(collection_name)
- raise ArgumentError, "The #{collection_name} variable appears to be empty. Did you " +
- "forget to pass the collection object for will_paginate?" unless collection
- end
+ def will_paginate(collection, options = {})
# early exit if there is nothing to render
- return nil unless WillPaginate::ViewHelpers.total_pages_for_collection(collection) > 1
-
- options = options.symbolize_keys.reverse_merge WillPaginate::ViewHelpers.pagination_options
- if options[:prev_label]
- WillPaginate::Deprecation::warn(":prev_label view parameter is now :previous_label; the old name has been deprecated", caller)
- options[:previous_label] = options.delete(:prev_label)
- end
-
+ return nil unless collection.total_pages > 1
+
+ options = WillPaginate::ViewHelpers.pagination_options.merge(options)
+
+ scope = 'views.will_paginate'
+ options[:previous_label] ||= will_paginate_translate(:previous_label, :scope => scope) { '← Previous' }
+ options[:next_label] ||= will_paginate_translate(:next_label, :scope => scope) { 'Next →' }
+
# get the renderer instance
renderer = case options[:renderer]
+ when nil
+ raise ArgumentError, ":renderer not specified"
when String
- options[:renderer].to_s.constantize.new
- when Class
- options[:renderer].new
- else
- options[:renderer]
+ klass = if options[:renderer].respond_to? :constantize then options[:renderer].constantize
+ else Object.const_get(options[:renderer]) # poor man's constantize
+ end
+ klass.new
+ when Class then options[:renderer].new
+ else options[:renderer]
end
# render HTML for pagination
renderer.prepare collection, options, self
renderer.to_html
end
-
- # Wrapper for rendering pagination links at both top and bottom of a block
- # of content.
- #
- # <% paginated_section @posts do %>
- #
- # <% for post in @posts %>
- #
...
- # <% end %>
- #
- # <% end %>
- #
- # will result in:
- #
- #
...
- #
- # ...
- #
- #
...
- #
- # Arguments are passed to a will_paginate call, so the same options
- # apply. Don't use the :id 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
-
- unless ActionView::Base.respond_to? :erb_variable
- concat pagination
- yield
- concat pagination
- else
- content = pagination + capture(&block) + pagination
- concat(content, block.binding)
- end
- end
# Renders a helpful message with numbers of displayed vs. total entries.
# You can use this as a blueprint for your own, similar helpers.
@@ -164,246 +100,64 @@ def paginated_section(*args, &block)
#
# <%= page_entries_info @posts, :entry_name => 'item' %>
# #-> Displaying items 6 - 10 of 26 in total
+ #
+ # Entry name is entered in singular and pluralized with
+ # String#pluralize method from ActiveSupport. If it isn't
+ # loaded, specify plural with :plural_name parameter:
+ #
+ # <%= page_entries_info @posts, :entry_name => 'item', :plural_name => 'items' %>
+ #
+ # By default, this method produces HTML output. You can trigger plain
+ # text output by passing :html => false in options.
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 1 #{entry_name}"
- else; "Displaying all #{collection.size} #{entry_name.pluralize}"
- end
+ entry_name = options[:entry_name] || (collection.empty?? 'entry' :
+ collection.first.class.name.underscore.gsub('_', ' '))
+
+ plural_name = if options[:plural_name]
+ options[:plural_name]
+ elsif entry_name == 'entry'
+ plural_name = 'entries'
+ elsif entry_name.respond_to? :pluralize
+ plural_name = entry_name.pluralize
else
- %{Displaying #{entry_name.pluralize} %d - %d of %d in total} % [
- collection.offset + 1,
- collection.offset + collection.length,
- collection.total_entries
- ]
- end
- end
-
- if respond_to? :safe_helper
- safe_helper :will_paginate, :paginated_section, :page_entries_info
- end
-
- def self.total_pages_for_collection(collection) #:nodoc:
- if collection.respond_to?('page_count') and !collection.respond_to?('total_pages')
- WillPaginate::Deprecation.warn %{
- 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}, caller
- class << collection
- def total_pages; page_count; end
- end
- end
- collection.total_pages
- end
- end
-
- # This class does the heavy lifting of actually building the pagination
- # links. It is used by the will_paginate helper internally.
- class LinkRenderer
-
- # The gap in page links is represented by:
- #
- # …
- attr_accessor :gap_marker
-
- def initialize
- @gap_marker = '…'
- 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
- @options = options
- @template = template
-
- # reset values in case we're re-using this instance
- @total_pages = @param_name = @url_string = nil
- 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
- links = @options[:page_links] ? windowed_links : []
- # previous/next buttons
- links.unshift page_link_or_span(@collection.previous_page, 'disabled prev_page', @options[:previous_label])
- links.push page_link_or_span(@collection.next_page, 'disabled next_page', @options[:next_label])
-
- html = links.join(@options[:separator])
- html = html.html_safe if html.respond_to? :html_safe
- @options[:container] ? @template.content_tag(:div, html, html_attributes) : html
- 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
- return @html_attributes if @html_attributes
- @html_attributes = @options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class])
- # pagination of Post models will have the ID of "posts_pagination"
- if @options[:container] and @options[:id] === true
- @html_attributes[:id] = @collection.first.class.name.underscore.pluralize + '_pagination'
+ raise ArgumentError, "must provide :plural_name for #{entry_name.inspect}"
end
- @html_attributes
- end
-
- protected
-
- # Collects link items for visible page numbers.
- def windowed_links
- prev = nil
-
- visible_page_numbers.inject [] do |links, n|
- # detect gaps:
- links << gap_marker if prev and n > prev + 1
- links << page_link_or_span(n, 'current')
- prev = n
- links
- end
- end
- # Calculates visible page numbers using the :inner_window and
- # :outer_window options.
- def visible_page_numbers
- inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i
- window_from = current_page - inner_window
- window_to = current_page + inner_window
-
- # adjust lower or upper limit if other is out of bounds
- if window_to > total_pages
- window_from -= window_to - total_pages
- window_to = total_pages
- end
- if window_from < 1
- window_to += 1 - window_from
- window_from = 1
- window_to = total_pages if window_to > total_pages
- end
-
- visible = (1..total_pages).to_a
- left_gap = (2 + outer_window)...window_from
- right_gap = (window_to + 1)...(total_pages - outer_window)
- visible -= left_gap.to_a if left_gap.last - left_gap.first > 1
- visible -= right_gap.to_a if right_gap.last - right_gap.first > 1
-
- visible
- end
-
- def page_link_or_span(page, span_class, text = nil)
- text ||= page.to_s
- text = text.html_safe if text.respond_to? :html_safe
-
- if page and page != current_page
- classnames = span_class && span_class.index(' ') && span_class.split(' ', 2).last
- page_link page, text, :rel => rel_value(page), :class => classnames
+ unless options[:html] == false
+ b = ''
+ eb = ''
+ sp = ' '
+ key = '_html'
else
- page_span page, text, :class => span_class
+ b = eb = key = ''
+ sp = ' '
end
- end
- def page_link(page, text, attributes = {})
- @template.link_to text, url_for(page), attributes
- end
-
- def page_span(page, text, attributes = {})
- @template.content_tag :span, text, attributes
- end
+ scope = 'views.will_paginate.page_entries_info'
- # Returns URL params for +page_link_or_span+, taking the current GET params
- # and :params 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 = 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!((?:\?|&)#{CGI.escape param_name}=)#{page}!, "\\1\0")
- 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] = "\0"
- break
+ if collection.total_pages < 2
+ will_paginate_translate "single_page#{key}", :scope => scope, :count => collection.size,
+ :name => entry_name, :plural => plural_name do |_, opts|
+ case opts[:count]
+ when 0; "No #{opts[:plural]} found"
+ when 1; "Displaying #{b}1#{eb} #{opts[:name]}"
+ else "Displaying #{b}all #{opts[:count]}#{eb} #{opts[:plural]}"
end
end
- end
- end
- # finally!
- @url_string.sub "\0", page.to_s
- end
-
- private
-
- def rel_value(page)
- case page
- when @collection.previous_page; 'prev' + (page == 1 ? ' start' : '')
- when @collection.next_page; 'next'
- when 1; 'start'
- end
- end
-
- def current_page
- @collection.current_page
- end
-
- def total_pages
- @total_pages ||= WillPaginate::ViewHelpers.total_pages_for_collection(@collection)
- end
-
- def param_name
- @param_name ||= @options[:param_name].to_s
- end
-
- # Recursively merge into target hash by using stringified keys from the other one
- 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
+ else
+ will_paginate_translate "multi_page#{key}", :scope => scope, :total => collection.total_entries, :plural => plural_name,
+ :from => collection.offset + 1, :to => collection.offset + collection.length do |_, opts|
+ %{Displaying %s #{b}%d#{sp}-#{sp}%d#{eb} of #{b}%d#{eb} in total} %
+ [ opts[:plural], opts[:from], opts[:to], opts[:total] ]
+ end
end
end
- def parse_query_parameters(params)
- if defined? Rack::Utils
- # For Rails > 2.3
- Rack::Utils.parse_nested_query(params)
- elsif defined?(ActionController::AbstractRequest)
- ActionController::AbstractRequest.parse_query_parameters(params)
- elsif defined?(ActionController::UrlEncodedPairParser)
- # For Rails > 2.2
- ActionController::UrlEncodedPairParser.parse_query_parameters(params)
- elsif defined?(CGIMethods)
- CGIMethods.parse_query_parameters(params)
+ def will_paginate_translate(key, options = {})
+ if defined? ::I18n
+ ::I18n.translate(key, options.merge(:default => Proc.new))
else
- raise "unsupported ActionPack version"
+ yield key, options
end
end
end
diff --git a/lib/will_paginate/view_helpers/action_view.rb b/lib/will_paginate/view_helpers/action_view.rb
new file mode 100644
index 000000000..cfe502d32
--- /dev/null
+++ b/lib/will_paginate/view_helpers/action_view.rb
@@ -0,0 +1,139 @@
+require 'will_paginate/view_helpers'
+require 'will_paginate/view_helpers/link_renderer'
+
+module WillPaginate
+ # = ActionView helpers
+ #
+ # This module serves for availability in ActionView templates. It also adds a new
+ # view helper: +paginated_section+.
+ #
+ # == Using the helper without arguments
+ # If the helper is called without passing in the collection object, it will
+ # try to read from the instance variable inferred by the controller name.
+ # For example, calling +will_paginate+ while the current controller is
+ # PostsController will result in trying to read from the @posts
+ # variable. Example:
+ #
+ # <%= will_paginate :id => true %>
+ #
+ # ... will result in @post collection getting paginated:
+ #
+ #
...
+ #
+ module ActionView
+ include ViewHelpers
+
+ def will_paginate(collection = nil, options = {}) #:nodoc:
+ options, collection = collection, nil if collection.is_a? Hash
+ collection ||= infer_collection_from_controller
+
+ options = options.symbolize_keys
+ options[:renderer] ||= LinkRenderer
+
+ super(collection, options).try(:html_safe)
+ end
+
+ def page_entries_info(collection = nil, options = {}) #:nodoc:
+ options, collection = collection, nil if collection.is_a? Hash
+ collection ||= infer_collection_from_controller
+
+ super(collection, options.symbolize_keys)
+ end
+
+ # Wrapper for rendering pagination links at both top and bottom of a block
+ # of content.
+ #
+ # <% paginated_section @posts do %>
+ #
+ # <% for post in @posts %>
+ #
...
+ # <% end %>
+ #
+ # <% end %>
+ #
+ # will result in:
+ #
+ #
...
+ #
+ # ...
+ #
+ #
...
+ #
+ # Arguments are passed to a will_paginate call, so the same options
+ # apply. Don't use the :id 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)
+ if pagination
+ pagination + capture(&block) + pagination
+ else
+ capture(&block)
+ end
+ end
+
+ def will_paginate_translate(key, options = {})
+ translate(key, options)
+ end
+
+ protected
+
+ def infer_collection_from_controller
+ collection_name = "@#{controller.controller_name}"
+ collection = instance_variable_get(collection_name)
+ raise ArgumentError, "The #{collection_name} variable appears to be empty. Did you " +
+ "forget to pass the collection object for will_paginate?" if collection.nil?
+ collection
+ end
+
+ class LinkRenderer < ViewHelpers::LinkRenderer
+ protected
+
+ def default_url_params
+ {}
+ end
+
+ def url(page)
+ @base_url_params ||= begin
+ url_params = base_url_params
+ merge_optional_params(url_params)
+ url_params
+ end
+
+ url_params = @base_url_params.dup
+ add_current_page_param(url_params, page)
+
+ @template.url_for(url_params)
+ end
+
+ def base_url_params
+ url_params = default_url_params
+ # page links should preserve GET parameters
+ symbolized_update(url_params, @template.params) if get_request?
+ url_params
+ end
+
+ def merge_optional_params(url_params)
+ symbolized_update(url_params, @options[:params]) if @options[:params]
+ end
+
+ def add_current_page_param(url_params, page)
+ unless param_name.index(/[^\w-]/)
+ url_params[param_name.to_sym] = page
+ else
+ page_param = parse_query_parameters("#{param_name}=#{page}")
+ symbolized_update(url_params, page_param)
+ end
+ end
+
+ def get_request?
+ @template.request.get?
+ end
+
+ private
+
+ def parse_query_parameters(params)
+ Rack::Utils.parse_nested_query(params)
+ end
+ end
+ end
+end
diff --git a/lib/will_paginate/view_helpers/link_renderer.rb b/lib/will_paginate/view_helpers/link_renderer.rb
new file mode 100644
index 000000000..b8e59159f
--- /dev/null
+++ b/lib/will_paginate/view_helpers/link_renderer.rb
@@ -0,0 +1,131 @@
+require 'cgi'
+require 'will_paginate/core_ext'
+require 'will_paginate/view_helpers'
+require 'will_paginate/view_helpers/link_renderer_base'
+
+module WillPaginate
+ module ViewHelpers
+ # This class does the heavy lifting of actually building the pagination
+ # links. It is used by +will_paginate+ helper internally.
+ class LinkRenderer < LinkRendererBase
+
+ # * +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)
+ super(collection, options)
+ @template = template
+ @container_attributes = @base_url_params = nil
+ 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
+ html = pagination.map do |item|
+ item.is_a?(Fixnum) ?
+ page_number(item) :
+ send(item)
+ end.join(@options[:link_separator])
+
+ @options[:container] ? html_container(html) : html
+ end
+
+ # Returns the subset of +options+ this instance was initialized with that
+ # represent HTML attributes for the container element of pagination links.
+ def container_attributes
+ @container_attributes ||= @options.except(*(ViewHelpers.pagination_options.keys - [:class]))
+ end
+
+ protected
+
+ def page_number(page)
+ unless page == current_page
+ link(page, page, :rel => rel_value(page))
+ else
+ tag(:em, page)
+ end
+ end
+
+ def gap
+ text = @template.will_paginate_translate('views.will_paginate.page_gap') { '…' }
+ %(#{text})
+ end
+
+ def previous_page
+ num = @collection.current_page > 1 && @collection.current_page - 1
+ previous_or_next_page(num, @options[:previous_label], 'previous_page')
+ end
+
+ def next_page
+ num = @collection.current_page < @collection.total_pages && @collection.current_page + 1
+ previous_or_next_page(num, @options[:next_label], 'next_page')
+ end
+
+ def previous_or_next_page(page, text, classname)
+ if page
+ link(text, page, :class => classname)
+ else
+ tag(:span, text, :class => classname + ' disabled')
+ end
+ end
+
+ def html_container(html)
+ tag(:div, html, container_attributes)
+ end
+
+ # Returns URL params for +page_link_or_span+, taking the current GET params
+ # and :params option into account.
+ def url(page)
+ raise NotImplementedError
+ end
+
+ private
+
+ def param_name
+ @options[:param_name].to_s
+ end
+
+ def link(text, target, attributes = {})
+ if target.is_a? Fixnum
+ attributes[:rel] = rel_value(target)
+ target = url(target)
+ end
+ attributes[:href] = target
+ tag(:a, text, attributes)
+ end
+
+ def tag(name, value, attributes = {})
+ string_attributes = attributes.inject('') do |attrs, pair|
+ unless pair.last.nil?
+ attrs << %( #{pair.first}="#{CGI::escapeHTML(pair.last.to_s)}")
+ end
+ attrs
+ end
+ "<#{name}#{string_attributes}>#{value}#{name}>"
+ end
+
+ def rel_value(page)
+ case page
+ when @collection.current_page - 1; 'prev' + (page == 1 ? ' start' : '')
+ when @collection.current_page + 1; 'next'
+ when 1; 'start'
+ end
+ end
+
+ def symbolized_update(target, other)
+ other.each do |key, value|
+ key = key.to_sym
+ existing = target[key]
+
+ if value.is_a?(Hash) and (existing.is_a?(Hash) or existing.nil?)
+ symbolized_update(existing || (target[key] = {}), value)
+ else
+ target[key] = value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/will_paginate/view_helpers/link_renderer_base.rb b/lib/will_paginate/view_helpers/link_renderer_base.rb
new file mode 100644
index 000000000..385b1b2bd
--- /dev/null
+++ b/lib/will_paginate/view_helpers/link_renderer_base.rb
@@ -0,0 +1,77 @@
+module WillPaginate
+ module ViewHelpers
+ # This class does the heavy lifting of actually building the pagination
+ # links. It is used by +will_paginate+ helper internally.
+ class LinkRendererBase
+
+ # * +collection+ is a WillPaginate::Collection instance or any other object
+ # that conforms to that API
+ # * +options+ are forwarded from +will_paginate+ view helper
+ def prepare(collection, options)
+ @collection = collection
+ @options = options
+
+ # reset values in case we're re-using this instance
+ @total_pages = nil
+ end
+
+ def pagination
+ items = @options[:page_links] ? windowed_page_numbers : []
+ items.unshift :previous_page
+ items.push :next_page
+ end
+
+ protected
+
+ # Calculates visible page numbers using the :inner_window and
+ # :outer_window options.
+ def windowed_page_numbers
+ inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i
+ window_from = current_page - inner_window
+ window_to = current_page + inner_window
+
+ # adjust lower or upper limit if other is out of bounds
+ if window_to > total_pages
+ window_from -= window_to - total_pages
+ window_to = total_pages
+ end
+ if window_from < 1
+ window_to += 1 - window_from
+ window_from = 1
+ window_to = total_pages if window_to > total_pages
+ end
+
+ # these are always visible
+ middle = window_from..window_to
+
+ # left window
+ if outer_window + 3 < middle.first # there's a gap
+ left = (1..(outer_window + 1)).to_a
+ left << :gap
+ else # runs into visible pages
+ left = 1...middle.first
+ end
+
+ # right window
+ if total_pages - outer_window - 2 > middle.last # again, gap
+ right = ((total_pages - outer_window)..total_pages).to_a
+ right.unshift :gap
+ else # runs into visible pages
+ right = (middle.last + 1)..total_pages
+ end
+
+ left.to_a + middle.to_a + right.to_a
+ end
+
+ private
+
+ def current_page
+ @collection.current_page
+ end
+
+ def total_pages
+ @total_pages ||= @collection.total_pages
+ end
+ end
+ end
+end
diff --git a/lib/will_paginate/view_helpers/merb.rb b/lib/will_paginate/view_helpers/merb.rb
new file mode 100644
index 000000000..1d209a3a1
--- /dev/null
+++ b/lib/will_paginate/view_helpers/merb.rb
@@ -0,0 +1,13 @@
+require 'will_paginate/view_helpers/base'
+require 'will_paginate/view_helpers/link_renderer'
+
+WillPaginate::ViewHelpers::LinkRenderer.class_eval do
+ protected
+
+ def url(page)
+ params = @template.request.params.except(:action, :controller).merge(param_name => page)
+ @template.url(:this, params)
+ end
+end
+
+Merb::AbstractController.send(:include, WillPaginate::ViewHelpers::Base)
\ No newline at end of file
diff --git a/spec/collection_spec.rb b/spec/collection_spec.rb
new file mode 100644
index 000000000..4d71dc90f
--- /dev/null
+++ b/spec/collection_spec.rb
@@ -0,0 +1,147 @@
+require 'will_paginate/array'
+require 'spec_helper'
+
+describe WillPaginate::Collection do
+
+ before :all do
+ @simple = ('a'..'e').to_a
+ end
+
+ it "should be a subset of original collection" do
+ @simple.paginate(:page => 1, :per_page => 3).should == %w( a b c )
+ end
+
+ it "can be shorter than per_page if on last page" do
+ @simple.paginate(:page => 2, :per_page => 3).should == %w( d e )
+ end
+
+ it "should include whole collection if per_page permits" do
+ @simple.paginate(:page => 1, :per_page => 5).should == @simple
+ end
+
+ it "should be empty if out of bounds" do
+ @simple.paginate(:page => 2, :per_page => 5).should be_empty
+ end
+
+ it "should default to 1 as current page and 30 per-page" do
+ result = (1..50).to_a.paginate
+ result.current_page.should == 1
+ result.size.should == 30
+ end
+
+ describe "old API" do
+ it "should fail with numeric params" do
+ Proc.new { [].paginate(2) }.should raise_error(ArgumentError)
+ Proc.new { [].paginate(2, 10) }.should raise_error(ArgumentError)
+ end
+
+ it "should fail with both options and numeric param" do
+ Proc.new { [].paginate({}, 5) }.should raise_error(ArgumentError)
+ end
+ end
+
+ it "should give total_entries precedence over actual size" do
+ %w(a b c).paginate(:total_entries => 5).total_entries.should == 5
+ end
+
+ it "should be an augmented Array" do
+ entries = %w(a b c)
+ collection = create(2, 3, 10) do |pager|
+ pager.replace(entries).should == entries
+ end
+
+ collection.should == entries
+ for method in %w(total_pages each offset size current_page per_page total_entries)
+ collection.should respond_to(method)
+ end
+ collection.should be_kind_of(Array)
+ collection.entries.should be_instance_of(Array)
+ # TODO: move to another expectation:
+ collection.offset.should == 3
+ collection.total_pages.should == 4
+ collection.should_not be_out_of_bounds
+ end
+
+ describe "previous/next pages" do
+ it "should have previous_page nil when on first page" do
+ collection = create(1, 1, 3)
+ collection.previous_page.should be_nil
+ collection.next_page.should == 2
+ end
+
+ it "should have both prev/next pages" do
+ collection = create(2, 1, 3)
+ collection.previous_page.should == 1
+ collection.next_page.should == 3
+ end
+
+ it "should have next_page nil when on last page" do
+ collection = create(3, 1, 3)
+ collection.previous_page.should == 2
+ collection.next_page.should be_nil
+ end
+ end
+
+ it "should show out of bounds when page number is too high" do
+ create(2, 3, 2).should be_out_of_bounds
+ end
+
+ it "should not show out of bounds when inside collection" do
+ create(1, 3, 2).should_not be_out_of_bounds
+ end
+
+ describe "guessing total count" do
+ it "can guess when collection is shorter than limit" do
+ collection = create { |p| p.replace array }
+ collection.total_entries.should == 8
+ end
+
+ it "should allow explicit total count to override guessed" do
+ collection = create(2, 5, 10) { |p| p.replace array }
+ collection.total_entries.should == 10
+ end
+
+ it "should not be able to guess when collection is same as limit" do
+ collection = create { |p| p.replace array(5) }
+ collection.total_entries.should be_nil
+ end
+
+ it "should not be able to guess when collection is empty" do
+ collection = create { |p| p.replace array(0) }
+ collection.total_entries.should be_nil
+ end
+
+ it "should be able to guess when collection is empty and this is the first page" do
+ collection = create(1) { |p| p.replace array(0) }
+ collection.total_entries.should == 0
+ end
+ end
+
+ it "should raise WillPaginate::InvalidPage on invalid input" do
+ for bad_input in [0, -1, nil, '', 'Schnitzel']
+ Proc.new { create bad_input }.should raise_error(WillPaginate::InvalidPage)
+ end
+ end
+
+ it "should raise Argument error on invalid per_page setting" do
+ Proc.new { create(1, -1) }.should raise_error(ArgumentError)
+ end
+
+ it "should not respond to page_count anymore" do
+ Proc.new { create.page_count }.should raise_error(NoMethodError)
+ end
+
+ private
+
+ def create(page = 2, limit = 5, total = nil, &block)
+ if block_given?
+ WillPaginate::Collection.create(page, limit, total, &block)
+ else
+ WillPaginate::Collection.new(page, limit, total)
+ end
+ end
+
+ def array(size = 3)
+ Array.new(size)
+ end
+end
diff --git a/spec/console b/spec/console
new file mode 100755
index 000000000..eae5da46d
--- /dev/null
+++ b/spec/console
@@ -0,0 +1,10 @@
+#!/usr/bin/env ruby
+irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
+opts = %w[ --simple-prompt -rirb/completion ]
+if ARGV.include? '-dm'
+ opts << '-rwill_paginate/data_mapper' << '-rfinders/data_mapper_test_connector'
+else
+ opts << '-rconsole_fixtures'
+end
+
+exec 'bundle', 'exec', irb, '-Ilib:spec', *opts
diff --git a/spec/console_fixtures.rb b/spec/console_fixtures.rb
new file mode 100644
index 000000000..d007b5c3e
--- /dev/null
+++ b/spec/console_fixtures.rb
@@ -0,0 +1,30 @@
+require 'bundler'
+Bundler.setup
+
+require 'will_paginate/finders/active_record'
+require 'finders/activerecord_test_connector'
+
+ActiverecordTestConnector.setup
+
+windows = RUBY_PLATFORM =~ /(:?mswin|mingw)/
+# used just for the `color` method
+log_subscriber = ActiveSupport::LogSubscriber.log_subscribers.first
+
+IGNORE_SQL = /\b(sqlite_master|sqlite_version)\b|^(CREATE TABLE|PRAGMA)\b/
+
+ActiveSupport::Notifications.subscribe(/^sql\./) do |*args|
+ data = args.last
+ unless data[:name] =~ /^Fixture/ or data[:sql] =~ IGNORE_SQL
+ if windows
+ puts data[:sql]
+ else
+ puts log_subscriber.send(:color, data[:sql], :cyan)
+ end
+ end
+end
+
+# load all fixtures
+ActiverecordTestConnector::Fixtures.create_fixtures \
+ ActiverecordTestConnector::FIXTURES_PATH, ActiveRecord::Base.connection.tables
+
+WillPaginate::Finders::ActiveRecord.enable!
diff --git a/spec/database.yml b/spec/database.yml
new file mode 100644
index 000000000..87ca11b41
--- /dev/null
+++ b/spec/database.yml
@@ -0,0 +1,22 @@
+sqlite3:
+ database: ":memory:"
+ adapter: sqlite3
+ timeout: 500
+
+mysql:
+ adapter: mysql
+ database: will_paginate
+ username:
+ encoding: utf8
+
+mysql2:
+ adapter: mysql2
+ database: will_paginate
+ username:
+ encoding: utf8
+
+postgres:
+ adapter: postgresql
+ database: will_paginate
+ username: postgres
+ min_messages: warning
diff --git a/spec/finders/active_record_spec.rb b/spec/finders/active_record_spec.rb
new file mode 100644
index 000000000..29c80b920
--- /dev/null
+++ b/spec/finders/active_record_spec.rb
@@ -0,0 +1,455 @@
+require 'spec_helper'
+require 'will_paginate/active_record'
+require File.expand_path('../activerecord_test_connector', __FILE__)
+ActiverecordTestConnector.setup
+
+WillPaginate::ActiveRecord.setup
+abort unless ActiverecordTestConnector.able_to_connect
+
+describe WillPaginate::ActiveRecord do
+
+ extend ActiverecordTestConnector::FixtureSetup
+
+ fixtures :topics, :replies, :users, :projects, :developers_projects
+
+ it "should integrate with ActiveRecord::Base" do
+ ActiveRecord::Base.should respond_to(:paginate)
+ end
+
+ it "should paginate" do
+ lambda {
+ users = User.paginate(:page => 1, :per_page => 5).to_a
+ users.length.should == 5
+ }.should run_queries(2)
+ end
+
+ it "should fail when encountering unknown params" do
+ lambda {
+ User.paginate :foo => 'bar', :page => 1, :per_page => 4
+ }.should raise_error(ArgumentError)
+ end
+
+ describe "relation" do
+ it "should return a relation" do
+ rel = nil
+ lambda {
+ rel = Developer.paginate(:page => 1)
+ rel.per_page.should == 10
+ rel.current_page.should == 1
+ }.should run_queries(0)
+
+ lambda {
+ rel.total_pages.should == 2
+ }.should run_queries(1)
+ end
+
+ it "should keep per-class per_page number" do
+ rel = Developer.order('id').paginate(:page => 1)
+ rel.per_page.should == 10
+ end
+
+ it "should be able to change per_page number" do
+ rel = Developer.order('id').paginate(:page => 1).limit(5)
+ rel.per_page.should == 5
+ end
+
+ it "supports the page() method" do
+ rel = Developer.page('1').order('id')
+ rel.current_page.should == 1
+ rel.per_page.should == 10
+ rel.offset.should == 0
+
+ rel = rel.limit(5).page(2)
+ rel.per_page.should == 5
+ rel.offset.should == 5
+ end
+
+ it "raises on invalid page number" do
+ lambda {
+ Developer.page('foo')
+ }.should raise_error(ArgumentError)
+ end
+
+ it "supports first limit() then page()" do
+ rel = Developer.limit(3).page(3)
+ rel.offset.should == 6
+ end
+
+ it "supports first page() then limit()" do
+ rel = Developer.page(3).limit(3)
+ rel.offset.should == 6
+ end
+ end
+
+ describe "counting" do
+ it "should not accept :count parameter" do
+ pending
+ lambda {
+ User.paginate :page => 1, :count => {}
+ }.should raise_error(ArgumentError)
+ end
+
+ it "should guess the total count" do
+ lambda {
+ topics = Topic.paginate :page => 2, :per_page => 3
+ topics.total_entries.should == 4
+ }.should run_queries(1)
+ end
+
+ it "should guess that there are no records" do
+ lambda {
+ topics = Topic.where(:project_id => 999).paginate :page => 1, :per_page => 3
+ topics.total_entries.should == 0
+ }.should run_queries(1)
+ end
+ end
+
+ it "should not ignore :select parameter when it says DISTINCT" do
+ users = User.select('DISTINCT salary').paginate :page => 2
+ users.total_entries.should == 5
+ end
+
+ it "should count with scoped select when :select => DISTINCT" do
+ pending
+ Topic.distinct.paginate :page => 2
+ end
+
+ describe "paginate_by_sql" do
+ it "should respond" do
+ User.should respond_to(:paginate_by_sql)
+ end
+
+ it "should paginate" do
+ lambda {
+ sql = "select content from topics where content like '%futurama%'"
+ topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 1
+ topics.total_entries.should == 1
+ topics.first['title'].should be_nil
+ }.should run_queries(2)
+ end
+
+ it "should respect total_entries setting" do
+ lambda {
+ sql = "select content from topics"
+ topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 1, :total_entries => 999
+ topics.total_entries.should == 999
+ }.should run_queries(1)
+ end
+
+ it "should strip the order when counting" do
+ lambda {
+ sql = "select id, title, content from topics order by title"
+ topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 2
+ topics.first.should == topics(:ar)
+ }.should run_queries(2)
+ end
+
+ it "shouldn't change the original query string" do
+ query = 'select * from topics where 1 = 2'
+ original_query = query.dup
+ Topic.paginate_by_sql(query, :page => 1)
+ query.should == original_query
+ end
+ end
+
+ it "doesn't mangle options" do
+ options = { :page => 1 }
+ options.expects(:delete).never
+ options_before = options.dup
+
+ Topic.paginate(options)
+ options.should == options_before
+ end
+
+ it "should get first page of Topics with a single query" do
+ lambda {
+ result = Topic.paginate :page => nil
+ result.to_a # trigger loading of records
+ result.current_page.should == 1
+ result.total_pages.should == 1
+ result.size.should == 4
+ }.should run_queries(1)
+ end
+
+ it "should get second (inexistent) page of Topics, requiring 2 queries" do
+ lambda {
+ result = Topic.paginate :page => 2
+ result.total_pages.should == 1
+ result.should be_empty
+ }.should run_queries(2)
+ end
+
+ it "should paginate with :order" do
+ result = Topic.paginate :page => 1, :order => 'created_at DESC'
+ result.should == topics(:futurama, :harvey_birdman, :rails, :ar).reverse
+ result.total_pages.should == 1
+ end
+
+ it "should paginate with :conditions" do
+ result = Topic.paginate :page => 1, :conditions => ["created_at > ?", 30.minutes.ago]
+ result.should == topics(:rails, :ar)
+ result.total_pages.should == 1
+ end
+
+ it "should paginate with :include and :conditions" do
+ result = 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
+
+ result.should == expected
+ result.total_entries.should == 1
+ end
+
+ it "should paginate with :include and :order" do
+ result = nil
+ lambda {
+ result = Topic.paginate(:page => 1, :include => :replies, :per_page => 10,
+ :order => 'replies.created_at asc, topics.created_at asc').to_a
+ }.should run_queries(2)
+
+ expected = Topic.find :all,
+ :include => 'replies',
+ :order => 'replies.created_at asc, topics.created_at asc',
+ :limit => 10
+
+ result.should == expected
+ result.total_entries.should == 4
+ end
+
+ it "should remove :include for count" do
+ lambda {
+ Developer.paginate(:page => 1, :per_page => 1, :include => :projects).to_a
+ $query_sql.last.should_not =~ /\bJOIN\b/
+ }.should run_queries(3..4)
+ end
+
+ it "should keep :include for count when they are referenced in :conditions" do
+ Developer.paginate(
+ :page => 1, :per_page => 1,
+ :include => :projects,
+ :conditions => 'projects.id > 2'
+ ).to_a
+
+ $query_sql.last.should =~ /\bJOIN\b/
+ end
+
+ describe "associations" do
+ it "should paginate with include" do
+ project = projects(:active_record)
+
+ result = project.topics.paginate \
+ :page => 1,
+ :include => :replies,
+ :conditions => ["replies.content LIKE ?", 'Nice%'],
+ :per_page => 10
+
+ expected = Topic.find :all,
+ :include => 'replies',
+ :conditions => ["project_id = ? AND replies.content LIKE ?", project.id, 'Nice%'],
+ :limit => 10
+
+ result.should == expected
+ end
+
+ it "should paginate" do
+ dhh = users(:david)
+ expected_name_ordered = projects(:action_controller, :active_record)
+ expected_id_ordered = projects(:active_record, :action_controller)
+
+ lambda {
+ # with association-specified order
+ result = dhh.projects.paginate(:page => 1)
+ result.should == expected_name_ordered
+ result.total_entries.should == 2
+ }.should run_queries(2)
+
+ # with explicit order
+ result = dhh.projects.paginate(:page => 1).reorder('projects.id')
+ result.should == expected_id_ordered
+ result.total_entries.should == 2
+
+ lambda {
+ dhh.projects.find(:all, :order => 'projects.id', :limit => 4)
+ }.should_not raise_error
+
+ result = dhh.projects.paginate(:page => 1, :per_page => 4).reorder('projects.id')
+ result.should == expected_id_ordered
+
+ # has_many with implicit order
+ topic = Topic.find(1)
+ expected = replies(:spam, :witty_retort)
+ # FIXME: wow, this is ugly
+ topic.replies.paginate(:page => 1).map(&:id).sort.should == expected.map(&:id).sort
+ topic.replies.paginate(:page => 1).reorder('replies.id ASC').should == expected.reverse
+ end
+
+ it "should paginate through association extension" do
+ project = Project.find(:first)
+ expected = [replies(:brave)]
+
+ lambda {
+ result = project.replies.only_recent.paginate(:page => 1)
+ result.should == expected
+ }.should run_queries(1)
+ end
+ end
+
+ it "should paginate with joins" do
+ result = nil
+ join_sql = 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id'
+
+ lambda {
+ result = Developer.paginate(:page => 1, :joins => join_sql, :conditions => 'project_id = 1')
+ result.to_a # trigger loading of records
+ result.size.should == 2
+ developer_names = result.map(&:name)
+ developer_names.should include('David')
+ developer_names.should include('Jamis')
+ }.should run_queries(1)
+
+ lambda {
+ expected = result.to_a
+ result = Developer.paginate(:page => 1, :joins => join_sql,
+ :conditions => 'project_id = 1', :count => { :select => "users.id" }).to_a
+ result.should == expected
+ result.total_entries.should == 2
+ }.should run_queries(1)
+ end
+
+ it "should paginate with group" do
+ result = nil
+ lambda {
+ result = Developer.paginate(:page => 1, :per_page => 10,
+ :group => 'salary', :select => 'salary', :order => 'salary').to_a
+ }.should run_queries(1)
+
+ expected = users(:david, :jamis, :dev_10, :poor_jamis).map(&:salary).sort
+ result.map(&:salary).should == expected
+ end
+
+ it "should not paginate with dynamic finder" do
+ lambda {
+ Developer.paginate_by_salary(100000, :page => 1, :per_page => 5)
+ }.should raise_error(NoMethodError)
+ end
+
+ it "should paginate with_scope" do
+ result = Developer.with_poor_ones { Developer.paginate :page => 1 }
+ result.size.should == 2
+ result.total_entries.should == 2
+ end
+
+ describe "scopes" do
+ it "should paginate" do
+ result = Developer.poor.paginate :page => 1, :per_page => 1
+ result.size.should == 1
+ result.total_entries.should == 2
+ end
+
+ it "should paginate on habtm association" do
+ project = projects(:active_record)
+ lambda {
+ result = project.developers.poor.paginate :page => 1, :per_page => 1
+ result.size.should == 1
+ result.total_entries.should == 1
+ }.should run_queries(2)
+ end
+
+ it "should paginate on hmt association" do
+ project = projects(:active_record)
+ expected = [replies(:brave)]
+
+ lambda {
+ result = project.replies.recent.paginate :page => 1, :per_page => 1
+ result.should == expected
+ result.total_entries.should == 1
+ }.should run_queries(2)
+ end
+
+ it "should paginate on has_many association" do
+ project = projects(:active_record)
+ expected = [topics(:ar)]
+
+ lambda {
+ result = project.topics.mentions_activerecord.paginate :page => 1, :per_page => 1
+ result.should == expected
+ result.total_entries.should == 1
+ }.should run_queries(2)
+ end
+ end
+
+ it "should paginate with :readonly option" do
+ lambda {
+ Developer.paginate :readonly => true, :page => 1
+ }.should_not raise_error
+ end
+
+ it "should not paginate an array of IDs" do
+ lambda {
+ Developer.paginate((1..8).to_a, :per_page => 3, :page => 2, :order => 'id')
+ }.should raise_error(ArgumentError)
+ end
+
+ protected
+
+ def run_queries(num)
+ QueryCountMatcher.new(num)
+ end
+
+ def show_queries(&block)
+ counter = QueryCountMatcher.new(nil)
+ counter.run block
+ ensure
+ queries = counter.performed_queries
+ if queries.any?
+ puts queries
+ else
+ puts "no queries"
+ end
+ end
+
+end
+
+class QueryCountMatcher
+ def initialize(num)
+ @expected_count = num
+ end
+
+ def matches?(block)
+ run(block)
+
+ if @expected_count.respond_to? :include?
+ @expected_count.include? @count
+ else
+ @count == @expected_count
+ end
+ end
+
+ def run(block)
+ $query_count = 0
+ $query_sql = []
+ block.call
+ ensure
+ @queries = $query_sql.dup
+ @count = $query_count
+ end
+
+ def performed_queries
+ @queries
+ end
+
+ def failure_message
+ "expected #{@expected_count} queries, got #{@count}\n#{@queries.join("\n")}"
+ end
+
+ def negative_failure_message
+ "expected query count not to be #{@expected_count}"
+ end
+end
diff --git a/spec/finders/activerecord_test_connector.rb b/spec/finders/activerecord_test_connector.rb
new file mode 100644
index 000000000..b9e8f6f6d
--- /dev/null
+++ b/spec/finders/activerecord_test_connector.rb
@@ -0,0 +1,105 @@
+require 'active_record'
+require 'active_record/fixtures'
+require 'active_support/multibyte' # needed for Ruby 1.9.1
+
+$query_count = 0
+$query_sql = []
+
+ignore_sql = /^(?:PRAGMA|SELECT (?:currval|CAST|@@IDENTITY|@@ROWCOUNT)|SHOW FIELDS)\b|\bFROM sqlite_master\b/
+
+ActiveSupport::Notifications.subscribe(/^sql\./) do |*args|
+ payload = args.last
+ unless payload[:name] =~ /^Fixture/ or payload[:sql] =~ ignore_sql
+ $query_count += 1
+ $query_sql << payload[:sql]
+ end
+end
+
+module ActiverecordTestConnector
+ extend self
+
+ attr_accessor :able_to_connect
+ attr_accessor :connected
+
+ FIXTURES_PATH = File.expand_path('../../fixtures', __FILE__)
+
+ Fixtures = defined?(ActiveRecord::Fixtures) ? ActiveRecord::Fixtures : ::Fixtures
+
+ # Set our defaults
+ self.connected = false
+ self.able_to_connect = true
+
+ def setup
+ unless self.connected || !self.able_to_connect
+ setup_connection
+ load_schema
+ add_load_path FIXTURES_PATH
+ self.connected = true
+ end
+ rescue Exception => e # errors from ActiveRecord setup
+ $stderr.puts "\nSkipping ActiveRecord tests: #{e}\n\n"
+ self.able_to_connect = false
+ end
+
+ private
+
+ def add_load_path(path)
+ dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies
+ dep.autoload_paths.unshift path
+ end
+
+ def setup_connection
+ db = ENV['DB'].blank?? 'sqlite3' : ENV['DB']
+
+ configurations = YAML.load_file(File.expand_path('../../database.yml', __FILE__))
+ raise "no configuration for '#{db}'" unless configurations.key? db
+ configuration = configurations[db]
+
+ # ActiveRecord::Base.logger = Logger.new(STDOUT) if $0 == 'irb'
+ puts "using #{configuration['adapter']} adapter"
+
+ ActiveRecord::Base.configurations = { db => configuration }
+ ActiveRecord::Base.establish_connection(db)
+ end
+
+ def load_schema
+ ActiveRecord::Base.silence do
+ ActiveRecord::Migration.verbose = false
+ load File.join(FIXTURES_PATH, 'schema.rb')
+ end
+ end
+
+ module FixtureSetup
+ def fixtures(*tables)
+ table_names = tables.map { |t| t.to_s }
+
+ fixtures = Fixtures.create_fixtures ActiverecordTestConnector::FIXTURES_PATH, table_names
+ @@loaded_fixtures = {}
+ @@fixture_cache = {}
+
+ unless fixtures.nil?
+ if fixtures.instance_of?(Fixtures)
+ @@loaded_fixtures[fixtures.table_name] = fixtures
+ else
+ fixtures.each { |f| @@loaded_fixtures[f.table_name] = f }
+ end
+ end
+
+ table_names.each do |table_name|
+ define_method(table_name) do |*fixtures|
+ @@fixture_cache[table_name] ||= {}
+
+ instances = fixtures.map do |fixture|
+ if @@loaded_fixtures[table_name][fixture.to_s]
+ @@fixture_cache[table_name][fixture] ||= @@loaded_fixtures[table_name][fixture.to_s].find
+ else
+ raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
+ end
+ end
+
+ instances.size == 1 ? instances.first : instances
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/data_mapper_spec.rb b/spec/finders/data_mapper_spec.rb
new file mode 100644
index 000000000..c452b8e81
--- /dev/null
+++ b/spec/finders/data_mapper_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+begin
+ require 'will_paginate/data_mapper'
+ require File.expand_path('../data_mapper_test_connector', __FILE__)
+rescue LoadError => error
+ warn "Error running DataMapper specs: #{error.message}"
+ datamapper_loaded = false
+else
+ datamapper_loaded = true
+end
+
+describe WillPaginate::DataMapper do
+
+ it "has per_page" do
+ Animal.per_page.should == 30
+ begin
+ Animal.per_page = 10
+ Animal.per_page.should == 10
+
+ subclass = Class.new(Animal)
+ subclass.per_page.should == 10
+ ensure
+ Animal.per_page = 30
+ end
+ end
+
+ it "doesn't make normal collections appear paginated" do
+ Animal.all.should_not be_paginated
+ end
+
+ it "paginates to first page by default" do
+ animals = Animal.paginate(:page => nil)
+
+ animals.should be_paginated
+ animals.current_page.should == 1
+ animals.per_page.should == 30
+ animals.offset.should == 0
+ animals.total_entries.should == 3
+ animals.total_pages.should == 1
+ end
+
+ it "paginates to first page, explicit limit" do
+ animals = Animal.paginate(:page => 1, :per_page => 2)
+
+ animals.current_page.should == 1
+ animals.per_page.should == 2
+ animals.total_entries.should == 3
+ animals.total_pages.should == 2
+ animals.map {|a| a.name }.should == %w[ Dog Cat ]
+ end
+
+ it "paginates to second page" do
+ animals = Animal.paginate(:page => 2, :per_page => 2)
+
+ animals.current_page.should == 2
+ animals.offset.should == 2
+ animals.map {|a| a.name }.should == %w[ Lion ]
+ end
+
+ it "paginates a collection" do
+ friends = Animal.all(:notes.like => '%friend%')
+ friends.paginate(:page => 1).per_page.should == 30
+ friends.paginate(:page => 1, :per_page => 1).total_entries.should == 2
+ end
+
+ it "paginates a limited collection" do
+ animals = Animal.all(:limit => 2).paginate(:page => 1)
+ animals.per_page.should == 2
+ end
+
+ it "has page() method" do
+ Animal.page(2).per_page.should == 30
+ Animal.page(2).offset.should == 30
+ Animal.page(2).current_page.should == 2
+ Animal.all(:limit => 2).page(2).per_page.should == 2
+ end
+
+end if datamapper_loaded
diff --git a/spec/finders/data_mapper_test_connector.rb b/spec/finders/data_mapper_test_connector.rb
new file mode 100644
index 000000000..31b0bf4a5
--- /dev/null
+++ b/spec/finders/data_mapper_test_connector.rb
@@ -0,0 +1,29 @@
+require 'sqlite3'
+require 'dm-core'
+require 'dm-core/support/logger'
+require 'dm-migrations'
+
+DataMapper.setup :default, 'sqlite3::memory:'
+
+# Define models
+class Animal
+ include DataMapper::Resource
+ property :id, Serial
+ property :name, String
+ property :notes, Text
+
+ def self.setup
+ Animal.create(:name => 'Dog', :notes => "Man's best friend")
+ Animal.create(:name => 'Cat', :notes => "Woman's best friend")
+ Animal.create(:name => 'Lion', :notes => 'King of the Jungle')
+ end
+end
+
+# Load fixtures
+Animal.auto_migrate!
+Animal.setup
+
+if 'irb' == $0
+ DataMapper.logger.set_log($stdout, :debug)
+ DataMapper.logger.auto_flush = true
+end
diff --git a/spec/finders/sequel_spec.rb b/spec/finders/sequel_spec.rb
new file mode 100644
index 000000000..c9abb6706
--- /dev/null
+++ b/spec/finders/sequel_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+begin
+ require 'will_paginate/sequel'
+ require File.expand_path('../sequel_test_connector', __FILE__)
+rescue LoadError, ArgumentError => error
+ warn "Error running Sequel specs: #{error.message}"
+ sequel_loaded = false
+else
+ sequel_loaded = true
+end
+
+describe Sequel::Dataset::Pagination, 'extension' do
+
+ class Car < Sequel::Model
+ end
+
+ it "should have the #paginate method" do
+ Car.should respond_to(:paginate)
+ end
+
+ it "should NOT have the #paginate_by_sql method" do
+ Car.should_not respond_to(:paginate_by_sql)
+ end
+
+ describe 'pagination' do
+ before(:all) do
+ Car.create(:name => 'Shelby', :notes => "Man's best friend")
+ Car.create(:name => 'Aston Martin', :notes => "Woman's best friend")
+ Car.create(:name => 'Corvette', :notes => 'King of the Jungle')
+ end
+
+ it "should imitate WillPaginate::Collection" do
+ result = Car.paginate(1, 2)
+
+ result.total_entries.should == 3
+ result.total_pages.should == 2
+ result.per_page.should == 2
+ result.current_page.should == 1
+ end
+
+ it "should perform" do
+ Car.paginate(1, 2).all.should == [Car[1], Car[2]]
+ end
+
+ it "should perform with #select and #order" do
+ result = Car.select("name as foo".lit).order(:name).paginate(1, 2).all
+ result.size.should == 2
+ result.first.values[:foo].should == "Aston Martin"
+ end
+
+ it "should perform with #filter" do
+ results = Car.filter(:name => 'Shelby').paginate(1, 2).all
+ results.size.should == 1
+ results.first.should == Car.find(:name => 'Shelby')
+ end
+ end
+
+end if sequel_loaded
diff --git a/spec/finders/sequel_test_connector.rb b/spec/finders/sequel_test_connector.rb
new file mode 100644
index 000000000..c7a09632e
--- /dev/null
+++ b/spec/finders/sequel_test_connector.rb
@@ -0,0 +1,9 @@
+require 'sequel'
+
+db = Sequel.sqlite
+
+db.create_table :cars do
+ primary_key :id, :integer, :auto_increment => true
+ column :name, :text
+ column :notes, :text
+end
diff --git a/test/fixtures/admin.rb b/spec/fixtures/admin.rb
similarity index 100%
rename from test/fixtures/admin.rb
rename to spec/fixtures/admin.rb
diff --git a/test/fixtures/developer.rb b/spec/fixtures/developer.rb
similarity index 68%
rename from test/fixtures/developer.rb
rename to spec/fixtures/developer.rb
index 0224f4bf8..3eec78586 100644
--- a/test/fixtures/developer.rb
+++ b/spec/fixtures/developer.rb
@@ -7,8 +7,7 @@ def self.with_poor_ones(&block)
end
end
- named_scope :distinct, :select => 'DISTINCT `users`.*'
- named_scope :poor, :conditions => ['salary <= ?', 80000], :order => 'salary'
+ scope :poor, :conditions => ['salary <= ?', 80000], :order => 'salary'
def self.per_page() 10 end
end
diff --git a/test/fixtures/developers_projects.yml b/spec/fixtures/developers_projects.yml
similarity index 100%
rename from test/fixtures/developers_projects.yml
rename to spec/fixtures/developers_projects.yml
diff --git a/test/fixtures/project.rb b/spec/fixtures/project.rb
similarity index 72%
rename from test/fixtures/project.rb
rename to spec/fixtures/project.rb
index 7f6d72cd1..8e949571d 100644
--- a/test/fixtures/project.rb
+++ b/spec/fixtures/project.rb
@@ -6,10 +6,8 @@ class Project < ActiveRecord::Base
# :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
+ def only_recent(params = {})
+ scoped.where(['replies.created_at > ?', 15.minutes.ago])
end
end
diff --git a/test/fixtures/projects.yml b/spec/fixtures/projects.yml
similarity index 73%
rename from test/fixtures/projects.yml
rename to spec/fixtures/projects.yml
index 74f3c32f6..4104a3d09 100644
--- a/test/fixtures/projects.yml
+++ b/spec/fixtures/projects.yml
@@ -3,4 +3,4 @@ active_record:
name: Active Record
action_controller:
id: 2
- name: Active Controller
+ name: Action Controller
diff --git a/test/fixtures/replies.yml b/spec/fixtures/replies.yml
similarity index 57%
rename from test/fixtures/replies.yml
rename to spec/fixtures/replies.yml
index 9a83c004a..25fff58b2 100644
--- a/test/fixtures/replies.yml
+++ b/spec/fixtures/replies.yml
@@ -2,28 +2,28 @@ witty_retort:
id: 1
topic_id: 1
content: Birdman is better!
- created_at: <%= 6.hours.ago.to_s(:db) %>
+ created_at: <%= 6.hours.ago.utc.to_s(:db) %>
another:
id: 2
topic_id: 2
content: Nuh uh!
- created_at: <%= 1.hour.ago.to_s(:db) %>
+ created_at: <%= 1.hour.ago.utc.to_s(:db) %>
spam:
id: 3
topic_id: 1
content: Nice site!
- created_at: <%= 1.hour.ago.to_s(:db) %>
+ created_at: <%= 1.hour.ago.utc.to_s(:db) %>
decisive:
id: 4
topic_id: 4
content: "I'm getting to the bottom of this"
- created_at: <%= 30.minutes.ago.to_s(:db) %>
+ created_at: <%= 30.minutes.ago.utc.to_s(:db) %>
brave:
id: 5
topic_id: 4
content: "AR doesn't scare me a bit"
- created_at: <%= 10.minutes.ago.to_s(:db) %>
+ created_at: <%= 10.minutes.ago.utc.to_s(:db) %>
diff --git a/spec/fixtures/reply.rb b/spec/fixtures/reply.rb
new file mode 100644
index 000000000..9d2722173
--- /dev/null
+++ b/spec/fixtures/reply.rb
@@ -0,0 +1,9 @@
+class Reply < ActiveRecord::Base
+ belongs_to :topic, :include => [:replies]
+
+ scope :recent,
+ :conditions => ['replies.created_at > ?', 15.minutes.ago],
+ :order => 'replies.created_at DESC'
+
+ validates_presence_of :content
+end
diff --git a/test/fixtures/schema.rb b/spec/fixtures/schema.rb
similarity index 100%
rename from test/fixtures/schema.rb
rename to spec/fixtures/schema.rb
diff --git a/spec/fixtures/topic.rb b/spec/fixtures/topic.rb
new file mode 100644
index 000000000..4ea3ee2b7
--- /dev/null
+++ b/spec/fixtures/topic.rb
@@ -0,0 +1,7 @@
+class Topic < ActiveRecord::Base
+ has_many :replies, :dependent => :destroy, :order => 'replies.created_at DESC'
+ belongs_to :project
+
+ scope :mentions_activerecord, :conditions => ['topics.title LIKE ?', '%ActiveRecord%']
+ scope :distinct, :select => "DISTINCT #{table_name}.*"
+end
diff --git a/test/fixtures/topics.yml b/spec/fixtures/topics.yml
similarity index 74%
rename from test/fixtures/topics.yml
rename to spec/fixtures/topics.yml
index 0a2690473..f93cf48bb 100644
--- a/test/fixtures/topics.yml
+++ b/spec/fixtures/topics.yml
@@ -3,7 +3,7 @@ futurama:
title: Isnt futurama awesome?
subtitle: It really is, isnt it.
content: I like futurama
- created_at: <%= 1.day.ago.to_s(:db) %>
+ created_at: <%= 1.day.ago.utc.to_s(:db) %>
updated_at:
harvey_birdman:
@@ -11,7 +11,7 @@ harvey_birdman:
title: Harvey Birdman is the king of all men
subtitle: yup
content: He really is
- created_at: <%= 2.hours.ago.to_s(:db) %>
+ created_at: <%= 2.hours.ago.utc.to_s(:db) %>
updated_at:
rails:
@@ -20,11 +20,11 @@ rails:
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) %>
+ created_at: <%= 20.minutes.ago.utc.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) %>
+ created_at: <%= 15.minutes.ago.utc.to_s(:db) %>
diff --git a/test/fixtures/user.rb b/spec/fixtures/user.rb
similarity index 100%
rename from test/fixtures/user.rb
rename to spec/fixtures/user.rb
diff --git a/test/fixtures/users.yml b/spec/fixtures/users.yml
similarity index 100%
rename from test/fixtures/users.yml
rename to spec/fixtures/users.yml
diff --git a/spec/per_page_spec.rb b/spec/per_page_spec.rb
new file mode 100644
index 000000000..94482fff5
--- /dev/null
+++ b/spec/per_page_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require 'will_paginate/per_page'
+
+describe WillPaginate::PerPage do
+
+ class MyModel
+ extend WillPaginate::PerPage
+ end
+
+ it "has the default value" do
+ MyModel.per_page.should == 30
+
+ WillPaginate.per_page = 10
+ begin
+ MyModel.per_page.should == 10
+ ensure
+ WillPaginate.per_page = 30
+ end
+ end
+
+ it "has an explicit value" do
+ MyModel.per_page = 12
+ begin
+ MyModel.per_page.should == 12
+ subclass = Class.new(MyModel)
+ subclass.per_page.should == 12
+ ensure
+ MyModel.send(:remove_instance_variable, '@per_page')
+ end
+ end
+
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 000000000..dddda9e58
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,76 @@
+require 'rspec'
+require File.expand_path('../view_helpers/view_example_group', __FILE__)
+require 'will_paginate/deprecation'
+
+module MyExtras
+ protected
+
+ def include_phrase(string)
+ PhraseMatcher.new(string)
+ end
+
+ def collection(params = {})
+ if params[:total_pages]
+ params[:per_page] = 1
+ params[:total_entries] = params[:total_pages]
+ end
+ WillPaginate::Collection.new(params[:page] || 1, params[:per_page] || 30, params[:total_entries])
+ end
+
+ def have_deprecation
+ DeprecationMatcher.new
+ end
+end
+
+RSpec.configure do |config|
+ # config.include My::Pony, My::Horse, :type => :farm
+ config.include MyExtras
+ # config.predicate_matchers[:swim] = :can_swim?
+
+ config.mock_with :mocha
+end
+
+class PhraseMatcher
+ def initialize(string)
+ @string = string
+ @pattern = /\b#{string}\b/
+ end
+
+ def matches?(actual)
+ @actual = actual.to_s
+ @actual =~ @pattern
+ end
+
+ def failure_message
+ "expected #{@actual.inspect} to contain phrase #{@string.inspect}"
+ end
+
+ def negative_failure_message
+ "expected #{@actual.inspect} not to contain phrase #{@string.inspect}"
+ end
+end
+
+class DeprecationMatcher
+ def initialize
+ @old_behavior = WillPaginate::Deprecation.behavior
+ @messages = []
+ WillPaginate::Deprecation.behavior = lambda { |message, callstack|
+ @messages << message
+ }
+ end
+
+ def matches?(block)
+ block.call
+ !@messages.empty?
+ ensure
+ WillPaginate::Deprecation.behavior = @old_behavior
+ end
+
+ def failure_message
+ "expected block to raise a deprecation warning"
+ end
+
+ def negative_failure_message
+ "expected block not to raise deprecation warnings, #{@messages.size} raised"
+ end
+end
diff --git a/spec/view_helpers/action_view_spec.rb b/spec/view_helpers/action_view_spec.rb
new file mode 100644
index 000000000..8bd3b4538
--- /dev/null
+++ b/spec/view_helpers/action_view_spec.rb
@@ -0,0 +1,362 @@
+require 'spec_helper'
+require 'active_support/rescuable' # needed for Ruby 1.9.1
+require 'action_controller'
+require 'will_paginate/view_helpers/action_view'
+require 'will_paginate/collection'
+
+ActionView::Base.send(:include, WillPaginate::ActionView)
+
+Routes = ActionDispatch::Routing::RouteSet.new
+
+Routes.draw do
+ match 'dummy/page/:page' => 'dummy#index'
+ match 'dummy/dots/page.:page' => 'dummy#dots'
+ match 'ibocorp(/:page)' => 'ibocorp#index',
+ :constraints => { :page => /\d+/ }, :defaults => { :page => 1 }
+
+ match ':controller(/:action(/:id(.:format)))'
+end
+
+describe WillPaginate::ActionView do
+
+ before(:all) do
+ I18n.load_path << File.expand_path('../../../lib/will_paginate/locale/en.yml', __FILE__)
+ I18n.reload!
+ end
+
+ before(:each) do
+ @assigns = {}
+ @controller = DummyController.new
+ @request = @controller.request
+ @template = '<%= will_paginate collection, options %>'
+ end
+
+ attr_reader :assigns, :controller, :request
+
+ def render(locals)
+ @view = ActionView::Base.new([], @assigns, @controller)
+ @view.request = @request
+ @view.singleton_class.send(:include, @controller._routes.url_helpers)
+ @view.render(:inline => @template, :locals => locals)
+ end
+
+ ## basic pagination ##
+
+ it "should render" do
+ paginate do |pagination|
+ assert_select 'a[href]', 3 do |elements|
+ validate_page_numbers [2,3,2], elements
+ assert_select elements.last, ':last-child', "Next →"
+ end
+ assert_select 'span', 1
+ assert_select 'span.disabled:first-child', '← Previous'
+ assert_select 'em', '1'
+ pagination.first.inner_text.should == '← Previous 1 2 3 Next →'
+ end
+ end
+
+ it "should render nothing when there is only 1 page" do
+ paginate(:per_page => 30).should be_empty
+ end
+
+ it "should paginate with options" do
+ paginate({ :page => 2 }, :class => 'will_paginate', :previous_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|
+ link.first['rel'].should == 'prev start'
+ end
+ assert_select elements.first, 'a', "Prev" do |link|
+ link.first['rel'].should == 'prev start'
+ end
+ assert_select elements.last, 'a', "Next" do |link|
+ link.first['rel'].should == 'next'
+ end
+ end
+ assert_select 'em', '2'
+ end
+ end
+
+ it "should paginate using a custom renderer class" do
+ paginate({}, :renderer => AdditionalLinkAttributesRenderer) do
+ assert_select 'a[default=true]', 3
+ end
+ end
+
+ it "should paginate using a custom renderer instance" do
+ renderer = WillPaginate::ActionView::LinkRenderer.new
+ def renderer.gap() '~~' end
+
+ 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
+
+ it "should have classnames on previous/next links" do
+ paginate do |pagination|
+ assert_select 'span.disabled.previous_page:first-child'
+ assert_select 'a.next_page[href]:last-child'
+ end
+ end
+
+ it "should match expected markup" do
+ paginate
+ expected = <<-HTML
+