Getting the acts_as_taggable_on_steroids and will_paginate plugins playing nicely together has been written about plenty of times before, but I still ended up doing my own thing when I tackled this on a project of mine. This topic came up in conversation again recently so I guess it's time I wrote up my solution.
Using the typical blog application as an example you might have a Post model which acts_as_taggable. Together, the plugins give you a Post.paginate_tagged_with method which looks as though it will do what you want, but unfortunately doesn't work as expected. You may well get the correct results, but the SQL that is generated to count the total number of results is incorrect so you'll see some strange behavior in you paging controls.
The solution I settled on, was to create a custom finder (paginater?) on the Post model. It looks like this:
class Post < ActiveRecord::Base
acts_as_taggable
def self.paged_find_tagged_with(tags, args = {})
if tags.blank?
paginate args
else
options = find_options_for_find_tagged_with(tags, :match_all => true)
options.merge!(args)
# The default count query generated by paginate includes COUNT(DISTINCT Posts.*) which errors, at least on mysql
# Below we override the default select statement used to perform the count so that it becomes COUNT(DISTINCT Posts.id)
paginate(options.merge(:count => { :select => options[:select].gsub('*', 'id') }))
end
end
end
You use it as you would the regular paginate_tagged_with:
@posts = Post.paged_find_tagged_with('rails', :page => 1)
The way this is works is that we first call find_options_for_find_tagged_with to get a hash of options required to perform a find by tag. If we hand this straight to paginate we'll get an error trying to SELECT COUNT(DISTINCT Posts.*). Thankfully however, will_paginate lets you override the options it uses to generate its count SQL. If we take options[:select] and replace the '*' with 'id' and use that as the :select for our :count, the SQL generated is SELECT COUNT(DISTINCT Posts.id) and everything works as expected.
That's it, really. It's worth pointing out that when no tags are passed in I'm calling paginate (without any additional options) to return all posts. This makes sense in my app and moves some logic out of the controller but you might want to do something different.
