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

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... :)

Related

Help converting Rails 2 Database logic to Rails 3.1/ PostgreSQL

How do I select a single random record for each user, but order the Array by the latest record pr. user.
If Foo uploads a new painting, I would like to select a single random record from foo. This way a user that uploads 10 paintings won't monopolize all the space on the front page, but still get a slot on the top of the page.
This is how I did it with Rails 2.x running on MySQL.
#paintings = Painting.all.reverse
first_paintings = []
#paintings.group_by(&:user_id).each do |user_id, paintings|
first_paintings << paintings[rand(paintings.size-1)]
end
#paintings = (first_paintings + (Painting.all - first_paintings).reverse).paginate(:per_page => 9, :page => params[:page])
The example above generates a lot of SQL query's and is properly badly optimized. How would you pull this off with Rails 3.1 running on PostgreSQL? I have 7000 records..
#paintings = Painting.all.reverse = #paintings = Painting.order("id desc")
If you really want to reverse the order of the the paintings result set I would set up a scope then just use that
Something like
class Painting < ActiveRecord::Base
scope :reversed, order("id desc")
end
Then you can use Painting.reversed anywhere you need it
You have definitely set up a belongs_to association in your Painting model, so I would do:
# painting.rb
default_scope order('id DESC')
# paintings_controller.rb
first_paintings = User.includes(:paintings).collect do |user|
user.paintings.sample
end
#paintings = (first_paintings + Painting.where('id NOT IN (?)', first_paintings)).paginate(:per_page => 9, :page => params[:page])
I think this solution results in the fewest SQL queries, and is very readable. Not tested, but I hope you got the idea.
You could use the dynamic finders:
Painting.order("id desc").find_by_user_id!(user.id)
This is assuming your Paintings table contains a user_id column or some other way to associate users to paintings which it appears you have covered since you're calling user_id in your initial code. This isn't random but using find_all_by_user_id would allow you to call .reverse on the array if you still wanted and find a random painting.

Rails 2.3.5 Problem Building Conditions Array dynamically when using in (?)

Rails 2.3.5
I've looked at a number of other questions relating to building conditions dynamically for an ActiveRecord find.
I'm aware there are some great gems out there like search logic and that this is better in Rails3. However, I'm using geokit for geospacial search and I'm trying to build just a standard conditions set that will allow me to combine a slew of different filters.
I have 12 different filters that I'm trying to combine dynamically for an advanced search. I need to be able to mix equality, greater than, less than, in (?) and IS NULLs conditions.
Here's an example of what I'm trying to get working:
conditions = []
conditions << ["sites.site_type in (?)", params[:site_categories]] if params[:site_categories]
conditions << [<< ["sites.operational_status = ?", 'operational'] if params[:oponly] == 1
condition_set = [conditions.map{|c| c[0] }.join(" AND "), *conditions.map{|c| c[1..-1] }.flatten]
#sites = Site.find :all,
:origin => [lat,lng],
:units => distance_unit,
:limit => limit,
:within => range,
:include => [:chargers, :site_reports, :networks],
:conditions => condition_set,
:order => 'distance asc'
I seem to be able to get this working fine when there are only single variables for the conditions expression but when I have something that is a (?) and has an array of values I'm getting an error for the wrong number of bind conditions. The way I'm joining and flattening the conditions (based on the answer from Combine arrays of conditions in Rails) seems not to handle an array properly and I don't understand the flattening logic enough to track down the issue.
So let's say I have 3 values in params[:site_categories] I'll the above code leaves me with the following:
Conditions is
[["sites.operational_status = ?", "operational"], ["sites.site_type in (?)", ["shopping", "food", "lodging"]]]
The flattened attempt is:
["sites.operational_status = ? AND sites.site_type in (?)", ["operational"], [["shopping", "food", "lodging"]]]
Which gives me:
wrong number of bind variables (4 for 2)
I'm going to step back and work on converting all of this to named scopes but I'd really like to understand how to get this working this way.
Rails 4
users = User.all
users = User.where(id: params[id]) if params[id].present?
users = User.where(state: states) if states.present?
users.each do |u|
puts u.name
end
Old answer
Monkey patch the Array class. Create a file called monkey_patch.rb in config/initializers directory.
class Array
def where(*args)
sql = args.first
unless (sql.is_a?(String) and sql.present?)
return self
end
self[0] = self.first.present? ? " #{self.first} AND #{sql} " : sql
self.concat(args[1..-1])
end
end
Now you can do this:
cond = []
cond.where("id = ?", params[id]) if params[id].present?
cond.where("state IN (?)", states) unless states.empty?
User.all(:conditions => cond)

Rails paginate array items one-by-one instead of page-by-page

I have a group of assets, let's call them "practitioners".
I'm displaying these practitioners in the header of a calendar interface.
There are 7 columns to the calendar. 7 columns = 7 practitioners per view/page.
Right now:
if the first page shows you practitioners 1-7, when you go the next page you will see practitioners 8-15, next page 16-23, etc. etc.
i am wondering how to page the practitioners so that if the first page shows you practitioners 1-7, the next page will show you practitioners 2-8, then 3-9, etc. etc.
i would greatly appreciate any help you can offer.
here is the rails code i am working with.
best regards,
harris novick
# get the default sort order
sort_order = RESOURCE_SORT_ORDER
# if we've been given asset ids, start our list with them
unless params[:asset_ids].blank?
params[:asset_ids] = params[:asset_ids].values unless params[:asset_ids].is_a?(Array)
sort_order = "#{params[:asset_ids].collect{|id| "service_provider_resources.id = #{id} DESC"}.join(",")}, #{sort_order}"
end
#asset_set = #provider.active_resources(:include => {:active_services => :latest_approved_version}).paginate(
:per_page => RESOURCES_IN_DAY_VIEW,
:page => params[:page],
:order => sort_order
)
Good question! I guess this is one thing WillPaginate doesn't really account for. I'm going by looking at WillPaginate's code here, but I didn't actually test this solution. If you intend to try it, let me know if it worked for you.
The logic is well separated, in WillPaginate::Collection. You need to change the behavior of the offset and total_entries= methods. You can do this with subclassing, but that means you can no longer use the special paginate finder, unfortunately. (It has WillPaginate::Collection hardcoded.)
You could have something like the following, perhaps in your lib/:
class SlidingWindowCollection < WillPaginate::Collection
def offset
current_page - 1
end
def total_entries=(number)
#total_entries = number.to_i
#total_pages = [#total_entries - per_page, 1].max
end
end
And then, your example code would look like:
#asset_set_scope = #provider.active_resources(:include => {:active_services => :latest_approved_version})
#asset_set = SlidingWindowCollection.create(params[:page], RESOURCES_IN_DAY_VIEW, #asset_set_scope.count) do |pager|
pager.replace(#asset_set_scope.all(:offset => pager.offset, :limit => pager.per_page, :order => sort_order))
end
Usage is a bit more complicated, I suppose. All the extra stuff is normally taken care of by the special finder paginate, such as figuring out the total number of entries and selecting the right entries. I suppose you could create a helper if it's something you intend to do often.
I think LIMIT will work for you. I don't know using pagination but you can try following
LIMIT params[:page], 7
Where params[:page] is number of the page,
So for page 1 it will show 7 rows from 1 i.e. 1-7
Smilarly,
for page 2 it will show 7 rows from 2 i.e. 2-8

rails - activerecord ... grab first result

I want to grab the most recent entry from a table. If I was just using sql, you could do
Select top 1 * from table ORDER BY EntryDate DESC
I'd like to know if there is a good active record way of doing this.
I could do something like:
table.find(:order => 'EntryDate DESC').first
But it seems like that would grab the entire result set, and then use ruby to select the first result. I'd like ActiveRecord to create sql that only brings across one result.
You need something like:
Model.first(:order => 'EntryDate DESC')
which is shorthand for
Model.find(:first, :order => 'EntryDate DESC')
Take a look at the documentation for first and find for details.
The Rails documentation seems to be pretty subjective in this instance. Note that .first is the same as find(:first, blah...)
From:http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002263
"Find first - This will return the first record matched by the options used. These options can either be specific conditions or merely an order. If no record can be matched, nil is returned. Use Model.find(:first, *args) or its shortcut Model.first(*args)."
Digging into the ActiveRecord code, at line 1533 of base.rb (as of 9/5/2009), we find:
def find_initial(options)
options.update(:limit => 1)
find_every(options).first
end
This calls find_every which has the following definition:
def find_every(options)
include_associations = merge_includes(scope(:find, :include), options[:include])
if include_associations.any? && references_eager_loaded_tables?(options)
records = find_with_associations(options)
else
records = find_by_sql(construct_finder_sql(options))
if include_associations.any?
preload_associations(records, include_associations)
end
end
records.each { |record| record.readonly! } if options[:readonly]
records
end
Since it's doing a records.each, I'm not sure if the :limit is just limiting how many records it's returning after the query is run, but it sure looks that way (without digging any further on my own). Seems you should probably just use raw SQL if you're worried about the performance hit on this.
Could just use find_by_sql http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002267
table.find_by_sql "Select top 1 * from table ORDER BY EntryDate DESC"

Rails, Get a random record when using :group

How do I get a random record when using :group?
#paintings = Painting.all(:group => "user_id", :order => "created_at DESC")
This gives me the latest painting for each user. Now I would like to select a random painting from each user instead of the latest. The order of the paintings should still be the same, so that the user that have been the most active will get his/her random painting displayed first.
painting150 (user1)
painting200 (user2)
painting231 (user3)
Is this possible?
Best regards.
Asbjørn Morell.
This answer is specific to Rails, but since you are using ActiveRecord, I am assuming it should be fine.
unique_paintings = []
#paintings.group_by(&:user_id).each do |user_id, paintings|
unique_paintings << paintings[rand(paintings.size-1)]
end
unique_paintings.sort_by(&:created_at)
The group_by most certainly messes up the created_at sort you did in the query, so I did a sort_by as the last step. You might want to get rid of it in the query since you'll have to do it anyway here.
#painting = #paintings[rand(#paintings.size-1)]
(or paintings.count, dont know the right method yet)
Assuming you have MySQL, you can try:
#paintings = Painting.all(:group => "user_id", :order => "RAND()")
you could do something like this but it will suffer as your number of records grow
#paintings = Painting.find(:all, :order => 'RAND()').map{ |i| i.user_id }.uniq

Resources