Ruby on Rails Tutorial (2)

Chapter 6 から Chapter 10 でログインおよび認証な実装を、とのこと。ちなみに git なログによれば Chapter 6 までは一応なぞっている模様。
とりあえず controller を作成して

$ rails generate controller Users new

メソドは new のみ。model も作成しています。

$ rails generate model User name:string email:string

なんとなくな記憶なんですが、属性は作られんかったような。って確認してみたら違いました。app/models/user.rb は記述ナシですが db/migrate 配下なソレは属性な記述がありますね。
とりあえず何はなくとも db:migrate しておく模様。

$ bundle exec rake db:migrate

annotate という gem がある模様ですが、スルー。属性に外からアクセスしたい場合には以下な記述になるのか。

class User < ActiveRecord::Base
  attr_accessible :name, :email
end

成程。ってことは対応するテーブルのフィールドがどうなってるか、ってのは db/migrate の対応するテーブルな記述を見ていくしかないのか。
次の項で

$ rails console --sandbox

でもごもごしてます。find(id) で検索したりな例が出てますが、User.find_by_email("mhartl@example.com") などというメソドを発見して、あーこーゆーのあったなとか思いだしたりとか。

User validations

テスツな環境は違うんで、ということで以下。

$ bundle exec rake db:test:prepare

で、Validating the presence of a name attribute. な実装が以下。

class User < ActiveRecord::Base 
  attr_accessible :name, :email

  validates :name, :presence => true
end

sandbox な REPL で確認してます。

$ rails console --sandbox
Loading development environment in sandbox (Rails 3.0.9)
Any modifications you make will be rolled back on exit
ruby-1.9.2-p180 :001 > user = User.new()
 => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, encrypted_password: nil> 
ruby-1.9.2-p180 :002 > user.valid?
 => false 
ruby-1.9.2-p180 :003 > 

以下な spec が云々、とありました。

describe User do

  before(:each) do
    @attr = { :name => "Example User", :email => "user@example.com" }
  end

  it "should require a name" do
    no_name_user = User.new(@attr.merge(:name => ""))
    no_name_user.should_not be_valid
  end
end

@attr.merge とか面白いですね。あと should_not というメソドに be_valid を渡して云々ってのも ruby ぽくて面白い。
あるいは name 属性の文字列長なシバリだったり

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

それに関する spec だったり

  it "should reject names that are too long" do
    long_name = "a" * 51
    long_name_user = User.new(@attr.merge(:name => long_name))
    long_name_user.should_not be_valid
  end

email 属性のフォーマットに関する記述、の前に以下な spec 追加して確認した後に

  it "should accept valid email addresses" do
    addresses = %w[user@foo.com THE_USER@foo.bar.org first.last@foo.jp]
    addresses.each do |address|
      valid_email_user = User.new(@attr.merge(:email => address))
      valid_email_user.should be_valid
    end
  end

  it "should reject invalid email addresses" do
    addresses = %w[user@foo,com user_at_foo.org example.user@foo.]
    addresses.each do |address|
      invalid_email_user = User.new(@attr.merge(:email => address))
      invalid_email_user.should_not be_valid
    end
  end

って結局 validation なナニなので

  • should be_valid
  • should_not be_valid

のどちらかになるのか。ちなみに model の記述は以下になる模様。

  validates :email, :presence => true,
                    :format   => { :with => email_regex }

email_regex は以下だそうな。

  email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

正規表現orz
一応テキストには上記正規表現についての簡単な解説もあります。次に出てくるのが email 属性は重複しない、というナニ。spec は以下。

  it "should reject duplicate email addresses" do
    # Put a user with given email address into the database.
    User.create!(@attr)
    user_with_duplicate_email = User.new(@attr)
    user_with_duplicate_email.should_not be_valid
  end

model な記述は以下になる模様。

  validates :email, :presence   => true,
                    :format     => { :with => email_regex },
                    :uniqueness => true

あと、email なナニは大文字小文字を別なモノとして認識しないので、ということで以下な spec が追加されてます。

  it "should reject email addresses identical up to case" do
    upcased_email = @attr[:email].upcase
    User.create!(@attr.merge(:email => upcased_email))
    user_with_duplicate_email = User.new(@attr)
    user_with_duplicate_email.should_not be_valid
  end

これ向けな validates の引数も用意されている模様。

  validates :email, :presence   => true,
                    :format     => { :with => email_regex },
                    :uniqueness => { :case_sensitive => false }

成程、としか言いようが無いなぁ。
で、uniqueness を model な定義だけでは担保できないらしく、email なフィールドは index に、とのこと。

$ rails generate migration add_email_uniqueness_index

して add_email_uniqueness_index なソレを以下にする模様。

class AddEmailUniquenessIndex < ActiveRecord::Migration
  def self.up
    add_index :users, :email, :unique => true
  end

  def self.down
    remove_index :users, :email
  end
end

ちなみに現状の実装はこの次の Chapter 7 に届いているのですが、試験はパスしてるみたいです。引き続き確認します。

Chapter 7

ええと spec に追加されているのは以下。

  • it should require a password
  • it should require a matching password confirmation
  • it should reject short passwords
  • it should reject long passwords

password_confirmation ってのはもっかい入れてね的ナニなのかな。
てか、この時点で password は model な属性でない云々な記述があって ? って思ったのですが、rails では暗号化されないと駄目、って理解で良いのかどうか。あと、password_confirmation なるソレも同様に virtual な属性らしい。
一応使ってる端末なプロジェクトも以下なナニは作成済み。

class AddPasswordToUsers < ActiveRecord::Migration
  def self.up
    add_column :users, :encrypted_password, :string
  end

  def self.down
    remove_column :users, :encrypted_password
  end
end

おそらく以下も実行済み?

$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare

以下な試験もパスしてますので大丈夫なのかな。

  • it should have an encrypted password attribute
  • it should set the encrypted password

これがテーブルへのカラムの追加の方法なのか。なんとなく model に対して云々って思ってたのですが、そうではないのね。
で、次の 7.2 あたりからが実装未着手な部分らしい。未だ Chapter 7 の半分もイケてない模様。とりあえず has_password? という boolean を戻す不思議なメソドが出てきます。
spec な記述が以下らしい。

    describe "has_password? method" do

      it "should be true if the passwords match" do
        @user.has_password?(@attr[:password]).should be_true
      end    

      it "should be false if the passwords don't match" do
        @user.has_password?("invalid").should be_false
      end 
    end
  • it should be true if the passwords match
  • it should be false if the passwords don't match

成程。で、Implementing has_password? ということで以下。

def has_password?(submitted_password)
  encrypted_password == encrypt(submitted_password)
end

で、migrate をナニ。

$ rails generate migration add_salt_to_users salt:string

string な salt を追加するのか。

class AddSaltToUsers < ActiveRecord::Migration
  def self.up
    add_column :users, :salt, :string
  end

  def self.down
    remove_column :users, :salt
  end
end

migration をナニしたら以下も忘れずに。

$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare

あと user.rb の private なあたりを以下にする模様。

  private

    def encrypt_password
      self.salt = make_salt unless has_password?(password)
      self.encrypted_password = encrypt(password)
    end

    def encrypt(string)
      secure_hash("#{salt}--#{string}")
    end

    def make_salt
      secure_hash("#{Time.now.utc}--#{password}")
    end

    def secure_hash(string)
      Digest::SHA2.hexdigest(string)
    end

ちょっと実装して試験してみます。以下。

$ bundle exec rspec spec/models/user_spec.rb \
> -e "should be true if the passwords match"
.

1 example, 0 failures

$ bundle exec rspec spec/models/user_spec.rb \
> -e "should be false if the passwords don't match"
.

1 example, 0 failures
$ bundle exec rspec spec/models/user_spec.rb -e "has_password? method"
Run filtered using {:full_description=>/(?-mix:has_password\? method)/}
..

2 examples, 0 failures
$

で、一つ目で has_password? なメソドが無い、と叱られます。rails console なナニではメソドがある、とのことなのですが。
って、以下を入れるの忘れてましたorz

    before(:each) do
      @user = User.create!(@attr)
    end

上記引用の通り試験パスしてます。なんつーか馬鹿だなぁ。"has_password? method" で OK でしたね。
次は authenticate なメソドらしい。password encryption なブロックに追加。

  • it should return nil on email/password mismatch
  • it should return nil for an email address with no user
  • it should return the user on email/password match

で、ついに find_by_email が出てくるのだな。

  def has_password?(submitted_password)
    encrypted_password == encrypt(submitted_password)
  end

  def self.authenticate(email, submitted_password)
    user = find_by_email(email)
    return nil  if user.nil?
    return user if user.has_password?(submitted_password)
  end

で、7.3 節に入った瞬間、話の流れが急になった気がするのでとりあえず実装とか記録は止めて読むだけにしときます。ので、とりあえずエントリ投入。