Paginating acts_as_taggable with will_paginate
Posted by Jim Morris on Mon Jul 30 15:04:50 -0700 2007
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)) end
This 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 end
I 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])
UPDATE Johns suggestion works as of today (2/12/2009) so none of the above is needed...
options = Car.find_options_for_tagged_with(params[:tag_name]).merge :page => params[:page] @cars = Car.paginate(options)
or my example from above...
opts= Faq.find_options_for_tagged_with('FAQ')
@faqs= Faq.paginator(opts.merge(:page => params[:page]))
Show