# encoding: utf-8
#
# = Pagination
#
# Simple class to hold the minimal info needed to paginate a set of objects by
# page number and/or letter. This class knows nothing about how to query the
# results (see Query#paginate), nor does it know anything about how to render
# the results or pagination controls (see ApplicationHelper::Paginator).
#
# You give it a page number and number of results; it in return can tell you
# which results you should display.
#
# It also stores a few other parameters which though it never uses, the query
# and/or render mechanisms need access to, such as the URL parameter used to
# select page and letter, and the list of letters for which there are results.
#
# == Query and ApplicationHelper::Paginator
#
# Together these three classes and modules make it possible to gather a set of
# results to an arbitrary query, and render them on an HTML view together with
# all the necessary controls. The basic usage is:
#
# # In your Rails controller:
# def index
# @pages = MOPaginator.new(
# :number_arg => :page,
# :number => params[:page],
# :num_per_page => 100,
# )
# query = Query.lookup(:Model, :flavor)
# @results = query.paginate(@pages)
# end
#
# # This paginates by letter and number:
# def index_by_letter
# @pages = MOPaginator.new(
# :letter_arg => :letter,
# :number_arg => :page,
# :letter => params[:letter],
# :number => params[:page],
# :num_per_page => 100,
# )
# query = Query.lookup(:Model, :flavor)
# @results = query.paginate(@pages)
# end
#
# # This is how you might use it without Query:
# def custom_index
# @results = Model.find(...)
# @pages.num_total = @results.length
# @subset = @results[@pages.from..@pages.to]
# end
#
# # Use the same code in your view template for either case:
# <% paginate_block(@pages) do %>
# <% for object in @results
# <%= link_to(object.name, :action => 'show_object', :id => object.id) %>
# <% end %>
# <% end %>
#
# == Attributes
#
# letter_arg:: URL parameter used to select letter.
# number_arg:: URL parameter used to select page number. (= +page_arg)
# letter:: Letter selected (or +nil+ if none).
# number:: Page number selected (or +nil+ if none). (= +page+)
# num_per_page:: Number of results to show per page.
# num_total:: Number of results available. (= +length)
# used_letters:: Array of letters that have results.
#
# == Class Methods
#
# new:: Instantiate, setting one or more attributes at the same time.
#
# == Instance Methods
#
# show_index:: Set page number so that it's showing the given result.
# num_pages:: Calculate number of pages of results available.
# from:: Index of the first result in selected page.
# to:: Index of the last result in selected page.
# from_to:: Same as pages.from..pages.to.
#
################################################################################
class MOPaginator
attr_accessor :letter_arg # Name of parameter to use for letter (if any).
attr_accessor :number_arg # Name of parameter to use for page number.
attr_reader :letter # Current letter (if any).
attr_reader :number # Current page number.
attr_reader :num_per_page # Number of results per page (default is 100).
attr_reader :num_total # Total number of results.
attr_reader :used_letters # List of letters that have results.
alias page_arg number_arg
alias page number
alias length num_total
def blank?; num_total == 0; end
def empty?; num_total == 0; end
# Create and initialize new instance.
def initialize(args={})
args.each do |key, val|
send("#{key}=", val)
end
@number ||= 1
@num_per_page ||= 100
@num_total ||= 0
end
# Validate the page number selection.
def number=(num)
if num
@number = num.to_i
@number = 1 if @number < 1
else
@number = 1
end
return @number
end
alias page= number=
# Validate the letter selection.
def letter=(char)
if char
@letter = char.to_s[0,1].upcase
@letter = nil if !@letter.match(/[A-Z]/)
else
@letter = nil
end
return @letter
end
# Validate the number of results.
def num_total=(num)
if num
@num_total = num.to_i
@num_total = 0 if @num_total < 1
else
@num_total = 0
end
return @num_total
end
alias length= num_total=
# Validate the number per page.
def num_per_page=(num)
@num_per_page = num.to_i
raise "Invalid num_per_page: #{num.inspect}" if @num_per_page < 1
return @num_per_page
end
# Validate +used_letters+ array. Force them all to uppercase, and remove
# duplicates and non-letters. (Set to +nil+ to mean assume all letters have
# results.)
def used_letters=(list)
if list
@used_letters = list.map {|l| l.to_s[0,1].upcase}.uniq.
select {|l| l.match(/[A-Z]/)}.sort
else
@used_letters = nil
end
end
# Number of pages of results available.
def num_pages
(num_total.to_f / num_per_page).ceil
end
# First index on current page.
def from
n = ((number || 0) - 1) * num_per_page
n = 0 if n < 0
return n
end
# Last index on current page.
def to
n = from + num_per_page - 1
n = num_total - 1 if n > num_total - 1
n = 0 if n < 0
return n
end
# Same as pages.from..pages.to.
def from_to
from..to
end
# Set page number so that it shows the given result (given by index, with
# zero being the first result).
def show_index(index)
self.number = (index.to_f / num_per_page).floor + 1
end
end