20200320のRailsに関する記事は28件です。

breadcrumbs_on_railsを使ったパンくず実装 その② オリジナルviewを使おう

パンくず実装の続きです。
といっても備忘録としての記事ですので、ほぼほぼサイトを参照してやったことを思い返すつもりで書いています。

ちなみに今回は、lib直下をいじりますので、少し習ったことがないことをしました。
ではでは、やっていきましょうか

まずは、custom builder を作りましょう。
といってもこの名称はなんでも良いです。
ただし、ほぼほぼコードの直打ちで作りますよ。

/lib/custom_breadcrumbs_builder.rb
class CustomBreadcrumbsBuilder < BreadcrumbsOnRails::Breadcrumbs::Builder
  def render
    @context.render '/shared/breadcrumbs', elements: @elements
  end
end

これは、views/sharedフォルダにオリジナルのviewを呼びにいっているという記述です。
render形式のため、elements:@elementsの要素をbreadcrumbs.html.hamlへ送っています。
shardフォルダにrenderさせるファイルを纏めているので、そこを呼んでいますが、別に場所はどこでも良いです。

/view/breadcrumbs.html.haml
- if elements.present?
  %ul.l-gretel-brock
    - elements.each do |element|
      %li.l-gretel-inline
        - if element.path.present?
          = link_to element.name ,element.path, class: 'l-gretel-link u-deco-none'
          = fa_icon 'chevron-right', class: 'l-gretel-icon'
        - else
          = element.name
    = elements.last.name

と書きますわね。CSSは適当煮付けてくださいな。
(gretelって名称使ってるけどむしろわかりづらいような・・・)

そして、使いたいところに、以下の一文を追加する。

test.html.haml
  = render_breadcrumbs builder: ::CustomBreadcrumbsBuilder

最後に、config/application.rbに以下の一文を追加しよう。

config/application.rb
module Test
  class Application < Rails::Application
    config.autoload_paths += %W(#{config.root}/lib) #ここの一文だけだよ!
end

これはローカル上で、lib直下にあるファイルを呼びに行きますよということです。

ここまでできれば一安心、早速みてみよう!
いかのように表示されました。
fa_iconが良い感じです。

Screenshot from Gyazo

ここまでやってみてダメだった人は、rails sして再起動、もしくはbandle installを試してみよう。
私は意外と忘れるアプリケーションサーバーの再起動は盲点でした。

あ、あと本番環境は、以下の一文追加必要ですので、ご注意ください。
いくつか文章探しましたが、これでやっと通った感じです。

config/environments/production.rb
Rails.application.configure do
###いっぱい文章ありましたが省略
  config.eager_load_paths += %W( #{config.root}/lib )
end

これで、デプロイしても問題ないはず!

試してみてください。
ちなみに参考した記事は以下の通り、私もtech:expert生です。
https://pg-happy.jp/rails-pankuzu-list.html

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

【Rails】bundler-auditの使い方

bundler-auditとは

bundler-auditはプロジェクトで利用しているGemの脆弱性の有無をチェックしてくれるGemです。
システムの脆弱性チェックの一つとして利用できますね。

検証した環境

  • Ruby 2.7.0
  • bundler-audit 0.6.1

準備

bundle install

Gemfileに以下を追加してbundle install

Gemfile
group :development do
  gem 'bundler-audit'
end
$ bundle install

脆弱性チェックに使用するDBを更新する

脆弱性チェックを実行する前に、bundler-auditが使用するDBの更新を行いましょう。

$ bundle exec bundler-audit update

このコマンドでは、実際には$HOME/.local/shareディレクトリ配下にrubysec/ruby-advisory-dbgit cloneしているようです。

脆弱性の存在するGemをチェックする

実際にbundler-auditを実行してGemの脆弱性をチェックしてみましょう。
以下のコマンドで実行できます。
脆弱性が見つからなかった場合はNo vulnerabilities foundと出力されます。

$ bundle exec bundler-audit
No vulnerabilities found

DBの更新と脆弱性チェックを同時に実行する

以下のコマンドで脆弱性チェックに使用するDBの更新と脆弱性チェックを同時に実行できます。
DBの更新忘れを防ぐためにも、基本的には以下のコマンドを利用するほうがいいでしょう。

$ bundle exec bundler-audit check --update

実際に脆弱性があるGemをインストールして検証してみる

bundler-auditの出力を確認するために、実際に脆弱性が存在するGemのインストール~脆弱性チェック~修正までやってみます。

脆弱性があるGemをインストール

実際に脆弱性が存在するGemをインストールしてみます。
ここでは試しにbootstrapv4.3.0をインストールしてみます。
Gemfileに以下を追記してbundle install

Gemfile
gem 'bootstrap', '4.3.0'
$ bundle install

脆弱性をチェックする

bundler-auditを実行してみます。

$ bundle exec bundler-audit
Name: bootstrap
Version: 4.3.0
Advisory: CVE-2019-8331
Criticality: Medium
URL: https://blog.getbootstrap.com/2019/02/13/bootstrap-4-3-1-and-3-4-1/
Title: XSS vulnerability in bootstrap
Solution: upgrade to >= 4.3.1

Vulnerabilities found!

脆弱性が見つかったGemとその内容、解決するバージョンまでわかりやすく出力してくれます。

脆弱性を無視する

場合によってはbundler-auditによって見つかった脆弱性を無視したいこともあるかと思います。
そういうときは--ignoreオプションに無視したいAdvisoryを指定しましょう。

$ bundle exec bundler-audit --ignore CVE-2019-8331
No vulnerabilities found

脆弱性のあるGemのバージョンを上げる

先ほどbundler-auditで出力された内容には

Solution: upgrade to >= 4.3.1

と書いてあったので、それに従ってGemfileを修正してみます。
(※実際にはGemfileにバージョンを指定していない場合の方が多いと思うので、その場合はbundle updateコマンド等を利用してください。)

Gemfile
-gem 'bootstrap', '4.3.0'
+gem 'bootstrap', '4.3.1'
$ bundle install

bootstrapのバージョンを上げたのでもう一度bundler-auditを実行してみます。

$ bundle exec bundler-audit
No vulnerabilities found

警告が出なくなりました!

参考

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

TDD(テスト駆動開発)って??

TDDについてのアウトプットを行います。

TDDとは?

簡単に言うと、

①テストコード ②実装、リファクタリングの順で多なっていく事

なんで、先にテストコードを書くん?

実装と修正のサイクルを定期的に行うため

その必要性は、常にエラーを意識しながら実装していくためです。
結果、その都度エラーを発見することができると言うことですね。

どんな方法で行うん?

①失敗のコードを書く 

②成功のコードを書く 

③リファクタリング

失敗のコードを書く意味って何なのでしょうか?

『エラーになると思っていたコードがエラーにならない!?』ってな事が起きないようにするためですね。

例えが合っているか怪しいですが、
(例)1+1=4とエラーコードを書きました。しかし、返ってきた答えがエラーではなく『成功』してしまう。ってな異常事態を防ぐためですね。

成功のコードを書くときは?

この時に注意することは、最低限のコードでいいからテストを成功させる事。
消して整ったコードを書いて成功させなければならないと言うわけではないと言う事です。

終わりに

最低限のアウトプットではありますが、ここからアップデートして行きます。

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

bundle install でNokorigiインストール時に「Gem::Ext::BuildError: ERROR: Failed to build gem native extension.」エラーが出た場合の対処方法

bundle installでnokogiriインストール時に下記のようなエラー

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

が出てしまった場合

gem install時であれば --use-system-libraries オプションを付けて再実行してみるところですがbundlerの場合はどうすればいいかというと

bundle config --local build.nokogiri --use-system-libraries

というような感じでコマンドを実行します。

--localオプションはapp/.bundle/configへ設定を書きます。
--globalオプションは~/.bundle/configに書くようです。

まあよほどの事情がない限り、プロジェクトごとの.bundle/configに書いておくほうが良いと思います。その他、環境変数にも定義できます。

bundle configの優先順位

bundle configですが、mysqlなどと同様、複数の箇所に設定を保持しておけます。用途によって適切な場所に設定を書いておくのがいいと思いますが、bundleコマンドのhelpによると

DESCRIPTION
       This command allows you to interact with Bundler´s configuration system.

       Bundler loads configuration settings in this order:

       1.  Local config (app/.bundle/config)

       2.  Environmental variables (ENV)

       3.  Global config (~/.bundle/config)

       4.  Bundler default config

という優先順位で参照されます。

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

インクリメンタルサーチ

インクリメンタルサーチとは

文字の入力のたび、自動的に検索が行われる検索方法のことです。

インデックスでデータの検索を高速化

テーブル内で検索が頻繁に行われるカラムにインデックスを設定することで検索の高速化します。
今回はtweetsテーブルのtextカラムにインデックスを貼ることで、データの検索を高速化します。
インデックスはデータベースの機能の一つで、テーブル内のデータ検索を高速化することができます。インデックスはカラムに対して設定することができ、設定したカラムでの検索が高速になります。
ちなみにインデックスを設定することを、「インデックスを貼る」と言います。

インデックスを設定

textカラムに対するインデックスを設定するので、tweetsテーブルに対してインデックスを貼るためのマイグレーションファイルを作成します。
ターミナル

$ rails g migration AddIndexToTweets

次に作成したマイグレーションファイルを編集します。

2020xxxxxxxxxxx_add_index_to_tweets.rb
class AddIndexToTweets < ActiveRecord::Migration
  def change
    add_index :tweets, :text, length: 32
  end
end

ターミナル

rails db:migrate

マイグレーションを実行したらtweetsテーブルのtextカラムに対してインデックスが設定できます。
次からはインクリメンタルサーチの実装をしていきます。

ルーティングなどAPI側の準備

アクションの中でHTMLとJSONなどのフォーマット毎に条件分岐する記述を追加します。
フォーマット毎に処理を分けるには、respond_toを使用します。
【例】

app/controlers/tweets_controller.rb
〜省略〜
def index
    @tweets = Tweet.includes(:user).order("created_at DESC").page(params[:page]).per(5)
  end

〜省略〜

  def search
    @tweets = Tweet.search(params[:keyword])
    respond_to do |format|
      format.html
      format.json
    end
  end
〜省略〜

投稿情報を取得したら、jbuilderを使ってJavaScript側に返します。
検索結果は、複数の投稿情報を表示させ、格納された配列を返すようなjbuilderの記述します。
app/views/tweets/に「search.json.jbuilder」ファイルを作成します。
【例】作成したsearch.json.jbuilderを編集します。今回はすでにtweetカラムに対してtextやimageなど設定済みです

app/views/tweets/search.json.jbuilder
json.array! @tweets do |tweet|
  json.id tweet.id
  json.text tweet.text
  json.image tweet.image
  json.user_id tweet.user_id
  json.nickname tweet.user.nickname
  json.user_sign_in current_user
end

JSON形式のデータを配列で返したい場合は、array!を使用します。

jbuilder:array!メソッド

jbuilderという拡張子を持つテンプレートでは、JSONという名前のJbuilderオブジェクトが自動的に利用できるようになります。
Jbuilderオブジェクトは、JSON形式の配列で返したい場合はarray!を使用します。
【例】JavaScript側に送る配列

[{
 id: 1,
 image: "https://~.jpg",
 nickname: "たかし",
 text: "プログラミングの初学者です",
 user_id: 1,
 user_sign_in:
    {created_at: "2020-03-19T01:23:45.000Z",
    email: "aaa@gmail.com",
    id: 1,
    nickname: "たかし",
    updated_at: "2020-03-19T01:23:45.000Z"}
}]

jbuilderを使用するとより少ない記述でJSON形式のデータを作ることができます。

テキストフィールドを作成

【例】

app/views/tweets/index.html.erb
<%= form_with(url: search_tweets_path, local: true, method: :get, class: "xxxx") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "yyyy" %>
  <%= form.submit "検索", class: "zzzz" %>
<% end %>
<div class="contents row">
  <% @tweets.each do |tweet| %>
    <%= render partial: "tweet", locals: { tweet: tweet } %>
  <% end %>
  <%= paginate(@tweets) %>
</div>

テキストフィールドが入力されるたびにイベントが発火できるように

文字を打ち込み終わって処理をさせたいときはkeyupメソッドを使用します。
keyupメソッドはjQueryオブジェクトで指定した要素にフォーカスがあるとき、キーが離されたら引数のfunctionを実行します。引数にfunctionを設定しない場合は、要素に設定されたfunctionを実行します。
app/assets/javascripts/に「search.js」ファイルを作成します。

'''app/assets/javascripts/search.js
$(function() {
$(".search-input").on("keyup", function() {
var input = $(".yyyy").val();
});
});
```
今回の実装で使用するテキストフィールドは先ほどの部分になります。

app/views/tweets/index.html.erb
<%= form_with(url: search_tweets_path, local: true, method: :get, class: "xxxx") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "yyyy" %>
  <%= form.submit "検索", class: "zzzz" %>
<% end %>

テキストフィールドのclass名はyyyyです。
クラス名が".yyyy”の部分のテキストフィールドがkeyupしたら、テキストフィールドの文字を取得して変数inputに代入します。
フォームの値を取得するときはval()を使います。

イベント時に非同期通信できるように

app/assets/javascripts/search.js
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({   #追加〜
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })   #〜追加
  });
});

HTTPメソッドはGETで、/tweets/searchのURLに{ keyword: input }を送信します。サーバーから値を返す際は、JSONになります。
ターミナルでrails routesを実行すると、上記のリクエストによって、tweets_controller.rbのsearchアクションが動きます。

app/controllers/tweets_controller.rb
〜省略〜
respond_to do |format|
      format.html
      format.json
    end
〜省略〜

$.ajaxのdataTypeでJSONを指定しているので、サーバーはJSON形式で値を返します。
普段のレスポンス(html形式のレスポンス)ではtweets_controller.rbのsearchアクションが実行されたら、app/views/tweets/search.html.erbが読まれますが、JSON形式の場合は、app/views/tweets/search.json.jbuilderが読まれます。

非同期通信の結果を得て、HTMLを作成

非同期通信の結果をdoneの関数の引数から受取り、ビューに追加するためのHTMLを作成します。

app/assets/javascripts/search.js
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {   #追加〜
      $(".contents.row").empty();
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })   #〜追加
  });
});
.done(function(tweets) {
      $(".contents.row").empty();
   })

インクリメンタルサーチでは、検索をする直前に投稿情報のリスト(テキストや画像)を削除してあげる必要があります。

app/views/tweets/index.html.erb
〜省略〜
<div class="contents row">
  <% @tweets.each do |tweet| %>
    <%= render partial: "tweet", locals: { tweet: tweet } %>
  <% end %>
  <%= paginate(@tweets) %>
</div>

検索結果欄で出力されている投稿情報をemptyメソッドを使用して削除します。

emptyメソッド

指定したDOM要素の子要素のみを削除するメソッドです。
投稿情報をすべて削除したいので、

の要素を取得します。
なのでsearch.jsの$(".contents.row").empty();で投稿の情報を削除できます。
app/assets/javascripts/search.js
if (tweets.length !== 0) {
      tweets.forEach(function(tweet){
        appendTweet(tweet);
      });
    }
    else {
      appendErrMsgToHTML("一致するツイートがありません");
    }

上記の関数は、jbuilderから得られた値を投稿情報のリストに追加するものです。
tweetsが空ではない場合はtweets.length !== 0を記述します。
forEachメソッドを用いて、tweetsの中身の数だけappendTweet関数を呼び出します。

forEachメソッド

与えられた関数を配列に含まれる各要素に対して一度ずつ呼び出します。

tweetsが空の場合

”一致するツイートがありません”という引数を与え、appendErrMsgToHTML関数を呼び出します。
tweetsに投稿の情報が入っている場合のappendTweet関数を定義します。

【例】_tweet.html.erb
<div class="content_post" style="background-image: url(<%= tweet.image %>);">
  <div class="more">
    <span><%= image_tag 'arrow_top.png' %></span>
    <ul class="more_list">
      <li>
        <%= link_to "詳細", tweet_path(tweet.id), method: :get %>
      </li>
      <% if user_signed_in? && current_user.id == tweet.user_id %>
        <li>
          <%= link_to '編集', "/tweets/#{tweet.id}/edit", method: :get %>
        </li>
        <li>
          <%= link_to '削除', "/tweets/#{tweet.id}", method: :delete %>
        </li>
      <% end %>
    </ul>
  </div>
  <%= simple_format(tweet.text) %>
  <span class="name">
    <a href="/users/<%= tweet.user_id %>">
      <span>投稿者</span><%= tweet.user.nickname %>
    </a>
  </span>
</div>
app/assets/javascripts/search.
$(function() {

  var search_list = $(".contents.row");   #追加〜

  function appendTweet(tweet) {
    if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
      var current_user = `<li>
                            <a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
                          </li>
                          <li>
                            <a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
                          </li>`
    } else {
      var current_user = ""
    }

    var html = `<div class="content_post" style="background-image: url(${tweet.image});">
                  <div class="more">
                    <span><img src="/assets/arrow_top.png"></span>
                    <ul class="more_list">
                      <li>
                        <a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
                      </li>
                      ${current_user}
                    </ul>
                  </div>
                  <p>${tweet.text}</p><br>
                  <span class="name">
                    <a href="/users/${tweet.user_id}">
                      <span>投稿者</span>${tweet.nickname}
                    </a>
                  </span>
                </div>`
    search_list.append(html);
  }   #〜追加

  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      search_list.empty();   #追加
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
  });
});

上記の追加の部分は削除した投稿情報のhtmlをもう一度作成しています
違う部分は<%= %>で出力しているものをjbuilderで取得した値に変えています。
<%= %>で出力しているものは、jbulider取得した値を${}で出力することができます。
次に、tweetsに投稿の情報が入っていない場合のappendErrMsgToHTML関数を定義します。

app/assets/javascripts/search.js
$(function() {

  var search_list = $(".contents.row");

  function appendTweet(tweet) {
    if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
      var current_user = `<li>
                            <a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
                          </li>
                          <li>
                            <a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
                          </li>`
    } else {
      var current_user = ""
    }

    var html = `<div class="content_post" style="background-image: url(${tweet.image});">
                  <div class="more">
                    <span><img src="/assets/arrow_top.png"></span>
                    <ul class="more_list">
                      <li>
                        <a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
                      </li>
                      ${current_user}
                    </ul>
                  </div>
                  <p>${tweet.text}</p><br>
                  <span class="name">
                    <a href="/users/${tweet.user_id}">
                      <span>投稿者</span>${tweet.nickname}
                    </a>
                  </span>
                </div>`
    search_list.append(html);
   }

  function appendErrMsgToHTML(msg) {   #追加〜
    var html = `<div class='name'>${ msg }</div>`
    search_list.append(html);
  }   #〜追加

  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      search_list.empty();
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
  });
});

追加した部分もtweetsに値が入っている場合とやっていることが同じです。
コントローラーで検索をかけ、その投稿情報がなかった場合は、「一致するツイートがありません」という文字列を引数に渡してHTML要素を作成しビューに追加しています。

エラー時の処理

最後に、通信に失敗した場合の処理を実装します。
アラートで「投稿検索に失敗しました」と表示します。

app/assets/javascripts/search.js
$(function() {

  var search_list = $(".contents.row");

  function appendTweet(tweet) {
    if(tweet.user_sign_in && tweet.user_sign_in.id == tweet.user_id){
      var current_user = `<li>
                            <a href="/tweets/${tweet.id}/edit" data-method="get" >編集</a>
                          </li>
                          <li>
                            <a href="/tweets/${tweet.id}" data-method="delete" >削除</a>
                          </li>`
    } else {
      var current_user = ""
    }

    var html = `<div class="content_post" style="background-image: url(${tweet.image});">
                  <div class="more">
                    <span><img src="/assets/arrow_top.png"></span>
                    <ul class="more_list">
                      <li>
                        <a href="/tweets/${tweet.id}" data-method="get" >詳細</a>
                      </li>
                      ${current_user}
                    </ul>
                  </div>
                  <p>${tweet.text}</p><br>
                  <span class="name">
                    <a href="/users/${tweet.user_id}">
                      <span>投稿者</span>${tweet.nickname}
                    </a>
                  </span>
                </div>`
    search_list.append(html);
  }

  function appendErrMsgToHTML(msg) {
    var html = `<div class='name'>${ msg }</div>`
    search_list.append(html);
  }

  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/tweets/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(tweets) {
      search_list.empty();
      if (tweets.length !== 0) {
        tweets.forEach(function(tweet){
          appendTweet(tweet);
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
    .fail(function() {   #追加〜
      alert('error');
    });   #〜追加
  });
});

サーバーエラーの場合、このfailの関数が呼ばれます。

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

超最低限のRailsアプリをテストする(モデル編)

この記事の基本的な方針

テストは重要です。
なぜなら、人間は不完全であるからです。
ではもし自分が完全であったら、テストは不要でしょうか?
いいえ。なぜなら他人にコードの妥当性を証明しなくてはならないからです。

テストの目的以下です。
 ①コードの妥当性を自分で確認すること
 ②コードの妥当性を他人に示すこと

ここでは別記事、超最低限のRailsアプリを丁寧に作る(もう一度きちんと復習して初心者を卒業しよう)で作ったアプリをテストします。

手を動かしながら読みたいようでしたら、以下でこの3画面アプリを手に入れてください。

Terminal
$ git clone https://github.com/annaPanda8170/minimum_rails_application.git
$ bundle install
$ bundle exec rake db:create
$ bundle exec rake db:migrate

基本解説はしません。手順のみ示します。

想定する読み手

既に一度Railsアプリをチュートリアルやスクール等で作ったことがある方を想定しております。
Mac使用で、パソコンの環境構築は完了していることが前提です。

具体的な手順

完成品GitHub

①登録条件確認

deviseでデフォルトで設定されたEmailとPasswordの制約を確認します。

見るべきは、(1)devise公式GitHub内のバリデーションに関するファイルと(2)config/initializers/devise.rbです。

(1)devise公式GitHub内のバリデーションに関するファイルには、「Passwordは空ではならない」、「Emailはユニークで空ではならない」とあります。

(2)config/initializers/devise.rbには

config/initializers/devise.rb
#省略
config.password_length = 6..128
#省略
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
#省略

とあります。

よって今回は、

Passwordは、
6文字から128文字である。
Emailは、
ユニークで かつ @の前後に@と空白以外が1文字以上ずつ」である。

を確認すべく、テストします。

②準備

Gemfile
#省略
group :development, :test do
#省略
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

group :development do
#省略
  gem 'spring-commands-rspec'
end
#省略

※spring-commands-rspecは起動時間を速くするためのものでなくても問題はありません。

Terminal
$ bundle install
$ rails g rspec:install
$ rails g rspec:model user
$ rails g factory_bot:model user
$ bundle exec spring binstub rspec

control + c
$ rails s
.rspec
--format documentation

※これでRspecの出力が読みやすくなるそうです。

ここで空っぽのまま一度起動してみます。

Terminal
$ bundle exec rspec
Output
#省略

Finished in 0.00255 seconds (files took 1.69 seconds to load)
1 example, 0 failures, 1 pending

※1個のテストに対して0個の失敗があり、1個保留ですという意味です。

③テスト構築

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  it "Passwordが6文字で、Emailが@が一つだけあり@の前後に@と空白以外が1文字ずつあれば登録できる"
  it "Passwordが5文字で登録できない"
  #パスワード文字数上限の方は省きます
  it "passwordとpassword_confirmationが異なっていると登録できない"
  it "Emailが@がないと登録できない"
  it "Emailが@が二つあると登録できない"
  it "Emailが途中に空白があると登録できない" 
  it "2人のユーザーについて、Emailがユニークであれば登録できる" 
  it "2人のユーザーについて、Emailがユニークでなければ登録できない" 
end
Terminal
$ bundle exec rspec
Output
2020-03-20 00:51:43 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead.

User
  Passwordが6文字で、Emailが@が一つだけあり@の前後に@と空白以外が1文字ずつあれば登録できる (PENDING: Not yet implemented)
  Passwordが5文字で登録できない (PENDING: Not yet implemented)
  passwordとpassword_confirmationが異なっていると登録できない (PENDING: Not yet implemented)
  Emailが@がないと登録できない (PENDING: Not yet implemented)
  Emailが@が二つあると登録できない (PENDING: Not yet implemented)
  Emailが途中に空白があると登録できない (PENDING: Not yet implemented)
  2人のユーザーについて、Emailがユニークであれば登録できる (PENDING: Not yet implemented)
  2人のユーザーについて、Emailがユニークでなければ登録できない (PENDING: Not yet implemented)

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User Passwordが6文字で、Emailが@が一つだけあり@の前後に@と空白以外が1文字ずつあれば登録できる
     # Not yet implemented
     # ./spec/models/user_spec.rb:4

  2) User Passwordが5文字で登録できない
     # Not yet implemented
     # ./spec/models/user_spec.rb:5

  3) User passwordとpassword_confirmationが異なっていると登録できない
     # Not yet implemented
     # ./spec/models/user_spec.rb:7

  4) User Emailが@がないと登録できない
     # Not yet implemented
     # ./spec/models/user_spec.rb:8

  5) User Emailが@が二つあると登録できない
     # Not yet implemented
     # ./spec/models/user_spec.rb:9

  6) User Emailが途中に空白があると登録できない
     # Not yet implemented
     # ./spec/models/user_spec.rb:10

  7) User 2人のユーザーについて、Emailがユニークであれば登録できる
     # Not yet implemented
     # ./spec/models/user_spec.rb:11

  8) User 2人のユーザーについて、Emailがユニークでなければ登録できない
     # Not yet implemented
     # ./spec/models/user_spec.rb:12


Finished in 0.00215 seconds (files took 2.4 seconds to load)
8 examples, 0 failures, 8 pending
spec/factories/users.rb
FactoryBot.define do
  factory :user do
    email {"a@a"}
    password {"111111"}
    password_confirmation {"111111"}
  end
end
spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  it "Passwordが6文字で、Emailが@が一つだけあり@の前後に@と空白以外が1文字ずつあれば登録できる" do
    expect(FactoryBot.build(:user)).to be_valid
  end
  it "Passwordが5文字で登録できない"
  #パスワード文字数上限の方は省きます
  it "passwordとpassword_confirmationが異なっていると登録できない"
  it "Emailが@がないと登録できない"
  it "Emailが@が二つあると登録できない"
  it "Emailが途中に空白があると登録できない" 
  it "2人のユーザーについて、Emailがユニークであれば登録できる" 
  it "2人のユーザーについて、Emailがユニークでなければ登録できない" 
end
Terminal
$ bundle exec rspec
Output
#省略

8 examples, 0 failures, 7 pending

エラー文は以下のように確認します。

Terminal
irb(main):001:0> user = User.new(email: "a@a", password: "11111", password_confirmation: "11111")
   (0.9ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
=> #<User id: nil, email: "a@a", created_at: nil, updated_at: nil>
irb(main):002:0> user.valid?
  User Exists (0.4ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'a@a' LIMIT 1
=> false
irb(main):003:0> user.errors
=> #<ActiveModel::Errors:0x00007fd9f12cc6f8 @base=#<User id: nil, email: "a@a", created_at: nil, updated_at: nil>, @messages={:password=>["is too short (minimum is 6 characters)"]}, @details={:password=>[{:error=>:too_short, :count=>6}]}>

④テスト本番

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  it "Passwordが6文字で、Emailが@が一つだけあり@の前後に@と空白以外が1文字ずつあれば登録できる" do
    expect(FactoryBot.build(:user)).to be_valid
  end
  it "Passwordが5文字で登録できない" do
    user = FactoryBot.build(:user, password: "11111", password_confirmation: "11111")
    user.valid?
    expect(user.errors[:password]).to include("is too short (minimum is 6 characters)")
  end
  #パスワード文字数上限の方は省きます
  it "passwordとpassword_confirmationが異なっていると登録できない" do
    user = FactoryBot.build(:user, password: "111111", password_confirmation: "211111")
    user.valid?
    expect(user.errors[:password_confirmation]).to include("doesn't match Password")
  end
  it "Emailが@がないと登録できない" do
    user = FactoryBot.build(:user, email: "aaa")
    user.valid?
    expect(user.errors[:email]).to include("is invalid")
  end
  it "Emailが@が二つあると登録できない" do
    user = FactoryBot.build(:user, email: "a@@a")
    user.valid?
    expect(user.errors[:email]).to include("is invalid")
  end
  it "Emailが途中に空白があると登録できない" do
    user = FactoryBot.build(:user, email: "a @a")
    user.valid?
    expect(user.errors[:email]).to include("is invalid")
  end
  it "2人のユーザーについて、Emailがユニークであれば登録できる" do
    FactoryBot.create(:user)
    expect(FactoryBot.build(:user, email: "b@b")).to be_valid
  end
  it "2人のユーザーについて、Emailがユニークでなければ登録できない" do
    FactoryBot.create(:user)
    user = FactoryBot.build(:user)
    user.valid?
    expect(user.errors[:email]).to include("has already been taken")
  end
end
Terminal
$ bundle exec rspec
Output
2020-03-20 02:01:54 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead.

User
  Passwordが6文字で、Emailが@が一つだけあり@の前後に@と空白以外が1文字ずつあれば登録できる
  Passwordが5文字で登録できない
  passwordとpassword_confirmationが異なっていると登録できない
  Emailが@がないと登録できない
  Emailが@が二つあると登録できない
  Emailが途中に空白があると登録できない
  2人のユーザーについて、Emailがユニークであれば登録できる
  2人のユーザーについて、Emailがユニークでなければ登録できない

Finished in 0.06768 seconds (files took 2.31 seconds to load)
8 examples, 0 failures

まとめ

もし、パスワード全パターンを試すようなことができれば完璧なテストですが、それはできません。
例えば6桁に限定したとしても、数字10種とアルファベット26文字の大文字と小文字で計算すると、
(10 + 26 + 26)^6 = 56,800,235,584
つまり500億パターン以上で、これを128桁まで考えると気が遠くなるパターンがあることがわかります。
そもそも全パターン試せるようなパスワードがあればパスワードとしての価値がありません。
物理的に全パターンを試すことが出来ない我々は、限界値の両端をうまくテストしなくてはなりません。

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

【Rails】スレッドのレス投稿機能

プログラミング初心者です。
Ruby on Railsで掲示板のスレッドにレスを紐付けて投稿していく機能を作成しました。
結構苦戦したので、備忘のため貼り付けます。

【スレッド(親、tree)controller】

  def show
    @tree = Tree.find(params[:id])
    @response = Response.new(:tree_id => params[:id]) #ここを投稿用に使う
    @responses = @tree.responses.all
  end

【レス(子、response)controller】

  def create
    @response = current_user.responses.new(response_params)

    if @response.save
      redirect_to tree_url(@response.tree_id), notice: "投稿「#{@response.text}」を登録しました"
    else
      render tree_url(@response.tree_id)
    end
  end

  private

  def response_params
    params.require(:response).permit(:text, :user_id, :tree_id)
  end

【スレッド(親、tree)viewのshow】

= form_with model: @response, url_for: { controller: :responses, action: :create }, local: true do |f|
  .form-group
    = f.label :text, "コメント"
    = f.text_field :text, class: "form-control", id: "response_text"
  = f.hidden_field :tree_id
  = f.submit "投稿", class: "btn btn-primary"

【routes.rb】

  post "/responses", to: "responses#create"
  resources :trees

感想

特にルーティングエラーが多く発生しました。肝となるコードは以下の2点です。

# 入力フォーム
url_for: { controller: :responses, action: :create }
# ルーティング
post "/responses", to: "responses#create"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoogleMapsAPIの経度緯度の取得について

前提

GoogleMapsAPIにより、GoogleMapが表示できていること。
検索した場所の保存ができる一連の流れ(コントローラ、データベース等)を作成していること。

やりたいこと

上記APIを使用し、webアプリの機能の一つとして実装したいことは下記のとおり。

①検索フォームに、店名や地名などの名称を入力し検索すると、地図上にマーカーが立つ。
②その場所を保存する。
【詳細】
別途用意した、保存フォームに検索フォームに入力した名称が自動入力される。そして①の検索の際、取得した緯度経度で場所の保存を行う。

環境

開発環境 AWScloud9
rails 5.1.6

①について

https://qiita.com/yoshi_yast/items/521c1f36306a180f45dd を参考にさせていただき、名称検索でマーカーを立てるところまで実装できた。
ちなみに、マーカーは、http://foonyan.sakura.ne.jp/wisteriahill/google_icons/index.html から使用した。

②について

①に追加する。

<script>
...
geocoder.geocode({
  address: place
}, function(results, status) {
  if (status == google.maps.GeocoderStatus.OK) {
    var bounds = new google.maps.LatLngBounds();
    for (var i in results) {
      if (results[0].geometry) {
...
//以降追加。id="lat"と"lng","name"をそれぞれ取得
        document.getElementById('lat').value=results[0].geometry.location.lat();
        document.getElementById('lng').value=results[0].geometry.location.lng();
        document.getElementById('name').value=place;

取得できた経度、緯度そして検索した名称(id=name)を保存フォームに渡す。

〇〇.html.erb
<%= form_for(place) do |f| %>
  <%= f.text_field :name, id:"name" %>
  <%= f.hidden_field :latitude, id:"lat" %>
  <%= f.hidden_field  :longitude, id:"lng" %>
  <%= f.submit %>
<% end %>

以上で取得から保存までができた。

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

[学習ログ]CVE-2020-5267について簡単に調べてみた[Rails]

tl;dr

  • CVE-2020-5267とは、バージョン6.0.2.2および5.2.4.2以前のActionViewに存在する脆弱性である。
    該当バージョンのActionViewにおけるjおよびescape_javascriptメソッドにXSSの脆弱性が存在する可能性がある。

  • railsの場合はGemfile中のrailsをバージョンアップすることで対策が可能(6.0.2.2 or 5.2.4.2)


昨日、Githubから依存関係の脆弱性を指摘する以下のようなアラートが通知された。

We found potential security vulnerabilities in your dependencies.
Only the owner of this repository can see this message.
Manage your notification settings or learn more about vulnerability alerts.

セキュリティアラートを確認すると、actionviewに関してアラートが出ていることが分かった。

Remediation
Upgrade actionview to version 5.2.4.2 or later. For example:
gem "actionview", ">= 5.2.4.2"
Always verify the validity and compatibility of suggestions with your codebase.

CVE-2020-5267とは?

CVE-2020-5267についてググると、どうやらXSSに関する脆弱性らしい(私の認識が間違っていたら申し訳ないです)。
以下、コピペ

CVE-2020-5267 Detail
Description
In ActionView before versions 6.0.2.2 and 5.2.4.2, there is a possible XSS vulnerability in ActionView's JavaScript literal escape helpers. Views that use the j or escape_javascript methods may be susceptible to XSS attacks. The issue is fixed in versions 6.0.2.2 and 5.2.4.2.

https://nvd.nist.gov/vuln/detail/CVE-2020-5267

There is a possible XSS vulnerability in ActionView's JavaScript literal
escape helpers. Views that use the j or escape_javascript methods
may be susceptible to XSS attacks.
Versions Affected: All.
Not affected: None.
Fixed Versions: 6.0.2.2, 5.2.4.2

https://www.openwall.com/lists/oss-security/2020/03/19/1

 補足:XSS(クロスサイトスクリプティング)とは?

クロスサイトスクリプティングとは、攻撃者の作成したスクリプトを脆弱性のある標的サイトのドメインの権限において閲覧者のブラウザで実行させる攻撃一般を指す。

https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0

CVE-2020-5267による影響

この脆弱性を利用した際のコードの一例も載せておきます

<script>let a = `<%= j unknown_input %>`</script>
<script>let a = `<%= escape_javascript unknown_input %>`</script>

CVE-2020-5267の対処方法(Railsの場合)

対処方法については二種類ある。

1.Railsのバージョンを更新する

冒頭で述べた通り、Railsのバージョンを6.0.2.4か5.2.4.2に更新することで、Railsに依存しているactionviewのバージョンを更新する。

一例
Gemfile

- gem 'rails' ~>"5.1.4.2"
+ gem 'rails' ~>"5.2.4.2"

https://weblog.rubyonrails.org/2020/3/19/Rails-6-0-2-2-and-5-2-4-2-has-been-released/

2.モンキーパッチをあてる

Railsのバージョンを更新できない場合は、以下のようなモンキーパッチをあてて対処する。

ActionView::Helpers::JavaScriptHelper::JS_ESCAPE_MAP.merge!(
  {
    "`" => "\\`",
    "$" => "\\$"
  }
)
module ActionView::Helpers::JavaScriptHelper
  alias :old_ej :escape_javascript
  alias :old_j :j
  def escape_javascript(javascript)
    javascript = javascript.to_s
    if javascript.empty?
      result = ""
    else
      result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"']|[`]|[$])/u, JS_ESCAPE_MAP)
    end
    javascript.html_safe? ? result.html_safe : result
  end
  alias :j :escape_javascript
end

https://github.com/rails/rails/security/advisories/GHSA-65cv-r6x7-79hv

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

Railsのtext_fieldにCSSをあてる方法

text_fieldにCSSってどうやってあてるんだっけ?

Railsアプリケーション作成時にフロント部分を作成していた時に

example.haml.html
.item__name
  商品名
  .name--input
    = f.text_field :name, placeholder: "40文字まで"

のようなhamlを作成していざscssを記述しようとしたときに
このtext_filedにどうやってCSSをあてるのかが
ふと考えると分からなかったんです。

同じようなnumber_fieldsubmitなどにも使えるやり方なので
是非覚えておいたほうがよいです。

そもそもtext_fieldで作られるHTMLは何なのか?

上の= f.text_field :name, placeholder: "40文字まで"で作られるHTMLを
chromeの検証で確認してみると
<input placeholder="40文字まで" type="text" name="item[name]" id=item_name>
というものが作成されているのがわかるかと思います。

text_fieldとかはRailsがviewを簡潔に記述するために用意してくれているヘルパーメソッドのため
簡単な記述で実際はこういうHTML文の作成もしてくれています。
この作成されたHTMLにあてるようにCSSを記述すればOKです。

今回はSCSSを使い記述しました。

CSSをあててみよう

example.scss
.item__name{
  input[type="text"]{
    width: 100%;
    height: 20px;
    font-size: 14px;
  }
  ::placeholder{
    padding: 5px 5px;
  }
}

と指定して記述すると、text_fieldにCSSをあてることができます。
placholderはCSSの擬似要素のため上記のような記述をする必要があります。

ヘルバーメソッドを使って記述した場合にどのようなHTML文が作成されているのかを確認すると解決できる部分でしたね。

参考先

rails フォームの大きさをcssで変更する

CSS: カスケーディングスタイルシート::placeholder

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

Railsチュートリアルメモ - 第12章

メモの目次記事はこちら

公式Railsチュートリアル第12章へのリンク

サマリ

  • パスワードの再設定
    • パスワード再設定メールの送信
    • トークンによる認証とパスワードの再設定

ポイント

  • パスワードリセット機能はUserモデルを拡張して使用するため、MVCのうち新たに作成するのはViewとControllerのみで良い
  • 処理のコントローラーのアクションがいろいろ登場するので、流れを整理しないと混乱する
sessions/new.html.erb
→password_resets_controller#new
→password_resets/new.html.erb
→password_resets_controller#create
→メール送信
→password_resets_controller#edit
→password_resets/edit.html.erb
→password_resets#update
  • @user.authenticated?(:reset, params[:id])でresetトークンとDBに保存されたダイジェストを照合する
  • パスワード再設定用メールを送信する際に、パスワードリセットトークンとダイジェストを生成する。
    • トークンはメール内のリンクに埋め込んでユーザーに送付する
    • ダイジェストはDBに保存し、リンクが開かれた際に両者を照合して本人であることを確認する
  • #edit時点では、メール内のリンクに埋め込んだURLパラメーターからメールアドレスを取得できるが、#updateは画面から呼び出すので、同じ方法でメールアドレスを取得することができない。そのため、edit.html.erbの中の隠しフィールドにメールアドレスを持たせて#updateに引き渡す。
    • <%= hidden_field_tag :email, @user.email %>

感想

  • 11章と内容的にはほぼ同じだったが、その分説明が少なくて少し混乱した
  • herokuでの動作は問題なかったが、ローカル動作時にパスワード再設定用リンクを開こうとすると画面にこのサイトは安全に接続できませんlocalhostから無効な応答が送信されました。と表示され、ログに以下が吐かれてしまった
2020-03-18 02:53:45 +0900: HTTP parse error, malformed request (): #<Puma::HttpParserError: Invalid HTTP format, parsing fails.>
  • 原因はメールリンクがhttpsになっているためで、httpでリンクを開くと画面が正常に表示された
    • localhostにhttpsで繋ぐ場合は少し追加の設定が必要 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Action Mailer

Action Mailer

Action MailerとはRuby on Railsに標準で組み込まれているメール送信機能です。
Railsからメールが送信されるようになります。

実装

Inquiryモデル作成

Action Mailerを実装するために、ユーザーの問い合わせ内容を保存するモデルを作成します。

$ bin/rails g model inquiry name:string message:string
~~ 省略 ~~
$ rails db:migrate

Inquiryモデルは氏名(name)、メッセージ(message)の属性を持ちます。

ApplicationMailerを継承するクラスを作成

ApplicationMailerを継承するクラスはbin/rails generate mailer 任意のメイラー名で作成できます。

$ bin/rails generate mailer inquiry
app/mailers/inquiry_mailer.rb
class InquiryMailer < ApplicationMailer
end

メール宛先、件名等作成

send_mailメソッドをInquiryMailerクラスに記載してメールを送信できるようにします。

app/mailers/inquiry_mailer.rb
def send_mail(inquiry)
  @inquiry = inquiry
  mail(
    from: 'system@example.com',
    to:   'manager@example.com',
    subject: 'お問い合わせ通知'
  )
end

それぞれ下記のように設定されています。
from : →  送信元のメールアドレス
to : → 送信先のメールアドレス
subject : → 件名
他には下記があります。
cc : → CCのメールアドレス
bcc : → BCCのメースアドレス

メール本文のレイアウト

メール本文のレイアウトを作成するためには命名規則に従ってerbファイルを作成します。
新規でapp/views/inquiry_mailer/send_mail.text.erbファイルを作成しメール内容を記載していきます。

app/views/inquiry_mailer/send_mail.text.erb
<%= @inquiry.name %> 様 から問い合わせがありました。
・お問い合わせ内容
<%= @inquiry.message %>

このようにerbのタグを使用してコードを記載することが可能です。

メールをプレビューで確認

メール本文のレイアウトを確認するためにAction Mailerにはプレビュー機能があります。

先ほどgenerateコマンドで生成したActionMailer::Previewを継承したinquiry_mailer_preview.rbがあるので、こちらにコーティングしていきます。

test/mailers/previews/inquiry_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/inquiry_mailer
class InquiryMailerPreview < ActionMailer::Preview
  def inquiry
     inquiry = Inquiry.new(name: "侍 太郎", message: "問い合わせメッセージ")

     InquiryMailer.send_mail(inquiry)
   end
end

実際に確認をしみましょう。
rails sでサーバを起動しhttp://ホスト名:3000/rails/mailers/inquiry_mailerにアクセスすると
image.png
このように表示されるので、・ inquiryをクリックすると
※inquiryのリンクは、InquiryMailerPreviewクラスに作成したinquiryメソッドに対応して表示されています。
image.png
が表示されて、メール本文のレイアウトを確認することができます。
こちらメールが送信されることはないので何度でも確認することができます。

メールサーバの設定

実際にメール送信を行うために設定をしていきます。
なお、Gmailでは2段階認証等があるためセキュリティの状態によっては送信できない可能性があります。

config/environments/development.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
 address:              'smtp.gmail.com',
 port:                  587,
 domain:               'gmail.com',
 user_name:            '<gmailのメールアドレス>',
 password:             '<gmailのパスワード>',
 authentication:       'plain',
 enable_starttls_auto:  true
}

上記のようにメールサーバの設定をコーディングします。

メール送信

メールを送信するためにdeliver_nowメソッドを使用する。
本記事のサンプルプログラムで実行するためには、InquiryMailer.send_mail(モデル変数).deliver_nowで実行します。

コントローラに設定すれば実行できますが今回はコンソールから実行します。
app/mailers/inquiry_mailer.rbto :from :を変更し実行します。

$ bin/rails c
    ~~ 省略 ~~ 
irb(main):001:0> inquiry = Inquiry.new(name: "侍 太郎", message: "問い合わせメッセージ")
irb(main):002:0> InquiryMailer.send_mail(inquiry).deliver_now

実行後に設定したメールアドレスにメールが送信されていれば成功です。

応用的なメール送信方法

HTML形式なレイアウトのメール送信

Action MailerでHTML形式のメールを送信するためには、app/views/メイラー名_mailer/メイラークラスのメソッド名.html.erbとファイル名を記載しメール本文もHTML形式で記載します。

app/views/inquiry_mailer/send_mail.html.erb:
<!DOCTYPE html>
<html>
  <head>
    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
  </head>
  <body>
    <h1><%= @inquiry.name %> 様 から問い合わせがありました。</h1>
    <p>・お問い合わせ内容</p>
    <p><%= @inquiry.message %></p>
  </body>
</html>

プレビュー機能でレイアウトを確認します。
image.png
HTML形式のメールレイアウトであることを確認できました。

また下記の2つのようにテキストメールとHTMLメールの2つが存在する場合はRailsはマルチパートメールを自動生成して送信します。
・app/views/メイラー名mailer/メイラークラスのメソッド名.text.erb
・app/views/メイラー名
mailer/メイラークラスのメソッド名.html.erb

マルチパートメールとはテキスト、HTMLメールの両方を送信してメールクライアント側で自動判定してメールを表示する仕組みです。

添付ファイル付きのメール送信

添付ファイルをメールに付ける機能を追加するには下記をメイラークラスに追記します。
attachments[添付ファイル名] = 添付するファイルデータ

app/mailers/inquiry_mailer.rb
class InquiryMailer < ApplicationMailer
  def send_mail(inquiry)
   @inquiry = inquiry
   # 追記
   attachments['sample.jpg'] = ..File.read(./tmp/sample.jpg')  
   mail(
     from: 'system@example.com',
     to:   'manager@example.com',
     subject: 'お問い合わせ通知'
   )
  end
end

画像ファイルにsample.jpgを添付しています。
image.png
プレビューではこのようになっています。

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

railsのサーバーの強制終了

railsのサーバーの強制終了について

railsを使って自宅で開発しているときに、

$ bin/rails s

を使ってローカルホストのサーバを立てる。

そのとき、間違って
サーバーを起動した状態のターミナルのタブを消してしまって、ctrl + c
での終了ができなくなってしまい、困ったという経験がある。

その時の対処法、すなわち強制終了の方法をメモしておく。

 kill コマンドを使って強制終了

実際に実験してみると、
まず、

$ bin/rails s

を打ってローカルのサーバを立ち上げる。

その後確認してほしいのは

プロジェクト名/tmp/pids/server.pid

のファイル。

実際に見てみると、

project/tmp/pids/server.pid
1    1159    #ここに何らかの数字が入っているはず
2

これはサーバーを立ち上げたときに生成される数字で、サーバーを終了させたときに自動で消去される。

この数字を使って、

$ kill 1159

とコマンドに打ち込めばサーバーは強制終了される。

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

Rails5 CSV import 一括ユーザー登録

Rails5でCSV読み込んで一括ユーザー登録機能作成

CSVの読み込みでちょっとつまずいたのでメモ。
ほんとーに初歩的なミスでした。
基本的にコチラを参考にさせてもらいました。
https://qiita.com/seitarooodayo/items/c9d6955a12ca0b1fd1d4

さて出てきたエラーはコチラ
ActiveRecord::RecordInvalid (Validation failed: Name can't be blank, Password can't be blank, Password can't be blank, Password is too short (minimum is 6 characters)):

よくあるバリデーションに引っかかったやつですね。
blankってことはnilになっちゃってると言うことか。
と言うことはcsvファイルが読み込まれてないのか?

Parameters: {"utf8"=>"✓", "authenticity_token"=>"lzZScpvbb+vbmrJaxYMW3SweQrA0ISM0ssFX9GKIIL1gU6EMWD3IutOb0x5QnaLQwVcW1hUnNMTnZSxTgsHAvw==", "file"=>#<ActionDispatch::Http::UploadedFile:0x00007fe4b41926e8 @tempfile=#<Tempfile:/tmp/RackMultipart20200320-11805-1j4vvvo.csv>, @original_filename="csvimporttest.csv", @content_type="text/csv", @headers="Content-Disposition: form-data; name=\"file\"; filename=\"csvimporttest.csv\"\r\nContent-Type: text/csv\r\n">, "commit"=>"ファイル読み込み登録"}
User Load (0.2ms) SELECT
users.* FROMusersWHEREusers.idIS NULL LIMIT 1

パラメータとしては渡っている。。
しかしSELECTusers.* FROMusersWHEREusers.idIS NULL
なんでだ、、binding.pryしてみよう。

11: def self.import(file)
12: CSV.foreach(file.path, headers: true) do |row|
13: # IDが見つかれば、レコードを呼び出し、見つかれなければ、新しく作成
14: user = find_by(id: row["id"]) || new
15: # CSVからデータを取得し、設定する
16: binding.pry
=> 17: user.attributes = row.to_hash.slice(*updatable_attributes)
18: # 保存する
19: user.save!
20: end
21: end

[1] pry(User)> puts user.attributes
{"id"=>nil, "name"=>nil, "firstchild"=>nil, "secondchild"=>nil, "thirdchild"=>nil, "password_digest"=>nil, "email"=>nil, "admin"=>false, "created_at"=>nil, "updated_at"=>nil}
=> nil
[2] pry(User)> puts user
=> nil
[3] pry(User)> puts row
Id
9
Name
シーエスブイ太郎1
Password
Csvtest1
password_confirmation
Csvtest1

あ、rowには値が入ってる。。
その先に値が渡ってない。
ん?IdとかNameとか大文字入ってるジャン。ひょっとして、、、あーやっぱり。
と言うことでMacbookのnumbersで作成したcsvファイルだったのですがデフォルトで入力値の頭文字が大文字になる仕様でした。。
気づかずそのまま使ってたのが原因でしたね。
初歩的な原因でしたけど、こう言う風に原因追求するのってプログラミングの面白さの一つですよね。
binding.pryはよく使うようにしてます。

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

【Rails】小数点以下の不要な0を制御する number_with_precision

小数点の表示で0の場合だけなくすという仕様で困ったことはありませんか?
具体的には以下のような仕様です。

1.000 => 1
1.100 => 1.1
1.110 => 1.11

解決方法

ActionView::Helpers::NumberHelperクラスのnumber_with_precision(optionの strip_insignificant_zeros: true)を使います。

number_with_precision([数値],precision: [有効にしたい小数点桁数], strip_insignificant_zeros: true)

precisionを省略した場合、デフォルトは3です。

コードで仕様説明

スクリーンショット 2020-03-20 8.37.10.png
小数点以下の不要な0が全て切捨てられています。
画像の[6]をみていただくとわかりますがprecisionが3のため小数点第3位の数字までしか表示されていません。

スクリーンショット 2020-03-20 8.46.51.png

画像の[7]はpricisionを4にしているため小数点第4位まで表示されています。

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

【Rails】小数点以下の不要な0を削除する number_with_precision

小数点の表示で0の場合だけなくすという仕様で困ったことはありませんか?
具体的には以下のような仕様です。

1.000 => 1
1.100 => 1.1
1.110 => 1.11

解決方法

ActionView::Helpers::NumberHelperクラスのnumber_with_precision(optionの strip_insignificant_zeros: true)を使います。

number_with_precision([数値],precision: [有効にしたい小数点桁数], strip_insignificant_zeros: true)

precisionを省略した場合、デフォルトは3です。

コードで仕様説明

スクリーンショット 2020-03-20 8.37.10.png
小数点以下の不要な0が全て切捨てられています。
画像の[6]をみていただくとわかりますがprecisionが3のため小数点第3位の数字までしか表示されていません。

スクリーンショット 2020-03-20 8.46.51.png

画像の[7]はpricisionを4にしているため小数点第4位まで表示されています。

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

Excon::Error::Socket: getaddrinfo: Name or service not known (SocketError)というエラー文の対応

環境

$ ruby -v
ruby 2.6.5
$ rails -v
Rails 5.2.4

Amazon Linux 2

状況

本番環境としてデプロイした後に、画像をCarriorwave,fogを通じてS3にアップロードしようとすると、エラーが発生。
エラー文は以下の2種類。

Excon::Error::Socket: getaddrinfo: Name or service not known (SocketError)
SocketError: getaddrinfo: Name or service not known

本件の原因

config/initializer/carrior_wave.rb
if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_provider = 'fog/aws'
    config.fog_credentials = {
      provider: 'AWS',
      region: 'ap-northeast-1a', # 問題箇所
      aws_access_key_id: 'アクセスキー',
      aws_secret_access_key: 'シークレットキー'
    }
    config.fog_directory = 'バケット名'
    config.cache_storage = :fog
  end
end

'ap-northeast-1a'なんてリージョンはなく、ap-northeast-1の間違いであった。

解決した対応

問題箇所のap-northeast-1aap-northeast-1にしてpumaを再起動した。

こんなしょうもないミスで2時間潰しました笑

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

[scss]hamlへの反映方法は2種類あった 備忘録

今までscssのhamlへの反映方法といったら
application.scss@import記述のみと思っていましたが。

実はそんなことをしなくてもコマンド入力で作成したscssには
自動的に反映される記述が入っているんです。
知りませんでした。。。
コメントアウトされ得た以下の3行があるとOKらしい

scss
// Place all the styles related to the reset controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

ですので、scssの反映方法を以下2種類をまとめたいと思います

コマンドで作成する方法

importいらないコマンド存在したー!!!

ターミナル
$ rails g assets 作成したいファイル名

以上です。
あとはscssに記述したものは自動的にhamlに反映されます。

手動でファイルを作成する方法

右クリックでファイル作成をした場合scssにimport記述は自動生成されませんので
application.scssにimportしてhamlにscssが当たるようにしましょう。

やり方
・hamlに反映させたいscssのファイル名をアンダーバー「 _ 」始まりにします。
application.scssにimportします。
・終わり

記述参考は下記

application.scss
@import "home";
@import "いれたいscssファイル名(アンダーバーは入れない)"; ⬅︎終わりは必ずセミコロンを入れる

#assetsの中でもフォルダの異なるものはURLをちゃんと記載しないと反映されない
@import "config/reset";
@import "フォルダ名/ファイル名";

参照記事

Railsドキュメント

終わりに

自分のいた学校は極力コマンドを使用しない学校でしたが
コマンドはコマンドで別の仕様があることを今回知りました。
アウトプットもそうですがインプットも必要。
人との情報交換は本当に大事だなと改めて感じました。

初学者な為、記事に不備やアドバイ等ございましたらご連絡頂けますと幸いです。
最後まで読んできただきありがとうございます。

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

RSpecで時間周りの備忘録

:lock: 時間の操作

Timecopを使って止めた時間の中でテストするのが良い.
Railsには ActiveSupport::Testing::TimeHelpersでも似たことができるが、ネストしてると期待通り動かないとか細かい点でTimecopの方がおすすめです.

ミリ秒を丸める

DB等でミリ秒が保持できない場合もあるのでテストが失敗することがある.
事前にミリ秒を丸めて扱っていた方が良い.

# Good
let(:now) { Time.current }

# Best
let(:now) { Time.current.change(usec: 0) }

Timecopの場合

# Bad
Timecop.freeze(Time.current)

# Best
Timecop.freeze
Timecop.freeze(Time.current.change(usec: 0))

⏳Timeout

RSpecを書いていると思いのほか時間がかかる処理が紛れ込んでしまう.
中には何かしらのタイミングで異常に時間がかかる処理もあったりする.

タイムアウトする様にすることで不安定なテストを特定することはできます.
ただし不安定なテストを改善するわけでじゃないので、無闇にタイムアウトの時間を伸ばすのはよくない.
後、実行環境が非力だとタイムアウトが多発するのにも注意が必要です.

require 'timeout'

it do
  # 5秒経過すると `Timeout::Error` がraiseされる
  Timeout::timeout(5) do
    expect{ subject.execute }.to_not raise_error
  end
end

メタデータで制御

テストが少ないうちは大して気にならないが、ある程度の数になるとCIの実行時間が問題になってくる.
そこでRSpecにタイムアウトを仕掛けられる様にした.

spec/support/timeout.rb に以下のコードを配置する


timeout.rb のコード

require 'timeout'

class RSpecTimeoutError < Timeout::Error
  def message
    'execution expired by RSpec'
  end
end

RSpec.configure do |config|
  rspec_timeout = ENV.fetch('RSPEC_TIMEOUT', 0).to_i
  config.around(:example) do |example|
    sec = example.metadata[:timeout] || rspec_timeout
    next example.run if sec <= 0
    Timeout::timeout(sec, RSpecTimeoutError) do
      example.run
    end
  end

  config.backtrace_exclusion_patterns << /#{Regexp.escape(Pathname.new(File.expand_path(__FILE__)).relative_path_from(Rails.root).to_s)}/
end


使い方

タイムアウトの指定は2種類あり、それぞれ値にはタイムアウトする秒を指定します.
ただし0を指定した時はタイムアウトは無効になります.

環境変数を指定

RSPEC_TIMEOUT を指定する事でタイムアウトが有効になります.
ローカル等の通常の環境ではタイムアウトはせず実行してもらいたいためです.

CI等の全てのテストを実行するような場合のみ環境変数を指定すると良いでしょう.

RSPEC_TIMEOUT=20 rspec

メタデータを指定

特定の箇所だけタイムアウトを制御したいときはメタデータを使います.

it('foo', timeout: 30) { ... }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】form_withについて簡単にまとめた

はじめに

form_withについて使いかたを簡単にまとめてみました。
今回はRubyonRailsAPIを色々と参照しました。

form_withの機能

フォームを構成するヘルパーメソッド。form_tagform_forと同じような挙動をするが、
現在はform_withを使用することが推奨されている。

基本的な構文は下記のとおり。

= form_with model: @user do |f|
  = f.text_field :name
  = f.submit '登録'

テキストフィールドに名前を入力して「登録」をおすとデータが送信される。
このとき、データはハッシュの階層構造であるparams[:user][:name]という形で送信される。
よってコントローラーでストロングパラメーターを用いる際は下記のようになる。

def user_params
  params.require(:user).permit(:name)
end

データの送信先については、:urlオプションを追加することによっても指定できるが、
渡されたモデルの状態(①新規②既存)によって自動推定をしてくれる。

①新規作成されたモデルが渡された場合
= form_with model: User.new do |f|
  = f.text_field :name
①で生成されるHTML
<form action="/users" method="post" data-remote="true">
  <input type="text" name="user[name]">
</form>
②既存のモデルが渡された場合
= form_with model: User.first do |f|
  = f.text_field :name
②で生成されるHTML
<form action="/users/1" method="post" data-remote="true">
  <input type="hidden" name="_method" value="patch">
  <input type="text" name="user[name]" value="<the name of the user>">
</form>

①のパターンでは、action="/users"であるのに対して、
②のパターンではaction="/users/1"となっている上に隠しinputフィールドを利用してメソッドをPatchに指定している。
これにより、上記①②のパターンでそれぞれ対応するアクションがcreateになったりupdateになったりする。

ビュー上に表示しないものを送信したいとき

hidden_fieldを使用することに対応可能。

= form_with model: @user do |f|
  = f.hidden_field :age, :value => @user.age #ビューに表示されない
  = f.text_field :name
  = f.submit

上記のように記載をすることで、ビュー上には表示されないがparams[:user][:age]の中に
@user.ageの値を格納した状態でデータを送ることができる。

ビュー上に表示させたいが編集はさせたくないとき

readonly: trueを指定すればOK。

= form_with model: @user do |f|
  = f.text_field :name
  = f.text_field :age, value: @user.age, readonly: true #ビューに表示されるが編集不可
  = f.submit

まとめ

form_withの仕組みが腹落ちしました。
特にモデルの状態の違いから生成されるhtmlが変わり、結果として対応するアクションが自動的に決まる仕組みのあたりは今まで曖昧な理解だったので今回がいい機会になりました。

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

SequelでPG::ConnectionBad: PQconsumeInput()エラーが出るようになって困った話

急に出始めた PG::ConnectionBad: PQconsumeInput() エラー

RailsでActiveRecordを使わずSequelをORMとして使っているのだが、いつからかRails起動直後のRDS(Postgresql)接続時に「PG::ConnectionBad: PQconsumeInput()」エラーが出るようになってしまった。

PG::ConnectionBad: PQconsumeInput() SSL error: decryption failed or bad record mac
PG::ConnectionBad: PQconsumeInput() server closed the connection unexpectedly
PG::ConnectionBad: PQconsumeInput() SSL error: sslv3 alert bad record mac

Sequel側は変えていないのでRDSで何か仕様変更があったのか・・・。RDSのCA証明書の変更の影響かと思ったが、変更する前から当エラーは出てた。

状況

  • Rails起動直後にいくつかのリクエストでこのエラーになる。全てではない。
  • ある程度エラーが出るとその後は出ない。
  • ステージング環境などで8時間くらいアクセスがないとまたでるようになる。
  • なので、本番環境ではRails再起動直後だけこのエラーがでる。
  • 開発環境のRDSではないPostgresqlだと出ない。

このエラーが出たらretryする仕組みも入れたけど、やはりそもそも出ないようにしたい。

connection_validatorを試す

真偽はわからないがpumaのようなマルチスレッドのアプリケーションサーバを使っているとこのエラーが出るらしい。Sequelのコネクションプーリングがスレッドセーフではない?

で、色々ググったところ日本語の情報はさっぱりの中、海外のサイトで以下の設定を入れれば解決するらしい情報を入手。

DB.extension(:connection_validator)
DB.pool.connection_validation_timeout = -1

https://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/connection_validator_rb.html

コネクションプーリング内のコネクションが有効かどうかチェックして、有効でなければ再接続する設定。
DB.pool.connection_validation_timeout はチェックする間隔(秒)で、デフォルト3600秒、−1のときは常にチェック。

connection_validation_timeoutの値を変えて検証

  • -1: エラーが出なくなった!
  • 1以上: 起動直後はエラーでる

検証結果

DB.pool.connection_validation_timeout = -1 なら効果あり。
3600で効果出て欲しかったなあ。

パフォーマンス試験

公式にもあるように、常にチェックだと気になるのがパフォーマンスの劣化。
connection_validatorありなしでApacheBenchでパフォーマンステストしてみた。

connection_validatorなし

$ ab -c 100 -n 10000 "http://xxxxxxxxxx"

Concurrency Level:      100
Time taken for tests:   22.620 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      15650000 bytes
HTML transferred:       11390000 bytes
Requests per second:    442.09 [#/sec] (mean)
Time per request:       226.196 [ms] (mean)
Time per request:       2.262 [ms] (mean, across all concurrent requests)
Transfer rate:          675.66 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0       5
Processing:     5  226 162.3    178    1102
Waiting:        5  226 162.3    178    1102
Total:          5  226 162.3    178    1102

connection_validatorあり (connection_validation_timeout = -1)

$ ab -c 100 -n 10000 "http://xxxxxxxxxx"

Concurrency Level:      100
Time taken for tests:   26.522 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      15650000 bytes
HTML transferred:       11390000 bytes
Requests per second:    377.05 [#/sec] (mean)
Time per request:       265.216 [ms] (mean)
Time per request:       2.652 [ms] (mean, across all concurrent requests)
Transfer rate:          576.26 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       2
Processing:    10  264 157.8    231    1126
Waiting:       10  264 157.8    231    1126
Total:         10  264 157.9    231    1126

(結果は3回試して一番良かった結果)

テスト結果

1リクエストあたり約40ms遅くなった。

結論

超絶負荷のかかる様なサーバではないので、40msのパフォーマンス劣化は許容範囲としてconnection_validatorを採用することにした。
auto_reconnectオプションとかあれば良いのに。

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

Rails deviseの導入〜新規登録、ログイン時のカラムの追加まで

初めに

某プログラミングスクールの卒業生です。
スクールに通う中で学んだことや、つまづいたことを備忘録としてまとめてます。
今回は、deviseを扱う際に必ず調べるであろう、独自カラムの追加方法をdeviseの導入からまとめておきたいと思います。

環境

・Ruby 2.5.7
・Rails 5.2.4.1

deviseの導入

deviseとは、ログインや新規登録機能等を簡単に実装できるgemです。

まず初めに、Gemfileに以下の1行追加して保存します。

Gemfile
gem 'devise'

次にターミナルで下記を実行し、deviseをアプリケーションに読み込ませます。

$ bundle install

最後にターミナルで下記を実行し、deviseの初期設定を行います。

$ rails g devise:install
Running via Spring preloader in process 29980
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views
===============================================================================

ターミナルに上記のような表示がされれば成功です。
以上でdeviseを使う準備が整いました。

deviseでモデルを作成する

準備が整ったので、いよいよdeviseを使ってみましょう。
今回は、ユーザー登録時の名前の追加と、ログイン時は名前とパスワードでログインできるようにカスタマイズしていきます。

まず初めに、モデルを作成します。
ターミナルで下記のを実行し、モデルを作成します。

$ rails g devise User
Running via Spring preloader in process 30045
      invoke  active_record
      create    db/migrate/20200319222813_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users

これで、「User」という名のモデルと、usersテーブル用のマイグレーションファイルが作成されました。

独自のカラムを追加しよう

モデルとマイグレーションファイルの作成ができたので、usersテーブルにカラムを追加しいきましょう。
今回はnameカラムを追加します。
先ほど、作成したマイグレーションファイルに以下の1行を追加します。

20200319222813_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
      t.string :name, null: false ←追加

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

      ## Rememberable
      t.datetime :remember_created_at
以下省略

追加したら、ターミナルでマイグレーションを実行しましょう。

$ rails db:migrate

実際にカラムが追加できたかschemaファイルで確認します。

schema.rb
ActiveRecord::Schema.define(version: 2020_03_19_222813) do

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "name", null: false ←追加されている
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

追加されていることが確認できれば成功です。

Viewを追加しよう

カラムの追加がおわったら、次にViewを作成しましょう。
ターミナルで下記を実行しましょう。

$ rails g devise:views
invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_error_messages.html.erb
      create    app/views/devise/shared/_links.html.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/email_changed.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb

これでdevise用のViewを作成する事ができました。

deviseをカスタマイズしよう

Viewが作成できたので、いよいよdeviseをカスタマイズしていきます。
今回は、ユーザー登録時の名前の追加と、ログイン時は名前とパスワードでログインできるようにカスタマイズしていきたいと思います。

新規登録画面に名前を追加  

deviseの新規登録画面はデフォルトで、以下のようになっています。
今回はここに名前(Name)を追加します。
スクリーンショット 2020-03-20 8.51.09.png

新規登録のViewにNameを追加します。
以下を参考に追加してみてください。
※registrationsが新規登録のviewなので覚えておきましょう

views/devise/registrations/new.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

<%# ここから %>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true %>
  </div>
<%# ここまで %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

スクリーンショット 2020-03-20 9.06.58.png

上記の追記で名前が追加できた事が確認できます。
これではまだ、名前の登録がデータベースに反映されません。
なので、最後にストロングパラメーターを設定しましょう。

controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ここから
  before_action :configure_permitted_parameters, if: :devise_controller?
  protected
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end
# ここまで
end

これでデータベースに名前が登録できるようになりました。
今後、カラムを増やす場合はストロングパラメーターにも忘れずに追加しましょう。

名前とパスワードでログインできるように変更

deviseのログイン画面はデフォルトで、以下のようになっています。
今回はEmailを名前(Name)に変更します。
スクリーンショット 2020-03-20 9.40.21.png

ログインのViewをEmailからNameを変更します。
以下を参考に変更してみてください。
※sessionsがログインのviewなので覚えておきましょう

views/devise/sessions/new.html.erb
<h2>Log in</h2>

<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>

  <%# ここから %>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true %>
  </div>
  <%# ここまで追加 %>

  <%# ここから %>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>
  <%# ここまで削除 %>

  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password, autocomplete: "current-password" %>
  </div>

  <% if devise_mapping.rememberable? %>
    <div class="field">
      <%= f.check_box :remember_me %>
      <%= f.label :remember_me %>
    </div>
  <% end %>

  <div class="actions">
    <%= f.submit "Log in" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

スクリーンショット 2020-03-20 9.46.08.png

上記の追記でEmailが名前(Name)が変更できた事が確認できます。
これではまだ、名前でのログインができません。
devise.rbを書き換えて名前でのログインができるように変更します。

config/initializers/devise.rb
# ==> Configuration for any authentication mechanism
  # Configure which keys are used when authenticating a user. The default is
  # just :email. You can configure it to use [:username, :subdomain], so for
  # authenticating a user, both parameters are required. Remember that those
  # parameters are used only when authenticating and not when retrieving from
  # session. If you need permissions, you should implement that in a before filter.
  # You can also supply a hash where the value is a boolean determining whether
  # or not authentication should be aborted when the value is not present.
  # config.authentication_keys = [:email]

devise.rbの中に上記のような記述があるかと思います。
その一番下の行をを変更してください。
※#を外すのを忘れないように注意してください。

config/initializers/devise.rb
# config.authentication_keys = [:email]
↓ 変更
config.authentication_keys = [:name]

これで名前をとパスワードでログインできるようになりました。
以上で新規登録とログインのカスタマイズが終わります。

最後に

備忘録程度のまとめになっているので、もしかしたら分かりにくいかもしれませんがご了承ください。
この投稿が少しでも役に立つ事があれば幸いです。

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

Rails deviseの導入〜新規登録、ログイン時のカラム

初めに

某プログラミングスクールの卒業生です。
スクールに通う中で学んだことや、つまづいたことを備忘録としてまとめてます。
今回は、deviseを扱う際に必ず調べるであろう、独自カラムの追加方法をdeviseの導入からまとめておきたいと思います。

環境

・Ruby 2.5.7
・Rails 5.2.4.1

deviseの導入

deviseとは、ログインや新規登録機能等を簡単に実装できるgemです。

まず初めに、Gemfileに以下の1行追加して保存します。

Gemfile
gem 'devise'

次にターミナルで下記を実行し、deviseをアプリケーションに読み込ませます。

$ bundle install

最後にターミナルで下記を実行し、deviseの初期設定を行います。

$ rails g devise:install
Running via Spring preloader in process 29980
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views
===============================================================================

ターミナルに上記のような表示がされれば成功です。
以上でdeviseを使う準備が整いました。

deviseでモデルを作成する

準備が整ったので、いよいよdeviseを使ってみましょう。
今回は、ユーザー登録時の名前の追加と、ログイン時は名前とパスワードでログインできるようにカスタマイズしていきます。

まず初めに、モデルを作成します。
ターミナルで下記のを実行し、モデルを作成します。

$ rails g devise User
Running via Spring preloader in process 30045
      invoke  active_record
      create    db/migrate/20200319222813_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users

これで、「User」という名のモデルと、usersテーブル用のマイグレーションファイルが作成されました。

独自のカラムを追加しよう

モデルとマイグレーションファイルの作成ができたので、usersテーブルにカラムを追加していきましょう。
今回はnameカラムを追加します。
先ほど、作成したマイグレーションファイルに以下の1行を追加します。

20200319222813_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
      t.string :name, null: false ←追加

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

      ## Rememberable
      t.datetime :remember_created_at
以下省略

追加したら、ターミナルでマイグレーションを実行しましょう。

$ rails db:migrate

実際にカラムが追加できたかschemaファイルで確認します。

schema.rb
ActiveRecord::Schema.define(version: 2020_03_19_222813) do

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "name", null: false ←追加されている
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

追加されていることが確認できれば成功です。

Viewを追加しよう

カラムの追加がおわったら、次にViewを作成しましょう。
ターミナルで下記を実行しましょう。

$ rails g devise:views
invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_error_messages.html.erb
      create    app/views/devise/shared/_links.html.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/email_changed.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb

これでdevise用のViewを作成する事ができました。

deviseをカスタマイズしよう

Viewが作成できたので、いよいよdeviseをカスタマイズしていきます。
今回は、ユーザー登録時の名前の追加と、ログイン時は名前とパスワードでログインできるようにカスタマイズしていきたいと思います。

新規登録画面に名前を追加  

deviseの新規登録画面はデフォルトで、以下のようになっています。
今回はここに名前(Name)を追加します。
スクリーンショット 2020-03-20 8.51.09.png

新規登録のViewにNameを追加します。
以下を参考に追加してみてください。
※registrationsが新規登録のviewなので覚えておきましょう

views/devise/registrations/new.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

<%# ここから %>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true %>
  </div>
<%# ここまで %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

スクリーンショット 2020-03-20 9.06.58.png

上記の追記で名前が追加できた事が確認できます。
これではまだ、名前の登録がデータベースに反映されません。
なので、最後にストロングパラメーターを設定しましょう。

controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ここから
  before_action :configure_permitted_parameters, if: :devise_controller?
  protected
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end
# ここまで
end

これでデータベースに名前が登録できるようになりました。
今後、カラムを増やす場合はストロングパラメーターにも忘れずに追加しましょう。

名前とパスワードでログインできるように変更

deviseのログイン画面はデフォルトで、以下のようになっています。
今回はEmailを名前(Name)に変更します。
スクリーンショット 2020-03-20 9.40.21.png

ログインのViewをEmailからNameを変更します。
以下を参考に変更してみてください。
※sessionsがログインのviewなので覚えておきましょう

views/devise/sessions/new.html.erb
<h2>Log in</h2>

<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>

  <%# ここから %>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true %>
  </div>
  <%# ここまで追加 %>

  <%# ここから %>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>
  <%# ここまで削除 %>

  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password, autocomplete: "current-password" %>
  </div>

  <% if devise_mapping.rememberable? %>
    <div class="field">
      <%= f.check_box :remember_me %>
      <%= f.label :remember_me %>
    </div>
  <% end %>

  <div class="actions">
    <%= f.submit "Log in" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

スクリーンショット 2020-03-20 9.46.08.png

上記の追記でEmailが名前(Name)が変更できた事が確認できます。
これではまだ、名前でのログインができません。
devise.rbを書き換えて名前でのログインができるように変更します。

config/initializers/devise.rb
# ==> Configuration for any authentication mechanism
  # Configure which keys are used when authenticating a user. The default is
  # just :email. You can configure it to use [:username, :subdomain], so for
  # authenticating a user, both parameters are required. Remember that those
  # parameters are used only when authenticating and not when retrieving from
  # session. If you need permissions, you should implement that in a before filter.
  # You can also supply a hash where the value is a boolean determining whether
  # or not authentication should be aborted when the value is not present.
  # config.authentication_keys = [:email]

devise.rbの中に上記のような記述があるかと思います。
その一番下の行をを変更してください。
※#を外すのを忘れないように注意してください。

config/initializers/devise.rb
# config.authentication_keys = [:email]
↓ 変更
config.authentication_keys = [:name]

これで名前をとパスワードでログインできるようになりました。
以上で新規登録とログインのカスタマイズが終わります。

最後に

備忘録程度のまとめになっているので、もしかしたら分かりにくいかもしれませんがご了承ください。
この投稿が少しでも役に立つ事があれば幸いです。

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

【Rails】小数点と3桁ずつ区切るカンマ

1234.50

 "#{(product.price).to_f}"
=> 1234.5

 "#{(product.price).to_s(:delimited)}"
=> 1,234.5
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】PayjpのRSpecテスト、モックを使用してコントローラテストを実装する

はじめに

payjpなどの外部APIと通信するRailsアプリの場合、
テストする時には実際に通信が走ってしまっては困ります。
今回は、ダミー(テストダブル)を作成して、ダミーのモックを返すようにすることで、コントローラのテストを実装します。

実装のサンプルコードだけ知りたい場合は、【サンプルコード】の項目までスクロールしてください。

普通にテストを実装すると

コントローラーのテストでは一般に以下を実装します。

  • コントローラーのアクションに対応するビューが正しく表示されるかどうか
  • リダイレクトが正しく行われるかどうか
  • インスタンス変数が正しく定義されているかどうか

もし、以下のようなコントローラーがあった場合について考えます。

card_controller.rb
def index
  customer = Payjp::Customer.retrieve("cus_xxxxxxxxxxxxx")
end

今回はindexのアクションはindexのビューが表示されることをテストします。
コントローラのスペックでは以下のようになります。

card_controller_spec.rb
it "cardの一覧画面(indexアクション)にアクセスすると、indexのビューが表示される" do
  get :index
  expect(response).to render_template("index")
end

しかしこのままでは実際のコントローラでPayjp::Customer.retrieve()のコードが走り、
実際のpayjpと通信してしまいます。
(テストなのに、payjpの顧客や売り上げを作成してしまうのはまずいですよね!)

payjpと通信してしまうのを回避する

通信を回避する方法として、allowというメソッドがあります。
使い方は以下のような感じです。

allow(Payjp::Customer).to receive(:create).and_return(true)

これはPayjp::Customer.create()という呼び出しがあった際に、trueを返すというものです。
これを使用すると、実際に通信が走ってしまうのを回避できると思います。

モックとは

モックとは、本物のように振る舞う偽物、結論以下のようなものです。

payjp_customer = double("Payjp::Customer")

先ほど、allowは最終的にtrueを返しました。
Payjp::Customer.create()ではpayjpの顧客を返すのが理想なため、顧客を返すにはどうしたら良いかというのを考えます。
payjpの実際の顧客データは以下の通りです。

顧客データ
{
  "cards": {
    "count": 0,
    "data": [],
    "has_more": false,
    "object": "list",
    "url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards"
  },
  "created": 1433127983,
  "default_card": null,
  "description": "test",
  "email": null,
  "id": "cus_121673955bd7aa144de5a8f6c262",
  "livemode": false,
  "metadata": null,
  "object": "customer",
  "subscriptions": {
    "count": 0,
    "data": [],
    "has_more": false,
    "object": "list",
    "url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/subscriptions"
  }
}

上記を丸々変数やメソッドにして返すようにしても良いのですが、rspecには
便利なメソッドが用意されています。

テストダブル - double()

です。
テストダブルはdouble("Payjp::Customer")のように使用します。こうすると、Payjp::Customerオブジェクトのダミーを用意することができます。(つまりモックを作ることができる)

これを使用すると以下のようにできます。

payjp_customer = double("Payjp::Customer")
allow(Payjp::Customer).to receive(:create).and_return(payjp_customer)

一歩応用すると

別のパターンを考えます。
もしコントローラに以下のようなコードがあったとします。

controller
customer = Payjp::Customer.create(....)
customer.cards.retrieve(....)

上記コードだとpayjpとの通信は、3回起きます。
- Payjp::Customer.create()
- customer.cards
- cards.retrieve()

なので、テストダブル、allow文は以下のようになります。

spec
payjp_customer = double("Payjp::Customer")
payjp_list = double("Payjp::ListObject")
payjp_card = double("Payjp::Card")
allow(Payjp::Customer).to receive(:create).and_return(payjp_customer) 
allow(payjp_customer).to receive(:cards).and_return(payjp_list) 
allow(payjp_list).to receive(:retrieve).and_return(payjp_card) 

ここで注目して欲しいのはallow文です。
allow文は通信の回避という側面以外に、メソッドの登録をしているとも取れます。
double("Payjp::Customer")で作成したオブジェクトはPayjp::Customerオブジェクトですが、実際のオブジェクトとは異なり、メソッドなどが全く定義されていないです。
(double("Payjp::Customer").cardsとしてもcardsというメソッドはありませんとエラーが出るでしょう。)

allow(payjp_customer).to receive(:cards).and_return(payjp_list)

とすることでpayjp_customerというテストダブルに、cardsというメソッドを教えていると捉えてもオッケーです。

【サンプルコード】 モックを使用したテストの実装

コントローラーのサンプル

より実践的な一例です。(*実際はもう少し、createアクションが完結になるよう努力しています。)

サンプルコード
def create
  Payjp.api_key = get_payjp_key
  customer = Payjp::Customer.create
  @credit = create_payjp_card(current_user, customer)
  if @credit.save
    redirect_to credits_path
  else
    render "new"
  end  
end

private
  def create_payjp_card(user, customer)
    customer.cards.create(card: token_parmas[:token])
    credit = Credit.new(
      user_id: user.id,
      customer_id: customer.id
    )
  end

specのサンプル

サンプルコード
context "createアクションにアクセスした時" do
  before do
    payjp_customer = double("Payjp::Customer")
    payjp_list = double("Payjp::ListObject")
    payjp_card = double("Payjp::Card")
    allow(Payjp::Customer).to receive(:create).and_return(payjp_customer)        
    allow(payjp_customer).to receive(:cards).and_return(payjp_list)        
    allow(payjp_list).to receive(:create).and_return(payjp_card) 
    allow(payjp_customer).to receive(:id).and_return("cus_xxxxxxxxxxxxx")
  end
  it "@creditが定義されていること" do
    post :create, params: {token: "tok_xxxxxxxx"}
    credit = create(:credit, user_id: user.id, customer_id: "cus_xxxxxxxxxxxxx")
    expect(assigns(:credit).customer_id).to eq(credit.customer_id)
  end
end

まとめ

モックの作成に、supportファイルを作成している方もいますが、私はdoubleを用いたテストの仕方をお勧めします。

ただし、allow文を使いすぎると、モックがきちんと作れているかのテストになってしまう気がします。railsではcontrollerテストがあまり推奨されていないように、他のテストでバリューを出すほうがいい気がします。

supportファイルでの実装
support/payjp_mock.rb
module PayjpMock
  def self.payjp_mock_data
    {
      "cards": {
        "count": 0,
        "data": [],
        "has_more": false,
        "object": "list",
        "url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards"
      },
      "created": 1433127983,
      "default_card": null,
      "description": "test",
      "email": null,
      "id": "cus_121673955bd7aa144de5a8f6c262",
      "livemode": false,
      "metadata": null,
      "object": "customer",
      "subscriptions": {
        "count": 0,
        "data": [],
        "has_more": false,
        "object": "list",
        "url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/subscriptions"
      }
    }
  end
end

参考資料

「【Rails】PAY.JPによる決済機能のモックを活用したコントローラーテスト」
https://qiita.com/tiphp452/items/87042d1800af9a312be9
「Rails で Payjp を使って決済システムを導入する」
https://qiita.com/hirotakasasaki/items/794c920016ac7c33da74
「使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」」
https://qiita.com/jnchito/items/640f17e124ab263a54dd
「決済処理を統合テストしたい」https://muut.com/i/payjp/general:n8jubya6r7gxcp4398dp9d697ed

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

RailsエンジニアがDDDやクリーンアーキテクチャに触れるためにとりあえずHanamiを始めてみる

Introduction

これまで Rails をそれなりにやってきて、ちょっとしたWebアプリならそれなりサクッと作れるようにはなりましたが、 Fat Controller だの Fat Model と言われるようにどこかで臨界点が来て、従来のMVCアーキテクチャとは違う別の設計を模索してみたい欲が出てきました :flushed:

そのとっかりとして、巷で話題の クリーンアーキテクチャ に触れるために、 Rails の対抗馬として名乗りを挙げた Ruby 製フレームワーク Hanami を始めてみようかと思いました :fire: :fire: :fire:

Install

まずは Hanami を立ち上げてみます。

$ mkdir hanami-tutorial
$ cd hanami-tutorial
$ bundle init

Gemfile を書き換えます。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

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

gem "hanami"

ライブラリをインストールして Rails と同様にサーバーを立ち上げます。

$ bundle
$ bundle exec hanami new .
$ bundle exec hanami server

導入完了!と思いきや、2020年3月20日現在、エラーになりました :cry:

Bundler could not find compatible versions for gem "dry-types":
  In snapshot (Gemfile.lock):
    dry-types (= 0.12.3)

  In Gemfile:
    hanami (~> 1.3) was resolved to 1.3.3, which depends on
      hanami-validations (>= 1.3, < 3) was resolved to 1.3.6, which depends on
        dry-validation (~> 0.11, < 0.12) was resolved to 0.11.2, which depends on
          dry-types (~> 0.12.0)

    hanami-model (~> 1.3) was resolved to 1.3.2, which depends on
      dry-types (~> 0.11.0)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

指示に従って、もう一度サーバーを立ち上げます。

$ bundle update
$ bundle exec hanami server

http://localhost:2300/ を開くと...

image.png

:yum:

Static Page

Rails チュートリアルと同様に、まずは静的なページを作ってみます。

ルーティングは Rails 感があります。

apps/web/config/routes.rb
root to: 'home#index'

アクションは Rails と違い独立したクラスを作ります。

apps/web/controllers/home/index.rb
module Web
  module Controllers
    module Home
      class Index
        include Web::Action

        def call(params)
        end
      end
    end
  end
end

ビューも Rails と違います。 Rails における View は Hanami においては ViewTemplate に分かれていて、 Rails の Helper などの UI に関わるロジックは View で、 HTML テンプレートは Template が担当します。

apps/web/views/home/index.rb
module Web
  module Views
    module Home
      class Index
        include Web::View
      end
    end
  end
end
apps/web/templates/home/index.html.erb
<h1>Bookshelf</h1>

http://localhost:2300/ を開くとページが差し替わったはずです。
これで自由にページを作れるようになりました :smile:

Template Engine

Hanami のデフォルトのテンプレートエンジンは Rails と同様に Erb ですが、 Slim や Haml で書きたいですよね。
Slim をインストールして、先ほど作ったテンプレートを書き換えます。

Gemfile
gem 'slim'
$ bundle
$ mv apps/web/templates/home/index.html.erb apps/web/templates/home/index.html.slim
apps/web/templates/home/index.html.slim
h1 Bookshelf

これで Slim は導入できました :thumbsup:

Pry

Ruby 2.7 以下だとデバッグのために Pry を導入したいですね。これも簡単。

Gemfile
gem 'pry'
$ bundle
$ bundle exec hanami console
[1] pry(main)> 

Ridgepole

意見は分かれますが、自分はプロトタイピング重視で、 Rails のマイグレーションではなく Ridgepole を使いたいです。

Gemfile
gem 'ridgepole'
$ bundle

Rails の database.yml に相当するものが無いので、便宜上新たに作ります。

config/database.yml
# ridgepole を使うために用意

development:
  adapter: sqlite3
  database: db/hanami_tutorial_development.sqlite

Ridgepole が使う Schemafile を生成します。まだテーブルを定義していないので空ファイルが出力されます。

$ bundle exec ridgepole -c config/database.yml -e > Schemafile

テーブルを作ってみます。

Schemafile
create_table :users, force: :cascade do |t|
  t.string :email, null: false
  t.timestamps null: false
  t.index :email, unique: true
end
$ bundle exec ridgepole -c config/database.yml -a
Apply `Schemafile`
-- create_table("users", {})
   -> 0.0052s
-- add_index("users", ["email"], {:unique=>true})
   -> 0.0035s

テーブルが作られたのでモデルを作ります。 Rails におけるモデルは Hanami においては RepositoryEntity に分かれます。クリーンアーキテクチャが見えてきましたね :point_up:

lib/hanami_tutorial/repositories/user_repository.rb
class UserRepository < Hanami::Repository
end
lib/hanami_tutorial/entities/user.rb
class User < Hanami::Entity
end

コンソールで動作確認してみます。

[1] pry(main)> UserRepository.new.create(email: 'test@example.com')
[2] pry(main)> UserRepository.new.find(1)
=> #<User:0x0000000000000000 ...
[3] pry(main)> UserRepository.new.users.where(email: 'test@example.com')
=> #<ROM::Relation::Composite name=users dataset= ...

Hanami は ActiveRecord ではなく Rom という ORM を使っていますが、 Rails エンジニアなら雰囲気で使えそうな気がしますよね? :grin:

Job Queue

Rails の ActiveJob に相当する非同期処理モジュールが Hanami にはありませんが、自分の経験上 Job Queue ミドルウェアを移行することはそんなに無いので、ここでは Sidekiq を入れてみましょう。

Gemfile
gem 'sidekiq'
config/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

ワーカーを作ります。引数で与えられた秒数だけ待機して foo! と叫ぶだけのやつです。

lib/hanami_tutorial/workers/sleepy_echo_worker.rb
class SleepyEchoWorker
  include Sidekiq::Worker

  def perform(time)
    sleep time
    puts 'foo!'
  end
end

Sidekiq を立ち上げます。

$ REDIS_URL=redis://localhost:6379 bundle exec sidekiq -r ./config/boot.rb 

コンソールでワーカーを呼び出します。

[1] pry(main)> SleepyEchoWorker.perform_async(3)

Sidekiq を立ち上げたターミナルで、3秒間待機した後に foo! とログに出ることが確認できましたね :sunglasses:

Conclusion

Rails エンジニアが Hanami でWebアプリを開発するための初期導入についてまとめてみました。今後 Hanami で開発する際にハマったことがあればここに追記していこうかと思います。

ありがとうございました :bow:

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

capistrano でreleasesがいっぱいになるときの対処法

以下の記事を参考にした。

Rails deployment to Amazon EC2 - No space left on device

以下のコードを追記すれば良いみたい。

config/deploy.rb
   set :keep_releases, 5
 + after "deploy:restart", "deploy:cleanup"

以上

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

capistranoで自動デプロイを実行すると、No space left on deviceと言われた

状況

bundle exec cap production deployというコマンドを叩き、自動デプロイを実行しようとすると、No space left on device
というエラーが出て、途中で止まってしまった。

対処法

まず、以下の記事を参考に対処法を探った。
No space left on device とエラーが出るときの対処法
Linux

teminal
$ df -i 
ファイルシス   Iノード  I使用  I残り I使用% マウント位置
devtmpfs        121370    281 121089     1% /dev
tmpfs           125870      2 125868     1% /dev/shm
tmpfs           125870    363 125507     1% /run
tmpfs           125870     16 125854     1% /sys/fs/cgroup
/dev/xvda1      424720 424576    144   100% /
tmpfs           125870      1 125869     1% /run/user/1001

どうやら、/dev/xvda1 ディレクトリナイがいっぱいになっている模様。
このコマンドを叩いて、ファイル数とたくさん消費しているディレクトリを探せば良いそうだ。

terminal
$ find /var/log/ -type f -name \* -exec cp -f /dev/null {} \;

これでログを消すと,

terminal
[takuya@ip-10-0-1-173 ~]$ df -i
ファイルシス   Iノード  I使用  I残り I使用% マウント位置
devtmpfs        121370    281 121089     1% /dev
tmpfs           125870      2 125868     1% /dev/shm
tmpfs           125870    343 125527     1% /run
tmpfs           125870     16 125854     1% /sys/fs/cgroup
/dev/xvda1      596360 424567 171793    72% /
tmpfs           125870      1 125869     1% /run/user/1001

容量が空いて、無事解決。

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