├── .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 | # - ...
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}#{name}>"
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 |
--------------------------------------------------------------------------------