- 投稿日:2019-01-26T23:42:45+09:00
Daru::View でグラフを表示してみた
グラフを描くツール Daru::View
Rubyでグラフを表示するDaru::Viewというツールを使ってみた。Daru::Viewは2017年に始まった比較的新しいプロジェクトで開発者はインド、バンガロール在住のShekhar Prasad Rajakさん。現在も活発な開発が続いている。Google ChartsやHighchartsを使用している。
インストール
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, :lynxgooglechartsを指定する。ほかに highchart などが指定できる。
# Google Charts を指定する Daru::View.plotting_library = :googlechartsグラフを描く。オプション無しで折れ線グラフになる。
chart = Daru::View::Plot.new(lynx) chart.show_in_iruby
show_in_iruby
はつけず、IRuby側で判定するべきだと思うが、そうしない理由がなにかあるのかもしれない。面グラフ
オプション type: :area を指定。
chart = Daru::View::Plot.new(lynx, type: :area) chart.show_in_iruby棒グラフ
オプション type: :bar を指定。
chart = Daru::View::Plot.new(lynx, type: :bar, height: 600) chart.show_in_irubyグラフの色を変更する
chart = Daru::View::Plot.new(lynx, type: :bar, height: 600, colors: ["Green"]) chart.show_in_irubycolors: ["Green"] などとすると変更される。
タイトルや軸を入れる
chart = Daru::View::Plot.new(lynx, type: :area, title: "カナダの山猫の年間捕獲数", hAxis: {title: "年"}, vAxis: {title: "頭数"}, colors: ["Orange"], height: 360, ) chart.show_in_irubyバブルチャート
usarrests = RDatasets.load :datasets, :USArrests chart = Daru::View::Plot.new(usarrests, type: :bubble, width: 700, height: 700, colors: ['yellow', 'red'] ) chart.show_in_iruby散布図
cars = RDatasets.load :datasets, :cars chart = Daru::View::Plot.new(cars, type: :scatter, width: 600, height: 600 ) chart.show_in_iruby(続く)
- 投稿日:2019-01-26T22:58:02+09:00
Ruby on Rails チュートリアル 第11章 アカウント有効化(AcctionMailer Activation)やSendGridの使い方など
著者略歴
YUUKI
ポートフォリオサイト:Pooks
RailsTutorial2周目11 アカウントの有効化
現時点では、新規登録したユーザーは初めから全ての機能にアクセスできるようになっている。
本章では、アカウントを有効化するステップを新規登録の途中に差し込むことで、本当にそのメールアドレスの持ち主なのか、確認できるようにする。具体的には
①有効化トークンやダイジェストを関連付けておく
②有効化トークンを含めたリンクをユーザーにメールで送信
③ユーザーがそのリンクをクリックすると有効化このような仕組みで、メールアドレスの持ち主であることを証明させる。
第12章でも似たような仕組みを使って、ユーザーがパスワードを忘れた時にパスワードを再設定できる仕組みを実装する。
これらの機能ごとに新しいリソースを作成し、コントローラ/ルーティング/データベース以降/の例について、1つずつ学んでいく。
最後に、Railsの開発環境や本番環境からメールを実際に送信する方法についても学ぶ。
アカウントを有効化する段取りは、ユーザーログイン&ユーザーの記憶と似ている。
①ユーザーの初期状態を「有効化されていない(unactivated)」にする
②ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する
③有効化ダイジェストはDBに保存。有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
④ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、DB内に保存しておいた有効化ダイジェストと比較することで、トークンを認証する
⑤ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み(activated)」に変更する。都合の良いことに、今回実装するアカウント有効化やパスワード再設定の仕組みと、以前に実装したパスワードや記憶トークンの仕組みにはよく似た点が多いので、多くのアイデアを使い回すことができる。
例えば、
User.digest
やUser.new_token
、改良版のuser.authenticated?
メソッドなど検索キー、string、digest、authenticationごとの点
検索キー string digest authentication password password_digest authenticate(password) id remember_token remember_digest authenticated?(:remember, token) activation_token activation_digest authenticated?(:activation, token) 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-activation11.1.1 AccountActivationsコントローラ
UsersリソースやSessionsリソースのときと同様に、AccountActivationsリソースを作るために、
まずはAccountActivationsコントローラ
を生成する。$ rails g controller AccountActivationsここで、有効化のメールには
edit_account_activation_url(activation_token, ...)これは、editアクションへの名前付きルートが必要になるということ。
そこで、まずは名前付きルートを扱えるようにするため、ルーティングにアカウント有効化用のresources行を追加する。routes.rbRails.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?ユーザーを有効化した時の、変更後のデータモデルはこうなる
出典:図 11.1: Userモデルにユーザー有効化用の属性を追加する
次のマイグレーションをコマンドラインで実行し、データモデルを追加すると、3つの属性が新しく追加される。
rails g migration add_activation_to_users ¥次に、admin属性の時と同様に、
activated属性
のデフォルトの論理値をfalseにしておく。[timestamp]_add_admin_to_users.rbclass 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:migrateActiveトークンのコールバック
ユーザーが新しい登録を完了するためには必ずアカウントの有効化が必要になる。
その為、「有効化トークン」や「有効化ダイジェスト」はユーザーオブジェクトが作成される前に、生成されるようにしておく必要がある。メールアドレスを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.rbclass 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.rbUser.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.ymlmichael: 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 => nil3:6章で、
email.downcase!
を使いemail属性を変更する方法を学んだ。(破壊的メソッド)
このメソッドを使って、user.rbのdowncase_email
メソッドを改良してみる。user.rbdef 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.erbUser#account_activation <%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erbaccount_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.rb
とuser_mailer.rb
の2つも確認。この2つのファイルはメールの動きを設定する(モデルで言うコントローラみたいなもの)
application_mailer.rbclass ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' enduser_mailer.rbclass 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 endapplication_mailerでは、アプリケーション全体で共通のデフォルトのfromアドレスがある。
user_mailerではmail to:
にて宛先のメールアドレスを設定している。また、メールフォーマットに対応するメイラーレイアウトも使われている。
生成されるHTMLメイラーのレイアウトやテキストメイラーのレイアウトはapp/views/layouts
で確認できる。生成されたコードにはインスタンス変数
@greeting
も含まれている。このインスタンス変数は、丁度普通のビューでコントローラのインスタンス変数を利用できるのと同じように、メイラービューで利用できる。
最初に、生成されたテンプレートをカスタマイズして、実際に有効化メールで使えるようにする。
次に、ユーザーを含むインスタンス変数を作成してビューで使えるようにし、
user.email
にメールを送信する。
user_mailer.rb
では、mailにsubjectキーを引数として渡している。この値は、メールの件名にあたる。application_mailer.rbclass ApplicationMailer < ActionMailer::Base default from: 'noreply@example.com' layout 'mailer' enduser_mailer.rbclass 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.rbRails.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のユーザーはこの値を実際には持っていない(ダイジェストしかない)演習
1:Railsのプレビュー機能を使って、ブラウザから先ほどのメールを表示してみる。
Dateの欄にはどんな内容が表示されているか?アクセスした時刻が表示されている。
11.2.3 送信メールのテスト
最後に、このメールプレビューのテストも作成して、プレビューをダブルチェックできるようにする。
mailer作成時にテストも自動で生成されているので、これを利用すればテストの作成は簡単。user_mailer_test.rbrequire '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+/', '$#!*+@' #falseassert_matchメソッドを使って、名前・有効化トークン・エスケープ済みメールアドレスがメール本文に含まれているかどうかをテストする。
また、
CGI.escape(user.email)こうすることで、引数に取ったemailをエスケープ処理することができる。
user_mailer_test.rbrequire '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.rbconfig.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.rbdef 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.rbtest "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この状態で実際に新規ユーザーとして登録してみる。
サーバーログ(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? => false11.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.rbdef 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 enduser_test.rbtest "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.rbclass 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を貼り付けて有効化ページにアクセスする。
この状態ではユーザーのログイン方法を変更していないのでこれでは何の意味もない。
ということで、ユーザーの有効化を行う為に、
ユーザーが有効である場合のみログインできるようにログイン方法を変更する必要がある。これを行うには、
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演習
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 => trueOK。
有効化に半日格闘した
何回やっても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.rbrequire '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で暗号化し、有効化ダイジェスト属性に代入 endusers_controller.rbdef 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 endaccount_activations_controller.rbclass 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) end11 tests, 15 assertions, 0 failures, 0 errors, 0 skips2:現在は
/users
のユーザーindexページを開くと全てのユーザーが表示され、/users/:id
のようにidと指定すると個別のユーザーを表示できる。しかし、非有効ユーザーは表示する意味がないので、その動作を
users_controller.rb
で変更する。users_controller.rbdef 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ヘリダイレクト end3:ここまでの演習問題で変更したコードをテストするために、/usersと/users/:idの両方に対する統合テストを作成する。
fixtureに非有効化ユーザーを作成
users.ymlnon_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.rbtest "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 end11.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_name
とpassword
のハッシュに実際の値を記入しないことソースコードに直接機密情報を書き込むのは危険。
そのような情報は環境変数に記述し、そこからアプリケーションに読み込む必要がある。今回の場合、そうした変数は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を確認したところ、スパムアカウント扱いされてアカウントが停止されてしまいました。
処理済みとなっていますが、本来なら送信済みになる筈です。
仕方なく、heroku logsからアカウント有効化URLをクリック。
URLをクリックする。
有効化完了。
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 => trueOK。
単語集
- バインド
関連付けるという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への問い合わせを一回にまとめることができるメソッド。
- 投稿日:2019-01-26T22:29:10+09:00
[Rails5.2 ActionCable]シンプルなチャットアプリを作ってみた!!
はじめに
この記事では、Rails5以降の機能である
Action Cable
をもちいて、双方向リアルタイム通信を実現させます。この記事の手順に沿えば非常にシンプルですが、チャットアプリが作れます。
1つのページ(チャット画面)でメッセージをやり取りするだけのシンプルなアプリです。
応用を利かすことができるようにシンプルさにこだわっています。環境
macOS
ChromeRails 5.2
Ruby 2.5.0gem
- slim-rails
- html2slim
- jquery-rails参考にさせていただいた記事
Githubにコードを公開しています
完成したアプリの動作
①
return
を押すと文章が投稿できる。
② リアルタイムでチャット画面が更新される。
③ データベースと連携してテキストが保存される。!注意!
*Room
モデルを作らないので、チャットルームを分けることはできません。
*User
モデルも作らないので、投稿者とメッセージの関連付けはできません。
*デザインは全くしていません。全体の工程
①ベースを作る
rails new
して、必要なgem
とview
controller
を揃えます。次の工程で作成するチャネルは何もしないとデフォルトの
coffeescript
で生成されます。それを避けたい場合は、下記ででプロジェクトを作成するか、
rails new AppName --skip-coffeeプロジェクト作成後に下記を削除して
bundle install
してください。# 20行目 gem 'coffee-rails', '~> 4.2'②チャネル作成
rails g channel room speakwebsocket(双方向リアルタイム通信)を実現してくれるファイルを生成する。
③サーバーとクライアントの設定をする
サーバーサイドは、
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 installjQuery を読み込みませます。
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.slimh1 チャットルームルーティング
app/config/routes.rbRails.application.routes.draw do root to: 'rooms#show' endここまでで、サーバーを起動すると
page#show
のページが表示されます。②チャネル作成
いよいよ
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.coffeeApp.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このように
Action Cable
をJSを使ってクライアントとサーバー間のデータをやり取りします。③クライアントとサーバーサイドの設定
フォームからデータを入力してアラートを出す
フォームの実装から取り掛かります。
先程作ったビューのviews/pages/show.html.slim
に設置します。**
form_with
を使うと余計なものも生成されるので、自作で設置していきます。**views/rooms/show.html.slimh1 チャットルーム 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.coffeeApp.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.rbclass 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キーを押すことで、ブラウザに入力データを受け取ってアラートを表示させることができます。
アラートではなく、ブラウザに表示させる
アラート出す処理を書いていたクライアントサイドを今度は、ブラウザに表示させるようにします。
まずは、ビューファイルをデータを表示できるように編集します。
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()以上で、ブラウザ上にフォームから入力したデータを出力することができます。
データベースとは連携していないので、ページを更新すれば消えてしまいます。
次の項目から、データを保存できるようにデータベースと連携させていきます。④データベースと連携させる
入力したデータを保存させるにはまずは、当たり前ですがモデルが必要です。早速、モデルを作ります。
モデル名
Message
カラムにはMessage
の中身を表すカラムcontent
をデータ型をtext
として作成します。rails g model Message content:textrails db:migrateビューの表示させるようにコントローラーに処理を書く
app/controllers/rooms_controller.rbclass 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.slimh1 チャットルーム 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.rbclass 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.rbclass MessageBroadcastJob < ApplicationJob queue_as :default def perform(*args) # Do something later end endここからブロードキャストの処理を追加していきます。
先程、作成したメッセージのテンプレートをチャネルに送信されるように処理を書きます。app/jobs/message_broadcast_job.rbclass 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.rbclass Message < ApplicationRecord validates :content, presence: true #これにより無記入投稿とエンター長押しの連続投稿の2つが同時に防げる after_create_commit {MessageBroadcastJob.perform_later self} end非同期に受け取ったデータを表示させる
クライアントサイドのファイルを書き換える。
app/assets/javascripts/channels/room.coffeereceived: (data) -> $('#messages_index').append data['message']ついに完成
フォームに入力したデータが非同期に双方向に表示されていることがわかるはずです。
最後に
基本的な
Action Cable
の使い方についてチャットアプリを作成しながら学んでみました。
間違いの指摘、効率の良い書き方があれば、コメントお待ちしております。最後まで読んでくださりありがとうございました。
- 投稿日:2019-01-26T20:42:07+09:00
cloud9 と GitHub を使ってRailsアプリを開発する際の下準備
内容
cloud9 と GitHub を使ってRailsアプリを開発する際の下準備
なぜcloud9上でRailsアプリを開発するのか?
A. 従来はローカル環境(Mac)とGitHubを利用してアプリ開発をしていた。
しかし、ある時からnokogiri
がインストールできないためローカルでの開発が困難に。「Rubyの開発をするにはcloud9でやったほうがいい」とのアドバイスをいただいたので、cloud9とGitHubで開発することにした。
具体的な下準備
cloud9 に git をインストール
command-on-cloud9gem intall gitバージョンを指定してRailsのインストール
command-on-cloud9gem install rails -v 5.1.6cloud9からGitHubにUP
command-on-cloud9git init git add . git commit -m "Write your comment here." git remote add origin [your GitHub repository] git push -u origin master
- 投稿日:2019-01-26T17:56:44+09:00
RubyでAES暗号化
はじめに
RubyでAES暗号化処理を実装する際のメモ。
毎回違う暗号が生成されるので安心。
"暗号化"の対義語は"復号"なのか"復号化"なのかは人によって結構わかれるみたいですね。
個人的には"復号"派です。事前準備
特に必要なし。
コード
暗号化
encrypt.rbrequire '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.rbrequire '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
- 投稿日:2019-01-26T17:35:04+09:00
ストロングパラメーターを設定する
ログイン画面の実装をする際に入力する値に対してストロングパラメーターを実装する方法を紹介します。
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 ... endcreateメゾットの中を説明していきます。
まず、ここでは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: "ログインに成功しました"
- 投稿日:2019-01-26T16:27:11+09:00
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 carloExampleを動かす
Carloがインストールできたら、まずはJavaScriptでアプリを作れる状態を作りましょう。https://github.com/GoogleChromeLabs/carlo にあるようにexample.jsとexample.htmlを用意します。
example.jsconst 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/ovtoのexamples/static/以下をコピーして、
bundle install
とrake
を実行してapp.jsを作ります。$ git clone https://github.com/yhara/ovto.git $ cp ovto/example/static/* . $ bundle install $ rakeexample.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で実行すると、以下のようになりました。
あとはpkgとかでexeにすれば単体で配布できるはずです。
これで、摂氏と華氏を変換するアプリが作れました…と言いたいところですが、2台のMacBookで試した結果、片方ではうまく動きませんでした (inputタグにフォーカスしてキーを叩いても文字が入力できなかった。app.jsを外してただのinputタグを入れても同じだったので、Opalは関係なさそうですが…)
まとめ
今回はCarloを使ってOpalでデスクトップアプリを作る方法を解説しました。こうやってJavaScript関連の資産をRubyで流用できるのがOpalの面白いところですね。
- 投稿日:2019-01-26T15:32:21+09:00
小さく薄く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/yarnGemfile差分
また、作成されるGemfileとデフォルトのGemfile(
Gemfile.default
)の差分は下記となります。デフォルトのGemfileと比べると、
sass-rails
,coffee-rails
から始まってweb-console
, またtestのcapybara
,selenium-webdrive
に至るまでViewに関するgem一式が削除されていることがわかりますね。git diff --no-index -U10 Gemfile.default Gemfilediff --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]参考リンク
- 投稿日:2019-01-26T12:46:20+09:00
知識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を起動する
dockerfile
とdocker-compose
を使ってコマンド一つでRails serverを起動する
dockerfile
を作成するdocker-compose.yml
を作成し、起動するRailsをローカル環境で起動する
これについては各所でがっつり解説されているので、ここでは流れ・用語の説明と参考資料の紹介にとどめます。
なお、この章の目的はRubyにまつわる各用語とRails serverの起動までの流れについて確認することが目的なので、既にバッチリの方は飛ばしてください。
参考資料:Ruby初学者のRuby On Rails 環境構築【Mac】ローカル環境にRubyを導入する
流れ
Homebrewの導入(更新)-> rbenvの導入 -> Rubyの導入ざっくりHomebrew解説
- MacOSにおけるパッケージ管理ツール
- Rubyのためだけのものではない
- 参考資料:homebrewとは何者か。仕組みについて調べてみた
ざっくりrbenv解説
- Rubyのバージョンを管理するためのツール
- Rubyのバージョン管理をrbenvで、rbenvのバージョン管理をHomebrewで行う(Homebrewは自分でやる)
- 参考資料:rbenvとは?(rbenvを利用したRubyのインストール)
ローカル環境にRailsを導入する
流れ
Bundlerの導入 -> BundlerでGemfileの作成 -> Gemfileの編集 -> Gemの取得(Railsの導入)ざっくりGem解説
- RubyGemsが公開しているライブラリ
- 要はRubyの開発で使える便利なツール群のこと
- RailsもGemの一つ
- 参考資料:Ruby on Rails 初心者必見!パッケージ管理ツール『gem』を徹底解説
ざっくりBundler解説
- Gemの管理ツール
Gemfile
から利用するGemの情報を読み取り、ライブラリを自動で構築してくれるすごいヤツ- この記事では
$ bundle init
と$ bundle install
さえ覚えておけばOK- 参考資料:Bundlerの使い方
- 参考資料:bundle install と bundle updateの違いについて
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.0Rubyのバージョンを示しています。Docker Hubでも確認できますが、イメージ本体にも情報がしっかり記されていますね。
このバージョンは重要なので覚えておきましょう。あるいはイメージをpull
する段階で指定してあげてもいいでしょう。BUNDLE_PATH=/usr/local/bundleBundlerのパスが指定されています。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 serverhttp://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
という名前でファイルを作成し、編集していきます。
まずは使用したイメージの情報です。dockerfileFROM ruby:2.6.0
:2.6.0
はバージョン情報です。指定しなければ最新版が自動で選択されますが、固定させた方がいいでしょう。
これはRubyのバージョンと同じなので、もし違うバージョンからRailsプロジェクトを作成したならそのバージョンに合わせてください。次にポートの開放とボリューム指定ですが、
dockerfile
ではホストPCのディレクトリを指定することはできません(後でdocker-compose.yml
で指定します)。よって、ここではポートの開放だけを行います。dockerfileFROM ruby:2.6.0 EXPOSE 3000ここでポートの開放を行なっても、
run
コマンドやdocker-compose.yml
で指定しなければ直接接続できないので注意してください。次はコンテナ内でRailsプロジェクト作成ですが、これは後でボリューム指定により共有する予定なので必要ありません。
ですが、Gemfile
が無くてはGemのインストールができず、rails
コマンドが実行できません。
そこで、Gemfile
だけをコンテナ内にコピーしてインストールすることにします。dockerfileFROM 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の起動です。dockerfileFROM 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 serverRailse sever の起動が
RUN
ではなくCMD
なのは、コンテナの立ち上げが完了してから実行してほしいからです。
もしRUN
で書いてしまうとそこで動作が止まってしまい、いつまでもコンテナが立ち上がらないということになってしまいます。では、これでうまく動作するかどうか確認してみましょう。
dockerfileからイメージの作成と起動$ docker build .エラーが出ずにRails serverの起動までできればOKです。
docker-compose.yml を作成し、起動する
では、先ほど作成した
dockerfile
を使ってdocker-compose.yml
を作っていきましょう。
先ほどと同じく、Railsプロジェクトのディレクトリに作成し、以下の通りに編集してください。docker-compose.ymlapp: build: . ports: - '3000:3000' volumes: - .:/usr/src/project_nameapp: …… アプリケーションの名前。今回はappとした。 build: …… docker-composeのパス指定。今回は同一ディレクトリにある。 ports: …… 開放するポートの指定。 volumes: …… ボリュームの指定。これで完了です。では起動してみましょう。
docker-composeからの起動$ docker-compose up -dhttp://localhost:3000/に接続して、いつものアレが表示されたらOKです!
ちなみに、ホストPCのディレクトリを参照しているため、ファイルを編集すると即座に反映されます。Gemfile
と同じですね。おわりに
というわけで、起動できました。とりあえず動いたので満足。
- 投稿日:2019-01-26T01:10:26+09:00
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.yml
のbefore_script
にインストール処理を書いて実行したらうまくいきました。原因分析
ruby:2.2.4
のイメージを使用していたときはこのようなことが起こらなかったたため、なぜ2.3.8
だと起こるのかが気になったので原因を調べました。イメージ単位で結果が異なったので原因はDockerの
ruby
イメージの内容の違いだと思いました。残念ながら2.2.4
のイメージはDockerfile
が簡単には見つけられそうになかったため2.3.8
のDockerfile
から見始めました。気になる部分がありました。既に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
が消えることがわかりました。つまりは
buildpack-deps:stretch
イメージでlibssl-dev
がインストールされるruby:2.3.8
イメージでlibssl1.0-dev
がインストールされる。また、libssl-dev
が消える。ruby:2.3.8
イメージでrubyインストール後にlibssl1.0-dev
がアンインストールされる。- 結果、libssl系は全部消える。
ということですね。そのためにこちら側で
libssl-dev
をインストールする必要があったということです。感想
個人的には
ruby:2.3.8
でrubyのインストール後に再度libssl-dev
をインストールしてほしいなとは思いましたが、親イメージのことを考慮するのは変だとか別の事情があるかもしれないとかだと思うのでまあDockerイメージの継承は難しいなと思いました。(雑参考リンク