Will_Paginate - how to get an array of all paginated object ids? - ruby-on-rails

I'm using will_paginate 2.3.x gem and I'm trying to get an array of all the ids of the objects that are in the WillPaginate::Collection. Is there a simple way to iterate all the items in the collection? With ActiveRecord collection this is simple:
all_ids = MyObject.find(:all).map { |o| o.id }
However, when paginated, the collection returns only the N elements that are in the first page:
not_all_ids = MyObject.paginate(:page => 1).map { |o| o.id }
What I want is to go through all the pages in the collection. Basically, I'm looking for a method that retrieves the next page. Any thoughts? thanks.
EDIT - I'm using rails 2.3. Also, I'm using pagination with some conditions but dropped them to simplify:
MyObject.paginate(:conditions => [...], :include => [...], :page => 1)

You could do something like that :
ids = MyObject.select("id").paginate(:page => 1)
if ids.total_pages > 1
(2..ids.total_pages).each do |page|
ids << MyObject.select("id").paginate(:page => page)
end
end
ids = ids.map(&:id)
But why using paginate in this case ?
ids = MyObject.select("id").map(&:id)
will be faster, and will use less resources ... if you're db contains 10000 elements and you're iterating 10 at a times you'll make 1000 cals to your db vs 1 :-)
ps: I'm using .select("id") so the request generated is :
SELECT id from users;
VS
SELECT * FROM users;
I'm also using a nice shortcut map(&:id), this is the equivalent of .map { |o| o.id }

Related

optimizing select query on has_many :through attributes association

I want to find all posts that are tagged with tags that are passed in a params array.
post has many tags through association.
currently my code looks like this:
if params.has_key?(:tags)
params[:tags].each do |tag|
#tags = Array.new if #tags.nil?
#tag = Tag.find_by_content(tag)
#tags << #tag if #tag
end
#allposts = Post.followed_by(#user).select { |p| p.tags.size != 0 && (p.tags & #tags).size == p.tags.size }
else
#allposts = Post.followed_by(#user)
end
what i'm basically doing is finding the actual tag models according to the params array and putting them into an array, then I run a select query on all posts searching for those with the same tags array.
is there a better and cleaner way to do this ?
You can roll your Tag.find query into a single request to the DB, and add an appropriate where clause to limit the posts returned:
finder = Post.followed_by(#user)
if params.has_key?(:tags)
#tags = Tag.where(:content => params[:tags])
finder = finder.with_tags(#tags)
end
#allposts = finder.all
in app/models/post.rb
scope :with_tags, lambda { |tags| joins(:tags).group('posts.id').where(:tags => { :id => tags.map { |t| t.id } } ).having("COUNT(*) = ?", tags.length) }
UPDATE
Here's what the with_tags scope does:
joins(:tags) Firstly we join the tags table to the posts table. Rails will do with with an inner join when you use the symbol syntax
where(:tags => { :id => tags.map { |t| t.id } } ) We want to filter the tags to only find those tags provided. Since we are providing a list of tag objects we use map to generate an array of IDs. This array is then used in the where clause to create a WHERE field IN (list) query - the hash within a hash syntax is used to denote the table, then column within the table.
group('posts.id') So now that we have a list of posts with the requisite tags, however, if there are multiple tags we will have posts listed multiple times (once for each matched tag), so we group by the posts.id so that we only have 1 row returned for each post (it's also required to that we can do the count in step 4)
having("count(*) = ?", tags.length) This is the final piece of the puzzle. Now that we've grouped by the post, we can count the number of matched tags associated with this post. So long as duplicate tags are not allowed then if the number of matched tags (count(*)) is the same as the number of tags we were searching with (tags.length) Then we can be sure that the post has all the tags we were searching with.
You can find a lot more information about the different query methods available for models by reading the Active Record Query Interface Guide

how to paginate records from multiple models? (do I need a polymorphic join?)

After quite a bit of searching, I'm still a bit lost. There are a few other similar questions out there that deal with paginating multiple models, but they are either unanswered or they pagainate each model separately.
I need to paginate all records of an Account at once.
class Account
:has_many :emails
:has_many :tasks
:has_many :notes
end
So, I'd like to find the 30 most recent "things" no matter what they are. Is this even possible with the current pagination solutions out there?
Like using some combination of eager loading and Kaminari or will_paginate?
Or, should I first set up a polymorphic join of all these things, called Items. Then paginate the most recent 30 items, then do a lookup of the associated records of those items.
And if so, I'm not really sure what that code should look like. Any suggestions?
Which way is better? (or even possible)
Rails 3.1, Ruby 1.9.2, app not in production.
with will_paginate :
#records = #do your work and fetch array of records you want to paginate ( various types )
then do the following :
current_page = params[:page] || 1
per_page = 10
#records = WillPaginate::Collection.create(current_page, per_page, records.size) do |pager|
pager.replace(#records)
end
then in your view :
<%=will_paginate #records%>
Good question... I'm not sure of a "good" solution, but you could do a hacky one in ruby:
You'd need to first fetch out the 30 latest of each type of "thing", and put them into an array, indexed by created_at, then sort that array by created_at and take the top 30.
A totally non-refactored start might be something like:
emails = Account.emails.all(:limit => 30, :order => :created_at)
tasks = Account.tasks.all(:limit => 30, :order => :created_at)
notes = Account.notes.all(:limit => 30, :order => :created_at)
thing_array = (emails + tasks + notes).map {|thing| [thing.created_at, thing] }
# sort by the first item of each array (== the date)
thing_array_sorted = thing_array.sort_by {|a,b| a[0] <=> b[0] }
# then just grab the top thirty
things_to_show = thing_array_sorted.slice(0,30)
Note: not tested, could be full of bugs... ;)
emails = account.emails
tasks = account.tasks
notes = account.notes
#records = [emails + tasks + notes].flatten.sort_by(&:updated_at).reverse
#records = WillPaginate::Collection.create(params[:page] || 1, 30, #records.size) do |pager|
pager.replace(#records)
end
Thats it... :)

Rails: combining optional params into a query

I have a view with a huge paginated list of records that need filtering.
Users can filter by records a few different ways (such as 'saved' records, 'read' records, and 'mark deleted' records) and I'd like for them to be able to combine these filters any possible way.
My current, flawed, non-functioning approach. The code below does not produce anything unless all of the params are specified and valid:
#view. Set the 'se' filter to true; leave all others as is
<%= link_to list_url(:id=>params[:id], :se=>"true", :st=>params[:st], :re=>params[:re]) do %>
<div class="button">Toggle SE</div>
<% end %>
#controller query. Add whichever params are passed into the conditions for the new page.
#query is paginated and sorted
#records = Record.where("user_id IN (?) AND see = ? AND star = ? AND delete = ? AND like = ?", #users.select("id"), params[:se], params[:st], params[:re]).paginate :page => params[:page], :order => (sort_column + " " + sort_direction)
What is the best way to create this filtering system?
I imagine a client-side sort would be faster than asking the server to become involved every time - is there a simple AJAX way to accomplish this kind of thing? Imagine filters the user can toggle on and off in any combination.
Try this:
conditions = {:user_id => #users.select("id")}
{
:se => :see,
:st => :star,
:del => :delete
}.each{|k1, k2| conditions[k2] = params[k1] unless params[k1].blank?}
#records = Record.where(conditions).paginate(...)
The conditions hash will be filled based on the values present in the params hash.
Edit 1
You can combine conditions hash and array.
#records = Record.where(conditions).where(
":created_at > ?", Date.today - 30).paginate(...)
You can change the user_id condition to what ever you want by specifying
conditions[:user_id] = #user.id
In the above statement, if the RHS is an array, rails automatically generates the IN clause. Otherwise, equality check(=) is performed.
Can also use Anonymous scopes: Combine arrays of conditions in Rails

grouping comments by parent object, ordering parent object by oldest comment

I have objects that have comments. As part of a periodic email summary, I want to determine comments for a period, and present the objects in the order of oldest commented object first.
Data:
object_id comment_id
30 40
40 42
32 41
30 43
32 44
Output:
Object #30
comment 40
comment 43
Object #40
comment 42
Object #32
comment 41
comment 44
I am using this code to get the data to an intermediate array - I tried to get it all in one swoop using .group_by(&:commentable_id) but the data didn't come out in correct order.
comments = account.comments.all(
:conditions => ["comments.created_at > ?", 8.hours.ago],
:order => "comments.created_at asc" ).map { |c| [c.commentable_id,c.id] }
=> [ [30,40], [40,42], [32,41], [30,43], [32,44] ]
If I can get that data to transform into the following form, I could just iterate over the array to build the email content...
[ [30,[40,43]], [40,[42]], [32,[41,44]] ]
But I wonder if I'm making this harder than I need to... Any advice?
(I'm using Rails 2.3 and Ruby ree-1.8.7)
You can use a group with an array aggregate to get to the array form that you're looking for.
Array aggregates are massively db dependent. MySQL's is GROUP_CONCAT. Postgres' is ARRAY_AGG. Sqlite doesn't have one out of the box, but I know you can define custom aggregate functions, so it's not impossible.
Haven't actually tried running this code, but here's something that should point you in the right direction:
result = Object.all(
:select => 'objects.id, GROUP_CONCAT(comment_id) AS comment_array',
:group => 'comments.id'
).map { |c| [c.id, c.comment_array] }
I used the naming from the first example, so you'll need to change 'object' to whatever your table is called. Hope it makes sense. Rails probably doesn't have inbuilt support for parsing an array, so it will probably return a string for comment_array, and you might have to parse it.
Having all the comments for a single object in a single block/element will definitely make life easier while doing any operation on them. However, I won't go as far as turning them into an array of array of arrays because it is already an array of arrays. I would prefer creating a hash like so:
comments_array = [ [30,40], [32,41], [40,42], [30,43], [32,44] ]
obj_with_comments = {}
comments_array.each do |x|
obj_with_comments[x.first] ||= []
obj_with_comments[x.first] << x.last
end
obj_with_comments #=> {40=>[42], 30=>[40, 43], 32=>[41, 44]}
But this presents another problem which is, hashes are not ordered, so you loose your ordering in some random fashion if you just iterate over the hash. However, you can create an array of objects then iterate over the hash like so:
objects = comments_array.collect{|x| x.first}.uniq
objects #=> [30, 32, 40]
# now get the hash value for each object key in order
objects.each { |obj| puts obj_with_comments[obj].inspect }
Hope that makes sense.
Try this:
comments = account.comments.all(
:conditions => ["comments.created_at > ?", 8.hours.ago],
:order => "comments.commentable_id ASC, comments.id ASC"
).map { |c| [c.commentable_id,c.id] }
This will return the following result set:
=> [ [30,40], [30,43], [32,41], [32,44], [40,42] ]
In the query above I am using id for sorting instead of create_at. If you are using MySQL and if the id's are auto generated this logic will work as the id of a new object will be higher than the id of an older object. If you don't allow editing of comments then this logic will work.
If you want to explicitly sort by the dates then use the following syntax:
comments = account.comments.all(
:joins => "accounts AS accounts ON comments.commentable_type = 'Account' AND
comments.commentable_id = accounts.id",
:conditions => ["comments.created_at > ?", 8.hours.ago],
:order => "accounts.id ASC, comments.id ASC"
).map { |c| [c.commentable_id,c.id] }

Using will_paginate with multiple models (Rails)

Pretty sure that I'm missing something really simple here:
I'm trying to display a series of pages that contain instances of two different models - Profiles and Groups. I need them ordering by their name attribute. I could select all of the instances for each model, then sort and paginate them, but this feels sloppy and inefficient.
I'm using mislav-will_paginate, and was wondering if there is any better way of achieving this? Something like:
[Profile, Group].paginate(...)
would be ideal!
Good question, I ran into the same problem a couple of times. Each time, I ended it up by writing my own sql query based on sql unions (it works fine with sqlite and mysql). Then, you may use will paginate by passing the results (http://www.pathf.com/blogs/2008/06/how-to-use-will_paginate-with-non-activerecord-collectionarray/). Do not forget to perform the query to count all the rows.
Some lines of code (not tested)
my_query = "(select posts.title from posts) UNIONS (select profiles.name from profiles)"
total_entries = ActiveRecord::Base.connection.execute("select count(*) as count from (#{my_query})").first['count'].to_i
results = ActiveRecord::Base.connection.select_rows("select * from (#{my_query}) limit #{limit} offset #{offset}")
Is it overkilled ? Maybe but you've got the minimal number of queries and results are consistent.
Hope it helps.
Note: If you get the offset value from a http param, you should use sanitize_sql_for_conditions (ie: sql injection ....)
You can get close doing something like:
#profiles, #groups = [Profile, Group].map do |clazz|
clazz.paginate(:page => params[clazz.to_s.downcase + "_page"], :order => 'name')
end
That will then paginate using page parameters profile_page and group_page. You can get the will_paginate call in the view to use the correct page using:
<%= will_paginate #profiles, :page_param => 'profile_page' %>
....
<%= will_paginate #groups, :page_param => 'group_page' %>
Still, I'm not sure there's a huge benefit over setting up #groups and #profiles individually.
in my last project i stuck into a problem, i had to paginate multiple models with single pagination in my search functionality.
it should work in a way that the first model should appear first when the results of the first model a second model should continue the results and the third and so on as one single search feed, just like facebook feeds.
this is the function i created to do this functionality
def multi_paginate(models, page, per_page)
WillPaginate::Collection.create(page, per_page) do |pager|
# set total entries
pager.total_entries = 0
counts = [0]
offsets = []
for model in models
pager.total_entries += model.count
counts << model.count
offset = pager.offset-(offsets[-1] || 0)
offset = offset>model.count ? model.count : offset
offsets << (offset<0 ? 0 : offset)
end
result = []
for i in 0...models.count
result += models[i].limit(pager.per_page-result.length).offset(offsets[i]).to_a
end
pager.replace(result)
end
end
try it and let me know if you have any problem with it, i also posted it as an issue to will_paginate repository, if everyone confirmed that it works correctly i'll fork and commit it to the library.
https://github.com/mislav/will_paginate/issues/351
Have you tried displaying two different sets of results with their own paginators and update them via AJAX? It is not exactly what you want, but the result is similar.

Resources