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])
I found this working with
acts_as_taggableandwill_paginate.@events = Event.paginate_by_id(Event.find_tagged_with(params[:id]), :page => params[:page])Maybe there is a more graceful way.
Hey man.... seriously .. u dont have to put so much work just to get it to work. Just use
acts_as_taggable_on_sterioids...And use 2 simple lines
This works unless you have
:match_all=> true in there. Once you do that, a group by is added that causeswill_paginateto get a MySQL syntax error. Still working on a way to fix it...Dave, I found this article while having the same problem.
The SQL error when using :match_all => true has to do with the will_paginate trying to count via the select created by acts_as_taggable. My solution was to add the following line to the beginning of the wp_count! function in finder.rb of will_paginate:
options[:select].sub!(/.*/, '\1.id') if options[:select] #count by id's, not wildcard (*)This will work unless the table you are counting does not have an id column, but using active record that should be extremely rare. If that's the case, you could always have it find the name of the class's key field.
the match_all option doesn't work on postgresql anyway, I found a different workaround that uses a subselect to find the ids, then looks up using IN (ids,..) if anyone needs it let me know and I'll post it.
I tried John Wong's comment but seems not working... (Dave u are referring to this right?)
I really think if such way would work its very clean.
i had an sql error using John's comment. Aaron's solution seems to work though.
Eric, a whole rails version later and using steroids, and still your 'eloquent' code snippet works great for me.
since i'm trying to keep my plugins as natural as possible, i thank you. oh and you too dave.
errr, dave = jim, wolfman, whathaveyou.
yes Eric's solution works for me too... one line thats it. I M LOVING RoR!!!