Paginating acts_as_taggable with will_paginate
Posted by Jim Morris Mon, 30 Jul 2007 22:04:50 GMT
A question I see asked a lot is how do I paginate acts_as_taggable (on steroids)?
I haven't seen any answers I liked, so I created my own, which I'm sure a few people won't like either ;) But it works for me (tm).
I use will_paginate, but this does not work with custom finds that plugins define themselves, as is the case with acts_as_taggable.
If you do use a custom find_by_sql you have to hit the database twice, once to find the total number of items and then the paginated find.
I have a situation where I generate a tag cloud with every page, and part of that tag cloud has already calculated the number of tags for each classification I use.
I combined this with the paginator gem to get myself pages without too many hits to the database.
The first thing I do is pass the total count I get from the tag cloud to the action that renders the index for all those items matching the cloud... If you look at the article cited above I make this modification, this is dumbed down a bit for the sake of simplicity...
tags= Post.tag_counts(:order => 'tags.name')
tags.each do |t|
link_to(h(t.name), tagged_post_path(:tag => t.name, :cnt => t.count))
endThis passes the count I have already calculated to the action that will list the paginated results.
In my controller I do this to get the paginated results using will_paginate and the Pagination gem...
def tagged
...
# page if we can
size= params[:cnt]
if size
per_page= 10
# use Paginator gem to do the actual paging
pager = ::Paginator.new(size, per_page) do |offset, per_page|
Post.find_tagged_with(params[:tag], :limit => per_page, :offset => offset)
end
# default to page 1 if not specified
page= params[:page] || 1
# gets a paged array of posts
@posts= returning WillPaginate::Collection.new(page, per_page, size) do |p|
p.replace pager.page(page).items
end
else
# fall back if we don't know the size
@posts= Post.find_tagged_with(params[:tag])
end
render :action => 'index'
end This fits in nicely with the tag cloud I need to calculate, and it uses will_paginate just like the regular index action does.
UPDATE
I refactored this to be more generally useful, I added the following as a
protected method in application.rb...
# paginate a call to find_tagged_with
# klass is the tagged class
# tag is the tag to find
# count is the total number of items with that tag, if nil count_tags is called
# per_page is numbe rof items per page
# page is the page we are on
# order is the order to return the items in
def tag_paginator(klass, tag, count=nil, per_page=10, page=1, order='updated_at DESC')
count ||= klass.count_tags(tag)
pager = ::Paginator.new(count, per_page) do |offset, per_page|
klass.find_tagged_with(tag, :order => order, :limit => per_page, :offset => offset)
end
page ||= 1
returning WillPaginate::Collection.new(page, per_page, count) do |p|
p.replace pager.page(page).items
end
endI call it from one of my other actions like this...
@faqs= tag_paginator(Post, 'FAQ', nil, per_page, params[:page], 'updated_at DESC')
Passing in nil as the third parameter causes the tag_paginator
method to call Post.count_tags which is not part of the
acts_as_taggable methods, I added it to the SingletonMethods module
myself...
module ActiveRecord
module Acts #:nodoc:
module Taggable #:nodoc:
module SingletonMethods
...
# Return the count of tag tags in this class
def count_tags(tag)
count_by_sql("select count(*) FROM tags, taggings WHERE " + sanitize_sql(['name = ? AND tags.id = taggings.tag_id AND taggable_type = ?', tag, name]))
end
....If you don't want to hack acts_as_taggable then simply leave that
part out and call the count_by_sql yourself.
My refactored tagged action from above now looks like this...
def tagged
...
tag= params[:tag]
per_page= 10
size= params[:cnt]
@posts= tag_paginator(Post, tag, size, 10, params[:page])