I have the following models: User, Product and Comment. The user can add, edit and delete comments to the product. I've successfully set up adding and deleting functionality and now I'm struggling with editing, for some reason, it caused me a number of difficulties.
My current code returns this error when I click on the edit comment link:
NoMethodError at /products/800/comments/8/edit
undefined method `comments' for nil:NilClass
Here's how my comment model looks like:
# id :integer not null, primary key
# body :text
# created_at :datetime not null
# updated_at :datetime not null
# user_id :integer
# product_id :integer
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :product
# validations ...
end
In User model I have has_many :comments and in Product - has_many :comments, dependent: :destroy.
In my routes.rb file I have the following nested resources:
resources :products, only: [:show] do
resources :comments, only: [:create, :edit, :update, :destroy]
end
My ProductsController has the only method show and nothing else, and here's how it looks like:
def show
product = Product.find(params[:id])
photos = ProductsPhoto.where(product: product)
case product.products_category.name
when 'Violin'
#product = [product, Violin.where(product: product).first, photos]
when 'Guitar'
#product = [product, Guitar.where(product: product).first, photos]
when 'Saxophone'
#product = [product, Saxophone.where(product: product).first, photos]
when 'Piano'
#product = [product, Piano.where(product: product).first, photos]
end
#comment = Comment.new
#comments = Comment.where(product_id: product.id).order('created_at DESC')
end
And now here's my CommentsController, which has create, edit, update and destroy:
class CommentsController < ApplicationController
def create
#product = Product.find(params[:product_id])
#comment = #product.comments.create(comment_params)
#comment.user_id = current_user.id
if #comment.save
redirect_to #product, notice: 'Comment Created!'
else
redirect_to #product, notice: 'Something went wrong...'
end
end
def show
end
def edit
#product = Product.find(params[:product_id])
#comment = #product.comments.find(params[:id])
end
def update
#product = Product.find(params[:product_id])
#comment = #product.comments.find(params[:id])
respond_to do |format|
if #comment.update_attributes(comment_params)
format.html do
redirect_to [#comment.product, #comment], notice: 'Comment Updated!'
end
else
format.html { render action: 'edit', notice: 'Something went wrong...' }
end
end
end
def destroy
#product = Product.find(params[:product_id])
#comment = #product.comments.find(params[:id])
#comment.destroy!
redirect_to #product, notice: 'Comment Deleted!'
end
private
def comment_params
params.require(:comment).permit(:body)
end
end
My _form view is located at views/products/_form.html.erb and looks like this:
<%= simple_form_for([#product[0], #product[0].comments.build]) do |f| %>
<%= f.error_notification %>
<%= f.input :body, required: true, placeholder: 'Type in your comment...', input_html: { class: 'form-control'}, label: false %>
<%= f.button :submit, class: 'btn btn-primary btn-block' %>
<% end %>
And in the views/products/show.html.erb I render the partial of comments and there are the links for destroying and editing the comment, they look like this:
<%= link_to edit_product_comment_path(comment.product, comment) do %>
<span class="glyphicon glyphicon-pencil"></span>
<% end %>
<%= link_to [comment.product, comment], method: :delete do %>
<span class="glyphicon glyphicon-remove"></span>
<% end %>
the delete link works fine and the edit link doesn't.
Maybe I've mistaken with routes, here're the routes for comments:
product_comments POST /products/:product_id/comments(.:format) comments#create
edit_product_comment GET /products/:product_id/comments/:id/edit(.:format) comments#edit
product_comment PATCH /products/:product_id/comments/:id(.:format) comments#update
PUT /products/:product_id/comments/:id(.:format) comments#update
DELETE /products/:product_id/comments/:id(.:format) comments#destroy
In my views/comments/edit.html.erb I render the same form:
<%= render 'products/form' %>
however, when I click on the edit link, I get the following error:
NoMethodError at /products/800/comments/8/edit
undefined method `comments' for nil:NilClass
at the very first line of the _form.html.erb.
I hope I've provided enough information to describe the problem.
So, could you please help me with resolving that issue with comments editing?
It's too early this morning...
This is not correct :
<%= simple_form_for([#product, #product.comments.build]) do |f| %>
Try :
<%= simple_form_for [#comment.product_id, #comment], url: product_comment_path(#comment.product_id, #comment) do |f| %>
Added section
(All of this is assuming that we are working with a single #comment, not the #comments collection. Based on your comments about the delete working.)
To reply to your comment, yes, absolutely you do need separate forms for "new" versus "edit" action for your child table, comments.
Form "new" naming convention is comments/_form.html.erb, the "edit" form would be comments/_form_edit.html.erb,
I would open the "new" action form for comments...
<%= simple_form_for([:product, #comment]) do |f| %>
And the "edit" action...
<%= simple_form_for [#comment.product_id, #comment], url: product_comment_path(#comment.product_id, #comment) do |f| %>
Sorry I forgot the URL on the previous version, I have updated the code snippet above to reflect this change as well. Caveat: There may be other ways to construct this link, but this is how I have implemented the case of having a product (parent) with reviews/comments (many children).
Nested attributes
In further response to your comment, I believe some of what you're looking for could be achieved by using nested attributes in forms. This is another topic. I would get the new/edit form simple cases working and then add complexity.
Related
I'm working on a ROR blog and have encountered some issues along the way. I'm currently learning Rails and just feel completely lost with connecting all the pieces. I've been working on my comments section for days and was finally able to create comments on posts, but I can't edit or delete them. I also referenced the SO questions below but am still running into problems.
Add Comment to User and Post models (Ruby on Rails)
Here's my layout:
Comment model params:
body \ user_id \ post_id
Model associations:
user.rb
has_many :posts
has_many :comments
post.rb
belongs_to :user
has_many :comments
comment.rb
belongs_to :user
belongs_to :post
routes.rb:
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get '/' => 'users#index'
get '/posts' => 'posts#index'
post '/posts/create' => 'posts#new'
post '/posts/edit' => 'posts#edit'
get '/signin' => 'sessions#new', as: :new_session
post '/create-session' => 'sessions#create', as: :create_session
get 'signout' => 'sessions#destroy', as: :destroy_session
resources :users
resources :posts
resources :comments
end
comments controller:
class CommentsController < ApplicationController
def index
#comment = Comment.all
end
def new
user = session[:user_id]
#comment = Comment.new(post_id: params[:post_id])
#post = Post.find(params[:post_id])
end
def create
#comment = Comment.new(comment_params)
#comment.user_id = session[:user_id]
#postid = params[:id]
if #comment.save
flash[:notice] = "comment created."
redirect_to '/posts'
else
flash[:error] = "Error creating comment."
redirect_to '/posts'
end
end
def edit
#post = Post.find(params[:id])
end
def update
#comment = Comment.find_by_id(params[:id])
#comment.update(comment_params)
flash[:notice] = "Comment updated."
redirect_to '/posts'
end
def destroy
#comment = Comment.find(params[:comment_id])
#comment.destroy
redirect_to '/posts'
end
private
def comment_params
params.require(:comment).permit(:body, :user_id, :post_id)
end
end
Posts show.html.erb page in views/posts folder:
<%# show all posts %>
<div id="single-post">
<h1>User - <%= #post.user.username %></h1>
<h2>Post - <%= #post.body %> </h2>
<%= link_to("Edit Post", edit_post_path(#post)) %>
</br>
<%= link_to("Delete Post", #post, method: 'delete') %>
</br>
<%= link_to("Add Comment", new_comment_path(post_id: #post.id)) %>
<%#<%= link_to("Edit Comment", edit_comment_path(post_id: #post.id, comment_id: #comment.id))%>
</div>
<h3><% #post.comments.reverse.each do |c| %> </h3>
<div id="single-comment">
<h4>Comment</h4>
<h5>From - <%= c.user.username %></h5>
<h6><%= c.body %> </h6>
</br>
<%= link_to("Edit Comment", edit_comment_path(#post.id)) %>
</br>
<%= link_to("Delete Comment", comment_path(#post.id), method: :delete) %>
</div>
<% end %>
</div>
new.html.erb form in views/comments folder
<div id="comment-form">
<%= form_for #comment do |f| %>
<%= f.label :body %>
<%= f.text_area :body, class: "text-area" %>
<%= f.hidden_field :post_id %>
<%= f.submit %>
<% end %>
</div>
Again I can add comments to posts. When I hover over the edit tag on the comment I'm seeing this: localhost:3000/comments/72/edit
I see this error when I click on edit
When I hover over the delete button I see this: localhost:3000/comments/72
I see this error when I click on delete
I'm at the point where I'm completely lost and feel I have tried everything possible but nothing seems to work. Please help! Here's the GitHub repo as well: https://github.com/angelr1076/rails-blog
The First argument in form cannot contain nil or be empty is telling you that #comment in <%= form_for #comment do |f| %> is nil. This is because in the edit action of your CommentsController you are setting #post instead of #comment.
Change this to be:
def edit
#comment = Comment.find(params[:id])
end
For deleting a comment, the Couldn't find Comment without an ID is telling you that the value you're passing to find is nil. This is because you're trying to use params[:comment_id] instead of params[:id]. Change the destroy action to:
def destroy
#comment = Comment.find(params[:id])
#comment.destroy
redirect_to '/posts'
end
Update:
Also as per your code, you should change edit and delete links to below
<%= link_to("Edit Comment", edit_comment_path(c)) %>
<%= link_to("Delete Comment", comment_path(c), method: :delete)
You are passing #post.id which is an id of post. Instead you should pass id of the comment using the block variable from your comments.each, noticing that the .id isn't needed here because it can be inferred by Rails.
I have looked through the other answers provided on StackOverflow, and none of them answered my question. Here is what is happening with my code.
Error
undefined method `update' for nil:NilClass
Problem:
It looks like the param id is not being sent to the controller from the form as they show up as nil in the console using byebug.
console readout:
(byebug) params[:id]
nil
(byebug) #support
nil
(byebug) params[:title]
nil
(byebug) params[:support]
<ActionController::Parameters {"title"=>"Test", "subtitle"=>"testing",
"website"=>"www.test.com", "type_of_support"=>"", "description"=>""}
permitted: false>
(byebug) params[:support][:id]
nil
(byebug) params[:support][:title]
"Test"
I do not believe that the problem is with the form as it is the same form partial used for the new/create action and the params are sent to the controller then and the object is created (though in that case there is no id, since it is generated when creating the object, not passed from the form).
You can see in my code below that the route for PATCH is just 'support' without the :id param. If I try to add that to the route, I get an error stating that there is no route matching 'support/'. So, I have to take away the :id param in the route for it to pass the information to the controller.
I am at a loss here. How do I pass the :id to the controller? How does rails do this? Before I manually change the routes, the automatic routes from resources :supports includes an :id param for the PATCH route and it works. What am I doing wrong that it won't allow me to add that to the route?
Code:
config/routes.rb
get 'support', as: 'supports', to: 'supports#index'
post 'support', to: 'supports#create'
get 'support/new', as: 'new_support', to: 'supports#new'
get 'support/:id/edit', as: 'edit_support', to: 'supports#edit'
get 'support/:title', as: 'support_page', to: 'supports#show'
patch 'support/', to: 'supports#update'
put 'support/:id', to: 'supports#update'
delete 'supports/:id', to: 'supports#destroy'
Results this for rake routes:
supports GET /support(.:format) supports#index
support POST /support(.:format) supports#create
new_support GET /support/new(.:format) supports#new
edit_support GET /support/:id/edit(.:format) supports#edit
support_page GET /support/:title(.:format) supports#show
PATCH /support(.:format) supports#update
PUT /support/:id(.:format) supports#update
DELETE /supports/:id(.:format) supports#destroy
app/controllers/supports_controllers.rb
class SupportsController < ApplicationController
before_action :set_support_by_title, only: [:show]
before_action :set_support_by_id, only: [:edit, :update, :destroy]
def index
#supports = Support.all
end
def show
end
def new
#support = Support.new
end
def edit
end
def create
#support = Support.new(support_params)
respond_to do |format|
if #support.save
format.html { redirect_to #support,
notice: 'Support was successfully created.' }
else
format.html { render :new }
end
end
end
def update
# byebug
respond_to do |format|
if #support.update(support_params)
format.html { redirect_to #support,
notice: 'Support was successfully updated.' }
else
format.html { render :edit }
end
end
end
def destroy
#support.destroy
respond_to do |format|
format.html { redirect_to supports_url,
notice: 'Support was successfully destroyed.' }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_support_by_title
#support = Support.find_by(title: params[:title])
# byebug
end
def set_support_by_id
#support = Support.find(params[:id])
# byebug
end
# Never trust parameters from the scary internet,
# only allow the white list through.
def support_params
params.require(:support).permit(:title,
:subtitle,
:website,
:type_of_support,
:description)
end
end
app/views/supports/edit.html.erb
<h1>Editing Support</h1>
<%= render 'form', support: #support %>
<%= link_to 'Show', support_page_path(#support.title) %> |
<%= link_to 'Back', supports_path %>
app/views/supports/_form.html.erb
<%= form_with(model: support, local: true) do |form| %>
<% if support.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(support.errors.count, "error") %>
prohibited this support from being saved:
</h2>
<ul>
<% support.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
Title:
<%= form.text_field :title, id: :support_title %>
</div>
<div class="field">
Subtitle:
<%= form.text_field :subtitle, id: :support_subtitle %>
</div>
<div class="field">
Website:
<%= form.text_field :website, id: :support_website %>
</div>
<div class="field">
Type of Support:
<%= form.text_field :type_of_support, id: :support_type %>
</div>
<div class="field">
Description:
<%= form.text_area :description, id: :support_description %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
While writing this question, I thought of something and tried it. And it worked. Instead of re-writing all of the routes myself, I wrote only 2 and used the resources :supports, except: [:index, :show] to have rails generate the others. This solved my issue.
Explanation
I knew that something was going on behind the scenes that I did not understand. The entire process worked fine before I started to change the routes. So, something in there was incorrect. (I still don't know what it is and how to change it.)
The only two routes that I really want to be changed are the two that users see. I don't care about how the routes look in the admin backend. So, that meant that I only needed to change the routes for index and show to be SEO friendly and look better in the browser. So, to do that I wrote the routes like this:
config/routes.rb
resources :supports, except: [:index, :show]
get 'support', as: 'support_index', to: 'supports#index'
get 'support/:title', as: 'support_page', to: 'supports#show'
This then created all of the new, create, edit, update, destroy routes for me. After doing this, this is how my routes now look:
supports POST /supports(.:format) supports#create
new_support GET /supports/new(.:format) supports#new
edit_support GET /supports/:id/edit(.:format) supports#edit
support PATCH /supports/:id(.:format) supports#update
PUT /supports/:id(.:format) supports#update
DELETE /supports/:id(.:format) supports#destroy
support_index GET /support(.:format) supports#index
support_page GET /support/:title(.:format) supports#show
As you can see, the PATCH route is now getting the param :id to be able to update the record.
Now, I just had to change a few of the redirects in the controller after create, update and destroy like this:
def create
#support = Support.new(support_params)
respond_to do |format|
if #support.save
format.html { redirect_to support_page_path(title: #support.title),
notice: 'Support was successfully created.' }
else
format.html { render :new }
end
end
end
def update
respond_to do |format|
if #support.update(support_params)
format.html { redirect_to support_page_path(title: #support.title),
notice: 'Support was successfully updated.' }
else
format.html { render :edit }
end
end
end
def destroy
#support.destroy
respond_to do |format|
format.html { redirect_to support_index_path,
notice: 'Support was successfully deleted.' }
end
end
These redirect_to statements now match the routes that I generated for index and show.
And now everything works.
So, problem solved, though I still don't know what I was doing wrong before. Any light that can be shed would be appreciated.
EDIT: I managed to delete! i had to define teh instance variable #movies = Movie.find(params[:id]) to the delete method in the controller.
I still can't update though. I get "param is missing or the value is empty: movie"
I forgot to add my contrller! sorry!
I'm trying to replicate one of my in class exercises into another app, for practice.
The idea is to be able to add new movies into a database, and get the option to update their info, and delete them as well. I can add new content, but I can't update them or delete them.
Appreciate any inputs I can get.
Routes.rb
Rails.application.routes.draw do
root "movies#index"
get "/movies", to: "movies#index"
get "/movies/new", to: "movies#new", as: "new_movie"
get '/movies/:id', to: "movies#movie_page", as: "movie_page"
post "/movies", to: "movies#add"
get "/movies/:id/edit", to: "movies#edit", as: "movie_edit"
put "/movies/:id", to: "movies#update"
patch "/movies/:id", to: "movies#update", as: "update_movie"
delete "/movies/:id", to: "movies#delete", as: "delete_movie"
end
Controller
class MoviesController < ApplicationController
def index
#movies = Movie.all
end
def movie_page
#movies = Movie.find(params[:id])
end
def new
#movies = Movie.new
end
def add
#movies = Movie.create(movie_params)
redirect_to movies_path
end
def edit
#movies = Movie.find(params[:id])
end
def update
#movies.update(movie_params)
redirect_to #movies, notice: "Shirt was updated."
end
def delete
#movies = Movie.find(params[:id])
#movies.destroy
# flash[:notice] = "Shirt was deleted."
redirect_to root_path, notice: "Shirt was deleted."
end
def movie_params
params.require(:movie).permit(:title, :description, :year_released)
end
# def set_movie
# #movies = Movie.find(params[:id])
# end
end
Form partial
<%= form_for #movies do |m| %>
<p>
<%= m.label :title %><br>
<%= m.text_field :title %>
</p>
<p>
<%= m.label :description %><br>
<%= m.text_field :description %>
</p>
<p>
<%= m.label :year_released %><br>
<%= m.text_field :year_released %>
</p>
<p>
<%= m.submit %>
</p>
<% end %>
Movie page html (individual movies, labeled by IDs)**I can't update or Delete, no route matches Delete.
When I press Update - I get param is missing or the value is empty: movie
<h1><%= #movies.title %></h1>
<h2>Released on : <%= #movies.year_released %> </h2>
<p> <%= #movies.description %> </p>
<%= link_to "Update", movie_edit_path(#movies) %>
<%= link_to "Delete", movies_path, method: :delete %
Edit page *I cant access this link. the form is the problem
<h1>Edit <%= #movies.title %> Info </h1>
<%= render "form" %>
<%= link_to "Cancel Edit", movie_edit_path(#movies) %>
Many thanks guys
def update
#movie = Move.find(params[:id])
#movie.update(movie_params)
redirect_to movie_path(#movie)
end
on your routes. all you need is resources :movies
you are getting param is empty because you have to pass in the id of the movie to update.
The major issue is that you do not load the variable #movies from the DB before you use it.
def update
#movies.update(movie_params)
redirect_to #movies, notice: "Shirt was updated."
end
def update
#movies.find(params[:id])
#movie.update(movie_params)
redirect_to #movies, notice: "Shirt was updated."
end
Aside from that you have tons of duplication and quite a few idiosyncrasies.
Rails uses these naming conventions for actions:
index
show (not movie_page)
new
create (not add)
edit
update
destroy (not delete)
You should follow them unless you have a damn good reason not to.
class MoviesController < ApplicationController
# cuts the duplication
before_filter :set_movie, except: [:new, :index]
def index
#movies = Movie.all
end
# GET /movies/:id
def show
end
# GET /movies/new
def new
#movie = Movie.new
end
# POST /movies
def create
#movie = Movie.create(movie_params)
redirect_to movies_path
end
# GET /movies/edit
def edit
end
# PUT|PATCH /movies/:id
def update
#movie.update(movie_params)
redirect_to #movie, notice: "Shirt was updated."
end
# DELETE /movies/:id
def destroy
#movie.destroy
redirect_to action: :index
end
private
def movie_params
params.require(:movie).permit(:title, :description, :year_released)
end
def set_movie
# Use the singular form when naming a variable with a single record
# failure to do so may result in tarring and feathering
#movie = Movie.find(params[:id])
end
end
I am trying to create an app where a user can follow or unfollow an article. To do that, I created three models, Customer, Article and Pin.
These are the relationships:
Customer
has_many articles
has_many pins
Article
has_many pins
belongs_to customer
Pins
belongs_to customer
belongs_to article
I believe a Pin must be nested within an Article. My route.rb look like this:
resources :articles do
resources :pins, :only => [:create, :destroy]
end
end
In article#index I have a form for creating or destroying the relationships:
# To create
<%= form_for [article, current_customer.pins.new] do |f| %>
<%= f.submit "Pin" %>
<% end %>
# To destroy which doesn't work because i guess you can't do the form like that
<%= form_for [article, current_customer.pins.destroy] do |f| %>
<%= f.submit "Pin" %>
<% end %>
Here are the corresponding controller actions:
def create
#article = Article.find(params[:article_id])
#pin = #article.pins.build(params[:pin])
#pin.customer = current_customer
respond_to do |format|
if #pin.save
format.html { redirect_to #pin, notice: 'Pin created' }
else
format.html { redirect_to root_url }
end
end
end
def destroy
#article = Article.find(params[:article_id])
#pin = #article.pins.find(params[:id])
#pin.destroy
respond_to do |format|
format.html { redirect_to root_url }
end
end
Now here my two questions:
How do I create a form that would delete the current relationship?
In my form I only want to show one of the buttons. How can I conditionally display the correct button?
You don't need a form to delete the relationship, links will do fine. I assume you'll be iterating through your articles in the index view -- if so, how about something like this?
<% #articles.each do |article| %>
...
<% if (pin = current_customer.pins.find_by_article(article)) %>
<%= link_to 'Unfollow', articles_pin_path(article, pin), :confirm => "Are you sure you want to unfollow this article?", :method => :delete %>
<% else %>
<%= link_to 'Follow', articles_pins_path(article), :method => :post %>
<% end %>
<% end %>
One caveat about using link_to for creating/destroying records is that if javascript is disabled, they will fall back to using GET rather than POST/DELETE. See the documentation for details.
So I have a fairly typical blog application with posts and comments.
Each comment belongs to one post
A post can have many comments.
Basically I want to add a form for comments to the show action for posts, without having post_id under attr_accessible in the comment model.
In my posts controller I have:
def show
#post = Post.find(params[:id])
#poster = "#{current_user.name} #{current_user.surname} (#{current_user.email})"
#comment = #post.comments.build( poster: #poster )
end
I'm not entirely sure what I should be doing in the comments controller (I'm not confident that the code above is right either if I'm honest). At the moment I have:
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.build(params[:post])
if #comment.save
redirect_to #post, notice: "Comment posted"
else
redirect_to #post, error: "Error!"
end
end
My routes:
resources :comments
resources :posts do
resources :comments
end
and finally the form:
<%= form_for #post.comments.build do |f| %>
<%= f.label :content, "WRITE COMMENT" %>
<%= f.text_area :content, rows: 3 %>
<%= f.hidden_field :post_id, value: #post.id %>
<%= f.submit "Post" %>
<% end %>
The problem here is that I have no way of passing my post_id from the show action of the posts controller to the create action of the comments controller. Any help is much appreciated. Thank you in advance!
Your posts controller looks fine... but assuming your routes looks like
resources :posts do
resources :comments
end
then your CommentsController#create should/could look like:
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.build(params[:comment])
if #comment.save
redirect_to #post, notice: "Comment posted"
else
redirect_to #post, error: "Error!"
end
end
And your form:
<%= form_for [#post, #comment] do |f| %>
<%= f.hidden_field :poster, value: #poster %>
<%= f.label :content, "WRITE COMMENT" %>
<%= f.text_area :content, rows: 3 %>
<%= f.submit "Post" %>
<% end %>
I will assume that your post model has_many comments and comment belongs_to post
than you in your routes file you can do something like this
resources :posts do
resources :comments
end
this will give you a url schem such as
/posts/:post_id/comments , allowing you to always have post_id of the comment parrent
The URL for your show post should be like post/show/(:id).
Now, in the comment form, you can place a hidden field, with value of params[:id].
hidden_field(:post_id, :value => params[:id])
When you submit your form, you can get the value of post_id using the hidden field.
def create
#comment = Comment.new(params[:comment])
#comment.post_id = params[:post_id]
if #comment.save
flash[:notice] = 'Comment posted.'
redirect_to post_path(#comment.post_id)
else
flash[:notice] = "Error!"
redirect_to post_path(#comment.post_id)
end
end