Permalink
Browse files

Awesome new features, supersmart tests, great coverage!

WARNING: will_paginate API changed. It still accepts 2
parameters but they are different: the first one is a
collection while the second is an optional params hash.

What's new:

  * paginate is better, smarter, more configurable
  * will_paginate helper is rewritten, supercharged
  * PaginatedCollection class eliminates passing tons
    of additional variables around
  * you can run tests even when not in a Rails app

See the README for the full scoop.



git-svn-id: svn://errtheblog.com/svn/plugins/will_paginate@203 1eaa51fe-a21a-0410-9c2e-ae7a00a434c4
  • Loading branch information...
mislav
mislav committed May 9, 2007
1 parent 3ea5817 commit 079d0d841ffcc824bccf122687e7e29132e47170
View
109 README
@@ -1,30 +1,107 @@
WillPaginate
===========
-Ruby port by: PJ Hyett
-Contributors: K. Adam Christensen, Chris Wanstrath, Dr. Nic Williams
-Original : http://www.strangerstudios.com/sandbox/pagination/diggstyle.php
+Quick quiz: Where does pagination logic belong?
-Example usage:
+ a) in the model;
+ b) in the controller;
+ c) in views;
+ d) all of the above.
-app/models/post.rb
+We think you know the answer (if you think hard enough).
- class Post < ActiveRecord::Base
- cattr_reader :per_page
- @@per_page = 50
- end
+This plugin makes magic happen. You *will* paginate!
-app/controller/posts_controller.rb
- def index
- @board = Board.find(params[:id])
- @posts, @page = Post.paginate_all_by_board_id(@board.id, :page => params[:page])
- end
+## Example usage:
-app/views/posts/index.rhtml
+Use a paginate finder in the controller:
- <%= will_paginate(@board.topic_count, Post.per_page) %>
+ @posts = Post.paginate_by_board_id @board.id, :page => params[:page]
+Yeah, `paginate` works just like `find` -- it just doesn't fetch all the records.
+Just don't forget to tell it which page you want!
+
+Render the posts in your view like you would normally do. When you need to render
+pagination, just stick this in:
+
+ <%= will_paginate @posts %>
+
+You're done. (Copy and paste the example fancy CSS styles from the bottom.)
+
+How does it know how much items to fetch per page? It asks your model by calling
+`Post.per_page`. You can define it like this:
+
+ class Post < ActiveRecord::Base
+ cattr_reader :per_page
+ @@per_page = 50
+ end
+
+... or like this:
+
+ class Post < ActiveRecord::Base
+ def per_page
+ 50
+ end
+ end
+
+... or don't worry about it at all. (WillPaginate defines it to be 30 if missing.)
+You can also specify the count explicitly when calling `paginate`:
+
+ @posts = Post.paginate :page => params[:page], :per_page => 50
+
+Find more options in sections below.
+
+
+## Details
+
+The `paginate` finder wraps the original finder and returns a PaginatedCollection
+instance that's in fact a proxy to the original collection. You can use the collection
+as you would any ActiveRecord resultset, but WillPaginate view helpers also need the
+object to know how to make pagination:
+
+ <ol>
+ <% for post in @posts -%>
+ <li>Render `post` in some nice way.</li>
+ <% end -%>
+ </ol>
+
+ <p>Now let's render us some pagination!</p>
+ <%= will_paginate @posts %>
+
+
+In model finders, "all" is implicit. No sense in paginating a single record, right?
+
+ * Post.paginate => Post.find :all
+ * Post.paginate_all_by_something => Post.find_all_by_something
+ * Post.paginate_by_something => Post.find_all_by_something
+
+Options for `paginate` finders are:
+
+ * page (default 1)
+ * per_page (default is read from the model, which is 30 if not overriden)
+ * total entries: ActiveRecord knows how to count, but you can still override it
+ * count: takes place of "select" for count() statement
+ * distinct: also just for count() statement
+
+Options for `will_paginate` view helper:
+
+ * class: CSS class name for the generated DIV (default "pagination")
+ * prev_label: default '&laquo; Previous',
+ * next_label: default 'Next &raquo;',
+ * inner_window: how many links are shown around the current page, defaults to 4
+ * outer_window: how many links are around the first and the last page, defaults to 1
+
+
+## Authors, credits
+
+Ruby port by: PJ Hyett, Mislav Marohnić (Sulien)
+Contributors: K. Adam Christensen, Chris Wanstrath, Dr. Nic Williams
+Original announcement: http://errtheblog.com/post/929
+Original PHP source: http://www.strangerstudios.com/sandbox/pagination/diggstyle.php
+
+
+## Want Digg style?
Copy the following css into your stylesheet for a good start:
View
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the dummy plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the dummy plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'WillPaginate'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
View
@@ -1,4 +1,7 @@
require 'will_paginate'
require 'finder'
-ActionView::Base.send(:include, WillPaginate)
-ActiveRecord::Base.send(:include, WillPaginate::Finder)
+
+ActiveRecord::Base.send :include, WillPaginate::Finder
+# Controllers will get some love soon
+# ActionController::Base.send :include, WillPaginate::ControllerHelpers
+ActionView::Base.send :include, WillPaginate::ViewHelpers
View
@@ -1,28 +1,60 @@
module WillPaginate
+ # A mixin for ActiveRecord::Base. Provides `per_page` class method
+ # and makes `paginate` finders possible with some method_missing magic.
+ #
module Finder
def self.included(base)
base.extend ClassMethods
class << base
+ alias_method_chain :method_missing, :paginate
define_method(:per_page) { 30 } unless respond_to? :per_page
end
end
module ClassMethods
- def method_missing_with_will_paginate(method_id, *args, &block)
- unless match = /^paginate/.match(method_id.to_s)
- return method_missing_without_will_paginate(method_id, *args, &block)
+ def method_missing_with_paginate(method, *args, &block)
+ # did somebody tried to paginate? if not, let them be
+ unless method.to_s.index('paginate') == 0
+ return method_missing_without_paginate(method, *args, &block)
end
+ options = args.last.is_a?(Hash) ? args.pop.symbolize_keys : {}
+
+ page = (options.delete(:page) || 1).to_i
+ entries_per_page = options.delete(:per_page) || per_page
- options = args.last.is_a?(Hash) ? args.pop : {}
- page = (page = options.delete(:page).to_i).zero? ? 1 : page
- limit_per_page = options.delete(:per_page) || per_page
- args << options
+ total_entries = unless options[:total_entries]
+ count_options = options.slice :conditions, :joins, :include, :order, :group, :select, :distinct
+ count_options[:select] = options[:count] if options.key? :count
+ count(count_options)
+ else
+ options.delete(:total_entries)
+ end
- with_scope :find => { :offset => (page - 1) * limit_per_page, :limit => limit_per_page } do
- [send(method_id.to_s.sub(/^paginate/, 'find'), *args), page]
+ # oh, you're not so fun anymore!
+ [:distinct, :count].each {|key| options.delete key } unless options.empty?
+
+ finder = method.to_s.sub /^paginate/, 'find'
+ # :all is implicit
+ if finder == 'find'
+ args.unshift(:all) if args.length < 2
+ elsif finder !~ /^find_all/
+ finder.sub! /^find/, 'find_all'
end
+
+ # do it!!
+ args << options.merge(:offset => (page - 1) * entries_per_page, :limit => entries_per_page)
+ entries = send finder, *args
+ # wrap and return the package
+ PaginatedCollection.new entries, page, entries_per_page, total_entries
end
- alias_method_chain :method_missing, :will_paginate
end
end
+
+ # TODO: Controllers need love, too!
+ #
+ # module ControllerHelpers
+ # def totally_awesome_helper
+ # patience
+ # end
+ # end
end
View
@@ -1,43 +1,97 @@
module WillPaginate
- def will_paginate(total_count, per_page, page = @page)
- adjacents = 2
- prev_page = page - 1
- next_page = page + 1
- last_page = (total_count / per_page.to_f).ceil
- lpm1 = last_page - 1
-
- returning '' do |pgn|
- if last_page > 1
- pgn << %{<div class="pagination">}
-
- # not enough pages to bother breaking
- if last_page < 7 + (adjacents * 2)
- 1.upto(last_page) { |ctr| pgn << (ctr == page ? content_tag(:span, ctr, :class => 'current') : link_to(ctr, params.merge(:page => ctr))) }
-
- # enough pages to hide some
- elsif last_page > 5 + (adjacents * 2)
+ # PaginatedCollection is a proxy to the original array of
+ # records. It is generated by AR::Base#paginate methods.
+ #
+ class PaginatedCollection
+ attr_reader :current_page, :per_page, :entries
+ attr_accessor :total_entries
+
+ def initialize entries, page, per_page, total
+ @entries = entries
+ @current_page = page
+ @per_page = per_page
+ @total_entries = total
+ @total_pages = (@total_entries / @per_page.to_f).ceil
+ end
+
+ def page_count
+ @total_pages
+ end
+
+ include Enumerable
+
+ def each &block
+ @entries.each &block
+ end
+
+ def to_a
+ @entries
+ end
- # close to beginning, only hide later pages
- if page < 1 + (adjacents * 2)
- 1.upto(3 + (adjacents * 2)) { |ctr| pgn << (ctr == page ? content_tag(:span, ctr, :class => 'current') : link_to(ctr, params.merge(:page => ctr))) }
- pgn << "..." + link_to(lpm1, params.merge(:page => lpm1)) + link_to(last_page, params.merge(:page => last_page))
-
- # in middle, hide some from both sides
- elsif last_page - (adjacents * 2) > page && page > (adjacents * 2)
- pgn << link_to('1', params.merge(:page => 1)) + link_to('2', params.merge(:page => 2)) + "..."
- (page - adjacents).upto(page + adjacents) { |ctr| pgn << (ctr == page ? content_tag(:span, ctr, :class => 'current') : link_to(ctr, params.merge(:page => ctr))) }
- pgn << "..." + link_to(lpm1, params.merge(:page => lpm1)) + link_to(last_page, params.merge(:page => last_page))
-
- # close to end, only hide early pages
- else
- pgn << link_to('1', params.merge(:page => 1)) + link_to('2', params.merge(:page => 2)) + "..."
- (last_page - (2 + (adjacents * 2))).upto(last_page) { |ctr| pgn << (ctr == page ? content_tag(:span, ctr, :class => 'current') : link_to(ctr, params.merge(:page => ctr))) }
+ protected
+
+ # delegate missing stuff to the collection array
+ def method_missing(method, *args, &block)
+ @entries.send method, *args, &block
+ end
+ end
+
+ module ViewHelpers
+ # Renders Digg-style pagination. (We know you wanna!)
+ # Returns nil if there is only one page in total (can't paginate that).
+ #
+ def will_paginate entries = @entries, options = {}
+ total_pages = entries.page_count
+
+ if total_pages > 1
+ page = entries.current_page
+ options = options.symbolize_keys.reverse_merge :class => 'pagination',
+ :prev_label => '&laquo; Previous',
+ :next_label => 'Next &raquo;',
+ :inner_window => 4, # links around the current page
+ :outer_window => 1 # links around beginning and end
+
+ inner_window, outer_window = options.delete(:inner_window), options.delete(:outer_window)
+ min = page - inner_window
+ max = page + inner_window
+
+ # adjust lower or upper limit if other is out of bounds
+ if max > total_pages then min -= max - total_pages
+ elsif min < 1 then max += 1 - min
+ end
+
+ current = min..max
+ beginning = 1..(1 + outer_window)
+ tail = (total_pages - outer_window)..total_pages
+ visible = [current, beginning, tail].map(&:to_a).sum
+
+ # build the list of the links
+ links = (1..total_pages).inject([]) do |list, n|
+ if visible.include? n
+ list << link_or_span(n, n == page, 'current')
+ elsif n == beginning.last + 1 || n == tail.first - 1
+ list << '...'
end
+ list
end
- pgn << (page > 1 ? link_to("&laquo; Previous", params.merge(:page => prev_page)) : content_tag(:span, "&laquo; Previous", :class => 'disabled'))
- pgn << (page < last_page ? link_to("Next &raquo;", params.merge(:page => next_page)) : content_tag(:span, "Next &raquo;", :class => 'disabled'))
- pgn << '</div>'
+
+ # next and previous buttons
+ prev, succ = page - 1, page + 1
+ links.unshift link_or_span(prev, prev.zero?, 'disabled', options.delete(:prev_label))
+ links.push link_or_span(succ, succ > total_pages, 'disabled', options.delete(:next_label))
+
+ content_tag :div, links.join(' '), options
+ else
+ nil
end
end
+
+ protected
+
+ def link_or_span(page, condition, span_class = nil, text = page.to_s)
+ # page links preserve other (GET) parameters
+ condition ? content_tag(:span, text, :class => span_class) :
+ link_to(text, {:page => page}.reverse_merge(params))
+ end
end
end
Oops, something went wrong.

0 comments on commit 079d0d8

Please sign in to comment.