Grab only the latest comment in Rails - ruby-on-rails

In a typical User - Post - Comment model in Rails, every user can create a Post and also can create Comment, question is how to grab every user latest comment on specific post.
Example:
Post A have 3 user making comment
User 1 have comment 1, 2, 3, 4, 5, 6
User 2 have comment 1, 2, 3, 4
User 3 have comment 1, 2
So the view I want is just the latest comment for every user:
Post A have 3 user making comment
User 1 latest comment that is 6
User 2 latest comment that is 4
user 3 latest comment that is 2
How to do it ?
thanks

Something like this:
post.comments.for_user(current_user).last
add a named_scope in your model
class Comment
named_scope :for_user, lambda{ |user| {:conditions=>{:user_id => user.id}}
end
That should do the trick.
If you rather do it in rails,
messages_by_users = post.messages.group_by(&:user)
messages_by_users.each do |key, value|
messages_by_users[key] = value.last
end

I have had to get this kind of data and usually I end up doing two queries. In my case I have Blogs and their Posts and I wanted a list of the 3 most recent blog posts with the restriction that the blogs are unique, I dont want 2 posts from the same blog. I ended up doing something like this (MySQL):
q = <<-EOQ
SELECT id,pub_date FROM
(
SELECT id,blog_id,pub_date
FROM posts
ORDER BY pub_date DESC
LIMIT 40
)
t
GROUP BY blog_id
ORDER BY pub_date DESC
LIMIT #{num_posts}
EOQ
post_ids = Post.connection.select_values(q)
Post.find(:all, :include => [:blog], :conditions => ["id IN (?)", post_ids], :order => "posts.pub_date DESC")
So in your case you might have something like:
q = <<-EOQ
SELECT id FROM
(
SELECT id,post_id
FROM comments
ORDER BY id DESC
LIMIT 40
)
t
GROUP BY post_id
ORDER BY id DESC
LIMIT 10
EOQ
post_ids = Post.connection.select_values(q)
Post.find(:all, :include => [:blog], :conditions => ["id IN (?)", post_ids], :order => "posts.id DESC")

Assuming that your database is assigning sequential IDs to the comments, you can do this:
class Comment
named_scope :most_recent, lambda {
lastest_comments = Comment.maximum :id, :group => "user_id, post_id"
{ :conditions => [ "comment_id in ?", lastest_comments.map(&:last) ] }
}
end
This gives you a two-query method that you can use in a variety of ways. The named_scope above pulls back the most recent comments for all users on all posts. This might be a problem if your database is gigantic, but you can certainly add conditions to make it more specific.
As it stands, it is a flexible method that allows you to do the following:
Comment.most_recent.find_by_user #user #-> the most recent comments on all posts by a user
#user.comments.most_recent #-> same as above
Comment.most_recent.find_by_post #post #-> the most recent comments on a single post by all users
#post.comments.most_recent #-> same as above
Comment.most_recent.find_by_user_and_post #user, #post #-> the specific most recent comment by a certain user on a certain post
#post.comments.most_recent.find_by_user #user #-> you get the idea

Related

How to list top 10 school with active record - rails

I have two models: School and Review.
The School model looks like this: {ID, name, city, state}
The Review model looks like this: {ID, content, score, school_id}
How do I list the top ten schools based on the score from the review model?
I thought maybe a method in the school-model, with something like this:
class School < ActiveRecord::Base
def top_schools
#top_schools = School.limit(10)
...
end
end
And then loop them in a <li> list:
<div>
<ul>
<% #top_schools.each do |school| %>
<li>school.name</li>
<%end>
</ul>
</div>
But, I dont really know how to finish the top_schools method.
You should make an average of reviews of each school.
The SQL query if you are running with MySQL should be something like:
SELECT schools.* FROM schools
JOIN reviews ON reviews.school_id=schools.id
GROUP BY schools.id
ORDER BY AVG(reviews.score) DESC
LIMIT 10
Translated in Rails:
In your School model:
scope :by_score, :joins => :reviews, :group => "schools.id", :order => "AVG(reviews.score) DESC"
In your controller:
#top_schools = School.by_score.limit(10)
The choice not to include the limitation in scope, can be more flexible and allow the display of 5 or 15.
I have only tested MySQL request. I am not sure on my rails translation.
Assuming that each school only has one review ( has_one and belongs_to), you'd want to order the reviews first and then find the corresponding school:
Review.order('score DESC').first(10).each do |r|
School.find_by_id(r.school_id)
end
I would add a new field total_score to the schools table - set to 0 by default. And then add a callback in the Review model to calculate the total score for the school when a new review is added/updated to that school.
Then do this:
School.order("total_score DESC").limit(10)
Edit: I totally missed the reviews model in that answer.
School.all(:select => "schools.*, AVG(reviews.score) as avg_score",
:joins => :reviews,
:group => 'schools.id',
:order => 'avg_score desc',
:limit => 10)
But this will get slower as you add reviews. I like the total_score answer.

Combined order with association in Rails 3

I have two models, post and comment. Posts has many comments and comments belongs to posts.
What I'm trying to do, is to have a list of posts that is ordered by creation date, unless it has comments. Then it needs to take the creation date of the latest comment.
Now I know that I can include associations like this:
Post.find(:all, :include => [:comments], :order => "comments.created_at DESC, posts.created_at DESC")
But this will order all posts with comments first and then the posts without comments. I don't want either ones ordered separately, but combined.
The solution also needs to be compatible with a paginate gem like will_paginate.
Edit
I have it now working with the following code:
posts_controller.rb
#posts = Post.order('updated_at DESC').page(params[:page]).per_page(10)
comments_controller.rb
#post = Post.find(params[:post_id])
#post.update_attributes!(:updated_at => Time.now)
I'd recommend:
Add a last_commented_at date field to your Post model. Whenever someone comments on the post update that field. It de-normalizes your db structure a bit, but your query will be faster and more strait-forward.
Postgres
Post.paginate(:include => [:comments],
:order => "COALESCE(comments.created_at, posts.created_at)",
:page => 1, :per_page => 10)
MySQL
Post.paginate(:include => [:comments],
:order => "IFNULL(comments.created_at, posts.created_at)",
:page => 1, :per_page => 10)

Ruby .each block doesn't add up all objects

For some reason it seems like .each block iterate only once, when I write lets say 3 or 4 comments, #total_comments displays only 1, instead of 3 or 4.
The relationships are:
user has many posts
post has many comments
Can anybody help please?
controller
#total_comments = 0
#posts = current_user.posts
#posts.each do |post|
#comments = post.comments
#new_comments = #comments.count -#comments.where("created_at < ?",1.day.ago).count
#total_comments += #new_comments
end
view
<%= #total_comments %>
To get the count of all comments you want something like this
#total_comments = Comment.count(
:conditions => ["post.user_id = ? and comment.created_at >= ?",
current_user.id, 1.day.ago],
:include => :post
)
Although without knowing your data model it's difficult to say exactly.
As discussed with #m_x, a neater and more rails-3/ruby 1.9 esque version of above
#total_comments = Comment
.joins( post: :user )
.where( users: {id: current_user.id} )
.where( "created_at < ?", 1.day.ago )
.count
use this instead :
#total_comments = Comment
.joins( post: :user )
.where( users: {id: current_user.id} )
.where( "comments.created_at < ?", 1.day.ago )
.count
EDIT
i definitely need some sleep. as #Ryan pointed out this is much simpler :
#total_comments = current_user.comments.where( "created_at < ?", 1.day.ago ).count
... but the OP has to add this to his User model :
has_many :comments, through: :posts
It looks like you forget to save you comments - some comments are not saved to database that's why you're loosing some of comments.
Also you could use autosave option to automatically save your variables every time when you're changing them.

Rails Arel selecting distinct columns

I've hit a slight block with the new scope methods (Arel 0.4.0, Rails 3.0.0.rc)
Basically I have:
A topics model, which has_many :comments, and a comments model (with a topic_id column) which belongs_to :topics.
I'm trying to fetch a collection of "Hot Topics", i.e. the topics that were most recently commented on. Current code is as follows:
# models/comment.rb
scope :recent, order("comments.created_at DESC")
# models/topic.rb
scope :hot, joins(:comments) & Comment.recent & limit(5)
If I execute Topic.hot.to_sql, the following query is fired:
SELECT "topics".* FROM "topics" INNER JOIN "comments"
ON "comments"."topic_id" = "topics"."id"
ORDER BY comments.created_at DESC LIMIT 5
This works fine, but it potentially returns duplicate topics - If topic #3 was recently commented on several times, it would be returned several times.
My question
How would I go about returning a distinct set of topics, bearing in mind that I still need to access the comments.created_at field, to display how long ago the last post was? I would imagine something along the lines of distinct or group_by, but I'm not too sure how best to go about it.
Any advice / suggestions are much appreciated - I've added a 100 rep bounty in hopes of coming to an elegant solution soon.
Solution 1
This doesn't use Arel, but Rails 2.x syntax:
Topic.all(:select => "topics.*, C.id AS last_comment_id,
C.created_at AS last_comment_at",
:joins => "JOINS (
SELECT DISTINCT A.id, A.topic_id, B.created_at
FROM messages A,
(
SELECT topic_id, max(created_at) AS created_at
FROM comments
GROUP BY topic_id
ORDER BY created_at
LIMIT 5
) B
WHERE A.user_id = B.user_id AND
A.created_at = B.created_at
) AS C ON topics.id = C.topic_id
"
).each do |topic|
p "topic id: #{topic.id}"
p "last comment id: #{topic.last_comment_id}"
p "last comment at: #{topic.last_comment_at}"
end
Make sure you index the created_at and topic_id column in the comments table.
Solution 2
Add a last_comment_id column in your Topic model. Update the last_comment_id after creating a comment. This approach is much faster than using complex SQL to determine the last comment.
E.g:
class Topic < ActiveRecord::Base
has_many :comments
belongs_to :last_comment, :class_name => "Comment"
scope :hot, joins(:last_comment).order("comments.created_at DESC").limit(5)
end
class Comment
belongs_to :topic
after_create :update_topic
def update_topic
topic.last_comment = self
topic.save
# OR better still
# topic.update_attribute(:last_comment_id, id)
end
end
This is much efficient than running a complex SQL query to determine the hot topics.
This is not that elegant in most SQL implementations. One way is to first get the list of the five most recent comments grouped by topic_id. Then get the comments.created_at by sub selecting with the IN clause.
I'm very new to Arel but something like this could work
recent_unique_comments = Comment.group(c[:topic_id]) \
.order('comments.created_at DESC') \
.limit(5) \
.project(comments[:topic_id]
recent_topics = Topic.where(t[:topic_id].in(recent_unique_comments))
# Another experiment (there has to be another way...)
recent_comments = Comment.join(Topic) \
.on(Comment[:topic_id].eq(Topic[:topic_id])) \
.where(t[:topic_id].in(recent_unique_comments)) \
.order('comments.topic_id, comments.created_at DESC') \
.group_by(&:topic_id).to_a.map{|hsh| hsh[1][0]}
In order to accomplish this you need to have a scope with a GROUP BY to get the latest comment for each topic. You can then order this scope by created_at to get the most recent commented on topics.
The following works for me using sqlite
class Comment < ActiveRecord::Base
belongs_to :topic
scope :recent, order("comments.created_at DESC")
scope :latest_by_topic, group("comments.topic_id").order("comments.created_at DESC")
end
class Topic < ActiveRecord::Base
has_many :comments
scope :hot, joins(:comments) & Comment.latest_by_topic & limit(5)
end
I used the following seeds.rb to generate the test data
(1..10).each do |t|
topic = Topic.new
(1..10).each do |c|
topic.comments.build(:subject => "Comment #{c} for topic #{t}")
end
topic.save
end
And the following are the test results
ruby-1.9.2-p0 > Topic.hot.map(&:id)
=> [10, 9, 8, 7, 6]
ruby-1.9.2-p0 > Topic.first.comments.create(:subject => 'Topic 1 - New comment')
=> #<Comment id: 101, subject: "Topic 1 - New comment", topic_id: 1, content: nil, created_at: "2010-08-26 10:53:34", updated_at: "2010-08-26 10:53:34">
ruby-1.9.2-p0 > Topic.hot.map(&:id)
=> [1, 10, 9, 8, 7]
ruby-1.9.2-p0 >
The SQL generated for sqlite(reformatted) is extremely simple and I hope Arel would render different SQL for other engines as this would certainly fail in many DB engines as the columns within Topic are not in the "Group by list". If this did present a problem then you could probably overcome it by limiting the selected columns to just comments.topic_id
puts Topic.hot.to_sql
SELECT "topics".*
FROM "topics"
INNER JOIN "comments" ON "comments"."topic_id" = "topics"."id"
GROUP BY comments.topic_id
ORDER BY comments.created_at DESC LIMIT 5
Since the question was about Arel, I thought I'd add this in, since Rails 3.2.1 adds uniq to the QueryMethods:
If you add .uniq to the Arel it adds DISTINCT to the select statement.
e.g. Topic.hot.uniq
Also works in scope:
e.g. scope :hot, joins(:comments).order("comments.created_at DESC").limit(5).uniq
So I would assume that
scope :hot, joins(:comments) & Comment.recent & limit(5) & uniq
should also probably work.
See http://apidock.com/rails/ActiveRecord/QueryMethods/uniq

RoR top five ratings

I have a model that has ratings in it for an post.
I want to display a list of the top 5 ratings for a given post, but I am completely lost on where to start. I figure the find method might have something useful, but I'm unsure. I've even considered looping through each record getting its size, adding it to a hash, sorting the hash, etc., but that seems too complicated.
Does anyone know how I might accomplish something like this?
Thank you
Edit: I found this to get all the posts that have the rating of agree:
Post.find(:all, :include => :post_ratings, :condtions => ['post_ratings.agree = ?', true])
The only problem is I can't figure out how to get the top five ratings from this query.
Might be worth giving a little more of an example of the code you're working with but I'll answer with a few assumptions.
If you have:
class Post
has_many :post_ratings
end
class PostRating
belongs_to :post
# Has a 'rating' attribute
end
You can find the top five post ratings with:
p = Post.find(:first) # For example
p.post_ratings.find(:all, :limit => 5, :order => 'rating desc')
To get the top five post ratings overall you can do:
PostRating.find(:all, :limit => 5, :order => 'rating desc')
UPDATE:
Following your edit it seems you have an 'agree' and a 'disagree' column. Not sure how that works in combination so I'll stick with the 'agree' column. What you'll need to do is count the ratings with agree flagged. Something like:
count_hsh PostRating.count(:group => 'post_id',
:order => 'count(*) desc',
:conditions => { :agree => true },
:limit => 5)
This will return you a hash mapping the post id to the count of agree ratings. You can then use that post_id to locate the posts themselves. The ratings are provided by the counts so the individual ratings are (I think) of no use though you could access them by calling post.post_ratings.
So, to get the top five posts:
#top_five_posts = []
count_hsh.each_pair do |post_id, ratings|
p = Post.find(post_id)
p[:rating_count] = ratings
#top_five_posts << p
end
This is probably more verbose than it could be but is hopefully illustrative. The p[:rating_count] is a virtual attribute which isn't in the database but will allow you to access the .rating_count method on the posts in your view if you wish to.
Assuming the same Post and PostRating from Shadwell's answer:
class Post
has_many :post_ratings
end
class PostRating
belongs_to :post
# Has a 'rating' attribute
end
To get the top five ratings for all Posts:
post_ratings.find(:all, :limit => 5, :order => 'rating desc')
To get the top five ratings for a specific Post you can:
p = Post.find(:first)
post_ratings.find_all_by_post_id(p.id, :limit => 5, :order => 'rating desc')
To find all posts sorted by average rating, you can use ActiveRecord::Calculations.
PostRating.average(:rating, :group => :post_id, :include => :post, :order => 'average desc')

Resources