agnostic-will_paginate 3.0.0

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.
Files changed (62) hide show
  1. data/.autotest +54 -0
  2. data/.gitignore +4 -0
  3. data/.gitmodules +3 -0
  4. data/.manifest +61 -0
  5. data/CHANGELOG.rdoc +105 -0
  6. data/LICENSE +18 -0
  7. data/README.rdoc +125 -0
  8. data/Rakefile +58 -0
  9. data/init.rb +1 -0
  10. data/lib/will_paginate.rb +45 -0
  11. data/lib/will_paginate/array.rb +33 -0
  12. data/lib/will_paginate/collection.rb +145 -0
  13. data/lib/will_paginate/core_ext.rb +69 -0
  14. data/lib/will_paginate/deprecation.rb +50 -0
  15. data/lib/will_paginate/finders.rb +9 -0
  16. data/lib/will_paginate/finders/active_record.rb +192 -0
  17. data/lib/will_paginate/finders/active_record/named_scope.rb +170 -0
  18. data/lib/will_paginate/finders/active_record/named_scope_patch.rb +39 -0
  19. data/lib/will_paginate/finders/active_resource.rb +51 -0
  20. data/lib/will_paginate/finders/base.rb +112 -0
  21. data/lib/will_paginate/finders/data_mapper.rb +30 -0
  22. data/lib/will_paginate/finders/sequel.rb +22 -0
  23. data/lib/will_paginate/version.rb +9 -0
  24. data/lib/will_paginate/view_helpers.rb +42 -0
  25. data/lib/will_paginate/view_helpers/action_view.rb +158 -0
  26. data/lib/will_paginate/view_helpers/base.rb +126 -0
  27. data/lib/will_paginate/view_helpers/link_renderer.rb +130 -0
  28. data/lib/will_paginate/view_helpers/link_renderer_base.rb +83 -0
  29. data/lib/will_paginate/view_helpers/merb.rb +13 -0
  30. data/spec/collection_spec.rb +147 -0
  31. data/spec/console +8 -0
  32. data/spec/console_fixtures.rb +8 -0
  33. data/spec/database.yml +22 -0
  34. data/spec/finders/active_record_spec.rb +461 -0
  35. data/spec/finders/active_resource_spec.rb +52 -0
  36. data/spec/finders/activerecord_test_connector.rb +108 -0
  37. data/spec/finders/data_mapper_spec.rb +62 -0
  38. data/spec/finders/data_mapper_test_connector.rb +20 -0
  39. data/spec/finders/sequel_spec.rb +53 -0
  40. data/spec/finders/sequel_test_connector.rb +9 -0
  41. data/spec/finders_spec.rb +76 -0
  42. data/spec/fixtures/admin.rb +3 -0
  43. data/spec/fixtures/developer.rb +13 -0
  44. data/spec/fixtures/developers_projects.yml +13 -0
  45. data/spec/fixtures/project.rb +15 -0
  46. data/spec/fixtures/projects.yml +6 -0
  47. data/spec/fixtures/replies.yml +29 -0
  48. data/spec/fixtures/reply.rb +7 -0
  49. data/spec/fixtures/schema.rb +38 -0
  50. data/spec/fixtures/topic.rb +6 -0
  51. data/spec/fixtures/topics.yml +30 -0
  52. data/spec/fixtures/user.rb +2 -0
  53. data/spec/fixtures/users.yml +35 -0
  54. data/spec/rcov.opts +2 -0
  55. data/spec/spec.opts +2 -0
  56. data/spec/spec_helper.rb +75 -0
  57. data/spec/tasks.rake +60 -0
  58. data/spec/view_helpers/action_view_spec.rb +344 -0
  59. data/spec/view_helpers/base_spec.rb +64 -0
  60. data/spec/view_helpers/link_renderer_base_spec.rb +84 -0
  61. data/spec/view_helpers/view_example_group.rb +111 -0
  62. metadata +152 -0
@@ -0,0 +1,33 @@
1
+ require 'will_paginate/collection'
2
+
3
+ class Array
4
+ # Paginates a static array (extracting a subset of it). The result is a
5
+ # WillPaginate::Collection instance, which is an array with a few more
6
+ # properties about its paginated state.
7
+ #
8
+ # Parameters:
9
+ # * <tt>:page</tt> - current page, defaults to 1
10
+ # * <tt>:per_page</tt> - limit of items per page, defaults to 30
11
+ # * <tt>:total_entries</tt> - total number of items in the array, defaults to
12
+ # <tt>array.length</tt> (obviously)
13
+ #
14
+ # Example:
15
+ # arr = ['a', 'b', 'c', 'd', 'e']
16
+ # paged = arr.paginate(:per_page => 2) #-> ['a', 'b']
17
+ # paged.total_entries #-> 5
18
+ # arr.paginate(:page => 2, :per_page => 2) #-> ['c', 'd']
19
+ # arr.paginate(:page => 3, :per_page => 2) #-> ['e']
20
+ #
21
+ # This method was originally {suggested by Desi
22
+ # McAdam}[http://www.desimcadam.com/archives/8] and later proved to be the
23
+ # most useful method of will_paginate library.
24
+ def paginate(options = {})
25
+ raise ArgumentError, "parameter hash expected (got #{options.inspect})" unless Hash === options
26
+
27
+ WillPaginate::Collection.create options[:page] || 1,
28
+ options[:per_page] || 30,
29
+ options[:total_entries] || self.length do |pager|
30
+ pager.replace self[pager.offset, pager.per_page].to_a
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,145 @@
1
+ module WillPaginate
2
+ # = Invalid page number error
3
+ # This is an ArgumentError raised in case a page was requested that is either
4
+ # zero or negative number. You should decide how do deal with such errors in
5
+ # the controller.
6
+ #
7
+ # If you're using Rails 2, then this error will automatically get handled like
8
+ # 404 Not Found. The hook is in "will_paginate.rb":
9
+ #
10
+ # ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found
11
+ #
12
+ # If you don't like this, use your preffered method of rescuing exceptions in
13
+ # public from your controllers to handle this differently. The +rescue_from+
14
+ # method is a nice addition to Rails 2.
15
+ #
16
+ # This error is *not* raised when a page further than the last page is
17
+ # requested. Use <tt>WillPaginate::Collection#out_of_bounds?</tt> method to
18
+ # check for those cases and manually deal with them as you see fit.
19
+ class InvalidPage < ArgumentError
20
+ def initialize(page, page_num) #:nodoc:
21
+ super "#{page.inspect} given as value, which translates to '#{page_num}' as page number"
22
+ end
23
+ end
24
+
25
+ # = The key to pagination
26
+ # Arrays returned from paginating finds are, in fact, instances of this little
27
+ # class. You may think of WillPaginate::Collection as an ordinary array with
28
+ # some extra properties. Those properties are used by view helpers to generate
29
+ # correct page links.
30
+ #
31
+ # WillPaginate::Collection also assists in rolling out your own pagination
32
+ # solutions: see +create+.
33
+ #
34
+ # If you are writing a library that provides a collection which you would like
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:
37
+ #
38
+ # gem 'will_paginate'
39
+ # require 'will_paginate/collection'
40
+ #
41
+ # # now use WillPaginate::Collection directly or subclass it
42
+ class Collection < Array
43
+ attr_reader :current_page, :per_page, :total_entries, :total_pages
44
+
45
+ # Arguments to the constructor are the current page number, per-page limit
46
+ # and the total number of entries. The last argument is optional because it
47
+ # is best to do lazy counting; in other words, count *conditionally* after
48
+ # populating the collection using the +replace+ method.
49
+ def initialize(page, per_page, total = nil)
50
+ @current_page = page.to_i
51
+ raise InvalidPage.new(page, @current_page) if @current_page < 1
52
+ @per_page = per_page.to_i
53
+ raise ArgumentError, "`per_page` setting cannot be less than 1 (#{@per_page} given)" if @per_page < 1
54
+
55
+ self.total_entries = total if total
56
+ end
57
+
58
+ # Just like +new+, but yields the object after instantiation and returns it
59
+ # afterwards. This is very useful for manual pagination:
60
+ #
61
+ # @entries = WillPaginate::Collection.create(1, 10) do |pager|
62
+ # result = Post.find(:all, :limit => pager.per_page, :offset => pager.offset)
63
+ # # inject the result array into the paginated collection:
64
+ # pager.replace(result)
65
+ #
66
+ # unless pager.total_entries
67
+ # # the pager didn't manage to guess the total count, do it manually
68
+ # pager.total_entries = Post.count
69
+ # end
70
+ # end
71
+ #
72
+ # The possibilities with this are endless. For another example, here is how
73
+ # WillPaginate used to define pagination for Array instances:
74
+ #
75
+ # Array.class_eval do
76
+ # def paginate(page = 1, per_page = 15)
77
+ # WillPaginate::Collection.create(page, per_page, size) do |pager|
78
+ # pager.replace self[pager.offset, pager.per_page].to_a
79
+ # end
80
+ # end
81
+ # end
82
+ #
83
+ # The Array#paginate API has since then changed, but this still serves as a
84
+ # fine example of WillPaginate::Collection usage.
85
+ def self.create(page, per_page, total = nil, &block)
86
+ pager = new(page, per_page, total)
87
+ yield pager
88
+ pager
89
+ end
90
+
91
+ # Helper method that is true when someone tries to fetch a page with a
92
+ # larger number than the last page. Can be used in combination with flashes
93
+ # and redirecting.
94
+ def out_of_bounds?
95
+ current_page > total_pages
96
+ end
97
+
98
+ # Current offset of the paginated collection. If we're on the first page,
99
+ # it is always 0. If we're on the 2nd page and there are 30 entries per page,
100
+ # the offset is 30. This property is useful if you want to render ordinals
101
+ # besides your records: simply start with offset + 1.
102
+ def offset
103
+ (current_page - 1) * per_page
104
+ end
105
+
106
+ # current_page - 1 or nil if there is no previous page
107
+ def previous_page
108
+ current_page > 1 ? (current_page - 1) : nil
109
+ end
110
+
111
+ # current_page + 1 or nil if there is no next page
112
+ def next_page
113
+ current_page < total_pages ? (current_page + 1) : nil
114
+ end
115
+
116
+ def total_entries=(number)
117
+ @total_entries = number.to_i
118
+ @total_pages = (@total_entries / per_page.to_f).ceil
119
+ end
120
+
121
+ # This is a magic wrapper for the original Array#replace method. It serves
122
+ # for populating the paginated collection after initialization.
123
+ #
124
+ # Why magic? Because it tries to guess the total number of entries judging
125
+ # by the size of given array. If it is shorter than +per_page+ limit, then we
126
+ # know we're on the last page. This trick is very useful for avoiding
127
+ # unnecessary hits to the database to do the counting after we fetched the
128
+ # data for the current page.
129
+ #
130
+ # However, after using +replace+ you should always test the value of
131
+ # +total_entries+ and set it to a proper value if it's +nil+. See the example
132
+ # in +create+.
133
+ def replace(array)
134
+ result = super
135
+
136
+ # The collection is shorter then page limit? Rejoice, because
137
+ # then we know that we are on the last page!
138
+ if total_entries.nil? and length < per_page and (current_page == 1 or length > 0)
139
+ self.total_entries = offset + length
140
+ end
141
+
142
+ result
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,69 @@
1
+ require 'set'
2
+ require 'will_paginate/array'
3
+
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
+ ## everything below copied from ActiveSupport so we don't depend on it ##
16
+
17
+ unless Hash.instance_methods.include_method? :except
18
+ Hash.class_eval do
19
+ # Returns a new hash without the given keys.
20
+ def except(*keys)
21
+ rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
22
+ reject { |key,| rejected.include?(key) }
23
+ end
24
+
25
+ # Replaces the hash without only the given keys.
26
+ def except!(*keys)
27
+ replace(except(*keys))
28
+ end
29
+ end
30
+ end
31
+
32
+ unless Hash.instance_methods.include_method? :slice
33
+ Hash.class_eval do
34
+ # Returns a new hash with only the given keys.
35
+ def slice(*keys)
36
+ allowed = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
37
+ reject { |key,| !allowed.include?(key) }
38
+ end
39
+
40
+ # Replaces the hash with only the given keys.
41
+ def slice!(*keys)
42
+ replace(slice(*keys))
43
+ end
44
+ end
45
+ end
46
+
47
+ unless String.instance_methods.include_method? :constantize
48
+ String.class_eval do
49
+ def constantize
50
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ self
51
+ raise NameError, "#{self.inspect} is not a valid constant name!"
52
+ end
53
+
54
+ Object.module_eval("::#{$1}", __FILE__, __LINE__)
55
+ end
56
+ end
57
+ end
58
+
59
+ unless String.instance_methods.include_method? :underscore
60
+ String.class_eval do
61
+ def underscore
62
+ self.to_s.gsub(/::/, '/').
63
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
64
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
65
+ tr("-", "_").
66
+ downcase
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,50 @@
1
+ # borrowed from ActiveSupport::Deprecation
2
+ module WillPaginate
3
+ module Deprecation
4
+ def self.debug() @debug; end
5
+ def self.debug=(value) @debug = value; end
6
+ self.debug = false
7
+
8
+ # Choose the default warn behavior according to RAILS_ENV.
9
+ # Ignore deprecation warnings in production.
10
+ BEHAVIORS = {
11
+ 'test' => Proc.new { |message, callstack|
12
+ $stderr.puts(message)
13
+ $stderr.puts callstack.join("\n ") if debug
14
+ },
15
+ 'development' => Proc.new { |message, callstack|
16
+ logger = defined?(::RAILS_DEFAULT_LOGGER) ? ::RAILS_DEFAULT_LOGGER : Logger.new($stderr)
17
+ logger.warn message
18
+ logger.debug callstack.join("\n ") if debug
19
+ }
20
+ }
21
+
22
+ def self.warn(message, callstack = caller)
23
+ if behavior
24
+ message = 'WillPaginate: ' + message.strip.gsub(/\s+/, ' ')
25
+ behavior.call(message, callstack)
26
+ end
27
+ end
28
+
29
+ def self.default_behavior
30
+ if defined?(RAILS_ENV)
31
+ BEHAVIORS[RAILS_ENV.to_s]
32
+ else
33
+ BEHAVIORS['test']
34
+ end
35
+ end
36
+
37
+ # Behavior is a block that takes a message argument.
38
+ def self.behavior() @behavior; end
39
+ def self.behavior=(value) @behavior = value; end
40
+ self.behavior = default_behavior
41
+
42
+ def self.silence
43
+ old_behavior = self.behavior
44
+ self.behavior = nil
45
+ yield
46
+ ensure
47
+ self.behavior = old_behavior
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,9 @@
1
+ require 'will_paginate/core_ext'
2
+
3
+ module WillPaginate
4
+ # Database logic for different ORMs
5
+ #
6
+ # See WillPaginate::Finders::Base
7
+ module Finders
8
+ end
9
+ end
@@ -0,0 +1,192 @@
1
+ require 'will_paginate/finders/base'
2
+ require 'active_record'
3
+
4
+ module WillPaginate::Finders
5
+ # = Paginating finders for ActiveRecord models
6
+ #
7
+ # WillPaginate adds +paginate+, +per_page+ and other methods to
8
+ # ActiveRecord::Base class methods and associations. It also hooks into
9
+ # +method_missing+ to intercept pagination calls to dynamic finders such as
10
+ # +paginate_by_user_id+ and translate them to ordinary finders
11
+ # (+find_all_by_user_id+ in this case).
12
+ #
13
+ # In short, paginating finders are equivalent to ActiveRecord finders; the
14
+ # only difference is that we start with "paginate" instead of "find" and
15
+ # that <tt>:page</tt> is required parameter:
16
+ #
17
+ # @posts = Post.paginate :all, :page => params[:page], :order => 'created_at DESC'
18
+ #
19
+ # In paginating finders, "all" is implicit. There is no sense in paginating
20
+ # a single record, right? So, you can drop the <tt>:all</tt> argument:
21
+ #
22
+ # Post.paginate(...) => Post.find :all
23
+ # Post.paginate_all_by_something => Post.find_all_by_something
24
+ # Post.paginate_by_something => Post.find_all_by_something
25
+ #
26
+ module ActiveRecord
27
+ include WillPaginate::Finders::Base
28
+
29
+ # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
30
+ # based on the params otherwise used by paginating finds: +page+ and
31
+ # +per_page+.
32
+ #
33
+ # Example:
34
+ #
35
+ # @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000],
36
+ # :page => params[:page], :per_page => 3
37
+ #
38
+ # A query for counting rows will automatically be generated if you don't
39
+ # supply <tt>:total_entries</tt>. If you experience problems with this
40
+ # generated SQL, you might want to perform the count manually in your
41
+ # application.
42
+ #
43
+ def paginate_by_sql(sql, options)
44
+ WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
45
+ query = sanitize_sql(sql.dup)
46
+ original_query = query.dup
47
+ # add limit, offset
48
+ add_limit! query, :offset => pager.offset, :limit => pager.per_page
49
+ # perfom the find
50
+ pager.replace find_by_sql(query)
51
+
52
+ unless pager.total_entries
53
+ count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
54
+ count_query = "SELECT COUNT(*) FROM (#{count_query})"
55
+
56
+ unless ['oracle', 'oci'].include?(self.connection.adapter_name.downcase)
57
+ count_query << ' AS count_table'
58
+ end
59
+ # perform the count query
60
+ pager.total_entries = count_by_sql(count_query)
61
+ end
62
+ end
63
+ end
64
+
65
+ def respond_to?(method, include_priv = false) #:nodoc:
66
+ super(method.to_s.sub(/^paginate/, 'find'), include_priv)
67
+ end
68
+
69
+ protected
70
+
71
+ def method_missing_with_paginate(method, *args, &block) #:nodoc:
72
+ # did somebody tried to paginate? if not, let them be
73
+ unless method.to_s.index('paginate') == 0
74
+ return method_missing_without_paginate(method, *args, &block)
75
+ end
76
+
77
+ # paginate finders are really just find_* with limit and offset
78
+ finder = method.to_s.sub('paginate', 'find')
79
+ finder.sub!('find', 'find_all') if finder.index('find_by_') == 0
80
+
81
+ options = args.pop
82
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
83
+ options = options.dup
84
+ options[:finder] = finder
85
+ args << options
86
+
87
+ paginate(*args, &block)
88
+ end
89
+
90
+ def wp_query(options, pager, args, &block) #:nodoc:
91
+ finder = (options.delete(:finder) || 'find').to_s
92
+ find_options = options.except(:count).update(:offset => pager.offset, :limit => pager.per_page)
93
+
94
+ if finder == 'find'
95
+ if Array === args.first and !pager.total_entries
96
+ pager.total_entries = args.first.size
97
+ end
98
+ args << :all if args.empty?
99
+ end
100
+
101
+ args << find_options
102
+ pager.replace send(finder, *args, &block)
103
+
104
+ unless pager.total_entries
105
+ # magic counting
106
+ pager.total_entries = wp_count(options, args, finder)
107
+ end
108
+ end
109
+
110
+ # Does the not-so-trivial job of finding out the total number of entries
111
+ # in the database. It relies on the ActiveRecord +count+ method.
112
+ def wp_count(options, args, finder) #:nodoc:
113
+ # find out if we are in a model or an association proxy
114
+ klass = (@owner and @reflection) ? @reflection.klass : self
115
+ count_options = wp_parse_count_options(options, klass)
116
+
117
+ # we may have to scope ...
118
+ counter = Proc.new { count(count_options) }
119
+
120
+ count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
121
+ # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
122
+ # then execute the count with the scoping provided by the with_finder
123
+ send(scoper, &counter)
124
+ elsif finder =~ /^find_(all_by|by)_([_a-zA-Z]\w*)$/
125
+ # extract conditions from calls like "paginate_by_foo_and_bar"
126
+ attribute_names = $2.split('_and_')
127
+ conditions = construct_attributes_from_arguments(attribute_names, args)
128
+ with_scope(:find => { :conditions => conditions }, &counter)
129
+ else
130
+ counter.call
131
+ end
132
+
133
+ count.respond_to?(:length) ? count.length : count
134
+ end
135
+
136
+ def wp_parse_count_options(options, klass) #:nodoc:
137
+ excludees = [:count, :order, :limit, :offset, :readonly]
138
+
139
+ unless ::ActiveRecord::Calculations::CALCULATIONS_OPTIONS.include?(:from)
140
+ # :from parameter wasn't supported in count() before this change
141
+ excludees << :from
142
+ end
143
+
144
+ # Use :select from scope if it isn't already present.
145
+ options[:select] = scope(:find, :select) unless options[:select]
146
+
147
+ if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
148
+ # Remove quoting and check for table_name.*-like statement.
149
+ if options[:select].gsub('`', '') =~ /\w+\.\*/
150
+ options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
151
+ end
152
+ else
153
+ excludees << :select
154
+ end
155
+
156
+ # count expects (almost) the same options as find
157
+ count_options = options.except *excludees
158
+
159
+ # merge the hash found in :count
160
+ # this allows you to specify :select, :order, or anything else just for the count query
161
+ count_options.update options[:count] if options[:count]
162
+
163
+ # forget about includes if they are irrelevant (Rails 2.1)
164
+ if count_options[:include] and
165
+ klass.private_methods.include_method?(:references_eager_loaded_tables?) and
166
+ !klass.send(:references_eager_loaded_tables?, count_options)
167
+ count_options.delete :include
168
+ end
169
+
170
+ count_options
171
+ end
172
+ end
173
+ end
174
+
175
+ ActiveRecord::Base.class_eval do
176
+ extend WillPaginate::Finders::ActiveRecord
177
+ class << self
178
+ alias_method_chain :method_missing, :paginate
179
+ end
180
+ end
181
+
182
+ # support pagination on associations
183
+ a = ActiveRecord::Associations
184
+ returning([ a::AssociationCollection ]) { |classes|
185
+ # detect http://dev.rubyonrails.org/changeset/9230
186
+ unless a::HasManyThroughAssociation.superclass == a::HasManyAssociation
187
+ classes << a::HasManyThroughAssociation
188
+ end
189
+ }.each do |klass|
190
+ klass.send :include, WillPaginate::Finders::ActiveRecord
191
+ klass.class_eval { alias_method_chain :method_missing, :paginate }
192
+ end