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