will_paginate 2.2.2 → 2.3.11

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of will_paginate might be problematic. Click here for more details.

@@ -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
- # # now use WillPaginate::Collection directly or subclass it
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, &block)
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
- # besides your records: simply start with offset + 1.
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
- unless Hash.instance_methods.include? 'except'
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.include? 'slice'
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)
@@ -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, &block)
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 send(finder, *args, &block)
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 where
98
- # Jamis Buck describes this and also uses a more efficient way for MySQL.
99
- def paginated_each(options = {}, &block)
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
- total += collection.each(&block).size
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 ['oracle', 'oci'].include?(self.connection.adapter_name.downcase)
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, &block) #:nodoc:
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
- return method_missing_without_paginate(method, *args, &block)
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, &block)
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
- unless options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
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 match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(finder)
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 = extract_attribute_names_from_match(match)
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 #:nodoc:
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
- def named_scope(name, options = {}, &block)
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, &block)
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 #:nodoc:
104
+ class Scope
97
105
  attr_reader :proxy_scope, :proxy_options
98
- [].methods.each { |m| delegate m, :to => :proxy_found unless m =~ /(^__|^nil\?|^send|class|extend|find|count|sum|average|maximum|minimum|paginate)/ }
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, &block)
115
+ def initialize(proxy_scope, options)
102
116
  [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
103
- extend Module.new(&block) if block_given?
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, &block)
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, &block)
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, &block)
6
- @reflection.klass.send :with_scope, *args, &block
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, &block)
13
+ def method_missing_without_paginate(method, *args)
16
14
  if @reflection.klass.scopes.include?(method)
17
- @reflection.klass.scopes[method].call(self, *args, &block)
15
+ @reflection.klass.scopes[method].call(self, *args) { |*a| yield(*a) if block_given? }
18
16
  else
19
- method_missing_without_scopes(method, *args, &block)
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, &block)
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, &block)
33
+ @reflection.klass.send(method, *args) { |*a| yield(*a) if block_given? }
36
34
  end
37
35
  end
38
36
  end
@@ -1,8 +1,8 @@
1
- module WillPaginate #:nodoc:
2
- module VERSION #:nodoc:
1
+ module WillPaginate
2
+ module VERSION
3
3
  MAJOR = 2
4
- MINOR = 2
5
- TINY = 2
4
+ MINOR = 3
5
+ TINY = 11
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -3,35 +3,36 @@ require 'will_paginate/core_ext'
3
3
  module WillPaginate
4
4
  # = Will Paginate view helpers
5
5
  #
6
- # Currently there is only one view helper: +will_paginate+. It renders the
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 link renderer instantiation; the
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[:prev_label] = 'Previous page'
17
+ # WillPaginate::ViewHelpers.pagination_options[:previous_label] = 'Previous page'
18
18
  #
19
- # By putting this into your environment.rb you can easily translate link texts to previous
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 => 'pagination',
25
- :prev_label => '&laquo; Previous',
26
- :next_label => 'Next &raquo;',
27
- :inner_window => 4, # links around the current page
28
- :outer_window => 1, # links around beginning and end
29
- :separator => ' ', # single space is friendly to spiders and non-graphic browsers
30
- :param_name => :page,
31
- :params => nil,
32
- :renderer => 'WillPaginate::LinkRenderer',
33
- :page_links => true,
34
- :container => true
25
+ :class => 'pagination',
26
+ :previous_label => '&laquo; Previous',
27
+ :next_label => 'Next &raquo;',
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
- # * <tt>:class</tt> -- CSS class name for the generated DIV (default: "pagination")
44
- # * <tt>:prev_label</tt> -- default: "« Previous"
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
- # * <tt>:param_name</tt> -- parameter name for page number in URLs (default: <tt>:page</tt>)
50
- # * <tt>:params</tt> -- additional parameters when generating pagination links
51
- # (eg. <tt>:controller => "foo", :action => nil</tt>)
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 automatically
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
- # All options beside listed ones are passed as HTML attributes to the container
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, :id => 'wp_posts' %>
70
+ # <%= will_paginate @posts, :style => 'font-size: small' %>
64
71
  #
65
72
  # ... will result in:
66
73
  #
67
- # <div class="pagination" id="wp_posts"> ... </div>
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
- # create the renderer instance
95
- renderer_class = options[:renderer].to_s.constantize
96
- renderer = renderer_class.new collection, options, self
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
- content = pagination + capture(&block) + pagination
126
- concat content, block.binding
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 entries 6 - 10 of 26 in total
134
- def page_entries_info(collection)
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; 'No entries found'
138
- when 1; 'Displaying <b>1</b> entry'
139
- else; "Displaying <b>all #{collection.size}</b> entries"
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 entries <b>%d&nbsp;-&nbsp;%d</b> of <b>%d</b> in total} % [
178
+ %{Displaying #{entry_name.pluralize} <b>%d&nbsp;-&nbsp;%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 <<-MSG
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 +will_paginate+ helper internally.
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">&hellip;</span>
208
+ attr_accessor :gap_marker
209
+
210
+ def initialize
211
+ @gap_marker = '<span class="gap">&hellip;</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 initialize(collection, options, template)
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, %w(disabled prev_page), @options[:prev_label])
186
- links.push page_link_or_span(@collection.next_page, %w(disabled next_page), @options[:next_label])
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">&hellip;</span>
209
- def gap_marker
210
- '<span class="gap">&hellip;</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
- @template.link_to text, url_for(page), :rel => rel_value(page), :class => classnames[1]
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
- @template.content_tag :span, text, :class => classnames.join(' ')
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
- unless @url_string
268
- @url_params = { :escape => false }
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 = (defined?(CGIMethods) ? CGIMethods : ActionController::AbstractRequest).
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] = page
328
+ @url_params[param_name] = page_one ? 1 : 2
280
329
  end
281
330
 
282
331
  url = @template.url_for(@url_params)
283
- @url_string = url.sub(%r!([?&/]#{CGI.escape param_name}[=/])#{page}!, '\1@')
284
- return url
332
+ return url if page_one
333
+
334
+ if complex
335
+ @url_string = url.sub(%r!((?:\?|&amp;)#{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
- @url_string.sub '@', page.to_s
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] = existing = {} if existing.nil?
318
- if existing.is_a?(Hash)
319
- stringified_merge(existing, value)
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
- target[key] = value
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