├── .rspec ├── .gitignore ├── spec ├── fixtures │ ├── user.rb │ ├── admin.rb │ ├── projects.yml │ ├── reply.rb │ ├── topic.rb │ ├── developers_projects.yml │ ├── developer.rb │ ├── project.rb │ ├── users.yml │ ├── replies.yml │ ├── topics.yml │ └── schema.rb ├── finders │ ├── sequel_test_connector.rb │ ├── data_mapper_test_connector.rb │ ├── sequel_spec.rb │ ├── data_mapper_spec.rb │ ├── activerecord_test_connector.rb │ ├── mongoid_spec.rb │ └── active_record_spec.rb ├── console ├── matchers │ ├── phrase_matcher.rb │ ├── deprecation_matcher.rb │ └── query_count_matcher.rb ├── fake_rubygems.rb ├── database.yml ├── console_fixtures.rb ├── per_page_spec.rb ├── spec_helper.rb ├── page_number_spec.rb ├── view_helpers │ ├── link_renderer_base_spec.rb │ ├── view_example_group.rb │ ├── base_spec.rb │ └── action_view_spec.rb └── collection_spec.rb ├── lib ├── will_paginate │ ├── version.rb │ ├── per_page.rb │ ├── i18n.rb │ ├── core_ext.rb │ ├── sequel.rb │ ├── view_helpers │ │ ├── sinatra.rb │ │ ├── link_renderer_base.rb │ │ ├── link_renderer.rb │ │ └── action_view.rb │ ├── locale │ │ └── en.yml │ ├── mongoid.rb │ ├── array.rb │ ├── deprecation.rb │ ├── page_number.rb │ ├── railtie.rb │ ├── data_mapper.rb │ ├── collection.rb │ ├── active_record.rb │ └── view_helpers.rb └── will_paginate.rb ├── script ├── each-gemfile └── test_all ├── Gemfile ├── Gemfile.rails4.1 ├── init.rb ├── .travis.yml ├── Gemfile.rails-edge ├── LICENSE ├── will_paginate.gemspec ├── README.md ├── Gemfile.rails4.1.lock ├── Gemfile.lock └── Gemfile.rails-edge.lock /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | doc 3 | *.gem 4 | coverage 5 | -------------------------------------------------------------------------------- /spec/fixtures/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/fixtures/admin.rb: -------------------------------------------------------------------------------- 1 | class Admin < User 2 | has_many :companies, :finder_sql => 'SELECT * FROM companies' 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/projects.yml: -------------------------------------------------------------------------------- 1 | active_record: 2 | id: 1 3 | name: Active Record 4 | action_controller: 5 | id: 2 6 | name: Action Controller 7 | -------------------------------------------------------------------------------- /lib/will_paginate/version.rb: -------------------------------------------------------------------------------- 1 | module WillPaginate #:nodoc: 2 | module VERSION #:nodoc: 3 | MAJOR = 3 4 | MINOR = 0 5 | TINY = 4 6 | 7 | STRING = [MAJOR, MINOR, TINY].join('.') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/reply.rb: -------------------------------------------------------------------------------- 1 | class Reply < ActiveRecord::Base 2 | scope :recent, lambda { 3 | where(['replies.created_at > ?', 15.minutes.ago]). 4 | order('replies.created_at DESC') 5 | } 6 | 7 | validates_presence_of :content 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/topic.rb: -------------------------------------------------------------------------------- 1 | class Topic < ActiveRecord::Base 2 | has_many :replies, :dependent => :destroy 3 | belongs_to :project 4 | 5 | scope :mentions_activerecord, lambda { 6 | where(['topics.title LIKE ?', '%ActiveRecord%']) 7 | } 8 | end 9 | -------------------------------------------------------------------------------- /lib/will_paginate.rb: -------------------------------------------------------------------------------- 1 | # You will paginate! 2 | module WillPaginate 3 | end 4 | 5 | if defined?(Rails::Railtie) 6 | require 'will_paginate/railtie' 7 | end 8 | 9 | if defined?(Sinatra) and Sinatra.respond_to? :register 10 | require 'will_paginate/view_helpers/sinatra' 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/developers_projects.yml: -------------------------------------------------------------------------------- 1 | david_action_controller: 2 | developer_id: 1 3 | project_id: 2 4 | joined_on: 2004-10-10 5 | 6 | david_active_record: 7 | developer_id: 1 8 | project_id: 1 9 | joined_on: 2004-10-10 10 | 11 | jamis_active_record: 12 | developer_id: 2 13 | project_id: 1 -------------------------------------------------------------------------------- /script/each-gemfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if [ -z "$1" ]; then 5 | echo "Usage: script/each-bundle COMMAND []" >&2 6 | exit 1 7 | fi 8 | 9 | for gemfile in $(ls Gemfile* | grep -v lock); do 10 | printf "\e[31m[%s]\e[0m\n" $gemfile 11 | BUNDLE_GEMFILE=$gemfile "$@" 12 | done 13 | -------------------------------------------------------------------------------- /spec/finders/sequel_test_connector.rb: -------------------------------------------------------------------------------- 1 | require 'sequel' 2 | 3 | Symbol.class_eval do 4 | # Active Record calculations tries `as` on some objects but chokes when that 5 | # object was a Symbol and it gets a Sequel::SQL::AliasedExpression. 6 | undef as if method_defined? :as 7 | end 8 | 9 | db = Sequel.sqlite 10 | 11 | db.create_table :cars do 12 | primary_key :id, :integer, :auto_increment => true 13 | column :name, :text 14 | column :notes, :text 15 | end 16 | -------------------------------------------------------------------------------- /spec/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' 3 | opts = %w[ --simple-prompt -rirb/completion ] 4 | if ARGV.include? '-dm' 5 | opts << '-rwill_paginate/data_mapper' << '-rfinders/data_mapper_test_connector' 6 | elsif ARGV.include? '-seq' 7 | opts << '-rwill_paginate/sequel' << '-rfinders/sequel_test_connector' 8 | else 9 | opts << '-rconsole_fixtures' 10 | end 11 | 12 | exec 'bundle', 'exec', irb, '-Ilib:spec', *opts 13 | -------------------------------------------------------------------------------- /spec/fixtures/developer.rb: -------------------------------------------------------------------------------- 1 | class Developer < User 2 | has_and_belongs_to_many :projects, :order => 'projects.name', :join_table => 'developers_projects' 3 | 4 | def self.with_poor_ones(&block) 5 | options = { :conditions => ['salary <= ?', 80000], :order => 'salary' } 6 | with_scope({ :find => options }, :overwrite) do 7 | yield 8 | end 9 | end 10 | 11 | scope :poor, lambda { 12 | where(['salary <= ?', 80000]).order('salary') 13 | } 14 | 15 | def self.per_page() 10 end 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | rails_version = '~> 4.0.0' 4 | 5 | gem 'activerecord', rails_version 6 | gem 'actionpack', rails_version 7 | 8 | gem 'rspec', '~> 2.6.0' 9 | gem 'mocha', '~> 0.9.8' 10 | 11 | gem 'sqlite3', '~> 1.3.6' 12 | 13 | group :mysql do 14 | gem 'mysql', '~> 2.9' 15 | gem 'mysql2', '~> 0.3.10' 16 | end 17 | gem 'pg', '~> 0.11', :group => :pg 18 | 19 | group :development do 20 | gem 'ruby-debug', :platforms => :mri_18 21 | gem 'debugger', :platforms => :mri_19 22 | end 23 | -------------------------------------------------------------------------------- /Gemfile.rails4.1: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | rails_version = '~> 4.1.2' 4 | 5 | gem 'activerecord', rails_version 6 | gem 'actionpack', rails_version 7 | gem 'activerecord-deprecated_finders' 8 | 9 | gem 'rspec', '~> 2.6.0' 10 | gem 'mocha', '~> 0.9.8' 11 | 12 | gem 'sqlite3', '~> 1.3.6' 13 | 14 | group :mysql do 15 | gem 'mysql', '~> 2.9' 16 | gem 'mysql2', '~> 0.3.10' 17 | end 18 | gem 'pg', '~> 0.11', :group => :pg 19 | 20 | group :development do 21 | gem 'debugger', :platforms => :mri_19 22 | end 23 | -------------------------------------------------------------------------------- /script/test_all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | status=0 5 | 6 | export PATH="${PWD}/bin:$PATH" 7 | 8 | for db in sqlite3 mysql mysql2 postgres; do 9 | if ! [[ $db = "mysql2" && $BUNDLE_GEMFILE =~ "rails3.0" ]]; then 10 | printf "\e[1;33m[DB] ${db}\e[m\n" 11 | spec_files="spec" 12 | [[ $db != "sqlite3" ]] && spec_files="spec/finders/active_record_spec.rb" 13 | echo "rspec $spec_files" 14 | DB=$db rspec -r fake_rubygems $spec_files || status="$?" 15 | fi 16 | done 17 | 18 | exit $status 19 | -------------------------------------------------------------------------------- /spec/matchers/phrase_matcher.rb: -------------------------------------------------------------------------------- 1 | class PhraseMatcher 2 | def initialize(string) 3 | @string = string 4 | @pattern = /\b#{Regexp.escape string}\b/ 5 | end 6 | 7 | def matches?(actual) 8 | @actual = actual.to_s 9 | @actual =~ @pattern 10 | end 11 | 12 | def failure_message 13 | "expected #{@actual.inspect} to contain phrase #{@string.inspect}" 14 | end 15 | 16 | def negative_failure_message 17 | "expected #{@actual.inspect} not to contain phrase #{@string.inspect}" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/fixtures/project.rb: -------------------------------------------------------------------------------- 1 | class Project < ActiveRecord::Base 2 | has_and_belongs_to_many :developers, :uniq => true, :join_table => 'developers_projects' 3 | 4 | has_many :topics 5 | # :finder_sql => 'SELECT * FROM topics WHERE (topics.project_id = #{id})', 6 | # :counter_sql => 'SELECT COUNT(*) FROM topics WHERE (topics.project_id = #{id})' 7 | 8 | has_many :replies, :through => :topics do 9 | def only_recent(params = {}) 10 | where(['replies.created_at > ?', 15.minutes.ago]) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate' 2 | 3 | # This is all duplication of what Railtie does, but is necessary because 4 | # the initializer defined by the Railtie won't ever run when loaded as plugin. 5 | 6 | if defined? ActiveRecord::Base 7 | require 'will_paginate/active_record' 8 | end 9 | 10 | if defined? ActionController::Base 11 | WillPaginate::Railtie.setup_actioncontroller 12 | end 13 | 14 | if defined? ActionView::Base 15 | require 'will_paginate/view_helpers/action_view' 16 | end 17 | 18 | WillPaginate::Railtie.add_locale_path config 19 | -------------------------------------------------------------------------------- /spec/fake_rubygems.rb: -------------------------------------------------------------------------------- 1 | # Makes the test suite compatible with Bundler standalone mode (used in CI) 2 | # because Active Record uses `gem` for loading adapters. 3 | Kernel.module_eval do 4 | 5 | remove_method :gem if 'method' == defined? gem 6 | 7 | def gem(*args) 8 | return if $VERBOSE.nil? 9 | $stderr << "warning: gem(#{args.map {|o| o.inspect }.join(', ')}) ignored" 10 | $stderr << "; called from:\n " << caller[0,5].join("\n ") if $DEBUG 11 | $stderr << "\n" 12 | end 13 | 14 | private :gem 15 | 16 | end 17 | 18 | $" << "rubygems.rb" 19 | -------------------------------------------------------------------------------- /spec/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | david: 2 | id: 1 3 | name: David 4 | salary: 80000 5 | type: Developer 6 | 7 | jamis: 8 | id: 2 9 | name: Jamis 10 | salary: 150000 11 | type: Developer 12 | 13 | <% for digit in 3..10 %> 14 | dev_<%= digit %>: 15 | id: <%= digit %> 16 | name: fixture_<%= digit %> 17 | salary: 100000 18 | type: Developer 19 | <% end %> 20 | 21 | poor_jamis: 22 | id: 11 23 | name: Jamis 24 | salary: 9000 25 | type: Developer 26 | 27 | admin: 28 | id: 12 29 | name: admin 30 | type: Admin 31 | 32 | goofy: 33 | id: 13 34 | name: Goofy 35 | type: Admin 36 | -------------------------------------------------------------------------------- /spec/database.yml: -------------------------------------------------------------------------------- 1 | sqlite3: 2 | database: ":memory:" 3 | adapter: sqlite3 4 | timeout: 500 5 | 6 | mysql: 7 | adapter: mysql 8 | database: will_paginate 9 | username: 10 | encoding: utf8 11 | socket: <%= ENV["BOXEN_MYSQL_SOCKET"] %> 12 | 13 | mysql2: 14 | adapter: mysql2 15 | database: will_paginate 16 | username: 17 | encoding: utf8 18 | socket: <%= ENV["BOXEN_MYSQL_SOCKET"] %> 19 | 20 | postgres: 21 | adapter: postgresql 22 | database: will_paginate 23 | username: <%= "postgres" if ENV["TRAVIS"] %> 24 | min_messages: warning 25 | port: <%= ENV["BOXEN_POSTGRESQL_PORT"] %> 26 | -------------------------------------------------------------------------------- /lib/will_paginate/per_page.rb: -------------------------------------------------------------------------------- 1 | module WillPaginate 2 | module PerPage 3 | def per_page 4 | defined?(@per_page) ? @per_page : WillPaginate.per_page 5 | end 6 | 7 | def per_page=(limit) 8 | @per_page = limit.to_i 9 | end 10 | 11 | def self.extended(base) 12 | base.extend Inheritance if base.is_a? Class 13 | end 14 | 15 | module Inheritance 16 | def inherited(subclass) 17 | super 18 | subclass.per_page = self.per_page 19 | end 20 | end 21 | end 22 | 23 | extend PerPage 24 | 25 | # default number of items per page 26 | self.per_page = 30 27 | end 28 | -------------------------------------------------------------------------------- /spec/matchers/deprecation_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | 3 | class DeprecationMatcher 4 | def initialize(message) 5 | @message = message 6 | end 7 | 8 | def matches?(block) 9 | @actual = hijack_stderr(&block) 10 | PhraseMatcher.new("DEPRECATION WARNING: #{@message}").matches?(@actual) 11 | end 12 | 13 | def failure_message 14 | "expected deprecation warning #{@message.inspect}, got #{@actual.inspect}" 15 | end 16 | 17 | private 18 | 19 | def hijack_stderr 20 | err = $stderr 21 | $stderr = StringIO.new 22 | yield 23 | $stderr.string.rstrip 24 | ensure 25 | $stderr = err 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | bundler_args: "--standalone --binstubs --without development" 2 | script: script/test_all 3 | services: mongodb 4 | before_script: 5 | - mysql -e 'create database will_paginate;' 6 | - psql -c 'create database will_paginate;' -U postgres 7 | rvm: 8 | - 1.8.7 9 | - 1.9.3 10 | - 2.1.2 11 | gemfile: 12 | - Gemfile 13 | - Gemfile.rails3.0 14 | - Gemfile.rails3.1 15 | - Gemfile.rails3.2 16 | - Gemfile.rails4.1 17 | - Gemfile.rails-edge 18 | matrix: 19 | exclude: 20 | - rvm: 1.8.7 21 | gemfile: Gemfile.rails-edge 22 | - rvm: 1.8.7 23 | gemfile: Gemfile 24 | - rvm: 1.8.7 25 | gemfile: Gemfile.rails4.1 26 | -------------------------------------------------------------------------------- /lib/will_paginate/i18n.rb: -------------------------------------------------------------------------------- 1 | module WillPaginate 2 | module I18n 3 | def self.locale_dir 4 | File.expand_path('../locale', __FILE__) 5 | end 6 | 7 | def self.load_path 8 | Dir["#{locale_dir}/*.{rb,yml}"] 9 | end 10 | 11 | def will_paginate_translate(keys, options = {}) 12 | if defined? ::I18n 13 | defaults = Array(keys).dup 14 | defaults << Proc.new if block_given? 15 | ::I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => :will_paginate)) 16 | else 17 | key = Array === keys ? keys.first : keys 18 | yield key, options 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/fixtures/replies.yml: -------------------------------------------------------------------------------- 1 | witty_retort: 2 | id: 1 3 | topic_id: 1 4 | content: Birdman is better! 5 | created_at: <%= 6.hours.ago.utc.to_s(:db) %> 6 | 7 | another: 8 | id: 2 9 | topic_id: 2 10 | content: Nuh uh! 11 | created_at: <%= 1.hour.ago.utc.to_s(:db) %> 12 | 13 | spam: 14 | id: 3 15 | topic_id: 1 16 | content: Nice site! 17 | created_at: <%= 1.hour.ago.utc.to_s(:db) %> 18 | 19 | decisive: 20 | id: 4 21 | topic_id: 4 22 | content: "I'm getting to the bottom of this" 23 | created_at: <%= 30.minutes.ago.utc.to_s(:db) %> 24 | 25 | brave: 26 | id: 5 27 | topic_id: 4 28 | content: "AR doesn't scare me a bit" 29 | created_at: <%= 10.minutes.ago.utc.to_s(:db) %> 30 | -------------------------------------------------------------------------------- /Gemfile.rails-edge: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activerecord-deprecated_finders', :github => 'rails/activerecord-deprecated_finders', :branch => 'master' 4 | gem 'arel', github: 'rails/arel', branch: 'master' 5 | 6 | gem 'activerecord', :github => 'rails/rails', :branch => 'master' 7 | gem 'actionpack', :github => 'rails/rails', :branch => 'master' 8 | 9 | gem 'thread_safe' 10 | 11 | gem 'rspec', '~> 2.6.0' 12 | gem 'mocha', '~> 0.9.8' 13 | 14 | gem 'sqlite3', '~> 1.3.6' 15 | 16 | group :mysql do 17 | gem 'mysql', '~> 2.9' 18 | gem 'mysql2', '~> 0.3.13' 19 | end 20 | gem 'pg', '~> 0.11', :group => :pg 21 | 22 | group :development do 23 | gem 'ruby-debug', :platforms => :mri_18 24 | gem 'debugger', :platforms => :mri_19 25 | end 26 | -------------------------------------------------------------------------------- /spec/matchers/query_count_matcher.rb: -------------------------------------------------------------------------------- 1 | class QueryCountMatcher 2 | def initialize(num) 3 | @expected_count = num 4 | end 5 | 6 | def matches?(block) 7 | run(block) 8 | 9 | if @expected_count.respond_to? :include? 10 | @expected_count.include? @count 11 | else 12 | @count == @expected_count 13 | end 14 | end 15 | 16 | def run(block) 17 | $query_count = 0 18 | $query_sql = [] 19 | block.call 20 | ensure 21 | @queries = $query_sql.dup 22 | @count = $query_count 23 | end 24 | 25 | def performed_queries 26 | @queries 27 | end 28 | 29 | def failure_message 30 | "expected #{@expected_count} queries, got #{@count}\n#{@queries.join("\n")}" 31 | end 32 | 33 | def negative_failure_message 34 | "expected query count not to be #{@expected_count}" 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/fixtures/topics.yml: -------------------------------------------------------------------------------- 1 | futurama: 2 | id: 1 3 | title: Isnt futurama awesome? 4 | subtitle: It really is, isnt it. 5 | content: I like futurama 6 | created_at: <%= 1.day.ago.utc.to_s(:db) %> 7 | updated_at: 8 | 9 | harvey_birdman: 10 | id: 2 11 | title: Harvey Birdman is the king of all men 12 | subtitle: yup 13 | content: He really is 14 | created_at: <%= 2.hours.ago.utc.to_s(:db) %> 15 | updated_at: 16 | 17 | rails: 18 | id: 3 19 | project_id: 1 20 | title: Rails is nice 21 | subtitle: It makes me happy 22 | content: except when I have to hack internals to fix pagination. even then really. 23 | created_at: <%= 20.minutes.ago.utc.to_s(:db) %> 24 | 25 | ar: 26 | id: 4 27 | project_id: 1 28 | title: ActiveRecord sometimes freaks me out 29 | content: "I mean, what's the deal with eager loading?" 30 | created_at: <%= 15.minutes.ago.utc.to_s(:db) %> 31 | -------------------------------------------------------------------------------- /lib/will_paginate/core_ext.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | # copied from ActiveSupport so we don't depend on it 4 | 5 | unless Hash.method_defined? :except 6 | Hash.class_eval do 7 | # Returns a new hash without the given keys. 8 | def except(*keys) 9 | rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys) 10 | reject { |key,| rejected.include?(key) } 11 | end 12 | 13 | # Replaces the hash without only the given keys. 14 | def except!(*keys) 15 | replace(except(*keys)) 16 | end 17 | end 18 | end 19 | 20 | unless String.method_defined? :underscore 21 | String.class_eval do 22 | def underscore 23 | self.to_s.gsub(/::/, '/'.freeze). 24 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). 25 | gsub(/([a-z\d])([A-Z])/,'\1_\2'). 26 | tr("-".freeze, "_".freeze). 27 | downcase 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/console_fixtures.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup 3 | 4 | require 'will_paginate/active_record' 5 | require 'finders/activerecord_test_connector' 6 | 7 | ActiverecordTestConnector.setup 8 | 9 | windows = RUBY_PLATFORM =~ /(:?mswin|mingw)/ 10 | # used just for the `color` method 11 | log_subscriber = ActiveSupport::LogSubscriber.log_subscribers.first 12 | 13 | IGNORE_SQL = /\b(sqlite_master|sqlite_version)\b|^(CREATE TABLE|PRAGMA)\b/ 14 | 15 | ActiveSupport::Notifications.subscribe(/^sql\./) do |*args| 16 | data = args.last 17 | unless data[:name] =~ /^Fixture/ or data[:sql] =~ IGNORE_SQL 18 | if windows 19 | puts data[:sql] 20 | else 21 | puts log_subscriber.send(:color, data[:sql], :cyan) 22 | end 23 | end 24 | end 25 | 26 | # load all fixtures 27 | ActiverecordTestConnector::Fixtures.create_fixtures \ 28 | ActiverecordTestConnector::FIXTURES_PATH, ActiveRecord::Base.connection.tables 29 | -------------------------------------------------------------------------------- /lib/will_paginate/sequel.rb: -------------------------------------------------------------------------------- 1 | require 'sequel' 2 | require 'sequel/extensions/pagination' 3 | require 'will_paginate/collection' 4 | 5 | module WillPaginate 6 | # Sequel already supports pagination; we only need to make the 7 | # resulting dataset look a bit more like WillPaginate::Collection 8 | module SequelMethods 9 | include WillPaginate::CollectionMethods 10 | 11 | def total_pages 12 | page_count 13 | end 14 | 15 | def per_page 16 | page_size 17 | end 18 | 19 | def size 20 | current_page_record_count 21 | end 22 | alias length size 23 | 24 | def total_entries 25 | pagination_record_count 26 | end 27 | 28 | def out_of_bounds? 29 | current_page > total_pages 30 | end 31 | 32 | # Current offset of the paginated collection 33 | def offset 34 | (current_page - 1) * per_page 35 | end 36 | end 37 | 38 | Sequel::Dataset::Pagination.send(:include, SequelMethods) 39 | end 40 | -------------------------------------------------------------------------------- /spec/per_page_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'will_paginate/per_page' 3 | 4 | describe WillPaginate::PerPage do 5 | 6 | class MyModel 7 | extend WillPaginate::PerPage 8 | end 9 | 10 | it "has the default value" do 11 | MyModel.per_page.should == 30 12 | 13 | WillPaginate.per_page = 10 14 | begin 15 | MyModel.per_page.should == 10 16 | ensure 17 | WillPaginate.per_page = 30 18 | end 19 | end 20 | 21 | it "casts values to int" do 22 | WillPaginate.per_page = '10' 23 | begin 24 | MyModel.per_page.should == 10 25 | ensure 26 | WillPaginate.per_page = 30 27 | end 28 | end 29 | 30 | it "has an explicit value" do 31 | MyModel.per_page = 12 32 | begin 33 | MyModel.per_page.should == 12 34 | subclass = Class.new(MyModel) 35 | subclass.per_page.should == 12 36 | ensure 37 | MyModel.send(:remove_instance_variable, '@per_page') 38 | end 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Mislav Marohnić 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'view_helpers/view_example_group' 3 | begin 4 | require 'ruby-debug' 5 | rescue LoadError 6 | # no debugger available 7 | end 8 | 9 | Dir[File.expand_path('../matchers/*_matcher.rb', __FILE__)].each { |matcher| require matcher } 10 | 11 | RSpec.configure do |config| 12 | config.include Module.new { 13 | protected 14 | 15 | def include_phrase(string) 16 | PhraseMatcher.new(string) 17 | end 18 | 19 | def have_deprecation(msg) 20 | DeprecationMatcher.new(msg) 21 | end 22 | 23 | def run_queries(num) 24 | QueryCountMatcher.new(num) 25 | end 26 | 27 | def ignore_deprecation 28 | ActiveSupport::Deprecation.silence { yield } 29 | end 30 | 31 | def show_queries(&block) 32 | counter = QueryCountMatcher.new(nil) 33 | counter.run block 34 | ensure 35 | queries = counter.performed_queries 36 | if queries.any? 37 | puts queries 38 | else 39 | puts "no queries" 40 | end 41 | end 42 | } 43 | 44 | config.mock_with :mocha 45 | config.backtrace_clean_patterns << /view_example_group/ 46 | end 47 | -------------------------------------------------------------------------------- /lib/will_paginate/view_helpers/sinatra.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'will_paginate/view_helpers' 3 | require 'will_paginate/view_helpers/link_renderer' 4 | 5 | module WillPaginate 6 | module Sinatra 7 | module Helpers 8 | include ViewHelpers 9 | 10 | def will_paginate(collection, options = {}) #:nodoc: 11 | options = options.merge(:renderer => LinkRenderer) unless options[:renderer] 12 | super(collection, options) 13 | end 14 | end 15 | 16 | class LinkRenderer < ViewHelpers::LinkRenderer 17 | protected 18 | 19 | def url(page) 20 | str = File.join(request.script_name.to_s, request.path_info) 21 | params = request.GET.merge(param_name.to_s => page.to_s) 22 | params.update @options[:params] if @options[:params] 23 | str << '?' << build_query(params) 24 | end 25 | 26 | def request 27 | @template.request 28 | end 29 | 30 | def build_query(params) 31 | Rack::Utils.build_nested_query params 32 | end 33 | end 34 | 35 | def self.registered(app) 36 | app.helpers Helpers 37 | end 38 | 39 | ::Sinatra.register self 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/will_paginate/locale/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | will_paginate: 3 | previous_label: "← Previous" 4 | next_label: "Next →" 5 | page_gap: "…" 6 | 7 | page_entries_info: 8 | single_page: 9 | zero: "No %{model} found" 10 | one: "Displaying 1 %{model}" 11 | other: "Displaying all %{count} %{model}" 12 | single_page_html: 13 | zero: "No %{model} found" 14 | one: "Displaying 1 %{model}" 15 | other: "Displaying all %{count} %{model}" 16 | 17 | multi_page: "Displaying %{model} %{from} - %{to} of %{count} in total" 18 | multi_page_html: "Displaying %{model} %{from} - %{to} of %{count} in total" 19 | 20 | # models: 21 | # entry: 22 | # zero: entries 23 | # one: entry 24 | # few: entries 25 | # other: entries 26 | 27 | # line_item: 28 | # page_entries_info: 29 | # single_page: 30 | # zero: "Your shopping cart is empty" 31 | # one: "Displaying one item in your cart" 32 | # other: "Displaying all %{count} items" 33 | # multi_page: "Displaying items %{from} - %{to} of %{count} in total" 34 | -------------------------------------------------------------------------------- /spec/fixtures/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | 3 | create_table "users", :force => true do |t| 4 | t.column "name", :text 5 | t.column "salary", :integer, :default => 70000 6 | t.column "created_at", :datetime 7 | t.column "updated_at", :datetime 8 | t.column "type", :text 9 | end 10 | 11 | create_table "projects", :force => true do |t| 12 | t.column "name", :text 13 | end 14 | 15 | create_table "developers_projects", :id => false, :force => true do |t| 16 | t.column "developer_id", :integer, :null => false 17 | t.column "project_id", :integer, :null => false 18 | t.column "joined_on", :date 19 | t.column "access_level", :integer, :default => 1 20 | end 21 | 22 | create_table "topics", :force => true do |t| 23 | t.column "project_id", :integer 24 | t.column "title", :string 25 | t.column "subtitle", :string 26 | t.column "content", :text 27 | t.column "created_at", :datetime 28 | t.column "updated_at", :datetime 29 | end 30 | 31 | create_table "replies", :force => true do |t| 32 | t.column "content", :text 33 | t.column "created_at", :datetime 34 | t.column "updated_at", :datetime 35 | t.column "topic_id", :integer 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/will_paginate/mongoid.rb: -------------------------------------------------------------------------------- 1 | require 'mongoid' 2 | require 'will_paginate/collection' 3 | 4 | module WillPaginate 5 | module Mongoid 6 | module CriteriaMethods 7 | def paginate(options = {}) 8 | extend CollectionMethods 9 | @current_page = WillPaginate::PageNumber(options[:page] || @current_page || 1) 10 | @page_multiplier = current_page - 1 11 | pp = (options[:per_page] || per_page || WillPaginate.per_page).to_i 12 | limit(pp).skip(@page_multiplier * pp) 13 | end 14 | 15 | def per_page(value = :non_given) 16 | if value == :non_given 17 | options[:limit] == 0 ? nil : options[:limit] # in new Mongoid versions a nil limit is saved as 0 18 | else 19 | limit(value) 20 | end 21 | end 22 | 23 | def page(page) 24 | paginate(:page => page) 25 | end 26 | end 27 | 28 | module CollectionMethods 29 | attr_reader :current_page 30 | 31 | def total_entries 32 | @total_entries ||= count 33 | end 34 | 35 | def total_pages 36 | (total_entries / per_page.to_f).ceil 37 | end 38 | 39 | def offset 40 | @page_multiplier * per_page 41 | end 42 | end 43 | 44 | ::Mongoid::Criteria.send(:include, CriteriaMethods) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/finders/data_mapper_test_connector.rb: -------------------------------------------------------------------------------- 1 | require 'sqlite3' 2 | require 'dm-core' 3 | require 'dm-core/support/logger' 4 | require 'dm-migrations' 5 | 6 | DataMapper.setup :default, 'sqlite3::memory:' 7 | 8 | # Define models 9 | class Animal 10 | include DataMapper::Resource 11 | property :id, Serial 12 | property :name, String 13 | property :notes, Text 14 | 15 | def self.setup 16 | Animal.create(:name => 'Dog', :notes => "Man's best friend") 17 | Animal.create(:name => 'Cat', :notes => "Woman's best friend") 18 | Animal.create(:name => 'Lion', :notes => 'King of the Jungle') 19 | end 20 | end 21 | 22 | class Ownership 23 | include DataMapper::Resource 24 | 25 | belongs_to :animal, :key => true 26 | belongs_to :human, :key => true 27 | 28 | def self.setup 29 | end 30 | end 31 | 32 | class Human 33 | include DataMapper::Resource 34 | 35 | property :id, Serial 36 | property :name, String 37 | 38 | has n, :ownerships 39 | has 1, :pet, :model => 'Animal', :through => :ownerships, :via => :animal 40 | 41 | def self.setup 42 | end 43 | end 44 | 45 | # Load fixtures 46 | [Animal, Ownership, Human].each do |klass| 47 | klass.auto_migrate! 48 | klass.setup 49 | end 50 | 51 | if 'irb' == $0 52 | DataMapper.logger.set_log($stdout, :debug) 53 | DataMapper.logger.auto_flush = true 54 | end 55 | -------------------------------------------------------------------------------- /will_paginate.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'rbconfig' 3 | require File.expand_path('../lib/will_paginate/version', __FILE__) 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'will_paginate' 7 | s.version = WillPaginate::VERSION::STRING 8 | 9 | s.summary = "Pagination plugin for web frameworks and other apps" 10 | s.description = "will_paginate provides a simple API for performing paginated queries with Active Record, DataMapper and Sequel, and includes helpers for rendering pagination links in Rails and Sinatra web apps." 11 | 12 | s.authors = ['Mislav Marohnić'] 13 | s.email = 'mislav.marohnic@gmail.com' 14 | s.homepage = 'https://github.com/mislav/will_paginate/wiki' 15 | s.license = 'MIT' 16 | 17 | s.rdoc_options = ['--main', 'README.md', '--charset=UTF-8'] 18 | s.extra_rdoc_files = ['README.md', 'LICENSE'] 19 | 20 | s.files = Dir['Rakefile', '{bin,lib,test,spec}/**/*', 'README*', 'LICENSE*'] 21 | 22 | # include only files in version control 23 | git_dir = File.expand_path('../.git', __FILE__) 24 | void = defined?(File::NULL) ? File::NULL : 25 | RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw/ ? 'NUL' : '/dev/null' 26 | 27 | if File.directory?(git_dir) and system "git --version >>#{void} 2>&1" 28 | s.files &= `git --git-dir='#{git_dir}' ls-files -z`.split("\0") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/will_paginate/array.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate/collection' 2 | 3 | class Array 4 | # Paginates a static array (extracting a subset of it). The result is a 5 | # WillPaginate::Collection instance, which is an array with a few more 6 | # properties about its paginated state. 7 | # 8 | # Parameters: 9 | # * :page - current page, defaults to 1 10 | # * :per_page - limit of items per page, defaults to 30 11 | # * :total_entries - total number of items in the array, defaults to 12 | # array.length (obviously) 13 | # 14 | # Example: 15 | # arr = ['a', 'b', 'c', 'd', 'e'] 16 | # paged = arr.paginate(:per_page => 2) #-> ['a', 'b'] 17 | # paged.total_entries #-> 5 18 | # arr.paginate(:page => 2, :per_page => 2) #-> ['c', 'd'] 19 | # arr.paginate(:page => 3, :per_page => 2) #-> ['e'] 20 | # 21 | # This method was originally {suggested by Desi 22 | # McAdam}[http://www.desimcadam.com/archives/8] and later proved to be the 23 | # most useful method of will_paginate library. 24 | def paginate(options = {}) 25 | page = options[:page] || 1 26 | per_page = options[:per_page] || WillPaginate.per_page 27 | total = options[:total_entries] || self.length 28 | 29 | WillPaginate::Collection.create(page, per_page, total) do |pager| 30 | pager.replace self[pager.offset, pager.per_page].to_a 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/will_paginate/deprecation.rb: -------------------------------------------------------------------------------- 1 | module WillPaginate::Deprecation 2 | class << self 3 | def warn(message, stack = caller) 4 | offending_line = origin_of_call(stack) 5 | full_message = "DEPRECATION WARNING: #{message} (called from #{offending_line})" 6 | logger = rails_logger || Kernel 7 | logger.warn full_message 8 | end 9 | 10 | private 11 | 12 | def rails_logger 13 | defined?(Rails) && Rails.logger 14 | end 15 | 16 | def origin_of_call(stack) 17 | lib_root = File.expand_path('../../..', __FILE__) 18 | stack.find { |line| line.index(lib_root) != 0 } || stack.first 19 | end 20 | end 21 | 22 | class Hash < ::Hash 23 | def initialize(values = {}) 24 | super() 25 | update values 26 | @deprecated = {} 27 | end 28 | 29 | def []=(key, value) 30 | check_deprecated(key, value) 31 | super 32 | end 33 | 34 | def deprecate_key(*keys) 35 | message = block_given? ? Proc.new : keys.pop 36 | Array(keys).each { |key| @deprecated[key] = message } 37 | end 38 | 39 | def merge(another) 40 | to_hash.update(another) 41 | end 42 | 43 | def to_hash 44 | ::Hash.new.update(self) 45 | end 46 | 47 | private 48 | 49 | def check_deprecated(key, value) 50 | if msg = @deprecated[key] and (!msg.respond_to?(:call) or (msg = msg.call(key, value))) 51 | WillPaginate::Deprecation.warn(msg) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/will_paginate/page_number.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | require 'forwardable' 3 | 4 | module WillPaginate 5 | # a module that page number exceptions are tagged with 6 | module InvalidPage; end 7 | 8 | # integer representing a page number 9 | class PageNumber < DelegateClass(Integer) 10 | # a value larger than this is not supported in SQL queries 11 | BIGINT = 9223372036854775807 12 | 13 | extend Forwardable 14 | 15 | def initialize(value, name) 16 | value = Integer(value) 17 | if 'offset'.freeze == name ? (value < 0 or value > BIGINT) : value < 1 18 | raise RangeError, "invalid #{name}: #{value.inspect}" 19 | end 20 | @name = name 21 | super(value) 22 | rescue ArgumentError, TypeError, RangeError => error 23 | error.extend InvalidPage 24 | raise error 25 | end 26 | 27 | alias_method :to_i, :__getobj__ 28 | 29 | def inspect 30 | "#{@name} #{to_i}" 31 | end 32 | 33 | def to_offset(per_page) 34 | PageNumber.new((to_i - 1) * per_page.to_i, 'offset'.freeze) 35 | end 36 | 37 | def kind_of?(klass) 38 | super || to_i.kind_of?(klass) 39 | end 40 | alias is_a? kind_of? 41 | end 42 | 43 | # Ultrahax: makes `Fixnum === current_page` checks pass 44 | Numeric.extend Module.new { 45 | def ===(obj) 46 | obj.instance_of? PageNumber or super 47 | end 48 | } 49 | 50 | # An idemptotent coercion method 51 | def self.PageNumber(value, name = 'page'.freeze) 52 | case value 53 | when PageNumber then value 54 | else PageNumber.new(value, name) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # will_paginate 2 | 3 | This version of `will_paginate` is optimized for Rails 5 on Ruby 2.2+ and 4 | compatibility with the [Ransack](https://github.com/activerecord-hackery/ransack) 5 | search gem: 6 | 7 | - Strings are now frozen to be immutable, to reduce object allocations and memory use. 8 | - Rails 5 composes ActionController::Parameters as a HWIA instead of inheriting from Hash. Ransack needs nested options hashes in AC::Parameters to be parsed correctly. This is handled here. 9 | - Legacy code for Rails 2 and 3 and Merb was removed to make the gem more lightweight. 10 | 11 | Installation: 12 | 13 | ```ruby 14 | gem 'will_paginate', github: 'jonatack/will_paginate' 15 | ``` 16 | 17 | ## Basic use 18 | 19 | In the controller: 20 | 21 | ```ruby 22 | # perform a paginated query 23 | @posts = Post.paginate(page: params[:page]) 24 | 25 | # use an explicit "per page" limit 26 | Post.paginate(page: params[:page], per_page: 30) 27 | 28 | # will_paginate returns a chainable Active Record relation 29 | Post.where(published: true).paginate(page: params[:page]).order(id: :desc) 30 | 31 | # the new, shorter #page method 32 | Post.page(params[:page]).order(created_at: :asc) 33 | ``` 34 | 35 | In the view, to render pagination links: 36 | 37 | ```erb 38 | <%= will_paginate @posts %> 39 | ``` 40 | 41 | And that's it! You just need to add some CSS styles to [make those pagination links prettier](http://mislav.github.io/will_paginate/). 42 | 43 | You can customize the default "per_page" value: 44 | 45 | ``` ruby 46 | # for the Post model 47 | class Post 48 | self.per_page = 10 49 | end 50 | 51 | # set per_page globally 52 | WillPaginate.per_page = 10 53 | ``` 54 | 55 | Happy paginating! :heart: 56 | -------------------------------------------------------------------------------- /Gemfile.rails4.1.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionpack (4.1.2) 5 | actionview (= 4.1.2) 6 | activesupport (= 4.1.2) 7 | rack (~> 1.5.2) 8 | rack-test (~> 0.6.2) 9 | actionview (4.1.2) 10 | activesupport (= 4.1.2) 11 | builder (~> 3.1) 12 | erubis (~> 2.7.0) 13 | activemodel (4.1.2) 14 | activesupport (= 4.1.2) 15 | builder (~> 3.1) 16 | activerecord (4.1.2) 17 | activemodel (= 4.1.2) 18 | activesupport (= 4.1.2) 19 | arel (~> 5.0.0) 20 | activerecord-deprecated_finders (1.0.3) 21 | activesupport (4.1.2) 22 | i18n (~> 0.6, >= 0.6.9) 23 | json (~> 1.7, >= 1.7.7) 24 | minitest (~> 5.1) 25 | thread_safe (~> 0.1) 26 | tzinfo (~> 1.1) 27 | arel (5.0.1.20140414130214) 28 | builder (3.2.2) 29 | columnize (0.8.9) 30 | debugger (1.6.8) 31 | columnize (>= 0.3.1) 32 | debugger-linecache (~> 1.2.0) 33 | debugger-ruby_core_source (~> 1.3.5) 34 | debugger-linecache (1.2.0) 35 | debugger-ruby_core_source (1.3.5) 36 | diff-lcs (1.1.3) 37 | erubis (2.7.0) 38 | i18n (0.6.9) 39 | json (1.8.1) 40 | minitest (5.3.5) 41 | mocha (0.9.12) 42 | mysql (2.9.1) 43 | mysql2 (0.3.16) 44 | pg (0.17.1) 45 | rack (1.5.2) 46 | rack-test (0.6.2) 47 | rack (>= 1.0) 48 | rspec (2.6.0) 49 | rspec-core (~> 2.6.0) 50 | rspec-expectations (~> 2.6.0) 51 | rspec-mocks (~> 2.6.0) 52 | rspec-core (2.6.4) 53 | rspec-expectations (2.6.0) 54 | diff-lcs (~> 1.1.2) 55 | rspec-mocks (2.6.0) 56 | sqlite3 (1.3.9) 57 | thread_safe (0.3.4) 58 | tzinfo (1.2.1) 59 | thread_safe (~> 0.1) 60 | 61 | PLATFORMS 62 | ruby 63 | 64 | DEPENDENCIES 65 | actionpack (~> 4.1.2) 66 | activerecord (~> 4.1.2) 67 | activerecord-deprecated_finders 68 | debugger 69 | mocha (~> 0.9.8) 70 | mysql (~> 2.9) 71 | mysql2 (~> 0.3.10) 72 | pg (~> 0.11) 73 | rspec (~> 2.6.0) 74 | sqlite3 (~> 1.3.6) 75 | -------------------------------------------------------------------------------- /spec/finders/sequel_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | begin 4 | require 'will_paginate/sequel' 5 | require File.expand_path('../sequel_test_connector', __FILE__) 6 | rescue LoadError, ArgumentError => error 7 | warn "Error running Sequel specs: #{error.message}" 8 | sequel_loaded = false 9 | else 10 | sequel_loaded = true 11 | end 12 | 13 | describe Sequel::Dataset::Pagination, 'extension' do 14 | 15 | class Car < Sequel::Model 16 | end 17 | 18 | it "should have the #paginate method" do 19 | Car.should respond_to(:paginate) 20 | end 21 | 22 | it "should NOT have the #paginate_by_sql method" do 23 | Car.should_not respond_to(:paginate_by_sql) 24 | end 25 | 26 | describe 'pagination' do 27 | before(:all) do 28 | Car.create(:name => 'Shelby', :notes => "Man's best friend") 29 | Car.create(:name => 'Aston Martin', :notes => "Woman's best friend") 30 | Car.create(:name => 'Corvette', :notes => 'King of the Jungle') 31 | end 32 | 33 | it "should imitate WillPaginate::Collection" do 34 | result = Car.paginate(1, 2) 35 | 36 | result.should_not be_empty 37 | result.size.should == 2 38 | result.length.should == 2 39 | result.total_entries.should == 3 40 | result.total_pages.should == 2 41 | result.per_page.should == 2 42 | result.current_page.should == 1 43 | end 44 | 45 | it "should perform" do 46 | Car.paginate(1, 2).all.should == [Car[1], Car[2]] 47 | end 48 | 49 | it "should be empty" do 50 | result = Car.paginate(3, 2) 51 | result.should be_empty 52 | end 53 | 54 | it "should perform with #select and #order" do 55 | result = Car.select("name as foo".lit).order(:name).paginate(1, 2).all 56 | result.size.should == 2 57 | result.first.values[:foo].should == "Aston Martin" 58 | end 59 | 60 | it "should perform with #filter" do 61 | results = Car.filter(:name => 'Shelby').paginate(1, 2).all 62 | results.size.should == 1 63 | results.first.should == Car.find(:name => 'Shelby') 64 | end 65 | end 66 | 67 | end if sequel_loaded 68 | -------------------------------------------------------------------------------- /spec/page_number_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'will_paginate/page_number' 3 | 4 | describe WillPaginate::PageNumber do 5 | describe "valid" do 6 | subject { described_class.new('12', 'page') } 7 | 8 | it { should eq(12) } 9 | its(:inspect) { should eq('page 12') } 10 | it { should be_a(WillPaginate::PageNumber) } 11 | it { should be_instance_of(WillPaginate::PageNumber) } 12 | it { should be_a(Numeric) } 13 | it { should be_a(Fixnum) } 14 | it { should_not be_instance_of(Fixnum) } 15 | 16 | it "passes the PageNumber=== type check" do |variable| 17 | (WillPaginate::PageNumber === subject).should be 18 | end 19 | 20 | it "passes the Numeric=== type check" do |variable| 21 | (Numeric === subject).should be 22 | (Fixnum === subject).should be 23 | end 24 | end 25 | 26 | describe "invalid" do 27 | def create(value, name = 'page') 28 | described_class.new(value, name) 29 | end 30 | 31 | it "errors out on non-int values" do 32 | lambda { create(nil) }.should raise_error(WillPaginate::InvalidPage) 33 | lambda { create('') }.should raise_error(WillPaginate::InvalidPage) 34 | lambda { create('Schnitzel') }.should raise_error(WillPaginate::InvalidPage) 35 | end 36 | 37 | it "errors out on zero or less" do 38 | lambda { create(0) }.should raise_error(WillPaginate::InvalidPage) 39 | lambda { create(-1) }.should raise_error(WillPaginate::InvalidPage) 40 | end 41 | 42 | it "doesn't error out on zero for 'offset'" do 43 | lambda { create(0, 'offset') }.should_not raise_error 44 | lambda { create(-1, 'offset') }.should raise_error(WillPaginate::InvalidPage) 45 | end 46 | end 47 | 48 | describe "coercion method" do 49 | it "defaults to 'page' name" do 50 | num = WillPaginate::PageNumber(12) 51 | num.inspect.should eq('page 12') 52 | end 53 | 54 | it "accepts a custom name" do 55 | num = WillPaginate::PageNumber(12, 'monkeys') 56 | num.inspect.should eq('monkeys 12') 57 | end 58 | 59 | it "doesn't affect PageNumber instances" do 60 | num = WillPaginate::PageNumber(12) 61 | num2 = WillPaginate::PageNumber(num) 62 | num2.object_id.should eq(num.object_id) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionpack (4.0.5) 5 | activesupport (= 4.0.5) 6 | builder (~> 3.1.0) 7 | erubis (~> 2.7.0) 8 | rack (~> 1.5.2) 9 | rack-test (~> 0.6.2) 10 | activemodel (4.0.5) 11 | activesupport (= 4.0.5) 12 | builder (~> 3.1.0) 13 | activerecord (4.0.5) 14 | activemodel (= 4.0.5) 15 | activerecord-deprecated_finders (~> 1.0.2) 16 | activesupport (= 4.0.5) 17 | arel (~> 4.0.0) 18 | activerecord-deprecated_finders (1.0.3) 19 | activesupport (4.0.5) 20 | i18n (~> 0.6, >= 0.6.9) 21 | minitest (~> 4.2) 22 | multi_json (~> 1.3) 23 | thread_safe (~> 0.1) 24 | tzinfo (~> 0.3.37) 25 | arel (4.0.2) 26 | builder (3.1.4) 27 | columnize (0.3.5) 28 | debugger (1.2.2) 29 | columnize (>= 0.3.1) 30 | debugger-linecache (~> 1.1.1) 31 | debugger-ruby_core_source (~> 1.1.5) 32 | debugger-linecache (1.1.2) 33 | debugger-ruby_core_source (>= 1.1.1) 34 | debugger-ruby_core_source (1.1.5) 35 | diff-lcs (1.1.3) 36 | erubis (2.7.0) 37 | i18n (0.6.9) 38 | linecache (0.46) 39 | rbx-require-relative (> 0.0.4) 40 | minitest (4.7.5) 41 | mocha (0.9.12) 42 | multi_json (1.10.1) 43 | mysql (2.9.0) 44 | mysql2 (0.3.11) 45 | pg (0.12.0) 46 | rake-compiler (~> 0.7) 47 | rack (1.5.2) 48 | rack-test (0.6.2) 49 | rack (>= 1.0) 50 | rake (0.9.2.2) 51 | rake-compiler (0.7.9) 52 | rake 53 | rbx-require-relative (0.0.5) 54 | rspec (2.6.0) 55 | rspec-core (~> 2.6.0) 56 | rspec-expectations (~> 2.6.0) 57 | rspec-mocks (~> 2.6.0) 58 | rspec-core (2.6.4) 59 | rspec-expectations (2.6.0) 60 | diff-lcs (~> 1.1.2) 61 | rspec-mocks (2.6.0) 62 | ruby-debug (0.10.4) 63 | columnize (>= 0.1) 64 | ruby-debug-base (~> 0.10.4.0) 65 | ruby-debug-base (0.10.4) 66 | linecache (>= 0.3) 67 | sqlite3 (1.3.6) 68 | thread_safe (0.3.4) 69 | tzinfo (0.3.39) 70 | 71 | PLATFORMS 72 | ruby 73 | 74 | DEPENDENCIES 75 | actionpack (~> 4.0.0) 76 | activerecord (~> 4.0.0) 77 | debugger 78 | mocha (~> 0.9.8) 79 | mysql (~> 2.9) 80 | mysql2 (~> 0.3.10) 81 | pg (~> 0.11) 82 | rspec (~> 2.6.0) 83 | ruby-debug 84 | sqlite3 (~> 1.3.6) 85 | -------------------------------------------------------------------------------- /lib/will_paginate/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate' 2 | require 'will_paginate/page_number' 3 | require 'will_paginate/collection' 4 | require 'will_paginate/i18n' 5 | 6 | module WillPaginate 7 | class Railtie < Rails::Railtie 8 | initializer "will_paginate" do |app| 9 | ActiveSupport.on_load :active_record do 10 | require 'will_paginate/active_record' 11 | end 12 | 13 | ActiveSupport.on_load :action_controller do 14 | WillPaginate::Railtie.setup_actioncontroller 15 | end 16 | 17 | ActiveSupport.on_load :action_view do 18 | require 'will_paginate/view_helpers/action_view' 19 | end 20 | 21 | self.class.add_locale_path config 22 | 23 | # early access to ViewHelpers.pagination_options 24 | require 'will_paginate/view_helpers' 25 | end 26 | 27 | def self.setup_actioncontroller 28 | ( defined?(ActionDispatch::ExceptionWrapper) ? 29 | ActionDispatch::ExceptionWrapper : ActionDispatch::ShowExceptions 30 | ).send :include, ShowExceptionsPatch 31 | ActionController::Base.extend ControllerRescuePatch 32 | end 33 | 34 | def self.add_locale_path(config) 35 | config.i18n.load_path.unshift(*WillPaginate::I18n.load_path) 36 | end 37 | 38 | # Extending the exception handler middleware so it properly detects 39 | # WillPaginate::InvalidPage regardless of it being a tag module. 40 | module ShowExceptionsPatch 41 | extend ActiveSupport::Concern 42 | included do 43 | alias_method :status_code_without_paginate, :status_code 44 | alias_method :status_code, :status_code_with_paginate 45 | end 46 | def status_code_with_paginate(exception = @exception) 47 | if exception.is_a?(WillPaginate::InvalidPage) || 48 | exception.cause.is_a?(WillPaginate::InvalidPage) 49 | Rack::Utils.status_code(:not_found) 50 | else 51 | original_method = method(:status_code_without_paginate) 52 | if original_method.arity != 0 53 | original_method.call(exception) 54 | else 55 | original_method.call() 56 | end 57 | end 58 | end 59 | end 60 | 61 | module ControllerRescuePatch 62 | def rescue_from(*args, &block) 63 | if idx = args.index(WillPaginate::InvalidPage) 64 | args[idx] = args[idx].name 65 | end 66 | super(*args, &block) 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/will_paginate/view_helpers/link_renderer_base.rb: -------------------------------------------------------------------------------- 1 | module WillPaginate 2 | module ViewHelpers 3 | # This class does the heavy lifting of actually building the pagination 4 | # links. It is used by +will_paginate+ helper internally. 5 | class LinkRendererBase 6 | 7 | # * +collection+ is a WillPaginate::Collection instance or any other object 8 | # that conforms to that API 9 | # * +options+ are forwarded from +will_paginate+ view helper 10 | def prepare(collection, options) 11 | @collection = collection 12 | @options = options 13 | 14 | # reset values in case we're re-using this instance 15 | @total_pages = nil 16 | end 17 | 18 | def pagination 19 | items = @options[:page_links] ? windowed_page_numbers : [] 20 | items.unshift :previous_page 21 | items.push :next_page 22 | end 23 | 24 | protected 25 | 26 | # Calculates visible page numbers using the :inner_window and 27 | # :outer_window options. 28 | def windowed_page_numbers 29 | inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i 30 | window_from = current_page - inner_window 31 | window_to = current_page + inner_window 32 | 33 | # adjust lower or upper limit if other is out of bounds 34 | if window_to > total_pages 35 | window_from -= window_to - total_pages 36 | window_to = total_pages 37 | end 38 | if window_from < 1 39 | window_to += 1 - window_from 40 | window_from = 1 41 | window_to = total_pages if window_to > total_pages 42 | end 43 | 44 | # these are always visible 45 | middle = window_from..window_to 46 | 47 | # left window 48 | if outer_window + 3 < middle.first # there's a gap 49 | left = (1..(outer_window + 1)).to_a 50 | left << :gap 51 | else # runs into visible pages 52 | left = 1...middle.first 53 | end 54 | 55 | # right window 56 | if total_pages - outer_window - 2 > middle.last # again, gap 57 | right = ((total_pages - outer_window)..total_pages).to_a 58 | right.unshift :gap 59 | else # runs into visible pages 60 | right = (middle.last + 1)..total_pages 61 | end 62 | 63 | left.to_a + middle.to_a + right.to_a 64 | end 65 | 66 | private 67 | 68 | def current_page 69 | @collection.current_page 70 | end 71 | 72 | def total_pages 73 | @total_pages ||= @collection.total_pages 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /Gemfile.rails-edge.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/rails/activerecord-deprecated_finders.git 3 | revision: 9068cadb719039bdda5690aab6d2e6db6bc1b970 4 | branch: master 5 | specs: 6 | activerecord-deprecated_finders (1.0.3) 7 | 8 | GIT 9 | remote: git://github.com/rails/arel.git 10 | revision: f50de54a6f0c59ab75526cfdb7896830130ffdf7 11 | branch: master 12 | specs: 13 | arel (6.0.0.20140505020427) 14 | 15 | GIT 16 | remote: git://github.com/rails/rails.git 17 | revision: e7a8fda0336fe733adbdd0bdb642a09327ef22f1 18 | branch: master 19 | specs: 20 | actionpack (4.2.0.alpha) 21 | actionview (= 4.2.0.alpha) 22 | activesupport (= 4.2.0.alpha) 23 | rack (~> 1.5.2) 24 | rack-test (~> 0.6.2) 25 | actionview (4.2.0.alpha) 26 | activesupport (= 4.2.0.alpha) 27 | builder (~> 3.1) 28 | erubis (~> 2.7.0) 29 | activemodel (4.2.0.alpha) 30 | activesupport (= 4.2.0.alpha) 31 | builder (~> 3.1) 32 | activerecord (4.2.0.alpha) 33 | activemodel (= 4.2.0.alpha) 34 | activesupport (= 4.2.0.alpha) 35 | arel (~> 6.0.0) 36 | activesupport (4.2.0.alpha) 37 | i18n (~> 0.6, >= 0.6.9) 38 | json (~> 1.7, >= 1.7.7) 39 | minitest (~> 5.1) 40 | thread_safe (~> 0.1) 41 | tzinfo (~> 1.1) 42 | 43 | GEM 44 | remote: https://rubygems.org/ 45 | specs: 46 | builder (3.2.2) 47 | columnize (0.3.6) 48 | debugger (1.6.0) 49 | columnize (>= 0.3.1) 50 | debugger-linecache (~> 1.2.0) 51 | debugger-ruby_core_source (~> 1.2.1) 52 | debugger-linecache (1.2.0) 53 | debugger-ruby_core_source (1.2.2) 54 | diff-lcs (1.1.3) 55 | erubis (2.7.0) 56 | i18n (0.6.9) 57 | json (1.8.1) 58 | linecache (0.46) 59 | rbx-require-relative (> 0.0.4) 60 | minitest (5.3.4) 61 | mocha (0.9.12) 62 | mysql (2.9.1) 63 | mysql2 (0.3.16) 64 | pg (0.15.1) 65 | rack (1.5.2) 66 | rack-test (0.6.2) 67 | rack (>= 1.0) 68 | rbx-require-relative (0.0.9) 69 | rspec (2.6.0) 70 | rspec-core (~> 2.6.0) 71 | rspec-expectations (~> 2.6.0) 72 | rspec-mocks (~> 2.6.0) 73 | rspec-core (2.6.4) 74 | rspec-expectations (2.6.0) 75 | diff-lcs (~> 1.1.2) 76 | rspec-mocks (2.6.0) 77 | ruby-debug (0.10.4) 78 | columnize (>= 0.1) 79 | ruby-debug-base (~> 0.10.4.0) 80 | ruby-debug-base (0.10.4) 81 | linecache (>= 0.3) 82 | sqlite3 (1.3.7) 83 | thread_safe (0.3.4) 84 | tzinfo (1.2.1) 85 | thread_safe (~> 0.1) 86 | 87 | PLATFORMS 88 | ruby 89 | 90 | DEPENDENCIES 91 | actionpack! 92 | activerecord! 93 | activerecord-deprecated_finders! 94 | arel! 95 | debugger 96 | mocha (~> 0.9.8) 97 | mysql (~> 2.9) 98 | mysql2 (~> 0.3.13) 99 | pg (~> 0.11) 100 | rspec (~> 2.6.0) 101 | ruby-debug 102 | sqlite3 (~> 1.3.6) 103 | thread_safe 104 | -------------------------------------------------------------------------------- /spec/view_helpers/link_renderer_base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'will_paginate/view_helpers/link_renderer_base' 3 | require 'will_paginate/collection' 4 | 5 | describe WillPaginate::ViewHelpers::LinkRendererBase do 6 | 7 | before do 8 | @renderer = described_class.new 9 | end 10 | 11 | it "should raise error when unprepared" do 12 | lambda { 13 | @renderer.pagination 14 | }.should raise_error 15 | end 16 | 17 | it "should prepare with collection and options" do 18 | prepare({}) 19 | @renderer.send(:current_page).should == 1 20 | end 21 | 22 | it "should have total_pages accessor" do 23 | prepare :total_pages => 42 24 | @renderer.send(:total_pages).should == 42 25 | end 26 | 27 | it "should clear old cached values when prepared" do 28 | prepare(:total_pages => 1) 29 | @renderer.send(:total_pages).should == 1 30 | # prepare with different object: 31 | prepare(:total_pages => 2) 32 | @renderer.send(:total_pages).should == 2 33 | end 34 | 35 | it "should have pagination definition" do 36 | prepare({ :total_pages => 1 }, :page_links => true) 37 | @renderer.pagination.should == [:previous_page, 1, :next_page] 38 | end 39 | 40 | describe "visible page numbers" do 41 | it "should calculate windowed visible links" do 42 | prepare({ :page => 6, :total_pages => 11 }, :inner_window => 1, :outer_window => 1) 43 | showing_pages 1, 2, :gap, 5, 6, 7, :gap, 10, 11 44 | end 45 | 46 | it "should eliminate small gaps" do 47 | prepare({ :page => 6, :total_pages => 11 }, :inner_window => 2, :outer_window => 1) 48 | # pages 4 and 8 appear instead of the gap 49 | showing_pages 1..11 50 | end 51 | 52 | it "should support having no windows at all" do 53 | prepare({ :page => 4, :total_pages => 7 }, :inner_window => 0, :outer_window => 0) 54 | showing_pages 1, :gap, 4, :gap, 7 55 | end 56 | 57 | it "should adjust upper limit if lower is out of bounds" do 58 | prepare({ :page => 1, :total_pages => 10 }, :inner_window => 2, :outer_window => 1) 59 | showing_pages 1, 2, 3, 4, 5, :gap, 9, 10 60 | end 61 | 62 | it "should adjust lower limit if upper is out of bounds" do 63 | prepare({ :page => 10, :total_pages => 10 }, :inner_window => 2, :outer_window => 1) 64 | showing_pages 1, 2, :gap, 6, 7, 8, 9, 10 65 | end 66 | 67 | def showing_pages(*pages) 68 | pages = pages.first.to_a if Array === pages.first or Range === pages.first 69 | @renderer.send(:windowed_page_numbers).should == pages 70 | end 71 | end 72 | 73 | protected 74 | 75 | def collection(params = {}) 76 | if params[:total_pages] 77 | params[:per_page] = 1 78 | params[:total_entries] = params[:total_pages] 79 | end 80 | WillPaginate::Collection.new(params[:page] || 1, params[:per_page] || 30, params[:total_entries]) 81 | end 82 | 83 | def prepare(collection_options, options = {}) 84 | @renderer.prepare(collection(collection_options), options) 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /lib/will_paginate/data_mapper.rb: -------------------------------------------------------------------------------- 1 | require 'dm-core' 2 | require 'dm-aggregates' 3 | require 'will_paginate/per_page' 4 | require 'will_paginate/page_number' 5 | require 'will_paginate/collection' 6 | 7 | module WillPaginate 8 | module DataMapper 9 | module Pagination 10 | def page(num) 11 | pagenum = ::WillPaginate::PageNumber(num.nil? ? 1 : num) 12 | per_page = query.limit || self.per_page 13 | options = {:offset => pagenum.to_offset(per_page).to_i} 14 | options[:limit] = per_page unless query.limit 15 | col = new_collection(query.merge(options)) 16 | col.current_page = pagenum 17 | col 18 | end 19 | 20 | def paginate(options) 21 | options = options.dup 22 | pagenum = options.fetch(:page) { raise ArgumentError, ":page parameter required" } 23 | per_page = options.delete(:per_page) || self.per_page 24 | total = options.delete(:total_entries) 25 | 26 | options.delete(:page) 27 | options[:limit] = per_page.to_i 28 | 29 | 30 | col = all(options).page(pagenum) 31 | col.total_entries = total.to_i unless total.nil? || (total.kind_of?(String) && total.strip.empty?) 32 | col 33 | end 34 | end 35 | 36 | module CollectionMethods 37 | include WillPaginate::CollectionMethods 38 | 39 | attr_accessor :current_page 40 | attr_writer :total_entries 41 | 42 | def paginated? 43 | !current_page.nil? 44 | end 45 | 46 | def per_page 47 | query.limit || model.per_page 48 | end 49 | 50 | def offset 51 | query.offset 52 | end 53 | 54 | def total_entries 55 | @total_entries ||= begin 56 | if loaded? and @array.size < per_page and (current_page == 1 or @array.size > 0) 57 | offset + @array.size 58 | else 59 | # :reload prevents Collection.filter from being run, which 60 | # would cause a stack overflow 61 | clean_query = query.merge(:reload => true) 62 | # seems like the only way 63 | clean_query.instance_variable_set('@limit', nil) 64 | clean_query.instance_variable_set('@offset', 0) 65 | new_collection(clean_query).count 66 | end 67 | end 68 | end 69 | 70 | def to_a 71 | if paginated? 72 | ::WillPaginate::Collection.create(current_page, per_page) do |col| 73 | col.replace super 74 | col.total_entries ||= total_entries 75 | end 76 | else 77 | super 78 | end 79 | end 80 | 81 | private 82 | 83 | def new_collection(query, resources = nil) 84 | col = super 85 | col.current_page = self.current_page 86 | col 87 | end 88 | 89 | def initialize_copy(original) 90 | super 91 | @total_entries = nil 92 | end 93 | end 94 | 95 | ::DataMapper::Model.append_extensions PerPage 96 | ::DataMapper::Model.append_extensions Pagination 97 | ::DataMapper::Collection.send(:include, Pagination) 98 | ::DataMapper::Collection.send(:include, CollectionMethods) 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/view_helpers/view_example_group.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'stringio' 3 | begin 4 | $stderr = StringIO.new 5 | require 'minitest/unit' 6 | rescue LoadError 7 | # Fails on Ruby 1.8, but it's OK since we only need MiniTest::Assertions 8 | # on Rails 4 which doesn't support 1.8 anyway. 9 | ensure 10 | $stderr = STDERR 11 | end 12 | require 'action_dispatch/testing/assertions' 13 | require 'will_paginate/array' 14 | 15 | module ViewExampleGroup 16 | 17 | include ActionDispatch::Assertions::SelectorAssertions 18 | include MiniTest::Assertions if defined? MiniTest 19 | 20 | def assert(value, message) 21 | raise message unless value 22 | end 23 | 24 | def paginate(collection = {}, options = {}, &block) 25 | if collection.instance_of? Hash 26 | page_options = { :page => 1, :total_entries => 11, :per_page => 4 }.merge(collection) 27 | collection = [1].paginate(page_options) 28 | end 29 | 30 | locals = { :collection => collection, :options => options } 31 | 32 | @render_output = render(locals) 33 | @html_document = nil 34 | 35 | if block_given? 36 | classname = options[:class] || WillPaginate::ViewHelpers.pagination_options[:class] 37 | assert_select("div.#{classname}", 1, 'no main DIV', &block) 38 | end 39 | 40 | @render_output 41 | end 42 | 43 | def html_document 44 | @html_document ||= HTML::Document.new(@render_output, true, false) 45 | end 46 | 47 | def response_from_page_or_rjs 48 | html_document.root 49 | end 50 | 51 | def validate_page_numbers(expected, links, param_name = :page) 52 | param_pattern = /\W#{Regexp.escape(param_name.to_s)}=([^&]*)/ 53 | 54 | links.map { |el| 55 | unescape_href(el) =~ param_pattern 56 | $1 ? $1.to_i : $1 57 | }.should == expected 58 | end 59 | 60 | def assert_links_match(pattern, links = nil, numbers = nil) 61 | links ||= assert_select 'div.pagination a[href]' do |elements| 62 | elements 63 | end 64 | 65 | pages = [] if numbers 66 | 67 | links.each do |el| 68 | href = unescape_href(el) 69 | href.should =~ pattern 70 | if numbers 71 | href =~ pattern 72 | pages << ($1.nil?? nil : $1.to_i) 73 | end 74 | end 75 | 76 | pages.should == numbers if numbers 77 | end 78 | 79 | def assert_no_links_match(pattern) 80 | assert_select 'div.pagination a[href]' do |elements| 81 | elements.each do |el| 82 | unescape_href(el).should_not =~ pattern 83 | end 84 | end 85 | end 86 | 87 | def unescape_href(el) 88 | CGI.unescape CGI.unescapeHTML(el['href']) 89 | end 90 | 91 | def build_message(message, pattern, *args) 92 | built_message = pattern.dup 93 | for value in args 94 | built_message.sub! '?', value.inspect 95 | end 96 | built_message 97 | end 98 | 99 | end 100 | 101 | RSpec.configure do |config| 102 | config.include ViewExampleGroup, :type => :view, :example_group => { 103 | :file_path => %r{spec/view_helpers/} 104 | } 105 | end 106 | 107 | module HTML 108 | Node.class_eval do 109 | def inner_text 110 | children.map(&:inner_text).join('') 111 | end 112 | end 113 | 114 | Text.class_eval do 115 | def inner_text 116 | self.to_s 117 | end 118 | end 119 | 120 | Tag.class_eval do 121 | def inner_text 122 | childless?? '' : super 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /spec/finders/data_mapper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | begin 4 | require 'will_paginate/data_mapper' 5 | require File.expand_path('../data_mapper_test_connector', __FILE__) 6 | rescue LoadError => error 7 | warn "Error running DataMapper specs: #{error.message}" 8 | datamapper_loaded = false 9 | else 10 | datamapper_loaded = true 11 | end 12 | 13 | describe WillPaginate::DataMapper do 14 | 15 | it "has per_page" do 16 | Animal.per_page.should == 30 17 | begin 18 | Animal.per_page = 10 19 | Animal.per_page.should == 10 20 | 21 | subclass = Class.new(Animal) 22 | subclass.per_page.should == 10 23 | ensure 24 | Animal.per_page = 30 25 | end 26 | end 27 | 28 | it "doesn't make normal collections appear paginated" do 29 | Animal.all.should_not be_paginated 30 | end 31 | 32 | it "paginates to first page by default" do 33 | animals = Animal.paginate(:page => nil) 34 | 35 | animals.should be_paginated 36 | animals.current_page.should == 1 37 | animals.per_page.should == 30 38 | animals.offset.should == 0 39 | animals.total_entries.should == 3 40 | animals.total_pages.should == 1 41 | end 42 | 43 | it "paginates to first page, explicit limit" do 44 | animals = Animal.paginate(:page => 1, :per_page => 2) 45 | 46 | animals.current_page.should == 1 47 | animals.per_page.should == 2 48 | animals.total_entries.should == 3 49 | animals.total_pages.should == 2 50 | animals.map {|a| a.name }.should == %w[ Dog Cat ] 51 | end 52 | 53 | it "paginates to second page" do 54 | animals = Animal.paginate(:page => 2, :per_page => 2) 55 | 56 | animals.current_page.should == 2 57 | animals.offset.should == 2 58 | animals.map {|a| a.name }.should == %w[ Lion ] 59 | end 60 | 61 | it "paginates a collection" do 62 | friends = Animal.all(:notes.like => '%friend%') 63 | friends.paginate(:page => 1).per_page.should == 30 64 | friends.paginate(:page => 1, :per_page => 1).total_entries.should == 2 65 | end 66 | 67 | it "paginates a limited collection" do 68 | animals = Animal.all(:limit => 2).paginate(:page => 1) 69 | animals.per_page.should == 2 70 | end 71 | 72 | it "has page() method" do 73 | Animal.page(2).per_page.should == 30 74 | Animal.page(2).offset.should == 30 75 | Animal.page(2).current_page.should == 2 76 | Animal.all(:limit => 2).page(2).per_page.should == 2 77 | end 78 | 79 | it "has total_pages at 1 for empty collections" do 80 | Animal.all(:conditions => ['1=2']).page(1).total_pages.should == 1 81 | end 82 | 83 | it "overrides total_entries count with a fixed value" do 84 | lambda { 85 | animals = Animal.paginate :page => 1, :per_page => 3, :total_entries => 999 86 | animals.total_entries.should == 999 87 | }.should run_queries(0) 88 | end 89 | 90 | it "supports a non-int for total_entries" do 91 | topics = Animal.paginate :page => 1, :per_page => 3, :total_entries => "999" 92 | topics.total_entries.should == 999 93 | end 94 | 95 | 96 | it "can iterate and then call WP methods" do 97 | animals = Animal.all(:limit => 2).page(1) 98 | animals.each { |a| } 99 | animals.total_entries.should == 3 100 | end 101 | 102 | it "augments to_a to return a WP::Collection" do 103 | animals = Animal.all(:limit => 2).page(1) 104 | array = animals.to_a 105 | array.size.should == 2 106 | array.should be_kind_of(WillPaginate::Collection) 107 | array.current_page.should == 1 108 | array.per_page.should == 2 109 | end 110 | 111 | it "doesn't have a problem assigning has-one-through relationship" do 112 | human = Human.create :name => "Mislav" 113 | human.pet = Animal.first 114 | end 115 | 116 | end if datamapper_loaded 117 | -------------------------------------------------------------------------------- /spec/finders/activerecord_test_connector.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'active_record/fixtures' 3 | require 'active_support/multibyte' # needed for Ruby 1.9.1 4 | require 'stringio' 5 | require 'erb' 6 | 7 | $query_count = 0 8 | $query_sql = [] 9 | 10 | ignore_sql = / 11 | ^( 12 | PRAGMA | SHOW\ max_identifier_length | 13 | SELECT\ (currval|CAST|@@IDENTITY|@@ROWCOUNT) | 14 | SHOW\ ((FULL\ )?FIELDS|TABLES) 15 | )\b | 16 | \bFROM\ (sqlite_master|pg_tables|pg_attribute)\b 17 | /x 18 | 19 | ActiveSupport::Notifications.subscribe(/^sql\./) do |*args| 20 | payload = args.last 21 | unless payload[:name] =~ /^Fixture/ or payload[:sql] =~ ignore_sql 22 | $query_count += 1 23 | $query_sql << payload[:sql] 24 | end 25 | end 26 | 27 | module ActiverecordTestConnector 28 | extend self 29 | 30 | attr_accessor :able_to_connect 31 | attr_accessor :connected 32 | 33 | FIXTURES_PATH = File.expand_path('../../fixtures', __FILE__) 34 | 35 | Fixtures = defined?(ActiveRecord::FixtureSet) ? ActiveRecord::FixtureSet : 36 | defined?(ActiveRecord::Fixtures) ? ActiveRecord::Fixtures : 37 | ::Fixtures 38 | 39 | # Set our defaults 40 | self.connected = false 41 | self.able_to_connect = true 42 | 43 | def setup 44 | unless self.connected || !self.able_to_connect 45 | setup_connection 46 | load_schema 47 | add_load_path FIXTURES_PATH 48 | self.connected = true 49 | end 50 | rescue Exception => e # errors from ActiveRecord setup 51 | $stderr.puts "\nSkipping ActiveRecord tests: #{e}\n\n" 52 | self.able_to_connect = false 53 | end 54 | 55 | private 56 | 57 | def add_load_path(path) 58 | dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies 59 | dep.autoload_paths.unshift path 60 | end 61 | 62 | def setup_connection 63 | db = ENV['DB'].blank?? 'sqlite3' : ENV['DB'] 64 | 65 | erb = ERB.new(File.read(File.expand_path('../../database.yml', __FILE__))) 66 | configurations = YAML.load(erb.result) 67 | raise "no configuration for '#{db}'" unless configurations.key? db 68 | configuration = configurations[db] 69 | 70 | # ActiveRecord::Base.logger = Logger.new(STDOUT) if $0 == 'irb' 71 | puts "using #{configuration['adapter']} adapter" 72 | 73 | ActiveRecord::Base.configurations = { db => configuration } 74 | ActiveRecord::Base.establish_connection(db.to_sym) 75 | ActiveRecord::Base.default_timezone = :utc 76 | end 77 | 78 | def load_schema 79 | begin 80 | $stdout = StringIO.new 81 | ActiveRecord::Migration.verbose = false 82 | load File.join(FIXTURES_PATH, 'schema.rb') 83 | ensure 84 | $stdout = STDOUT 85 | end 86 | end 87 | 88 | module FixtureSetup 89 | def fixtures(*tables) 90 | table_names = tables.map { |t| t.to_s } 91 | 92 | fixtures = Fixtures.create_fixtures ActiverecordTestConnector::FIXTURES_PATH, table_names 93 | @@loaded_fixtures = {} 94 | @@fixture_cache = {} 95 | 96 | unless fixtures.nil? 97 | if fixtures.instance_of?(Fixtures) 98 | @@loaded_fixtures[fixtures.table_name] = fixtures 99 | else 100 | fixtures.each { |f| @@loaded_fixtures[f.table_name] = f } 101 | end 102 | end 103 | 104 | table_names.each do |table_name| 105 | define_method(table_name) do |*fixtures| 106 | @@fixture_cache[table_name] ||= {} 107 | 108 | instances = fixtures.map do |fixture| 109 | if @@loaded_fixtures[table_name][fixture.to_s] 110 | @@fixture_cache[table_name][fixture] ||= @@loaded_fixtures[table_name][fixture.to_s].find 111 | else 112 | raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'" 113 | end 114 | end 115 | 116 | instances.size == 1 ? instances.first : instances 117 | end 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/collection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate/array' 2 | require 'spec_helper' 3 | 4 | describe WillPaginate::Collection do 5 | 6 | before :all do 7 | @simple = ('a'..'e').to_a 8 | end 9 | 10 | it "should be a subset of original collection" do 11 | @simple.paginate(:page => 1, :per_page => 3).should == %w( a b c ) 12 | end 13 | 14 | it "can be shorter than per_page if on last page" do 15 | @simple.paginate(:page => 2, :per_page => 3).should == %w( d e ) 16 | end 17 | 18 | it "should include whole collection if per_page permits" do 19 | @simple.paginate(:page => 1, :per_page => 5).should == @simple 20 | end 21 | 22 | it "should be empty if out of bounds" do 23 | @simple.paginate(:page => 2, :per_page => 5).should be_empty 24 | end 25 | 26 | it "should default to 1 as current page and 30 per-page" do 27 | result = (1..50).to_a.paginate 28 | result.current_page.should == 1 29 | result.size.should == 30 30 | end 31 | 32 | it "should give total_entries precedence over actual size" do 33 | %w(a b c).paginate(:total_entries => 5).total_entries.should == 5 34 | end 35 | 36 | it "should be an augmented Array" do 37 | entries = %w(a b c) 38 | collection = create(2, 3, 10) do |pager| 39 | pager.replace(entries).should == entries 40 | end 41 | 42 | collection.should == entries 43 | for method in %w(total_pages each offset size current_page per_page total_entries) 44 | collection.should respond_to(method) 45 | end 46 | collection.should be_kind_of(Array) 47 | collection.entries.should be_instance_of(Array) 48 | # TODO: move to another expectation: 49 | collection.offset.should == 3 50 | collection.total_pages.should == 4 51 | collection.should_not be_out_of_bounds 52 | end 53 | 54 | describe "previous/next pages" do 55 | it "should have previous_page nil when on first page" do 56 | collection = create(1, 1, 3) 57 | collection.previous_page.should be_nil 58 | collection.next_page.should == 2 59 | end 60 | 61 | it "should have both prev/next pages" do 62 | collection = create(2, 1, 3) 63 | collection.previous_page.should == 1 64 | collection.next_page.should == 3 65 | end 66 | 67 | it "should have next_page nil when on last page" do 68 | collection = create(3, 1, 3) 69 | collection.previous_page.should == 2 70 | collection.next_page.should be_nil 71 | end 72 | end 73 | 74 | describe "out of bounds" do 75 | it "is out of bounds when page number is too high" do 76 | create(2, 3, 2).should be_out_of_bounds 77 | end 78 | 79 | it "isn't out of bounds when inside collection" do 80 | create(1, 3, 2).should_not be_out_of_bounds 81 | end 82 | 83 | it "isn't out of bounds when the collection is empty" do 84 | collection = create(1, 3, 0) 85 | collection.should_not be_out_of_bounds 86 | collection.total_pages.should == 1 87 | end 88 | end 89 | 90 | describe "guessing total count" do 91 | it "can guess when collection is shorter than limit" do 92 | collection = create { |p| p.replace array } 93 | collection.total_entries.should == 8 94 | end 95 | 96 | it "should allow explicit total count to override guessed" do 97 | collection = create(2, 5, 10) { |p| p.replace array } 98 | collection.total_entries.should == 10 99 | end 100 | 101 | it "should not be able to guess when collection is same as limit" do 102 | collection = create { |p| p.replace array(5) } 103 | collection.total_entries.should be_nil 104 | end 105 | 106 | it "should not be able to guess when collection is empty" do 107 | collection = create { |p| p.replace array(0) } 108 | collection.total_entries.should be_nil 109 | end 110 | 111 | it "should be able to guess when collection is empty and this is the first page" do 112 | collection = create(1) { |p| p.replace array(0) } 113 | collection.total_entries.should == 0 114 | end 115 | end 116 | 117 | it "should not respond to page_count anymore" do 118 | Proc.new { create.page_count }.should raise_error(NoMethodError) 119 | end 120 | 121 | it "inherits per_page from global value" do 122 | collection = described_class.new(1) 123 | collection.per_page.should == 30 124 | end 125 | 126 | private 127 | 128 | def create(page = 2, limit = 5, total = nil, &block) 129 | if block_given? 130 | described_class.create(page, limit, total, &block) 131 | else 132 | described_class.new(page, limit, total) 133 | end 134 | end 135 | 136 | def array(size = 3) 137 | Array.new(size) 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/will_paginate/view_helpers/link_renderer.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | require 'will_paginate/core_ext' 3 | require 'will_paginate/view_helpers' 4 | require 'will_paginate/view_helpers/link_renderer_base' 5 | 6 | module WillPaginate 7 | module ViewHelpers 8 | # This class does the heavy lifting of actually building the pagination 9 | # links. It is used by +will_paginate+ helper internally. 10 | class LinkRenderer < LinkRendererBase 11 | 12 | # * +collection+ is a WillPaginate::Collection instance or any other object 13 | # that conforms to that API 14 | # * +options+ are forwarded from +will_paginate+ view helper 15 | # * +template+ is the reference to the template being rendered 16 | def prepare(collection, options, template) 17 | super(collection, options) 18 | @template = template 19 | @container_attributes = @base_url_params = nil 20 | end 21 | 22 | # Process it! This method returns the complete HTML string which contains 23 | # pagination links. Feel free to subclass LinkRenderer and change this 24 | # method as you see fit. 25 | def to_html 26 | html = pagination.map do |item| 27 | item.is_a?(Fixnum) ? 28 | page_number(item) : 29 | send(item) 30 | end.join(@options[:link_separator]) 31 | 32 | @options[:container] ? html_container(html) : html 33 | end 34 | 35 | # Returns the subset of +options+ this instance was initialized with that 36 | # represent HTML attributes for the container element of pagination links. 37 | def container_attributes 38 | @container_attributes ||= @options.except(*(ViewHelpers.pagination_options.keys + [:renderer] - [:class])) 39 | end 40 | 41 | protected 42 | 43 | def page_number(page) 44 | unless page == current_page 45 | link(page, page, :rel => rel_value(page)) 46 | else 47 | tag(:em, page, :class => 'current'.freeze) 48 | end 49 | end 50 | 51 | def gap 52 | text = @template.will_paginate_translate(:page_gap) { '…'.freeze } 53 | %(#{text}) 54 | end 55 | 56 | def previous_page 57 | num = @collection.current_page > 1 && @collection.current_page - 1 58 | previous_or_next_page(num, @options[:previous_label], 'previous_page'.freeze) 59 | end 60 | 61 | def next_page 62 | num = @collection.current_page < total_pages && @collection.current_page + 1 63 | previous_or_next_page(num, @options[:next_label], 'next_page'.freeze) 64 | end 65 | 66 | def previous_or_next_page(page, text, classname) 67 | if page 68 | link(text, page, :class => classname) 69 | else 70 | tag(:span, text, :class => classname + ' disabled'.freeze) 71 | end 72 | end 73 | 74 | def html_container(html) 75 | tag(:div, html, container_attributes) 76 | end 77 | 78 | # Returns URL params for +page_link_or_span+, taking the current GET params 79 | # and :params option into account. 80 | def url(page) 81 | raise NotImplementedError 82 | end 83 | 84 | private 85 | 86 | def param_name 87 | @options[:param_name].to_s 88 | end 89 | 90 | def link(text, target, attributes = {}) 91 | if target.is_a? Fixnum 92 | attributes[:rel] = rel_value(target) 93 | target = url(target) 94 | end 95 | attributes[:href] = target 96 | tag(:a, text, attributes) 97 | end 98 | 99 | def tag(name, value, attributes = {}) 100 | string_attributes = attributes.inject('') do |attrs, pair| 101 | unless pair.last.nil? 102 | attrs << %( #{pair.first}="#{CGI::escapeHTML(pair.last.to_s)}") 103 | end 104 | attrs 105 | end 106 | "<#{name}#{string_attributes}>#{value}" 107 | end 108 | 109 | def rel_value(page) 110 | case page 111 | when @collection.current_page - 1; 'prev'.freeze + (page == 1 ? ' start'.freeze : ''.freeze) 112 | when @collection.current_page + 1; 'next'.freeze 113 | when 1; 'start'.freeze 114 | end 115 | end 116 | 117 | def symbolized_update(target, other) 118 | other.each do |key, value| 119 | key = key.to_sym 120 | existing = target[key] 121 | 122 | if value.is_a?(Hash) and (existing.is_a?(Hash) or existing.nil?) 123 | symbolized_update(existing || (target[key] = {}), value) 124 | else 125 | if value.instance_variable_defined?(:@parameters) 126 | value = value.instance_variable_get(:@parameters) 127 | end 128 | target[key] = value 129 | end 130 | end 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /spec/finders/mongoid_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | begin 4 | require 'will_paginate/mongoid' 5 | rescue LoadError => error 6 | warn "Error running Sequel specs: #{error.message}" 7 | mongoid_loaded = false 8 | else 9 | Mongoid.database = Mongo::Connection.new.db('will_paginate_test') 10 | 11 | class MongoidModel 12 | include Mongoid::Document 13 | end 14 | 15 | mongoid_loaded = true 16 | end 17 | 18 | describe WillPaginate::Mongoid do 19 | before(:all) do 20 | MongoidModel.delete_all 21 | 4.times { MongoidModel.create! } 22 | end 23 | 24 | let(:criteria) { MongoidModel.criteria } 25 | 26 | describe "#page" do 27 | it "should forward to the paginate method" do 28 | criteria.expects(:paginate).with(:page => 2).returns("itself") 29 | criteria.page(2).should == "itself" 30 | end 31 | 32 | it "should not override per_page if set earlier in the chain" do 33 | criteria.paginate(:per_page => 10).page(1).per_page.should == 10 34 | criteria.paginate(:per_page => 20).page(1).per_page.should == 20 35 | end 36 | end 37 | 38 | describe "#per_page" do 39 | it "should set the limit if given an argument" do 40 | criteria.per_page(10).options[:limit].should == 10 41 | end 42 | 43 | it "should return the current limit if no argument is given" do 44 | criteria.per_page.should == nil 45 | criteria.per_page(10).per_page.should == 10 46 | end 47 | 48 | it "should be interchangable with limit" do 49 | criteria.limit(15).per_page.should == 15 50 | end 51 | 52 | it "should be nil'able" do 53 | criteria.per_page(nil).per_page.should be_nil 54 | end 55 | end 56 | 57 | describe "#paginate" do 58 | it "should use criteria" do 59 | criteria.paginate.should be_instance_of(::Mongoid::Criteria) 60 | end 61 | 62 | it "should not override page number if set earlier in the chain" do 63 | criteria.page(3).paginate.current_page.should == 3 64 | end 65 | 66 | it "should limit according to per_page parameter" do 67 | criteria.paginate(:per_page => 10).options.should include(:limit => 10) 68 | end 69 | 70 | it "should skip according to page and per_page parameters" do 71 | criteria.paginate(:page => 2, :per_page => 5).options.should include(:skip => 5) 72 | end 73 | 74 | specify "first fallback value for per_page option is the current limit" do 75 | criteria.limit(12).paginate.options.should include(:limit => 12) 76 | end 77 | 78 | specify "second fallback value for per_page option is WillPaginate.per_page" do 79 | criteria.paginate.options.should include(:limit => WillPaginate.per_page) 80 | end 81 | 82 | specify "page should default to 1" do 83 | criteria.paginate.options.should include(:skip => 0) 84 | end 85 | 86 | it "should convert strings to integers" do 87 | criteria.paginate(:page => "2", :per_page => "3").options.should include(:limit => 3, :limit => 3) 88 | end 89 | 90 | describe "collection compatibility" do 91 | describe "#total_count" do 92 | it "should be calculated correctly" do 93 | criteria.paginate(:per_page => 1).total_entries.should == 4 94 | criteria.paginate(:per_page => 3).total_entries.should == 4 95 | end 96 | 97 | it "should be cached" do 98 | criteria.expects(:count).once.returns(123) 99 | criteria.paginate 100 | 2.times { criteria.total_entries.should == 123 } 101 | end 102 | end 103 | 104 | it "should calculate total_pages" do 105 | criteria.paginate(:per_page => 1).total_pages.should == 4 106 | criteria.paginate(:per_page => 3).total_pages.should == 2 107 | criteria.paginate(:per_page => 10).total_pages.should == 1 108 | end 109 | 110 | it "should return per_page" do 111 | criteria.paginate(:per_page => 1).per_page.should == 1 112 | criteria.paginate(:per_page => 5).per_page.should == 5 113 | end 114 | 115 | describe "#current_page" do 116 | it "should return current_page" do 117 | criteria.paginate(:page => 1).current_page.should == 1 118 | criteria.paginate(:page => 3).current_page.should == 3 119 | end 120 | 121 | it "should be casted to PageNumber" do 122 | criteria.paginate(:page => 1).current_page.should be_instance_of(WillPaginate::PageNumber) 123 | end 124 | end 125 | 126 | it "should return offset" do 127 | criteria.paginate(:page => 1).offset.should == 0 128 | criteria.paginate(:page => 2, :per_page => 5).offset.should == 5 129 | criteria.paginate(:page => 3, :per_page => 10).offset.should == 20 130 | end 131 | 132 | it "should not pollute plain mongoid criterias" do 133 | %w(total_entries total_pages current_page).each do |method| 134 | criteria.should_not respond_to(method) 135 | end 136 | criteria.offset.should be_nil # this is already a criteria method 137 | end 138 | end 139 | end 140 | end if mongoid_loaded 141 | -------------------------------------------------------------------------------- /lib/will_paginate/view_helpers/action_view.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate/view_helpers' 2 | require 'will_paginate/view_helpers/link_renderer' 3 | 4 | module WillPaginate 5 | # = ActionView helpers 6 | # 7 | # This module serves for availability in ActionView templates. It also adds a new 8 | # view helper: +paginated_section+. 9 | # 10 | # == Using the helper without arguments 11 | # If the helper is called without passing in the collection object, it will 12 | # try to read from the instance variable inferred by the controller name. 13 | # For example, calling +will_paginate+ while the current controller is 14 | # PostsController will result in trying to read from the @posts 15 | # variable. Example: 16 | # 17 | # <%= will_paginate :id => true %> 18 | # 19 | # ... will result in @post collection getting paginated: 20 | # 21 | # 22 | # 23 | module ActionView 24 | include ViewHelpers 25 | 26 | def will_paginate(collection = nil, options = {}) #:nodoc: 27 | options, collection = collection, nil if collection.is_a? Hash 28 | collection ||= infer_collection_from_controller 29 | 30 | options = options.symbolize_keys 31 | options[:renderer] ||= LinkRenderer 32 | 33 | super(collection, options) 34 | end 35 | 36 | def page_entries_info(collection = nil, options = {}) #:nodoc: 37 | options, collection = collection, nil if collection.is_a? Hash 38 | collection ||= infer_collection_from_controller 39 | 40 | super(collection, options.symbolize_keys) 41 | end 42 | 43 | # Wrapper for rendering pagination links at both top and bottom of a block 44 | # of content. 45 | # 46 | # <% paginated_section @posts do %> 47 | #
    48 | # <% for post in @posts %> 49 | #
  1. ...
  2. 50 | # <% end %> 51 | #
52 | # <% end %> 53 | # 54 | # will result in: 55 | # 56 | # 57 | #
    58 | # ... 59 | #
60 | # 61 | # 62 | # Arguments are passed to a will_paginate call, so the same options 63 | # apply. Don't use the :id option; otherwise you'll finish with two 64 | # blocks of pagination links sharing the same ID (which is invalid HTML). 65 | def paginated_section(*args, &block) 66 | pagination = will_paginate(*args) 67 | if pagination 68 | pagination + capture(&block) + pagination 69 | else 70 | capture(&block) 71 | end 72 | end 73 | 74 | def will_paginate_translate(keys, options = {}) 75 | if respond_to? :translate 76 | if Array === keys 77 | defaults = keys.dup 78 | key = defaults.shift 79 | else 80 | defaults = nil 81 | key = keys 82 | end 83 | translate(key, options.merge(:default => defaults, :scope => :will_paginate)) 84 | else 85 | super 86 | end 87 | end 88 | 89 | protected 90 | 91 | def infer_collection_from_controller 92 | collection_name = "@#{controller.controller_name}".freeze 93 | collection = instance_variable_get(collection_name) 94 | raise ArgumentError, "The #{collection_name} variable appears to be empty. Did you " + 95 | "forget to pass the collection object for will_paginate?" if collection.nil? 96 | collection 97 | end 98 | 99 | class LinkRenderer < ViewHelpers::LinkRenderer 100 | protected 101 | 102 | def default_url_params 103 | {} 104 | end 105 | 106 | def url(page) 107 | @base_url_params ||= begin 108 | url_params = merge_get_params(default_url_params) 109 | url_params[:only_path] = true 110 | merge_optional_params(url_params) 111 | end 112 | 113 | url_params = @base_url_params.dup 114 | add_current_page_param(url_params, page) 115 | 116 | @template.url_for(url_params) 117 | end 118 | 119 | def merge_get_params(url_params) 120 | if @template.respond_to? :request and @template.request and @template.request.get? 121 | symbolized_update(url_params, @template.params) 122 | end 123 | url_params 124 | end 125 | 126 | def merge_optional_params(url_params) 127 | symbolized_update(url_params, @options[:params]) if @options[:params] 128 | url_params 129 | end 130 | 131 | def add_current_page_param(url_params, page) 132 | unless param_name.index(/[^\w-]/) 133 | url_params[param_name.to_sym] = page 134 | else 135 | page_param = parse_query_parameters("#{param_name}=#{page}") 136 | symbolized_update(url_params, page_param) 137 | end 138 | end 139 | 140 | private 141 | 142 | def parse_query_parameters(params) 143 | Rack::Utils.parse_nested_query(params) 144 | end 145 | end 146 | 147 | ::ActionView::Base.send :include, self 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /spec/view_helpers/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'will_paginate/view_helpers' 3 | require 'will_paginate/array' 4 | require 'active_support' 5 | require 'active_support/core_ext/string/inflections' 6 | require 'active_support/inflections' 7 | 8 | describe WillPaginate::ViewHelpers do 9 | 10 | before(:all) do 11 | # make sure default translations aren't loaded 12 | I18n.load_path.clear 13 | end 14 | 15 | before(:each) do 16 | I18n.reload! 17 | end 18 | 19 | include WillPaginate::ViewHelpers 20 | 21 | describe "will_paginate" do 22 | it "should render" do 23 | collection = WillPaginate::Collection.new(1, 2, 4) 24 | renderer = mock 'Renderer' 25 | renderer.expects(:prepare).with(collection, instance_of(Hash), self) 26 | renderer.expects(:to_html).returns('') 27 | 28 | will_paginate(collection, :renderer => renderer).should == '' 29 | end 30 | 31 | it "should return nil for single-page collections" do 32 | collection = mock 'Collection', :total_pages => 1 33 | will_paginate(collection).should be_nil 34 | end 35 | 36 | it "should call html_safe on result" do 37 | collection = WillPaginate::Collection.new(1, 2, 4) 38 | 39 | html = mock 'HTML' 40 | html.expects(:html_safe).returns(html) 41 | renderer = mock 'Renderer' 42 | renderer.stubs(:prepare) 43 | renderer.expects(:to_html).returns(html) 44 | 45 | will_paginate(collection, :renderer => renderer).should eql(html) 46 | end 47 | end 48 | 49 | describe "pagination_options" do 50 | let(:pagination_options) { WillPaginate::ViewHelpers.pagination_options } 51 | 52 | it "deprecates setting :renderer" do 53 | begin 54 | lambda { 55 | pagination_options[:renderer] = 'test' 56 | }.should have_deprecation("pagination_options[:renderer] shouldn't be set") 57 | ensure 58 | pagination_options.delete :renderer 59 | end 60 | end 61 | end 62 | 63 | describe "page_entries_info" do 64 | before :all do 65 | @array = ('a'..'z').to_a 66 | end 67 | 68 | def info(params, options = {}) 69 | collection = Hash === params ? @array.paginate(params) : params 70 | page_entries_info collection, {:html => false}.merge(options) 71 | end 72 | 73 | it "should display middle results and total count" do 74 | info(:page => 2, :per_page => 5).should == "Displaying strings 6 - 10 of 26 in total" 75 | end 76 | 77 | it "uses translation if available" do 78 | translation :will_paginate => { 79 | :page_entries_info => {:multi_page => 'Showing %{from} - %{to}'} 80 | } 81 | info(:page => 2, :per_page => 5).should == "Showing 6 - 10" 82 | end 83 | 84 | it "uses specific translation if available" do 85 | translation :will_paginate => { 86 | :page_entries_info => {:multi_page => 'Showing %{from} - %{to}'}, 87 | :string => { :page_entries_info => {:multi_page => 'Strings %{from} to %{to}'} } 88 | } 89 | info(:page => 2, :per_page => 5).should == "Strings 6 to 10" 90 | end 91 | 92 | it "should output HTML by default" do 93 | info({ :page => 2, :per_page => 5 }, :html => true).should == 94 | "Displaying strings 6 - 10 of 26 in total" 95 | end 96 | 97 | it "should display shortened end results" do 98 | info(:page => 7, :per_page => 4).should include_phrase('strings 25 - 26') 99 | end 100 | 101 | it "should handle longer class names" do 102 | collection = @array.paginate(:page => 2, :per_page => 5) 103 | model = stub('Class', :name => 'ProjectType', :to_s => 'ProjectType') 104 | collection.first.stubs(:class).returns(model) 105 | info(collection).should include_phrase('project types') 106 | end 107 | 108 | it "should adjust output for single-page collections" do 109 | info(('a'..'d').to_a.paginate(:page => 1, :per_page => 5)).should == "Displaying all 4 strings" 110 | info(['a'].paginate(:page => 1, :per_page => 5)).should == "Displaying 1 string" 111 | end 112 | 113 | it "should display 'no entries found' for empty collections" do 114 | info([].paginate(:page => 1, :per_page => 5)).should == "No entries found" 115 | end 116 | 117 | it "uses model_name.human when available" do 118 | name = stub('model name', :i18n_key => :flower_key) 119 | name.expects(:human).with(:count => 1).returns('flower') 120 | model = stub('Class', :model_name => name) 121 | collection = [1].paginate(:page => 1) 122 | 123 | info(collection, :model => model).should == "Displaying 1 flower" 124 | end 125 | 126 | it "uses custom translation instead of model_name.human" do 127 | name = stub('model name', :i18n_key => :flower_key) 128 | name.expects(:human).never 129 | model = stub('Class', :model_name => name) 130 | translation :will_paginate => {:models => {:flower_key => 'tulip'}} 131 | collection = [1].paginate(:page => 1) 132 | 133 | info(collection, :model => model).should == "Displaying 1 tulip" 134 | end 135 | 136 | private 137 | 138 | def translation(data) 139 | I18n.backend.store_translations(:en, data) 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/will_paginate/collection.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate/per_page' 2 | require 'will_paginate/page_number' 3 | 4 | module WillPaginate 5 | # Any will_paginate-compatible collection should have these methods: 6 | # 7 | # current_page, per_page, offset, total_entries, total_pages 8 | # 9 | # It can also define some of these optional methods: 10 | # 11 | # out_of_bounds?, previous_page, next_page 12 | # 13 | # This module provides few of these methods. 14 | module CollectionMethods 15 | def total_pages 16 | total_entries.zero? ? 1 : (total_entries / per_page.to_f).ceil 17 | end 18 | 19 | # current_page - 1 or nil if there is no previous page 20 | def previous_page 21 | current_page > 1 ? (current_page - 1) : nil 22 | end 23 | 24 | # current_page + 1 or nil if there is no next page 25 | def next_page 26 | current_page < total_pages ? (current_page + 1) : nil 27 | end 28 | 29 | # Helper method that is true when someone tries to fetch a page with a 30 | # larger number than the last page. Can be used in combination with flashes 31 | # and redirecting. 32 | def out_of_bounds? 33 | current_page > total_pages 34 | end 35 | end 36 | 37 | # = The key to pagination 38 | # Arrays returned from paginating finds are, in fact, instances of this little 39 | # class. You may think of WillPaginate::Collection as an ordinary array with 40 | # some extra properties. Those properties are used by view helpers to generate 41 | # correct page links. 42 | # 43 | # WillPaginate::Collection also assists in rolling out your own pagination 44 | # solutions: see +create+. 45 | # 46 | # If you are writing a library that provides a collection which you would like 47 | # to conform to this API, you don't have to copy these methods over; simply 48 | # make your plugin/gem dependant on this library and do: 49 | # 50 | # require 'will_paginate/collection' 51 | # # WillPaginate::Collection is now available for use 52 | class Collection < Array 53 | include CollectionMethods 54 | 55 | attr_reader :current_page, :per_page, :total_entries 56 | 57 | # Arguments to the constructor are the current page number, per-page limit 58 | # and the total number of entries. The last argument is optional because it 59 | # is best to do lazy counting; in other words, count *conditionally* after 60 | # populating the collection using the +replace+ method. 61 | def initialize(page, per_page = WillPaginate.per_page, total = nil) 62 | @current_page = WillPaginate::PageNumber(page) 63 | @per_page = per_page.to_i 64 | self.total_entries = total if total 65 | end 66 | 67 | # Just like +new+, but yields the object after instantiation and returns it 68 | # afterwards. This is very useful for manual pagination: 69 | # 70 | # @entries = WillPaginate::Collection.create(1, 10) do |pager| 71 | # result = Post.find(:all, :limit => pager.per_page, :offset => pager.offset) 72 | # # inject the result array into the paginated collection: 73 | # pager.replace(result) 74 | # 75 | # unless pager.total_entries 76 | # # the pager didn't manage to guess the total count, do it manually 77 | # pager.total_entries = Post.count 78 | # end 79 | # end 80 | # 81 | # The possibilities with this are endless. For another example, here is how 82 | # WillPaginate used to define pagination for Array instances: 83 | # 84 | # Array.class_eval do 85 | # def paginate(page = 1, per_page = 15) 86 | # WillPaginate::Collection.create(page, per_page, size) do |pager| 87 | # pager.replace self[pager.offset, pager.per_page].to_a 88 | # end 89 | # end 90 | # end 91 | # 92 | # The Array#paginate API has since then changed, but this still serves as a 93 | # fine example of WillPaginate::Collection usage. 94 | def self.create(page, per_page, total = nil) 95 | pager = new(page, per_page, total) 96 | yield pager 97 | pager 98 | end 99 | 100 | # Current offset of the paginated collection. If we're on the first page, 101 | # it is always 0. If we're on the 2nd page and there are 30 entries per page, 102 | # the offset is 30. This property is useful if you want to render ordinals 103 | # side by side with records in the view: simply start with offset + 1. 104 | def offset 105 | current_page.to_offset(per_page).to_i 106 | end 107 | 108 | def total_entries=(number) 109 | @total_entries = number.to_i 110 | end 111 | 112 | # This is a magic wrapper for the original Array#replace method. It serves 113 | # for populating the paginated collection after initialization. 114 | # 115 | # Why magic? Because it tries to guess the total number of entries judging 116 | # by the size of given array. If it is shorter than +per_page+ limit, then we 117 | # know we're on the last page. This trick is very useful for avoiding 118 | # unnecessary hits to the database to do the counting after we fetched the 119 | # data for the current page. 120 | # 121 | # However, after using +replace+ you should always test the value of 122 | # +total_entries+ and set it to a proper value if it's +nil+. See the example 123 | # in +create+. 124 | def replace(array) 125 | result = super 126 | 127 | # The collection is shorter then page limit? Rejoice, because 128 | # then we know that we are on the last page! 129 | if total_entries.nil? and length < per_page and (current_page == 1 or length > 0) 130 | self.total_entries = offset + length 131 | end 132 | 133 | result 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/will_paginate/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate/per_page' 2 | require 'will_paginate/page_number' 3 | require 'will_paginate/collection' 4 | require 'active_record' 5 | 6 | module WillPaginate 7 | # = Paginating finders for ActiveRecord models 8 | # 9 | # WillPaginate adds +paginate+, +per_page+ and other methods to 10 | # ActiveRecord::Base class methods and associations. 11 | # 12 | # In short, paginating finders are equivalent to ActiveRecord finders; the 13 | # only difference is that we start with "paginate" instead of "find" and 14 | # that :page is required parameter: 15 | # 16 | # @posts = Post.paginate :all, :page => params[:page], :order => 'created_at DESC' 17 | # 18 | module ActiveRecord 19 | # makes a Relation look like WillPaginate::Collection 20 | module RelationMethods 21 | include WillPaginate::CollectionMethods 22 | 23 | attr_accessor :current_page 24 | attr_writer :total_entries, :wp_count_options 25 | 26 | def per_page(value = nil) 27 | if value.nil? then limit_value 28 | else limit(value) 29 | end 30 | end 31 | 32 | # TODO: solve with less relation clones and code dups 33 | def limit(num) 34 | rel = super 35 | if rel.current_page 36 | rel.offset rel.current_page.to_offset(rel.limit_value).to_i 37 | else 38 | rel 39 | end 40 | end 41 | 42 | # dirty hack to enable `first` after `limit` behavior above 43 | def first(*args) 44 | if current_page 45 | rel = clone 46 | rel.current_page = nil 47 | rel.first(*args) 48 | else 49 | super 50 | end 51 | end 52 | 53 | # fix for Rails 3.0 54 | def find_last(*args) 55 | if !loaded? && args.empty? && (offset_value || limit_value) 56 | @last ||= to_a.last 57 | else 58 | super 59 | end 60 | end 61 | 62 | def offset(value = nil) 63 | if value.nil? then offset_value 64 | else super(value) 65 | end 66 | end 67 | 68 | def total_entries 69 | @total_entries ||= begin 70 | if loaded? and size < limit_value and (current_page == 1 or size > 0) 71 | offset_value + size 72 | else 73 | @total_entries_queried = true 74 | result = count 75 | result = result.size if result.respond_to?(:size) and !result.is_a?(Integer) 76 | result 77 | end 78 | end 79 | end 80 | 81 | def count(*args) 82 | if limit_value 83 | excluded = [:order, :limit, :offset, :reorder] 84 | excluded << :includes unless eager_loading? 85 | rel = self.except(*excluded) 86 | # TODO: hack. decide whether to keep 87 | rel = rel.apply_finder_options(@wp_count_options) if defined? @wp_count_options 88 | 89 | column_name = (select_for_count(rel) || :all) 90 | rel.count(column_name) 91 | else 92 | super(*args) 93 | end 94 | end 95 | 96 | # workaround for Active Record 3.0 97 | def size 98 | if !loaded? and limit_value and group_values.empty? 99 | [super, limit_value].min 100 | else 101 | super 102 | end 103 | end 104 | 105 | # overloaded to be pagination-aware 106 | def empty? 107 | if !loaded? and offset_value 108 | result = count 109 | result = result.size if result.respond_to?(:size) and !result.is_a?(Integer) 110 | result <= offset_value 111 | else 112 | super 113 | end 114 | end 115 | 116 | def clone 117 | copy_will_paginate_data super 118 | end 119 | 120 | # workaround for Active Record 3.0 121 | def scoped(options = nil) 122 | copy_will_paginate_data super 123 | end 124 | 125 | def to_a 126 | if current_page.nil? then super # workaround for Active Record 3.0 127 | else 128 | ::WillPaginate::Collection.create(current_page, limit_value) do |col| 129 | col.replace super 130 | col.total_entries ||= total_entries 131 | end 132 | end 133 | end 134 | 135 | private 136 | 137 | def copy_will_paginate_data(other) 138 | other.current_page = current_page unless other.current_page 139 | other.total_entries = nil if defined? @total_entries_queried 140 | other.wp_count_options = @wp_count_options if defined? @wp_count_options 141 | other 142 | end 143 | 144 | def select_for_count(rel) 145 | if rel.select_values.present? 146 | select = rel.select_values.join(", ".freeze) 147 | select if select !~ /[,*]/ 148 | end 149 | end 150 | end 151 | 152 | module Pagination 153 | def paginate(options) 154 | options = options.dup 155 | pagenum = options.fetch(:page) { raise ArgumentError, ":page parameter required" } 156 | per_page = options.delete(:per_page) || self.per_page 157 | total = options.delete(:total_entries) 158 | 159 | count_options = options.delete(:count) 160 | options.delete(:page) 161 | 162 | rel = limit(per_page.to_i).page(pagenum) 163 | rel = rel.apply_finder_options(options) if options.any? 164 | rel.wp_count_options = count_options if count_options 165 | rel.total_entries = total.to_i unless total.blank? 166 | rel 167 | end 168 | 169 | def page(num) 170 | rel = if ::ActiveRecord::Relation === self 171 | self 172 | else 173 | all 174 | end 175 | 176 | rel = rel.extending(RelationMethods) 177 | pagenum = ::WillPaginate::PageNumber(num.nil? ? 1 : num) 178 | per_page = rel.limit_value || self.per_page 179 | rel = rel.offset(pagenum.to_offset(per_page).to_i) 180 | rel = rel.limit(per_page) unless rel.limit_value 181 | rel.current_page = pagenum 182 | rel 183 | end 184 | end 185 | 186 | module BaseMethods 187 | # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string 188 | # based on the params otherwise used by paginating finds: +page+ and 189 | # +per_page+. 190 | # 191 | # Example: 192 | # 193 | # @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000], 194 | # :page => params[:page], :per_page => 3 195 | # 196 | # A query for counting rows will automatically be generated if you don't 197 | # supply :total_entries. If you experience problems with this 198 | # generated SQL, you might want to perform the count manually in your 199 | # application. 200 | # 201 | def paginate_by_sql(sql, options) 202 | pagenum = options.fetch(:page) { raise ArgumentError, ":page parameter required" } || 1 203 | per_page = options[:per_page] || self.per_page 204 | total = options[:total_entries] 205 | 206 | WillPaginate::Collection.create(pagenum, per_page, total) do |pager| 207 | query = sanitize_sql(sql.dup) 208 | original_query = query.dup 209 | 210 | # add limit, offset 211 | query << " LIMIT #{pager.per_page} OFFSET #{pager.offset}".freeze 212 | 213 | # perfom the find 214 | pager.replace find_by_sql(query) 215 | 216 | unless pager.total_entries 217 | count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s.]+$/mi, ''.freeze 218 | count_query = "SELECT COUNT(*) FROM (#{count_query})".freeze 219 | count_query << ' AS count_table'.freeze 220 | # perform the count query 221 | pager.total_entries = count_by_sql(count_query) 222 | end 223 | end 224 | end 225 | end 226 | 227 | # mix everything into Active Record 228 | ::ActiveRecord::Base.extend PerPage 229 | ::ActiveRecord::Base.extend Pagination 230 | ::ActiveRecord::Base.extend BaseMethods 231 | 232 | klasses = [::ActiveRecord::Relation] 233 | if defined? ::ActiveRecord::Associations::CollectionProxy 234 | klasses << ::ActiveRecord::Associations::CollectionProxy 235 | else 236 | klasses << ::ActiveRecord::Associations::AssociationCollection 237 | end 238 | 239 | # support pagination on associations and scopes 240 | klasses.each { |klass| klass.send(:include, Pagination) } 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /lib/will_paginate/view_helpers.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'will_paginate/core_ext' 3 | require 'will_paginate/i18n' 4 | require 'will_paginate/deprecation' 5 | 6 | module WillPaginate 7 | # = Will Paginate view helpers 8 | # 9 | # The main view helper is +will_paginate+. It renders the pagination links 10 | # for the given collection. The helper itself is lightweight and serves only 11 | # as a wrapper around LinkRenderer instantiation; the renderer then does 12 | # all the hard work of generating the HTML. 13 | module ViewHelpers 14 | class << self 15 | # Write to this hash to override default options on the global level: 16 | # 17 | # WillPaginate::ViewHelpers.pagination_options[:page_links] = false 18 | # 19 | attr_accessor :pagination_options 20 | end 21 | 22 | # default view options 23 | self.pagination_options = Deprecation::Hash.new \ 24 | :class => 'pagination'.freeze, 25 | :previous_label => nil, 26 | :next_label => nil, 27 | :inner_window => 4, # links around the current page 28 | :outer_window => 1, # links around beginning and end 29 | :link_separator => ' '.freeze, # single space is friendly to spiders and non-graphic browsers 30 | :param_name => :page, 31 | :params => nil, 32 | :page_links => true, 33 | :container => true 34 | 35 | label_deprecation = Proc.new { |key, value| 36 | "set the 'will_paginate.#{key}' key in your i18n locale instead of editing pagination_options" if defined? Rails 37 | } 38 | pagination_options.deprecate_key(:previous_label, :next_label, &label_deprecation) 39 | pagination_options.deprecate_key(:renderer) { |key, _| "pagination_options[#{key.inspect}] shouldn't be set globally" } 40 | 41 | include WillPaginate::I18n 42 | 43 | # Returns HTML representing page links for a WillPaginate::Collection-like object. 44 | # In case there is no more than one page in total, nil is returned. 45 | # 46 | # ==== Options 47 | # * :class -- CSS class name for the generated DIV (default: "pagination") 48 | # * :previous_label -- default: "« Previous" 49 | # * :next_label -- default: "Next »" 50 | # * :inner_window -- how many links are shown around the current page (default: 4) 51 | # * :outer_window -- how many links are around the first and the last page (default: 1) 52 | # * :link_separator -- string separator for page HTML elements (default: single space) 53 | # * :param_name -- parameter name for page number in URLs (default: :page) 54 | # * :params -- additional parameters when generating pagination links 55 | # (eg. :controller => "foo", :action => nil) 56 | # * :renderer -- class name, class or instance of a link renderer (default in Rails: 57 | # WillPaginate::ActionView::LinkRenderer) 58 | # * :page_links -- when false, only previous/next links are rendered (default: true) 59 | # * :container -- toggles rendering of the DIV container for pagination links, set to 60 | # false only when you are rendering your own pagination markup (default: true) 61 | # 62 | # All options not recognized by will_paginate will become HTML attributes on the container 63 | # element for pagination links (the DIV). For example: 64 | # 65 | # <%= will_paginate @posts, :style => 'color:blue' %> 66 | # 67 | # will result in: 68 | # 69 | # 70 | # 71 | def will_paginate(collection, options = {}) 72 | # early exit if there is nothing to render 73 | return nil unless collection.total_pages > 1 74 | 75 | options = WillPaginate::ViewHelpers.pagination_options.merge(options) 76 | 77 | options[:previous_label] ||= 78 | will_paginate_translate(:previous_label) { '← Previous'.freeze } 79 | options[:next_label] ||= 80 | will_paginate_translate(:next_label) { 'Next →'.freeze } 81 | 82 | # get the renderer instance 83 | renderer = case options[:renderer] 84 | when nil 85 | raise ArgumentError, ":renderer not specified" 86 | when String 87 | klass = if options[:renderer].respond_to? :constantize then options[:renderer].constantize 88 | else Object.const_get(options[:renderer]) # poor man's constantize 89 | end 90 | klass.new 91 | when Class then options[:renderer].new 92 | else options[:renderer] 93 | end 94 | # render HTML for pagination 95 | renderer.prepare collection, options, self 96 | output = renderer.to_html 97 | output = output.html_safe if output.respond_to?(:html_safe) 98 | output 99 | end 100 | 101 | # Returns HTML representing link ref tags links for a 102 | # WillPaginate::Collection-like object. 103 | # 104 | # A port of something that is already in Kaminari: 105 | # https://github.com/amatsuda/kaminari/pull/200/files 106 | # 107 | # ==== Examples 108 | # Basic usage: 109 | # 110 | # In head: 111 | # 112 | # My Website 113 | # <%= yield :head %> 114 | # 115 | # 116 | # Somewhere in body: 117 | # <% content_for :head do %> 118 | # <%= pagination_link_tags @items %> 119 | # <% end %> 120 | # 121 | # #-> 122 | # 123 | # 124 | def pagination_link_tags(collection, params = {}) 125 | output = [] 126 | link = ''.freeze 127 | output << link % ['prev'.freeze, 128 | url_for(params.merge(page: collection.previous_page, only_path: false) 129 | )] if collection.previous_page 130 | output << link % ['next'.freeze, 131 | url_for(params.merge(page: collection.next_page, only_path: false)) 132 | ] if collection.next_page 133 | output.join('\n'.freeze).html_safe 134 | end 135 | 136 | # Renders a message containing number of displayed vs. total entries. 137 | # 138 | # <%= page_entries_info @posts %> 139 | # #-> Displaying posts 6 - 12 of 26 in total 140 | # 141 | # The default output contains HTML. Use ":html => false" for plain text. 142 | def page_entries_info(collection, options = {}) 143 | model = options[:model] 144 | model = collection.first.class unless model or collection.empty? 145 | model ||= 'entry'.freeze 146 | model_key = if model.respond_to? :model_name 147 | model.model_name.i18n_key # ActiveModel::Naming 148 | else 149 | model.to_s.underscore 150 | end 151 | 152 | if options.fetch(:html, true) 153 | b, eb = ''.freeze, ''.freeze 154 | sp = ' '.freeze 155 | html_key = '_html'.freeze 156 | else 157 | b = eb = html_key = ''.freeze 158 | sp = ' '.freeze 159 | end 160 | 161 | model_count = collection.total_pages > 1 ? 5 : collection.size 162 | defaults = ["models.#{model_key}".freeze].freeze 163 | defaults << Proc.new { |_, opts| 164 | if model.respond_to? :model_name 165 | model.model_name.human(:count => opts[:count]) 166 | else 167 | name = model_key.to_s.tr('_'.freeze, ' '.freeze) 168 | raise "can't pluralize model name: #{model.inspect}" unless name.respond_to? :pluralize 169 | opts[:count] == 1 ? name : name.pluralize 170 | end 171 | } 172 | model_name = will_paginate_translate defaults, :count => model_count 173 | 174 | if collection.total_pages < 2 175 | i18n_key = :"page_entries_info.single_page#{html_key}" 176 | keys = [:"#{model_key}.#{i18n_key}", i18n_key] 177 | 178 | will_paginate_translate keys, :count => collection.total_entries, :model => model_name do |_, opts| 179 | case opts[:count] 180 | when 0; "No #{opts[:model]} found" 181 | when 1; "Displaying #{b}1#{eb} #{opts[:model]}" 182 | else "Displaying #{b}all#{sp}#{opts[:count]}#{eb} #{opts[:model]}" 183 | end 184 | end 185 | else 186 | i18n_key = :"page_entries_info.multi_page#{html_key}" 187 | keys = [:"#{model_key}.#{i18n_key}", i18n_key] 188 | params = { 189 | :model => model_name, :count => collection.total_entries, 190 | :from => collection.offset + 1, :to => collection.offset + collection.length 191 | } 192 | will_paginate_translate keys, params do |_, opts| 193 | %{Displaying %s #{b}%d#{sp}-#{sp}%d#{eb} of #{b}%d#{eb} in total} % 194 | [ opts[:model], opts[:from], opts[:to], opts[:count] ] 195 | end 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /spec/view_helpers/action_view_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'active_support/rescuable' # needed for Ruby 1.9.1 3 | require 'action_controller' 4 | require 'action_view' 5 | require 'will_paginate/view_helpers/action_view' 6 | require 'will_paginate/collection' 7 | 8 | Routes = ActionDispatch::Routing::RouteSet.new 9 | 10 | Routes.draw do 11 | get 'dummy/page/:page' => 'dummy#index' 12 | get 'dummy/dots/page.:page' => 'dummy#dots' 13 | get 'ibocorp(/:page)' => 'ibocorp#index', 14 | :constraints => { :page => /\d+/ }, :defaults => { :page => 1 } 15 | 16 | get ':controller(/:action(/:id(.:format)))' 17 | end 18 | 19 | describe WillPaginate::ActionView do 20 | 21 | before(:all) do 22 | I18n.load_path.concat WillPaginate::I18n.load_path 23 | end 24 | 25 | before(:each) do 26 | I18n.reload! 27 | end 28 | 29 | before(:each) do 30 | @assigns = {} 31 | @controller = DummyController.new 32 | @request = @controller.request 33 | @template = '<%= will_paginate collection, options %>' 34 | end 35 | 36 | attr_reader :assigns, :controller, :request 37 | 38 | def render(locals) 39 | @view = ActionView::Base.new([], @assigns, @controller) 40 | @view.request = @request 41 | @view.singleton_class.send(:include, @controller._routes.url_helpers) 42 | @view.render(:inline => @template, :locals => locals) 43 | end 44 | 45 | ## basic pagination ## 46 | 47 | it "should render" do 48 | paginate do |pagination| 49 | assert_select 'a[href]', 3 do |elements| 50 | validate_page_numbers [2,3,2], elements 51 | assert_select elements.last, ':last-child', "Next →" 52 | end 53 | assert_select 'span', 1 54 | assert_select 'span.disabled:first-child', '← Previous' 55 | assert_select 'em.current', '1' 56 | pagination.first.inner_text.should == '← Previous 1 2 3 Next →' 57 | end 58 | end 59 | 60 | it "should override existing page param value" do 61 | request.params :page => 1 62 | paginate do |pagination| 63 | assert_select 'a[href]', 3 do |elements| 64 | validate_page_numbers [2,3,2], elements 65 | end 66 | end 67 | end 68 | 69 | it "should render nothing when there is only 1 page" do 70 | paginate(:per_page => 30).should be_empty 71 | end 72 | 73 | it "should paginate with options" do 74 | paginate({ :page => 2 }, :class => 'will_paginate', :previous_label => 'Prev', :next_label => 'Next') do 75 | assert_select 'a[href]', 4 do |elements| 76 | validate_page_numbers [1,1,3,3], elements 77 | # test rel attribute values: 78 | assert_select elements[1], 'a', '1' do |link| 79 | link.first['rel'].should == 'prev start' 80 | end 81 | assert_select elements.first, 'a', "Prev" do |link| 82 | link.first['rel'].should == 'prev start' 83 | end 84 | assert_select elements.last, 'a', "Next" do |link| 85 | link.first['rel'].should == 'next' 86 | end 87 | end 88 | assert_select '.current', '2' 89 | end 90 | end 91 | 92 | it "should paginate using a custom renderer class" do 93 | paginate({}, :renderer => AdditionalLinkAttributesRenderer) do 94 | assert_select 'a[default=true]', 3 95 | end 96 | end 97 | 98 | it "should paginate using a custom renderer instance" do 99 | renderer = WillPaginate::ActionView::LinkRenderer.new 100 | def renderer.gap() '~~' end 101 | 102 | paginate({ :per_page => 2 }, :inner_window => 0, :outer_window => 0, :renderer => renderer) do 103 | assert_select 'span.my-gap', '~~' 104 | end 105 | 106 | renderer = AdditionalLinkAttributesRenderer.new(:title => 'rendered') 107 | paginate({}, :renderer => renderer) do 108 | assert_select 'a[title=rendered]', 3 109 | end 110 | end 111 | 112 | it "should have classnames on previous/next links" do 113 | paginate do |pagination| 114 | assert_select 'span.disabled.previous_page:first-child' 115 | assert_select 'a.next_page[href]:last-child' 116 | end 117 | end 118 | 119 | it "should match expected markup" do 120 | paginate 121 | expected = <<-HTML 122 | 127 | HTML 128 | expected.strip!.gsub!(/\s{2,}/, ' ') 129 | expected_dom = HTML::Document.new(expected).root 130 | 131 | html_document.root.should == expected_dom 132 | end 133 | 134 | it "should output escaped URLs" do 135 | paginate({:page => 1, :per_page => 1, :total_entries => 2}, 136 | :page_links => false, :params => { :tag => '
' }) 137 | 138 | assert_select 'a[href]', 1 do |links| 139 | query = links.first['href'].split('?', 2)[1] 140 | query.split('&').sort.should == %w(page=2 tag=%3Cbr%3E) 141 | end 142 | end 143 | 144 | ## advanced options for pagination ## 145 | 146 | it "should be able to render without container" do 147 | paginate({}, :container => false) 148 | assert_select 'div.pagination', 0, 'main DIV present when it shouldn\'t' 149 | assert_select 'a[href]', 3 150 | end 151 | 152 | it "should be able to render without page links" do 153 | paginate({ :page => 2 }, :page_links => false) do 154 | assert_select 'a[href]', 2 do |elements| 155 | validate_page_numbers [1,3], elements 156 | end 157 | end 158 | end 159 | 160 | ## other helpers ## 161 | 162 | it "should render a paginated section" do 163 | @template = <<-ERB 164 | <%= paginated_section collection, options do %> 165 | <%= content_tag :div, '', :id => "developers" %> 166 | <% end %> 167 | ERB 168 | 169 | paginate 170 | assert_select 'div.pagination', 2 171 | assert_select 'div.pagination + div#developers', 1 172 | end 173 | 174 | it "should not render a paginated section with a single page" do 175 | @template = <<-ERB 176 | <%= paginated_section collection, options do %> 177 | <%= content_tag :div, '', :id => "developers" %> 178 | <% end %> 179 | ERB 180 | 181 | paginate(:total_entries => 1) 182 | assert_select 'div.pagination', 0 183 | assert_select 'div#developers', 1 184 | end 185 | 186 | ## parameter handling in page links ## 187 | 188 | it "should preserve parameters on GET" do 189 | request.params :foo => { :bar => 'baz' } 190 | paginate 191 | assert_links_match /foo\[bar\]=baz/ 192 | end 193 | 194 | it "doesn't allow tampering with host, port, protocol" do 195 | request.params :host => 'disney.com', :port => '99', :protocol => 'ftp' 196 | paginate 197 | assert_links_match %r{^/foo/bar} 198 | assert_no_links_match /disney/ 199 | assert_no_links_match /99/ 200 | assert_no_links_match /ftp/ 201 | end 202 | 203 | it "should not preserve parameters on POST" do 204 | request.post 205 | request.params :foo => 'bar' 206 | paginate 207 | assert_no_links_match /foo=bar/ 208 | end 209 | 210 | it "should add additional parameters to links" do 211 | paginate({}, :params => { :foo => 'bar' }) 212 | assert_links_match /foo=bar/ 213 | end 214 | 215 | it "should add anchor parameter" do 216 | paginate({}, :params => { :anchor => 'anchor' }) 217 | assert_links_match /#anchor$/ 218 | end 219 | 220 | it "should remove arbitrary parameters" do 221 | request.params :foo => 'bar' 222 | paginate({}, :params => { :foo => nil }) 223 | assert_no_links_match /foo=bar/ 224 | end 225 | 226 | it "should override default route parameters" do 227 | paginate({}, :params => { :controller => 'baz', :action => 'list' }) 228 | assert_links_match %r{\Wbaz/list\W} 229 | end 230 | 231 | it "should paginate with custom page parameter" do 232 | paginate({ :page => 2 }, :param_name => :developers_page) do 233 | assert_select 'a[href]', 4 do |elements| 234 | validate_page_numbers [1,1,3,3], elements, :developers_page 235 | end 236 | end 237 | end 238 | 239 | it "should paginate with complex custom page parameter" do 240 | request.params :developers => { :page => 2 } 241 | 242 | paginate({ :page => 2 }, :param_name => 'developers[page]') do 243 | assert_select 'a[href]', 4 do |links| 244 | assert_links_match /\?developers\[page\]=\d+$/, links 245 | validate_page_numbers [1,1,3,3], links, 'developers[page]' 246 | end 247 | end 248 | end 249 | 250 | it "should paginate with custom route page parameter" do 251 | request.symbolized_path_parameters.update :controller => 'dummy', :action => nil 252 | paginate :per_page => 2 do 253 | assert_select 'a[href]', 6 do |links| 254 | assert_links_match %r{/page/(\d+)$}, links, [2, 3, 4, 5, 6, 2] 255 | end 256 | end 257 | end 258 | 259 | it "should paginate with custom route with dot separator page parameter" do 260 | request.symbolized_path_parameters.update :controller => 'dummy', :action => 'dots' 261 | paginate :per_page => 2 do 262 | assert_select 'a[href]', 6 do |links| 263 | assert_links_match %r{/page\.(\d+)$}, links, [2, 3, 4, 5, 6, 2] 264 | end 265 | end 266 | end 267 | 268 | it "should paginate with custom route and first page number implicit" do 269 | request.symbolized_path_parameters.update :controller => 'ibocorp', :action => nil 270 | paginate :page => 2, :per_page => 2 do 271 | assert_select 'a[href]', 7 do |links| 272 | assert_links_match %r{/ibocorp(?:/(\d+))?$}, links, [nil, nil, 3, 4, 5, 6, 3] 273 | end 274 | end 275 | # Routes.recognize_path('/ibocorp/2').should == {:page=>'2', :action=>'index', :controller=>'ibocorp'} 276 | # Routes.recognize_path('/ibocorp/foo').should == {:action=>'foo', :controller=>'ibocorp'} 277 | end 278 | 279 | ## internal hardcore stuff ## 280 | 281 | it "should be able to guess the collection name" do 282 | collection = mock 283 | collection.expects(:total_pages).returns(1) 284 | 285 | @template = '<%= will_paginate options %>' 286 | controller.controller_name = 'developers' 287 | assigns['developers'] = collection 288 | 289 | paginate(nil) 290 | end 291 | 292 | it "should fail if the inferred collection is nil" do 293 | @template = '<%= will_paginate options %>' 294 | controller.controller_name = 'developers' 295 | 296 | lambda { 297 | paginate(nil) 298 | }.should raise_error(ActionView::TemplateError, /@developers/) 299 | end 300 | 301 | describe '#pagination_link_tags' do 302 | let(:helper) { 303 | helper = Class.new { 304 | attr_reader :controller 305 | include ActionView::Helpers::UrlHelper 306 | include Routes.url_helpers 307 | include WillPaginate::ActionView 308 | }.new 309 | helper.default_url_options[:controller] = 'dummy' 310 | helper.default_url_options[:host] = 'example.com' 311 | helper } 312 | let(:collection) { (1..30).to_a } 313 | let(:page_one) { collection.paginate(:page => 1, :per_page => 10)} 314 | let(:page_two) { collection.paginate(:page => 2, :per_page =>10)} 315 | let(:page_three) { collection.paginate(:page => 3, :per_page => 10)} 316 | 317 | context 'the first page' do 318 | subject { helper.pagination_link_tags page_one } 319 | it { should include( 320 | '') } 321 | it { should_not match /rel="prev"/ } 322 | end 323 | 324 | context 'the middle page' do 325 | subject { helper.pagination_link_tags page_two } 326 | it { should include( 327 | '') } 328 | it { should include( 329 | '') } 330 | end 331 | 332 | context 'the last page' do 333 | subject { helper.pagination_link_tags page_three } 334 | it { should include( 335 | '') } 336 | it { should_not match /rel="next"/ } 337 | end 338 | 339 | context 'with optional parameters for searches' do 340 | subject { helper.pagination_link_tags( 341 | page_three, { search: 'term', boolean_param: true }) } 342 | it { should include( 343 | '' 344 | ) 345 | } 346 | end 347 | end 348 | 349 | ## i18n 350 | 351 | it "is able to translate previous/next labels" do 352 | translation :will_paginate => { 353 | :previous_label => 'Go back', 354 | :next_label => 'Load more' 355 | } 356 | 357 | paginate do |pagination| 358 | assert_select 'span.disabled:first-child', 'Go back' 359 | assert_select 'a[rel=next]', 'Load more' 360 | end 361 | end 362 | 363 | it "renders using ActionView helpers on a custom object" do 364 | helper = Class.new { 365 | attr_reader :controller 366 | include ActionView::Helpers::UrlHelper 367 | include Routes.url_helpers 368 | include WillPaginate::ActionView 369 | }.new 370 | helper.default_url_options[:controller] = 'dummy' 371 | 372 | collection = WillPaginate::Collection.new(2, 1, 3) 373 | @render_output = helper.will_paginate(collection) 374 | 375 | assert_select 'a[href]', 4 do |links| 376 | urls = links.map {|l| l['href'] }.uniq 377 | urls.should == ['/dummy/page/1', '/dummy/page/3'] 378 | end 379 | end 380 | 381 | it "renders using ActionDispatch helper on a custom object" do 382 | helper = Class.new { 383 | include ActionDispatch::Routing::UrlFor 384 | include Routes.url_helpers 385 | include WillPaginate::ActionView 386 | }.new 387 | helper.default_url_options.update \ 388 | :only_path => true, 389 | :controller => 'dummy' 390 | 391 | collection = WillPaginate::Collection.new(2, 1, 3) 392 | @render_output = helper.will_paginate(collection) 393 | 394 | assert_select 'a[href]', 4 do |links| 395 | urls = links.map {|l| l['href'] }.uniq 396 | urls.should == ['/dummy/page/1', '/dummy/page/3'] 397 | end 398 | end 399 | 400 | private 401 | 402 | def translation(data) 403 | I18n.available_locales # triggers loading existing translations 404 | I18n.backend.store_translations(:en, data) 405 | end 406 | end 407 | 408 | class AdditionalLinkAttributesRenderer < WillPaginate::ActionView::LinkRenderer 409 | def initialize(link_attributes = nil) 410 | super() 411 | @additional_link_attributes = link_attributes || { :default => 'true' } 412 | end 413 | 414 | def link(text, target, attributes = {}) 415 | super(text, target, attributes.merge(@additional_link_attributes)) 416 | end 417 | end 418 | 419 | class DummyController 420 | attr_reader :request 421 | attr_accessor :controller_name 422 | 423 | include ActionController::UrlFor 424 | include Routes.url_helpers 425 | 426 | def initialize 427 | @request = DummyRequest.new 428 | end 429 | 430 | def params 431 | @request.params 432 | end 433 | 434 | def env 435 | {} 436 | end 437 | 438 | def _prefixes 439 | [] 440 | end 441 | end 442 | 443 | class IbocorpController < DummyController 444 | end 445 | 446 | class DummyRequest 447 | attr_accessor :symbolized_path_parameters 448 | alias :path_parameters :symbolized_path_parameters 449 | 450 | def initialize 451 | @get = true 452 | @params = {} 453 | @symbolized_path_parameters = { :controller => 'foo', :action => 'bar' } 454 | end 455 | 456 | def get? 457 | @get 458 | end 459 | 460 | def post 461 | @get = false 462 | end 463 | 464 | def relative_url_root 465 | '' 466 | end 467 | 468 | def script_name 469 | '' 470 | end 471 | 472 | def params(more = nil) 473 | @params.update(more) if more 474 | @params 475 | end 476 | 477 | def host_with_port 478 | 'example.com' 479 | end 480 | alias host host_with_port 481 | 482 | def optional_port 483 | '' 484 | end 485 | 486 | def protocol 487 | 'http:' 488 | end 489 | end 490 | -------------------------------------------------------------------------------- /spec/finders/active_record_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'will_paginate/active_record' 3 | require File.expand_path('../activerecord_test_connector', __FILE__) 4 | 5 | ActiverecordTestConnector.setup 6 | abort unless ActiverecordTestConnector.able_to_connect 7 | 8 | describe WillPaginate::ActiveRecord do 9 | 10 | extend ActiverecordTestConnector::FixtureSetup 11 | 12 | fixtures :topics, :replies, :users, :projects, :developers_projects 13 | 14 | it "should integrate with ActiveRecord::Base" do 15 | ActiveRecord::Base.should respond_to(:paginate) 16 | end 17 | 18 | it "should paginate" do 19 | lambda { 20 | users = User.paginate(:page => 1, :per_page => 5).to_a 21 | users.length.should == 5 22 | }.should run_queries(2) 23 | end 24 | 25 | it "should fail when encountering unknown params" do 26 | lambda { 27 | User.paginate :foo => 'bar', :page => 1, :per_page => 4 28 | }.should raise_error(ArgumentError) 29 | end 30 | 31 | describe "relation" do 32 | it "should return a relation" do 33 | rel = nil 34 | lambda { 35 | rel = Developer.paginate(:page => 1) 36 | rel.per_page.should == 10 37 | rel.current_page.should == 1 38 | }.should run_queries(0) 39 | 40 | lambda { 41 | rel.total_pages.should == 2 42 | }.should run_queries(1) 43 | end 44 | 45 | it "should keep per-class per_page number" do 46 | rel = Developer.order('id').paginate(:page => 1) 47 | rel.per_page.should == 10 48 | end 49 | 50 | it "should be able to change per_page number" do 51 | rel = Developer.order('id').paginate(:page => 1).limit(5) 52 | rel.per_page.should == 5 53 | end 54 | 55 | it "remembers pagination in sub-relations" do 56 | rel = Topic.paginate(:page => 2, :per_page => 3) 57 | lambda { 58 | rel.total_entries.should == 4 59 | }.should run_queries(1) 60 | rel = rel.mentions_activerecord 61 | rel.current_page.should == 2 62 | rel.per_page.should == 3 63 | lambda { 64 | rel.total_entries.should == 1 65 | }.should run_queries(1) 66 | end 67 | 68 | it "supports the page() method" do 69 | rel = Developer.page('1').order('id') 70 | rel.current_page.should == 1 71 | rel.per_page.should == 10 72 | rel.offset.should == 0 73 | 74 | rel = rel.limit(5).page(2) 75 | rel.per_page.should == 5 76 | rel.offset.should == 5 77 | end 78 | 79 | it "raises on invalid page number" do 80 | lambda { 81 | Developer.page('foo') 82 | }.should raise_error(ArgumentError) 83 | end 84 | 85 | it "supports first limit() then page()" do 86 | rel = Developer.limit(3).page(3) 87 | rel.offset.should == 6 88 | end 89 | 90 | it "supports first page() then limit()" do 91 | rel = Developer.page(3).limit(3) 92 | rel.offset.should == 6 93 | end 94 | 95 | it "supports #first" do 96 | rel = Developer.order('id').page(2).per_page(4) 97 | rel.first.should == users(:dev_5) 98 | rel.first(2).should == users(:dev_5, :dev_6) 99 | end 100 | 101 | it "supports #last" do 102 | rel = Developer.order('id').page(2).per_page(4) 103 | rel.last.should == users(:dev_8) 104 | rel.last(2).should == users(:dev_7, :dev_8) 105 | rel.page(3).last.should == users(:poor_jamis) 106 | end 107 | 108 | it "keeps pagination data after 'scoped'" do 109 | rel = Developer.page(2).scoped 110 | rel.per_page.should == 10 111 | rel.offset.should == 10 112 | rel.current_page.should == 2 113 | end 114 | end 115 | 116 | describe "counting" do 117 | it "should guess the total count" do 118 | lambda { 119 | topics = Topic.paginate :page => 2, :per_page => 3 120 | topics.total_entries.should == 4 121 | }.should run_queries(1) 122 | end 123 | 124 | it "should guess that there are no records" do 125 | lambda { 126 | topics = Topic.where(:project_id => 999).paginate :page => 1, :per_page => 3 127 | topics.total_entries.should == 0 128 | }.should run_queries(1) 129 | end 130 | 131 | it "forgets count in sub-relations" do 132 | lambda { 133 | topics = Topic.paginate :page => 1, :per_page => 3 134 | topics.total_entries.should == 4 135 | topics.where('1 = 1').total_entries.should == 4 136 | }.should run_queries(2) 137 | end 138 | 139 | it "remembers custom count options in sub-relations" do 140 | topics = Topic.paginate :page => 1, :per_page => 3, :count => {:conditions => "title LIKE '%futurama%'"} 141 | topics.total_entries.should == 1 142 | topics.length.should == 3 143 | lambda { 144 | topics.order('id').total_entries.should == 1 145 | }.should run_queries(1) 146 | end 147 | 148 | it "supports empty? method" do 149 | topics = Topic.paginate :page => 1, :per_page => 3 150 | lambda { 151 | topics.should_not be_empty 152 | }.should run_queries(1) 153 | end 154 | 155 | it "support empty? for grouped queries" do 156 | topics = Topic.group(:project_id).paginate :page => 1, :per_page => 3 157 | lambda { 158 | topics.should_not be_empty 159 | }.should run_queries(1) 160 | end 161 | 162 | it "supports `size` for grouped queries" do 163 | topics = Topic.group(:project_id).paginate :page => 1, :per_page => 3 164 | lambda { 165 | topics.size.should == {nil=>2, 1=>2} 166 | }.should run_queries(1) 167 | end 168 | 169 | it "overrides total_entries count with a fixed value" do 170 | lambda { 171 | topics = Topic.paginate :page => 1, :per_page => 3, :total_entries => 999 172 | topics.total_entries.should == 999 173 | # value is kept even in sub-relations 174 | topics.where('1 = 1').total_entries.should == 999 175 | }.should run_queries(0) 176 | end 177 | 178 | it "supports a non-int for total_entries" do 179 | topics = Topic.paginate :page => 1, :per_page => 3, :total_entries => "999" 180 | topics.total_entries.should == 999 181 | end 182 | 183 | it "removes :include for count" do 184 | lambda { 185 | developers = Developer.paginate(:page => 1, :per_page => 1).includes(:projects) 186 | developers.total_entries.should == 11 187 | $query_sql.last.should_not =~ /\bJOIN\b/ 188 | }.should run_queries(1) 189 | end 190 | 191 | it "keeps :include for count when they are referenced in :conditions" do 192 | developers = Developer.paginate(:page => 1, :per_page => 1).includes(:projects) 193 | with_condition = developers.where('projects.id > 1') 194 | with_condition = with_condition.references(:projects) if with_condition.respond_to?(:references) 195 | with_condition.total_entries.should == 1 196 | 197 | $query_sql.last.should =~ /\bJOIN\b/ 198 | end 199 | 200 | it "should count with group" do 201 | Developer.group(:salary).page(1).total_entries.should == 4 202 | end 203 | 204 | it "should count with select" do 205 | Topic.select('title, content').page(1).total_entries.should == 4 206 | end 207 | 208 | it "removes :reorder for count with group" do 209 | Project.group(:id).reorder(:id).page(1).total_entries 210 | $query_sql.last.should_not =~ /\ORDER\b/ 211 | end 212 | 213 | it "should not have zero total_pages when the result set is empty" do 214 | Developer.where("1 = 2").page(1).total_pages.should == 1 215 | end 216 | end 217 | 218 | it "should not ignore :select parameter when it says DISTINCT" do 219 | users = User.select('DISTINCT salary').paginate :page => 2 220 | users.total_entries.should == 5 221 | end 222 | 223 | describe "paginate_by_sql" do 224 | it "should respond" do 225 | User.should respond_to(:paginate_by_sql) 226 | end 227 | 228 | it "should paginate" do 229 | lambda { 230 | sql = "select content from topics where content like '%futurama%'" 231 | topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 1 232 | topics.total_entries.should == 1 233 | topics.first.attributes.has_key?('title').should be_false 234 | }.should run_queries(2) 235 | end 236 | 237 | it "should respect total_entries setting" do 238 | lambda { 239 | sql = "select content from topics" 240 | topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 1, :total_entries => 999 241 | topics.total_entries.should == 999 242 | }.should run_queries(1) 243 | end 244 | 245 | it "defaults to page 1" do 246 | sql = "select content from topics" 247 | topics = Topic.paginate_by_sql sql, :page => nil, :per_page => 1 248 | topics.current_page.should == 1 249 | topics.size.should == 1 250 | end 251 | 252 | it "should strip the order when counting" do 253 | lambda { 254 | sql = "select id, title, content from topics order by topics.title" 255 | topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 2 256 | topics.first.should == topics(:ar) 257 | }.should run_queries(2) 258 | 259 | $query_sql.last.should include('COUNT') 260 | $query_sql.last.should_not include('order by topics.title') 261 | end 262 | 263 | it "shouldn't change the original query string" do 264 | query = 'select * from topics where 1 = 2' 265 | original_query = query.dup 266 | Topic.paginate_by_sql(query, :page => 1) 267 | query.should == original_query 268 | end 269 | end 270 | 271 | it "doesn't mangle options" do 272 | options = { :page => 1 } 273 | options.expects(:delete).never 274 | options_before = options.dup 275 | 276 | Topic.paginate(options) 277 | options.should == options_before 278 | end 279 | 280 | it "should get first page of Topics with a single query" do 281 | lambda { 282 | result = Topic.paginate :page => nil 283 | result.to_a # trigger loading of records 284 | result.current_page.should == 1 285 | result.total_pages.should == 1 286 | result.size.should == 4 287 | }.should run_queries(1) 288 | end 289 | 290 | it "should get second (inexistent) page of Topics, requiring 2 queries" do 291 | lambda { 292 | result = Topic.paginate :page => 2 293 | result.total_pages.should == 1 294 | result.should be_empty 295 | }.should run_queries(2) 296 | end 297 | 298 | it "should paginate with :order" do 299 | result = Topic.paginate :page => 1, :order => 'created_at DESC' 300 | result.should == topics(:futurama, :harvey_birdman, :rails, :ar).reverse 301 | result.total_pages.should == 1 302 | end 303 | 304 | it "should paginate with :conditions" do 305 | result = Topic.paginate :page => 1, :order => 'id ASC', 306 | :conditions => ["created_at > ?", 30.minutes.ago] 307 | result.should == topics(:rails, :ar) 308 | result.total_pages.should == 1 309 | end 310 | 311 | it "should paginate with :include and :conditions" do 312 | klass = Topic 313 | klass = klass.references(:replies) if klass.respond_to?(:references) 314 | 315 | result = klass.paginate \ 316 | :page => 1, 317 | :include => :replies, 318 | :conditions => "replies.content LIKE 'Bird%' ", 319 | :per_page => 10 320 | 321 | expected = klass.find :all, 322 | :include => 'replies', 323 | :conditions => "replies.content LIKE 'Bird%' ", 324 | :limit => 10 325 | 326 | result.should == expected 327 | result.total_entries.should == 1 328 | end 329 | 330 | it "should paginate with :include and :order" do 331 | result = nil 332 | lambda { 333 | result = Topic.paginate(:page => 1, :include => :replies, :per_page => 10, 334 | :order => 'replies.created_at asc, topics.created_at asc').to_a 335 | }.should run_queries(2) 336 | 337 | expected = Topic.find :all, 338 | :include => 'replies', 339 | :order => 'replies.created_at asc, topics.created_at asc', 340 | :limit => 10 341 | 342 | result.should == expected 343 | result.total_entries.should == 4 344 | end 345 | 346 | describe "associations" do 347 | it "should paginate with include" do 348 | project = projects(:active_record) 349 | 350 | topics = project.topics 351 | topics = topics.references(:replies) if topics.respond_to?(:references) 352 | 353 | result = topics.paginate \ 354 | :page => 1, 355 | :include => :replies, 356 | :conditions => ["replies.content LIKE ?", 'Nice%'], 357 | :per_page => 10 358 | 359 | topics = Topic 360 | topics = topics.references(:replies) if topics.respond_to?(:references) 361 | 362 | expected = topics.find :all, 363 | :include => 'replies', 364 | :conditions => ["project_id = ? AND replies.content LIKE ?", project.id, 'Nice%'], 365 | :limit => 10 366 | 367 | result.should == expected 368 | end 369 | 370 | it "should paginate" do 371 | dhh = users(:david) 372 | expected_name_ordered = projects(:action_controller, :active_record) 373 | expected_id_ordered = projects(:active_record, :action_controller) 374 | 375 | lambda { 376 | # with association-specified order 377 | result = ignore_deprecation { 378 | dhh.projects.includes(:topics).paginate(:page => 1, :order => 'projects.name') 379 | } 380 | result.to_a.should == expected_name_ordered 381 | result.total_entries.should == 2 382 | }.should run_queries(2) 383 | 384 | # with explicit order 385 | result = dhh.projects.paginate(:page => 1).reorder('projects.id') 386 | result.should == expected_id_ordered 387 | result.total_entries.should == 2 388 | 389 | lambda { 390 | dhh.projects.find(:all, :order => 'projects.id', :limit => 4) 391 | }.should_not raise_error 392 | 393 | result = dhh.projects.paginate(:page => 1, :per_page => 4).reorder('projects.id') 394 | result.should == expected_id_ordered 395 | 396 | # has_many with implicit order 397 | topic = Topic.find(1) 398 | expected = replies(:spam, :witty_retort) 399 | # FIXME: wow, this is ugly 400 | topic.replies.paginate(:page => 1).map(&:id).sort.should == expected.map(&:id).sort 401 | topic.replies.paginate(:page => 1).reorder('replies.id ASC').should == expected.reverse 402 | end 403 | 404 | it "should paginate through association extension" do 405 | project = Project.order('id').first 406 | expected = [replies(:brave)] 407 | 408 | lambda { 409 | result = project.replies.only_recent.paginate(:page => 1) 410 | result.should == expected 411 | }.should run_queries(1) 412 | end 413 | end 414 | 415 | it "should paginate with joins" do 416 | result = nil 417 | join_sql = 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id' 418 | 419 | lambda { 420 | result = Developer.paginate(:page => 1, :joins => join_sql, :conditions => 'project_id = 1') 421 | result.to_a # trigger loading of records 422 | result.size.should == 2 423 | developer_names = result.map(&:name) 424 | developer_names.should include('David') 425 | developer_names.should include('Jamis') 426 | }.should run_queries(1) 427 | 428 | lambda { 429 | expected = result.to_a 430 | result = Developer.paginate(:page => 1, :joins => join_sql, 431 | :conditions => 'project_id = 1', :count => { :select => "users.id" }).to_a 432 | result.should == expected 433 | result.total_entries.should == 2 434 | }.should run_queries(1) 435 | end 436 | 437 | it "should paginate with group" do 438 | result = nil 439 | lambda { 440 | result = Developer.paginate(:page => 1, :per_page => 10, 441 | :group => 'salary', :select => 'salary', :order => 'salary').to_a 442 | }.should run_queries(1) 443 | 444 | expected = users(:david, :jamis, :dev_10, :poor_jamis).map(&:salary).sort 445 | result.map(&:salary).should == expected 446 | end 447 | 448 | it "should not paginate with dynamic finder" do 449 | lambda { 450 | Developer.paginate_by_salary(100000, :page => 1, :per_page => 5) 451 | }.should raise_error(NoMethodError) 452 | end 453 | 454 | it "should paginate with_scope" do 455 | result = Developer.with_poor_ones { Developer.paginate :page => 1 } 456 | result.size.should == 2 457 | result.total_entries.should == 2 458 | end 459 | 460 | describe "scopes" do 461 | it "should paginate" do 462 | result = Developer.poor.paginate :page => 1, :per_page => 1 463 | result.size.should == 1 464 | result.total_entries.should == 2 465 | end 466 | 467 | it "should paginate on habtm association" do 468 | project = projects(:active_record) 469 | lambda { 470 | result = ignore_deprecation { project.developers.poor.paginate :page => 1, :per_page => 1 } 471 | result.size.should == 1 472 | result.total_entries.should == 1 473 | }.should run_queries(2) 474 | end 475 | 476 | it "should paginate on hmt association" do 477 | project = projects(:active_record) 478 | expected = [replies(:brave)] 479 | 480 | lambda { 481 | result = project.replies.recent.paginate :page => 1, :per_page => 1 482 | result.should == expected 483 | result.total_entries.should == 1 484 | }.should run_queries(2) 485 | end 486 | 487 | it "should paginate on has_many association" do 488 | project = projects(:active_record) 489 | expected = [topics(:ar)] 490 | 491 | lambda { 492 | result = project.topics.mentions_activerecord.paginate :page => 1, :per_page => 1 493 | result.should == expected 494 | result.total_entries.should == 1 495 | }.should run_queries(2) 496 | end 497 | end 498 | 499 | it "should paginate with :readonly option" do 500 | lambda { 501 | Developer.paginate :readonly => true, :page => 1 502 | }.should_not raise_error 503 | end 504 | 505 | it "should not paginate an array of IDs" do 506 | lambda { 507 | Developer.paginate((1..8).to_a, :per_page => 3, :page => 2, :order => 'id') 508 | }.should raise_error(ArgumentError) 509 | end 510 | 511 | it "errors out for invalid values" do |variable| 512 | lambda { 513 | # page that results in an offset larger than BIGINT 514 | Project.page(307445734561825862) 515 | }.should raise_error(WillPaginate::InvalidPage, "invalid offset: 9223372036854775830") 516 | end 517 | end 518 | --------------------------------------------------------------------------------