will_paginate 2.3.17 → 3.0.pre
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 +24 -80
- data/LICENSE +1 -1
- data/README.rdoc +125 -0
- data/Rakefile +26 -22
- data/lib/will_paginate.rb +10 -84
- data/lib/will_paginate/array.rb +25 -8
- data/lib/will_paginate/collection.rb +15 -28
- data/lib/will_paginate/core_ext.rb +26 -0
- data/lib/will_paginate/deprecation.rb +50 -0
- data/lib/will_paginate/finders.rb +9 -0
- data/lib/will_paginate/finders/active_record.rb +158 -0
- data/lib/will_paginate/finders/active_resource.rb +51 -0
- data/lib/will_paginate/finders/base.rb +112 -0
- data/lib/will_paginate/finders/data_mapper.rb +30 -0
- data/lib/will_paginate/finders/sequel.rb +23 -0
- data/lib/will_paginate/railtie.rb +26 -0
- data/lib/will_paginate/version.rb +5 -5
- data/lib/will_paginate/view_helpers.rb +25 -436
- data/lib/will_paginate/view_helpers/action_view.rb +142 -0
- data/lib/will_paginate/view_helpers/base.rb +126 -0
- data/lib/will_paginate/view_helpers/link_renderer.rb +130 -0
- data/lib/will_paginate/view_helpers/link_renderer_base.rb +83 -0
- data/lib/will_paginate/view_helpers/merb.rb +13 -0
- data/spec/collection_spec.rb +147 -0
- data/spec/console +8 -0
- data/spec/console_fixtures.rb +8 -0
- data/spec/database.yml +22 -0
- data/spec/finders/active_record_spec.rb +377 -0
- data/spec/finders/active_resource_spec.rb +52 -0
- data/spec/finders/activerecord_test_connector.rb +114 -0
- data/spec/finders/data_mapper_spec.rb +62 -0
- data/spec/finders/data_mapper_test_connector.rb +20 -0
- data/spec/finders/sequel_spec.rb +53 -0
- data/spec/finders/sequel_test_connector.rb +9 -0
- data/spec/finders_spec.rb +76 -0
- data/{test → spec}/fixtures/admin.rb +0 -0
- data/{test → spec}/fixtures/developer.rb +2 -3
- data/{test → spec}/fixtures/developers_projects.yml +0 -0
- data/{test → spec}/fixtures/project.rb +2 -6
- data/{test → spec}/fixtures/projects.yml +1 -1
- data/{test → spec}/fixtures/replies.yml +0 -0
- data/{test → spec}/fixtures/reply.rb +1 -1
- data/{test → spec}/fixtures/schema.rb +0 -0
- data/spec/fixtures/topic.rb +7 -0
- data/{test → spec}/fixtures/topics.yml +0 -0
- data/{test → spec}/fixtures/user.rb +0 -0
- data/{test → spec}/fixtures/users.yml +0 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +74 -0
- data/spec/tasks.rake +60 -0
- data/spec/view_helpers/action_view_spec.rb +345 -0
- data/spec/view_helpers/base_spec.rb +64 -0
- data/spec/view_helpers/link_renderer_base_spec.rb +84 -0
- data/spec/view_helpers/view_example_group.rb +103 -0
- metadata +60 -65
- data/README.md +0 -53
- data/lib/will_paginate/finder.rb +0 -269
- data/lib/will_paginate/i18n.rb +0 -29
- data/lib/will_paginate/locale/en.yml +0 -33
- data/lib/will_paginate/named_scope.rb +0 -170
- data/lib/will_paginate/named_scope_patch.rb +0 -37
- data/lib/will_paginate/per_page.rb +0 -27
- data/test/ci.rb +0 -60
- data/test/collection_test.rb +0 -160
- data/test/console +0 -8
- data/test/database.yml +0 -16
- data/test/finder_test.rb +0 -527
- data/test/fixtures/topic.rb +0 -12
- data/test/gemfiles/Gemfile.1.2 +0 -13
- data/test/gemfiles/Gemfile.1.2.lock +0 -39
- data/test/gemfiles/Gemfile.2.0 +0 -16
- data/test/gemfiles/Gemfile.2.0.lock +0 -28
- data/test/gemfiles/Gemfile.2.1 +0 -16
- data/test/gemfiles/Gemfile.2.1.lock +0 -28
- data/test/gemfiles/Gemfile.2.2 +0 -16
- data/test/gemfiles/Gemfile.2.2.lock +0 -28
- data/test/helper.rb +0 -34
- data/test/lib/activerecord_test_case.rb +0 -38
- data/test/lib/activerecord_test_connector.rb +0 -86
- data/test/lib/load_fixtures.rb +0 -12
- data/test/lib/view_test_process.rb +0 -186
- data/test/view_test.rb +0 -380
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'will_paginate/per_page'
|
2
|
-
|
3
1
|
module WillPaginate
|
4
2
|
# = Invalid page number error
|
5
3
|
# This is an ArgumentError raised in case a page was requested that is either
|
@@ -19,24 +17,8 @@ module WillPaginate
|
|
19
17
|
# requested. Use <tt>WillPaginate::Collection#out_of_bounds?</tt> method to
|
20
18
|
# check for those cases and manually deal with them as you see fit.
|
21
19
|
class InvalidPage < ArgumentError
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
def self.validate(page_value, per_page_value)
|
26
|
-
page = page_value.to_i
|
27
|
-
raise self.new(page_value, page) if page < 1
|
28
|
-
per_page = per_page_value.to_i
|
29
|
-
offset = (page - 1) * per_page
|
30
|
-
raise self, "invalid offset: #{offset.inspect}" if offset < 0 or offset > BIGINT
|
31
|
-
[page, per_page]
|
32
|
-
end
|
33
|
-
|
34
|
-
def initialize(value, page_num = nil)
|
35
|
-
if page_num
|
36
|
-
super "#{value.inspect} given as value, which translates to '#{page_num}' as page number"
|
37
|
-
else
|
38
|
-
super value
|
39
|
-
end
|
20
|
+
def initialize(page, page_num) #:nodoc:
|
21
|
+
super "#{page.inspect} given as value, which translates to '#{page_num}' as page number"
|
40
22
|
end
|
41
23
|
end
|
42
24
|
|
@@ -51,10 +33,12 @@ module WillPaginate
|
|
51
33
|
#
|
52
34
|
# If you are writing a library that provides a collection which you would like
|
53
35
|
# to conform to this API, you don't have to copy these methods over; simply
|
54
|
-
# make your plugin/gem dependant on
|
36
|
+
# make your plugin/gem dependant on the "will_paginate" gem:
|
55
37
|
#
|
38
|
+
# gem 'will_paginate'
|
56
39
|
# require 'will_paginate/collection'
|
57
|
-
#
|
40
|
+
#
|
41
|
+
# # now use WillPaginate::Collection directly or subclass it
|
58
42
|
class Collection < Array
|
59
43
|
attr_reader :current_page, :per_page, :total_entries, :total_pages
|
60
44
|
|
@@ -62,8 +46,12 @@ module WillPaginate
|
|
62
46
|
# and the total number of entries. The last argument is optional because it
|
63
47
|
# is best to do lazy counting; in other words, count *conditionally* after
|
64
48
|
# populating the collection using the +replace+ method.
|
65
|
-
def initialize(page, per_page
|
66
|
-
@current_page
|
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
|
+
|
67
55
|
self.total_entries = total if total
|
68
56
|
end
|
69
57
|
|
@@ -94,7 +82,7 @@ module WillPaginate
|
|
94
82
|
#
|
95
83
|
# The Array#paginate API has since then changed, but this still serves as a
|
96
84
|
# fine example of WillPaginate::Collection usage.
|
97
|
-
def self.create(page, per_page, total = nil)
|
85
|
+
def self.create(page, per_page, total = nil, &block)
|
98
86
|
pager = new(page, per_page, total)
|
99
87
|
yield pager
|
100
88
|
pager
|
@@ -110,7 +98,7 @@ module WillPaginate
|
|
110
98
|
# Current offset of the paginated collection. If we're on the first page,
|
111
99
|
# it is always 0. If we're on the 2nd page and there are 30 entries per page,
|
112
100
|
# the offset is 30. This property is useful if you want to render ordinals
|
113
|
-
#
|
101
|
+
# besides your records: simply start with offset + 1.
|
114
102
|
def offset
|
115
103
|
(current_page - 1) * per_page
|
116
104
|
end
|
@@ -124,8 +112,7 @@ module WillPaginate
|
|
124
112
|
def next_page
|
125
113
|
current_page < total_pages ? (current_page + 1) : nil
|
126
114
|
end
|
127
|
-
|
128
|
-
# sets the <tt>total_entries</tt> property and calculates <tt>total_pages</tt>
|
115
|
+
|
129
116
|
def total_entries=(number)
|
130
117
|
@total_entries = number.to_i
|
131
118
|
@total_pages = (@total_entries / per_page.to_f).ceil
|
@@ -12,6 +12,8 @@ class Array
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
+
## everything below copied from ActiveSupport so we don't depend on it ##
|
16
|
+
|
15
17
|
unless Hash.instance_methods.include_method? :except
|
16
18
|
Hash.class_eval do
|
17
19
|
# Returns a new hash without the given keys.
|
@@ -41,3 +43,27 @@ unless Hash.instance_methods.include_method? :slice
|
|
41
43
|
end
|
42
44
|
end
|
43
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)
|
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,158 @@
|
|
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
|
+
# In Rails, this is automatically called to mix-in pagination functionality to ActiveRecord.
|
30
|
+
def self.enable!
|
31
|
+
::ActiveRecord::Base.class_eval do
|
32
|
+
extend ActiveRecord
|
33
|
+
end
|
34
|
+
|
35
|
+
# support pagination on associations and scopes
|
36
|
+
[::ActiveRecord::Relation, ::ActiveRecord::Associations::AssociationCollection].each do |klass|
|
37
|
+
klass.send(:include, ActiveRecord)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
|
42
|
+
# based on the params otherwise used by paginating finds: +page+ and
|
43
|
+
# +per_page+.
|
44
|
+
#
|
45
|
+
# Example:
|
46
|
+
#
|
47
|
+
# @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000],
|
48
|
+
# :page => params[:page], :per_page => 3
|
49
|
+
#
|
50
|
+
# A query for counting rows will automatically be generated if you don't
|
51
|
+
# supply <tt>:total_entries</tt>. If you experience problems with this
|
52
|
+
# generated SQL, you might want to perform the count manually in your
|
53
|
+
# application.
|
54
|
+
#
|
55
|
+
def paginate_by_sql(sql, options)
|
56
|
+
WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
|
57
|
+
query = sanitize_sql(sql.dup)
|
58
|
+
original_query = query.dup
|
59
|
+
# add limit, offset
|
60
|
+
query << " LIMIT #{pager.per_page} OFFSET #{pager.offset}"
|
61
|
+
# perfom the find
|
62
|
+
pager.replace find_by_sql(query)
|
63
|
+
|
64
|
+
unless pager.total_entries
|
65
|
+
count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
|
66
|
+
count_query = "SELECT COUNT(*) FROM (#{count_query})"
|
67
|
+
|
68
|
+
unless ['oracle', 'oci'].include?(self.connection.adapter_name.downcase)
|
69
|
+
count_query << ' AS count_table'
|
70
|
+
end
|
71
|
+
# perform the count query
|
72
|
+
pager.total_entries = count_by_sql(count_query)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def wp_query(options, pager, args, &block) #:nodoc:
|
80
|
+
finder = (options.delete(:finder) || 'find').to_s
|
81
|
+
find_options = options.except(:count).update(:offset => pager.offset, :limit => pager.per_page)
|
82
|
+
|
83
|
+
if finder == 'find'
|
84
|
+
if Array === args.first and !pager.total_entries
|
85
|
+
pager.total_entries = args.first.size
|
86
|
+
end
|
87
|
+
args << :all if args.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
args << find_options
|
91
|
+
pager.replace send(finder, *args, &block)
|
92
|
+
|
93
|
+
unless pager.total_entries
|
94
|
+
# magic counting
|
95
|
+
pager.total_entries = wp_count(options, args, finder)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Does the not-so-trivial job of finding out the total number of entries
|
100
|
+
# in the database. It relies on the ActiveRecord +count+ method.
|
101
|
+
def wp_count(options, args, finder) #:nodoc:
|
102
|
+
# find out if we are in a model or an association proxy
|
103
|
+
klass = (@owner and @reflection) ? @reflection.klass : self
|
104
|
+
count_options = wp_parse_count_options(options, klass)
|
105
|
+
|
106
|
+
# we may have to scope ...
|
107
|
+
counter = Proc.new { count(count_options) }
|
108
|
+
|
109
|
+
count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
|
110
|
+
# scope_out adds a 'with_finder' method which acts like with_scope, if it's present
|
111
|
+
# then execute the count with the scoping provided by the with_finder
|
112
|
+
send(scoper, &counter)
|
113
|
+
elsif finder =~ /^find_(all_by|by)_([_a-zA-Z]\w*)$/
|
114
|
+
# extract conditions from calls like "paginate_by_foo_and_bar"
|
115
|
+
attribute_names = $2.split('_and_')
|
116
|
+
conditions = construct_attributes_from_arguments(attribute_names, args)
|
117
|
+
with_scope(:find => { :conditions => conditions }, &counter)
|
118
|
+
else
|
119
|
+
counter.call
|
120
|
+
end
|
121
|
+
|
122
|
+
count.respond_to?(:length) ? count.length : count
|
123
|
+
end
|
124
|
+
|
125
|
+
def wp_parse_count_options(options, klass) #:nodoc:
|
126
|
+
excludees = [:count, :order, :limit, :offset, :readonly]
|
127
|
+
|
128
|
+
# Use :select from scope if it isn't already present.
|
129
|
+
# FIXME: this triggers extra queries when going through associations
|
130
|
+
# if options[:select].blank? && current_scoped_methods && current_scoped_methods.select_values.present?
|
131
|
+
# options[:select] = current_scoped_methods.select_values.join(", ")
|
132
|
+
# end
|
133
|
+
|
134
|
+
if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
|
135
|
+
# Remove quoting and check for table_name.*-like statement.
|
136
|
+
if options[:select].gsub('`', '') =~ /\w+\.\*/
|
137
|
+
options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
|
138
|
+
end
|
139
|
+
else
|
140
|
+
excludees << :select
|
141
|
+
end
|
142
|
+
|
143
|
+
# count expects (almost) the same options as find
|
144
|
+
count_options = options.except *excludees
|
145
|
+
|
146
|
+
# merge the hash found in :count
|
147
|
+
# this allows you to specify :select, :order, or anything else just for the count query
|
148
|
+
count_options.update options[:count] if options[:count]
|
149
|
+
|
150
|
+
# forget about includes if they are irrelevant when counting
|
151
|
+
if count_options[:include] and count_options[:conditions].blank? and count_options[:group].blank?
|
152
|
+
count_options.delete :include
|
153
|
+
end
|
154
|
+
|
155
|
+
count_options
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'will_paginate/finders/base'
|
2
|
+
require 'active_resource'
|
3
|
+
|
4
|
+
module WillPaginate::Finders
|
5
|
+
# Paginate your ActiveResource models.
|
6
|
+
#
|
7
|
+
# @posts = Post.paginate :all, :params => {
|
8
|
+
# :page => params[:page], :order => 'created_at DESC'
|
9
|
+
# }
|
10
|
+
#
|
11
|
+
module ActiveResource
|
12
|
+
include WillPaginate::Finders::Base
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def wp_query(options, pager, args, &block) #:nodoc:
|
17
|
+
unless args.empty? or args.first == :all
|
18
|
+
raise ArgumentError, "finder arguments other than :all are not supported for pagination (#{args.inspect} given)"
|
19
|
+
end
|
20
|
+
params = (options[:params] ||= {})
|
21
|
+
params[:page] = pager.current_page
|
22
|
+
params[:per_page] = pager.per_page
|
23
|
+
|
24
|
+
pager.replace find_every(options, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Takes the format that Hash.from_xml produces out of an unknown type
|
28
|
+
# (produced by WillPaginate::Collection#to_xml_with_collection_type),
|
29
|
+
# parses it into a WillPaginate::Collection,
|
30
|
+
# and forwards the result to the former +instantiate_collection+ method.
|
31
|
+
# It only does this for hashes that have a :type => "collection".
|
32
|
+
def instantiate_collection_with_collection(collection, prefix_options = {}) #:nodoc:
|
33
|
+
if collection.is_a?(Hash) && collection["type"] == "collection"
|
34
|
+
collectables = collection.values.find{ |c| c.is_a?(Hash) || c.is_a?(Array) }
|
35
|
+
collectables = [collectables].compact unless collectables.kind_of?(Array)
|
36
|
+
instantiated_collection = WillPaginate::Collection.create(collection["current_page"], collection["per_page"], collection["total_entries"]) do |pager|
|
37
|
+
pager.replace instantiate_collection_without_collection(collectables, prefix_options)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
instantiate_collection_without_collection(collection, prefix_options)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
ActiveResource::Base.class_eval do
|
47
|
+
extend WillPaginate::Finders::ActiveResource
|
48
|
+
class << self
|
49
|
+
# alias_method_chain :instantiate_collection, :collection
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'will_paginate/core_ext'
|
2
|
+
|
3
|
+
module WillPaginate
|
4
|
+
module Finders
|
5
|
+
# = Database-agnostic finder module
|
6
|
+
#
|
7
|
+
# Out of the box, will_paginate supports hooking in several ORMs to
|
8
|
+
# provide paginating finders based on their API. As of this writing, the
|
9
|
+
# supported libraries are:
|
10
|
+
#
|
11
|
+
# * ActiveRecord
|
12
|
+
# * DataMapper
|
13
|
+
# * Sequel
|
14
|
+
#
|
15
|
+
# It's easy to write your own adapter for anything that can load data with
|
16
|
+
# explicit limit and offset settings. DataMapper adapter is a nice and
|
17
|
+
# compact example of writing an adapter to bring the +paginate+ method to
|
18
|
+
# DataMapper models.
|
19
|
+
#
|
20
|
+
# == The importance of SQL's <tt>ORDER BY</tt>
|
21
|
+
#
|
22
|
+
# In most ORMs, <tt>:order</tt> parameter specifies columns for the
|
23
|
+
# <tt>ORDER BY</tt> clause in SQL. It is important to have it, since
|
24
|
+
# pagination only makes sense with ordered sets. Without the order clause,
|
25
|
+
# databases aren't required to do consistent ordering when performing
|
26
|
+
# <tt>SELECT</tt> queries.
|
27
|
+
#
|
28
|
+
# Ordering by a field for which many records share the same value (e.g.
|
29
|
+
# "status") can still result in incorrect ordering with some databases (MS
|
30
|
+
# SQL and Postgres for instance). With these databases it's recommend that
|
31
|
+
# you order by primary key as well. That is, instead of ordering by
|
32
|
+
# "status DESC", use the alternative "status DESC, id DESC" and this will
|
33
|
+
# yield consistent results.
|
34
|
+
#
|
35
|
+
# Therefore, make sure you are doing ordering on a column that makes the
|
36
|
+
# most sense in the current context. Make that obvious to the user, also.
|
37
|
+
# For perfomance reasons you will also want to add an index to that column.
|
38
|
+
module Base
|
39
|
+
def per_page
|
40
|
+
@per_page ||= 30
|
41
|
+
end
|
42
|
+
|
43
|
+
def per_page=(limit)
|
44
|
+
@per_page = limit.to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
# This is the main paginating finder.
|
48
|
+
#
|
49
|
+
# == Special parameters for paginating finders
|
50
|
+
# * <tt>:page</tt> -- REQUIRED, but defaults to 1 if false or nil
|
51
|
+
# * <tt>:per_page</tt> -- defaults to <tt>CurrentModel.per_page</tt> (which is 30 if not overridden)
|
52
|
+
# * <tt>:total_entries</tt> -- use only if you manually count total entries
|
53
|
+
# * <tt>:count</tt> -- additional options that are passed on to +count+
|
54
|
+
# * <tt>:finder</tt> -- name of the finder method to use (default: "find")
|
55
|
+
#
|
56
|
+
# All other options (+conditions+, +order+, ...) are forwarded to +find+
|
57
|
+
# and +count+ calls.
|
58
|
+
def paginate(*args, &block)
|
59
|
+
options = args.pop
|
60
|
+
page, per_page, total_entries = wp_parse_options(options)
|
61
|
+
|
62
|
+
WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
|
63
|
+
query_options = options.except :page, :per_page, :total_entries
|
64
|
+
wp_query(query_options, pager, args, &block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Iterates through all records by loading one page at a time. This is useful
|
69
|
+
# for migrations or any other use case where you don't want to load all the
|
70
|
+
# records in memory at once.
|
71
|
+
#
|
72
|
+
# It uses +paginate+ internally; therefore it accepts all of its options.
|
73
|
+
# You can specify a starting page with <tt>:page</tt> (default is 1). Default
|
74
|
+
# <tt>:order</tt> is <tt>"id"</tt>, override if necessary.
|
75
|
+
#
|
76
|
+
# {Jamis Buck describes this}[http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord]
|
77
|
+
# and also uses a more efficient way for MySQL.
|
78
|
+
def paginated_each(options = {}, &block)
|
79
|
+
options = { :order => 'id', :page => 1 }.merge options
|
80
|
+
options[:page] = options[:page].to_i
|
81
|
+
options[:total_entries] = 0 # skip the individual count queries
|
82
|
+
total = 0
|
83
|
+
|
84
|
+
begin
|
85
|
+
collection = paginate(options)
|
86
|
+
total += collection.each(&block).size
|
87
|
+
options[:page] += 1
|
88
|
+
end until collection.size < collection.per_page
|
89
|
+
|
90
|
+
total
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
def wp_parse_options(options) #:nodoc:
|
96
|
+
raise ArgumentError, 'parameter hash expected' unless Hash === options
|
97
|
+
raise ArgumentError, ':page parameter required' unless options.key? :page
|
98
|
+
|
99
|
+
if options[:count] and options[:total_entries]
|
100
|
+
raise ArgumentError, ':count and :total_entries are mutually exclusive'
|
101
|
+
end
|
102
|
+
|
103
|
+
page = options[:page] || 1
|
104
|
+
per_page = options[:per_page] || self.per_page
|
105
|
+
total = options[:total_entries]
|
106
|
+
|
107
|
+
return [page, per_page, total]
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|