What's the point of nested resources in Rails? - ruby-on-rails

Say I have the prototypical blog app in Rails. I would have a Post model which has many Comments. My routes.rb might look like this:
resources :post do
resources :comment
end
This means that for instance the edit path for a comment looks like this: /post/21/comment/42/edit.
It appears to make sense when we have a has many/belongs to relationship between two models.
However, once you notice that the id of the post is not really needed to find the comment (or even the post), it starts to make less sense.
To see what I mean, consider these two equivalent implementations of the edit action in the controller:
# Nested resource version
def edit
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
# ...
end
vs.
# Un-nested resource version
def edit
#comment = Comment.find(params[:id])
#post = #comment.post
# ...
end
My question is: Is there a use case for this I haven't considered? Or are nested resources only good for making pretty URLs?

Related

How to fit a form into a rails show after another form? Multiple same forms

I am making this form for a reply function in a blog-like app with the same recipe as the comment in which it should nest. (Comment recipe)
I get the following error when I try to view my app in the browser:
No route matches {:action=>"index", :post_id=>"10", :controller=>"replies", :comment_id=>nil} missing required keys: [:comment_id]
This is my reply view file for my replies/_form.html.haml
%h5 Reply
= form_for [ #post, #comment, #reply ] do |f|
%p
= f.label :name
%br
= f.text_field :name
%p
= f.label :talk
%br
= f.text_area :talk
%p
= f.submit 'Submit'
This is my replies controller getting a hold on the comments_id much like the recipe said I should do between the comments and the post_id:
class RepliesController < ApplicationController
def create
#reply = Reply.new(reply_params)
#reply.comment_id = params[:comment_id]
#reply.save
redirect_to post_path(#reply.comment.post)
end
end
And this is my id passing in the comments show controller as it is similar in the post show controller. Or should I add something more to the post show controller now?
def show
#reply = Reply.new
#reply.comment_id = #comment.id
end
I tried adding replies though the rails console. They show up neatly, so I think my routes file works. Something with the id's and the handling of the collections isn't going great though. The form part is not working.
I don't like adding gems if I don't have to, I want to understand my app.
EDIT: I should probably add that my app has a view that looks like a indexing-form system in a indexing-form system.
Below the post there are comments - with a form, and below these there are replies - with a form.
EDIT 2: First argument in form cannot contain nil or be empty is what I get now al the time.
EDIT 3: I still can't make my forms under the comments. This is my routes file, maybe it clarifies.
resources :posts do
resources :comments do
resources :replies do
end
end
end
I am debugging now by making print outs and found out that in my _form haml file for a reply rails can find the #post, but not the comment nor replies (but they have to be created of course with the form), while I can get almost exactly the same structure to work in my _form for a comment.
Is it even possible in Rails to have multiple forms printed out on the same page?
Still all help is appreciated!
EDIT 4: I have gotten a little further. Now Rails says:
No route matches {:action=>"index", :post_id=>"2", :controller=>"replies", :comment_id=>nil} missing required keys: [:comment_id]
As a direct effect of changing my show action in the post controller:
def show
#post = Post.find(params[:id])
## create a blank comment
#comment = Phase.new
#comment.post_id = #post.id
##
## The same for a blank reply
#reply = Reply.new
#reply.comment_id = #comment.id
##
end
The last line of this action seems to not make any difference. Now I just need to be able to grab this comment_id in the reply form and then I am done. Nearly a week of struggle.
I believe you may not be getting a #comment.id because you are doing Comment.new(reply_params) and objects only get id fields whens you use .create OR .save after instantiating with new. Let me know if this helps!
Because of this you are getting the nil error for #comment in the form.
EDIT
It seems like you are looking to get the comment_id from the form, another solution would be to get it from the URL by having this happen under comments path. You'd have to nest your resources so it's something like comments/:id/reply/:reply_id or comments/:id/reply/new.
Here's some great examples for nested attributes: http://guides.rubyonrails.org/routing.html#nested-resources
SECOND EDIT
Okay, I was a bit confused because typically you would have the form in new not in show. Either way I would be looking at the instance variables being passed in. How do your associations look?
if it's Post has_many :comments and Comment has_many :replies, then you can do something like this in your show actions:
def new
#post = Post.find(params[:post_id]
#comment = #post.comments.find(params[:comment_id]) #scopes your search to only the comments belonging to this post
#reply = #comment.replies.new
end
The more important point to get out of this is that each instance variable needs to be passed in. that being said, I'm not sure which ones are currently working for you
As far as create you should be doing something similar, creating through the associations. This rails magic handles all the foreign keys and assigning the id's etc:
def create
#post = Post.find(params[:post_id]
#comment = Comment.find(params[:comment_id])
#reply = #comment.replies.new(reply_params)
if #reply.save
# some logic
redirect_to wherever_path
else
# other logic
render :new
end
end
The more important point to get out of this is that each instance variable needs to be passed in. that being said, I'm not sure which ones are currently working for you
Let me know how this works!

Form for 3 association models

I am still a newbie so please forgive this silly question. I got 3 models:
- User (generated by devise), Comment and Post
User has many posts and comments
Comment belongs to both post and user
Post has many comments and belongs to user
My routes.rb
resources :users do
resources :posts do
resources :comments
end
end
My form code:
<%= form_for([#user,#post,#comment]) do |f| %>
...
<% end %>
I want to generate to user_post_comments_path but the above form_for generate to post_comments_path. Why? Did I misunderstand something. Thanks alot
The Rails routing and Form handling is very confusing and I still get it wrong all the time...
I think that your problem is that some of your variables is not set (nil) and so rails can not determine what you are actually up to.
I would also like to recommend to you that you don't nest your routes like that unless you have to for the sake of the urls.
It's usually enough to nest one level deep and use the current_user to assign to models when creating them. This also reduces the security risk involved when posting ids of other users:
def create
#post = current_user.posts.build(post_params)
[...]
end
def create
#comment = current_user.comments.build(comment_params)
#comment.post = Post.find params[:post_id]
[...]
end

rails respond_with associated record

for example, there's a Question model that has_many :answers
I'd like to show a form for new answer on the questions#show ( just like stackoverflow does )
routes would be something like:
resources :questions do
resources :answers
end
What is the best way to show the errors for an invalid record under these circumstances?
The problem is that I can't render an action I need from within AnswersController ( since it would be questions#show ), the only way seem to be redirect_to and store errors in flash hash.
It just looks like a pretty much common scenario, I believe there should be some better way
This may be one of a few cases where it's actually justified to add a new resourceful member route to your QuestionsController:
resources :questions do
post 'answer', :on => :member
end
which would recognize question/:id/answer with a POST request routed to questions#answer, allowing you keep all the logic in one controller:
class QuestionsController < ApplicationController
...
def show
#question = Question.find(params[:id])
end
def answer
#question = Question.find(params[:id])
#answer = #question.answers.build(params[:question][:answer])
if #answer.save
# show question with newly posted answer at url /question/:id
redirect_to #question
else
# show question with invalid editable answer at url /question/:id/answer
render 'show'
end
end
...
end
Explanation: In my opinion, the decision to handle the logic in one controller as opposed to two comes down to what you consider to be the resource of interest. Normally, you would consider each model to represent a distinct resource and thus create a separate controller to handle actions related to each resource. However, when there are multiple deeply coupled models for which multiple actions (e.g. show, new, create) are handled in a single view, it might be cleaner to think of the models as forming a single resource.
In this example, I think of the resource as a collective one consisting of both the question and its answers. Since this collective resource is uniquely identified by the question itself, I would let the question controller handle it. The show action in the questions controller already involves retrieving the collective question-answers resource, so you might think of the answer action (and potentially unanswer and reanswer actions) as the analogue of update to that collective resource.
Thinking of the resources this way is largely a matter of design preference, and there will be trade-offs depending on the requirements.
You can render questions#show from the AnswersController like this:
render :template => 'questions/show'
If you want to jump to a specific anchor on the page, you have to define that in your answer form:
<%= form_for(#answer, :url => question_answers_url(#question, :anchor => 'answer_form')) do |f| -%>
Have a partial which has the form which posts data to the answers_controller#create action.
So, in your questions#show page - show.html.erb, render a partial
<%= render :partial => "answers/answer_form" %>
In the _answer_form.html.erb, have a form which will post data to answers#create
<% form_for #answer do |f| %>
# have a text area
<% end %>
As far as the flash is concerned
In the AsnwersController
def create
#question = Question.find(params[:question_id])
#answer = Answer.build(params)
if #answer.save
flash[:notice] = "Answer was posted successfully"
else
flash[:error] = "There were a few errors please try again"
end
redirect_to question_path(#question)
end

Rails: Forum has_many topics, saving the association?

I might be going about this the wrong way in the first place, so I will give a bit of background first.
As you can tell from the title, I am building a forum from scratch. I thought it was working correctly; however, I am a bit unsure as to how to update/save the forum object from within the topics "create" method in it's controller.
What I tried to do:
In the "New" method, I sent the Forum's id via the routing. So on the new-topic page has a address that looks like this: "localhost:3000/new-topic/1". The one being the Forum's id. In the method itself, I try to attach it to the new topic object.
#topic = Topic.new
#topic.forum = Forum.find(params[:id])
My create method then tries to use that forum.
#topic = Topic.new(params[:topic])
#topic.forum.topics << #topic #Simplified down.
if #topic.save
#topic.forum.save
...
I get the feeling that I am going about this the wrong way. I was looking at someone's tutorial and they got the forum by calling params[:forum_id] but they didn't show they routing they did to achieve that.
How do I do this correctly and/or what is the correct way to route all of this? For the record, I do plan on using this same method for the Topic => Post association. Thanks for any help.
You should use nested REST routes:
# routes.rb
resources :forums do
resources :topics
end
this will result in the following routes:
GET /forums/:forum_id/topics/new # displays the form
POST /forums/:forum_id/topics # creates the topic
and in controller you should use builders, they have several advantages like security, scope preserving etc.:
def new
#forum = Forum.find(params[:forum_id])
#topic = #forum.topics.build
def create
#forum = Forum.find(params[:forum_id])
#topic = #forum.topics.build(params[:topic])
if #topic.save
...
http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Resources.html#method-i-resources

Best practice for creating a model with two belongs_to associations?

This is a consistency problem that I'm running into often.
Let's consider a typical Forum:
User can create Posts
Posts belong to a Topic
Posts also belong to the User that created them
What's the best practice for choosing between these two options:
# Initialize #post on the User
def create
#post = current_user.posts.build(params[:post])
#post.topic_id = #topic.id
if #post.save
...
end
end
Or
# Initialize #post on the Topic
def create
#post = #topic.posts.build(params[:post])
#post.user_id = current_user.id
if #post.save
...
end
end
Or is there a better way, considering that, in the above examples, either #post's user_id or topic_id would have to be added to attr_accesssible (feels hacky)?
The cleanest approach I managed to find is using CanCan: when having a rule can :create, Post, :user_id => user.id and adding load_resource in your controller it will set the attributes.
But it is not always suitable. It would be nice to have some generic solution to initalize nested objects in one shot.
Update. I've come up with another option:
#post = #topic.posts.where(user_id: current_user.id).build(params[:post])
Generally speaking, all of these approaches break the Law of Demeter. It would be better to encapsulate in a method of the model, like this:
class Topic < ActiveRecord::Base
def new_post(params={}, author=nil)
posts.build(params).tap {|p| p.user = author}
end
end
Then in controller:
#post = #topic.new_post(params[:post], current_user)
You never need to monkey with IDs or attr_accessible. If a User has_many Posts and a Topic has_many Posts than you can do
# Initialize #post on the User
def create
#post = current_user.posts.build(params[:post])
#post.topic = #topic #assuming you've gotten the topic from somewhere
if #post.save
...
end
end
There really isn't a big difference in building from the user or from the topic, but going from the user seems more natural to me.
I prefer
#post = #topic.posts.build(params[:post])
#post.user = current_user
Although I dont see any problem with the other approach, building post via topic make more natural to me(as posts are mostly displayed in the context of its topic rather than the user itself).

Resources