To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / .svn / pristine / 7d / 7d0db9ae9f70acc895b7957ff399876fc83ab2bc.svn-base @ 1297:0a574315af3e
History | View | Annotate | Download (15.3 KB)
| 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 |