Ruby on Rails Tutorial (5)

Chapter 6 突入。イベント運営しつつぼちぼち進めます。つうか何故にここでテザリングなんだと小一時間orz
Chapter 9 までは User 云々が続く模様。とりあえずブランチを作成。

$ git checkout master
$ git checkout -b modeling-users

6.1 User model

まずはデータ構造を云々とのこと。signup なページの mockup も示されています。そいえば model を云々するのはここでは初めてになるんですね。
テキストにざっくり目を通した後に以下をナニ。

$ rails generate controller Users new --no-test-framework
$ rails generate model User name:string email:string
$ bundle exec rake db:migrate

あ、app/view/users ってのは作ってましたね。controller の generate な出力が以下。

   identical  app/controllers/users_controller.rb
       route  get "users/new"
      invoke  erb
       exist    app/views/users
    conflict    app/views/users/new.html.erb
       force    app/views/users/new.html.erb
      invoke  helper
   identical    app/helpers/users_helper.rb
      invoke  assets
      invoke    coffee
   identical      app/assets/javascripts/users.js.coffee
      invoke    scss
   identical      app/assets/stylesheets/users.css.scss

あら? これって一回ヤッてるんかな。。あ、やっぱそうでした。全部元に戻すのって git checkout 何だったか。

$ git checkout .

元に戻ったはずなので、model の generate を実行。出力が以下。

      invoke  active_record
      create    db/migrate/20130119012313_create_users.rb
      create    app/models/user.rb
      invoke    rspec
      create      spec/models/user_spec.rb

migrate のソレがタイムスタンプになってます。確かにねぇ、というカンジ。そのまま rake db:migrate もナニ。

$ bundle exec rake db:migrate
==  CreateUsers: migrating ====================================================
-- create_table(:users)
   -> 0.0523s
==  CreateUsers: migrated (0.0525s) ===========================================

中身確認。

$ sqlite3 db/development.sqlite3
SQLite version 3.7.9 2011-11-01 00:52:41
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .schema
CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "email" varchar(255), "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL);
CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
sqlite> .tables
schema_migrations  users            

で、一回 rollback もかけてみるなど。

$ bundle exec rake db:rollback
==  CreateUsers: reverting ====================================================
-- drop_table("users")
   -> 0.0691s
==  CreateUsers: reverted (0.0693s) ===========================================

で、もっかい接続して状態確認。

$ sqlite3 db/development.sqlite3 
SQLite version 3.7.9 2011-11-01 00:52:41
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .schema
CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
sqlite> .tables
schema_migrations

再度作成。

$ bundle exec rake db:migrate
6.1.2 The model file

某イベント対応がちょい落ち着いたので続きに着手。Model annotatetion て何でしょ、と言いつつ Gemfile に以下を追加して云々。

group :development do
  gem 'annotate', '2.5.0'
end

で、

$ bundle exec annotate

したら確かにこうなりました。すばらです。

$ cat app/models/user.rb 
# == Schema Information
#
# Table name: users
#
#  id         :integer          not null, primary key
#  name       :string(255)
#  email      :string(255)
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class User < ActiveRecord::Base
  attr_accessible :email, :name
end
6.1.3 Creating user objects
$ rails console --sandbox
Loading development environment in sandbox
Any modifications you make will be rolled back on exit
>> 

の中で User なソレを云々してますね。一通りヤッてみますがログはスルーで。
つうか --sandbox 良いですね。exit したら rollback されるのか。

6.1.4 Finding user objects

ここもスルー。

6.1.5 Updating user objects

update_attributes ての、知りませんでした。ここもログはスルーで目を通すのみ。

6.2 User validations

今は何でもあり状態なので validation を云々、ということなのか。

6.2.1 Initial user tests

初期状態が以下 (コメント除く)。

require 'spec_helper'

describe User do
  pending "add some examples to (or delete) #{__FILE__}"
end

これを実行してみるとどうなるかというと

$ bundle exec rspec spec/models/
Rack::File headers parameter replaces cache_control after Rack 1.5.
*

Pending:
  User add some examples to (or delete) /home/rms/OLDHome/rms/Documents/rails_proj/sample_app/spec/models/user_spec.rb
    # No reason given
    # ./spec/models/user_spec.rb:15

Finished in 0.0005 seconds
1 example, 0 failures, 1 pending

Randomized with seed 16161

とのこと。とりあえず試験を追加していくのかな。
以下を盛り込んで試験実行。

require 'spec_helper'

describe User do

  before { @user = User.new(name: "Example User", email: "user@example.com") }

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }
end

試験 red 終了。おかしいな。って以下を云々しないといけない模様。

$ bundle exec rake db:test:prepare

sqlite3 で接続してみても何も出なかったので、まさかとは思ったのですがやはりでしたか。この後に試験実行リトライしてみたら green 終了でした。

6.2.2 Validating presence

name 属性がブランクだと invalid なソレを盛り込む模様。とりあえず以下な試験を追加とのこと。

  it { should be_valid }

  describe "when name is not present" do
    before { @user.name = " " }
    it { should_not be_valid }
  end

今はまだ model に validate なソレを追加していないので試験は失敗するはず。

Failures:

  1) User when name is not present 
     Failure/Error: it { should_not be_valid }
       expected valid? to return false, got true
     # ./spec/models/user_spec.rb:27:in `block (3 levels) in <top (required)>'

では以下を model に追加して

validates :name, presence: true

試験リトライで green 確認。今度は email の試験ってことで試験を以下に。

require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }

  it { should be_valid }

  describe "when name is not present" do
    before { @user.name = " " }
    it { should_not be_valid }
  end

  describe "when email is not present" do
    before { @user.email = " " }
    it { should_not be_valid }
  end
end

当り前ですが試験は red ですね。で、こちらも validate を追加して green 終了。

6.2.3 Length validation

長さチェック。以下な試験を追加。

  describe "when name is too long" do
    before { @user.name = "a" * 51 }
    it { should_not be_valid }
  end

当り前ですが結果は red 終了。name 属性な validate を以下にして

  validates :name,  presence: true, length: { maximum: 50 }

試験 green 終了。

6.2.4 Format validation

こんどは email な書式チェックなのかな。

  describe "when email format is invalid" do
    it "should be invalid" do
      addresses = %w[user@foo,com user_at_foo.org example.user@foo.
                     foo@bar_baz.com foo@bar+baz.com]
      addresses.each do |invalid_address|
        @user.email = invalid_address
        @user.should_not be_valid
      end      
    end
  end

  describe "when email format is valid" do
    it "should be valid" do
      addresses = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
      addresses.each do |valid_address|
        @user.email = valid_address
        @user.should be_valid
      end      
    end
  end

追加して red 確認。model の実装は以下らしい。

  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }

試験 green なのは良いのですが、正規表現がアレ。(何

6.2.5 Uniqueness validation

値重複の確認か。以下な試験追加。

  describe "when email address is already taken" do
    before do
      user_with_same_email = @user.dup
      user_with_same_email.save
    end

    it { should_not be_valid }
  end

試験 red を確認。email な属性の validate が以下になる模様。

  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true

試験 green を確認。大文字小文字なソレも云々できる模様。試験が以下。

  describe "when email address is already taken" do
    before do
      user_with_same_email = @user.dup
      user_with_same_email.email = @user.email.upcase
      user_with_same_email.save
    end

    it { should_not be_valid }
  end

試験 red を確認して以下に修正。

  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }

試験 green 確認。

The uniqueness caveat

unique の担保は DB 側のレイヤに委ねる模様。まず migrate なソレを generate します。

$ rails generate migration add_index_to_users_email
      invoke  active_record
      create    db/migrate/20130119065011_add_index_to_users_email.rb

で、生成されたソレを以下に

class AddIndexToUsersEmail < ActiveRecord::Migration
  def change
    add_index :users, :email, unique: true
  end
end

した上で rake db:migrate 実行。

$ bundle exec rake db:migrate
==  AddIndexToUsersEmail: migrating ===========================================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0181s
==  AddIndexToUsersEmail: migrated (0.0183s) ==================================

で、User な model に以下を盛り込む模様。

  before_save { |user| user.email = email.downcase }

ちょっとこの項、もう少しきちんとテキスト確認した方が良さげ。

6.3 Adding a secure password

パスワードな属性を追加するのか。とりあえず gem 追加らしい。以下を Gemfile に追加とのこと。

gem 'bcrypt-ruby', '3.0.1'

で、bundle install を実行。大丈夫かな。問題なく導入できたようです。盛り込みですがまずは試験を追加。

describe User do

  before do
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }
  it { should respond_to(:password_digest) }

試験 red を確認後にまずは migrate を云々。

$ rails generate migration add_password_digest_to_users password_digest:string
      invoke  active_record
      create    db/migrate/20130119065930_add_password_digest_to_users.rb

で、順に以下とのこと。

$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare
$ bundle exec rspec spec/

試験 green でした。良かった。

6.3.2 Password and confirmation

password_confirmation という属性を追加とのこと。試験が以下で。

require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "user@example.com", 
                     password: "foobar", password_confirmation: "foobar")
  end

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }
  it { should respond_to(:password_digest) }
  it { should respond_to(:password) }
  it { should respond_to(:password_confirmation) }

  it { should be_valid }

以下も追加かな。

  describe "when password is not present" do
    before { @user.password = @user.password_confirmation = " " }
    it { should_not be_valid }
  end

  describe "when password doesn't match confirmation" do
    before { @user.password_confirmation = "mismatch" }
    it { should_not be_valid }
  end

  describe "when password confirmation is nil" do
    before { @user.password_confirmation = nil }
    it { should_not be_valid }
  end

で、試験実行も red 終了ですね。

6.3.3 User authentication

この項で以下な試験を追加らしい。

  it { should respond_to(:authenticate) }
  .
  .
  .
  describe "with a password that's too short" do
    before { @user.password = @user.password_confirmation = "a" * 5 }
    it { should be_invalid }
  end

  describe "return value of authenticate method" do
    before { @user.save }
    let(:found_user) { User.find_by_email(@user.email) }

    describe "with valid password" do
      it { should == found_user.authenticate(@user.password) }
    end

    describe "with invalid password" do
      let(:user_for_invalid_password) { found_user.authenticate("invalid") }

      it { should_not == user_for_invalid_password }
      specify { user_for_invalid_password.should be_false }
    end
  end

盛り込み後、試験は実行してません。

6.3.4 User has secure password

最終的に User な model は以下な定義になる模様。

class User < ActiveRecord::Base
  attr_accessible :name, :email, :password, :password_confirmation
  has_secure_password

  before_save { |user| user.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,
                    format:     { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  validates :password, presence: true, length: { minimum: 6 }
  validates :password_confirmation, presence: true
end

むむむ、試験 green ですね。ちと諸々再確認します。
とりあえずエントリ投入。