will_mostly_paginate 2.4.2

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.
@@ -0,0 +1,154 @@
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)
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 this library and do:
37
+ #
38
+ # require 'will_paginate/collection'
39
+ # # WillPaginate::Collection is now available for use
40
+ class Collection < Array
41
+ attr_reader :current_page, :per_page, :total_entries, :total_pages, :page_all
42
+ attr_accessor :next_exists
43
+
44
+ # Arguments to the constructor are the current page number, per-page limit the
45
+ # total number of entries, and whether or not to enumerate through all pages. The
46
+ # last two arguments are optional because it is best to do lazy counting; in other
47
+ # words, count *conditionally* after populating the collection using the +replace+
48
+ # method, and because it is assumed that you want to know the total number of pages
49
+ # in your collection.
50
+ def initialize(page, per_page, total = nil, page_all=true)
51
+ @current_page = page.to_i
52
+ raise InvalidPage.new(page, @current_page) if @current_page < 1
53
+ @per_page = per_page.to_i
54
+ raise ArgumentError, "`per_page` setting cannot be less than 1 (#{@per_page} given)" if @per_page < 1
55
+
56
+ @page_all = page_all
57
+ @next_exists = true # Worst case, we'll get an empty page...
58
+ self.total_entries = total if total
59
+ end
60
+
61
+ # Just like +new+, but yields the object after instantiation and returns it
62
+ # afterwards. This is very useful for manual pagination:
63
+ #
64
+ # @entries = WillPaginate::Collection.create(1, 10) do |pager|
65
+ # result = Post.find(:all, :limit => pager.per_page, :offset => pager.offset)
66
+ # # inject the result array into the paginated collection:
67
+ # pager.replace(result)
68
+ #
69
+ # unless pager.total_entries
70
+ # # the pager didn't manage to guess the total count, do it manually
71
+ # pager.total_entries = Post.count
72
+ # end
73
+ # end
74
+ #
75
+ # The possibilities with this are endless. For another example, here is how
76
+ # WillPaginate used to define pagination for Array instances:
77
+ #
78
+ # Array.class_eval do
79
+ # def paginate(page = 1, per_page = 15)
80
+ # WillPaginate::Collection.create(page, per_page, size) do |pager|
81
+ # pager.replace self[pager.offset, pager.per_page].to_a
82
+ # end
83
+ # end
84
+ # end
85
+ #
86
+ # The Array#paginate API has since then changed, but this still serves as a
87
+ # fine example of WillPaginate::Collection usage.
88
+ def self.create(page, per_page, total = nil, page_all=true)
89
+ pager = new(page, per_page, total, page_all)
90
+ yield pager
91
+ pager
92
+ end
93
+
94
+ # Helper method that is true when someone tries to fetch a page with a
95
+ # larger number than the last page. Can be used in combination with flashes
96
+ # and redirecting.
97
+ def out_of_bounds?
98
+ !page_all || current_page > total_pages
99
+ end
100
+
101
+ # Current offset of the paginated collection. If we're on the first page,
102
+ # it is always 0. If we're on the 2nd page and there are 30 entries per page,
103
+ # the offset is 30. This property is useful if you want to render ordinals
104
+ # side by side with records in the view: simply start with offset + 1.
105
+ def offset
106
+ (current_page - 1) * per_page
107
+ end
108
+
109
+ # current_page - 1 or nil if there is no previous page
110
+ def previous_page
111
+ current_page > 1 ? (current_page - 1) : nil
112
+ end
113
+
114
+ # current_page + 1 or nil if there is no next page
115
+ def next_page
116
+ if page_all
117
+ current_page < total_pages ? (current_page + 1) : nil
118
+ else
119
+ next_exists ? (current_page + 1) : nil
120
+ end
121
+ end
122
+
123
+ # sets the <tt>total_entries</tt> property and calculates <tt>total_pages</tt>
124
+ def total_entries=(number)
125
+ @total_entries = number.to_i
126
+ @total_pages = (@total_entries / per_page.to_f).ceil
127
+ @next_exists = ((@total_entries - per_page * current_page) > 0)
128
+ end
129
+
130
+ # This is a magic wrapper for the original Array#replace method. It serves
131
+ # for populating the paginated collection after initialization.
132
+ #
133
+ # Why magic? Because it tries to guess the total number of entries judging
134
+ # by the size of given array. If it is shorter than +per_page+ limit, then we
135
+ # know we're on the last page. This trick is very useful for avoiding
136
+ # unnecessary hits to the database to do the counting after we fetched the
137
+ # data for the current page.
138
+ #
139
+ # However, after using +replace+ you should always test the value of
140
+ # +total_entries+ and set it to a proper value if it's +nil+. See the example
141
+ # in +create+.
142
+ def replace(array)
143
+ result = super
144
+
145
+ # The collection is shorter then page limit? Rejoice, because
146
+ # then we know that we are on the last page!
147
+ if total_entries.nil? and length < per_page and (current_page == 1 or length > 0)
148
+ self.total_entries = offset + length
149
+ end
150
+
151
+ result
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,43 @@
1
+ require 'set'
2
+ require 'will_mostly_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
+ unless Hash.instance_methods.include_method? :except
16
+ Hash.class_eval do
17
+ # Returns a new hash without the given keys.
18
+ def except(*keys)
19
+ rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
20
+ reject { |key,| rejected.include?(key) }
21
+ end
22
+
23
+ # Replaces the hash without only the given keys.
24
+ def except!(*keys)
25
+ replace(except(*keys))
26
+ end
27
+ end
28
+ end
29
+
30
+ unless Hash.instance_methods.include_method? :slice
31
+ Hash.class_eval do
32
+ # Returns a new hash with only the given keys.
33
+ def slice(*keys)
34
+ allowed = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
35
+ reject { |key,| !allowed.include?(key) }
36
+ end
37
+
38
+ # Replaces the hash with only the given keys.
39
+ def slice!(*keys)
40
+ replace(slice(*keys))
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,271 @@
1
+ require 'will_mostly_paginate/core_ext'
2
+
3
+ module WillPaginate
4
+ # A mixin for ActiveRecord::Base. Provides +per_page+ class method
5
+ # and hooks things up to provide paginating finders.
6
+ #
7
+ # Find out more in WillPaginate::Finder::ClassMethods
8
+ #
9
+ module Finder
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ class << base
13
+ alias_method_chain :method_missing, :paginate
14
+ # alias_method_chain :find_every, :paginate
15
+ define_method(:per_page) { 30 } unless respond_to?(:per_page)
16
+ end
17
+ end
18
+
19
+ # = Paginating finders for ActiveRecord models
20
+ #
21
+ # WillPaginate adds +paginate+, +per_page+ and other methods to
22
+ # ActiveRecord::Base class methods and associations. It also hooks into
23
+ # +method_missing+ to intercept pagination calls to dynamic finders such as
24
+ # +paginate_by_user_id+ and translate them to ordinary finders
25
+ # (+find_all_by_user_id+ in this case).
26
+ #
27
+ # In short, paginating finders are equivalent to ActiveRecord finders; the
28
+ # only difference is that we start with "paginate" instead of "find" and
29
+ # that <tt>:page</tt> is required parameter:
30
+ #
31
+ # @posts = Post.paginate :all, :page => params[:page], :order => 'created_at DESC'
32
+ #
33
+ # In paginating finders, "all" is implicit. There is no sense in paginating
34
+ # a single record, right? So, you can drop the <tt>:all</tt> argument:
35
+ #
36
+ # Post.paginate(...) => Post.find :all
37
+ # Post.paginate_all_by_something => Post.find_all_by_something
38
+ # Post.paginate_by_something => Post.find_all_by_something
39
+ #
40
+ # == The importance of the <tt>:order</tt> parameter
41
+ #
42
+ # In ActiveRecord finders, <tt>:order</tt> parameter specifies columns for
43
+ # the <tt>ORDER BY</tt> clause in SQL. It is important to have it, since
44
+ # pagination only makes sense with ordered sets. Without the <tt>ORDER
45
+ # BY</tt> clause, databases aren't required to do consistent ordering when
46
+ # performing <tt>SELECT</tt> queries; this is especially true for
47
+ # PostgreSQL.
48
+ #
49
+ # Therefore, make sure you are doing ordering on a column that makes the
50
+ # most sense in the current context. Make that obvious to the user, also.
51
+ # For perfomance reasons you will also want to add an index to that column.
52
+ module ClassMethods
53
+ # This is the main paginating finder.
54
+ #
55
+ # == Special parameters for paginating finders
56
+ # * <tt>:page</tt> -- REQUIRED, but defaults to 1 if false or nil
57
+ # * <tt>:per_page</tt> -- defaults to <tt>CurrentModel.per_page</tt> (which is 30 if not overridden)
58
+ # * <tt>:total_entries</tt> -- use only if you manually count total entries
59
+ # * <tt>:count</tt> -- additional options that are passed on to +count+
60
+ # * <tt>:finder</tt> -- name of the ActiveRecord finder used (default: "find")
61
+ # * <tt>:page_all</tt> -- whether or not to count the total number of pages
62
+ #
63
+ # All other options (+conditions+, +order+, ...) are forwarded to +find+
64
+ # and +count+ calls.
65
+ def paginate(*args)
66
+ options = args.pop
67
+ page, per_page, total_entries, page_all = wp_parse_options(options)
68
+ finder = (options[:finder] || 'find').to_s
69
+
70
+ if finder == 'find'
71
+ # an array of IDs may have been given:
72
+ total_entries ||= (Array === args.first and args.first.size)
73
+ # :all is implicit
74
+ args.unshift(:all) if args.empty?
75
+ end
76
+
77
+ WillPaginate::Collection.create(page, per_page, total_entries, page_all) do |pager|
78
+ count_options = options.except :page, :per_page, :total_entries, :page_all, :finder
79
+ find_options = count_options.except(:count).update(:offset => pager.offset, :limit => pager.per_page + (page_all ? 0 : 1))
80
+
81
+ args << find_options
82
+ results = send(finder, *args) { |*a| yield(*a) if block_given? }
83
+
84
+ if page_all
85
+ pager.replace(results)
86
+ # magic counting for user convenience:
87
+ pager.total_entries = wp_count(count_options, args, finder) unless pager.total_entries
88
+ else
89
+ # If we're not paging all pages, then we only need to know if the next page exists
90
+ if results.length > pager.per_page
91
+ pager.next_exists = true
92
+ pager.replace(results[0..-2])
93
+ else
94
+ pager.next_exists = false
95
+ pager.replace(results)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ # Iterates through all records by loading one page at a time. This is useful
102
+ # for migrations or any other use case where you don't want to load all the
103
+ # records in memory at once.
104
+ #
105
+ # It uses +paginate+ internally; therefore it accepts all of its options.
106
+ # You can specify a starting page with <tt>:page</tt> (default is 1). Default
107
+ # <tt>:order</tt> is <tt>"id"</tt>, override if necessary.
108
+ #
109
+ # See {Faking Cursors in ActiveRecord}[http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord]
110
+ # where Jamis Buck describes this and a more efficient way for MySQL.
111
+ def paginated_each(options = {})
112
+ options = { :order => 'id', :page => 1 }.merge options
113
+ options[:page] = options[:page].to_i
114
+ options[:total_entries] = 0 # skip the individual count queries
115
+ total = 0
116
+
117
+ begin
118
+ collection = paginate(options)
119
+ with_exclusive_scope(:find => {}) do
120
+ # using exclusive scope so that the block is yielded in scope-free context
121
+ total += collection.each { |item| yield item }.size
122
+ end
123
+ options[:page] += 1
124
+ end until collection.size < collection.per_page
125
+
126
+ total
127
+ end
128
+
129
+ # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
130
+ # based on the params otherwise used by paginating finds: +page+ and
131
+ # +per_page+.
132
+ #
133
+ # Example:
134
+ #
135
+ # @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000],
136
+ # :page => params[:page], :per_page => 3
137
+ #
138
+ # A query for counting rows will automatically be generated if you don't
139
+ # supply <tt>:total_entries</tt>. If you experience problems with this
140
+ # generated SQL, you might want to perform the count manually in your
141
+ # application.
142
+ #
143
+ def paginate_by_sql(sql, options)
144
+ WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
145
+ query = sanitize_sql(sql.dup)
146
+ original_query = query.dup
147
+ # add limit, offset
148
+ add_limit! query, :offset => pager.offset, :limit => pager.per_page
149
+ # perfom the find
150
+ pager.replace find_by_sql(query)
151
+
152
+ unless pager.total_entries
153
+ count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
154
+ count_query = "SELECT COUNT(*) FROM (#{count_query})"
155
+
156
+ unless self.connection.adapter_name =~ /^(oracle|oci$)/i
157
+ count_query << ' AS count_table'
158
+ end
159
+ # perform the count query
160
+ pager.total_entries = count_by_sql(count_query)
161
+ end
162
+ end
163
+ end
164
+
165
+ def respond_to?(method, include_priv = false) #:nodoc:
166
+ case method.to_sym
167
+ when :paginate, :paginate_by_sql
168
+ true
169
+ else
170
+ super || super(method.to_s.sub(/^paginate/, 'find'), include_priv)
171
+ end
172
+ end
173
+
174
+ protected
175
+
176
+ def method_missing_with_paginate(method, *args) #:nodoc:
177
+ # did somebody tried to paginate? if not, let them be
178
+ unless method.to_s.index('paginate') == 0
179
+ if block_given?
180
+ return method_missing_without_paginate(method, *args) { |*a| yield(*a) }
181
+ else
182
+ return method_missing_without_paginate(method, *args)
183
+ end
184
+ end
185
+
186
+ # paginate finders are really just find_* with limit and offset
187
+ finder = method.to_s.sub('paginate', 'find')
188
+ finder.sub!('find', 'find_all') if finder.index('find_by_') == 0
189
+
190
+ options = args.pop
191
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
192
+ options = options.dup
193
+ options[:finder] = finder
194
+ args << options
195
+
196
+ paginate(*args) { |*a| yield(*a) if block_given? }
197
+ end
198
+
199
+ # Does the not-so-trivial job of finding out the total number of entries
200
+ # in the database. It relies on the ActiveRecord +count+ method.
201
+ def wp_count(options, args, finder)
202
+ excludees = [:count, :order, :limit, :offset, :readonly]
203
+ excludees << :from unless ActiveRecord::Calculations::CALCULATIONS_OPTIONS.include?(:from)
204
+
205
+ # we may be in a model or an association proxy
206
+ klass = (@owner and @reflection) ? @reflection.klass : self
207
+
208
+ # Use :select from scope if it isn't already present.
209
+ options[:select] = scope(:find, :select) unless options[:select]
210
+
211
+ if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
212
+ # Remove quoting and check for table_name.*-like statement.
213
+ if options[:select].gsub(/[`"]/, '') =~ /\w+\.\*/
214
+ options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
215
+ end
216
+ else
217
+ excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
218
+ end
219
+
220
+ # count expects (almost) the same options as find
221
+ count_options = options.except *excludees
222
+
223
+ # merge the hash found in :count
224
+ # this allows you to specify :select, :order, or anything else just for the count query
225
+ count_options.update options[:count] if options[:count]
226
+
227
+ # forget about includes if they are irrelevant (Rails 2.1)
228
+ if count_options[:include] and
229
+ klass.private_methods.include_method?(:references_eager_loaded_tables?) and
230
+ !klass.send(:references_eager_loaded_tables?, count_options)
231
+ count_options.delete :include
232
+ end
233
+
234
+ # we may have to scope ...
235
+ counter = Proc.new { count(count_options) }
236
+
237
+ count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
238
+ # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
239
+ # then execute the count with the scoping provided by the with_finder
240
+ send(scoper, &counter)
241
+ elsif finder =~ /^find_(all_by|by)_([_a-zA-Z]\w*)$/
242
+ # extract conditions from calls like "paginate_by_foo_and_bar"
243
+ attribute_names = $2.split('_and_')
244
+ conditions = construct_attributes_from_arguments(attribute_names, args)
245
+ with_scope(:find => { :conditions => conditions }, &counter)
246
+ else
247
+ counter.call
248
+ end
249
+
250
+ (!count.is_a?(Integer) && count.respond_to?(:length)) ? count.length : count
251
+ end
252
+
253
+ def wp_parse_options(options) #:nodoc:
254
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
255
+ options = options.symbolize_keys
256
+ raise ArgumentError, ':page parameter required' unless options.key? :page
257
+
258
+ if options[:count] and options[:total_entries]
259
+ raise ArgumentError, ':count and :total_entries are mutually exclusive'
260
+ end
261
+
262
+ page = options[:page] || 1
263
+ per_page = options[:per_page] || self.per_page
264
+ total = options[:total_entries]
265
+ page_all = options.keys.include?(:page_all) ? options[:page_all] : true
266
+ [page, per_page, total, page_all]
267
+ end
268
+
269
+ end
270
+ end
271
+ end