development full of
merriment and sense

Pagination with acts_as_taggable_on_steroids, acts_as_ferret, and will_paginate

Geoffrey on August 20, 2007 at 2:40 pm

UPDATE (6/6/08): This is out of date for the latest Rails and will_paginate.

So I needed to paginate large collection of data in a new app I am working on. will_paginate is a good drop in replacement for the the default rails paginator. But I am also using acts_as_taggable_on_steroids (for tagging) and acts_as_ferret (for searching), so i needed special pagination for those scenarios.

acts_as_ferret

A quick Google search led me to this for paginating acts_as_ferret search results. I modified the offset calculation and ended up with this:

module ActsAsFerret
  module ClassMethods
    def paginate_search(query, options = {})
      options, page, per_page = wp_parse_options!(options)
      offset = (page.to_i - 1) * per_page
      options.merge!(:offset => offset, :limit => per_page)
      result = result = find_by_contents(query, options)
      returning WillPaginate::Collection.new(page, per_page, result.total_hits) do |pager|
        pager.replace result
      end
    end
  end
end

Drop that in a file lib/ferret_pagination.rb, require it in you environment.rb, and you can now do this in your controller:

@entries = Entry.paginate_search params[:query],
                                       :page => params[:page],
                                       :per_page => 20

acts_as_taggable (on steroids)

So with that out of the way, I was now ready to tackle paginating entries tagged with a certain tag. Another quick google search turned up some ideas in the will_paginate comments. I used this one as a starting point and this is what I ended up with:

module ActiveRecord
  module Acts #:nodoc:
    module Taggable #:nodoc:
      module SingletonMethods
        # Return the number of time this class has been tagged with this tag
        def tagging_counts(tag)
          count_by_sql("select count(*) FROM tags, taggings WHERE " + sanitize_sql(['tags.name = ? AND tags.id = taggings.tag_id AND taggings.taggable_type = ?', tag, name]))
        end

        # paginate a call to find_tagged_with
        # tag is the tag to find
        # options is the option to use for pagination (:page, :per_page) and for find_tagged_with
        def paginate_by_tag(tag, options = {})
          options, page, per_page = wp_parse_options!(options)
          offset = (page.to_i - 1) * per_page
          options.merge!(:offset => offset, :limit => per_page.to_i)
          items = find_tagged_with(tag, options)
          count = tagging_counts(tag)
          returning WillPaginate::Collection.new(page, per_page, count) do |p|
            p.replace items
          end
        end
      end
    end
  end
end

Again, drop that in a file lib/taggable_pagination.rb, require it in you environment.rb, and you can now do this in your controller:

@entries = Entry.paginate_by_tag @tag.name,
                                     :order => 'entries.created_at DESC',
                                     :page => params[:page],
                                     :per_page => 20

Thanks

Thanks to Brandon for posting the ferret pagination code, Jim for the acts as taggable pagination code, and PJ for the will_paginate code.

UPDATED: Corrected problem noted in comments

Filed under: Rails, ferret, pagination, tagging

25 Comments

  1. Thanks for the solution to getting will_paginate and a_a_t_o_s to work together. You saved me a headache and a ton of time.

    Comment by John — September 3, 2007 @ 9:07 pm

  2. Thanks for this… But I cannot get will_paginate and a_a_t_o_s working… It paginates the first page right (and adds the links at the bottom) BUT when I hit ‘next’ or 2… I get this error:

    undefined method `-’ for “2″:String

    Which is coming from this line in taggable_pagination.rb:

    offset = (page - 1) * per_page

    I know what the error is saying but no clue how to fix it…

    ALSO I want to pass in a condition, at the moment if I pass in a condition I get some kind of hash error…

    I’m sure the way to do it is by changing the tagging_counts(tag) method (to something like tagging_counts(tag, second_variable) )…

    Any ideas?

    Comment by Coopr — September 13, 2007 @ 6:46 am

  3. The correction should be:

    offset = (page.to_i - 1) * per_page

    because the page value is coming from the parameters passed in to the controller, which are always Strings.

    Comment by Geoffrey — September 13, 2007 @ 9:36 am

  4. Hmm I’m still getting the same error…

    I have changed it to look like: offset = (page.to_i - 1) * per_page

    But I still receive the error: undefined method `-’ for “2″:String

    This is what my controller looks like:

    @businesses = Business.paginate_by_tag params[:tag],
    :order => ‘businesses.name DESC’,
    :page => params[:page],
    :per_page => 1

    Any other thoughts?

    Comment by Coopr — September 15, 2007 @ 11:12 pm

  5. I tell lies…. Hahaha - it’s all working - thanks for the help.

    Still wondering how to add conditions into a search, so I can do something like:

    @businesses = Business.paginate_by_tag params[:tag],
    :conditions => ['suburb LIKE ?', "%#{params[:location]}%”],
    :order => ‘businesses.name DESC’,
    :page => params[:page],
    :per_page => 10

    I may just have to do a paginate_by_sql - might be easier? Thanks in advance.

    Comment by Coopr — September 16, 2007 @ 4:06 am

  6. Thanks a lot !
    you are great!

    Comment by kritias — September 21, 2007 @ 6:53 pm

  7. If you want to add conditions, you have to change the method a little bit

    module ActsAsFerret
    module ClassMethods
    def paginate_search(query, options = {}, find_options = {})
    options, page, per_page = wp_parse_options!(options)
    offset = (page.to_i - 1) * per_page
    options.merge!(:offset => offset, :limit => per_page)
    result = result = find_by_contents(query, options, find_options)
    returning WillPaginate::Collection.new(page, per_page, result.total_hits) do |pager|
    pager.replace result
    end
    end
    end
    end

    then, in your controller

    @entries = Entry.paginate_search params[:query],
    {:page => params[:page],
    :per_page => 20},
    {:conditions => some_condition}

    Comment by Jd — September 24, 2007 @ 3:54 pm

  8. Thanks for this, but I can’t make it work. It say:undefined method `wp_parse_options!’ for Article:Class. It seems the parameters can’t pass to the module in ‘ActsAsFerret’.

    the code in the controller is:
    @articles_result = Article.paginate_search( params{session[:keyword]},:page => params[:page], :per_page => 2).

    I don’t know how to fix it. Thank you very much in advance.

    Comment by Qi — October 4, 2007 @ 4:42 am

  9. Forget what I send, the will_paginate plugin broken. Now it is working. Thanks for the solution

    Comment by Qi — October 4, 2007 @ 6:06 am

  10. thanks a lot!!
    this article is a great help!
    but I have a question

    in paginate_by_tag, why you have to implement tagging_count?
    you cant just do “count = find_tagged_with(tag).size” ?

    Comment by kritias — November 1, 2007 @ 10:43 am

  11. hello all
    i need some help.
    i was earlier using paginating_find and moved on to will_paginate now

    i my view i am showing the total count of the ferret search results.

    Search for “book” Produced Results

    i have @products.size ————which failed
    i tried @products.total_hits ——failed again
    i made the result variable in AAF module as instance var and tried
    @results.total_hits———-also failed

    can anybody help me with this pls.
    Jags

    Everything from above worked for me
    thanks a ton ……..thanks

    Comment by jags — November 13, 2007 @ 9:32 am

  12. hi all

    how can i get the total_hits i.e the total no of results found for the search so that it can be displayed on the rhtml

    pls check on my comment above for details

    thanks

    Comment by jags — November 16, 2007 @ 12:35 am

  13. I include the code file by writing the following in environment.rb
    require File.dirname(__FILE__) + ‘/rails_root/vendor/plugins/acts_as_ferret/lib/ferret_pagination.rb’

    but I got errors, saying can’t find the method.
    Can anyone help me?

    Comment by Infinit — December 1, 2007 @ 11:21 am

  14. If you saved the ferret_pagination.rb in the lib/ directory you should be able to do this in the config/environment.rb:

    Rails::Initializer.run do |config|

    end

    # Include your application configuration below
    require ‘ferret_pagination’

    I believe it has to be after the Rail::Initializer so that everything else is loaded and in place. Hope this helps

    Comment by Geoffrey — December 1, 2007 @ 10:15 pm

  15. Another version, similar to Jd’s. For if you want conditions without act_as_ferret support (sounds complicated, it is!).

    def paginate_by_tag(tag, options = {}, find_options = {})
    page, per_page = wp_parse_options!(options)
    offset = (page.to_i - 1) * per_page
    find_options.merge!(:offset => offset, :limit => per_page.to_i)
    items = find_tagged_with(tag, find_options)
    count = tagging_counts(tag)
    returning WillPaginate::Collection.new(page, per_page, count) do |p|
    p.replace items
    end

    Thanks for the tips!

    Comment by Luca — December 30, 2007 @ 1:27 pm

  16. The most recent version of will _paginate has changed sufficiently to make the above examples break. Specifically the wp_parse_options! method no longer returns [options, page, per_page, total]. Intead it simply returns [page, per_page, total].

    Comment by Francois — January 20, 2008 @ 8:44 pm

  17. I adapted it for the 1 Feb 08 version of will_paginate, and adjusted the count to also consider conditions. I’m new at this so keen for any feedback - but the file I have in the lib folder is:

    module ActiveRecord
    module Acts #:nodoc:
    module Taggable #:nodoc:
    module SingletonMethods
    # paginate a call to find_tagged_with
    # tag is the tag to find
    # options is the option to use for pagination (:page, :per_page) and for find_tagged_with
    def paginate_by_tag(tag, options = {})
    page, per_page, total = wp_parse_options!(options)
    if(options[:conditions].blank?)
    counts = tag_counts(:conditions => sanitize_sql(”tags.name = ‘” + tag + “‘”))
    else
    counts = tag_counts(:conditions => sanitize_sql(options[:conditions] + ” and tags.name = ‘” + tag + “‘”))
    end

    count = 0
    count = counts[0].count if counts.size == 1

    offset = (page.to_i - 1) * per_page
    options.merge!(:offset => offset, :limit => per_page.to_i)
    items = find_tagged_with(tag, options)
    returning WillPaginate::Collection.new(page, per_page, count) do |p|
    p.replace items
    end
    end
    end
    end
    end
    end

    Comment by Tim Haines — February 6, 2008 @ 7:47 pm

  18. After having deleted ‘option’ in: “options, page, per_page = wp_parse_options!(options)” — comment of Francois — January 20, 2008 — rails finished croaking.

    Now I wanted the field ‘lemma’ of my database (an old german dictionary) alphabetically sorted. Following http://www.railsenvy.com/2007/2/19/acts-as-ferret-tutorial
    to rebuild the index, I finally wrote in (or copied into) the Controller:

    sorter = Ferret::Search::SortField.new(:lemma_for_sort, :reverse => false)
    @members = Member.paginate_search params[:query],
    :page => params[:page] ,
    :per_page => 25,
    :sort => sorter

    “lemma_for_sort” (in the tutorial “title_for_sort”): a method defined in the model - see tutorial.

    Oh wonder, it works! But only thanks to the great help of Geoffrey and his commentators.

    Comment by karl — February 10, 2008 @ 2:33 pm

  19. I have included the following in my controller.

    @entries = Entry.paginate_by_tag @tag.name,
    :order => ‘entries.created_at DESC’,
    :page => params[:page],
    :per_page => 20

    Now, how do I get to the next page? What’s the code in the view? Can anyone help me? Thanks!

    Comment by marc — February 19, 2008 @ 10:35 am

  20. Marc, I tried:

    Result: something like
    « Previous 1 2 3 4 5 6 7 8 9 10 11 Next »

    Comment by Karl — February 21, 2008 @ 7:27 am

  21. excuse me, typographic error, I try it again:

    <%= will_paginate @members %>

    Result: something like
    « Previous 1 2 3 4 5 6 7 8 9 10 11 Next »

    Comment by Karl — February 21, 2008 @ 7:39 am

  22. Thanks! I shud have looked harder for it. =)

    Comment by marc — February 21, 2008 @ 12:16 pm

  23. I had to modify the taggable_paging fix a bit to make it work with the latest version of will_paginate. This solution will also accept all the same options as find_tagged_with.

    Oh, and this drove me crazy… naming the method “paginate_by_tag” won’t work, because will_paginate, being too clever for its own good, will intercept the method_missing call before it gets to the module and look for find_all_by_tag. I renamed the method to _paginate_by_tag to get around this (I know there must be a more elegant solution).

    module ActiveRecord
    module Acts #:nodoc:
    module Taggable #:nodoc:
    module SingletonMethods

    def count_tagged_with(*args)
    options = find_options_for_find_tagged_with(*args)
    options.blank? ? 0 : count(”#{table_name}.id”, options.merge(:select => nil, :distinct => true))
    end

    def _paginate_tagged_with(tags, options = {})
    page, per_page = wp_parse_options!(options)
    offset = (page.to_i - 1) * per_page
    count = count_tagged_with(tags, options)
    options.merge!(:offset => offset, :limit => per_page.to_i)
    items = find_tagged_with(tags, options)
    returning WillPaginate::Collection.new(page, per_page, count) do |p|
    p.replace items
    end
    end

    end
    end
    end
    end

    Comment by Jeremy — February 21, 2008 @ 2:48 pm

  24. Woops, bit of a mistake there… the method here is called _paginate_tagged_with… will_paginate will send paginate_by_tag to find_all_by_tag, and paginate_tagged_with to find_tagged_with (which actually exists, but won’t get you proper pagination!).

    Comment by Jeremy — February 21, 2008 @ 2:53 pm

  25. Jeremy please be more specific, what should the name of your method be called?

    Comment by Nathan — May 28, 2008 @ 10:34 am

RSS feed for comments on this post.
TrackBack URL

Sorry, the comment form is closed at this time.

  • gdagley on Twitter

  • gdagley on del.icio.us

Powered by WordPress