20190126のRubyに関する記事は10件です。

Daru::View でグラフを表示してみた

グラフを描くツール Daru::View

 Rubyでグラフを表示するDaru::Viewというツールを使ってみた。Daru::Viewは2017年に始まった比較的新しいプロジェクトで開発者はインド、バンガロール在住のShekhar Prasad Rajakさん。現在も活発な開発が続いている。Google ChartsHighchartsを使用している。

インストール

2019年1月26日時点では、Daru, Daru::Viewともに、gemパッケージではなく、githubリポジトリから最新版をインストールする。少なくとも私の環境では、安定版のdaruでは正常に動作しない。ここではspecific_installを利用して、GitHubのリポジトリからインストールした。

gem install specific_install
gem install https://github.com/SciRuby/daru
gem install https://github.com/SciRuby/daru-view

そのまえにJupyter + IRuby環境がインストールされている必要がある。これは少々面倒だが、IRuby インストールガイドに詳しい方法が解説されている。

使ってみる

jupyter lab もしくは jupyter notebook とタイプしてJupyterを起動。

折れ線グラフ

1821–1934年のカナダの山猫の年間捕獲数のデータセットLynxを折れ線グラフで描出してみる。まずは山猫のデータセットを読み込んでDaru::DataFrameオブジェクトを作る。

require 'daru/view'
require 'rdatasets' # gem install rdatasets
lynx = Daru::DataFrame.from_rdatasets :datasets, :lynx

image.png

googlechartsを指定する。ほかに highchart などが指定できる。

# Google Charts を指定する
Daru::View.plotting_library = :googlecharts

グラフを描く。オプション無しで折れ線グラフになる。

chart = Daru::View::Plot.new(lynx)
chart.show_in_iruby

image.png

show_in_iruby はつけず、IRuby側で判定するべきだと思うが、そうしない理由がなにかあるのかもしれない。

面グラフ

オプション type: :area を指定。

chart = Daru::View::Plot.new(lynx, type: :area)
chart.show_in_iruby

image.png

棒グラフ

オプション type: :bar を指定。

chart = Daru::View::Plot.new(lynx, type: :bar, height: 600)
chart.show_in_iruby

image.png

グラフの色を変更する

chart = Daru::View::Plot.new(lynx, type: :bar, height: 600, colors: ["Green"])
chart.show_in_iruby

colors: ["Green"] などとすると変更される。
image.png

タイトルや軸を入れる

chart = Daru::View::Plot.new(lynx,
    type: :area,
    title: "カナダの山猫の年間捕獲数",
    hAxis: {title: "年"},
    vAxis: {title: "頭数"},
    colors: ["Orange"],
    height: 360,
    )
chart.show_in_iruby

image.png

バブルチャート

usarrests = RDatasets.load :datasets, :USArrests
chart = Daru::View::Plot.new(usarrests,
    type: :bubble,
    width: 700,
    height: 700,
    colors: ['yellow', 'red']
    )
chart.show_in_iruby

image.png

散布図

cars = RDatasets.load :datasets, :cars
chart = Daru::View::Plot.new(cars,
    type: :scatter,
    width: 600,
    height: 600
    )
chart.show_in_iruby

image.png

(続く)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails チュートリアル 第11章 アカウント有効化(AcctionMailer Activation)やSendGridの使い方など

前回の続き

著者略歴
YUUKI
ポートフォリオサイト:Pooks
RailsTutorial2周目

11 アカウントの有効化

現時点では、新規登録したユーザーは初めから全ての機能にアクセスできるようになっている。
本章では、アカウントを有効化するステップを新規登録の途中に差し込むことで、本当にそのメールアドレスの持ち主なのか、確認できるようにする。

具体的には

①有効化トークンやダイジェストを関連付けておく
②有効化トークンを含めたリンクをユーザーにメールで送信
③ユーザーがそのリンクをクリックすると有効化

このような仕組みで、メールアドレスの持ち主であることを証明させる。

第12章でも似たような仕組みを使って、ユーザーがパスワードを忘れた時にパスワードを再設定できる仕組みを実装する。

これらの機能ごとに新しいリソースを作成し、コントローラ/ルーティング/データベース以降/の例について、1つずつ学んでいく。

最後に、Railsの開発環境や本番環境からメールを実際に送信する方法についても学ぶ。

アカウントを有効化する段取りは、ユーザーログイン&ユーザーの記憶と似ている。

①ユーザーの初期状態を「有効化されていない(unactivated)」にする
②ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する
③有効化ダイジェストはDBに保存。有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
④ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、DB内に保存しておいた有効化ダイジェストと比較することで、トークンを認証する
⑤ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み(activated)」に変更する。

都合の良いことに、今回実装するアカウント有効化やパスワード再設定の仕組みと、以前に実装したパスワードや記憶トークンの仕組みにはよく似た点が多いので、多くのアイデアを使い回すことができる。

例えば、User.digestUser.new_token、改良版のuser.authenticated?メソッドなど

検索キー、string、digest、authenticationごとの点

検索キー string digest authentication
email password password_digest authenticate(password)
id remember_token remember_digest authenticated?(:remember, token)
email activation_token activation_digest authenticated?(:activation, token)
email reset_token reset_digest authenticated?(:reset, token)

出典:表 11.1: ログイン/記憶トークン/アカウントの有効化/パスワードの再設定で似ている点

今章でアカウント有効化に必要なリソース、データモデルを作っていく。
アカウント有効化時のメール送信部分は
メイラーを使って作っていく。

また、authenticated?メソッドを使って、実際にアカウントを有効化する部分も実装していく。

11.1 AccountActivationsリソース

8章で扱ったセッション機能を使って、アカウント有効化の作業をリソースとしてモデル化する。

アカウントの有効化リソースはActive Recordのモデルとは関係ないため、関連付けはしない。
その代わり、この作業に必要なデータ(有効化トークンや有効化ステータスなど)をUserモデルに追加する。

アカウント有効化もリソースとして扱いたいが、いつもとは少し使い方が異なる。

例えば、有効化用のリンクにアクセスして有効化のステータスを変更する部分では、RESTのルールに従うとPATCHリクエストとupdateアクションになるべき。

しかし、有効化リンクはメールでユーザーに送られる。
ユーザーがこのリンクをクリックすれば、それはブラウザで普通にクリックしたときと同じであり、その場合ブラウザから発行されるのは(updateアクションで使うPATCHリクエストではなく)GETリクエストになってしまう。
このため、ユーザーからのGETリクエストを受けるために、updateアクションではなくeditアクションに変更して使っていく。

$ git checkout -b account-activation

11.1.1 AccountActivationsコントローラ

UsersリソースやSessionsリソースのときと同様に、AccountActivationsリソースを作るために、
まずはAccountActivationsコントローラを生成する。

$ rails g controller AccountActivations

ここで、有効化のメールには

edit_account_activation_url(activation_token, ...)

これは、editアクションへの名前付きルートが必要になるということ。
そこで、まずは名前付きルートを扱えるようにするため、ルーティングにアカウント有効化用のresources行を追加する。

routes.rb
Rails.application.routes.draw do
  get 'sessions/new'

  get 'users/new'

  root 'static_pages#home'
  get     '/help',    to: 'static_pages#help'
  get     '/about',   to: 'static_pages#about'
  get     '/contact', to: 'static_pages#contact'
  get     '/signup',  to: 'users#new'
  post    '/signup',  to: 'users#create'
  get     '/login',   to: 'sessions#new'
  post    '/login',   to: 'sessions#create'
  delete  '/logout',  to: 'sessions#destroy'
  resources :users                                      # usersリソースをRESTfullな構造にするためのコード。
  resources :account_activations, only: [:edit]         # editアクションのみaccount_activationsリソースを適用
end
HTTPリクエスト URL Action 名前付きルート
GET /account_activation//edit edit edit_account_activation_url(token)

出典:表 11.2: アカウント有効化用のRESTfulなルーティング設定 (リスト 11.1)

これからアカウント有効化用のデータモデルとメイラーを作って行く。

演習

1:テストがパスすることを確認

確認済み

2:名前付きルートでは、_pathではなく_urlを使うように記してある。その理由は?

_urlは(http://~)を指定してメールから飛べるようにしている。
(_pathだと相対パスな為http://〜からの指定ができない)

11.1.2 AccountActivationのデータモデル

有効化のメールは一位の有効化トークンが必要。

例えば送信メールとデータベースのそれぞれに同じ文字列を置いておく方法がある。
しかし、この方法だとデータベースの内容が漏れた時、多大な被害に繋がってしまう。

攻撃者がDBへのアクセスに成功して、新しく登録されたユーザーアカウントの有効化トークンを盗み取り、本来のユーザーが使う前にそのトークンを使ってしまうケースなど、セキュリティが甘いケースに繋がってしまう。

このような事態を防ぐために、パスワードの実装や記憶トークンの実装と同じように、仮想的な属性を使ってハッシュ化した文字列をDBに保存するようにする。

具体的には

user.activation_token

このようなコードで仮想属性の有効化トークンにアクセスし

user.authenticated?(:activation, token)

Userモデルで編集したauthenticated?メソッドを使って、activated属性を追加して論理値を取るようにする。

これで、自動生成の論理値メソッドと同じような感じで、ユーザーが有効であるかどうかをテストできるようになる。

if user.activated?

ユーザーを有効化した時の、変更後のデータモデルはこうなる

image.png

出典:図 11.1: Userモデルにユーザー有効化用の属性を追加する

次のマイグレーションをコマンドラインで実行し、データモデルを追加すると、3つの属性が新しく追加される。

rails g migration add_activation_to_users ¥

次に、admin属性の時と同様に、activated属性のデフォルトの論理値をfalseにしておく。

[timestamp]_add_admin_to_users.rb
class AddActivationToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :activation_digest, :string
    add_column :users, :activated, :boolean, default: false
    add_column :users, :activated_at, :datetime
  end
end

マイグレーションを実行して変更を反映。

$ rails db:migrate

Activeトークンのコールバック

ユーザーが新しい登録を完了するためには必ずアカウントの有効化が必要になる。
その為、「有効化トークン」や「有効化ダイジェスト」はユーザーオブジェクトが作成される前に、生成されるようにしておく必要がある。

メールアドレスをDB保存時も、保存前に全部小文字に変換するようにしたが、その時はbefore_saveコールバックにdowncaseメソッドをバインドした。

オブジェクトにbefore_saveコールバックを用意しておくと、オブジェクトが保存される直前、オブジェクトの作成時や更新時にそのコールバックが呼び出される。

しかし、今回はオブジェクトが作成された時だけコールバックを呼び出したい。
(それ以外の時は呼び出したくない)

そこで、before_createコールバックを使う。

before_create :create_activation_digest

このコードはメソッド参照と呼ばれるもので、こうするとRailsはcreate_activation_digestというメソッドを探し、ユーザーを作成する前に実行するようになる。

6章では、before_saveに明示的にブロックを渡していたが、メソッド参照の方がおすすめ。

また、create_activation_digest(作成した有効化記憶トークンメソッド)メソッド自体はUserモデル内でしか使わない為、privateキーワードの中に書く。

private

   def create_activation_digest
      # 有効化トークンとダイジェストを作成および代入する
   end

クラス内でprivateキーワードより下に記述したメソッドが非公開なのは、コンソールで確かめられる。

>> User.first.create_activation_digest
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
NoMethodError: private method `create_activation_digest' called for #<User:0x007f8da4098e98>
Did you mean?  restore_activation_digest!
        from (irb):1

今回はbefore_createコールバックを使う目的は、トークンとそれに対応するダイジェストを割り当てるためである。

self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)

このコードでは、記憶トークンや記憶ダイジェストのために作ったメソッドを使いまわしている。
9章で扱ったrememberメソッドと比べてみる。

# 永続セッションのためにユーザーをデータベースに記憶する
def remember
  self.remember_token = User.new_token
  update_attribute(:remember_digest, User.digest(remember_token))
end

主な違いは、後者のupdate_attributeの使い方にある。

この違いは、記憶トークンやダイジェストは既にDBにいるデータベースのために作成されるのに対し、before_createコールバックの方はユーザーが作成される前に呼び出される。

このコールバックがあることで、User.newで新しいユーザーが定義されると、activation_token属性やactivation_digest属性が得られるようになる。

後者のactivation_digest属性は既にDBのカラムとの関連付けができあがっているので、ユーザーが保存されるときに一緒に保存される。
 

上記の説明をUserモデルに実装してみる。

user.rb
class User < ApplicationRecord
  # インスタンス変数の定義
  attr_accessor :remember_token , :activation_token                             # 記憶トークンと有効化トークンを定義
  before_save :downcase_email                                                   # DB保存前にemailの値を小文字に変換する
  before_create :create_activation_digest                                       # 作成前に適用

private

  # メールアドレスを全て小文字にする

  def downcase_email
    self.email = email.downcase                                                 # emailを小文字化してUserオブジェクトのemail属性に代入
  end

  # 有効化トークンとダイジェストを作成および代入する

  def create_activation_digest
    self.activation_token   =   User.new_token                                  # ハッシュ化した記憶トークンを有効化トークン属性に代入
    self.activation_digest  =   User.digest(activation_token)                   # 有効化トークンをBcryptで暗号化し、有効化ダイジェスト属性に代入
  end

サンプルユーザーの作成とテスト

seedsファイルを更新しサンプルユーザーを作成、
fixtureを更新してテスト時のサンプルユーザーを事前に作成しておく。

seeds.rb
User.create!(name:                    "Example User",
             email:                   "example@railstutorial.org",
             password:                "foobar",
             password_confirmation:   "foobar",
             admin:     true,
             activated: true,
             activated_at: Time.zone.now )

99.times do |n|
  name          = Faker::Name.name
  email         = "example-#{n+1}@railstutorial.org"
  password      = "password"
  User.create!(name:  name,
              email: email,
              password:              password,
              password_confirmation: password,
              activated: true,
              activated_at: Time.zone.now)
end

Time.zone.nowはRailsの組み込みヘルパーであり、サーバーのタイムゾーンに応じたタイムスタンプを返す。

users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true
  activated: true
  activated_at: <%= Time.zone.now %>

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>
  activated: true
  activated_at: <%= Time.zone.now %>

lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>
  activated: true
  activated_at: <%= Time.zone.now %>

malory:
  name: Malory Archer
  email: boss@example.gov
  password_digest: <%= User.digest('password') %>
  activated: true
  activated_at: <%= Time.zone.now %>

<% 30.times do |n| %>
user_<%= n %>:
  name:  <%= "User #{n}" %>
  email: <%= "user-#{n}@example.com" %>
  password_digest: <%= User.digest('password') %>
  activated: true
  activated_at: <%= Time.zone.now %>
<% end %>

DBを初期化して、サンプルデータを再度生成し直し、変更を反映

$ rails db:migrate:reset
$ rails db:seed

演習

1:テストが通ることを確認

確認済み

2:コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとするとNoMethodErrorが発生することを確認。また、そのUserオブジェクトからダイジェストの値も確認。

>> user = User.new
>> user.create_activation_digest
NoMethodError: private method `create_activation_digest' called for #<User:0x000000045483b0>
Did you mean?  restore_activation_digest!
        from (irb):2
>> user.digest
NoMethodError: undefined method `digest' for #<User:0x000000045483b0>
        from (irb):3
>> user.activation_digest
=> nil

3:6章で、email.downcase!を使いemail属性を変更する方法を学んだ。(破壊的メソッド)
このメソッドを使って、user.rbのdowncase_emailメソッドを改良してみる。

user.rb
  def downcase_email
    email.downcase!                                                             # emailを小文字化してUserオブジェクトのemail属性に代入
  end
$ rails t
11 tests, 15 assertions, 0 failures, 0 errors, 0 skips

テストがパスしたのでOK。

11.2 アカウント有効化のメール送信

データのモデル化が終わったので、今度はアカウント有効化メールの送信に必要なコードを追加する。

このメソッドではAction Mailerライブラリを使ってUserのメイラーを追加する。
このメイラーはUsersコントローラのcreateアクションで有効化リンクをメールで送信するために使う。

メイラーの構成はコントローラのアクションとよく似ており、メールのテンプレートをビューと同じ要領で定義する。
このテンプレートの中に有効化トークンと、メールアドレスを有効にするアカウントのアドレスのリンクを含め、使っていく。

11.2.1 送信メールのテンプレート

メイラーは、モデルやコントローラと同様にrails generateで生成できる。

$ rails g mailer UserMailer account_activation password_reset

このコマンドを実行したことにより、今回必要となるaccount_activationメソッドと、次章で必要となるpassword_resetメソッドが生成された。

また、上記のコマンドは生成したメイラーごとに、ビューのテンプレートが2つずつ生成される

アカウント有効化メイラーのビュー2つ

①テキストメール用のテンプレート
②HTMLメール用のテンプレート

アカウント有効化に使うテンプレートを確認してみる。

account_activation.text.erb
User#account_activation

<%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erb
account_activation.html.erb
<h1>User#account_activation</h1>

<p>
  <%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb
</p>

また、mailerディレクトリの中に生成されたメイラーファイルapplication_mailer.rbuser_mailer.rbの2つも確認。

この2つのファイルはメールの動きを設定する(モデルで言うコントローラみたいなもの)

application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: 'from@example.com'
  layout 'mailer'
end
user_mailer.rb
class UserMailer < ApplicationMailer

  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.user_mailer.account_activation.subject
  #
  def account_activation
    @greeting = "Hi"

    mail to: "to@example.org"
  end

  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.user_mailer.password_reset.subject
  #
  def password_reset
    @greeting = "Hi"

    mail to: "to@example.org"
  end
end

application_mailerでは、アプリケーション全体で共通のデフォルトのfromアドレスがある。
user_mailerではmail to:にて宛先のメールアドレスを設定している。

また、メールフォーマットに対応するメイラーレイアウトも使われている。
生成されるHTMLメイラーのレイアウトやテキストメイラーのレイアウトはapp/views/layoutsで確認できる。

生成されたコードにはインスタンス変数@greetingも含まれている。

このインスタンス変数は、丁度普通のビューでコントローラのインスタンス変数を利用できるのと同じように、メイラービューで利用できる。

最初に、生成されたテンプレートをカスタマイズして、実際に有効化メールで使えるようにする。

次に、ユーザーを含むインスタンス変数を作成してビューで使えるようにし、user.emailにメールを送信する。
user_mailer.rbでは、mailにsubjectキーを引数として渡している。この値は、メールの件名にあたる。

application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: 'noreply@example.com'
  layout 'mailer'
end
user_mailer.rb
class UserMailer < ApplicationMailer

  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end

  def password_reset
    @greeting = "Hi"

    mail to: "to@example.org"
  end
end

テンプレートビューは、通常のビューと同様ERBで自由にカスタマイズできる。

ここでは挨拶文にユーザー名を含め、カスタムの有効化リンクを追加する。
この後、Railsサーバーでユーザーをメールアドレスで検索して有効化トークンを認証できるようにしたいので、リンクにはメールアドレスとトークンを両方含めておく必要がある。

AccountActivationsリソースで有効化をモデル化したので、トークン自体は名前付きルートの引数で使われる。

edit_account_activation_url(@user.activation_token, ...)

例えば

edit_user_url(user)

上のメソッドは、絶対パスのuser_urlでurlを生成し、引数のユーザーの編集ページにアクセスする
つまり

http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit

上記のURLのq5lt38hQDc_959PVoo6b7Aという部分はnew_tokenメソッドで生成されたもの。

URLで使えるようにBase64でエンコードされている。
これは丁度/users/1/editの1のようなユーザーIDと同じ役割を果たす。

このトークンは、特にAccountActivationsコントローラのeditアクションではparamsハッシュでparams[:id]として参照できる。

クエリパラメータを使って、このURLにメールアドレスをうまく組み込んでみる。

account_activations/q5lt38hQDc_959Voo6b7A/edit?email=foo%40example.com

この時、メールアドレスの@は%40と言う文字にエスケープしている。
(@は通常URLでは扱えない)

Railsでクエリパラメータを設定するには、名前付きルートに対して、次のようなハッシュを追加しする。

edit_account_activation_url(@user.activation_token, email: @user.email)

このようにして名前付きルートでクエリパラメータを定義すると、Railsが特殊な文字を自動的にエスケープしてくれる。

コントローラでparams[:email]からメールアドレスを取り出す時には、自動的にエスケープを解除してくれる。

ここまでできれば、user_mailerで定義した@userインスタンス変数、editへの名前付きルート、ERBを組み合わせて、必要なリンクを作成できる。

アカウント有効化のHTMLテンプレートでは、正しいリンクを組み立てるためにlink_toメソッドを使われている。

account_activation.text.erb
こんにちは <%= @user.name %>,

YUUKIのポートフォリオへようこそ!下記リンクをクリックしたらあなたのアカウントは有効化されます♫:

<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
account_activation.html.erb
<h1>YUUKIのポートフォリオ</h1>

<p>こんにちは <%= @user.name %>,</p>

<p>
 YUUKIを有効化する:
</p>

<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
                                                    email: @user.email ) %>

演習

1:コンソールを開き、CGIモジュールのescapeメソッドでメールアドレスの文字列をエスケープできることを確認してみる。
このメソッドでDon't panic!をエスケープすると、どんな結果になるか?

> CGI.escape('foo@example.com')
=> "foo%40example.com"
>> 

11.2.2 送信メールのプレビュー

テンプレートで定義した実際の表示を確認するため、メールプレビューという裏技を使ってみる。

Railsでは、特殊なURLにアクセスするとメールのメッセージをその場でプレビューすることができる。

メールを実際に送信しなくてもいいので大変便利。
これを利用するには、アプリケーションのdevelopment環境(開発環境)の設定に手を加える必要がある。

development.rb
Rails.application.configure do
  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = 'us-east-2.console.aws.amazon.com'
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }

このように、host名のとこに自分の開発環境のホスト名を記入する。

developmentサーバーを再起動してdevelopmentの設定を読み込んだら、次はUserメイラーのプレビューファイルを更新する。

user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/account_activation
  def account_activation
    UserMailer.account_activation
  end

  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset
  def password_reset
    UserMailer.password_reset
  end

end

user_mailer.rbファイルで指定したaccount_activation(user)の引数には有効なUserオブジェクトを渡す必要があるため、このままではプレビューファイルは動かない。
これを回避するため、user変数が開発用データベースの最初のユーザーになるよう定義して、それをUserMailer.account_activation_tokenの引数として渡す。

user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/account_activation
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset
  def password_reset
    UserMailer.password_reset
  end

end

ここで、user.activation_tokenの値にも代入している点に注目。
アカウント有効化ビューのテンプレートでは、アカウント有効化のトークンが必要なので、代入は省略できない。

なお、activation_tokenは仮の属性でしかないので、DBのユーザーはこの値を実際には持っていない(ダイジェストしかない)

スクリーンショット 2019-01-21 13.29.20.png

スクリーンショット 2019-01-21 14.06.23.png

演習

1:Railsのプレビュー機能を使って、ブラウザから先ほどのメールを表示してみる。
Dateの欄にはどんな内容が表示されているか?

アクセスした時刻が表示されている。

11.2.3 送信メールのテスト

 最後に、このメールプレビューのテストも作成して、プレビューをダブルチェックできるようにする。
mailer作成時にテストも自動で生成されているので、これを利用すればテストの作成は簡単。

user_mailer_test.rb
require 'test_helper'

class UserMailerTest < ActionMailer::TestCase
  test "account_activation" do
    mail = UserMailer.account_activation
    assert_equal "Account activation", mail.subject
    assert_equal ["to@example.org"], mail.to
    assert_equal ["from@example.com"], mail.from
    assert_match "Hi", mail.body.encoded
  end

  test "password_reset" do
    mail = UserMailer.password_reset
    assert_equal "Password reset", mail.subject
    assert_equal ["to@example.org"], mail.to
    assert_equal ["from@example.com"], mail.from
    assert_match "Hi", mail.body.encoded
  end

end

上記のテストでは、assert_matchというメソッドが使われており、これを使えば正規表現で文字列をテストできる

assert_match 'foo',   'foobar'  #true
assert_match 'baz',   'foobar'  #false
assert_match '/¥w+/', 'foobar'  #true
assert_match '/¥w+/', '$#!*+@'  #false

assert_matchメソッドを使って、名前・有効化トークン・エスケープ済みメールアドレスがメール本文に含まれているかどうかをテストする。

また、

CGI.escape(user.email)

こうすることで、引数に取ったemailをエスケープ処理することができる。

user_mailer_test.rb
require 'test_helper'

class UserMailerTest < ActionMailer::TestCase

  test "account_activation" do
    user = users(:michael)
    user.activation_token = User.new_token
    mail = UserMailer.account_activation(user)
    assert_equal "Account activation", mail.subject
    assert_equal [user.email], mail.to
    assert_equal ["noreply@example.com"], mail.from
    assert_match user.name,               mail.body.encoded
    assert_match user.activation_token,   mail.body.encoded
    assert_match CGI.escape(user.email),  mail.body.encoded
  end
end

なお、このテストではまだ失敗する。

上記テストコードでは、fixtureユーザーに有効化トークンを追加している点に注目。
(user.activation_token = のところ)
追加しない場合は、空白になる。

なお、生成されたパスワード設定のテストも削除しているが、のちに戻す。

このテストをパスさせるには、テストファイル内のドメイン名を正しく設定する必要がある。

test.rb
  config.action_mailer.delivery_method = :test
  config.action_mailer.default_url_options = { host: 'example.com' }

これでテストはパスする。

$ rails t
1 tests, 9 assertions, 0 failures, 0 errors, 0 skips

*ここで注意だが、account_activation.htmlにて、pタグで長く名付けるとassert_matchでテストが失敗する。

account_activation.html.rb
<p>
 YUUKIのポートフォリオへようこそ!下記リンクをクリックしたらあなたのアカウントは有効化されます:
</p>

失敗

account_activation.html.rb
<p>
 有効化:
</p>

成功

2:CGI.escapeの部分を削除するとテストが失敗することを確認

確認済み

11.2.4 ユーザーのcreateアクションを更新

あとはユーザー登録を行うcreateアクションに数行追加するだけで、メイラーをアプリケーションで実際に使うことができる。

users_controller.rb
  def create
    @user = User.new(user_params)                                               # newビューにて送ったformの中身(nameやemailの値)をuser_paramsで受け取り、ユーザーオブジェクトを生成、@userに代入
    if @user.save
      UserMailer.account_activation(@user).deliver_now                          # アカウント有効化メールの送信
      flash[:info] = "メールを確認してアカウントを有効化してね"                 # アカウント有効化メッセージの表示
      redirect_to root_url                                                      # ホームへ飛ばす
    else
      render 'new'
    end
  end

ここで、UserMailerを使って入力されたメールアドレス宛にアカウント有効化のメッセージを送っている点と、
ユーザー登録時にリダイレクト先をroot_urlへ飛ばしてる点に、
ログインしないように変更した点に注目。

登録時のリダイレクトの挙動が変更されたため、テストは失敗する。

そこで、該当箇所のテストはとりあえずコメントアウトしておく。

users_signup_test.rb
  test "valid signup information" do                                            # 新規登録が成功(フォーム送信)したかのテスト
    get signup_path                                                             # signup_path(/signup)ユーザー登録ページにアクセス
    assert_difference 'User.count', 1 do                                        # User.countでユーザー数をカウント、1とし、ユーザー数が変わったらtrue、変わってなければfalse
      post users_path, params: { user: { name:                 "Example User",  # signup_path(/signup)からusers_path(/users)へparamsハッシュのuserハッシュの値を送れるか検証
                                        email:                 "user@example.com",
                                        password:              "password",
                                        password_confirmation: "password" } }
    end
    follow_redirect!                                                            # 指定されたリダイレクト先(users/show)へ飛べるか検証
    #assert_template 'users/show'                                                # users/showが描画されているか確認
    assert_not   flash.blank?                                                   # flashが空ならfalse,空じゃなければtrue
    #assert is_logged_in?                                                        # 新規登録時にセッションが空じゃなければtrue
  end

この状態で実際に新規ユーザーとして登録してみる。

スクリーンショット 2019-01-22 15.07.42.png

サーバーログ(rails sの画面)を確認

UserMailer#account_activation: processed outbound mail in 177.1ms
Sent mail to itotasuku2@gmail.com (6.6ms)
Date: Tue, 22 Jan 2019 06:07:32 +0000
From: noreply@example.com
To: ●●@gmail.com
Message-ID: <5c46b32415fe1_3ed625687987001@ip-172-31-25-8.mail>
Subject: Account activation
Mime-Version: 1.0
Content-Type: multipart/alternative;
 boundary="--==_mimepart_5c46b32414b75_3ed625687986994e";
 charset=UTF-8
Content-Transfer-Encoding: 7bit


----==_mimepart_5c46b32414b75_3ed625687986994e

おぉ、アカウント有効化メールアドレスが表示されている。

ただし、これは実際にメールが生成されるわけではないので注意。
のちに実際のメールを送信する方法を詳解する。

演習

1:新規ユーザー登録でリダイレクト先が適切なURLに変わったことを確認。その後、Railsサーバーのログから送信メールの有効化トークンの値を確認。

Redirected to https://eac437457e484fe491559aaa135f7f93.vfs.cloud9.us-east-2.amazonaws.com/
Completed 302 Found in 470ms (ActiveRecord: 11.3ms)

リダイレクト先がroo_urlの指示通り、ホームページへリダイレクトできている。

"authenticity_token"=>"GqyypcjNdXgYvimMg9lCL6tXBhcYqsMIBqIA6R9EkgU+34HzSxxH4TmLun6dMaeiM9RwFfaImLGm5KspzMKxNg=="

有効化トークン(authenticity_token)がハッシュ化されている

2:コンソールを開き、DB常にユーザーが作成されたことを確認。
また、ユーザーはDB上にはいるが、有効化のステータスがfalseのままになっていることを確認。

>> user = User.find(101)
>> user.activated?
=> false

11.3 アカウントを有効化する

メールが生成できたら、今度はAccountActivationsコントローラのeditアクションを書いていく。

また、アクションへのテストを書き、しっかりとテストできていることが確認できたら、AccountActivationsコントローラからUserモデルにコードを移していく。

11.3.1 authenticated?メソッドの抽象化

有効化トークンとメールはそれぞれ

params[:id]
params[:email]

で参照できる。

なので、パスワードのモデルと記憶トークンで学んだことを元に、次のようなコードでユーザーを検索して認証することにする。

user = User.find_by(email: params[:email])
if user && user.authenticated?(:activation, params[:id])

上のコードで使っているauthenticated?メソッドは、アカウント有効化のダイジェストと、渡されたトークンが一致するかどうかをチェックしている。

ただし、このメソッドは記憶トークン用なので、今は正常に動作しない。

# トークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
  return false if remember_digest.nil?
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

remember_digestはUserモデルの属性なので、モデル内では次のように置き換えることができる。

self.remember_digest

今回は、上記のコードのrememberの部分をどうにかして変数として扱いたい。
つまり、次のコードの例のように、状況に応じて呼び出すメソッドを切り替えたい。

self.FOOBAR.digest

これから実装するauthenticated?メソッドでは、受け取ったパラメータに応じて呼び出すメソッドを切り替える手法を使う。

この手法をメタプログラミングと呼ぶ。

今回はsendメソッドを用いてペアプログラミングを行う。

このメソッドは、渡されたオブジェクトにメッセージを送ることによって、呼び出すメソッドを動的に決めることができる。

Railsコンソールでやってみる。

まずは、Rubyのオブジェクトに対してsendメソッドを実行し、配列の長さを得る。

$ rails c
                                                                Running via Spring preloader in process 19959
Loading development environment (Rails 5.1.6)
>> a = [1,2,3]
=> [1, 2, 3]
>> a.length
=> 3
>> a.send(:length)
=> 3
>> a.send("length")
=> 3

この時、sendを通してシンボルの:lengthや文字列の"length"は、いずれもlengthメソッドと同じ結果になった。つまり、どちらもオブジェクトにlengthメソッドを渡しているため、等価である。

もう1つの例は、DBの最初のユーザーが持つactivation_digest属性にアクセスする。

$ rails c
Running via Spring preloader in process 7511
Loading development environment (Rails 5.1.6)
>> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2019-01-20 02:17:40", updated_at: "2019-01-20 02:17:40", password_digest: "$2a$10$ambbgHUgH.09zBb8AbfXqOPn2//.8cblJ2qQKEsPXyA...", remember_digest: nil, admin: true, activation_digest: "$2a$10$Hv0qwXYkDj7k6huuD.Kfguk4/2eyMimSbllKnJ0rIJv...", activated: true, activated_at: "2019-01-20 02:17:40">
>> user.activation_digest
=> "$2a$10$Hv0qwXYkDj7k6huuD.Kfguk4/2eyMimSbllKnJ0rIJvJKstLZfLba"
>> user.send(:activation_digest)
=> "$2a$10$Hv0qwXYkDj7k6huuD.Kfguk4/2eyMimSbllKnJ0rIJvJKstLZfLba"
>> user.send("activation_digest")
=> "$2a$10$Hv0qwXYkDj7k6huuD.Kfguk4/2eyMimSbllKnJ0rIJvJKstLZfLba"
>> attribute = :activation
=> :activation
>> user.send("#{attribute}_digest")
=> "$2a$10$Hv0qwXYkDj7k6huuD.Kfguk4/2eyMimSbllKnJ0rIJvJKstLZfLba"
>> 

最後の例では、シンボル:activationと等しいattribute変数を定義し、文字列の式展開(interpolation)を使って引数を正しく組み立ててから、sendに渡している。

文字列activationでも同じことができるが、Rubyではシンボルを使うのが一般的。

"#{attribute}_digest"

シンボルと文字列どちらを使った場合でも、上のコードでは次のように文字列に変換される。

"activation_digest"

sendメソッドの動作原理を理解した所で、この仕組みを利用してauthenticated?メソッドを書き換えてみる。

def authenticated?(remember_token)
  digest = self.send("remember_digest")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(remember_token)
end

上のコードの各引数を一般化し、文字列の式展開も利用すると、次のようなコードになる。

def authenticated?(attritubte, token)
 digest = self.send("#{attritubte}_digest")
 return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token)
end

他の認証でも使えるように、上では2番目の引数tokenの名前を変更して一般化している点に注意。

また、このコードはモデル内にあるのでselfは省略することもできる。
最終的にRubyらしく書かれたコードは以下

def authenticated?(attribute, token)
  digest = send("#{attribute}_digest")
  return false if digest.nil?
  BCrypt::Password.new(digest).is_password?(token)

ここまでできれば、次のように呼び出すことでauthenticated?の従来の振る舞いを再現できる。

user.authenticated?(:remember, remember_token)

抽象化したauthenticated?メソッドをUserモデルに書いてみる。

user.rb
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end

この時点ではテストは失敗する。

テストが失敗する理由はcurrent_userメソッドとnilダイジェストのテストの両方で、
authenticated?が古いママとなっており、さらに引数も2つではなくまだ1つのままだから。

これを解消するため、両者を更新して、新しい一般的なメソッドを使うようにする。

sessions_helper.rb
  def current_user
    if (user_id = session[:user_id])                                            # 一時的なセッションユーザーがいる場合処理を行い、user_idに代入
      @current_user ||= User.find_by(id: user_id)                               # 現在のユーザーがいればそのまま、いなければsessionユーザーidと同じidを持つユーザーをDBから探して@current_user(現在のログインユーザー)に代入
    elsif (user_id = cookies.signed[:user_id])                                  # user_idを暗号化した永続的なユーザーがいる(cookiesがある)場合処理を行い、user_idに代入
      user = User.find_by(id: user_id)                                          # 暗号化したユーザーidと同じユーザーidをもつユーザーをDBから探し、userに代入
        if user && user.authenticated?(:remember, cookies[:remember_token])     # DBのユーザーがいるかつ、受け取った記憶トークンを暗号化した記憶ダイジェストと、remember値が入ってるユーザーがいる場合処理を行う
        log_in user                                                             # 一致したユーザーでログインする
        @current_user = user                                                    # 現在のユーザーに一致したユーザーを設定
        end
    end
  end
user_test.rb
  test "authenticated? should return false for a user with nil digest" do       # authenticatedメソッドで記憶ダイジェストを暗号化できるか検証
    assert_not @user.authenticated?(:remember, '')                              # @userのユーザーの記憶ダイジェストと、引数で受け取った値が同一ならfalse、異なるならtrueを返す
  end

上記の変更を加えたらテストは成功する。

$ rails t
11 tests, 15 assertions, 0 failures, 0 errors, 0 skips

このようなリファクタリングを施すとエラーが発生しやすくなるので、しっかりしたテストスイートが不可欠。

演習

1:コンソール内でユーザーを作成してみる。
新しいユーザーの記憶トークンと有効化トークンはどのような値になっているか?
また、各トークンに対応するダイジェストの値はどうなっているか?

>> user = User.create(name: "tesuto4", email: "tesuto@test.co.jp", password: "tesuto4", password_confirmation: "tesuto4")                      
   (0.1ms)  SAVEPOINT active_record_1
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "tesuto@test.co.jp"], ["LIMIT", 1]]
  SQL (2.8ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?)  [["name", "tesuto4"], ["email", "tesuto@test.co.jp"], ["created_at", "2019-01-23 04:17:30.872700"], ["updated_at", "2019-01-23 04:17:30.872700"], ["password_digest", "$2a$10$Edcro0emsnf9BuYd0WXzMuHYGxzKI1AFiBh.kPtLz6qc81Okuf0by"], ["activation_digest", "$2a$10$d9vrDK5aEZCfFgaQIE/CUuLjRjrjAWHLomBJ/7u.4PdumyRNGqJQ2"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 102, name: "tesuto4", email: "tesuto@test.co.jp", created_at: "2019-01-23 04:17:30", updated_at: "2019-01-23 04:17:30", password_digest: "$2a$10$Edcro0emsnf9BuYd0WXzMuHYGxzKI1AFiBh.kPtLz6q...", remember_digest: nil, admin: false, activation_digest: "$2a$10$d9vrDK5aEZCfFgaQIE/CUuLjRjrjAWHLomBJ/7u.4Pd...", activated: false, activated_at: nil>
>> user.remember_token
=> nil
>> user.activation_token
=> "l39qT2c2s5LXb1r5C3OmjQ"
>> user.remember_digest
=> nil
>> user.activation_digest
=> "$2a$10$d9vrDK5aEZCfFgaQIE/CUuLjRjrjAWHLomBJ/7u.4PdumyRNGqJQ2"

アカウント有効化用のトークン・ダイジェストは生成されたが、ユーザーが有効化していないので、記憶トークン/記憶ダイジェスト値はnil。

2:authenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認。

>> user.remember_token = User.new_token
=> "JfMcsK1wbYkz8L_uG2kzhA"
>> user.update_attribute(:remember_digest, User.digest(user.remember_token))
   (0.2ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2019-01-23 04:37:39.302291"], ["remember_digest", "$2a$10$WiFAB0sIsY9pXkoioQ9nDeb3Z0VbdJbqZTe3kUWN7fAavjo5YOY.a"], ["id", 102]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

まず記憶トークンと記憶ダイジェストを生成。

>> user.authenticated?(:remember,user.remember_token)
=> true

引数に渡したダイジェスト値と、トークン値が一致したらtrue

trueが返ってきたので、認証が成功した。
(ログインが成功した)

11.3.2 editアクションで有効化

authenticated?メソッドを完成させたので、editアクションを書いてみる。

このアクションでは、paramsハッシュで渡されたメールアドレスに対応するユーザーを認証する。

ユーザーが有効であることを確認する中核派、次の部分になる。

if user && !user.activated? && user.authenticated?(:activation, params[:id])

!user.activated?という記述に注目。

このコードは既に有効になっているユーザーを誤って再度有効化しないために必要。
正当であろうとなかろうと、有効化が行われるとユーザーはログイン状態になる。

もしこのコードがなければ、攻撃者がユーザーの有効化リンクを後から盗みだしてクリックするだけで、本当のユーザーとしてログインできてしまう。

そうした攻撃を防ぐためにこのコードは非常に重要。

上記の論理値に基づいてユーザーを認証するには、ユーザーを認証してからactivated_atタイムスタンプを更新する必要がある。

user.update_attribute(:activated,    true)
user.update_attribute(:activated_at, Time.zone_now)

上記のコードをeditアクションで使う。
これで日付更新ができる。

account_activations_controller.rb
class AccountActivationsController < ApplicationController
  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.update_attribute(:activated,     true)
      user.update_attribute(:activated_at,  Time.zone.now)
      log_in user
      flash[:success] = "アカウントを有効にしたンゴよ〜"
      redirect_to user
    else
      flash[:danger] = "きみはまだまだだね^^"
      redirect_to root_url
    end
  end
end

有効化のアクションを書いたら、実際にアカウントを有効化させてみる。

ブラウザでユーザーを新規登録後、rails sに載っているURLを貼り付けて有効化ページにアクセスする。

スクリーンショット 2019-01-24 9.01.05.png

この状態ではユーザーのログイン方法を変更していないのでこれでは何の意味もない。
ということで、ユーザーの有効化を行う為に、
ユーザーが有効である場合のみログインできるようにログイン方法を変更する必要がある。

 これを行うには、user.activated?がtrueの場合にのみログインを許可し、
そうでない場合はルートURLにリダイレクトしてwarningで警告を表示する。

sessions_controller.rb
  # ユーザーを新規作成する

  def create
    @user = User.find_by(email: params[:session][:email].downcase)              # paramsハッシュで受け取ったemail値を小文字化し、email属性に渡してUserモデルから同じemailの値のUserを探して、user変数に代入
    if @user && @user.authenticate(params[:session][:password])                 # user変数がデータベースに存在し、なおかつparamsハッシュで受け取ったpassword値と、userのemail値が同じ(パスワードとメールアドレスが同じ値であれば)true
      if @user.activated?                                                       # userが有効の処理
        log_in @user                                                            # sessions_helperのlog_inメソッドを実行し、sessionメソッドのuser_id(ブラウザに一時cookiesとして保存)にidを送る
        params[:session][:remember_me] == '1' ? remember(@user) : forget(@user) # ログイン時、sessionのremember_me属性が1(チェックボックスがオン)ならセッションを永続的に、それ以外なら永続的セッションを破棄する
        redirect_back_or @user                                                  # userの前のページもしくはdefaultにリダイレクト
      else                                                                      # userが有効でない処理
        message   = "アカウントは有効ではありません"
        message  += "メールで送られたURLから有効化してね"
        flash[:warning] = message
        redirect_to root_url
      end
    else
      flash.now[:danger] = 'Invalid email/password combination'                 # flashメッセージを表示し、新しいリクエストが発生した時に消す
      render 'new'                                                              # newビューの出力
    end
  end

スクリーンショット 2019-01-24 19.07.29.png

演習 

1:コンソールからメールのURLを調べて、有効化トークンはどれか確認する。

<a href="https://eac437457e484fe491559aaa135f7f93.vfs.cloud9.us-east-2.amazonaws.com/account_activations/cDACvOyopUBbsOnFsIr2sg/edit?email=tyaou%40example.com">Activate</a>

これのcDACv〜の部分。

2:URLから有効化リンクに飛んでユーザーの認証に成功し、有効化できることを確認する。
また、有効化ステータスがtrueになるかも確認。

$ rails c
Running via Spring preloader in process 29877
Loading development environment (Rails 5.1.6)
>> user = User.find(110)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 110], ["LIMIT", 1]]
=> #<User id: 110, name: "tyaou", email: "tyaou@example.com", created_at: "2019-01-24 10:06:17", updated_at: "2019-01-24 10:06:27", password_digest: "$2a$10$7/vxN5esSjb1gxWrtZb9K.vkYnnbL/GsI5JwMx9/Kqh...", remember_digest: nil, admin: false, activation_digest: "$2a$10$Ty2/gM.jACJf3ll87jO9gO/hrVRkdgRHZdx57lakxbm...", activated: true, activated_at: "2019-01-24 10:06:27">
>> user.activated
=> true

OK。

有効化に半日格闘した

何回やってもeditアクションの
if user && !user.activated? && user.authenticated?(:activation, params[:id])

autenticated?の部分がtrueにならずに色々試行錯誤したのだが、
結果、account_activation.html.erbを全部日本語から英語にしたら直った。

<h1>YUUKI</h1>

<p>hello <%= @user.name %>,</p>

<p>
 activate:
</p>

<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
                                                    email: @user.email ) %>

こうしたってこと。

日本語だとメイラーのHTMLが文字化けしてURLが正しく発行されないので注意。
まじで疲れたよ・・・

11.3.3 有効化のテストとリファクタリング

アカウント有効化の統合テストを追加する。

正しい情報でユーザー登録を行った場合のテストは既にあるので、7章で書いたテストに若干手を加える。

users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  # test "the truth" do
  #   assert true
  # end

  def setup
    ActionMailer::Base.deliveries.clear                                         # Mailerファイルを初期化しユーザーをセットアップ
  end

  test "invalid signup information" do                                          # 新規登録が失敗(フォーム送信が)した時用のテスト
    get signup_path                                                             # ユーザー登録ページにアクセス
    assert_no_difference 'User.count' do                                        # User.countでユーザー数が変わっていなければ(ユーザー生成失敗)true,変わっていればfalse
    post signup_path, params: { user: {    name: "",                             # signup_pathからusers_pathに対してpostリクエスト送信(/usersへ)、paramsでuserハッシュとその下のハッシュで値を受け取れるか確認
                                          email: "user@invalid",
                                          password:              "foo",
                                          password_confirmation: "bar" } }
    end
  assert_template 'users/new'                                                   # newアクションが描画(つまり@user.save失敗)されていればtrue、なければfalse
  assert_select   'div#error_explanation'                                       # divタグの中のid error_explanationが描画されていれば成功
  assert_select   'div.field_with_errors'                                       # divタグの中のclass field_with_errorsが描画されていれば成功
  assert_select   'form[action="/signup"]'                                      # formタグの中に`/signup`があれば成功

  end

  test "valid signup information with account activation" do                    # 新規登録が成功(フォーム送信)したかのテスト
    get signup_path                                                             # signup_path(/signup)ユーザー登録ページにアクセス
    assert_difference 'User.count', 1 do                                        # User.countでユーザー数をカウント、1とし、ユーザー数が変わったらtrue、変わってなければfalse
      post users_path, params: { user: { name:                 "Example User",  # signup_path(/signup)からusers_path(/users)へparamsハッシュのuserハッシュの値を送れるか検証
                                        email:                 "user@example.com",
                                        password:              "password",
                                        password_confirmation: "password" } }
    end
    assert_equal 1, ActionMailer::Base.deliveries.size                          # Actionメイラーが1かどうか検証
    user = assigns(:user)                                                       # usersコントローラの@userにアクセスし、userに代入
    assert_not user.activated?                                                  # userが有効化されていればfalse、されていなければtrue
    # 有効化していない状態でログインしてみる 
    log_in_as(user)                                                             # 有効化されていないuserでログイン
    assert_not is_logged_in?                                                    # 有効化されていなければtrue
    # 有効化トークンが不正な場合
    get edit_account_activation_path("invalid token", email: user.email)
    assert_not is_logged_in?
    # トークンは正しいがメールアドレスが無効な場合
    get edit_account_activation_path(user.activation_token, email: 'wrong')
    assert_not is_logged_in?
    # 有効化トークンが正しい場合
    get edit_account_activation_path(user.activation_token, email: user.email)
    assert user.reload.activated?
    follow_redirect!                                                            # 指定されたリダイレクト先(users/show)へ飛べるか検証
    assert_template 'users/show'                                                # users/showが描画されているか確認
    assert is_logged_in?                                                        # 新規登録時にセッションが空じゃなければtrue
  end

end

このテストで重要な行はこれ

assert_equal 1, ActionMailer::Base.deliveries.size

このコードは、配信されたメッセージがきっかり1つであるかどうかを確認している。

配列deliveriesは変数なので、setupメソッドでこれを初期化しておかないとdeliveries.clear、並行して行われる他のテストでメールが配信されたときにエラーが発生してしまう。

また、assignsメソッドを使うと対応するアクション内のインスタンス変数にアクセスできるようになる。

例えば、Usersコントローラのcreateアクションでは@userというインスタンス変数が定義されているが、テストでassings(:user)と書くとこのインスタンス変数にアクセスできるようになる。

これでテストはパスする。

$ rails t
2 tests, 14 assertions, 0 failures, 0 errors, 0 skips

テストができたので、ユーザー操作の一部をコントローラからモデルに移動するというリファクタリング行う準備ができた。

Userモデルでは、activateメソッドを作成してユーザーの有効化属性を更新し、send_activation_emailメソッドを作成して有効化メールを送信する。

user.rb
  # アカウントを有効にする
  def activate
    update_attribute(:activated,        true)
    update_attribute(:activated_at,     Time.zone.now)
  end

  # 有効化用のメールを送信する
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

private

  # メールアドレスを全て小文字にする

  def downcase_email
    self.email = email.downcase                                                 # emailを小文字化してUserオブジェクトのemail属性に代入
  end

  # 有効化トークンとダイジェストを作成および代入する

  def create_activation_digest
    self.activation_token   =   User.new_token                                  # ハッシュ化した記憶トークンを有効化トークン属性に代入
    self.activation_digest  =   User.digest(activation_token)                   # 有効化トークンをBcryptで暗号化し、有効化ダイジェスト属性に代入
  end
users_controller.rb
  def create
    @user = User.new(user_params)                                               # newビューにて送ったformの中身(nameやemailの値)をuser_paramsで受け取り、ユーザーオブジェクトを生成、@userに代入
    if @user.save
      @user.send_activation_email                                               # アカウント有効化メールの送信
      flash[:info] = "メールを確認してアカウントを有効化してね"                 # アカウント有効化メッセージの表示
      redirect_to root_url                                                      # ホームへ飛ばす
    else
      render 'new'
    end
  end
account_activations_controller.rb
class AccountActivationsController < ApplicationController

  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.activate
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end
end

user.rbではuser.という記法を使っていない点に注目。
Userモデルにはそのような変数はないので、これがあるとエラーになる。

-user.update_attribute(:activated,    true)
-user.update_attribute(:activated_at, Time.zone.now)
+update_attribute(:activated,    true)
+update_attribute(:activated_at, Time.zone.now)

userをselfに切り替える方法もあるが、selfは必ずしも必須ではない。

これで、リファクタリングを施したテストがパスすればOK。

2 tests, 14 assertions, 0 failures, 0 errors, 0 skips

演習

1:activateメソッドはupdate_attributeを二回呼び出しているが、
これは各行で1回ずつDBへ問い合わせしていることになる。

update_attributeの呼び出しを1回のupdate_columnsというメソッドでまとめてみる。

また、変更後テストがパスするかも確認。

user.rb
  # アカウントを有効にする
  def activate
    update_columns(activated: true, activated_at: Time.zone.now)
  end
11 tests, 15 assertions, 0 failures, 0 errors, 0 skips

2:現在は/usersのユーザーindexページを開くと全てのユーザーが表示され、/users/:idのようにidと指定すると個別のユーザーを表示できる。

しかし、非有効ユーザーは表示する意味がないので、その動作をusers_controller.rbで変更する。

users_controller.rb
  def index
    @users = User.where(activated: true).paginate(page: params[:page])          # Userを取り出して分割した値を@usersに代入
  end

  def show
    @user = User.find(params[:id])                                              # paramsで:idパラメータを受け取る(/users/1にアクセスしたら1を受け取る)
    redirect_to root_url and return unless @user.activated?                     # activatedがfalseならルートURLヘリダイレクト
  end

3:ここまでの演習問題で変更したコードをテストするために、/usersと/users/:idの両方に対する統合テストを作成する。

fixtureに非有効化ユーザーを作成

users.yml
non_activated:
 name: Non Activated
 email: non_activated@example.gov
 password_digest: <%= User.digest('password') %>
 activated: false
 activated_at: <%= Time.zone.now %>

setupでテストユーザーを読み込む

users_controller_test.rb
  test "should not allow the not activated attribute" do
    log_in_as (@non_activated_user)                                             # 非有効化ユーザーでログイン
    assert_not @non_activated_user.activated?                                   # 有効化でないことを検証
    get users_path                                                              # /usersを取得
    assert_select "a[href=?]", user_path(@non_activated_user), count: 0         # 非有効化ユーザーが表示されていないことを確認
    get user_path(@non_activated_user)                                          # 非有効化ユーザーidのページを取得
    assert_redirected_to root_url                                               # ルートurlにリダイレクトされればtrue
  end

11.4 本番環境でのメール送信

ここまでの実装で、development環境に置けるアカウント有効化の流れは完成した。

次はサンプルアプリケーションの設定を変更し、production(実行)環境で実際にメールを送信してみる。

具体的には、

①無料のサービス(SendGrid)を利用してメール送信の設定をする
②アプリケーションの設定
③デプロイ

という順に行う。

SendGridの使い方

本番環境からメールを送信するために、SendGridというHerokuアドオンを利用してアカウントを検証する。

チュートリアルでは、starter tierというサービスを使う。
(1日のメール数が最大400通という制限があるが、無料で利用できる)

SendGridをHerokuで利用するためには、クレジットカードの登録が必要。
https://heroku.com/verify
にアクセスし、クレジットカードを登録する。

アドオンをアプリケーションに追加するには

$ heroku addons:create sendgrid:starter
Creating sendgrid:starter on ⬢ yuuki-heroku-sample... free
Created sendgrid-rectangular-51178 as SENDGRID_PASSWORD, SENDGRID_USERNAME
Use heroku addons:docs sendgrid to view documentation

次に、SendGridアドオンの設定を行う。

production環境のSMTPに情報を記入する
本番Webサイトのアドレスをhost変数に定義する必要もある。

production.rb
  # Ignore bad email addresses and do not raise email delivery errors.
  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
  config.action_mailer.raise_delivery_errors = false
  config.action_mailer.delivery_method = :smtp
  host = 'https://yuuki-heroku-sample.herokuapp.com'
  config.action.mailer.default_url_options = { host: host }
  ActionMailer::Base.smtp_settings = {
    :address        => 'smtp.sendgrid.net',
    :port           => '587',
    :authentication => :plain,
    :user_name      => ENV['SENDGRID_USERNAME'],
    :password       => ENV['SENDGRID_PASSWORD'],
    :domain         => 'heroku.com',
    :enable_starttle_auto => true
  }

ここで重要なのが、
user_namepasswordのハッシュに実際の値を記入しないこと

ソースコードに直接機密情報を書き込むのは危険。
そのような情報は環境変数に記述し、そこからアプリケーションに読み込む必要がある。

今回の場合、そうした変数はSendGridアドオンが自動的に設定してくれるが、のちに環境変数を自分で設定しなければならない。

のちに扱うHerokuの環境変数を表示したい場合は、次のコマンドを実行する。

$ heroku config:get SENDGRID_USERNAME
〇〇@heroku.com
$ heroku config:get SENDGRID_PASSWORD
〇〇

この時点で、トピックブランチをmasterにマージしておく

$ git add -A
$ git commit -m "Add account activation"
$ git checkout master
$ git merge account-activation

続いて、リモートリポジトリにプッシュし、herokuにデプロイ。

$ rails t
$ git push
$ git push heroku
$ heroku run rails db:migrate

ここで、デプロイ後のページで有効かメールを送信してみる。

しかし、なかなかメールが届かない・・・。

*SendGridを確認したところ、スパムアカウント扱いされてアカウントが停止されてしまいました。

スクリーンショット 2019-01-26 22.11.15.png

処理済みとなっていますが、本来なら送信済みになる筈です。

仕方なく、heroku logsからアカウント有効化URLをクリック。

スクリーンショット 2019-01-26 22.43.03.png

URLをクリックする。

スクリーンショット 2019-01-26 22.43.40.png

有効化完了。

SendGridはすぐにスパム判定をしてしまうようで、自分と同じTutorialを読んでいる方もこの部分で苦戦している様子。

演習

1:本番環境でメールを送る。

確認済み

2:実査にメールをクリックして有効化したあと、heorku logsから有効化に関するログがどうなっているかを調べる

2019-01-26T13:43:36.501607+00:00 heroku[router]: at=info method=GET path="/account_activations/BLndwjAJEDxPtb90w3aNpQ/edit?email,yuukitetsuyanet%40gmail.com" host=yuuki-heroku-sample.herokuapp.com request_id=1129f9d8-c64a-476e-81b2-2d3d9a1fceff fwd="122.50.45.13" dyno=web.1 connect=1ms service=113ms status=302 bytes=1031 protocol=https
2019-01-26T13:43:36.712279+00:00 heroku[router]: at=info method=GET path="/users/2" host=yuuki-heroku-sample.herokuapp.com request_id=4f7857c1-a5ba-485f-abe6-feccfa902f9e fwd="122.50.45.13" dyno=web.1 connect=1ms service=19ms status=200 bytes=4444 protocol=https

一応、heroku run rails cでユーザーYUUKIのactivationがtrueか確認してみる。

$ heroku run rails c
$ user = User.find(2)
$ user.activated
=> true

OK。

単語集

  • バインド

関連付けるというIT用語。

  • before_create

オブジェクト生成時のみ適用させるコールバック。

  • Action Mailer

ActionMailerを使用することで、アプリケーションのメイラークラスやビューで、メールを送信することができる。
ActionMailerはActionMailer::Baseを継承し、app/mailerに配置され、app/views にあるビューと関連付けられる。

具体的な使い方は、まずメールを送信する為のメイラーとビューを生成し、メイラー内のアクションでメール送信の動きを付け、メイラービューでメールとして送られる中身のテンプレートを作成する。

  • クエリパラメータ

URLの末尾で指定される?の部分。?の部分の後にパラメータを埋め込む。
実際のパラメータ値を「クエリ文字列」と呼び、その種類にはアクセス解析や広告からの流入を調べるパシッブパラエータと、実際のページのコンテンツの内容を変更させるアクティブパラメータがある

  • assert_match

第二引数が第一引数の正規表現にマッチする場合はtrue

使い方

assert_match (正規表現, string,[msg] )
  • メタプログラミング

プログラムでプログラムを作成するというもの。Rubyでは様々なメソッドで利用可能だが、この章ではsendメソッドを用いてメタプログラミングを行う

  • send

動的にメソッドを呼べるメソッド。

  • update_columns

update_attributeメソッドの呼び出しを1行でまとめ、DBへの問い合わせを一回にまとめることができるメソッド。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails5.2 ActionCable]シンプルなチャットアプリを作ってみた!!

はじめに

この記事では、Rails5以降の機能であるAction Cableをもちいて、双方向リアルタイム通信を実現させます。

この記事の手順に沿えば非常にシンプルですが、チャットアプリが作れます。

1つのページ(チャット画面)でメッセージをやり取りするだけのシンプルなアプリです。
応用を利かすことができるようにシンプルさにこだわっています。

環境

macOS
Chrome

Rails 5.2
Ruby 2.5.0

gem
- slim-rails
- html2slim
- jquery-rails

参考にさせていただいた記事

Githubにコードを公開しています

Githubはこちら

完成したアプリの動作

returnを押すと文章が投稿できる。
② リアルタイムでチャット画面が更新される。
③ データベースと連携してテキストが保存される。

!注意!
Roomモデルを作らないので、チャットルームを分けることはできません。
Userモデルも作らないので、投稿者とメッセージの関連付けはできません。
*デザインは全くしていません。

ezgif.com-video-to-gif.gif

全体の工程

①ベースを作る

rails newして、必要なgemview controllerを揃えます。

次の工程で作成するチャネルは何もしないとデフォルトのcoffeescriptで生成されます。

それを避けたい場合は、下記ででプロジェクトを作成するか、

rails new AppName --skip-coffee

プロジェクト作成後に下記を削除してbundle installしてください。

# 20行目
gem 'coffee-rails', '~> 4.2'
②チャネル作成
rails g channel room speak

websocket(双方向リアルタイム通信)を実現してくれるファイルを生成する。

③サーバーとクライアントの設定をする

サーバーサイドは、ruby クライアントサイドは、coffee script + jQueryで実装していきます。
ここまでで、クライアントから入力されたメッセージを受け取り、表示させることができます。
しかし、入力したメッセージはモデルがないので、データベースには保存されません。
入力されたデータを双方向にリアルタイムに表示させるだけです。

④入力情報をデータベースへの保存させる
rails g model Message

messageモデルを作成し、データベースへ保存させます。

実際に作っていこう

①ベースを作る

rails new chat_app

この記事では、slim jqueryを使って開発するのでインストールしていきます。下記を追加してください。

slimの導入でhtmlの記述を減らし効率よく書くことができます。
jqueryの導入は、扱いなれているからです。

gem 'slim-rails'
gem 'html2slim'

gem 'jquery-rails'
bundle install

jQuery を読み込みませます。

app/assets/javascripts/application.js
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
****上記の1行を追記****
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require_tree .

そして、既存のviews以下の.htnl.erb.html.slimに変換する。
下記のコマンドで、既存ファイルを削除して、slimに変換できます。

bundle exec erb2slim app/views/layouts/ --delete

コントローラー、ビュー作成、ルーティングを行う

コントローラーの作成
ここでは、モデルを作っていないので、アクションは定義しません。

rails g controller rooms show

ビューの作成

app/views/rooms/show.html.slim
h1 チャットルーム

ルーティング

app/config/routes.rb
Rails.application.routes.draw do
  root to: 'rooms#show'
end

ここまでで、サーバーを起動するとpage#showのページが表示されます。

スクリーンショット 2019-01-26 20.59.58.png

②チャネル作成

いよいよAction Cableの実装です。

まずは、ファイルを生成しましょう。

下記のようにコマンドに入力してください。
rails g channel <任意のチャネル名> <任意のチャネルのアクション>

ここでは、チャネル名 room チャネルアクション名 speakとします。

rails g channel room speak

結果
2つのファイルが生成されます。

create  app/channels/room_channel.rb
identical  app/assets/javascripts/cable.js
create  app/assets/javascripts/channels/room.coffee
各ファイルの説明

サーバーサイドの処理をするファイルです。
初期は下記のようになっています。

app/channels/room_channel.rb
# Be sure to restart your server when you modify this file. Action Cable runs in an EventMachine loop that does not support auto reloading.
class RoomChannel < ApplicationCable::Channel
  def subscribed
    # stream_from "some_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak
  end
end

クライアントサイドの処理をするファイルです。

app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
  connected: ->
    # Called when the subscription is ready for use on the server

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    # Called when there's incoming data on the websocket for this channel

  speak: ->
    @perform 'speak'
チャネルの動作確認

チャネルを作成した時点でブラウザ上のコンソール(javascriptを使って)からAction Cableの動作を確認できます。

確認方法は簡単です。

rails sでlocalhostを開き、検証画面を表示させます。
macでChromeをお使いなら画面の適当な場所で右クリック→検証で、検証画面が表示されます。
表示された検証画面のConsoleをクリック。

ここでは、Appが使えます。JSでAction Cableの機能を呼び出すインスタンスです。
下記のようにコンソールを実行して返答も同じなら接続できています。

 > App
< {cable: Consumer, room: Subscription}

> App.cable
< Consumer {url: "ws://localhost:3000/cable", subscriptions: Subscriptions, connection: Connection}

またチャネル名、チャネルのアクション名をコンソールに入力しても確認できます。

 > App.room.speak()
 < true

スクリーンショット 2019-01-26 21.06.54.png

このようにAction CableをJSを使ってクライアントとサーバー間のデータをやり取りします。

③クライアントとサーバーサイドの設定

フォームからデータを入力してアラートを出す

フォームの実装から取り掛かります。
先程作ったビューのviews/pages/show.html.slimに設置します。

** form_withを使うと余計なものも生成されるので、自作で設置していきます。**

views/rooms/show.html.slim
h1 チャットルーム

form
  input type="text" data-behavior="room_speak"

ここにある data属性は、returnキーを押し入力データを受け取るイベントを発火させる目印の役割を担っています。

クライアントサイドの処理

フォームにデータを入力し、アラートさせる処理を書いていきます。
クライアントサイドは、coffee つまりjavascriptで出来ています。

説明をすると@performメソッドによりブラウザから入力されたデータをサーバーへ送信することができます。

第1引数にメソッド名を指定します。 ここではspeak
第2引数に送信するデータを指定します。

ここでは、speakの引数をmessageとしています。returnキーを押すと入力されたデータ(event.target.value)がspeakの引数に入ります。

app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",

  # 省略

  received: (data) ->
    # alertを表示させる処理
    alert data['message']

  speak: (message) ->
    @perform 'speak', message: message

  # returnキーでデータを受け取る処理
  $(document).on 'keypress', '[data-behavior~=room_speak]', (event) ->
  if event.keyCode is 13
    # コンソールで接続確認で使ったコード
    App.room.speak event.target.value
    event.target.value = ''
    event.preventDefault()
サーバーサイドの処理

まずは、subscribed内のコメントを外します。そして購読(サブスクライブ)させるチャネルをstream_forで指定します。
そして、speakに指定したチャネルに指定したデータを送信するという処理を書いていきます。

app/channel/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)   #サーバーサイドのspeakアクションの定義
    ActionCable.server.broadcast 'room_channel', message: data['message']
  end
end

ここまでで、実装したフォームに何かを入力し、returnキーを押すことで、ブラウザに入力データを受け取ってアラートを表示させることができます。

スクリーンショット 2019-01-26 21.19.18.png

アラートではなく、ブラウザに表示させる

アラート出す処理を書いていたクライアントサイドを今度は、ブラウザに表示させるようにします。

まずは、ビューファイルをデータを表示できるように編集します。

h1 チャットルーム

form
  input type="text" data-behavior="chat_post"

#messages_index
  // 下記に<p>XXX</p>として表示させる。

jQueryで受け取ったデータをブラウザ上に表示させる処理。

App.room = App.cable.subscriptions.create "RoomChannel",

  # 省略

  received: (data) ->
    # alertを表示させる処理
    # alert data['message']
    # 下記に変更
    $('#messages_index').append('<p>' + data['message'] + '</p>');


  speak: (message) ->
    @perform 'speak', message: message

  # returnキーでデータを受け取る処理
  $(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
  if event.keyCode is 13
    # コンソールで接続確認で使ったコード
    App.room.speak event.target.value
    event.target.value = ''
    event.preventDefault()

以上で、ブラウザ上にフォームから入力したデータを出力することができます。
データベースとは連携していないので、ページを更新すれば消えてしまいます。
次の項目から、データを保存できるようにデータベースと連携させていきます。

スクリーンショット 2019-01-26 21.42.50.png

④データベースと連携させる

入力したデータを保存させるにはまずは、当たり前ですがモデルが必要です。早速、モデルを作ります。

モデル名 Message カラムにはMessageの中身を表すカラムcontentをデータ型をtextとして作成します。

rails g model Message content:text
rails db:migrate
ビューの表示させるようにコントローラーに処理を書く
app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def show
    @messages = Message.all
  end
end
データを表示できるようにビューを書き換える

ここまでのviews/rooms/show.html.slimでデータを表示させていましたが構造を変えます。

まず、データを受け取って表示するだけのテンプレートを作成します。ビューは手動で作成します。

views/messages/_message.html.slim
.message
  p= message.content

次にそのテンプレートを表示させるビューを作ります。前に作ったファイルを編集していきます。

views/rooms/show.html.slim
h1 チャットルーム

form
  input type="text" data-behavior="room_speak"

#messages_index
    - @messages.each do |message|
        = render 'messages/message', message: message
サーバーサイドでMessageをデータベースに保存させる

speakアクションを書き換える。

app/channel/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    # 下記に変更します!!
    Message.create! content: data['message']
  end
end

書き換える前に行っていたここの処理をActiveJobにさせる。

その前に、ActiveJobとは??

ジョブを宣言してサーバーサイドでキュー操作を抽象化して実行させるためのフレームワーク。

つまりここでやりたいことは、以前サーバーサイドでspeakが行なっていた処理をJobにやらせようということ。
現在は、Messageを作成する処理をおこなっているが、その処理をJobにやらせましょう。

Jobを作成

rails g job MessageBroadcast

生成されるファイル

create  test/jobs/message_broadcast_job_test.rb
create  app/jobs/message_broadcast_job.rb

ファイルの中身

app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(*args)
    # Do something later
  end
end

ここからブロードキャストの処理を追加していきます。
先程、作成したメッセージのテンプレートをチャネルに送信されるように処理を書きます。

app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast "room_channel", message: render_message(message)
  end

  private
    def render_message(message)
      ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
    end
end

モデルMessageを編集してバリデーションをつけ、ジョブの実行タイミングを指定。
Messageがcreateされた後にブロードキャストするように指定している。

app/models/message.rb
class Message < ApplicationRecord
  validates :content, presence: true #これにより無記入投稿とエンター長押しの連続投稿の2つが同時に防げる
  after_create_commit {MessageBroadcastJob.perform_later self}
end
非同期に受け取ったデータを表示させる

クライアントサイドのファイルを書き換える。

app/assets/javascripts/channels/room.coffee
received: (data) ->
  $('#messages_index').append data['message']

ついに完成

フォームに入力したデータが非同期に双方向に表示されていることがわかるはずです。

最後に

基本的なAction Cableの使い方についてチャットアプリを作成しながら学んでみました。
間違いの指摘、効率の良い書き方があれば、コメントお待ちしております。

最後まで読んでくださりありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

cloud9 と GitHub を使ってRailsアプリを開発する際の下準備

内容

cloud9 と GitHub を使ってRailsアプリを開発する際の下準備

なぜcloud9上でRailsアプリを開発するのか?

A. 従来はローカル環境(Mac)とGitHubを利用してアプリ開発をしていた。
しかし、ある時からnokogiriがインストールできないためローカルでの開発が困難に。

「Rubyの開発をするにはcloud9でやったほうがいい」とのアドバイスをいただいたので、cloud9とGitHubで開発することにした。

具体的な下準備

cloud9 に git をインストール

command-on-cloud9
gem intall git

バージョンを指定してRailsのインストール

command-on-cloud9
gem install rails -v 5.1.6

cloud9からGitHubにUP

command-on-cloud9
git init
git add .
git commit -m "Write your comment here."
git remote add origin [your GitHub repository]
git push -u origin master
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyでAES暗号化

はじめに

RubyでAES暗号化処理を実装する際のメモ。
毎回違う暗号が生成されるので安心。
"暗号化"の対義語は"復号"なのか"復号化"なのかは人によって結構わかれるみたいですね。
個人的には"復号"派です。

事前準備

特に必要なし。

コード

暗号化

encrypt.rb
require 'openssl'
require 'base64'

# ======================================
# <暗号化>
# ======================================
# plain_text: 暗号化したい文字列
# password  : 好きなパスワード
# bit       : 鍵の長さをビット数で指定。128, 192, 256が指定できる。
#             基本的には256を指定しておけば安心。
# ======================================
def aes_encrypt(plain_text, password, bit)

  # saltを生成
  salt = OpenSSL::Random.random_bytes(8)

  # 暗号器を生成
  enc = OpenSSL::Cipher::AES.new(bit, :CBC)
  enc.encrypt

  # パスワードとsaltをもとに鍵とivを生成し、設定
  key_iv = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, 2000, enc.key_len + enc.iv_len, "sha256")
  enc.key = key_iv[0, enc.key_len]
  enc.iv = key_iv[enc.key_len, enc.iv_len]

  # 文字列を暗号化
  encrypted_text = enc.update(plain_text) + enc.final

  # Base64でエンコード
  encrypted_text = Base64.encode64(encrypted_text).chomp
  salt = Base64.encode64(salt).chomp

  # 暗号とsaltを返す
  return [encrypted_text, salt]
end

復号

decrypt.rb
require 'openssl'
require 'base64'
# ======================================
# <復号>
# ======================================
# encrypted_text: 復号したい文字列
# password      : 暗号化した時に指定した文字列
# salt          : 暗号化した時に生成されたsalt
# bit           : 暗号化した時に指定したビット数
# ======================================
def aes_decrypt(encrypted_text, password, salt, bit) 

  # Base64でデコード
  encrypted_text = Base64.decode64(encrypted_text)
  salt = Base64.decode64(salt)

  # 復号器を生成
  dec = OpenSSL::Cipher::AES.new(bit, :CBC)
  dec.decrypt

  # パスワードとsaltをもとに鍵とivを生成し、設定
  key_iv = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, 2000, dec.key_len + dec.iv_len, "sha256")
  dec.key = key_iv[0, dec.key_len]
  dec.iv = key_iv[dec.key_len, dec.iv_len]

  # 暗号を復号
  plain_text = dec.update(encrypted_text) + dec.final
  return plain_text
end

使用例

Usage.rb
# 暗号化したい文字列とパスワードを用意
plain_text = "Michael jackson"
password = "**password**"

# 暗号化
# encrypted_textとsaltは実行するたびに違う文字列が生成される
encrypted_text, salt = aes_encrypt(plain_text, password, 128)
puts encrypted_text
# => oQ57Y74tgVtCQdFimQgy7A==
puts salt
# => 0skezwgOtRA=

# 複合
decrypted_text = aes_decrypt(encrypted_text, password, salt, 128)
puts decrypted_text
# => Michael jackson

参考

Ruby 2.6.0 リファレンスマニュアル
class OpenSSL::Cipher
module OpenSSL::PKCS5

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ストロングパラメーターを設定する

ログイン画面の実装をする際に入力する値に対してストロングパラメーターを実装する方法を紹介します。

form_foからログイン情報を入力する

# app/view/sessions/new.html.erb
...
<%= form_for :session, url: login_path do |f|%>
  <div class="form-group">
    <%= f.label :email, class: "text-white"%>
    <%= f.text_field :email, class:"form-control"%>
  </div>
  <div class="form-group">
    <%= f.label :password, class: "text-white" %>
    <%= f.password_field :password,class: "form-control" %>
  </div>
  <%= f.submit "ログイン",class:"btn-block btn-white"%>
<% end %>
...
# config/routes.erb

Rails.application.routes.draw do
...
  post "/login", to: "sessions#create"
...
end

このフォームに「email」と「password」を入力してログインボタンを押すと、
paramsの中にsessionというハッシュがpostされます。
sessionの中にはemailとpasswordに関するハッシュが含まれています。

コントローラーでストロングパラメーターを設定する

# app/cotroller/sessions_controller.rb

class SessionsController < ApplicationController
...
  def create
    user =User.find_by(email_params)
    if user && user.authenticate(password_params[:password])
      log_in  user
      redirect_to root_path,success: "ログインに成功しました"
    else
      flash.now[:danger]="ログインに失敗しました"
      render :new
    end
  end
...
  private
  def log_in(user)
    session[:user_id]=user.id
  end

  def email_params
    params.require(:session).permit(:email)
  end

  def password_params
    params.require(:session).permit(:password)
  end
...
end

createメゾットの中を説明していきます。
まず、ここではfind_byによってUserテーブルからemail_paramsに合った最初の値を取得します。

user =User.find_by(email_params)

email_paramsはprivate以下にあるメゾットです。
private以下にあるため、class外から呼び出すことができません。
ここでrequireメソッドによって、paramsから引数に設定したsessionの値を取得することができます。
そしてpermitメソッドによってsessionの中のemailの値だけの使用を許可します。
passwordも同じように設定します。

def email_params
  params.require(:session).permit(:email)
end

次にその下のif文でuserとuser.authenticate()がTrueのとき
ログインが成功するようになっています。
authenticateメソッドは、引数に指定したパスワードの値と、userテーブルに登録されているpasswordと一致しているかを検証します。

if user && user.authenticate(password_params[:password])

ただし、このメゾットを使う場、modelsのuser.rbでhas_secure_passwordの記述が必要です。

# app/models/user.rb

class User < ApplicationRecord
...
  has_secure_password
...
end

再び、sessions_controller.rbのcreateメゾットに戻ります。
log_in userはprivateのlog_in()メゾットの引数にuserを渡しています。
次の行のコードのredirect_toで第1引数に指定したURLにリダイレクトされます。

# app/cotroller/sessions_controller.rb

log_in  user
redirect_to root_path,success: "ログインに成功しました"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby+Carloでデスクトップアプリを作る

Opal Advent Calendar 2016ではOpalとElectronを組み合わせてデスクトップアプリを作るという記事がありました。今回はOpalとCarloという組み合わせを試してみたので手順を説明します。

CarloはElectron同様、JavaScriptでデスクトップアプリを作るためのものですが、環境に入っているChromeを利用するため、配布サイズが小さいというメリットがあるようです

Carloのインストール

基本的にはnpm i carloだけなのですが、環境によって(?)bignum@0.11.0がコンパイルできないことがありました。その場合はnodebrewでNode.js 8.11.2を入れることでなんとかできました(下記手順)。あるいは最新のNodeでも大丈夫かもしれません。

$ brew install nodebrew
...
==> Caveats
You need to manually run setup_dirs to create directories required by nodebrew:
  /usr/local/opt/nodebrew/bin/nodebrew setup_dirs

Add path:
  export PATH=$HOME/.nodebrew/current/bin:$PATH

To use Homebrew's directories rather than ~/.nodebrew add to your profile:
  export NODEBREW_ROOT=/usr/local/var/nodebrew
...
$ /usr/local/opt/nodebrew/bin/nodebrew setup_dirs  
$ nodebrew install 8.11.2
$ export PATH=$HOME/.nodebrew/current/bin:$PATH
$ nodebrew use 8.11.2
$ node -v
$ npm i carlo  

Exampleを動かす

Carloがインストールできたら、まずはJavaScriptでアプリを作れる状態を作りましょう。https://github.com/GoogleChromeLabs/carlo にあるようにexample.jsとexample.htmlを用意します。

example.js
const carlo = require('carlo');

(async () => {
  // Launch the browser.
  const app = await carlo.launch();

  // Terminate Node.js process on app window closing.
  app.on('exit', () => process.exit());

  // Tell carlo where your web files are located.
  app.serveFolder(__dirname);

  // Expose 'env' function in the web environment.
  await app.exposeFunction('env', _ => process.env);

  // Navigate to the main page of your app.
  await app.load('example.html');
})();
example.html
<script>
async function run() {
  // Call the function that was exposed in Node.
  const data = await env();
  for (const type in data) {
    const div = document.createElement('div');
    div.textContent = `${type}: ${data[type]}`;
    document.body.appendChild(div);
  }
}
</script>
<body onload="run()">

node example.jsで実行すると、Chromeが起動して、環境変数の一覧が表示されます。

Opalのコードを組み込む

Opalのコードをこれに組み込むのは簡単で、.rbをopal -cで.jsに変換し、それをscriptタグで読み込むだけです。

例として、Ovtoのサンプルコードを組み込んでみます。yhara/ovtoexamples/static/以下をコピーして、bundle installrakeを実行してapp.jsを作ります。

$ git clone https://github.com/yhara/ovto.git
$ cp ovto/example/static/* .
$ bundle install
$ rake

example.htmlは以下のようにします。

example.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>Carlo + Ovto example</title>
    <script src='app.js' type='text/javascript'></script>
  </head>
  <body>
    <div id='ovto'></div>
  </body>
</html>

node example.jsで実行すると、以下のようになりました。

スクリーンショット 2019-01-26 15.28.01.png

あとはpkgとかでexeにすれば単体で配布できるはずです。

これで、摂氏と華氏を変換するアプリが作れました…と言いたいところですが、2台のMacBookで試した結果、片方ではうまく動きませんでした:thinking: (inputタグにフォーカスしてキーを叩いても文字が入力できなかった。app.jsを外してただのinputタグを入れても同じだったので、Opalは関係なさそうですが…)

まとめ

今回はCarloを使ってOpalでデスクトップアプリを作る方法を解説しました。こうやってJavaScript関連の資産をRubyで流用できるのがOpalの面白いところですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

小さく薄くrails newする(ViewやJSが必要ない場合)

HerokuでRubyのスクリプトのような何かを動かす時に、activerecord等は利用したいのでRuby単体ではなくてRailsとしての方が都合がいいのだけどViewやJS関係は特に必要ない。そんな時があります。
そういうわけで、上記のような用途向けのView関連、JS関連のファイルの作成の全てをスキップした rails new のオプション構成を調べました。

確認環境は

  • Rails 5.2.2 (Ruby 2.6.0)

です。

結論

結論は下記です。

rails new . --database=postgresql --skip-yarn --skip-git --skip-action-mailer --skip-active-storage --skip-action-cable --skip-sprockets --skip-javascript --skip-turbolinks --skip-test --api --skip-bundle

各オプションの解説

各オプションの概要は下記の通りです。

オプション 効果
--database=DATABASE 使用するdatabaseを指定
--skip-yarn Yarnを利用しない
--skip-git .gitignore を作成しない
--skip-action-mailer Action Mailer関連のファイルを作成しない
--skip-active-storage Active Storage関連のファイルを作成しない
--skip-action-cable Action Cable関連のファイルを作成しない
--skip-sprockets Sprockets関連のファイルを作成しない
--skip-javascript JavaScript関連のファイルを作成しない
--skip-turbolinks turbolinks gemを利用しない
--skip-test test関連ファイルを作成しない
--api APIとして利用するapp向けの小さい構成
--skip-bundle bundle installを実行しない

詳しくは

rails new --help

でどうぞ。

補足しておくと、

  • 利用DBはPostgreSQL
  • 予め .gitignore は用意しているので デフォルトの .gitignore は作成しない ( -skip-git )
  • どうせあとでRspecをインストールするので標準のtest関連ファイルは必要ない ( --skip-test )
  • どうせあと後でGemfileに必要なgemを追加して bunde install するので bundle install しない ( --skip-bundle )

です。

参考: 上記オプション付与時の差分等

removeされるファイル一覧

参考までに、上記オプション付与時にremoveされるファイルは下記の通りです。

ちゃんとどのファイルが削除されるのか表示されて便利だ……。

rails new . --database=postgresql --skip-yarn --skip-git --skip-action-mailer --skip-action-cable --skip-sprockets --skip-javascript --skip-turbolinks --skip-test --api --skip-bundle
  (中略)
      remove  app/assets
      remove  lib/assets
      remove  tmp/cache/assets
      remove  app/helpers
      remove  test/helpers
      remove  app/views
      remove  public/404.html
      remove  public/422.html
      remove  public/500.html
      remove  public/apple-touch-icon-precomposed.png
      remove  public/apple-touch-icon.png
      remove  public/favicon.ico
      remove  app/assets/javascripts
      remove  config/initializers/assets.rb
      remove  app/views/layouts/mailer.html.erb
      remove  app/views/layouts/mailer.text.erb
      remove  app/mailers
      remove  test/mailers
      remove  app/assets/javascripts/cable.js
      remove  app/channels
      remove  config/initializers/cookies_serializer.rb
      remove  config/initializers/content_security_policy.rb
      remove  config/initializers/new_framework_defaults_5_2.rb
      remove  bin/yarn

Gemfile差分

また、作成されるGemfileとデフォルトのGemfile( Gemfile.default )の差分は下記となります。

デフォルトのGemfileと比べると、sass-rails , coffee-rails から始まって web-console , またtestの capybara , selenium-webdrive に至るまでViewに関するgem一式が削除されていることがわかりますね。

git diff --no-index -U10 Gemfile.default Gemfile
diff --git a/Gemfile.default b/Gemfile
index e45e610..c5441b8 100644
--- a/Gemfile.default
+++ b/Gemfile
@@ -1,62 +1,40 @@
 source 'https://rubygems.org'
 git_source(:github) { |repo| "https://github.com/#{repo}.git" }

 ruby '2.6.0'

 # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
 gem 'rails', '~> 5.2.2'
 # Use postgresql as the database for Active Record
 gem 'pg', '>= 0.18', '< 2.0'
 # Use Puma as the app server
 gem 'puma', '~> 3.11'
-# Use SCSS for stylesheets
-gem 'sass-rails', '~> 5.0'
-# Use Uglifier as compressor for JavaScript assets
-gem 'uglifier', '>= 1.3.0'
-# See https://github.com/rails/execjs#readme for more supported runtimes
-# gem 'mini_racer', platforms: :ruby
-
-# Use CoffeeScript for .coffee assets and views
-gem 'coffee-rails', '~> 4.2'
-# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
-gem 'turbolinks', '~> 5'
 # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
-gem 'jbuilder', '~> 2.5'
-# Use Redis adapter to run Action Cable in production
-# gem 'redis', '~> 4.0'
+# gem 'jbuilder', '~> 2.5'
 # Use ActiveModel has_secure_password
 # gem 'bcrypt', '~> 3.1.7'

-# Use ActiveStorage variant
-# gem 'mini_magick', '~> 4.8'
-
 # Use Capistrano for deployment
 # gem 'capistrano-rails', group: :development

 # Reduces boot times through caching; required in config/boot.rb
 gem 'bootsnap', '>= 1.1.0', require: false

+# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
+# gem 'rack-cors'
+
 group :development, :test do
   # Call 'byebug' anywhere in the code to stop execution and get a debugger console
   gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
 end

 group :development do
-  # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
-  gem 'web-console', '>= 3.3.0'
   gem 'listen', '>= 3.0.5', '< 3.2'
   # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
   gem 'spring'
   gem 'spring-watcher-listen', '~> 2.0.0'
 end

-group :test do
-  # Adds support for Capybara system testing and selenium driver
-  gem 'capybara', '>= 2.15'
-  gem 'selenium-webdriver'
-  # Easy installation and use of chromedriver to run system tests with Chrome
-  gem 'chromedriver-helper'
-end

 # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
 gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

知識0から始めるRails on Docker

はじめに

Railsの勉強を始めようと思ったが、どうせならDockerを使って仮想環境で開発したい。
しかしどちらも実務で使ったことがないためよくわからない(というかRubyすら触ったことがない)。

ということで、一から調べてみた。RailsとDockerの欲張りセット。
筆者はMacBookProを使っているため、MacOS向けの記事となります。

tl;td

  • Rubyにまつわる各用語の確認、ローカル環境でRailsの導入・起動方法の確認
  • 手動でDocker上でのRailsの導入・起動方法の確認
  • dockerfileにRailsの導入・起動情報を文書化して格納し、docker-composeから呼び出して起動

対象読者

  • わたし
  • これからRailsを勉強しようという方
  • これからDockerを勉強しようという方

ロードマップ(超簡易版)

最終目標:Dockerによる仮想環境上でRails serverを起動し、開発できるようにする
※RailsのデフォルトサーバーはPumaです。この記事ではデフォルトのままなのでPumaを使いますが、便宜上「Rails server」と呼称します。

  • Railsをローカル環境で起動する
    • ローカル環境にRubyを導入する
    • ローカル環境にRailsを導入する
    • Railsプロジェクトを作成し、Rails serverを起動
  • Docker環境を構築して仮想環境でRails serverを起動する
    • ローカル環境にDockerを導入する
    • DockerでRubyを使える仮想環境を構築する
    • 仮想環境でRails serverを起動する
  • dockerfiledocker-composeを使ってコマンド一つでRails serverを起動する
    • dockerfileを作成する
    • docker-compose.ymlを作成し、起動する

Railsをローカル環境で起動する

これについては各所でがっつり解説されているので、ここでは流れ・用語の説明と参考資料の紹介にとどめます。
なお、この章の目的はRubyにまつわる各用語とRails serverの起動までの流れについて確認することが目的なので、既にバッチリの方は飛ばしてください。
参考資料:Ruby初学者のRuby On Rails 環境構築【Mac】

ローカル環境にRubyを導入する

流れ
Homebrewの導入(更新)-> rbenvの導入 -> Rubyの導入

ざっくりHomebrew解説

ざっくりrbenv解説

ローカル環境にRailsを導入する

流れ
Bundlerの導入 -> BundlerでGemfileの作成 -> Gemfileの編集 -> Gemの取得(Railsの導入)

ざっくりGem解説

ざっくりBundler解説

Railsプロジェクトを作成し、Rails serverを起動

流れ
Railsからプロジェクトを作成 -> Rails serverの起動 -> 接続確認

Docker環境を構築して仮想環境でRails serverを起動する

ローカル環境にDockerを導入する

Docker公式から最新版を取得しましょう。
アカウントの登録が必要です。

なお、Dockerの基礎的な仕組みやコマンドなどについては、以下の参考資料がおすすめです。

参考資料:Dockerでプログラマが最低限知るべきことが、最速でわかるチュートリアル
docker

参考資料:【図解】Dockerの全体像を理解する -前編-

ざっくりDocker解説

  • 仮想環境の構築・管理を行うツール
  • 擬似的に様々な環境を構築することができ、ローカル環境と本番環境との差異を減らすことができる
  • 「本番環境で急に動かなくなった問題」や「環境構築面倒すぎる問題」の救世主……らしい

DockerでRubyを使える仮想環境を構築する

参考資料:RailsアプリをDockerで開発するための手順

ここからが本題です。
まずは利用するイメージを決めます。Docker Hubにrubyという便利なイメージが用意されているので、ありがたく使わせていただきましょう。

rubyイメージの取得
$ docker pull ruby

イメージが取得できたら早速run……する前に、このイメージについて調べましょう。

イメージの情報を出力
$ docker inspect ruby

コマンドを実行すると大量の文字が吐き出されます。
目眩がするかもしれませんが、頑張って大事な記述を確認しましょう。注目するのは以下の場所です。

"ContainerConfig": {
……
  "Env": [
                "PATH=/usr/local/bundle/bin:/usr/local/bundle/gems/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "RUBY_MAJOR=2.6",
                "RUBY_VERSION=2.6.0",
                "RUBY_DOWNLOAD_SHA256=acb00f04374899ba8ee74bbbcb9b35c5c6b1fd229f1876554ee76f0f1710ff5f",
                "GEM_HOME=/usr/local/bundle",
                "BUNDLE_PATH=/usr/local/bundle",
                "BUNDLE_SILENCE_ROOT_WARNING=1",
                "BUNDLE_APP_CONFIG=/usr/local/bundle"
            ],
……
RUBY_VERSION=2.6.0

Rubyのバージョンを示しています。Docker Hubでも確認できますが、イメージ本体にも情報がしっかり記されていますね。
このバージョンは重要なので覚えておきましょう。あるいはイメージをpullする段階で指定してあげてもいいでしょう。

BUNDLE_PATH=/usr/local/bundle

Bundlerのパスが指定されています。BundlerによるGemfileを使ったGem管理を行えることがわかりますね。

ではこのイメージからコンテナを起動しましょう。
適当な名前のディレクトリを作成し、ターミナルの作業フォルダとします。
以降はプロジェクト名をproject_nameとします。
project_name以外の名前でも構いません。本記事ではこの名前で統一するということです。

rubyイメージからコンテナを起動
$ docker run -i -t --name TEST -p 3000:3000 -v "$PWD":/usr/src/project_name ruby /bin/bash

まずは各オプションについて。

-i       :コンテナのSTDIN(標準入力)にアタッチ。標準入力に入力できる状態にするということ。
-t       :疑似ターミナル (pseudo-TTY) を割り当て。ターミナル画面で操作できるようにするということ。
--name   :コンテナの名前。今回は'TEST'を指定。
-p       :ポートの指定。今回は3000ポートを解放しています。
-v       :ボリュームの指定。[ホストPCのディレクトリ指定]:[コンテナ内のディレクトリ指定]。
/bin/bash:コンテナ起動後に実行するコマンド。今回はシェルを指定。

上記の説明は簡略したもので語弊を含むので、気になった項目は別途調べることをお勧めします。
-i-tの二つはセットで使う(-it)ことが多いので是非覚えておきましょう。
ボリュームについては概念が難しいですが、とりあえず[ホストPCで指定したディレクトリ]を[コンテナ内で指定したディレクトリ]として扱えるようにする、程度の理解でいいと思います。

参考資料:Dockerリファレンス

うまく起動できたら下のような状態になるはずです。

root@03eb04059b1f:/#

コンテナの中のターミナルを操作しているようなイメージですね。
コンテナの中から一旦抜ける時はcontrol + pq、再接続するときは$ docker attach [コンテナの指定]です。
exitでも抜けられますが、コンテナが停止してしまうため注意。停止した場合、ポートを閉じてしまうようです。
$ docker execでもコンテナに接続できます。ただし、プロセス接続するごとにプロセスが増えます。

仮想環境でRails serverを起動する

それでは今起動したコンテナでRails serverの準備をしましょう。
先ほどコンテナを起動した際に、コンテナ内のproject_nameディレクトリをボリュームとして指定しました。
指定したディレクトリが作成されているはずなので、そこまで移動しましょう。

Railsプロジェクトの確認
$ cd /usr/src/project_name

このディレクトリにRailsプロジェクトを作成していきます。
まずは何をするにもGemfileが必要なので、Bundlerを使って作成しましょう。

Gemfileの作成
$ bundle init

次にRailsを導入していきます。
Gemfileを編集する必要がありますが、これはコンテナ内で行う必要はありません。Gemifileを作成したディレクトリはコンテナ起動時にホストPC側のディレクトリとセットでボリューム指定したため、ホストPC側の指定したディレクトリにもGemfileが作成されています。このGemfileを編集するだけでOKです。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

- # gem "rails"
+ gem "rails"
コンテナ内のGemfile確認に反映されているか確認
$ cat Gemfile

これでGemfileの準備ができたので、GemのインストールとRailsプロジェクトの作成を行いましょう。

Gemのインストール
$ bundle install
$ bundle exec rails new .

rails exec .は作業ディレクトリに対してRailsプロジェクトを作成します。
実行するとGemfileの上書き確認をされるので許可しましょう。

ローカル環境ではこの時点でRails serverを起動できましたが、rubyイメージから作成したコンテナではまだRails serverは起動できません。
ですが、なぜ起動できないか確認するためにも一度試してみましょう。

起動確認(失敗する).
$ bundle exec rails server
エラーメッセージ(最後のみ抜粋)
/usr/src/project_name/vendor/bundle/ruby/2.6.0/gems/execjs-2.7.0/lib/execjs/runtimes.rb:58:in `autodetect': Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes. (ExecJS::RuntimeUnavailable)

ExecJS::RuntimeUnavailableというエラーについて調べてみると、どうやらJavaScriptランタイムというものが必要のようです。
今回はnode.jsを導入することにしましょう。
参考資料:rails sコマンド実行時に「Could not find a JavaScript runtime.」とエラーが出る場合の対処法

rubyイメージではyumコマンドは対応していないようなので、apt-getを使ってインストールしましょう。
参考資料:apt-get - パッケージの操作・管理 - Linuxコマンド

node.jsのインストール
$ apt-get update
$ apt-get install nodejs

完了したら今度こそRails serverが起動できるはずです。

Rails_server起動
$ bundle exec rails server

http://localhost:3000/に接続して起動できているか確認しましょう。

dockerfile と docker-compose を使ってコマンド一つでRails serverを起動する

ここからは今まで行ってきたことを設定として文書化していきます。
先ほどまで使用していたコンテナはもう使わないので、$ exitで停止しておきましょう。
ただし、作成したRailsプロジェクトはそのままにしておいてください。

参考資料:Dockerfile リファレンス
参考資料:Compose ファイル・リファレンス
参考資料:RailsアプリをDockerで開発するための手順

dockerfile を作成する

まず、前の章で行ったことを整理しておきましょう。

  • rubyイメージからコンテナを作成(本記事ではバージョン2.6.0)
  • ポート3000:3000を開放し、ボリュームを指定してコンテナを起動
  • コンテナ内でRailsプロジェクトを作成
  • コンテナにnode.jsをインストール
  • Rails serverを実行

dockerfileを書くということは、これらの作業を文書化していくということです。

それではdockerfileを書いていきます。Railsプロジェクトを作成したディレクトリにdockerfileという名前でファイルを作成し、編集していきます。
まずは使用したイメージの情報です。

dockerfile
FROM ruby:2.6.0

:2.6.0はバージョン情報です。指定しなければ最新版が自動で選択されますが、固定させた方がいいでしょう。
これはRubyのバージョンと同じなので、もし違うバージョンからRailsプロジェクトを作成したならそのバージョンに合わせてください。

次にポートの開放とボリューム指定ですが、dockerfileではホストPCのディレクトリを指定することはできません(後でdocker-compose.ymlで指定します)。よって、ここではポートの開放だけを行います。

dockerfile
FROM ruby:2.6.0

EXPOSE 3000

ここでポートの開放を行なっても、runコマンドやdocker-compose.ymlで指定しなければ直接接続できないので注意してください。

次はコンテナ内でRailsプロジェクト作成ですが、これは後でボリューム指定により共有する予定なので必要ありません。
ですが、Gemfileが無くてはGemのインストールができず、railsコマンドが実行できません。
そこで、Gemfileだけをコンテナ内にコピーしてインストールすることにします。

dockerfile
FROM ruby:2.6.0

ENV APP_ROOT /usr/src/project_name 

WORKDIR ${APP_ROOT}

COPY Gemfile ${APP_ROOT}
COPY Gemfile.lock ${APP_ROOT}

RUN bundle install

EXPOSE 3000

各コマンドは以下の通りです。

ENV [key] [value] …… 環境変数の定義。
WORKDIR [ディレクトリ指定] …… 作業ディレクトリの指定
COPY [ソース指定] [保存先指定] …… ファイルのコピー。
RUN [コマンド] …… コマンドの実行。

これでコンテナ内でもrailsコマンドが利用できるようになります。

最後にnode.jsのインストールとRails serverの起動です。

dockerfile
FROM ruby:2.6.0

ENV APP_ROOT /usr/src/project_name 

WORKDIR ${APP_ROOT}

RUN apt-get update && \
    apt-get install -y nodejs

COPY Gemfile ${APP_ROOT}
COPY Gemfile.lock ${APP_ROOT}

RUN bundle install

EXPOSE 3000

CMD bundle exec rails server

Railse sever の起動がRUNではなくCMDなのは、コンテナの立ち上げが完了してから実行してほしいからです。
もしRUNで書いてしまうとそこで動作が止まってしまい、いつまでもコンテナが立ち上がらないということになってしまいます。

では、これでうまく動作するかどうか確認してみましょう。

dockerfileからイメージの作成と起動
$ docker build .

エラーが出ずにRails serverの起動までできればOKです。

docker-compose.yml を作成し、起動する

では、先ほど作成したdockerfileを使ってdocker-compose.ymlを作っていきましょう。
先ほどと同じく、Railsプロジェクトのディレクトリに作成し、以下の通りに編集してください。

docker-compose.yml
app:
  build: .
  ports: 
    - '3000:3000'
  volumes: 
    - .:/usr/src/project_name
app: …… アプリケーションの名前。今回はappとした。
build: …… docker-composeのパス指定。今回は同一ディレクトリにある。
ports: …… 開放するポートの指定。
volumes: …… ボリュームの指定。

これで完了です。では起動してみましょう。

docker-composeからの起動
$ docker-compose up -d

http://localhost:3000/に接続して、いつものアレが表示されたらOKです!
ちなみに、ホストPCのディレクトリを参照しているため、ファイルを編集すると即座に反映されます。Gemfileと同じですね。

おわりに

というわけで、起動できました。とりあえず動いたので満足。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

cmake error when installing rugged with ruby 2.3 image

概要

タイトルの通りです。Dockerのruby:2.3.8イメージでruggedをインストールしようとした際に発生したcmake時のエラーと対処について記録します。

事象と対応

私は以下のようにgitlab-ci.ymlを設定してGitLabのジョブを実行していました。単にrailsのアプリをインストールしてテストを実行しようとしているだけの設定です。

image: ruby:2.3.8

before_script:
  - apt-get install -y --no-install-recommends cmake
  - bundle install --path vendor/bundle

test:
  script:
  - bundle exec rake spec

前述の通りruggedをインストールするため、依存パッケージのcmakeも前もってインストールしています。

いざ実行してみるとジョブが成功しません。エラーログはこのような感じです。

current directory:
/builds/myname-anonymous/rails-4.2.11-ruby-2.3.8/vendor/bundle/ruby/2.3.0/gems/rugged-0.27.7/ext/rugged
/usr/local/bin/ruby -I /usr/local/lib/ruby/site_ruby/2.3.0 -r
./siteconf20190125-288-nqejbk.rb extconf.rb
checking for gmake... no
checking for make... yes
checking for cmake... yes
checking for pkg-config... yes
-- cmake .. -DBUILD_CLAR=OFF -DTHREADSAFE=ON -DBUILD_SHARED_LIBS=OFF
-DCMAKE_C_FLAGS=-fPIC -DCMAKE_BUILD_TYPE=RelWithDebInfo
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include
    --without-opt-include=${opt-dir}/include
    --with-opt-lib
    --without-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=/usr/local/bin/$(RUBY_BASE_NAME)
    --with-sha1dc
    --without-sha1dc
    --use-system-libraries
extconf.rb:21:in `sys': ERROR: 'cmake .. -DBUILD_CLAR=OFF -DTHREADSAFE=ON
-DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS=-fPIC -DCMAKE_BUILD_TYPE=RelWithDebInfo 
' failed (RuntimeError)
    from extconf.rb:83:in `block (2 levels) in <main>'
    from extconf.rb:80:in `chdir'
    from extconf.rb:80:in `block in <main>'
    from extconf.rb:77:in `chdir'
    from extconf.rb:77:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can
be found here:

/builds/myname-anonymous/rails-4.2.11-ruby-2.3.8/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0/rugged-0.27.7/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in
/builds/myname-anonymous/rails-4.2.11-ruby-2.3.8/vendor/bundle/ruby/2.3.0/gems/rugged-0.27.7
for inspection.
Results logged to
/builds/myname-anonymous/rails-4.2.11-ruby-2.3.8/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0/rugged-0.27.7/gem_make.out

An error occurred while installing rugged (0.27.7), and Bundler cannot continue.
Make sure that `gem install rugged -v '0.27.7' --source 'https://rubygems.org/'`
succeeds before bundling.

In Gemfile:
  rugged

言われたとおり、/builds/myname-anonymous/rails-4.2.11-ruby-2.3.8/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0/rugged-0.27.7/mkmf.logを見るために、ログを入れた上で再度実行してみると以下のようなログがでました。

Running after script...
$ cat /builds/myname-anonymous/rails-4.2.11-ruby-2.3.8/vendor/bundle/ruby/2.3.0/extensions/x86_64-linux/2.3.0/rugged-0.27.7/mkmf.log
find_executable: checking for gmake... -------------------- no
--------------------
find_executable: checking for make... -------------------- yes
--------------------
find_executable: checking for cmake... -------------------- yes
--------------------
find_executable: checking for pkg-config... -------------------- yes
--------------------
"cmake .. -DBUILD_CLAR=OFF -DTHREADSAFE=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS=-fPIC -DCMAKE_BUILD_TYPE=RelWithDebInfo  "
-- The C compiler identification is GNU 6.3.0
-- Check for working C compiler: /usr/bin/cc
... 省略
-- Looking for qsort_s - not found
-- Looking for clock_gettime in rt
-- Looking for clock_gettime in rt - found
-- Checking for module 'libcurl'
--   Found libcurl, version 7.52.1
--   Resolved libraries: /usr/lib/x86_64-linux-gnu/libcurl.so
-- Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the system variable OPENSSL_ROOT_DIR (missing:  OPENSSL_LIBRARIES OPENSSL_INCLUDE_DIR) 
CMake Error at src/CMakeLists.txt:157 (MESSAGE):
  Unable to autodetect a usable HTTPS backend.Please pass the backend name
  explicitly (-DUSE_HTTPS=backend)


-- Configuring incomplete, errors occurred!
See also "/builds/myname-anonymous/rails-4.2.11-ruby-2.3.8/vendor/bundle/ruby/2.3.0/gems/rugged-0.27.7/vendor/libgit2/build/CMakeFiles/CMakeOutput.log".
See also "/builds/myname-anonymous/rails-4.2.11-ruby-2.3.8/vendor/bundle/ruby/2.3.0/gems/rugged-0.27.7/vendor/libgit2/build/CMakeFiles/CMakeError.log".
ERROR: Job failed: exit code 1

Could NOT find OpenSSL とあります。どうやらOpenSSLがないらしいです。いろいろ調べるとlibssl-devパッケージをインストールする必要があったらしいため、gitlab-ci.ymlbefore_scriptにインストール処理を書いて実行したらうまくいきました。

原因分析

ruby:2.2.4のイメージを使用していたときはこのようなことが起こらなかったたため、なぜ2.3.8だと起こるのかが気になったので原因を調べました。

イメージ単位で結果が異なったので原因はDockerのrubyイメージの内容の違いだと思いました。残念ながら2.2.4のイメージはDockerfileが簡単には見つけられそうになかったため2.3.8Dockerfileから見始めました。気になる部分がありました。既にlibssl1.0-devがインストールされているようです。しかもその上には# ruby 2.3 on stretch can only support libssl1.0-dev (libssl dev from buildpack-deps is 1.1.x)という記述があります。どうやら2.3のバージョンはlibssl1.0-devしかサポートしていないので、親イメージのlibssl-devの代わりとしてインストールしているようです。じゃあ親イメージのbuildpack-deps:stretchにはlibssl-devがインストールされてるんだなと思い調べるとされてました

そうなってくるとなんでわざわざこちら側でもlibssh-devをインストールしなきゃいけないのかと思い両方のDockerfileを眺めていたら、怪しい部分を発見しました。ruby:2.3.8のイメージはこの行でrubyのインストールに使用したlibssl1.0-devをアンインストールしているらしいです。まあでもlibssl1.0-devの方だしlibssl-devの方はアンインストールされてないんじゃないかなと思ったらそうではありませんでした。gitlab-ci.ymlにいろいろログを出力させて、libssl1.0-devのインストール時にlibssl-devが消えることがわかりました。つまりは

  1. buildpack-deps:stretchイメージでlibssl-devがインストールされる
  2. ruby:2.3.8イメージでlibssl1.0-devがインストールされる。また、libssl-devが消える。
  3. ruby:2.3.8イメージでrubyインストール後にlibssl1.0-devがアンインストールされる。
  4. 結果、libssl系は全部消える。

ということですね。そのためにこちら側でlibssl-devをインストールする必要があったということです。

感想

個人的にはruby:2.3.8でrubyのインストール後に再度libssl-devをインストールしてほしいなとは思いましたが、親イメージのことを考慮するのは変だとか別の事情があるかもしれないとかだと思うのでまあDockerイメージの継承は難しいなと思いました。(雑

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む