Tuesday, April 17, 2012

devise認証やってみる(3/12)サイト共通レイアウト、メソッド、ビュー、ルートの追加 [rails3]


CommunityGuides 3/12 - Basic Layout, Adding Methods, Views and Routes
http://www.communityguides.eu/articles/3

サイト共通レイアウト、メソッド、ビュー、ルートの追加

ちょっとhamlを使ってみる。

gem 'haml'
% bundle install

application.html.erb を http://html2haml.heroku.com/ で変換してそのままペーストした。
application.html.erb を application.html.haml にリネーム。

app/asset/stylesheets/
http://dl.dropbox.com/u/56229/communityguides/download/communityguides.css
http://dl.dropbox.com/u/56229/communityguides/download/awesome-buttons.css
を保存。

application.html.hamlにcssリンク追加
= stylesheet_link_tag :comguide
= stylesheet_link_tag :button



トップページのview

app/views/articles/index.html.haml



app/views/articles/show.html.haml


css入れたら見た目が一気に本家と同じになる。。。



新しいルートの追加

aboutページを作ってみる

まずtest.

tests/functional/articles_controller_test.rb

test "should get about" do
  get :about
  assert_response :success
end

失敗するので、
1. route追加
2. メソッド追加
3. view作成
する。

route追加

config/routes.rb

resources :articles do
  collection do
      get 'about'
    end
end


エラー:The action 'about' could not be found
メソッド追加

(訂正:app/controllers/application_controller.rbではなく)
app/controllers/articles_controller.rb
...
before_filter :authenticate_user!, :except => [:index, :show, :about]
...
def about
end


view作成

app/views/articles/about.html.erb
%h1 About CommunityGuides

成功。




2つ目のインデックスページ作成。

トップはおすすめ記事のみにして、2つ目にはすべての記事を表示させる。

controllerにはindex.html.hamlを共有する2つのメソッドを。

いつもどおり、テストを書く>失敗する>そしてテストをパスするコードを書く、というやり方。でやれ、と。

tests/functional/articles_controller_test.rb
test "should get all articles index" do
  get :all
  assert_response :success
  assert_not_nil assigns(:articles)
end

failure!
Expected response to be a <:success>, but was <302>
が出るので、assert_response :redirect に変えたけどいいのか?(ダメです。追記参照。)

もうひとつ 1) Failure:
test_should_get_all_articles_index(ArticlesControllerTest) [/users/keepon/Desktop/Dropbox/lr3/comguide/test/functional/articles_controller_test.rb:63]:
expected to not be nil.

9 tests, 13 assertions, 1 failures, 0 errors, 0 skips

assert_not_nil assigns(:articles) の部分でnilになってるぽいけど、これはどこのことかよくわからない。article.yml とか?

---------------------
追記

原因はarticles_controller.rbのbefore_filterでした。exceptにindexとshowだけ入っている状態だったので以下のようにまとめて記述。

  before_filter :authenticate_user!, :except => [:index, :all, :show, :about]

 非ログイン時に許可されていないメソッドを呼び出していたためにredirectになっていた。assert_not_nil assigns(:articles)が評価される以前に、ログインしているかしていないかの評価のところでredirectされていた。

ログインが必要なサイトでは、ログイン状況パターンも考慮したテストでなければ、ということですね。
---------------------


ナビゲーションバーにリンク追加

= link_to "All Articles", all_articles_path
            = link_to "About", about_articles_path

おわり

Saturday, April 14, 2012

devise認証やってみる(2/12)MVCの流れとテスト [rails3]

CommunityGuides 2/12 - Model-View-Controller Principle, Routes and Tests http://www.communityguides.eu/articles/2


MVCの流れ
(省略、、、。本家に図があります)

例えば、controllerのshowメソッドではこんなふうに@articleを定義

def show
  @article = Article.find(params[:id])

  respond_to do |format|
    format.html # show.html.erb
    format.xml  { render :xml =>@article }
  end 
end


例えば、route.rbではメソッドをルーティング(?) config/routes.rb
Communityguides::Application.routes.draw do
  devise_for :users

  resources :articles

  root :to =>"articles#index" 
end

% rake routes でroute一覧を表示できる。

articles GET    /articles(.:format)               articles#index
#(/articlesへのGETリクエストはindexメソッドで処理される。(articles_path) )
            POST   /articles(.:format)               articles#create
#(同じパスへのPOSTリクエストはcreateメソッドにより処理。)

new_article GET    /articles/new(.:format)           articles#new
#(/articles/new へのGETリクエストはnewメソッドで処理される(new_article_path) )

など。。。



テスト(初めての)

自動生成テストの修正
deviseの認証が必要なメソッドがある
devise helpersをtest_helpers.rbに追加

test_helper.rb

class ActionController::TestCase
  include Devise::TestHelpers
end


config/environments/test.rb に
config.action_mailer.default_url_options = { :host => 'localhost:3000' }



test/fixtures/users.yml

user1:
  id: 1
  email: user1@communityguides.eu
  encrypted_password: abcdef1
  #password_salt:  efvfvffdv
  confirmed_at: <%= Time.now %>

user2:
  id: 2
  email: user2@communityguides.eu
  encrypted_password: abcdef2
  #password_salt:  hjujzjjzt
  confirmed_at: <%= Time.now %>

user3:
  id: 3
  email: user3@communityguides.eu
  encrypted_password: abcdef3
  #password_salt:  gheureuf
  confirmed_at: <%= Time.now %>

testに使うダミーusers?

test/functional/articles_controller_test.rb
書いてある通りに。。。

require 'test_helper'

class ArticlesControllerTest < ActionController::TestCase
  setup do
    @article = articles(:one)
  end

  test "should get index" do
    get :index
    assert_response :success
    assert_not_nil assigns(:articles)
  end

  test "should get new" do
    sign_in users(:user2)
    get :new
    assert_response :success
  end

  test "should create article" do
    sign_in users(:user2)
    assert_diffrence('Article.count') do
      post :create, :article => @article.attributes
    end

    assert_redirected_to article_path(assings(:article))
  end

  test "should show article" do
    get :show, :id => @article.to_param
    assert_response :success
  end

  test "should get edit" do
    sign_in users(:user2)
    get :edit, :id => @article.to_param
    assert_response :success
  end

  test "should update article" do
    sign_in users(:user2)
    put :update, :id => @article.to_param, :article => @article.attributes
    assert_redirected_to article_path(assigns(:article))
  end

  test "should destroy article" do
    sign_in users(:user2)
    assert_difference('Article.count', -1) do
      delete :destroy, :id => @article.to_param
    end

    assert_redirected_to articles_path
  end
end


% rake db:test:prepare
% rake

=> 7 tests, 0 assertions, 0 failures, 7 errors, 0 skips

ActiveRecord::StatementInvalid: SQLite3::SQLException: table users has no column named password_salt:
と出るのでusers.ymlからpassword_saltの部分をコメントアウト。

schema.rbでuserテーブル確認してもなかったので、deviseのバージョンによる違いかなと。。。

7 tests, 10 assertions, 0 failures, 0 errors, 0 skips

テストか。。。

devise認証やってみる(1/12)イントロ、認証 [rails3]

CommunityGuides 1/12 - Introduction, Authentication with Devise
http://www.communityguides.eu/articles/1
に、書いてある通りにやってみる。

my 環境
ruby 1.9.2p290
Rails 3.2.0
devise (2.0.4)

tutorialサイトの方は、Rails 3.0.5, Devise 1.1.7


プロジェクトのプランニング

例の通り「コミュニティガイド」というサイトを作る
ユーザ登録したユーザは記事を書いたり、編集。
記事はコメントや評価される。

・ひとつの記事は、一人のユーザに属す。一人のユーザは複数の記事を持てる
・ひとつのコメントは、一人のユーザとひとつの記事に属す。
・ひとつの評価は、一人のユーザとひとつの記事に属す。
・一人のユーザはひとつの国に属し、ひとつの国はたくさんのユーザを持つ。
(日本語って冗長。。。)


Model構造

Model User:
country_id
string: name, email, weburl
text: shortbio

Model Article:
user_id
string: title, version, message
text: teaser, body, changelog, freezebody
integer: state
date: submitted, accepted

Model Comment:
user_id
article_id
text: body

Model Rating:
user_id
article_id
integer: stars

Model Country:
name

xxxx_idの部分は他モデルの要素と連携。xxxxの部分に他モデルで使っている名前が入る。
ひとりのユーザはひとつの国に属す、ので Model Userにはcountry_idが必要というわけ。




作成開始

% rails new comguide
(本物はcommuniyguideでやってますが、自分は2回目なのでcomguideでやってます)

% cd comguide
% rails generate scaffold Article user_id:integer title:string teaser:text body:text version:string changelog:text message:string freezebody:text state:integer submitted:date accepted:date
% rake db:migrate
% rails server


Gemfileにgem 'devise'して
% bundle install

% rails generate devise:install

3つのことをやれというメッセージ

config/environments/development.rbに
“config.action_mailer.default_url_options = { :host => ‘localhost:3000’ }”

config/routes.rbに
root :to => “articles#index”
public/index.htmlを消してなければ消す。

views/layouts/application.html.erbに警告メッセージの枠を追加する、のは後回しにする
flash messages


deviseが使うメールアカウントの設定

テスト用にgmailアカウント取得。

config/environments/development.rb
以下追記
  # Send emails via Gmail
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    :address              => "smtp.gmail.com",
    :port                 => 587,
    :domain               => 'gmail.com',
    :user_name            => 'テスト用アカウント@gmail.com',
    :password             => 'パスワード',
    :authentication       => 'plain',
    :enable_starttls_auto => true  }


deviseのバージョンのせいか、tutorialとファイル内容が少し違う。


route追加
route.rbに
resources :articles

*resourcesはCRUD系の7つの基本ルーティングをまとめて作成する。



Userモデル作成
% rails g devise User

メアド確認と失敗ログイン制限を追加。
:confirmable, :lockable

user.rb
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password, :password_confirmation, :remember_me
end

deviseは2.0からschema styleが変わっているらしい。
https://github.com/plataformatec/devise/wiki/How-To:-Upgrade-to-Devise-2.0-migration-schema-style

migrationファイルにも追加と書いてあるが、tutorialと違うのでcomfirmableとlockableのところのコメントアウトを全部外した。

db/migrate/….create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration

  def change
    create_table(:users) do |t|
      ## Database authenticatable
      t.string :email,              :null => false, :default => ""
      t.string :encrypted_password, :null => false, :default => ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, :default => 0
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Encryptable
      # t.string :password_salt

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      t.integer  :failed_attempts, :default => 0 # Only if lock strategy is :failed_attempts
      t.string   :unlock_token # Only if unlock strategy is :email or :both
      t.datetime :locked_at

      ## Token authenticatable
      # t.string :authentication_token

      t.timestamps

    end

    add_index :users, :email,                :unique => true
    add_index :users, :reset_password_token, :unique => true
    add_index :users, :confirmation_token,   :unique => true
    add_index :users, :unlock_token,         :unique => true
    # add_index :users, :authentication_token, :unique => true
  end
end

% rake db:migrate



viewを編集

application.html.erb

ログイン/ログアウト/サインアップ部
<% if user_signed_in? %>
<%= current_user.email %>
<%= link_to "My Profile", edit_user_registration_path %>
<%= link_to "Sign out", destroy_user_session_path %>
<% else %>
<%= link_to "Sign up", new_user_registration_path %>
<%= link_to "Sign in", new_user_session_path %>
<% end %>nk_to "Sign in", new_user_session_path %>

フラッシュメッセージ部分
<% flash.each do |key, value| %>
<%= value %>
<% end %>
*hashなんですね。

実は、ここで最初のscaffoldしてないことに気づいてこのタイミングでやりました。。。



ログインしていない時のアクションを限定

ariticles_controllerにbefore_filterを追記して、ログインしていない時にできるアクションをindexとshowに限定。

before_filter :authenticate_user!, :except => [:index, :show]



動作確認
登録、メアド確認などokだがsign_outするとエラー。

Routing Error
No route matches [GET] "/users/sign_out"

なぜならsign_outのroutesはDELETEメソッドだから、らしい。
http://stackoverflow.com/questions/6557311/no-route-matches-users-sign-out-devise-rails-3

application.html.erbのdestroy_user_session_pathのところに追加
:method => :delete

Signed out successfully.

1/12おわり。