Friday, August 9, 2013

プログラミング初心者歴3年目!初めてのデプロイにたどり着くために役立ったサイトやツール、本など




以下、xHagoのプレゼン用に作った補助資料です。ギリギリになってしまい、まとまりがない状態かもしれません。気が向いたら追加&整理したいと思います。初心者向けです。

書籍(と感想)


Javaの絵本
Javascriptとの区別もつかない頃なんとなく入手。 なんじゃこれ?いつどうやって使うの?

Perlはじめの一歩
ホリエモンが最初に勉強するならPerl一択みたいなことを言っていたのでPerl。プログラムのノリを多少理解。めんどくさいかも、と思う。

 「初めてのPerl
ページが多い。言語に対してのまじめな姿勢に、なるほどこういうものなのか。 全部読んでない。

 「初めてのプログラミング
わかりやすい気がする!ところどころ妙に難しいけど。 プログラミング自体初めての人に薦めるならこれ。
英語版はウェブで読める。
あれ、日本語訳も。。。

 「たのしいRuby
okinawa.rbで読み会に使用。 毎週みんなで問題を解いた。得た知識はこの本からが一番多いと思う。 今もトイレに置いてある。

 「Sinatra: Up and Running
薄くて高い。本家のwikiでいいような。でもたまに眺めている。

 「Learning Rails 3
Agile…のほうが難しい人にはいいと思う。自分はこれがいいレベルだった。 ebookなのでいまだに内容が更新され続けているのが嬉しい。

 「Agile Web Development with Rails 4
自分が持っているのは3。全部やってない。


勉強サイト 


勉強サイトで初心者向けのコースを何個かやってみるのもいいのかもしれないと最近思う。同じレベルのことを違うサイトでやると知識がなじむ気がする。

Code schoolのTry Ruby
Code schoolのRuby bits
ドットインストールのRuby入門
code academyのRuby

ドットインストールのさくらVPSのセットアップ
そのままなぞってやったらできた。


生き残ったツール 

定番モノしかないです。開発環境の細かい設定はまだ言うほど知らないのでスルー。にしてもまだ他にある気がする。。。

evernote (メモ)
URLのクリップ用に使い始めたが、それよりも自分がやったこと、打ち込んだコマンドとその結果をあとで見直す時に効果を感じる。絶対忘れるので。

sublimetext2(エディタ)
vim(とかemacs?)のショートカットを覚えるのがどうも苦手という人にはベストなのではないかと。カーソル増やして複数箇所を一気に編集とか一度やると離れられない。3が出たらたぶん買う。日本語を書くときはcoteditorを使う。

google(調べる)
エラーコードでひたすら検索。一番重要なツール。普通ですが。


勉強会など

okinawa.rbの人々との出会い。
初めて触れる人の温かみ。広がる知識。

xhagoで無理やりプレゼン。

Rails Hackathon in Okinawaで無理やりハカソーン。


ウェブ上で動くところまでできた主なもの

http://meta.export2csv.net/
サイトをスクレイピングしてmeta関連の要素をすべてcsvに書き出すほぼ自分用サービス (sinatra, さくらvps)

やすもーねっと
有給の残り日数を管理するサービス、まだこれからですが。
(Rails3, さくらvps)


雑感

やっているとよく思うのが「やればやるほど遠くなる」ということです。
ひとつがわかれば次のわからないことが連鎖的に出てきます。

まあでもやるしかないのでひたすら調べ続けてやっていくのですが。


Saturday, May 19, 2012

環境変数の設定 OSX bash

dev.twitterから発行されるconsumer keyやconsumer secret keyがgithubに上がっていってしまわないように環境変数に設定する。プログラムの中から環境変数を呼び出して使用。

設定
export 変数名=変数

確認
echo $変数名

例)
export TWITTER_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
echo $TWITTER_SECRET


プログラムからの呼び出し(omniauthの設定ファイル)

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
  #provider :facebook,"App ID","App Secret"
end


アプリが1つ以上になるとちょっと不便になってくる気がするけどとりあえずこれで。

Tuesday, May 15, 2012

devise認証やってみる(4/12)モデルの関連付け、バリデーション、ユニットテスト、セキュリティ [rails3]


devise認証やってみる(4/12)モデルの関連付け、バリデーション、ユニットテスト、セキュリティ [rails3]
CommunityGuides 4/12 - Relations, Validations, Unit Tests, Security

(回を重ねるごとにキレイになるどころか、より自分メモ的になってしまって読みづらくなってきてます。。。)


モデルの関連付け

migratationファイルの修正

DBに空白が入らないように、いくつかの項目に :null => false で制約をかける。

db/migrate/…create_users.rb

class DeviseCreateUsers < ActiveRecord::Migration
  def self.up
    create_table(:users) do |t|
      t.database_authenticatable :null => false
      t.recoverable
      t.rememberable
      t.trackable
      t.confirmable
      t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :time
      # t.token_authenticatable
     
+     # author information
+     t.string :fullname
+     t.text :shortbio
+     t.string :weburl
+     t.integer :country_id, :null => false, :default => 1                # foreign key to country table
     
      t.timestamps
    end

+   add_index :users, :fullname                                          
+   add_index :users, :country_id                                      
    add_index :users, :email,                :unique => true
    add_index :users, :reset_password_token, :unique => true
    add_index :users, :confirmation_token,   :unique => true   
    # add_index :users, :unlock_token,         :unique => true
  end

  def self.down
    drop_table :users
  end
end


 ( +の行を追加。バージョン差異で吐き出されているコードが多少違うので不安を感じつつ。。。 )


 class CreateArticles < ActiveRecord::Migration
  def change
    create_table :articles do |t|
      t.integer :user_id, :null => false # + foreign key
      t.string :title, :null => false # +
      t.text :teaser, :null => false # +
      t.text :body, :null => false # +
      t.string :version
      t.text :changelog
      t.string :message # 却下時のユーザへのメッセージ
      t.text :freezebody # 記事を受け取ると同時にtextbodyをこのフィールドにコピー
      t.integer :state, :null => false, :default => 0 # + 0...下書き, 1...送信済み, 2...却下, 3...全記事, 4...オススメ記事
      t.date :submitted
      t.date :accepted

      t.timestamps
    end
   
    add_index :articles, :user_id # +
  end
end




  def self.down
    drop_table :articles
  end
はとりあえず入れないでおく。。。

% rake db:drop
% rake db:migrate



モデルの関連づけ ユーザと記事

ユーザは複数の記事を持っている。
ひとつの記事はひとりのユーザに属する。

user.rb
has_many :articles, :dependent => :destroy
attr_accessibleの行に :fullname, :shortbio, :weburl
を追加


article.rb

空だったのでまるまる追加。

class Article < ActiveRecord::Base
  belongs_to :user

  validates :user_id, :presence => true
  validates :title, :presence => true, :length => { :maximum => 80 }
  validates :teaser, :presence => true, :length => { :maximum => 500 }
  validates :body, :presence => true
  validates :version, :length => { :maximum => 120 }
  validates :changelog, :length => { :maximum => 2000 }
  validates :message, :length => { :maximum => 5000 }
  validates :state, :presence => true, :numericality => true, :inclusion => { :in => 0..4 }
end


:dependent => :destroy
関連するユーザが削除されると記事も削除される

その他ヴァリデーションはこちらを見てね、と。



ここまでのテストではarticle controllerに対してfunctional testをやってきたが、ヴァリデーションのテストはunit testを使用。

test/unit/article_test.rb

require 'test_helper'

class ArticleTest < ActiveSupport::TestCase

  test "should not have empty title teaser body" do
    article = Article.new
    article.user_id = 1
    assert article.invalid?
    assert article.errors[:title].any?
    assert article.errors[:teaser].any?
    assert article.errors[:body].any?
    assert !article.save
  end

  test "must belong to a user" do
    article = Article.new :title => "Title", :teaser => "Teaser", :body => "Body"
    assert article.invalid?
    assert !article.save
  end

  test "should not have a state outside boundaries" do
    article = Article.new :title => "Title", :teaser => "Teaser", :body => "Body"
    article.user_id = 1
   
    article.state = -1
    assert !article.save
    article.state = 'a'
    assert !article.save
    article.state = 5
    assert !article.save
   
    article.state = 0  
    assert article.save
    article.state = 2
    assert article.save
    article.state = 4  
    assert article.save
  end
end


Rescue

などのように存在しない記事を呼び出された場合。

通常はエラー画面や public/404.html などだがflash messageをページ内に表示させて対応したほうが親切。ということで。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  ...
  rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
  ...
  protected
    def record_not_found
      flash[:error] = 'The article you requested could not be found.'
      redirect_to root_url
    end 
end                     # end of controller file

ActiveRecordからnotfoundのエラーをもらってrecord_not_foundメソッドに渡して、rootのflash message部分に表示させる、のような感じ?

rescue_fromの詳細は、



もっとテストする

ちなににユニットテストも rake ?
% rake
9 tests, 13 assertions, 0 failures, 0 errors, 0 skips

テストが通るということは「適切なテストがテストスートにない」ということを表している。

他ユーザによる記事編集、ログインしていない状況、など。。。

未登録ユーザのリダイレクト

test/functional/articles_controller_test.rb
require 'test_helper'

class ArticlesControllerTest < ActionController::TestCase

  setup do
    @article_user1 = articles(:one)
    @article_user2 = articles(:three)
  end

  # index and all
  test "should get index and all anonymous" do
    get :index
    assert_response :success
    assert_not_nil assigns(:articles)
    get :all
    assert_response :success
    assert_not_nil assigns(:articles)
  end 
  test "should get index and all signed in" do
    sign_in users(:user2)
    get :index
    assert_response :success
    assert_not_nil assigns(:articles)
    get :all
    assert_response :success
    assert_not_nil assigns(:articles)
  end 

  # about 
  test "should get about anonymous" do
    get :about
    assert_response :success       
  end
  test "should get about signed in" do
    sign_in users(:user2)
    get :about
    assert_response :success       
  end

  # show
  test "should show article anonymous" do
    get :show, :id => @article_user1.to_param
    assert_response :success
  end
  test "should show article signed in" do
    sign_in users(:user2)
    get :show, :id => @article_user1.to_param
    assert_response :success
  end

  # new and edit
  test "should not get new and edit anonymous" do
    get :new
    assert_redirected_to new_user_session_path
    get :edit, :id => @article_user1.to_param
    assert_redirected_to new_user_session_path
  end
  test "should get new signed in" do
    sign_in users(:user2)
    get :new
    assert_response :success
  end
  test "new article has to belong to current user" do
    sign_in users(:user2)
    get :new
    assert assigns(:article).user_id == users(:user2).id, "Article does not belong to current user"
  end
  test "should get edit to own article signed in" do
    sign_in users(:user2)
    get :edit, :id => @article_user2.to_param
    assert_response :success
  end
  test "edited article has to belong to current user" do
    sign_in users(:user2)
    get :edit, :id => @article_user1.to_param
    assert_redirected_to root_url, "Should be redirected to root url if article of other user is requested"
    assert_equal 'The article you requested could not be found.', flash[:error]
  end

  # create
  test "should not create article anonymous" do
    assert_no_difference('Article.count') do
      post :create, :article => @article_user1.attributes
    end
  end
  test "should not create article linked to other user" do
    sign_in users(:user2)
    post :create, :article => { :user_id => users(:user1).id, :title => 'Title', :teaser => 'Teaser', :body => 'Body' }
    assert assigns(:article).user_id == users(:user2).id, "Article does not belong to current user"  
  end
  test "should create article signed in" do
    sign_in users(:user2)
    assert_difference('Article.count', 1, "Article count has not changed") do
       post :create, :article => { :user_id => users(:user2).id, :title => 'Title', :teaser => 'Teaser', :body => 'Body' }
    end
    assert_redirected_to article_path(assigns(:article))
    assert_equal 'Article was successfully created.', flash[:notice]
  end

  # update
  test "should not update article anonymous" do 
    put :update, :id => @article_user1.to_param, :article => @article_user1.attributes
    assert_redirected_to new_user_session_path
  end
  test "should update article signed in" do
    sign_in users(:user2)
    put :update, :id => @article_user2.to_param, :article => @article_user2.attributes
    assert_redirected_to article_path(assigns(:article))
  end
  test "should not update article linked to other user" do
    sign_in users(:user2)
    put :update, :id => @article_user1.to_param, :article => @article_user1.attributes
    assert_redirected_to root_url, "Should be redirected to root url if article of other user is requested"
    assert_equal 'The article you requested could not be found.', flash[:error]
  end

  # destroy
  test "should not destroy article anonymous" do
    assert_no_difference('Article.count') do
      delete :destroy, :id => @article_user1.to_param
    end
    assert_redirected_to new_user_session_path
  end
  test "should destroy article signed in" do
    sign_in users(:user2)
    assert_difference('Article.count', -1) do
      delete :destroy, :id => @article_user2.to_param
    end
    assert_redirected_to articles_path
  end
  test "should not destroy article linked to other user" do
    sign_in users(:user2)
    assert_no_difference('Article.count', "Article count has changed") do
      delete :destroy, :id => @article_user1.to_param
    end
    assert_redirected_to root_url, "Should be redirected to root url if article of other user is requested"
    assert_equal 'The article you requested could not be found.', flash[:error]
  end 
end


test/fixtures/articles.yml
one:
  id: 1
  user_id: 1
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 0
  submitted: 2011-02-11
  accepted: 2011-02-11

two:
  id: 2
  user_id: 1
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 3
  submitted: 2011-02-11
  accepted: 2011-02-11

three:
  id: 3
  user_id: 2
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 0
  submitted: 2011-02-11
  accepted: 2011-02-11

four:
  id: 4
  user_id: 2
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 1
  submitted: 2011-02-11
  accepted: 2011-02-11

five:
  id: 5
  user_id: 2
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 2
  submitted: 2011-02-11
  accepted: 2011-02-11

six:
  id: 6
  user_id: 2
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 3
  submitted: 2011-02-11
  accepted: 2011-02-11 

seven:
  id: 7
  user_id: 2
  title: MyString
  teaser: MyText
  body: MyText
  version: MyString
  changelog: MyText
  message: MyString
  freezebody: MyText
  state: 4
  submitted: 2011-02-11
  accepted: 2011-02-11

% rake

  1) Failure:
test_edited_article_has_to_belong_to_current_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:76]:
Expected response to be a <:redirect>, but was <200>

  2) Failure:
test_new_article_has_to_belong_to_current_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:66]:
Article does not belong to current user

  3) Failure:
test_should_not_create_article_linked_to_other_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:89]:
Article does not belong to current user

  4) Failure:
test_should_not_destroy_article_linked_to_other_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:133]:
Article count has changed.
"Article.count" didn't change by 0.
<7> expected but was
<6>.

  5) Failure:
test_should_not_update_article_linked_to_other_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:113]:
Expected response to be a redirect to <http://test.host/> but was a redirect to <http://test.host/articles/1>

20 tests, 32 assertions, 5 failures, 0 errors, 0 skips

失敗5つ。

articles controller の修正

@article = Article.find(params[:id])
をすべて以下に変更
@article = current_user.articles.find(params[:id])

現在ログイン中のユーザにひもづく記事だけを呼び出すようになる。

% rake
errorが出た。

  6) Error:
test_should_show_article_anonymous(ArticlesControllerTest):
NoMethodError: undefined method `articles' for nil:NilClass

結局showだけ
@article = Article.find(params[:id])
のままにしておいて、 edit, update, destroy だけを変更したら通った。showは普通に考えたらcurrent_userに限定する必要はない気がするけど違うのかな?

20 tests, 35 assertions, 2 failures, 0 errors, 0 skips


new
@article = current_user.articles.new

 and create
@article = current_user.articles.new(params[:article])


% rake
あとひとつ

  1) Failure:
test_should_not_create_article_linked_to_other_user(ArticlesControllerTest) [... ... .../test/functional/articles_controller_test.rb:89]:
Article does not belong to current user

20 tests, 35 assertions, 1 failures, 0 errors, 0 skips

なぜか?

test "should not create article linked to other user" do
   sign_in users(:user2)
   post :create, :article => { :user_id => users(:user1).id, :title => 'Title', :teaser => 'Teaser', :body => 'Body' }
+  puts assigns(:article).user_id
   assert assigns(:article).user_id == users(:user2).id, "Article does not belong to current user" 
end

puts assigns(:article).user_idから出力されるのは「1」。つまりこの記事のuser_idはtestで指示した通りに1を持ってしまっている。もしuserがuser_idをparamに入力したらRailsはそのidをアサインする。ようになっている。mass asignment。

Railsはこのマスアサインメントの管理のために attr_accessible を使う。

article model:
app/models/article.rb
class Article < ActiveRecord::Base
  belongs_to :user

+  attr_accessible :title, :teaser, :body, :version, :changelog

  validates :user_id, :presence => true
  validates :title, :presence => true, :length => { :maximum => 80 }
  validates :teaser, :presence => true, :length => { :maximum => 500 }
  validates :body, :presence => true
  validates :version, :length => { :maximum => 120 }
  validates :changelog, :length => { :maximum => 2000 }
  validates :message, :length => { :maximum => 5000 }
  validates :state, :presence => true, :numericality => true, :inclusion => { :in => 0..4 }
end

attr_accessibleではmass assignmentで有効なすべての属性?を定義する。もしパラメータがその他の属性を含む場合は、assignされない。

attr_protected という有効ではない属性を明示的に宣言するメソッドもある。がブラックリストよりもホワイトリストの方がより安全なのでお勧めしない。ホワイトは漏れがあった場合、それを追加すればいいが、ブラックの場合はすべてを追加する必要があり、なおかつ漏れがあるとセキュリティリスクが発生する。


エラー出るけどfailure 0 なら意図通りオッケー。(エラー3つ出るけどほんとにいいのか?)

  1) Error:
test_should_create_article_signed_in(ArticlesControllerTest):
ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes: user_id

20 tests, 30 assertions, 0 failures, 3 errors, 0 skips


疑問点:
逆にmass assignさせてもいいケースはどんなケースなのか?
mass assignmentはどう便利なのか?
user_idとかはできるようにしとくと危ないのはわかるけど、全部できないようにしといたらどんな不便があるのか?

回答:
 mass assignmentできるということはユーザが値をコントロールできるということ。

ユーザ入力を許すフィールドはattr_accessibleしてオッケーだけど、システムが振る番号や関連付けのためのuser_idなど、ユーザに任意で入力させたくない項目はmass assignの対象から外しておく、ということ。

Tuesday, April 17, 2012

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


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

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

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

gem 'haml'
% bundle install

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

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

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



トップページのview

app/views/articles/index.html.haml



app/views/articles/show.html.haml


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



新しいルートの追加

aboutページを作ってみる

まずtest.

tests/functional/articles_controller_test.rb

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

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

route追加

config/routes.rb

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


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

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


view作成

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

成功。




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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

おわり

Saturday, April 14, 2012

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

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


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

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

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

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


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

  resources :articles

  root :to =>"articles#index" 
end

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

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

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

など。。。



テスト(初めての)

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

test_helper.rb

class ActionController::TestCase
  include Devise::TestHelpers
end


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



test/fixtures/users.yml

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

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

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

testに使うダミーusers?

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

require 'test_helper'

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

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

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

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

    assert_redirected_to article_path(assings(:article))
  end

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

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

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

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

    assert_redirected_to articles_path
  end
end


% rake db:test:prepare
% rake

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

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

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

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

テストか。。。

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

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

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

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


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

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

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


Model構造

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

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

Model Comment:
user_id
article_id
text: body

Model Rating:
user_id
article_id
integer: stars

Model Country:
name

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




作成開始

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

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


Gemfileにgem 'devise'して
% bundle install

% rails generate devise:install

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

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

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

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


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

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

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


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


route追加
route.rbに
resources :articles

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



Userモデル作成
% rails g devise User

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

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

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

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

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

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

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

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

      ## Rememberable
      t.datetime :remember_created_at

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

      ## Encryptable
      # t.string :password_salt

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

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

      ## Token authenticatable
      # t.string :authentication_token

      t.timestamps

    end

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

% rake db:migrate



viewを編集

application.html.erb

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

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

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



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

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

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



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

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

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

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

Signed out successfully.

1/12おわり。

Sunday, March 25, 2012

omuniauth使ったrailsサンプルサイト作成中のメモ


omuniauthを使ったサンプルサイトを作った過程の自分用メモです。まだ途中です。


demo
http://muitter.herokuapp.com/

code
https://github.com/cieux1/muitter

omniauth
https://github.com/intridea/omniauth/wiki

ほぼこちらのサイト通りにやった。感謝。
http://blog.twiwt.org/e/c3afce



# rails app作成

% rails new muitter

#Gemfile
gem 'omniauth'
gem 'omniauth-twitter'

% bundle


# config/initializer/omniauth.rb 作成
# add application info you registered at dev.twitter.com.
# *call back URL at dev.twitter can NOT be empty.

Rails.application.config.middleware.use OmniAuth::Builder do
     provider :twitter,"xxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxx"
     #provider :facebook,"App ID","App Secret"
end



% rails g controller sessions


# app/sessions_controller.rb

class SessionsController < ApplicationController
  def callback
    auth = request.env["omniauth.auth"]
    user = User.find_by_provider_and_uid(auth["provider"], auth["uid"]) || User.create_with_omniauth(auth)
    session[:user_id] = user.id
    redirect_to root_url, :notice => "Signed in!"
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, :notice => "Signed out!"
  end
end



# routes.rb

match '/auth/:provider/callback' => 'sessions#callback'
match "/signout" => "sessions#destroy", :as => :signout



% rails g model user


# app/user.rb

class User < ActiveRecord::Base
  def self.create_with_omniauth(auth)
    create! do |user|
      user.provider = auth["provider"]
      user.uid = auth["uid"]
      user.name = auth["info"]["name"]
      user.screen_name = auth["info"]["nickname"]
    end
  end
end

# チュートリアルでは[user_info]になっているがtwitterAPIの仕様変更で[user]に変更になった?
https://github.com/intridea/omniauth/issues/249#issuecomment-3229038

# user_infoだとauth/failure のcallback routeが見つからないとでるので変更したら動いた。




# db/migrate/XXXXX_create_users.rb

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :provider, :null => false
      t.string :uid, :null => false
      t.string :screen_name, :null => false, :uniq => true
      t.string :name, :null => false

      t.timestamps
    end
    add_index :users, [:provider, :uid]
    add_index :users, [:screen_name]
  end

  def self.down
    drop_table :users
  end
end



% rake db:migrate



# app/controllers/application_controller.rb

# add helper method

class ApplicationController < ActionController::Base
  protect_from_forgery

  helper_method :current_user

  private

  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end
end


% rails g scaffold Mutter user_id:integer mutterbody:text


# app/views/layouts/application.html.erb
# add login/show twiiter ID part to basic template erb.

<% if current_user %>
  Welcome <%= current_user.name %>!
  <%= link_to "Sign Out", signout_path %>
<% else %>
  <%= link_to "Sign in with Twitter", "/auth/twitter" %>
<% end %>



# route.rb
# set mutter/index as root.

root :to => "mutters#index"



# model/mutter.rb
belongs_to :user


# model/user.rb
has_many :mutters


# _form.html.erb
# ログイン中のuserのidをmutterのuser_idにひもづける処理。(なかなかわからなかった)
<%= f.number_field :user_id, :type => 'hidden', :value => current_user.id %>


このへん省略というか忘れた。。。というかなんで英語で書いてたのか。。。


okinawa ruby毎週meetupでomniauthが入った時点?でUser modelに必要なtableが作成されていてあとは使うだけ、という状態になっていることを教えてもらい道が開けた感。

#rails consoleでデータ呼び出しフォーマットの確認
% rails c

Mutter.all
User.find_by_id(1)

これで例えば、userとmutterをひもづけているmutterのuser_idがnilのままじゃん、とかが確認できた。




view / erb

#ログイン時とログインしていない時の見た目の切り替え
if current_user

# 自分の書き込みかどうかの判定
<% if current_user && mutter.user.screen_name == current_user.screen_name %>
...
<% end %>

#mutters_controller.rb
# ログインしているかどうかの判定

http://guides.rubyonrails.org/action_controller_overview.html

# before_filter ログインしていないと以下の操作ができないように。
    before_filter :require_login, :only => [:new, :create, :edit, :create, :update, :destroy]
...
...
    private

    def require_login
        unless logged_in?
            #flash[:error] = "You must be logged in to create/update a mutter."
            redirect_to root_url, :notice => 'You must sign in to create/update a mutter.'
        end
    end
    def logged_in?
        !!current_user
    end
 

need to be fixed!!!!!!!!!!!!!!!!!!
ログイン時、urlのid指定したら他の人の書き込みが触れてしまう。。。
どこでやるのかな。。これ。



heroku に deploy

#ssh keyの設定

github のssh key と herokuの ssh keyは同一である必要。
2つの端末で同じアカウントからpushしていることはどーなんだ?

ssh keyは端末のID的なもの?privateとpublicのペアができるので、public keyをサーバ側に保存する。
public keyは ~/.ssh/xxx_rsa.pub



レポジトリを登録してそこにpushする。
レポジトリは複数登録できる。
git add remote ....

一覧を見るには
git remote -v



# herokuはsqlite3ではなく、PostgreSQL
# Gemfileを修正
http://railsapps.github.com/rails-heroku-tutorial.html

group :development, :test do
  gem 'sqlite3'
end
group :production do
  gem 'pg'
end


# rails serverをthinに変更。webrickは頼りない?

webrickはlocalのままに残すなら

group :production do
  gem 'thin'
end


# herokuのstackによってrailsやrubyのバージョンが違う。
最新環境は cedar (beta)で、rails3.2の場合はこれでないと動かない?
heroku createする時に指定しなければならない。

http://devcenter.heroku.com/articles/cedar
http://devcenter.heroku.com/articles/cedar-migration

$ heroku create --stack cedar --remote heroku-cedar app名
$ git push heroku-cedar HEAD:master

なぜかHEADにしないとエラー。stackを[変更]するほうのtutorialを参考にしてたからか?
error: src refspec cedar does not match any.
error: failed to push some refs to 'git@heroku.com:app名.git'

http://stackoverflow.com/questions/4181861/src-refspec-master-does-not-match-any-when-pushing-commits-in-git



#gitignoreの設定
gitignoreは設定されるとそのファイルが gitから無視される。
設定される前に上がったものは基本的には消せない。

global_gitignore

globalは端末全体に設定される。
http://d.hatena.ne.jp/passingloop/20110903/p1

~/.global_gitignoreというファイルを作って記述
git config --global core.excludesfile ~/.global_gitignore で有効化。

例多数) https://github.com/github/gitignore/tree/master/Global



# 一度pushしてしまったファイルを消すのは大変&基本やってはいけない。
http://help.github.com/remove-sensitive-data/

git filter-branch
$ git filter-branch --tree-filter 'rm -f ファイル名' HEAD

$ git filter-branch --tree-filter 'rm -f omniauth.rb' HEAD

historyが書き変わるのでcloneされていたらおかしくなる?



# db:migrate

% heroku run rake db:migrate
成功するとlocalの時と同じようなログが出る。

% heroku restart
再起動で反映

% heroku run rakd db:reset
で元に戻す。



#pageを開く
% heroku open




# 修正の流れ

修正
git add app/asset/stylesheets/application.css
git commit -m 'adjusted font size.'
git push heroku-cedar HEAD:master

の繰り返し。


# 気になること
stack変えるために新規でheroku createしたので、githubにpushする方法がよくわからなくなった模様。。。