20200221のRailsに関する記事は30件です。

【Rails】多対多の関連付け

1体多関連の復習

まずは1体多の関連を復習し、それからどのような場合に多対多の関連を付けるべきなのかを見ていきましょう。

関連ですので、二つモデルを用意します。一つはAuthorもう一つはBookにしましょう。

それぞれ著者と書籍を表すクラスです。書籍は一人の著者に書かれており、一人の著者は複数の書籍を出版しているとします。
aso1.png

この時、1体多の関連を作成するには、Bookクラスにauthor_idというカラムを定義し、次のようにクラスを記述するのでした。

class Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :author
end

このように定義することで、次のようなメソッドを使うことができます。

author = Author.find(id)
# ある著者が書いた書籍の配列
# author_idがauthorのidと同じ値であるbookを取得
author.books

book = Book.find(id)
# ある書籍の著者
# bookが持つauthor_idと同じ値をidに持つauthorを取得
book.author

しかし、実際には一つの書籍に複数の著者がいるという状況の方が一般的です。

ですので、book.authorsauthor.booksの双方向で複数の値を取得できるのが理想的です。現状、bookには一つのauthor_idを持つことしかできないので、何かしらの改修が必要になります。

多対多の実装

そこで多対多の関連が必要となってきます。
多対多の関連にはAuthor,Bookの他に中間クラスと言われるAuthorBookクラスを追加します。
このAuthorBookクラスはauthor_idとbook_idを持っており、著者と書籍の関連1つに付き1レコード生成されます。

ではモデルを作っていきましょう。

rails g model author_book author:references book:references
rake db:migrate

author:references,book:referencesと書くことでそれぞれのクラスと紐付けがされます。

次に各クラスを修正していきます。

class Author < ApplicationRecord
  has_many :author_books
  has_many :books, through: :author_books
end


class Book < ApplicationRecord
  has_many :author_books
  has_many :authors, through: :author_books
end

class AuthorBook < ApplicationRecord
  belongs_to :book
  belongs_to :author
end

aso2.png

これで準備は整いました。では実験してみましょう

book = Book.create
author1 = Author.create
author2 = Author.create
book.authors

この時点ではまだ中間クラスによる関連付けが存在しないので、書籍に関連する著者は表示されないはずです。

AuthorBook.create(author_id: author1.id, book_id: Book.id)
AuthorBook.create(author_id: author2.id, book_id: Book.id)
book.authors

では次はどうでしょうか?
設定がちゃんと出来れいれば2つのauthorインスタンスが表示されているはずです。

簡単ですが多対多の関連付けの説明は以上になります。

多対多の関連は例えば、投稿に対するイイね機能や俳優と出演作品の関連などに用いられます。
複数のモデルが出てきて複雑になってきましたが、使いこなして複雑なデータ構造も扱えるようにしましょう。

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

rails s できない場合の原因事例

※初心者向け
アウトプットの練習の為記述しております。

rails sできなかったケースの1つ

かなりイージミスですので初心者の方のみ参考にしてください。

ターミナルでrails sを記述し実行したのですがエラーが発生しました。
下記のメッセージ内容です。

 => Booting Puma
 => Rails 5.2.4.1 application starting in development 
 => Run `rails server -h` for more startup options
 Puma starting in single mode...
 * Version 3.12.2 (ruby 2.5.1-p57), codename: Llamas in Pajamas
 * Min threads: 5, max threads: 5
 * Environment: development
 Exiting
 Traceback (most recent call last):
         44: from bin/rails:3:in `<main>'
         43: from bin/rails:3:in `load'
 .....

これは結果から言いますと、別のローカルサーバーが既に立ち上がっていた為、エラーとなりました。

解決策して、使用していないローカルサーバーをcontrolボタン+Cボタンで切り再びrails s
これで解決しました。

もしかしたらと思ったら試してください。

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

Rails の helper でブロックを受け取って、そのブロックの中で render をつかいたい

こんなことがしたい(けどうまくいかない)

ブロックを受け取って h1 を出力するヘルパーと、それの使用例

hoge_helper.rb
  def render_h1_with_block(&block)
    content_tag(:h1, yield, class: 'title')
  end
hoge/index.html.erb
<%= render_h1_with_block do %>
  <%= render 'fuga' %>
<% end %>
hoge/_fuga.html.erb
  <span>I am fuga.</span>

これは失敗する。h1 の外側に _fuga.html.erbがレンダリングされてしまっている。 render_h1_with_blockyield したタイミングでレスポンスボディに render 'fuga' の結果が追加されてしまうようだ。

意図しない結果になったindex.html
<span>I am fuga.</span>
<h1 class="title"></h1>

解決策: capture を使う

CaptureHelper#capture をつかって render の結果を保留すればうまくいく。

The capture method extracts part of a template as a String object. You can then use this object anywhere in your templates, layout, or helpers.

(適当な翻訳) capture メソッドはテンプレートの一部を String オブジェクトとして切り出します。そのオブジェクトはテンプレート、レイアウト、ヘルパーで利用できます。

ActionView::Helpers::CaptureHelper#capture - Ruby on Rails API Document より引用

hoge_helper.rb
  def render_h1_with_block(&block)
    content_tag(:h1, capture(&block), class: 'title')
  end

公式ドキュメントに従って yieldcapture(&block) に書き換えてみた。

意図通りの結果になったindex.html
<h1 class="title"><span>I am fuga.</span></h1>

うまくいった!

ドキュメント

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

【初心者向け】Railsで投稿される日時を日本時間に変更する(タイムゾーンの修正)

タイムゾーンを日本に変更するにはconfig/application.rbを修正する

参考

伊藤さんいつも参考にしています、ありがとうございます。
https://qiita.com/jnchito/items/831654253fb8a958ec25

どのように記述するのか

module hogehoge
  class Application < Rails::Application

   config.time_zone = 'Tokyo'
   config.active_record.default_timezone = :local

  end
end

上記2文を追加するだけ。

追加した記述の2文を解説

config.time_zone = 'Tokyo'

↑実際の表示を修正する記述

config.active_record.default_timezone = :local

↑DBに保存する際にどの時間帯で保存するかの記述

伊藤さんが推奨されているlメソッドが便利でした!!しかも設定簡単

module hogehoge
  class Application < Rails::Application

   config.time_zone = 'Tokyo'
   config.active_record.default_timezone = :local

  end
end

この設定を下記のように修正します。

module hogehoge
  class Application < Rails::Application

    config.i18n.default_locale = :ja
    config.active_record.default_timezone = :local

  end
end

config.time_zone = 'Tokyo'を書き換えただけですね。

config/localesにja.ymlファイルを作成

ja:
  time:
    formats:
      default: "%Y/%m/%d %H:%M:%S"

と記述しました。

このあと、必ずサーバーを再起動(再起動しないと反映されないっぽい)

あとは、時間を表示させたいviewに

<%= l post.created_at %>

っといった形式にするだけ。

ここは違う、こうした方が良い等々ございましたらご指摘いただけますと幸いです。
最後までみていただき、ありがとうございます。

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

rails でのドラクエ再現挑戦(モンスターのデータ)

はじめに

ドラゴンクエストシリーズをRuby,Ruby on railsで作る場合はどうするのか?という事を色々試してみたいと思います。

実行

ドラゴンクエストシリーズ(以下ドラクエ)には色んな要素がありますが

まずはモンスターのデータです。
サンプルでスライムのデータを記述しています。

monsters.rb
monsters = []
monster  = {name: "スライム", species_id: 1, HP: 5, MP: 0, attack: 4, defence: 4, speed: 5, pattern: [1,1,1,1,1,1,1,1], exp: 1, gold, 2}
monsters << monster
(後略)

種族は別テーブルで保存しておいて、idで呼び出すことにします。
種族テーブルは次にような感じです。

species.rb
species = ["スライム","ドラキー","ゴースト",(後略)]

行動パターンには配列を置いて、別テーブルに保存しておいた値をidで呼び出します。

patterns.rb
patterns = []
pattern  = {name: "攻撃", target: 0, value: attack / 2, message: "#{monster.name}のこうげき"}
patterns << pattern

targetは効果対象で、0が相手・1が味方、となります。valueはダメージ計算式です。messageは行動時にどんなメッセージが表示されるかを表します。そのモンスターの名前が表示されるように、#{monster.name}を記述しています。

モンスター関連のデータはこんな感じで良いかと思います。

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

Rails Whereで取得した値にdestoryが使えない理由

詰まったこと

Whereを使い取得した値をdestroyを使って削除しようとしたが、以下のエラーが出てできなかった。

ArgumentError (wrong number of arguments (given 0, expected 1)):

解決方法

whereで取得した値は配列になっているため、destroyを使えない。
find_byを使うと削除できるようになる。

以下の記事を参考
Railsのwhereメソッドと仲良くなろう

product = Product.where(product_id: 3)
product.destroy

⬇︎

product = Product.find_by(product_id: 3)
product.destroy
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails6][sprockets4.0.0]Sprocketsを無効にするためにapp/assetsフォルダ以下を削除すると、Sprockets::Railtie::ManifestNeededErrorが出た

はじめに

Railsが用意しているjavascript, css, 画像などのアセットを管理するGemに、SprocketsWebpackerがあります。
この2つを軽く説明すると、

名称 説明
Sprockets Rails3.1から導入されたアセットパイプライン。app/assetsディレクトリでアセットを管理する
Webpacker Rails6から導入された、WebpackというバンドラをRails用にラップしたgem

WebpackerはSprocketsとの共存を考えて設計されており、Sprockets, Webpackerどちらも使うことを想定しています。
ただまあどちらもアセットを管理するgemなわけで、ややこしくなるしどちらか1つだけ使えばいいのでは?と僕は考えています。
以前作成したAsobiというWebアプリでも、Webpackerでアセットを管理したので、最終的にSprocketsが管理するapp/assetsディレクトリを丸ごと削除しました。
次のWebアプリでもWebpackerを使う予定だったので、app/assetsを削除したのですが…

環境

  • Ruby 2.7.0
  • Rails 6.0.2

起きたこと

Sprocketsを使わないようにするための手順はいくつかありますが、最終的にapp/assetsというディレクトリは完全にいらなくなります。
なので最初にapp/assetsを削除しました。

$ rm -rf app/assets

試しにここでrails sでRailsを起動しました。
ここでは正常にRailsサーバが起動すると思ったのですが…

WARNING: Nokogiri was built against LibXML version 2.9.10, but has dynamically loaded 2.9.4
=> Booting Puma
=> Rails 6.0.2.1 application starting in development
=> Run `rails server --help` for more startup options
Exiting
Traceback (most recent call last):

# 中略...

/Users/user/rails_test/vendor/bundle/ruby/2.7.0/gems/sprockets-rails-3.2.1/lib/sprockets/railtie.rb:105:in `block in <class:Railtie>': Expected to find a manifest file in `app/assets/config/manifest.js` (Sprockets::Railtie::ManifestNeededError)
But did not, please create this file and use it to link any assets that need
to be rendered by your app:

Example:
  //= link_tree ../images
  //= link_directory ../javascripts .js
  //= link_directory ../stylesheets .css
and restart your server

どうやらapp/assets/config/manifest.jsが無いためSprockets::Railtie::ManifestNeededErrorというエラー出てrails sが終了したようです。
だが待ってほしい。これまでこんなエラー出てこなかったぞ?

原因

こちらのページによると、どうやらSprocketsのバージョンが4.0.0になってから起きるようになったエラーのようです。

Redmine doesn't start with Sprockets::Railtie::ManifestNeededError if sprockets 4.0.0 is installed.

$ bin/rails c
Traceback (most recent call last):
28: from bin/rails:4:in <main>'
27: from bin/rails:4:in
require'
.
.
.
/Users/maeda/redmines/gems/ruby/2.6.0/gems/sprockets-rails-3.2.1/lib/sprockets/railtie.rb:105:in block in <class:Railtie>': Expected to find a manifest file inapp/assets/config/manifest.js` (Sprockets::Railtie::ManifestNeededError)
But did not, please create this file and use it to link any assets that need
to be rendered by your app:

Example:
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
and restart your server

Redmine doesn't start with Sprockets::Railtie::ManifestNeededError if sprockets 4.0.0 is installed.と書いてありますね。

Sprocketsのissueにも同様のエラー報告がありました。

Actual behavior
An error is thrown since 4.0, this didn't occur on 3.7.2:

rake aborted!
Sprockets::Railtie::ManifestNeededError: Expected to find a manifest file in app/assets/config/manifest.js
But did not, please create this file and use it to link any assets that need
to be rendered by your app:

Example:
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
and restart your server

3.7.2では起きなかったエラーとのことで、やはりSprockets 4.0.0から起きているエラーのようです。

対処法

Sprocketsのバージョンを3.7.2に下げる

バージョン4.0.0から起きているのであれば、バージョンを3.7.2に下げれはエラーが解消されるはず!
まずはGemfileに以下のコードを追記します。

# Sprockets4.0だと、app/assetsディレクトリを削除するとSprockets::Railtie::ManifestNeededErrorが発生する
gem 'sprockets', '~> 3.7.2'

そしてbundle updateを実行します。(bundle installじゃないよ)
改めてrails sを実行すると、無事エラーが解消されました。

$ rails s
WARNING: Nokogiri was built against LibXML version 2.9.10, but has dynamically loaded 2.9.4
=> Booting Puma
=> Rails 6.0.2.1 application starting in development
=> Run `rails server --help` for more startup options
/Users/user/rails_test/vendor/bundle/ruby/2.7.0/gems/actionpack-6.0.2.1/lib/action_dispatch/middleware/stack.rb:37: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/Users/user/rails_test/vendor/bundle/ruby/2.7.0/gems/actionpack-6.0.2.1/lib/action_dispatch/middleware/static.rb:110: warning: The called method `initialize' is defined here
Puma starting in single mode...
* Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000
* Listening on tcp://[::1]:3000
Use Ctrl-C to stop

rails newのオプションの--skip-sprocketsを指定する

rails newのオプションに--skip-sprocketsというものがあります。
これを指定してRailsアプリを作成すると、config/application.rbのrequireが一部変化します。

application.rb
# --skip-sprocketsを指定した場合
require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
# require "sprockets/railtie"
require "rails/test_unit/railtie"


# 指定しない場合、この1文のみ記述される
require 'rails/all'

require "sprockets/railtie"がコメントアウトされています。
この状態であれば、app/assetsディレクトリを削除してもSprockets::Railtie::ManifestNeededErrorは発生しません。

余談 : --skip-sprocketsを指定してもSprocketsはインストールされる

Railsガイドに書いてありますが、この--skip-sprocketsapplication.rbの変更と一部のgemを除外してくれるだけです。

# Railsガイドで説明されている、除外されるgem
gem 'sass-rails'
gem 'uglifier'
gem 'coffee-rails'

上記の除外されるgemのリストにSprocketsはありません。
そう、--skip-sprocketsオプションでSprocketsを使わないRailsアプリを作成しても、Sprocketsはインストールされます。更にapp/assetsも生成されます。

これに関しては、GitHubのRailsにも以下のissueが投げられています。

The app/assets folder, and its underlying structure, is created, despite the fact that dropping stylesheets into app/assets/stylesheets no longer causes them to be loaded. This is misleading, at best.

つまり、「--skip-sprocketsを指定したらapp/assetsを作らないようにした方が誤解がないのでは?」というissueが3年前に投げられています。
でも3年経っても変化はないので、恐らくこの状況を変えるつもりはないのでしょう。

まとめ

Sprocketsを使わないのであれば、Sprocketsのバージョンを3.7.2に落としましょう。
もしくはrails newのオプションで--skip-sprocketsを指定しましょう。

参考文献

https://www.redmine.org/issues/32223
https://github.com/rails/sprockets/issues/643
https://github.com/rails/rails/issues/29749
https://railsguides.jp/asset_pipeline.html

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

Rails"モデル"は不倫とガチ恋をする

はじめに

添野です。プログラミングを勉強中です。
今日はRails苦戦中の方向けの記事で、モデルについてです。
RailsのMVCの"M"、"モデル"がよく分かりません、という声をよく耳にします。

モデルとは

結論をいいます、モデルとは「メンヘラPretender」です。
Pretender→https://www.youtube.com/watch?v=TQ8WlA2GXbk
※Pretenderをdisっている訳ではないです。筆者はかなり髭男ファンです。Pretenderを歌いたいがために月に4回ボイトレに行っています。

モデルの役割

2つです。
・他のモデルとの人間関係を暴露する(アソシエーション)
・データベースとイチャイチャする

下記はモデルファイルの一例です。

tweet.rb
class Tweet < ApplicationRecord
  belongs_to :user
  has_many :comments

  def self.search(search)
    return Tweet.all unless search
    Tweet.where('text LIKE(?)', "%#{search}%")
  end
end

一行ずつ、順番に解説していきます。

他のモデルとの人間関係を暴露する 「アソシエーション」

belongs_to :user has_many :comments

日本語訳「私はuserモデルにガチ恋しています」「私はcommentsモデルたちと不倫しています」
まず、belongs_to = ガチ恋 has_many = 不倫を覚えてください。

下記の図は、tweetモデルさんの人間関係を表しています。

ファイル名
tweetさんが主役です。

tweetさんはuserさんに対し秘めたる思いを抱えており、つまりガチ恋をしています
しかしこれは叶わぬ恋であり、想いは一方通行なのです。美しいですね。君の運命の人は僕じゃないんですね。辛いけど否めないんです。つまりPretenderです。

ところが、tweetさんはOfficial髭男dismのように綺麗な心を持っていません。
tweetさんはメンヘラであり、commentsさんたちにN股不倫をかけています
そしてmodelファイルのなかでtweetさんはこれを堂々と宣言しています。tweetさんはかなりヤバイです。

データベースとイチャイチャする

tweetさんはメンヘラであり他のモデル達とイチャイチャしたがると上に書きましたが、データベースともイチャイチャしたがります。最高ですね。

def self.search(search)

日本語訳「メソッドを定義しろ Tweetクラスのsearchメソッドで 引数はsearch」
カレーの話をします。searchメソッドはルーティングとコントローラに定義したカレールーの隠し味です。index,new,show...みたいなのが7種類あったと思いますが、これらはカレールーです。この7種類の他にも隠し味としてリンゴ(searchメソッド)をカレールーにブチ込んだ、みたいな感じです。引数searchは、検索ワードです

return Tweet.all unless search

日本語訳「返り値をよこせ Tweetモデルの全てを 検索ワードが空っぽなら」
入力欄に何も入れずに検索したら、ツイートを全部表示しろという意味です。

Tweet.where('text LIKE(?)', "%#{search}%")

日本語訳「Tweetモデルの中身を取得しろ(含んでいれば,"検索ワード"に)」
記号だらけでゴチャゴチャしてますが、中身は簡単です。

".where"は値を取得できます。取得する時の条件も設定できる優れものです。

'text LIKE(?)',"%#{search}%"は見た目の通り呪文であり、「引数searchを含んでいれば」という意味です。
引数searchは検索ワードです。

この行は、一行前のunlessの影響を受けており、検索ワードが空じゃない時だけ仕事をします。

end

日本語訳「グッバイ」
ここからサビか!と思いきや、終わります。

書いていて思いましたが、サビの初っ端が「グッバイ」ってセンスありすぎですよね。
一番盛り上がるところで「グッバイ」て。そんなことあります?

逆に、終わりの歌詞は

「たったひとつ確かなことがあるとするのならば 君は綺麗だ」

エェェッッッッッモ...

そんな悲しいことある...?さとっちゃん、センスありすぎアリス議員です。

まとめ

モデルの役割は2つありました。

・他のモデルとの人間関係を暴露する(アソシエーション)
・データベースとイチャイチャする

他にも空っぽのツイートを禁止するバリデーション機能などもありますが、とりあえず今回の2つを覚えればOKです。モデルがやっていることは、モデルのコードに書いてあることが全てです。「○○がよく分からん」と感じたら○○に書かれているコードを読みましょう。コードは、そのコードに書かれていること以外のことはしません

以上がRails"モデル"の説明になります。

他にもRailsの疑問を解消するための記事がありますので、ご活用ください。共にTECH::EXPERTを駆け抜けましょう。

おすすめ記事
Rails消化のコツ
Railsは"5つの属性"を意識しろ
Rails用語集 基礎


添野又吉(@enjoy_omame)
https://twitter.com/enjoy_omame

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

DockerでRailsアプリケーション立ち上げ〜Githubプッシュまで

学んだこと、細かい設定、エラーをメモ

環境

Ruby 2.6.5
Rails 5.2.4
Docker 19.03.5
bundler 2.1.4

実装:Docker環境

各ファイル用意

適当にディレクトリ作成してその中に下記のファイルを入れる

ターミナル
$ mkdir hoge

Dockerfile
docker-compose.yml
Gemfile
Gemfile.lock

Dockerfile
Dockerの新しいimageを作成する際に使用する設定ファイル。
コンテナはこの設定(image)を元に作成される。
Dockerfile → クラス
コンテナ → インスタンス
みたいなイメージ?

Dockerfile
FROM ruby:2.6.5
RUN apt-get update -qq && apt-get install -y build-essential nodejs
RUN mkdir /app
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install
COPY . /app

docker-compose.yml
Dockerで複数のコンテナを設定に従ってまとめて起動するために使用する。
今回はRailsを実行するコンテナ、Mysqlを実行するコンテナの2つを起動する設定を記述。

docker-compose.yml
version: '3'
services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db
    tty: true
    stdin_open: true
  db:
    image: mysql:5.7
    volumes:
      - db-volume:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
volumes:
  db-volume:
Gemfile
source 'https://rubygems.org'
gem 'rails', '5.2.4'
Gemfile.lock
空でOK

Railsプロジェクト作成

ターミナル
$ docker-compose run web rails new . --force --database=mysql

docker-compose run web
docker-compose.ymlで設定したWebコンテナ(Railsコンテナ)の中で後に続くコマンドが実行される。

イメージをビルド

ターミナル
$ docker-compose build

Dockerfileが実行

database.yml編集

passwordhostの指定

database.yml
password: docker-compose.ymlの MYSQL_ROOT_PASSWORD で設定した文字列(今回はpassword)
host: MySQLサーバーのコンテナ名(今回はdb)

コンテナ起動

・起動

ターミナル
$ docker-compose up -d

・起動確認

ターミナル
$ docker-compose ps

stateUpになっていれば起動確認

・コンテナ、ネットワークの削除
--rmi allでイメージも削除

ターミナル
$ docker-compose down --rmi all

データベース作成

ターミナル
$ docker-compose run web bundle exec rails db:create

bundlerのバージョンを変更したらエラー

1系がインストールされていることに気がついたので2系にしたらエラーがでた。
こちらの記事を参考に。

Dockerfile
FROM ruby:2.6.5
RUN apt-get update -qq && apt-get install -y build-essential nodejs
RUN mkdir /app
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN gem install bundler # 追加
RUN bundle install
COPY . /app

bundle installの前に最新のbundlerをインストールすることで解決。

Docker内のMySQL接続方法

ターミナル
dockerで立ち上げた MySQLへの接続
$ docker-compose up -d →コンテナ起動
$ docker-compose ps →現在立ち上がってるコンテナ確認
 ↪︎ NAMEのdb名を確認(my_youtube_space_db_1)
$ docker exec -it my_youtube_space_db_1 bash → コンテナへ接続
# mysql -u root -p →シャープが表示されたら入力する
Enter password: → docker-compose.ymlで設定したパスワード入力

後はローカルのMySQLいじる時の操作と同じ 

実装:Githubへプッシュ

Git Flow

ターミナル
$ git flow init
enter * 7回

リモート作成

github → Your profile → Repositories → NEW → リモート名記述 → Create repository

リモートとローカルの紐付け

ターミナル
$ git remote add origin url
$ git push -u origin master
$ git push --all

.gitignore編集

.gitignore
# 追記
/config/database.yml
docker-compose.yml

add commit push

割愛

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

【Rails】SwitchPoint利用時にSchemaCacheを設定しSHOW FULL FIELDSを防ぐ

SwitchPointとは?

https://github.com/eagletmt/switch_point
DBの書き込み接続と読み込み接続を切り替えることができるgemです。

SchemaCacheについて

Railsではrake db:schema:cache:dumpを使うことでdb/schema_cache.ymlにテーブルやカラムの情報が書き出されます。
このキャッシュを使うことでActiveRecordが型情報などを特定する手助けになします。

SwitchPointによりSchemaCacheが使えなくなる

SwitchPointを使うとActiveRecord::Baseとは別のConnectionPoolを保持してしまうため、SchemaCacheが自動で読み込まれません。
これによる、SQL実行時にSHOW FULL FIELDS FROM some_tableが実行され不要な遅延を発生させてしまいます。

対策

config/initializers/switch_point.rb
ActiveSupport.on_load(:after_initialize) do
  ApplicationRecord.switch_point_proxy.connection.pool.schema_cache = ActiveRecord::Base.connection.schema_cache
  ApplicationRecord.switch_point_proxy.connection.schema_cache = ActiveRecord::Base.connection.schema_cache
end

initializer内でswitch_point_proxyのもつConnectionPoolにActiveRecord::Baseと同様のschema_cacheを設定してあげることで対策できます。

※ただしこのコールバックより適当な実行タイミングがあるかは未確認

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

[Rails] 2つのテーブルをjoinsで結合したときのwhereのRailsライクな書き方

きっかけ

2つのテーブルをjoinsで結合した際に、結合した側のテーブルのカラムを使用して
whereで絞り込みをする際に、どうしてもwhereの中でSQLライクな書き方をしていました。

それをRailsライクな書き方で書く方法を見つけたので残しておきます。

前提

以下のような関連があったとします。

Author has_many Book(s)
Book belongs_to Author

あるAuthorが複数のBookを持っている、という状況です。
これらのテーブルは、主に以下のようなカラムを持ち合わせています。

Author
 - id
 - name

Books
 - id
 - name
 - author_id

idとnameという同じ名前のカラムがあります。

本題

   Author.joins(:books)

とすれば、SQLのINNER JOINが実行されるので
author.idbook.author_idが等しい、つまり
author.id == book.author_idとなるようなレコードが抽出されます。
ということは、book.author_id == nilのレコードはこの時点で弾かれます。

ではここで、Bookのnameに対してwhereを使って絞り込みを行いたいと思います。

SQLライクな書き方

その際、自分はいつもこのようなSQLライクな書き方をしていました。

   Author.joins(:books).where("books.name": "XXXXX")

これをRailsライクな書き方で書こう、ということです。

Railsライクな書き方

以下のようにネストするだけです。

   Author.joins(:books).where(books: { name: "XXXXX" } )

これで上記SQLライクの場合と同じように調べられます。
ぱっと見、わかりやすくなったなと個人的には思っています。



おわり

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

RailsでHamlを使う方法⭐︎

RailsでHamlを使えるようにする方法

復習がてらにhamlを使ってコーディングしていたのでこの際アウトプットしておきます。

手順↓

  • hamlのgemをインストールする
  • 拡張子をerbからhamlにする

以上。超簡単

Hamlのgemをインストールする

Gemfile
gem 'haml-rails'

Gemfileに以上の記載をして
ターミナルで必ず bundle installを実行しましょう。

$ bundle install

この時点でhamlは使えるようになりますが拡張子も一気にerbからhamlに変更しておくと便利なので実行します。

拡張子の変更をターミナルで実行する

$ rails haml:erb2haml

実行するとターミナル上で何か聞かれますがyコマンドを押しておくとオッケーです。

以上。簡単

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

if文を用いて出力メソッドを一呼び出し

※初心者向け
※アウトプット練習の為

開発環境
rails 5.2.4.1
ruby 2.5.1

問題内容

20時から翌朝7時までにオウムに喋られると問題があるのでその場合は「NG」、
それ以外は「OK」と出力するメソッドを作成します。
オウムが喋る時をture、喋らない時をfalseと入力することにし、時刻も同時に入力します。


呼び出し方:
parrot_trouble(talking, hour)

def  parrot_trouble(talking, hour)
   if (talking  && (hour < 7 || hour > 20))
     puts "NG"
   else
     puts "OK"
   end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

erb拡張子をhaml拡張子に変更する

※初心者向け
※アウトプット練習の為、記述しております。

今回は拡張子の変換について記述しております。

開発環境
rails 5.2.4.1
ruby 2.5.1

やりたい事下記のファイルの拡張子(.erb)をファイルの内容も含め全てhamlへ変換したい。

 /view/_form.html.erb
      /item.html.erb
      /new.html.erb   

まずはGemファイルに記述 

 gem 'erb2haml'

ターミナル

 bundle install
 rails haml:erb2haml

実行結果拡張子

 /view/_form.html.haml
      /item.html.haml
      /new.html.haml

以上で変換完了です

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

simple_format で textデータに改行を反映させる

text_areaでデータを入力してそのまま表示させると改行が反映されずに読みづらい文章になってしまいます。

<%= form_with model: @post, local: true do |f| %>
  <%= f.label :text %>
  <%= f.text_area :description, rows: 5, class: 'form-control', id: 'post_description' %>
<% end %>

みたいなformに

hello.
hello.
hello.

と入力して

<%= @post.description %>

みたいな感じで普通に表示させると

hello.hello.hello.

改行が反映されずにくっついています。

調べるとsimple_formatを使うのがいいようです。

<%= simple_format(@post.description) %>
hello.
hello.
hello.

になるはずです。長い文章の入力でも大丈夫そうです。

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

bundle install した際にnokogiriのインストールでエラー

環境

  • Windows 10 Pro
  • Ubuntu 18.04.3 LTS on WSL(VSCode Remote Development)
C:>ver
Microsoft Windows [Version 10.0.18363.657]

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.2 LTS
Release:        18.04
Codename:       bionic

エラー内容

$ bundle install --path vendor/bundle
(skip)
Fetching nokogiri 1.10.8
Installing nokogiri 1.10.8 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /mnt/f/Dropbox/ToDoApp_rooter_ex/rails-todo-sample/vendor/bundle/ruby/2.6.0/gems/nokogiri-1.10.8/ext/nokogiri
/home/takao/.rbenv/versions/2.6.5/bin/ruby -I /home/takao/.rbenv/versions/2.6.5/lib/ruby/2.6.0 -r ./siteconf20200221-3705-ydqzxx.rb extconf.rb --use-system-libraries
checking if the C compiler accepts ... yes
Building nokogiri using system libraries.
pkg-config could not be used to find libxslt
Please install either `pkg-config` or the pkg-config gem per

    gem install pkg-config -v "~> 1.1"

pkg-config could not be used to find libexslt
Please install either `pkg-config` or the pkg-config gem per

    gem install pkg-config -v "~> 1.1"

checking for xmlParseDoc() in libxml/parser.h... yes
checking for xsltParseStylesheetDoc() in libxslt/xslt.h... no
checking for xsltParseStylesheetDoc() in -lxslt... yes
checking for exsltFuncRegister() in libexslt/exslt.h... no
checking for exsltFuncRegister() in -lexslt... yes
checking for xmlHasFeature()... yes
checking for xmlFirstElementChild()... yes
checking for xmlRelaxNGSetParserStructuredErrors()... yes
checking for xmlRelaxNGSetParserStructuredErrors()... yes
checking for xmlRelaxNGSetValidStructuredErrors()... yes
checking for xmlSchemaSetValidStructuredErrors()... yes
checking for xmlSchemaSetParserStructuredErrors()... yes
creating Makefile
:
(skip)
:
An error occurred while installing nokogiri (1.10.8), and Bundler cannot continue.
Make sure that `gem install nokogiri -v '1.10.8' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  rails was resolved to 6.0.2.1, which depends on
    actioncable was resolved to 6.0.2.1, which depends on
      actionpack was resolved to 6.0.2.1, which depends on
        actionview was resolved to 6.0.2.1, which depends on
          rails-dom-testing was resolved to 2.0.3, which depends on
            nokogiri

「pkg-configをインストールしてください」と書いてあるが,既にpkg-configは入っているので,問題は別にありそう.

解決策

checking for xmlParseDoc() in libxml/parser.h... yes
checking for xsltParseStylesheetDoc() in libxslt/xslt.h... no
checking for xsltParseStylesheetDoc() in -lxslt... yes
checking for exsltFuncRegister() in libexslt/exslt.h... no
checking for exsltFuncRegister() in -lexslt... yes
checking for xmlHasFeature()... yes
checking for xmlFirstElementChild()... yes
checking for xmlRelaxNGSetParserStructuredErrors()... yes
checking for xmlRelaxNGSetParserStructuredErrors()... yes
checking for xmlRelaxNGSetValidStructuredErrors()... yes
checking for xmlSchemaSetValidStructuredErrors()... yes
checking for xmlSchemaSetParserStructuredErrors()... yes
creating Makefile

この部分に注目すると,libxslt内のファイルが見つかっていないようなので,libxsltをインストール.

$ sudo apt-get install libxslt-dev

再びbundle installを行う.

$ bundle install --path vendor/bundle
:
(skip)
:
Bundle complete! 17 Gemfile dependencies, 75 gems now installed.
Bundled gems are installed into `./vendor/bundle`

無事bundle installできた.

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

bundle install時に起きたmysql2のgemエラー

注意

自分用のメモ書き&同じ状況のエラーに遭遇した初学者のための投稿になっていますので記事内容が読みづらく雑であったり間違えている箇所があるかと思いますが大目に見てくださると助かります。
間違えている箇所についてはコメント欄にて指摘していただけると助かります。

内容

既存のrailsプロジェクトを久しぶりに修正しようとした際にbundle installをしたら下記のエラーが出ました。

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

???
何もいじっていなかったので戸惑いながらエラー文を読み三行目のgem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'を実行。
解決できずで何も変わらず、、、

解決

まずエラー時は赤くなっている箇所のみではなくとりあえず遡ってちゃんと読むのが大事と改めて学びました。
この時はこんなことが書いてある箇所が少し遡ったところに書いてありました。。。

mysql client is missing. You may need to 'brew install mysql' or 'port install mysql', and
try again.

MySQLが見当たらない、、これをしてくれと書いてあるではないか!!と思い、brew install mysqlをまず実行、、、そしたら無事解決しbundle installができました!!
もう一方のport in stall mysqlはMacPortsという、macOSおよびDarwin OS上のソフトウェアの導入を単純化するパッケージ管理システムのひとつでmysqlを導入できるものらしいです。

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

Rails Unicorn起動時のエラー

本記事投稿のいきさつ

最近Railsの勉強を始めたが、その中でエラー対応に苦戦したためメモ代わりにここに残したいと思います。
また、不慣れのため表現や書き方など、分かりづらい部分があるかと思います。
優しい目で見ていただければ幸いです。

エラー

Capistranoでデプロイ後にunicornを再起動をしたかったのですが、EC2でunicorn接続をした際上手く接続ができずlessコマンドでエラーログを確認。

ArgumentError: Already running on PID:~~

との表示がされていました。

仮説

エラー文から以前のunicorn接続のプロセスが残っていると思い

ps aux | grep unicorn

入力し確認したところどうやら余分なプロセスは確認できませんでした。
もしプロセスが表示されていれば

kill プロセスid

で解決できます。しかし他に原因があるようです。
ネットで調べたところ unicorn.rb に問題がある場合にもこのエラーが発生するとのことでした。

原因と対策

unicorn.rbを確認したところ

unicorn.rb
pid "#{app_path}tmp/pids/unicorn.pid"

の設定を発見しました。
Capistranoの導入でディレクトリの構造が変わるのですが、それに伴う設定の変更を一箇所出来ていませんでした。
そのため記述を以下に変更

unicorn.rb
pid "#{app_path}/shared/tmp/pids/unicorn.pid"

これで無事動くようになりました。
エラー文だけで判断せず調べることも重要ということを改めて再認識した事象でした。

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

Elastic Beanstalk で Railsアプリをデプロイする時にハマったとこ

Elastic Beanstalk で Railsアプリをデプロイする時にハマったとこ

まとめ

Elastic Beanstalk へデプロイした経験から、ハマりやすいポイントをまとめました。
詳細については、以下の1回目2回目のところを参照してみてください。

  • セキュリティグループ
  • MySQL エンコード
  • .ebextension 設定
  • 各gemの対応

1回目

  • 文字コード(日本語の場合、utf8 へ変更必要)
  • セキュリティグループ (Mysql2::Error: Can't connect to MySQL server on '**********.*****.ap-northeast-1.rds.amazonaws.com' (4))
    • EC2 と RDS を同じVPC/サブネット上に置く方法
    • RDS のセキュリティグループに EC2 からのアクセスを許可する。
  • rails db:createをやってくれない?
    (Mysql2::Error: Unknown Databese'**********')

    • 下記コマンドにて、自分でMySQLに接続して、DB作成。
    • MySQL への接続(EC2上で(eb ssh))
      • mysql -h **************.***.ap-northeast-1.rds.amazonaws.com -P 3306 -u ** -p
    • DB作成
      • create database ***********;
    • utf8 へ変更されているかコマンドを実行して確認
  • initializers/carrierwave.rb 用に beanstalk へS3設定を追記

2回目

  • eb init
  • eb create
  • RDS 作成
  • ebに環境変数を格納
  • .ebextensionを記述

  • eb deploy

  • EC2 cd /var/app/ondeck/ 内で

    • bundle update --bundler
    • gem install bundler:2.0.2
  • local上の Ruby のversionを上げることでglobal環境のversionと合わせる

# install可能なversionを表示
$ rbenv install -l

# versionを指定してinstall
$ rbenv install 2.6.5

# インストールしたversionを使用可能な状態にする⇒shimsへの反映
$ rbenv rehash
$ rbenv local 2.6.5
$ rbenv global 2.6.5
  • bundler のバージョンがあっていない?

(参照:Gem::GemNotFoundExceptionと出てきたときの対処法 - Qiita)

$ gem install bundler -v '1.17.3'
$ bundle install
$ gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/' -- --with-cppflags=-I/usr/local/opt/openssl/include --with-ldflags=-L/usr/local/opt/openssl/lib
$ mysql -h *****RDSインスタンス*****.ap-northeast-1.rds.amazonaws.com -P 3306 -u **DB名** -p

→ 接続できた。
つまり、他の問題?

  • EC2内から直接、DB(-)を作成するコマンドを打ったところ、ハイフンは使えないということを言われる。
    → 環境設定の ***-
    *** を _** へ変更。

  • 再度、eb deployしたところ、同じエラーが出る。
    → わからないので、MySQLの中から _** を作成してしまって、再度 eb deploy

  • エラーコードが変わって、MySQLのエンコードが問題っぽい。
    [AWS][RDS][MySQL] 文字コードをutf8mb4にする - Qiita

【MySQL】Mysql2::Error: Incorrect string value 【エラー】 - Qiita

ALTER DATABASE データベース名 default character set utf8;
  • 下記エラーコードが出たので、crontabのコードを削除して、再度デプロイしてみる。
container_command 06-crontab in .ebextensions/02_setup_app.config

ようやくデプロイ完了!!

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

Railsは"5つの属性"を意識しろ

はじめに

TECH::EXPERT 72期生の添野です。短期集中Aチームです。
今日も、Rails苦戦中の方におすすめの記事です。

○○メソッドを単体で覚えてもキリがない

急ですが、皆さんは下記3つの呪文を詳細に説明できるでしょうか。
・ paramsメソッド
・ AcctiveRecordクラス
・ newメソッド

私は無理です。しかし、次のように一言添えることはできます。
「paramsは、ビューやコントローラで使うよね」
「AcctiveRecordは主にコントローラで使うよね」
「newメソッドはAcctiveRecordクラス自体が持ってる属性みたいな感じ」

いかがでしょうか?字面ではピンとこないかもですが、同期生との対面でコレを唱えると、「うお、コイツめっちゃ理解してやがる」と思われたりします。実際のところ、ふんわりとですが、内容を理解しています。

ポケモンはなぜ覚えやすいのか

突然ですが、皆さんはポケモンをやったことがあるでしょうか。
ピカチュウがどんなのか、って言われたら大半の人が「黄色いポケモン」もしくは「でんきタイプだよね」とか言うと思います。ポケモンって890種類いるらしいのですが、どうしてこんなに覚えやすいのでしょうか?

「属性」があるからです。ピカチュウは、「でんき」「黄色い」「ねずみ」などの属性を持っています。

メソッドはポケモン

頭のいい方は、私が何を言いたいか分かってしまったかもです。
上記3つの呪文について、私は「属性」を使って覚えています

「paramsは、ビューコントローラで使うよね」
「AcctiveRecordは主にコントローラで使うよね」
「newメソッドはAcctiveRecord クラス自体が持ってる属性みたいな感じ」

Railsの属性は5つしかない

実は、Railsの属性はめちゃくちゃ少ないです。下記5つだけです。
1. D属性 "データベース"
2. M属性 "モデル"
3. R属性 "ルーティン"
4. C属性 "コントローラ"
5. V属性 "ビュー"

基本はこれだけ覚えれば十分です。
これらの5属性は、それぞれファイルが分かれており、記述もクセがあります。属性分けできることを知っていれば覚えやすくなります

まとめ

私は、Railsレッスンで度々出現する呪文を5つの属性で覚えています。
皆さんも、明日から是非使ってみてください。

他にも、皆さんの役に立ちたいと思い作成した記事がありますので、是非ご活用ください。
Rails用語集 基礎
TECH::EXPERTはカレーづくり教室だった話
Rails消化のコツ

使えるものは何でも使っていきましょう。
それでは。


添野又吉(@enjoy_omame)
https://twitter.com/enjoy_omame

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

【RubyonRails】メッセージごとに異なるflashを表示する【Bootstrap】

ポートフォリオを作る中で、bootstrapを利用したflashを実装したのでメモしておきます。

flashとは

そもそもflashとは何ぞやという人のために簡単に説明すると、Webサービスによくある「ログインに成功しました!」みたいなメッセージです。

railsではあれを簡単に実装できます。
さらにbootstrapを使えばデザインもいい感じに実装できます。

前提

  • deviseを導入している
  • bootstrapをgemで導入している

ヘルパーの設定

app/helpers/devise_helper.rbを作成し、以下のように記述してください。

app/helpers/devise_helper.rb
module DeviseHelper
    def bootstrap_alert(key)
        case key
        when "alert"
            "warning"
        when "notice"
            "success"
        when "error"
            "danger"
        end
    end
end

deviseに使われてるkeyをbootstrapのアラートごとに割り当てます。

app/views/layouts/_flashes.html.erb

app/views/layouts/_flashes.html.erbを作成して以下を記述。

app/views/layouts/_flashes.html.erb
<% flash.each do |key,value| %>
    <div class="alert alert-<%= bootstrap_alert(key) %>">
        <strong>
            <%= value %>
        </strong>
    </div>
<% end %> 

「 _ 」から始まるファイルは他ファイルでも共有できるようになっていますので、application.html.erbなどに

app/views/layouts/application.html.erb
<%= render layauts/flash %>

と記述してください。

これでflashが表示されたらOKです。

まとめ

webサイト作成時flashはなかなか切り離せないので、こんな風にbootstrapを利用して簡単に設定できるのは良いですね。

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

【GCP/Rails】RakeタスクでGCSにjsonを放り込む

やりたいこと

railsのrakeタスクを実行し、GCSに送信したいと思います。
今回は、主に下記のドキュメントを参考にしました。
google-cloud-ruby/google-cloud-storage at 120144431b3542b34f83f748632d933040ef3153 · googleapis/google-cloud-ruby · GitHub

rakeタスク

rakeタスクに関しては、他の記事を参考にしてください。
RailsでRakeタスクの作成 - Qiita

GCSへアップロード

最終的な実装は下記の通りです。

# 認証
storage = Google::Cloud::Storage.new(
  project_id: ENV["GOOGLE_CLOUD_PROJECT"],
  credentials: ENV["GOOGLE_CLOUD_KEYFILE_JSON"]
)

# User情報をテーブルに書きだす。
json_file = Test.all.to_json
File.open("./test.json", 'w') do |json|
  json.write(json_file)
end

# バケットを指定する
bucket = storage.bucket ENV["GOOGLE_CLOUD_STORAGE_BUCKET"]
file = bucket.create_file "./test.json", "test.json"

サービスアカウントの作成

GCPにログインし、サービスアカウントを作成します。
権限としては、クラウドストレージの権限を付与しました。

環境変数の設定

.envファイルに上記で指定している環境変数を指定します。
サービスアカウントを環境変数に入れる場合は、GOOGLE_CLOUD_KEYFILE_JSONを使用すると、問題なく動作しました。
その他のプロジェクトID、バケットも環境変数で指定します。

google-cloud-ruby/credentials.rb at 120144431b3542b34f83f748632d933040ef3153 · googleapis/google-cloud-ruby · GitHub

アウトプット

rakeタスクを実行し、下記のようにファイルが作成されていれば成功です。

image.png

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

Railsチュートリアル 第14章 ユーザーをフォローする - [Follow] のWebインターフェイス

フォローのサンプルデータ

別記事で解説します。

演習 - フォローのサンプルデータ

1.コンソールを開き、User.first.followers.countの結果がリスト 14.14で期待している結果と合致していることを確認してみましょう。

「最初のユーザーをフォローしている人の数」ということですね。「4番目から41番めのユーザーの合計数」、すなわち(3..40).countの値と一致するはずです。

>> User.first.followers.count
=> 38

>> (3..40).count
=> 38

>> User.first.followers.count == (3..40).count
=> true

一致していますね。

2. 先ほどの演習と同様に、User.first.following.countの結果も合致していることを確認してみましょう。

「最初のユーザーがフォローしている人の数」ということですね。「3番目から51番めのユーザーの合計数」、すなわち(2..50).countの値と一致するはずです。

>> User.first.following.count
=> 49

>> (2..50).count
=> 49

>> User.first.following.count == (2..50).count
=> true

一致していますね。

統計と [Follow] フォーム

別記事で解説します。

演習 - 統計と [Follow] フォーム

1.1. ブラウザから /users/2 にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5 では [Unfollow] ボタンが表示されているはずです。

ログインユーザーのidが1で、DBの内容がフォローのサンプルデータの内容であるとすると、 /users/2 へのアクセス結果は以下の通りです。

スクリーンショット 2020-02-01 22.59.29.png

対応するRailsサーバーのログ(抜粋)は以下のようになります。

Started GET "/users/2" for 172.17.0.1 at 2020-02-01 13:55:21 +0000
  Rendering users/show.html.erb within layouts/application
  Rendered shared/_stats.html.erb (15.4ms)
  Rendered users/_follow.html.erb (3.2ms)
  Rendered users/_follow_form.html.erb (32.1ms)
  Rendered collection of microposts/_micropost.html.erb [30 times] (16.0ms)
  Rendered users/show.html.erb within layouts/application (148.5ms)

users/_follow.html.erb、ならびにusers/_follow_form.html.erbが描画されているのがわかります。

また、/users/5 へのアクセス結果は以下の通りです。

スクリーンショット 2020-02-01 22.59.35.png

対応するRailsサーバーのログ(抜粋)は以下のようになります。

Started GET "/users/5" for 172.17.0.1 at 2020-02-01 13:57:32 +0000
  Rendering users/show.html.erb within layouts/application
  Rendered shared/_stats.html.erb (10.7ms)
  Rendered users/_unfollow.html.erb (5.2ms)
  Rendered users/_follow_form.html.erb (31.5ms)
  Rendered collection of microposts/_micropost.html.erb [30 times] (27.0ms)
  Rendered users/show.html.erb within layouts/application (141.3ms)

users/_unfollow.html.erb、ならびにusers/_follow_form.html.erbが描画されているのがわかります。

1.2. /users/1 にアクセスすると、どのような結果が表示されるでしょうか?

ログインユーザーのidが1である場合、 /users/1 においては、[Follow]/[Unfollow]ボタンをレンダリングする場所そのものが確保されず、これらのボタンも表示されないはずです。どうなっているでしょうか。

スクリーンショット 2020-01-03 14.04.18.png

確かに想定通りの動作になっています。

対応するRailsサーバーのログ(抜粋)は以下のようになります。

Started GET "/users/1" for 172.17.0.1 at 2020-02-01 14:05:02 +0000
  Rendering users/show.html.erb within layouts/application
  Rendered shared/_stats.html.erb (12.5ms)
  Rendered users/_follow_form.html.erb (0.5ms)
  Rendered collection of microposts/_micropost.html.erb [30 times] (16.7ms)
  Rendered users/show.html.erb within layouts/application (132.9ms)

users/_follow.html.erbおよびusers/_unfollow.html.erbはいずれもレンダリングされず、users/_follow_form.html.erbのみがレンダリングされているのがわかります。

2. ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。

現在ログインしているユーザーは、id=1のユーザーであることを前提とします。前述の演習「演習 - フォローのサンプルデータ」より、以下の表示内容となることが期待されます。

  • Homeページ(/)とプロフィールページ(/users/1)の両方に統計情報パーシャルが表示される
  • 「following」の前に表示される数はUser.first.following.countと一致する
  • 「followers」の前に表示される数はUser.first.followers.countと一致する
>> User.first.following.count
=> 49

>> User.first.followers.count
=> 38

まずはHomeページの表示結果です。「49 following / 38 followers」という表示内容に問題はありません。

スクリーンショット 2020-02-02 19.32.31.png

このとき、Railsサーバーに記録されるログ(抜粋)は以下のようになります。

Started GET "/" for 172.17.0.1 at 2020-02-02 10:32:27 +0000
  Rendering static_pages/home.html.erb within layouts/application
  Rendered shared/_user_info.html.erb (7.1ms)
  Rendered shared/_stats.html.erb (10.7ms)
  Rendered shared/_error_messages.html.erb (0.5ms)
  Rendered shared/_micropost_form.html.erb (20.3ms)
  Rendered collection of microposts/_micropost.html.erb [30 times] (118.9ms)
  Rendered shared/_feed.html.erb (151.2ms)
  Rendered shared/_home_logged_in.erb (289.7ms)
  Rendered static_pages/home.html.erb within layouts/application (317.0ms)

統計情報パーシャルの実体であるshared/_stats.html.erbが描画されているのがわかりますね。

続いてプロフィールページの表示結果です。Homeページと同様、「49 following / 38 followers」という表示内容に問題はありません。

スクリーンショット 2020-02-02 19.32.25.png

このとき、Railsサーバーに記録されるログ(抜粋)は以下のようになります。

Started GET "/users/1" for 172.17.0.1 at 2020-02-02 10:31:33 +0000
  Rendering users/show.html.erb within layouts/application
  Rendered shared/_stats.html.erb (16.7ms)
  Rendered users/_follow_form.html.erb (0.5ms)
  Rendered collection of microposts/_micropost.html.erb [30 times] (13.5ms)
  Rendered users/show.html.erb within layouts/application (120.5ms)

こちらも、統計情報パーシャルの実体であるshared/_stats.html.erbが描画されている様子が記録されています。

3.1. Homeページに表示されている統計情報に対してテストを書いてみましょう。

ヒント: リスト 13.28で示したテストに追加してみてください。

テストコードの実体

統計情報に対するテストコードの実体は以下のようになります。

assert_select 'strong', { id: 'following', text: /#{@user.following.count.to_s}/ }
assert_select 'strong', { id: 'followers', text: /#{@user.followers.count.to_s}/ }

上記コードは、以下の事柄についてテストを行っています。

  • CSS idがfollowingであり、テキストとしてログイン済みユーザーがフォローしているユーザーの数を含むstrong要素が描画されていること
  • CSS idがfollowersであり、テキストとしてログイン済みユーザーのフォロワーの数を含むstrong要素が描画されていること

Homeページに対するテストをどこに書くか

Homeページに対するテストの実装は、test/integration/site_layout_test.rbに既に存在します。しかしながら、test/integration/site_layout_test.rb上のテストというのは、header要素内やfooter要素内といった「サイト全体に適用されるレイアウトに関するテスト」と考えるべき趣旨のものです。コンテンツ内容に関するテストを含めるのは適当ではないと考えます。

というわけで、新たに統合テストを生成してしまいましょう。「Homeページに対するテスト」ということで、統合テストの名前はhomeとします。

# rails generate integration_test home
      invoke  test_unit
      create    test/integration/home_test.rb

実際のテストの記述

実際にHomeページに対するテストを記述していきます。記述場所は、只今生成したばかりのtest/integration/home_test.rbです。

test/integration/home_test.rb
require 'test_helper'

class HomeTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:rhakurei)
    log_in_as(@user)
  end

  test "home should include following and followers with login" do
    get root_path
    assert_select 'strong', { id: 'following', text: /#{@user.following.count.to_s}/ }
    assert_select 'strong', { id: 'followers', text: /#{@user.followers.count.to_s}/ }
  end
end

Homeページ上の統計情報の描画に対するテストが成功することを確認する

現時点で、上記のテストは問題なく成功します。

# rails test test/integration/home_test.rb
Running via Spring preloader in process 754
Started with run options --seed 26350

  1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.34328s
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips

Homeページ上の統計情報の描画に対するテストが失敗する例

例えば、app/views/shared/_home_logged_in.erbに以下の欠落がある場合を考えてみます。

app/views/shared/_home_logged_in.erb
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="stats">
-       <%= render 'shared/stats' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>

この状態でtest/integration/home_test.rbに対するテストを行うと、以下のようにテストが失敗します。

# rails test test/integration/home_test.rb
Running via Spring preloader in process 767
Started with run options --seed 11814

 FAIL["test_home_should_include_following_and_followers_with_login", HomeTest, 3.5389004000026034]
 test_home_should_include_following_and_followers_with_login#HomeTest (3.54s)
        Expected at least 1 element matching "strong", found 0..
        Expected 0 to be >= 1.
        test/integration/home_test.rb:11:in `block in <class:HomeTest>'

  1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.54956s
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

発展 - ログイン済みユーザーのHomeページに対するテストとして考えられる例

マイクロポスト投稿フォームが表示されていること

test "home should include new micropost form with login" do
  log_in_as @user
  get root_path
  assert_select 'form', id: 'new_micropost'
end

マイクロポスト表示フィードがレンダリングされること

test "home should render micropost feed placeholder with login" do
  log_in_as @user
  get root_path
  assert_select 'div' do
    assert_select 'h3', text: 'Micropost Feed'
  end
end

単純に「テキストが'Micropost Feed'であるh3要素を含むdiv要素が存在すること」についてテストを行っています。

3.2. 同様にして、プロフィールページにもテストを追加してみましょう。

テストコードの内容そのものは、上記演習3.1.のものと同一です。

プロフィールページの表示内容に対するテストの実体はtest/integration/users_profile_test.rbです。前述の内容を踏まえ、test/integration/users_profile_test.rb全体の変更内容は以下のようになります。

test/integration/users_profile_test.rb
  require 'test_helper'

  class UsersProfileTest < ActionDispatch::IntegrationTest
    include ApplicationHelper

    def setup
      @user = users(:rhakurei)
    end

    test "profile display" do
      get user_path(@user)
      assert_template 'users/show'
      assert_select 'title', full_title(@user.name)
      assert_select 'h1', text: @user.name
      assert_select 'h1>img.gravatar'
      assert_match @user.microposts.count.to_s, response.body
+     assert_select 'strong', { id: 'following', text: /#{@user.following.count.to_s}/ }
+     assert_select 'strong', { id: 'followers', text: /#{@user.followers.count.to_s}/ }
      assert_select 'div.pagination', count: 1
      @user.microposts.paginate(page: 1).each do |micropost|
        assert_match micropost.content, response.body
      end
    end
  end

プロフィールページ上の統計情報の描画に対するテストが成功することを確認する

現状の実装では、上記テストは問題なく成功します。

# rails test test/integration/users_profile_test.rb
Running via Spring preloader in process 584
Started with run options --seed 41315

  1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.06740s
1 tests, 71 assertions, 0 failures, 0 errors, 0 skips

プロフィールページ上の統計情報の描画に対するテストが失敗する例

例えば、app/views/users/show.html.erbに以下の欠落がある場合を考えてみます。

app/views/users/show.html.erb(バグあり)
  <% provide(:title, @user.name) %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        ...略
      </section>
      <section>
-       <%= render 'shared/stats' %>
      </section>
    </aside>
    <div class="col-md-8">
      ...略
    </div>
  </div>

この状態でtest/integration/users_profile_test.rbに対するテストを行うと、以下のようにテストが失敗します。

# rails test test/integration/users_profile_test.rb
Running via Spring preloader in process 672
Started with run options --seed 11646

 FAIL["test_profile_display", UsersProfileTest, 4.164021200005664]
 test_profile_display#UsersProfileTest (4.16s)
        Expected at least 1 element matching "strong", found 0..
        Expected 0 to be >= 1.
        test/integration/users_profile_test.rb:17:in `block in <class:UsersProfileTest>'

  1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.17069s
1 tests, 7 assertions, 1 failures, 0 errors, 0 skips

[Following] と [Followers] ページ

ページの基本的な仕様

「フォローしているユーザー一覧」「フォロワー一覧」いずれも、そのレイアウトは類似するものとなります。具体的には、以下の要素が含まれることになります。

  • サイドバー
    • ログインユーザーの基本情報
    • ログインユーザーがフォローしているユーザーの数
    • ログインユーザーのフォロワーの数
    • フォローしているユーザー、またはフォロワーのアイコンを縮小表示して格子状に並べたもの
      • 当該ユーザーのプロフィールページへのリンクが貼られている
  • フォローしているユーザー、もしくはフォロワーのリスト

Railsチュートリアル本文では、フォローしているユーザーの一覧のモックアップを図 14.14で、フォロワーの一覧のモックアップを図 14.15で示しています。

フォロー/フォロワーページの認可のテスト

「フォローしているユーザーの一覧、フォロワーの一覧、いずれのページもログイン済みユーザーでなければアクセスできないこととする」「非ログインユーザーがこれら一覧ページにアクセスしようとした場合、 /login にリダイレクトする」という仕様を採用することをまず前提とします。これはTwitterにおける実装に倣ったものです。

となると、「これらのページへのアクセスにおいて、認可機構が正しく働いているか」のテストが必要となります。情報セキュリティに関する部分の仕様であるだけに、この部分の動作が正しいものであることは重要です。というわけで、実装より先にテストを書いていくこととします。

テストそのものの実体は以下のようになります。

test "should redirect following when not logged in" do
  get following_user_path(@user)
  assert_redirected_to login_url
end

test "should redirect followers when not logged in" do
  get followers_user_path(@user)
  assert_redirected_to login_url
end

追加するテストの実装先は、test/controllers/users_controller_test.rbです。

test/controllers/users_controller_test.rb
  require 'test_helper'

  class UsersControllerTest < ActionDispatch::IntegrationTest

    def setup
      @user       = users(:rhakurei)
      @other_user = users(:mkirisame)
    end

    ...略
+
+   test "should redirect following when not logged in" do
+     get following_user_path(@user)
+     assert_redirected_to login_url
+   end
+
+   test "should redirect followers when not logged in" do
+     get followers_user_path(@user)
+     assert_redirected_to login_url
+   end
  end

現時点でテストが成功しないことの確認

新たに実装したテストは、当然ながら現時点では成功しません。

# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 840
Started with run options --seed 47643

ERROR["test_should_redirect_followers_when_not_logged_in", UsersControllerTest, 1.9549646000086796]
 test_should_redirect_followers_when_not_logged_in#UsersControllerTest (1.96s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'followers' could not be found for UsersController
            test/controllers/users_controller_test.rb:80:in `block in <class:UsersControllerTest>'

ERROR["test_should_redirect_following_when_not_logged_in", UsersControllerTest, 2.14522200000647]
 test_should_redirect_following_when_not_logged_in#UsersControllerTest (2.15s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'following' could not be found for UsersController
            test/controllers/users_controller_test.rb:75:in `block in <class:UsersControllerTest>'

  11/11: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.21912s
11 tests, 18 assertions, 0 failures, 2 errors, 0 skips

そもそもfollowingfollowersというアクションはまだ実装していないので、テストが通らないのみ当然といえば当然です。ただ、RoutingErrorではなくActionNotFoundなので、ルーティングの実装は正常に行えているようです。

Usersコントローラーに、followingアクションとfollowersアクションを実装する

先ほどテストで発生したエラーを解決するために、Usersコントローラーにfollowingアクションとfollowersアクションを実装していきます。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
    ...略
+
+   def following
+   end
+
+   def followers
+   end

    private
      ...略
  end

Usersコントローラーにfollowingアクションとfollowersアクションがある状態でのテスト結果

この時点でtest/controllers/users_controller_test.rbを対象としてテストを行うと、その結果は以下のようになります。

# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 861
Started with run options --seed 8813

ERROR["test_should_redirect_followers_when_not_logged_in", UsersControllerTest, 2.068188299992471]
 test_should_redirect_followers_when_not_logged_in#UsersControllerTest (2.07s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: UsersController#followers is missing a template for this request format and variant.

        request.formats: ["text/html"]
        request.variant: []

        NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
            test/controllers/users_controller_test.rb:80:in `block in <class:UsersControllerTest>'

ERROR["test_should_redirect_following_when_not_logged_in", UsersControllerTest, 4.581023499995354]
 test_should_redirect_following_when_not_logged_in#UsersControllerTest (4.58s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: UsersController#following is missing a template for this request format and variant.

        request.formats: ["text/html"]
        request.variant: []

        NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
            test/controllers/users_controller_test.rb:75:in `block in <class:UsersControllerTest>'

  11/11: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.58386s
11 tests, 18 assertions, 0 failures, 2 errors, 0 skips

上記エラーが発生する状態で、Webブラウザから /users/1/following というリソースにアクセスすると、Railsサーバーには以下のようなログが記録されます。

Started GET "/users/1/following" for 172.17.0.1 at 2020-02-03 22:46:07 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#following as HTML
  Parameters: {"id"=>"1"}
Completed 406 Not Acceptable in 1125ms
...略

HTTPリクエストが406というエラーコードを返して終了している、という状態ですね。「406」というエラーコードの意味はさておき、この処理で返ってくるHTTPのレスポンスコードは「3XX(リダイレクト)」でなければなりません。

Usersコントローラーのfollowingアクションとfollowersアクションに対し、「ログイン済みユーザーでなければログイン画面にリダイレクトする」という動作が行われるようにする

表題記載の動作が行われるようにするためには、Usersコントローラーのbeforeフィルターに以下のコードを追加します。

before_action :logged_in_user, only: [:following, :followers]

実際にapp/controllers/users_controller.rbに適用する変更は以下のようになります。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
-   before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
+   before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers]
    before_action :correct_user,   only: [:edit, :update]
    before_action :admin_user,     only: :destroy

    ...略
  end

この時点で、test/controllers/users_controller_test.rbを対象としたテストが成功するようになる

この時点で、test/controllers/users_controller_test.rbを対象としたテストは成功するようになります。

# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 958
Started with run options --seed 46648

  11/11: [=================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.49225s
11 tests, 20 assertions, 0 failures, 0 errors, 0 skips

しかしながら、現時点でfollowingアクションおよびfollowersアクションの動作は何も実装していません。これらの動作の実装が必要となります。

followingアクションとfollowersアクションの動作の実装

followingアクションとfollowersアクションの動作に対するテスト

following/followerをテストするためのfixture

test/fixtures/relationships.yml
one:
  follower: rhakurei
  followed: skomeiji

two:
  follower: rhakurei
  followed: rusami

three:
  follower: skomeiji
  followed: rhakurei

four:
  follower: mkirisame
  followed: rhakurei

このfixtureは、以下のようなフォロー関係を定義しています。

  • rhakureiがskomeijiとrusamiをフォローする
  • skomeijiとmkirisameがrhakureiをフォローする

following/followerページに対する統合テストを生成する

「実際にWebブラウザに描画される内容をテストしたい」という場面なので、テストの種類は統合テストとなります。following/followerページのビューの実装が現状存在しないので、対応する統合テストも現状存在しません。まずは必要な統合テストを生成することが始まりですね。テストの名前はfollowingとします。

# rails generate integration_test following
      invoke  test_unit
      create    test/integration/following_test.rb

following/followerページのテストの実体

前項で生成された統合テストのファイル名はtest/integration/following_test.rbとなります。テストそのものの内容は以下のようになります。

test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:rhakurei)
    log_in_as(@user)
  end

  test "following page" do
    get following_user_path(@user)
    assert_not @user.following.empty?
    assert_match @user.following.count.to_s, response.body
    @user.following.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followers page" do
    get followers_user_path(@user)
    assert_not @user.followers.empty?
    assert_match @user.followers.count.to_s, response.body
    @user.followers.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end
end
assert_not @user.following.empty?assert_not @user.followers.empty?というテストの意味合い
assert_not @user.following.empty?

@user.following.empty?の戻り値がtrueである場合、このあとの@user.following.eachブロック内にあるassert_selectというテストが実行されなくなってしまいます。そのような状況で「テストが成功した」と主張するのは不適当です。ゆえに、「@user.following.empty?の戻り値がtrueである場合はテストを失敗させる」という処理を先に実行しています。@user.following.empty?の戻り値がtrueとなる場合には、例えば「fixtureの内容が不適当な場合」があります。

また、@user.followersに対する以下のテストも意味合いは同様です。

assert_not @user.followers.empty?

現状における、test/integration/following_test.rbを対象としたテストの結果

「Usersコントローラーに、following/followers両アクションのみが実装されており、following/followersアクションの処理内容が実装されていない」という状態で、test/integration/following_test.rbを実行してみます。結果は以下のようになります。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 1077
Started with run options --seed 36731

ERROR["test_followers_page", FollowingTest, 2.2801150000013877]
 test_followers_page#FollowingTest (2.28s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: UsersController#followers is missing a template for this request format and variant.

        request.formats: ["text/html"]
        request.variant: []

        NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
            test/integration/following_test.rb:19:in `block in <class:FollowingTest>'

ERROR["test_following_page", FollowingTest, 3.6601012000028277]
 test_following_page#FollowingTest (3.66s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: UsersController#following is missing a template for this request format and variant.

        request.formats: ["text/html"]
        request.variant: []

        NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
            test/integration/following_test.rb:10:in `block in <class:FollowingTest>'

  2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.66550s
2 tests, 0 assertions, 0 failures, 2 errors, 0 skips

HTTPリクエストが406というエラーコードを返して終了している、という状態ですね。「406」というエラーコードの意味はさておき、この処理で返ってくるHTTPのレスポンスコードは「200」でなければなりません。

Usersコントローラーにおける、followingアクションとfollowersアクションの動作の実装

  • 「誰がフォローしているユーザーか」「誰のフォロワーか」の「誰」の部分については、GETリクエストに渡すパラメータのid属性の値によって与える
  • ユーザー一覧の表示に対し、ページネーション処理を行う

上記箇条書きの内容を前提条件とすると、followingアクションとfollowersアクションの動作の実装内容は以下のようになります。

def following
  @title = "Following"
  @user  = User.find(params[:id])
  @users = @user.following.paginate(page: params[:page])
  render 'show_follow'
end

def followers
  @title = "Followers"
  @user  = User.find(params[:id])
  @users = @user.followers.paginate(page: params[:page])
  render 'show_follow'
end

後述するように、フォローしているユーザーの一覧/フォロワーの一覧とも、一つのERbで両方の場合をカバーできる程度にページ構造は酷似しています。ゆえに、「コントローラーの2つのアクションが同一ビューを描画する」という実装になるわけです。

最終的に、app/controllers/users_controller.rbに対して加える変更の内容は以下のようになります。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
    before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers]
    before_action :correct_user,   only: [:edit, :update]
    before_action :admin_user,     only: :destroy

    ...略

    def following
+     @title = "Following"
+     @user  = User.find(params[:id])
+     @users = @user.following.paginate(page: params[:page])
+     render 'show_follow'
    end

    def followers
+     @title = "Followers"
+     @user  = User.find(params[:id])
+     @users = @user.following.paginate(page: params[:page])
+     render 'show_follow'
    end

    private

      ...略
  end

followingアクションとfollowersアクションに必要なビューの実装

当然ながら、show_followというビューそのものの実装も必要となります。ファイル名はapp/views/users/show_follow.html.erbとします。

app/views/users/show_follow.html.erb
<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

再びtest/integration/following_test.rbを対象としたテストを実行する

Usersコントローラーのfollowingアクションとfollowersアクション、これらのアクションに必要なビュー、以上の実装が完了しました。この時点で、再びtest/integration/following_test.rbを対象としたテストを実行してみましょう。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 1129
Started with run options --seed 47849

  2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.18609s
2 tests, 10 assertions, 0 failures, 0 errors, 0 skips

ここまで実装したコードの内容に間違いがなければ、test/integration/following_test.rbを対象としたテストは成功するはずです。

# rails test
Running via Spring preloader in process 1142
Started with run options --seed 56786

  72/72: [=================================] 100% Time: 00:00:10, Time: 00:00:10

Finished in 10.74545s
72 tests, 357 assertions, 0 failures, 0 errors, 0 skips

テストスイート全体に対するテストも成功しましたね。

following/followerページの表示結果

現在のユーザーにフォローされているユーザーの一覧表示のスクリーンショットを以下に示します。アドレスバー部分を見てのとおり、followingアクションを経由してshow_followビューが呼び出された結果となります。

スクリーンショット 2020-02-05 19.07.38.png

続いて、現在のユーザをフォローしているユーザーの一覧表示のスクリーンショットを以下に示します。アドレスバー部分を見てのとおり、followersアクションを経由してshow_followビューが呼び出された結果となります。

スクリーンショット 2020-02-05 19.07.53.png

ログイン済みであれば、ログインユーザー以外のユーザーに対しても、当該ユーザーをフォローしているユーザーを一覧表示することも可能です。以下のスクリーンショットがその例です。

スクリーンショット 2020-02-05 19.14.19.png

演習 - [Following] と [Followers] ページ

1.1. ブラウザから /users/1/followers と /users/1/following を開き、それぞれが適切に表示されていることを確認してみましょう。

以下のスクリーンショットの通りです。

スクリーンショット 2020-02-05 19.07.38.png

スクリーンショット 2020-02-05 19.07.53.png

1.2. /users/1/followers や /users/1/following において、サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?

/users/1/followers における、サイドバーにある単一の画像に対応するHTMLコードは、例えば以下のようになります。

サイドバーにある単一の画像に対応するHTMLコード
<a href="/users/3">
<img alt="Berry Cremin" class="gravatar" src="https://secure.gravatar.com/avatar/2065436fdfe2d27dc7f06b6787a4a1af?s=30">
</a>

リンク先が /users/3 であることを踏まえて、実際に当該画像をクリックしてみます。すると、Railsサーバーは以下のようなログを出力します。

Started GET "/users/3" for 172.17.0.1 at 2020-02-05 22:50:12 +0000
...略
Completed 200 OK in 1276ms (Views: 1154.3ms | ActiveRecord: 69.4ms)

/users/3 へのGETリクエストが発行され、「200 OK」でリクエストが完了しています。「サイドバーにある画像は、リンクとしてうまく機能していることが確認できた」といえそうです。

2. リスト 14.29assert_selectに関連するコードをコメントアウトしてみて、テストが正しくredに変わることを確認してみましょう。

app/views/users/show_follow.html.erbに以下の欠落がある場合、当該テストは、assert_selectのところで失敗するはずです。

app/views/users/show_follow.html.erb
  <% provide(:title, @title) %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= gravatar_for @user %>
        <h1><%= @user.name %></h1>
        <span><%= link_to "view my profile", @user %></span>
        <span><b>Microposts:</b> <%= @user.microposts.count %></span>
      </section>
      <section class="stats">
        <%= render'shared/stats' %>
        <% if @users.any? %>
          <div class="user_avatars">
            <% @users.each do |user|%>
-             <%= link_to gravatar_for(user, size: 30), user %>
            <% end %>
          </div>
        <% end %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3><%= @title %></h3>
      <% if @users.any? %>
        <ul class="users follow">
-         <%= render @users %>
        </ul>
        <%= will_paginate %>
      <% end %>
    </div>
  </div>

実際にテストを実行してみましょう。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 1196
Started with run options --seed 9425

 FAIL["test_followers_page", FollowingTest, 2.336555000001681]
 test_followers_page#FollowingTest (2.34s)
        Expected at least 1 element matching "a[href="/users/919532091"]", found 0..
        Expected 0 to be >= 1.
        test/integration/following_test.rb:23:in `block (2 levels) in <class:FollowingTest>'
        test/integration/following_test.rb:22:in `block in <class:FollowingTest>'

 FAIL["test_following_page", FollowingTest, 2.4256373999960488]
 test_following_page#FollowingTest (2.43s)
        Expected at least 1 element matching "a[href="/users/314048677"]", found 0..
        Expected 0 to be >= 1.
        test/integration/following_test.rb:14:in `block (2 levels) in <class:FollowingTest>'
        test/integration/following_test.rb:13:in `block in <class:FollowingTest>'

  2/2: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.42861s
2 tests, 8 assertions, 2 failures, 0 errors, 0 skips

想定通りの形でテストが失敗しました。

followingアクションおよびfollowersアクションの統合テストにおける、Railsチュートリアル本文記載のテストの不具合

実は、Railsチュートリアル本文のリスト 14.29に記述されているテストには、1つの不具合があります。不具合の内容とその修正については、別記事に記載しています。

[Follow] ボタン (基本編)

Relationshipsコントローラーの作成

「フォロー」「フォロー解除」という動作は、それぞれリレーションシップの作成と削除に対応しています。RESTアーキテクチャを前提とした場合、少なくともcreateアクションとdestroyアクションが確実に必要となる場面ですね。

というわけで、まずはRelationshipsコントローラーの作成から始めます。

# rails generate controller Relationships
Running via Spring preloader in process 1262
      create  app/controllers/relationships_controller.rb
      invoke  erb
      create    app/views/relationships
      invoke  test_unit
      create    test/controllers/relationships_controller_test.rb
      invoke  helper
      create    app/helpers/relationships_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/relationships.coffee
      invoke    scss
      create      app/assets/stylesheets/relationships.scss

Relationshipsコントローラーに対するテストの記述

認可に関係する動作なので、実装に万全を期すために、テストを先に書いてから実装に取り掛かっていくこととしましょう。

test/controllers/relationships_controller_test.rb
require 'test_helper'

class RelationshipsControllerTest < ActionDispatch::IntegrationTest

  test "create should require logged-in user" do
    assert_no_difference 'Relationships.count' do
      post relationships_path
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_differende 'Relationships.count' do
      delete relationship_path(relationships(:one))
    end
    assert_redirected_to login_url
  end
end

テストの記述内容に問題がないのであれば、現時点におけるtest/controllers/relationships_controller_test.rbに対するテストの実行結果は以下のようになります。

# rails test test/controllers/relationships_controller_test.rb
Running via Spring preloader in process 1284
Started with run options --seed 280

ERROR["test_create_should_require_logged-in_user", RelationshipsControllerTest, 1.4866881000052672]
 test_create_should_require_logged-in_user#RelationshipsControllerTest (1.49s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'create' could not be found for RelationshipsController
            test/controllers/relationships_controller_test.rb:7:in `block (2 levels) in <class:RelationshipsControllerTest>'
            test/controllers/relationships_controller_test.rb:6:in `block in <class:RelationshipsControllerTest>'

ERROR["test_destroy_should_require_logged-in_user", RelationshipsControllerTest, 1.673922499991022]
 test_destroy_should_require_logged-in_user#RelationshipsControllerTest (1.67s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'destroy' could not be found for RelationshipsController
            test/controllers/relationships_controller_test.rb:14:in `block (2 levels) in <class:RelationshipsControllerTest>'
            test/controllers/relationships_controller_test.rb:13:in `block in <class:RelationshipsControllerTest>'

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.68059s
2 tests, 0 assertions, 0 failures, 2 errors, 0 skips

AbstractController::ActionNotFoundというエラーが発生しています。Relationshipsコントローラーに、createアクションもdestroyアクションも定義されていないためにエラーが発生しているのですね。

Relationshipsコントローラーに、createアクション・destroyアクション・logged_in_userフィルターを追加する

何はなくとも、まずRelationshipsコントローラーにcreateアクションおよびdestroyアクションの実装が必要となります。logged_in_userフィルターによるアクセス制御も同時に追加します。

app/controllers/relationships_controller.rb
  class RelationshipsController < ApplicationController
+   before_action :logged_in_user
+
+   def create
+   end
+
+   def destroy
+   end
  end

createアクション・destroyアクション・logged_in_userフィルターがあるRelationshipsコントローラーに対するテストの結果

ここまでの実装が完了したところで、現時点のtest/controllers/relationships_controller_test.rbを対象に、改めてテストを実行してみます。

# rails test test/controllers/relationships_controller_test.rb
Running via Spring preloader in process 1323
Started with run options --seed 62701

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.55615s
2 tests, 4 assertions, 0 failures, 0 errors, 0 skips

テストが無事成功しました。

Relationshipsコントローラーの完全な実装

Relationshipsコントローラーの完全な実装、すなわちapp/controllers/relationships_controller.rbの最終的な中身は、以下のようになります。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    user = User.find(params[:id])
    current_user.follow(user)
    redirect_to user
  end

  def destroy
    user = Relationships.find(params[:id]).followed
    current_user.unfollow(user)
    redirect_to user
  end
end

非ログインユーザーが、Relationshipsリソースに直接POSTDELETEを行った場合の動作

実は、beforeフィルターがない状態でも、「非ログインユーザーがRelationshipsリソースに直接POSTDELETEを実行した場合、RDBの内容に変化は生じない」という動作は実現されています。その流れは以下の通りです。

  1. 非ログインユーザーが(curl)Relationshipsリソースに直接POSTDELETEを実行する
  2. createアクションにせよdestroyアクションにせよ、current_usernilになる
  3. followunfollowが呼び出された時点で例外が発生する

しかしながら、「アプリケーションロジックの正常な動作が、例外の発生に依存したものとなる」というのは避けたいパターンです。しかもそれが、「nilに対する参照」という例外であるならばなおさらです。ゆえに今回は、「beforeフィルターを追加する」という実装を行っています。

演習 - [Follow] ボタン (基本編)

1. ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?

下記が初期状態のスクリーンショットです。「ログインユーザーはid=2のユーザーをフォローしていない」という状態です。

スクリーンショット 2020-02-07 7.52.39.png

[Unfollow]ボタンではなく[Follow]ボタンが表示されていますね。

では[Follow]ボタンを押してみましょう。結果は以下のようになります。

スクリーンショット 2020-02-07 7.52.47.png

[Follow]ボタンではなく[Unfollow]ボタンが表示されていますね。

では[Unfollow]ボタンを押してみましょう。結果は以下のようになります。

スクリーンショット 2020-02-07 7.52.53.png

[Unfollow]ボタンではなく[Follow]ボタンが表示されています。

2. 先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?

フォローが実行されたときの処理として、Railsサーバーには以下のログが出力されています。

Started POST "/relationships" ...略
Redirected to http://localhost:8080/users/2
Completed 302 Found in 65ms (ActiveRecord: 38.6ms)


Started GET "/users/2" for 172.17.0.1 at 2020-02-06 22:52:42 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#show as HTML
  Parameters: {"id"=>"2"}
  ...略
  Rendering users/show.html.erb within layouts/application
  ...略
  Rendered shared/_stats.html.erb (16.1ms)
  ...略
  Rendered users/_unfollow.html.erb (9.7ms)
  Rendered users/_follow_form.html.erb (41.2ms)
  ...略
  Rendered users/show.html.erb within layouts/application (168.1ms)
  ...略
Completed 200 OK in 629ms (Views: 576.3ms | ActiveRecord: 28.2ms)

_unfollow.html.erbが描画されている」というのが重要です。「_unfollow.html.erbは、_follow_form.html.erbにおいて、ログインユーザーが対象のユーザーをフォローしている場合に描画される」ように実装したのでしたよね。

一方、フォロー解除が実行されたときの処理としては、Railsサーバーには以下のログが出力されています。

Started DELETE "/relationships/88" ...略
Redirected to http://localhost:8080/users/2
Completed 302 Found in 55ms (ActiveRecord: 36.7ms)


Started GET "/users/2" for 172.17.0.1 at 2020-02-06 22:52:49 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#show as HTML
  Parameters: {"id"=>"2"}
  ...略
  Rendering users/show.html.erb within layouts/application
  ...略
  Rendered shared/_stats.html.erb (12.9ms)
  ...略
  Rendered users/_follow.html.erb (2.0ms)
  Rendered users/_follow_form.html.erb (30.1ms)
  ...略
  Rendered collection of microposts/_micropost.html.erb [30 times] (14.1ms)
  ...略
  Rendered users/show.html.erb within layouts/application (160.8ms)
  ...略
Completed 200 OK in 881ms (Views: 828.7ms | ActiveRecord: 24.9ms)

_follow.html.erbが描画されている」というのが重要です。「_follow.html.erbは、_follow_form.html.erbにおいて、ログインユーザーが対象のユーザーをフォローしていない場合に描画される」ように実装したのでしたよね。

なお、参考として、app/views/users/_follow_form.html.erbそのものの実装内容は以下のようになっていることを明記しておきます。

app/views/users/_follow_form.html.erb
<% unless current_user?(@user) %>
  <div id="follow_form">
    <% if current_user.following?(@user) %>
      <%= render 'unfollow' %>
    <% else %>
      <%= render 'follow' %>
    <% end %>
  </div>
<% end %>

[Follow] ボタン (Ajax編)

現状の[Follow]/[Unfollow]ボタンの実装の問題点

現状の[Follow]/[Unfollow]ボタンの実装では、「ボタンをクリックした後、ログインユーザー自身のプロフィールページにリダイレクトされる」という動作になっています。

しかしながら、[Follow]/[Unfollow]ボタンが表示されているのは、専用の投稿フォームではなく、任意のユーザーのプロフィールページです。「何らかのアクションをとると、勝手にページの移動が発生する」という挙動は、フォーム以外に内容のないページならともかく、そうでないページの場合はユーザーの期待に反する動作である可能性が高いです。

このような場合は、「ページ移動が発生せず、[Follow]/[Unfollow]ボタンのあるページに留まる」という実装のほうが望ましいのではないでしょうか。

Ajaxを使えば、上記の問題点に対し、より望ましい形の実装に持っていける

Ajaxを使えば、WebブラウザとWebサーバーの間での「非同期」処理が可能になります。「ページを移動することなくリクエストを送信する」という処理です。「ページ移動が発生せず、[Follow]/[Unfollow]ボタンのあるページに留まる」という処理も、Ajaxによって実現が可能です。

RailsにおけるAjaxの利用

Railsにおいても、Ajaxの利用は容易に可能です。ビューに記述されているform_forメソッドにremote: trueというオプションを追加すれば、それだけでRailsアプリケーションは自動的にAjaxを使うようになります。

form_for ..., remote: true

Ajaxを使ったフォローフォーム・フォロー解除フォーム

Ajaxを使ったフォローフォームのコードは以下のようになります。

app/views/users/_follow.html.erb
- <%= form_for(current_user.active_relationships.build) do |f| %>
+ <%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
    <div><%= hidden_field_tag :followed_id, @user.id %></div>
    <%= f.submit "Follow", class: "btn btn-primary" %>
  <% end %>

一方、Ajaxを使ったフォロー解除フォームのコードは以下のようになります。

app/views/users/_unfollow.html.erb
  <%= form_for(current_user.active_relationships.find_by( followed_id: @user.id),
-                                                         html:{ method: :delete })
+                                                         html:{ method: :delete },
+                                                         remote:true)
  do |f| %>
    <%= f.submit "Unfollow", class: "btn" %>
  <% end %>

上記埋め込みRubyで生成されるHTMLの内容

上記の埋め込みRubyでは、例えば以下のようなHTMLが生成されます。

<form class="new_relationship" id="new_relationship" action="/relationships" accept-charset="UTF-8" data-remote="true" method="post">
...略
</form>

formタグの内部でdata-remote="true"が設定されている」というのがポイントです。この属性設定は、「JavaScriptによるフォーム操作を許可することをRailsに知らせる」という意味があります。

コントローラー側のAjax対応

respond_toメソッド

Ajaxに対応するためには、コントローラー側の実装も一部変更する必要があります。具体的には、「respond_toメソッドを使い、リクエストの種類によって応答を場合分けする」という実装が必要になります。respond_toメソッドの基本的な用法は以下のようになります。

respond_to do |format|
  format.html { redirect_to user }
  format.js
end

respond_toは引数としてブロックを取りますが、その動作は「ブロック内のコードのうち、いずれかの1行が処理される」というものになります。

Relationshipsコントローラーの実装を変更する

Relationshipsコントローラーの実体であるapp/controllers/relationships_controller.rbの内容は、以下のように変更します。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

「Relationshipsコントローラーのアクションで使われる変数を、ローカル変数のuserではなくインスタンス変数の@userに変更した」という点には注意が必要です。

user@userに変更した理由を説明するにあたっては、「app/views/users/_follow_form.html.erbというビューは、@userの内容に応じて動作を分岐させるという実装である」というのが重要なポイントです。この実装を踏まえると、「ページ遷移が発生することなしに、[follow]/[unfollow]ボタンの描画状態に変化が発生する」というユースケースを実現するためには、Relationshipsコントローラーのアクションで直接@userを書き換える必要が出てきます。そのためuser@userに変更する必要が発生した、という次第です。

WebブラウザでJavaScriptが無効に設定されていた場合のための、Railsの設定の変更

Webブラウザ側でJavaScriptが無効にされていると、当然ながらWebブラウザでAjaxリクエストを発行することはできません。RailsアプリケーションでAjax対応を前提とした実装を行った場合、JavaScript無効のWebブラウザでアプリケーションを動かすためには、Rails側の設定を変更する必要があります。具体的には、「認証トークンがremoteフォームに埋め込まれるようにする」必要があります。

# 認証トークンをremoteフォームに埋め込む
config.action_view.embed_authenticity_token_in_remote_forms = true

変更対象となるファイルはconfig/application.rbです。

config/application.rb
  require_relative 'boot'

  require 'rails/all'

  ...略

  module SampleApp
    class Application < Rails::Application
      ...略
+
+     # 認証トークンをremoteフォームに埋め込む
+     config.action_view.embed_authenticity_token_in_remote_forms = true
    end
  end

Ajaxリクエストを受信したときに呼び出される埋め込みRubyファイルの実装

生成すべきファイルの名前

まずは前提知識から。RailsアプリケーションがHTTPのGETリクエストを受信すると、対応するアクション(indexshownewedit)と同じ名前を持つHTML用の埋め込みRuby(例えばshowアクションに対するshow.html.erb)が自動で呼び出されます。Railsチュートリアルを第14章まで進めてきた人であれば、ここまでは既知かと思います。

RailsアプリケーションがAjaxリクエストを受信した場合も、その動作はHTTPのGETリクエストに対する動作と酷似したものになります。すなわち、「対応するアクションと同じ名前を持つ埋め込みRubyが自動で呼び出される」という動作をするのです。但し、Ajaxリクエストに対する動作の場合は、「呼び出される埋め込みRubyは、HTML用ではなくてJavascript用のものとなる」という違いがあります。

「Ajaxリクエストに対して実行される動作の内容の定義と、各動作に対応するerbファイルの名前」は、今回の場合、以下のような関係になります。

動作 対応するアクションの内容 対応するファイル名
フォロー Relationshipオブジェクトのcreate app/views/relationships/create.js.erb
フォロー解除 Relationshipオブジェクトのdestroy app/views/relationships/destroy.js.erb

jQueryによるDOM操作

前提となる記法

$("#follow_form")

上記の$(#follow_form)というオブジェクトは、「follow_formというCSS idを持つ要素」を指します。フォームそのものを指すものではありません。なお、クラスを指す場合は、#の代わりに.を用います。こちらもCSSと同様ですね。

$("#follow_form").html("foobar")

例えば、follow_formというCSS idを持つフォロー用フォーム全体を"foobar"という文字列で置き換えたい場合、以上のようなコードを使います。

 create.js.erbdestroy.js.erbの実際の中身

app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('user/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');
app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html("<%= @user.followers.count %>");

上記コードのポイントは以下です。

  • JS-ERbでは、素のJavaScriptとは異なり、組み込みRubyを使うことができる
  • JS-ERbでRailsのrenderメソッドを使ってJavaScriptファイル内にHTMLを挿入する際には、escape_javascriptメソッドで「JavaScriptのダメ文字」をエスケープする必要がある

Ajaxによる[Follow]/[Unfollow]ボタンの実装における注意事項

Ajaxによる[Follow]/[Unfollow]ボタンの実装が完了したら、一旦開発環境のサンプルアプリケーションからログアウトした上で、Railsサーバーを再起動し、再度ログインしましょう。

Ajaxによる[Follow]/[Unfollow]ボタンの実装後、[Unfollow]ボタンからDELETEリクエストを発行する動作が正常に行われるようにするためには、おそらく「ログアウト→再ログイン」という操作が必要となります。そうでないと、「/relationships/:id に対し、DELETEではなくPOSTを発行してしまい、ActionController::RoutingErrorが発生する」という事態になります。

演習 - [Follow] ボタン (Ajax編)

1. ブラウザから /users/2 にアクセスし、うまく動いているかどうか確認してみましょう。

スクリーンショット 2020-02-14 8.25.10.png

followersの数は0で、[Follow]ボタンが表示されています。

ここで[Follow]ボタンをクリックしてみます。次に出た画面のスクリーンショットは以下です。

スクリーンショット 2020-02-14 8.25.21.png

followersの数が1増え、[Follow]ボタンが[Unfollow]ボタンに変わりました。

ここで[Unfollow]ボタンをクリックしてみます。次に出た画面のスクリーンショットは以下です。

スクリーンショット 2020-02-14 8.25.16.png

followersの数が1減り、[Unfollow]ボタンが[Follow]ボタンに変わりました。

2. 先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。

フォローを実行した直後のログ

[Follow]ボタンをクリックし、POSTリクエストが発行されてから、リクエストが完了するまでのRailsサーバーのログを以下に示します。

Started POST "/relationships" ...略
Processing by RelationshipsController#create as JS
  ...略
  Rendering relationships/create.js.erb
  ...略
  Rendered users/_unfollow.html.erb (4.6ms)
  ...略
  Rendered relationships/create.js.erb (30.4ms)
Completed 200 OK in 137ms (Views: 66.4ms | ActiveRecord: 35.8ms)

relationships/create.js.erbの描画が行われ、その中でusers/_unfollow.html.erbの描画が行われる」という順序でテンプレートの描画が行われたことがわかります。

フォロー解除を実行した直後のログ

[Follow]ボタンをクリックし、POSTリクエストが発行されてから、リクエストが完了するまでのRailsサーバーのログを以下に示します。

Started DELETE "/relationships/93" ...略
Processing by RelationshipsController#destroy as JS
  ...略
  Rendering relationships/destroy.js.erb
  Rendered users/_follow.html.erb (1.5ms)
  ...略
  Rendered relationships/destroy.js.erb (30.9ms)
Completed 200 OK in 178ms (Views: 78.8ms | ActiveRecord: 50.1ms)

relationships/destroy.js.erbの描画が行われ、その中でusers/_follow.html.erbの描画が行われる」という順序でテンプレートの描画が行われたことがわかります。

フォローをテストする

ユーザーのフォローに対するテスト

ユーザーのフォローに対するテストの核心は以下のコードです。

assert_difference '@user.following.count', 1 do
  post relationships_path, params: { followed_id: @other.id }
end

テストの構造は、「/relationships に対してPOSTリクエストを発行し、それに対してRDBのレコード数が増えていることをテストする」というものになります。

Ajax版のテストは、以下の内容になります。通常版のテストとの違いは、postメソッドにおけるxhr: trueというオプションの有無だけです。

assert_difference '@user.following.count', 1 do
  post relationships_path, params: { followed_id: @other.id }, xhr: true
end

ユーザーのフォロー解除に対するテスト

relationship = @user.active_relationships.find_by(followed_id: @other.id)
assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship)
end

「HTTPリクエストを発行し、それに対するRDBのレコード数の増減をテストする」というテストの構造は、前述「ユーザーのフォローに対するテスト」と類似したものとなります。より具体的な手順は以下の通りになります。

  1. 1人のユーザーをフォローする
  2. 1.で生成されたRelationshipモデルのオブジェクトを、relationship変数に代入する
  3. relationship変数を引数としてDELETEリクエストを発行し、フォロー数が1減ったことをテストする

2.の操作は、「ログインユーザーの能動的リレーションシップから、1.でフォローしたユーザーのidを検索する」という操作により行われます。

relationship = @user.active_relationships.find_by(followed_id: @other.id)
assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship), xhr: true
end

Ajax版のテストは、以下の内容になります。通常版のテストとの違いは、postメソッドにおけるxhr: trueというオプションの有無だけです。

test/controllers/relationships_controller_test.rbに対する変更の内容

上記を踏まえると、test/controllers/relationships_controller_test.rb全体に対する変更の内容は、以下のようになります。

test/controllers/relationships_controller_test.rb
  require 'test_helper'

  class FollowingTest < ActionDispatch::IntegrationTest
    def setup
      @user  = users(:rhakurei)
      @other = users(:mkirisame)
      log_in_as(@user)
    end

    test "following page" do
      get following_user_path(@user)
      assert_not @user.following.empty?
      assert_match @user.following.count.to_s, response.body
      @user.following.each do |user|
        assert_select "a[href=?]", user_path(user), minimum: 2
      end
    end

    test "followers page" do
      get followers_user_path(@user)
      assert_not @user.followers.empty?
      assert_match @user.followers.count.to_s, response.body
      @user.followers.each do |user|
        assert_select "a[href=?]", user_path(user), minimum: 2
      end
    end
+
+   test "should follow a user the standard way" do
+     assert_difference '@user.following.count', 1 do
+       post relationships_path, params: { followed_id: @other.id }
+     end
+   end
+   test "should follow a user with Ajax" do
+     assert_difference '@user.following.count', 1 do
+       post relationships_path, xhr: true, params: { followed_id: +other_id }
+     end
+   end
+   test "should unfollow a user the standard way" do
+     @user.follow(@other)
+     relationship = @user.active_relationships.find_by(followed_id: +other.id)
+     assert_difference '@user.following.count', -1 do
+       delete relationship_path(relationship)
+     end
+   end
+   test "should unfollow a user with Ajax" do
+     @user.follo(@other)
+     relationship = @user.active_relationships.find_by(followed_id: other.id)
+     assert_difference '@user.following count', -1 do
+       delete relationship_path(relationship), xhr: true
+     end
+   end
  end

演習 - フォローをテストする

別記事で解説します。

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

Railsチュートリアル 第14章 ユーザーをフォローする - 演習「フォローをテストする」

1. リスト 14.36respond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認してみましょう。実際、どのテストケースが落ちたでしょうか?

RelationshipsController#createにおいて、format.html以下の行が欠落している場合

RelationshipsController#createに以下の変更を加えた場合の動作から見てみましょう。

app/controllers/relationships_controller.rb#create
  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
-     format.html { redirect_to @user }
      format.js
    end
  end

上記変更を適用した上で、test/integration/following_test.rbを対象としてテストを実行した結果は以下です。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 1590
Started with run options --seed 42916

ERROR["test_should_follow_a_user_the_standard_way", FollowingTest, 3.4918121000082465]
 test_should_follow_a_user_the_standard_way#FollowingTest (3.49s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:7:in `create'
            test/integration/following_test.rb:30:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:29:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.89168s
6 tests, 13 assertions, 0 failures, 1 errors, 0 skips

エラー内容がActionController::UnknownFormatであることがポイントです。上記エラーログでは端折られていますが、今回発生したActionController::UnknownFormatエラーのより詳細なエラーメッセージは以下のようになります。

ActionController::UnknownFormat: RelationshipsController#create is missing a template for this request format and variant.

        request.formats: ["text/html"]
        request.variant: []

「RelationshipsController#create is missing a template(略)」とありますね。createコントローラーに対するビューのテンプレート、ファイル名としてはapp/views/relationships/create.html.erbが存在しないためのエラーです。

RelationshipsController#createにおいて、format.js以下の行が欠落している場合

続いて、RelationshipsController#createに以下の変更を加えた場合の動作です。

app/controllers/relationships_controller.rb#create
  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
-     format.js
    end
  end

上記変更を適用した上で、test/integration/following_test.rbを対象としてテストを実行した結果は以下です。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 1604
Started with run options --seed 30420

  6/6: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.26699s
6 tests, 14 assertions, 0 failures, 0 errors, 0 skips

テストは正常に完了します。テスト「should follow a user with Ajax」が落ちることを期待していたのですが、違いますね。

RelationshipsController#destroyにおいて、format.html以下の行が欠落している場合

今度はRelationshipsController#destroyに以下の変更を加えた場合の動作です。

app/controllers/relationships_controller.rb#destroy
  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
-     format.html { redirect_to @user }
      format.js
    end
  end

上記変更を適用した上で、test/integration/following_test.rbを対象としてテストを実行した結果は以下です。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 559
Started with run options --seed 5514

ERROR["test_should_unfollow_a_user_the_standard_way", FollowingTest, 4.025347000000693]
 test_should_unfollow_a_user_the_standard_way#FollowingTest (4.03s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:16:in `destroy'
            test/integration/following_test.rb:46:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:45:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.16709s
6 tests, 13 assertions, 0 failures, 1 errors, 0 skips

ActionController::UnknownFormatエラーでテストが落ちています。「RelationshipsController#createにおいて、format.html以下の行が欠落している場合」と同様の挙動ですね。

RelationshipsController#destroyにおいて、format.js以下の行が欠落している場合

最後はRelationshipsController#destroyに以下の変更を加えた場合の動作です。

app/controllers/relationships_controller.rb#destroy
  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
-     format.js
    end
  end

上記変更を適用した上で、test/integration/following_test.rbを対象としてテストを実行した結果は以下です。

# rails test test/integration/following_test.rb
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /usr/local/bundle/specifications/i18n-0.9.5.gemspec:17.
NOTE: Gem::Specification#rubyforge_project= is deprecated with no replacement. It will be removed on or after 2019-12-01.
Gem::Specification#rubyforge_project= called from /usr/local/bundle/specifications/i18n-0.9.5.gemspec:17.
Running via Spring preloader in process 573
Started with run options --seed 63825

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.95860s
6 tests, 14 assertions, 0 failures, 0 errors, 0 skips

テストは正常に完了します。

発展 -

2. リスト 14.40xhr: trueがある行のうち、片方のみを削除するとどういった結果になるでしょうか? このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのか考えてみてください。

test/integration/following_test.rbの内容を以下のように改変した場合、テストの結果はどうなるでしょうか。「テスト『should follow a user with Ajax』において、xhr: trueがある行を削除する」という改変です。

test/integration/following_test.rb
  require 'test_helper'

  class FollowingTest < ActionDispatch::IntegrationTest
    def setup
      @user  = users(:rhakurei)
      @other = users(:mkirisame)
      log_in_as(@user)
    end

    ...略

    test "should follow a user the standard way" do
      assert_difference '@user.following.count', 1 do
        post relationships_path, params: { followed_id: @other.id }
      end
    end

    test "should follow a user with Ajax" do
      assert_difference '@user.following.count', 1 do
-       post relationships_path, xhr: true, params: { followed_id: @other.id }
      end
    end

    test "should unfollow a user the standard way" do
      @user.follow(@other)
      relationship = @user.active_relationships.find_by(followed_id: @other.id)
      assert_difference '@user.following.count', -1 do
        delete relationship_path(relationship)
      end
    end

    test "should unfollow a user with Ajax" do
      @user.follow(@other)
      relationship = @user.active_relationships.find_by(followed_id: @other.id)
      assert_difference '@user.following.count', -1 do
        delete relationship_path(relationship), xhr: true
      end
    end
  end

上記テストの実行結果は以下のようになります。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 586
Started with run options --seed 63280

 FAIL["test_should_follow_a_user_with_Ajax", FollowingTest, 1.9365238999998837]
 test_should_follow_a_user_with_Ajax#FollowingTest (1.94s)
        "@user.following.count" didn't change by 1.
        Expected: 3
          Actual: 2
        test/integration/following_test.rb:36:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.76945s
6 tests, 14 assertions, 1 failures, 0 errors, 0 skips

上記テストでは、テスト「should follow a user with Ajax」において、POSTリクエスト自体が発行されていません。POSTリクエストが発行されないことには、RelationshipsController#createメソッドも実行されません。当該RelationshipsController#createの内容は以下の通りです。

RelationshipsController#create
def create
  @user = User.find(params[:followed_id])
  current_user.follow(@user)
  respond_to do |format|
    format.html { redirect_to @user }
    format.js
  end
end

RDB上のデータにフォローを反映する処理は、上記コードのうちcurrent_user.follow(@user)となります。current_user.follow(@user)が実行されなければ、@user.following.countの値も変化することはありません。結果、「@user.following.countの値が想定と違う」という理由でテストが落ちることになります。

逆に、「テスト『should unfollow a user with Ajax』において、xhr: trueがある行を削除する」という改変を加えた場合(下記ソース)も、同様の形でテストが落ちます。

test/integration/following_test.rb
  require 'test_helper'

  class FollowingTest < ActionDispatch::IntegrationTest
    def setup
      @user  = users(:rhakurei)
      @other = users(:mkirisame)
      log_in_as(@user)
    end

    ...略

    test "should follow a user the standard way" do
      assert_difference '@user.following.count', 1 do
        post relationships_path, params: { followed_id: @other.id }
      end
    end

    test "should follow a user with Ajax" do
      assert_difference '@user.following.count', 1 do
        post relationships_path, xhr: true, params: { followed_id: @other.id }
      end
    end

    test "should unfollow a user the standard way" do
      @user.follow(@other)
      relationship = @user.active_relationships.find_by(followed_id: @other.id)
      assert_difference '@user.following.count', -1 do
        delete relationship_path(relationship)
      end
    end

    test "should unfollow a user with Ajax" do
      @user.follow(@other)
      relationship = @user.active_relationships.find_by(followed_id: @other.id)
      assert_difference '@user.following.count', -1 do
-       delete relationship_path(relationship), xhr: true
      end
    end
  end

上記テストの実行結果は以下のようになります。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 599
Started with run options --seed 27288

 FAIL["test_should_unfollow_a_user_the_standard_way", FollowingTest, 4.007498499999201]
 test_should_unfollow_a_user_the_standard_way#FollowingTest (4.01s)
        "@user.following.count" didn't change by -1.
        Expected: 2
          Actual: 3
        test/integration/following_test.rb:45:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.10546s
6 tests, 14 assertions, 1 failures, 0 errors, 0 skips

今度は、テスト「should unfollow a user with Ajax」中でDELETEリクエストが発行されなくなりました。RelationshipsController#destroy中にあるcurrent_user.unfollow(@user)という処理が実行されず、@user.following.countの数が変化しなかった結果、テストが落ちたのですね。

RelationshipsController#destroy
def destroy
  @user = Relationship.find(params[:id]).followed
  current_user.unfollow(@user)
  respond_to do |format|
    format.html { redirect_to @user }
    format.js
  end
end

発展 - FollowingTestで、より適切なテストを行えるようにしてみる

Railsチュートリアル本文リスト 14.40のテストでは、例えばcreate.js.erbdestroy.js.erbの記述内容にまで踏み込んだテストができていませんでした。別記事「Railsチュートリアル 第14章 ユーザーをフォローする - 演習「フォローをテストする」 - FollowingTestの問題点と、その改良」にて、当該テストの問題点や改良策について記述しています。

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

Railsチュートリアル 第14章 ユーザーをフォローする - 演習「フォローをテストする」 - FollowingTestの問題点と、その改良

前提となるテストの内容

Railsチュートリアル本文、リスト 14.40記載のテストコードです。

test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest
  def setup
    @user  = users(:rhakurei)
    @other = users(:mkirisame)
    log_in_as(@user)
  end

  # ...略

  test "should follow a user the standard way" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, params: { followed_id: @other.id }
    end
  end

  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, xhr: true, params: { followed_id: @other.id }
    end
  end

  test "should unfollow a user the standard way" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship)
    end
  end

  test "should unfollow a user with Ajax" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship), xhr: true
    end
  end
end

RelationshipsController#createにおいて、format.js以下の行が欠落していても上記テストが落ちない理由

前提として、RelationshipsController#createの実装は以下のようになっています。

app/controllers/relationships_controller.rb#create
def create
  @user = User.find(params[:followed_id])
  current_user.follow(@user)
  respond_to do |format|
    format.html { redirect_to @user }
    format.js
  end
end

createメソッド内2行目の時点で、followメソッドの処理に問題がなければ、@user.following.countの数は1増えます。

ここで、format.js以下の行が欠落している場合、RelationshipsController#createは、「Ajaxによる呼び出しであっても、非Ajax(通常のリダイレクトを伴う呼び出し)として以降の処理を行います。となると、「format.js以下の行が欠落していても、format.html以下の行さえあれば、とりあえず当該テストは通ってしまう」という動作になるのです。

現状の実装で、テスト「should follow a user with Ajax」はどんな場合に落ちるのか

Railsチュートリアル本文のリスト 14.40において、テスト「should follow a user with Ajax」のソースコードは、以下のようになっています。

test "should follow a user with Ajax" do
  assert_difference '@user.following.count', 1 do
    post relationships_path, xhr: true, params: { followed_id: @other.id }
  end
end

RelationshipsController#createにおいて、format.js以下の行が欠落していてもこのテストが落ちない」というのであれば、どのような場合にこのテストは落ちるのでしょうか。

一つの答えは、「app/views/relationshipsディレクトリに、create.js.erbがなくてcreate.css.erbだけが存在する場合」です。この場合、当該テストは以下のようなエラーで落ちることになります。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 507
Started with run options --seed 49836

ERROR["test_should_follow_a_user_with_Ajax", FollowingTest, 4.884168800000225]
 test_should_follow_a_user_with_Ajax#FollowingTest (4.88s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: RelationshipsController#create is missing a template for this request format and variant.

        request.formats: ["text/javascript", "text/html", "application/xml", "*/*"]
        request.variant: []
            test/integration/following_test.rb:37:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:36:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.96005s
6 tests, 13 assertions, 0 failures, 1 errors, 0 skips

ActionController::UnknownFormatエラーでテストが落ちていますね。

test/integration/following_test.rbの、より望ましいであろう実装

ビューによりレンダリングされる内容に対してテストを行うのであれば、以下のような実装がより望ましいかと思います。

test/integration/following_test.rb
  require 'test_helper'

  class FollowingTest < ActionDispatch::IntegrationTest
    def setup
      @user  = users(:rhakurei)
      @other = users(:mkirisame)
      log_in_as(@user)
    end

...略

    test "should follow a user the standard way" do
      assert_difference '@user.following.count', 1 do
        post relationships_path, params: { followed_id: @other.id }
      end
+     assert_redirected_to @other
    end

    test "should follow a user with Ajax" do
      assert_difference '@user.following.count', 1 do
        post relationships_path, xhr: true, params: { followed_id: @other.id }
      end
+     assert_match 'Unfollow', @response.body
    end

    test "should unfollow a user the standard way" do
      @user.follow(@other)
      relationship = @user.active_relationships.find_by(followed_id: @other.id)
      assert_difference '@user.following.count', -1 do
        delete relationship_path(relationship)
      end
+     assert_redirected_to @other
    end

    test "should unfollow a user with Ajax" do
      @user.follow(@other)
      relationship = @user.active_relationships.find_by(followed_id: @other.id)
      assert_difference '@user.following.count', -1 do
        delete relationship_path(relationship), xhr: true
      end
+     assert_match 'Follow', @response.body
    end
  end

上記テストの追加内容は以下の通りです。

  • 非Ajaxの場合、@otherの内容たるユーザーのプロフィールページにリダイレクトされることをテストする
  • Ajaxの場合、以下の処理が行われることをテストする
    • Followボタンが押された場合、「Unfollow」というキャプションを持つボタンを生成するJavaScriptコードが返されること
    • Unfollowボタンが押された場合、「Follow」というキャプションを持つボタンを生成するJavaScriptコードが返されること

「より望ましいであろう実装」のテスト結果

「should follow a user the standard way」のテストが落ちるパターン

以下のように、「RelationshipsController#createにおいて、format.html以下の行が欠落している場合」、「should follow a user the standard way」のテストが落ちることが期待されます。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
-     format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

実際にテスト「should follow a user the standard way」を実行すると、結果は以下のようになります。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 664
Started with run options --seed 25360

ERROR["test_should_follow_a_user_the_standard_way", FollowingTest, 4.109552399999302]
 test_should_follow_a_user_the_standard_way#FollowingTest (4.11s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:7:in `create'
            test/integration/following_test.rb:30:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:29:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.22645s
6 tests, 19 assertions, 0 failures, 1 errors, 0 skips

「should follow a user with Ajax」のテストが落ちるパターン

以下のように、「RelationshipsController#createにおいて、format.js以下の行が欠落している」という場合、「should follow a user with Ajax」のテストが落ちることが期待されます。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
-     format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

実際にテスト「should follow a user with Ajax」を実行すると、結果は以下のようになります。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 677
Started with run options --seed 49139

 FAIL["test_should_follow_a_user_with_Ajax", FollowingTest, 1.8547959999996237]
 test_should_follow_a_user_with_Ajax#FollowingTest (1.85s)
        Expected /Unfollow/ to match "Turbolinks.clearCache()\nTurbolinks.visit(\"http://www.example.com/users/391532587\", {\"action\":\"replace\"})".
        test/integration/following_test.rb:39:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.41998s
6 tests, 22 assertions, 1 failures, 0 errors, 0 skips

「should unfollow a user the standard way」のテストが落ちるパターン

以下のように、「RelationshipsController#destroyにおいて、format.html以下の行が欠落している場合」、「should follow a user the standard way」のテストが落ちることが期待されます。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
-     format.html { redirect_to @user }
      format.js
    end
  end
end

実際にテスト「should unfollow a user the standard way」を実行すると、結果は以下のようになります。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 690
Started with run options --seed 23512

ERROR["test_should_unfollow_a_user_the_standard_way", FollowingTest, 3.8890324999993027]
 test_should_unfollow_a_user_the_standard_way#FollowingTest (3.89s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:16:in `destroy'
            test/integration/following_test.rb:46:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:45:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.18608s
6 tests, 19 assertions, 0 failures, 1 errors, 0 skips

「should unfollow a user with Ajax」のテストが落ちるパターン

以下のように、「RelationshipsController#destroyにおいて、format.js以下の行が欠落している場合」、「should unfollow a user with Ajax」のテストが落ちることが期待されます。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
-     format.js
    end
  end
end

実際にテスト「should unfollow a user with Ajax」を実行すると、結果は以下のようになります。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 703
Started with run options --seed 27626

 FAIL["test_should_unfollow_a_user_with_Ajax", FollowingTest, 1.7373090000000957]
 test_should_unfollow_a_user_with_Ajax#FollowingTest (1.74s)
        Expected /Follow/ to match "Turbolinks.clearCache()\nTurbolinks.visit(\"http://www.example.com/users/391532587\", {\"action\":\"replace\"})".
        test/integration/following_test.rb:57:in `block in <class:FollowingTest>'

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.29870s
6 tests, 22 assertions, 1 failures, 0 errors, 0 skips

ソースコードの実装に問題がない場合

最後に、app/controllers/relationships_controller.rbの内容が以下の通り問題なく記述されている場合、テストの結果はどうなるでしょうか。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

実際にテストを実行してみます。

# rails test test/integration/following_test.rb
Running via Spring preloader in process 716
Started with run options --seed 34373

  6/6: [===================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.64541s
6 tests, 22 assertions, 0 failures, 0 errors, 0 skips

無事テストが通りました。というわけで、テストの内容に問題はないようです。

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

[超短文]herokuでデータベース操作する方法

herokuでデプロイした後に、データベースをSQLコマンドで操作する画面に行く方法です。

$ heroku pg:psql

このコマンドを実行すると操作画面に遷移します。

あとは

$ SELECT * FROM [テーブル名]

でデータ取得するもよし、

$ DELETE FROM [テーブル名]

でデータ削除するもよしです。

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

【Rails】自動で次のページへ!!jscrollによるページネーションの無限スクロール

Image from Gyazo

自動で次のページに移行

ページネーションを実装して思うことは、最近は次へボタンなんて存在せず、スクロールするとどんどん新しい商品やツイートが表示されたりするということだ。

今回はそれを実装する。
使うのは、gem'kaminari'(ページネーション機能) + プラグイン”jScroll”(無限スクロール)である。

まずはgem'kaminari'でページネーションを実装

参考になるページはここです。
【Rails初心者】ページネーションを実装して自分好みにデザインを変える

Gemfile
gem 'kaminari'

gemを追記したら

ターミナル
$ bundle install

インストールが完了したら、コントローラーに記述する。

products_controller.rb
class UsersController < ApplicationController
  def index
    @products = Product.order(created_at: "DESC").includes(:host).page(params[:page]).per(1)
     #.page(params[:page]).per(1)は1ページ、1つのproductだけ表示の意味
  end
end

次はページネーションの次へ、戻るなどのボタンを表示させる。

index.haml
= render 'shared/main-header'
.container-fluid
  .row.py-5.w-100
    = render 'shared/side'
    .col-8.main
      .jscroll
        = render partial: 'user', collection: @products, as: "product", class: "jscroll"
        .skill-list
          = paginate @products # ここを追加するだけ

これでページネーションの実装完了です。

kaminariは遅いので、加工します。

kaminariは便利ですが、読み込みが10秒ぐらいかかって非常に重い。
だから、記述を変えます。

参考になるサイト:巨大レコードのkaminariページネーションは工夫が必要

products_controller.rb
class UsersController < ApplicationController
  def index
    @products = Product.order(created_at: "DESC").includes(:host).page(params[:page]).without_count.per(1)
    # .without_countを追加しています
  end
end

.without_countを追加することで、本来のkaminariの機能は使えなくなります。
次へボタンと戻るボタンは表示可能です。

index.haml
= render 'shared/main-header'
.container-fluid
  .row.py-5.w-100
    = render 'shared/side'
    .col-8.main
      .jscroll
        = render partial: 'user', collection: @products, as: "product", class: "jscroll"
        .skill-list
          // = paginate @products これを削除して、下記を追加
          = link_to_prev_page @products, '前のページ', class: "prev"
          = link_to_next_page @products, '次のページ', class: "next"

これで戻るボタンと次へボタンを表示させました。
1秒ほどで、ページを開けるようになりましたね!!

では、本題の下にスクロールすると、勝手に次のページに移行するようにしましょう!!

無限スクロール

jScrollのインストール

CDNで追加する場合

スクリプトを読み込ませて簡単に導入しましょう!

application.html.haml
%head
  %script{src: "https://cdnjs.cloudflare.com/ajax/libs/jscroll/2.4.1/jquery.jscroll.min.js"}

直接JSファイルを導入する場合

jscrollのGithubからダウンロードしよう。

ZIPファイルを解答したら、
jquery.jscroll.min.jsをasset/javascripts配下におきます。

そしたら、jScrollを読み込ませるようにします

assets/javascripts/application.js
//= require jquery.jscroll.min.js 
実はここが失敗が起こりやすい箇所です!!!!

解説します
私は下記のようにしてます

application.js
//= require jquery
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require_tree .
//= require jquery.jscroll.min.js
//= require popper
//= require bootstrap-sprockets

重要なのはここです。

application.js
//= require jquery
//= require jquery.jscroll.min.js

これだけだとエラーなく利用できます。

エラー
Uncaught TypeError: $(...).jscroll is not a function
// 「.jscrollってイベント処理ねええぞゴラアアアアアアア!!」と怒られます。これに苦戦しましたね。

間違えて下記にしてしまうと使えません

失敗例
//= require jquery
//= require jquery.jscroll.min.js
//= require jquery3

//= require jquery と //= require jquery3が入っていて、
エラーが起こるパターンです。
もしも複数入っている場合、片方消しましょう。

jQueryで無限スクロールの処理作成

では続きを進めていきます。
それではjQueryで無限スクロールの処理を作成します。

jscroll用のjs
$(document).on('turbolinks:load', function() {
  $('.jscroll').jscroll({
    // 無限に追加する要素は、どこに入れる?
    contentSelector: '.jscroll', 
    // 次のページにいくためのリンクの場所は? >aタグの指定
    nextSelector: 'a.next',
    // 読み込み中の表示はどうする?
    loadingHtml: '読み込み中'
  });
});

これでdiv.jscrollをスクロールすると.jscrollに次のページの要素を追加するよ。ということになります

詳しいオプションは下記のサイトが参考になります。
無限スクロールのプラグイン「jScroll」の使い方

これで完了です!!
エラーが難しかったですが、原因がわかってよかったです。

参考リンク

kaminari系

【Rails初心者】ページネーションを実装して自分好みにデザインを変える
gem'kaminari'のオプション
参考になるサイト:巨大レコードのkaminariページネーションは工夫が必要

無限スクロール系

kaminari + jscrollを使った無限スクロールの実装
無限スクロールのプラグイン「jScroll」の使い方
Github:jscroll

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

Ruby on Railsについて 自分の中でのまとめ

TECH::EXPERTで学んだことに対して、自分用の備忘録、復習として色々と書いていこうと思います。

Ruby on Railsの概要

Ruby on RailsはRubyでWebアプリケーションを作成する際によく使用されるフレームワークです。
Webアプリケーションには共通している機能(ユーザー登録とかログインとか)が多くあるため、それらの機能をまとめてフレームワークという形で提供されています。つまりWebアプリケーションのフォーマットのような物です。

最初に理解すべきこと

WEBアプリケーションにおける処理の流れを表したものとして、MVCモデルという物があります。これを理解することで、TECH::EXPERTでのRailsのカリキュラムはかなり分かりやすくなるのでは...と思いました。自分自身しっかりと理解しているとは言えないため、ブログという形で復習していきたいと思います。

MVCモデル

MVCモデルで出てくるキーワードは以下となります。
*ルーティング
*コントローラ
*ビュー
*モデル

ルーティング

クライアントから送られてきたリクエストからどのような処理をするべきか、という判断をしてコントローラ(後述)役目になります。通販サイトで商品をクリックし場合、商品の詳細ページを表示させろ!という判断をするわけです。

コントローラ

ルーティングで振り分けられたリクエストを実際に処理する部分です。リクエストによってトップページを出力させたり、データを削除したりと処理内容は変わってきます。

ビュー

クライアントへ返す見た目を設定するものです。hoge.html.erbというファイルでhtmlにRubyの処理を埋め込む事ができます。ERBファイルを元に作成されたhtmlファイルがレスポンスとしてクライアントに返されます。

モデル

データベースに保存してあるデータを参照する場合や更新する場合はモデルを介して処理が行われます。勉強不足でこれ以上の説明ができません...そのうち詳細に書きます。

まとめ

こんな感じでなるべく毎日ブログを書こうと思います。よろしくお願いします。

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

herokuのデータを上書き DBカラムに変更があった場合

herokuにデプロイしたアプリのDBカラムを追加、変更してデプロイし直した時に不具合(詰まった)があったので記載します。

2回目以降のデプロイ

$ heroku login
$ git add . 
$ git commit -m ""
$ git push heroku master
$ heroku open

ここの部分の説明は割愛します。

1回目のデプロイはこちらを参考に進めます。
RailsDBをMySQLに変更してHerokuでデプロイまでする手順
Rails6 ローカルで頑張って作ったwebアプリをHerokuでデプロイした話

詰まった点

変更したコードの部分は2回目以降も反映されたが、DBの変更(今回はカラムの追加)が反映されなかった。

logで確認すると、「そんなメソッド知らないよ」とか言っていました。

ローカルでは問題なく動いているのを確認した後で調べてみると、DBの変更はheroku masterでは反映されないのでリセットする必要があるとのこと。

解決方法

$ rails db:migrate:reset

heroku masterの後に入力してリセットとマイグレーションを行いました。無事にheroku上でも動いています。

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

Rails6 テキストを一発でコピーできるボタンを実装する

目的

  • テキストを一発でコピーできるボタンの実装方法をまとめる

作業をする前に

  • 今回の作業方法は公式のGithubのREADMEに沿って説明する。
  • clipboard-rails

実施方法

  1. Gemzeroclipboard-railsをインストール

    1. Gemfileに下記を追記する。

      gem 'jquery-rails'
      gem 'clipboard-rails'
      
    2. 下記コマンドを実行してGemfileを元にGemをインストールする。

      $ bundle update
      
  2. 設定ファイル記載

    1. 下記に存在するファイルをエディタで開く。
      • アプリ名/app/assets/javascripts
        • /application.js
    2. 下記の内容をapplication.jsに追記する。

      //= require jquery
      //= require jquery_ujs
      //= require clipboard
      
      $(document).ready(function(){  
      
      var clipboard = new Clipboard('.clipboard-btn');
      console.log(clipboard);
      
      });
      
    3. 下記に存在するファイルをエディタで開く。

      • アプリ名/app/views/layouts
        • application.html.erb
    4. <head>の一番最初に下記を追記する。

      <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
      
  3. ボタンを実装

    1. 任意のビューファイルに下記の記載を行う。

      <div>
        <!-- Target -->
        <textarea id="bar" name="" rows="7" cols="100">
        <p>コピーターゲット</p>
        </textarea>
        <!-- Trigger -->
        <button class="clipboard-btn" data-clipboard-action="copy" data-clipboard-target="#bar">
            Copy to clipboard
        </button>
      </div>
      
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む