Sequenceable
Sequenceable is a plugin for Sequel models. It supports the use of models as ordered collections, allowing you to flip through records one-by-one or in pages, locate the first or last records, and identify the position of a record in the sequence. Sequenceable uses select count to quickly identify the position of a record, meaning it is resilient against shuffled and deleted records in a sequence.
Records can be returned in a "window": an in-order slice of the entire collection, much like the pages of a book. You can find windows from the model itself, or, if you have a specific record, return a window which contains it. This can be done in one of three ways:
- Absolute: Like pages of a book, the collection is divided into a set of non-overlapping windows. The window which contains the target record is returned.
- Float: Returns a window centered around the record. When the window nears the beginning or end of the sequence, it shifts over to include the desired number of records per window.
- Clip: Returns a window centered around the record, but near the beginning or end of the sequence, "falls off" the edge.
To use Sequenceable, simply mix it into your model:
include Sequenceable
# As users of an OC-48, we feel more photographs per page are in order.
self.window_size = 24
# Sort by author, then update time
order_by 'author', 'updated_on'
# Sort in descending order
self.sequence_direction = 'desc'
# Restrict photographs to those which are not deleted
self.sequence_filter = ['deleted = ?', false]
...
endThe Photograph class now support operations like first, last, and window_count, which returns the number of windows required to span the entire sequence. You can retrieve a collection of records with Photograph.window. In addition, each photograph gains methods like next, previous, and window--returning a window in context around the photo.
The end result of all this is that pagination becomes fast and easy:
def list
# The request may have specified a desired page.
@page = params[:page]
# Alternatively, the request might have specified a photograph ID.
# We display a window around the target photograph.
@photograph = Photograph.find_by_id params[:id]
if @page
# Show the requested page
@photograph = nil
# Find an array of photographs for this page.
@photographs = Photograph.window @page
elsif @photograph
# Find an array of photographs around the target.
@photographs = @photograph.window
else
# Show the most recent page
@photographs = Photograph.window :last
@page = :last
end
end
endYou can also use sequence_include to specify parameters for find :include, which lets you restrict sequences based on associated joins.
Generating links for a model is a more interesting proposition. Here's one way to do it.
# Generate pagination links from a Sequenceable class and index.
# The index can be :first or :last for the corresponding pages, an instance
# of the class (in which case the page which would contain that instance
# is highlighted, or a page number. Limit determines how many numeric links
# to include--use :all to include all pages.
def page_nav(klass, index = nil, limit = 15)
# Translate :first, :last into corresponding windows.
case index
when :first
page = 0
when :last
page = klass.window_count - 1
when klass
# Index is actually an instance of the target class
page = index.window_absolute_index
else
# Index is a page number
page = index.to_i
end
pages = Array.new
links = '<ol class="pagination actions">'
window_count = klass.window_count
# Determine which pages to create links to
if limit.kind_of? Integer and window_count > limit
# There are more pages than we can display.
# Default first and last pages are the size of the collection
first_page = 1
last_page = window_count - 2
# The desired number of previous or next pages
previous_pages = (Float(limit - 3) / 2).floor
next_pages = (Float(limit - 3) / 2).floor
if (offset = first_page - (page - previous_pages)) > 0
# Window extends before the start of the pages
last_page = first_page + (limit - 2)
elsif (offset = (page + next_pages) - last_page) > 0
# Window extends beyond the end of the pages
first_page = last_page - (limit - 2)
else
# Window is somewhere in the middle
first_page = page - previous_pages
last_page = page + next_pages
end
# Generate list of pages
pages = [0] + (first_page..last_page).to_a + [window_count - 1]
else
# The window encompasses the entire set of pages
pages = (0 ... window_count).to_a
end
if page > 0
# Add "previous page" link.
links << "<li><a class=\"previous\" href=\"#{klass.url}/page/#{page - 1}\">« Previous</a></li>"
else
links << "<li class=\"placeholder\"><span class=\"previous\"></span></li>"
end
# Convert pages to links
unless pages.empty?
pages.inject(pages.first - 1) do |previous, i|
if (i - previous) > 1
# These pages are not side-by-side.
links << '<li class="elided"><span>…</span></li>'
end
if i == page
# This is a link to the current page.
links << "<li class=\"current\"><span>#{i + 1}</span></li>"
else
# This is a link to a different page.
links << "<li><a href=\"#{klass.url}/page/#{i}\">#{i + 1}</a></li>"
end
# Remember this as the previous page.
i
end
end
if page < klass.window_count - 1
# Add "next page" link.
links << "<li><a class=\"next\" href=\"#{klass.url}/page/#{page + 1}\">Next »</a></li>"
else
links << "<li class=\"placeholder\"><span class=\"next\"></span></li>"
end
links << '</ol>'
endPlease enjoy! :-)