Parameter in find method - ruby-on-rails

I'm following a guide to Ruby on Rails and there is something I don't understand. I have this model called Comment which belongs_to another two models, called User and Book.
This model's controller, Comments has the following create action:
def create
book = Book.find(params[:comment][:book_id])
comment = book.comments.build(comment_params)
comment.user = current_user
if comment.save
redirect_to comment.book
end
end
comment_params is just this:
def comment_params
params.require(:comment).permit(:body)
end
This create action is called when clicking the "Submit" button from this form, which is a partial called _comments located in the books view folder and rendered in the books' show action:
<%= form_for #book.comments.build do |f| %>
<%= f.hidden_field :book_id, value: #book.id %>
<%= f.text_area :body %>
<%= f.submit "Submit" %>
<% end %>
#book is indeed defined in books#show.
I don't understand why I have to pass the [:comment] parameter to the Book.find method in order to find the book. I thought just the [:book_id] would suffice.

You're not actually passing the :comment parameter but rather accessing the :book_id that's nested in the :comment hash. Your params look something like:
{
:comment => {
:book_id => 1
}
}
If you simply passed params[:book_id] you would get back nil.

If book_id is a field in comments table you don't need to retrieve the book. Just do
def create
comment = Comment.new(comment_params)
comment.user = current_user
if comment.save
redirect_to comment.book
end
end
Also, if the User model has a comments association, and in this action you are sure that a current_user is set, you can do
def create
comment = current_user.comments.build(comment_params)
if comment.save
redirect_to comment.book
end
end

Related

undefined method `commentable' for #<ActiveRecord::Associations::CollectionProxy []> (accesing commentable id and type via polymorphic association)

I tried to fix this issue for a day but cannot find solution.
What I am doing now is to render the comment form
<%= form_for #comment, remote: true do |form|%>
<%= form.hidden_field :question_id, value: #question.id%>
<%= form.hidden_field :commentable_id, :value => #question.comments.commentable.id %>
<%= form.hidden_field :commentable_type, :value => #question.comments.commentable.class.name %>
<% end %>
I just dont know how to access the commetable_id and type that was created with the comment controller to record the id of the comment and the type of the commenting model (e.g. client or lawyer)
# GET /questions/1 or /questions/1.json
def show
set_question
#comment = Comment.new
#comments = Comment.all
end
# GET /questions/1 or /questions/1.json
def show
set_question
#comment = Comment.new
#comments = Comment.all
end
resources :questions do
resources :comments do
end
end
As the error message says, you can't call commentable on #question.comments - an association. Pass an array to the form_for method with the commentable (Question) object and the comment object. You don't need to set any hidden fields.
<%= form_for [#question, #comment] do |f| %>
<div><%= f.label :content %></div>
<div><%= f.text_area :content %></div>
<div><%= f.submit 'Post' %></div>
<% end %>
Replace content with the field where you're storing the comment's content.
This should generate a form tag with an action attribute of /questions/1/comments which upon submission is processed by CommentsController#create.
class CommentsController < ApplicationController
before_action :set_commentable
def create
#comment = #commentable.comments.new(comment_params)
if #comment.save
redirect_to #commentable, notice: 'Comment created'
else
render :new
end
end
private
def set_commentable
# e.g. request.path => '/questions/1/comments'
resource, id = request.path.split('/')[1, 2] # ['questions', '1']
#commentable = resource.singularize.classify.constantize.find(id)
end
def comment_params
params.require(:comment).permit(:content)
end
end
In the set_commentable method, the commentable type and its id are detected from the request path. Since resource is 'questions', resource.singularize.classify.constantize returns the Question model. The commentable object is then found using the find method. The CommentsController#create method creates the comment and redirects to the commentable object which is the question show page (/questions/:id). If there's an error, it renders the new view (you have to create views/comments/new.html.erb to render the form with errors).
You are using question.comments which indicates has many relation, So it will return you an active record association array, So if you want to find the commentable id, you need to take single record from the array. For example if you want first comment commentable id, then use
#question.comments.first.commentable.id
If each comment has different commentable id, you need to iterate through loop.
If you have some factor to apply condition, then apply the condition
#question.comments.where('your condition').first.commentable.id
If you use above code, still you will get the array, so I have used .first. Use a uniq value in where condition, so you will have only single record with that value.

Passing validation errors array from one controller to another

I have Comment belongs_to Post and Post has_many Comments, the comment model as following:
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
validates :text, presence: true
end
The form which adds new comments is located in Posts show view, as following:
<%= form_with(model: [ #post, #post.comments.build ], local: true) do |form| %>
<% if #comment.errors.any?%>
<div id="error_explanation">
<ul>
<% #comment.errors.messages.values.each do |msg| %>
<%msg.each do |m| %>
<li><%= m %></li>
<%end %>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= form.text_area :text , {placeholder: true}%>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
The Comments create action, as following:
class CommentsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#comment = Comment.new(comment_params)
#comment.post_id = params[:post_id]
#comment.user_id = current_user.id
if #comment.save
redirect_to post_path(#post)
else
render 'posts/show'
end
end
private
def comment_params
params.require(:comment).permit(:text)
end
end
I need to render the posts/show page to show Comment validation errors, but the problem is I'm in CommentsController controller not PostsController so all objects used in pages/show view will be null.
How can I pass the #comment object to pages/show?
I thought about using flash array, but I'm looking for more conventional way.
Yep, rendering pages/show will just use the view template, but not the action, so anything you define in the pages#show controller action won't be available. #comment will be available though as you define that in the comments#create action. Without seeing the PostsController I don't know what else you're loading in the pages#show action - you could consider moving anything required in both to a method on ApplicationController, then calling from both places. Another option would be to change your commenting process to work via AJAX (remote: true instead of local: true on the form), and responding with JS designed to re-render just the comment form (you can move it into a partial used both in pages/show.html.erb and the comments#create response).
Couple of other notes on your code above - in comments#create, you can use:
#comment = #post.comments.new(comment_params)
to avoid needing to set the post_id on #comment manually.
For the form, I'd be tempted to setup a new comment in pages#show:
#comment = #post.comments.build
And then reference that in the form, it'll make it easier if you do re-use that between pages#show and comments#create:
<%= form_with(model: [ #post, #comment ], local: true) do |form| %>
Hope that helps!

Getting an RecordNotFound exception when trying to find object by foreign_key

There are posts and comments form for each.
I'm trying to add a comment to every post through the form. It's all happening on the same page.
View file code:
<% #posts.each do |post| %>
<%=post.title%>
<%=post.text%>
<%post.comments.each do |com|%>
<h3> <%=com.content%> </h3>
<%end%>
<%= form_for post.comments.build do |f| %>
<p>comments:</p>
<%= f.text_area :content, size: "12x12" %>
<%=f.submit%>
<% end %>
<% end %>
Comments controller code:
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.build(comment_params)
#comment.save
redirect_to root_path
end
It seems that program can not access to :post_id.
I have all associations in my models and :post_id in my db schema.
Github link for this app
You need to add <%= f.hidden_field :post_id %> in your form and permit :post_id in comment_params.
Also, you may want to reduce create method code to one line.
def create
Comment.create(comment_params)
redirect_to root_path
end
You need to permit :post_id for your strong parameters in the comments controller:
def comment_params
params.require(:comment).permit(:content, :post_id)
end
I found a problem.
The mistake is in searching by params[:post_id], while i need to by [:comment][:post_id] after adding the hidden_field

Best practice in creating belongs_to object

Let's say we have the following situation:
class User < ActiveRecord::Base
has_many :tickets
end
class Ticket < ActiveRecord::Base
belongs_to :user
end
For simplicity let's say Ticket has only some text field description and integer user_id. If we open User's views/users/show.html.erb view and inside User controller we have this code which finds correct user which is selected:
def show
#user = User.find(params[:id])
end`
Now inside that show.html.erb view we also have small code snipped which creates user's ticket. Would this be a good practice in creating it?
views/users/show.html.erb
<%= simple_form_for Ticket.new do |f| %>
<%= f.hidden_field :user_id, :value => #user.id %>
<%= f.text_area :description %>
<%= f.submit "Add" %>
<% end %>
controller/tickets_controller.rb
def create
#ticket = Ticket.new(ticket_params)
#user = User.find(ticket_params[:user_id])
#ticket.save
end
def ticket_params
params.require(:ticket).permit(:user_id, :description)
end
So, when we create a ticket for user, ticket's description and his user_id (hidden field inside view) are passed to tickets_controller.rb where new Ticket is created.
Is this a good practice in creating a new object which belongs to some other object? I am still learning so I would like to make this clear :) Thank you.
You should be able to do something like this in your form:
<%= f.association :user, :as => :hidden, :value => #user.id %>
This will pass user_id through your controller to your model and automatically make an association. You no longer need the #user= line in your controller.
Don't forget that the user could modify the form on their end and send any id they want. :)
See https://github.com/plataformatec/simple_form#associations for more info.
How about getting the user from the controller using current_user so that you protect yourself from anyone that would manipulate the value of the user_id in the form. Also I think this way is much cleaner
views/users/show.html.erb
<%= simple_form_for Ticket.new do |f| %>
<%= f.text_area :description %>
<%= f.submit "Add" %>
<% end %>
controller/tickets_controller.rb
def create
#ticket = Ticket.new(ticket_params)
#ticket.user = current_user
#ticket.save
end
def ticket_params
params.require(:ticket).permit(:user_id, :description)
end

Is there a way to do this in Rails without mass assignment?

Members create votes that both belong to them and to another model, Issues. Currently I'm doing this with a hidden form and passing the appropriate parameters. Here's the code on the issues index view:
<%= form_for(#vote) do |f| %>
<%= f.hidden_field "issue_id", :value => issue.id %>
<%= f.hidden_field "member_id", :value => session[:member_id] %>
<%= f.hidden_field "type", :value => :Upvote %>
<%= f.label issue.upvotes_count(issue.id) %>
<%= submit_tag "Up", :class => 'up-vote' %>
<% end %>
This doesn't seem ideal as it leaves issue_id and member_id open to mass assignment. Is there a better way to do this with a button_to tag or something?
Here's the controller code:
class VotesController < ApplicationController
#GET
def new
#vote = Vote.new
end
# POST
def create
#vote = Vote.new(params[:vote])
#vote.member_id = current_member
if #vote.save
redirect_to issues_path
else
redirect_to issues_path, notice: "you must be logged in to vote"
end
end
end
and
class IssuesController < ApplicationController
# GET
def index
#issues = Issue.find(:all)
#vote = Vote.new
end
# GET
def show
#issue = Issue.find(params[:id])
respond_to do |format|
format.html
format.js
end
end
end
Use scope in the controller:
#issue = Issue.find(params[:issue_id])
#vote = #issue.votes.new(params[:vote])
#vote.save
and do not pass member_id and issue_id to hidden fields.
If you have proper nested RESTful routes you should be able to get params[:issue_id] directly.
If issue and member_id are available before you vote.save! in the controller, you can set them manually there.
Normally you get values like member_id from current_user in the controller rather than passing it via form parameters. How you have it currently does expose you to mass-assignment.
Do members have to login before voting? If so, then you don't need to include member_id as a hidden field because you can grab current_user in the controller and this will provide good protection since there wouldn't be any advantage for a member to hack issue_id or type.

Resources