pagination and sorting

Hi all,

I've got a list of machines with columns like so:

Hostname, Operating System, Location

I have setup pagination with paginating_find and that works nicely. I have also setup column sorting with my own little helper and controller code. Because I wrote the helper myself I have got it to preserve the page parameter when changing sort mode.

So when you move to a certain page and do a sort, the page is kept in the same place. However, if I sort a column and then change to a different page, the sorting reverts to the default which makes changing the page useless a lot of the time.

What is the preferred method to combine column sorting and pagination easily? This must be a fairly common exercise but I can't get my head round how to do it.

Any help would be appreciated.

Thanks

Matt

Matt Harrison wrote:

Hi all,

I've got a list of machines with columns like so:

Hostname, Operating System, Location

I have setup pagination with paginating_find and that works nicely. I
have also setup column sorting with my own little helper and
controller code. Because I wrote the helper myself I have got it to
preserve the page parameter when changing sort mode.

So when you move to a certain page and do a sort, the page is kept in
the same place. However, if I sort a column and then change to a
different page, the sorting reverts to the default which makes
changing the page useless a lot of the time.

What is the preferred method to combine column sorting and pagination
easily? This must be a fairly common exercise but I can't get my head
round how to do it.

Any help would be appreciated.

Can't believe no-one has any ideas about this, maybe it just got buried
under all the other mail.

Grateful someone could take a sec to help me :slight_smile:

Thanks

Matt

Matt,

I find that the best is to just reset to the beginning with any change
of sort or pagination. It's too confusing not to. For example, if you
are showing 5 records per page in a 50 record set and you are on page
4, then you suddenly switch to 25 records per page, what should it do?
Throw an error (there is no page 4 anymore)? Or have you on page 2? Or
back to page 1?

And with sorting, if I'm on page 4 of 7 pages sorted by last name,
then suddenly switch to sorting by first name, staying on page 4... at
least to me... is more confusing than just reverting back to page one.
Of course, you could stay on page 4 with the new sort. So sorting
seems to be more of a design choice. But pagination changes: go back
to page 1.

-Danimal

As for some technical ways to do it... go look at ActiveScaffold and
see how they handle it.

For me, I have a combination of URL parameters and session values that
get passed around. The session values are so some things will stay no
matter what (like pagination) and the parameters are for one-time
adjustments (like moving to other pages).

For example, if I have a model Widgets with a widgets controller, I
might have a URL like:

http://localhost:3000/widgets?page=1&per_page=25&sort=updated_at&sort_direction=ASC

Then I have a before_filter that builds the @widgets collection using
WillPaginate and some custom sorting code.

I can change the "per_page" value and it is stored in a session
variable. But the "page" and "sort" and "sort_direction" are only
stored in the params hash. So if I go to some other page then come
back to: http://localhost:3000/widgets/ without any params, it will
still be 25 records per page, but by default on page 1 with the
default sort and default direction.

I hope that helps you along. I suspect there may be some good plugins
or gems for handling pagination and sorting well. I started with
WillPaginate and then added in the sorting code myself. At least for
me, it works and works well.

-Danimal

Danimal wrote:

Matt,

I find that the best is to just reset to the beginning with any change
of sort or pagination. It's too confusing not to. For example, if you
are showing 5 records per page in a 50 record set and you are on page
4, then you suddenly switch to 25 records per page, what should it do?
Throw an error (there is no page 4 anymore)? Or have you on page 2? Or
back to page 1?

And with sorting, if I'm on page 4 of 7 pages sorted by last name,
then suddenly switch to sorting by first name, staying on page 4... at
least to me... is more confusing than just reverting back to page one.
Of course, you could stay on page 4 with the new sort. So sorting
seems to be more of a design choice. But pagination changes: go back
to page 1.

-Danimal

Hi, thanks for the reply.

I agree that there are some cases where it should reset the page or the
sort. OTOH picture this:

I'm on page 1 and I sort by hostname ASC, it gives me the hostnames
starting at A etc. Then I want to move through this sorted list so I go
to page 2, and my sort has been reset because I can't work out how to
modify the links generated by paginating_find.

I'm not fiddling with the page size yet so that isn't an issue, and it
probably should go back to page 1 if I change the sort order, however I
really would think that keeping the sort order when changing pages is
kinda important.

I might be going mad with this so let me know if i'm babbling :stuck_out_tongue:

Thanks

Matt

Matt,

No, you are making sense. I think you and I got mixed up backwards.

If you are KEEPING sort order but CHANGING pages, then don't change
the sort.
If you are KEEPING pages but CHANGING sort order, then I would argue,
force a page change to page 1.

So, in your example, if you sort by hostname ASC then go to page 2, it
should maintain the hostname sort. But if you are on page 2 and then
change your sort to IP address, it should jump you to page 1 (although
as I stated in my previous post, you could stay on page 2 in this
instance).

It's not a big deal programmatically because you take your collection
of objects first (i.e. via find() ), then you sort, then you paginate.

The only thing that could mess this up is if your dataset changes
between pages. If that is a concern, then you'd want to think through
the best way to handle that, which could be one of multiple options.

So, to reiterate, here's how I handle it. Here is my index method for
widgets:

[code]
def index
  @widgets = paginate_and_sort Widgets.find(:all) # or whatever means
to get an array of widget objects
end
[/code]

Then, I have this defined on application.rb (cause I use it all over):

[code]
  def paginate_and_sort(collection)
    per_page = (session[:per_page] || 5).to_i
    page = (params[:page] ||= 1).to_i
    page = 1 if (page - 1) * per_page > collection.size or page < 1
    offset = (page - 1) * per_page
    @sorted_on = params[:sort] unless params[:sort_direction] == "reset"
    @sorted_dir = params[:sort_direction]
    if (["ASC", "DESC"].include?(@sorted_dir))
      collection = if @sorted_dir == "DESC"
        collection.sort {|a,b| b.send(params[:sort]) <=>
a.send(params[:sort])}
      else
        collection.sort {|a,b| a.send(params[:sort]) <=>
b.send(params[:sort])}
      end
    else
      collection = collection.sort {|a,b| b.updated_at <=>
a.updated_at }
    end

    WillPaginate::Collection.new(page, per_page,
collection.length).concat(collection[offset..(offset + per_page - 1)])
  end
[/code]

I then set the session[:per_page] elsewhere, i.e. when the user
changes that in a different action, I set it as a session variable.

So note that though not the cleanest code, it works and works well. I
build the values needed for will_paginate then I sort the collection
either by the sort & direction or by updated_at (that's my default,
yours may differ). Then pass that into will_paginate which will return
a pseudo-array of the paginated objects.

HTH!

-Danimal

Danimal wrote:

Matt,

No, you are making sense. I think you and I got mixed up backwards.

If you are KEEPING sort order but CHANGING pages, then don't change
the sort.
If you are KEEPING pages but CHANGING sort order, then I would argue,
force a page change to page 1.

So, in your example, if you sort by hostname ASC then go to page 2, it
should maintain the hostname sort. But if you are on page 2 and then
change your sort to IP address, it should jump you to page 1 (although
as I stated in my previous post, you could stay on page 2 in this
instance).

It's not a big deal programmatically because you take your collection
of objects first (i.e. via find() ), then you sort, then you paginate.

The only thing that could mess this up is if your dataset changes
between pages. If that is a concern, then you'd want to think through
the best way to handle that, which could be one of multiple options.

So, to reiterate, here's how I handle it. Here is my index method for
widgets:

[code]
def index
  @widgets = paginate_and_sort Widgets.find(:all) # or whatever means
to get an array of widget objects
end
[/code]

Then, I have this defined on application.rb (cause I use it all over):

[code]
  def paginate_and_sort(collection)
    per_page = (session[:per_page] || 5).to_i
    page = (params[:page] ||= 1).to_i
    page = 1 if (page - 1) * per_page > collection.size or page < 1
    offset = (page - 1) * per_page
    @sorted_on = params[:sort] unless params[:sort_direction] == "reset"
    @sorted_dir = params[:sort_direction]
    if (["ASC", "DESC"].include?(@sorted_dir))
      collection = if @sorted_dir == "DESC"
        collection.sort {|a,b| b.send(params[:sort]) <=>
a.send(params[:sort])}
      else
        collection.sort {|a,b| a.send(params[:sort]) <=>
b.send(params[:sort])}
      end
    else
      collection = collection.sort {|a,b| b.updated_at <=>
a.updated_at }
    end

    WillPaginate::Collection.new(page, per_page,
collection.length).concat(collection[offset..(offset + per_page - 1)])
  end
[/code]

I then set the session[:per_page] elsewhere, i.e. when the user
changes that in a different action, I set it as a session variable.

So note that though not the cleanest code, it works and works well. I
build the values needed for will_paginate then I sort the collection
either by the sort & direction or by updated_at (that's my default,
yours may differ). Then pass that into will_paginate which will return
a pseudo-array of the paginated objects.

HTH!

-Danimal

Well I'm glad we're on the same page (excuse the pun). I've managed to fix the "changing page resets sort" problem by using the session as the store instead of params. That means I don't have to fiddle with paginating_find and get the params passed.

I'm onto the next problem now:

/machines/index => Shows a list of machine which are paginated and can be sorted.

/machines/view/1 => Views a machine by ID and lists records associated with the machine.

I want to paginate and sort the list of records but I'm not sure if this can be done with associated records. Ideally it would be in the records controller but for some reason I chose to put it in the machines/view action.

Any suggestions on paginating and sorting an associated collection? Incidentally, the records also have uploads associated with them so I will have to do it again with the uploads list.

Thanks for the help so far and I'd appreciate any further advice :slight_smile:

Thanks

Matt