├── .envrc ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── .rspec ├── Brewfile ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── environments ├── Gemfile.non-rails.rb ├── Gemfile.rails-edge.rb ├── Gemfile.rails5.0.rb ├── Gemfile.rails5.1.rb ├── Gemfile.rails5.2.rb ├── Gemfile.rails6.0.rb ├── Gemfile.rails6.1.rb └── Gemfile.rails7.0.rb ├── init.rb ├── lib ├── will_paginate.rb └── will_paginate │ ├── active_record.rb │ ├── array.rb │ ├── collection.rb │ ├── core_ext.rb │ ├── deprecation.rb │ ├── i18n.rb │ ├── locale │ └── en.yml │ ├── mongoid.rb │ ├── page_number.rb │ ├── per_page.rb │ ├── railtie.rb │ ├── sequel.rb │ ├── version.rb │ ├── view_helpers.rb │ └── view_helpers │ ├── action_view.rb │ ├── hanami.rb │ ├── link_renderer.rb │ ├── link_renderer_base.rb │ └── sinatra.rb ├── script ├── bootstrap ├── ci-matrix ├── release └── test_all ├── spec-non-rails ├── mongoid_spec.rb ├── sequel_spec.rb └── spec_helper.rb ├── spec ├── collection_spec.rb ├── console ├── console_fixtures.rb ├── database.yml ├── finders │ ├── active_record_spec.rb │ └── activerecord_test_connector.rb ├── fixtures │ ├── admin.rb │ ├── developer.rb │ ├── developers_projects.yml │ ├── project.rb │ ├── projects.yml │ ├── replies.yml │ ├── reply.rb │ ├── schema.rb │ ├── topic.rb │ ├── topics.yml │ ├── user.rb │ └── users.yml ├── matchers │ └── query_count_matcher.rb ├── page_number_spec.rb ├── per_page_spec.rb ├── spec_helper.rb └── view_helpers │ ├── action_view_spec.rb │ ├── base_spec.rb │ ├── link_renderer_base_spec.rb │ └── view_example_group.rb └── will_paginate.gemspec /.envrc: -------------------------------------------------------------------------------- 1 | PATH_add ./bin 2 | 3 | # shellcheck shell=bash 4 | export MYSQL_HOST=127.0.0.1 5 | export MYSQL_PORT=3307 6 | 7 | export POSTGRES_HOST=localhost 8 | export POSTGRES_PORT=5433 9 | export POSTGRES_USER=postgres 10 | export POSTGRES_PASSWORD=postgres 11 | 12 | export MONGODB_HOST=localhost 13 | export MONGODB_PORT=27018 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test Suite 3 | 'on': 4 | - push 5 | - pull_request 6 | jobs: 7 | test-rails: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: 12 | - '2.4' 13 | - '2.5' 14 | - '2.6' 15 | - '2.7' 16 | - '3.0' 17 | - '3.1' 18 | - '3.2' 19 | - '3.3' 20 | gemfile: 21 | - Gemfile 22 | - environments/Gemfile.rails5.0.rb 23 | - environments/Gemfile.rails5.1.rb 24 | - environments/Gemfile.rails5.2.rb 25 | - environments/Gemfile.rails6.0.rb 26 | - environments/Gemfile.rails6.1.rb 27 | - environments/Gemfile.rails-edge.rb 28 | exclude: 29 | - ruby: '2.4' 30 | gemfile: Gemfile 31 | - ruby: '2.5' 32 | gemfile: Gemfile 33 | - ruby: '2.6' 34 | gemfile: Gemfile 35 | - ruby: '3.0' 36 | gemfile: environments/Gemfile.rails5.0.rb 37 | - ruby: '3.1' 38 | gemfile: environments/Gemfile.rails5.0.rb 39 | - ruby: '3.2' 40 | gemfile: environments/Gemfile.rails5.0.rb 41 | - ruby: '3.3' 42 | gemfile: environments/Gemfile.rails5.0.rb 43 | - ruby: '3.0' 44 | gemfile: environments/Gemfile.rails5.1.rb 45 | - ruby: '3.1' 46 | gemfile: environments/Gemfile.rails5.1.rb 47 | - ruby: '3.2' 48 | gemfile: environments/Gemfile.rails5.1.rb 49 | - ruby: '3.3' 50 | gemfile: environments/Gemfile.rails5.1.rb 51 | - ruby: '3.0' 52 | gemfile: environments/Gemfile.rails5.2.rb 53 | - ruby: '3.1' 54 | gemfile: environments/Gemfile.rails5.2.rb 55 | - ruby: '3.2' 56 | gemfile: environments/Gemfile.rails5.2.rb 57 | - ruby: '3.3' 58 | gemfile: environments/Gemfile.rails5.2.rb 59 | - ruby: '2.4' 60 | gemfile: environments/Gemfile.rails6.0.rb 61 | - ruby: '2.4' 62 | gemfile: environments/Gemfile.rails6.1.rb 63 | - ruby: '2.4' 64 | gemfile: environments/Gemfile.rails-edge.rb 65 | - ruby: '2.5' 66 | gemfile: environments/Gemfile.rails-edge.rb 67 | - ruby: '2.6' 68 | gemfile: environments/Gemfile.rails-edge.rb 69 | - ruby: '2.7' 70 | gemfile: environments/Gemfile.rails-edge.rb 71 | - ruby: '3.0' 72 | gemfile: environments/Gemfile.rails-edge.rb 73 | runs-on: ubuntu-latest 74 | env: 75 | BUNDLE_GEMFILE: "${{ matrix.gemfile }}" 76 | services: 77 | mysql: 78 | image: mysql:5.7 79 | env: 80 | MYSQL_DATABASE: will_paginate 81 | MYSQL_ALLOW_EMPTY_PASSWORD: true 82 | ports: 83 | - 3306:3306 84 | postgres: 85 | image: postgres:11 86 | env: 87 | POSTGRES_DB: will_paginate 88 | POSTGRES_PASSWORD: postgres 89 | ports: 90 | - 5432:5432 91 | steps: 92 | - uses: actions/checkout@v4 93 | - uses: ruby/setup-ruby@v1 94 | with: 95 | ruby-version: "${{ matrix.ruby }}" 96 | bundler-cache: true 97 | - name: Run tests 98 | env: 99 | MYSQL_HOST: 127.0.0.1 100 | MYSQL_PORT: 3306 101 | POSTGRES_HOST: localhost 102 | POSTGRES_PORT: 5432 103 | POSTGRES_USER: postgres 104 | POSTGRES_PASSWORD: postgres 105 | run: | 106 | docker-wait() { 107 | local container 108 | container="$(docker ps -q -f ancestor=$1)" 109 | timeout 90s bash -c "until docker exec $container $2; do sleep 5; done" 110 | } 111 | 112 | docker-wait postgres:11 "pg_isready" 113 | docker-wait mysql:5.7 "mysqladmin ping" 114 | 115 | bundler binstubs rspec-core 116 | script/test_all 117 | test-nonrails: 118 | strategy: 119 | fail-fast: false 120 | matrix: 121 | ruby: 122 | - '2.4' 123 | - '2.5' 124 | - '2.6' 125 | - '2.7' 126 | - '3.0' 127 | - '3.1' 128 | - '3.2' 129 | runs-on: ubuntu-latest 130 | env: 131 | BUNDLE_GEMFILE: environments/Gemfile.non-rails.rb 132 | services: 133 | mongodb: 134 | image: mongo:4.2 135 | ports: 136 | - 27017:27017 137 | steps: 138 | - uses: actions/checkout@v4 139 | - uses: ruby/setup-ruby@v1 140 | with: 141 | ruby-version: "${{ matrix.ruby }}" 142 | bundler-cache: true 143 | - name: Run tests 144 | run: | 145 | docker-wait() { 146 | local container 147 | container="$(docker ps -q -f ancestor=$1)" 148 | timeout 90s bash -c "until docker exec $container $2; do sleep 5; done" 149 | } 150 | 151 | docker-wait mongo:4.2 "mongo --quiet" 152 | 153 | bundler binstubs rspec-core 154 | script/test_all 155 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile*.lock 2 | Brewfile.lock.json 3 | .bundle 4 | doc 5 | *.gem 6 | coverage 7 | /bin 8 | vendor/bundle 9 | tags 10 | .ruby-version 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | # brew 'mongodb/brew/mongodb-community@4.0', restart_service: true 2 | brew 'mysql@5.7', restart_service: true 3 | brew 'postgresql', restart_service: true 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to set up your environment for running tests: 2 | 3 | 1. Run `script/bootstrap` 4 | 5 | **Note:** on systems without Homebrew, you must ensure that MySQL 5.7, PostgreSQL 12, and MongoDB 4.x Community Edition are up and running. 6 | 7 | 2. Run `script/test_all` 8 | 9 | This ensures that the Active Record part of the suite is run across `sqlite3`, `mysql`, and `postgres` database adapters. 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # We test against other Rails versions, too. See `environments/` 4 | rails_version = '~> 7.1.3' 5 | 6 | gem 'activerecord', rails_version 7 | gem 'actionpack', rails_version 8 | 9 | gem 'rspec', '~> 3.12' 10 | gem 'mocha', '~> 2.0' 11 | 12 | gem 'sqlite3', '~> 1.5.0' 13 | gem 'mysql2', '~> 0.5.2', :group => :mysql 14 | gem 'pg', '~> 1.2', :group => :pg 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # will_paginate 2 | 3 | will_paginate is a pagination library that integrates with Ruby on Rails, Sinatra, Hanami::View, and Sequel. 4 | 5 | ``` ruby 6 | gem 'will_paginate', '~> 4.0' 7 | ``` 8 | 9 | See [installation instructions][install] on the wiki for more info. 10 | 11 | ℹ️ will_paginate is now in _maintenance mode_ and it will not be receiving new features. [See alternatives](https://www.ruby-toolbox.com/categories/pagination) 12 | 13 | ## Basic will_paginate use 14 | 15 | ``` ruby 16 | ## perform a paginated query: 17 | @posts = Post.paginate(page: params[:page]) 18 | 19 | # or, use an explicit "per page" limit: 20 | Post.paginate(page: params[:page], per_page: 30) 21 | 22 | ## render page links in the view: 23 | <%= will_paginate @posts %> 24 | ``` 25 | 26 | And that's it! You're done. You just need to add some CSS styles to [make those pagination links prettier][css]. 27 | 28 | You can customize the default "per_page" value: 29 | 30 | ``` ruby 31 | # for the Post model 32 | class Post 33 | self.per_page = 10 34 | end 35 | 36 | # set per_page globally 37 | WillPaginate.per_page = 10 38 | ``` 39 | 40 | New in Active Record 3: 41 | 42 | ``` ruby 43 | # paginate in Active Record now returns a Relation 44 | Post.where(published: true).paginate(page: params[:page]).order(id: :desc) 45 | 46 | # the new, shorter page() method 47 | Post.page(params[:page]).order(created_at: :desc) 48 | ``` 49 | 50 | See [the wiki][wiki] for more documentation. [Report bugs][issues] on GitHub. 51 | 52 | Happy paginating. 53 | 54 | 55 | [wiki]: https://github.com/mislav/will_paginate/wiki 56 | [install]: https://github.com/mislav/will_paginate/wiki/Installation "will_paginate installation" 57 | [issues]: https://github.com/mislav/will_paginate/issues 58 | [css]: http://mislav.github.io/will_paginate/ 59 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.1' 3 | services: 4 | mysql: 5 | image: mysql:5.7 6 | environment: 7 | - MYSQL_DATABASE=will_paginate 8 | - MYSQL_ALLOW_EMPTY_PASSWORD=true 9 | ports: 10 | - 3307:3306 11 | postgres: 12 | image: postgres:11 13 | environment: 14 | - POSTGRES_DB=will_paginate 15 | - POSTGRES_PASSWORD=postgres 16 | ports: 17 | - 5433:5432 18 | mongodb: 19 | image: mongo:4.2 20 | ports: 21 | - 27018:27017 22 | -------------------------------------------------------------------------------- /environments/Gemfile.non-rails.rb: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rspec', '~> 3.12' 4 | gem 'mocha', '~> 2.0' 5 | 6 | gem 'sqlite3', '~> 1.4.0' 7 | 8 | gem 'sequel', '~> 5.29' 9 | gem 'mongoid', '~> 7.2.0' 10 | -------------------------------------------------------------------------------- /environments/Gemfile.rails-edge.rb: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activerecord', git: 'https://github.com/rails/rails.git', branch: 'main' 4 | gem 'actionpack', git: 'https://github.com/rails/rails.git', branch: 'main' 5 | 6 | gem 'thread_safe' 7 | 8 | gem 'rspec', '~> 3.12' 9 | gem 'mocha', '~> 2.0' 10 | 11 | gem 'sqlite3', '~> 1.4.0' 12 | 13 | gem 'mysql2', '~> 0.5.2', :group => :mysql 14 | gem 'pg', '~> 1.2', :group => :pg 15 | -------------------------------------------------------------------------------- /environments/Gemfile.rails5.0.rb: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | rails_version = '~> 5.0.7' 4 | 5 | gem 'activerecord', rails_version 6 | gem 'actionpack', rails_version 7 | gem 'rails-dom-testing' 8 | 9 | gem 'rspec', '~> 3.12' 10 | gem 'mocha', '~> 2.0' 11 | 12 | gem 'sqlite3', '~> 1.3.6' 13 | 14 | gem 'mysql2', '~> 0.5.2', :group => :mysql 15 | gem 'pg', '~> 1.2.3', :group => :pg 16 | 17 | # ruby 2.4 compat re: nokogiri 18 | gem 'loofah', '< 2.21.0' 19 | -------------------------------------------------------------------------------- /environments/Gemfile.rails5.1.rb: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | rails_version = '~> 5.1.7' 4 | 5 | gem 'activerecord', rails_version 6 | gem 'actionpack', rails_version 7 | 8 | gem 'rspec', '~> 3.12' 9 | gem 'mocha', '~> 2.0' 10 | 11 | gem 'sqlite3', '~> 1.3.6' 12 | 13 | gem 'mysql2', '~> 0.5.2', :group => :mysql 14 | gem 'pg', '~> 1.2.3', :group => :pg 15 | 16 | # ruby 2.4 compat re: nokogiri 17 | gem 'loofah', '< 2.21.0' 18 | -------------------------------------------------------------------------------- /environments/Gemfile.rails5.2.rb: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | rails_version = '~> 5.2.4' 4 | 5 | gem 'activerecord', rails_version 6 | gem 'actionpack', rails_version 7 | 8 | gem 'rspec', '~> 3.12' 9 | gem 'mocha', '~> 2.0' 10 | 11 | gem 'sqlite3', '~> 1.3.6' 12 | gem 'mysql2', '~> 0.5.2', :group => :mysql 13 | gem 'pg', '~> 1.2.3', :group => :pg 14 | 15 | # ruby 2.4 compat re: nokogiri 16 | gem 'loofah', '< 2.21.0' 17 | -------------------------------------------------------------------------------- /environments/Gemfile.rails6.0.rb: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | rails_version = '~> 6.0.0' 4 | 5 | gem 'activerecord', rails_version 6 | gem 'actionpack', rails_version 7 | 8 | gem 'rspec', '~> 3.12' 9 | gem 'mocha', '~> 2.0' 10 | 11 | gem 'sqlite3', '~> 1.4.0' 12 | 13 | gem 'mysql2', '~> 0.5.2', :group => :mysql 14 | gem 'pg', '~> 1.2', :group => :pg 15 | -------------------------------------------------------------------------------- /environments/Gemfile.rails6.1.rb: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | rails_version = '~> 6.1.0' 4 | 5 | gem 'activerecord', rails_version 6 | gem 'actionpack', rails_version 7 | 8 | gem 'rspec', '~> 3.12' 9 | gem 'mocha', '~> 2.0' 10 | 11 | gem 'sqlite3', '~> 1.4.0' 12 | 13 | gem 'mysql2', '~> 0.5.2', :group => :mysql 14 | gem 'pg', '~> 1.2', :group => :pg 15 | -------------------------------------------------------------------------------- /environments/Gemfile.rails7.0.rb: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # We test against other Rails versions, too. See `environments/` 4 | rails_version = '~> 7.0.2' 5 | 6 | gem 'activerecord', rails_version 7 | gem 'actionpack', rails_version 8 | 9 | gem 'rspec', '~> 3.12' 10 | gem 'mocha', '~> 2.0' 11 | 12 | gem 'sqlite3', '~> 1.5.0' 13 | gem 'mysql2', '~> 0.5.2', :group => :mysql 14 | gem 'pg', '~> 1.2', :group => :pg 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | elsif defined?(Rails::Initializer) 8 | raise "will_paginate 3.0 is not compatible with Rails 2.3 or older" 9 | end 10 | 11 | if defined?(Sinatra) and Sinatra.respond_to? :register 12 | require 'will_paginate/view_helpers/sinatra' 13 | end 14 | -------------------------------------------------------------------------------- /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 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 | column_name = if rel.select_values.present? 87 | select = rel.select_values.join(", ") 88 | select if select !~ /[,*]/ 89 | end || :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 | total_entries <= offset_value 109 | else 110 | super 111 | end 112 | end 113 | 114 | def clone 115 | copy_will_paginate_data super 116 | end 117 | 118 | # workaround for Active Record 3.0 119 | def scoped(options = nil) 120 | copy_will_paginate_data super 121 | end 122 | 123 | def to_a 124 | if current_page.nil? then super # workaround for Active Record 3.0 125 | else 126 | ::WillPaginate::Collection.create(current_page, limit_value) do |col| 127 | col.replace super 128 | col.total_entries ||= total_entries 129 | end 130 | end 131 | end 132 | 133 | private 134 | 135 | def copy_will_paginate_data(other) 136 | other.current_page = current_page unless other.current_page 137 | other.total_entries = nil if defined? @total_entries_queried 138 | other 139 | end 140 | end 141 | 142 | module Pagination 143 | def paginate(options) 144 | options = options.dup 145 | pagenum = options.fetch(:page) { raise ArgumentError, ":page parameter required" } 146 | options.delete(:page) 147 | per_page = options.delete(:per_page) || self.per_page 148 | total = options.delete(:total_entries) 149 | 150 | if options.any? 151 | raise ArgumentError, "unsupported parameters: %p" % options.keys 152 | end 153 | 154 | rel = limit(per_page.to_i).page(pagenum) 155 | rel.total_entries = total.to_i unless total.blank? 156 | rel 157 | end 158 | 159 | def page(num) 160 | rel = if ::ActiveRecord::Relation === self 161 | self 162 | elsif !defined?(::ActiveRecord::Scoping) or ::ActiveRecord::Scoping::ClassMethods.method_defined? :with_scope 163 | # Active Record 3 164 | scoped 165 | else 166 | # Active Record 4 167 | all 168 | end 169 | 170 | rel = rel.extending(RelationMethods) 171 | pagenum = ::WillPaginate::PageNumber(num.nil? ? 1 : num) 172 | per_page = rel.limit_value || self.per_page 173 | rel = rel.offset(pagenum.to_offset(per_page).to_i) 174 | rel = rel.limit(per_page) unless rel.limit_value 175 | rel.current_page = pagenum 176 | rel 177 | end 178 | end 179 | 180 | module BaseMethods 181 | # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string 182 | # based on the params otherwise used by paginating finds: +page+ and 183 | # +per_page+. 184 | # 185 | # Example: 186 | # 187 | # @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000], 188 | # :page => params[:page], :per_page => 3 189 | # 190 | # A query for counting rows will automatically be generated if you don't 191 | # supply :total_entries. If you experience problems with this 192 | # generated SQL, you might want to perform the count manually in your 193 | # application. 194 | # 195 | def paginate_by_sql(sql, options) 196 | pagenum = options.fetch(:page) { raise ArgumentError, ":page parameter required" } || 1 197 | per_page = options[:per_page] || self.per_page 198 | total = options[:total_entries] 199 | 200 | WillPaginate::Collection.create(pagenum, per_page, total) do |pager| 201 | query = sanitize_sql(sql.dup) 202 | original_query = query.dup 203 | oracle = self.connection.adapter_name =~ /^(oracle|oci$)/i 204 | 205 | # add limit, offset 206 | if oracle 207 | query = <<-SQL 208 | SELECT * FROM ( 209 | SELECT rownum rnum, a.* FROM (#{query}) a 210 | WHERE rownum <= #{pager.offset + pager.per_page} 211 | ) WHERE rnum >= #{pager.offset} 212 | SQL 213 | elsif (self.connection.adapter_name =~ /^sqlserver/i) 214 | query << " OFFSET #{pager.offset} ROWS FETCH NEXT #{pager.per_page} ROWS ONLY" 215 | else 216 | query << " LIMIT #{pager.per_page} OFFSET #{pager.offset}" 217 | end 218 | 219 | # perfom the find 220 | pager.replace find_by_sql(query) 221 | 222 | unless pager.total_entries 223 | count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s.]+$/mi, '' 224 | count_query = "SELECT COUNT(*) FROM (#{count_query})" 225 | count_query << ' AS count_table' unless oracle 226 | # perform the count query 227 | pager.total_entries = count_by_sql(count_query) 228 | end 229 | end 230 | end 231 | end 232 | 233 | # mix everything into Active Record 234 | ::ActiveRecord::Base.extend PerPage 235 | ::ActiveRecord::Base.extend Pagination 236 | ::ActiveRecord::Base.extend BaseMethods 237 | 238 | klasses = [::ActiveRecord::Relation] 239 | if defined? ::ActiveRecord::Associations::CollectionProxy 240 | klasses << ::ActiveRecord::Associations::CollectionProxy 241 | else 242 | klasses << ::ActiveRecord::Associations::AssociationCollection 243 | end 244 | 245 | # support pagination on associations and scopes 246 | klasses.each { |klass| klass.send(:include, Pagination) } 247 | end 248 | end 249 | -------------------------------------------------------------------------------- /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/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/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(/::/, '/'). 24 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). 25 | gsub(/([a-z\d])([A-Z])/,'\1_\2'). 26 | tr("-", "_"). 27 | downcase 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /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.logger) && 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, &block) 35 | message = block_given? ? block : 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/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 = {}, &block) 12 | if defined? ::I18n 13 | defaults = Array(keys).dup 14 | defaults << block 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 | -------------------------------------------------------------------------------- /lib/will_paginate/locale/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | will_paginate: 3 | previous_label: "← Previous" 4 | previous_aria_label: "Previous page" 5 | next_label: "Next →" 6 | next_aria_label: "Next page" 7 | page_gap: "…" 8 | container_aria_label: "Pagination" 9 | page_aria_label: "Page %{page}" 10 | 11 | page_entries_info: 12 | single_page: 13 | zero: "No %{model} found" 14 | one: "Displaying 1 %{model}" 15 | other: "Displaying all %{count} %{model}" 16 | single_page_html: 17 | zero: "No %{model} found" 18 | one: "Displaying 1 %{model}" 19 | other: "Displaying all %{count} %{model}" 20 | 21 | multi_page: "Displaying %{model} %{from} - %{to} of %{count} in total" 22 | multi_page_html: "Displaying %{model} %{from} - %{to} of %{count} in total" 23 | 24 | # models: 25 | # entry: 26 | # zero: entries 27 | # one: entry 28 | # few: entries 29 | # other: entries 30 | 31 | # line_item: 32 | # page_entries_info: 33 | # single_page: 34 | # zero: "Your shopping cart is empty" 35 | # one: "Displaying one item in your cart" 36 | # other: "Displaying all %{count} items" 37 | # multi_page: "Displaying items %{from} - %{to} of %{count} in total" 38 | -------------------------------------------------------------------------------- /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 | @total_entries = options.delete(:total_entries) 12 | 13 | pp = (options[:per_page] || per_page || WillPaginate.per_page).to_i 14 | limit(pp).skip(@page_multiplier * pp) 15 | end 16 | 17 | def per_page(value = :non_given) 18 | if value == :non_given 19 | options[:limit] == 0 ? nil : options[:limit] # in new Mongoid versions a nil limit is saved as 0 20 | else 21 | limit(value) 22 | end 23 | end 24 | 25 | def page(page) 26 | paginate(:page => page) 27 | end 28 | end 29 | 30 | module CollectionMethods 31 | attr_reader :current_page 32 | 33 | def total_entries 34 | @total_entries ||= count 35 | end 36 | 37 | def total_pages 38 | (total_entries / per_page.to_f).ceil 39 | end 40 | 41 | def offset 42 | @page_multiplier * per_page 43 | end 44 | end 45 | 46 | ::Mongoid::Criteria.send(:include, CriteriaMethods) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/will_paginate/page_number.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module WillPaginate 4 | # a module that page number exceptions are tagged with 5 | module InvalidPage; end 6 | 7 | # integer representing a page number 8 | class PageNumber < Numeric 9 | # a value larger than this is not supported in SQL queries 10 | BIGINT = 9223372036854775807 11 | 12 | extend Forwardable 13 | 14 | def initialize(value, name) 15 | value = Integer(value) 16 | if 'offset' == name ? (value < 0 or value > BIGINT) : value < 1 17 | raise RangeError, "invalid #{name}: #{value.inspect}" 18 | end 19 | @name = name 20 | @value = value 21 | rescue ArgumentError, TypeError, RangeError => error 22 | error.extend InvalidPage 23 | raise error 24 | end 25 | 26 | def to_i 27 | @value 28 | end 29 | 30 | def_delegators :@value, :coerce, :==, :<=>, :to_s, :+, :-, :*, :/, :to_json 31 | 32 | def inspect 33 | "#{@name} #{to_i}" 34 | end 35 | 36 | def to_offset(per_page) 37 | PageNumber.new((to_i - 1) * per_page.to_i, 'offset') 38 | end 39 | 40 | def kind_of?(klass) 41 | super || to_i.kind_of?(klass) 42 | end 43 | alias is_a? kind_of? 44 | end 45 | 46 | # An idemptotent coercion method 47 | def self.PageNumber(value, name = 'page') 48 | case value 49 | when PageNumber then value 50 | else PageNumber.new(value, name) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/will_paginate/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate/page_number' 2 | require 'will_paginate/collection' 3 | require 'will_paginate/i18n' 4 | 5 | module WillPaginate 6 | class Railtie < Rails::Railtie 7 | initializer "will_paginate" do |app| 8 | ActiveSupport.on_load :active_record do 9 | require 'will_paginate/active_record' 10 | end 11 | 12 | ActiveSupport.on_load :action_controller do 13 | WillPaginate::Railtie.setup_actioncontroller 14 | end 15 | 16 | ActiveSupport.on_load :action_view do 17 | require 'will_paginate/view_helpers/action_view' 18 | end 19 | 20 | # early access to ViewHelpers.pagination_options 21 | require 'will_paginate/view_helpers' 22 | end 23 | 24 | def self.setup_actioncontroller 25 | ( defined?(ActionDispatch::ExceptionWrapper) ? 26 | ActionDispatch::ExceptionWrapper : ActionDispatch::ShowExceptions 27 | ).send :include, ShowExceptionsPatch 28 | ActionController::Base.extend ControllerRescuePatch 29 | end 30 | 31 | # Extending the exception handler middleware so it properly detects 32 | # WillPaginate::InvalidPage regardless of it being a tag module. 33 | module ShowExceptionsPatch 34 | extend ActiveSupport::Concern 35 | included do 36 | alias_method :status_code_without_paginate, :status_code 37 | alias_method :status_code, :status_code_with_paginate 38 | end 39 | def status_code_with_paginate(exception = @exception) 40 | actual_exception = if exception.respond_to?(:cause) 41 | exception.cause || exception 42 | elsif exception.respond_to?(:original_exception) 43 | exception.original_exception 44 | else 45 | exception 46 | end 47 | 48 | if actual_exception.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, **kwargs, &block) 63 | if idx = args.index(WillPaginate::InvalidPage) 64 | args[idx] = args[idx].name 65 | end 66 | super(*args, **kwargs, &block) 67 | end 68 | end 69 | end 70 | end 71 | 72 | ActiveSupport.on_load :i18n do 73 | I18n.load_path.concat(WillPaginate::I18n.load_path) 74 | end 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/will_paginate/version.rb: -------------------------------------------------------------------------------- 1 | module WillPaginate #:nodoc: 2 | module VERSION #:nodoc: 3 | MAJOR = 4 4 | MINOR = 0 5 | TINY = 1 6 | 7 | STRING = [MAJOR, MINOR, TINY].join('.') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /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', 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 => ' ', # 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] ||= will_paginate_translate(:previous_label) { '← Previous' } 78 | options[:next_label] ||= will_paginate_translate(:next_label) { 'Next →' } 79 | 80 | # get the renderer instance 81 | renderer = case options[:renderer] 82 | when nil 83 | raise ArgumentError, ":renderer not specified" 84 | when String 85 | klass = if options[:renderer].respond_to? :constantize then options[:renderer].constantize 86 | else Object.const_get(options[:renderer]) # poor man's constantize 87 | end 88 | klass.new 89 | when Class then options[:renderer].new 90 | else options[:renderer] 91 | end 92 | # render HTML for pagination 93 | renderer.prepare collection, options, self 94 | output = renderer.to_html 95 | output = output.html_safe if output.respond_to?(:html_safe) 96 | output 97 | end 98 | 99 | # Renders a message containing number of displayed vs. total entries. 100 | # 101 | # <%= page_entries_info @posts %> 102 | # #-> Displaying posts 6 - 12 of 26 in total 103 | # 104 | # The default output contains HTML. Use ":html => false" for plain text. 105 | def page_entries_info(collection, options = {}) 106 | model = options[:model] 107 | model = collection.first.class unless model or collection.empty? 108 | model ||= 'entry' 109 | model_key = if model.respond_to? :model_name 110 | model.model_name.i18n_key # ActiveModel::Naming 111 | else 112 | model.to_s.underscore 113 | end 114 | 115 | if options.fetch(:html, true) 116 | b, eb = '', '' 117 | sp = ' ' 118 | html_key = '_html' 119 | else 120 | b = eb = html_key = '' 121 | sp = ' ' 122 | end 123 | 124 | model_count = collection.total_pages > 1 ? 5 : collection.size 125 | defaults = ["models.#{model_key}"] 126 | defaults << Proc.new { |_, opts| 127 | if model.respond_to? :model_name 128 | model.model_name.human(:count => opts[:count]) 129 | else 130 | name = model_key.to_s.tr('_', ' ') 131 | raise "can't pluralize model name: #{model.inspect}" unless name.respond_to? :pluralize 132 | opts[:count] == 1 ? name : name.pluralize 133 | end 134 | } 135 | model_name = will_paginate_translate defaults, :count => model_count 136 | 137 | if collection.total_pages < 2 138 | i18n_key = :"page_entries_info.single_page#{html_key}" 139 | keys = [:"#{model_key}.#{i18n_key}", i18n_key] 140 | 141 | will_paginate_translate keys, :count => collection.total_entries, :model => model_name do |_, opts| 142 | case opts[:count] 143 | when 0; "No #{opts[:model]} found" 144 | when 1; "Displaying #{b}1#{eb} #{opts[:model]}" 145 | else "Displaying #{b}all#{sp}#{opts[:count]}#{eb} #{opts[:model]}" 146 | end 147 | end 148 | else 149 | i18n_key = :"page_entries_info.multi_page#{html_key}" 150 | keys = [:"#{model_key}.#{i18n_key}", i18n_key] 151 | params = { 152 | :model => model_name, :count => collection.total_entries, 153 | :from => collection.offset + 1, :to => collection.offset + collection.length 154 | } 155 | will_paginate_translate keys, params do |_, opts| 156 | %{Displaying %s #{b}%d#{sp}-#{sp}%d#{eb} of #{b}%d#{eb} in total} % 157 | [ opts[:model], opts[:from], opts[:to], opts[:count] ] 158 | end 159 | end 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /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}" 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 | GET_PARAMS_BLACKLIST = [:script_name, :original_script_name] 103 | 104 | def default_url_params 105 | {} 106 | end 107 | 108 | def url(page) 109 | @base_url_params ||= begin 110 | url_params = merge_get_params(default_url_params) 111 | url_params[:only_path] = true 112 | merge_optional_params(url_params) 113 | end 114 | 115 | url_params = @base_url_params.dup 116 | add_current_page_param(url_params, page) 117 | 118 | @template.url_for(url_params) 119 | end 120 | 121 | def merge_get_params(url_params) 122 | if @template.respond_to?(:request) and @template.request 123 | if @template.request.get? 124 | symbolized_update(url_params, @template.params, GET_PARAMS_BLACKLIST) 125 | elsif @template.request.respond_to?(:query_parameters) 126 | symbolized_update(url_params, @template.request.query_parameters, GET_PARAMS_BLACKLIST) 127 | end 128 | end 129 | url_params 130 | end 131 | 132 | def merge_optional_params(url_params) 133 | symbolized_update(url_params, @options[:params]) if @options[:params] 134 | url_params 135 | end 136 | 137 | def add_current_page_param(url_params, page) 138 | unless param_name.index(/[^\w-]/) 139 | url_params[param_name.to_sym] = page 140 | else 141 | page_param = parse_query_parameters("#{param_name}=#{page}") 142 | symbolized_update(url_params, page_param) 143 | end 144 | end 145 | 146 | private 147 | 148 | def parse_query_parameters(params) 149 | Rack::Utils.parse_nested_query(params) 150 | end 151 | end 152 | 153 | ::ActionView::Base.send :include, self 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/will_paginate/view_helpers/hanami.rb: -------------------------------------------------------------------------------- 1 | require 'hanami/view' 2 | require 'will_paginate/view_helpers' 3 | require 'will_paginate/view_helpers/link_renderer' 4 | 5 | module WillPaginate 6 | module Hanami 7 | module Helpers 8 | include ViewHelpers 9 | 10 | def will_paginate(collection, options = {}) #:nodoc: 11 | options = options.merge(:renderer => LinkRenderer) unless options[:renderer] 12 | str = super(collection, options) 13 | str && raw(str) 14 | end 15 | end 16 | 17 | class LinkRenderer < ViewHelpers::LinkRenderer 18 | protected 19 | 20 | def url(page) 21 | str = File.join(request_env['SCRIPT_NAME'].to_s, request_env['PATH_INFO']) 22 | params = request_env['rack.request.query_hash'].merge(param_name.to_s => page.to_s) 23 | params.update @options[:params] if @options[:params] 24 | str << '?' << build_query(params) 25 | end 26 | 27 | def request_env 28 | @template.params.env 29 | end 30 | 31 | def build_query(params) 32 | Rack::Utils.build_nested_query params 33 | end 34 | end 35 | 36 | def self.included(base) 37 | base.include Helpers 38 | end 39 | 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /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?(Integer) ? 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 ||= { 39 | :role => 'navigation', 40 | :"aria-label" => @template.will_paginate_translate(:container_aria_label) { 'Pagination' } 41 | }.update @options.except(*(ViewHelpers.pagination_options.keys + [:renderer] - [:class])) 42 | end 43 | 44 | protected 45 | 46 | def page_number(page) 47 | aria_label = @template.will_paginate_translate(:page_aria_label, :page => page.to_i) { "Page #{page}" } 48 | if page == current_page 49 | tag(:em, page, :class => 'current', :"aria-label" => aria_label, :"aria-current" => 'page') 50 | else 51 | link(page, page, :rel => rel_value(page), :"aria-label" => aria_label) 52 | end 53 | end 54 | 55 | def gap 56 | text = @template.will_paginate_translate(:page_gap) { '…' } 57 | %(#{text}) 58 | end 59 | 60 | def previous_page 61 | num = @collection.current_page > 1 && @collection.current_page - 1 62 | aria_label = @template.will_paginate_translate(:previous_aria_label) { "Previous page" } 63 | previous_or_next_page(num, @options[:previous_label], 'previous_page', aria_label) 64 | end 65 | 66 | def next_page 67 | num = @collection.current_page < total_pages && @collection.current_page + 1 68 | aria_label = @template.will_paginate_translate(:next_aria_label) { "Next page" } 69 | previous_or_next_page(num, @options[:next_label], 'next_page', aria_label) 70 | end 71 | 72 | def previous_or_next_page(page, text, classname, aria_label = nil) 73 | if page 74 | link(text, page, :class => classname, :'aria-label' => aria_label) 75 | else 76 | tag(:span, text, :class => classname + ' disabled', :'aria-label' => aria_label) 77 | end 78 | end 79 | 80 | def html_container(html) 81 | tag(:div, html, container_attributes) 82 | end 83 | 84 | # Returns URL params for +page_link_or_span+, taking the current GET params 85 | # and :params option into account. 86 | def url(page) 87 | raise NotImplementedError 88 | end 89 | 90 | private 91 | 92 | def param_name 93 | @options[:param_name].to_s 94 | end 95 | 96 | def link(text, target, attributes = {}) 97 | if target.is_a?(Integer) 98 | attributes[:rel] = rel_value(target) 99 | target = url(target) 100 | end 101 | attributes[:href] = target 102 | tag(:a, text, attributes) 103 | end 104 | 105 | def tag(name, value, attributes = {}) 106 | string_attributes = attributes.map do |pair| 107 | unless pair.last.nil? 108 | %( #{pair.first}="#{CGI::escapeHTML(pair.last.to_s)}") 109 | end 110 | end 111 | "<#{name}#{string_attributes.compact.join("")}>#{value}" 112 | end 113 | 114 | def rel_value(page) 115 | case page 116 | when @collection.current_page - 1; 'prev' 117 | when @collection.current_page + 1; 'next' 118 | end 119 | end 120 | 121 | def symbolized_update(target, other, blacklist = nil) 122 | other.each_pair do |key, value| 123 | key = key.to_sym 124 | existing = target[key] 125 | next if blacklist && blacklist.include?(key) 126 | 127 | if value.respond_to?(:each_pair) and (existing.is_a?(Hash) or existing.nil?) 128 | symbolized_update(existing || (target[key] = {}), value) 129 | else 130 | target[key] = value 131 | end 132 | end 133 | end 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /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 either 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vi:ft=sh: 3 | set -e 4 | 5 | brew install libpq 6 | brew install --skip-post-install mysql@5.7 7 | 8 | bundle config --local build.mysql2 -- "$(ruby -r rbconfig -e 'puts RbConfig::CONFIG["configure_args"]' | xargs -n1 | grep with-openssl-dir)" 9 | bundle config --local build.pg -- --with-pg-config=$(brew --prefix libpq)/bin/pg_config 10 | bundle config --local path "$PWD/vendor/bundle" 11 | bundle install 12 | bundle binstubs rspec-core 13 | -------------------------------------------------------------------------------- /script/ci-matrix: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "yaml" 3 | 4 | ci_config = File.expand_path('../../.github/workflows/test.yml', __FILE__) 5 | data = YAML.load(File.read(ci_config)) 6 | matrix = data.dig('jobs', 'test-rails', 'strategy', 'matrix') 7 | ruby_versions = matrix.fetch('ruby') 8 | gemfiles = matrix.fetch('gemfile') 9 | 10 | requirements = { 11 | 'environments/Gemfile.rails5.0.rb' => ['>= 2.2', '< 3.0'], 12 | 'environments/Gemfile.rails5.1.rb' => ['>= 2.2', '< 3.0'], 13 | 'environments/Gemfile.rails5.2.rb' => ['>= 2.2', '< 3.0'], 14 | 'environments/Gemfile.rails6.0.rb' => '>= 2.5', 15 | 'environments/Gemfile.rails6.1.rb' => '>= 2.5', 16 | 'Gemfile' => '>= 2.7', 17 | 'environments/Gemfile.rails-edge.rb' => '>= 3.1', 18 | } 19 | 20 | commands = {} 21 | 22 | commands['excludes'] = -> { 23 | excludes = [] 24 | gemfiles.each do |gemfile| 25 | req = Gem::Requirement.new(requirements.fetch(gemfile)) 26 | ruby_versions.each do |version| 27 | unless req.satisfied_by?(Gem::Version.new(version)) 28 | excludes << { 'ruby' => version, 'gemfile' => gemfile } 29 | end 30 | end 31 | end 32 | 33 | matrix['exclude'] = excludes 34 | File.open(ci_config, 'w') do |file| 35 | yaml_str = YAML.dump(data) 36 | file.write(yaml_str) 37 | end 38 | } 39 | 40 | cmd = commands.fetch(ARGV[0]) do 41 | $stderr.puts "available commands: #{commands.keys.join(', ')}" 42 | exit 1 43 | end 44 | cmd.(*ARGV[1..-1]) 45 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail 3 | 4 | eval "$(gem build *.gemspec | awk '/(Name|Version|File): /{print tolower($1) $2}' | sed 's/:/=/')" 5 | 6 | git commit -m "${name} ${version}" -- lib/will_paginate/version.rb 7 | git tag "v${version}" 8 | git push origin HEAD "v${version}" 9 | 10 | gem push "$file" 11 | rm -rf "$file" 12 | 13 | gh release create "v${version}" --title "${name} ${version}" --generate-notes 14 | -------------------------------------------------------------------------------- /script/test_all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | binstubs_path="bin" 5 | if [[ -n $CI && $BUNDLE_GEMFILE == */* ]]; then 6 | binstubs_path="${BUNDLE_GEMFILE%/*}/bin" 7 | fi 8 | export PATH="${binstubs_path}:$PATH" 9 | 10 | if [[ $BUNDLE_GEMFILE == *non-rails* ]]; then 11 | echo "bin/rspec spec-non-rails" 12 | exec rspec spec-non-rails 13 | fi 14 | 15 | status=0 16 | for db in sqlite3 mysql postgres; do 17 | printf "\e[1;33m[DB] %s\e[m\n" "$db" 18 | echo "bin/rspec spec" 19 | DB="$db" rspec spec || status="$?" 20 | done 21 | exit $status 22 | -------------------------------------------------------------------------------- /spec-non-rails/mongoid_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative './spec_helper' 2 | require 'will_paginate/mongoid' 3 | 4 | RSpec.describe WillPaginate::Mongoid do 5 | 6 | class MongoidModel 7 | include Mongoid::Document 8 | end 9 | 10 | before(:all) do 11 | Mongoid.configure do |config| 12 | mongodb_host = ENV["MONGODB_HOST"] || "localhost" 13 | mongodb_port = ENV["MONGODB_PORT"] || "27017" 14 | config.clients.default = { 15 | hosts: ["#{mongodb_host}:#{mongodb_port}"], 16 | database: "will_paginate_test", 17 | } 18 | config.log_level = :warn 19 | end 20 | 21 | MongoidModel.delete_all 22 | 4.times { MongoidModel.create! } 23 | end 24 | 25 | let(:criteria) { MongoidModel.criteria } 26 | 27 | describe "#page" do 28 | it "should forward to the paginate method" do 29 | criteria.expects(:paginate).with(:page => 2).returns("itself") 30 | expect(criteria.page(2)).to eq("itself") 31 | end 32 | 33 | it "should not override per_page if set earlier in the chain" do 34 | expect(criteria.paginate(:per_page => 10).page(1).per_page).to eq(10) 35 | expect(criteria.paginate(:per_page => 20).page(1).per_page).to eq(20) 36 | end 37 | end 38 | 39 | describe "#per_page" do 40 | it "should set the limit if given an argument" do 41 | expect(criteria.per_page(10).options[:limit]).to eq(10) 42 | end 43 | 44 | it "should return the current limit if no argument is given" do 45 | expect(criteria.per_page).to eq(nil) 46 | expect(criteria.per_page(10).per_page).to eq(10) 47 | end 48 | 49 | it "should be interchangable with limit" do 50 | expect(criteria.limit(15).per_page).to eq(15) 51 | end 52 | 53 | it "should be nil'able" do 54 | expect(criteria.per_page(nil).per_page).to be_nil 55 | end 56 | end 57 | 58 | describe "#paginate" do 59 | it "should use criteria" do 60 | expect(criteria.paginate).to be_instance_of(::Mongoid::Criteria) 61 | end 62 | 63 | it "should not override page number if set earlier in the chain" do 64 | expect(criteria.page(3).paginate.current_page).to eq(3) 65 | end 66 | 67 | it "should limit according to per_page parameter" do 68 | expect(criteria.paginate(:per_page => 10).options).to include(:limit => 10) 69 | end 70 | 71 | it "should skip according to page and per_page parameters" do 72 | expect(criteria.paginate(:page => 2, :per_page => 5).options).to include(:skip => 5) 73 | end 74 | 75 | specify "first fallback value for per_page option is the current limit" do 76 | expect(criteria.limit(12).paginate.options).to include(:limit => 12) 77 | end 78 | 79 | specify "second fallback value for per_page option is WillPaginate.per_page" do 80 | expect(criteria.paginate.options).to include(:limit => WillPaginate.per_page) 81 | end 82 | 83 | specify "page should default to 1" do 84 | expect(criteria.paginate.options).to include(:skip => 0) 85 | end 86 | 87 | it "should convert strings to integers" do 88 | expect(criteria.paginate(:page => "2", :per_page => "3").options).to include(:limit => 3) 89 | end 90 | 91 | describe "collection compatibility" do 92 | describe "#total_count" do 93 | it "should be calculated correctly" do 94 | expect(criteria.paginate(:per_page => 1).total_entries).to eq(4) 95 | expect(criteria.paginate(:per_page => 3).total_entries).to eq(4) 96 | end 97 | 98 | it "should be cached" do 99 | criteria.expects(:count).once.returns(123) 100 | criteria.paginate 101 | 2.times { expect(criteria.total_entries).to eq(123) } 102 | end 103 | end 104 | 105 | it "should calculate total_pages" do 106 | expect(criteria.paginate(:per_page => 1).total_pages).to eq(4) 107 | expect(criteria.paginate(:per_page => 3).total_pages).to eq(2) 108 | expect(criteria.paginate(:per_page => 10).total_pages).to eq(1) 109 | end 110 | 111 | it "should return per_page" do 112 | expect(criteria.paginate(:per_page => 1).per_page).to eq(1) 113 | expect(criteria.paginate(:per_page => 5).per_page).to eq(5) 114 | end 115 | 116 | describe "#current_page" do 117 | it "should return current_page" do 118 | expect(criteria.paginate(:page => 1).current_page).to eq(1) 119 | expect(criteria.paginate(:page => 3).current_page).to eq(3) 120 | end 121 | 122 | it "should be casted to PageNumber" do 123 | page = criteria.paginate(:page => 1).current_page 124 | expect(page.instance_of? WillPaginate::PageNumber).to be 125 | end 126 | end 127 | 128 | it "should return offset" do 129 | expect(criteria.paginate(:page => 1).offset).to eq(0) 130 | expect(criteria.paginate(:page => 2, :per_page => 5).offset).to eq(5) 131 | expect(criteria.paginate(:page => 3, :per_page => 10).offset).to eq(20) 132 | end 133 | 134 | it "should not pollute plain mongoid criterias" do 135 | %w(total_entries total_pages current_page).each do |method| 136 | expect(criteria).not_to respond_to(method) 137 | end 138 | end 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec-non-rails/sequel_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative './spec_helper' 2 | require 'sequel' 3 | require 'will_paginate/sequel' 4 | 5 | Sequel.sqlite.create_table :cars do 6 | primary_key :id, :integer, :auto_increment => true 7 | column :name, :text 8 | column :notes, :text 9 | end 10 | 11 | RSpec.describe Sequel::Dataset::Pagination, 'extension' do 12 | 13 | class Car < Sequel::Model 14 | self.dataset = dataset.extension(:pagination) 15 | end 16 | 17 | it "should have the #paginate method" do 18 | expect(Car.dataset).to respond_to(:paginate) 19 | end 20 | 21 | it "should NOT have the #paginate_by_sql method" do 22 | expect(Car.dataset).not_to respond_to(:paginate_by_sql) 23 | end 24 | 25 | describe 'pagination' do 26 | before(:all) do 27 | Car.create(:name => 'Shelby', :notes => "Man's best friend") 28 | Car.create(:name => 'Aston Martin', :notes => "Woman's best friend") 29 | Car.create(:name => 'Corvette', :notes => 'King of the Jungle') 30 | end 31 | 32 | it "should imitate WillPaginate::Collection" do 33 | result = Car.dataset.paginate(1, 2) 34 | 35 | expect(result).not_to be_empty 36 | expect(result.size).to eq(2) 37 | expect(result.length).to eq(2) 38 | expect(result.total_entries).to eq(3) 39 | expect(result.total_pages).to eq(2) 40 | expect(result.per_page).to eq(2) 41 | expect(result.current_page).to eq(1) 42 | end 43 | 44 | it "should perform" do 45 | expect(Car.dataset.paginate(1, 2).all).to eq([Car[1], Car[2]]) 46 | end 47 | 48 | it "should be empty" do 49 | result = Car.dataset.paginate(3, 2) 50 | expect(result).to be_empty 51 | end 52 | 53 | it "should perform with #select and #order" do 54 | result = Car.select(Sequel.lit("name as foo")).order(:name).paginate(1, 2).all 55 | expect(result.size).to eq(2) 56 | expect(result.first.values[:foo]).to eq("Aston Martin") 57 | end 58 | 59 | it "should perform with #filter" do 60 | results = Car.filter(:name => 'Shelby').paginate(1, 2).all 61 | expect(results.size).to eq(1) 62 | expect(results.first).to eq(Car.find(:name => 'Shelby')) 63 | end 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /spec-non-rails/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | 3 | RSpec.configure do |config| 4 | config.mock_with :mocha 5 | config.expose_dsl_globally = false 6 | config.expect_with :rspec do |expectations| 7 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 8 | end 9 | config.shared_context_metadata_behavior = :apply_to_host_groups 10 | config.disable_monkey_patching! 11 | end 12 | -------------------------------------------------------------------------------- /spec/collection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'will_paginate/array' 2 | require 'spec_helper' 3 | 4 | RSpec.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 | expect(@simple.paginate(:page => 1, :per_page => 3)).to eq(%w( a b c )) 12 | end 13 | 14 | it "can be shorter than per_page if on last page" do 15 | expect(@simple.paginate(:page => 2, :per_page => 3)).to eq(%w( d e )) 16 | end 17 | 18 | it "should include whole collection if per_page permits" do 19 | expect(@simple.paginate(:page => 1, :per_page => 5)).to eq(@simple) 20 | end 21 | 22 | it "should be empty if out of bounds" do 23 | expect(@simple.paginate(:page => 2, :per_page => 5)).to 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 | expect(result.current_page).to eq(1) 29 | expect(result.size).to eq(30) 30 | end 31 | 32 | it "should give total_entries precedence over actual size" do 33 | expect(%w(a b c).paginate(:total_entries => 5).total_entries).to eq(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 | expect(pager.replace(entries)).to eq(entries) 40 | end 41 | 42 | expect(collection).to eq(entries) 43 | for method in %w(total_pages each offset size current_page per_page total_entries) 44 | expect(collection).to respond_to(method) 45 | end 46 | expect(collection).to be_kind_of(Array) 47 | expect(collection.entries).to be_instance_of(Array) 48 | # TODO: move to another expectation: 49 | expect(collection.offset).to eq(3) 50 | expect(collection.total_pages).to eq(4) 51 | expect(collection).not_to 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 | expect(collection.previous_page).to be_nil 58 | expect(collection.next_page).to eq(2) 59 | end 60 | 61 | it "should have both prev/next pages" do 62 | collection = create(2, 1, 3) 63 | expect(collection.previous_page).to eq(1) 64 | expect(collection.next_page).to eq(3) 65 | end 66 | 67 | it "should have next_page nil when on last page" do 68 | collection = create(3, 1, 3) 69 | expect(collection.previous_page).to eq(2) 70 | expect(collection.next_page).to 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 | expect(create(2, 3, 2)).to be_out_of_bounds 77 | end 78 | 79 | it "isn't out of bounds when inside collection" do 80 | expect(create(1, 3, 2)).not_to 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 | expect(collection).not_to be_out_of_bounds 86 | expect(collection.total_pages).to eq(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 | expect(collection.total_entries).to eq(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 | expect(collection.total_entries).to eq(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 | expect(collection.total_entries).to 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 | expect(collection.total_entries).to 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 | expect(collection.total_entries).to eq(0) 114 | end 115 | end 116 | 117 | it "should not respond to page_count anymore" do 118 | expect { create.page_count }.to raise_error(NoMethodError) 119 | end 120 | 121 | it "inherits per_page from global value" do 122 | collection = described_class.new(1) 123 | expect(collection.per_page).to eq(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 | -------------------------------------------------------------------------------- /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? '-seq' 5 | opts << '-rwill_paginate/sequel' << '-rfinders/sequel_test_connector' 6 | else 7 | opts << '-rconsole_fixtures' 8 | end 9 | 10 | exec 'bundle', 'exec', irb, '-Ilib:spec', *opts 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/database.yml: -------------------------------------------------------------------------------- 1 | sqlite3: 2 | database: ":memory:" 3 | adapter: sqlite3 4 | timeout: 500 5 | 6 | mysql: 7 | adapter: mysql2 8 | database: will_paginate 9 | username: <%= ENV["MYSQL_USER"] || "root" %> 10 | encoding: utf8 11 | <% if ENV["MYSQL_PORT"] %> 12 | host: <%= ENV["MYSQL_HOST"] %> 13 | port: <%= ENV["MYSQL_PORT"] %> 14 | <% elsif File.exist?("/var/run/mysql5/mysqld.sock") %> 15 | host: localhost 16 | socket: /var/run/mysql5/mysqld.sock 17 | <% elsif File.exist? "/tmp/mysql.sock" %> 18 | host: localhost 19 | socket: /tmp/mysql.sock 20 | <% end %> 21 | 22 | postgres: 23 | adapter: postgresql 24 | database: will_paginate 25 | min_messages: warning 26 | username: <%= ENV["POSTGRES_USER"] %> 27 | password: <%= ENV["POSTGRES_PASSWORD"] %> 28 | host: <%= ENV["POSTGRES_HOST"] %> 29 | port: <%= ENV["POSTGRES_PORT"] %> 30 | -------------------------------------------------------------------------------- /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 | 7 | RSpec.describe WillPaginate::ActiveRecord do 8 | 9 | extend ActiverecordTestConnector::FixtureSetup 10 | 11 | fixtures :topics, :replies, :users, :projects, :developers_projects 12 | 13 | it "should integrate with ActiveRecord::Base" do 14 | expect(ActiveRecord::Base).to respond_to(:paginate) 15 | end 16 | 17 | it "should paginate" do 18 | expect { 19 | users = User.paginate(:page => 1, :per_page => 5).to_a 20 | expect(users.length).to eq(5) 21 | }.to execute(2).queries 22 | end 23 | 24 | it "should fail when encountering unknown params" do 25 | expect { 26 | User.paginate :foo => 'bar', :page => 1, :per_page => 4 27 | }.to raise_error(ArgumentError) 28 | end 29 | 30 | describe "relation" do 31 | it "should return a relation" do 32 | rel = nil 33 | expect { 34 | rel = Developer.paginate(:page => 1) 35 | expect(rel.per_page).to eq(10) 36 | expect(rel.current_page).to eq(1) 37 | }.to execute(0).queries 38 | 39 | expect { 40 | expect(rel.total_pages).to eq(2) 41 | }.to execute(1).queries 42 | end 43 | 44 | it "should keep per-class per_page number" do 45 | rel = Developer.order('id').paginate(:page => 1) 46 | expect(rel.per_page).to eq(10) 47 | end 48 | 49 | it "should be able to change per_page number" do 50 | rel = Developer.order('id').paginate(:page => 1).limit(5) 51 | expect(rel.per_page).to eq(5) 52 | end 53 | 54 | it "remembers pagination in sub-relations" do 55 | rel = Topic.paginate(:page => 2, :per_page => 3) 56 | expect { 57 | expect(rel.total_entries).to eq(4) 58 | }.to execute(1).queries 59 | rel = rel.mentions_activerecord 60 | expect(rel.current_page).to eq(2) 61 | expect(rel.per_page).to eq(3) 62 | expect { 63 | expect(rel.total_entries).to eq(1) 64 | }.to execute(1).queries 65 | end 66 | 67 | it "supports the page() method" do 68 | rel = Developer.page('1').order('id') 69 | expect(rel.current_page).to eq(1) 70 | expect(rel.per_page).to eq(10) 71 | expect(rel.offset).to eq(0) 72 | 73 | rel = rel.limit(5).page(2) 74 | expect(rel.per_page).to eq(5) 75 | expect(rel.offset).to eq(5) 76 | end 77 | 78 | it "raises on invalid page number" do 79 | expect { 80 | Developer.page('foo') 81 | }.to raise_error(ArgumentError) 82 | end 83 | 84 | it "supports first limit() then page()" do 85 | rel = Developer.limit(3).page(3) 86 | expect(rel.offset).to eq(6) 87 | end 88 | 89 | it "supports first page() then limit()" do 90 | rel = Developer.page(3).limit(3) 91 | expect(rel.offset).to eq(6) 92 | end 93 | 94 | it "supports #first" do 95 | rel = Developer.order('id').page(2).per_page(4) 96 | expect(rel.first).to eq(users(:dev_5)) 97 | expect(rel.first(2)).to eq(users(:dev_5, :dev_6)) 98 | end 99 | 100 | it "supports #last" do 101 | rel = Developer.order('id').page(2).per_page(4) 102 | expect(rel.last).to eq(users(:dev_8)) 103 | expect(rel.last(2)).to eq(users(:dev_7, :dev_8)) 104 | expect(rel.page(3).last).to eq(users(:poor_jamis)) 105 | end 106 | end 107 | 108 | describe "counting" do 109 | it "should guess the total count" do 110 | expect { 111 | topics = Topic.paginate :page => 2, :per_page => 3 112 | expect(topics.total_entries).to eq(4) 113 | }.to execute(1).queries 114 | end 115 | 116 | it "should guess that there are no records" do 117 | expect { 118 | topics = Topic.where(:project_id => 999).paginate :page => 1, :per_page => 3 119 | expect(topics.total_entries).to eq(0) 120 | }.to execute(1).queries 121 | end 122 | 123 | it "forgets count in sub-relations" do 124 | expect { 125 | topics = Topic.paginate :page => 1, :per_page => 3 126 | expect(topics.total_entries).to eq(4) 127 | expect(topics.where('1 = 1').total_entries).to eq(4) 128 | }.to execute(2).queries 129 | end 130 | 131 | it "supports empty? method" do 132 | topics = Topic.paginate :page => 1, :per_page => 3 133 | expect { 134 | expect(topics).not_to be_empty 135 | }.to execute(1).queries 136 | end 137 | 138 | it "support empty? for grouped queries" do 139 | topics = Topic.group(:project_id).paginate :page => 1, :per_page => 3 140 | expect { 141 | expect(topics).not_to be_empty 142 | }.to execute(1).queries 143 | end 144 | 145 | it "supports `size` for grouped queries" do 146 | topics = Topic.group(:project_id).paginate :page => 1, :per_page => 3 147 | expect { 148 | expect(topics.size).to eq({nil=>2, 1=>2}) 149 | }.to execute(1).queries 150 | end 151 | 152 | it "overrides total_entries count with a fixed value" do 153 | expect { 154 | topics = Topic.paginate :page => 1, :per_page => 3, :total_entries => 999 155 | expect(topics.total_entries).to eq(999) 156 | # value is kept even in sub-relations 157 | expect(topics.where('1 = 1').total_entries).to eq(999) 158 | }.to execute(0).queries 159 | end 160 | 161 | it "supports a non-int for total_entries" do 162 | topics = Topic.paginate :page => 1, :per_page => 3, :total_entries => "999" 163 | expect(topics.total_entries).to eq(999) 164 | end 165 | 166 | it "overrides empty? count call with a total_entries fixed value" do 167 | expect { 168 | topics = Topic.paginate :page => 1, :per_page => 3, :total_entries => 999 169 | expect(topics).not_to be_empty 170 | }.to execute(0).queries 171 | end 172 | 173 | it "removes :include for count" do 174 | expect { 175 | developers = Developer.paginate(:page => 1, :per_page => 1).includes(:projects) 176 | expect(developers.total_entries).to eq(11) 177 | expect($query_sql.last).not_to match(/\bJOIN\b/) 178 | }.to execute(1).queries 179 | end 180 | 181 | it "keeps :include for count when they are referenced in :conditions" do 182 | developers = Developer.paginate(:page => 1, :per_page => 1).includes(:projects) 183 | with_condition = developers.where('projects.id > 1') 184 | with_condition = with_condition.references(:projects) if with_condition.respond_to?(:references) 185 | expect(with_condition.total_entries).to eq(1) 186 | 187 | expect($query_sql.last).to match(/\bJOIN\b/) 188 | end 189 | 190 | it "should count with group" do 191 | expect(Developer.group(:salary).page(1).total_entries).to eq(4) 192 | end 193 | 194 | it "should count with select" do 195 | expect(Topic.select('title, content').page(1).total_entries).to eq(4) 196 | end 197 | 198 | it "removes :reorder for count with group" do 199 | Project.group(:id).reorder(:id).page(1).total_entries 200 | expect($query_sql.last).not_to match(/\ORDER\b/) 201 | end 202 | 203 | it "should not have zero total_pages when the result set is empty" do 204 | expect(Developer.where("1 = 2").page(1).total_pages).to eq(1) 205 | end 206 | end 207 | 208 | it "should not ignore :select parameter when it says DISTINCT" do 209 | users = User.select('DISTINCT salary').paginate :page => 2 210 | expect(users.total_entries).to eq(5) 211 | end 212 | 213 | describe "paginate_by_sql" do 214 | it "should respond" do 215 | expect(User).to respond_to(:paginate_by_sql) 216 | end 217 | 218 | it "should paginate" do 219 | expect { 220 | sql = "select content from topics where content like '%futurama%'" 221 | topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 1 222 | expect(topics.total_entries).to eq(1) 223 | expect(topics.first.attributes.has_key?('title')).to be(false) 224 | }.to execute(2).queries 225 | end 226 | 227 | it "should respect total_entries setting" do 228 | expect { 229 | sql = "select content from topics" 230 | topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 1, :total_entries => 999 231 | expect(topics.total_entries).to eq(999) 232 | }.to execute(1).queries 233 | end 234 | 235 | it "defaults to page 1" do 236 | sql = "select content from topics" 237 | topics = Topic.paginate_by_sql sql, :page => nil, :per_page => 1 238 | expect(topics.current_page).to eq(1) 239 | expect(topics.size).to eq(1) 240 | end 241 | 242 | it "should strip the order when counting" do 243 | expected = topics(:ar) 244 | expect { 245 | sql = "select id, title, content from topics order by topics.title" 246 | topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 2 247 | expect(topics.first).to eq(expected) 248 | }.to execute(2).queries 249 | 250 | expect($query_sql.last).to include('COUNT') 251 | expect($query_sql.last).not_to include('order by topics.title') 252 | end 253 | 254 | it "shouldn't change the original query string" do 255 | query = 'select * from topics where 1 = 2' 256 | original_query = query.dup 257 | Topic.paginate_by_sql(query, :page => 1) 258 | expect(query).to eq(original_query) 259 | end 260 | end 261 | 262 | it "doesn't mangle options" do 263 | options = { :page => 1 } 264 | options.expects(:delete).never 265 | options_before = options.dup 266 | 267 | Topic.paginate(options) 268 | expect(options).to eq(options_before) 269 | end 270 | 271 | it "should get first page of Topics with a single query" do 272 | expect { 273 | result = Topic.paginate :page => nil 274 | result.to_a # trigger loading of records 275 | expect(result.current_page).to eq(1) 276 | expect(result.total_pages).to eq(1) 277 | expect(result.size).to eq(4) 278 | }.to execute(1).queries 279 | end 280 | 281 | it "should get second (inexistent) page of Topics, requiring 1 query" do 282 | expect { 283 | result = Topic.paginate :page => 2 284 | expect(result.total_pages).to eq(1) 285 | expect(result).to be_empty 286 | }.to execute(1).queries 287 | end 288 | 289 | describe "associations" do 290 | it "should paginate" do 291 | dhh = users(:david) 292 | expected_name_ordered = projects(:action_controller, :active_record) 293 | expected_id_ordered = projects(:active_record, :action_controller) 294 | 295 | expect { 296 | # with association-specified order 297 | result = ignore_deprecation { 298 | dhh.projects.includes(:topics).order('projects.name').paginate(:page => 1) 299 | } 300 | expect(result.to_a).to eq(expected_name_ordered) 301 | expect(result.total_entries).to eq(2) 302 | }.to execute(2).queries 303 | 304 | # with explicit order 305 | result = dhh.projects.paginate(:page => 1).reorder('projects.id') 306 | expect(result).to eq(expected_id_ordered) 307 | expect(result.total_entries).to eq(2) 308 | 309 | expect { 310 | dhh.projects.order('projects.id').limit(4).to_a 311 | }.not_to raise_error 312 | 313 | result = dhh.projects.paginate(:page => 1, :per_page => 4).reorder('projects.id') 314 | expect(result).to eq(expected_id_ordered) 315 | 316 | # has_many with implicit order 317 | topic = Topic.find(1) 318 | expected = replies(:spam, :witty_retort) 319 | # FIXME: wow, this is ugly 320 | expect(topic.replies.paginate(:page => 1).map(&:id).sort).to eq(expected.map(&:id).sort) 321 | expect(topic.replies.paginate(:page => 1).reorder('replies.id ASC')).to eq(expected.reverse) 322 | end 323 | 324 | it "should paginate through association extension" do 325 | project = Project.order('id').first 326 | expected = [replies(:brave)] 327 | 328 | expect { 329 | result = project.replies.only_recent.paginate(:page => 1) 330 | expect(result).to eq(expected) 331 | }.to execute(1).queries 332 | end 333 | end 334 | 335 | it "should paginate with joins" do 336 | result = nil 337 | join_sql = 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id' 338 | 339 | expect { 340 | result = Developer.where('developers_projects.project_id = 1').joins(join_sql).paginate(:page => 1) 341 | result.to_a # trigger loading of records 342 | expect(result.size).to eq(2) 343 | developer_names = result.map(&:name) 344 | expect(developer_names).to include('David') 345 | expect(developer_names).to include('Jamis') 346 | }.to execute(1).queries 347 | 348 | expect { 349 | expected = result.to_a 350 | result = Developer.where('developers_projects.project_id = 1').joins(join_sql).paginate(:page => 1) 351 | expect(result).to eq(expected) 352 | expect(result.total_entries).to eq(2) 353 | }.to execute(1).queries 354 | end 355 | 356 | it "should paginate with group" do 357 | result = nil 358 | expect { 359 | result = Developer.select('salary').order('salary').group('salary'). 360 | paginate(:page => 1, :per_page => 10).to_a 361 | }.to execute(1).queries 362 | 363 | expected = users(:david, :jamis, :dev_10, :poor_jamis).map(&:salary).sort 364 | expect(result.map(&:salary)).to eq(expected) 365 | end 366 | 367 | it "should not paginate with dynamic finder" do 368 | expect { 369 | Developer.paginate_by_salary(100000, :page => 1, :per_page => 5) 370 | }.to raise_error(NoMethodError) 371 | end 372 | 373 | describe "scopes" do 374 | it "should paginate" do 375 | result = Developer.poor.paginate :page => 1, :per_page => 1 376 | expect(result.size).to eq(1) 377 | expect(result.total_entries).to eq(2) 378 | end 379 | 380 | it "should paginate on habtm association" do 381 | project = projects(:active_record) 382 | expect { 383 | result = ignore_deprecation { project.developers.poor.paginate :page => 1, :per_page => 1 } 384 | expect(result.size).to eq(1) 385 | expect(result.total_entries).to eq(1) 386 | }.to execute(2).queries 387 | end 388 | 389 | it "should paginate on hmt association" do 390 | project = projects(:active_record) 391 | expected = [replies(:brave)] 392 | 393 | expect { 394 | result = project.replies.recent.paginate :page => 1, :per_page => 1 395 | expect(result).to eq(expected) 396 | expect(result.total_entries).to eq(1) 397 | }.to execute(2).queries 398 | end 399 | 400 | it "should paginate on has_many association" do 401 | project = projects(:active_record) 402 | expected = [topics(:ar)] 403 | 404 | expect { 405 | result = project.topics.mentions_activerecord.paginate :page => 1, :per_page => 1 406 | expect(result).to eq(expected) 407 | expect(result.total_entries).to eq(1) 408 | }.to execute(2).queries 409 | end 410 | end 411 | 412 | it "should not paginate an array of IDs" do 413 | expect { 414 | Developer.paginate((1..8).to_a, :per_page => 3, :page => 2, :order => 'id') 415 | }.to raise_error(ArgumentError) 416 | end 417 | 418 | it "errors out for invalid values" do |variable| 419 | expect { 420 | # page that results in an offset larger than BIGINT 421 | Project.page(307445734561825862) 422 | }.to raise_error(WillPaginate::InvalidPage, "invalid offset: 9223372036854775830") 423 | end 424 | end 425 | -------------------------------------------------------------------------------- /spec/finders/activerecord_test_connector.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'active_record/fixtures' 3 | require 'stringio' 4 | require 'erb' 5 | require 'time' 6 | require 'date' 7 | require 'yaml' 8 | 9 | # forward compatibility with Rails 7 (needed for time expressions within fixtures) 10 | class Time 11 | alias_method :to_fs, :to_s 12 | end unless Time.new.respond_to?(:to_fs) 13 | 14 | # monkeypatch needed for Ruby 3.1 & Rails 6.0 15 | YAML.module_eval do 16 | class << self 17 | alias_method :_load_orig, :load 18 | def load(yaml_str) 19 | _load_orig(yaml_str, permitted_classes: [Symbol, Date, Time]) 20 | end 21 | end 22 | end if YAML.method(:load).parameters.include?([:key, :permitted_classes]) 23 | 24 | $query_count = 0 25 | $query_sql = [] 26 | 27 | ignore_sql = / 28 | ^( 29 | PRAGMA | SHOW\ (max_identifier_length|search_path) | 30 | SELECT\ (currval|CAST|@@IDENTITY|@@ROWCOUNT) | 31 | SHOW\ ((FULL\ )?FIELDS|TABLES) 32 | )\b | 33 | \bFROM\ (sqlite_master|pg_tables|pg_attribute)\b 34 | /x 35 | 36 | ActiveSupport::Notifications.subscribe(/^sql\./) do |*args| 37 | payload = args.last 38 | unless payload[:name] =~ /^Fixture/ or payload[:sql] =~ ignore_sql 39 | $query_count += 1 40 | $query_sql << payload[:sql] 41 | end 42 | end 43 | 44 | module ActiverecordTestConnector 45 | extend self 46 | 47 | attr_accessor :connected 48 | 49 | FIXTURES_PATH = File.expand_path('../../fixtures', __FILE__) 50 | 51 | # Set our defaults 52 | self.connected = false 53 | 54 | def setup 55 | unless self.connected 56 | setup_connection 57 | load_schema 58 | add_load_path FIXTURES_PATH 59 | self.connected = true 60 | end 61 | end 62 | 63 | private 64 | 65 | module Autoloader 66 | def const_missing(name) 67 | super 68 | rescue NameError 69 | file = File.join(FIXTURES_PATH, name.to_s.underscore) 70 | if File.exist?("#{file}.rb") 71 | require file 72 | const_get(name) 73 | else 74 | raise $! 75 | end 76 | end 77 | end 78 | 79 | def add_load_path(path) 80 | if ActiveSupport::Dependencies.respond_to?(:autoloader=) 81 | Object.singleton_class.include Autoloader 82 | else 83 | dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies 84 | dep.autoload_paths.unshift path 85 | end 86 | end 87 | 88 | def setup_connection 89 | db = ENV['DB'].blank?? 'sqlite3' : ENV['DB'] 90 | 91 | erb = ERB.new(File.read(File.expand_path('../../database.yml', __FILE__))) 92 | configurations = YAML.load(erb.result) 93 | raise "no configuration for '#{db}'" unless configurations.key? db 94 | configuration = configurations[db] 95 | 96 | # ActiveRecord::Base.logger = Logger.new(STDOUT) if $0 == 'irb' 97 | puts "using #{configuration['adapter']} adapter" 98 | 99 | ActiveRecord::Base.configurations = { db => configuration } 100 | ActiveRecord::Base.establish_connection(db.to_sym) 101 | if ActiveRecord.respond_to?(:default_timezone=) 102 | ActiveRecord.default_timezone = :utc 103 | else 104 | ActiveRecord::Base.default_timezone = :utc 105 | end 106 | end 107 | 108 | def load_schema 109 | begin 110 | $stdout = StringIO.new 111 | ActiveRecord::Migration.verbose = false 112 | load File.join(FIXTURES_PATH, 'schema.rb') 113 | ensure 114 | $stdout = STDOUT 115 | end 116 | end 117 | 118 | module FixtureSetup 119 | def fixtures(*tables) 120 | table_names = tables.map { |t| t.to_s } 121 | 122 | fixtures = ActiveRecord::FixtureSet.create_fixtures(ActiverecordTestConnector::FIXTURES_PATH, table_names) 123 | @@loaded_fixtures = {} 124 | @@fixture_cache = {} 125 | 126 | unless fixtures.nil? 127 | fixtures.each { |f| @@loaded_fixtures[f.table_name] = f } 128 | end 129 | 130 | table_names.each do |table_name| 131 | define_method(table_name) do |*names| 132 | @@fixture_cache[table_name] ||= {} 133 | 134 | instances = names.map do |name| 135 | if @@loaded_fixtures[table_name][name.to_s] 136 | @@fixture_cache[table_name][name] ||= @@loaded_fixtures[table_name][name.to_s].find 137 | else 138 | raise StandardError, "No fixture with name '#{name}' found for table '#{table_name}'" 139 | end 140 | end 141 | 142 | instances.size == 1 ? instances.first : instances 143 | end 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /spec/fixtures/admin.rb: -------------------------------------------------------------------------------- 1 | class Admin < User 2 | has_many :companies 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/developer.rb: -------------------------------------------------------------------------------- 1 | class Developer < User 2 | has_and_belongs_to_many :projects, :join_table => 'developers_projects' 3 | 4 | scope :poor, lambda { 5 | where(['salary <= ?', 80000]).order('salary') 6 | } 7 | 8 | def self.per_page() 10 end 9 | end 10 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /spec/fixtures/project.rb: -------------------------------------------------------------------------------- 1 | class Project < ActiveRecord::Base 2 | has_and_belongs_to_many :developers, :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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_fs(:db) %> 6 | 7 | another: 8 | id: 2 9 | topic_id: 2 10 | content: Nuh uh! 11 | created_at: <%= 1.hour.ago.utc.to_fs(:db) %> 12 | 13 | spam: 14 | id: 3 15 | topic_id: 1 16 | content: Nice site! 17 | created_at: <%= 1.hour.ago.utc.to_fs(: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_fs(: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_fs(:db) %> 30 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_fs(: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_fs(: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_fs(: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_fs(:db) %> 31 | -------------------------------------------------------------------------------- /spec/fixtures/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /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/matchers/query_count_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :execute do |expected_count| 2 | match do |block| 3 | run(block) 4 | 5 | if expected_count.respond_to? :include? 6 | expected_count.include? @count 7 | else 8 | @count == expected_count 9 | end 10 | end 11 | 12 | def run(block) 13 | $query_count = 0 14 | $query_sql = [] 15 | block.call 16 | ensure 17 | @queries = $query_sql.dup 18 | @count = $query_count 19 | end 20 | 21 | chain(:queries) {} 22 | supports_block_expectations 23 | 24 | failure_message do 25 | "expected #{expected_count} queries, got #{@count}\n#{@queries.join("\n")}" 26 | end 27 | 28 | failure_message_when_negated do 29 | "expected query count not to be #{expected_count}" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/page_number_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'will_paginate/page_number' 3 | require 'json' 4 | 5 | RSpec.describe WillPaginate::PageNumber do 6 | describe "valid" do 7 | def num 8 | WillPaginate::PageNumber.new('12', 'page') 9 | end 10 | 11 | it "== 12" do 12 | expect(num).to eq(12) 13 | end 14 | 15 | it "inspects to 'page 12'" do 16 | expect(num.inspect).to eq('page 12') 17 | end 18 | 19 | it "is a PageNumber" do 20 | expect(num.instance_of? WillPaginate::PageNumber).to be 21 | end 22 | 23 | it "is a kind of Numeric" do 24 | expect(num.is_a? Numeric).to be 25 | end 26 | 27 | it "is a kind of Integer" do 28 | expect(num.is_a? Integer).to be 29 | end 30 | 31 | it "isn't directly a Integer" do 32 | expect(num.instance_of? Integer).not_to be 33 | end 34 | 35 | it "passes the PageNumber=== type check" do |variable| 36 | expect(WillPaginate::PageNumber === num).to be 37 | end 38 | 39 | it "passes the Numeric=== type check" do |variable| 40 | expect(Numeric === num).to be 41 | end 42 | 43 | it "fails the Numeric=== type check" do |variable| 44 | expect(Integer === num).not_to be 45 | end 46 | 47 | it "serializes as JSON number" do 48 | expect(JSON.dump(page: num)).to eq('{"page":12}') 49 | end 50 | end 51 | 52 | describe "invalid" do 53 | def create(value, name = 'page') 54 | described_class.new(value, name) 55 | end 56 | 57 | it "errors out on non-int values" do 58 | expect { create(nil) }.to raise_error(WillPaginate::InvalidPage) 59 | expect { create('') }.to raise_error(WillPaginate::InvalidPage) 60 | expect { create('Schnitzel') }.to raise_error(WillPaginate::InvalidPage) 61 | end 62 | 63 | it "errors out on zero or less" do 64 | expect { create(0) }.to raise_error(WillPaginate::InvalidPage) 65 | expect { create(-1) }.to raise_error(WillPaginate::InvalidPage) 66 | end 67 | 68 | it "doesn't error out on zero for 'offset'" do 69 | expect { create(0, 'offset') }.not_to raise_error 70 | expect { create(-1, 'offset') }.to raise_error(WillPaginate::InvalidPage) 71 | end 72 | end 73 | 74 | describe "coercion method" do 75 | it "defaults to 'page' name" do 76 | num = WillPaginate::PageNumber(12) 77 | expect(num.inspect).to eq('page 12') 78 | end 79 | 80 | it "accepts a custom name" do 81 | num = WillPaginate::PageNumber(12, 'monkeys') 82 | expect(num.inspect).to eq('monkeys 12') 83 | end 84 | 85 | it "doesn't affect PageNumber instances" do 86 | num = WillPaginate::PageNumber(12) 87 | num2 = WillPaginate::PageNumber(num) 88 | expect(num2.object_id).to eq(num.object_id) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/per_page_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'will_paginate/per_page' 3 | 4 | RSpec.describe WillPaginate::PerPage do 5 | 6 | class MyModel 7 | extend WillPaginate::PerPage 8 | end 9 | 10 | it "has the default value" do 11 | expect(MyModel.per_page).to eq(30) 12 | 13 | WillPaginate.per_page = 10 14 | begin 15 | expect(MyModel.per_page).to eq(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 | expect(MyModel.per_page).to eq(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 | expect(MyModel.per_page).to eq(12) 34 | subclass = Class.new(MyModel) 35 | expect(subclass.per_page).to eq(12) 36 | ensure 37 | MyModel.send(:remove_instance_variable, '@per_page') 38 | end 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'view_helpers/view_example_group' 3 | 4 | Dir[File.expand_path('../matchers/*_matcher.rb', __FILE__)].each { |matcher| require matcher } 5 | 6 | RSpec::Matchers.alias_matcher :include_phrase, :include 7 | 8 | RSpec.configure do |config| 9 | config.include Module.new { 10 | protected 11 | 12 | def have_deprecation(msg) 13 | output(/^DEPRECATION WARNING: #{Regexp.escape(msg)}/).to_stderr 14 | end 15 | 16 | def ignore_deprecation 17 | if ActiveSupport::Deprecation.respond_to?(:silence) 18 | ActiveSupport::Deprecation.silence { yield } 19 | else 20 | yield 21 | end 22 | end 23 | } 24 | 25 | config.mock_with :mocha 26 | config.backtrace_exclusion_patterns << /view_example_group/ 27 | config.expose_dsl_globally = false 28 | config.expect_with :rspec do |expectations| 29 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 30 | end 31 | config.shared_context_metadata_behavior = :apply_to_host_groups 32 | config.disable_monkey_patching! 33 | end 34 | -------------------------------------------------------------------------------- /spec/view_helpers/action_view_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 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 'foo/bar' => 'foo#bar' 17 | get 'baz/list' => 'baz#list' 18 | end 19 | 20 | RSpec.describe WillPaginate::ActionView do 21 | 22 | before(:all) do 23 | I18n.load_path.concat WillPaginate::I18n.load_path 24 | I18n.enforce_available_locales = false 25 | 26 | ActionController::Parameters.permit_all_parameters = false 27 | end 28 | 29 | before(:each) do 30 | I18n.reload! 31 | end 32 | 33 | before(:each) do 34 | @assigns = {} 35 | @controller = DummyController.new 36 | @request = @controller.request 37 | @template = '<%= will_paginate collection, options %>' 38 | end 39 | 40 | attr_reader :assigns, :controller, :request 41 | 42 | def render(locals) 43 | lookup_context = [] 44 | lookup_context = ActionView::LookupContext.new(lookup_context) 45 | 46 | klass = ActionView::Base 47 | klass = klass.with_empty_template_cache if klass.respond_to?(:with_empty_template_cache) 48 | @view = klass.new(lookup_context, @assigns, @controller) 49 | @view.request = @request 50 | @view.singleton_class.send(:include, @controller._routes.url_helpers) 51 | @view.render(:inline => @template, :locals => locals) 52 | end 53 | 54 | ## basic pagination ## 55 | 56 | it "should render" do 57 | paginate do |pagination| 58 | assert_select 'a[href]', 3 do |elements| 59 | validate_page_numbers [2,3,2], elements 60 | expect(text(elements[2])).to eq('Next →') 61 | end 62 | assert_select 'span', 1 do |spans| 63 | expect(spans[0]['class']).to eq('previous_page disabled') 64 | expect(text(spans[0])).to eq('← Previous') 65 | end 66 | assert_select 'em.current', '1' 67 | expect(text(pagination[0])).to eq('← Previous 1 2 3 Next →') 68 | end 69 | end 70 | 71 | it "should override existing page param value" do 72 | request.params :page => 1 73 | paginate do |pagination| 74 | assert_select 'a[href]', 3 do |elements| 75 | validate_page_numbers [2,3,2], elements 76 | end 77 | end 78 | end 79 | 80 | it "should render nothing when there is only 1 page" do 81 | expect(paginate(:per_page => 30)).to be_empty 82 | end 83 | 84 | it "should paginate with options" do 85 | paginate({ :page => 2 }, :class => 'will_paginate', :previous_label => 'Prev', :next_label => 'Next') do 86 | assert_select 'a[href]', 4 do |elements| 87 | validate_page_numbers [1,1,3,3], elements 88 | # test rel attribute values: 89 | expect(text(elements[0])).to eq('Prev') 90 | expect(elements[0]['rel']).to eq('prev') 91 | expect(text(elements[1])).to eq('1') 92 | expect(elements[1]['rel']).to eq('prev') 93 | expect(text(elements[3])).to eq('Next') 94 | expect(elements[3]['rel']).to eq('next') 95 | end 96 | assert_select '.current', '2' 97 | end 98 | end 99 | 100 | it "should paginate using a custom renderer class" do 101 | paginate({}, :renderer => AdditionalLinkAttributesRenderer) do 102 | assert_select 'a[default=true]', 3 103 | end 104 | end 105 | 106 | it "should paginate using a custom renderer instance" do 107 | renderer = WillPaginate::ActionView::LinkRenderer.new 108 | def renderer.gap() '~~' end 109 | 110 | paginate({ :per_page => 2 }, :inner_window => 0, :outer_window => 0, :renderer => renderer) do 111 | assert_select 'span.my-gap', '~~' 112 | end 113 | 114 | renderer = AdditionalLinkAttributesRenderer.new(:title => 'rendered') 115 | paginate({}, :renderer => renderer) do 116 | assert_select 'a[title=rendered]', 3 117 | end 118 | end 119 | 120 | it "should have classnames on previous/next links" do 121 | paginate do |pagination| 122 | assert_select 'span.disabled.previous_page:first-child' 123 | assert_select 'a.next_page[href]:last-child' 124 | end 125 | end 126 | 127 | it "should match expected markup" do 128 | paginate 129 | expected = <<-HTML.strip.gsub(/\s{2,}/, ' ') 130 | 135 | HTML 136 | expected_dom = parse_html_document(expected) 137 | 138 | if expected_dom.respond_to?(:canonicalize) 139 | expect(html_document.canonicalize).to eq(expected_dom.canonicalize) 140 | else 141 | expect(html_document.root).to eq(expected_dom.root) 142 | end 143 | end 144 | 145 | it "should output escaped URLs" do 146 | paginate({:page => 1, :per_page => 1, :total_entries => 2}, 147 | :page_links => false, :params => { :tag => '
' }) 148 | 149 | assert_select 'a[href]', 1 do |links| 150 | query = links.first['href'].split('?', 2)[1] 151 | parts = query.gsub('&', '&').split('&').sort 152 | expect(parts).to eq(%w(page=2 tag=%3Cbr%3E)) 153 | end 154 | end 155 | 156 | ## advanced options for pagination ## 157 | 158 | it "should be able to render without container" do 159 | paginate({}, :container => false) 160 | assert_select 'div.pagination', 0, 'main DIV present when it shouldn\'t' 161 | assert_select 'a[href]', 3 162 | end 163 | 164 | it "should be able to render without page links" do 165 | paginate({ :page => 2 }, :page_links => false) do 166 | assert_select 'a[href]', 2 do |elements| 167 | validate_page_numbers [1,3], elements 168 | end 169 | end 170 | end 171 | 172 | ## other helpers ## 173 | 174 | it "should render a paginated section" do 175 | @template = <<-ERB 176 | <%= paginated_section collection, options do %> 177 | <%= content_tag :div, '', :id => "developers" %> 178 | <% end %> 179 | ERB 180 | 181 | paginate 182 | assert_select 'div.pagination', 2 183 | assert_select 'div.pagination + div#developers', 1 184 | end 185 | 186 | it "should not render a paginated section with a single page" do 187 | @template = <<-ERB 188 | <%= paginated_section collection, options do %> 189 | <%= content_tag :div, '', :id => "developers" %> 190 | <% end %> 191 | ERB 192 | 193 | paginate(:total_entries => 1) 194 | assert_select 'div.pagination', 0 195 | assert_select 'div#developers', 1 196 | end 197 | 198 | ## parameter handling in page links ## 199 | 200 | it "should preserve parameters on GET" do 201 | request.params :foo => { :bar => 'baz' } 202 | paginate 203 | assert_links_match /foo\[bar\]=baz/ 204 | end 205 | 206 | it "doesn't allow tampering with host, port, protocol" do 207 | request.params :host => 'disney.com', :port => '99', :protocol => 'ftp' 208 | paginate 209 | assert_links_match %r{^/foo/bar} 210 | assert_no_links_match /disney/ 211 | assert_no_links_match /99/ 212 | assert_no_links_match /ftp/ 213 | end 214 | 215 | it "doesn't allow tampering with script_name" do 216 | request.params :script_name => 'p0wned', :original_script_name => 'p0wned' 217 | paginate 218 | assert_links_match %r{^/foo/bar} 219 | assert_no_links_match /p0wned/ 220 | end 221 | 222 | it "should only preserve query parameters on POST" do 223 | request.post 224 | request.params :foo => 'bar' 225 | request.query_parameters = { :hello => 'world' } 226 | paginate 227 | assert_no_links_match /foo=bar/ 228 | assert_links_match /hello=world/ 229 | end 230 | 231 | it "should add additional parameters to links" do 232 | paginate({}, :params => { :foo => 'bar' }) 233 | assert_links_match /foo=bar/ 234 | end 235 | 236 | it "should add anchor parameter" do 237 | paginate({}, :params => { :anchor => 'anchor' }) 238 | assert_links_match /#anchor$/ 239 | end 240 | 241 | it "should remove arbitrary parameters" do 242 | request.params :foo => 'bar' 243 | paginate({}, :params => { :foo => nil }) 244 | assert_no_links_match /foo=bar/ 245 | end 246 | 247 | it "should override default route parameters" do 248 | paginate({}, :params => { :controller => 'baz', :action => 'list' }) 249 | assert_links_match %r{\Wbaz/list\W} 250 | end 251 | 252 | it "should paginate with custom page parameter" do 253 | paginate({ :page => 2 }, :param_name => :developers_page) do 254 | assert_select 'a[href]', 4 do |elements| 255 | validate_page_numbers [1,1,3,3], elements, :developers_page 256 | end 257 | end 258 | end 259 | 260 | it "should paginate with complex custom page parameter" do 261 | request.params :developers => { :page => 2 } 262 | 263 | paginate({ :page => 2 }, :param_name => 'developers[page]') do 264 | assert_select 'a[href]', 4 do |links| 265 | assert_links_match /\?developers\[page\]=\d+$/, links 266 | validate_page_numbers [1,1,3,3], links, 'developers[page]' 267 | end 268 | end 269 | end 270 | 271 | it "should paginate with custom route page parameter" do 272 | request.symbolized_path_parameters.update :controller => 'dummy', :action => 'index' 273 | paginate :per_page => 2 do 274 | assert_select 'a[href]', 6 do |links| 275 | assert_links_match %r{/page/(\d+)$}, links, [2, 3, 4, 5, 6, 2] 276 | end 277 | end 278 | end 279 | 280 | it "should paginate with custom route with dot separator page parameter" do 281 | request.symbolized_path_parameters.update :controller => 'dummy', :action => 'dots' 282 | paginate :per_page => 2 do 283 | assert_select 'a[href]', 6 do |links| 284 | assert_links_match %r{/page\.(\d+)$}, links, [2, 3, 4, 5, 6, 2] 285 | end 286 | end 287 | end 288 | 289 | it "should paginate with custom route and first page number implicit" do 290 | request.symbolized_path_parameters.update :controller => 'ibocorp', :action => 'index' 291 | paginate :page => 2, :per_page => 2 do 292 | assert_select 'a[href]', 7 do |links| 293 | assert_links_match %r{/ibocorp(?:/(\d+))?$}, links, [nil, nil, 3, 4, 5, 6, 3] 294 | end 295 | end 296 | # Routes.recognize_path('/ibocorp/2').should == {:page=>'2', :action=>'index', :controller=>'ibocorp'} 297 | # Routes.recognize_path('/ibocorp/foo').should == {:action=>'foo', :controller=>'ibocorp'} 298 | end 299 | 300 | ## internal hardcore stuff ## 301 | 302 | it "should be able to guess the collection name" do 303 | collection = mock 304 | collection.expects(:total_pages).returns(1) 305 | 306 | @template = '<%= will_paginate options %>' 307 | controller.controller_name = 'developers' 308 | assigns['developers'] = collection 309 | 310 | paginate(nil) 311 | end 312 | 313 | it "should fail if the inferred collection is nil" do 314 | @template = '<%= will_paginate options %>' 315 | controller.controller_name = 'developers' 316 | 317 | expect { 318 | paginate(nil) 319 | }.to raise_error(ActionView::TemplateError, /@developers/) 320 | end 321 | 322 | ## i18n 323 | 324 | it "is able to translate previous/next labels" do 325 | translation :will_paginate => { 326 | :previous_label => 'Go back', 327 | :next_label => 'Load more' 328 | } 329 | 330 | paginate do |pagination| 331 | assert_select 'span.disabled:first-child', 'Go back' 332 | assert_select 'a[rel=next]', 'Load more' 333 | end 334 | end 335 | 336 | it "renders using ActionView helpers on a custom object" do 337 | helper = Class.new { 338 | attr_reader :controller 339 | include ActionView::Helpers::UrlHelper 340 | include Routes.url_helpers 341 | include WillPaginate::ActionView 342 | }.new 343 | helper.default_url_options[:controller] = 'dummy' 344 | 345 | collection = WillPaginate::Collection.new(2, 1, 3) 346 | @render_output = helper.will_paginate(collection) 347 | 348 | assert_select 'a[href]', 4 do |links| 349 | urls = links.map {|l| l['href'] }.uniq 350 | expect(urls).to eq(['/dummy/page/1', '/dummy/page/3']) 351 | end 352 | end 353 | 354 | it "renders using ActionDispatch helper on a custom object" do 355 | helper = Class.new { 356 | include ActionDispatch::Routing::UrlFor 357 | include Routes.url_helpers 358 | include WillPaginate::ActionView 359 | }.new 360 | helper.default_url_options.update \ 361 | :only_path => true, 362 | :controller => 'dummy' 363 | 364 | collection = WillPaginate::Collection.new(2, 1, 3) 365 | @render_output = helper.will_paginate(collection) 366 | 367 | assert_select 'a[href]', 4 do |links| 368 | urls = links.map {|l| l['href'] }.uniq 369 | expect(urls).to eq(['/dummy/page/1', '/dummy/page/3']) 370 | end 371 | end 372 | 373 | # TODO: re-enable once Rails 6.1.4 ships 374 | it "page_entries_info" do 375 | @template = "<%= page_entries_info collection, options %>" 376 | output = render( 377 | collection: WillPaginate::Collection.new(1, 1, 3), 378 | options: {html: false}, 379 | ) 380 | expect(output).to eq("Displaying entries 1 - 0 of 3 in total") 381 | end 382 | 383 | private 384 | 385 | def translation(data) 386 | I18n.available_locales # triggers loading existing translations 387 | I18n.backend.store_translations(:en, data) 388 | end 389 | 390 | # Normalizes differences between HTML::Document and Nokogiri::HTML 391 | def text(node) 392 | node.inner_text.gsub('→', '→').gsub('←', '←') 393 | end 394 | end 395 | 396 | class AdditionalLinkAttributesRenderer < WillPaginate::ActionView::LinkRenderer 397 | def initialize(link_attributes = nil) 398 | super() 399 | @additional_link_attributes = link_attributes || { :default => 'true' } 400 | end 401 | 402 | def link(text, target, attributes = {}) 403 | super(text, target, attributes.merge(@additional_link_attributes)) 404 | end 405 | end 406 | 407 | class DummyController 408 | attr_reader :request 409 | attr_accessor :controller_name 410 | 411 | include ActionController::UrlFor 412 | include Routes.url_helpers 413 | 414 | def initialize 415 | @request = DummyRequest.new(self) 416 | end 417 | 418 | def params 419 | @request.params 420 | end 421 | 422 | def env 423 | {} 424 | end 425 | 426 | def _prefixes 427 | [] 428 | end 429 | end 430 | 431 | class IbocorpController < DummyController 432 | end 433 | 434 | class DummyRequest 435 | attr_accessor :symbolized_path_parameters 436 | alias :path_parameters :symbolized_path_parameters 437 | 438 | def initialize(controller) 439 | @controller = controller 440 | @get = true 441 | @params = {}.with_indifferent_access 442 | @symbolized_path_parameters = { :controller => 'foo', :action => 'bar' } 443 | end 444 | 445 | def routes 446 | @controller._routes 447 | end 448 | 449 | def get? 450 | @get 451 | end 452 | 453 | def post 454 | @get = false 455 | end 456 | 457 | def relative_url_root 458 | '' 459 | end 460 | 461 | def script_name 462 | '' 463 | end 464 | 465 | def params(more = nil) 466 | @params.update(more) if more 467 | ActionController::Parameters.new(@params) 468 | end 469 | 470 | def query_parameters 471 | if get? 472 | params 473 | else 474 | ActionController::Parameters.new(@query_parameters) 475 | end 476 | end 477 | 478 | def query_parameters=(more) 479 | @query_parameters = more.with_indifferent_access 480 | end 481 | 482 | def host_with_port 483 | 'example.com' 484 | end 485 | alias host host_with_port 486 | 487 | def optional_port 488 | '' 489 | end 490 | 491 | def protocol 492 | 'http:' 493 | end 494 | end 495 | -------------------------------------------------------------------------------- /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 | RSpec.describe WillPaginate::ViewHelpers do 9 | 10 | before(:all) do 11 | # make sure default translations aren't loaded 12 | I18n.load_path.clear 13 | I18n.enforce_available_locales = false 14 | end 15 | 16 | before(:each) do 17 | I18n.reload! 18 | end 19 | 20 | include WillPaginate::ViewHelpers 21 | 22 | describe "will_paginate" do 23 | it "should render" do 24 | collection = WillPaginate::Collection.new(1, 2, 4) 25 | renderer = mock 'Renderer' 26 | renderer.expects(:prepare).with(collection, instance_of(Hash), self) 27 | renderer.expects(:to_html).returns('') 28 | 29 | expect(will_paginate(collection, :renderer => renderer)).to eq('') 30 | end 31 | 32 | it "should return nil for single-page collections" do 33 | collection = mock 'Collection', :total_pages => 1 34 | expect(will_paginate(collection)).to be_nil 35 | end 36 | 37 | it "should call html_safe on result" do 38 | collection = WillPaginate::Collection.new(1, 2, 4) 39 | 40 | html = mock 'HTML' 41 | html.expects(:html_safe).returns(html) 42 | renderer = mock 'Renderer' 43 | renderer.stubs(:prepare) 44 | renderer.expects(:to_html).returns(html) 45 | 46 | expect(will_paginate(collection, :renderer => renderer)).to eql(html) 47 | end 48 | end 49 | 50 | describe "pagination_options" do 51 | let(:pagination_options) { WillPaginate::ViewHelpers.pagination_options } 52 | 53 | it "deprecates setting :renderer" do 54 | begin 55 | expect { 56 | pagination_options[:renderer] = 'test' 57 | }.to have_deprecation("pagination_options[:renderer] shouldn't be set") 58 | ensure 59 | pagination_options.delete :renderer 60 | end 61 | end 62 | end 63 | 64 | describe "page_entries_info" do 65 | before :all do 66 | @array = ('a'..'z').to_a 67 | end 68 | 69 | def info(params, options = {}) 70 | collection = Hash === params ? @array.paginate(params) : params 71 | page_entries_info collection, {:html => false}.merge(options) 72 | end 73 | 74 | it "should display middle results and total count" do 75 | expect(info(:page => 2, :per_page => 5)).to eq("Displaying strings 6 - 10 of 26 in total") 76 | end 77 | 78 | it "uses translation if available" do 79 | translation :will_paginate => { 80 | :page_entries_info => {:multi_page => 'Showing %{from} - %{to}'} 81 | } 82 | expect(info(:page => 2, :per_page => 5)).to eq("Showing 6 - 10") 83 | end 84 | 85 | it "uses specific translation if available" do 86 | translation :will_paginate => { 87 | :page_entries_info => {:multi_page => 'Showing %{from} - %{to}'}, 88 | :string => { :page_entries_info => {:multi_page => 'Strings %{from} to %{to}'} } 89 | } 90 | expect(info(:page => 2, :per_page => 5)).to eq("Strings 6 to 10") 91 | end 92 | 93 | it "should output HTML by default" do 94 | expect(info({ :page => 2, :per_page => 5 }, :html => true)).to eq( 95 | "Displaying strings 6 - 10 of 26 in total" 96 | ) 97 | end 98 | 99 | it "should display shortened end results" do 100 | expect(info(:page => 7, :per_page => 4)).to include_phrase('strings 25 - 26') 101 | end 102 | 103 | it "should handle longer class names" do 104 | collection = @array.paginate(:page => 2, :per_page => 5) 105 | model = stub('Class', :name => 'ProjectType', :to_s => 'ProjectType') 106 | collection.first.stubs(:class).returns(model) 107 | expect(info(collection)).to include_phrase('project types') 108 | end 109 | 110 | it "should adjust output for single-page collections" do 111 | expect(info(('a'..'d').to_a.paginate(:page => 1, :per_page => 5))).to eq("Displaying all 4 strings") 112 | expect(info(['a'].paginate(:page => 1, :per_page => 5))).to eq("Displaying 1 string") 113 | end 114 | 115 | it "should display 'no entries found' for empty collections" do 116 | expect(info([].paginate(:page => 1, :per_page => 5))).to eq("No entries found") 117 | end 118 | 119 | it "uses model_name.human when available" do 120 | name = stub('model name', :i18n_key => :flower_key) 121 | name.expects(:human).with(:count => 1).returns('flower') 122 | model = stub('Class', :model_name => name) 123 | collection = [1].paginate(:page => 1) 124 | 125 | expect(info(collection, :model => model)).to eq("Displaying 1 flower") 126 | end 127 | 128 | it "uses custom translation instead of model_name.human" do 129 | name = stub('model name', :i18n_key => :flower_key) 130 | name.expects(:human).never 131 | model = stub('Class', :model_name => name) 132 | translation :will_paginate => {:models => {:flower_key => 'tulip'}} 133 | collection = [1].paginate(:page => 1) 134 | 135 | expect(info(collection, :model => model)).to eq("Displaying 1 tulip") 136 | end 137 | 138 | private 139 | 140 | def translation(data) 141 | I18n.backend.store_translations(:en, data) 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /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 | RSpec.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 | expect { 13 | @renderer.pagination 14 | }.to raise_error(NoMethodError) 15 | end 16 | 17 | it "should prepare with collection and options" do 18 | prepare({}) 19 | expect(@renderer.send(:current_page)).to eq(1) 20 | end 21 | 22 | it "should have total_pages accessor" do 23 | prepare :total_pages => 42 24 | expect(@renderer.send(:total_pages)).to eq(42) 25 | end 26 | 27 | it "should clear old cached values when prepared" do 28 | prepare(:total_pages => 1) 29 | expect(@renderer.send(:total_pages)).to eq(1) 30 | # prepare with different object: 31 | prepare(:total_pages => 2) 32 | expect(@renderer.send(:total_pages)).to eq(2) 33 | end 34 | 35 | it "should have pagination definition" do 36 | prepare({ :total_pages => 1 }, :page_links => true) 37 | expect(@renderer.pagination).to eq([: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 | expect(@renderer.send(:windowed_page_numbers)).to eq(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 | -------------------------------------------------------------------------------- /spec/view_helpers/view_example_group.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'stringio' 3 | require 'minitest/assertions' 4 | require 'rails/dom/testing/assertions' 5 | require 'will_paginate/array' 6 | 7 | module ViewExampleGroup 8 | 9 | include Rails::Dom::Testing::Assertions::SelectorAssertions 10 | include Minitest::Assertions 11 | 12 | def assert(value, message) 13 | raise message unless value 14 | end 15 | 16 | def paginate(collection = {}, options = {}, &block) 17 | if collection.instance_of? Hash 18 | page_options = { :page => 1, :total_entries => 11, :per_page => 4 }.merge(collection) 19 | collection = [1].paginate(page_options) 20 | end 21 | 22 | locals = { :collection => collection, :options => options } 23 | 24 | @render_output = render(locals) 25 | @html_document = nil 26 | 27 | if block_given? 28 | classname = options[:class] || WillPaginate::ViewHelpers.pagination_options[:class] 29 | assert_select("div.#{classname}", 1, 'no main DIV', &block) 30 | end 31 | 32 | @render_output 33 | end 34 | 35 | def parse_html_document(html) 36 | Nokogiri::HTML::Document.parse(html) 37 | end 38 | 39 | def html_document 40 | @html_document ||= parse_html_document(@render_output) 41 | end 42 | 43 | def document_root_element 44 | html_document.root 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 | expect(links.map { |el| 55 | unescape_href(el) =~ param_pattern 56 | $1 ? $1.to_i : $1 57 | }).to eq(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 | expect(href).to match(pattern) 70 | if numbers 71 | href =~ pattern 72 | pages << ($1.nil?? nil : $1.to_i) 73 | end 74 | end 75 | 76 | expect(pages).to eq(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 | expect(unescape_href(el)).not_to match(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, :file_path => %r{spec/view_helpers/} 103 | end 104 | -------------------------------------------------------------------------------- /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 | s.required_ruby_version = '>= 2.0' 9 | 10 | s.summary = "Pagination plugin for web frameworks and other apps" 11 | s.description = "will_paginate provides a simple API for performing paginated queries with Active Record and Sequel, and includes helpers for rendering pagination links in Rails, Sinatra, and Hanami web apps." 12 | 13 | s.authors = ['Mislav Marohnić'] 14 | s.email = 'mislav.marohnic@gmail.com' 15 | s.homepage = 'https://github.com/mislav/will_paginate' 16 | s.license = 'MIT' 17 | 18 | s.metadata = { 19 | 'bug_tracker_uri' => 'https://github.com/mislav/will_paginate/issues', 20 | 'changelog_uri' => "https://github.com/mislav/will_paginate/releases/tag/v#{s.version}", 21 | 'documentation_uri' => "https://www.rubydoc.info/gems/will_paginate/#{s.version}", 22 | 'source_code_uri' => "https://github.com/mislav/will_paginate/tree/v#{s.version}", 23 | 'wiki_uri' => 'https://github.com/mislav/will_paginate/wiki' 24 | } 25 | 26 | s.rdoc_options = ['--main', 'README.md', '--charset=UTF-8'] 27 | s.extra_rdoc_files = ['README.md', 'LICENSE'] 28 | 29 | s.files = Dir['lib/**/*', 'README*', 'LICENSE*'] 30 | 31 | # include only files in version control 32 | git_dir = File.expand_path('../.git', __FILE__) 33 | void = defined?(File::NULL) ? File::NULL : 34 | RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw/ ? 'NUL' : '/dev/null' 35 | 36 | if File.directory?(git_dir) and system "git --version >>#{void} 2>&1" 37 | s.files &= `git --git-dir='#{git_dir}' ls-files -z`.split("\0") 38 | end 39 | end 40 | --------------------------------------------------------------------------------