Having an issue with Rails [GET] routes - ruby-on-rails

I am wrapping my head around the Rails framework and routes. I am building a site where users can post Rails articles and tips and I've already added a ton of functionality to my site but I am having an issue with nested resources. I want my users to create post. I also want the same user and other users to leave comments on the post. Now, the tricky part is that I need a way for them to edit their own comment. So once they go to a post>comment>edit, I am receiving a No route matches [GET] "/posts/48/comments/edit" and this is from the Post show template. For tht particular post, I can tell by the error that it cannot find the id of that comment to edit it. I am sure that this is nested resources issue but I cant wrap my head on it. Looking at my code, everything seems in tact to me. Any ideas? Thanks in advance for any insight.
routes.rb file
PostitTemplate::Application.routes.draw do
root to: 'posts#index'
get '/register', to: 'users#new'
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
get '/logout', to: 'sessions#destroy'
resources :users, only: [:create, :edit, :update]
resources :posts, except: [:destroy] do
member do
post 'vote'
end
resources :comments, only: [:create, :edit, :update] do
member do
post 'vote'
end
end
end
resources :categories, only: [:new, :create]
end
comments_controller
class CommentsController < ApplicationController
before_action :require_user
def create
#post = Post.find(params[:post_id])
#comment = Comment.new(params.require(:comment).permit(:body))
#comment.post = #post
#comment.creator = current_user
if #comment.save
flash[:notice] = "Your comment was created!"
redirect_to post_path(#post)
else
render 'posts/show'
end
end
def edit
#comment = Comment.find(params[:id])
#post = Post.find(params[:post_id])
end
def update
#comment = Comment.find(params[:id])
if #comment.update(comment_params)
flash[:notice] = "You updated your comment!"
redirect_to post_path
else
render :edit
end
end
private
def comment_params
params.require(:comment).permit(:body)
end
def set_comment
#comment = Comment.find(params[:id])
end
end
posts_controller
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :vote]
before_action :require_user, only: [:new, :create, :edit, :update, :vote]
before_action :require_creator, only:[:edit, :update]
def index
#posts = Post.all.page(params[:page]).per_page(10)
end
def show
#comment = Comment.new
end
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
#post.creator = current_user
if #post.save
flash[:notice] = "You created a post!"
redirect_to posts_path
else
render :new
end
end
def edit
end
def update
if #post.update(post_params)
flash[:notice] = "You updated the post!"
redirect_to post_path(#post)
else
render :edit
end
end
def vote
Vote.create(voteable: #post, creator: current_user, vote: params[:vote])
respond_to do |format|
format.js { render :vote } # Renders views/posts/vote.js.erb
end
end
private
def post_params
params.require(:post).permit(:url, :title, :description)
end
def set_post
#post = Post.find(params[:id])
end
def require_creator
access_denied if #post.creator != current_user
end
end
show.html.erb(This is the show post template and on line 33, I want to link to the comments controller edit action)
<div class="page-header">
<h2>
<%= #post.title %>
<small>
posted by <%= link_to #post.creator.username %> about <%= time_ago_in_words(#post.created_at) + ' ago' %>
| <%= link_to 'check out the link', fix_url(#post.url) %> |
<%= link_to 'edit', edit_post_path(#post) %>
</small>
</h2>
</div>
<h3><%= #post.description %></h3>
<%= render 'shared_partials/errors', errors_obj: #comment %>
<%= form_for [#post, #comment] do |f| %>
<%= f.text_area :body, :class=> "input", :placeholder=> "Comment goes here", :rows => "6" %>
</br>
<div class="button">
<%= f.submit "Create a comment", class: 'btn btn-primary' %>
</div>
<% end %>
<div class="page-header">
<h4>All Comments</h4>
</div>
<% #post.comments.each do |comment| %>
<div class="comments">
<h5><%= comment.body %></h5>
<li>
<small class="muted">
posted by <%= link_to comment.creator.username %> about <%= time_ago_in_words(comment.created_at) + ' ago' %>
<% if logged_in? && (comment.creator == current_user) %> |
<%= link_to 'edit', edit_post_comment_path(#post, #comment) %> |
<i class="icon-user icon"></i>
<% end %>
</small>
</li>
</div>
<% end %>
Finally my rake routes
root_path GET / posts#index
register_path GET /register(.:format) users#new
login_path GET /login(.:format) sessions#new
POST /login(.:format) sessions#create
logout_path GET /logout(.:format) sessions#destroy
users_path POST /users(.:format) users#create
edit_user_path GET /users/:id/edit(.:format) users#edit
user_path PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
vote_post_path POST /posts/:id/vote(.:format) posts#vote
vote_post_comment_path POST /posts/:post_id/comments/:id/vote(.:format) comments#vote
post_comments_path POST /posts/:post_id/comments(.:format) comments#create
edit_post_comment_path GET /posts/:post_id/comments/:id/edit(.:format) comments#edit
post_comment_path PATCH /posts/:post_id/comments/:id(.:format) comments#update
PUT /posts/:post_id/comments/:id(.:format) comments#update
posts_path GET /posts(.:format) posts#index
POST /posts(.:format) posts#create
new_post_path GET /posts/new(.:format) posts#new
edit_post_path GET /posts/:id/edit(.:format) posts#edit
post_path GET /posts/:id(.:format) posts#show
PATCH /posts/:id(.:format) posts#update
PUT /posts/:id(.:format) posts#update
categories_path POST /categories(.:format) categories#create
new_category_path GET /categories/new(.:format) categories#new

Replace
<%= link_to 'edit', edit_post_comment_path(#post, #comment) %>
with
<%= link_to 'edit', edit_post_comment_path(#post, comment) %>
You need to pass the current comment, not the new one.
Also, you don't necessarily have to nest the comment resource.

Related

Redirect in destroy action not working properly

I am building a simple blog app using Ruby on Rails that allows users to log in/out, sign up and perform actions on their articles and profiles based on permissions and restrictions.
I have encountered a problem with the destroy User action. In the users/index view(where all existing users are listed), it causes no errors due to the url path containing no {:id}, but the redirect_to root_path does not work. If the same action is executed in the users/show page(personal profile page with some info and associated articles), due to the url being localhost/users/id, when the user is deleted I get "Couldn't find User with 'id'=33" error shown below. If I manually go to the root route, the successful account deletion message shows up and the action is performed correctly. So this is not a metter of DESTROY not working, but of redirection I believe. I have tried redirecting to different paths but it still doesn't work. Here are the related files:
routes.rb
Rails.application.routes.draw do
root "pages#home"
get "about", to: "pages#about"
resources :articles
get "signup", to: "users#new"
resources :users, except: [:new]
get 'login', to: 'sessions#new'
post 'login', to: 'sessions#create'
get 'logout' => :destroy, to: 'sessions#destroy'
end
pages_controller
class PagesController < ApplicationController
def home
redirect_to articles_path if logged_in?
end
def about
end
end
users_controller
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
before_action :require_user, only: [:edit, :update]
before_action :require_same_user, only: [:edit, :update, :destroy]
def index
#users = User.all
end
def show
#articles = #user.articles
end
def new
#user = User.new
end
def edit
end
def create
#user = User.new(user_params)
if(#user.save)
session[:user_id] = #user.id #logs user in automatically once they are signed up
flash[:notice] = "Welcome to AlphaBlog, #{#user.username}!"
redirect_to articles_path
else
render 'new'
end
end
def update
if #user.update(user_params)
flash[:notice] = "Account updated!"
redirect_to #user
else
render 'edit'
end
end
def destroy
#user.destroy
session[:user_id] = nil
flash[:notice] = "Account and all associated articles deleted!"
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:username, :email, :password)
end
def set_user
#user = User.find(params[:id])
end
def require_same_user
if current_user != #user
flash[:alert] = "You can only edit your own profile!"
redirect_to current_user
end
end
end
sessions_controller
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
session[:user_id] = user.id
flash[:notice] = "Logged in successfully!"
redirect_to user
else
flash.now[:alert] = "There was something wrong with your login details!"
render 'new'
end
end
def destroy
session[:user_id] = nil
flash[:notice] = "Logged out."
redirect_to root_path
end
end
users/index.html.erb
<div class = "header">
<h1>
AlphaBlog
<% if logged_in? %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<% else %>
<%= link_to 'Home', root_path(), method: :get, class: "index-button-to" %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<% end %>
<%= render 'layouts/log_in_out_navigation'%>
</h1>
</div>
<h2>Alpha Bloggers</h2>
<div class="index-container">
<%# cycle through all articles and show them all in a table %>
<% #users.each do |user| %>
<div class = "index-article-container">
<div class="index-article-user" style = "color:rgb(16, 136, 255);">
<%= user.username %>
</div>
<div class="white">
<div class="index-article-title">
<%= gravatar_for(user, size: 150) %>
</div>
<div class="index-article-description">
<%# gives the plural word for multiple articles %>
<%= pluralize(user.articles.count, "article") %>
</div>
<div class="index-article-actions">
<%# shows selected article page %>
<%= link_to 'View Profile', user, class: "index-link-to show" %>
<% if logged_in? && current_user.username == user.username %>
<%# shows selected article EDIT page. edit_article_path because in routes,
the prefix for edit is edit_article && (article) because we need the id for the path as well%>
<%= link_to 'Edit Profile', edit_user_path(user), data: { turbo_method:
:get}, class: "index-link-to edit" %>
<%= link_to 'Delete Profile', user_path(current_user), data: {
turbo_method: :delete, turbo_confirm: "Are you sure? (This will also delete all of your
articles)" }, class: "index-link-to delete" %>
<% end %>
</div>
</div>
<div class="index-created-updated">
Joined <%= time_ago_in_words(user.created_at) %> ago.
</div>
</div>
<% end %>
users/show.html.erb
<div class = "header">
<h1>
AlphaBlog
<% if logged_in? %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<%= link_to 'Bloggers', users_path, method: :get, class: "index-button-to" %>
<% else %>
<%= link_to 'Home', root_path(), method: :get, class: "index-button-to" %>
<%= link_to 'Articles', articles_path, method: :get, class: "index-button-to" %>
<%= link_to 'Bloggers', users_path, method: :get, class: "index-button-to" %>
<% end %>
<%= render 'layouts/log_in_out_navigation'%>
</h1>
</div>
<h2> <%= #user.username %>'s profile </h2>
<div class="show-users-image">
<%# gravatar_for method created in helpers/application_helper %>
<%= gravatar_for #user, size: 200 %>
<% if logged_in? && current_user.username == #user.username %>
<div class="index-profile-actions">
<%= link_to "Edit Profile", edit_user_path(#user), class: "index-link-to edit" %>
<%= link_to 'Delete Profile', user_path(current_user), data: { turbo_method: :delete,
turbo_confirm: "Are you sure? (This will also delete all of your articles)" }, class: "index-
link-
to delete", style: "margin-top:0.3vh" %>
</div>
<% end %>
</div>
<h3 style = "text-align:center">Articles</h3>
<%= render 'articles/article' %>
error page
The way to do this in Rails 7 is to update the destroy action in the UsersController by adding status: :see_other after the redirect, as follows:
def destroy
#user.destroy
session[:user_id] = nil
flash[:notice] = "Account and all associated articles deleted!"
redirect_to root_path, status: :see_other
end
I think the answer here is really a very different layout of your routes and controller (or to not reivent the wheel in the first place). Passing the user id through the parameters would be fine if your making a system where you are managing other users - but its pretty wonky when users are managing their own profiles.
For example this is how users CRUD their own profiles in a vanilla Devise setup:
Verb URI Pattern Controller#Action
------------------------------------------------------------------------
GET /users/cancel(.:format) devise/registrations#cancel
GET /users/sign_up(.:format) devise/registrations#new
GET /users/edit(.:format) devise/registrations#edit
PATCH /users(.:format) devise/registrations#update
PUT /users(.:format) devise/registrations#update
DELETE /users(.:format) devise/registrations#destroy
POST /users(.:format) devise/registrations#create
Note the lack of the :id parameter in the URI Pattern. Thats because its implied that the resource in question is the currently signed in user, and that the user is identified through the session (or a token).
The controller is named Registrations to avoid the ambiguity if the programmer later wants to add a UsersController to manage other users.
If you want to do something similiar you can generate singular routes by using the resource macro instead of resources.
# routes for user registration
resource :registrations,
only: [:new, :edit, :update, :create, :destroy]
# routes for viewing other users
resources :users, only: [:index, :show]
Which will generate:
Prefix Verb URI Pattern Controller#Action
-----------------------------------------------------------------------
new_registrations GET /registrations/new(.:format) registrations#new
edit_registrations GET /registrations/edit(.:format) registrations#edit
registrations GET /registrations(.:format) registrations#show
PATCH /registrations(.:format) registrations#update
PUT /registrations(.:format) registrations#update
DELETE /registrations(.:format) registrations#destroy
POST /registrations(.:format) registrations#create
Name it whatever you want. The core takeaway here is to not confuse two completely different problems - user management and user registrations and have separate endpoints and controllers for each responsibilty.
Then in your controller you simply authenticate the user from the session and redirect the user if they are not authenticated:
# Handles user account registration, updates and deleting accounts
class RegistrationsController < ApplicationController
before_action :require_user, except: [:new, :create]
# Displays the form for signing up a user
# GET /registrations
def new
#user = User.new
end
# Register a new user and sign them in
# POST /registrations
def create
#user = User.new(user_params)
if #user.save
reset_session # avoids session fixation attacks
session[:user_id] = #user.id #logs user in automatically once they are signed up
flash[:notice] = "Welcome to AlphaBlog, #{#user.username}!"
redirect_to articles_path
else
render :new
end
end
# Form for editing the users own profile
# GET /registrations/edit
def edit
#user = current_user
end
# Update the currently signed in user
# PATCH /registrations
def update
#user = current_user
if #user.update(user_params)
flash[:notice] = "Account updated!"
redirect_to current_user
else
render :new
end
end
# Cancel the current users registration
# DELETE /registrations
def delete
current_user.delete
reset_session # avoids session fixation attacks
flash[:notice] = "Account and all associated articles deleted!"
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:username, :email, :password)
end
end
# Displays users
# Managing accounts is handled by RegistrationsController
class UsersController < ApplicationController
# GET /users
def index
#users = User.all
end
# GET /users/1
def show
#user = User.find(params[:id])
#articles = #user.articles
end
end
Since their is no id in the path you you need to set the delete button to send to the right path:
<%= button_to "Delete your account", registrations_path, method: :delete %>
And adjust your forms:
<%= form_with(model: #user, url: registrations_path) do |form| %>
# ...
<% end %>
Doing this the correct way would really be to do it the same way that Devise does and have a normal link that sends a GET request to a "Are you sure you want to delete your account?" page when then requires the user to enter their password or email and submits a DELETE request so that users don't accidentially delete their accounts.
But then again don't reinvent the authentication wheel unless you want a long and tedious lesson into wheelmaking.
I'm not sure you have truly got to the bottom of this. In your original approach I suspect two things are going on:
In users/index.html.erb you have link_to 'Delete Profile', user_path(current_user) but I think you want user_path(user). What you currently have will have every delete button try to delete the same user.
The fact that error says your are attempting to execute 'show' rather than 'destroy' makes me suspect that you do not have turbo loaded properly. You don't say what version of rails you are using, but for versions earlier than 7 you don't get turbo out of the box, and you should use UJS instead.
compare this https://guides.rubyonrails.org/getting_started.html#deleting-an-article with this https://guides.rubyonrails.org/v6.1/getting_started.html#deleting-an-article

No route matches [POST] "parent/id/child/new" error on nested association

Companies has_many Quotes, trying to create a new Quote in the Quotes controller, nested routes in place, yet i get a;
No route matches [POST] "/companies/123/quotes/new"
routes.rb
resources :companies do
resources :quotes, only: [ :new, :create, :show, :index]
resources :employees, only: [:show, :index]
end
rake routes
Prefix Verb URI Pattern Controller#Action
company_quotes GET /companies/:company_id/quotes(.:format) quotes#index
POST /companies/:company_id/quotes(.:format) quotes#create
new_company_quote GET /companies/:company_id/quotes/new(.:format) quotes#new
company_quote GET /companies/:company_id/quotes/:id(.:format) quotes#show
company_employees GET /companies/:company_id/employees(.:format) employees#index
company_employee GET /companies/:company_id/employees/:id(.:format) employees#show
companies GET /companies(.:format) companies#index
POST /companies(.:format) companies#create
new_company GET /companies/new(.:format) companies#new
edit_company GET /companies/:id/edit(.:format) companies#edit
company GET /companies/:id(.:format) companies#show
PATCH /companies/:id(.:format) companies#update
PUT /companies/:id(.:format) companies#update
DELETE /companies/:id(.:format) companies#destroy
root GET / companies#new
new.html.erb
<div class='form-group col-md-6 quote-form'>
<%= simple_form_for [#company, #quote] do |quote| %>
<%= render 'quote_fields', :f => quote %>
<%= quote.submit "Get Quote", class: 'btn btn-primary' %>
<% end %>
</div>
_quote_fields.html.erb
<%= f.input :lives_overseas, as: :radio_buttons, collection: [['Yes', true],
['No', false]], readonly: nil %>
<%= f.input :payment_frequency, collection: Quote.payment_frequencies.map { |k,v| [ k.humanize, k ] } %>
<%= params.inspect %>
quotes_controller.rb
class QuotesController < ApplicationController
before_action :authenticate_user!, only: [ :new, :create, :show, :index ]
def new
#company = Company.find(params[:company_id])
#quote = #company.quotes.build
end
def create
#company = Company.find(params[:company_id])
#quote = #company.quotes.build(params[:quote])
if #quote.save
render 'show'
end
end
def show
#company = Company.find(params[:company_id])
#quote = #company.quotes.find(params[:id])
#employees = #company.employees.all
# puts debug(params)
end
def index
#company = Company.find(params[:company_id])
#quotes = #company.quotes.all
#employees = #company.employees.all
end
end
Shouldn't the submit action on the quote form be posting to; POST /companies/:company_id/quotes(.:format), which is clearly present from rake routes?
Any ideas what I'm doing wrong here? Thanks for the guidance.
No route matches [POST] "/companies/123/quotes/new"
You are having nested resources, so you need to change
<%= simple_form_for #quote do |quote| %>
to
<%= simple_form_for [#comapny,#quote] do |quote| %>
And change #quote = #company.quotes.build to just #quote = Quote.new in quotes#new method

Routing Error uninitialized constant Comments – Making Nested comments

I'm learning Rails. I've got an app that has "Ideas", which have "Comments"
I've created the comments using this guide (https://gorails.com/episodes/comments-with-polymorphic-associations)
I am using the 'Ancestry' gem to attempt make them nested using this guide on railscasts (http://railscasts.com/episodes/262-trees-with-ancestry)
Anyways I'm getting this error "ActionController::RoutingError (uninitialized constant Comments):"
This is where I'm getting the error – when I hit the "Reply" link
<h3> Comments </h3>
<% #idea.comments.each do |comment| %>
<div>
<%= comment.body %>
<div class="actions">
<%= link_to "Reply", new_comment_path(:parent_id => comment) %>
</div>
</div>
<% end %>
The above is being rendered on the "Ideas/show.html" page
<p id="notice"><%= notice %></p>
<p>
<strong>Description:</strong>
<%= #idea.description %>
</p>
<%= render partial: "comments/comments", locals: {commentable: #idea} %>
<%= render partial: "comments/form", locals: {commentable: #idea} %>
<% if #idea.user == current_user %>
<%= link_to 'Edit', edit_idea_path(#idea) %>
<% end %>
<%= link_to 'Back', ideas_path %>
I want to send them here to "Comments/new" which is the same as my form page
<%= form_for [commentable, Comment.new] do |f| %>
<div class="form-group">
<%= f.hidden_field :parent_id %>
<%= f.text_area :body, class: "form-control", placeholder: "Add a comment here" %>
</div>
<%= f.submit class: "btn btn-primary" %>
<% end %>
Routes.rb
Rails.application.routes.draw do
resources :ideas do
resources :comments, module: :ideas
end
devise_for :users
root 'ideas#index'
get "about" => "pages#about"
get "new_comment" => "comments/new"
end
Comments_controller.rb
class CommentsController < ApplicationController
before_action :authenticate_user!
def new
#comment = Comment.new(:parent_id => params[:parent_id])
end
def create
#comment = #commentable.comments.new comment_params
#comment.user = current_user
#comment.save
redirect_to #commentable, notice: "Your comment was posted"
end
private
def comment_params
params.require(:comment).permit(:body)
end
end
Ideas_controller.rb
class IdeasController < ApplicationController
before_action :set_idea, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!, except: [:index, :show]
before_action :correct_user, only: [:edit, :update, :destroy]
respond_to :html
def index
#ideas = Idea.all
end
def show
end
def new
#idea = Idea.new
#idea.comments.build
respond_with(#idea)
end
def edit
end
def create
#idea = current_user.ideas.build(idea_params)
if #idea.save
redirect_to #idea, notice: "Idea was successfully created."
else
render :action => 'new'
end
end
def update
if #idea.update(idea_params)
redirect_to #idea, notice: "Your idea has been updated"
else
render action: 'edit'
end
end
def destroy
#idea.destroy
redirect_to ideas_url
end
private
def set_idea
#idea = Idea.find(params[:id])
end
def correct_user
#idea = current_user.ideas.find_by(id: params[:id])
redirect_to ideas_path, notice: "You can't edit this" if #idea.nil?
end
def idea_params
params.require(:idea).permit(:description)
end
end
Any help would be much appreciated, thank you.
EDIT _ Added my routes
idea_comments GET /ideas/:idea_id/comments(.:format) ideas/comments#index
POST /ideas/:idea_id/comments(.:format) ideas/comments#create
new_idea_comment GET /ideas/:idea_id/comments/new(.:format) ideas/comments#new
edit_idea_comment GET /ideas/:idea_id/comments/:id/edit(.:format) ideas/comments#edit
idea_comment GET /ideas/:idea_id/comments/:id(.:format) ideas/comments#show
PATCH /ideas/:idea_id/comments/:id(.:format) ideas/comments#update
PUT /ideas/:idea_id/comments/:id(.:format) ideas/comments#update
DELETE /ideas/:idea_id/comments/:id(.:format) ideas/comments#destroy
ideas GET /ideas(.:format) ideas#index
POST /ideas(.:format) ideas#create
new_idea GET /ideas/new(.:format) ideas#new
edit_idea GET /ideas/:id/edit(.:format) ideas#edit
idea GET /ideas/:id(.:format) ideas#show
PATCH /ideas/:id(.:format) ideas#update
PUT /ideas/:id(.:format) ideas#update
DELETE /ideas/:id(.:format) ideas#destroy
new_user_session GET /users/sign_in(.:format) devise/sessions#new
user_session POST /users/sign_in(.:format) devise/sessions#create
destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
user_password POST /users/password(.:format) devise/passwords#create
new_user_password GET /users/password/new(.:format) devise/passwords#new
edit_user_password GET /users/password/edit(.:format) devise/passwords#edit
PATCH /users/password(.:format) devise/passwords#update
PUT /users/password(.:format) devise/passwords#update
cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel
user_registration POST /users(.:format) devise/registrations#create
new_user_registration GET /users/sign_up(.:format) devise/registrations#new
edit_user_registration GET /users/edit(.:format) devise/registrations#edit
PATCH /users(.:format) devise/registrations#update
PUT /users(.:format) devise/registrations#update
DELETE /users(.:format) devise/registrations#destroy
root GET / ideas#index
about GET /about(.:format) pages#about
new_comment GET /new_comment(.:format) comments/new#new_comment

Undefined Method 'reviews_path' for #<#<Class:0x3dfd490>:0x51970e0>

References and External Links
Ruby on Rails - Settting up Reviews functionality
NoMethodError in Discussions#new
http://ruby.about.com/od/rubyonrails/ss/blogpart4_4.htm
Background
I'm implementing a feature in my application that allow users to rate and review pictures.
I am using a Posts/Comments relationship model for a Pictures/Reviews relationship.
Models
class Review < ActiveRecord::Base
belongs_to :picture
end
class Picture < ActiveRecord::Base
has_many :reviews
end
Above, I established a one-to-many relationship between pictures and reviews.
Reviews Migration
class CreateReviews < ActiveRecord::Migration
def change
create_table :reviews do |t|
t.string :username
t.text :body
t.references :picture, index: true
t.timestamps
end
end
end
Matched Routes
match '/pictures/:id/reviews', to: 'reviews#show', via: 'get', :as => 'picture_reviews'
match '/pictures/:id/reviews/edit', to: 'reviews#edit', via: 'get'
match '/pictures/:id/reviews/new', to: 'reviews#new', via: 'get', :as => 'new_reviews'
I will name the route for reviews#edit after I fix this issue with reviews#new.
Error Message
NoMethodError in Reviews#new
Undefined method 'reviews_path' for #<#<Class:0x45c1b00>:0x39ae810>
Extracted source (Around line #8):
5 <div class = 'edit-form'>
6 <div class = 'center'>
7
8 <% form_for #review do |f| %>
9
10 <p>
11 <%= f.label :username %><br />
I checked to see if any files contained 'review-path', but all routes were properly named.
Routes
favorite_picture_path PUT /pictures/:id/favorite(.:format) pictures#favorite
pictures_path GET /pictures(.:format) pictures#index
POST /pictures(.:format) pictures#create
new_picture_path GET /pictures/new(.:format) pictures#new
edit_picture_path GET /pictures/:id/edit(.:format) pictures#edit
picture_path GET /pictures/:id(.:format) pictures#show
PATCH /pictures/:id(.:format) pictures#update
PUT /pictures/:id(.:format) pictures#update
DELETE /pictures/:id(.:format) pictures#destroy
users_path GET /users(.:format) users#index
POST /users(.:format) users#create
new_user_path GET /users/new(.:format) users#new
edit_user_path GET /users/:id/edit(.:format) users#edit
user_path GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
sessions_path POST /sessions(.:format) sessions#create
new_session_path GET /sessions/new(.:format) sessions#new
session_path DELETE /sessions/:id(.:format) sessions#destroy
contacts_path POST /contacts(.:format) contacts#create
new_contact_path GET /contacts/new(.:format) contacts#new
root_path GET / pictures#welcome
users_new_path GET /users/new(.:format) users#new
about_path GET /about(.:format) pictures#about
GET /contacts(.:format) contacts#new
GET /users/:id/favorites(.:format) users#favorites
signup_path GET /signup(.:format) users#new
signin_path GET /signin(.:format) sessions#new
signout_path DELETE /signout(.:format) sessions#destroy
picture_reviews_path GET /pictures/:id/reviews(.:format) reviews#index
GET /pictures/:id/reviews/edit(.:format) reviews#edit
new_reviews_path GET /pictures/:id/reviews/new(.:format) reviews#new
updated_path GET /updated(.:format) pictures#new_updates
GET /top-rated(.:format) pictures#high_ratings
ReviewsController (Part 1)
class ReviewsController < ApplicationController
before_action :set_review, only: [:show, :edit, :update, :destroy]
def index
#picture = Picture.find(params[:id])
#review = Review.all
end
def show
#picture = Picture.find(params[:id])
#review = Review.find(params[:id])
end
def new
#review = Review.new
end
def edit
#picture = Picture.find(params[:picture_id])
#review = Review.find(params[:id])
end
def create
#picture = Picture.find(params[:picture_id])
#review = #picture.reviews.build(params[:review])
if #review.save
flash[:notice] = 'Review was successfully created.'
redirect_to #picture
else
flash[:notice] = "Error creating review: #{#review.errors}"
redirect_to #picture
end
end
Reviews Controller(Part 2)
def update
#picture = Picture.find(params[:picture_id])
#review = Review.find(params[:id])
if #review.update_attributes(params[:review])
flash[:notice] = "Review updated"
redirect_to #picture
else
flash[:error] = "There was an error updating your review"
redirect_to #picture
end
end
def destroy
#picture = Picture.find(params[:picture_id])
#review = Review.find(params[:id])
#review.destroy
redirect_to(#review.post)
end
private
def set_review
#review = Review.find(params[:id])
end
def review_params
params.require(:review).permit(:username, :body, :picture_id)
end
end
Reviews#Index Page
<h3>Reviews for <%= "#{#picture.title}" %></h3>
<table>
<thead>
</thead>
<tbody>
</tbody>
</table>
<div class = 'center'>
<p><%= link_to 'New Review', new_reviews_path(#review), :class => "btn btn-info" %></p>
<p><%= link_to 'Back', picture_path, :class => "btn btn-info" %></p>
</div>
Link to the Reviews#new page
<p><%= link_to 'New Review', new_reviews_path(#review), :class => "btn btn-info" %></p>
Reviews#New Page
<% #title = "New Review" %>
<h3>New Review</h3>
<div class = 'edit-form'>
<div class = 'center'>
<% form_for #review do |f| %>
<p>
<%= f.label :username %><br />
<%= f.text_field :username %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit "Submit Review" %>
</p>
<% end %>
</div>
</div>
<div class = 'center'>
<%= link_to 'Back', picture_reviews_path(#picture) %>
</div>
Pictures#Show Page
<% #title = "#{#picture.title}" %>
<h4 class = 'indent'>Picture Statistics</h4>
<ul id = 'view'>
<li><strong>Title:</strong> <%= #picture.title %></li>
<li><strong>Category:</strong> <%= #picture.category %></li>
<li><strong>Rating:</strong> <%= pluralize(#picture.rating, 'Star') %></li>
<li><strong>Favorited:</strong> By <%= pluralize(#picture.users.count, 'User') %></li></br>
</ul>
<% if #picture.rating > 4 %>
<button class = 'top-picture'>Top Rated</button>
<% end %>
<%= form_for #picture do |f| %>
<div class = 'indent'>
<p>
<%= f.label :stars, 'Rating' %>
<div class= "rating">
1 ☆<%= f.radio_button :stars, '1' %>
2 ☆<%= f.radio_button :stars, '2' %>
3 ☆<%= f.radio_button :stars, '3' %>
4 ☆<%= f.radio_button :stars, '4' %>
5 ☆<%= f.radio_button :stars, '5' %>
</div>
</p>
<p><input class="btn btn-info" type="submit" value="Rate"></p>
<p><%= link_to 'Reviews', picture_reviews_path(#picture), :class => "btn btn-info" %></p>
<% end %>
<p><%= link_to 'Index', pictures_path, :class => "btn btn-info" %></p>
</div>
I've tried using nested resources like so
resources :pictures do
put :favorite, on: :member
resources :reviews
end
resources :users
resources :sessions, only: [:new, :create, :destroy]
resources :contacts, only: [:new, :create]
That didn't work because It routed my pictures using :picture_id instead of the standard :id field. Since it routed to :picture_id it couldn't find any pictures.
picture_reviews_path GET /pictures/:picture_id/reviews(.:format) reviews#index
GET /pictures/:picture_id/reviews/edit/:id(.:format) reviews#edit
new_reviews_path GET /pictures/:picture_id/reviews/new(.:format) reviews#new
Picture Columns
Picture.column_names
=> ['id', 'title', 'category', 'stars', 'created_at', 'updated_at',
'ratings_count', 'ratings_total']
The problem with nesting routes, is that it calls a path using a column_name not found in the table. That is why I decided to go back to matching routes.
I believe the problem lies in my ReviewsController for which there may be duplicated code.
before_action :set_review, only: [:show, :edit, :update, :destroy]
#review = Review.find(params[:id])
def set_review
#review = Review.find(params[:id])
end
I think I could remove the #review = Review.find line from every method, but my main concern is that the set_review method was defined as a private method so that might not be possible.
Help is greatly appreciated and thanks in advanced.
Update
I think the problem lies in my new action in my ReviewsController.
This is just an extended version of #japed answer.
1. You have no route to the create or update action
Both actions works on POST request, hence url_helpers alone won't tell rails what to do with POST request when it gets it. What you need is to change your routes back to nested resources (it was good the way it was, your issue was caused by another bit of code). So, you need:
resources :pictures do
...
resources :reviews
end
Also remove all other routes for this controller as they may affect your final routes. Remeber to restart your server after changing your routes.
2. The controller:
Firstly, note that there are a lot of repetitions there - you are setting #picture in all the actions. Currently your problem is that it is using params[:id] in some actions and params[:picture_id] in others. It should always be picture_id, id should be reserved to be review's id, as you are inside reviews_controller.
The best way to do this is to create another before_filter which will set up the #picture variable:
class ReviewsContorller < ApplicationController
before_filter :set_picture
# This is perfectly fine, but needs to be executed after :set_picture
before_filter :set_review, only: [:show, :edit, :update, :destroy]
...
private
...
def set_picture
#picture = Picture.find(params[:picture_id])
end
def set_review
#review = picture.reviews.find(params[:id])
end
end
Note that the #review is pulled from #picture association - this is important security check, if you used Review.find instead, all the users are automatically able to view, edit and create new reviews for all the photos, without knowing which photo they are really commenting for. It should not be a great issue in your case, but it is good to get this into the habit.
3. The form:
<% form_for #review do |f| %>
This would seems all right, however imagine you are your application - how would you know what is the correct post url for this form? Rails is quite intelligent framework and it is trying to guess it by the resource supplied. In this case, you pass an instance of Review class, hence it will try to send the from to review_path(#review.id). The problem is, that this path does not exists in your routes, so you will get undefined_method 'review_path' here.
Also note, that the proper route you want is /picture/:picture_id/reviews for new reviews or /picture/:picture_id/review/:idfor existing reviews. Hence rails will need the parent picture object to be passed as well to figure out the rightpicture_id`. You can do this by passing an array of resources, with the one which the form is really for being the last so:
<% form_for [#picture, #review] do |f| %>
This will tell rails to look for picture_reviews_path(#picture.id) for new review or picture_review_path(#picture.id, #review.id) for existing reviews. If you have nested resources in your routes, both of those should exists.
4. Other links
Your current routes defines a named path new_reviews which will not longer exist after you use nested resources - it will be renamed to new_picture_review, so you need to change all the occurrences of new_reviews_path to new_picture_review(#picture)
As you're doing nested routes, you need to find by :picture_id as you've just found
class ReviewsController < ApplicationController
before_action { #picture = Picture.find(params[:picture_id] }
end
As your error says the issue is because reviews_path doesn't exist because you've nested it
So this
<% form_for #review do |f| %>
Wants to change to
<% form_for [#picture, #review] do |f| %>
So that it goes to the picture_reviews_path
Also this
<p><%= link_to 'New Review', new_reviews_path(#review), :class => "btn btn-info" %></p>
Wants to become
<p><%= link_to 'New Review', new_picture_reviews_path(#picture, #review), :class => "btn btn-info" %></p>
Can you use Shallow Nesting Routes? That is, you'll have a nested resource where needed, but when unambiguous you get a shorter path, with just one parameter for the review. You can still find your way back to the picture, using the picture_id in the review.
resources :pictures, shallow: true do
put :favorite, on: :member
resources :reviews, shallow: true
end
resources :users
resources :sessions, only: [:new, :create, :destroy]
resources :contacts, only: [:new, :create]
Then, improve the models to help the associations to bind well, with inverse_of:
class Review < ActiveRecord::Base
belongs_to :picture, inverse_of: :reviews
end
class Picture < ActiveRecord::Base
has_many :reviews, inverse_of: :picture
end
This should mean there's only one copy of a picture in memory. And then in the ReviewsController:
class ReviewsController < ApplicationController
before_action :set_review, only: [:show, :edit, :update, :destroy]
def index
#picture = Picture.find(params[:id])
# reference #picture.reviews to get all reviews in the view
end
def show
#picture = Picture.find(params[:id])
# use #picture.reviews to get all reviews in the view
end
def new
# where will you get the picture this belongs to?
# Need to collect the picture_id param. and build the associated review
#picture = Picture.find(param[:picture_id])
#review = #picture.reviews.build()
end
def edit
#picture = Picture.find(params[:picture_id])
# use #picture.reviews in the view controller to get the associated reviews
end
def create
#picture = Picture.find(params[:picture_id])
#review = #picture.reviews.build(params[:review])
if #review.save
flash[:notice] = 'Review was successfully created.'
redirect_to #picture
else
flash[:notice] = "Error creating review: #{#review.errors}"
redirect_to #picture
end
end
I think there's one other significant issue. You keep using a piece of code like this:
#review = Picture.find(id)
But that returns zero or more elements. It will help you understand the code better if you reflect that this is, normally, an array:
#reviews = Picture.find(id)
But even better, don't do that. You have the associations. Use them in the view.
#picture.reviews
This will return an array. If zero length, there are no reviews. If non-zero, that's how many review elements there are.
Then you won't make the mistake of picking up an array variable called #review, which appears to be singular (meaning that link_to #review appears to make sense, but will fail), and instead use an array:
<%- #picture.reviews.each do |review| %>
<% link_to review ...%>
Hope that helps!

Form for nested resource: undefined method `model_name' for NilClass:Class

I'm doing an online tutorial and was tasked with implementing a comment resource that is nested under a post resource, which in turn is under a topic resource. I've figured out all of the steps of the exercise except getting the 'comment create' form to show up on post#show. I have been trying for several hours to fix it but keep getting the following error: undefined method "model_name" for NilClass:Class. I have no idea what it is referring to by model_name and in my desperation I even looked at the tutorial's code on GitHub, but I still can't see where I'm making the mistake.
My form is in a partial and the error points to the first line, so I'll paste that code here first:
<%= form_for [topic, post, comment] do |f| %>
<div class="controls">
<%= f.text_area :body, rows: 8 %>
</div>
<div class="control-group">
<div class="controls">
<%= f.submit "Add Comment", class: 'btn' %>
</div>
</div>
<% end %>
I'll post my routes so you can see the nesting:
X::Application.routes.draw do
devise_for :users
resources :topics do
resources :posts, except: [:index] do
resources :comments, only: [:create]
end
end
match "about" => 'welcome#about', via: :get
root to: 'welcome#index'
end
Here is my Comments controller:
class CommentsController < ApplicationController
def new
end
def create
#topic = Topic.find(params[:topic_id])
#post = Post.find(params[:post_id])
#comment = current_user.comments.build(params[:comment])
#comment.post = #post
if #comment.save
flash[:notice] = "Comment was saved."
redirect_to #post
else
flash[:error] = "Error! Try again."
render :new
end
end
end
Here is my post#show view (form should be rendering at the bottom):
<h1><%= markdown #post.title %></h1>
<div class="row">
<div class="span8">
<small>
<%= image_tag(#post.user.avatar.tiny.url) if #post.user.avatar? %>
submitted <%= time_ago_in_words(#post.created_at) %> ago by
<%= #post.user.name %>
</small>
<br><br>
<p><%= image_tag(#post.image.url) if #post.image? %></p>
<p><%= markdown #post.body %></p>
</div>
<div class="span2">
<% if can? :edit, #post %>
<%= link_to "Edit Post", edit_topic_post_path(#topic,#post), class: 'btn btn-small' %>
<% end %>
</div>
</div>
<br><br>
<h4>Comments</h4>
<%= render #comments %>
<br>
<%= render :template => "/comments/_form.html.erb", locals: { topic: #topic, post: #post, comment: #comment } %>
I would be SO grateful if anyone could help me with this problem. And let me know if there's any info I should add. Thank you!
Generally from what i have read its not recommended using 3 level nested resources. Could you link to the tutorial you are following?
Anyhow I would suggest using shallow on your routes instead.
resources :topics, shallow: true do
resources :posts, except: [:index], shallow: true do
resources :comments, only: [:create]
end
end
Will give you the following resources, which in return will make it easier to create correct forms in your view.
post_comments POST /posts/:post_id/comments(.:format) comments#create
topic_posts POST /topics/:topic_id/posts(.:format) posts#create
new_topic_post GET /topics/:topic_id/posts/new(.:format) posts#new
edit_post GET /posts/:id/edit(.:format) posts#edit
post GET /posts/:id(.:format) posts#show
PATCH /posts/:id(.:format) posts#update
PUT /posts/:id(.:format) posts#update
DELETE /posts/:id(.:format) posts#destroy
topics GET /topics(.:format) topics#index
POST /topics(.:format) topics#create
new_topic GET /topics/new(.:format) topics#new
edit_topic GET /topics/:id/edit(.:format) topics#edit
topic GET /topics/:id(.:format) topics#show
PATCH /topics/:id(.:format) topics#update
PUT /topics/:id(.:format) topics#update
DELETE /topics/:id(.:format) topics#destroy
Now you should only have to send post_id to your comments form. Read more here I haven't worked much with 3 level nesting but this should give you an idea of how you could make it easier for yourself.
EDIT:
In the create action you should build the comment using #post and then set the user to current_user.
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.build(params[:comment])
#comment.user = current_user
if #comment.save
flash[:notice] = "Comment was saved."
redirect_to #post
else
flash[:error] = "Error! Try again."
render :new
end
end
And now no need for #topic
<%= render :template => "/comments/_form.html.erb", locals: { post: #post, comment: #comment } %>
Add '#comment = Comment.new' to the "show" action of the PostController. It had been defined only in the CommentController "create" action.

Resources