20200328のRubyに関する記事は19件です。

【Ruby on Rails】Ajax通信で特定の要素を変更する

はじめに

RailsのAjax通信使って特定の要素を編集・更新する方法について記載します。
例で簡単なカテゴリー一覧画面の1つのカテゴリーの名前を更新する処理を使います。

バージョン

  • ruby 2.6.3
  • rails 5.2.1

画面概要

以下のような画面です。
スクリーンショット 2020-03-28 22.13.26.png

この右のほうにある鉛筆マークの編集アイコンボタンを押すと、該当の行のカテゴリー名がフォームに変わります。
スクリーンショット 2020-03-28 19.32.07.png

カテゴリー名を変更し、「更新」を押すと、
スクリーンショット 2020-03-28 19.33.11.png

変更後のカテゴリー名にデータが更新され、表示を非同期で変更します。
例では、「交通」→「交通費」に変更します。
スクリーンショット 2020-03-28 22.13.58.png

処理概要

  1. 要素にAjax通信するオプションを追加
    link_toにremote: trueオプションをつけます。

  2. Ajax通信するためのルーティングを追加

  3. コントローラやモデルでオブジェクトの取得または更新

  4. 追加したアクションのためのJavaScript用のviewファイルを用意し、画面の該当の要素を変更するためのJavaScriptを記載

実装前

実装前の主要なコード部分は以下になります。

app/controllers/categories_controller.rb
def index
  @categories = Category.where(user: current_user).order(:created_at)
end
app/views/categories/index.html.erb
<ul class="todo-list" id="own-categories">
  <% @categories.each do |category| %>
    <li id="category-list-id-<%= category.id %>">
      <%= render 'a_category_list', category: category %>    
    </li>
  <% end  %>
</ul>
app/views/categories/_a_category_list.html.erb
<span class="handle ui-sortable-handle">
  <i class="fa fa-ellipsis-v"></i>
  <i class="fa fa-ellipsis-v"></i>
</span>
<span class="text"><%= category.name %></span>
<%= category.common_mark %>
<div class="tools">
  <%= link_to edit_category_path(category), class: "text-redpepper space-left" do %>
    <i class="fa fa-lg fa-edit"></i>
  <% end %>
  <%= category.common_btn %>
</div>

カテゴリー名を編集するフォームに変更するAjax処理の実装

鉛筆マークの編集アイコンボタンを押下して、カテゴリー名の要素をフォーム要素に変更する処理を記載していきます。
※注意:説明しないclass属性やid属性などがありますが、Ajax通信処理とは関係ありません。

この処理にはgem rails-ujsが必要なので追加してbundle installします。

Gemfile
gem 'rails-ujs'

1. Ajax通信するオプションを追加

編集アイコンボタンにremote: trueを仕込む。

app/views/categories/_a_category_list.html.erb
<%= link_to edit_category_path(category), remote: true, class: "text-redpepper space-left" do %>
  <i class="fa fa-lg fa-edit"></i>
<% end %>

2. Ajax通信するためのルーティングを追加

Ajax通信するときでも、ルーティングのresourcesメソッドが柔軟に対応してくれます。
ここでは、editアクションを追加します。

config/routes.rb
resources :categories, only: [:index, :edit]

3. コントローラでオブジェクトの取得

app/controllers/categories_controller.rb
def edit
  @category = Category.find(params[:id])
end

4. 追加したアクションのためのJavaScript用のviewファイルを用意し、画面の該当の要素を変更するためのJavaScriptを記載

editアクションなので、app/views/categories/edit.js.erbを作成します。

app/views/categories/edit.js.erb
id = "<%= @category.id %>";
target = document.querySelector(`#category-list-id-${id}`);
html = "<%= j(render partial: 'form', locals: { category: @category }) %>";
target.innerHTML = html;
app/views/categories/_form.html.erb
<%= form_with(model: category) do |form| %>
  <div class="row">
    <div class="col-xs-10">
      <div class="input-group input-group-sm">
        <%= form.text_field :name, value: category.name, required: true, class:"form-control", max: 15 %>
        <span class="input-group-btn">
          <%= form.submit submit_btn_letters, class: "btn btn-brown" %>
        </span>
      </div>
    </div>
    <div class="col-xs-1">
      <%= category.cancel_btn %>
    </div>
  </div>
<% end %>

これで、カテゴリー名を編集するためのフォームが表示できます。
スクリーンショット 2020-03-28 19.32.07.png

カテゴリー名を更新するAjax処理の実装

編集フォームに変更できたので、次に、変更したい文字「交通費」に変更して、
更新ボタンを押下し、更新後のカテゴリー名を表示する処理を記載していきます。

1. Ajax通信するオプションを追加

form_withにremote: trueオプションを追加します。
(明示的に記載しなくてもデフォルトでremote: trueにはなっている。)

app/views/categories/_form.html.erb
<%= form_with(model: category, remote: true) do |form| %>
  <!-- 省略 -->
<% end %>

2. Ajax通信するためのルーティングを追加

updateアクションを追加

config/routes.rb
resources :categories, only[:index, :edit, :update]

3. コントローラでオブジェクトの更新

カテゴリー名を「交通費」で更新します。

app/controllers/categories_controller.rb
def update
  @category = Category.find(params[:id])
  @category.update(category_params)
end

private
def category_params
  params.require(:category).permit(:name, :is_common)
end

4. 追加したアクションのためのJavaScript用のviewファイルを用意し、画面の該当の要素を変更するためのJavaScriptを記載

編集フォームを更新したカテゴリー名に変更する処理を行います。
updateアクションなのでapp/views/categories/update.js.erbを作成します。

app/views/categories/update.js.erb
id = "<%= @category.id %>";
target = document.querySelector(`#category-list-id-${id}`);
html = "<%= j(render partial: 'categories/a_category_list', locals: { category: @category }) %>";
target.innerHTML = html;

以上で、カテゴリー名を非同期で更新することができました。
スクリーンショット 2020-03-28 22.13.58.png

おわりに

JavaScriptでAjax処理を書いてもいいのですが、シンプルなAjax通信はRailsであれば簡単にできます。
ただ、〇〇.js.erbの書き方は癖が強いので要注意です。

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

Qiitaの自分の全ての投稿をダウンロードするRubyスクリプト

Qiitaの自分の投稿をダウンロードするスクリプトを書くのが流行っているみたいだったので、僕も流行りに乗ってRubyで書いてみました。
限定共有記事も含めて自分の全ての投稿をダウンロードできます。
標準ライブラリのみを使用しているので、gemのインストール等は必要ありません。

サンプルコード

QIITA_ACCESS_TOKENread_qiita権限がついてるアクセストークンに書き換えてください。
アクセストークンの発行はこちらから → アクセストークンの発行 - Qiita

OUT_DIRは出力先ディレクトリパスにご自由に書き換えてください。

download_qiita_items.rb
require 'open-uri'
require 'fileutils'
require 'json'
require 'date'

# アクセストークン
QIITA_ACCESS_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

# 出力先ディレクトリ
OUT_DIR = File.expand_path('./qiita_items', __dir__)

# =====================================================================

# 出力先ディレクトリを作成
FileUtils.mkdir_p(OUT_DIR)

1.upto(100) do |i|
  # 記事一覧を取得
  response = URI.open(
    "https://qiita.com/api/v2/authenticated_user/items?page=#{i}&per_page=100",
    'Authorization' => "Bearer #{QIITA_ACCESS_TOKEN}"
  )
  items = JSON.parse(response.read)

  # 記事が見つからなかった場合は終了する
  break if items.empty?

  # 記事を書き出す
  items.each do |item|
    filename = "#{item['title']}.md"
    File.write(File.join(OUT_DIR, filename), item['body'])
  end
end

実行例

$ ruby download_qiita_items.rb
$ ls qiita_items
Docker Imageのタグ一覧を確認できるCLIツール`docker-tags`を作りました.md
GitHub Markdown APIの使い方.md
Gitコミット件数を確認するコマンド.md
JavaScriptで元の配列を変更せずに並び替えした配列を作る.md
Qiita APIを使って記事一覧を取得する.md
React+material-uiでコピーボタン付きのテキストボックスを作る.md
Slack APIを使用してメッセージを送信する.md
TerraformでVPC・EC2インスタンスを構築してssh接続する.md
【AWS】Qiita 週間いいね数ランキング【自動更新】.md
【Docker】Qiita 週間いいね数ランキング【自動更新】.md
【Git】自分が今いるブランチを確認するコマンド.md
【Go】AWS SSMパラメータストアの値を取得する.md
...
...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VisualStudioCodeのRubyアウトラインが一部表示されないときは繰り返し処理でdoをつけているせいかもしれない

VSCodeのアウトラインは便利ですね、ショートカットでアウトラインにフォーカス当たるようにしたりしています。

def hoge
end

def fuga
end

def piyo
end

とすると。
◇ hoge
◇ fuga
◇ piyo
みたいに表示されます。ところが

def hoge
end

def fuga
  while cond1 do
  end
  while cond2 do
  end
end

def piyo
end

とすると、
◇ hoge
◇ fuga
になってしまうのです。piyo以下が表示されなくなってしまいます。しかし、繰り返し処理の後ろのdoは省略できます

def hoge
end

def fuga
  while cond1
  end
  while cond2
  end
end

def piyo
end

とすると、ちゃんと表示されるのです。
◇ hoge
◇ fuga
◇ fuga

一つの関数の中に繰り返し処理が複数出てくるようなコードはよくないのかもれないけれど、ひとまず解決。

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

Rubyでナノ秒、PostgreSQLだとマイクロ秒

環境: circleci/ruby:2.4.2-stretch-node-browsers, circleci/postgres:10.12

とあるアプリケーションで、新しくCircleCIを設定してRspecを動かしてみたら、エラーが大量に出ました。

expected: 2020-03-25 12:36:20.477148202 +0900
got: 2020-03-25 12:36:20.477148000 +0900

Rubyはナノ秒(小数点以下9桁)まで時刻を保持し、PostgreSQLはマイクロ秒(小数点以下6桁)までしか保持しないので、比較が失敗しまくっていると判明。ひえー、どうすんのこれ。助けて!

用語

参照: Orders_of_magnitude_(time)

名前 単位
ミリ秒 10-3 0.123
マイクロ秒 10-6 0.123456
ナノ秒 10-9 0.123456789

以下、3桁ずつピコ 10-12、フェムト 10-15、アット 10-18、ゼプト 10-21、ヨクト 10-24と続きます。

Macでは

Mac(rbenvでインストール)では、RubyのTime#nsecはナノ秒(9桁)を表しますが、実際はマイクロ秒(6桁)で切り取られるので、Postgresとの違いに気づきませんでした。

t = Time.now
t.to_f #=> 1585394326.856343
t.nsec #=> 856343000

Ubuntuだとナノ秒までしっかり入ります。

t = Time.now
t.to_f #=> 1585394640.8082483
t.nsec #=> 808248257

対策

Time#<=> を上書きしてミリ秒で比較することでエラーを減らすことができました。<=> を上書きするだけで、== も != も < も対策できます。Time#<=> を上書きすると、ActiveSupport::TimeWithZone の比較にも使われます。

マイクロ秒で比較(floor(6))ではうまく行きませんでした。

spec/supports/time_patch_helper.rb
if ENV['CIRCLECI']
  class Time
    alias_method :old_cmp, :<=>
    def <=>(val)
      if val.try(:acts_like_time?)
        self.to_f.floor(3) <=> val.to_f.floor(3)
      else
        old_cmp(val)
      end
    end
  end
end

ちなみにRubyには時刻の小数点以下を丸めるメソッド Time#round があり、Ruby 2.7 では切り落としを行う Time#floorが追加されています。

今後やりたいこと

  • 上記の対策よりもうまい方法を考える。
  • Rubyのソースコードをチェックしたり、Linuxでコンパイルしてみたりする(たぶんC言語の#defineに違いがある)。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

応用 Lesson2 検索力についてのまとめ

応用カリキュラム Lesson2対応 リンク集

エンジニアにとって検索力がとても重要なことを学び、
テキストにあるワードを検索してみました。

初投稿

まずQiitaの使い方からよく分かりませんので、その辺は徐々に。
とりあえず自身のアウトプットとして書きます。
どなたかの何かのお役に立てたら光栄です。

では、ワードごとにリンクを貼っていきます。
あえて、ワードに対する答えは載せません。
あくまでリンク集です。(自分のためにも…)

【リンク集 ~Link Page〜】

1. jQueryのメリット・デメリットは簡単に言うと?

<検索ワード>
 jQuery メリット

<参考サイト>
 AtoOne 様
 https://www.atoone.co.jp/column/10176/

2. Queryは最近のトレンドの中でどのような立ち位置か?

<検索ワード>
 jQuery トレンド 立ち位置

<参考サイト>
 Hideout der Eremit 様
 https://dereremit.jp/archives/260

3. deviseとは簡単に言うと?

<検索ワード>
 deviceとは

<参考サイト>
 キツネの惑星 様
 https://kitsune.blog/rails-devise

4. deviseと同様な機能で他によく使われるものは何か?違いは何か?なぜdevise以外にそれが生まれたか?

これ、めちゃくちゃ苦戦しました。
もっと分かりやすい、的確なサイトがあると思います。

<検索ワード>
 rails 認証 gem トレンド …他多数?

<参考サイト>
 Tech Blog 様
 https://www.for-engineer.life/entry/rails-sorcery/

5. unicornとは簡単に言うと何か?

6. unicornと同様な機能で他によく使われるものは何か?違いは何か?なぜunicorn以外にそれが生まれたか?

<検索ワード>
 unicornとは

<参考サイト>
 @takahiro1127
 https://qiita.com/takahiro1127/items/fcb81753eaf381b4b33c

〜気づき〜

検索力にはある程度自信を持っていましたが、
信憑性のある、確からしい情報を得るためには
テクニックがあることを知りました。

同時に自分の無力さに愕然(笑)

今後も学習のノート代わり、アウトプットとして活用させていただきます。

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

Rails6でアプリ起動時にWebpacker configuration file not foundエラーが発生した時の対処方法

執筆背景

scaffoldを使って簡単なアプリを作ろうと以下のコマンドを打ち込むと、Webpackerがインストールされていないというエラーが発生したため、記述しておこうと思う。

$ rails new app_name
$ cd app
$ rails g scaffold post content:text
$ rails s

(中略)
Webpacker configuration file not found /Users/ユーザ名/app_name/config/webpacker.yml.
Please run rails webpacker:install 
Error: No such file or directory 
@ rb_sysopen - /Users/ユーザ名/app_name/config/webpacker.yml 
(RuntimeError)

ちなみに、このときのrubyのバージョンは2.7.0、railsのバージョンは、6.0.2.2です。

結論

このエラーは、webpackerがインストールされていない場合に起こるエラーみたいです(rails6からwebpackerが標準になったため)。

webpackerのインストールには、Node.jsとYarnのインストールが必要で、これらをインストールした上でwebpackerをインストールすると解決します。

作業内容

Gemfileを確認し、gem 'webpacker'の記述を確認。

Gemfile
gem 'webpacker', '~> 4.0'

bundle installしたのち、webpackerをインストールしようと試みるもNode.jsがインストールされていないと怒られる。

$ bundle install
$ rails webpacker:install
sh: node: command not found
sh: nodejs: command not found
Node.js not installed. Please download and install Node.js https://nodejs.org/en/download/

Node.jsをインストールしたのち、再度webpackerのインストールを試みると、Yarnをインストールしろと怒られる。

$ brew install node
(中略)
==> node
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

$ rails webpacker:install
Yarn not installed. 
Please download and install 
Yarn from https://yarnpkg.com/lang/en/docs/install/

Yarnをインストールしたのち、再度webpackerのインストールを試みたところインストールすることができた。

$ sudo npm install -g yarn
added 1 package in 3.509s

$ yarn -v
1.22.4
$ yarn install
success Saved lockfile.
✨  Done in 3.49s.

$ rails webpacker:install
✨  Done in 6.34s.
Webpacker successfully installed ? ?

アプリを再起動。

$ rails s
Use Ctrl-C to stop
Started GET "/posts/index" for ::1 at 2020-03-28 18:06:09 +0900

うまくいった!
スクリーンショット 2020-03-28 18.07.56.png

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

【Ruby】インスタンスメソッド・クラスメソッド・特異メソッドの違い

インスタンスメソッド、クラスメソッド、特異メソッドの違いについてまとめてみました!!

インスタンスメソッドとは?

  • インスタンスから呼べるメソッドのこと

定義の仕方と解説

スクリーンショット 2020-03-28 11.14.25.png
Human.instance_methods.grep(/say_hello/)でメソッドがあることがわかるのでインスタンスメソッド

クラスメソッドとは?

  • クラスから呼べるメソッドのこと

定義の仕方と解説1

スクリーンショット 2020-03-28 11.26.42.png

定義の仕方と解説2

スクリーンショット 2020-03-28 11.29.37.png

クラスメソッドのselfはクラスそのものです。
厳密にいうとselfが書かれた場所に参照するレシーバを返します。
selfの代わりにクラス名をそのまま使ってもクラスメソッドになります。

Human.methods.grep(/say_hello/)でメソッドがあることがわかるのでクラスメソッド
Human.singleton_methods.grep(/say_hello/)でメソッドがあることがわかるので特異メソッド(シングルトンメソッド
上記の検証からクラスメソッドは特異メソッドであることがわかります。
 

 特異メソッド(シングルトンメソッド)とは?

  • 1つのオブジェクト固有のメソッド
  • クラスメソッドは特異メソッド

特異メソッドがオブジェクト固有のメソッドとは?

  • オブジェクト固有とはオブジェクトIDが同じということ
  • インスタンスのオブジェクトIDは一つ一つ違う

コードで説明

  • インスタンスのobject_idをみるとそれぞれ違う。クラス、モジュールは同じ
  • classメソッドはオブジェクトのクラスが確認できる
  • ancestorsメソッドは継承関係を確認できる
  • object_idは各オブジェクトに対して一意な整数を確認できる

class Human
end

p BasicObject.class
=> Class
p BasicObject.ancestors
=> [BasicObject]
p BasicObject.object_id
=> 580
p BasicObject.object_id
=> 580
BasicObjectはnewメソッドがなくインスタンス化できない
❌ p BasicObject.new.object_id
❌ p BasicObject.new.object_id

p Kernel.class
=> Module
p Kernel.ancestors
=> [Kernel]
p Kernel.object_id
=> 600
p Kernel.object_id
=> 600
Kernelはモジュールのためnewメソッドがなくインスタンス化できない
❌ p Kernel.new.object_id
❌ p Kernel.new.object_id

p Object.class
=> Class
p Object.ancestors
=> [Object, Kernel, BasicObject]
p Object.object_id
=> 60
p Object.object_id
=> 60
p Object.new.object_id
=> 620
p Object.new.object_id
=> 640
p Human.class
=> Class
p Human.ancestors
=> [Human, Object, Kernel, BasicObject]
p Human.object_id
=> 660
p Human.object_id
=> 660
p Human.new.object_id
=> 680
p Human.new.object_id
=> 700

参考

Ruby 2.7.0 リファレンスマニュアル

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

【ruby,rails】環境変数から読み込んだ秘密鍵の文字列の改行コードがエスケープされていて秘密鍵としてパースできないときの対処法

概要

環境変数から読み込んだ秘密鍵の文字列の改行コードがエスケープされてしまうことによって、秘密鍵としてパースできないときの文字列処理を共有します。

解決策

たとえば下記のような秘密鍵があったとき、

-----BEGIN RSA PRIVATE KEY-----
MIGrAgEAAiEAvnrd8LBnzAGxCW+i7KtVQSiTsssMtbwcs5styeKsn2kCAwEAAQIh
AKBF8glb5Xqa0cQG0ygg4hIFdipmvEJhiCuhX93krDCBAhEA51bAM0gFPvxyk9Xe
ioIOBQIRANLJEv4Xw7MwT7EEEARL5RUCEBa8bu1bUbCsDPK8nT+NoqUCEQCIzFCU
MY4j7BW8N3vBnhPlAhBgs4tSfe6RbpertixmCygk
-----END RSA PRIVATE KEY-----

環境変数に設定するときは下記のように改行コードを使って、文字列を1行にして設定すると思います。

-----BEGIN RSA PRIVATE KEY-----\nMIGrAgEAAiEAvnrd8LBnzAGxCW+i7KtVQSiTsssMtbwcs5styeKsn2kCAwEAAQIh\nAKBF8glb5Xqa0cQG0ygg4hIFdipmvEJhiCuhX93krDCBAhEA51bAM0gFPvxyk9Xe\nioIOBQIRANLJEv4Xw7MwT7EEEARL5RUCEBa8bu1bUbCsDPK8nT+NoqUCEQCIzFCU\nMY4j7BW8N3vBnhPlAhBgs4tSfe6RbpertixmCygk\n-----END RSA PRIVATE KEY-----\n

この1行にした秘密鍵をアプリケーション内で読み込むと、下記のようにバックスラッシュが2つになってしまいます。

 "-----BEGIN RSA PRIVATE KEY-----\\nMIGrAgEAAiEAvnrd8LBnzAGxCW+i7KtVQSiTsssMtbwcs5styeKsn2kCAwEAAQIh\\nAKBF8glb5Xqa0cQG0ygg4hIFdipmvEJhiCuhX93krDCBAhEA51bAM0gFPvxyk9Xe\\nioIOBQIRANLJEv4Xw7MwT7EEEARL5RUCEBa8bu1bUbCsDPK8nT+NoqUCEQCIzFCU\\nMY4j7BW8N3vBnhPlAhBgs4tSfe6RbpertixmCygk\\n-----END RSA PRIVATE KEY-----\\n"

このまま秘密鍵オブジェクトを作成しようとすると、エラーになります。

OpenSSL::PKey::RSA.new(str)
=> OpenSSL::PKey::RSAError: Neither PUB key nor PRIV key: nested asn1 error

そこで、この2つのバックスラッシュを1つのバックスラッシュに置換することで、秘密鍵オブジェクを作成することができるようになります。

str.gsub(/\\n/, "\n")
=> "-----BEGIN RSA PRIVATE KEY-----\nMIGrAgEAAiEAvnrd8LBnzAGxCW+i7KtVQSiTsssMtbwcs5styeKsn2kCAwEAAQIh\nAKBF8glb5Xqa0cQG0ygg4hIFdipmvEJhiCuhX93krDCBAhEA51bAM0gFPvxyk9Xe\nioIOBQIRANLJEv4Xw7MwT7EEEARL5RUCEBa8bu1bUbCsDPK8nT+NoqUCEQCIzFCU\nMY4j7BW8N3vBnhPlAhBgs4tSfe6RbpertixmCygk\n-----END RSA PRIVATE KEY-----\n"

OpenSSL::PKey::RSA.new(str.gsub(/\\n/, "\n"))
=> #<OpenSSL::PKey::RSA:0x00007f9d7dd734e0>

もちろん、秘密鍵だけではなく、改行コードのバックスラッシュがエスケープされたすべての文字列に対しても同様の解決策が有効です。

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

ユーザー管理に必須!Gemの『devise』を使おう!

Railsにおいてユーザーを管理する際に必要となるのが『devise』と呼ばれるGemです。

68747470733a2f2f7261772e6769746875622e636f6d2f706c617461666f726d617465632f6465766973652f6d61737465722f6465766973652e706e67.png

deciseの設定を忘れてしまわないように備忘録として記述します。

deviseの機能

このGemを使用することによって、ユーザーを管理する機能を追加する際に簡単に管理を行うことができるようになります。

deviseの設定

1.Gemfileへの記述

# 省略
gem 'devise'  # 最終行に追記

2.ターミナルにてbundle install

$ bundle install

bundle installでGemを更新・反映する。

3.deviseの設定ファイルを作成

$ rails g devise:install

4.deviseの機能を持ったUserモデルの作成

注意したいのがここ!通常のrails g model 〇〇ではないという点です。

ここでは下記のように記述してUserモデルを作成します。

# deviseコマンドでモデルを作成
$ rails g devise user

これでdevise機能を持ったUserモデルが作成されました。

またroutes.rbには自動的に下記のコードが追加されます。

config.routes.rb
Rails.application.routes.draw do
  devise_for :users
#以下略

devise_for :usersの記述によりユーザーのログイン・ログアウト、新規登録で必要なルーティングが作成されます。

5.rails db:migrateを行う

ターミナル上でrails db:migrateを行います。

# 作成されたマイグレーションファイルを実行
$ rails db:migrate

上記を行うことでデータベース上にusersテーブルが作成されます。

まとめ

deviseの設定をスムーズに行えるようになりました。ただしユーザーのログイン・ログアウト・新規登録に関するバックエンドの実装に関しては曖昧な部分もあるため、追記で記述しアウトプットの機会を持ちたいと考えています。

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

Rails6 + CentOS8にRMagickをサクッと導入する ( MiniMagicも)

railsで画像アップロード機能を作ったとき、
「クソッ、画像が回転してしまう!!!」
という場面にぶち当たる方、多くいらっしゃると思います。

そこで軽くググって、
「RMagickを導入すればいいのか!!!」

「gem "rmagick"」、bundle install

なにかしらのエラーが発生。
「できねえじゃねえか!!!」

というのを経験した方に読んでいただければといいと思います。

バージョン、前提条件

Ruby 2.6.5
Ruby on Rails 6.0.2.1
CentOS 8

なお、画像アップロードにはActiveStorageは使わず、carrierwaveを使っています。(carrierwaveの説明はここではしません。)
余談ですが、carrierwaveではなくActiveStorageを使うメリットってなんですかね...
もしもActiveStorage絶対使うマンの方いらっしゃれば教えてください。。。

CentOS8にImageMagickをダウンロード

1. 開発ツールを入れます。

$ sudo dnf groupinstall 'Development Tools'

2. wgetでソースをインストール

$ wget https://www.imagemagick.org/download/ImageMagick.tar.gz

3. ソースの解凍

$ tar xvfz ImageMagick.tar.gz

4.コンパイル

$ cd ImageMagick-7.0.10-2/
$ ./configure
$ make
$ make install

5.パスの設定

$ vi .bash_profile
$ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
$ export LD_LIBRARY_PATH=/usr/local/lib
$ source .bash_profile

Gemに追加

Gemfile
gem "rmagick"
$ bundle install

こんだけです。

追記: rails g taskでエラーがでたため、MiniMagickに鞍替え

rails g taskで以下のエラーが出た。

/home/user/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/bootsnap-
1.4.6/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in
`require': libMagickCore-7.Q16HDRI.so.7: cannot open shared object file: No such file or directory - 
/home/user/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/rmagick-4.1.0/lib/RMagick2.so (LoadError)

...は?ちょっと分からなかったため、MiniMagickを使うことに。(スミマセン、もうちょっと調べてきます。。。)

Gemfile
# gem "rmagick"
gem "minimagick"

また、uploader(ここは解説なし)も変更

# include CarrierWave::RMagick
include CarrierWave::MiniMagick

参考

https://www.rootlinks.net/2019/11/08/install-imagemagick-on-centos-8/
https://qiita.com/tajihiro/items/3f10c59f19fcd0a9e660

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

Rails6 + CentOS8にRMagickをサクッと導入する

railsで画像アップロード機能を作ったとき、
「クソッ、画像が回転してしまう!!!」
という場面にぶち当たる方、多くいらっしゃると思います。

そこで軽くググって、
「RMagickを導入すればいいのか!!!」

「gem "rmagick"」、bundle install

なにかしらのエラーが発生。
「できねえじゃねえか!!!」

というのを経験した方に読んでいただければといいと思います。

バージョン、前提条件

Ruby 2.6.5
Ruby on Rails 6.0.2.1
CentOS 8

なお、画像アップロードにはActiveStorageは使わず、carrierwaveを使っています。(carrierwaveの説明はここではしません。)
余談ですが、carrierwaveではなくActiveStorageを使うメリットってなんですかね...
もしもActiveStorage絶対使うマンの方いらっしゃれば教えてください。。。

CentOS8にImageMagickをダウンロード

1. 開発ツールを入れます。

$ sudo dnf groupinstall 'Development Tools'

2. wgetでソースをインストール

$ wget https://www.imagemagick.org/download/ImageMagick.tar.gz

3. ソースの解凍

$ tar xvfz ImageMagick.tar.gz

4.コンパイル

$ cd ImageMagick-7.0.10-2/
$ ./configure
$ make
$ make install

5.パスの設定

$ vi .bash_profile
$ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
$ export LD_LIBRARY_PATH=/usr/local/lib
$ source .bash_profile

Gemに追加

Gemfile
gem "rmagick"
$ bundle install

こんだけです。

参考

https://www.rootlinks.net/2019/11/08/install-imagemagick-on-centos-8/
https://qiita.com/tajihiro/items/3f10c59f19fcd0a9e660

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

Railsチュートリアル〜6章〜ついにモデル作成へ突入

userモデルの作成

5章を足早に終わらせて続いて6章に入っていきます。
ここではユーザー登録ページを作成していく。
・ユーザー用のデータモデルの作成
・データを保存する手段の確保について学ぶ。

Railsではデータモデルとして扱うデフォルトのデータ構造のことをモデルと呼ぶ。Railsでは、データを永続化するデフォルトの解決策としてデータベースを使ってデータを長期間保存する。データベースとやり取りをするデフォルトのライブラリはActive Recordという。
Active Recordはデータオブジェクトの作成・保存・検索のためのメソッドを持っている。

データベースの移行

今回の目的は永続性のあるモデルを構築することである。
Railsでユーザーをモデリングする時は属性を明示的に識別する必要がなく、デフォルトでリレーショナルデータベースを使用する。リレーショナルデータベースはデータ行で構成されるテーブルからなり、各行はデータ属性のカラム(列)を持つ。nameとemailのカラムをもつusersテーブルを作成する。

Userモデルを生成する
$ rails generate model User name:string email:string
      invoke  active_record
      create    db/migrate/20160523010738_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

generateコマンドの新しい結果の一つとして、マイグレーションと呼ばれるファイルが生成される。マイグレーションはデータベースの構造をインクリメンタルに変更する手段を提供する。(インクリメンタル=次第に増やす)要するにマイグレーションはデータベースの構造増やす手段を提供すること。

マイグレーション自体は、データベースに与える変更を定義したchangeメソッドの集まりである。changeメソッドはcreate_tableというRailsのメソッドを呼びユーザーを保存するためのテーブルをデータベースに作成する。
このメソッドはブロック変数を1つ持つブロックを受け取る。このブロックの中でnameとemailカラムをデータベースに作る。
モデル名は単数形(User)だがテーブル名は複数型(users)である。
ブロックの最後の行t.timestampsは特別なコマンドで、created_atとupdated_atという2つの「マジックカラム (Magic Columns)」を作成します。これらは、あるユーザーが作成または更新されたときに、その時刻を自動的に記録するタイムスタンプである。

マイグレーションは、次のようにdb:migrateコマンドを使い実行することができる。これを「マイグレーションの適応」と呼ぶ。

$ rails db:migrate

db:migrateが実行されると、db/development.sqlite3という名前のファイルが生成されます。これはSQLite5の実体である。development.sqlite3ファイルを開くためのDB Browser for SQLiteというツールを使うと、データベースの構造を見ることができる。

ほぼすべてのマイグレーションは、元に戻すことが可能
元に戻すことを「ロールバック (rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。

  $ rails db:rollback

modelファイル

ユーザーオブジェクトの検索

User.find(1)
User.findにユーザーのidを渡し,その結果Active Recordはそのidのユーザーを返します。

一般的なfindメソッド以外に、Active Recordには特定の属性でユーザーを検索する方法もあります。
 User.find_by(email: "mhartl@example.com")

>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
firstはデータベースの最初のユーザーを返す。

>> User.all
=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl",
email: "mhartl@example.com", created_at: "2016-05-23 19:05:58",
updated_at: "2016-05-23 19:05:58">, #<User id: 2, name: "A Nother",
email: "another@example.org", created_at: "2016-05-23 19:18:46",
updated_at: "2016-05-23 19:18:46">]>
User.allでデータベースのすべてのUserオブジェクトが返ってくることがわかります。また、返ってきたオブジェクトのクラスがActiveRecord::Relationとなっています。これは、各オブジェクトを配列として効率的にまとめてくれるクラスである。

ユーザーオブジェクトの更新

基本的な更新の方法は2つである。

>> user           # userオブジェクトが持つ情報のおさらい
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true

変更をデータベースに保存するために最後にsaveを実行する必要があることを忘れない。
保存を行わずにreloadを実行すると、データベースの情報を元にオブジェクトを再読み込みするので失敗する。

属性を更新するもう1つの方法は、update_attributesを使うケース

>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"

update_attributesメソッドは属性のハッシュを受け取り、成功時には更新と保存を続けて同時に行う。
特定の属性のみを更新したい場合は、次のようにupdate_attributeを使います。このupdate_attributeには、検証を回避するといった効果もある。

>> user.update_attribute(:name, "El Duderino")
=> true
>> user.name
=> "El Duderino"

ユーザーの検証

UserモデルにNameとemailの属性を与えた。ここに制限を加えてデータを登録する際に適切な管理が行われるようにする。(具体的には名前を空にしない、emailはアドレスのフォーマットに従う。メールアドレスがデータベース内で重複しない。等)

Active Recordでは検証(バリデーション)という機能を通して、こういった制約を課すことができる。
よく使われるケース
存在性(presence)
長さ(length)
フォーマット(format)
一意性(uniqueness)
確認(confirmation)
の検証である。

有効性の検証

有効なモデルのオブジェクトを作成しその属性のうちの一つを有効でない属性に意図的に変更する。→バリデーション機能で失敗するかどうかテストする。
デフォルトのUserテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

有効なオブジェクトに対してテストを書くためにsetupという特殊なメソッドを使って有効なUserオブジェクト(@user)を作成する。setupメソッド内に書かれた処理は各テストが走る直前に実行される。@userはインスタンス変数だが、setupメソッド内で宣言しておけばすべてのテスト内でインスタンス変数が使用できる。

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end
end

assertメソッドを使ってテストする。
@user.valid?がtrueを返すと成功し、falseを返すと失敗する。

$ rails test:models
modelに関するテストはこのコマンドを使用する。

存在性の検証

最も基本的なバリデーションである。
渡された属性が存在することを検証する。ここではユーザーがデータベースに保存される前にname emailフィールドの両方が存在することを保証する。

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  test "should be valid" do
    assert @user.valid?
  end

  test "name should be present" do
    @user.name = "     "
    assert_not @user.valid?
  end
  test "email should be present" do
    @user.email = "     "
    assert_not @user.valid?
end
end

name属性の存在性に関するテストを追加します。具体的には@user変数のname属性に対して空白の文字列をセット
assert_notメソッドを使って Userオブジェクトが有効でなくなったことを確認する。

name属性の存在性を検証する。

class User < ApplicationRecord
  validates :name, presence: true
end

長さの検証

ここではユーザーモデル上の名前の長さを50字を上限とし、51文字以上の名前
emailの文字を255字を上限とするように長さの制限をかける。
nameの長さの検証に用いるテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "name should not be too long" do
    @user.name = "a" * 51
    assert_not @user.valid?
  end

  test "email should not be too long" do
    @user.email = "a" * 244 + "@example.com"
    assert_not @user.valid?
  end
end

長さの検証を追加する

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end

フォーマットの検証

name属性の検証には、空文字でない、名前が51文字未満であるという最小限の制約しかいまのところない。
email属性の場合は、有効なメールアドレスかどうかを判定するために、もっと厳重な要求を満たさなければなりません。ここではメールアドレスにおなじみのパターンuser@example.comに合っているかどうかも確認する。(なお今回ここで使うテストや検証は、形式があからさまに無効なものを拒否するだけであり、すべての有効なメールアドレスを受け入れられるものではない点に注意してください。)

>> %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp]
=> ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]
>> addresses.each do |address|
?>   puts address
>> end
USER@foo.COM
THE_US-ER@foo.bar.org
first.last@foo.jp``

メールアドレスのバリデーションは扱いが難しく、エラーが発生しやすい部分なので、有効なメールアドレスと無効なメールアドレスをいくつか用意して、バリデーション内のエラーを検知していきます。具体的には、user@example,comのような無効なメールアドレスが弾かれることと、user@example.comのような有効なメールアドレスが通ることを確認しながら、バリデーションを実装していきます (ちなみに今の状態では、空でない文字列はすべてメールアドレスとして通ってしまいます) 。

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email validation should accept valid addresses" do
    valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org
                         first.last@foo.jp alice+bob@baz.cn]
    valid_addresses.each do |valid_address|
      @user.email = valid_address
      assert @user.valid?, "#{valid_address.inspect} should be valid"
    end
  end
end
ここでは、assertメソッドの第2引数にエラーメッセージを追加していることに注目してください。これによって、どのメールアドレスでテストが失敗したのかを特定できるようになります。

assert @user.valid?, "#{valid_address.inspect} should be valid

次に、user@example,com (ドットではなくカンマになっている) やuser_at_foo.org (アットマーク ‘@’ がない) といった無効なメールアドレスを使って 「無効性 (Invalidity)」についてテストしていく。

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email validation should reject invalid addresses" do
    invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
                           foo@bar_baz.com foo@bar+baz.com]
    invalid_addresses.each do |invalid_address|
      @user.email = invalid_address
      assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
    end
  end
end

メールフォーマットを正規表現で検証する。

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX }
end

一意性を検証する

メールアドレスの一意性を強制するために (ユーザー名として使うために)、validatesメソッドの:uniqueオプションを使う。
今回のテストではRubyのオブジェクトを作るだけでなく、実際にメモリ上にレコードをデータベースに登録する必要がある。

重複するメールアドレス拒否のテスト

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email addresses should be unique" do
    duplicate_user = @user.dup
  duplicate_user.email = @user.email.upcase
    @user.save
    assert_not duplicate_user.valid?
  end
end

上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています。dupは、同じ属性を持つデータを複製するためのメソッドです。@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはずである。

メールアドレスの一意性を検証する
emailのバリデーションにuniqueness: trueというオプションの追加

class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
end

メールアドレスの大文字小文字を無視した一意性の検証

app/models/user.rb
class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end
trueをcase_sensitive: falseに置き換え

データベースのインデックス

データベースにカラムを作成するとき、そのカラムでレコードを検索する (find) 必要があるかどうかを考えることは重要です。例えば、リスト 6.2のマイグレーションによって作成された email属性について考えてみましょう。第7章ではユーザーをサンプルアプリにログインできるようにしますが、このとき、送信されたものと一致するメールアドレスのユーザーのレコードをデータベースの中から探しだす必要があります。
インデックスなどの機能を持たない素朴なデータモデルにおいてユーザーをメールアドレスで検索するには、データベースのひとりひとりのユーザーの行を端から順に読み出し、そのemail属性と渡されたメールアドレスを比較するという非効率的な方法しかない。

emailカラムにインデックスを追加することで、この問題を解決することができます。(インデックスは本の索引機能のようなもの)

emailインデックスを追加すると、データモデリングの変更が必要になります。Railsではマイグレーションでインデックスを追加します。Userモデルを生成すると自動的に新しいマイグレーションが作成されたことを思い出してください。
今回の場合は、既に存在するモデルに構造を追加するので、次のようにmigrationジェネレーターを使ってマイグレーションを直接作成する必要があります。

$ rails generate migration add_index_to_users_email

ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。リスト 6.29のように定義を記述する必要があります16。

メールアドレスの一意性を強制するためのマイグレーション

db/migrate/[timestamp]_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration[5.0]
  def change
    add_index :users, :email, unique: true
  end
end

上のコードでは、usersテーブルのemailカラムにインデックスを追加するためにadd_indexというRailsのメソッドを使っています。インデックス自体は一意性を強制しませんが、オプションでunique: trueを指定することで強制できるようになります。

最後に、データベースをマイグレートします。

$ rails db:migrate

この時点では、テストDB用のサンプルデータが含まれているfixtures内で一意性の制限が保たれていないため、テストは red になります。つまり、リスト 6.1でユーザー用のfixtureが自動的に生成されていましたが、ここのメールアドレスが一意になっていないことが原因です (リスト 6.30) (実はこのデータはいずれも有効ではありませんが、fixture内のサンプルデータはバリデーションを通っていなかったので今まで問題にはなっていなかっただけでした)。

リスト 6.30: Userのデフォルトfixture red
test/fixtures/users.yml
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/
# FixtureSet.html

これで1つの問題が解決されましたが、メールアドレスの一意性を保証するためには、もう1つやらなければならないことがあります。それは、いくつかのデータベースのアダプタが、常に大文字小文字を区別するインデックス を使っているとは限らない問題への対処です。例えば、Foo@ExAMPle.Comfoo@example.comが別々の文字列だと解釈してしまうデータベースがありますが、私達のアプリケーションではこれらの文字列は同一であると解釈されるべきです。この問題を避けるために、今回は「データベースに保存される直前にすべての文字列を小文字に変換する」という対策を採ります。例えば"Foo@ExAMPle.CoM"という文字列が渡されたら、保存する直前に"foo@example.com"に変換してしまいます。これを実装するためにActive Recordのコールバック (callback) メソッドを利用します。このメソッドは、ある特定の時点で呼び出されるメソッドです。今回の場合は、オブジェクトが保存される時点で処理を実行したいので、before_saveというコールバックを使います。これを使って、ユーザーをデータベースに保存する前にemail属性を強制的に小文字に変換します17。作成したコードをリスト 6.32に示します。(本チュートリアルで初めて紹介したテクニックですが、このテクニックについては11.1でもう一度取り上げます。そこではコールバックを定義するときにメソッドを参照するという慣習について説明します。)

email属性を小文字に変換してメールアドレスの一意性を保証する

app/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end

before_saveコールバックにブロックを渡してユーザーのメールアドレスを設定します。設定されるメールアドレスは、現在の値をStringクラスのdowncaseメソッドを使って小文字バージョンにしたもの。

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

セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それを (そのままではなく) ハッシュ化したものをデータベースに保存します。要するにハッシュ関数を使って、入力されたデータを元に戻せない (不可逆な) データにする処理を指す。

ユーザーの認証は、パスワードの送信、ハッシュ化、データベース内のハッシュ化された値との比較、という手順で進んでいきます。比較の結果が一致すれば、送信されたパスワードは正しいと認識され、そのユーザーは認証されます。。こうすることで、生のパスワードをデータベースに保存するという危険なことをしなくてもユーザー登録できる。

ハッシュ化されたパスワード
セキュアなパスワードの実装は

class User < ApplicationRecord
  .
  .
  .
  has_secure_password
end

上のようにモデルにこのメソッドを追加すると、次のような機能が使えるようになる。

・セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる。
・2つのペアの仮想的な属性 (passwordとpassword_confirmation) が使えるようになる。存在性と値が一致するかどうかのバリデーションも追加される。
・authenticateメソッドが使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalseを返すメソッド) 。

has_secure_password機能を使えるようにするには、モデル内にpassword_digestという属性を含める。
password_digestカラム用の適切なマイグレーションを生成。
マイグレーション名は自由に指定できる。add_password_digest_to_usersというマイグレーションファイルを生成するためには、次のコマンドを実行します。

$ rails generate migration add_password_digest_to_users password_digest:string

上のコマンドではpassword_digest:stringという引数を与えて、今回必要になる属性名と型情報を渡している。

password_digestカラムを追加するマイグレーション

db/migrate/[timestamp]_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :password_digest, :string
  end
end

リスト 6.35では、add_columnメソッドを使ってusersテーブルpassword_digestカラムを追加しています。これを適用させるには、データベースでマイグレーションを実行します。

$ rails db:migrate

また、has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要になります。パスワードを適切にハッシュ化することで、たとえ攻撃者によってデータベースからパスワードが漏れてしまった場合でも、Webサイトにログインされないようにできます。サンプルアプリケーションでbcryptを使うために、bcrypt gemをGemfileに追加します。

リスト 6.36: bcryptをGemfileに追加する

source 'https://rubygems.org'

gem 'rails',          '5.1.6'
gem 'bcrypt',         '3.1.12'
.
.
.

最後にbundle installを実行

6.3.2 ユーザーがセキュアなパスワードを持っている
Userモデルにpassword_digest属性を追加し、Gemfileにbcryptを追加したことで、ようやくUserモデル内でhas_secure_passwordが使えるようになりました

Userモデルにhas_secure_passwordを追加する

app/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
end

次にユーザーモデルにパスワードとパスワード確認を追加する。

def setup
  @user = User.new(name: "Example User", email: "user@example.com")
end

テストをパスさせるために、パスワードとパスワード確認の値を追加します。

リスト 6.39: パスワードとパスワード確認を追加する

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
end

リスト 6.41: パスワードの最小文字数をテストする red

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end
end
ここで、次のような多重代入 (Multiple Assignment) を使っていることに注目してください。

@user.password = @user.password_confirmation = "a" * 5

これはpasswordとpasswordconfirmationに対して同時に代入をしています

リスト 6.16ではmaximumを使ってユーザー名の最大文字数を制限していましたが、これと似たような形式のminimumというオプションを使って、最小文字数のバリデーションを実装することができます。

validates :password, length: { minimum: 6 }

また、空のパスワードを入力させないために、存在性のバリデーションも一緒に追加します。結果として、Userモデルのコードはリスト 6.42のようになります。ちなみにhas_secure_passwordメソッドは存在性のバリデーションもしてくれるのですが、これは新しくレコードが追加されたときだけに適用される性質を持っています。したがって、例えばユーザーが ' ' (6文字分の空白スペース) といった文字列をパスワード欄に入力して更新しようとすると、バリデーションが適用されずに更新されてしまう問題が発生してしまうのです。

セキュアパスワードの完全な実装

app/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }
end

この時点では、テストは greenになるはずです。

ユーザーの作成と認証
以上でUserモデルの基本部分が完了しましたので、今度は7.1でユーザー情報表示ページを作成するときに備えて、データベースに新規ユーザーを1人作成しましょう。また、Userモデルにhas_secure_passwordを追加した効果についても (例えばauthenticateメソッドの効果なども) 見ていきましょう。

ただしWebからのユーザー登録はまだできない (第7章で完成させます) ので、今回はRailsコンソールを使ってユーザーを手動で作成することにしましょう。6.1.3で説明したcreateを使いますが、後々実際のユーザーを作成する必要が出てくるので、今回はサンドボックス環境は使いません。したがって、今回作成したユーザーを保存すると、データベースに反映されます。それでは、まずrails consoleコマンドを実行してセッションを開始し、次に有効な名前・メールアドレス・パスワード・パスワード確認を渡してユーザーを作成してみましょう。

$ rails console
>> User.create(name: "Michael Hartl", email: "mhartl@example.com",
?>             password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">

うまくデータベースに保存されたかどうかを確認するために、開発環境用のデータベースをDB Browser for SQLiteで開き、usersテーブルの中身を見てみましょう (図 6.9)21。もしクラウドIDEを使っている場合は、データベースのファイルをダウンロードして開いてください (図 6.5)。このとき、先ほど定義したUserモデルの属性 (図 6.8) に対応したカラムがあることにも注目しておいてください。

images/figures/sqlite_user_row_with_password_4th_edition
図 6.9: SQLiteデータベースdb/development.sqlite3に登録されたユーザーの行
コンソールに戻ってpassword_digest属性を参照してみると、リスト 6.42のhas_secure_passwordの効果を確認できます。

>> user = User.find_by(email: "mhartl@example.com")
>> user.password_digest
=> "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3yOcVFzb6oK"

これは、Userオブジェクトを作成したときに、"foobar"という文字列がハッシュ化された結果です。bcryptを使って生成されているので、この文字列から元々のパスワードを導出することは、コンピュータを使っても非現実的です

また6.3.1で説明したように、has_secure_passwordをUserモデルに追加したことで、そのオブジェクト内でauthenticateメソッドが使えるようになっています。このメソッドは、引数に渡された文字列 (パスワード) をハッシュ化した値と、データベース内にあるpassword_digestカラムの値を比較します。試しに、先ほど作成したuserオブジェクトに対して間違ったパスワードを与えてみましょう。

>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false

間違ったパスワードを与えた結果、user.authenticateがfalseを返したことがわかります。次に、正しいパスワードを与えてみましょう。今度はauthenticateがそのユーザーオブジェクトを返すようになります。

>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">

第8章では、このauthenticateメソッドを使ってログインする方法を解説します。なお、authenticateがUserオブジェクトを返すことは重要ではなく、返ってきた値の論理値がtrueであることが重要です。4.2.3で紹介した、!!でそのオブジェクトが対応する論理値オブジェクトに変換できることを思い出してください。この性質を利用すると、user.authenticateがいい感じに仕事をしてくれるようになります。

>> !!user.authenticate("foobar")
=> true

今日はここまで

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

[graphql-ruby] [ActionCableSubscriptions] subscriptionをtriggerする際に、送るオブジェクトが独自のクラスの場合

問題

HashOpenStructActiveRecordでない、独自のクラスを下のよう感じでtriggerした場合

AppSchema.subscriptions.trigger(
  "your_subscription_name",
  { },
  {
    users: users,
  }
)

このようなエラーが出る

There was an exception - RuntimeError(        Failed to implement User.id, tried:

        - `Types::UserType#id`, which did not exist
        - `String#id`, which did not exist
        - Looking up hash key `:id` or `"id"` on `#<User:0x000055a03c518290>`, but it wasn't a Hash

        To implement this field, define one of the methods above (and check for typos)
)

原因

渡したオブジェクトがHashっぽくないって事でエラーになっている様子
しかしながら、全く同じオブジェクトをsubscriptionの最初の呼び出しの際に渡しても、それは正常に送ってくれる ...

module Subscriptions
  class YourSubscriptionName < GraphQL::Schema::Subscription

    field :users, [Types::UserType], null: false
    def subscribe)
      users = ...

      # これはなぜか問題ない
      {
        users: users
      }
    end
  end
end

解決方法

issueに上がっていた内容

  1. 独自クラスにGlobalIDを埋め込む
  2. Subscriptionの宣言時に、独自のserializerを設定する

方法1

GlobalIDを使って下のようにメソッドを増やす
*データベース絡みじゃない特殊なケースはIDをjsonデータなどにして、findの時に渡されるid(自分が先に設定したjson)をパースするなどの工夫も出来る(らしい)

# activerecordでない、独自のクラス
class User
  include GlobalID::Identification

  # 最低でも id、==、self.findが必要
  attr_reader :id

  def initialize(id)
    @id = id
  end

  def self.find(id)
    # どうにかこうにかデータを持ってくる
    User.new(id)
  end

  def ==(that)
    self.id == that.id
  end
end

方法2

subscriptionのクラスを宣言する際にserializerを設定する ... らしい

class AppSchema < GraphQL::Schema
  # 通常はこう
  # use GraphQL::Subscriptions::ActionCableSubscriptions

  use GraphQL::Subscriptions::ActionCableSubscriptions.new(serializer: YourSerialize)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】検索キーワードを空白で2つに分ける

はじめに

Itemモデルを検索する想定で中身は下記とする。

id name
1 test
2 sample
3 test sample

やりたい事

検索フォームにて空白(スペース)を含むキーワードが入力された場合、
空白を境に文字を分けそれぞれのキーワードで検索を行い結果を返したい。

items_controller.rb
    # 例えば下のキーワードで検索された場合
    @keyword = "test sample"
    # そのまま検索しようとすると
    @items = Item.where("name LIKE ?", "%#{@keyword}%")
    # 結果は"test sample"という文字列を含んだid:3のItemのみが返ってくる
    @items.ids => [3]

今回は「test sample」のキーワードで検索された際に、
Itemモデルのデータ全てを返せる事をゴールとする。

1.検索キーワードを空白で分ける

まずはキーワードの内の空白を検知して分割する為にsplitメソッドを使う。

splitの使い方
    "対象の文字列".split("区切りにする文字","分割する回数")
    # 分割する回数は省略可、省略した場合は区切りにする文字全ての箇所で分割する。
    # またsplitは配列の形でデータを返すので注意

実際に使用して書き換える。

items_controller.rb
    @keyword = "test sample"
    @keywords = @keyword.split(/[[:blank:]]+/)

ここで区切りにする文字として指定した正規表現の内容は、
[[:blank:]] → 空白演算子、半角・全角両方の空白を対象にする為。
+ → 空白が連続して入力された場合でも対応できるようにする為。

2.分割したキーワード毎に検索を行う

items_controller.rb
    # 分割したキーワード毎に検索を行いインスタンスにセット
    @keywords.each do |keyword|
      @items = Item.where("name LIKE ?", "%#{keyword}%")
    end

splitは配列でデータを返すのでeachメソッドで中身全てを取り出す。

3.結果

items_controller.rb
    @keyword = "test sample"
    @keywords = @keyword.split(/[[:blank:]]+/)
    @keywords.each do |keyword|
      @items = Item.where("name LIKE ?", "%#{keyword}%")
    end
    @items = @items.order("id ASC")
    @items.ids => [1, 2, 3]

4.最後に

検索の機能でまず実装しようと思ったのがこれだったので備忘録として。
他にも色々必要な機能があると思うので考えていきたいと思います。

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

RailsアプリにDockerとCircleCIを導入した際、DB周りでエラーになったときの対処法

こんにちは、ペーパーエンジニアのよしこです。

CircleCIでgit push時に自動テスト(RSpec)しているRailsアプリに、Dockerを導入しました。

その際にデータベース周りで躓いたエラーがあります。

同じエラーの報告が少なかったので、エラー解消した対処法を共有します。

エラー文

git pushをトリガーにCircleCIがテストを実行するのですが、その前にデータベースを構築します。
そこでエラーが出ていました。

circleci/config.yml
# DBをセットアップ
- run:
    name: DBをセットアップ
    command: bin/rails db:schema:load --trace

※全文は下記

  #!/bin/bash -eo pipefail
bin/rails db:schema:load --trace
** Invoke db:schema:load (first_time)
** Invoke environment (first_time)
** Execute environment
** Invoke db:load_config (first_time)
** Execute db:load_config
** Invoke db:check_protected_environments (first_time)
** Invoke environment 
** Invoke db:load_config 
** Execute db:check_protected_environments
rails aborted!
PG::ConnectionBad: could not translate host name "db" to address: Name or service not known
/home/circleci/project/vendor/bundle/gems/pg-0.20.0/lib/pg.rb:56:in `initialize'
.
.  省略
.
bin/rails:4:in `require'
bin/rails:4:in `<main>'
Tasks: TOP => db:schema:load => db:check_protected_environments

Exited with code exit status 1
CircleCI received exit code 1

環境

Ruby : 2.6.3
Rails : 5.1.6
postgres : 12.2
CircleCI : 2.1
Docker-compose version: '3'

結論

database.ymlに、test環境でhost: localhostを追加することで解決しました。

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

  username: postgres
  password: 
  host: db
  timeout: 5000

development:
  <<: *default
  database: haito_notice_development

test:
  <<: *default
  database: haito_notice_test
  host: localhost    # <<<<<追加<<<<<

production:
  <<: *default
  password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>

Dockerのimage上ではdbとしてDB構築していますので、develop環境やproduction環境ではhost: dbとする必要があります。

しかし私の場合は、CircleCIのDocker imageをtest環境かつhost: 127.0.0.1 (localhost)で構築しているため、これに合わせてあげる必要があったのではと理解しています。

CircleCI

基本、公式のドキュメントを元に記載していますが、
別のエラー対処のためにworkflowsやcommandsを現在は適用していないため参考程度に。

circleci/config.yml
version: 2.1
jobs:
  build:
    docker:
      - image: circleci/ruby:2.6.3-stretch-node
        environment:
          BUNDLE_JOBS: 3
          BUNDLE_RETRY: 3
          BUNDLE_PATH: vendor/bundle
          PGHOST: 127.0.0.1
          PGUSER: postgres
          RAILS_ENV: test
      - image: circleci/postgres:12-alpine
        environment:
          POSTGRES_USER: postgres
          POSTGRES_DB: app_test
    steps:
      - checkout
      - restore_cache:
          name: 依存関係キャッシュを復元
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-
      - run:
          name: Bundler を指定
          command: bundle -v
      - run:
          name: バンドルをインストール
          command: bundle check || bundle install
      - save_cache:
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
          paths:
            - vendor/bundle
      - run:
          name: 静的コード解析を実行(RuboCop)
          command: bundle exec rubocop
      - run:
          name: DBの起動まで待機
          command: dockerize -wait tcp://localhost:5432 -timeout 1m
      - run:
          name: DBをセットアップ
          command: bin/rails db:schema:load --trace
          # ここでエラーが起こっていました!!!
      - run:
          name: テストを実行(RSpec)
          command: |
            bundle exec rspec --profile 10 \
                              --format RspecJunitFormatter \
                              --out test_results/rspec.xml \
                              --format progress \
                              $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
      - store_test_results:
          path: test_results

Docker

念の為記載します。
こちらもDocker公式のドキュメントを参考に作っています。

# Dockerfile

FROM ruby:2.6.3
RUN apt-get update -qq && \
    apt-get install -y nodejs \
                      postgresql-client

RUN mkdir /app
ENV APP_ROOT /app
WORKDIR $APP_ROOT

COPY ./Gemfile $APP_ROOT/Gemfile
COPY ./Gemfile.lock $APP_ROOT/Gemfile.lock

RUN bundle install
COPY ./ $APP_ROOT

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]
entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /app/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
docker-compose.yml
version: '3'
services:
  db:
    image: postgres:12-alpine
    environment:
      POSTGRES_HOST_AUTH_METHOD: 'trust'
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    depends_on:
      - db

ご指摘やご不明な点などがございましたらお気軽にご連絡ください。

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

bundle installできないとき

直った

$ gem install bundler
$ rbenv rehash

参考

http://ztbuz.hateblo.jp/entry/2013/12/14/193235

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

どこのページからでもリンクさせたい時

はじめに

この記事が、初めての投稿なので見にくかったらすみません。。
自分の備忘録も兼ねて、学んだことを書いていきます!

トップページからしか、リンク出来ない!!

フリマアプリの作成中のことです。

ヘッダーに、ユーザー新規登録ボタンを置いていました。
トップページでは、そのボタンから登録ページへ遷移できます。
ですが、商品検索ページや商品詳細ページでは、ボタンを押してもエラーになってしまいました!!image.png

絶対パスを相対パスに変更

結論から言うと、絶対パスを相対パスに変えたら解決しました。

絶対パス↓

header.html.haml
 %li.listsRight__item.listsRight__item--new
   = link_to "新規登録",  "users/sign_up"

相対パス↓

header.html.haml
 %li.listsRight__item.listsRight__item--new
   = link_to "新規登録",  "/users/sign_up"

"users" の前に、"/(スラッシュ)" を付けただけですね。

絶対パスになっていたため、トップページ(http://localhost:3000/) を基準にしてしか飛ぶ事ができませんでした。
それを相対パスにする事で、商品ページ(http://localhost:3000/items/) など、どのページからでも遷移できるようになりました!

最後に

絶対パス/相対パスは、ただのターミナルでのディレクトリ指定方法だと考えていました。
ですが、実際の挙動にも影響を与えることを知り、基本が一番大切だなと改めて思いました。

もし解釈が間違えていたら、コメントをお願いします!

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

Dockerを学習するための記事一覧

Dockerを学習するための記事をまとめてみました

エラーの対策も記述してます。

Running bundle update will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
ERROR: Service 'web' failed to build: The command '/bin/sh -c bundle install' returned a non-zero code: 6

参考になる記事一覧

今回の動画ではインストール完了済みなので、この記事は飛ばしています。
DockerをMacにインストールする(更新: 2019/7/13)

今回実施する記事は下記になります。
DockerでRailsの環境構築

Mysqlの設定方法は下記が参考になります。
丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)

下記で $ docker-compose run web bundle installの方法が記載されてました。
DockerでRailsのプロジェクトを立ち上げるまで

下記のエラーが出た場合

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
ERROR: Service 'web' failed to build: The command '/bin/sh -c bundle install' returned a non-zero code: 6

bundle updateを実施します

ターミナル
$ docker-compose run web bundle update

docker-composeでよく使うコマンド(Ruby on Rails)

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

[HowTo]マッチングアプリ的な友達機能を実装してみる。

現在、個人アプリを作成しているのですが、その中の機能として「友達機能」を実装することにしました。
スクールではカリキュラムになかった内容であったため、自身で色々と調べて実装したので、備忘録として、
以下のように記事にさせていただきます。
皆様の実装に少しでも役立てていただければ、幸いです。

はじめに-機能に対する考え方

今回の「友達機能」実装にあたり、当初、「友達申請=>承認=>友達になる」というよくある流れでの実装を考えてました。
しかしながら、少し堅苦しい感じがしたので、もう少し気軽に友達申請を実現するために、最近流行っているマッチングアプリ的に「いいね」を友達申請の代わりに使用し、お互いに「いいね」されたら「友達」にするというやり方での実装を考えました。
以降、その考えをベースに実装をしております。

実装イメージ

今回の機能の実装イメージは以下となります。

友達申請(いいね)の動作イメージ

友達申請(いいね)以下のようにハートマークをクリックすると完了します。
この動作をお互いに行うことで”友達”として認定します。

demo

お互いにいいねがされると友達として認識されます。

demo

テーブル準備

まず、今回の友達機能を実装するにあたり"relationship"テーブルを準備いたします。

ターミナル
rails g model relationship

マイグレーションファイルは以下のように作成します。

migration_file
class CreateRelationships < ActiveRecord::Migration[5.2]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :following_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :following_id
    add_index :relationships, [:follower_id, :following_id], unique: true
  end
end

マイグレーションファイルへの記述完了次第、ターミナルでrails db:migrateをしておきましょう。

ターミナル
$ rails db:migrate

モデル編集

テーブルが作成できましたので、続いてモデルを編集していきます。
今回は、友達申請(いいね)する人を"following”、友達申請(いいね)される人を"follower"とし、
友達となっているかどうかを"matchers"メソッドを作って判別します。

User(一部)
class User < ApplicationRecord
  has_many :following_relationships, foreign_key: "follower_id", class_name: "Relationship", dependent: :destroy
  has_many :followings, through: :following_relationships
  has_many :follower_relationships, foreign_key: "following_id", class_name: "Relationship", dependent: :destroy
  has_many :followers, through: :follower_relationships

  def following?(other_user)
    following_relationships.find_by(following_id: other_user.id)
  end

  def follow!(other_user)
    following_relationships.create!(following_id: other_user.id)
  end

  def unfollow!(other_user)
    following_relationships.find_by(following_id: other_user.id).destroy
  end

#友達判定
  def matchers
    followings & followers
  end
end

Relationshipモデルについては、follower/followingをUserに帰属させます。

relationship
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :following, class_name: "User"
  validates :follower_id, presence: true
  validates :following_id, presence: true
end

以上でモデルに関しての編集は完了となります。

ビューの編集

今回の”友達申請(いいね)”に関してのビューを作成します。
(以下は”友達申請(いいね)”に関してのみの記述となりますので、必要に応じて肉付けしてください)

今回のポイントは以下の通りです。
- if文で既に友達申請(いいね)をしているか否かを見極めます。
- 既に友達申請(いいね)をしている場合は、ハートを赤くしておき、ハートを押すと未申請状態(白色)にします。
- 逆に未申請の場合は、ハートを白くしておき、ハートを押すと申請済み状態(赤色)にします。
- これらの処理をform_withで情報を飛ばすことで実装してます。

view(一部)
- if current_user.following?(@user)
 = form_with model: @relationship,url: relationship_path, method: :delete, remote: true  do |f|
  = button_tag type: 'submit', class: 'btn-liked',id: "unfollow_form" do
    %i.fas.fa-heart.fa-3x
- else
 = form_with model: @relationship, remote: true  do |f|
  %div= hidden_field_tag :following_id, @user.id
   = button_tag type: 'submit', class: 'btn-likes',id: "follow_form" do
    %i.far.fa-heart.fa-3x

コントローラ

上記にてビューも完成したので、そちらに合わせてRelationshipコントローラを編集していきます。
友達申請(いいね)をしたときは、”create"メソッドを使用し、
友達申請(いいね)を取り消すときは、"destroy"メソッドを使用してます。
今回、基本的に申請時・申請取り消し時にページ遷移の必要はなかったので、Userページにredirectしてます。

RelationshipsController
class RelationshipsController < ApplicationController

  def create
    current_user.following_relationships.create(create_params)
    redirect_to user_path(params[:following_id])
  end

  def destroy
    @user=current_user
    @relationship =  Relationship.where(following_id: params[:id],follower_id:@user.id)
    @relationship.destroy_all
    redirect_to user_path(params[:id])
  end

  private

  def create_params
    params.permit(:following_id)
  end
end

以上で、上記イメージの友達機能が実装できます!
今回はいいねボタンを友達申請ボタンの代わりに使用しましたが、viewを変更するだけで実装できます!
色々と試してみていただけますと幸いです。

参照

Railsでマッチング機能を作ってみる
https://qiita.com/Utr/items/da03bf4f23aba03da656

Ruby on Rails ~フォロー(友達申請)機能の実装(コードメモ)
https://qiita.com/wtb114/items/dbba4364871aacf520cd

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

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