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.

@@ -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