How to pass a collection to paginate?


#1

There must be a better way to write this code:

 @project_pages, @projects= paginate :project,
     :per_page => 10,
     :conditions => ["account_id = ?", account]

?!

If only I could pass the sub-collection
account = …
@projects = account.project

to paginate, instead of letting it extract it with a find :all + sql
conditions

Alain.


#2

On Dec 15, 2005, at 5:19 PM, Alain R. wrote:

account = ...
@projects = account.project

to paginate, instead of letting it extract it with a find :all + sql
conditions

I’ve been wishing for this for a while as well. I’m not sure if all
the pieces are in place yet, as I haven’t made a complete prototype,
but here’s one piece that might be useful if you’re thinking of
fixing this problem:

http://dev.rubyonrails.org/ticket/2466

The idea is that you should be able to set a constraint where the
code within the block will have certain conditions (like yours)
enforced on it. In that way, the pagination methods (or some
derivatives) will fetch the correct result set instead of :all.

Duane J.
(canadaduane)


#3

Duane

http://dev.rubyonrails.org/ticket/2466
The idea is that you should be able to set a constraint where

It would be much simpler/natural to simply feed the prevously fetched
collection to paginate.
=> we could write code like this:

 @project_pages, @projects= paginate :collection => 

@john.pending_projects,
:per_page => 10

Without a feature like this, we are forced to turn model querying
methods - m.pending_projects() - back into sql code for the condition.
It’s really unlike Rais.

Alain


#4

On Dec 16, 2005, at 10:23 AM, Alain R. wrote:

@john.pending_projects,
:per_page => 10

Well, adapted from API docs <http://api.rubyonrails.org/classes/
ActionController/Pagination.html> :

 @projects = someperson.projects
 @project_pages = Paginator.new self, @projects.length, 10, params

[‘page’]

Is this what you are looking for?

izidor


#5

On Dec 16, 2005, at 11:26 AM, Izidor J. wrote:

=> we could write code like this:
params[‘page’]

Is this what you are looking for?

No, this is not what you are looking for :slight_smile:

One needs to also limit the @projects, something like this:

page = params[‘page’].to_i
page = 1 if page <= 0
offset = (page - 1) * 10
@projects = someperson.projects[ offset … offset+10-1]

@project_pages = Paginator.new self, @projects.length, 10, params
[‘page’]

izidor


#6

On Dec 16, 2005, at 11:34 AM, Izidor J. wrote:

ActionController/Pagination.html> :

page = params[‘page’].to_i
page = 1 if page <= 0
offset = (page - 1) * 10
@projects = someperson.projects[ offset … offset+10-1]

@project_pages = Paginator.new self, @projects.length, 10, params
[‘page’]

Heh, third time’s a charm…

Object count must be from the full collection.

@project_pages = Paginator.new self, someperson.projects.length, 10,
params[‘page’]

All this could be generalized and wrapped in a nice small helper,
e.g. paginate_collection or something…

izidor


#7

Izidor

Nice but…

There’s still a problem: how to have the paginator add parameters in the
url?

For example, I want to list all the members for a given project, instead
of just all the members in the db (scaffold’s default) :

part 1:


=> in the view, I add the ‘project’ id as a parameter :

  <%= link_to  project.name,
       {:action => :list, :project => project}  %>

=> when clicked, it translates into

http://localhost:3001/members/list?project=1

(Note the “project=1” at the end of the url)

part 2 :


In the list action, I filter the members by project

def list

 @project = Project.find(params[:project])  <<- use the filter here

 records_per_page = 10
 page = params['page'].to_i
 page = 1 if page <= 0
 offset   = (page - 1) * records_per_page
 @members = @project.members[offset .. offset+records_per_page-1]
 @member_pages = Paginator.new self,
      @project.members.length,
      records_per_page,
      params['page']

end

problem: I cannot have the paginator add/keep “project=1” in the params,
so it generates this html

Next page

(Note: no more “project=1” in the url)
=> when I click on “Next page”, I get an error because the project id is
missing.

Any idea?


#8

On Friday 16 December 2005 20:45, Alain R. wrote:

part 1:
(Note the “project=1” at the end of the url)

Any idea?

Keep the project_id in the session, and have it initialized when the
list is
called with an explicit project_id.

def list
@project_id = params[:project] || session[:current_project_id]
session[:current_project_id] = @project_id
@project = Project.find(@project_id) <<- use the filter here

  records_per_page = 10
  page = params['page'].to_i
  page = 1 if page <= 0
  offset   = (page - 1) * records_per_page
  @members = @project.members[offset .. offset+records_per_page-1]
  @member_pages = Paginator.new self,
       @project.members.length,
       records_per_page,
       params['page']

end


This will have the nice side effect that any selection of project will
be
“sticky”, so the user can go to view / edit the project, go to other
parts of
the application and when come back to the list, still it’s the “current”
project that is shown.


#9

François,

Thanks.

I’m spoiled: it’s too much code for my eyes :slight_smile: It hurts: I keep thinking
about where we started :

def list
@member_pages, @members = paginate :member, :per_page => 10
end

All I wanted was a way to avoid adding the ‘condition’ clause, because
it looked a little ugly!!
:slight_smile:

If ‘scaffold’ knew how to handle ‘belongs_to’, I’m sure somebody would
have felt the same pain, and created a clean and more Railish solution.

Alain


#10

On 12/16/05, Alain R. removed_email_address@domain.invalid wrote:

Here’s some example code from one of my projects. I can’t remember
who sent over the original ‘paginate_collection’ code, but please
pretend that I’ve given proper credit for the idea. Heh.

Hopefully it helps, and won’t make your eyes bleed. Also, hopefully,
Gmail won’t kill it with text-wrapping.

This goes in some user library that you can load from

environment.rb, or you can put it in controllers/application.rb, etc.
def paginate_collection(collection, options = {})
# This helps us paginate collections that are extremely difficult
to produce via assocations.
# Invoke it from a controller like this:
# @pages, @users = paginate_collection(@some_collection, :page =>
params[:page])
default_options = {:per_page => 10, :page => 1}
options = default_options.merge options

pages = Paginator.new self, collection.size, options[:per_page],

options[:page]
first = pages.current.offset
last = [first + options[:per_page], collection.size].min
slice = collection[first…last]
return [pages, slice]
end

#controller method:
def list_exceptions
@exceptions_for_user = App.find(:all, :conditions => [“user_id = ?
and dispositions.status_code = ‘F’”, session[:user].id], :include =>
[:disposition])
@exception_pages, @exceptions =
paginate_collection(@exceptions_for_user, :page => params[:page])
end


#11

Hrm. Can you really leave out the parentheses when doing
multiple-assignment like that?
Try using parens in the call to paginate_collection in your controller
action.


#12

Wilson,

I feel I’m very close, but I keep getting a funny error : (full
stacktrace at bottom)

   undefined method `zero?' for "2":String

, where ‘2’ is my_collection.size

I looked into Rails’ source, and it happens in the Paginator code :

   def page_count
     @page_count ||= @item_count.zero? ? 1 :   ## <<---- ERROR HERE
                       (q,r=@item_count.divmod(@items_per_page);

r==0? q : q+1)
end

@item_count’ seems to be a string. How is this possible: the only place
its value is changed is in the constructor:

 class Paginator
   ...
   def initialize(controller, item_count, items_per_page,

current_page=1)

@item_count = item_count || 0
end

And ‘item_count’, the 2nd parameter, is set to the collection.size
pages = Paginator.new self, collection.size,
options[:per_page], options[:page]

Alain

Full code :

class Member_controller < …
def list
@project = Project.find(1)
@members, @member_pages = paginate_collection @project.members ,
:page => params[:page]
end

end

class ApplicationController < ActionController::Base

def paginate_collection(collection, options = {})
# Invoke it from a controller like this:
# @pages, @users = paginate_collection(@some_collection, :page =>
params[:page])

  default_options  = {:per_page => 10, :page => 1}
  options          = default_options.merge options

  pages            = Paginator.new self, collection.size,

options[:per_page], options[:page]
first = pages.current.offset # <<------- ERROR
OCCURS HERE
last = [first + options[:per_page],
collection.size].min
slice = collection[first…last]

  return [pages, slice]
end

end

Error Stacktrace :

actionpack-1.11.2/… :
lib/action_controller/pagination.rb:253:in page_count' lib/action_controller/pagination.rb:261:inhas_page_number?’
lib/action_controller/pagination.rb:287:in initialize' lib/action_controller/pagination.rb:267:innew’
lib/action_controller/pagination.rb:267:in []' lib/action_controller/pagination.rb:235:incurrent’
#{RAILS_ROOT}/app/controllers/application.rb:11:in paginate_collection' #{RAILS_ROOT}/app/controllers/members_controller.rb:12:inlist’


#13

Try taking the space out between paginate_collection and the opening
parentheses.
Hopefully I’m not leading out astray here. Heh.


#14

Wilson B. wrote:

Try using parens in the call to paginate_collection in your
controller
action.

Same problem, even when I use parentheses :

 @members, @member_pages = paginate_collection  (@project.members ,
          :page => params[:page])

Alain