Rails 5 - Understanding the new action and rendering errors - ruby-on-rails

My questions is regarding section 5.10 of the official Rails guide
I have an articles model with fields Title and Text
article.rb :
class Article < ApplicationRecord
validates :title, presence: true, length: { minimum: 5 }
end
articles_controller.rb :
class ArticlesController < ApplicationController
def index
#articles = Article.all
end
def show
#article = Article.find(params[:id])
end
def new
#article = Article.new
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
render 'new'
end
end
private
def article_params
params.require(:article).permit(:title, :text)
end
end
The guide says that
#article = Article.new
needs to be added to the new action otherwise otherwise #article would be nil in our view, and calling #article.errors.any? would throw an error. Here is the new.html.erb :
<%= form_with scope: :article, url: articles_path, local: true do |form| %>
<% if #article.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#article.errors.count, "error") %> prohibited
this article from being saved:
</h2>
<ul>
<% #article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= form.label :title %><br>
<%= form.text_field :title %>
</p>
<p>
<%= form.label :text %><br>
<%= form.text_area :text %>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
<%= link_to 'Back', articles_path %>
What I am able to understand is when there is an error, it is present in #articles so #article.errors.any? helps in showing the errors when the 'new' view is rendered. It indeed works as expected but what I am not able to understand is #article = Article.new in the new action should reset the #article and the errors should be lost after the user is redirected to new. But somehow, the errors are not lost and are indeed being displayed. How is this happening ?

Both render and redirect are different things.
render Renders the content that will be returned to the browser as the response body.
redirect or redirect_to - Redirect is concerned about telling the browser it needs to make a new request to a different location or to the same location as given in path.
It is clearly mentioned in artcle 5.10
Notice that inside the create action we use render instead of redirect_to when save returns false. The render method is used so that the #article object is passed back to the new template when it is rendered. This rendering is done within the same request as the form submission, whereas the redirect_to will tell the browser to issue another request.
Note: You can read render vs redirect in detail
As per your question
def new
#article = Article.new
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
render 'new' # this will add error (validations)
end
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
redirect_to 'new' # this will not add any error as this is new request and #article will initialise again.
new #same as redirect
end
end
Edit: Creating Form Objects with ActiveModel. A form object is an object designed specifically to be passed to form_for
We always check for errors #article.errors.any? it will execute if #article object contains any error messages
Please read form_for doc.

render doesn't run any code in the new method, it just uses the new.html.x view. As such, #article = Article.new is never executed.
If you wanted the code from new run, you would need to actually call that method:
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
new #actually runs the code in the 'new' method
render 'new' # doesn't go anywhere near the new method, just uses its view
end
end

Related

Error occured “ActionController::InvalidAuthenticityToken in ArticlesController#create” when user log out and try to submit POST form

Here I use Devise to help me authenticate user. I test my app by simulating this scenario:
Open app in 2 browser tabs (A and B)
In tab A, logging in and open page called "new"
Refresh tab B, and logging out from tab B
Back to tab A then submit form "new" via tab A
But I got error:
ActionController::InvalidAuthenticityToken in
ArticlesController#create
instead of redirect user to login page.
Here's my code:
controllers/articles_controller.rb
class ArticlesController < ApplicationController
before_action :is_logged_in?
before_action :is_admin?, except: [:index, :show]
def new
#article = Article.new
end
def index
#articles = Article.all
end
def edit
#article = Article.find(params[:id])
end
def update
#article = Article.find(params[:id])
if #article.update(article_params)
redirect_to #article
else
render :edit
end
end
def destroy
#article = Article.find(params[:id])
#article.destroy
redirect_to articles_path
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
render :new
end
end
def show
#article = Article.find(params[:id])
end
private
def article_params
params.require(:article).permit(:title, :text)
end
def is_admin?
if !admin_signed_in?
flash[:notice] = "Access Forbidden!"
redirect_to root_path
end
end
def is_logged_in?
if !user_signed_in? && !admin_signed_in?
flash[:notice] = "Log in first!"
redirect_to new_user_session_url
end
end
end
routes.rb
Rails.application.routes.draw do
devise_for :admins
devise_for :users
root 'welcome#index'
resources :articles do
resources :comments
end
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
views/articles/new.html.erb
<h1>Form Create Article</h1>
<%= render 'form' %>
<%= link_to 'Back', articles_path %>
views/articles/_form.html.erb
<%= form_with model: #article, local: true do |form| %>
<% if #article.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#article.errors.count, "error") %> prohibited
this article from being saved:
</h2>
<ul>
<% #article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= form.label :title %><br>
<%= form.text_field :title %>
</p>
<p>
<%= form.label :text %><br>
<%= form.text_area :text %>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
How to make my app redirects user to login page in case user submit form when they are not authenticated? Thanks.
ActionController::InvalidAuthenticityToken in
ArticlesController#create
What is happening?
As you are logged in as same user in both the tabs, in Tab B, when you logged out from the app, a new authenticity token gets updated in the server making the already existing authenticity token in the Tab A as Invalid. So when you submit the form it fails with that exception
But why is_logged_in? check is not passed in the first place when you submit the form in Tab A?
After when you logged out from the app in Tab B, you are submitting the form in Tab A without refreshing it. Now the sessions are still active and valid in Tab A as the cookies are not updated resulting in the is_logged_in? check to fail and allowing the create action to take place.
How to make my app redirects user to login page in case user submit
form when they are not authenticated?
In a normal scenario, your code should work fine! But as your scenario is different, you probably need to rescue the InvalidAuthenticityToken error and redirect the user to login page when the form is submitted in Tab A.

Get selected value from form.select

I have 2 models - project and todo, and the project contain many todos. I'm trying to write a form that would add a todo to a specific project, that's selected in the tray.
Unfortunately I get an error
Couldn't find Project with 'id'=
from todos_controller when i redirect to /todos
#project = Project.find(params[:project_id])
/projects/index.html.erb
<h1> Задачи </h1>
<% #projects.each do |project_name|%>
<h3><%= project_name.title %></h3>
<ul>
<% project_name.todos.each do |project_todo| %>
<li>
<p><%= project_todo.text %></p>
</li>
<% end %>
</ul>
<% end %>
<h1> Новая задача </h1>
<%= form_with scope: :todo, url: todos_path, local: true do |form| %>
<p>
<% form.label :text %><br>
<%= form.text_field :title, placeholder: "Название задачи" %>
</p>
<%= form.select( :project_id, options_from_collection_for_select(Project.all, :id, :title)) %>
<p>
ОТМЕНА
<%= link_to form.submit %>
</p>
<% end %>
projects_controller.rb
class ProjectsController < ApplicationController
def index
#projects = Project.all
respond_to do |format|
format.html
format.json {render json: #projects}
end
end
todos_controller.rb
class TodosController < ApplicationController
def index
#todos = Todo.all
respond_to do |format|
format.html
format.json {render json: #todos}
end
end
def update
end
def create
#project = Project.find(params[:project_id])
#todo = #project.todo.create(todo_params)
redirect_to projects_path
end
end
routes.rb
Rails.application.routes.draw do
resources :projects, :todos
root 'projects#index'
end
I also could not display the submit button as a text link.
modify create action as
def create
#project = Project.find(params[:todo][project_id])
#todo = #project.todo.create(todo_params)
redirect_to projects_path
end
if not works then check params hash from server log and check project_id is present in params or not.
Above will work but I think you do not need to find project if you modify create action as
def create
#todo = Todo.create(todo_params)
redirect_to projects_path
end
also define method as
def todo_params
params.require(:todo).permit(:project_id, :title)
end
Your controller expects a project_id parameter which is probably not passed! Try to add the project id to your params hash when you trigger the todos#create method
params[:project_id] is nil because the form you are submitting for todo, so you need to access project_id as
Project.find(params[:todo][project_id]) in create action.
So replace your create action as follow
def create
#project = Project.find(params[:todo][project_id])
#todo = #project.todo.create(todo_params)
redirect_to projects_path
end
Dont forget to include project_id in todo_params method.

Rails routing : nested resources

I'm running into a problem when trying to create a new object using nested resources in Rails. My routing is set up as:
resources :coins do
resources :questions
end
When I attempt to create a new question, it does not save. I'm redirected to the 'questions' page and the form from the 'new' page including everything that was typed into it remains on the page (rather than the list of questions that are supposed to be there when it saves). My controller is as follows:
class QuestionsController < ApplicationController
before_action :find_question, only: [:show]
before_action :find_coin
before_action :authenticate_user!, except: [:index, :show]
def index
#questions = Question.where(coin_id: #coin.id).order("created_at DESC")
end
def show
end
def new
#coin
#question = current_user.questions.build
end
def create
#question = current_user.questions.build(question_params)
if #question.save
redirect_to coin_question(#question.coin_id, #question.id)
else
render 'new'
end
end
private
def find_question
#question = Question.find(params[:id])
end
def find_coin
#coin = Coin.find(params[:coin_id])
end
def question_params
params.require(:question).permit(:content, :ques_num, :coin_id)
end
end
My 'new' page then displays the following form:
<%= simple_form_for #question, url: coin_questions_path(#coin.id) do |f| %>
<%= f.input :ques_num %>
<%= f.input :content %>
<%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
This is my first time using nested resources and its tripping me up a little bit. I really appreciate any assistance here.
Your create action is failing and so it's executing the else statement which is just rendering back your form with the data you entered. The easiest thing to do is to just check out the log file and see why the save it being blocked.
go to /log/development.log and if you're using a mac press Command and the down arrow which will bring you all the way to the bottom of the file.
Also you may want to check out your model validations. If you don't have flash setup or aren't outputting the errors to your view a validation may be causing the form not to save and you wouldn't see the errors.
you could add some error handling to your view like this
<%= form_with(model: question, local: true) do |form| %>
<% if question.errors.any? %>
<div id="error_explanation">
<h2 class="text-danger"><%= pluralize(question.errors.count, "error") %> prohibited this question from being saved:</h2>
<ul class="text-danger">
<% question.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
for your controller try
def create
#question = Question.new(question_params)
if #question.save
flash[:success] = "question created successfully!"
redirect_to question_url(#question.id)
else
render 'new'
end
end
I think there will be error to create question.
if #question.save
redirect_to coin_question(#question.coin_id, #question.id)
else
render 'new'
end
So if record have any error to save it will redirect to new form.
Just use following code to know what is the errors to creating question
def create
#question = current_user.questions.build(question_params)
if #question.save
flash[:notice] = 'Question created'
redirect_to coin_question(#question.coin_id, #question.id)
else
flash[:notice] = 'Some error here!'
render 'new'
end
end
You need to setup flash to show flash error.

Getting undefined method `errors' for nil:NilClass Ruby on Rails Guide

Okay, I've been searching for a question here thats exactly the same as mine and I can't find one, so I'm forced to ask it myself. I'm following the guide on here: http://guides.rubyonrails.org/getting_started.html
I ran into this error saying: "undefined method `errors' for nil:NilClass" for my Edit Article page.
Here is my extracted source:
<h1>Editing article</h1>
<%= form_for :article, url: articles_path(#article), method: :patch do |f| %>
<% if #article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#article.errors.count, "error") %> prohibited
this article from being saved: </h2>
And here is my trace:
app/views/articles/edit.html.erb:4:in block in _app_views_articles_edit_html_erb___778692675__618464548
app/views/articles/edit.html.erb:3:in _app_views_articles_edit_html_erb___778692675__618464548
Here is my action Controller
class ArticlesController < ApplicationController
def new
#article = Article.new
end
def edit
#articles = Article.find(params[:id])
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
render 'new'
end
end
def show
#article = Article.find(params[:id])
end
def index
#articles = Article.all
end
def update
#article = Article.find(params[:id])
if #article.update(article_params)
redirect_to #article
else
render 'edit'
end
end
private
def article_params
params.require(:article).permit(:title, :text)
end
end
And here is my edit.html.erb
<h1>Editing article</h1>
<%= form_for :article, url: articles_path(#article), method: :patch do |f| %>
<% if #article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#article.errors.count, "error") %> prohibited this article
from being saved: </h2>
<ul>
<% #article.errors.full_messages.each do |msg| %><li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
Variable Definition
Something you might want to consider, alongside Arup's answer, is the error itself:
"undefined method `errors' for nil:NilClass"
What you're getting back is an exception because you're trying to call a method on a nil class object. As Arup pointed out, this is generally caused by your calling of an #instance_variable without defining it first
I want to highlight the fact that your error says the problem is you have an undefined method for your object. Most would treat the problem as your method is undefined for some reason; in reality the problem is you don't have the object defined.
--
Fix
The way to fix the error, as pointed out by Arup is to reference the #instance variable that's defined in the edit method, like this:
#app/controllers/articles_controller.rb
Class ArticlesController < ApplicationController
def edit
#article = Article.find params[:id]
end
end
#app/views/articles/edit.html.erb
<%= #article.errors %>
Something else you will want to consider is the following:
#app/views/articles/edit.html.erb
<%= form_for #article do |f| %>
# ...
<% end %>
Yes, you have a typo with the instance variable.
<% if #article.errors.any? %>
should be
<% if #articles.errors.any? %>
Because inside the controller action #edit, you have defined #articles not #article. But it should be named #article since it's a single article. Thus keep <% if #article.errors.any? %> as it is, change the #edit method as
def edit
#article = Article.find(params[:id])
end
Remember instance variables returns nil, if you attempt to use it before defining it in Ruby. The same happened in your case. You defined #articles, but used #article, which was not defined by you before attempting to use it, thus returns nil. And nil.errors throws the error as you see.
The problem is that #article is nil (null)
In your controller, edit the new action, so it looks like this:
def new
#article = Article.new
end
Refresh the web page, and there is no more error.
Edit new action in controller
def new
#article = Article.new
end
Since your instance of article is nill.

undefined method `errors' for nil:NilClass

Here is my Edited details:
I have my controller like,
class Enr::Rds::SurvRdsapXrefsController < Enr::Controller
def index
#enr_rds_surv_rdsap_xrefs = Enr::Rds::SurvRdsapXref.paginate(page: params[:page])
end
def show
end
def new
#enr_rds_surv_rdsap_xref = Enr::Rds::SurvRdsapXref.new
end
def edit
#enr_rds_surv_rdsap_xref = Enr::Rds::SurvRdsapXref.find(params[:id])
end
def create
#enr_rds_surv_rdsap_xref = Enr::Rds::SurvRdsapXref.new(params[:enr_rds_surv_rdsap_xref])
respond_to do |format|
if #enr_rds_surv_rdsap_xref.save
format.html { redirect_to :enr_rds_surv_rdsap_xrefs, notice: "Survey link was successfully created." }
format.js
format.json { render json: #enr_rds_surv_rdsap_xref, status: :created, location: #enr_rds_surv_rdsap_xref }
else
format.html { render action: "new" }
format.js
format.json { render json: #enr_rds_surv_rdsap_xref.errors, status: :unprocessable_entity }
end
end
end
def update
end
def destroy
end
end
Here is my view form like
<%= form_for(#enr_rds_surv_rdsap_xref, :remote => true) do |f| %>
<% if #enr_rds_surv_rdsap_xref.errors.any? %>
<div id="error_explanation">
<div class="validate">
The form contains <%= pluralize#enr_rds_surv_rdsap_xref.errors.count, "error") %>.
</div>
<ul>
<% #enr_rds_surv_rdsap_xref.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="control-group">
<%= f.label :section %><br />
<%= f.text_field :section %>
</div>
<%= f.submit %>
<% end %>
When i click the index page to create a new link, the index page showing error like
NoMethodError in Enr/rds/surv_rdsap_xrefs#index
undefined method `errors' for nil:NilClass
Thanks for the suppport and please suggest me to rectify the error. I am new to ROR. Thanks
Your error reveals that the rendering of the index template is causing the error, which means you're rendering the form for the new survey (the code snippet above) in the index template. This is fine, but if you're going to do that, you'll have to instantiate a new survey in index, as well as in new.
At the simplest, you could just copy the code in new to index:
def index
#enr_rds_surv_rdsap_xrefs = Enr::Rds::SurvRdsapXref.paginate(page: params[:page])
#enr_rds_surv_rdsap_xref = Enr::Rds::SurvRdsapXref.new
end
def new
#enr_rds_surv_rdsap_xref = Enr::Rds::SurvRdsapXref.new
end
To keep your code a bit DRYer you might change where the new instance is created. A pattern you'll often see is something similar to:
before_filter :build_record, :only => [:new, :index]
protected
def build_record
#survey = YourSurvey.new
end
This way you don't even need to write the new/index methods if you don't have any other logic.
Do you also set #survey in the new action in your controller? The error means that when the view is rendered #survey is nil, so there must be a problem with setting that instance variable.
Do you get the error when you go to the 'new' view or when you try to submit the form (create)?
The form contains <%= pluralize#enr_rds_surv_rdsap_xref.errors.count, "error") %>
This line of the code is the problem. You are lacking a "(" between the pluralize and the #enr...
Explained: RoR thinks that the object is: pluralize#enr... instead of the # All alone, and he has no errors for this kind of object.

Resources