Wolfmans Howlings

A programmers Blog about Ruby, Rails and a few other issues

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))
  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])

Posted in  | Tags ,  | 10 comments | no trackbacks