Agile Web Development with Rails, Edition 4

14.2 Iteration I2: Authenticating Users 13.3 Playtime

14.1 Iteration I1: Adding Users

Scaffold the user model

rails generate scaffold User name:string password_digest:string
[deprecated] I18n.enforce_available_locales will default to true in the future. If you really want to skip validation of your locale you can set I18n.enforce_available_locales = false to avoid this message.
      invoke  active_record
      create    db/migrate/20140204200054_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/unit/user_test.rb
      create      test/fixtures/users.yml
       route  resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      invoke    test_unit
      create      test/functional/users_controller_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      create        test/unit/helpers/users_helper_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/users.js.coffee
      invoke    scss
      create      app/assets/stylesheets/users.css.scss
      invoke  scss
   identical    app/assets/stylesheets/scaffolds.css.scss

uncomment out bcrypt-ruby

edit Gemfile
# To use ActiveModel has_secure_password
gem 'bcrypt-ruby', '~> 3.0.0'

Restart the server.

Run the migration

rake db:migrate
mv 20140204200054_create_users.rb 20140204000009_create_users.rb
[deprecated] I18n.enforce_available_locales will default to true in the future. If you really want to skip validation of your locale you can set I18n.enforce_available_locales = false to avoid this message.
==  CreateUsers: migrating ====================================================
-- create_table(:users)
   -> 0.0009s
==  CreateUsers: migrated (0.0010s) ===========================================
 

Add validation, has_secure_password

edit app/models/user.rb
class User < ActiveRecord::Base
    attr_accessible :name, :password, :password_confirmation
  validates :name, presence: true, uniqueness: true
    has_secure_password
end

Avoid redirect after create, update operations

edit app/controllers/users_controller.rb
  def create
    @user = User.new(params[:user])
 
    respond_to do |format|
      if @user.save
        format.html { redirect_to users_url,
          notice: "User #{@user.name} was successfully created." }
        format.json { render json: @user,
          status: :created, location: @user }
      else
        format.html { render action: "new" }
        format.json { render json: @user.errors,
          status: :unprocessable_entity }
      end
    end
  end
edit app/controllers/users_controller.rb
  def update
    @user = User.find(params[:id])
 
    respond_to do |format|
      if @user.update_attributes(params[:user])
        format.html { redirect_to users_url,
          notice: "User #{@user.name} was successfully updated." }
        format.json { head :ok }
      else
        format.html { render action: "edit" }
        format.json { render json: @user.errors,
          status: :unprocessable_entity }
      end
    end
  end

Display users sorted by name

edit app/controllers/users_controller.rb
  def index
    @users = User.order(:name)
 
    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @users }
    end
  end

Add Notice

edit app/views/users/index.html.erb
<h1>Listing users</h1>
<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>
 
<table>
  <tr>
    <th>Name</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>
 
<% @users.each do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= link_to 'Show', user %></td>
    <td><%= link_to 'Edit', edit_user_path(user) %></td>
    <td><%= link_to 'Destroy', user, confirm: 'Are you sure?', method: :delete %></td>
  </tr>
<% end %>
</table>
 
<br />
 
<%= link_to 'New User', new_user_path %>

Update form used to both create and update users

edit app/views/users/_form.html.erb
<div class="depot_form">
 
<%= form_for @user do |f| %>
  <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@user.errors.count, "error") %>
        prohibited this user from being saved:</h2>
      <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
 
  <fieldset>
  <legend>Enter User Details</legend>
 
  <div class="field">
    <%= f.label :name, 'Name:' %>
    <%= f.text_field :name, size: 40 %>
  </div>
 
  <div class="field">
    <%= f.label :password, 'Password:' %>
    <%= f.password_field :password, size: 40 %>
  </div>
 
  <div class="field">
    <%= f.label :password_confirmation, 'Confirm:' %>
    <%= f.password_field :password_confirmation, size: 40 %>
  </div>
 
  <div class="actions">
    <%= f.submit %>
  </div>
 
  </fieldset>
<% end %>
 
</div>

Demonstrate creating a new user

get /users

Listing users

Name

New User
get /users/new

New user

Enter User Details
Back
post /users
You are being redirected.
get http://localhost:3000/users

Listing users

User dave was successfully created.

Name
dave Show Edit Destroy

New User

Show how this is stored in the database

sqlite3> select * from users
             id = 1
           name = dave
password_digest = $2a$10$zpQ0xHePm8N7DN2qVVGiT.2t7nAH1xndZxeKyUQYsSyYZzKxIKrZS
     created_at = 2014-02-04 20:01:01.558763
     updated_at = 2014-02-04 20:01:01.558763

Update tests to reflect the changes in redirection and uniqueness

edit test/functional/users_controller_test.rb
require 'test_helper'
 
class UsersControllerTest < ActionController::TestCase
  setup do
    @input_attributes = {
      name:                  "sam",
      password:              "private",
      password_confirmation: "private"
    }
 
    @user = users(:one)
  end
  #...
  test "should create user" do
    assert_difference('User.count') do
      post :create, user: @input_attributes
    end
 
    assert_redirected_to users_path
  end
  #...
  test "should update user" do
    put :update, id: @user.to_param, user: @input_attributes
    assert_redirected_to users_path
  end
end

Make sure that all test names are unique

edit test/fixtures/users.yml
# Read about fixtures at
# http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
 
one:
  name: dave
  password_digest: <%= BCrypt::Password.create('secret') %>
 
two:
  name: susannah
  password_digest: MyString
rake test
[deprecated] I18n.enforce_available_locales will default to true in the future. If you really want to skip validation of your locale you can set I18n.enforce_available_locales = false to avoid this message.
[deprecated] I18n.enforce_available_locales will default to true in the future. If you really want to skip validation of your locale you can set I18n.enforce_available_locales = false to avoid this message.
Loaded suite /home/rubys/.rvm/gems/ruby-1.9.2-p320/gems/rake-10.1.1/lib/rake/rake_test_loader
Started
 
CartTest:
     PASS add duplicate product (0.66s) 
     PASS add unique products (0.01s) 
 
ProductTest:
     PASS image url (0.04s) 
     PASS product attributes must not be empty (0.00s) 
     PASS product is not valid without a unique title (0.00s) 
     PASS product is not valid without a unique title - i18n (0.00s) 
     PASS product price must be positive (0.00s) 
 
Finished in 0.726706 seconds.
 
7 tests, 28 assertions, 0 failures, 0 errors, 0 skips
[deprecated] I18n.enforce_available_locales will default to true in the future. If you really want to skip validation of your locale you can set I18n.enforce_available_locales = false to avoid this message.
Loaded suite /home/rubys/.rvm/gems/ruby-1.9.2-p320/gems/rake-10.1.1/lib/rake/rake_test_loader
Started
 
CartsControllerTest:
     PASS should create cart (0.30s) 
     PASS should destroy cart (0.08s) 
     PASS should get edit (0.06s) 
     PASS should get index (0.01s) 
     PASS should get new (0.01s) 
     PASS should show cart (0.01s) 
     PASS should update cart (0.01s) 
 
LineItemsControllerTest:
     PASS should create line item (0.04s) 
     PASS should create line item via ajax (0.09s) 
     PASS should destroy line item (0.00s) 
     PASS should get edit (0.01s) 
     PASS should get index (0.01s) 
     PASS should get new (0.01s) 
     PASS should show line item (0.01s) 
     PASS should update line item (0.01s) 
 
OrderNotifierTest:
     PASS received (0.09s) 
     PASS shipped (0.06s) 
 
OrdersControllerTest:
     PASS requires item in cart (0.01s) 
     PASS should create order (0.02s) 
     PASS should destroy order (0.01s) 
     PASS should get edit (0.01s) 
     PASS should get index (0.01s) 
     PASS should get new (0.01s) 
     PASS should show order (0.00s) 
     PASS should update order (0.01s) 
 
ProductsControllerTest:
     PASS can't delete product in cart (0.01s) 
     PASS should create product (0.05s) 
     PASS should destroy product (0.00s) 
     PASS should get edit (0.01s) 
     PASS should get index (0.01s) 
     PASS should get new (0.01s) 
     PASS should show product (0.01s) 
     PASS should update product (0.01s) 
 
StoreControllerTest:
     PASS markup needed for store.js.coffee is in place (0.02s) 
     PASS should get index (0.01s) 
 
UsersControllerTest:
     PASS should create user (0.09s) 
     PASS should destroy user (0.00s) 
     PASS should get edit (0.01s) 
     PASS should get index (0.01s) 
     PASS should get new (0.04s) 
     PASS should show user (0.01s) 
     PASS should update user (0.09s) 
 
Finished in 1.228006 seconds.
 
42 tests, 72 assertions, 0 failures, 0 errors, 0 skips
[deprecated] I18n.enforce_available_locales will default to true in the future. If you really want to skip validation of your locale you can set I18n.enforce_available_locales = false to avoid this message.
Loaded suite /home/rubys/.rvm/gems/ruby-1.9.2-p320/gems/rake-10.1.1/lib/rake/rake_test_loader
Started
 
DslUserStoriesTest:
     PASS buying a product (0.68s) 
     PASS two people buying (0.13s) 
 
UserStoriesTest:
     PASS buying a product (0.06s) 
 
Finished in 0.868600 seconds.
 
3 tests, 47 assertions, 0 failures, 0 errors, 0 skips

14.2 Iteration I2: Authenticating Users 13.3 Playtime