Mercurial > hg > soundsoftware-site
comparison vendor/plugins/classic_pagination/lib/pagination.rb @ 0:513646585e45
* Import Redmine trunk SVN rev 3859
author | Chris Cannam |
---|---|
date | Fri, 23 Jul 2010 15:52:44 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:513646585e45 |
---|---|
1 module ActionController | |
2 # === Action Pack pagination for Active Record collections | |
3 # | |
4 # The Pagination module aids in the process of paging large collections of | |
5 # Active Record objects. It offers macro-style automatic fetching of your | |
6 # model for multiple views, or explicit fetching for single actions. And if | |
7 # the magic isn't flexible enough for your needs, you can create your own | |
8 # paginators with a minimal amount of code. | |
9 # | |
10 # The Pagination module can handle as much or as little as you wish. In the | |
11 # controller, have it automatically query your model for pagination; or, | |
12 # if you prefer, create Paginator objects yourself. | |
13 # | |
14 # Pagination is included automatically for all controllers. | |
15 # | |
16 # For help rendering pagination links, see | |
17 # ActionView::Helpers::PaginationHelper. | |
18 # | |
19 # ==== Automatic pagination for every action in a controller | |
20 # | |
21 # class PersonController < ApplicationController | |
22 # model :person | |
23 # | |
24 # paginate :people, :order => 'last_name, first_name', | |
25 # :per_page => 20 | |
26 # | |
27 # # ... | |
28 # end | |
29 # | |
30 # Each action in this controller now has access to a <tt>@people</tt> | |
31 # instance variable, which is an ordered collection of model objects for the | |
32 # current page (at most 20, sorted by last name and first name), and a | |
33 # <tt>@person_pages</tt> Paginator instance. The current page is determined | |
34 # by the <tt>params[:page]</tt> variable. | |
35 # | |
36 # ==== Pagination for a single action | |
37 # | |
38 # def list | |
39 # @person_pages, @people = | |
40 # paginate :people, :order => 'last_name, first_name' | |
41 # end | |
42 # | |
43 # Like the previous example, but explicitly creates <tt>@person_pages</tt> | |
44 # and <tt>@people</tt> for a single action, and uses the default of 10 items | |
45 # per page. | |
46 # | |
47 # ==== Custom/"classic" pagination | |
48 # | |
49 # def list | |
50 # @person_pages = Paginator.new self, Person.count, 10, params[:page] | |
51 # @people = Person.find :all, :order => 'last_name, first_name', | |
52 # :limit => @person_pages.items_per_page, | |
53 # :offset => @person_pages.current.offset | |
54 # end | |
55 # | |
56 # Explicitly creates the paginator from the previous example and uses | |
57 # Paginator#to_sql to retrieve <tt>@people</tt> from the model. | |
58 # | |
59 module Pagination | |
60 unless const_defined?(:OPTIONS) | |
61 # A hash holding options for controllers using macro-style pagination | |
62 OPTIONS = Hash.new | |
63 | |
64 # The default options for pagination | |
65 DEFAULT_OPTIONS = { | |
66 :class_name => nil, | |
67 :singular_name => nil, | |
68 :per_page => 10, | |
69 :conditions => nil, | |
70 :order_by => nil, | |
71 :order => nil, | |
72 :join => nil, | |
73 :joins => nil, | |
74 :count => nil, | |
75 :include => nil, | |
76 :select => nil, | |
77 :group => nil, | |
78 :parameter => 'page' | |
79 } | |
80 else | |
81 DEFAULT_OPTIONS[:group] = nil | |
82 end | |
83 | |
84 def self.included(base) #:nodoc: | |
85 super | |
86 base.extend(ClassMethods) | |
87 end | |
88 | |
89 def self.validate_options!(collection_id, options, in_action) #:nodoc: | |
90 options.merge!(DEFAULT_OPTIONS) {|key, old, new| old} | |
91 | |
92 valid_options = DEFAULT_OPTIONS.keys | |
93 valid_options << :actions unless in_action | |
94 | |
95 unknown_option_keys = options.keys - valid_options | |
96 raise ActionController::ActionControllerError, | |
97 "Unknown options: #{unknown_option_keys.join(', ')}" unless | |
98 unknown_option_keys.empty? | |
99 | |
100 options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s) | |
101 options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name]) | |
102 end | |
103 | |
104 # Returns a paginator and a collection of Active Record model instances | |
105 # for the paginator's current page. This is designed to be used in a | |
106 # single action; to automatically paginate multiple actions, consider | |
107 # ClassMethods#paginate. | |
108 # | |
109 # +options+ are: | |
110 # <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name | |
111 # <tt>:class_name</tt>:: the class name to use, if it can't be inferred by | |
112 # camelizing the singular name | |
113 # <tt>:per_page</tt>:: the maximum number of items to include in a | |
114 # single page. Defaults to 10 | |
115 # <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and | |
116 # Model.count | |
117 # <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params) | |
118 # <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params) | |
119 # <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params) | |
120 # and Model.count | |
121 # <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params) | |
122 # and Model.count | |
123 # <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params) | |
124 # and Model.count | |
125 # <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params) | |
126 # | |
127 # <tt>:count</tt>:: parameter passed as :select option to Model.count(*params) | |
128 # | |
129 # <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records | |
130 # | |
131 def paginate(collection_id, options={}) | |
132 Pagination.validate_options!(collection_id, options, true) | |
133 paginator_and_collection_for(collection_id, options) | |
134 end | |
135 | |
136 # These methods become class methods on any controller | |
137 module ClassMethods | |
138 # Creates a +before_filter+ which automatically paginates an Active | |
139 # Record model for all actions in a controller (or certain actions if | |
140 # specified with the <tt>:actions</tt> option). | |
141 # | |
142 # +options+ are the same as PaginationHelper#paginate, with the addition | |
143 # of: | |
144 # <tt>:actions</tt>:: an array of actions for which the pagination is | |
145 # active. Defaults to +nil+ (i.e., every action) | |
146 def paginate(collection_id, options={}) | |
147 Pagination.validate_options!(collection_id, options, false) | |
148 module_eval do | |
149 before_filter :create_paginators_and_retrieve_collections | |
150 OPTIONS[self] ||= Hash.new | |
151 OPTIONS[self][collection_id] = options | |
152 end | |
153 end | |
154 end | |
155 | |
156 def create_paginators_and_retrieve_collections #:nodoc: | |
157 Pagination::OPTIONS[self.class].each do |collection_id, options| | |
158 next unless options[:actions].include? action_name if | |
159 options[:actions] | |
160 | |
161 paginator, collection = | |
162 paginator_and_collection_for(collection_id, options) | |
163 | |
164 paginator_name = "@#{options[:singular_name]}_pages" | |
165 self.instance_variable_set(paginator_name, paginator) | |
166 | |
167 collection_name = "@#{collection_id.to_s}" | |
168 self.instance_variable_set(collection_name, collection) | |
169 end | |
170 end | |
171 | |
172 # Returns the total number of items in the collection to be paginated for | |
173 # the +model+ and given +conditions+. Override this method to implement a | |
174 # custom counter. | |
175 def count_collection_for_pagination(model, options) | |
176 model.count(:conditions => options[:conditions], | |
177 :joins => options[:join] || options[:joins], | |
178 :include => options[:include], | |
179 :select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count])) | |
180 end | |
181 | |
182 # Returns a collection of items for the given +model+ and +options[conditions]+, | |
183 # ordered by +options[order]+, for the current page in the given +paginator+. | |
184 # Override this method to implement a custom finder. | |
185 def find_collection_for_pagination(model, options, paginator) | |
186 model.find(:all, :conditions => options[:conditions], | |
187 :order => options[:order_by] || options[:order], | |
188 :joins => options[:join] || options[:joins], :include => options[:include], | |
189 :select => options[:select], :limit => options[:per_page], | |
190 :group => options[:group], :offset => paginator.current.offset) | |
191 end | |
192 | |
193 protected :create_paginators_and_retrieve_collections, | |
194 :count_collection_for_pagination, | |
195 :find_collection_for_pagination | |
196 | |
197 def paginator_and_collection_for(collection_id, options) #:nodoc: | |
198 klass = options[:class_name].constantize | |
199 page = params[options[:parameter]] | |
200 count = count_collection_for_pagination(klass, options) | |
201 paginator = Paginator.new(self, count, options[:per_page], page) | |
202 collection = find_collection_for_pagination(klass, options, paginator) | |
203 | |
204 return paginator, collection | |
205 end | |
206 | |
207 private :paginator_and_collection_for | |
208 | |
209 # A class representing a paginator for an Active Record collection. | |
210 class Paginator | |
211 include Enumerable | |
212 | |
213 # Creates a new Paginator on the given +controller+ for a set of items | |
214 # of size +item_count+ and having +items_per_page+ items per page. | |
215 # Raises ArgumentError if items_per_page is out of bounds (i.e., less | |
216 # than or equal to zero). The page CGI parameter for links defaults to | |
217 # "page" and can be overridden with +page_parameter+. | |
218 def initialize(controller, item_count, items_per_page, current_page=1) | |
219 raise ArgumentError, 'must have at least one item per page' if | |
220 items_per_page <= 0 | |
221 | |
222 @controller = controller | |
223 @item_count = item_count || 0 | |
224 @items_per_page = items_per_page | |
225 @pages = {} | |
226 | |
227 self.current_page = current_page | |
228 end | |
229 attr_reader :controller, :item_count, :items_per_page | |
230 | |
231 # Sets the current page number of this paginator. If +page+ is a Page | |
232 # object, its +number+ attribute is used as the value; if the page does | |
233 # not belong to this Paginator, an ArgumentError is raised. | |
234 def current_page=(page) | |
235 if page.is_a? Page | |
236 raise ArgumentError, 'Page/Paginator mismatch' unless | |
237 page.paginator == self | |
238 end | |
239 page = page.to_i | |
240 @current_page_number = has_page_number?(page) ? page : 1 | |
241 end | |
242 | |
243 # Returns a Page object representing this paginator's current page. | |
244 def current_page | |
245 @current_page ||= self[@current_page_number] | |
246 end | |
247 alias current :current_page | |
248 | |
249 # Returns a new Page representing the first page in this paginator. | |
250 def first_page | |
251 @first_page ||= self[1] | |
252 end | |
253 alias first :first_page | |
254 | |
255 # Returns a new Page representing the last page in this paginator. | |
256 def last_page | |
257 @last_page ||= self[page_count] | |
258 end | |
259 alias last :last_page | |
260 | |
261 # Returns the number of pages in this paginator. | |
262 def page_count | |
263 @page_count ||= @item_count.zero? ? 1 : | |
264 (q,r=@item_count.divmod(@items_per_page); r==0? q : q+1) | |
265 end | |
266 | |
267 alias length :page_count | |
268 | |
269 # Returns true if this paginator contains the page of index +number+. | |
270 def has_page_number?(number) | |
271 number >= 1 and number <= page_count | |
272 end | |
273 | |
274 # Returns a new Page representing the page with the given index | |
275 # +number+. | |
276 def [](number) | |
277 @pages[number] ||= Page.new(self, number) | |
278 end | |
279 | |
280 # Successively yields all the paginator's pages to the given block. | |
281 def each(&block) | |
282 page_count.times do |n| | |
283 yield self[n+1] | |
284 end | |
285 end | |
286 | |
287 # A class representing a single page in a paginator. | |
288 class Page | |
289 include Comparable | |
290 | |
291 # Creates a new Page for the given +paginator+ with the index | |
292 # +number+. If +number+ is not in the range of valid page numbers or | |
293 # is not a number at all, it defaults to 1. | |
294 def initialize(paginator, number) | |
295 @paginator = paginator | |
296 @number = number.to_i | |
297 @number = 1 unless @paginator.has_page_number? @number | |
298 end | |
299 attr_reader :paginator, :number | |
300 alias to_i :number | |
301 | |
302 # Compares two Page objects and returns true when they represent the | |
303 # same page (i.e., their paginators are the same and they have the | |
304 # same page number). | |
305 def ==(page) | |
306 return false if page.nil? | |
307 @paginator == page.paginator and | |
308 @number == page.number | |
309 end | |
310 | |
311 # Compares two Page objects and returns -1 if the left-hand page comes | |
312 # before the right-hand page, 0 if the pages are equal, and 1 if the | |
313 # left-hand page comes after the right-hand page. Raises ArgumentError | |
314 # if the pages do not belong to the same Paginator object. | |
315 def <=>(page) | |
316 raise ArgumentError unless @paginator == page.paginator | |
317 @number <=> page.number | |
318 end | |
319 | |
320 # Returns the item offset for the first item in this page. | |
321 def offset | |
322 @paginator.items_per_page * (@number - 1) | |
323 end | |
324 | |
325 # Returns the number of the first item displayed. | |
326 def first_item | |
327 offset + 1 | |
328 end | |
329 | |
330 # Returns the number of the last item displayed. | |
331 def last_item | |
332 [@paginator.items_per_page * @number, @paginator.item_count].min | |
333 end | |
334 | |
335 # Returns true if this page is the first page in the paginator. | |
336 def first? | |
337 self == @paginator.first | |
338 end | |
339 | |
340 # Returns true if this page is the last page in the paginator. | |
341 def last? | |
342 self == @paginator.last | |
343 end | |
344 | |
345 # Returns a new Page object representing the page just before this | |
346 # page, or nil if this is the first page. | |
347 def previous | |
348 if first? then nil else @paginator[@number - 1] end | |
349 end | |
350 | |
351 # Returns a new Page object representing the page just after this | |
352 # page, or nil if this is the last page. | |
353 def next | |
354 if last? then nil else @paginator[@number + 1] end | |
355 end | |
356 | |
357 # Returns a new Window object for this page with the specified | |
358 # +padding+. | |
359 def window(padding=2) | |
360 Window.new(self, padding) | |
361 end | |
362 | |
363 # Returns the limit/offset array for this page. | |
364 def to_sql | |
365 [@paginator.items_per_page, offset] | |
366 end | |
367 | |
368 def to_param #:nodoc: | |
369 @number.to_s | |
370 end | |
371 end | |
372 | |
373 # A class for representing ranges around a given page. | |
374 class Window | |
375 # Creates a new Window object for the given +page+ with the specified | |
376 # +padding+. | |
377 def initialize(page, padding=2) | |
378 @paginator = page.paginator | |
379 @page = page | |
380 self.padding = padding | |
381 end | |
382 attr_reader :paginator, :page | |
383 | |
384 # Sets the window's padding (the number of pages on either side of the | |
385 # window page). | |
386 def padding=(padding) | |
387 @padding = padding < 0 ? 0 : padding | |
388 # Find the beginning and end pages of the window | |
389 @first = @paginator.has_page_number?(@page.number - @padding) ? | |
390 @paginator[@page.number - @padding] : @paginator.first | |
391 @last = @paginator.has_page_number?(@page.number + @padding) ? | |
392 @paginator[@page.number + @padding] : @paginator.last | |
393 end | |
394 attr_reader :padding, :first, :last | |
395 | |
396 # Returns an array of Page objects in the current window. | |
397 def pages | |
398 (@first.number..@last.number).to_a.collect! {|n| @paginator[n]} | |
399 end | |
400 alias to_a :pages | |
401 end | |
402 end | |
403 | |
404 end | |
405 end |