20200421のRubyに関する記事は20件です。

インクリメンタルサーチを噛み砕く

近況報告

やあ。テックキャンプのリモートワーク化がはじまって四週間目に突入しました。我がチームはついに最終課題をパスし間も無く就職活動に入ります。そこで大きな壁,履歴書の自己開示。わたしが一番苦手とする場所です。「わたしはプログラミングでご飯食べていくってきめたの!」って叫んで内定決まればいいのに笑

さて,今日の記事は個人アプリにインクリメンタルサーチを実装のついでの忘備録です。実況感覚で書いていきます。

インクリメンタルサーチ

なにそれ

 英語でincremental search,直訳は増加検索ですね。通常の検索は検索ボタンを押すとページが更新されて検索結果が出てきますが,この検索は一文字入れるごとに非同期通信で検索結果がぽんぽん出てきます。youtubeやGoogleとか検索の予測がでますね。あれです。文字を打つごとに予測される結果が減るからデクリメンタルサーチでもいいんじゃね。ちなみに,予測の変換に履歴が出てくるのはcookieがかかわるので少々異なります。

非同期通信・・・ユーザーの望むタイミングとは別で行われる通信です。通信自体は行われています 。railsなら,ユーザーのリクエストをMVCで逐一処理してそれに応じたビューをユーザーに届けています。ただ,例えばgoogle検索で「ツイッ」まで書いたところでリダイレクトして予測変換「ツイッター」が記載されたビューに更新されたとします。検索フォームの部分だけ変わればいいのに,変化のないGoogleのロゴとかも更新されるの無駄な気がしません?痒いところに届くのが非同期通信です。私のイメージは,必要な情報だけをビューに上書きする通信です。

なんで必要?

 いちいち検索ボタンを押して読み込むより早く検索結果をユーザーに届けることができます。個人アプリのような小さい規模ではそこまで破壊力があるわけではありません。ただ,ロゴが舌なめずりしている某サイトだと通信が0.1秒遅れると売り上げが1%落ちると聞きました。1兆円の売り上げなら100億円の損失です。まさに時は金なりですね!

環境(前提)

・rails 2.5.2
・ruby 5.2.3
gem 'jquery-rails'
DBに検索対象のレコードが入っている
検索機能の実装完了前提

今回はjqueryのgemを用いるから

gem 'jquery-rails'

からのbundle install。
application.jsでjqueryが使えるように,

//= require jquery

の記述の有無を確認。

これで準備おっけー

実装(実況感覚)

検索フォームに入力するとテーブルのnameカラムを検索する機能を実装していきます。

インデックスの実装

インデックスってなんだっけ?笑

カリキュラムを見直すと,検索の際にどれを検索すればいいのかをピックアップしてくれる機能なんだってさ。
UFOキャッチャーで例えると,UFOが検索するための端末としたらインデックスは商品(レコード)が引っかかりやすくなるために付いているリングみたいな役割なのかな。 ワカリニクイタトエデース

ただ,インデックスは検索に用いるカラムのみを複製して1つのテーブルみたいにしてるから,作りすぎるとデータベースの容量の圧迫になるみたい。

さて,インデックスを追加していきます

$ rails g migration AddIndexTo検索したいテーブルs

実行したら,マイグレーションファイルに飛んで

class AddIndexTo検索したいテーブル名 < ActiveRecord::Migration
  def change
    add_index :検索したいテーブルs, :検索したいカラム, length: 32(最大文字数)
  end
end

を記述したらマイグレート。ミスったらDB:rollbackなれdb:dropとなれ。

APIの準備

インクリメンタルサーチの実装の順番はrailsの実装とあんまり変わりません。
コントローラーに条件分岐の記入

reviews_controller.rb
  def search
    @reviews = Reviews.search(params[:keyword])
    respond_to do |format|
      format.html
      format.json
    end

formatごとのレスポンスで場合分けします。
format.htmlはhtml全部読み込みますよ,format.jsonはJS形式のみ読み込みますよって感じ。この分岐のおかげで非同期通信が可能になります。

コントローラーで条件分岐したら,DBから該当のでーたをビューまで運ぶ箱,jbuilder(パラムスに似てるね)を作成する。検索しているテーブルのビューファイルにフォルダsearch.json.jbuilderを作成。jbuilderを用いると,そのままJSでも用いることができる。

search.json.jbuilder
json.array! @reviews do |review| ←検索に引っかかった各インスタンスを配列でバラすぜ!中身は以下の通りだ!
  json.id review.id
  json.content review.content 
  json.image review.image 
  json.taste review.taste
  json.fragrance review.fragrance
  json.individuality review.individuality
  json.fruity review.fruity
  json.smorky review.smorky
  json.age review.age
  json.user_id review.user_id 
  json.user_sign_in current_user
end

テキストフィールドの実装

次はビューの作成をしていきます。検索フォームはこんな感じ↓

index.html.erb
<%= form_with(url: review_tweets_path, local: true, method: :get, class: "search-form") do |form| %>
  <%= form.text_field :keyword, placeholder: "検索する", class: "search-input" %>
  <%= form.submit "検索", class: "search-btn" %>
<% end %>

ではJSの記述に入ります。記述量が多いので少しずつ刻んでいきます。

search.js
$(function() {
  $(".search-input").on("keyup", function() { ←フォームに何か打ち込まれたら作動するよ
    var input = $(".search-input").val(); ←inputを打ちこまれた文字と定義するよ
    console.log(input); ←JSが動いているか確認。うまく表示されていたら消す。
  });
});

フォームの情報をJSON形式でコントローラーに送る

今までやったことの確認をすると,
・respond_toで行き先決定
・jbuilderでデータベースの情報の引き出し準備
・ビューでフォームの情報をJSで抽出する
うん,MVCだね。railsは非同期通信でも大まかなやることは同じです。もしこれ行こうエラーが発生したらMVCに則ってエラーを解消していきます。

search.js
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
//以下を追加
    $.ajax({ ←ajaxは非同期通信するよ!っていう合図
      type: 'GET', ←HTTPメソッドはDBを得るからGET
      url: '/review/search', ←非同期通信が行われるurl
      data: { keyword: input }, ←さーばーに送信するキー。これを元にインデックスから引っ張ってくる。
      dataType: 'json'←データの形式
    })
  });
});

これでフォームの情報はJSON形式で送られ,コントローラーのrespond_toでJSONに振り分けられてモデルに進みます。多分。

非同期通信がビューまで届いているか確認します。メソッドdoneを用いて届いた情報をみてみる。

search.js
$(function() {
  $(".search-input").on("keyup", function() {
    var input = $(".search-input").val();
    $.ajax({
      type: 'GET',
      url: '/review/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(reviews) {
      console.log(reviews);
    })
  });
});

事故発生。読み込めてない。。。順番に確認していこう。
JSは読み込めていたから,クラスのミスはない。
ajaxの読み込みミスは、、、url間違えていました、、、
分岐は誤字なし,jbuilderは、、、nameを書き忘れる痛恨のミス!何調べようとしてたんだ

も一回検索フォームにキーワードを打ち込む、、、うん、うまくいった!!!

スクリーンショット 2020-04-21 19.21.46.png

さて,次に進みます。次はJSを利用してHTMLを上書きします。
まずdone以下を以下のように編集します。

search.js
~省略~
    .done(function(reviews) { ←reviewを引張ってきたから以下の実行するよ
      $(".showZone").empty(); ←該当クラスの中の子要素全て消すね
      if (reviews.length !== 0) { ←もし1でもあったら
        reviews.forEach(function(review){ ←1つずつ並べて
          appendReview(review); ←そのレコードを出現させるよ
        });
      }
      else {
        appendErrMsgToHTML("一致するツイートがありません");
      }
    })
~省略~

次はappendReview(review)に該当する定義を作成してクラスshowZoneに上書きするHTMLを作成します。

search.js

$(function() {

  var search_list = $(".showZone"); ←こういうやつは先に定義にしとく

  function appendReview(review) {
    var html = `<div class="reviewWhisky">
            <a href="/reviews/${review.id}">
            <div class="topZone">
              <div class="topZone__image">
                <img id="goShowPage" src="${review.image.url}" width="180" height="180">
              </div>
             <div class="bottomZone">
             <p>${review.name}</p>
            </div></div></a></div>`
   //htmlは検証からコピーが最速。データベースからの読み込み表示は上の書き方参考にしてね
    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: '/reviews/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(reviews) {
      search_list.empty();
      if (reviews.length !== 0) {
        reviews.forEach(function(review){
          appendReview(review); ←appendHTML召喚のメソッドと覚えておけばOK
        });
      }
      else {
        appendErrMsgToHTML("一致するのがありません");
      }
    })
  });
});

では,もし何も投稿が該当しなかった時用のを用意しましょ。いま一枚もない場合,何も写っていない状態だから,バグじゃないよーてわかるような記述を書きますappendErrMsgToHTMLに意味を持たせます)。

search.js
~省略~
    search_list.append(html);
  }
  function appendErrMsgToHTML(msg) {
    var html = `<div class='name'>${ msg }</div>`←新しい要素なので必要ならCSS
    search_list.append(html);
~省略~
    .fail(function() {←うまく行かなかった時用にエラー表示を準備
      alert('error');
    });
  });
});

これで一通り完成かな。ちなみに中の人はキータ執筆と同時進行で行なっていたのですが,search.jsファイルが読み込まれなくなる不具合に遭遇し2時間詰みました笑

解決策としては別のファイルを作成して作り直したらうまくいきまっした、、よかった、、、ただ,何が原因のエラーだったんだろう。。。

 おわりに

 まあなんとか実装できました。非同期通信のjsはコード1つ1つがわかりやすいので比較的サクッといけますね。非同期通信で重要なのは,通信の根本はMVCであること!

余談

 非同期通信とturbolinks(以下TB)は仲が悪いみたいです。TBってなんやねんって話ですよね。カリキュラムでも無下に扱われていました。この子は名前の通り,リンク先に早くたどり着くため手助けをしてくれます。ただ,早くする方法というのは,余計な動きを削ること,ここではJSを起動しないことです。非同期通信はJSを用いた通信なので,turbolinksが邪魔をしてしまう可能性がある(実際邪魔する)訳ですねー。

ただ悪い子ではないので見つけ次第gemをコメントアウト!とかしないであげてください。
$(document).on('turbolinks:load', function(){
とJSの初めに打ってあげたり,link_toにつづけて
data:{"turbolinks": :false}
と打ってあげると,JSが動いてくれます

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

【Rails Tutorial12章】パスワード再設定機能の実装手順を整理してみました【後編】

はじめに

Railsチュートリアル12章の内容を、少しでも理解の助けとなればと思い、割としっかり目に整理しました!
備忘録です。

前提

Railsチュートリアル1〜11章までの内容が完了していること。
特に、アカウント有効化機能が実装されている事。

内容

Railsチュートリアル12章のパスワード再設定機能の実装手順を、前中後半の3回に分けて整理おります。
後編である今回は、パスワード再設定機能の本実装を行っていきます!

前編→パスワード再設定用のリソース作成
中編→パスワード再設定メール送信機能の実装

3.パスワードを再設定する。

  ●送信用メールの作成が完了したので、次はPasswordResetsコントローラのeditアクションの実装を行っていく。
  
  ●その次に統合テストを使って上手く動作しているかを確認するためのテストを書いていく。

3-1.パスワード再設定機能実装のための準備を行っていく。

  1.まずはパスワード再設定用メールに添付してあるリンクを動作させるために、パスワード再設定フォームのビューを作成する必要がある。
    ➡︎app/views/password_resets/edit.html.erbファイルで行う。

    ➡︎フォームに関してはパスワード入力フィールドと、パスワード確認入力フィールドを作成すれば十分。

  2.パスワードを再設定を行おうとしているユーザーを取得する必要がある。
    ➡︎ユーザーがメールに貼り付けられているリンクをクリックしてパスワード再設定ページへ移動してくる時、そしてパスワードを実際に再設定する時にユーザーを取得する必要がある。

    ➡︎メールアドレスをキーとして取得するのだが、そのためにはeditアクションとupdateアクションの両方で、メールアドレスが必要となる。
      →editアクションではリンクに含まれているメアドから検索してそのまま取得できる。
      →updateアクションでは、フォームを送信するとメールアドレスの情報は消えてしまうので、上手くユーザーを取得できるように準備をしなければならない。

    ➡︎updateアクションでメールアドレスからユーザーを取得するための準備。
      →どうすれば良いかというと、リンクに含まれているメアドを一時的に保持してあげればいい。そのために隠しフィールドを用意してページ内に一旦保持させる手法をとることにする。
      →隠しフィールドを用意することで、パスワード再設定の情報と一緒にメールアドレスも送信されるようになる。
      →パスワード再設定用のフォームにhidden_field_tagメソッドを使う。キーに:email属性、値に@user.emailでユーザーのアドレスがparams[:email]の値として送信されるようにする。
        →<%= hidden_field_tag :email, @user.email %>
        →注意。f.hidden_field_tagとしないこと。こうしてしまうと、form_for(@user)のブロック引数を受け取って、params[:user][:email]にメールアドレスが保存されてしまう。
      →しかし、これにてメールアドレスからユーザーを検索して取得する準備が整いました。

  3.次に上記のフォームを描画するための処理を、PasswordResetsコントローラのeditアクションへ記述してあげる。
    ➡︎app/controllers/password_resets_controller/rbファイルへ移動する。
      →まずprivateキーワード内に、メールアドレスと一致するユーザーの検索を行うメソッドを定義する。
        →メソッド名をget_userとする。
        →params[:email]のメールアドレスを持つユーザーを取得して、@userインスタンス変数へ代入する処理を記述する。
      →同じくprivateキーワード内で、取得したユーザーが有効なユーザーかどうかを確認するメソッドを定義する。
        →メソッド名をvalid_userとする。
        →そのユーザーが存在する事、有効化されている事、認証済みである事をunless文により判定する条件式を記述する。
          →@user(ユーザーの存在) && @user.activated?(有効化の確認) && @user.authenticated?(:reset, params[:id])(ダイジェストとトークンの一致を確認)とする。
        →正しいユーザーでなければ、ルートURLへダイレクトさせる処理を記述。
      →有効なユーザーかどうかの確認は、editアクションで必要なのはもちろんupdateアクションでも必要になるので、beforeフィルターを使ってバリデーションを行ってあげる。

    ➡︎以上でパスワード再設定用のフォームが描画されるようになったので、あとはupdateアクションにパスワードを実際に更新するための処理を記述していく。

app/views/password_resets/edit.html.erb
  #3-1.2
  <% provide(:title, 'パスワードの再設定') %>
 
    <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
      <%= render 'shared/error_messages' %>
 
      <%= hidden_field_tag :email, @user.email %>
 
      <%= f.label :password %>
      <%= f.password_field :password %>
 
      <%= f.label :password_confirmation %>
      <%= f.password_field :password_confirmation %>
 
      <%= f.submit "パスワードを更新する" %>
    <% end %>
app/controllers/password_resets_controller.rb
  #3-1.3
  class PasswordResetsController < ApplicationController
    before_action :get_user,   only: [:edit, :update]
    before_action :valid_user, only: [:edit, :update]
    .
    .
    .
    private
     #メールアドレスと一致するユーザーの検索を行う
      def get_user
        @user = User.find_by(email: params[:email])
      end
 
      #取得したユーザーが有効なユーザーかどうかを確認する
      def valid_user
        unless (@user && @user.activated? &&
          @user.authenticated?(:reset, params[:id]))
          redirect_to root_url
        end
      end
end

3-2.実際にパスワードを更新する機能を実装していく。

  ➡︎上記で作成したフォームからの送信に対応するupdateアクションを作成していく。

  ➡︎updateアクションの記述を行っていく上で、4つ考慮しなければならない事項がある。

    1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。
      ➡︎パスワード再設定の期限を設定して、その期限の間に再設定されなかった場合は期限切れとする処理を行うメソッドを定義する。
        →app/models/user.rbファイルで記述する。
          →メソッド名をpassword_reset_expired?とすることにする。
          →期限は2時間とし、現在時刻から2時間以上経っている場合にはtrueを返す処理を記述する。
            →reset_sent_at < 2.hours.ago
            →注意。「2時間より少ない」という意味ではなく、「2時間以上前」と読むのが正しい。
            →つまり、「パスワード再設定メールの送信時刻が、現在時刻よりも2時間以上早い場合」と読むのが正しい。
          →準備が完了したので、パスワード再設定の有効期限が切れているかどうかをチェックするメソッドを定義していく。

      ➡︎パスワード再設定の有効期限が切れていた場合にユーザーを弾くメソッドを定義する。
        →app/controllers/password_reset_controller/rbファイルへ移動する。
          →メソッド名をcheck_expirationとする。
          →取得したユーザーのパスワード再設定有効期限が切れてるかどうかを判定する条件式を記述する。
            →上記で準備した「パスワード再設定のメール送信時刻が、現在時刻よりも2時間以上前ならtrueを返す」メソッドを利用する。
          →trueならフラッシュで警告メッセージを表示させ、パスワード再設定フォームへリダイレクトさせる。
          →このメソッドはprivateキーワード内で定義してあげる。

      ➡︎edit、updateアクションに対しbeforeフィルターで、check_expirationメソッドを適用してあげる。

    2つ目:無効なパスワードであれば失敗させる。そして、失敗した理由も表示させる。
      ➡︎app/controllers/password_resets_controller.rbファイルのupdateアクションに直接記述していく。
        →updateアクション内のどの条件式にも当てはまらない場合はeditのビューを再描画させ、パーシャルにエラーメッセージが表示されるようにする(パスワード再設定フォームに記述済み)。

    3つ目:新しいパスワードが空文字になっていないかを確認する。
      ➡︎app/controllers/password_resets_controller.rbファイルのupdateアクションに直接記述していく。
        →フォームから送信されたパスワードが空かどうかを判定する条件式を記述する。
        →空ならerrors.addを使って@userオブジェクトを直接指定して、空の文字列に対してデフォルトのメッセージを表示させるようにする。
          →@user.errors.add(:password, :blank)
        →空でなければ4の処理へ移行させる。

    4つ目:新しいパスワードが正しければ、更新する。
      ➡︎app/controllers/password_resets_controller.rbファイルのupdateアクションに直接記述していく。
        →パスワードが更新・保存された場合の条件式を記述する。
        →ユーザーをログインさせる。
        →ユーザーのリセットダイジェストを、nilで更新・保存する。
          →再設定が成功しても最低2時間は再設定が有効になってしまっているので、ダイジェストを空にすることで強制的に無効にさせる。
        →フラッシュメッセージを表示させる。
        →ユーザー詳細ページへリダイレクトさせる処理を記述する。

      ➡︎上記の条件式ではupdate_attributesメソッドを使用しているが、引数にuser_paramsメソッドを渡すことでより厳密な属性検索をかけさせるようにしたい。
        →なのでprivateキーワード内で、パスワードとパスワード確認のみを許可するためのuser_paramsメソッドを定義してあげる。
        →params.require(:user).permit(:password, :password_confirmation)

      ➡︎これで、updateアクションが動作するようになった。

app/controllers/password_resets_controller.rb
  class PasswordResetsController < ApplicationController
    .
    .
    #1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。
    before_action :check_expiration, only: [:edit, :update]
    .
    .
    .
    def update
      #3つ目:新しいパスワードが空文字になっていないかを確認する。
      if params[:user][:password].empty?
        @user.errors.add(:password, :blank)
        render 'edit'
      #4つ目:新しいパスワードが正しければ、更新する。
      elsif @user.update_attributes(user_params)
        log_in @user
        flash[:success] = "パスワードが更新されました"
        redirect_to @user
      #2つ目:無効なパスワードであれば失敗させる。
      else
        render 'edit'
      end
    end
 
    private
      def user_params
        params.require(:user).permit(:password, :password_confirmation)
      end
      .
      .
      .
      #1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。
      def check_expiration
        if @user.password_reset_expired?
          flash[:danger] = "パスワード再設定の有効期限が切れてます"
          redirect_to new_password_reset_url
        end
      end
  end
app/models/user.rb
  class User < ApplicationRecord
    .
    .
    .
    #1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。
    def password_reset_expired?
      reset_sent_at < 2.hours.ago
    end
    .
    .
    .
  end

3-3.パスワードの再設定をテストする。

  1.新しいパスワードの送信に成功した場合と、失敗した場合の統合テストを書いていく。
    ➡︎まずはファイルを作成する。
      →$ rails g integration_test password_resets

    ➡︎test/integration/password_resets_test.rbファイルへ移動して、パスワード再設定の統合テストを記述していく。
      →(1)setupメソッドを定義する。
        →メイラーの中身が空の配列を作成する。
        →テスト用のログインユーザーを取得する。
      →(2)パスワード再設定のテストを行う。
        →パスワード再設定用メール送信フォームに対してGETリクエストを送信させる記述を行う。
        →パスワード再設定用メール送信フォームのビューテンプレートが描画されていることを検証。
      →(3)まずは無効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う。
        →フラッシュが存在することを検証。
        →パスワード再設定用のメールを送信するビューテンプレートが表示されていることを検証。
      →(4)今度は有効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う。
        →更新後のリセットダイジェストが、更新前のダイジェストと(ダイジェストが空になったことで)イコールでないことを検証する。
        →メイラーの配列の値の個数が1になっていることを検証する。
        →フラッシュが表示されていることを検証する。
        →ルートURLへリダイレクトされていることを検証する。
      →(5)パスワード再設定フォームのテストを行う。
        →assignsメソッドでPasswordResetsコントローラから直接@userインスタンス変数を参照し、ユーザーを取得しておく。
      →(6)無効なメールアドレスで、パスワード再設定フォームへGETリクエストを送信させる記述を行う。
        →ルートURLへリダイレクトされることを検証する。
      →(7)ここでユーザーを無効なユーザーに切り替える処理を記述(攻撃者によるアカウント乗っ取りのテスト)。
        →メールアドレスは有効だが、無効なユーザーでパスワード再設定フォームヘGETリクエストを送信させる記述を行う。
        →ルートURLへリダイレクトされることを検証する。
      →(8)そして今度は有効なユーザーへ切り替える。
        →メールアドレスは有効だがトークンが無効なまま、パスワード再設定フォームヘGETリクエストを送信させる記述を行う。
        →ルートURLへリダイレクトされることを検証する。
      →(9)有効なメールアドレスとトークンで、パスワード再設定フォームヘGETリクエストを送信させる記述を行う。
        →ちゃんとパスワード再設定ビューテンプレートが描画されていることを検証する。
        →メールアドレス用の隠しフィールドセレクタのvalue値に、取得したユーザーのメールアドレスが表示されていることを検証する。
      →(10)無効なパスワードとパスワード確認を、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。
        →エラーメッセージセレクタが表示されていることを検証する。
      →(11)パスワードとパスワード確認入力が空のまま、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。
        →この場合もエラーメッセージセレクタが表示されていることを検証する。
      →(12)有効なパスワードとパスワード確認入力で、PasswordResetsコントローラのupdateアクションヘPATCHリクエストが送信された場合の記述をする。
        →ログインユーザーが存在することを検証する。
        →フラッシュが表示されていることを確認する。
        →そのユーザーのページへ、ちゃんとリダイレクトされているかどうかを検証する。

      ➡︎テストスイートGREEN。

test/integration/password_resets_test.rb
  require 'test_helper'
  class PasswordResetsTest < ActionDispatch::IntegrationTest
  #(1)setupメソッドを定義する。
    def setup
      ActionMailer::Base.deliveries.clear
      @user = users(:テスト用のユーザー)
    end
 
  #(2)パスワード再設定のテストを行う。
    test "password resets" do
      get new_password_reset_path
      assert_template 'password_resets/new'
    #(3)まずは無効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う
      post password_resets_path, params: { password_reset: { email: "無効なメアド" } }
      assert_not flash.empty?
      assert_template 'password_resets/new'
    #(4)今度は有効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う。
      post password_resets_path,
         params: { password_reset: { email: @user.email } }
      assert_not_equal @user.reset_digest, @user.reload.reset_digest
      assert_equal 1, ActionMailer::Base.deliveries.size
      assert_not flash.empty?
      assert_redirected_to root_url
    #(5)パスワード再設定フォームのテストを行う。
      user = assigns(:user)
    #(6)無効なメールアドレスで、パスワード再設定フォームへGETリクエストを送信させる記述を行う。
      get edit_password_reset_path(user.reset_token, email: "無効なメアド")
      assert_redirected_to root_url
    #(7)ここでユーザーを無効なユーザーに切り替える処理を記述(攻撃者によるアカウント乗っ取りのテスト)。
      user.toggle!(:activated)
      get edit_password_reset_path(user.reset_token, email: user.email)
      assert_redirected_to root_url
    #(8)そして今度は有効なユーザーで(トークンは無効)。
      get edit_password_reset_path('無効なトークン', email: user.email)
      assert_redirected_to root_url
    #(9)有効なメールアドレスとトークンで、パスワード再設定フォームヘGETリクエストを送信させる記述を行う。
      get edit_password_reset_path(user.reset_token, email: user.email)
      assert_template 'password_resets/edit'
      assert_select "input[name=email][type=hidden][value=?]", user.email
    #(10)無効なパスワードとパスワード確認を、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。
      patch password_reset_path(user.reset_token),
        params: { email: user.email,
                  user: { password:              "無効なパス",
                          password_confirmation: "無効なパス確認" } }
      assert_select 'div#error_explanation'
    #(11)パスワードとパスワード確認入力が空のまま、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。
      patch password_reset_path(user.reset_token),
        params: { email: user.email,
                  user: { password:              "",
                          password_confirmation: "" } }
      assert_select 'div#error_explanation'
    #(12)有効なパスワードとパスワード確認入力で、PasswordResetsコントローラのupdateアクションヘPATCHリクエストが送信された場合の記述をする。
      patch password_reset_path(user.reset_token),
        params: { email: user.email,
                  user: { password:              "有効なパス",
                          password_confirmation: "有効なパス確認" } }
      assert is_logged_in?
      assert_not flash.empty?
      assert_redirected_to user
    end
  end


最後に

 以上で後編のパスワード再設定機能の本実装が完了しました。
 前編→パスワード再設定用のリソース作成
 中編→パスワード再設定メール送信機能の実装

参考

 Railsチュートリアル 第12章パスワードの再設定

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

Kinx ライブラリ - Binary

Binary

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。

今回は Binary です。

バイナリ(Binary)は配列(Array)に似ているが、保持できる値が 0x00 ~ 0xFF までのバイト列である点が異なる。内部実装的にも単純にバイトの Vector として実装されており、バイナリデータを格納するのに適している。

Binary 特殊オブジェクト

Binary にも特殊オブジェクト Binary があり、Binary オブジェクトに対して関数定義することができる。尚、後述するがバイナリデータはスプレッド演算子を使って配列と相互変換できる。以下の例はバイナリを配列に変換して配列に対して map している例となる。

Binary.toStringArray = function(bin) {
    return [...bin].map(&(e) => *e);
};
System.println([...<97, 98, 99>]);            // => [97, 98, 99]
System.println(<97, 98, 99>.toStringArray()); // => ["a", "b", "c"]

Binary

ファイル・ロード

バイナリデータは以下のように File.BINARY を指定することでファイルから読み込むこともできる。指定しなかった場合は File.TEXT と同様、文字列としてファイルを読み込む。

var file = File.load("filename", File.BINARY);

組み込み特殊メソッド

Binary には Array と同じようなメソッドが登録されている。が、基本的にバイナリ列に適用することになるため、あまり複雑なことはできない。pushpopunshiftshift は破壊的な操作を行う。

メソッド 意味
Binary.length(bin) バイナリ bin の要素数を返す。
Binary.push(bin, e) バイナリ bin の最後の要素として e を追加する。
Binary.pop(bin) バイナリ bin の最後の要素を返し、要素から削除する。
Binary.unshift(bin, e) バイナリ bin の最初の要素として e を追加する。
Binary.shift(bin) バイナリ bin の最初の要素を返し、要素から削除する。後ろの要素は順次先頭方向に移動する。
Binary.join(bin, sep, fmt) バイナリ bin の要素を全てつなげた文字列として返す。要素と要素の間は sep でつなぐ。fmt が省略された場合 "0x02x" でフォーマットされる。
Binary.reverse(bin) バイナリ bin の要素を逆順にした新たなバイナリを返す。
Binary.toString(bin, sep, fmt) バイナリ bin を文字列化して返す。デフォルトのセパレータは ", "fmt が省略された場合 "0x02x" でフォーマットされる。
Binary.apply(bin, func) func(bin) を実行する。
Binary.each(bin, callback) bin の各要素 e に対して callback(e, index) を実行する。
Binary.map(bin, callback) bin の各要素 e に対して callback(e, index) の結果の集合となる新たなバイナリを返す。
Binary.filter(bin, callback) bin の各要素 e に対して callback(e, index) の結果が真となるもののみの集合として新たなバイナリを返す。
Binary.reject(bin, callback) bin の各要素 e に対して callback(e, index) の結果が偽となるもののみの集合として新たなバイナリを返す。
Binary.reduce(bin, callback, initr) 前の要素までの結果を r (初期値は initr、指定されていなければ null)として、ary の各要素 e に対し callback(r, e) を行った最終結果を返す。
Binary.sort(bin, comp) 任意の bin の要素 e1, e2 に対する comp(e1, e2) の結果を利用して bin 全体をソートした新たなバイナリを返す。
Binary.clone(bin) bin のコピーを作成する。
Binary.println(bin) bin の各要素 e に対して System.println(e) を適用する。

特殊オペレーター

後置 [] オペレーター

バイナリの要素にアクセスする。

var e = ary[5];  // index は 0 はじまりなので 6 番目の要素

左辺値にもなり、バイト列を直接書き換えることができる。

var bin = <1,2,3,4,5>;
bin[2] = 255;
System.println(bin);  // => <0x01, 0x02, 0xff, 0x04, 0x05>

二項 + オペレーター

バイナリ同士をつなげる。右辺値がバイナリではない場合、SystemException("Unsupported Operator") 例外が発生する。

var bin = <1, 2, 3>;
var r = bin + <2, 4>;  // => <0x01, 0x02, 0x03, 0x02, 0x04>

単項 * オペレーター

単項 * オペレーターをバイナリに適用した場合、バイナリを文字列に変換する。

var a = *<97, 98, 99>;  // => "abc"

尚、文字列に単項 * オペレーターを適用すると配列になるため、バイナリには戻らない。代わりに、バイナリは下記のスプレッド演算子で配列と相互変換ができる。

スプレッド演算子

配列とバイナリで相互変換可能。配列からバイナリに変換する際は、数値であれば 0x00~0xFF の値に丸められる。文字の場合は先頭の文字のみ使われる。

var a = <97, 98, 99>; // == <0x61, 0x62, 0x63>
var b = [...a];       // => [97, 98, 99]
var c = <...b>;       // => <0x61, 0x62, 0x63>
var d = <...[1024,1025,1026,"abc"]>;  // => <0x00, 0x01, 0x02, 0x61>
                                      //    0x61 == 97 == 'a'

おわりに

バイナリデータも割と扱いたくなるので、一通り機能を揃えてみました。バイト単位でゴニョゴニョできると良いことあるかもしれない。なんとなく例えば x64 向け JIT ライブラリとか作れそうですね。

では、また次回。

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

アップデートによりシェルスクリプトがbashからzshに変わった時の対処方法

まえがき

2020/3末、macをアップデートしてからrails sでサーバを立ち上げると、

Your Ruby version is 2.6.3, but your Gemfile specified 2.6.5

となるので、

bundle install

で、一旦はサーバーが立ち上がるようになるのですが、翌日などに立ち上げると、表示が出て立ち上がらなくなった

状況確認

① Rubyのバージョンの確認

ruby -v

すると、

ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]

2.6.5はインストールしているのですが、2.6.3はインストールした記憶がない…(したのかな?)

② rbenvの確認

which ruby

すると、

ユーザー名@mba desktop % which ruby
/usr/bin/ruby

このように表示された場合、rbenvが反映されていません。システムのRubyを参照したままになっています。

③ シェルスクリプトの確認

echo $SHELL
# 結果がbin/bashの場合
cat ~/.bash_profile
# 結果がbin/zshの場合
cat ~/.zshrc

すると、結果が、

bin/zsh

で、

cat ~/.zshrc

したところ、

cat: /Users/ユーザー名/.zshrc: No such file or directory

となりました。

次に、

echo 'eval "$(rbenv init -)"' >>  ~/.zshrc

を実行後、再度、

cat ~/.zshrc

を実行、結果は、

eval "$(rbenv init -)"

原因

アップデートした際、シェルスクリプトがbashからzshに変わった事で異常が起きている。

対処法

①open ~/.zshrc
でファイルを開き、以下に置き換える。

# Homebrew
export PATH=/usr/local/bin:$PATH
# Homebrew rbenv
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"

その上で、次を実行。

source ~/.zshrc
rbenv global 2.6.5

ターミナルを再起動して、

ruby -v

を実行。結果は、

ruby 2.6.5p114 (2019-10-01 revision 67812)

と表示されました。次に、

which ruby 

を実行。

/Users/ユーザー名/.rbenv/shims/ruby

と表示されました。これでとりあえず大丈夫です。

あとがき

神里よしとさんの主宰するプログラミングのオンラインサロン「人生逆転サロン」のメンターのたけさんに質問して教えて頂いた流れを忘れないように、ここに書き留めておきます。

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

【Ruby】Ruby入門

java、python、javascript、Kotlin、Go、PHPに続きRubyを勉強しました。

【Ruby】Ruby入門

「順次進行」「条件分岐」「繰り返し」

実行

Hello.rb
print "Hello"
print "Hello!"
puts "Hello"
p "Hello!"
出力
HelloHello!Hello
"Hello!"

変数

Variable.rb
name = "aoki"
p ("私は" + name + "です")
出力
"私はaokiです"

オブジェクト

Obu.rb
p 30.div(5)
p 22.remainder(3)

puts"Good Hello".class

puts"Goodmorning".index("d")
出力
6
1
String
3

配列

Array.rb
a = [["sato","susuki"],["shougayaki","tyoko"]]

p "朝食担当は" + a[0][0]+"です"
p "昼食担当は" + a[0][1]+"です"
p "夕飯は" + a[1][0]+"です"
p "おやつは" + a[1][1]+"です"
出力
"朝食担当はsatoです"
"昼食担当はsusukiです"
"夕飯はshougayakiです"
"おやつはtyokoです"

演算子

Operator.rb
x = 5
y = 2
z = 5

p x == 2 || y == 2
p x =! y
p z >=1 && z <= 6
出力
true
false
true

条件分岐

Humidity.rb
hum = 70

if hum >= 60
    p hum.to_s + "%"
    p "ジメジメです"
elsif hum <= 60 && hum >= 40
    p hum
    p "快適です"
else hum <= 40
    p hum
    p "カラカラです" 
end
出力
"70%"
"ジメジメです"

繰り返し

Loop.rb
arr = [2,3,4,5,6,7]
sum = 0

for i in arr 
    sum += i
end

p sum
出力
27

メソッド

Me.rb
def div (a,b,c)
    return(a + b + c) / 4 
end

result = div(21, 21 , 21)
p result
出力
15

クラス

Class.rb
class Huu
    def initialize(name)
        # オブジェクトを生成をすると値を渡すことが出来る
        @name = name
        # インスタンス変数
    end

    def name
        puts "hey guy this is #{@name} "
    end
end

huu01 = Huu.new("sato")
huu01.name()
出力
hey guy this is sato

環境構築

windowsでVScodeRubyをインストールしました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】seedから画像をdbに保存する方法

seedデータからdbに画像を保存するやり方です。
本番環境でもできます。

(db/seeds.rb)

20.times do |n|
 name = Faker::Games::Pokemon.name
 email = Faker::Internet.email
 password = 'password'
 User.create!(
  name: name,
  email: email,
  password: password,
  password_confirmation: password,
  icon: open("./db/fixtures/icon#{n}.jpg")
 )
end

こんな感じで
icon: open("./db/fixtures/icon#{n}.jpg")
で呼び出しています。
自分は(db/fixtures)の中にicon0.jpgicon1.jpgみたいな感じで20個jpgファイルを置きました。
ご参考までに

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

Rails で PostgreSQL のパーティションを利用したい

なにこれ?

Ruby on Rails で PostgreSQL のハッシュパーティションを使いたいよねー、という方!こうすればできました。たぶん、rails db:migrate or db:rollback したら DDL が発行されてよしなに設定されるはず。

サンプルコード

class CreateStaffs < ActiveRecord::Migration[6.0]
  def change
    reversible do |dir|
      dir.up do
        execute <<~"SQL"
                    CREATE TABLE staffs (id uuid PRIMARY KEY UNIQUE, email varchar NOT NULL, cname varchar NOT NULL, created_at timestamp(6) NOT NULL, updated_at timestamp(6) NOT NULL) PARTITION BY HASH (id);
                    #{(0..99).map{|num| "CREATE TABLE staffs_partition_#{num} PARTITION OF staffs FOR VALUES WITH(MODULUS 100,REMAINDER #{num});"}.join}
        SQL
      end
      dir.down do
        execute <<-SQL
                   DROP TABLE staffs;
        SQL
      end
    end
  end
end

おわりに

PostgreSQL 11 からの機能なので、使えない環境の方もいらっしゃると思われます。が、あとで楽できそうなので必要に応じてパーテイションを作っておくことが良さそうですね。

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

Rubyの基礎知識・文法について

背景

Rubyの基礎知識を備忘録としてのこします。

基礎文法

出力の仕方
puts ”文字列”or

コメントアウト
# コメントアウト

文字列の連結
”文字列” + ”文字列”

変数の定義
変数名 = 

変数展開
文字列の中に#{変数名}とする。

if
if 条件式
    処理
elsif 条件式
    処理
else
    処理
end

比較演算子
a==b  # aとbが等しい
a!=b  # aとbは異なる

配列
配列名=[1,2,3]

each
配列名.each do |変数名|
    処理
end

ハッシュ
ハッシュ名={キー1:1,キー2:2,}

メソッドの定義方法
def メソッド名
    処理
end

クラスの定義方法
class クラス名(大文字で始める)
    処理
end

インスタンスを生成
変数名=クラス名.new

クラス内で変数を作る方法
attr_accessor :変数名
※インスタンスを生成すると同時に生成される

クラス内で変数を作る
self.変数名

他ファイルでクラスを使う方法
require ./ファイル名"

クラスの継承方法

class 子クラス<親クラス

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

【Ruby on Rails】Railsをインストールする

皆さまこんにちは。

前回Railsのインストールまで終わったので本日は早速アプリケーションを作っていきます。

新規アプリケーションの作成

今回はbodymakeという名前のアプリケーションを作っていきます。

$ rails _5.2.1_ new bodymake -d mysql

実行後みてみると、『bodymake』とできていますね!
その下にあるREADME.mdは不要なので削除してしまいましょう。
右クリック→deleteで削除できます。

Gemfileを編集

赤枠に該当する箇所をそのように修正しましょう。
bodymake - AWS Cloud9 (1).png

さらに同じGemfileの一番下に下記を追加しましょう。

gem 'pry-rails'
gem 'compass-rails', '3.1.0'
gem 'sprockets', '3.7.2'
gem 'hirb'
gem 'hirb-unicode'

railsでは必要に応じてこのgemを随時追加していきます。

Gemfile.lockを更新する

gemを追加しましたがこのままではまだ実行されておりません。
実行するために下記を入力しましょう。

$ cd bodymake

僕の場合bodymakeというディレクトリを作ったのでそっちに移動します。
他の名前で作った人は『cd 作ったディレクトリ名』を入力してください。

$ pwd
/home/ec2-user/environment/bodymake

pwdと入力してディレクトリが移動したか確認しましょう。
bodymakeと記載があるのでうまく移動してます。

$ bundle update

上記実行するとGemfileを更新することができます。

ひとまずこれでRailsのインストールが完了しました。

次回はデータベース周りの準備をしていきます。

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

ArgumentError Nil location provided. Can't build URI. が出た時の対処法  プロフ画像の実装

ArgumentError 


前にも何回かお見かけしたエラー
その時はなんとなく解決できたが今回はとても詰まってしまったので忘れないうちに残しておく


そもそもArgumentErrorとは?

直訳すると変数のエラー Argument = 引数 と言うことを今回初めて知った(今更)

主に引数の個数が合っていない場合とか引数の中身が空とかそんな時に出るエラーらしい

スクリーンショット 2020-04-21 16.02.20.png

詳しく内容を見ていくとTopicsのindexビューにてエラー
赤線の12行目がおかしいよ

Nil location provided. Can't build URI.
翻訳すると

『提供された場所はありません。 URIを構築できません。』

そもそも何をしていてこんなエラーが出たかと言うと、ある場所にユーザーが登録したプロフィール画像を表示したいと思いコードを書いてました。

書いたコード

<% if topic.user.profile_photo == nil %>
  <%= image_tag “icons/profile-icon.png”, class:”profile_photo_mini” %> %>
<% else %>
  <%= image_tag topic.user.profile_photo.url, class:”profile_photo_mini” %>
<% end %>

よくある簡単なif文で、もしこのトピックに対するユーザーのプロフィール画像がnilの場合はあらかじめ用意してあるテスト画像を表示
そうでない場合(この場合プロフィール画像が登録してある場合)はその登録している画像を表示する と言ったコードです。

このprofile_photo画像はユーザー登録の際にマストではないので当然登録していないユーザーもいます。 

なのでif文で逃げ道を作ったのですがどうもこのif文が思ったように機能していない。

他の方の記事を見ると、画像登録していないユーザーを消せば解決!!みたいなことが書いてありました。確かにエラーは無くなりますが、それは自分が求めている内容ではないので再び思考。

エラー内容に戻り

Nil location provided. Can't build URI.

『提供された場所はありません。 URIを構築できません。』

んーURLかと思ったらURIなのね 
てかURIって何?

再びぐぐるとこんな記事を発見(寿司に例えてて面白い)
https://webtan.impress.co.jp/e/2010/03/09/7539
URI(Uniform Resource Identifier)といいURLより広義に使われるようです

よく考えたら違うんですが、画像の指定場所が違うのかな?とか思って確認するも問題なし。具体的に画像のパスを指定するもエラー改善せず。


おかしいなあif文の書き方も間違っていないはずなのになー

ニッチもサッチもいかなくなりメンターの方に相談。

ここから解決方法!!

<% if topic.user.profile_photo == nil %>
 <%= image_tag “icons/profile-icon.png”, class:”profile_photo_mini” %> %>
<% else %>
<%= image_tag topic.user.profile_photo.url, class:”profile_photo_mini” %>
<% end %>

この一行目の部分を

<% if topic.user.profile_photo.blank? %>

に書き換えることでちゃんとif文が機能するようになりました。

要は中身がnilではなくて空なのでうまくいかなかったようです。

.blank? でnilの場合と空の場合trueになります。

rails cで中身を確認したら表示はnilになっているのになんでなんだー!!
意味わからん!!と質問したら


今は深く考えなくていいです。 画像の場合は特殊な考え方ですからそう言うものだと覚えれば大丈夫です。とのこと


一応説明はしてくれたのですが僕の方の理解が追いつかず深い話ができませんでしたorz


ImageUploaderのクラスや CarrierWaveが関係しているらしいことまでは分かりました。


取り急ぎ解決しましたので参考までにぜひ!!

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

ArgumentError Nil location provided. Can't build URI. が出た時の対処法 プロフ画像の実装

ArgumentError 


前にも何回かお見かけしたエラー
その時はなんとなく解決できたが今回はとても詰まってしまったので忘れないうちに残しておく


そもそもArgumentErrorとは?

直訳すると変数のエラー Argument = 引数 と言うことを今回初めて知った(今更)

主に引数の個数が合っていない場合とか引数の中身が空とかそんな時に出るエラーらしい

スクリーンショット 2020-04-21 16.02.20.png

詳しく内容を見ていくとTopicsのindexビューにてエラー
赤線の12行目がおかしいよ

Nil location provided. Can't build URI.
翻訳すると

『提供された場所はありません。 URIを構築できません。』

そもそも何をしていてこんなエラーが出たかと言うと、ある場所にユーザーが登録したプロフィール画像を表示したいと思いコードを書いてました。

書いたコード

<% if topic.user.profile_photo == nil %>
  <%= image_tag “icons/profile-icon.png”, class:”profile_photo_mini” %> %>
<% else %>
  <%= image_tag topic.user.profile_photo.url, class:”profile_photo_mini” %>
<% end %>

よくある簡単なif文で、もしこのトピックに対するユーザーのプロフィール画像がnilの場合はあらかじめ用意してあるテスト画像を表示
そうでない場合(この場合プロフィール画像が登録してある場合)はその登録している画像を表示する と言ったコードです。

このprofile_photo画像はユーザー登録の際にマストではないので当然登録していないユーザーもいます。 

なのでif文で逃げ道を作ったのですがどうもこのif文が思ったように機能していない。

他の方の記事を見ると、画像登録していないユーザーを消せば解決!!みたいなことが書いてありました。確かにエラーは無くなりますが、それは自分が求めている内容ではないので再び思考。

エラー内容に戻り

Nil location provided. Can't build URI.

『提供された場所はありません。 URIを構築できません。』

んーURLかと思ったらURIなのね 
てかURIって何?

再びぐぐるとこんな記事を発見(寿司に例えてて面白い)
https://webtan.impress.co.jp/e/2010/03/09/7539
URI(Uniform Resource Identifier)といいURLより広義に使われるようです

よく考えたら違うんですが、画像の指定場所が違うのかな?とか思って確認するも問題なし。具体的に画像のパスを指定するもエラー改善せず。


おかしいなあif文の書き方も間違っていないはずなのになー

ニッチもサッチもいかなくなりメンターの方に相談。

ここから解決方法!!

<% if topic.user.profile_photo == nil %>
 <%= image_tag “icons/profile-icon.png”, class:”profile_photo_mini” %> %>
<% else %>
<%= image_tag topic.user.profile_photo.url, class:”profile_photo_mini” %>
<% end %>

この一行目の部分を

<% if topic.user.profile_photo.blank? %>

に書き換えることでちゃんとif文が機能するようになりました。

要は中身がnilではなくて空なのでうまくいかなかったようです。

.blank? でnilの場合と空の場合trueになります。

rails cで中身を確認したら表示はnilになっているのになんでなんだー!!
意味わからん!!と質問したら


今は深く考えなくていいです。 画像の場合は特殊な考え方ですからそう言うものだと覚えれば大丈夫です。とのこと


一応説明はしてくれたのですが僕の方の理解が追いつかず深い話ができませんでしたorz


ImageUploaderのクラスや CarrierWaveが関係しているらしいことまでは分かりました。


取り急ぎ解決しましたので参考までにぜひ!!

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

#Ruby #rspec VCR でVCR::Errors::UnhandledHTTPRequestError が...

  • わりと不親切なエラーメッセージで、何が悪いのか分かりづらい気がする
  • specの文法を間違っているとエラーが発生する
  • 1回目のリクエスト ( VCR casettes を作ったときのリクエスト ) と 2回目以降のオフライン実行で、リクエストURLが異なるとエラーが発生する
  • filter_sensitive_data の設定で特定の値をマスクしており、それがURL中にも利用されている場合、2回目以降のオフライン実行で実際のリクエストURLが異なっても、テストが通る (そもそも元のリクエストURLがcassettesに記録されていないので、考えてみれば当たり前か)
# https://relishapp.com/vcr/vcr/v/5-1-0/docs/getting-started

# gem install vcr
# gem install webmock
# gem install faraday

require "vcr"
require "faraday"
require "pry" # for debug

VCR.configure do |c|
  c.hook_into :webmock
  # need `mkdir ./casettes`
  c.cassette_library_dir = 'cassettes'
  c.debug_logger = File.open("vcr.log", 'w')
  c.filter_sensitive_data("<SECRET_KEY>") { ENV["SECRET_KEY"] }
end

describe do
  # OK
  it do
    VCR.use_cassette('example1') do
      response = Faraday.get("https://example.com")
      expect(response.success?).to be true
    end
  end

  # NG
  # VCR.use_cassette must be in "it"
  #      VCR::Errors::UnhandledHTTPRequestError:
  #
  #
  #        ================================================================================
  #        An HTTP request has been made that VCR does not know how to handle:
  #          GET https://example.com/
  #
  #        There is currently no cassette in use. There are a few ways
  #        you can configure VCR to handle this request:

  VCR.use_cassette('example2') do
    it do
      response = Faraday.get("https://example.com")
      expect(response.success?).to be true
    end
  end

  # Once OK but Twice NG
  # Because request URL different everytime
  #
  #   VCR::Errors::UnhandledHTTPRequestError:
  #
  #
  #   ================================================================================
  #   An HTTP request has been made that VCR does not know how to handle:
  #     GET https://example.com/?param=0.20117927761663523

  #   VCR is currently using the following cassette:
  #     - /Users/yumainaura/example3.yml
  #       - :record => :once
  #       - :match_requests_on => [:method, :uri]

  it do
    VCR.use_cassette('example3') do
      response = Faraday.get("https://example.com?param=#{Random.rand}")
      expect(response.success?).to be true
    end
  end

  # OK even if ENV SECRET_KEY different between every run
  #
  # e.g
  #   SECRET_KEY=xxx rspec vcr_unhandled_failure.rb
  #   SECRET_KEY=yyy rspec vcr_unhandled_failure.rb
  #   SECRET_KEY=zzz rspec vcr_unhandled_failure.rb
  #
  #
  # VCR cassettes:
  #
  # ---
  # http_interactions:
  # - request:
  #     method: get
  #     uri: https://example.com/?param=<SECRET_KEY>
  #     body:
  #       encoding: US-ASCII
  #       string: ''
  it do
    VCR.use_cassette('example4') do
      response = Faraday.get("https://example.com?param=#{ENV.fetch("SECRET_KEY")}")
      expect(response.success?).to be true
    end
  end
end

# guid in error message
#        ================================================================================
#        An HTTP request has been made that VCR does not know how to handle:
#          GET https://example.com/

#        There is currently no cassette in use. There are a few ways
#        you can configure VCR to handle this request:

#          * If you're surprised VCR is raising this error
#            and want insight about how VCR attempted to handle the request,
#            you can use the debug_logger configuration option to log more details [1].
#          * If you want VCR to record this request and play it back during future test
#            runs, you should wrap your test (or this portion of your test) in a
#            `VCR.use_cassette` block [2].
#          * If you only want VCR to handle requests made while a cassette is in use,
#            configure `allow_http_connections_when_no_cassette = true`. VCR will
#            ignore this request since it is made when there is no cassette [3].
#          * If you want VCR to ignore this request (and others like it), you can
#            set an `ignore_request` callback [4].

#        [1] https://www.relishapp.com/vcr/vcr/v/5-1-0/docs/configuration/debug-logging
#        [2] https://www.relishapp.com/vcr/vcr/v/5-1-0/docs/getting-started
#        [3] https://www.relishapp.com/vcr/vcr/v/5-1-0/docs/configuration/allow-http-connections-when-no-cassette
#        [4] https://www.relishapp.com/vcr/vcr/v/5-1-0/docs/configuration/ignore-request
#        ================================================================================
#      # /Users/yumainaura/ghq/github.com/YumaInaura/YumaInaura/rspec/vcr.rb:27:in `block (3 levels) in <top (required)>'

# Finished in 0.0327 seconds (files took 0.72476 seconds to load)
# 1 example, 1 failure

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/3084

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

#Ruby + #rspec + VCR で 外部接続のテストをする、わりと最小の書き方

code

# https://relishapp.com/vcr/vcr/v/5-1-0/docs/getting-started

# gem install vcr
# gem install webmock
# gem install faraday

require "vcr"
require "faraday"
require "pry" # for debug

VCR.configure do |c|
  c.hook_into :webmock
  c.cassette_library_dir = 'cassettes'
  c.debug_logger = File.open("vcr.log", 'w')
end

describe do
  it do
    VCR.use_cassette('example') do
      response = Faraday.get("https://example.com")
      expect(response.success?).to be true
    end
  end
end

result

  is expected to equal true

Finished in 0.04331 seconds (files took 1.16 seconds to load)
1 example, 0 failures

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/3083

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

[Rails]TwitterAPI認証+deviseでのログインの実装方法

経緯

railsにdeviseをインストールした状態でTwitterAPI認証でのログインを実装しました。
色んなサイトを参考にした結果、迷走し2日以上かけてしまいました。
今後も使うかもしれない自分用の備忘録的サムシングとして、またみなさんにササッと実装いただけたらと思い投稿しました。

開発環境

ruby 2.7.0
rails 5.2.4
AWS EC2/RDS

前提条件

・Twitterアカウントがある。
・deviseインストール済のアプリがある。
・環境変数の設定ができている。
・本番環境でアプリの動作ができている状態。

TwitterのAPI認証取得および設定

a. API取得

https://qiita.com/kngsym2018/items/2524d21455aac111cdee
私はこちらのサイトを参考にしました。

callbackURLには以下のように入れてください。
URL : https://[自分のサイトのリンク]/users/auth/twitter/callback
(https化されてない場合は、うまく動作しないかもしれません。)

Terms of service URLとPrivacy policy URL何か埋めてください。次の項目で必須となってます。
こんな感じになります。
スクリーンショット 2020-04-20 20.45.44.png

b. 取得できたAPI KeyとAPI Secretはメモ
スクリーンショット 2020-04-20 20.36.28.png

c. Permissionsの設定
read onlyなどは用途に合わせて選んでください。
ユーザー登録にメルアドが必要なので「Request email addresses from users」チェック
スクリーンショット 2020-04-20 20.48.48.png

Gemの設定

Gemfile
gem 'omniauth'
gem 'omniauth-twitter'

忘れずに

$ bundle

API keyとAPI secretの設定

a. メモしたAPI keyとAPI secretを.envファイルに記述します。
ここではそれぞれの名前はTWITTER_CONSUMER_KEYとTWITTER_CONSUMER_SECRETにしています。
ドメインネーム(アプリのドメインorIPアドレス)も記述します

.env
TWITTER_CONSUMER_KEY = "****************************"
TWITTER_CONSUMER_SECRET = "****************************"

DOMAIN_NAME = "*****************"

b. .gitignoreに記述

.gitignore
.
.
.env
.
.

deviseの設定

記述した認証用のキーをdeviseが識別できるように記述します。

config/initializers/devise.rb
config.omniauth :twitter, ENV['TWITTER_CONSUMER_KEY'], ENV['TWITTER_CONSUMER_SECRET'], scope: 'email', oauth_callback: "#{ENV['DOMAIN_NAME']}/users/auth/twitter/callback"
OmniAuth.config.logger = Rails.logger if Rails.env.development? # debug用

UserモデルとTwitter認証モデルを紐付ける

a. deviseに:omniauthable, omniauth_providers: [:facebook, :twitter, :google_oauth2]を追加
facebookとグーグル認証も将来的に使うかもしないので一応付けておきます。

app/models/user.rb
devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :validatable,
       # 外部API認証用に以下追加
       :omniauthable, omniauth_providers: [:facebook, :twitter, :google_oauth2]

b. マイグレーションファイルにカラムの追加

$ rails g migration AddColumnsToUsers uid:string provider:string
$ rails db:migrate

c. Userモデルにfindメソッドを実装

app/models/user.rb
# Twitter認証ログイン用
# ユーザーの情報があれば探し、無ければ作成する
def self.find_for_oauth(auth)
  user = User.find_by(uid: auth.uid, provider: auth.provider)

  user ||= User.create!(
    uid: auth.uid,
    provider: auth.provider,
    name: auth[:info][:name],
    email: User.dummy_email(auth),
    password: Devise.friendly_token[0, 20]
  )

  user
end

def self.dummy_email(auth)
  "#{Time.now.strftime('%Y%m%d%H%M%S').to_i}-#{auth.uid}-#{auth.provider}@example.com"
end

メールアドレスでの認証も実装している場合は、OAuthでの認証時もメールアドレスを保存する必要があります。
emailはダミー用のアドレスを作成して代用します。
email中に時間まで記載しているのは、認証後に退会などで論理削除された後も再度認証ログインできるように今回は設定しています。

d. omniauthコントローラーの設定
deviseのusers配下にあります。
無い方は$ rails g devise:controllers usersを実行すると見えるようになります。

app/controllers/users/omniauth_callbacks_controller.rb
def twitter
  callback_from :twitter
end

private
  # コールバック時に行う処理
  def callback_from(provider)
    provider = provider.to_s

    @user = User.find_for_oauth(request.env['omniauth.auth'])

    # persisted?でDBに保存済みかどうか判断
    if @user.persisted?
      # サインアップ時に行いたい処理があればここに書きます。
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth']
      redirect_to new_user_registration_url
    end
  end

ルーティングの設定

omniauth_callbacks_pathの追加

config/routes.rb
devise_for :users, :controllers => {
    :registrations => 'users/registrations',
    :sessions => 'users/sessions',
    :passwords => 'users/passwords',
    # このpathを通して外部API認証が行われる。
    :omniauth_callbacks => 'users/omniauth_callbacks'
   }

ビューに認証用リンクの設定

書き方はなんでもありだと思いますが一例として
a. loginページにリンク追加

app/views/devise/registrations/new.html.erb
<%= link_to user_twitter_omniauth_authorize_path do %>
  <i class="fab fa-twitter-square fa-2x"></i>
<% end %>

b. signupページにリンク追加

app/views/devise/sessions/new.html.erb
<%= link_to user_twitter_omniauth_authorize_path do %>
  <i class="fab fa-twitter-square fa-2x"></i>
<% end %>

c. cssにtwitterアイコンのカラーを追加

app/assets/stylesheets/application.css.scss
.fa-twitter-square {
  color: #1da1f2;
}

本番環境で動作確認

ここまでの操作でできているはずです。

a. アップロードの準備

.envのアップロード
ローカル環境でアプリケーション直下に移動して

$ scp -i ~/.ssh/practice-aws.pem .env ec2-user@××.××.×××.×××:[アプリ名]/

本番環境に入って、

// プリコンパイルを実行
$ bundle exec rails assets:precompile RAILS_ENV=production
// マイグレートを実行
$ bundle exec rails db:migrate RAILS_ENV=production
// サーバー起動
$ rails s -e production

b. ビューの確認
スクリーンショット 2020-04-21 14.16.42.png
ツイッターのアイコンが表示出来ています。

c. 認証ログイン後確認
スクリーンショット 2020-04-21 14.19.30.png
ツイッターでの表示名でログインできていることが確認できました。

まとめ

TwitterAPIを使ってワンクリックでサインアップすることができるようになったため、よりユーザービリティが上がったと思います。
はじめてAPI認証でのログインに挑戦してみて、WEBのつながりが見えてきて非常に勉強になりました。

今回時間がかかった要因

a. ローカルでの試験運用ができなかったこと
TwitterDeveloperのwebsiteURLに他サイトで推奨されているようなhttp://127.0.0.1:3000 などを入れても「invalid URL」といったメッセージが返ってくるため、(他のやりようがあったみたいですが少々面倒だったので)ローカル→本番→試験→NGを繰り返していました。

結果的にはめちゃめちゃ時間くっちゃいましたので、面倒でもローカルでの有効化方法を模索すればよかったと反省です。良い方法があれば教えていただけると嬉しいです。

b. deviseでの認証の場合、やり方が通常と異なること
通常では/config/initializers下にomniauth.rbというファイルを新規作成してコントローラーも別で作ってってやり方になると思いますが、そちらでやっていたためdeviseが入ってるとログイン認証はデバイスが絡んでいるため、エラーになっていました。
deviseの便利さに甘えて、大事なことに気づくことが遅れてしまいました。反省です。

参考

https://qiita.com/kazuooooo/items/47e7d426cbb33355590e
https://qiita.com/zenizh/items/94aec2d94a2b4e9a1d0b
https://qiita.com/puremoru0315/items/f1d459b663fd3b715dee
https://kurose.me/twitter-omniauth/#Twitter

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

Railsでstubを使う

よく使う構文をメモ

post = Post.new

# 引数なし
allow(post).to receive(:title).and_return('title')

# 引数あり
allow(post).to receive(:title).with(arg1, arg2).and_return('title')

# エラーを起こさせる
allow(post).to receive(:subtitle).and_raise(StandardError.new)

# クラスに対してインスタンスメソッドのスタブ化
allow_any_instance_of(Post).to receive(:title).and_return('title')

参考

開発
Rails tips: RSpecでシンプルなスタブを使う(翻訳)

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

if文を簡単に

if文をワンライナーで書くには?

str = "is this a book?"
if str.include?("?")
puts "yes, it is."
end

str = "is this a book?"
puts "yes, it is." if str.include("?")

学んだこと
・オブジェクト名.include?("文字列"とか配列)
・include?は正規表現は使えないっぽい
・三項演算子というものもある

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

railsで一覧ページに[gem kaminari]を導入する流れについてまとめてみた。

kaminariの導入の仕方

1.Gemfileにkaminariを追加して保存。

Gemfile.
gem 'kaminari','~> 1.1.1'

2.kaminariをインストールする

$ bundle install

3.kaminariの設定ファイルを作成する

$ rails g kaminari:config

4.kaminariがページャで利用するテンプレートを作成する

$ rails g kaminari:views default

5.ページャを実装する

ページネーションさせたい場所に

app/views/books/index.html.erb
    <%= @books.each do |book| %>
        :
        :
    <% end %>
    <%= paginate @books %> <"←これを追加">
app/controllers/books_controller.rb
@books = Books.all.page(params[:page]).per(10) <"←.page以降追加">

この場合1ページの表示件数が10件まで表示される。

10件以上投稿するとこのように次のページへと分かれる。
スクリーンショット 2020-04-10 14.31.07.png

kaminariにBootstrapを適用させる

Bootstrapをすでに導入していれば簡単にkaminariに適用させることができる。

$ rails g kaminari:views bootstrap3

これでapp/views/kaminariフォルダにBootstrap用のViewが生成され、Bootstrap用のテンプレートに適用される。

参考サイト

https://qiita.com/ayies128/items/f5fec87df31b39d84f23

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

YouTubeっぽいコメント欄を作ってみよう!(返信機能追加)

はじめに

こんにちは!今回はYouTubeっぽいコメント欄を作ってみようと思います!
色々調べていたのですが、似たような記事がなかったので自力で実装した部分が多いです(><)
もっと良い方法があるかもしれないので、今後アップグレードしていければと思います。

それではいきましょう!

完成イメージ

exhoop_200420_reply.gif

ユーザーアイコンの実装については過去記事を参照ください。
https://qiita.com/naoki00m/items/6430ab0b62766b582c9a

開発環境

  • ruby 2.5.1
  • Rails 5.2.3
  • mysql
  • Haml 5.1.2
  • Ruby Sass 3.7.4

前提条件

  • 投稿に紐づくコメント機能が実装できていること(TECH CAMPでいうところのPictweetコメント機能)

実装内容

  • どのコメントに対する返信かを保存するカラムを追加
    (Commentテーブルにreplyカラムを追加)

  • コメントに対する返信フォームを作成
    (form_withを使用)

  • 返信フォームを表示するためのボタンを作成
    (FontAwesomeを使用)

  • 返信数をコメントの下に表示
    (replyカラム内の値を計算)

  • 返信数をクリックして返信を閲覧表示

テーブル

Comment テーブル

Column Type Options
user_id integer null: false
video_id integer null: false
text text null: false
reply integer

Assosiation

  • belongs_to :user
  • belongs_to :video

実装手順

1. どのコメントに対する返信かを保存するカラムを追加

Commentテーブルにreplyカラムを追加します。これはどのコメントに対する返信かを表すカラムです。
id:1のコメントに対してid:2で返信した場合、idが2のレコードにあるreplyカラムに1が保存されるといった具合です。

ターミナル.
$ rails g migration AddReplyToComment reply:integer

生成されたマイグレーションファイルを反映させましょう。

ターミナル.
$ rails db:migrate

2. コメントに対する返信フォームを作成 + 返信フォームを表示するためのボタンを作成

まずはビューの実装から!

今回はVideoのshowアクション内でコメントを表示しています。一般的にはPostのshowアクションと同義です。

show.html.haml
(省略)
- @comments.each do |comment|
  -# replyカラムがnull(空)の場合(返信ではない場合)
  - if comment.reply.blank?
    .content__show__comment__bottom__top
      .content__show__comment__bottom__top__user
        -# ユーザーアイコンを表示
        .content__show__comment__bottom__top__user__icon
          = link_to "/users/#{comment.user.id}" do
            - if comment.user.image.present?
              = image_tag comment.user.image.url, width: '100%'
            - else
              = image_tag('/images/default_user.jpg', width: '100%')
        -# ユーザー名を表示
        .content__show__comment__bottom__top__user__name
          = link_to "/users/#{comment.user.id}" do
            = comment.user.name
        -# 返信ボタン、削除ボタンを表示
        .content__show__comment__bottom__top__icons
          -# コメントした本人であれば返信ボタンと削除ボタンを表示
          - if user_signed_in? && current_user.id == comment.user.id
            .content__show__comment__bottom__top__icons__reply
              = link_to '#' do
                = icon('fa', 'reply', id: "reply-btn#{comment.id}")
            .content__show__comment__bottom__top__icons__delete
              = link_to video_comment_path(comment.video_id, comment.id), method: :delete do
                = icon('fa', 'trash-alt')
          -# ログインしているがコメントした人でない場合返信ボタンのみ表示
          - elsif user_signed_in? && current_user.id != comment.user.id
            .content__show__comment__bottom__top__icons__reply
              = link_to '#' do
                = icon('fa', 'reply', id: "reply-btn#{comment.id}")
          -# それ以外は何も表示しない
          - else
              = ''
  -# ユーザーがログインしている場合のみ返信できるようにする
  - if user_signed_in?
    .content__show__comment__bottom__reply
      -# idをcomment.idに応じて変化させる
      = form_with(model: [@video, @new_comment], local: true, id: "reply-form#{comment.id}") do |f|
        = f.text_area :text, placeholder: "Add a public reply...", id: "reply-text"
        -# 画面上には表示しないが、裏でreplyカラムに保存させるためにhidden_fieldを用いる!
        = f.hidden_field :reply, value: comment.id
        .content__show__comment__bottom__reply__btn
          .content__show__comment__bottom__reply__btn__cansel
            = 'Cansel'
          = f.submit "Reply", class: 'content__show__comment__bottom__reply__btn__send'

そしたらJqueryでコメントのフォームに動きをつけます!

  • 入力フォーム選択時にCansel, Replyボタンを表示する
  • Canselボタン選択時に入力フォームを非表示にする

ファイル名は◯◯.jsになっていればなんでもOKです!

video-comment.js
// 返信フォームの表示・非表示切り替え

$(function() {
  // replyという配列に1から1000までを順番に格納していく
  // 本当はビューやコントローラーからcomment.idを取ってきて格納できるのが理想
  var reply = [];
  for ( var i = 1 ; i <= 1000 ; i++ ) {
    reply.push(i);
  }
  // eachでreplyの中から1つずつ値を取り出しビューで定義したid名に付与
  $.each(reply, function(index, value) {
    // 入力フォームは初め隠しておく
    $("#reply-form" + value).hide();
    // 返信ボタンをクリックしたら入力フォームを表示
    $("#reply-btn" + value).on("click", function() {
      $("#reply-form" + value).show();
    });
    // Canselボタンをクリックしたら入力フォームを非表示に
    $(".content__show__comment__bottom__reply__btn__cansel").on("click", function() {
      $("#reply-form" + value).hide();
    });
  });
});

3. 返信数をコメントの下に表示

返信ができるようになったので試しに返信してみましょう!

テーブルのreplyカラムにidが保存されているか確認してください。
ビューに記載した「if comment.reply.blank?」を外すと返信も表示されますが、
コメントの下に紐づくような感じで表示はまだできていません。

replyカラム内の値を計算しよう!

さて、replyカラムに保存できるようになりましたが、コメントに対して何件の返信があるかを表示するには
コントローラーでこんな記述をする必要があります。

videos_controller.rb
class VideosController < ApplicationController
  (省略)

  def show
    (省略)
    @new_comment = Comment.new
    @comments = @video.comments.includes(:user)
    # Commentテーブルのreplyカラムの数をカウント
    @replies = Comment.group(:reply).count
  end
end

試しにビューファイルで下記のように記載してみるとこのように表示されます。

show.html.haml
= @replies

左側がreplyカラムの中身、右側がその合計数になります。
今回の例でいくとコメントが2件あり、コメント1に対する返信が3件、コメント2に対する返信が1件あるといった状況です。

{nil=>2, 1=>3, 2=>1}

この@repliesは配列の形になっているので下記のように書いてあげると合計数だけを取り出すことができます。

show.html.haml
= @replies[1]
3

これを用いて返信数を実装していきます!

show.html.haml
(省略)
.content__show__comment__bottom__view-btn
  -# 返信がなければ
  - if @replies[comment.id] == nil
    = ""
  -# 返信が1件なら
  - elsif @replies[comment.id] == 1
    = "View #{ @replies[comment.id] } reply"
  -# 返信が2件以上なら
  - else
    = "View #{ @replies[comment.id] } replies"

4. 返信数をクリックして返信を閲覧表示

最後に返信をそれぞれのコメントの下に表示できるようにしよう!

ここで重要となるのはコメントをeach doで繰り返し表示している中で
更にreplyを繰り返し表示させるということ。
こうすることでそれぞれのコメントの下に返信が紐づいているように見えます。

show.html.haml
-# 前の実装
- @comments.each do |comment|
  - if comment.reply.blank?
  (省略)
  -# ここから実装
  -# replyという変数を用意して、再度繰り返し表示
  - @comments.each do |reply|
    -# (重要)replyカラムがcomment.idと同じ場合のみ表示する
    - if reply.reply == comment.id
      .content__show__comment__bottom__view-reply
        .content__show__comment__bottom__view-reply__top
          .content__show__comment__bottom__view-reply__top__user
            .content__show__comment__bottom__view-reply__top__user__icon
              = link_to "/users/#{reply.user.id}" do
                - if reply.user.image.present?
                  = image_tag reply.user.image.url, width: '100%'
                - else
                  = image_tag('/images/default_user.jpg', width: '100%')
            .content__show__comment__bottom__view-reply__top__user__name
              = link_to "/users/#{reply.user.id}" do
                = reply.user.name
            .content__show__comment__bottom__view-reply__top__icons
              - if user_signed_in? && current_user.id == reply.user.id
                .content__show__comment__bottom__view-reply__top__icons__delete
                  = link_to video_comment_path(reply.video_id, reply.id), method: :delete do
                    = icon('fa', 'trash-alt')
              - else
                = ''
        .content__show__comment__bottom__view-reply__text
          // 改行を含んだテキストも表示できるようにsimple_formatを使用
          = simple_format(reply.text)

Jqueryで動きを加えて完了です!

video-comment.js
// 返信の表示・非表示切り替え
$(function() {
  $(".content__show__comment__bottom__view-reply").hide();
  $(".content__show__comment__bottom__view-btn").on("click", function() {
    // toggleにすると同じボタンを押したときに表示・非表示を切り替えてくれる
    $(".content__show__comment__bottom__view-reply").toggle();
  });
});

コード量は少し長いですが、ポイントを掴めれば実装できると思うので是非真似してみてください!

今後の追加実装予定

  • 返信を非表示にするときは「View」ではなく「Hide」にする
  • メンションをつけられるようにする
  • コメント・返信した際に通知としてストックされるようにする

参考

https://qiita.com/rockguitar67/items/4fd28cba0c243a8d0ba5
https://pikawaka.com/rails/count

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

rbenvを使用したRubyのバージョン管理

この記事は、rbenvがインストールされている事を前提に書いてあります。
インストールに関しては他記事をご参照してください。

目的

Rubyのバージョンを変えずに作業していると、簡単なrbenvのバージョン管理方法を忘れてしまいがちなので、すぐに思い出せるように簡潔にまとめました。

Rubyのインストール

今回はバーション2.5.7のインストールします。
他のバーションをインストールするときは「2.5.7」の部分を書き換えてください。

ターミナル
# インストールできるRubyのバージョンを一覧表示
$ rbenv install -l

# Ruby 2.5.7をインストール
$ rbenv install 2.5.7

# インストールしたRubyのコマンドを使えるようにする
$ rbenv rehash

# 既にインストールされているバージョンの確認
$ rbenv versions

バージョンの変更

PCの全体で使うバージョンの設定

ターミナル
$ rbenv global 2.5.7

カレントディレクトリだけのバーションの設定

ターミナル
$ rbenv local 2.5.7

不要になったバージョンの削除

ターミナル
$ rbenv uninstall 2.5.7

Ruby インストール後にrailsコマンドが使えないとき

Rubyのインストール後に、Railsを使おうとすると「rails: command not found」と表示されると思います。
なので、変更したバージョンにRailsをインストールしましょう。

ターミナル
# gem のupdate
$ gem update --system
# bundler のinstall
$ gem install bundler
# rails のinstall
$ gem install rails

Railsインストール部分はこちらの記事を引用させていただきました。こちらの方が詳しく書いてあるのでおすすめです。

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

見たら「ん?」となるエラーバーのグラフ

はじめに

実験でも数値計算でも、観測に誤差が伴うものをグラフにする時にはエラーバー(誤差棒)をつけると思います。エラーバーのつけ方には流儀がありますが、とりあえず1シグマ、つまり「誤差の乗り方がガウス分布だと仮定した時の標準偏差」をエラーバーとすることが多いです。

で、エラーバーは1シグマなので、「真の値をそのエラーバーの中に含む確率」が68%です。つまり観測点の3つに1つは「真の値」がエラーバーの範囲外にあることになります。なお、ここでは「真の値」を「観測を十分な回数繰り返した時に収束する値」のこととします。

さて、発表を見ていて、たまに「ん?」と思うようなグラフを見かけます。以下では、そんな「ん?」なグラフの実例と、その原因について見てみようと思います。

ケース1:正しい誤差棒

入力値$x$に対して、観測値$y$が$y=x$となる単純な系を考えましょう。ただし、観測するたびに誤差$\varepsilon$がのるものとします。これを素直にシミュレートすると、こんなスクリプトになるでしょうか。

def func(x)
  ysum = 0.0
  y2sum = 0.0
  n = 10
  n.times do
    y = x + (rand - 0.5) * 2.0
    ysum += y
    y2sum += y * y
  end
  ysum /= n.to_f
  y2sum /= n.to_f
  sigma = (y2sum - ysum ** 2) / (n - 1).to_f
  return ysum, Math.sqrt(sigma)
end

srand(1)

10.times do |i|
  x = i + 1
  y, e = func(x)
  puts "#{x} #{y} #{e}"
end

結果はこんな感じになります。

sample1.png

10点の観測点のうち、$y=x$の直線にエラーバーがかかっていない点が3点あります。この直線にエラーバーがかかる確率が68%なのですから、だいたい3つが外れることになります。こういうグラフを見ると、「あ、エラーバーがまっとうだな」と思うわけです。

ケース2:標準偏差のサンプル依存性その1

学校等で、標準偏差は観測サンプル数$N$に対して$1/\sqrt{N}$で小さくなることを学んだと思います。それを確認してみましょう。

0から1の値を取る一様乱数を$N$回観測し、その平均値と標準偏差を$N$に対してプロットしてみましょう。

こんな感じのスクリプトになるでしょうか。

def func(n)
  s = 0.0
  s2 = 0.0
  n.times do
    r = rand
    s += r
    s2 += r * r
  end
  s /= n.to_f
  s2 /= n.to_f
  sigma = Math.sqrt((s2 - s ** 2) / (n - 1))
  return s, sigma
end

srand(1)

10.times do |i|
  n = 2 ** (i + 2)
  a, e = func(n)
  puts "#{n} #{a} #{e}"
end

まず、標準偏差の$N$依存性はこんな感じです。

sample2_sigma.png

たしかに$N$を増やすに連れて、$1/\sqrt{N}$で小さくなっていくのがわかります。

次に、エラーバーを見てみましょう。

sample2_errorbar.png

$N$が大きくなるにつれてエラーバーが小さくなりますが、やはりエラーバーが$0.5$にかからない観測点が3点あることがわかります。

ケース3:標準偏差のサンプル依存性その2

「真の値をそのエラーバーの中に含む確率が68%」になるのは、ノイズが独立に入ること、つまり「観測点の間に相関が無い」時です。逆に、観測点の間に相関があるとおかしなことになります。

よくあるパターンの一つは、ある観測点が別の観測点のデータを共有してる場合です。

先ほどと同様に、標準偏差は観測サンプル数$N$に対して$1/\sqrt{N}$で小さくなることを見てみようと思います。前回は$N$を4から2048まで変えながら、全て独立に平均値と標準偏差を求めましたが、今回は2048回測定し、そのうち4回の平均、8回の平均、16回の平均・・・と、一度測定したデータを使って平均値と標準偏差を計算してみましょう。

この場合、$N$が小さい時のデータは、必ず$N$が大きいデータに含まれることになります。例えばN=8のデータのうち4つは、N=4の計算に使ったデータを共有します。

スクリプトはこんな感じです。

def func(n)
  s = 0.0
  s2 = 0.0
  n.times do
    r = rand
    s += r
    s2 += r * r
  end
  s /= n.to_f
  s2 /= n.to_f
  sigma = Math.sqrt((s2 - s ** 2) / (n - 1))
  return s, sigma
end

srand(1)

s = 0.0
s2 = 0.0
i = 0
10.times do |j|
  n = 2 ** (j + 2)
  while (i < n)
    i += 1
    r = rand
    s += r
    s2 += r * r
  end
  a = s / n.to_f
  a2 = s2 / n.to_f
  sigma = Math.sqrt((a2 - a ** 2) / (n - 1))
  puts "#{n} #{a} #{sigma}"
end

この場合でも、標準偏差が$1/\sqrt{N}$に比例するのは同じです。

sample3_sigma.png

ただし、誤差棒がおかしくなります。

sample3_errorbar.png

10点の観測点のうちエラーバーが0.5にかからない点が5点あり、しかも最初に6点が「同じ側」に連続でずれています。相関が無いのであれば、真の値の両側に均等にばらつくはずで、これを見たら「おかしい」と思わなければいけません。

ケース4:観測に振動を含む場合

何か時間的に指数関数的に減衰する量を観測する場合を考えましょう。ノイズがひどいので、何度も観測してサンプル平均を取ることにします。

さて、何かが減衰する場合、単に指数関数で減衰するだけでなく、振動がのる場合があります。振動ののり方はいろいろありますが、単純に

$$
y = \exp(-a t) + b \sin(c t + \Delta) + \varepsilon
$$

という形の系を考えましょう。右辺第二項が振動項です。$\Delta$は位相で、「一回の観測の間は一定だが、観測毎に異なる」ようなランダムな値とします。第三項の$\varepsilon$は観測誤差で、観測のたびに独立なノイズがのるものとします。

グラフにするとこんな感じになります(ノイズを含まない場合)。a=0.2, b = 0.3, c = 10, $\Delta=0$です。

exp.png

平均としては指数関数的減衰ですが、それに正弦波が乗っています。

「一回の観測の間は一定だが、観測の度に異なる位相」というのは、グラフにするとこんな感じです。

exp2.png

一回目の実験Exp. 1と、二回目の実験Exp. 2では、位相がずれています。振動数は同じなので、何度も観測して平均すると、平均値である指数関数減衰(黒線)に近づいていくことが期待されます。

このグラフを見たら「指数関数に振動が乗っている」ことがすぐにわかりますが、観測点の間隔が広い場合、これが振動なのかノイズなのか区別をつけることが難しくなります。例えば、先ほどのグラフ(紫の線)を間隔1でプロットしたものがこちらです。

exp3.png

このグラフを見て「ノイズが乗っている」のか「振動が乗っている」のかを判断することは難しいでしょう。しかし、エラーバーをつけてみるとそれがわかる(少なくとも何かおかしなことが起きていることがわかる)場合があります。

この振動項の有無がエラーバーに与える影響を見るスクリプトはこんな感じでしょうか。

NDATA = 10
NSAMPLE = 20

srand(1)

def func(x, delta, a_sin)
  y = Math.exp(-x * 0.2) + (rand - 0.5) * 0.1
  y += a_sin * Math.sin(x * 10.0 + delta)
  y
end

def stat(d)
  n = d.size.to_f
  ave = d.sum / n
  ave2 = d.map { |i| i ** 2 }.sum / n
  sigma = Math.sqrt((ave2 - ave ** 2) / (n - 1.0))
  return ave, sigma
end

def run(filename, a_sin)
  data = Array.new(NDATA) { [] }

  NSAMPLE.times do
    delta = rand * 2 * Math::PI
    NDATA.times do |i|
      y = func(i+1, delta, a_sin)
      data[i].push y
    end
  end

  open(filename, "w") do |f|
    NDATA.times do |i|
      a, s = stat(data[i])
      f.puts "#{i+1} #{a} #{s}"
    end
  end
  puts filename
end

run("test6_a.dat", 0.0)
run("test6_b.dat", 0.3)

まず、振動項が無く、ノイズだけの場合はこんな感じになります。

sample6_a.png

まぁこんな感じかな、というグラフですね。

次に、ノイズに加えて振動項がある場合です。

sample6_b.png

慣れた人が見れば「ん?」と思うはずです。平均値の振る舞いが綺麗なわりに、エラーバーが大きすぎます。

それを見るために、観測値に$\exp(a t)$をかけてみましょう。指数関数的な減衰を打ち消して、観測値が1にそろうはずです。

まずは振動項の無い場合です。

sample6_mod_a.png

観測値は、真の値のまわりを揺らいでおり、エラーバーにかからない点が2点、ぎりぎりかかってるかどうかが1点で、まぁそんな感じかな、と思うグラフです。

次に、振動項がある場合です。

sample6_mod_b.png

全ての点において、エラーバーががっつり「真の値」を含んでいます。これを見ればエラーバーが大きすぎることがわかるでしょう。

振動の位相は観測毎にランダムなので、平均をとれば振動がない場合と同じ期待値に収束します。しかし、振動の位相は同じ観測では固定なので、観測点同士が強く相関することになります。この場合は、振動の分散も拾ってしまうため、エラーバーが大きくでます。

このように、振動の周期が観測間隔に比べて小さい場合、単なるノイズと区別しづらいので注意が必要です。もちろん、フーリエ変換すれば変なピークが立つので隠れた振動に気が付きますが、そもそも「振動があるかも」という可能性に思い至らなければフーリエ変換しようと思わないでしょう。その場合でも、適切にエラーバーをつけていれば、少なくとも何かおかしいことに気が付きます。

まとめ

見たら「ん?」と思うようなエラーバーのグラフを紹介してみました。とにかく「データがのる」と思われる曲線を描いてみて、そこにエラーバーがかからない観測点が3点に1つ無い場合、何かがおかしい可能性が高いです。記事に書いたように、ありがちなのは観測点同士に相関があることですが、分散を$N$ないし$N-1$で割り忘れている、なんて場合もあります(観測点数が少ない場合、意外に気づきにくいです)。

この記事が誰かの参考になれば幸いです。

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