annotate vendor/plugins/classic_pagination/lib/pagination.rb @ 8:0c83d98252d9 yuya

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