annotate .svn/pristine/7d/7d0db9ae9f70acc895b7957ff399876fc83ab2bc.svn-base @ 1082:997f6d7738f7 bug_531

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