I have two models: posts and comments. Each comment belongs to a post. I would like to have a page of all comments not just the comments for a post. I can't seem to get this seemingly simple thing to work.
Here is my controller:
def top
#topcomments = #comments.order("created_at desc")
end
I am getting an 'undefined method order' error.
If you want to access comments directly, and not through a relationship with another model, you need to access the model itself, Comment:
def top
#topcomments = Comment.order('created_at desc')
end
how would you get the post for each comment
Assuming you have a relationship set up correctly between comments and posts, you would just access .post for each comment. You can use includes(:post) to avoid the n+1 problem.
def top
#topcomments = Comment.order('created_at desc').includes(:post)
#topcomments.each |comment|
comment.post # here is the post
end
end
def top
#topcomments = Comment.order("created_at desc")
end
Related
This is an optimization question for an existing application, I've made the code generic to both make it annonmous and also easier to understand, instead of our proprietary models I'm describing a Forum discussion type situation. I've modified all this code for this example and not tested it, so if there are any typos I apologize, I'll try to fix them if they are pointed out to me.
Lets say I have a rails app with four models: Event, User, Forum, and Post.
the important relationships are as follows:
User has many events.
Forum has many posts.
Post has many events.
The front end is a single page javascript app, so all database data needs to be returned in json format.
Context:
when a User clicks on a post, an event is created with the name
'Show' which marks the post as no longer new.
The user needs to be
logged in to see which posts are new clicking on a forum calls the
following endpoint:
There are multiple users so the events able is a many to many relationship between posts and users.
example.com/forum/15/all_posts
heres the relevant code:
Forum Controller:
#forums_controller.rb
def all_posts
current_user = User.find(session[:user_id])
forum = Forum.includes(:posts).where(id: params[:id]).take
forum.posts.each do |post|
post.current_user = current_user
end
render json: forum.to_json(
include: [
{ posts: {
methods: [:is_new]
}}
]
)
end
Posts model:
#post.rb (posts model)
has_many :events
attr_accessor :current_user
def is_new
if current_user #user may not be logged in
!!self.events.where(user_id: current_user.id, name: 'Show').take
else
false
end
end
the model is where the action is at, so we've tried to keep logic out of the controller, but since the session is not available in the model we end up with this crazy work around of adding current_user as an attr_accessor so that methods can return the appropriate data for the user in question.... I don't like this but I've never come up with a better way to do it. We've repeated this pattern elsewhere and I would love to hear alternatives.
Here's my problem:
The call to is_new is used on the front end to determine what posts to hi-light but it's also triggering an n+1 scenario If there are 10 posts, this endpoint would net me a total 12 queries which is no good if my events table is huge. If I moved all the logic to the controller I could probably do this in 2 queries.
in short I have two questions:
MOST IMPORTANT: How can I fix this n+1 situation?
Is there a better way in general? I don't like needing an each loop before calling to_json I don't find this pattern to be elegant or easy to understand. at the same time I don't want to move all the code into the controller. What is the rails way to do this?
If working with scope is an option, I will try something like:
class Post < ApplicationRecord
scope :is_new, -> { where(user_id: current_user.id, name: 'Show') } if current_user.id?
end
If is a better option to send the current_user in your case, you can also do it:
class Post < ApplicationRecord
scope :is_new, ->(current_user) {...}
end
This is just pseudo-code to give an example:
First Answer
When I posted this I forgot you are rendering json from ForumsController.
Post
scope :for_user, -> (user = nil) do
includes(events: :users).where(users: {id: user.id}) if user
end
def is_new_for_user?(user = nil)
return true if user.nil?
self.events.empty?{ |e| e.name == 'Show' }
end
PostController
def index
#posts = Post.for_user(current_user)
end
posts/index.html.erb
...
<% if post.is_new_for_user?(current_user) %>
...
<% end
...
Second Answer
This is still pseudo-code. I didn't test anything.
Forum
scope :for_user, -> (user = nil) do
if user
includes(posts: [events: :users]).where(users: {id: user.id})
else
includes(:posts)
end
end
ForumsController
def all_posts
current_user = User.find(session[:user_id])
forum = Forum.for_user(current_user).where(id: params[:id]).take
render json: forum.to_json(
include: [
{ posts: {
methods: [:is_new_for_user?(current_user)]
}}
]
)
end
I need to display post with the latest made comment in it, on top of page. And every Post has many comments and many Comments belong to one post.
Here is my index method from Posts controller
def index
#posts = Post.all.order("posts.created_at desc")
def new
#post = Post.new
end
end
I looked through Rails docs and found .order and .where methods, and I think those two methods are the solution to my problem but I am not sure how to use it
try this:
#posts = Post.joins(:comments).order("comments.created_at DESC")
def index
#posts = Post.all.order("created_at DESC")
#latest_post = #posts.first
end
update
I interpreted word comments as posts.
update 2
First you need to find the latest comment:
#latest_comment = Comment.all.order('created_at DESC').first
Having that, you can extract the ID of the Post to which this comment belong:
post_id = latest_comment.post_id
Now you have the latest comment and the respective post's id. I would modify index page like this:
def index
#posts = Post.all.order("created_at DESC")
#latest_comment = Comment.all.order('created_at DESC').first
#post_of_latest_comment = Post.find(#latest_comment.post_id)
end
I am not sure what do you mean by displaying it on top, but I am pretty sure with this code you can do it in your view.
update 3
In your view, you hsould have something like this:
<h1>Top comment</h1>
<%= #latest_comment.text %>
By .textI mean some attribute of model Comment wich contains the content of the comment, plain text. If you need more help with this, show what atributes your Post and Coment model have.
I tried a modified line from Pitabas Prathal:
Post.joins(:comments).order("comments.created_at DESC").group('post_id')
where post_id is the foreign key in the Comment model from the Post model. It worked fine for me :)
I'm trying to include a few other recent articles when someone views a particular article in my Rails app.
I have the following method in my controller:
def show
#article = Article.find(params[:id])
#recents = Article.where(!#article).order("created_at DESC").limit(4).offset(1)
end
As the expert eye might see, #recents isn't correct. It's my best guess. :)
How do I show some recent articles but not repeat the one they are currently viewing?
You should use a scope in the model, for it has a lot of advanteges. Learn about scopes here. Your case should be something like this:
In the model:
class Article < ActiveRecord::Base
scope :recent, ->(article_id) { where.not(id: article_id).order(created_at: :desc).limit(4) }
end
and in the controller:
def show
#article = Article.find(params[:id])
#recent = Article.recent(#article.id)
end
This way the recent scope will always get the four last articles leaving out the article you pass in as an argument. And scopes are chainable so you could do something like this as well:
def some_action
#user = User.find(params[:user_id])
#user_recent_articles = #user.articles.recent(0)
end
You are getting the user recent articles. I pass a zero because the scope asks for an argument. You could create a different scope if you want to do it the cleanest way.
This, assuming a user has_many articles.
Well, hope it helps!
try with #recents = Article.where.not(id: #article.id).order("created_at DESC").limit(4)
click here - section 2.4 :).
I think there is a way of only making one call instead of 2 the way you have it now.
I am building an app that allows users to post. Those posts can be upvoted and downvoted. Each post record keeps track of upvotes:integer and downvotes:integer. I want to be able to order the records by which has the most upvotes total (in other words: upvotes-downvotes). I have absolutely no idea how to do this because I do not quite understand how Class methods interact with the object they are called on. This is my attempt:
My controller:
def index
#posts = Post.find(:all).most_votes.order(vote_difference)
end
My Post.rb Model:
def self.most_votes
vote_difference = (upvotes-downvotes)
end
Any ideas on how to do this?
Turns out you can actually insert the calculation right into the .order() value:
#posts = Post.find(:all).order('upvotes + downvotes')
I have a User and Post scaffolded in my rails application. I wanted to get all of the posts associated with a user. For example, I want to be able to do something like this:
localhost:3000/users/1/posts
And it should give me all of the posts with the user_id = 1. I know how to get all the posts via the rails console for a specific user. I was just wondering how i can accomplish something like the example above. Would I have to add another method in the users controller?
You can do it without adding new action. Like this:
class PostsController < ApplicationController
def index
#user = User.find(params[:user_id])
#posts = #user.posts
end
end
another one line example is the following, add this to your index
#posts = User.find(params[:user_id]).posts
I wrote a longish answer to a related question that may be helpful:
https://stackoverflow.com/a/17911920/321583