BUILDING A SOCIAL NETWORKING
WEBSITE WITH RUBY ON RAILS

RailsSpace will_paginate

Posted 3 months ago by Raul Parolari

The old pagination used in Rails 1.2.3 has been removed from the code in 2.0, but it is still supported via the plugin classic_pagination. However, there are good reasons to kiss her goodbye:

  • first, a practical one: the classic_pagination plugin exists just for temporary compatibility; there is no guarantee that it will be maintained.
  • second, the plugin forces the programmer to jump through some hoops: see listing 11.10 (or in the final version the method ‘paginate’ in application.rb), which, depending on the argument class, calls the plugin (‘super’) or does ‘manual pagination’; the writer’s comment (which aptly captures the absurdity of the situation) is: “Paginate by hand”.
  • third, it has issues of performances when the number of pages is large.
  • finally, a design opinion: this code is a bit uncharacteristic for Rails; the class chosen for Pagination is the ActionController, but the logic seems instead more related to ActiveRecord and ActionView (as pagination is a ‘presentation’ of ‘Model’ instances). The only advantage that I see in having this in the Controller is that the params method is available (while in a Model, it is not); so, a parameter such as ‘params[:page]’ does not need to be passed (in a scale from 0 to 10, weight=1 !).

There are several plugins that replace pagination; I examine here one of the most praised, will_paginate.

Let us start from the good news; give a look at the listing we just mentioned, 11.10 (method ‘paginate’ in application.rb): well, it just disappears! there is simply no need for it. It is remarkable that at the introduction of that method (start of chapter 11.1.4), the book comments:

community_controller.rb (listing 11.9)
# what we would really like to do is this:
def search
  if params[:q]
    ..
    @pages, @users = paginate(@users)
  end
end

Well, with will_paginate that wish becomes reality! if the collection on which we want to paginate has already been computed, eg in @users, we can just write:

@users = @users.paginate(:page => params[:page], :per_page => 10)

And to paginate on a model we could write (example):

@specs = Spec.paginate(:page => curr_page, :per_page => 10)

But wait a moment: the old pagination returned two objects (see above, @pages, @users), one to manage the pagination, the other with the results for the current page; now, we have only one object, @specs in the example!

The answer is: indeed we have just one object, and it is a bit smarter; it knows both the result of the search and the one for the pagination! (removing the confusion around those 2 objects was such a gift!).

All right; let us now proceed methodically and show how the code in chapters 10&11 can be changed. First, delete the old plugin and install will_paginate (see http://rock.errtheblog.com/will_paginate):

Let’s start recoding a few methods; the new code is:

controllers/community_controller.rb, method index:
def index  
  @title = "Community"
  @letters = ("A".."Z").to_a
  if params[:id] 
    @initial  = params[:id] 
    curr_page = params[:page]
    specs  = Spec.search_on_last_name(@initial)
    users  = specs.collect { |spec| spec.user }
    @users = users.paginate(:page => curr_page, :per_page => 10)
    end
end

Here the pagination is done in the last step, on users (not any longer on specs). So, the search method invoked is just a model search method:

models/spec.rb:
# search specs based on last_name initial
def self.search_on_last_name(name_initial)
  find(:all, :conditions => ["last_name like ?", name_initial + '%'], 
             :order => "last_name, first_name")
end

Now, the partials which present the paginated users: first, the _user_table.rhtml (or _user_table.html.erb if you use the rails 2.0 naming convention; both are supported in 2.0); I show what changes from the old to the new version:

views/_user_table.rhtml; old code:
  <% if paginated? %>
    <tr>
      <td colspan="4" align="right">
      Pages: <%= pagination_links(@pages, :params => params) %>
    </td>
    </tr>
  <% end %>
views/_user_table.rhtml; new code:
  <tr>
    <td colspan="4" align="right">
      <%= will_paginate @users %>
    </td>
  </tr>

Finally, we have to recode the _result_summary.rhtml file, that displays the total number of results and the ones displayed in the page. Here I did not find really friendly methods (I mean methods that avoid the business of ‘offset +- 1’); so we will pretend that the friendly methods exist, and then we will add them to the plugin:

views/_result_table.rhtml:
<% if @users %>
  <p>
  Found <%= pluralize(@users.total_entries, "match") %>.
  <% if @users.paginated? %>
    Displaying users <%= @users.first_item %>–<%= @users.last_item %>.
  <% end %>
  </p>
<% end %>

Now we add to the plugin the new methods added, by reopening.. ehm, which is the will_paginate class? let’s ask Ruby! bring up the rails console:

ruby script/console
>> Spec.paginate(:all, :page => 1).class
>> WillPaginate::Collection

All right, so we need to reopen the WillPaginate::Collection class; we have learnt how to reopen a class in a recent post (on Session); thus, under the lib directory we write a file (that must be required) that will do:

lib/will_paginate_ext.rb:
class WillPaginate::Collection 

  def first_item; offset + 1; end

  def last_item ; first_item + length - 1; end

  def paginated?; total_entries > size; end 
end

[Note: in case anyone finds similar methods in will_paginate, let me know and I will edit this post. The point here is just to centralize the treacherous +-1 and >,>= operations].

All this work pays handsomely when we add the search for users across spec/faq/user (chapter 11). As mentioned at the beginning, the long and twisted method ‘paginate’ (listing 11.10) in application.rb disappears! we just need to change one call from the community controller in the action ‘search’:

views/controllers/community_controller.rb; old code:
  @pages, @users = paginate(@users)
controllers/community_controller.rb; new code:
  @users = @users.paginate(:page => params[:page], :per_page => 10)

This single call builds the paginated @users, that can be fed to the View search.html.erb (which calls the same partials that we have seen above). With one shot (ie, one line), we have solved the problem; nice!

And, to finish chapter 11, we port the pagination for the A/S/L search: the old code is:

views/controllers/community_controller.rb browse method; old code:
  specs = Spec.find_by_asl(params)
  @pages, @users = paginate(specs.collect { |spec| spec.user })
And the new code is:
views/controllers/community_controller.rb browse method
  specs = Spec.find_by_asl(params)
  users  = specs.collect { |spec| spec.user }
  @users = users.paginate(:page => params[:page], :per_page => 10)
Notice that we could concatenate statements, for example:
  specs  = Spec.find_by_asl(params)
  @users = specs.collect { |spec| spec.user }.paginate(:page => params[:page], :per_page => 10)

This is very much in vogue in Ruby/Rails as it is a compact notation, which eliminates intermediate local variables (and if you really want to impress your friends, you can even concatenate the 3 statements!).

Personally, I still like lines of code of about 80 characters (instead of 100, or 140 with the 3 lines concatenated; which is not unusual in Rails internal code!). In any case, the important is to understand all these types of notations.

Last observations:

  • All the code presented above was tested ok (using the web browser, and running the test suite).
  • RailsSpace uses pagination again later, in chapter 15, for posts in blogs; the modification should be easy, if you have read this post (so, we leave it as an exercise to the motivated reader :-).
  • Finally: for more on will_paginate, see the brilliant screencast from Ryan Bates at http://media.railscasts.com/videos/051_will_paginate.mov

Comments

There are 7 comments on this post. Post yours →

John Miller
posted 2 months ago

Thanks for detailing these changes. I have two questions:

  1. You say

“thus, under the lib directory we write a file (that must be required) that will do: lib/willpaginateext.rb:”

but I wonder where this file gets required? Searching around, I find app/helpers/application_helper.rb where ‘string’ and ‘object’ are required, so I added

require ‘willpaginateext’

Right? Then I see at the bottom of that page the ‘def paginate’ method; I assume that can be deleted?

  1. You say

“RailsSpace uses pagination again later, in chapter 15, for posts in blogs; the modification should be easy, if you have read this post (so, we leave it as an exercise to the motivated reader :-).”

However, I’m not sure we have the same idea about ‘easy’! My naive attempt at this is to look for ” throughout the site, and I find it in only one place:

/app/views/profile/_blog.html

Following your example, I removed the and corresponding statements, and changed

to:

Therefore, the entire file looks like:

  Post  of

  Posts &ndash; of

“posts/post”, :collection => @posts %>

Is this correct? And are there any other changes to make so that blog posts will be properly paginated?

Thanks!


John Miller
posted 2 months ago

Trying again to get formatting:

Thanks for detailing these changes. I have two questions:

  1. You say

“thus, under the lib directory we write a file (that must be required) that will do: lib/willpaginateext.rb:”

but I wonder where this file gets required? Searching around, I find app/helpers/application_helper.rb where ‘string’ and ‘object’ are required, so I added

require ‘willpaginateext’

Right? Then I see at the bottom of that page the ‘def paginate’ method; I assume that can be deleted?

  1. You say

“RailsSpace uses pagination again later, in chapter 15, for posts in blogs; the modification should be easy, if you have read this post (so, we leave it as an exercise to the motivated reader :-).”

However, I’m not sure we have the same idea about ‘easy’! My naive attempt at this is to look for ” throughout the site, and I find it in only one place:

/app/views/profile/_blog.html

Following your example, I removed the and corresponding statements, and changed

to:

Therefore, the entire file looks like:

  Post  of

  Posts &ndash; of

“posts/post”, :collection => @posts %>

Is this correct? And are there any other changes to make so that blog posts will be properly paginated?

Thanks!


John Miller
posted 2 months ago

Sorry, there are no instructions for getting any kind of formatting into these comments.


ike
posted about 1 month ago

Hey John,

i did add require ‘will_paginate_ext’ into helpers/application_helper.rb and deleted the paginated? method in there as well.

here is what needs to be done to make chapter 15 (posts in blogs) working with will_paginate:

  1. app/controllers/application.rb method makeprofilevars: change @pages, @posts = paginate(@blog.posts, :per_page => 3) into @posts = @blog.posts.paginate(:page => params[:page], :per_page => 3)

  2. app/controllers/post_controller.rb method index: change @pages, @posts = paginate(@blog.posts) into @posts = @blog.posts.paginate(:page => params[:page], :per_page => 4)

  3. app/views/posts/index.html.erb change

into

  1. app/views/profile/_blog.html.erb change

into

  1. app/views/profile/_blog.html.erb change

into

that’s basically all. tests and browsing works for me now.

Regards, Ike


ike
posted about 1 month ago

formatting really seems to be hard here ;-)

  1. app/views/posts/index.html.erb change \ into \

ike
posted about 1 month ago

… anyway.

just apply the same changes to views/posts/index.html.erb and views/profile/blog.html.erb, which have been made above in the post in _resulttable.rhtml.

Have a good one, Ike


Falk
posted 19 days ago

@Raul Parolari

[Note: in case anyone finds similar methods in will_paginate, let me know and I will edit this post. The point here is just to centralize the treacherous +-1 and >,>= operations]

“willpaginate” offers the function “pageentries_info(collection)” to display a standard result summary.

API-Link:

http://rock.errtheblog.com/will_paginate/classes/WillPaginate/ViewHelpers.html

With the help of the function “pageentriesinfo(collection) you could insert:

for example, or use the given source code in the api to create a personalized version of the result summary.

Regards, Falk


Post a comment

Required fields in bold.