2011年12月27日火曜日

Railsのソースを読んでみる#4 modelの生成部分

今回みるのはrailtiesの「rails/commands.rb」なのだ。

これは受け取ったコマンドに応じた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

今回は「rails new ****」として出来たディレクトリの「アプリ名/script/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入り口

今回からはrailsのコアである「railties」のソースを追いかけてみるのだ。
「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コマンド

「メタプログラミングRuby」という書籍を読んだのだけど、一刻も早く実践しなければ全力で記憶から消え去りそうな気配がありすぎるので何かつくろうと思ったのだけど何も思い浮かばないので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

Rubyにおけるincludeとextendの違いは言葉で言われてもピンと来ないので実際に試した方が早いのね。
irbで試しましょう。

とりあえずこんなモジュールを用意します。
module A
 def hello
  "hello"
 end
end

そして使ってみます。まずはinclude。
class B
 include A
end

test = B.new
test.hello => #"hello"
B.hello => #"NoMethodError"

次にextend。
class C
 extend A
end

C.hello => #"hello"
test = C.new
test.hello => #"NoMethodError"
test.extend(A)
test.hello => #"hello"
つまりincludeはインスタンスメソッドを差し込むのに対して、extendは特異メソッドを挿入するかたちになります。
だからクラス定義の中でextendすると、クラスの特異メソッド=クラスメソッドになります。

2011年11月17日木曜日

[Rails]OmniAuthの使い方

RailsでOAuthを使うにはOmniAuthが簡単で便利。
というわけで使ってみよう。

gem 'omniauth'

とGemfileに追記して、bundle installします。
「config/initializers/omniauth.rb」というファイルが出来ているので
以下のように編集します。

Rails.application.config.middleware.use OmniAuth::Builder do
provider :XXX, "Consumerキー", "Consumerシークレット"
end

「XXX」の部分やOAuthを使って利用したいサービスを指定します。
例):twitter, :facebook, :githubなど
Consumerキーやシークレットはそれぞれのサービスで取得したキーやシークレットの値を指定します。
これをそのままハードコーディングしてしまうのは問題なので一般的には以下のように指定します。

例)provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']

環境変数からとってくるようにします。
環境変数の指定は各OSの手順に従えばよいですが、heroku上で利用する場合は以下のコマンドを使います。

heroku config:add TWITTER_KEY="XXXXXX"

これで「ENV["TWITTER_KEY"]」とすれば「XXXXX」が取得できます。


2011年10月29日土曜日

[こんなん作ってみました]Otaku Yellow Page

Ruby on Railsを使って何かつくってみたいな、と思って
「Otaku Yellow Page」なるものをつくってみました。


いろいろいじってみてください。

ソースコードは全てGitHubにあげています。
Rails3.1のサンプルコードとしてチェックしてみてください。
RSpecごとぶっこんでます。
ツッコミどころがあれば教えてくださいね。


推しメンを登録して、推しカブリを検索できるサービスです。
とりあえずTwitterとmixiとAmebaに対応しました(プロフィールにリンクするのみ)。

主に使っているのは以下のものです。
・Ruby1.9.2
・Ruby on Rails 3.1.0
・Haml
・SCSS (ほとんど使えてない、、、)
・RSpec2.6.4
・Capybara
・Bootstrap, from Twitter

本当はOmniAuthを使ってTwttier, mixi, Amebaと連携したかったのだけれどmixiとAmebaのAPIではあまり思ったことがやれなさそうだったので使わずにイエローページという位置づけにしてみました。


2011年10月24日月曜日

Rails3.1をHerokuで動かす

どハマりしましたが、とても参考になる記事がありました。


書いていある通りでうまくいきました。

2011年10月15日土曜日

アプリのvalidationで失敗したら

XCodeを4.2にアップデートしたら過去のアプリがvalidationに失敗するようになってしまいました。以下のようなメッセージが出ます。

application executable is missing a required architecture. At least one of the following architecture(s) must be present: armv6

XCodeを更新したことでarmv7のアーキテクチャ(のみ)でビルドしようとするようになったらしく、古いバージョンまでサポートするように(Deployment targetを4.0にする、など)設定していると、ダメだよ!と言われるようです。

「Build setting」の「Architectures」を見ると「$(ARCHS_STANDARD_32_BIT)」となっていて、これがarmv7を指しているみたい。
なのでここに「+」して「armv6」を追加してやります。
これでvalidationが通るようになります。

2番目の回答が今回の解決方法に相当します。


2011年10月13日木曜日

Bing APIで検索してみよう

画像検索用のAPIはいくつか公開されていますが、Bingはその中でも簡単に使えて制限が少ないと思います。


何はともあれまずはAppIDが必要になるので、こちらから登録しておきます。
AppIDを取得したら

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

となります。


iPhoneアプリで利用する場合、ほぼ確実に年齢制限が必要になるのでAppStoreにリリースする際はちゃんと設定しましょう(リジェクトされます)。

2011年10月4日火曜日

[iOS]突然シミュレータ上で起動しなくなる場合

クリーンしてからビルドし直してシミュレータで起動しようとしたところ、アプリが起動しようとしたところでクラッシュ。
何度もやり直してもダメ。
コンソールには以下のようなメッセージが残っています。

app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Could not load NIB in bundle〜(省略)〜

MainWindow.nibが読み込めませんでした、というようなことを言っているのだけどさっぱり理由が分からない。そんなんいじってない。
なんで、こうなったのかはさっぱり分からないのだけど、とりあえず解決したのでメモ。。。

1) プロジェクト名/ja.lproj と en.lprojフォルダにある「MainWindow.xib」を2つともファイルインスペクターに追加する

2) ビルドする

で、OK。
なんかの拍子でプロジェクトから消えてしまうらしい。。。
誤ってDeleteでも押したんだろうか。。。


2011年10月1日土曜日

[iOS]UITableViewを下に引っ張って更新するUI


Twitterクライアントでよく見かけるテーブルを下に引っ張るとアイコンが回転してロード中を示すUIがあります。
左の画像みたいなもの。

これを実現する便利なライブラリは2つあります。

1つ目は「PullToRefresh」というもの。
左の画像は「PullToRefresh」のGitHubのReadmeから引っ張って来たものです。
簡単なのはこちらで、Readmeに書いてある通り(英語ですが迷わない程度に簡単です)です。
ごくごく簡単なサンプルも付属しているので、こちらも併せて読めば簡単です。
でもこれはUITableViewControllerを拡張する形になっているので、複数のパーツから構成されている画面に存在するUITableViewを拡張したい、というケースでは使えません。

そういうケースでは「EGOTableViewPullRefresh」を使います。

1)EGORefreshTableHeaderView.h/mをコピーする
2)FrameworkにQuarzCoreを追加する
3)対象のUITableViewを持つControllerクラスでEGORefreshTableHeaderDelegateの実装を宣言
4)同様にUIScrollViewDelegateも実装宣言する
5) EGORefreshTableHeaderViewのインスタンス変数を定義
6)下記のような感じで初期化する
  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は引っ張ったときに出てくる最終更新日時を設定するものです。適宜呼んでやりましょう。

7) UIScrollViewのdelegateを以下のように実装する
- (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)
で参考に学んだことを凝縮してお送りしています。

今回はUserテーブルとMovieテーブルを結ぶFanテーブルに対応するコントローラを作成していきます。仕様としては、映画の一覧から選択すると好きな映画として登録、削除を選ぶと好きな映画の対象から外すというものとします。
インターフェースは映画情報の一覧画面(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",
:dependent => :destroy
has_many :movies, :through => :fans
has_manyは1対多関係における1側(多を持つ側)を表します。
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::Base
belongs_to :user
belongs_to :movie
validates :user_id, :presence => true
validates :movie_id, :presence => true
end
1対多の多側(相手が1つ)なのでbelongs_toで相手側モデルを指定します。
belongs_toのパラメータは単数になります(user, movie)。
そしてMovieモデルを編集します。
以下の2行を追加します。

has_many :fans
has_many :users, :through => :fans
Userモデルで定義した内容と逆方向です。
これで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
ここでは好きな映画を登録する「favorite_movie」、解除する「unfavorite_movie」、さらにその映画が好きかどうかを返す「favorite?」メソッドが存在するかどうかといったことなどをチェックしています。
では対応するコードを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
「favorite_movie!」ではFanモデルのインスタンスを新たに生成しています。
「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」を表すテーブルを用意します。
スキーマとしては以下のようになります。
ユーザーIDinteger
映画IDinteger
これを「Fanモデル」としましょう。このFanモデルをUserモデルと、さらにFanモデルとMovieモデルを紐づけます。そうするとFanモデルを経由してUserモデルとMovieモデルが対応付けられます。
まあとにかく、やってみましょう。
最初にFanテーブルをつくります。

>rails generate model Fan user_id:integer movie_id:integer
ユーザーIDと映画IDにはインデックスを設定したいので、生成されたマイグレーションファイルを編集します。db/migrate/XXXXX_create_fans.rb を編集します。
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
赤字の部分を追加します。
「rake db:migrate」でマイグレーションして、「rake db:test:prepare」で検査環境に反映させます。そしてこのfan.rb用の試験コードを作成しましょう。
# -*- 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


映画情報に関するファクトリメソッドもUserモデルと同じように存在するもとしています。
buildメソッドは保存前のモデルオブジェクトを生成します。
buildメソッドで受け取ったインスタンスの「save!」メソッドを呼び出すことで検証しています。今まで使って来た「should」が使われていませんが、save!メソッドは失敗すると例外が発生するので、このままで問題ありません。
次にuser.moviesといった呼び出し方が出来るかどうかの検査項目をspec/model/user_spec.rbに追加します。
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
user.fansやuser.moviesといった呼び出し方が出来ることを検証しています。
同様にmoviesの検証も追加します。
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
先ほどのUserモデル向けの検査と向きが逆方向になっただけで、ほぼ同じ内容です。
つまりmovie.fansやmovie.usersといった呼び出しの検証です。
この検査をパスするための実装は次回からとりかかります。

2011年9月15日木曜日

Rails3を初歩から学ぶ #20 ユーザー削除

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

一連のユーザーリソースに対するアクションも残すところあと1つ。
今回はユーザー削除に対応する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)
で参考に学んだことを凝縮してお送りしています。

前回editアクションを実装しましたが、そこで表示されるページで更新ボタンを押したときに呼ばれるのがupdateアクションです。今回はこちらを実装していきます。

describe "PUT 'update'" do
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'
目新しい部分としてはユーザー情報が更新されたことを確認するさいに「@user.reload」としてDBから読み直しています。読み直した値をつかって設定した内容と等しいことを確認しています。
ではアクションを実装します。
def update
@user = User.find(params[:id])
if @user.update_attributes(params[:user])
flash[:success] = "ユーザー情報を更新しました"
redirect_to @user
else
@title = "設定変更"
render 'edit'
end
end
edit.html.erbの入力フィールドは「user[name]」といった名前がつけられいて、コントローラに渡ってくると「params[:user]」として渡されます。入力された名前にアクセスするには「params[:user][:name]」とします。
ここでは「params[:user]」として入力されたフィールドを全てupdate_attributesに渡しています。
update_attributesでは渡されたパラメータをまとめてDBを更新します。
ここで悪意あるユーザーが不正なパラメータ(例えば admin=trueなど)を指定してもUserモデルでattr_accessibleを指定しているパラメータ以外は更新されません(第4回にやりましたね)。

さて、これでテストはパスします。
が、例えば適当なユーザーでサインインして「http://localhost:3000/users/1/edit」などとサインインユーザー以外のユーザーIDの編集ページを直接指定すると編集画面に遷移できてしまいます。
このままでは自分以外のユーザーに勝手に情報を変更されてしまいます。
自分の編集ページ以外にはアクセスできないようにしましょう。
テストコードは以下の通りです。

describe "自分以外のユーザー情報を更新できないこと" do
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 #自分以外のユーザー情報を更新できないこと
まず今現在のユーザーとアクセス先のユーザーが一致するかどうかをチェックするヘルパmソッドを定義します。app/helpers/sessions_helper.rbに下記のメソッドを追加します。

def current_user?(user)
user == current_user
end
current_userはcookieからユーザーを取得するメソッドです。渡されたユーザーとcookieを元にDBから取得したユーザーを比較した結果を返します。
次にこのヘルパの呼び出し処理を追加します。app/controllers/users_controller.rbに以下のプライベートメソッドを追加します。

def correct_user
@user = User.find(params[:id])
redirect_to(root_path) unless current_user?(@user)
end
そして以下のフィルタを定義してedit、updateアクションの実行前にユーザーが一致するかをチェックするようにします。
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 %>
<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>
2行目以降は元々new.html.erbにあった内容そのものです。
これまでは入力内容が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)
で参考に学んだことを凝縮してお送りしています。

未認証状態で「http://localhost:3000/users/」でアクセスするとフィルタで弾かれてサインイン画面へ遷移します。
そしてサインインするとユーザーごとの個人ページ(/users/:id)に遷移します。
これはちょっと不親切ですね。
最初にindexを表示しようとしていたのだから、そこへ行ってあげるのが親切でしょう。
じゃあそうしましょうか、というとき、これはどうやって検証すればよいのでしょうか。
ここで検証したいのは「未認証状態でindex -> サインインページへ遷移 -> サインインしてindexへ」という一連の流れを検証したいのですが今までのRSpecファイルでは対応できそうもありません。

そこで登場するのがインテグレーションテストです。
まずはrailsコマンドでインテグレーションテストを生成します。
rails generate integration_test user_session
generateコマンドに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
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
これまでredirect_toとしていた箇所を先ほど追加したredirect_back_orに置き換えています。これでインテグレーションテストもパスします。
実際に動かして試してみましょう。

2011年9月7日水曜日

ActionMailerをGmail経由で使いたい

RailsのActionMailerでgmailを使いたいのだ。
やり方はこちら。


若干、記事が古いのが気になる。
当たって砕けましょう。

まずRails3だとプラグインをインストールするコマンドが違います。
rails plugin install git://github.com/adamwiggins/gmail_smtp.git
あとは書いてある通りなのだけど、Ruby1.8.7以降を使っている場合は上記記事のコメント欄に書いてある修正が必要になります。
「vendor/plugins/gmail_smtp/lib/smtp_tls.rb」に対してコメント欄の通りの修正を加えます。
check_auth_args user, secret, authtype if user or secret という箇所が最初の方にあるので、、、

if RUBY_VERSION > "1.8.6"
 check_auth_args user, secret
else
 check_auth_args user, secret, authtype if user or secret
end

とします。
これをHerokuにアップデートすればgmail経由で送信できるようになります。
config/environment.rbは手を入れずにOK.


fakerがあるとHerokuでpushできない

なぜかfakerを使っているとHerokuでpushした際に
no such file to load -- faker
などと言われて失敗します。
対処法はこちら

Gemfileでfakerを指定している箇所を「:require => false」としたうえで、sample_data.rakeの「require 'faker'」を削除してやります。
まあfakerは使えなくなりますが、とりあえずpushできるようにはなります。

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 destroy
sign_out
redirect_to root_path
end
destroyアクションは以前sessions_controller.rbを生成した際に既に空で定義されているハズです。なのでそこに先ほど定義したサインアウトメソッドとリダイレクトの呼び出しを追加するのみです。
アプリケーションの入り口を示す意味で「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>
<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>
まとめてリンクを設定しました。設定変更のedit_user_pathだけは現時点で該当するアクションを実装していませんが、ついでに定義してしまいました。
こちらは次回作成しましょう。
それでは app/views/layouts/application.html.erb に今作成した部分テンプレートの呼び出しを追加します。

<div class="container">
<%= render 'layouts/header' %>
<section class="round">
<% flash.each do |key, value| %>
<%= content_tag(:div, value, :class => "flash #{key}") %>
<% end %>
</section>
<%= yield %>
</div>
renderメソッドでlayouts/headerを指定しています。これで部分テンプレート「_header.html.erb」が呼ばれます。その他、見た目用に少し手を入れています。CSS未設定なので見た目もへったくれも無いですが。


ここまで出来たらテストを実行してNGが0件であることを確認して、ブラウザで動作を見てみましょう。

ではテストを実行して全てパスすることを確認したら実際に動かして確認してみましょう。

2011年9月5日月曜日

BambooとCedar

Herokuを使ってみようと思ってドキュメントを読んでいたのだけど、Cedarって何よ。
Bambooって何ぞ。
ちんぷんかんぷんじゃないか。
というわけで調べてみました。

CedarというのはBambooの後継でRails3以降を使うならCedarを使わないといけない。
で、Bambooって何よというところで参考は以下。


Heroku上でRailsを動かすプラットフォームという位置づけ。
つまり、、、それは何ですか???
WEBrickみたいのもの?
オフィシャルを当たりましょう。英語だけど。


ここでもHerokuのランタイムだよ、と書いてある。
Java VMか、Dalvikか、という感じと解釈すればいいのかな。



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|
"Tarou#{n}"
end
このようにFactory.sequenceメソッドを使って定義すると「Factory.next(:name)」とすることで続々と「Tarou1」「Tarou2」とユーザー名を生成することが出来ます。
では試験コードです。
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]
def index
@title = "ユーザー一覧"
@users = User.all
end
前回定義したbefore_filterにindexアクションを追加します。
複数指定するときは配列になります。
indexアクションではUser.allで全ユーザーを取得しています。
次はindexに対応したviewを作成します。app/views/users/index.html.erbファイルを作成した以下のように編集します。

<h1>ユーザー一覧</h1>
<ul class="users">
<%= render @users %>
</ul>
複数存在するユーザーごとに同じタグを適用することになるので、ここでは部分テンプレートを使用しています。renderメソッドに渡した値に応じた部分テンプレートに該当部分が置き換えられます。
通常は「render 'hoge'」などと指定して、対応する「_hoge.html.erb」に置き換えられます。ここではindexアクションで格納したUser配列をまとめて渡しています。こうすると自動的に1つ1つの要素に分解して部分テンプレートを適用してくれます。
このとき対応するテンプレートは「_user.html.erb」となります。部分テンプレートの先頭には「_」がつきます。
ではその_user.html.erbを以下のように作成しましょう。

<li>
<%= link_to user.name, user %>
</li>
link_toメソッドは1つめのパラメータを値に、2つ目のパラメータをリンク先にaタグを生成します。上記の例ではリンク先としてuserを渡していますが、これも例によって「/users/:id」に変換されます。

2011年9月2日金曜日

Rails3を初歩から学ぶ #14 アクセス制限してみよう

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

前回まででサインイン処理を実現しました。
が、認証せずに「http://localhost:3000/users/1」とするとアクセスできてしまいます。
認証済みユーザー以外はアクセスして欲しくないページには認証を事前に確認するようにしましょう。
spec/controller/users_controller_spec.rbを編集します。

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 authenticate
deny_access unless signed_in?
end
def deny_access
redirect_to new_session_path, :notice => "サインインしてください"
end
signed_in?メソッドは前回実装したものでcookieの情報から認証状態をチェックします。
これで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 "新規登録成功時はサインイン済みとすること" do
post :create, :user => @attr
controller.should be_signed_in
end
RSpecではbool値を返すメソッドに対して「be_***」という検証用メソッドを自動で追加します。ここではsigined_inメソッドがtrueを返すことを期待しています。
このテストコードにより無事、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にセッション情報を保存するようにしてやりましょう。
Cookieで保持する情報はユーザー入力を介さないフィールドであるidとsaltを使います。
そしてCookieで保持しているidとsaltから該当するUserオブジェクトを取得するメソッドを用意します。
app/model/user.rb に以下のpublicメソッドを追加します。

def self.authenticate_with_salt(id, cookie_salt)
user = find_by_id(id)
(user && user.salt == cookie_salt) ? user :nil
end
渡されたidをキーにUserオブジェクトを取得し、渡されたsaltと比較します。
では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
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
認証成功時にヘルパメソッドを通じてcookieに値を保存します。
しかしまだこのままではコントローラからヘルパメソッドを呼ぶことができません。
アプリケーション共通で使えるように app/controller/application_controller.rb に下記の赤字部分を追記します。


class ApplicationController < ActionController::Base
protect_from_forgery
include SessionsHelper
end
これでどこからでもSessionsHelperに定義したヘルパメソッドが呼べるようになりました。ではテストを実行してみましょう。
遂に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
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
まずencrypt_passwordではnew_record?メソッドを使ってDBに保存済みであるかどうかを確認します。新規登録時はUserモデルのインスタンスを生成してsaveメソッドを呼ぶ前にここに到達するのでnew_record?はtrueを返します。
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)
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
has_password?メソッドでは渡されたパスワード(ユーザーが入力したもの)を暗号化して、それがインスタンス変数(DBに保存されている)パスワードと一致することを確認しています。
これは後続のクラスメソッド「authenticate」から呼ばれます。authenticateメソッドではユーザーが入力したユーザー名とパスワードを受け取りユーザー名をキーにUserインスタンスを取得します。そして取得したUserインスタンスのパスワードとユーザー入力パスワードが一致するかをhas_password?メソッドを通じて検証しています。検証失敗時はnilを返します。

次にコントローラのcreateアクションを実装します。
app/controller/sessions_controller.rb のcreateアクションを以下のように実装します。

def create
user = User.authenticate(params[:session][:name],
params[:session][:password])
if user.nil?
flash.now[:error] = "サインイン出来ませんでした"
@title = "サインイン"
render 'new'
else
redirect_to user
end
end
サインインページからのパラメータがparams[:session]に格納されているので、ユーザー名とパスワードを取り出して先ほど実装したUser.authenticateに渡して認証します。
失敗すると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
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'
あまり目新しいものはありませんが1つだけ。
サインイン失敗時のメッセージ表示確認として「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)
で参考に学んだことを凝縮してお送りしています。

ユーザーを新規登録する処理まで完成しましたので、今回から登録済みユーザーを認証する部分をつくってきましょう。セッション管理用に新たなコントローラを用意します。既存のUserモデルを参照するのでモデルは新たにつくる必要はありません。
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 -*-
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
Usersコントローラのnewアクション向けのテストコードとほぼ同じです。
フィールドの名前がsessionとなることが異なります。
newアクション用のコントローラは@titleに「サインイン」を格納するのみ。
対応するviewコードは「app/views/session/new.html.erb」です。
以下のように編集します。

<h1>サインイン</h1>
<%= 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>
これも基本的にはusers/newと同じなのですが「form_for」に渡しているパラメータが異なります。users/newではコントーラで生成したUserモデルのインスタンス変数を指定していましたが、サインイン時には該当するインスタンスが無いので「:session」とSessionsコントローラへ振り分けるよう指定します。
ただしこのままだとviewと同じ「sessions/new」に対してPOSTするHTMLが出力されていまうので、「:url => sessions_path」として「sessions/」に対してPOSTするよう指定しています。

2011年8月24日水曜日

Rails3を初歩から学ぶ #9 createアクションをつくろう

前回createアクションの検査までつくったのでコントローラを実装しましょう。

def create
@user = User.new(params[:user])
if @user.save
flash[:success] = "登録に成功しました"
redirect_to @user
else
@title = "新規登録"
@user.password = ""
@user.password_confirmation =""
render 'new'
end
end
User.newでインスタンスを生成した段階ではDBには保存していません。
保存するにはインスタンスから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.each do |key, value| %>
<%= content_tag(:div, value, :class => "flash #{key}") %>

<% end %>
<%= yield %>
</body>
ここでは単純にflashに設定された全てのキーと値を表示するようにしています。
「content_tag」ヘルパは何となく何するか分かると思いますが、指定されたタグを指定された値で指定された属性を付加して生成するものです。
この結果
<div class="flash success">登録に成功しました<div>
といったタグが生成されます。
ここまで出来たらRSpecを実行して検査をパスすることを確認してください。
また「rails server」としてサーバを起動して、「http://localhost:3000/users/new」にアクセスして必要な情報を入力して登録してみましょう。

次回からは認証プロセスをつくっていってみましょう。