will_paginate 2.2.2 → 2.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of will_paginate might be problematic. Click here for more details.
- data/CHANGELOG.rdoc +110 -0
- data/README.rdoc +33 -61
- data/Rakefile +30 -93
- data/lib/will_paginate.rb +24 -20
- data/lib/will_paginate/collection.rb +7 -6
- data/lib/will_paginate/core_ext.rb +13 -2
- data/lib/will_paginate/finder.rb +42 -17
- data/lib/will_paginate/named_scope.rb +54 -16
- data/lib/will_paginate/named_scope_patch.rb +7 -9
- data/lib/will_paginate/version.rb +4 -4
- data/lib/will_paginate/view_helpers.rb +150 -74
- data/test/collection_test.rb +3 -0
- data/test/database.yml +3 -3
- data/test/finder_test.rb +71 -14
- data/test/fixtures/developer.rb +1 -0
- data/test/fixtures/topic.rb +4 -0
- data/test/helper.rb +4 -1
- data/test/lib/activerecord_test_case.rb +7 -0
- data/test/lib/activerecord_test_connector.rb +9 -3
- data/test/lib/view_test_process.rb +106 -0
- data/test/tasks.rake +59 -0
- data/test/view_test.rb +114 -85
- metadata +46 -20
- data/CHANGELOG +0 -49
@@ -33,12 +33,12 @@ module WillPaginate
|
|
33
33
|
#
|
34
34
|
# If you are writing a library that provides a collection which you would like
|
35
35
|
# to conform to this API, you don't have to copy these methods over; simply
|
36
|
-
# make your plugin/gem dependant on the "will_paginate" gem:
|
36
|
+
# make your plugin/gem dependant on the "mislav-will_paginate" gem:
|
37
37
|
#
|
38
|
-
# gem 'will_paginate'
|
38
|
+
# gem 'mislav-will_paginate'
|
39
39
|
# require 'will_paginate/collection'
|
40
40
|
#
|
41
|
-
# #
|
41
|
+
# # WillPaginate::Collection is now available for use
|
42
42
|
class Collection < Array
|
43
43
|
attr_reader :current_page, :per_page, :total_entries, :total_pages
|
44
44
|
|
@@ -82,7 +82,7 @@ module WillPaginate
|
|
82
82
|
#
|
83
83
|
# The Array#paginate API has since then changed, but this still serves as a
|
84
84
|
# fine example of WillPaginate::Collection usage.
|
85
|
-
def self.create(page, per_page, total = nil
|
85
|
+
def self.create(page, per_page, total = nil)
|
86
86
|
pager = new(page, per_page, total)
|
87
87
|
yield pager
|
88
88
|
pager
|
@@ -98,7 +98,7 @@ module WillPaginate
|
|
98
98
|
# Current offset of the paginated collection. If we're on the first page,
|
99
99
|
# it is always 0. If we're on the 2nd page and there are 30 entries per page,
|
100
100
|
# the offset is 30. This property is useful if you want to render ordinals
|
101
|
-
#
|
101
|
+
# side by side with records in the view: simply start with offset + 1.
|
102
102
|
def offset
|
103
103
|
(current_page - 1) * per_page
|
104
104
|
end
|
@@ -112,7 +112,8 @@ module WillPaginate
|
|
112
112
|
def next_page
|
113
113
|
current_page < total_pages ? (current_page + 1) : nil
|
114
114
|
end
|
115
|
-
|
115
|
+
|
116
|
+
# sets the <tt>total_entries</tt> property and calculates <tt>total_pages</tt>
|
116
117
|
def total_entries=(number)
|
117
118
|
@total_entries = number.to_i
|
118
119
|
@total_pages = (@total_entries / per_page.to_f).ceil
|
@@ -1,7 +1,18 @@
|
|
1
1
|
require 'set'
|
2
2
|
require 'will_paginate/array'
|
3
3
|
|
4
|
-
|
4
|
+
# helper to check for method existance in ruby 1.8- and 1.9-compatible way
|
5
|
+
# because `methods`, `instance_methods` and others return strings in 1.8 and symbols in 1.9
|
6
|
+
#
|
7
|
+
# ['foo', 'bar'].include_method?(:foo) # => true
|
8
|
+
class Array
|
9
|
+
def include_method?(name)
|
10
|
+
name = name.to_sym
|
11
|
+
!!(find { |item| item.to_sym == name })
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
unless Hash.instance_methods.include_method? :except
|
5
16
|
Hash.class_eval do
|
6
17
|
# Returns a new hash without the given keys.
|
7
18
|
def except(*keys)
|
@@ -16,7 +27,7 @@ unless Hash.instance_methods.include? 'except'
|
|
16
27
|
end
|
17
28
|
end
|
18
29
|
|
19
|
-
unless Hash.instance_methods.
|
30
|
+
unless Hash.instance_methods.include_method? :slice
|
20
31
|
Hash.class_eval do
|
21
32
|
# Returns a new hash with only the given keys.
|
22
33
|
def slice(*keys)
|
data/lib/will_paginate/finder.rb
CHANGED
@@ -61,7 +61,7 @@ module WillPaginate
|
|
61
61
|
#
|
62
62
|
# All other options (+conditions+, +order+, ...) are forwarded to +find+
|
63
63
|
# and +count+ calls.
|
64
|
-
def paginate(*args
|
64
|
+
def paginate(*args)
|
65
65
|
options = args.pop
|
66
66
|
page, per_page, total_entries = wp_parse_options(options)
|
67
67
|
finder = (options[:finder] || 'find').to_s
|
@@ -79,7 +79,7 @@ module WillPaginate
|
|
79
79
|
|
80
80
|
args << find_options
|
81
81
|
# @options_from_last_find = nil
|
82
|
-
pager.replace
|
82
|
+
pager.replace(send(finder, *args) { |*a| yield(*a) if block_given? })
|
83
83
|
|
84
84
|
# magic counting for user convenience:
|
85
85
|
pager.total_entries = wp_count(count_options, args, finder) unless pager.total_entries
|
@@ -94,9 +94,9 @@ module WillPaginate
|
|
94
94
|
# You can specify a starting page with <tt>:page</tt> (default is 1). Default
|
95
95
|
# <tt>:order</tt> is <tt>"id"</tt>, override if necessary.
|
96
96
|
#
|
97
|
-
# See http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord
|
98
|
-
# Jamis Buck describes this and
|
99
|
-
def paginated_each(options = {}
|
97
|
+
# See {Faking Cursors in ActiveRecord}[http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord]
|
98
|
+
# where Jamis Buck describes this and a more efficient way for MySQL.
|
99
|
+
def paginated_each(options = {})
|
100
100
|
options = { :order => 'id', :page => 1 }.merge options
|
101
101
|
options[:page] = options[:page].to_i
|
102
102
|
options[:total_entries] = 0 # skip the individual count queries
|
@@ -104,7 +104,10 @@ module WillPaginate
|
|
104
104
|
|
105
105
|
begin
|
106
106
|
collection = paginate(options)
|
107
|
-
|
107
|
+
with_exclusive_scope(:find => {}) do
|
108
|
+
# using exclusive scope so that the block is yielded in scope-free context
|
109
|
+
total += collection.each { |item| yield item }.size
|
110
|
+
end
|
108
111
|
options[:page] += 1
|
109
112
|
end until collection.size < collection.per_page
|
110
113
|
|
@@ -127,7 +130,7 @@ module WillPaginate
|
|
127
130
|
#
|
128
131
|
def paginate_by_sql(sql, options)
|
129
132
|
WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
|
130
|
-
query = sanitize_sql(sql)
|
133
|
+
query = sanitize_sql(sql.dup)
|
131
134
|
original_query = query.dup
|
132
135
|
# add limit, offset
|
133
136
|
add_limit! query, :offset => pager.offset, :limit => pager.per_page
|
@@ -138,7 +141,7 @@ module WillPaginate
|
|
138
141
|
count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
|
139
142
|
count_query = "SELECT COUNT(*) FROM (#{count_query})"
|
140
143
|
|
141
|
-
unless
|
144
|
+
unless self.connection.adapter_name =~ /^(oracle|oci$)/i
|
142
145
|
count_query << ' AS count_table'
|
143
146
|
end
|
144
147
|
# perform the count query
|
@@ -158,10 +161,14 @@ module WillPaginate
|
|
158
161
|
|
159
162
|
protected
|
160
163
|
|
161
|
-
def method_missing_with_paginate(method, *args
|
164
|
+
def method_missing_with_paginate(method, *args) #:nodoc:
|
162
165
|
# did somebody tried to paginate? if not, let them be
|
163
166
|
unless method.to_s.index('paginate') == 0
|
164
|
-
|
167
|
+
if block_given?
|
168
|
+
return method_missing_without_paginate(method, *args) { |*a| yield(*a) }
|
169
|
+
else
|
170
|
+
return method_missing_without_paginate(method, *args)
|
171
|
+
end
|
165
172
|
end
|
166
173
|
|
167
174
|
# paginate finders are really just find_* with limit and offset
|
@@ -174,16 +181,30 @@ module WillPaginate
|
|
174
181
|
options[:finder] = finder
|
175
182
|
args << options
|
176
183
|
|
177
|
-
paginate(*args
|
184
|
+
paginate(*args) { |*a| yield(*a) if block_given? }
|
178
185
|
end
|
179
186
|
|
180
187
|
# Does the not-so-trivial job of finding out the total number of entries
|
181
188
|
# in the database. It relies on the ActiveRecord +count+ method.
|
182
189
|
def wp_count(options, args, finder)
|
183
190
|
excludees = [:count, :order, :limit, :offset, :readonly]
|
184
|
-
|
191
|
+
excludees << :from unless ActiveRecord::Calculations::CALCULATIONS_OPTIONS.include?(:from)
|
192
|
+
|
193
|
+
# we may be in a model or an association proxy
|
194
|
+
klass = (@owner and @reflection) ? @reflection.klass : self
|
195
|
+
|
196
|
+
# Use :select from scope if it isn't already present.
|
197
|
+
options[:select] = scope(:find, :select) unless options[:select]
|
198
|
+
|
199
|
+
if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
|
200
|
+
# Remove quoting and check for table_name.*-like statement.
|
201
|
+
if options[:select].gsub('`', '') =~ /\w+\.\*/
|
202
|
+
options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
|
203
|
+
end
|
204
|
+
else
|
185
205
|
excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
|
186
206
|
end
|
207
|
+
|
187
208
|
# count expects (almost) the same options as find
|
188
209
|
count_options = options.except *excludees
|
189
210
|
|
@@ -191,19 +212,23 @@ module WillPaginate
|
|
191
212
|
# this allows you to specify :select, :order, or anything else just for the count query
|
192
213
|
count_options.update options[:count] if options[:count]
|
193
214
|
|
215
|
+
# forget about includes if they are irrelevant (Rails 2.1)
|
216
|
+
if count_options[:include] and
|
217
|
+
klass.private_methods.include_method?(:references_eager_loaded_tables?) and
|
218
|
+
!klass.send(:references_eager_loaded_tables?, count_options)
|
219
|
+
count_options.delete :include
|
220
|
+
end
|
221
|
+
|
194
222
|
# we may have to scope ...
|
195
223
|
counter = Proc.new { count(count_options) }
|
196
224
|
|
197
|
-
# we may be in a model or an association proxy!
|
198
|
-
klass = (@owner and @reflection) ? @reflection.klass : self
|
199
|
-
|
200
225
|
count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
|
201
226
|
# scope_out adds a 'with_finder' method which acts like with_scope, if it's present
|
202
227
|
# then execute the count with the scoping provided by the with_finder
|
203
228
|
send(scoper, &counter)
|
204
|
-
elsif
|
229
|
+
elsif finder =~ /^find_(all_by|by)_([_a-zA-Z]\w*)$/
|
205
230
|
# extract conditions from calls like "paginate_by_foo_and_bar"
|
206
|
-
attribute_names =
|
231
|
+
attribute_names = $2.split('_and_')
|
207
232
|
conditions = construct_attributes_from_arguments(attribute_names, args)
|
208
233
|
with_scope(:find => { :conditions => conditions }, &counter)
|
209
234
|
else
|
@@ -1,27 +1,22 @@
|
|
1
|
-
## stolen from: http://dev.rubyonrails.org/browser/trunk/activerecord/lib/active_record/named_scope.rb?rev=9084
|
2
|
-
|
3
1
|
module WillPaginate
|
4
2
|
# This is a feature backported from Rails 2.1 because of its usefullness not only with will_paginate,
|
5
3
|
# but in other aspects when managing complex conditions that you want to be reusable.
|
6
4
|
module NamedScope
|
7
5
|
# All subclasses of ActiveRecord::Base have two named_scopes:
|
8
6
|
# * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
|
9
|
-
# * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly:
|
10
|
-
#
|
11
|
-
# Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)
|
7
|
+
# * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
|
12
8
|
#
|
13
9
|
# These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
|
14
10
|
# intermediate values (scopes) around as first-class objects is convenient.
|
15
11
|
def self.included(base)
|
16
12
|
base.class_eval do
|
17
13
|
extend ClassMethods
|
18
|
-
named_scope :all
|
19
14
|
named_scope :scoped, lambda { |scope| scope }
|
20
15
|
end
|
21
16
|
end
|
22
17
|
|
23
18
|
module ClassMethods
|
24
|
-
def scopes
|
19
|
+
def scopes
|
25
20
|
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
|
26
21
|
end
|
27
22
|
|
@@ -46,7 +41,7 @@ module WillPaginate
|
|
46
41
|
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
|
47
42
|
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
|
48
43
|
#
|
49
|
-
# All scopes are available as class methods on the ActiveRecord descendent upon which the scopes were defined. But they are also available to
|
44
|
+
# All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
|
50
45
|
# <tt>has_many</tt> associations. If,
|
51
46
|
#
|
52
47
|
# class Person < ActiveRecord::Base
|
@@ -76,14 +71,27 @@ module WillPaginate
|
|
76
71
|
# end
|
77
72
|
# end
|
78
73
|
#
|
79
|
-
|
74
|
+
#
|
75
|
+
# For testing complex named scopes, you can examine the scoping options using the
|
76
|
+
# <tt>proxy_options</tt> method on the proxy itself.
|
77
|
+
#
|
78
|
+
# class Shirt < ActiveRecord::Base
|
79
|
+
# named_scope :colored, lambda { |color|
|
80
|
+
# { :conditions => { :color => color } }
|
81
|
+
# }
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# expected_options = { :conditions => { :colored => 'red' } }
|
85
|
+
# assert_equal expected_options, Shirt.colored('red').proxy_options
|
86
|
+
def named_scope(name, options = {})
|
87
|
+
name = name.to_sym
|
80
88
|
scopes[name] = lambda do |parent_scope, *args|
|
81
89
|
Scope.new(parent_scope, case options
|
82
90
|
when Hash
|
83
91
|
options
|
84
92
|
when Proc
|
85
93
|
options.call(*args)
|
86
|
-
end
|
94
|
+
end) { |*a| yield(*a) if block_given? }
|
87
95
|
end
|
88
96
|
(class << self; self end).instance_eval do
|
89
97
|
define_method name do |*args|
|
@@ -93,14 +101,20 @@ module WillPaginate
|
|
93
101
|
end
|
94
102
|
end
|
95
103
|
|
96
|
-
class Scope
|
104
|
+
class Scope
|
97
105
|
attr_reader :proxy_scope, :proxy_options
|
98
|
-
|
106
|
+
|
107
|
+
[].methods.each do |m|
|
108
|
+
unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
|
109
|
+
delegate m, :to => :proxy_found
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
99
113
|
delegate :scopes, :with_scope, :to => :proxy_scope
|
100
114
|
|
101
|
-
def initialize(proxy_scope, options
|
115
|
+
def initialize(proxy_scope, options)
|
102
116
|
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
|
103
|
-
extend Module.new(
|
117
|
+
extend Module.new { |*args| yield(*args) } if block_given?
|
104
118
|
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
|
105
119
|
end
|
106
120
|
|
@@ -108,18 +122,42 @@ module WillPaginate
|
|
108
122
|
load_found; self
|
109
123
|
end
|
110
124
|
|
125
|
+
def first(*args)
|
126
|
+
if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
|
127
|
+
proxy_found.first(*args)
|
128
|
+
else
|
129
|
+
find(:first, *args)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def last(*args)
|
134
|
+
if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
|
135
|
+
proxy_found.last(*args)
|
136
|
+
else
|
137
|
+
find(:last, *args)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def empty?
|
142
|
+
@found ? @found.empty? : count.zero?
|
143
|
+
end
|
144
|
+
|
145
|
+
def respond_to?(method, include_private = false)
|
146
|
+
super || @proxy_scope.respond_to?(method, include_private)
|
147
|
+
end
|
148
|
+
|
111
149
|
protected
|
112
150
|
def proxy_found
|
113
151
|
@found || load_found
|
114
152
|
end
|
115
153
|
|
116
154
|
private
|
117
|
-
def method_missing(method, *args
|
155
|
+
def method_missing(method, *args)
|
118
156
|
if scopes.include?(method)
|
119
157
|
scopes[method].call(self, *args)
|
120
158
|
else
|
121
159
|
with_scope :find => proxy_options do
|
122
|
-
proxy_scope.send(method, *args
|
160
|
+
proxy_scope.send(method, *args) { |*a| yield(*a) if block_given? }
|
123
161
|
end
|
124
162
|
end
|
125
163
|
end
|
@@ -1,9 +1,7 @@
|
|
1
|
-
## based on http://dev.rubyonrails.org/changeset/9084
|
2
|
-
|
3
1
|
ActiveRecord::Associations::AssociationProxy.class_eval do
|
4
2
|
protected
|
5
|
-
def with_scope(*args
|
6
|
-
@reflection.klass.send
|
3
|
+
def with_scope(*args)
|
4
|
+
@reflection.klass.send(:with_scope, *args) { |*a| yield(*a) if block_given? }
|
7
5
|
end
|
8
6
|
end
|
9
7
|
|
@@ -12,11 +10,11 @@ end
|
|
12
10
|
klass.class_eval do
|
13
11
|
protected
|
14
12
|
alias :method_missing_without_scopes :method_missing_without_paginate
|
15
|
-
def method_missing_without_paginate(method, *args
|
13
|
+
def method_missing_without_paginate(method, *args)
|
16
14
|
if @reflection.klass.scopes.include?(method)
|
17
|
-
@reflection.klass.scopes[method].call(self, *args
|
15
|
+
@reflection.klass.scopes[method].call(self, *args) { |*a| yield(*a) if block_given? }
|
18
16
|
else
|
19
|
-
method_missing_without_scopes(method, *args
|
17
|
+
method_missing_without_scopes(method, *args) { |*a| yield(*a) if block_given? }
|
20
18
|
end
|
21
19
|
end
|
22
20
|
end
|
@@ -25,14 +23,14 @@ end
|
|
25
23
|
# Rails 1.2.6
|
26
24
|
ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do
|
27
25
|
protected
|
28
|
-
def method_missing(method, *args
|
26
|
+
def method_missing(method, *args)
|
29
27
|
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
|
30
28
|
super
|
31
29
|
elsif @reflection.klass.scopes.include?(method)
|
32
30
|
@reflection.klass.scopes[method].call(self, *args)
|
33
31
|
else
|
34
32
|
@reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
|
35
|
-
@reflection.klass.send(method, *args
|
33
|
+
@reflection.klass.send(method, *args) { |*a| yield(*a) if block_given? }
|
36
34
|
end
|
37
35
|
end
|
38
36
|
end
|
@@ -3,35 +3,36 @@ require 'will_paginate/core_ext'
|
|
3
3
|
module WillPaginate
|
4
4
|
# = Will Paginate view helpers
|
5
5
|
#
|
6
|
-
#
|
6
|
+
# The main view helper, #will_paginate, renders
|
7
7
|
# pagination links for the given collection. The helper itself is lightweight
|
8
|
-
# and serves only as a wrapper around
|
8
|
+
# and serves only as a wrapper around LinkRenderer instantiation; the
|
9
9
|
# renderer then does all the hard work of generating the HTML.
|
10
10
|
#
|
11
11
|
# == Global options for helpers
|
12
12
|
#
|
13
13
|
# Options for pagination helpers are optional and get their default values from the
|
14
|
-
# WillPaginate::ViewHelpers.pagination_options hash. You can write to this hash to
|
14
|
+
# <tt>WillPaginate::ViewHelpers.pagination_options</tt> hash. You can write to this hash to
|
15
15
|
# override default options on the global level:
|
16
16
|
#
|
17
|
-
# WillPaginate::ViewHelpers.pagination_options[:
|
17
|
+
# WillPaginate::ViewHelpers.pagination_options[:previous_label] = 'Previous page'
|
18
18
|
#
|
19
|
-
# By putting this into
|
19
|
+
# By putting this into "config/initializers/will_paginate.rb" (or simply environment.rb in
|
20
|
+
# older versions of Rails) you can easily translate link texts to previous
|
20
21
|
# and next pages, as well as override some other defaults to your liking.
|
21
22
|
module ViewHelpers
|
22
23
|
# default options that can be overridden on the global level
|
23
24
|
@@pagination_options = {
|
24
|
-
:class
|
25
|
-
:
|
26
|
-
:next_label
|
27
|
-
:inner_window
|
28
|
-
:outer_window
|
29
|
-
:separator
|
30
|
-
:param_name
|
31
|
-
:params
|
32
|
-
:renderer
|
33
|
-
:page_links
|
34
|
-
:container
|
25
|
+
:class => 'pagination',
|
26
|
+
:previous_label => '« Previous',
|
27
|
+
:next_label => 'Next »',
|
28
|
+
:inner_window => 4, # links around the current page
|
29
|
+
:outer_window => 1, # links around beginning and end
|
30
|
+
:separator => ' ', # single space is friendly to spiders and non-graphic browsers
|
31
|
+
:param_name => :page,
|
32
|
+
:params => nil,
|
33
|
+
:renderer => 'WillPaginate::LinkRenderer',
|
34
|
+
:page_links => true,
|
35
|
+
:container => true
|
35
36
|
}
|
36
37
|
mattr_reader :pagination_options
|
37
38
|
|
@@ -40,31 +41,37 @@ module WillPaginate
|
|
40
41
|
# rendering the pagination in that case...
|
41
42
|
#
|
42
43
|
# ==== Options
|
43
|
-
#
|
44
|
-
# * <tt>:
|
44
|
+
# Display options:
|
45
|
+
# * <tt>:previous_label</tt> -- default: "« Previous" (this parameter is called <tt>:prev_label</tt> in versions <b>2.3.2</b> and older!)
|
45
46
|
# * <tt>:next_label</tt> -- default: "Next »"
|
47
|
+
# * <tt>:page_links</tt> -- when false, only previous/next links are rendered (default: true)
|
46
48
|
# * <tt>:inner_window</tt> -- how many links are shown around the current page (default: 4)
|
47
49
|
# * <tt>:outer_window</tt> -- how many links are around the first and the last page (default: 1)
|
48
50
|
# * <tt>:separator</tt> -- string separator for page HTML elements (default: single space)
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
# * <tt>:renderer</tt> -- class name of the link renderer (default: WillPaginate::LinkRenderer)
|
53
|
-
# * <tt>:page_links</tt> -- when false, only previous/next links are rendered (default: true)
|
51
|
+
#
|
52
|
+
# HTML options:
|
53
|
+
# * <tt>:class</tt> -- CSS class name for the generated DIV (default: "pagination")
|
54
54
|
# * <tt>:container</tt> -- toggles rendering of the DIV container for pagination links, set to
|
55
55
|
# false only when you are rendering your own pagination markup (default: true)
|
56
|
-
# * <tt>:id</tt> -- HTML ID for the container (default: nil). Pass +true+ to have the ID
|
57
|
-
# generated from the class name of objects in collection: for example, paginating
|
56
|
+
# * <tt>:id</tt> -- HTML ID for the container (default: nil). Pass +true+ to have the ID
|
57
|
+
# automatically generated from the class name of objects in collection: for example, paginating
|
58
58
|
# ArticleComment models would yield an ID of "article_comments_pagination".
|
59
59
|
#
|
60
|
-
#
|
60
|
+
# Advanced options:
|
61
|
+
# * <tt>:param_name</tt> -- parameter name for page number in URLs (default: <tt>:page</tt>)
|
62
|
+
# * <tt>:params</tt> -- additional parameters when generating pagination links
|
63
|
+
# (eg. <tt>:controller => "foo", :action => nil</tt>)
|
64
|
+
# * <tt>:renderer</tt> -- class name, class or instance of a link renderer (default:
|
65
|
+
# <tt>WillPaginate::LinkRenderer</tt>)
|
66
|
+
#
|
67
|
+
# All options not recognized by will_paginate will become HTML attributes on the container
|
61
68
|
# element for pagination links (the DIV). For example:
|
62
69
|
#
|
63
|
-
# <%= will_paginate @posts, :
|
70
|
+
# <%= will_paginate @posts, :style => 'font-size: small' %>
|
64
71
|
#
|
65
72
|
# ... will result in:
|
66
73
|
#
|
67
|
-
# <div class="pagination"
|
74
|
+
# <div class="pagination" style="font-size: small"> ... </div>
|
68
75
|
#
|
69
76
|
# ==== Using the helper without arguments
|
70
77
|
# If the helper is called without passing in the collection object, it will
|
@@ -91,10 +98,22 @@ module WillPaginate
|
|
91
98
|
return nil unless WillPaginate::ViewHelpers.total_pages_for_collection(collection) > 1
|
92
99
|
|
93
100
|
options = options.symbolize_keys.reverse_merge WillPaginate::ViewHelpers.pagination_options
|
94
|
-
|
95
|
-
|
96
|
-
|
101
|
+
if options[:prev_label]
|
102
|
+
WillPaginate::Deprecation::warn(":prev_label view parameter is now :previous_label; the old name has been deprecated", caller)
|
103
|
+
options[:previous_label] = options.delete(:prev_label)
|
104
|
+
end
|
105
|
+
|
106
|
+
# get the renderer instance
|
107
|
+
renderer = case options[:renderer]
|
108
|
+
when String
|
109
|
+
options[:renderer].to_s.constantize.new
|
110
|
+
when Class
|
111
|
+
options[:renderer].new
|
112
|
+
else
|
113
|
+
options[:renderer]
|
114
|
+
end
|
97
115
|
# render HTML for pagination
|
116
|
+
renderer.prepare collection, options, self
|
98
117
|
renderer.to_html
|
99
118
|
end
|
100
119
|
|
@@ -122,24 +141,41 @@ module WillPaginate
|
|
122
141
|
# blocks of pagination links sharing the same ID (which is invalid HTML).
|
123
142
|
def paginated_section(*args, &block)
|
124
143
|
pagination = will_paginate(*args).to_s
|
125
|
-
|
126
|
-
|
144
|
+
|
145
|
+
unless ActionView::Base.respond_to? :erb_variable
|
146
|
+
concat pagination
|
147
|
+
yield
|
148
|
+
concat pagination
|
149
|
+
else
|
150
|
+
content = pagination + capture(&block) + pagination
|
151
|
+
concat(content, block.binding)
|
152
|
+
end
|
127
153
|
end
|
128
154
|
|
129
155
|
# Renders a helpful message with numbers of displayed vs. total entries.
|
130
156
|
# You can use this as a blueprint for your own, similar helpers.
|
131
157
|
#
|
132
158
|
# <%= page_entries_info @posts %>
|
133
|
-
# #-> Displaying
|
134
|
-
|
159
|
+
# #-> Displaying posts 6 - 10 of 26 in total
|
160
|
+
#
|
161
|
+
# By default, the message will use the humanized class name of objects
|
162
|
+
# in collection: for instance, "project types" for ProjectType models.
|
163
|
+
# Override this with the <tt>:entry_name</tt> parameter:
|
164
|
+
#
|
165
|
+
# <%= page_entries_info @posts, :entry_name => 'item' %>
|
166
|
+
# #-> Displaying items 6 - 10 of 26 in total
|
167
|
+
def page_entries_info(collection, options = {})
|
168
|
+
entry_name = options[:entry_name] ||
|
169
|
+
(collection.empty?? 'entry' : collection.first.class.name.underscore.sub('_', ' '))
|
170
|
+
|
135
171
|
if collection.total_pages < 2
|
136
172
|
case collection.size
|
137
|
-
when 0;
|
138
|
-
when 1;
|
139
|
-
else; "Displaying <b>all #{collection.size}</b>
|
173
|
+
when 0; "No #{entry_name.pluralize} found"
|
174
|
+
when 1; "Displaying <b>1</b> #{entry_name}"
|
175
|
+
else; "Displaying <b>all #{collection.size}</b> #{entry_name.pluralize}"
|
140
176
|
end
|
141
177
|
else
|
142
|
-
%{Displaying
|
178
|
+
%{Displaying #{entry_name.pluralize} <b>%d - %d</b> of <b>%d</b> in total} % [
|
143
179
|
collection.offset + 1,
|
144
180
|
collection.offset + collection.length,
|
145
181
|
collection.total_entries
|
@@ -149,12 +185,11 @@ module WillPaginate
|
|
149
185
|
|
150
186
|
def self.total_pages_for_collection(collection) #:nodoc:
|
151
187
|
if collection.respond_to?('page_count') and !collection.respond_to?('total_pages')
|
152
|
-
WillPaginate::Deprecation.warn
|
188
|
+
WillPaginate::Deprecation.warn %{
|
153
189
|
You are using a paginated collection of class #{collection.class.name}
|
154
190
|
which conforms to the old API of WillPaginate::Collection by using
|
155
191
|
`page_count`, while the current method name is `total_pages`. Please
|
156
|
-
upgrade yours or 3rd-party code that provides the paginated collection
|
157
|
-
MSG
|
192
|
+
upgrade yours or 3rd-party code that provides the paginated collection}, caller
|
158
193
|
class << collection
|
159
194
|
def total_pages; page_count; end
|
160
195
|
end
|
@@ -164,16 +199,29 @@ module WillPaginate
|
|
164
199
|
end
|
165
200
|
|
166
201
|
# This class does the heavy lifting of actually building the pagination
|
167
|
-
# links. It is used by
|
202
|
+
# links. It is used by the <tt>will_paginate</tt> helper internally.
|
168
203
|
class LinkRenderer
|
204
|
+
|
205
|
+
# The gap in page links is represented by:
|
206
|
+
#
|
207
|
+
# <span class="gap">…</span>
|
208
|
+
attr_accessor :gap_marker
|
209
|
+
|
210
|
+
def initialize
|
211
|
+
@gap_marker = '<span class="gap">…</span>'
|
212
|
+
end
|
213
|
+
|
169
214
|
# * +collection+ is a WillPaginate::Collection instance or any other object
|
170
215
|
# that conforms to that API
|
171
216
|
# * +options+ are forwarded from +will_paginate+ view helper
|
172
217
|
# * +template+ is the reference to the template being rendered
|
173
|
-
def
|
218
|
+
def prepare(collection, options, template)
|
174
219
|
@collection = collection
|
175
220
|
@options = options
|
176
221
|
@template = template
|
222
|
+
|
223
|
+
# reset values in case we're re-using this instance
|
224
|
+
@total_pages = @param_name = @url_string = nil
|
177
225
|
end
|
178
226
|
|
179
227
|
# Process it! This method returns the complete HTML string which contains
|
@@ -182,8 +230,8 @@ module WillPaginate
|
|
182
230
|
def to_html
|
183
231
|
links = @options[:page_links] ? windowed_links : []
|
184
232
|
# previous/next buttons
|
185
|
-
links.unshift page_link_or_span(@collection.previous_page,
|
186
|
-
links.push page_link_or_span(@collection.next_page,
|
233
|
+
links.unshift page_link_or_span(@collection.previous_page, 'disabled prev_page', @options[:previous_label])
|
234
|
+
links.push page_link_or_span(@collection.next_page, 'disabled next_page', @options[:next_label])
|
187
235
|
|
188
236
|
html = links.join(@options[:separator])
|
189
237
|
@options[:container] ? @template.content_tag(:div, html, html_attributes) : html
|
@@ -203,13 +251,6 @@ module WillPaginate
|
|
203
251
|
|
204
252
|
protected
|
205
253
|
|
206
|
-
# The gap in page links is represented by:
|
207
|
-
#
|
208
|
-
# <span class="gap">…</span>
|
209
|
-
def gap_marker
|
210
|
-
'<span class="gap">…</span>'
|
211
|
-
end
|
212
|
-
|
213
254
|
# Collects link items for visible page numbers.
|
214
255
|
def windowed_links
|
215
256
|
prev = nil
|
@@ -252,38 +293,60 @@ module WillPaginate
|
|
252
293
|
|
253
294
|
def page_link_or_span(page, span_class, text = nil)
|
254
295
|
text ||= page.to_s
|
255
|
-
classnames = Array[*span_class]
|
256
296
|
|
257
297
|
if page and page != current_page
|
258
|
-
|
298
|
+
classnames = span_class && span_class.index(' ') && span_class.split(' ', 2).last
|
299
|
+
page_link page, text, :rel => rel_value(page), :class => classnames
|
259
300
|
else
|
260
|
-
|
301
|
+
page_span page, text, :class => span_class
|
261
302
|
end
|
262
303
|
end
|
263
304
|
|
305
|
+
def page_link(page, text, attributes = {})
|
306
|
+
@template.link_to text, url_for(page), attributes
|
307
|
+
end
|
308
|
+
|
309
|
+
def page_span(page, text, attributes = {})
|
310
|
+
@template.content_tag :span, text, attributes
|
311
|
+
end
|
312
|
+
|
264
313
|
# Returns URL params for +page_link_or_span+, taking the current GET params
|
265
314
|
# and <tt>:params</tt> option into account.
|
266
315
|
def url_for(page)
|
267
|
-
|
268
|
-
|
316
|
+
page_one = page == 1
|
317
|
+
unless @url_string and !page_one
|
318
|
+
@url_params = {}
|
269
319
|
# page links should preserve GET parameters
|
270
320
|
stringified_merge @url_params, @template.params if @template.request.get?
|
271
321
|
stringified_merge @url_params, @options[:params] if @options[:params]
|
272
322
|
|
273
|
-
if param_name.index(/[^\w-]/)
|
274
|
-
page_param = (
|
275
|
-
parse_query_parameters("#{param_name}=#{page}")
|
323
|
+
if complex = param_name.index(/[^\w-]/)
|
324
|
+
page_param = parse_query_parameters("#{param_name}=#{page}")
|
276
325
|
|
277
326
|
stringified_merge @url_params, page_param
|
278
327
|
else
|
279
|
-
@url_params[param_name] =
|
328
|
+
@url_params[param_name] = page_one ? 1 : 2
|
280
329
|
end
|
281
330
|
|
282
331
|
url = @template.url_for(@url_params)
|
283
|
-
|
284
|
-
|
332
|
+
return url if page_one
|
333
|
+
|
334
|
+
if complex
|
335
|
+
@url_string = url.sub(%r!((?:\?|&)#{CGI.escape param_name}=)#{page}!, "\\1\0")
|
336
|
+
return url
|
337
|
+
else
|
338
|
+
@url_string = url
|
339
|
+
@url_params[param_name] = 3
|
340
|
+
@template.url_for(@url_params).split(//).each_with_index do |char, i|
|
341
|
+
if char == '3' and url[i, 1] == '2'
|
342
|
+
@url_string[i] = "\0"
|
343
|
+
break
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
285
347
|
end
|
286
|
-
|
348
|
+
# finally!
|
349
|
+
@url_string.sub "\0", page.to_s
|
287
350
|
end
|
288
351
|
|
289
352
|
private
|
@@ -308,20 +371,33 @@ module WillPaginate
|
|
308
371
|
@param_name ||= @options[:param_name].to_s
|
309
372
|
end
|
310
373
|
|
374
|
+
# Recursively merge into target hash by using stringified keys from the other one
|
311
375
|
def stringified_merge(target, other)
|
312
376
|
other.each do |key, value|
|
313
|
-
key = key.to_s
|
377
|
+
key = key.to_s # this line is what it's all about!
|
314
378
|
existing = target[key]
|
315
379
|
|
316
|
-
if value.is_a?(Hash)
|
317
|
-
target[key] =
|
318
|
-
|
319
|
-
|
320
|
-
return
|
321
|
-
end
|
380
|
+
if value.is_a?(Hash) and (existing.is_a?(Hash) or existing.nil?)
|
381
|
+
stringified_merge(existing || (target[key] = {}), value)
|
382
|
+
else
|
383
|
+
target[key] = value
|
322
384
|
end
|
323
|
-
|
324
|
-
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
def parse_query_parameters(params)
|
389
|
+
if defined? Rack::Utils
|
390
|
+
# For Rails > 2.3
|
391
|
+
Rack::Utils.parse_nested_query(params)
|
392
|
+
elsif defined?(ActionController::AbstractRequest)
|
393
|
+
ActionController::AbstractRequest.parse_query_parameters(params)
|
394
|
+
elsif defined?(ActionController::UrlEncodedPairParser)
|
395
|
+
# For Rails > 2.2
|
396
|
+
ActionController::UrlEncodedPairParser.parse_query_parameters(params)
|
397
|
+
elsif defined?(CGIMethods)
|
398
|
+
CGIMethods.parse_query_parameters(params)
|
399
|
+
else
|
400
|
+
raise "unsupported ActionPack version"
|
325
401
|
end
|
326
402
|
end
|
327
403
|
end
|