What have you found for these years?

2008-02-18

[RoR ] 不知道有沒有用的 paginator

 作者  godfat (godfat 真常)                                        看板  Ruby
標題 [RoR ] 不知道有沒有用的 paginator
時間 Mon Feb 18 15:40:58 2008
───────────────────────────────────────


我現在還是不太明白為什麼 rails 2 要拿掉 paginate?
只是聽說該 paginate 做得很差,所以要拿掉?
為什麼不是改善而是拿掉?分頁的系統難道有問題嗎?

但我仍然需要 paginate. 最常被提起的大概是 will_paginate 吧。
不過我討厭同樣的需求卻要一直換東西,又更討厭用被建議不要使用的東西。
後來我看到了 gem paginator: http://paginator.rubyforge.org/

我承認我不喜歡 plugin, 因為 rails 的東西不等於 ruby 的東西。
如果可以的話,我只想用 rubygems 發佈的東西,其他都不想用。
所以我先試了這個 gem 版的 paginator.

試了之後,我發覺其實他原理根本就很單純,單純到乾脆自己寫一個
最符合自己使用習慣的也花不了多少時間。所以後來我隨手寫了一個,
目前是放在 ludy 裡面,畢竟全程式碼也沒多少,為此開個專案感覺怪怪的?

Ludy::RailsPaginator:
http://ludy.rubyforge.org/classes/Ludy/RailsPaginator.html

用法:

pager = Ludy::RailsPaginator.new PokeActionLog,
:conditions => ['poker_id = ? OR pokee_id = ?',
self.id, self.id ],
:order => 'created_at DESC'

pager.per_page = 20

第一個參數是要被分頁 model 的 class, 這裡是 PokeActionLog,
第二個參數是一個 hash, 這個 hash 會分別傳給 counter 和 fetcher.
所謂 counter 和 fetcher 是來自其 parent, Ludy::Paginator.

Ludy::Paginator:
http://ludy.rubyforge.org/classes/Ludy/Paginator.html

基本上 RailsPaginator 只是這個東西的簡單 wrapper,
另外還有 ArrayPaginator, 這比較容易理解,以這個當例子。

Ludy::ArrayPaginator:
http://ludy.rubyforge.org/classes/Ludy/ArrayPaginator.html

所謂 counter 就是取得總數的 function, 而 fetcher 就是取得資料的 function.
在 array 中,counter 就是 Array#size, 而 fetcher 當然就是 Array#[].

call fetcher 時會有兩個參數,一個是 offset, 另一個是 per_page.
對 array 來說,這就是 array[offset, per_page].
從 offset 的地方取出 per_page 個資料。

所以 ArrayPaginator 的實作是這樣:

class ArrayPaginator < Paginator
attr_reader :data
def initialize data
@data = data
super(lambda{ |offset, per_page|
@data[offset, per_page]
}, lambda{
@data.size
})
end
end

非常單純。

RailsPaginator 用一樣的概念:

class RailsPaginator < Paginator
attr_reader :model_class
def initialize model_class, opts = {}
@model_class = model_class
super(lambda{ |offset, per_page|
@model_class.find :all, opts.merge(:offset => offset, :limit =>
per_page)
}, lambda{
@model_class.count opts
})
end
# it simply call super(page.to_i), so RailsPaginator also eat string.
def page page; super page.to_i; end
alias_method :[], :page
end

至於最下面那個 page method, 只是為了方便,使得:

pager.page 1 和 pager.page '1' 等價

這樣做的原因是 params 來的東西都是字串,to_i 既然經常會用到,乾脆就幫忙做掉。

另外,paginator 本身 include Enumerable, 是為 pages 的集合。
page 本身沒有 include Enumerable, 不過 method_missing 會將所有的
method delegate 給那頁資料的 array, 所以是資料的集合。

最後我寫在 controller 的東西是這樣:

log_page = @target_user.log_page(params[:page] || 1)
@current_page = log_page.page # 第?頁
@next_page = log_page.next.ergo.page # 下一頁的 page instance
@prev_page = log_page.prev.ergo.page # 上一頁的 page instance

# p.s. ergo 很好用... 可以少寫很多判斷

@last_log = log_page.end + 1 # 這一頁的最後一筆是第?筆
@first_log = log_page.begin + 1 # 這一頁的第 一筆是第?筆
@log_count = log_page.pager.count # 總共有幾筆?


# 每一個 log 要做額外處理
@logs = log_page.to_a.map{ |l| l.prepare_message(@target_user); l }

*

我之所以會把 counter 和 fetcher 拆成外部傳入的 function,
是希望 pager 本身能成為一個能重複利用的 instance.
只要設定好 counter 和 fetcher, 接著不管資料怎麼變動,
都用同一個 pager 就好了。就算真的改變了,一樣可以再改寫:

pager.counter = lambda{ another_thing.count :conditions => [...] }
pager.fetcher = lambda{ yet_another_thing.find :all }

當然這樣是不對稱啦。目前我是還沒寫類似這樣的:

rails_pager.opt = :conditions => [...], :order => 'created_at DESC'

或是

rails_pager.model = AnotherModel

不過這要加上去都是輕而易舉的。嗯,既然提到了,等會就來加好了...

*

只是不知道 rails paginate 拿掉的原因,所以做這東西也不知道是否真的有用?
反正當練練基本功也沒什麼不好就是了,我一直是這樣覺得。全部的程式也沒幾行。

test case 在這:

require 'ludy/paginator'
require 'ludy/symbol/to_proc' if RUBY_VERSION < '1.9.0'

class TestPaginator < Test::Unit::TestCase
def self.data; @data ||= (0..100).to_a; end
def for_pager pager
# assume data.size is 101, data is [0,1,2,3...]
pager.per_page = 10
assert_equal 11, pager.size

assert_nil pager[0]
assert_equal((0..9).to_a, pager.page(1).to_a)
assert_equal((10..19).to_a, pager[2].to_a)
assert_equal(20, pager.page(3).first)
assert_equal((90..99).to_a, pager[10].to_a)
assert_equal([100], pager.page(11).to_a)
assert_nil(pager.page(12))

assert_equal(pager[1], pager[2].prev)
assert_equal(pager.page(11), pager[10].next)
assert_nil(pager[1].prev)
assert_nil(pager[10].next.next)

assert_equal pager[4].data, pager[4].fetch
assert_equal(pager[1], pager.pages.first)
assert_equal(pager[2], pager.to_a[1])
assert_equal(5050, pager.inject(0){|r, i| r += i.inject(&:+) })

assert_equal 4, pager[4].page

assert_equal 10, pager[2].begin
assert_equal 19, pager[2].end
assert_equal 100, pager[11].end
end
def test_basic
pager = Ludy::Paginator.new(
lambda{ |offset, per_page|
# if for rails,
# Data.find :all, :offset => offset, :limit => per_page
TestPaginator.data[offset, per_page]
}, lambda{
# if for rails,
# Data.count
TestPaginator.data.size
})
for_pager pager
end
def test_offset_bug
a = (0..9).to_a
pager = ArrayPaginator.new a
pager.per_page = 5
assert_equal 5, pager[1].size
assert_equal 5, pager[2].size
assert_nil pager[3]
end
class Topic
class << self
def count opts = {}
101
end
def find all, opts = {}
TestPaginator.data[opts[:offset], opts[:limit]]
end
end
end
def test_for_rails
for_pager Ludy::RailsPaginator.new(Topic)
end
def test_for_array
for_pager Ludy::ArrayPaginator.new(TestPaginator.data)
end
end

--
By Gamers, For Gamers - from the past Interplay

--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 220.128.121.85

0 retries:

Post a Comment

All texts are licensed under CC Attribution 3.0