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