2011年12月27日火曜日
Railsのソースを読んでみる#4 modelの生成部分
これは受け取ったコマンドに応じたcase文がつらつらと続いているのだけれど、
まずは「rails g model User name:string email:string」とした場合を想定してみましょう。
コマンドにg(generate)を指定した場合はcommands.rbの28行目の
28 require "rails/commands/#{command}"
が実行されて「rails/commands/generate」が読み込まれます。
で、そのgenerate.rbの中身が以下の通り。
1 require 'rails/generators'
2 require 'active_support/core_ext/object/inclusion'
3
4 Rails::Generators.configure!
5
6 if ARGV.first.in?([nil, "-h", "--help"])
7 Rails::Generators.help 'generate'
8 exit
9 end
10
11 name = ARGV.shift
12 Rails::Generators.invoke name, ARGV, :behavior => :invoke, :destination_root => Rails.root
これだけ。
先の例で言うと11行目で変数nameに「model」が格納された状態で
Rails::Generators.invoke が呼び出されています。
そのメソッドの在処は1つ上の階層の「rails/generators.rb」です。
164 def self.invoke(namespace, args=ARGV, config={})
165 names = namespace.to_s.split(':')
166 if klass = find_by_namespace(names.pop, names.any? && names.join(':'))
167 args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? }
168 klass.start(args, config)
169 else
170 puts "Could not find generator #{namespace}."
171 end
172 end
166行目でfind_by_namespace("model")で呼び出します。
この呼び出しの結果、「Rails::Generators::ModelGenerator」が帰ってきます。
というわけで168行目にて「Rails::Generators::ModelGenerator」のstartを呼んでいます。
このstartメソッドはどこにあるのだろうか、というわけで「rails console」を立ち上げて聞いてみましょう。
require 'rails/generators'
require 'rails/generators/rails/model/model_generators'
Rails::Generators::ModelGenerator.ancestors
上記を実行するとこんな結果が返ってきます。
[Rails::Generators::ModelGenerator, Rails::Generators::NamedBase, Rails::Generators::Base, Rails::Generators::Actions, Thor::Actions, Thor::Group, Thor::Shell, Thor::Invocation, Thor::Base, Object, PP::ObjectMixin, ActiveSupport::Dependencies::Loadable, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]
今いるrailtiesの中ではgrepしても該当のstartメソッドが無さそうなので「Thor」と呼ばれるモジュールにありそうです。
そもそも「Thor」とは何ぞや。
次回はそこら辺りを調べてみましょう。
2011年12月17日土曜日
Railsのソースを読んでみる#3 もう1つのrails
ちなみちRails3.1.3です。
1 #!/usr/bin/env ruby
2 # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 APP_PATH = File.expand_path('../../config/application', __FILE__)
4 require File.expand_path('../../config/boot', __FILE__)
5 require 'rails/commands'
3行目では「アプリ名/config/application」というパスをAPP_PATHに格納しています。
そして「アプリ名/config/boot.rb」を読み込み、「rails/commands」(こちらはrailtieの方)を読み込んでいます。
まずは「boot.rb」
1 require 'rubygems'
2
3 # Set up gems listed in the Gemfile.
4 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5
6 require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
ここでは環境変数「BUNDLE_GEMFILE」に「アプリ名/Gemfile」を格納しています。
そして「bundler/setup」を読み込みます。
ここでやっているのはファイル名から分かるとうりGemfileで定義した各種モジュールを読み込んでいます。
bundler/setup.rb にて「Bundler.setup」をコールします。これは1つうえの階層の「bundler.rb」にあります。
こちらを読んでいきましょう。
101 def setup(*groups)
102 # Just return if all groups are already loaded
103 return @setup if defined?(@setup)
104
105 if groups.empty?
106 # Load all groups, but only once
107 @setup = load.setup
108 else
109 @completed_groups ||= []
110 # Figure out which groups haven't been loaded yet
111 unloaded = groups - @completed_groups
112 # Record groups that are now loaded
113 @completed_groups = groups
114 # Load any groups that are not yet loaded
115 unloaded.any? ? load.setup(*unloaded) : load
116 end
117 end
この時点ではgroupsは指定しないし@setupも空なので107行目が実行されます。
loadというのはRuntimeクラスのインスタンスを返してくるメソッドで、そのsetupメソッドを呼んでいます。
そのRuntimeというのはどっから来るんだ、というと同ファイルのもっと上の方にあります。
18 autoload :Definition, 'bundler/definition'
19 autoload :Dependency, 'bundler/dependency'
20 autoload :Dsl, 'bundler/dsl'
21 autoload :Environment, 'bundler/environment'
22 autoload :GemHelper, 'bundler/gem_helper'
23 autoload :Graph, 'bundler/graph'
24 autoload :Index, 'bundler/index'
25 autoload :Installer, 'bundler/installer'
26 autoload :LazySpecification, 'bundler/lazy_specification'
27 autoload :LockfileParser, 'bundler/lockfile_parser'
28 autoload :RemoteSpecification, 'bundler/remote_specification'
29 autoload :Resolver, 'bundler/resolver'
30 autoload :Runtime, 'bundler/runtime'
31 autoload :Settings, 'bundler/settings'
32 autoload :SharedHelpers, 'bundler/shared_helpers'
33 autoload :SpecSet, 'bundler/spec_set'
34 autoload :Source, 'bundler/source'
35 autoload :Specification, 'bundler/shared_helpers'
36 autoload :UI, 'bundler/ui'
autoloadというのは最初の引数に指定した名前を参照したタイミングでrequireするメソッドです。
なので「Runtime.new」とされた時点で30行目の「bundler/runtime」が読み込まれます。
この中ではGemfileで指定しているファイルを順次ロードしているだけなので詳細はパス。
「rails/commands」の方は次回見てみるのだ。
2011年12月11日日曜日
Railsのソースを読んでみる#2 railtie入り口
「railties-3.1.0/lib/rails/cli.rb」を追いかける。
1 require 'rbconfig'
2 require 'rails/script_rails_loader'
3
4 # If we are inside a Rails application this method performs an exec and thus
5 # the rest of this script is not run.
6 Rails::ScriptRailsLoader.exec_script_rails!
7
8 require 'rails/ruby_version_check'
9 Signal.trap("INT") { puts; exit(1) }
10
11 if ARGV.first == 'plugin'
12 ARGV.shift
13 require 'rails/commands/plugin_new'
14 else
15 require 'rails/commands/application'
16 end
まず「rbconfig」と「rails/script_rails_loader」を読み込んでいます。
6行目では「script/rails」が存在すれば、つまり「rails new」でアプリケーションを生成した状態で
実行した場合はそちらのrailsコマンドを実行します。
8,9行目ではrailsを動かせるrubyのバージョンであるかチェックしています。
11行目以降では「rails plugin」と指定した場合は「rails/commands/plugin_new」を、
それ以外は「application/commands/application」を読み込んでいます。
「rails plugin new Hoge」とすればプラグイン開発用の雛形が生成されるのだ。
11行目以降の部分はrailsアプリを新たに作成する場合に実行されるコードです。
そちらはちょっとパスするとして、生成したあとの「script/rails」を読んでいきましょう。
それは次回。
2011年12月9日金曜日
Railsのソースを読んでみる#1 railsコマンド
Linuxカーネルのソース読書を数年前に断念した前科があるけれど、いいの。
今回は最初から全部読む気ないから。
ピックアップ的に気になるところを読んでみよう。
Railsのソースは普通にインストールすれば
/usr/lib/ruby/gems/1.8/gems
にある(Macの場合)。
上記はRuby1.8の例なので適宜読み替えてください。
でもrvmなんかを使っていると場所が変わっているので、rvmで目的のverに切り替えてから「which rails」とすれば実行パスが出てくるので、その周辺にたぶんあります。
私の例では以下のパスでした。
~/.rvm/gems/ruby-1.9.2-p290/gems
でもどっから見たらいいのかよう分からん。
とりあえずrailsのはbinディレクトリしかないのでとりあえずrailsコマンドの中身を見てみる。
1 #!/usr/bin/env ruby
2
3 if File.exists?(File.join(File.expand_path('../..', __FILE__), '.git'))
4 railties_path = File.expand_path('../../railties/lib', __FILE__)
5 $:.unshift(railties_path)
6 end
7 require "rails/cli"
3行目では「gems」に「.git」があるか確認しています。
あれば「XXX/gems/railties/lib」をrailties_pathに格納します。
「$:」はロードパスを示す組み込み変数で「$LOAD_PATH」としても同じです。
5行目ではそのロードパスの先頭にrailties_pathを挿入しています。
7行目では「railities/lib/rails/cli.rb」を読み込んでいます。
要するに「railties/lib/rails/cli.rb」を読んでいるだけなんだけど、最初に「.git」の有無をチェックしているのはなぜだろう。
存在した場合だけロードパスにrailtiesのパスを先頭に追加している。
ググってみても分からん。
とりあえず課題として置いておこう。
とにもかくにもここから分かるのはRailsの一番核になっているのが
railtieだということ。
ここら辺りで予習しておくと良いでしょう。
2011年12月1日木曜日
[ruby]includeとextend
module Adef hello"hello"endend
class Binclude Aendtest = B.newtest.hello => #"hello"B.hello => #"NoMethodError"
class Cextend AendC.hello => #"hello"test = C.newtest.hello => #"NoMethodError"test.extend(A)test.hello => #"hello"
2011年11月17日木曜日
[Rails]OmniAuthの使い方
Rails.application.config.middleware.use OmniAuth::Builder doprovider :XXX, "Consumerキー", "Consumerシークレット"end
2011年10月29日土曜日
[こんなん作ってみました]Otaku Yellow Page
2011年10月24日月曜日
2011年10月15日土曜日
アプリのvalidationで失敗したら
2011年10月13日木曜日
Bing APIで検索してみよう
http://api.search.live.net/json.aspx?Appid=(AppID)&query=(検索文字)&sources=(検索対象)
とGETしてやれば結果がJSONで返ってきます。
sorcesには以下のように種類があります。詳細はドキュメントを参照してください。
web、image、video、newsなどなど。
oasisの画像検索をしたい場合は
http://api.search.live.net/json.aspx?Appid=(AppID)&query=oasis&sources=image
となります。
2011年10月4日火曜日
[iOS]突然シミュレータ上で起動しなくなる場合
2011年10月1日土曜日
[iOS]UITableViewを下に引っ張って更新するUI
Twitterクライアントでよく見かけるテーブルを下に引っ張るとアイコンが回転してロード中を示すUIがあります。
EGORefreshTableHeaderView *view = [[EGORefreshTableHeaderView alloc]
initWithFrame:CGRectMake(0.0f, 0.0f - self.tableView.bounds.size.height,
self.view.frame.size.width, self.tableView.bounds.size.height)];
view.delegate = self;
[self.tableView addSubview:view];
_refreshHeaderView = view;
[view release];
[_refreshHeaderView refreshLastUpdatedDate];
最後のrefreshLastUpdatedDateは引っ張ったときに出てくる最終更新日時を設定するものです。適宜呼んでやりましょう。- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[_refreshHeaderView egoRefreshScrollViewDidScroll:scrollView];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
[_refreshHeaderView egoRefreshScrollViewDidEndDragging:scrollView];
}
8)
EGORefreshTableHeaderDelegateで定義されるメソッドを実装する
- (void)egoRefreshTableHeaderDidTriggerRefresh:(EGORefreshTableHeaderView*)view
※ロード中アイコンが表示されている間にやっておきたい処理を実装する(非同期で)
- (BOOL)egoRefreshTableHeaderDataSourceIsLoading:(EGORefreshTableHeaderView*)view
※ロード中かどうかをBOOL値で返す(NOを返したタイミングでロード中解除)
- (NSDate*)egoRefreshTableHeaderDataSourceLastUpdated:(EGORefreshTableHeaderView*)view
※最終更新日時を返す。基本的には[NSDate date]しておけばよい
2011年9月24日土曜日
Rails3を初歩から学ぶ #23 関連テーブルのコントローラ
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
インターフェースは映画情報の一覧画面(Moviesコントローラのindexアクション)を使うことにして、Fansコントローラとしてはcreate(Fanテーブルへの追加)とdestroy(Fanテーブルからの削除)のみになります。
> rails generate controller Fansではテストを作成していきましょう。
# -*- coding: utf-8 -*-対応するコードを書いていきます。
require 'spec_helper'
describe FansController do
describe "アクセスコントロールの検証" do
it "未認証ユーザーによるcreate要求はサインイン画面へ" do
post :create
response.should redirect_to(new_session_path)
end
it "未認証ユーザーによるdestroy要求はサインイン画面へ" do
delete :destroy, :id => 1
response.should redirect_to(new_session_path)
end
end #アクセスコントロールの検証
describe "POST 'create'" do
before(:each) do
@user = Factory(:user)
@movie = Factory(:movie)
controller.sign_in(@user)
end
it "お気に入り登録できること" do
lambda do
post :create, :fan => { :movie_id => @movie }
response.should be_redirect
end.should change(Fan, :count).by(1)
end
end #POST 'create'
describe "DELETE 'destroy'" do
before(:each) do
@user = Factory(:user)
@movie = Factory(:movie)
controller.sign_in(@user)
@user.favorite_movie!(@movie)
@fan = @user.fans.find_by_movie_id(@movie)
end
it "お気に入り解除できること" do
lambda do
delete :destroy, :id => @fan
response.should be_redirect
end.should change(Fan, :count).by(-1)
end
end #DELETE 'destroy'
end
まずはルートを定義しましょう。
resources :fans, :only => [:create, :destroy]createとdestroyアクションしか不要なので「:only」で限定しています。
そしてapp/controller/fans_controller.rbを編集します。
class FansController < ApplicationControllerこれでテストは全てパスできます。
before_filter :authenticate
def create
@movie = Movie.find(params[:fan][:movie_id])
current_user.favorite_movie!(@movie)
redirect_to movies_path
end
def destroy
@movie = Fan.find(params[:id])
current_user.unfavorite_movie!(@movie)
redirect_to movies_path
end
end
最後にviewをつくって完成です。
映画情報の個別ページ(showアクション)にお気に入り登録/解除するボタンを設置しましょう。
<h1>タイトル:<%= @movie.name %><h1>
<% if current_user.favorite?(@movie) %>
<%= form_for current_user.fans.find_by_movie_id(@movie), :html => { :method => :delete } do |f| %>
<div class="actions"><%= f.submit "お気に入り解除" %></div>
<% end %>
<% else %>
<%= form_for current_user.fans.build(:movie_id => @movie.id) do |f| %>
<div><%= f.hidden_field :movie_id %></div>
<div class="actions"><%= f.submit "お気に入り" %></div>
<% end %>
<% end %>
「favorite?」メソッドの結果に応じてお気に入りに登録するか解除するか、表示するボタンを切り替えています。
「form_for」に指定するリソースはお気に入り登録時はbuildメソッドで生成した(DB保存前の)新しいリソース、解除時は渡された映画情報IDから検索して取得したリソースを指定しています。
登録時はPOSTで映画情報IDを渡したいのでhiddenフィールドで指定しています。
今回までで一通りRails3のおおまかな説明はおしまい!
2011年9月22日木曜日
Rails3を初歩から学ぶ #22 関連の実装
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
まずはUserモデルを編集します。
validatesをしている辺りに以下の行を追加します。
has_many :fans, :foreign_key => "user_id",has_manyは1対多関係における1側(多を持つ側)を表します。
:dependent => :destroy
has_many :movies, :through => :fans
Userモデル1つにつき複数のfanモデル、movieモデルを持つことを意味します。
has_manyのパラメータは複数形になります(fans, movies)。
これでuser.fansとするとfanオブジェクトの配列にアクセスできます。
foreign_keyは外部キーを指定します。
「:dependent => :destroy」はUserモデルを削除したときに連動して該当するuser_idのFanテーブルの項目を削除するように指定します。
2行目のhas_many文は「user.movies」とすることでそのユーザーが好きな映画の配列にアクセス可能とするための文です。userとmovieの間は直接ではなくFanテーブルを間に挟んでいます。user_idとmovie_idをフィールドに持つFanテーブルを通じてUserテーブルとMovieテーブルの多対多関係を表しています。
こうすることでuser.moviesとするだけで、そのユーザーの好きな映画情報にアクセスできます。
では次にapp/model/fan.rbを編集します。
class Fan < ActiveRecord::Base1対多の多側(相手が1つ)なのでbelongs_toで相手側モデルを指定します。
belongs_to :user
belongs_to :movie
validates :user_id, :presence => true
validates :movie_id, :presence => true
end
belongs_toのパラメータは単数になります(user, movie)。
そしてMovieモデルを編集します。
以下の2行を追加します。
has_many :fansUserモデルで定義した内容と逆方向です。
has_many :users, :through => :fans
これでmovie.usersとすることでその映画を好きだといっているユーザー配列にアクセスできます。
次にuserモデルに好きな映画を登録する機能を追加しましょう。
まずはspec/model/user_spec.rbにテストコードを追加します。
beforeブロックの赤字部分と、以降の検査コードを追加します。
describe "fans" do
before(:each) do
@user = Factory(:user)
@movie = Factory(:movie)
end
〜(省略)〜
it "favorite_movie!メソッドが機能すること" do
@user.should respond_to(:favorite_movie!)
end
it "unfavorite_movie!メソッドが機能すること" do
@user.should respond_to(:unfavorite_movie!)
end
it "favorite?メソッドが機能すること" do
@user.should respond_to(:favorite?)
end
it "追加した映画が登録されていること" do
@user.favorite_movie!(@movie)
@user.movies.should include(@movie)
end
it "削除した映画が登録されていないこと" do
@user.favorite_movie!(@movie)
@user.unfavorite_movie!(@movie)
@user.movies.should_not include(@movie)
end
では対応するコードをapp/model/user.rbに定義しましょう。
def favorite_movie!(movie)
fans.create!(:movie_id => movie.id)
end
def unfavorite_movie!(movie)
fans.find_by_movie_id(movie).destroy
end
def pushed?(member)
members.include?(member)
end
「unfavorit_movie!」では渡されたmovieのidからfanインスタンスを検索して、該当するfanモデルを削除します。
2011年9月17日土曜日
Rails3を初歩から学ぶ #21 関連モデルの作成
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
まずはMovieモデルを用意しましょう。
rails generate model Movie name:string映画の名前だけをフィールドに持ったテーブルです。
rake db:migrate
rake db:test:prepare
このMovieモデルにおけるvalidatesの実装やテストコードについては省略します。
またコントーラ、ビュー、ルート定義もUserモデルと同じ(パスワードの暗号化が無い分、こちらの方がシンプル)なので説明は省略します。
Userモデルを参考にしながら作ってみてください。
そしてこの段階でUserとMovieというテーブルが出来ました。
ここで例えばuser.moviesとすると、そのuserインスタンスが好きだと言っている映画のタイトルが配列で取れる、なんてことを実現したいとします。
また同時にmovie.usersとすると、そのmovieのことを好きだと言っているユーザーの配列が取れる、ということも実現したいのです。
このようなとき、「userが好きだといっているmovie」を表すテーブルを用意します。
スキーマとしては以下のようになります。
ユーザーID | integer |
映画ID | integer |
まあとにかく、やってみましょう。
最初にFanテーブルをつくります。
>rails generate model Fan user_id:integer movie_id:integer
class CreateFans < ActiveRecord::Migration
def self.up
create_table :fans do |t|
t.integer :user_id
t.integer :movie_id
t.timestamps
end
add_index :fans, :user_id
add_index :fans, :movie_id
end
def self.down
drop_table :fans
end
end
# -*- coding: utf-8 -*-
require 'spec_helper'
describe Fan do
before(:each) do
@user = Factory(:user)
@movie = Factory(:movie)
@fan = @user.fans.build(:movie_id => @movie.id)
end
it "登録できること" do
@fan.save!
end
describe "Userオブジェクトからのアクセス" do
before(:each) do
@fan.save
end
it "fanインスタンスからuserインスタンスを取得できること" do
@fan.user.should == @user
end
it "fanインスタンスからmovieインスタンスを取得できること" do
@fan.movie.should == @movie
end
end #Userオブジェクトからのアクセス
describe "バリデーションの検証" do
it "user_idが必要" do
@fan.user_id = nil
@fan.should_not be_valid
end
it "movie_idが必要" do
@fan.movie_id = nil
@fan.should_not be_valid
end
end #バリデーションの検証
end
describe "fans" do
before(:each) do
@user = Factory(:user)
end
it "fansメソッドが機能すること" do
@user.should respond_to(:fans)
end
it "moviesメソッドが機能すること" do
@user.should respond_to(:movies)
end
end #fans
describe "fans" do
before(:each) do
@movie = Factory(:movie)
end
it "fansメソッドが機能すること" do
@movie.should respond_to(:fans)
end
it "usersメソッドが機能すること" do
@movie.should respond_to(:users)
end
end #recommends
2011年9月15日木曜日
Rails3を初歩から学ぶ #20 ユーザー削除
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
今回はユーザー削除に対応するdeleteアクションを実装します。
仕様としてはユーザー情報ページに「退会する」というリンクを設けて、クリックしたら退会するものとします。
ではテストをつくりましょう。
describe "DELETE 'destroy'" doちょっと長いですが、今まで数回に分けていたものを一度に書いているだけで特に目新しいものはないです。
describe "削除失敗するケースの検証" do
before(:each) do
@user = Factory(:user)
end
it "サインインしていなければサインインページに飛ばす" do
delete :destroy, :id => @user
response.should redirect_to(new_session_path)
end
it "削除対象が異なるユーザーならルートに飛ばす" do
wrong_user = Factory(:user, :name => "Sabrou")
controller.sign_in(wrong_user)
delete :destroy, :id => @user
response.should redirect_to(root_path)
end
end #削除失敗するケースの検証
describe "削除成功の検証" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
end
it "ユーザー数が減っていること" do
lambda do
delete :destroy, :id => @user
end.should change(User, :count).by(-1)
end
it "削除したら新規登録ページに移動する" do
delete :destroy, :id => @user
response.should redirect_to(new_user_path)
end
end #削除成功の検証
end #DELETE 'destroy'
次にコントローラを実装します。
def destroyこの段階でテストはパスします。
User.find(params[:id]).destroy
flash[:success] = "退会しました"
redirect_to new_user_path
end
ユーザー情報ページに退会用のリンクをつくりましょう。
app/views/layouts/_header.html.erbに以下の一行を追加します。
<li><%= link_to "退会する", current_user, :method => :delete, :confirm => "退会します。よろしいですか?", :title => "#{current_user.name}" %></li>link_toメソッドのパラメータとして「:confirm」を指定して確認メッセージを表示するようにしています。
これでUserリソースに対する操作は一通り実装しました。
次回からは映画情報のモデルを(ようやく)つくります。
2011年9月13日火曜日
Rails3を初歩から学ぶ #19 情報の更新
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
describe "PUT 'update'" do目新しい部分としてはユーザー情報が更新されたことを確認するさいに「@user.reload」としてDBから読み直しています。読み直した値をつかって設定した内容と等しいことを確認しています。
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
end
describe "更新に失敗するケースの検証" do
before(:each) do
@attr = { :name => "", :password => "", :password_confirmation => ""}
end
it "設定変更ページを再表示すること" do
put :update, :id => @user, :user=> @attr
response.should reder_template('edit')
end
it "設定変更ページのタイトルであること" do
put :update, :id => @user, :user => @attr
response.should have_selector("title", :content => "設定変更")
end
end #更新に失敗するケースの検証
describe"更新に成功するケース" do
before(:each) do
@attr = { :name => "Saburou", :password => "barbaz",
:password_confirmation => "barbaz"}
end
it "ユーザー情報が更新されていること" do
put :update, :id => @user, :user => @attr
@user.reload
@user.name.should == @attr[:name]
end
it "ユーザー個人ページへ遷移すること" do
put :update, :id => @user, :user => @attr
response.should redirect_to(user_path(@user))
end
end #更新に成功するケースの検証
end #PUT 'update'
ではアクションを実装します。
def updateedit.html.erbの入力フィールドは「user[name]」といった名前がつけられいて、コントローラに渡ってくると「params[:user]」として渡されます。入力された名前にアクセスするには「params[:user][:name]」とします。
@user = User.find(params[:id])
if @user.update_attributes(params[:user])
flash[:success] = "ユーザー情報を更新しました"
redirect_to @user
else
@title = "設定変更"
render 'edit'
end
end
ここでは「params[:user]」として入力されたフィールドを全てupdate_attributesに渡しています。
update_attributesでは渡されたパラメータをまとめてDBを更新します。
ここで悪意あるユーザーが不正なパラメータ(例えば admin=trueなど)を指定してもUserモデルでattr_accessibleを指定しているパラメータ以外は更新されません(第4回にやりましたね)。
さて、これでテストはパスします。
が、例えば適当なユーザーでサインインして「http://localhost:3000/users/1/edit」などとサインインユーザー以外のユーザーIDの編集ページを直接指定すると編集画面に遷移できてしまいます。
このままでは自分以外のユーザーに勝手に情報を変更されてしまいます。
自分の編集ページ以外にはアクセスできないようにしましょう。
テストコードは以下の通りです。
describe "自分以外のユーザー情報を更新できないこと" doまず今現在のユーザーとアクセス先のユーザーが一致するかどうかをチェックするヘルパmソッドを定義します。app/helpers/sessions_helper.rbに下記のメソッドを追加します。
before(:each) do
@user = Factory(:user)
@wrong_user = Factory(:user, :name => "Jirou")
controller.sign_in(@wrong_user)
end
it "自分以外のユーザー編集ページにアクセスしたらルートに飛ばす" do
get :edit, :id => @user
response.should redirect_to(root_path)
end
it "自分以外のユーザー情報更新が要求されたらルートに飛ばす" do
put :update, :id => @user, :user => {}
response.should redirect_to(root_path)
end
end #自分以外のユーザー情報を更新できないこと
def current_user?(user)current_userはcookieからユーザーを取得するメソッドです。渡されたユーザーとcookieを元にDBから取得したユーザーを比較した結果を返します。
user == current_user
end
次にこのヘルパの呼び出し処理を追加します。app/controllers/users_controller.rbに以下のプライベートメソッドを追加します。
def correct_userそして以下のフィルタを定義してedit、updateアクションの実行前にユーザーが一致するかをチェックするようにします。
@user = User.find(params[:id])
redirect_to(root_path) unless current_user?(@user)
end
before_filter :correct_user, :only => [:edit, :update]これでユーザー情報のアップデートはできあがり。
2011年9月11日日曜日
Rails3を初歩から学ぶ#18 エラーメッセージの表示
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
最終的には映画情報(忘れているかもしれませんが、これはお気に入りの映画を登録するアプリケーションなのでした!)を編集できるようにしますが、とりあえず現時点ではユーザー名とパスワードのみ変更可能とします。
まずはeditアクション用のテストケースです。
describe "GET 'edit'" doフィールド一個しか見てないとか若干の手抜き感がありますが、特に目新しいことは無いのでこのまま進めます。
describe "未認証ユーザーの検証" do
it "未認証ならサインインページに遷移すること" do
user = Factory(:user)
get :edit, :id => user
response.should redirect_to(new_session_path)
end
end #未認証ユーザーの検証
describe "認証済みユーザーの検証" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
end
it "アクセスできること" do
get :edit, :id => @user
response.should be_success
end
it "タイトルの検証" do
get :edit, :id => @user
response.should have_selector("title", :content => "設定変更")
end
it "パスワード確認フィールドが存在すること" do
get :edit, :id => @user
response.should have_selector("input[name='user[password_confirmation]'][type='password']")
end #代表してここだけ調べる
end #認証済みユーザーの検証
end #GET edit
次にコントローラの実装です。
before_filter :authenticate, :except => [:new, :create]
def edit
@title = "設定変更"
@user = User.find(params[:id])
end
フィルタなんですがこれまでは「:only」で対象となるアクションを指定していましたが、認証が不要なページの方が少ないので「:except」で認証が不要なページのみを指定するようにしています。
次はapp/views/users/edit.html.erbを以下のように編集します。
<h1>設定変更</h1>新規登録時と同じフォームを表示するので部分テンプレート化しました。
<%= form_for(@user) do |f| %>
<%= render 'fields', :f => f %>
<div class="actions">
<%= f.submit "更新" %>
</div>
<% end %>
new.html.erbも部分テンプレートを呼び出すように変更します。
<h1>ユーザー新規登録</h1>さっぱりしました。では部分テンプレートをつくりましょう。
<%= form_for(@user) do |f| %>
<%= render 'fields', :f => f %>
<div class="actions">
<%= f.submit "新規登録" %>
</div>
<% end %>
app/views/users/_fields.html.erbです。
<%= render 'shared/error_messages', :object => f.object %>2行目以降は元々new.html.erbにあった内容そのものです。
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.label :password_confirmation, "Confirmation" %><br />
<%= f.password_field :password_confirmation %>
</div>
これまでは入力内容がUserモデルで定義した妥当性検証でNGになった場合に何も表示していませんでしたが、なぜエラーになったのかメッセージを表示するようにしました。
エラーの格納方法はRailsで共通なので使い回しできるようapp/views/shared/以下に部分テンプレートを用意します。名前は「_error_messages.html.erb」です。
<% if object.errors.any? %>
<div id="error_explanation">
<h2>登録できませんでした</h2>
<p>以下の内容をご確認ください</p>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
ここのobjectというのは呼び出し元をたどっていくと@userになります。
Userモデルの検証結果(validatesの実行結果)を@userインスタンス変数がもっていますので、この内容を表示しています。
エラーメッセージは英語です。
日本語化する方法もありますが、ここでは実施しません。
こちらのページに手順が紹介されています。
rails3 エラーメッセージ日本語化のメモ
2011年9月8日木曜日
rubyをバージョンアップしたらrakeが動かない
Rubyを1.9にしたらrakeが動かなくなっていました。。。
なにをやっても下記のようなメッセージが出る。
~/gems/rake-0.9.2/lib/rake/task_arguments.rb:73: warning: already initialized constant EMPTY_TASK_ARGS
~/gems/rake-0.9.2/lib/rake/invocation_chain.rb:49: warning: already initialized constant EMPTY
~/gems/rake-0.9.2/lib/rake/file_utils.rb:10: warning: already initialized constant RUBY
~/gems/rake-0.9.2/lib/rake/file_utils.rb:84: warning: already initialized constant LN_SUPPORTED
~/gems/rake-0.9.2/lib/rake/dsl_definition.rb:143: warning: already initialized constant Commands
~/gems/rake-0.9.2/lib/rake/file_list.rb:44: warning: already initialized constant ARRAY_METHODS
~/gems/rake-0.9.2/lib/rake/file_list.rb:47: warning: already initialized constant MUST_DEFINE
~/gems/rake-0.9.2/lib/rake/file_list.rb:51: warning: already initialized constant MUST_NOT_DEFINE
~/gems/rake-0.9.2/lib/rake/file_list.rb:55: warning: already initialized constant SPECIAL_RETURN
~/gems/rake-0.9.2/lib/rake/file_list.rb:61: warning: already initialized constant DELEGATING_METHODS
~/gems/rake-0.9.2/lib/rake/file_list.rb:364: warning: already initialized constant DEFAULT_IGNORE_PATTERNS
~/gems/rake-0.9.2/lib/rake/file_list.rb:370: warning: already initialized constant DEFAULT_IGNORE_PROCS
~/gems/rake-0.9.2/lib/rake.rb:64: warning: already initialized constant FileList
~/gems/rake-0.9.2/lib/rake.rb:65: warning: already initialized constant RakeFileUtils
いろいろと解決策はあるようですが、一番簡単なのは以下のようにbundle exec経由でrakeを使うこと。
bundle exec rake ****
なんだかなぁー、、
Rails3を初歩から学ぶ #17 インテグレーションテスト
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
そしてサインインするとユーザーごとの個人ページ(/users/:id)に遷移します。
これはちょっと不親切ですね。
最初にindexを表示しようとしていたのだから、そこへ行ってあげるのが親切でしょう。
じゃあそうしましょうか、というとき、これはどうやって検証すればよいのでしょうか。
ここで検証したいのは「未認証状態でindex -> サインインページへ遷移 -> サインインしてindexへ」という一連の流れを検証したいのですが今までのRSpecファイルでは対応できそうもありません。
そこで登場するのがインテグレーションテストです。
まずはrailsコマンドでインテグレーションテストを生成します。
rails generate integration_test user_sessiongenerateコマンドにintegration_testを指定するとインテグレーションテストを生成します。ここではuser_sessionという名前で生成しています。
実際に生成されるのは spec/requests/user_session_spec.rb です。早速編集しましょう。
# -*- coding: utf-8 -*-だいたい英文のようになっているので何しているか分かると思います。
require 'spec_helper'
describe "サインイン後の遷移先" do
it "サインイン前に要求したページに遷移する" do
user = Factory(:user)
visit users_path
#この段階でサインインページにリダイレクトされているハズ
fill_in :name, :with => user.name
fill_in :password, :with => user.password
click_button
#サインイン前に要求していたindexに遷移すること
response.should render_template('users/index')
end
end
それでは実装に入りましょう。まずはアクセス拒否時にアクセス先を記録する処理と、サインイン後にリダイレクト先を決定するヘルパメソッドを追加します。
app/helpers/session_helper.rb に以下のメソッドを追加します。
def deny_access
store_location
redirect_to new_session_path, :notice => "サインインしてください"
end
def redirect_back_or(default)
redirect_to(session[:return_to] || default)
clear_return_to
end
※以下2メソッドはprivateで定義する
def store_location
session[:return_to] = request.fullpath
end
def clear_return_to
session[:return_to] = nil
end
deny_accessメソッドは既に存在していますが赤文字部分を追加します。
このstore_locationメソッドでセッションに要求先URLを保存します。
そしてredirect_back_orメソッドでセッションをチェックして直前にアクセス拒否されていたら拒否されたページにリダイレクトしています。
リダイレクトしたらセッションからアクセス先情報を削除しています。
あとはサインイン成功時のリダイレクト処理をredirect_back_orに置き換えればOKです。
app/controller/sessions_controller.rbのcreateメソッドを以下の赤文字部分のように修正します。
def createこれまでredirect_toとしていた箇所を先ほど追加したredirect_back_orに置き換えています。これでインテグレーションテストもパスします。
user = User.authenticate(params[:session][:name],
params[:session][:password])
if user.nil?
flash.now[:error] = "サインインできませんでした"
@title = "サインイン"
render 'new'
else
sign_in user
redirect_back_or user
end
end
実際に動かして試してみましょう。
2011年9月7日水曜日
ActionMailerをGmail経由で使いたい
rails plugin install git://github.com/adamwiggins/gmail_smtp.git
check_auth_args user, secret, authtype if user or secret という箇所が最初の方にあるので、、、if RUBY_VERSION > "1.8.6"check_auth_args user, secretelsecheck_auth_args user, secret, authtype if user or secretendとします。
fakerがあるとHerokuでpushできない
no such file to load -- faker
2011年9月6日火曜日
Rails3を初歩から学ぶ#16 サインアウト
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
spec/controller/sessions_controller_spec.rb に以下の検証コードを追加します。
describe "DELETE 'destroy'" doもう目新しいことはありません。
it "未認証状態になり入り口に戻る" do
user = Factory(:user)
controller.sign_in(user)
delete :destroy
controller.should_not be_signed_in
response.should redirect_to(root_path)
end
end #DELETE 'destroy'
次にsign_inに対応するsign_outをヘルパメソッドとして定義しましょう。
app/helpers/sessions_helper.rbに追加します。
def sign_out
cookies.delete(:remember_token)
end
これだけです。次にdeleteアクションを実装しましょう。
sessions_controller.rbを編集します。
ちなみにusers_controller.rbじゃないです。意味的にこちらのdeleteはユーザーを削除することになるので、実装はいつかそのうちに。
def destroydestroyアクションは以前sessions_controller.rbを生成した際に既に空で定義されているハズです。なのでそこに先ほど定義したサインアウトメソッドとリダイレクトの呼び出しを追加するのみです。
sign_out
redirect_to root_path
end
アプリケーションの入り口を示す意味で「root_path」を指定していますが、これはまだ未定義なのでconfig/route.rbに定義します。
resources :users赤字の箇所が追加分です。
resources :sessions, :only => [:new, :create, :destroy]
match '/sessions/delete', :to => 'sessions#destroy' root :to => 'sessions#new'
ここではルートとしてサインインページを指定しています。
この状態で「http://localhost:3000/」とするとサインインページに行って欲しいんですが、public/index.htmlというファイルが存在すると、これが強制的に表示されます。
なのでrmコマンドで削除しておきましょう。
後はどこかにサインアウト用のリンクを追加すれば完成です。
これを期に各ページ共通のヘッダ部分を定義しましょう。
部分テンプレートとして定義します。
app/views/layouts/_header.html.erb を作成して以下のように編集します。
<header>まとめてリンクを設定しました。設定変更のedit_user_pathだけは現時点で該当するアクションを実装していませんが、ついでに定義してしまいました。
<nav class="round">
<ul>
<% if signed_in? %>
<li><%= link_to "ユーザー一覧", users_path %></li>
<li><%= link_to "プロフィール", current_user %></li>
<li><%= link_to "設定変更", edit_user_path(current_user) %></li>
<li><%= link_to "サインアウト", sessions_delete_path, :method => :delete %></li>
<% else %>
<li><%= link_to "サインイン", new_session_path %></li>
<% end %>
</ul>
</nav>
</header>
こちらは次回作成しましょう。
それでは app/views/layouts/application.html.erb に今作成した部分テンプレートの呼び出しを追加します。
<div class="container">renderメソッドでlayouts/headerを指定しています。これで部分テンプレート「_header.html.erb」が呼ばれます。その他、見た目用に少し手を入れています。CSS未設定なので見た目もへったくれも無いですが。
<%= render 'layouts/header' %>
<section class="round">
<% flash.each do |key, value| %>
<%= content_tag(:div, value, :class => "flash #{key}") %>
<% end %>
</section>
<%= yield %>
</div>
ここまで出来たらテストを実行してNGが0件であることを確認して、ブラウザで動作を見てみましょう。
ではテストを実行して全てパスすることを確認したら実際に動かして確認してみましょう。
2011年9月5日月曜日
BambooとCedar
2011年9月4日日曜日
Rails3を初歩から学ぶ #15 部分テンプレート
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
今度は登録ユーザーの一覧が見えるindexアクションを実装しましょう。
例によってテストから作成するのですが、ユーザー一覧を検証したいのでユーザー数はある程度欲しいところではあります。
以前導入したFactoryGirlの機能を使えばそれも簡単です。
spec/factories.rb に以下の定義を追加します。
Factory.sequence :name do |n|このようにFactory.sequenceメソッドを使って定義すると「Factory.next(:name)」とすることで続々と「Tarou1」「Tarou2」とユーザー名を生成することが出来ます。
"Tarou#{n}"
end
では試験コードです。
spec/controller/users_controller_spec.rbに以下のindexアクション用の検証コードを追加します。
describe "GET 'index'" do
describe "未認証ユーザーの検証"do
it "サインインページに飛ばす" do
get :index
response.should redirect_to(new_session_path)
flash[:notice].should =~ /サインインしてください/
end
end #未認証ユーザーの検証
describe"認証済みユーザーの検証" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
second = Factory(:user, :name => "Jirou")
third = Factory(:user, :name => "Saburou")
@users = [@user, second,third]
30.times do
@users << Factory(:user, :name => Factory.next(:name))
end
end
it "アクセスできること" do
get :index
response.should be_success
end
it "タイトル検証" do
get :index
response.should have_selector("title", :content => "ユーザー一覧")
end
it "ユーザー名のリストを表示していること" do
get :index
@users[0..2].each do |user|
response.should have_selector("li", :content => user.name)
end
end
end #認証済みユーザーの検証
end #GET index
特に目新しいものはないんですが、認証済みユーザーの検証をするbeforeブロックで先ほど定義したFactory.next(:user)を使ってユーザー名を自動生成しています。
ではアクションを定義しましょう。
before_filter :authenticate, :only => [:show, :index]前回定義したbefore_filterにindexアクションを追加します。
def index
@title = "ユーザー一覧"
@users = User.all
end
複数指定するときは配列になります。
indexアクションではUser.allで全ユーザーを取得しています。
次はindexに対応したviewを作成します。app/views/users/index.html.erbファイルを作成した以下のように編集します。
<h1>ユーザー一覧</h1>複数存在するユーザーごとに同じタグを適用することになるので、ここでは部分テンプレートを使用しています。renderメソッドに渡した値に応じた部分テンプレートに該当部分が置き換えられます。
<ul class="users">
<%= render @users %>
</ul>
通常は「render 'hoge'」などと指定して、対応する「_hoge.html.erb」に置き換えられます。ここではindexアクションで格納したUser配列をまとめて渡しています。こうすると自動的に1つ1つの要素に分解して部分テンプレートを適用してくれます。
このとき対応するテンプレートは「_user.html.erb」となります。部分テンプレートの先頭には「_」がつきます。
ではその_user.html.erbを以下のように作成しましょう。
<li>link_toメソッドは1つめのパラメータを値に、2つ目のパラメータをリンク先にaタグを生成します。上記の例ではリンク先としてuserを渡していますが、これも例によって「/users/:id」に変換されます。
<%= link_to user.name, user %>
</li>
2011年9月2日金曜日
Rails3を初歩から学ぶ #14 アクセス制限してみよう
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
describe "GET 'show'" do赤字の部分を追加します。
describe "サインインしていなければアクセス禁止" do
before(:each) do
@user = Factory(:user)
end
it "サインインページにリダイレクトされること" do
get :show, :id => @user
response.should redirect_to(signin_path)
end
end #サインインしていなければアクセス禁止
describe "サインインしていればアクセス可能" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
end
it "should be successful" do
get :show, :id => @user
response.should be_success
end
it "正しいユーザーを表示していること" do
get :show, :id => @user
assigns(:user).should == @user
end
it "タイトルの検証" do
get :show, :id => @user
response.should have_selector("title", :content => @user.name)
end
end #サインインしていればアクセス可能
end #GET 'show'
既存の検証コードはbeforeブロックでサインインするようにしています。
そして今回新たにサインインしていないときはサインイン画面に遷移することを検証しています。では実装しましょう。
まずは認証済みかどうかをチェックして、認証していない場合はサインインページに飛ばす処理ですが、これは色々と使い回しそうなのでヘルパメソッドとして定義しておきます。
app/helpers/sessions_helper.rb に以下の2つのメソッド(public)を追加します。
def authenticatesigned_in?メソッドは前回実装したものでcookieの情報から認証状態をチェックします。
deny_access unless signed_in?
end
def deny_access
redirect_to new_session_path, :notice => "サインインしてください"
end
これでnilが返った場合はdeny_accessメソッドによりサインインページに飛ばしています。
ではこのヘルパを呼び出しましょう。
app/controller/users_controller.rbに以下の行を追加します。
before_filter :authenticate, :only => :showこれでshowアクション実行前に先ほど定義したauthenticateが呼ばれるようになります。
これでテストもパスするようになるので実際にサーバからサインインせずに
「http://localhost:3000/users/1」などとアクセスしてみてください。
サインインページに飛ばされます。
で、この状態で新規登録ページからユーザーを登録してみると、サインページに飛んでしまいます。実はまだユーザー登録時にサインインを処理していないので未認証状態なんですね。それはまあ良しとして問題はテストを実施してもNG0件なってしまうことです。
users_controller_spec.rbにてサインイン時に以下のような検査コードがあります。
response.should redirect_to(user_path(assigns(:user)))これはサーバからusers/:idへのリダイレクトメッセージが返ることを検証しているもので、確かにこの検証からすればNGにはなりません。今、問題になっているのはリダイレクトしたらその先のbefore_filterで弾かれているからです。
というわけで新規ユーザーを登録したらサインイン済みになっているかどうかを検証するテストを追加しておきましょう。
spec/controller/users_controller_spec.rb の「POST create」の「登録に成功するケースの検証」のブロックに以下の検証コードを追加します。
it "新規登録成功時はサインイン済みとすること" doRSpecではbool値を返すメソッドに対して「be_***」という検証用メソッドを自動で追加します。ここではsigined_inメソッドがtrueを返すことを期待しています。
post :create, :user => @attr
controller.should be_signed_in
end
このテストコードにより無事、NGが検出されるようになりました。
では対応しましょう。
といってもapp/controller/users_controller.rb のcreateアクションに下記赤字の1行を追加するのみです。
def createこれで新規登録と同時にサインインした状態になるようにできました。
@user = User.new(params[:user])
if @user.save
sign_in @user
flash[:success] = "登録に成功しました"
redirect_to @user
else
@title = "新規登録"
@user.password = ""
@user.password_confirmation =""
render 'new'
end
end
2011年8月31日水曜日
Rails3を初歩から学ぶ #13 認証情報を保持する
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
Cookieで保持する情報はユーザー入力を介さないフィールドであるidとsaltを使います。
そしてCookieで保持しているidとsaltから該当するUserオブジェクトを取得するメソッドを用意します。
app/model/user.rb に以下のpublicメソッドを追加します。
def self.authenticate_with_salt(id, cookie_salt)渡されたidをキーにUserオブジェクトを取得し、渡されたsaltと比較します。
user = find_by_id(id)
(user && user.salt == cookie_salt) ? user :nil
end
ではcookieに書き込む側と読み込む側を実装します。
app/helpers/sessions_helper.rbに記述します。
module SessionsHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id,user.salt]
end
def current_user
User.authenticate_with_salt(*remember_token)
end
def signed_in?
!current_user.nil?
end
private
def remember_token
cookies.signed[:remember_token] || [nil, nil]
end
end
sign_inメソッドでは渡されたuserオブジェクトのidとsaltをcookieに保存しています。
現在アクセスしているユーザーを参照するcurrent_userメソッドではまずremember_tokenメソッドでcookieで保存している情報(idとsalt)を取得します。そして取得したidとsaltを先ほど実装したauthenticate_with_saltメソッドで認証します。パラメータに「*」がついていますが、これは配列を分解して渡すことを意味します(ここでは[id, salt]という配列を「id」「salt」という2つの変数として渡す)。
そしてauthenticate_with_saltの認証結果に応じた結果(成功=userオブジェクト、失敗=nil)を返します。
ではサインインのタイミングでcookieに値を保存するようコントローラに追記しましょう。
app/controller/sessions_controllerに下記の赤字部分を追加します。
def create認証成功時にヘルパメソッドを通じてcookieに値を保存します。
user = User.authenticate(params[:session][:name],
params[:session][:password])
if user.nil?
flash.now[:error] = "サインインできませんでした"
@title = "サインイン"
render 'new'
else
sign_in user
redirect_to user
end
end
しかしまだこのままではコントローラからヘルパメソッドを呼ぶことができません。
アプリケーション共通で使えるように app/controller/application_controller.rb に下記の赤字部分を追記します。
class ApplicationController < ActionController::BaseこれでどこからでもSessionsHelperに定義したヘルパメソッドが呼べるようになりました。ではテストを実行してみましょう。
protect_from_forgery
include SessionsHelper
end
遂に0件になったハズです。
2011年8月29日月曜日
Rails3を初歩から学ぶ #12 セッション管理(実装編)
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
Userモデルにはnameとencrypted_passwordというフィールドを用意していますが、今までDBにはnameしか保存していません。
新規登録時にはpasswordを入力してもらっていますが、これは暗号化前の値であってDBに保存していません。なので、まずは新規登録時にpasswordを暗号化してencrypted_passwordに保存するようにしましょう。
app/model/user.rbを編集します。
require 'digest'SHAハッシュを使いたいのでdigestをrequireします。
そしてクラス宣言後のvalidatesなどが並んでいる箇所に
before_save :encrypt_passwordとフィルタを追加します。
before_saveはsaveする前に呼び出されるフィルタです。ここではencrypt_passwordというプライベートメソッドを呼び出しています。ここで暗号化してから保存するようになります。ではencrypt_password(とその周辺)を実装します。
privateまずencrypt_passwordではnew_record?メソッドを使ってDBに保存済みであるかどうかを確認します。新規登録時はUserモデルのインスタンスを生成してsaveメソッドを呼ぶ前にここに到達するのでnew_record?はtrueを返します。
def encrypt_password
self.salt = make_salt if new_record?
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
new_record?がtrueを返したときはmake_saltメソッドでsaltを生成します。
以前説明したようにパスワードのハッシュを算出する際にはsaltと呼ばれるユーザー毎に異なる値を生成して付加しています。
ここではパスワードと現在時刻を組み合わせたものにSHAハッシュをかけてsaltを生成しています。
saltが用意できたらencryptメソッドでユーザーから渡されたパスワードを暗号化します。
encryptでは先ほど生成したsaltとユーザーから渡されたパスワードを組み合わせてSHAハッシュをかけています。
ここまででもう一度今の段階でrspecを実施しておきましょう。エラーの数が増えていなければ今回の修正が特に影響していないことが分かります(サインイン処理がまだ未実装なのでエラーは出ています)。
ではサインイン処理をごそっと実装しましょう。
まずはapp/mode/user.rbに以下のメソッドを追加します(publicなので先ほど編集したprivateよりも上の位置に追加するかpublicと明示して追加します)。
def has_password?(submitted_password)has_password?メソッドでは渡されたパスワード(ユーザーが入力したもの)を暗号化して、それがインスタンス変数(DBに保存されている)パスワードと一致することを確認しています。
encrypted_password == encrypt(submitted_password)
end
def self.authenticate(name, submitted_password)
user = find_by_name(name)
return nil if user.nil?
return user if user.has_password?(submitted_password)
end
これは後続のクラスメソッド「authenticate」から呼ばれます。authenticateメソッドではユーザーが入力したユーザー名とパスワードを受け取りユーザー名をキーにUserインスタンスを取得します。そして取得したUserインスタンスのパスワードとユーザー入力パスワードが一致するかをhas_password?メソッドを通じて検証しています。検証失敗時はnilを返します。
次にコントローラのcreateアクションを実装します。
app/controller/sessions_controller.rb のcreateアクションを以下のように実装します。
def createサインインページからのパラメータがparams[:session]に格納されているので、ユーザー名とパスワードを取り出して先ほど実装したUser.authenticateに渡して認証します。
user = User.authenticate(params[:session][:name],
params[:session][:password])
if user.nil?
flash.now[:error] = "サインイン出来ませんでした"
@title = "サインイン"
render 'new'
else
redirect_to user
end
end
失敗するとnilが返ってくるのでflash.nowにエラーの旨を格納してサインインページを再描画しています。
認証成功時はユーザーごとのページ(users/:id)にリダイレクトします。
この段階でテストを実行するとNGが1件のみとなっているハズです。
このNGはcurrent_userが未定義であることに起因しています。
current_userメソッドの役割は今現在のUserインスタンスを取得して、サインイン状態をチェックするための窓口となるものです。
しかし今回の実装ではサインインページからcreateアクション実行時の一瞬だけ認証を実行してどこにも認証済みであるかどうかの情報を保持していません。
というわけでCookieに認証情報を書き込んでやる必要があるのですが、これは次回に。
2011年8月27日土曜日
Rails3を初歩から学ぶ #11 セッション管理(RSpec編)
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
createアクションがそれに該当します。
まずはテストからですが、ここで検証したいのは以下の点です。
「サインインに失敗するケース」
・サインイン画面を再表示すること
・タイトルの検証
・サインインに失敗した旨のメッセージを表示すること
「サインインに成功するケース」
・正しいユーザー名/パウワードでサインインできること
・ユーザー個別のページに遷移すること
ここで問題となるのが「サインインできたかどうか」をどうやって判定するかです。
そのために必要なのは「操作中のユーザーのインスタンス」を参照して、そのインスタンスが「サインイン済みであること」のチェックが出来ることです。
それぞれを可能にするヘルパメソッド「currenjt_user」「signed_in?」が定義されることを想定してテストコードを書いてみましょう。
describe "POST 'create'" doあまり目新しいものはありませんが1つだけ。
describe "サインインに失敗するケース" do
before(:each) do
@attr = { :name => "Yam Cha", :password => "invalid" }
end
it "サインイン画面を再表示すること" do
post :create, :session => @attr
response.should render_template('new')
end
it "再表示した画面のタイトル検証" do
post :create, :session => @attr
response.should have_selector("title", :content => "サインイン")
end
it "サインイン失敗した旨を表示する" do
post :create, :session => @attr
flash.now[:error].should =~ /サインインできませんでした/
end
end #サインインに失敗するケース
describe "サインインに成功するケース" do
before(:each) do
@user = Factory(:user)
@attr = { :name => @user.name, :password => @user.password }
end
it "正しいユーザー名/パスワードでサインインできること" do
post :create, :session => @attr
controller.current_user.should == @user
controller.should be_signed_in
end
it "サインインに成功したらユーザー情報ページへ遷移する" do
post :create, :session => @attr
response.should redirect_to(user_path(@user))
end
end #サインインに成功するケース
end # POST 'create'
サインイン失敗時のメッセージ表示確認として「flash.now[:error]」を参照しています。
以前登場したときは「flash[:success]」のように直接アクセスしていたのに今回は「now」を経由しています。
なんでしょうか。
これは以前説明した「redirect_to」と「render」の違い、つまりアクションを経由するか否かに関係します。
新規登録時の処理では以下のようになります。
1. Usersコントローラのcreateアクションでflashに値をセットしてredirect_to
2. Usesコントローラのshowアクションを実行
3. Usersビューのshow.html.erbからHTMLを生成(ここでflash表示&クリア)
一方、今回のサインイン失敗時の流れは以下の通りです。
1. Sessionsコントローラのcreateアクションでflashに値をセットしてrender
2. Sessionsビューのnew.html.erbからHTMLを生成(ここでflash表示)
つまり間にアクションを挟みません。flashはアクションを一度またいで値を保持する機能なので、後者のケースでは2終了段階でflashはクリアされていません。
次に何らかのアクションを実行すると再度同じflashが表示されてしまいます。
このような時に「flash.now」をnowを経由することで一度表示した段階でクリアされるよう仕向けることが出来ます。
当然ですが、今の段階でRSpecを実行するとエラーになります。特にcurrent_userやsigned_in?ではNoMethodErrorになりますが、これらは次回からつくっていきましょう。
2011年8月26日金曜日
Rails3を初歩から学ぶ #10 サインイン画面をつくる
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。
rails generate controller Sessions new create destroyまとめてnew、create、destroyアクションも生成しておきます。
それぞれサインイン画面の表示、セッションの生成(サインイン)と破棄(サインアウト)に対応しています。
RSpec用のテストファイルも同時に生成されていますが、spec/views/sessions/の下は必要ないので削除してしまいましょう。
それではルートを定義しておきましょう。
new、create、destroyそれぞれに対するgetが定義されているので削除しておいて、代わりに以下のように定義します。
resources :sessions, :only => [:new, :create, :destroy]「resources」に渡すとindex,create,new,edit,etc....と一式自動で定義されますが、今回のsessionsコントローラに対してはサインイン画面を表示するnewとセッションを生成するcreateとセッションを破棄するdestroyのみが必要なので「:only」オプションで指定します。
それでは生成されたコントローラに対してテストコードを書いていきます。
# -*- coding: utf-8 -*-Usersコントローラのnewアクション向けのテストコードとほぼ同じです。
require 'spec_helper'
describe SessionsController do
render_views
describe "GET 'new'" do
it "should be successful" do
get 'new'
response.should be_success
end
it "タイトルの検証" do
get :new
response.should have_selector("title", :content => "サインイン")
end
it "ユーザー名入力フィールドがあること" do
get :new
response.should have_selector("input[name='session[name]'][type='text']")
end
it "パスワード入力フィールドがあること" do
get :new
response.should have_selector("input[name='session[password]']
[type='password']")
end
end #GET 'new'
end
フィールドの名前がsessionとなることが異なります。
newアクション用のコントローラは@titleに「サインイン」を格納するのみ。
対応するviewコードは「app/views/session/new.html.erb」です。
以下のように編集します。
<h1>サインイン</h1>これも基本的にはusers/newと同じなのですが「form_for」に渡しているパラメータが異なります。users/newではコントーラで生成したUserモデルのインスタンス変数を指定していましたが、サインイン時には該当するインスタンスが無いので「:session」とSessionsコントローラへ振り分けるよう指定します。
<%= form_for(:session, :url => sessions_path) do |f| %>
<div class="field">
<%= f.label :name %>&;t;br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :password %>%lt;br />
<%= f.password_field :password %>
</div>
<div class="actions">
<%= f.submit "サインイン" %>
</div>
<% end %>
<p><%= link_to "新規登録はこちら", new_user_path %></p>
ただしこのままだとviewと同じ「sessions/new」に対してPOSTするHTMLが出力されていまうので、「:url => sessions_path」として「sessions/」に対してPOSTするよう指定しています。
2011年8月24日水曜日
Rails3を初歩から学ぶ #9 createアクションをつくろう
def createUser.newでインスタンスを生成した段階ではDBには保存していません。
@user = User.new(params[:user])
if @user.save
flash[:success] = "登録に成功しました"
redirect_to @user
else
@title = "新規登録"
@user.password = ""
@user.password_confirmation =""
render 'new'
end
end
保存するにはインスタンスからsaveメソッドを呼びます。戻り値がtrueなら成功、falseなら失敗なので戻り値に応じて画面を遷移します。
redirect_toとrenderの違いですが、renderは単に指定されたテンプレートで描画するのみです。この例でいえば登録失敗時に「new.html.erb」を再表示します。
一方のredirect_toはリダイレクト先のURLをクライアントに返して、クライアントからそのURLにアクセスさせます。つまりもう一度アクションが呼ばれることになります。
ここではリダイレクト先としてインスタンス変数を渡していますが、Userモデルのインスタンス変数を渡すと、ActiveControllerは「user_path/@user.id」として解釈してくれます。例えば登録した@userのidが1であればリダイレクト先として「user_path/1」がクライアントに渡り、クライアントはそのアドレスに向けてGETします。
すると今度はshowアクションが呼ばれるわけです。
というわけで、createアクション向けには対応するviewのテンプレートを用意する必要はありません(今回の例では、です)。
が、このままではせっかく設定したflashが表示されないので表示させてやりましょう。flashの表示はshowアクションに限ったことではないのでアプリケーション共通で使用する「view/layout/application.html.erb」に追加します。
<body>ここでは単純にflashに設定された全てのキーと値を表示するようにしています。
<% flash.each do |key, value| %>
<%= content_tag(:div, value, :class => "flash #{key}") %>
<% end %>
<%= yield %>
</body>
「content_tag」ヘルパは何となく何するか分かると思いますが、指定されたタグを指定された値で指定された属性を付加して生成するものです。
この結果
<div class="flash success">登録に成功しました<div>といったタグが生成されます。
ここまで出来たらRSpecを実行して検査をパスすることを確認してください。
また「rails server」としてサーバを起動して、「http://localhost:3000/users/new」にアクセスして必要な情報を入力して登録してみましょう。
次回からは認証プロセスをつくっていってみましょう。