This article deepens my understanding by writing a Rails tutorial commentary article to further solidify my knowledge It is part of my study. In rare cases, ridiculous content or incorrect content may be written. Please note. I would appreciate it if you could tell me implicitly ...
Source Rails Tutorial 6th Edition
Since we were able to implement user creation, login, and memory of login information Next, create update, display, and delete functions that were left unattended in user resources.
Edit the edit action to update the user. The new actions of the sessions controller that we have implemented so far Prepare a form like the new action of the Users controller and You can implement the operation of sending the input value of the form to the update action. Of course, the user can edit it, but using the authentication implemented so far We will implement access control.
The edit page contains the target user's ID in the URL ex) users/1/edit
Use this to extract the user from the URL ID and save it in an instance variable.
def edit
@user = User.find(id: params[:id])
end
By doing this, specify the `` `@ user``` object as the model object in the form to be created next.
erb:edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(model: @user, local: true) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="https://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
Reusing the _error_messages partial to display an error message when entering an invalid value in a form.
Also, there is `` `target =" _ blank "``` in the link part of gravatar, but by writing like this, you can display the link destination in a new tab.
In addition, the value currently contained in the @user variable is automatically entered in the input field of the form. It seems that Rails will pull the attribute information automatically saved and display it. Guu competent.
Except for the actual HTML generated from this erb
<input type="hidden" name="_method" value="patch">
There is such a description. Rails can't send a PATCH request, which is an update request, because the web browser can't send it.
By specifying patch in the hidden input field, it is forged as a pseudo PATCH request.
Another thing to keep in mind is that the new and edit actions use almost the same erb code
Why Rails can tell if it's a new user or an existing user
This is because ActiveRecord's `new_record?`
Method can be used to determine whether it is new or existing.
>> new_user = User.new
(1.3ms) SELECT sqlite_version(*)
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil>
>> user1 = User.first
User Load (0.6ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "[email protected]", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-20 03:53:57", password_digest: [FILTERED], remember_digest: "$2a$12$tYO.HIfYezXpTk2zRp9s6uqJY4wUkPM28NfYuJ7vxq/...">
>> new_user.new_record?
=> true
>> user1.new_record?
=> false
>>
In the actual erbform_For model objects when using with
new_record?```Look at the result of
Determine if it is post or patch.
Finally, set a link to the edit action in the navigation bar.
<li><%= link_to "settings", edit_user_path(current_user) %></li>
#####Exercise 1.I was curious, so as a result of investigating what kind of vulnerabilities there are, the following article was helpful, so I will leave it. https://webegins.com/target-blank/ "noopener"Super important!
2.The form part is almost the same in the new view and the edit view. It's about the text of the submit button. Therefore, use the provide method to change the text content of the submit button. Partially refactor together.
erbb:_form.html.erb
<%= form_with(model: @user, local: true) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit yield(:btn_text), class: "btn btn-primary" %>
<% end %>
erb:new.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:btn_text, "Create my account") %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render 'form' %>
</div>
</div>
erb:edit.html.erb
<% provide(:title, "Edit user") %>
<% provide(:btn_text, "Save changes") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render 'form' %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="https://gravatar.com/emails" target="_blank" rel="noopener">change</a>
</div>
</div>
</div>
####Editing failure As with user registration, we will implement it regarding editing failures when trying to update with an invalid value. We will implement the update action, but it is the same as creating a user using params with the create action. Update using the params sent from the edit action. The structure is quite similar. Of course, it is dangerous to update the DB directly with params, so this time also StrongParameter(Previously defined user_params method)use.
def update
@user = User.find(params[:id])
if @user.update(user_params)
else
render 'edit'
end
end
At the moment with User model validation_error_messages Because there is a partial It is designed to return an error for invalid values.
#####Exercise 1.Fail.
####Test when editing fails
Create user editing-related integration tests.
This time, as the title suggests, I will write a test when editing fails.
rails g integration_test user_edit
users_edit_test.rb
require 'test_helper'
class UserEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
patch user_path(@user) , params:{user:{name: "",
email: "foo@bar",
password: "foo",
password_confirmation: "bar"}}
assert_template 'users/edit'
end
end
1.GET request on edit page, check if edit page is drawn 2.Send patch request, invalid value to update action 3.Check if the edit page is redrawn.
It becomes a test in the order of.
#####Exercise
1. assert_select 'div.alert', "The form contains 4 errors."
####Successful editing with TDD This time, we will implement the operation at the time of success. User⁻ The image is already working because it is implemented in Gravatar. name,email,Implement successful editing of other attributes such as password.
A test to write an integration test before implementing a feature and determine if the feature is acceptable when the feature has been implemented Is called an "acceptance test". Let's actually implement successful editing with TDD.
It is easy to understand if you implement it by referring to the test at the time of failure that you implemented earlier.(Of course we will send valid data)
test "successful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
name = "foo"
email = "[email protected]"
patch user_path(@user) , params:{user:{name: name,
email: email,
password: "",
password_confirmation: ""}}
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
Deepen your understanding by writing Rails tutorial commentary articles to make sure It is part of my study. In rare cases, ridiculous content or incorrect content may be written. Please note. I would appreciate it if you could tell me implicitly ...
Source Rails Tutorial 6th Edition
###What to do in this chapter Since we were able to implement user creation, login, and memory of login information Next, create update, display, and delete functions that were left unattended in user resources.
###Update user Edit the edit action to update the user. The new actions of the sessions controller that we have implemented so far Prepare a form like the new action of the Users controller and You can implement the operation of sending the input value of the form to the update action. Of course, the user can edit it, but using the authentication implemented so far We will implement access control.
####Edit form The edit page contains the target user's ID in the URL ex) users/1/edit
Use this to extract the user from the URL ID and save it in an instance variable.
def edit
@user = User.find(id: params[:id])
end
By doing this, the next form you will create will be a model object.@user
Specify the object.
erb:edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(model: @user, local: true) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="https://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
To display an error message when entering an invalid value in the form_error_messages Reusing partials.
Also at the link part of gravatartarget="_blank"
However, by writing like this, the link destination can be displayed in a new tab.
Furthermore, in the input field of the form@The value currently contained in the user variable is automatically entered. It seems that Rails will pull the attribute information automatically saved and display it. Guu competent.
Except for the actual HTML generated from this erb
<input type="hidden" name="_method" value="patch">
There is such a description. Rails can't send a PATCH request, which is an update request, because the web browser can't send it.
By specifying patch in the hidden input field, it is forged as a pseudo PATCH request.
Another thing to keep in mind is that the new and edit actions use almost the same erb code
Why Rails can tell if it's a new user or an existing user
ActiveRecordnew_record?
This is because the method can determine whether it is new or existing.
>> new_user = User.new
(1.3ms) SELECT sqlite_version(*)
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil>
>> user1 = User.first
User Load (0.6ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "[email protected]", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-20 03:53:57", password_digest: [FILTERED], remember_digest: "$2a$12$tYO.HIfYezXpTk2zRp9s6uqJY4wUkPM28NfYuJ7vxq/...">
>> new_user.new_record?
=> true
>> user1.new_record?
=> false
>>
In the actual erb, when using ``` form_with, see the result of
new_record?` `` For the model object.
Determine if it is post or patch.
Finally, set a link to the edit action in the navigation bar.
<li><%= link_to "settings", edit_user_path(current_user) %></li>
I was curious, so as a result of investigating what kind of vulnerabilities there are, the following article was helpful, so I will leave it. https://webegins.com/target-blank/ "noopener" super important!
The form part is almost the same in the new view and the edit view. It's about the text of the submit button. Therefore, use the provide method to change the text content of the submit button. Partially refactor together.
erbb:_form.html.erb
<%= form_with(model: @user, local: true) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit yield(:btn_text), class: "btn btn-primary" %>
<% end %>
erb:new.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:btn_text, "Create my account") %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render 'form' %>
</div>
</div>
erb:edit.html.erb
<% provide(:title, "Edit user") %>
<% provide(:btn_text, "Save changes") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render 'form' %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="https://gravatar.com/emails" target="_blank" rel="noopener">change</a>
</div>
</div>
</div>
As with user registration, we will implement it regarding editing failures when trying to update with an invalid value. We will implement the update action, but it is the same as creating a user using params with the create action. Update using the params sent from the edit action. The structure is quite similar. Of course, it is dangerous to update the DB directly with params, so we will use StrongParameter (previously defined user_params method) this time as well.
def update
@user = User.find(params[:id])
if @user.update(user_params)
else
render 'edit'
end
end
Because there is a User model validation and a _error_messages partial at this time It is designed to return an error for invalid values.
Create user editing-related integration tests.
This time, as the title suggests, I will write a test when editing fails.
rails g integration_test user_edit
users_edit_test.rb
require 'test_helper'
class UserEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
patch user_path(@user) , params:{user:{name: "",
email: "foo@bar",
password: "foo",
password_confirmation: "bar"}}
assert_template 'users/edit'
end
end
It becomes a test in the order of.
1. assert_select 'div.alert', "The form contains 4 errors."
This time, we will implement the operation at the time of success. User⁻ The image is already working because it is implemented in Gravatar. Implement successful editing of other attributes such as name, email, password.
A test to write an integration test before implementing a feature and determine if the feature is acceptable when the feature has been implemented Is called an "acceptance test". Let's actually implement successful editing with TDD.
It will be easier to understand if you implement it by referring to the test at the time of failure that you implemented earlier. (Of course I will send valid data)
test "successful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
name = "foo"
email = "[email protected]"
patch user_path(@user) , params:{user:{name: name,
email: email,
password: "",
password_confirmation: ""}}
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
Of course the test fails. First, it doesn't implement flash messages. You have not specified a redirect. These two get caught. And the most important part. Since the password value is empty, validation is caught and it cannot be updated normally.
The former two are implemented on this line.
def update
@user = User.find(params[:id])
if @user.update(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
At this point, @ user.update has a blank password and is caught in validation and branches to an else statement.
The test doesn't work either.
As a countermeasure, add exception handling when the password is empty.
In such a case, it is convenient to use the option allow_nil: true
.
With this, even if it is blank, it will not be caught in validation.
This option passes the existence verification, but when the object is created on the `` `has_secure_passwordmethod side Since the validation of existence works, nil is repelled when creating a new one, and if it is nil when updating, the password is not changed. The operation can be realized. In addition, the validation defined in the model by adding this
allow_nil: trueoption
has_secure_password```It also solves the problem that the same error message is displayed due to duplicate method validation.
Succeed
The default Gravatar image is displayed instead.
Authentication in a web application identifies the user. Authorization is to manage the range of operations that can be performed by the user. The update and edit actions that have been implemented so far have major flaws, In the current state, all users can be edited regardless of which user is logged in. The Setting link in the navigation bar takes you to the edit page of the logged-in user If you specify the edit action of various users directly in the URL, you can access it and update it.
This is bad, so change the behavior to the correct one. In particular If you are not logged in, forward to the login page + message is displayed. If you are logged in but are trying to access another user, forward to the root URL.
Before the edit, update action is executed using the before filter in the Users controller Be sure to implement to force login.
before_action :logged_in_user, only:[:edit,update]
.
.
.
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
By implementing in this way, the logged_in_user method is always executed before executing the edit and update actions. When you are not logged in, a flash message prompts you to log in. Redirect to the login page.
And at this stage, if you access the edit view without logging in, you will be taken to the login page. The test fails because it is now skipped.
In user_edit_test.rb, log in before accessing the edit action so that the test will pass. Use the log_in_as method because it is defined for testing.
The test will now pass. However, commenting out the before_action line does not reject the test. This is a serious security hole and it's bad if it doesn't get hit in the test I will fix it so that it will repel in the test firmly.
test "should redirect edit when not logged in" do
get edit_user_path(@user)
assert_not flash.empty?
assert_redirected_to login_path
end
test "should redirect update when not logged in" do
patch user_path(@user), params:{ user: {name: @user.name,
email: @user.email }}
assert_not flash.empty?
assert_redirected_to login_path
end
By adding tests like this To always test if log_in_user is running before executing the edit, update actions It will open security holes in tests.
FAIL["test_should_get_new", #<Minitest::Reporters::Suite:0x00007f1d1cf4dab8 @name="UsersControllerTest">, 0.06502773099964543]
test_should_get_new#UsersControllerTest (0.07s)
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/login>
Response body: <html><body>You are being <a href="http://www.example.com/login">redirected</a>.</body></html>
test/controllers/users_controller_test.rb:10:in `block in <class:UsersControllerTest>'
FAIL["test_invalid_signup_information", #<Minitest::Reporters::Suite:0x00007f1d1cff74c8 @name="UsersSignupTest">, 0.08553676799965615]
test_invalid_signup_information#UsersSignupTest (0.09s)
expecting <"users/new"> but rendering with <[]>
test/integration/users_signup_test.rb:12:in `block in <class:UsersSignupTest>'
FAIL["test_valid_signup_information", #<Minitest::Reporters::Suite:0x00007f1d1d0494d0 @name="UsersSignupTest">, 0.09624041300003228]
test_valid_signup_information#UsersSignupTest (0.10s)
"User.count" didn't change by 1.
Expected: 2
Actual: 1
test/integration/users_signup_test.rb:20:in `block in <class:UsersSignupTest>'
9/9: [================================================================] 100% Time: 00:00:01, Time: 00:00:01
Next, even if you are logged in, you will not be able to edit unless you are the person. We will proceed with TDD.
First, add a second user to fixture to create a situation where you log in as another user.
users.yml
archer:
name: Sterling Archer
email: [email protected]
password_digest: <%= User.digest('password') %>
Then log in as @other_user in the test and Write a test to update @user.
test "should redirect edit when logged in as wrong user" do
log_in_as(@other_user)
get edit_user_path(@user)
assert flash.empty?
assert_redirected_to root_path
end
test "should redirect update when logged in as wrong user" do
log_in_as(@other_user)
patch user_path(@user), params:{ user: { name: @user.name,
email: @user.email}}
assert flash.empty?
assert_redirected_to root_path
end
This is a test because the flash message is not displayed and is just skipped to the root URL.
I write a test and of course it doesn't pass Write the code to pass the test.
Specifically, if you create a correct_user
method and there is no user before executing the edit, update action
Write the process to skip to the root URL.
users_controller.rb
before_action :correct_user, only:[:edit,:update]
private
def correct_user
@user = User.find(params[:id])
redirect_to root_url unless @user == current_user
end
The test will now pass.
Finally, define the `current_user? ``` method and incorporate it into the
`correct_user``` method you defined earlier.
def correct_user
@user = User.find(params[:id])
redirect_to root_url unless current_user?(@user)
end
def current_user?(user)
user && user == current_user
end
If you did not protect the update action, directly with the curl command etc. without going through the edit action (edit page) If you send a value, you can update it.
The edit action is easier to test. (Actually log in and display the edit path of another user)
It also makes this update feature useful. In particular
logged_in_user
When the method repels access to the edit page of an unlogged-in user and jumps to the login page
If you log in as it is, you will be skipped to the user details page (show) without asking questions,
It's a bit inconvenient to see the show page when I log in to access the edit page.
Improve this so that when you log in, you will be taken to the edit page (friendly forwarding)
Since the test can be implemented like this Check to access the edit page without logging in and redirect to the edit page after logging in.
test "successful edit with friendly forwarding" do
get edit_user_path(@user)
log_in_as(@user)
assert_redirected_to edit_user_url(@user)
name = "foo"
email = "[email protected]"
patch user_path(@user) , params:{user:{name: name,
email: email,
password: "",
password_confirmation: ""}}
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
Now that I've written a test that fails at this point, I'll write the code so that this test passes. Write a process to save the page at the time of request and redirect to it at login.
Define a method in sessions_helper.
sessions_helper.rb
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
def store_location
session[:forwarding_url] = request.original_url if request.get?
end
store_location
Then
I am writing a process to save the URL of the request destination in a temporary session.
At this time, I have to save only the GET request
In the unlikely event that you log in to access the form page and delete the intentionally saved login information cookies When I submit the contents of the form, URLs such as post and patch are saved. If you use the `` `redirect_back_or``` method in that state, a GET request will be sent to the URL that expects post, patch, etc. by redirect. It will be sent and there is a high possibility that an error will occur. You can avoid these risks by focusing on GET requests.
logged_in_user
To the methodstore_location
Put the method, save the request destination url,
By putting the `redirect_back_or method`
in the create action of sessions_controller
At login, if there is a URL saved in the session, redirect to it.
sessions_controller.rb
def create
@user = User.find_by(email: params[:session][:email].downcase)
if @user&.authenticate(params[:session][:password])
log_in(@user)
params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
redirect_back_or @user
else
flash.now[:danger] = "Invalid email/password combination"
render 'new'
end
end
users_controller.rb
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
By the way, unless there is a return or a direct call to the last line of the method The redirect is done at the end of the method.
The test passes with the above contents.
user_edit_test.rb
test "successful edit with friendly forwarding" do
get edit_user_path(@user)
log_in_as(@user)
assert_nil session[:forwarding_url] #Add here
assert_redirected_to edit_user_url(@user)
name = "foo"
email = "[email protected]"
patch user_path(@user) , params:{user:{name: name,
email: email,
password: "",
password_confirmation: ""}}
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
(byebug) session[:forwarding_url]
"https://12b7e3b6aec94b45960b81560e233372.vfs.cloud9.us-east-2.amazonaws.com/users/1/edit"
(byebug) request.get?
true
I've been doing more and more at the end of the chapter I will summarize it for the time being.
rails t
git add -A
git commit -m "Finish user edit, update, index and destroy actions"
git co master
git merge updating-users
git push
rails t
git push heroku
heroku pg:reset DATABASE
heroku run rails db:migrate
heroku run rails db:seed
The production DB is `pg: reset DATABASE`
. In addition, the application name that resets the DB to prevent mistakes
You will be asked to enter it, so enter it and reset
Or with the --confirm option
reset DATABASE -c App name
You can do it.
After that, migrate and add samples on heroku and finish.
[To the previous chapter](https://qiita.com/take_webengineer/items/48bb1a43ffac4290959f)
[To the next chapter]()
Recommended Posts