Ch 10: Updating, Showing, Deleting Users Flashcards
Create new branch for updating users
git checkout -b updating-users /
Retrieving appropriate user
app/controllers/users_controller.rbclass UsersController < ApplicationController..def edit @user = User.find(params[:id]) end
Create edit view which is very similar to new view
Create edit view which is very similar to new view
<% provide(:title, 'Edit User') %><%= render :layout => 'shared/card_container', :locals => {:title => 'Sign Up'} do %> <%= form_for(@user) do |f| %> <%= 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="http://gravatar.com/emails" target="_blank">change^/a> ^/div><% end %>
- The “Name” and “Email” fields in Figure 10.2 also shows how Rails automatically pre-fills the Name and Email fields using the attributes of the existing @user variable.
target=”_blank”
target=”_blank”
- neat trick to get the browser to open the page in a new window or tab, which is sometimes convenient behavior when linking to third-party sites.
hidden input field
hidden input field
^input name=""_method"" type=""hidden"" value=""patch"" />
- Examining the HTML of the page shows the use of hidden values
- Since web browsers can’t natively send PATCH requests as required by the REST conventions, Rails fakes it with a POST request and a hidden input field.
- how does Rails know to use a POST request for new users and a PATCH for editing users? The answer is that it is possible to tell whether a user is new or already exists in the database via Active Record’s new_record? boolean method:”
$ rails console >> User.new.new_record? => true >> User.first.new_record? => false
* When constructing a form using form\_for(@user), Rails uses POST if @user.new\_record? is true and PATCH if it is false.
settings link
settings link
<%= link_to "Settings", edit_user_path(current_user) %>
- This is easy using the named route edit_user_path from Table 7.1, together with the handy current_user helper method
factoring forms
factoring forms
- to consolidate the similar form for new and edit user you just create a _form.html.erb file for the user then pass the variable to the appropriate path to submit for each form
- creating _form file”
app/views/users/_form.html.erb <%= form_for(@user) do |f| %> <%= render 'shared/error_messages', object: @user %> <%= 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 %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit yield(:button_text), class: "btn btn-primary" %> <% end %> /
- factoring new”
app/views/users/new.html.erb <% provide(:title, 'Sign Up') %> <% provide(:button_text, 'Create my account') %> <%= render :layout => 'shared/card_container', :locals => {:title => 'Sign Up'} do %> <%= render 'form' %> <% end %> /
- factoring edit
app/views/users/edit.html.erb<% provide(:title, 'Edit User') %><% provide(:button_text, 'Save changes') %><%= render :layout => 'shared/card_container', :locals => {:title => 'Sign Up'} do %> <%= render 'form' %> ^div class="gravatar_edit"> <%= gravatar_for @user %> ^a href="http://gravatar.com/emails" target="_blank">change^/a> ^/div><% end %>
Create update action for users_controller.rb
Create update action for users_controller.rb
app/controllers/users_controller.rb..def update @user = User.find(params[:id]) if @user.update_attributes(user_params) # Handle a successful update. else render 'edit' end end
- Note the use of user_params in the call to update_attributes, which uses strong parameters to prevent mass assignment vulnerability
- update_attributes: updates the user based on the submitted params hash
create an integration test to catch any regressions of edit form
create an integration test to catch any regressions of edit form
rails generate integration_test users_edit /
test code
test code
test/integration/users_edit_test.rbrequire 'test_helper'class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:stephen) end test "unsuccessful edit" do get edit_user_path(@user) assert_template 'users/edit' patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } } assert_template 'users/edit' endend
- Note the use of the patch method to issue a PATCH request, which follows the same pattern as get, post, and delete.
successful edit code
successful edit code
test/integration/users_edit_test.rbrequire 'test_helper'class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . .test "successful edit" do get edit_user_path(@user) assert_template 'users/edit' name = "Foo Bar" email = "foo@bar.com" 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
- Check for a nonempty flash message and a successful redirect to the profile page
- Verify that the user’s information correctly changed in the database.
- The password and confirmation are blank, which is convenient for users who don’t want to update their passwords every time they update their names or email addresses.
- @user.reload reloads the user’s values from the database and confirm that they were successfully updated.
Update the update action so it will get the tests to pass is similar to the final form of the create action
Update the update action so it will get the tests to pass is similar to the final form of the create action
app/controllers/users_controller.rbclass UsersController < ApplicationController . . .def update @user = User.find(params[:id]) if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end /
allow_nil: true so users can input empty password on update
allow_nil: true so users can input empty password on update
class User < ApplicationRecord attr_accessor :remember_token before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } has_secure_password validates :password, presence: true, length: { minimum: 6 }, allow_nil: true . . .end
- make an exception to the password validation if the password is empty. We can do this by passing the allow_nil: true option to validates
- In case you’re worried that Listing 10.13 might allow new users to sign up with empty passwords, recall hat has_secure_password includes a separate presence validation that specifically catches nil passwords.
authentication vs. authoraization
authentication vs. authoraization
- authentication allows us to identify users of our site, while authorization lets us control what they can do.
before_action
before_action
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # Before filters # Confirms a logged-in user. def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end endend
- arrange for a particular method to be called before the given actions.
- To require users to be logged in, we define a logged_in_user method and invoke it using before_action :logged_in_user,
- By default, before filters apply to every action in a controller, so here we restrict the filter to act only on the :edit and :update actions by passing the appropriate only: options hash.
fixing test to have user logged in first
fixing test to have user logged in first
test/integration/users_edit_test.rbrequire 'test_helper'class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do log_in_as(@user) get edit_user_path(@user) . . . end test "successful edit" do log_in_as(@user) get edit_user_path(@user) . . . endend
- add log_in_as(@user) to unsuccessful edit and successful edit
- start creating a successful edit test with log_in_as as well
Add test for checking if the before action is doing its job
Add test for checking if the before action is doing its job
test/controllers/users_controller_test.rbrequire 'test_helper'class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "should redirect edit when not logged in" do get edit_user_path(@user) assert_not flash.empty? assert_redirected_to login_url 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_url endend
- Because the before filter operates on a per-action basis, we’ll put the corresponding tests in the Users controller test.
- The plan is to hit the edit and update actions with the right kinds of requests and verify that the flash is set and that the user is redirected to the login path.
- The second test involves using the patch method to send a PATCH request to user_path(@user). Such a request gets routed to the update action in the Users controller, as required.
Create a test that check if users are only be allowed to edit their own information.
Create a test that check if users are only be allowed to edit their own information.
- Create another use in the fixture file to test if one user can edit the other
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %>archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %>
Add tests for redirection when trying to edit wrong user
Add tests for redirection when trying to edit wrong user
test/controllers/users_controller_test.rbrequire 'test_helper'class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end . . . 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_url 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_url endend
- Add other user to setup
Define and add a before action for correct user in the controller
Define and add a before action for correct user in the controller
before_action :correct_user, only: [:edit, :update]...# Confirms the correct user. def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user end /
refactor some by adding a current_user? boolean method for use in the correct_user before filter
refactor some by adding a current_user? boolean method for use in the correct_user before filter
- add it to the sessions helper
app/helpers/sessions_helper.rbmodule SessionsHelper . . . # Returns true if the given user is the current user. def current_user?(user) user == current_user end # Returns the user corresponding to the remember token cookie. def current_user . . . end . . .end /
- add your new boolean to your new correct_user method
def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end
friendly forwarding
friendly forwarding
- sending the user to their own intended page instead of somebody else’s
- e.g. with an unfriendly forward, if a non-logged-in user tries to visit the edit page, after logging in the user will be redirected to /users/1 instead of /users/1/edit.
- It would be much friendlier to redirect them to their intended destination instead.
Change successful edit test in users_edit_test to add friendly forwarding
Change successful edit test in users_edit_test to add friendly forwarding
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 Bar" email = "foo@bar.com" 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
- the resulting test tries to visit the edit page, then logs in, and then checks that the user is redirected to the edit page instead of the default profile page.
- also removes the test for rendering the edit template since that’s no longer the expected behavior.
- Now that we have a failing test, we’re ready to implement friendly forwarding.