Railsチュートリアル 第8章

8.1 セッション

8.1.1

演習1

省略.

演習2

routes.rbに記述したルーティングのみが表示されました.

8.1.2

演習1

routes.rbにおける設定

post '/login', to: 'sessions#create'

により実現されていますね.

8.1.3

演習1

省略.

8.1.4

Flashメッセージについて今ひとつよくわからなかったので, Railsガイドを参照しました.

flashはセッションの中の特殊な部分であり、リクエストごとにクリアされます。この特徴から、flashは「直後のリクエスト」でのみ参照可能になります。

これを踏まえて, Flashメッセージが残ってしまう理由を説明しましょう.

ログインボタンを押すことにより, /loginに対しPOSTリクエストが送られます. このとき変数flashにエラーメッセージが格納されます.

これと同時にnewページがレンダリングされますが, これは同一のPOSTリクエストの中で行われます.

上の引用文からもわかるように, 次のリクエストでもまだ変数flashにエラーメッセージは格納されたままです. だからHomeページに移動してもフラッシュメッセージが表示されたままなのです.

次にHelpページなどにアクセスし, もう一度HTTPリクエストを送ることにより, やっとflashの中身がクリアされます.

8.1.5

演習1

flash.nowがうまく機能していることがブラウザ上でも確認できます.

8.2 ログイン

Applicationコントローラの定義内に, 追加した覚えのないメソッドがある...どこかで見落としたのでしょうか.

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception # 見知らぬ行
    include SessionHelper
end

8.2.1

演習1

Sxi%2FKPGHw...のような暗号化された値で保存されていますね.

演習2

有効期限の欄にはセッションと書かれています.

8.2.2

演習1

たしかにnilが返ってきます.

irb(main):003:0> User.find_by(id: 2)
  User Load (0.9ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
=> nil

演習2

以下のようになりました. 例外を発生させないことと, 2回目にデータベースへの問い合わせが無いことが確認できますね.

>> session = {}
=> {}
>> session[:user_id] = nil
=> nil

>> @current_user ||= User.find_by(id: session[:user_id])
   (0.6ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1
=> nil

>> session[:user_id] = User.first.id
  User Load (0.5ms)  SELECT  `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=> 1

>> @current_user ||= User.find_by(id: session[:user_id])
  User Load (1.2ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2018-06-28 05:35:56", updated_at: "2018-06-28 05:35:56", password_digest: "$2a$10$i8HBze1p.5miIytb9ahZjefM27b8EXVMSMjV/B0za7t...">

>> @current_user ||= User.find_by(id: session[:user_id])
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2018-06-28 05:35:56", updated_at: "2018-06-28 05:35:56", password_digest: "$2a$10$i8HBze1p.5miIytb9ahZjefM27b8EXVMSMjV/B0za7t...">

8.2.3

Bootstrapのドロップダウンがうまく機能しません. 何行かコードを追加すると動くという記事もありましたが, 浅い理解でコードを書き換えるのも嫌なので後回しにします.

と思ったらただのタイポでした. ではBootstrapが動かないと報告している記事はなんだったんでしょう.

演習1, 2

省略.

8.2.4

演習1, 2

省略.

8.2.5

演習1, 2

省略.

8.3 ログアウト

演習1

省略.

演習2

ログアウトと同時にcookieの値が変化していることがわかります.

Railsチュートリアル 第7章

7.1 ユーザーを表示する

7.1.1

演習1

以下の通りでした.

--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: static_pages
  action: about
permitted: false

演習2

全く同じものが表示されました.

irb(main):002:0> puts user.attributes.to_yaml
---
id: 4
name: Michael Jackson
email: mhartl@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &1 2018-06-25 09:33:51.000000000 Z
  zone: &2 !ruby/object:ActiveSupport::TimeZone
    name: Etc/UTC
  time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &3 2018-06-25 09:41:15.000000000 Z
  zone: *2
  time: *3
password_digest: "$2a$10$tM4ebAaSFIzfhqzi6.Lo/Ofs6/TfKpnP7LrgkCRV/z02U.CjwKubu"
=> nil

7.1.2

演習1, 2

こんな感じでしょうかね.

# app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>,
<%= @user.created_at %>, <%= @user.updated_at %><br>

<%= Time.now %>

7.1.3

Dockerで環境を構築している場合は, Byebugを利用するためにひと工夫必要なようです. 詳しくはこちらに記載しました.

users_controller.rbdebuggerを追記したら, 以下のコマンドでコンテナの標準入出力とローカルマシンのターミナルをアタッチします.

docker attach <Rails' container name>

この状態で/users/1にアクセスすると, Byebugのプロンプトがターミナルに表示されます.

[1, 9] in /myapp/app/controllers/users_controller.rb
   1: class UsersController < ApplicationController
   2:   def show
   3:       @user = User.find(params[:id])
   4:       byebug
=> 5:    end
   6:   
   7:   def new
   8:   end
   9: end
(byebug)

paramsの内容をYAML形式で表示してみましょう.

(byebug)puts params.to_yaml
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: show
  id: '1'
permitted: false
nil

yメソッドも試してみます.

 (byebug) y params
*** NoMethodError Exception: undefined method `y' for #<UsersController:0x00007fe159010210>

Byebugではyメソッドは使えないみたいですね.

演習2

newアクションではインスタンスメソッドの@userは定義されていませんね.

7.1.4

演習1, 2

省略.

演習3

リスト7.12と7.13を見比べると, コードの量が異なりますね. ハッシュを引数にとる場合は, ハッシュの中から値を取り出すコードを書く必要があります. しかしキーワード引数であれば, それは不要です.

他にも, メソッドが予期していない引数を受け取った場合にも挙動が異なります. ハッシュ引数の場合, 以下のような誤った引数を指定してもエラーが発生しません.

grabatar_for @user, size: 100, shape: :square

ハッシュを受け取っている以上, 間違った引数と認識されないわけですね. しかしキーワード引数の場合, 上記のような引数を指定するとエラーが発生します. 定義していないキーワードが存在するからですね.

7.2 ユーザー登録フォーム

7.2.1

演習1

:name:nomeと書き換えユーザー登録ページにアクセスすると, 以下のようなエラーが表示されました.

undefined method `nome' for #<User:0x00007fe154bddef8>

演習2

Vimの置換コマンドを使ってffoobarに書き換えましょう.

:%s/f\./foobar\.

ファイル全体のf.foobar.に置換しています. これだけではブロック引数の|f|の部分は置換できないので, そこは手動で書き換えましょう.

以下のようにしてしまうと無駄なところまで置換されるので注意ですね.

:%s/f/foobar

この状態でユーザー登録ページにアクセスしても, 問題なく表示されます. では何が問題なのでしょうか...? わかりませんでした. でもあまりよくないとのことですから, 戻しておきました. 戻すときは以下のコマンドで良いですね.

:%s/foobar/f

7.2.2

演習1

いちいちHTMLタグを書くのなんて面倒ですもんね.

7.3 ユーザー登録失敗

7.3.1

Rails側ではシンボル, HTML側では文字列というのが少しややこしいですね.

7.3.2

演習1

省略.

7.3.3

演習1

省略.

演習2

入力データの検証に失敗した場合, createコントローラによって, newページがレンダリングされます. あくまでアクセスしているURLはcreateに対応する/usersということでしょうかね.

7.3.4

演習1

こちらを参考にしました.

assert_select 'div#error_explanation'
assert_select 'div.field_with_errors'
assert_select 'li', "Name can't be blank"
assert_select 'li', "Email is invalid"
assert_select 'li', "Password is too short (minimum is 6 characters)"
assert_select 'li', "Password confirmation doesn't match Password"

演習2

何が問題になっているかを整理しましょう.

config/routes.rbに以下のルーティングを設定しました.

post '/signup', to: 'users#create'

これによって, /signupへのPOSTリクエストに, createアクションが対応するようになりました.

しかしテストコードでPOSTリクエストをしているURLは/usersのままです.

assert_no_difference 'User.count' do
    post users_path, params: #略
end

それなのにテストをパスしまっているのはどうしてだろうというのが, この演習の問いです.

答えとしては, routes.rbに上記の設定をしたとしても, /usersへのPOSTリクエストとcreateアクションの対応は消えたわけではないとうことでしょうね.

演習3

以下のように変更し, テストにパスすることを確認しました.

assert_no_difference 'User.count' do
    post signup_path, params: #略
end

演習4

正しいアクションが指定されているかを確認するテストを書き加え, テストにパスすることを確認しました.

assert_select 'form[action="/signup"]'

7.4 ユーザー登録成功

7.4.1

演習1, 2

省略.

7.4.2

演習1, 2

省略.

7.4.3

演習1, 2

省略.

7.4.4

演習1

これまでのように, HTMLタグの有無を確認するテストをはじめは思いつきました.

assert_select 'div.alert-success'

flashが空でないかを確認するのであれば, 以下のように書けます.

assert_not flash.empty?

この書き方でテストがパスすることが確認できました. しかしこれだと演習2の変更がうまくいっているかどうかのテストはできませんね.

演習2, 3

省略.

演習4

データの保存に失敗した場合はリダイレクトが行われません. したがってテンプレートファイルのassertionでこのバグを検知することができます.

7.5 プロのデプロイ

省略.

Docker環境でByebugを使う方法

概要

Railsチュートリアルの第7章でByebugを使っています. Docker環境でByebugを利用するためには, 追加手順が必要なので書き残しておきます.

本当ははじめから全てが整った環境を構築できれば申し分ないですが, 途中からでも以下の手順を実行すれば問題ありません.

手順

以前書いたこちらの記事の通りに環境が出来上がっている状態からスタートします.

docker-compose.ymlに, 以下の設定を加えます.

# docker-compose.yml
services:
  web:
    stdin_open: true
    tty: true

これら2つの設定により, コンテナ内の標準入出力をローカルマシンのターミナルにアタッチする準備が整います.

さてComposeファイルを修正したので, それを反映させなくてはなりません. 先ほども述べたように, すでに環境が構築し終わった後にこの変更を加えています. この場合は以下のコマンドを実行すればOKです.

$ docker-compose up -d

公式ドキュメントを見て, コマンドの説明を見てみましょう.

If there are existing containers for a service, and the service’s configuration or image was changed after the container’s creation, docker-compose up picks up the changes by stopping and recreating the containers (preserving mounted volumes).

service's configurationへの変更は, コンテナを再構築することにより適用されるということですね.

コンテナが起動した状態でもいいの?

上で引用した説明分に書かれているように, すでにコンテナを起動している状態でも上のコマンドを実行することができます.

docker-compose restartではないの?

これについては公式ドキュメントに書いてあります.

If you make changes to your docker-compose.yml configuration these changes are not reflected after running this command.

このコマンドではComposeファイルの変更は反映されません.

docker-compose buildからやり直す必要はないの?

これも公式ドキュメントに書いてあります.

If you change a service’s Dockerfile or the contents of its build directory, run docker-compose build to rebuild it.

このコマンドを使うのは, DockerfileGemfileに変更があった場合です.

Byebugの使い方

上の手順を実行することにより, Byebugを使う準備が整いました. ここからはByebugの使い方です. 以下のコマンドを実行し, コンテナが起動した状態にあるとします.

$ docker-compose up -d

次のコマンドで, Railsが動いているコンテナのIDもしくはコンテナ名を調べます.

$ docker ps

あとは今調べたコンテナの標準入出力を, ローカルマシンのターミナルにアタッチするコマンドを実行します.

$ docker attach <container name or ID>

この状態でWebアプリケーションにアクセスすると, Byebugの出力がターミナルに表示されます.

デタッチする

コンテナの標準入出力を, ローカルマシンのターミナルにアタッチする方法がわかりました. 大事なのはここからです. デタッチを正しく行わないと面倒なことになります.

デバッグが終了し, コンテナの標準入出力をデタッチしたい場合は以下を実行してください.

Ctrl+P, Ctrl+Q

つまりCtrlキーを押しっぱにしたままpqと押せばいいわけですね. もしCtrl+Cを押してしまった場合は面倒なことになる可能性があります. そのときは こちらの記事を参照してください.

【Rails, Docker】コンテナが起動後すぐに落ちてしまうトラブルの原因とその対処法

概要

DockerでRails環境を構築すると, コンテナが起動しないトラブルに見舞われることがあります. その原因と対処法を説明します.

トラブルケース

以下のようなコマンドでRails Serverが稼働しているコンテナに入ったとします.

docker-compose exec <Rails container> bash

一通り作業を終え, コンテナから出る際に

Ctrl+D

で抜けるとコンテナが停止してしまいます. そこであわてて

docker-compose up -d

としても, ほんの一瞬起動するだけで, すぐコンテナが停止してしまいます.

対処法

原因よりも大事であろう対処法を先に書きます. 以下のファイルを削除してください.

application_root/tmp/pids/server.pid

普通applicaion_rootはローカルマシンにマウントしているでしょうから, コンテナが起動できなくても削除できるはずです. 削除し終わったら, いつも通り起動させれば問題なく動きます.

docker-compose up -d

原因のヒント

ここから原因について述べていきましょう. まずは原因のヒントとなるログメッセージを見ます.

この症状が見られた際, -dをつけずに起動を試みると, 起動中のログメッセージが表示されます.

docker-compose up

ログメッセージを見ると, このように書かれた行が見つかります.

web_1  | => Rails 5.2.0 application starting in development 
web_1  | => Run `rails server -h` for more startup options
web_1  | A server is already running. Check /myapp/tmp/pids/server.pid.
web_1  | Exiting
micropost_web_1 exited with code 1

起動しようと頑張っていますが, 最終的にはExitしていますね. その理由がまさにこれです.

A server is already running.

つまり原因は, 「すでにサーバーが起動しているんだし, 俺の出番は無ぇな」とRailsが判断してしまっていることにあります. Rails Serverが起動しているものと, Railsが勘違いしてしまったわけですね.

その勘違いの原因となっているのが, tmp/pids/server.pidというファイルです. 中身を見てみましょう.

$ cat tmp/pids/server.pid
1

ひとつだけ書かれた数字はRails ServerのPIDです. 大学で数学を専攻していた方は単項イデアル整域だと勘違いするかもしれませんが, これはProcess IDの略です.

Linuxでは各プロセスにIDをつけて管理しています. またDockerでは, プロセスIDが1であるプロセスが終了した場合, コンテナも終了するようになっています.

これでトラブルが発生した流れが見えてきましたね.

トラブル発生の流れ

Ctrl+Dで強制終了してしまったことにより, tmp/pids/server.pidが残ったままになってしまいました. docker-compose downで終了したならば, これらの一時ファイルやネットワーク, コンテナを自動で消去してくれるはずでした.

この状態で起動を試みると, Railstmp/pids/内の消去されなかったファイルを見て, Rails Serverが起動していると勘違いを起こします.

するとRails Serverは起動を中止します.

プロセスIDが1であるプロセスが終了したため, コンテナも停止します.

まとめ

  • 正しいコマンドで終了しましょう.
  • トラブルが起きたら落ち着いて対処しましょう.

Railsチュートリアル 第6章

6.1 Userモデル

6.1.1

演習1

schema.rbの内容は以下の通りです.

ActiveRecord::Schema.define(version: 2018_06_25_044152) do

  create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

演習2, 3

省略.

6.1.2

演習1, 2

以下のコマンドを実行し, コンテナの中に入ります.

$ docker-compose exec web bash

Railsの対話モードに入ります.

$ rails console

クラスについて調べます.

>> user = User.new
   (0.5ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

>> user.class.superclass
=> ApplicationRecord(abstract)
>> user.class.superclass.superclass
=> ActiveRecord::Base

6.1.3

演習1

たしかにStringクラスです.

>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil>

>> user.name.class
=> String
>> user.email.class
=> String

演習2

こんな感じです.

>> user.created_at.class
=> ActiveSupport::TimeWithZone
>> user.updated_at.class
=> ActiveSupport::TimeWithZone

6.1.4

演習1

find_by_nameメソッドも使えました.

>> User.find_by(name: "Michael Hartl")
  User Load (0.8ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = 'Michael Hartl' LIMIT 1
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-06-25 05:01:54", updated_at: "2018-06-25 05:01:54">

>> User.find_by_name("Michael Hartl")
  User Load (0.9ms)  SELECT  `users`.* FROM `users` WHERE `users`.`name` = 'Michael Hartl' LIMIT 1
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-06-25 05:01:54", updated_at: "2018-06-25 05:01:54">

演習2, 3

>> User.all.class
=> User::ActiveRecord_Relation

>> User.all.length
  User Load (1.0ms)  SELECT `users`.* FROM `users`
=> 2

6.1.5

演習1

>> user.name = "Michael Jackson"
=> "Michael Jackson"

>> user.save
   (0.6ms)  SAVEPOINT active_record_1
  User Update (1.0ms)  UPDATE `users` SET `name` = 'Michael Jackson', `updated_at` = '2018-06-25 05:23:28' WHERE `users`.`id` = 1
   (0.6ms)  RELEASE SAVEPOINT active_record_1
=> true

演習2

>> user.update_attributes(email: "mjackson@example.com")
   (0.6ms)  SAVEPOINT active_record_1
  User Update (0.6ms)  UPDATE `users` SET `email` = 'mjackson@example.com', `updated_at` = '2018-06-25 05:24:00' WHERE `users`.`id` = 1
   (0.9ms)  RELEASE SAVEPOINT active_record_1
=> true

演習3

>> user.created_at = 1.year.ago
=> Sun, 25 Jun 2017 05:24:28 UTC +00:00

6.2 ユーザーを検証する

6.2.1

省略.

6.2.2

演習1

nameemailは空白であってはならないからですね.

>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.valid?
=> false
>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]

演習2

ハッシュに使われているキーがシンボルであることに注意すると, 以下のようにエラーメッセージを取得することができます.

>> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
>> u.errors.messages[:email]
=> ["can't be blank"]

6.2.3

演習1, 2

以下のようなエラーメッセージが得られます.

>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.name = "a"*51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> u.valid?
=> false
>> u.errors.full_messages
=> ["Name is too long (maximum is 50 characters)", "Email can't be blank"]

6.2.4

省略.

6.2.5

  • アプリケーションレベルの一意性
  • データベースレベルの一意性
  • インデックス
  • コールバックメソッド などかなり盛りだくさんなセクションでしたが, チュートリアル通りに進めればよいだけなので, 省略します.

    6.3 セキュアなパスワードを追加する

    6.3.1

    パスワード保存用のカラムをマイグレーションファイルに定義してdb:migrate. ここまでしっかりやってくれば予想ができる手順ですね. それにしてもhas_secure_passwordの万能感がハンパないです.

    6.3.2

    rails consoleにて確かめました.

>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil, password_digest: nil>
>> user.valid?
=> false
>> user.errors.full_messages
=> ["Password can't be blank"]

6.3.3

rails consoleにて確かめました.

>> user = User.new
>> user.name = "Foo"
>> user.email = "foo@bar.com"
>> user.password = "pass"

>> user.valid?
=> false
>> user.errors.full_messages
=> ["Password is too short (minimum is 6 characters)"]

6.3.4

演習1

まずはUserを作り, 保存します.

>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com", password: "foobar", password_confirmation: "foobar")

>> user.save

一旦exitしてから再びconsoleを起動します. 検索してみると確かにさっき保存したレコードが存在しますね.

>> user = User.find_by_name("Michael Hartl")
=> #<User id: 4, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-06-25 09:33:51", updated_at: "2018-06-25 09:33:51", password_digest: "$2a$10$tM4ebAaSFIzfhqzi6.Lo/Ofs6/TfKpnP7LrgkCRV/z0...">

演習2

名前を書き換えて保存を試みます.

>> user.name = "Michael Jackson"
=> "Michael Jackson"
>> user.save
=> false
>> user.errors.full_messages
=> ["Password can't be blank", "Password is too short (minimum is 6 characters)"]

パスワードを要求されてしまいました. データを更新するには認証が必要ということでしょうかね.

演習3

検証を回避しつつデータを更新する方法が, 6.1.5に書かれています.

>> user.update_attribute(:name, "Michael Jackson")
=> true

Railsチュートリアル第5章

5.1 構造を追加する

5.1.1

レイアウトファイルを更新し, home.html.erbも編集をします. curlコマンドでRailsのロゴをダウンロードしました. 確認のためにFinderからロゴ画像を開いたところ, 邪魔な.DS_Storeファイルが生成されてしまいました.

$ git status

# 中略

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    ../../../.DS_Store
    ../../.DS_Store
    ../.DS_Store
    rails.png

調べてみると, いらないファイルのようですね. 一気に削除してしまいましょう. しかしこの削除作業は今後も行う可能性があるので, 削除コマンドのエイリアスを作成することにします. ~/.bashrcエイリアスを定義します.

# ~/.bashrc
alias delds='find ~/path/to/app/root/ -name .DS_Store | xargs rm -v'

変更を適用しましょう.

$ source ~/.bashrc

あとは定義したコマンドを実行すればファイルを削除できます. これらのファイルは今後発生する可能性があるわけですが, Gitにも追跡して欲しくありません. ということで.gitignoreにも書いておきましょう.

# .gitignore

.DS_Store

演習1, 2, 3

本文と同様なので省略しますが, 猫画像は消しておきましょう.

5.1.2

省略します.

5.1.3

こちらも書かれている通りにやるだけなので省略します.

5.2 Sassとアセットパイプライン

省略します.

5.3 レイアウトのリンク

5.3.1

このセクションの内容はすでに3章で終えていますね.

5.3.2

演習1, 2, 3

エディタのUNDO機能を使って変更を元に戻すと書かれていますが, Gitの機能を使うほうが楽な気がします. まずは演習以前の内容をコミットしておきます.

$ git commit -a

演習を終えたら, ワークツリーの変更をなかったことにします.

# アプリケーションルートで実行
$ git checkout .

5.3.3

省略します.

5.3.4

演習1

省略します.

演習2

application_helper_test.rbは以下のようになりました.

require 'test_helper'

class ApplicationHelperTest < ActionView::TestCase
    def setup
        @base_title = 'Ruby on Rails Tutorial Sample App'
    end
    test "full title helper" do
        assert_equal full_title ,@base_title
        assert_equal full_title("Help"), "Help | #{@base_title}"
    end
end

5.4 ユーザー登録: 最初のステップ

5.4.1

はじめ間違って以下のように実行してしまいました.

docker-compose exec -T web rails generate controller User new

どこが違うのかというと, Userが単数形になっています. これのせいでエラーが出て, 少し時間を無駄にしてしまいました.

5.4.2

演習1, 2

省略します.

演習3

テストコードは以下のようになりました.

# test/integration/site_layout_test.rb

get signup_path
    assert_select "title", full_title("Sign up")

Railsチュートリアル 第4章

Railsチュートリアルの第4章です. この章はわりと演習が楽しかったですね.

4.1 動機

トピックブランチを作成し, チェックアウトします.

4.1.1

省略.

4.1.2

カスタムヘルパーの作成は, 4.2を終えてからにします.

4.2 文字列とメソッド

4.2.1

省略.

4.2.2

省略.

4.2.3

ぱっと見, 文字列展開ちゃんとされるの?と思いましたが大丈夫でしたね.

>> string = "foobar"
>> puts "The string '#{string}' is nonempty." unless string.empty?

演習1

省略.

演習2

リバースしたかどうかが分かる単語でも試してみるとよいですね.

演習3

trueが返ってきました.

演習4

nilが返ってきました.

>> s = "onomatopoeia"
>> puts "It's a palinrome!" if s == s.reverse
=> nil

4.2.4

演習1, 2

真似して書くだけですね.

演習3

次のようになりました.

>> palindrome_tester("racecar").nil?
It's a palindrome!
=> true

4.2.5

ここでやり残していたカスタムヘルパーの作成を行いました.

テストコードを書き直します.

# test/controllers/static_pages_controller_test.rb
# の一部を抜粋しています

def setup
  @base_title = "Ruby on Rails Tutorial Sample App"
end

test "should get root" do
  get root_url
  assert_response :success
end

test "should get home" do
  get static_pages_home_url
  assert_response :success
  assert_select "title", @base_title #書き直し
end

レイアウトを変更します.

# app/views/layouts/application.html.erb
# の一部を抜粋しています

<title><%= full_title(yield(:title)) %></title>

Homeではbase_titleしか表示させないので, タイトル設定を消します.

# app/views/static_pages/home.html.erb

<% provide(:title, "Home") %>  # この行を削除
<h1>Sample App</h1>
<p>
        This is the home page for the 
        <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a> 
        sample application.
</p>

4.3 他のデータ構造

4.3.1

演習1, 2, 3

# 指定された文字列を変数sに格納
>> s = "A man, a plan, a canal, Panama"
=> "A man, a plan, a canal, Panama"

# 文字列を分割して配列にする
>> a = s.split(", ")
=> ["A man", "a plan", "a canal", "Panama"]

# 再びひとつの文字列に
>> s = a.join
=> "A mana plana canalPanama"

# スペースを無くします
>> s = s.split.join
=> "AmanaplanacanalPanama"

# このままでは回文ではありませんが,
>> s == s.reverse
=> false

# 小文字に揃えれば回文です
# メソッドチェーンの順番を変えたものもチェックしておきました.
>> s.downcase == s.downcase.reverse
=> true
>> s.downcase == s.reverse.downcase
=> true

演習4

# 範囲オブジェクトを配列に変換し, 変数tに格納
>> t = ("a".."z").to_a   
=> ["a", "b", (途中省略), "z"]

# 指定された2つとも取り出せるかと思いきやダメ
>> t[7,-7]
=> nil
>> t[7]
=> "h"
>> t[-7]
=> "t"

# インデックス2以降から3つ取り出す, という意味なんですね
>> t[2,3]
=> ["c", "d", "e"]

4.3.2

演習1

範囲オブジェクトをかっこで囲うのを忘れてはいけませんね. またputsを書かないと出力が目に見えません.

>> (0..16).each {|n| puts n**2}
0
1
4
9
16
25
36
49
64
81
100
121
144
169
196
225
256
=> 0..16

演習2

ブロックを使わずとも定義できてしまいますね.

>> def yeller (array)
>>   array.join.upcase
>> end
=> :yeller

>> yeller("abc".split)
=> "ABC"

ブロックを使うなら以下のようになりますかね.

>> def yeller (array)
>>   array.map {|s| s.upcase}.join
>> end
=> :yeller

>> yeller("abc".split)
=> "ABC"

演習3

本文中にある方法だと, 異なる8文字の順列を作ることになります. ここはもう一歩踏み込んで, 重複ありの文字列を生成させてみます. 以下のような実装を思いつきました.

>> def random_dubdomain
>>   (0..7).map do
>>     ("a".."z").to_a.shuffle[0]
>>   end
>>   .join
>> end
=> :random_dubdomain

>> random_dubdomain
=> "uknjxsoa"
>> random_dubdomain
=> "htoenews"

はじめ8.timesを使うことを思いつきましたが, 無駄にローカル変数を定義することになりそうです.

>> def random_subdomain
>>   tmp = []
>>   8.times do 
>>     tmp << ("a".."z").to_a.shuffle[0]
>>   end
>>   tmp.join
>> end
=> :random_subdomain

>> random_subdomain
=> "xruoispc"
>> random_subdomain
=> "wqdizcoc"

演習4

ヒント通りにやりました. split('')がポイントですね.

>> def string_shuffle (s)
>>   s.split('').shuffle.join
>> end
=> :string_shuffle

>> string_shuffle("foobar")
=> "ofbaro"

4.3.3

演習1

rails consoleでは日本語が打てないので英語で対処しました.

>> hash = { "one" => "uno", "two" => "dos", "three" => "tres" }
=> {"one"=>"uno", "two"=>"dos", "three"=>"tres"}
>> hash.each do |key, value|
>>   puts "'#{key}' is  '#{value}' in Spanish" 
>> end
'one' is  'uno' in Spanish
'two' is  'dos' in Spanish
'three' is  'tres' in Spanish
=> {"one"=>"uno", "two"=>"dos", "three"=>"tres"}

演習2

サザエさんにご登場いただきました. 本文にあるように, 中括弧{ }の内側には空白を入れています.

>> person1 = { first: "Masuo", last: "Fuguta" }
=> {:first=>"Masuo", :last=>"Fuguta"}
>> person2 = { first: "Sazae", last: "Fuguta" }
=> {:first=>"Sazae", :last=>"Fuguta"}
>> person3 = { first: "Tarao", last: "Fuguta" }
=> {:first=>"Tarao", :last=>"Fuguta"}

>> params = { father: person1, mather: person2, child: person3 }
=> {:father=>{:first=>"Masuo", :last=>"Fuguta"}, :mather=>{:first=>"Sazae", :last=>"Fuguta"}, :child=>{:first=>"Tarao", :last=>"Fuguta"}}

>> params[:father][:first] == person1[:first]
=> true

演習3

ランダムな文字列を作るメソッドを定義します.

>> def random_string
>>   (0..15).map do
>>     ("a".."z").to_a.shuffle[0]
>>   end
>>   .join
>> end
=> :random_string

userを作ります.

>> user = { name: "logicoffee", email: "logicoffee@example.com", password_digest: random_string }
=> {:name=>"logicoffee", :email=>"logicoffee@example.com", :password_digest=>"wjvqvczpmxmmvdwe"}

演習4

ここを見てみると, 2つのハッシュを組み合わせて新たなハッシュを返すメソッドのようですね. キーが重複している場合は上書きされるようです. やってみましょう.

>> { "a" => 100, "b" => 200 }.merge({ "b" => 300 })
=> {"a"=>100, "b"=>300}

やはりそのようですね.

4.4 Rubyにおけるクラス

4.4.1

演習1, 2, 3

範囲オブジェクトの比較には注意が必要ですね.

>> r = Range.new(0,10)
=> 0..10

>> r == 0..10
Traceback (most recent call last):
        1: from (irb):4
ArgumentError (bad value for range)
  
>> r == (0..10) 
=> true

4.4.2

演習1

>> r = 0..10
=> 0..10
>> r.class
=> Range
>> r.class.superclass
=> Object
>> r.class.superclass.superclass
=> BasicObject

>> h = { name: "Taro" }
=> {:name=>"Taro"}
>> h.class
=> Hash
>> h.class.superclass
=> Object
>> h.class.superclass.superclass
=> BasicObject

>> s = :name
=> :name
>> s.class
=> Symbol
>> s.class.superclass
=> Object
>> s.class.superclass.superclass
=> BasicObject

演習2

たしかにselfキーワードを省略できますね.

>> class Word < String
>>  def palindrome?
>>     self == reverse
>>  end
>> end
=> :palindrome?

>> s = Word.new("level")
=> "level"

>> s.palindrome?
=> true

4.4.3

演習1

例のように=> :Stringとはなりませんでした.

>> class String
>>   def palindrome?
>>     self == self.reverse
>>   end
>> end
=> :palindrome?

>> "racecar".palindrome?
=> true
>> "onomatopoeia".palindrome?
=> false
>> "Malayalam".downcase.palindrome?
=> true

演習2, 3

>> class String
>>   def shuffle
>>     self.split("").shuffle.join
>>   end
>> end
=> :shuffle

>> "abcdefg".shuffle
=> "dgcbeaf"

>> class String
>>   def shuffle
>>     split("").shuffle.join
>>   end
>> end
=> :shuffle

>> "onomatopoeia".shuffle
=> "eopiooataonm"

4.4.4

省略します.

4.4.5

演習1, 2, 3

example_user.rbを用意します. full_nameメソッドと, alphabetical_nameメソッドで, インスタンス変数へのアクセス方法を変えてみました.

class User
    attr_accessor :first_name, :last_name, :email
    def initialize (attributes = {})
        @first_name = attributes[:first_name]
        @last_name  = attributes[:last_name]
        @email      = attributes[:email]
    end

    def full_name
        "#{first_name} #{last_name}"
    end

    def alphabetical_name
        "#{@last_name}, #{@first_name}"
    end

    def formatted_email
        "#{full_name} <#{email}>"
    end
end

特に問題なくできました.

>> require './example_user.rb'
=> true

>> user = User.new(first_name: "Michael", last_name: "Hartle", email: "mhartl@example.com")
=> #<User:0x000055993280b6c0 @first_name="Michael", @last_name="Hartle", @email="mhartl@example.com">

>> user.full_name
=> "Michael Hartle"
>> user.alphabetical_name
=> "Hartle, Michael"
>> user.formatted_email
=> "Michael Hartle <mhartl@example.com>"
>> user.full_name.split == user.alphabetical_name.split(', ').reverse
=> true