20200103のRailsに関する記事は20件です。

Rack入門 Rack Middleware編 (3/3)

前回 は、Rackのプロトコルを理解するために簡単なアプリを作りました。
今回はRackの重要な概念であるRack Middlewareについて学びます。

目次

  1. Rack入門 概念編(1/3)
  2. Rack入門 Rack Application編 (2/3)
  3. [本記事] Rack入門 Rack Middleware編 (3/3)

Rack Middlewareとは

はじめにややこしいことを言いますが、Rackはミドルウェア(Middleware)です。
アプリサーバーとフレームワーク間のやりとりを仲介しているため、ミドルウェアと呼ばれます。

今回学ぶのはミドルウェアとは何か、ではなくてRack Middlewareについてです。
Rackには以下2つの概念があります。

  • Rack Application

    • 前回学んだ、callメソッドを持つオブジェクトのことです
    • StatusCode・Headers・Bodyの3つをレスポンスとして返します
    • Rack Endpointとも呼ばれます
  • Rack Middleware

    • 今回学ぶものです
    • Rack Middlewareはcallメソッドを持つclassである必要があります
    • Rack Applicationと違い、Responseを直接返すのではなく別の処理を呼び出しデータを加工するために使います

Rack Middlewareは、渡ってきたenv情報を加工し、次のmiddlewareまたはendpointに処理を引き渡すものです。

rack03_01.png

Hello Rack Middleware

Rack Middlewareはenv情報を加工し、次に引き渡すために利用します。
Bodyに「Hello Rack Middleware」と追加するだけのRack Middlewareを作成してみます。

class App
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]]
  end
end

# Rack Middlewareは以下条件を満たす必要がある
#   - classであること
#   - initializeでappを受け取ること
#   - callメソッドを実装し、Status/Headers/Bodyを返すこと (Rack Endpointと同じ条件)
class HelloRackMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)

    fixed_body = ["Hello Rack Middleware!\n"] + body

    [status, headers, fixed_body]
  end
end

use HelloRackMiddleware
run App.new

Rack Middlewareをclassとして作り、useメソッドを呼び出すことでmiddlewareを追加できます。
アプリを起動し、レスポンスを見てみます。

$ curl http://localhost:9292/

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
Hello Rack Middleware!
HELLO Rack Endpoint!

* Connection #0 to host localhost left intact
* Closing connection 0

Hello Rack Middleware! という文字列がBodyに追加されていますね。

Middlewareはいくつでも追加できます。ただし、useの順序によってMiddlewareの動作順が異なることには注意が必要です。

class App
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]]
  end
end

class HelloRackMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)

    fixed_body = ["Hello Rack Middleware!\n"] + body

    [status, headers, fixed_body]
  end
end

class AnotherMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)

    fixed_body = ["Another Middleware!\n"] + body

    [status, headers, fixed_body]
  end
end

use HelloRackMiddleware
use AnotherMiddleware
run App.new

レスポンスは以下のとおりです。

Hello Rack Middleware!
Another Middleware!
HELLO Rack Endpoint!

Content-Lengthを追加するMiddleware

もう少し実用的なmiddlewareを作ってみましょう。
Response HeaderにContent-Lengthを挿入するmiddlewareを作ります。

class App
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["HELLO WORLD!", "Hello"]]
  end
end

class ContentLengthMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    body_size = 0

    body.each do |b|
      body_size += b.bytesize
    end
    headers["Content-Length"] = body_size.to_s

    [status, headers, body]
  end
end

use ContentLengthMiddleware

run App.new

アクセスしてみましょう。

$ curl -v http://localhost:9292/ 

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 17
<
* Connection #0 to host localhost left intact
HELLO WORLD!Hello* Closing connection 0

ちゃんとContent-Lengthが設定されていますね。

Rackにあらかじめ用意されているRack Middleware

Content-Lengthを設定するmiddlewareなど、よく使うと思われるMiddlewareはrack本家で実装されています。
いくつか主要なものを紹介します。

その他のmiddlewareは https://github.com/rack/rack/tree/master/lib/rack を参照してください。

Railsで使われているRack Middleware

Railsもrackのプロトコルに従い作成されています。
Railsで実際に使われているmiddlewareを覗いてみます。

$ rails new racktest

...省略...


$ cd racktest

$ bin/rake middleware

Running via Spring preloader in process 10795
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Racktest::Application.routes

いくつかrackで実装されているMiddlewareも利用しているのがわかります。
Railsで利用しているMiddlewareについては、https://guides.rubyonrails.org/rails_on_rack.html#internal-middleware-stack にて詳しく説明されています。

まとめ

  • Rack Middlewareは、渡ってきたリクエスト/レスポンスを加工するために利用する
  • Middlewareはcallメソッドを実装したclassである必要がある

Rackをより理解するためには

全3回にわたって、Rackの基本的な概念を簡単なアプリを作りながら学びました。
思ったよりシンプルな構造でしたね。

よりRackを理解するための参考資料を、以下に記載しておきます。

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

Rack入門 Rack Application編 (2/3)

前回 はRackが必要とされた背景と、基本的な概念について説明しました。
今回は実際にRackプロトコルを使いアプリサーバーと通信するプログラムを作りながら、Rackに関する理解を深めていきます。

目次

  1. Rack入門 概念編(1/3)
  2. [本記事] Rack入門 Rack Application編 (2/3)
  3. Rack入門 Rack Middleware編 (3/3)

Hello Rack Application

基本を理解するために、簡単なRackアプリケーションを作ってみます。ブラウザーからアクセスされたら、「Hello Rack!」と返すだけのシンプルなアプリです。

まずは以下Gemfileを用意し、bundle installをしてください。アプリサーバーはpumaを使います。

source "https://rubygems.org"

gem "rack"
gem "puma"

次に config.ru というファイルを作成します。拡張子がruですが、中身はrubyのコードです。

class HelloRackApp
  # callメソッドはenvを受け取り、3つの値(StatusCode, Headers, Body)を配列として返す
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["Hello Rack!"]]
  end
end

# callメソッドを呼び出せるObjectをrunに渡し、rackアプリを起動する
run HelloRackApp.new

ターミナル上で以下のコマンドを実行し、Rackアプリを起動します。

bundle exec rackup -s puma

rackupコマンドはデフォルトではconfig.ruを読み込み、Rackアプリを起動します。
bundle exec rackup config.ru としてファイルを指定することもできます。
-s pumaはアプリサーバーの指定です。指定しない場合はpumaかthinかwebrickのいずれかが使われます。

$ bundle exec rackup -s puma

Puma starting in single mode...
* Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://127.0.0.1:9292
* Listening on tcp://[::1]:9292
Use Ctrl-C to stop

port9292でRackアプリが起動しました。
pumaはWebサーバーとしての機能も持っているため、ブラウザーやコマンドからもアクセスできます。
curlでアクセスしてみます。

$ curl -v http://localhost:9292

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
Hello Rack!* Closing connection 0

Hello Rack!とレスポンスが返ってきました。Statusは200 OK、Headerには指定したContent-Type: text/plain も含まれています。ブラウザーからもアクセスしてみます。

rack02_01.png

Rackアプリケーションの形式

Rackアプリはアプリサーバーと通信するために、以下の形式(プロトコル)にしたがって作成する必要があります。

  1. RackアプリはObjectであり、callメソッドを呼び出せること。callメソッドは引数を一つ受け取ること
  2. callメソッド呼び出し後、以下3つの値を配列として返すこと
    • (HTTP) Status Code
    • Headers
    • Body
  3. Status Codeは3桁のHTTPのステータスコード(100以上の数値)であること
  4. Headersはeachメソッドを実装し、yieldの際にkey/valueのペアを渡すこと。key/valueは必ずStringであること
  5. Bodyはeachメソッドを実装し、yieldの際にStringを渡すこと

実際はさらに細かく仕様が決まっていますが、要はこれだけです。
Rackの詳細な仕様は https://github.com/rack/rack/blob/master/SPEC に記載されています。

yieldの際にStringを渡すこと、というのがわかりにくいですね。
rubyのyieldについて復習しておきます。

[1,2,3,4,5].each { |v| puts v }

上記コードの実行結果は以下となります。

1
2
3
4
5

このvがyieldしたときに渡ってくる値です。eachは以下のように実装できます。

class MyArray
  def initialize(values)
    @values = values
  end

  def each
    for v in @values
      yield v
    end
  end
end

MyArray.new([1,2,3,4,5]).each { |v| puts v }

RackのHeadersはkey/valueの2つの値を渡しますが、この値がStringであれば良いです。
Bodyは、yieldの引数として渡す値がStringであれば良いですね。

Rackアプリが満たすべき要求はこれだけです。
もう一度最初のRackアプリのコードを見てみます。

class HelloRackApp
  def call(env)
    [ 
      200,                                 # 1番目はStatusで、3桁の数値を返す

      { "Content-Type" => "text/plain" },  # 2番目はHeaders, Hashはeachメソッドを実装済
                                           # key/valueはStringあること

      ["Hello Rack!"]                      # 3番目はBody, Arrayはeachメソッドを実装済み
                                           # 中身はStringであること
    ]
  end
end
run HelloRackApp.new

上記の形式に従っていれば、立派なRackアプリケーションの完成です。

HelloRackAppはclassである必要はありません。callメソッドを持ったObjectであればよくて、Headers/Bodyもeachを持ってさえいればよいので、以下のコードでも動きます。

class MyHeader
  def initialize(bodies)
    @bodies = bodies
  end

  def each
    yield "Content-Type", "text/plain"
    yield "Content-Length", @bodies.join.bytesize.to_s # NOTE: ValueはStringにしないといけない
  end
end

class MyBody
  def initialize(bodies)
    @bodies = bodies
  end

  def each
    for v in @bodies
      yield v
    end
  end
end

# NOTE: procはcallで呼び出し可能
app = proc do |env|
  bodies = ["こんにちはRack!\n\n", "Hello Rack!\n"]

  [200, MyHeader.new(bodies), MyBody.new(bodies)]
end

run app
$ curl -v http://localhost:9292

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 34
<
こんにちはRack!

Hello Rack!
* Connection #0 to host localhost left intact
* Closing connection 0

envを使ってリクエストを取り出す

callメソッドの引数envには、ブラウザーなどからのリクエストが格納されています。
以下のコードで中身を覗いてみます。

class HelloRackApp
  def call(env)
    require "pp"
    pp env

    [200, {}, []]
  end
end

run HelloRackApp.new

rackup後、サーバーにアクセスして結果を見てみます。

{"rack.version"=>[1, 3],
 "rack.errors"=>
  #<Rack::Lint::ErrorWrapper:0x00007f90a41c0c00 @error=#<IO:<STDERR>>>,
 "rack.multithread"=>true,
 "rack.multiprocess"=>false,
 "rack.run_once"=>false,
 "SCRIPT_NAME"=>"",
 "QUERY_STRING"=>"",
 "SERVER_PROTOCOL"=>"HTTP/1.1",
 "SERVER_SOFTWARE"=>"puma 4.3.1 Mysterious Traveller",
 "GATEWAY_INTERFACE"=>"CGI/1.2",
 "REQUEST_METHOD"=>"GET",
 "REQUEST_PATH"=>"/",
 "REQUEST_URI"=>"/",
 "HTTP_VERSION"=>"HTTP/1.1",
 "HTTP_HOST"=>"localhost:9292",
 "HTTP_USER_AGENT"=>"curl/7.64.1",
 "HTTP_ACCEPT"=>"*/*",
 "puma.request_body_wait"=>0,
 "SERVER_NAME"=>"localhost",
 "SERVER_PORT"=>"9292",
 "PATH_INFO"=>"/",
 "REMOTE_ADDR"=>"::1",
 "puma.socket"=>#<TCPSocket:fd 18, AF_INET6, ::1, 9292>,
 "rack.hijack?"=>true,
 "rack.hijack"=>
  #<Proc:0x00007f90a41c0fc0 /Users/nishio/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/rack-2.0.8/lib/rack/lint.rb:525>,
 "rack.input"=>
  #<Rack::Lint::InputWrapper:0x00007f90a41c0c28
   @input=#<Puma::NullIO:0x00007f90a585a8f8>>,
 "rack.url_scheme"=>"http",
 "rack.after_reply"=>[],
 "puma.config"=> #<Puma::Configuration:0x00007f90a41d7798>
 "rack.tempfiles"=>[]
 }

envには以下の全てが混ざったデータがHash形式で格納されています。

  1. HTTPリクエストヘッダー

    • HTTP_VERSIONやHTTP_USER_AGENTなど、HTTPリクエストヘッダーの値がkey/value形式で入っている
    • Keyは他の情報と区別するために、HTTP_ というprefixを付与している
  2. HTTPリクエスト情報

    • QUERY_STRING や REQUEST_URIなど、リクエストヘッダー以外のリクエスト情報が入っている
  3. pumaやrackなどのアプリからの情報

    • rack.version や puma.config など、各種アプリが付与した情報が入っている

envの値を利用することで、リクエストに応じたレスポンスを返せます。
試しに、以下アプリを作ってみます。

  1. http://localhost:9292/hello にアクセスしたら、Hello!を返す
  2. 上記以外のURIにアクセスしたら、404 Not Found を返す
class App
  def call(env)
    # NOTE:
    #   REQUEST_PATHも利用できるが、実装されていないケースが存在する
    #   PATH_INFOは必ず存在するので、urlはPATH_INFOを使って取り出す方が安全である
    #   https://github.com/envato/jwt_signed_request/issues/15
    path = env["PATH_INFO"]

    if path == "/hello"
      [200, { "Content-Type" => "text/plain" }, ["HELLO!"]]
    else
      [404, { "Content-Type" => "text/plain" }, ["404 Not Found"]]
    end
  end
end

run App.new

上記アプリにアクセスしてみましょう。

$ curl -v http://localhost:9292/hello

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET /hello HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
HELLO!* Closing connection 0
$ curl -v http://localhost:9292/testtest

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET /testtest HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
404 Not Found* Closing connection 0

ファイルを返すアプリを作成する

画像ファイルをレスポンスとして返すアプリを作ってみます。
RackのBodyは文字列でさえあればなんでもよいので、以下のように実装すれば画像を返せます。

class App
  def call(env)
    path = "./neko.jpg"  # 画像ファイルは好きなものを用意

    file = File.open(path, "rb")
    image_data = [file.read]  # Bodyは必ずeachメソッドを持つ必要があるのでArrayとする

    [200, { "Content-Type" => "image/jpeg" }, image_data]
  ensure
    file.close
  end
end

run App.new

上記コードにブラウザーからアクセスしてみます。

rack02_02.png

確かにこれでも動きはしますが、ファイル全体を一度メモリ上に読み出してBodyに詰める必要があります。
読み込み効率が悪いので、レスポンスがしんどいですね。

Rackはファイルをレスポンスすることも考え、以下のようなコードを記述できます。

class App
  def call(env)
    path = "./neko.jpg"  # 画像ファイルは好きなものを用意

    file = File.open(path, "rb")

    [200, { "Content-Type" => "image/jpeg" }, file]
  end
end

run App.new

RubyのFileは以下メソッドを持っています。

  1. eachメソッド

    • eachを使えば、一定データ量ずつファイルから読み出せます
    • 例えば File.each(100) とすれば、ファイルを100バイトずつ読み出せます
  2. closeメソッド

    • ファイルはopenしたら必ずcloseする必要があります

Rackは、 close メソッドを持ったオブジェクトをbodyとして渡すと、各種処理を実行後にcloseを呼び出してくれます。

このように、Fileオブジェクトを渡すことを想定した仕様があらかじめ定義されています。

rackupコマンドを使わずrackアプリを起動する

最後に、rackupコマンドを使わずにアプリを起動するコードを紹介します。

$ bundle exec ruby config.ru

実装は以下コードとなります。

class App
  def call(env)
    [200, { "Content-Type": "text/plain" }, ["Hello Rack!"]]
  end
end

require "rack"

app = Rack::Builder.new do
  run App.new
end

require "rack/handler/puma"
Rack::Handler::Puma.run app

rackpuma関係のファイルをrequireし、Rack::Builderで初期化したものをPumaのHandlerに渡せばよいです。
rackupコマンドは、このあたりのファイル読み込みをあらかじめ自動で行ってくれる便利コマンドです。

まとめ

  • Rackアプリはあらかじめ決められたRackプロトコルにしたがって作成する
  • Rackアプリはcallメソッドを持ったオブジェクトであること
  • callメソッドは、引数を1つ受け取り、StatusCode・Headers・Bodyのペアを返す
  • callメソッドの引数(env)から、リクエスト情報を取り出せる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rack入門 概念編 (1/3)

RailsやSinatraなどのRuby製Webフレームワークを利用されている方は、Rackというキーワードを一度は目にしたことがあるのではないでしょうか。

よく聞くけど詳しくは知らない、そんなやつがRackです。

今回は自分の知識の整理も兼ねて、Rackとは何ものなのかについて調べたメモを、ここに残します。長かったので、全3回に分割しています。

目次

  1. [本記事] Rack入門 概念編(1/3)
  2. Rack入門 Rack Application編 (2/3)
  3. Rack入門 Rack Middleware編 (3/3)

Rackとは何か。ひとことで

  • Webアプリケーションサーバーとアプリケーションを接続するための標準化されたインターフェースのこと

Rackとは何か。詳細

Rackの公式リポジトリでは、Rackは以下のように記載されています。

a modular Ruby webserver interface

訳) モジュール化されたRuby Webサーバーインターフェース

そして、 Supported web servers として PumaやUnicorn が上げられています。

このWebサーバーという表現はいろいろとややこしいので、本稿ではPuma/Unicornのことは アプリケーションサーバー(アプリサーバー) と呼びます。

Rackはなぜ必要なのか。その前に基本的な概念についておさらいします。

アプリケーションサーバーとは

アプリケーションサーバーって、なんでしたでしょうか。
ここで一度振り返っておきます。

Webサーバー、アプリサーバーの代表的なソフトウェアは以下のとおりです。

  • Webサーバー

    • nginx, apache など
  • アプリケーションサーバー

    • Rubyでは Puma, Unicorn など
    • JavaではTomcat など

Webシステムの多くは、いわゆる3層アーキテクチャと呼ばれる構成で設計されており、Webサーバーとアプリサーバーは別に運用します。
以下のような概念図をよく目にするのではないでしょうか。

rack01_01.png

DBサーバーについては、今回は関係ないので説明を割愛します。

別にこの構成に従わなくてもウェブシステムは運用できますが、システムの性能を引き出すために3層アーキテクチャを採用することが多いです。

Webサーバー

Webサーバーは、ユーザーからのリクエストを受け取り、処理結果をレスポンスとして返すソフトウェアのことです。

受け取ったリクエストはWebサーバー自身が処理することもありますし、そのまま処理をアプリサーバーに委譲することもあります。
静的なコンテンツ(画像やファイル)は、Webサーバーでデータを引っ張ってきて返すことが多いです。
ビジネスロジックはRubyなどで書いたアプリに処理を委譲して、処理結果をレスポンスとして返すことが多いです。

nginxなどのWebサーバーは、大量のアクセスが来ても素早く効率的にさばくための機構を持っています。
またSSL通信やデータを圧縮して返すなど、Webの面倒事を引き受けてくれるソフトウェアです。

アプリケーションサーバー

アプリサーバーは、アプリをあらかじめ起動しておき、リクエストが来たらすばやく処理をするためのサーバーです。

アプリサーバーがないと、どうなるでしょうか。

昔のWebシステムでは、アプリサーバーは立てませんでした。
CGI全盛期は、Webサーバーにリクエストが来た後、プログラムを都度起動していました。
リクエストのたびにプログラムの起動から始めるというのは、巨大なアプリケーションになればなるほど起動コストが無視できないですよね。

なので、あらかじめプログラムをメモリ上に読み込んでおき、アクセスが来たら処理を高速に返せるようアプリサーバーを用意します。

PumaやUnicornなどのアプリサーバーは、Webサーバーとしての最低限の機能も持っています。
Webサーバーを立てずにPumaだけでもシステムを運用できます。
ただ、大量のアクセスを効率的にさばくことはPuma単体ではできない(そこはWebサーバーの役割としている)ので、一般的には前段にWebサーバーを立ててリクエストを処理をします。

Rackはどこで使われ、なぜ必要か

Rackはアプリサーバーとアプリ(Webフレームワーク)間のデータのやり取りに利用します。

PumaやUnicornといったアプリサーバーは、特定のアプリ専用(たとえばRails専用)というわけではなく、RackのプロトコルにしたがっているWebフレームワークであれば何でも利用できます。

rack01_02.png

Rackに対応しているRuby製のフレームワークは、Railsの他にSinatra, Padrino, Hanamiなどがあります。

アプリサーバーとアプリ間の通信仕様を定めておく(=インターフェースの標準化をしておく)ことで、
アプリサーバーとアプリケーションフレームワークの組み合わせを自由に変えることができます
Rails専用のアプリサーバーを作る、Sinatra専用のアプリサーバーを作るっていうのは大変ですよね。

アプリ・フレームワーク間の標準インターフェースを作るという流れは、PythonのWSGI(Web Server Gateway Interface)がはじめました。

PythonはもともとWebフレームワークとフレームワーク専用のアプリサーバーが乱立していましたが、WSGIはアプリとサーバーのその組み合わせを自由に変えられるよう標準インターフェースを定めました。
これにより、移植性の高いアプリサーバー/フレームワークを作ることができるようになりました。

Rackの細かいところはWSGIと異なりますが、このWSGIの影響を強く受けています。

まとめ

  • アプリサーバーとは、アプリをあらかじめ起動しておきレスポンスを高速に返すためのサーバー
  • Rackはアプリサーバーとフレームワーク(アプリ)間のインターフェースを定めている
  • Rackのプロトコルに従うことで、アプリサーバーとWebフレームワークの組み合わせを自由に変えることができる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Strong Parameters

Mass Assignment

複数のカラムをまとめて指定すること

user_controller.rb
user = User.new(email: "hoge@email.com", password: "hoge")

Strong Parameters

許可されたカラムのみ使えるようにすること

  • require 必須とする属性
  • permit 許可する属性
user_controller.rb
private
  def user_params
    params.require(:user).permit(:email, :passwordt)
  end

リンク

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

【Rails】devise_token_authで新規ユーザー登録時にUnpermitted parameters: が出たときの対処法

はじめに

gem devise_token_authを使っていて、新規ユーザー登録時に以下エラーが出たときの対処法を残します。

Unpermitted parameters: 保存されて欲しいカラム名

自分でUserテーブルにカラムを追加して、保存しようとしたら
発生する内容です。

今回、自分の場合は:age:genderを追加しようとしたところ発生しました。(以下参照)

Unpermitted parameters: :age, :gender

これを解決していきます。

環境

OS: macOS Catalina 10.15.1
zsh: 5.7.1
Ruby: 2.6.5
Rails: 6.0.2.1

結論:registrations_controllerに追記

registrations_controller
class Api::V1::Auth::RegistrationsController < DeviseTokenAuth::RegistrationsController

  private

  def sign_up_params
    # ここに :age, :genderを追記
    params.permit(:name, :email, :age, :gender, :password, :password_confirmation)
  end

  def account_update_params
    params.permit(:name, :email)
  end
end

※反映するにはrails server再起動が必要です。

理由

Strong Parameterではじかれてしまっているのが問題でした。

sign_up_paramsの中に追記することで、新規ユーザー登録のときだけ許可されるようになります。

こいつも許可してあげてよ!という指定をすればOKですね。

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

どなたかの参考になれば幸いです:relaxed:

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

3ヶ月で Rails 4.2 → 5.2 にした軌跡

概要

資格スクエアの PM をやっています岩瀬と言います。資格スクエアは、法律系難関資格に特化したオンライン学習サービスです。

サービスを開始してから数年が経ち、1つの大きな問題がありました。それは Rails のメジャーアップグレードです。

長年の負債が溜まっていたので、なかなかメジャーアップグレードできずに苦しんでいたのですが、フリーランスのカルパスさん(yhirano55)の加入で一気に進みました。

カルパスさんが方針を考えて実装し、それをレビューでサポートしたラスタムさん(rastamhadi)はじめ、チーム一丸となって取り組んだ結果、わずか3ヶ月で成し遂げられるミラクルを目撃したので、その内容をまとめました。

記事の目的

テストカバレッジが低くても、Rails のメジャーアップグレードができるという知見を共有するためです。

背景

Rails メジャーアップグレードは適宜対応していくのが最善ですが、以下の課題があったため、作業が難航していた状態でした。

  • サービス成長を優先していたため、Rails のメジャーアップグレードが後回しになっていた
  • サービス開始から5年以上が経過していたため、負債が溜まりメンテナンスコストが高くなっていた
  • サービス立ち上げ時はテストコードを書いていなかったため、カバレッジが 32 %と低い状態であった

ですが、Rails 6.0 が出るにあたって、いよいよ Rails 4 系のサポートが終了するため、早急に Rails 5 系に上げる必要がありました。

どうしようか悩んでいた時、ラスタムさんの紹介によってカルパスさんが加入して本格的に着手することになりました。

バージョン

以下は Rails のメジャーアップグレード作業に着手した際に使用していたバージョンです。

名称 バージョン
Rails 4.2.11.1
Ruby 2.3.6

規模

資格スクエア は、2013年11月にサービスを立ち上げてから5年以上が経過しており、中規模と言えるような状態になっていました。規模の目安として、あくまで参考ですが2020年1月現在での各モジュールの数は以下になります。

名称
Controllers 177個
Models 190個
Code LOC 108,612行

戦略

上記の背景と規模を踏まえて、次の戦略で進めることにしました。

  1. メジャーアップグレードを優先して最小のテストカバレッジで進める
  2. テストでカバーできないものは動作確認で洗い出す
  3. 動作確認でも洗い出せなかったものは本番環境に反映後、即対応する

タイムライン

今回の取り組んだアップグレードのざっくりしたタイムラインは以下になります。

年月日 内容
2019年8月27日 Rails 5.0 へメジャーアップグレード作業開始
2019年10月17日 Rails 5.0.7.2 にアップグレード完了
2019年10月23日 Ruby 2.6.5 にアップグレード完了
2019年11月19日 Rails 5.2.3 にアップグレード完了

手順

Rails 4.2 → 5.0

それではポイントを押さえて、実際に行ったメジャーアップグレード手順を紹介します。

1. 足場の確保

まずは開発環境を整え、テスト実行の足場を確保しました。

  • 落ちているテストを直す
  • 開発用途の gem を更新する(lock_diff を活用して changelog を追う)

2. モデルとテーブル定義の不一致を解消する

モデルによるレコード生成をテストコードで担保したいのですが、そもそもモデルとテーブル定義が不一致だったので、それを解消しました。

  • annotate でモデルとテーブル定義の制約条件(validation)が一致していることを確認しやすいようにする
  • 開発環境と本番環境のテーブル定義を一致させる
  • テーブルの NOT NULL 制約とモデルの presence validation を一致させる
  • テーブルの unique index とモデルの uniqueness validation を一致させる

3. 全モデルに最低限の単体テストを書く

ここで全モデルのテストコードを書きました。単体テストを網羅するのはあまりにも大変なため、最低限レコード生成を担保するテストを整備しました。

  • 全モデルの factory を定義する
  • レコードの生成が valid となる最低限のテストを用意する

ex. spec/models/certificate_spec.rb

require 'rails_helper'

RSpec.describe Certificate do
  describe 'factory' do
    it 'has a valid factory' do
      expect(build(:certificate)).to be_valid
      expect(create(:certificate)).to be_persisted
    end
  end
end

4. 管理画面に最低限の結合テストを書く

管理画面は gem を使わず、独自実装であったため結合テストで最低限画面表示できることを担保しました。

ex. spec/features/admin/certificates_spec.rb

require 'rails_helper'

RSpec.feature '管理ツール 資格管理', type: :feature do
  scenario '一覧が閲覧できる' do
    prepare_logged_in_admin_user

    within(:css, '#sidebar') do
      click_link '資格管理'
    end

    expect(page).to have_current_path admin_certificates_path
    expect(page).to have_selector 'h1.page-header', text: '資格管理'
  end
end

5. Rails 5.0 の変更点に応じて修正する

Rails 4.2からRails 5.0へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。

  • Rails 5.0 から全 Model は ApplicationRecord という抽象クラスを継承する形式のため変更する
  • ActionController::Parameters は今後 HashWithIndifferentAccess を継承しないので対応する
  • migrationファイルを置き換える

ex. db/migrate/20150302014237_create_admin_users.rb

- class CreateAdminUsers < ActiveRecord::Migration
+ class CreateAdminUsers < ActiveRecord::Migration[4.2]

6. Rails 5.0 に上げる Pull Request を作成する

ここで Rails 5.0 に上げる目処が立ったので、 Pull Request を作成しました。

  • gem の依存バージョンを解決する
  • Rails 5.0.7.2 にアップグレードする
  • 設定ファイルの更新(app:update)
  • テストが実行できる
  • テストを実行し、落ちているものを修正する
  • すべての警告を抑止する

7. テスト環境で隅々まで動作確認する

最低限のテストコードでカバーしているため、テスト環境で動作確認を行いました。その動作確認の結果、エラーになった箇所を1つ1つ解決していきました。資格スクエアで遭遇したエラーは以下になります。

  • Rails 5.0 では存在しないコールバックをスキップすると ArgumentError になるので不要なコールバックを削除する
  • params をそのまま使うと Rails 5.0 からセキュリティの都合で ArgumentError になるため、hash に変更する
  • Rails 5.0 で collection_check_boxes の hidden 要素が最後から最初に変わったため、修正する

8. Rails 5.0 を本番環境にデプロイする

いよいよ Rails 5.0 に変更できる準備が整ったのでデプロイしました。
何か問題が発生しても対応できるようにエンジニアメンバーが稼働している勤務時間帯に反映しました。

9. 本番環境で発生した問題に対処する

実際、デプロイ後にやはり問題が発生しました。具体的には以下のものになります。

  • 本番反映前の Rails 4.2 で作られたジョブを Rails 5.0 で動作させるために一時的にパッチを当てる

ex. config/initializers/temporary_active_record_patch.rb

class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
  class MysqlDateTime < ActiveRecord::Type::DateTime
  end
end
  • Rails アップグレードに伴い、セッションが切れる問題が発生したためにセッションをクリアするコマンドを実行する
$ RAILS_ENV=production bundle exec rails r 'Rails.cache.clear'

これで Rails 4.2 → 5.0 へメジャーアップグレード完了です。

Rails 5.0 → 5.2

Rails 5.0 にメジャーアップグレードできましたが、ここで立ち止まらず引き続きマイナーアップグレードを行いました。それは Rails 5.2 まで上げると system spec が使えるようになり、javascript の動作をテストコードで担保できるようになるからです。

1. 整理整頓

メジャーアップグレードで把握した使用 gem の内、不要なものは削除するなど、メンテコストを下げました。

2. Rails 5.1 の変更点に応じて修正する

Rails 5.0からRails 5.1へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。

  • ActionController::Parameters における HashWithIndifferentAccess のメソッド呼び出しのサポートが削除されたので対応する
  • render :text を render :plain に変更する
  • render :nothing を head :ok に変更する

3. ついでに API のテストカバレッジを上げて整理する

Rails で API を担っている領域のテストカバレッジが低かったので、requests spec で担保しました。

  • 正常系と異常系の requests spec を整備する
  • 不要なエンドポイントを削除する

4. Rails 5.1 に上げる Pull Request を作成する

ここで Rails 5.1 に上げる目処が立ったので、 Pull Request を作成しました。

  • gem の依存バージョンを解決する
  • Rails 5.1 にアップグレードする
  • 設定ファイルの更新(app:update)
  • テストが実行できる
  • テストを実行し、落ちているものを修正する
  • すべての警告を抑止する

5. Rails 5.2 の変更点に応じて修正する

引き続きRails 5.1からRails 5.2へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。

  • uniq は Rails 5.1 で削除されるので distinct に置換する

6. Rails 5.2 に上げる Pull Request を作成する

ここで Rails 5.2 に上げる目処が立ったので、 Pull Request を作成しました。

  • gem の依存バージョンを解決する
  • Rails 5.2 にアップグレードする
  • 設定ファイルの更新(app:update)
  • テストが実行できる
  • テストを実行し、落ちているものを修正する
  • すべての警告を抑止する

7. テスト環境で隅々まで動作確認する

この段階でテストカバレッジは 47 % と以前よりはだいぶ上がっていますが、
まだまだテストコードで担保できていない機能があるので、テスト環境で動作確認を行いました。

8. Rails 5.2 を本番環境にデプロイする

Rails 5.2 に変更できる準備が整ったのでデプロイしました。

9. 本番環境で発生した問題に対処する

Rails 5.2 のデプロイ後は特にエラーは発生しませんでした。Rails 5.0 の時と同様にセッションが切れる問題は再度発生することが予想されたので対応しました。

これで Rails 5.0 → 5.2 へマイナーアップグレード完了です。

その後

Rails 6.0 にはすぐに上げることができるのですが、特に使いたい機能があるわけではなかったので、ここで一旦立ち止まり強化することにしました。対応したことは以下になります。

  • system spec による結合テストの整備(2020年1月時点でカバレッジ 53 %)
  • Dependabot によるバージョン追従
  • CI の実行速度の改善と整備
  • CD の開発

まとめ

Rails 4.2 から 5.2 までアップグレードした3ヶ月の軌跡をまとめました。

最初、自分が資格スクエアをメジャーアップグレードしようと考えた時は、正直テストカバレッジが低いので相当難しいなという印象でした。

しかし、効果的な戦略と的確な変更によってわずか3ヶ月で成し遂げられた状況を見て、とても感動しました。そしてシステム面では新しい機能が使用できたり、パフォーマンスが向上したりと様々な恩恵を受けています。

この方法が自分たちと同じようにメジャーアップグレードに悩んでいる方々にとって参考になれば幸いです。

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

railsのDBで型をintegerからfloatへ変更しnumber_fieldで小数点を扱えるようにする

前回の記事の続きで初めてfloat型を扱ったのでメモ

やりたい事

  • DBに間違ってinteger型で作ったカラムをfloat型に変更
  • form_for内でnumber_fieldで小数点を扱えるようにする
  • DBに保存した値を合計するメソッドを作る

参考

【Rails】form_for の number_field で小数を入力できるようにする
https://qiita.com/tegnike/items/07f789eb22c7a7bf6a19

マイグレーションを使ったカラムの追加、削除、データ型の変更 [ 自分用メモ ]
https://qiita.com/dawn_628/items/13fa64dc6d600e921ce3

【Rails】カラムの合計値を求める!
https://qiita.com/tomokichi_ruby/items/8758a91566957cfc5429

number_fieldでは整数のみしか扱えない

new.html.erb
<%= form_for @count_time do |f| %>
 <%= f.number_field :count_hour, class:'input_form' %>
 <%= f.submit '登録', class:'input_submit' %>
<% end %>

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

というフォームがあって,

小数点の数字をいれると,
スクリーンショット 2020-01-03 17.35.00.png
というふうになるので

new.html.erb
<%= form_for @count_time do |f| %>
 <%= f.number_field :count_hour, step: '0.1', class:'input_form' %>
<%# step: '0.1'を追加 %>
 <%= f.submit '登録', class:'input_submit' %>
<% end %>

step: '0.1'を追加すると小数点以下第一位までの数値を扱えるようになります

このままだとDBには整数でしか保存されない

DBの count_hourinteger型なので整数のみしか保存してくれないのでカラムの型を変更する

ターミナル
$ rails g migration change_data_count_hour_to_count_time
Running via Spring preloader in process 94544
      invoke  active_record
      create    db/migrate/20200103074431_change_data_count_hour_to_count_time.rb

カラム名がcount_hourでテーブル名がcount_timeになります

20200103074431_change_data_count_hour_to_count_time.rb
class ChangeDataCountHourToCountTime < ActiveRecord::Migration[5.2]
  def change
    change_column :count_times, :count_hour, :float #追記。テーブル名は複数形で
  end
end

rails db:migrateします

これでfloat型に変更できたのでnumber_fieldから送られる小数点の値がDBへ保存できるようになりました

DBの値をビューへ描画

index.html.erb
  <table>
    <tr>
      <th>日付</th>
      <th>時間</th>
    </tr>
    <% @count_time.each do |count_time| %>
      <tr>
        <td><%= count_time.created_at.to_date %></td>
        <td><%= count_time.count_hour %>時間</td>
      </tr>
    <% end %>
  </table>

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

sumメソッドを使い学習時間の合計を表示させる

count_time_controller.rb
class CountTimesController < ApplicationController
 def index
  @count_time = CountTime.where(user_id: current_user.id).order('updated_at DESC').limit(5)
  @count_hour = @count_time.sum(:count_hour)
 end
end

sumメソッドの引数にカラムを指定するで合計値を出します

完成

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

まとめ

ひとまずこれで自分が最初に想像していた主要機能が完成しました。開発にそんな時間がかからないアプリでしたが知らない事も学べたので良かったかなと。あとは思い付いた追加の機能を開発していこうと思います。

終わり

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

HerokuでS3に画像をアップロードした話[Rails][S3][CarrierWave][fog]

転職活動用にポートフォリオを作っています。
DTPの作品も多く、画像が全部でおよそ18MBあったのですが、Herokuの無料版の容量は5MBのため、画像の外部ストレージとしてS3を利用するようにしました。

調べたら、多くの人がつまづいているようでしたので、自分のためにも手順をメモしておこうと思います。

S3の利用設定

まずは、S3の利用設定を行います。手順については、こちらのサイトを大変参考にさせていただきました。

【Rails】S3へ『CarrierWave+fog』を使って画像アップロードする方法

↑こちらの記事以上に丁寧な説明はないと思いますので、S3の設定についてはこちらをご参照いただければ幸いです。

gemをインストール

画像アップロードに必要な、以下のgemをインストールします。

  • carrierwave…画像をアップロードするgem
  • mini_magick…画像をリサイズするgem
  • fog…S3などのクラウドストレージ内の画像を参照するgem

それぞれ、設定の書き方は以下の通り。

Gemfile
group :production do
  gem 'pg',             '0.18.0'
  gem 'rails_12factor', '0.0.2'
  gem 'fog-aws'
end

gem 'carrierwave'
gem 'mini_magick'

忘れずにbundle installします。

なお、私の場合ですがfogというgemではaws関連のdependencyのgemがインストールされず、fog-awsを利用しないと先に進めませんでした。

アップローダーの生成

 CarrierWaveをインストールすると、画像のアップローダーの生成ができるようになりますので、以下のコマンドで生成します。

$ rails generate uploader カラム名

最後の「カラム名」は、画像を必要とするモデルのデータベースのカラム名です。単数形、頭文字大文字で表記します。私は、すでにあったテーブルのカラム名から、Imageにしました。

なお、画像のファイル名が文字列のため、imageカラムのデータ型はstring型です。

$ rails generate uploader Image
      create  app/uploaders/image_uploader.rb ←生成

CarrierWaveに、imageカラムがuploaderと関連していることを認識させるためには、mount_uploaderというメソッドを使います。(今回モデル名はwork

models/work.rb
class Work < ApplicationRecord
  mount_uploader :image, ImageUploader
end

 viewファイルの書き換え

そして、今度はビューファイルをいくつか変更します。まずは、ファイルをアップロードする機能をviewに持たせます。画像をアップロードするフォームのタグをfile_fieldにしました。

views/works/new.html.haml
= f.label :image, "画像:"
= f.file_field :image

そして、workの一覧ページの書き方も、CarrierWaveに合わせて書き換えます。

views/index.html
-# 書き換え前
- @works.each do |work|
   = image_tag work.image

-# 書き換え後
- @works.each do |work|
  = image_tag work.image.url

work.imageの後にurlをつけるのがポイントです。
以上の動作により、無事画像のアップロードができました。

Image from Gyazo

本番環境で画像のアップロード先を変える

この時、画像ファイルはデフォルトでは、ローカルのフォルダにアップロードさせています。

views/uploaders/image_uploader.rb
storage :file

storage :fileという記述は、ローカルのファイルシステムに画像を保存するための記述です。

画像で示すと、こちらにありました。
Image from Gyazo

これだと、Herokuにデプロイした時に容量を圧迫してしまうので、本番環境だけS3に画像をアップするようにしたいと思います。

views/uploaders/image_uploader
if Rails.env.production?
  storage :fog
else
  storage :file
end

if Rails.env.production?で本番環境ではクラウドストレージに保存するようにしています。
さらに、同じく、image_uploaderに下記のように記載します。

views/uploaders/image_uploader
if Rails.env.production?
    CarrierWave.configure do |config|
      config.fog_credentials = {
        # Amazon S3用の設定
        :provider              => 'AWS',
        :region                => ENV['S3_REGION'],  # S3に設定したリージョン。
        :aws_access_key_id     => ENV['S3_ACCESS_KEY'],
        :aws_secret_access_key => ENV['S3_SECRET_KEY']
      }
      config.fog_directory     =  ENV['S3_BUCKET']
    end

それぞれ、以下のように設定します。ちなみに、東京のリージョンはap-northeast-1です。

$ heroku config:set S3_ACCESS_KEY="S3のAccessキーを入力"
$ heroku config:set S3_SECRET_KEY="同様に、Secretキーを入力"
$ heroku config:set S3_BUCKET="Bucketの名前を入力"
$ heroku config:set S3_REGION="Regionの名前を入力"

ここまで書いたらようやく本番環境にデプロイです。デプロイして動作を試します。

・・・できた!!!無事アップロード完了です!!

Image from Gyazo

追伸:エラーへの対処

なお、本番環境ではすぐに順調に動作をしたわけではなく、私の場合はAccess Deniedというエラーに遭いました。

S3のパブリックアクセス設定を変更するを変更したところ、なんとかなりましたので、ここに情報を共有しておきます。
Image from Gyazo

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

コントローラー名とアクション名でSCSSを呼び出す

例えば WelcomeController の index アクションが呼ばれた場合、デフォルトの状態だとビューには welcome.css(.scss) がロードされるのだけど、これを呼ぶかどうかについて、ファイルの有無とか controller での変数で判定できたらいいのになという話。

色々方法はあるかと思うのですが、以下のようにしてみました。

assets/stylesheets/application.css

= require_self
*= require_directory .
デフォルトだと require_tree . となっている箇所を require_directory . にする。こうすることでルートディレクトリ( assets/stylsheets/
)の css しか自動でインポートしないようにする。

ついでにスタイルシートのディレクトリも以下のように views っぽくする。

f:id:ltcmdr927:20121209105229p:plain

HEAD タグは application.html.erb で編集しているので、そこを修正する。

views/layouts/shared/application.html.erb

<%= stylesheet_link_tag "application", :media => "all" %>
<%= partial_stylesheet_link_tag controller.controller_name, controller.action_name, @partial_css_disabled %>
<%= javascript_include_tag "application" %>
stylesheet_link_tag と javascript_include_tag の間に新しい helper メソッドを追加する。引数でビューを読んだ時のコントローラ名とアクション名を指定。@partial_css_disabled は後述。

app/helpers/application_helper.rb

module ApplicationHelper

def partial_stylesheet_link_tag(controller_name, action_name, disabled = true)
if disabled == false || disabled.nil?
if File.exist?("#{Rails.root.to_s}/app/assets/stylesheets/#{controller_name}/#{action_name}.css.scss")
return stylesheet_link_tag "#{controller_name}/#{action_name}"
end
end
end

end
partial_stylesheet_link_tag メソッドを書く。disabled が false かあるいは nil かを判定し、正なら scss ファイルの有無を判定し、最終的にコントローラ/アクションの stylesheet_link_tag を戻す、という感じ。

基本的にはファイルの有無で stylesheet タグが追加されるかどうかを判定するようになるので、例えばファイルは存在するけどタグは追加したくないという場合は、下記のように controller で @partial_css_disabled 変数に true を指定してやる。 true を指定するとファイルが存在していてもタグが追加されない。

app/controller/welcome_controller.rb

class WelcomeController < ApplicationController

def index
@partial_css_disabled = true
end

end
若干イモっぽい感じがしなくもないですが、個人的にはこれで管理しやすくなりました。(コントローラの階層構造が深くなった場合については調査して後々追記予定。)

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

Railsチュートリアルメモ - 第4章・第5章

メモ目次はこちら

Railsチュートリアル第4章はこちら
Railsチュートリアル第5章はこちら

Railsチュートリアルで気づいたことのメモを記載していきます

4.1 動機

Rubyに関する知識を深めていく章。
手始めに条件分岐で異なる文字列を返すhelperを自作する。

4.2 文字列とメソッド

ポイント

  • 文字列の式展開 => Rubyはシングルクォート文字列の中では式展開を行わない(シェルスクリプトと同じ)
  • putsメソッド(printメソッド + 行末の改行)
  • nil
  • 暗黙の戻り値 => メソッド内にreturnを明示しなくても最後に評価した式をreturnする
  • mixed in => moduleをincludeすること
  • Railsでは自動的にヘルパーモジュールをmixed inしてくれるため、include行を書かなくてもOK

4.3 他のデータ構造

たぶんここらへんで眠くなって落脱する人が多い気がする、、、
初心者は流し読みにして、後でわからないところが出てきたら戻ってくるくらいの方が良さそう

ポイント

  • Rubyの配列はゼロオリジン(ゼロ・インデックス)
  • 比較演算子(==、!=)
  • 破壊的メソッド(メソッドの最後が!)
  • pushメソッド (または同等の<<演算子)
  • 範囲 (range) e.g (0..9).to_a

Rubyは改行と空白を区別しない(式の途中で改行してもOK)

4.4 Rubyにおけるクラス

ポイント

  • リテラルコンストラクタ、名前付きコンストラクタ
  • superclassメソッド
  • selfキーワード
  • eachメソッド
  • ブロック e.g. { |i| puts 2 * i } => pythonでいうlambda?
  • ハッシュとシンボル
  • 以下は等価
    • { :name => "Michael Hartl" }
    • { name: "Michael Hartl" }
  • 組み込みクラスの変更が可能
  • attr_accessor => attributeに対しるaccessorを作成するための記法
  • initialize => 初期化メソッド(pythonでいうinitと同じ)

4.5 最後に

割愛

5.1 構造を追加する

html/cssについての解説の章。

ポイント

  • パーシャル(DOMの一部を_始まりのファイルに切り出せる)

5.2 Sassとアセットパイプライン

SASS(CSSプリプロセッサー)の説明とRailsでの指定の仕方についての章。

ポイント

  • アセットディレクトリ(*/assetsで表される静的ファイルを目的別に分類するディレクトリ)
  • マニフェストファイル(アセットディレクトリにあるファイルをどのようにまとめるかを定義するファイル。実際の処理はSprocketsというgemが行う)
  • プリプロセッサエンジン(.scssや.erbなどを変換して、html/css/jsに置き換えてくれるやつ。Sprocketsがアセットをまとめてくれた後にプリプロセッサエンジンが実行される)
  • Sassの変数
    • $始まりで変数を定義できる
    • bootstrap lessで定義されている変数(@gray-light)なども$gray-lightと書けば使用可能

5.3 レイアウトのリンク

  • 名前付きルート
    • config/routes.rbの中でget '/help', to: 'static_pages#help', as: 'helf'のように書くとhelp_pathやhelp_urlのような形で呼び出せるようになる
  • 統合テストの作成
    • rails generate integration_test site_layoutで統合テストを作成できる
    • rails test:integrationで統合テストの実行

5.4 ユーザー登録: 最初のステップ

6章以降でユーザー登録処理を作成していくための導入。ここではこれまでの章の振り返り的な内容。

5.5 最後に

割愛

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

【Rails】バリデーション

覚えたバリデーションのメモとして残します。

トリガされるメソッド

  • create
  • create!
  • save
  • save!
  • update
  • update!

書き方

app/models/person.rb
class Person < ApplicationRecord
  #ここに記載
  #validates :属性, バリデーションヘルパー
end

バリデーションヘルパー

presence: true
属性が「空でない」
length: {maximum: 140}
属性の値の長さが140以下
uniqueness: true
属性の値が一意(unique)であり重複していない
format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i }
有効なメールアドレスかどうか

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

Routing Error No route matches [PATCH] "/admin/users/1"が発生

最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!

Formtastic::UnknownInputError Unable to find input class Input

このエラーが起きた状況は、Active Adminで作成したユーザー管理画面において編集機能を追加するために
actionsに:editを追加した所、発生しました。

app/admin/users.rb
actions :index , :show , :edit

Routing Errorと書いていることから、ルーティングに何らかのエラーが出ていることが考えられる。
さらにその下に
No route matches [PATCH] "/admin/users/1"
と書いてあることから、"/admin/users"ってところと[PATCH]がマッチしていないことが考えられる。

"rake routes"で、どの様に設定されているルーティングを見てみた所、エラー通りadmin_user_pathの[PATCH]が無かったです。
しかし、routes.rbの設定を変更してもエラーが直らず、というかactionsの方を追記しないといけないのではと思い。
以下の様に記述した所、Routing Errorが直りました。

app/admin/users.rb
actions :index , :show , :edit, :update

これに直ぐに気づくことができなかったのが恥ずかしいです。。。

参考にした記事

エラー解決Routing Error No route matches - Qiita
rails APIモード+ActiveAdminで、管理者画面にてユーザーを削除しようとしたらNo route matchesのエラーが発生した際の対処法 - Qiita

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

rails: belongs_to :user, optional: trueとは?

簡単に説明すると、belongs_toでoptional: trueを設定することにより外部キーのnilを許可できるようになります。

optional: trueはどんなときに使われるのか?

例えば、Q&Aのようなサービスを作る際にuserテーブルとquestionテーブルがあるとする。
その際に、ログインしていないユーザーからでも質問をポストできるシステムを作るときに使われたりする。
基本的にアソシエーションを書くときは外部キーが無いっていうことはあまりないがたまにある時もあるのでその際にoptional: trueをつけなかったりするとバグの温床になるので気をつける必要がある。

ただDBの設計上、外部キーのnilを許可することが少ない気がするので、あまり使う設定ではないかもしれません。

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

pickadate.jsを使ってフォームにカレンダーを表示させる

こんにちは!スージーです!
久し振りにrailsでアプリ開発を開始したのでその際に使ったdatepickerについてまとめ

datepickerとは

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

日付入力フォームを選択した時に表れるカレンダー

参考記事

結局、どのdatepickerが一番使い勝手がよいのかhttps://qiita.com/knt45/items/6d74f6785cd4547ae53b

Rails Application Build Guides
https://rails.densan-labs.net/form/datetime_register_form.html

pickadate.js
https://amsul.ca/pickadate.js/

pickadate-rails
https://github.com/veracross/pickadate-rails

準備

Gemfile
gem 'pickadate-rails'

bundle install

application.js
//= require pickadate/picker 追記
//= require pickadate/picker.date 追記
//= require pickadate/picker.time 追記
//= require jquery_ujs
//= require_tree .
application.scss
@import "pickadate/classic";
@import "pickadate/classic.date";
@import "pickadate/classic.time";

cssレイアウトはdefault又はclassicが選べるようになっている

Jaファイル編集

datepicker.js
$(function() {
  $( "#datepicker" ).pickadate(); // カレンダー表示のイベント
});

Jsはこれだけで使えるようになる。リファレンスには色々なレイアウトのカレンダーが用意されている。

turbokinksでイベント発火しない

jQueryあるあるですがturbolinksをtrueのままにしているとページをリロードしないとイベント発火しないので、今回は不要なので削除。application.js,application.html.erbのturbolinksに関するコードは削除。gem 'turbolinks'も削除してbundle installします。

contorollerのアクション作成

datepickers_controller.rb
class datepickersController < ApplicationController

  def new
    @datepicker = Datepicker.new
  end

  def create
    @datepicker = Datepicker.create(datepicker_params)
    if @datepicker.save
      redirect_to :root
    else
      render :new
    end
  end

  private

  def datepicker_params
    params.require(:start_day).permit(:start_day).merge(user_id: current_user.id)
  end
end

この辺はお決まりのnew/createアクションです

完成

カレンダーで日付を選択して登録できている事をパラメータを確認

ターミナル
Started POST "/start_days" for ::1 at 2020-01-03 12:50:13 +0900
Processing by StartDaysController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"4Ug39AI0MM0azF2XqNEcTOSQNjpNWQ+ovCSI7PFrPPatvEM8avbMZ6MII5JdxZnni+Hssbo7x6NTAjA0udiH7Q==", "datepicker"=>{"datepicker"=>"3 January, 2020"}, "commit"=>"登録"}

パラメータの値がちゃんとDBに保存されています

まとめ

日付の選択は何かを登録する場面で使う事が多いかと思いますし、年月日をそれぞれ入力するのはUI/UX的に煩雑になってしまいますが、datepickerを使えば便利になりますし、簡単に実装もできました。

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

[Validation設定]ユーザー登録

Validation設定

ユーザー登録時につけたバリデーションの記録です。

今回でたエラー
image.png

セキュリティの関係で^\Aにしろと言われました

user.rb
validates :kana_firstname, :kana_lastname, presence: true, 
            # カナのみ可
            format: { with: /\A([ァ-ン]|ー)+\z/, message: "is must NOT contain any other characters than alphanumerics." }

これでエラーは無くなりました!

参考

user.rbに書いた他のバリデーション

models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
        :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:facebook, :google_oauth2]

  validates :nickname, :firstname, :lastname, :birthday, presence: true
  validates :password, presence: true, length: { minimum: 7 }, 
            # 英数字のみ可
            format: { with: /\A[a-z0-9]+\z/i, message: "is must NOT contain any other characters than alphanumerics." }
  validates :email, presence: true, 
            # 重複不可
            uniqueness: { case_sensitive: false }, 
            # 英数字のみ可,@を挟んだemailの形になっているか
            # /^\S+@\S+\.\S+$/   /\A[a-z0-9]+\z/i  /^[a-zA-Z0-9]+$/
            format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i, message: "is must NOT contain any other characters than alphanumerics." }
  validates :kana_firstname, :kana_lastname, presence: true, 
            # カナのみ可
            format: { with: /\A([ァ-ン]|ー)+\z/, message: "is must NOT contain any other characters than alphanumerics." }
            has_many :sns_credentials

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

Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストの作成・削除に関する機能を、テスト駆動開発で実装する

別のユーザーに所属するマイクロポストをfixtureに追加する

今後のテストで使うために、マイクロポストのfixtureに対して、最初のテストで使ったユーザーとは別のユーザーに属するマイクロポストを追加していきます。

test/fixtures/microposts.yml
  ...略
+
+ ants:
+   content: "Oh, Is that what you want? Because that's haw you get ants!"
+   created_at: <%= 2.years.ago %>
+   user: :mkirisame
+
+ zone:
+   content: "Danger zone!"
+   created_at: <%= 3.days.ago %>
+   user: :mkirisame
+
+ tone:
+   content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
+   created_at: <%= 10 minutes.ago %>
+   user: :rusami
+
+ van:
+   content: "Dude, this van's, like, rolling probable cause."
+   created_at: <%= 4.hours.ago %>
+   user: :rusami

マイクロポストのUIに対する統合テストを追加する

マイクロポストのUIに対する統合テストの生成

例によってrails generate integration_testコマンドによってテストを生成していきます。テストの名前はmicroposts_interface_testとします。

# rails generate integration_test microposts_interface
Running via Spring preloader in process 2380
      invoke  test_unit
      create    test/integration/microposts_interface_test.rb

マイクロポストのUIに対する統合テストの実装

マイクロポストのUIに対する統合テストの全体像は、以下のようになります。先程生成したばかりのtest/integration/microposts_interface_test.rbにコードを記述していきます。

test/integration/microposts_interface_test.rb
require 'test_helper'

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

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    # 無効な送信
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "" } }
    end
    assert_select 'div#error_explanation'
    # 有効な送信
    content = "This micropost really ties the room together"
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost: { content: content } }
    end
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    # 投稿を削除する
    assert_select 'a', text: 'delete'
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(first_micropost)
    end
    # 違うユーザーのプロフィールにアクセス(削除リンクがないことの確認)
    get user_path(users(:mkirisame))
    assert_select 'a', text: 'delete', count: 0
  end
end

テストの内容は以下になります。

  • Homeページに、マイクロポストを表示する部分が実装されていること
  • 内容が空のマイクロポストがMicropostsリソースにPOSTされた際に、RDBにマイクロポストが保存されないこと
    • その際、エラーメッセージがerror_explanation属性を持つdiv要素としてレンダリングされていること
  • 有効な内容のマイクロポストがMicropostsリソースにPOSTされた際に、RDBにマイクロポストが保存されること
    • その際、Homeページにリダイレクトされること
    • リダイレクト後のHomeページに、当該マイクロポストの内容がレンダリングされていること
  • 「delete」という文字を内容とするa要素が存在すること
    • マイクロポストの削除リンクを表す
  • Micropostsリソースが正しいDELETEリクエストを受け取った際に、RDBから対象のマイクロポストが削除されること
    • 削除されるマイクロポストは、setupメソッド中で指定したユーザーの最新のマイクロポスト
  • ログインユーザーとは違うユーザーのプロフィールにアクセスした際に、「delete」という文字を内容とするa要素が存在しないこと

マイクロポストのUIに対する統合テスト実装時点でのテストの実行結果

上記コードを保存した時点でtest/integration/microposts_interface_test.rbを対象にテストを行うと、結果は以下のようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 2401
Started with run options --seed 51205

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.5630840999947395]
 test_micropost_interface#MicropostsInterfaceTest (3.56s)
        Expected at least 1 element matching "div.pagination", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:11:in `block in <class:MicropostsInterfaceTest>'

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

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

以下のようなメッセージが出て、テストが失敗しています。

Expected at least 1 element matching "div.pagination", found 0..
Expected 0 to be >= 1.
test/integration/microposts_interface_test.rb:11

私の環境では、test/integration/microposts_interface_test.rbの11行目には以下のコードが記述されています。

test/integration/microposts_interface_test.rb(11行目)
assert_select 'div.pagination'

まずはHomeページにマイクロポストのフィードを表示する部分を実装する必要がありそうですね。

Homeページにマイクロポストのフィードを表示する部分を実装する

Railsチュートリアル本文とは順番が前後しますが、テストの失敗内容を考えると、先に実装するのは「Homeページにマイクロポストのフィードを表示する部分」となります。

Railsチュートリアル本文には、「マイクロポスト投稿フォームと、マイクロポストのフィード表示部分が実装されたHomeページのモックアップ」が、図 13.13として示されています。

現在ログインしているユーザーのマイクロポストを全て取得する

ログイン済みユーザーのHomeページに表示するマイクロポストのフィードの内容は、「当該ユーザーが投稿したマイクロポスト」となります。このことは全てのユーザーに共通します。

「フィードの内容を取得する」という処理の内容は、全てのユーザーに共通する内容です。そのため、当該処理はUserモデルに実装するのが自然な流れとなります。編集対象のファイルはapp/models/user.rbですね。

また、当該メソッドの名前はfeedとします。

feedメソッドの内容

feedメソッドの内容は、現在のところは、「whereメソッドにより、ログインユーザー自身の全てのマイクロポストを取得する」というものにしておきます。

User#feed
# 試作feedの定義
# 完全な実装は次章の「ユーザーをフォローする」を参照
def feed
  Micropost.where("User.id = ?", id)
end

なお、Railsチュートリアル本文では、今後「指定したユーザーの全てのマイクロポストを取得する」という内容にしていくことが示唆されています。

whereメソッドの第1引数中で使われている?の意味

Micropost.where("User.id = ?", id)

上記whereメソッドの第1引数で使われている?は、「SQLクエリを組み立てる際、引数中の危険な文字をエスケープする処理を行わせる」という動作を意味します。これは、「SQLインジェクション」という深刻なセキュリティホールを回避するために非常に重要な実装となります。Railsに限らず、SQL文に変数を代入する処理を記述する際には、常にエスケープ処理とセットで記述することが強く望まれます。

あえてUser.idを与えるような実装にした理由

「ログインユーザー自身の全てのマイクロポストを取得する」のみであれば、以下の実装内容でも実現可能です。

def feed
  microposts
end

にもかかわらず、あえてUser.idを与えるような実装にしたのは、今後「指定したユーザーの全てのマイクロポストを取得する」という実装内容にしていくことを想定したためです。

Userモデルに、実際にfeedメソッドを実装する

上記の内容を踏まえて、app/models/user.rbに対しては以下の変更を反映していきます。

app/models/user.rb
  class User < ApplicationRecord
    ...略
+
+   # 試作feedの定義
+   # 完全な実装は次章の「ユーザーをフォローする」を参照
+   def feed
+     Micropost.where("user_id = ?", id)
+   end

    private

      ...略
  end

homeアクションにフィードのインスタンス変数を追加する

Home画面にフィードを表示させるには、StaticPagesコントローラーのhomeメソッドに、フィード内容を与えるためのインスタンス変数を定義する必要があります。名前は@feed_itemsとしましょう。

@feed_itemsの定義は、さしあたって以下のようになります。

@feed_items = current_user.feed.paginate(page: params[:page]) if logged_in?

app/controllers/static_pages_controller.rbの内容は、さしあたって以下のように変更します。

app/controllers/static_pages_controller.rb
  class StaticPagesController < ApplicationController
    def home
+     @feed_items = current_user.feed.paginate(page: params[:page]) if logged_in?
    end

    def help
    end

    def about
    end

    def contact
    end
  end

ステータスフィードのパーシャル

Homeビューにステータスフィードを表示させるためのパーシャルも必要になります。パーシャルそのものの名前はfeedとします。対応するファイル名はapp/views/shared/_feed.html.erbとなります。

app/views/shared/_feed.html.erbの内容は以下のようになります。

app/views/shared/_feed.html.erb
<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>

ステータスフィードのパーシャルからでも、単一のマイクロポストを表示するパーシャルを使うことができる

Railsチュートリアル本文においては、「ステータスフィードのパーシャルは、単一のマイクロポストを表示するパーシャルとは内容が異なる」という点が強調されています。

<%= render @feed_items %>

@feed_itemsを構成する個々の要素(より具体的には、前述feedメソッドの個々の戻り値)は、Micropostクラスのインスタンスです。このような場合においてRailsは、自身の機能により、暗黙的にMicropostのパーシャルを呼び出すことが可能なのです。

以下に、Micropostのパーシャルの実体であるapp/views/microposts/_micropost.html.erbを再掲しておきます。

app/views/microposts/_micropost.html.erb(再掲)
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
</li>

Micropostに限らず、Railsのビューに対してリソースが渡された場合、Railsは対応する名前のパーシャルを、渡されたリソースのディレクトリ内から探し出すことができます。

ログインしている場合とログインしていない場合でHomeページの表示内容を変化させる

Homeページにステータスフィードを実装する前提条件として、「ログインしている場合とログインしていない場合でHomeページの表示内容を変化させる」という処理を実装する必要があります。

ここまでHomeページに実装してきたサインアップページヘのリンクは、ログインしていない場合にのみ必要となる内容です。ログインしている場合に対するHomeページの内容は、また別に定義する必要があります。

サインアップページへのリンクを、ログインしていない場合にのみ表示する

まずは「サインアップページへのリンクを、ログインしていない場合にのみ表示する」という実装内容を反映していきましょう。少々コードが汚いですが、ひとまずif-elseで分岐を行うようにします。

対象のファイルはapp/views/static_pages/home.html.erbですね。

app/views/static_pages/home.html.erb
  <% provide(:title, "Home") %>
  <% if logged_in? %>
    <%#TODO: ログイン済みユーザーに対する処理の実装 %>
  <% else %>
    <div class="center jumbotron">
      <h1>Welcome to the Sample App</h1>

      <h2>
        This is the home page for the
        <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
        sample application.
      </h2>

      <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
    </div>

    <%= link_to image_tag("rails.png", alt: "Rails logo"),
                'http://rubyonrails.org/' %>
  <% end %>

Homeページにステータスフィードを追加する

  • Userモデルにおいて、ログインユーザー自身の全マイクロポストをRDBから取得する機能
  • StaticPagesコントローラーのhomeアクションにおける、フィード内容を中身とするインスタンス変数の定義
  • ステータスフィードのパーシャル
  • ログインしている場合とログインしていない場合でHomeページの表示内容を変化させる機能

以上の実装が完成すれば、Homeページにステータスフィードを追加することができるようになります。

では、Homeパージにステータスフィードを追加していきます。

  <% provide(:title, "Home") %>
  <% if logged_in? %>
-   <%#TODO: ログイン済みユーザーに対する処理の実装 %>
+   <div class="row">
+     <aside class="col-md-4">
+       <%#TODO: ユーザー情報表示部の実装 %>
+       <%#TODO: マイクロポスト投稿フォームの実装 %>
+     </aside>
+     <div class="col-md-8">
+       <h3>Micropost Feed</h3>
+       <%= render 'shared/feed' %>
+     </div>
+   </div>
  <% else %>
    <div class="center jumbotron">
      <h1>Welcome to the Sample App</h1>

      <h2>
        This is the home page for the
        <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
        sample application.
      </h2>

      <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
    </div>

    <%= link_to image_tag("rails.png", alt: "Rails logo"),
                'http://rubyonrails.org/' %>
  <% end %>

これでHomeページにステータスフィードが追加できたはずです。

Homeページへのステータスフィードの追加が完了した時点でのテストの実行結果

Homeページへのステータスフィードの追加が完了した時点で、test/integration/microposts_interface_test.rbを対象としたテストを実行すると、結果は以下のようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 2446
Started with run options --seed 43986

ERROR["test_micropost_interface", MicropostsInterfaceTest, 3.7222297999833245]
 test_micropost_interface#MicropostsInterfaceTest (3.72s)
NoMethodError:         NoMethodError: undefined method `document' for nil:NilClass
            test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>'

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

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

私の環境では、test/integration/microposts_interface_test.rbの13行目から16行目の内容は以下のようになっています。

test/integration/microposts_interface_test.rb(13〜16行目)
# 無効な送信
assert_no_difference 'Micropost.count' do
  post microposts_path, params: { micropost: { content: "" } }
end
assert_select 'div#error_explanation'

エラーメッセージの内容は以下のようになっています。

NoMethodError: undefined method `document' for nil:NilClass

「無効なマイクロポストのPOSTに対し、有効なオブジェクトを返してきていない」という趣旨のエラーと考えられます。まずは「無効なマイクロポストのPOSTに対する処理」を実装する必要がありそうですね。

マイクロポスト作成機能の実装についての概要

「無効なマイクロポストのPOSTに対する処理」という言及を先にしてしまいましたが、まずはマイクロポスト作成機能の実装そのものについての説明が必要ですね。

「マイクロポストを作成する」という処理は、「Micropostsコントローラーに対するHTTPのPOSTリクエストの発行」をトリガーとして行われます。「HTTPのPOSTリクエストを、コントローラーのcreateアクションに向けて発行する」という流れそのものは、過去に実装した「ユーザーのサインアップ」と類似しています。

無効なマイクロポストのPOSTに対する処理

Micropostsコントローラーのcreateアクション

マイクロポストの投稿に際し、Webブラウザで入力する必要十分なパラメーターをStrong Parametersに設定する

マイクロポストの投稿に際し、Webブラウザのフォームで入力する必要があるパラメーターは、マイクロポストのcontent属性のみです。それ以外のパラメーターは、逆にPOSTリクエストから変更可能であってはなりません。

Micropostコントローラーのcreateアクションでは、「Webブラウザから送信されるPOSTリクエストのうち、content属性のみを受け取るようにし、それ以外の属性は受け取らないようにする」という実装が必要になります。ここで使うのはStrong Parameters機能ですね。

privateメソッド以降にmicropost_paramsというメソッドを定義し、Strong Parameters機能の使用に必要な実装を行っていきます。

private

  def micropost_params
    params.require(:micropost).permit(:content)
  end

micropost_paramsメソッドでは、以下の条件について記述しています。

  • POSTリクエストのクエリパラメータにはmicropostパラメータを必要とする
  • POSTリクエストのmicropostパラメータのうち、contentパラメータのみを受理する

RDBに保存されるマイクロポストを生成する

@micropost = current_user.microposts.build(micropost_params)

上記コードにより、ログイン済みのユーザーに紐付けられた新たなマイクロポストを生成し、インスタンス変数@micropostに格納します。micropost_paramsというのは、前の項で記述した通り、「POSTリクエストに必要なパラメータと、POSTリクエストから受理するパラメータ」について記述するメソッドとなります。

無効なマイクロポストがPOSTされた場合の処理

ここまで言及した内容により、ひとまず「無効なマイクロポストがPOSTされた場合の処理」を記述していくことができるようになります。対象としたapp/controllers/microposts_controller.rbです。

app/controllers/microposts_controller.rb
  class MicropostsController < ApplicationController
    before_action :logged_in_user, only: [:create, :destroy]

    def create
+     @micropost = current_user.microposts.build(micropost_params)
+     if @micropost.save
+       #TODO: ここに有効なマイクロポストの投稿に対する処理を書く
+     else
+       render 'static_pages/home'
+     end
    end

    ...略
+
+   private
+
+     def micropost_params
+       params.require(:micropost).permit(:content)
+     end
  end

上記実装が完了した時点でのテストの実行結果

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 2472
Started with run options --seed 5099

ERROR["test_micropost_interface", MicropostsInterfaceTest, 3.8275596000021324]
 test_micropost_interface#MicropostsInterfaceTest (3.83s)
ActionView::Template::Error:         ActionView::Template::Error: undefined method `any?' for nil:NilClass
            app/views/shared/_feed.html.erb:1:in `_app_views_shared__feed_html_erb__1031165391329753733_47227964624820'
            app/views/static_pages/home.html.erb:10:in `_app_views_static_pages_home_html_erb__2360347270319602419_47227964499260'
            app/controllers/microposts_controller.rb:9:in `create'
            test/integration/microposts_interface_test.rb:14:in `block (2 levels) in <class:MicropostsInterfaceTest>'
            test/integration/microposts_interface_test.rb:13:in `block in <class:MicropostsInterfaceTest>'

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

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

何か別のところでエラーを返すようになりましたね。

どのようなプロセスでエラーに至るのか見当がつかないので、debuggerメソッドを挿入して確認してみましょう。

...略
[5, 14] in /var/www/sample_app/app/controllers/microposts_controller.rb
    5:     @micropost = current_user.microposts.build(micropost_params)
    6:     if @micropost.save
    7:       #TODO: ここに有効なマイクロポストの投稿に対する処理を書く
    8:     else
    9:       debugger
=> 10:       render 'static_pages/home'
   11:     end
   12:   end
   13: 
   14:   def destroy
(byebug) 
[1, 7] in /var/www/sample_app/app/views/shared/_feed.html.erb
   1: <% debugger %>
=> 2: <% if @feed_items.any? %>
   3:   <ol class="microposts">
   4:     <%= render @feed_items %>
   5:   </ol>
   6:   <%= will_paginate @feed_items %>
   7: <% end %>
(byebug) @feed_items
nil

「マイクロポストの投稿が失敗し、static_pages/homeがレンダリングされる時点で@feed_itemsnilになってしまうこと」がエラーの原因のようですね。

マイクロポストの投稿が失敗した場合に、@feed_itemsに空の配列を渡すようにする

Railsチュートリアル本文には、上記動作について、以下のように記述されています。

ただしささいなことではありますが、マイクロポストの投稿が失敗すると、 Homeページは@feed_itemsインスタンス変数を期待しているため、現状では壊れてしまいます。最も簡単な解決方法は、リスト 13.50のように空の配列を渡しておくことです。

実際にそのように実装してみましょう。

app/controllers/microposts_controller.rb
  class MicropostsController < ApplicationController
    before_action :logged_in_user, only: [:create, :destroy]

    def create
      @micropost = current_user.microposts.build(micropost_params)
      if @micropost.save
        #TODO: ここに有効なマイクロポストの投稿に対する処理を書く
      else
+       @feed_items = []
        render 'static_pages/home'
      end
    end

    ...略
  end

マイクロポストの投稿が失敗した場合に、@feed_itemsに空の配列を渡すようにした時点でのテストの結果

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 2573
Started with run options --seed 310

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.6217254000075627]
 test_micropost_interface#MicropostsInterfaceTest (3.62s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>'

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

Finished in 3.62327s
1 tests, 3 assertions, 1 failures, 0 errors, 0 skips

error_explanationクラスを持つdiv要素が見つからないというメッセージを出力してテストが失敗していますね。どうやらstatic_pages/homeのレンダリングまではうまくいっているようです。今度は、「static_pages/homeerror_explanationクラスを持つdiv要素がレンダリングされるようにする」必要があります。

マイクロポスト投稿フォームの仮パーシャルを追加する

Railsチュートリアル本文中の記載に従えば、ログイン済みユーザーのマイクロポスト表示画面において「error_explanationクラスを持つdiv要素」がレンダリングされるのは、マイクロポスト投稿フォーム内となります。まずは、マイクロポスト投稿フォームとなる予定の仮パーシャルを作成し、そこに「error_explanationクラスを持つdiv要素」がレンダリングされるようにしましょう。

app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages' object: f.object %>
<% end %>

@micropostというインスタンス変数が出てきました。パーシャルapp/views/shared/_micropost_form.html.erbを使うのはHomeページなので、StaticPagesコントローラーのhomeアクションにも@micropostが必要になります。

StaticPagesコントローラーのhomeアクションに、インスタンス変数@micropostの定義を追加する

StaticPagesコントローラーのhomeアクションにおける@micropostの定義は以下になります。

@micropost = current_user.microposts.build if logged_in?

結果、StaticPagesコントローラーのhomeアクションの新たな定義は以下のようになります。

  class StaticPagesController < ApplicationController
    def home
-     @feed_items = current_user.feed.paginate(page: params[:page]) if logged_in?
+     if logged_in?
+       @micropost  = current_user.microposts.build
+       @feed_items = current_user.feed.paginate(page: params[:page])
+     end
    end

    def help
    end

    def about
    end

    def contact
    end
  end

logged_in?が真であることを条件とするメソッド呼び出しが2つになったので、if logged_in?を後置ifから前置ifに変更しています。

マイクロポスト投稿フォームの仮パーシャルをHomeページで使うようにする

今作成したマイクロポスト投稿フォームの仮パーシャルは、Homeページで使うものです。app/views/static_pages/home.html.erbの内容も、当該パーシャルを使用するように変更する必要があります。変更内容は以下の通りです。

app/views/static_pages/home.html.erb
  <% provide(:title, "Home") %>
  <% if logged_in? %>
    <div class="row">
      <aside class="col-md-4">
        <%#TODO: ユーザー情報表示部の実装 %>
-       <%#TODO: マイクロポスト投稿フォームの実装 %>
+       <section class="micropost_form">
+         <%= render 'shared/micropost_form' %>
+       </section>
      </aside>
      <div class="col-md-8">
        <h3>Micropost Feed</h3>
        <%= render 'shared/feed' %>
      </div>
  <% else %>
    <div class="center jumbotron">
      <h1>Welcome to the Sample App</h1>

      <h2>
        This is the home page for the
        <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
        sample application.
      </h2>

      <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
    </div>

    <%= link_to image_tag("rails.png", alt: "Rails logo"),
                'http://rubyonrails.org/' %>
  <% end %>

エラーメッセージのパーシャルを再定義する

新たに必要となる実装

app/views/shared/_micropost_form.html.erb(抜粋)
<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages' object: f.object %>
<% end %>

このコードは、エラーメッセージのパーシャルであるapp/views/shared/_error_messages.html.erbの内容が現状のままでは正常に動作しません。理由は以下です。

  • 現状のapp/views/shared/_error_messages.html.erbでは、内部で使うインスタンス変数が@user決め打ちである
  • 一方で、app/views/shared/_micropost_form.html.erbから渡されるインスタンス変数は@micropostである

app/views/shared/_error_messages.html.erb@user@micropostも受け取ることができるような実装に変えねばなりません。

app/views/shared/_error_messages.html.erbを、@user@micropostも受け取ることができるような実装に変更する

renderメソッドのオプションとして、以下のハッシュを与えているのがポイントです。

object: f.object

上記は「パーシャル中で使う変数名をキーとし、当該変数の内容となるオブジェクトを値とするハッシュ」です。例えば以上のハッシュをrenderメソッドのオプションとして与えた場合、レンダリングされるパーシャル(今回はerror_messagesパーシャル)にobjectという変数名でf.objectの内容を使うことができるようになります。

app/views/shared/_error_messages.html.erbの変更内容は以下の通りになります。

app/views/shared/_error_messages.html.erb
- <% if @user.errors.any? %>
+ <% if object.errors.any? %>
    <div id="error_explanation">
      <div class="alert alert-danger">
-       The form contains <%= pluralize(@user.errors.count, "error") %>.
+       The form contains <%= pluralize(object.errors.count, "error") %>.
      </div>
      <ul>
-     <% @user.errors.full_messages.each do |msg| %>
+     <% object.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

エラーメッセージのパーシャルを書き換えた時点でのテストの実行結果

# rails test
Running via Spring preloader in process 2599
Started with run options --seed 63753

ERROR["test_password_resets", PasswordResetsTest, 3.290402600017842]
 test_password_resets#PasswordResetsTest (3.29s)
ActionView::Template::Error:         ActionView::Template::Error: undefined local variable or method `object' for #<#<Class:0x000055e83a1e2530>:0x000055e83cbba6f0>
        Did you mean?  object_id
            app/views/shared/_error_messages.html.erb:1:in `_app_views_shared__error_messages_html_erb__812901978115774136_47227970213020'
            app/views/password_resets/edit.html.erb:7:in `block in _app_views_password_resets_edit_html_erb___393354210430426510_47227969853980'
            app/views/password_resets/edit.html.erb:6:in `_app_views_password_resets_edit_html_erb___393354210430426510_47227969853980'
            test/integration/password_resets_test.rb:39:in `block in <class:PasswordResetsTest>'

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 5.616371100011747]
 test_micropost_interface#MicropostsInterfaceTest (5.62s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:16:in `block in <class:MicropostsInterfaceTest>'

  59/59: [=================================] 100% Time: 00:00:08, Time: 00:00:08

Finished in 8.64566s
59 tests, 299 assertions, 1 failures, 1 errors, 0 skips

PasswordResetsTestで以下のエラーが発生するようになりました。

ActionView::Template::Error: undefined local variable or method `object'

また、スタックトレースには以下の記載が残されています。

app/views/password_resets/edit.html.erb:7
app/views/password_resets/edit.html.erb:6

エラーメッセージのパーシャルを書き換えたので、既存のソースコードも書き換える必要がある

エラーメッセージのパーシャル、とりわけ引数の渡し方を変更したため、エラーメッセージのパーシャルを使用している既存実装も書き換える必要が発生しました。具体的には、app/views/password_resets/edit.html.erb内の実装ですね。

app/views/password_resets/edit.html.erb
  <% provide(:title, 'Reset password') %>
  <h1>Reset password</h1>

  <div class="row">
    <div class="col-md-6 col-md-offset-3">
      <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
-       <%= render 'shared/error_messages' %>
+       <%= render 'shared/error_messages', object: f.object %>

        <%= hidden_field_tag :email, @user.email %>

        <%= f.label :password %>
        <%= f.password_field :password, class: 'form-control' %>

        <%= f.label :password_confirmation %>
        <%= f.password_field :password_confirmation, class: 'form-control' %>

        <%= f.submit "Update password", class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>

app/views/password_resets/edit.html.erbの実装を変更した時点でのテストの結果

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 2691
Started with run options --seed 19709

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 4.357053499988979]
 test_micropost_interface#MicropostsInterfaceTest (4.36s)
        Expected response to be a <3XX: redirect>, but was a <204: No Content>
        Response body: 
        test/integration/microposts_interface_test.rb:22:in `block in <class:MicropostsInterfaceTest>'

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

Finished in 4.36191s
1 tests, 5 assertions, 1 failures, 0 errors, 0 skips

test/integration/microposts_interface_test.rbの22行目で、「リダイレクトされるべきところがリダイレクトされていない」というメッセージを出してテストが失敗しています。

私の環境では、test/integration/microposts_interface_test.rbの17〜22行目のコードは以下のようになっています。

test/integration/microposts_interface_test.rb(22行目)
# 有効な送信
content = "This micropost really ties the room together"
assert_difference 'Micropost.count', 1 do
  post microposts_path, params: { micropost: { content: content } }
end
assert_redirected_to root_url

無効なマイクロポストの送信に対する処理の実装は完成できたと判断できます。今度は、「有効な内容のマイクロポストを送信した場合に対する処理」を実装する必要があります。

有効なマイクロポストのPOSTに対する処理

app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      #TODO: ここに有効なマイクロポストの投稿に対する処理を書く <- この部分の実装
    else
      @feed_items = []
      render 'static_pages/home'
    end
  end

  # ...略
end

有効なマイクロポストがPOSTされた場合、RDBのMicropostsテーブルへの保存が成功します。すなわち、@micropost.savetrueを返すということになります。

有効なマイクロポストのPOSTに対する処理の内容

Railsチュートリアル本文において、RDBのMicropostsテーブルへの保存が成功した場合の処理は以下のようになる旨が記載されています。

  1. マイクロポストの投稿が成功した旨をフラッシュメッセージで表示する
  2. Homeページ( / )にリダイレクトする

対応するコードは以下です。

flash[:success] = "Micropost created!"
redirect_to root_url

有効なマイクロポストのPOSTに対する処理の実装

では、app/controllers/microposts_controller.rbに対応するコードを追加していきましょう。

app/controllers/microposts_controller.rb
  class MicropostsController < ApplicationController
    before_action :logged_in_user, only: [:create, :destroy]

    def create
      @micropost = current_user.microposts.build(micropost_params)
      if @micropost.save
-       #TODO: ここに有効なマイクロポストの投稿に対する処理を書く
+       flash[:success] = "Micropost created!"
+       redirect_to root_url
      else
        @feed_items = []
        render 'static_pages/home'
      end
    end

    def destroy
    end

    private

      def micropost_params
        params.require(:micropost).permit(:content)
      end
  end

有効なマイクロポストのPOSTに対する処理を実装した時点でのテストの結果

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 36
Started with run options --seed 50895

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 5.493641928999978]
 test_micropost_interface#MicropostsInterfaceTest (5.49s)
        <delete> expected but was
        <sample app>..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:26:in `block in <class:MicropostsInterfaceTest>'

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

Finished in 5.49914s
1 tests, 8 assertions, 1 failures, 0 errors, 0 skips

私の環境では、test/integration/microposts_interface_test.rbの25〜26行目は以下のコードが記述されています。

test/integration/microposts_interface_test.rb(25〜26行目)
# 投稿を削除する
assert_select 'a', text: 'delete'

「マイクロポストの削除用リンクが描画されていない」という趣旨のメッセージですね。

有効なマイクロポストのPOSTに対する処理の実装までで成功するようになったテストの内容

なお、以下のテストについては、ここまでの実装で問題なく成功しています。

test/integration/microposts_interface_test.rb(17〜24行目)
# 有効な送信
content = "This micropost really ties the room together"
assert_difference 'Micropost.count', 1 do
  post microposts_path, params: { micropost: { content: content } }
end
assert_redirected_to root_url
follow_redirect!
assert_match content, response.body

「今投稿したばかりのマイクロポストの内容が、リダイレクト後のHomeページに描画されている」というテストも成功しています。

マイクロポストを削除する機能をMicropostsリソースに追加する

前提条件

「ユーザーは、自身が投稿したマイクロポストのみを削除することができる」という動作となることが前提です。

また、Railsチュートリアル本文には、「マイクロポストの削除リンクを含むHomeページのモックアップ」として、図 13.16が示されています。

違うユーザーのマイクロポストを削除しようとした際の動作に対するテストを追加する

違うユーザーのマイクロポストを削除しようとした場合、以下の動作となる必要があります。

  • マイクロポストは削除されず、RDB上のマイクロポストの数も変化しない
  • Homeページ( / )にリダイレクトされる

こちらはtest/controllers/microposts_controller_test.rbに追加していきます。テストの名前は「should redirect destroy for wrong micropost」とします。

test/controllers/microposts_controller_test.rb
  require 'test_helper'

  class MicropostsControllerTest < ActionDispatch::IntegrationTest

    def setup
      @micropost = microposts(:orange)
    end

    test "should redirect create when not logged in" do
      assert_no_difference 'Micropost.count' do
        post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
      end
      assert_redirected_to login_url
    end

    test "should redirect destroy when not logged in" do
      assert_no_difference 'Micropost.count' do
        delete micropost_path(@micropost)
      end
      assert_redirected_to login_url
    end

+   test "should redirect destroy for wrong micropost" do
+     log_in_as(users(:rhakurei))
+     micropost = microposts(:ants)
+     assert_no_difference 'Micropost.count' do
+       delete micropost_path(micropost)
+     end
+     assert_redirected_to root_url
+   end
  end

マイクロポストのパーシャルに削除リンクを追加する

動作のポイントは以下です。

  • マイクロポストを投稿したユーザー自身のみに削除リンクが表示される
  • 実際の削除の前に、「You sure?」という確認メッセージを出力する

上述動作を踏まえた上で、マイクロポストの削除リンクのコードは以下のようになります。

<% if current_user?(micropost.user) %>
  <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %>
<% end %>

マイクロポストの削除リンクのコードを、app/views/microposts/_micropost.html.erbに追加していきます。

app/views/microposts/_micropost.html.erb
  <li id="micropost-<%= micropost.id %>">
    <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
    <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
    <span class="content"><%= micropost.content %></span>
    <span class="timestamp">
      Posted <%= time_ago_in_words(micropost.created_at) %> ago.
+     <% if current_user?(micropost.user) %>
+       <%= link_to "delete", micropost, method: :delete, data: { confierm: "You sure?" } %>
+     <% end %>
    </span>
  </li>

マイクロポストのパーシャルに削除リンクを追加した時点でのテストの結果

マイクロポストのパーシャルに削除リンクを追加した時点での、test/integration/microposts_interface_test.rbを対象としたテストの結果は以下のようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 49
Started with run options --seed 16580

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 5.129021706999993]
 test_micropost_interface#MicropostsInterfaceTest (5.13s)
        "Micropost.count" didn't change by -1.
        Expected: 38
          Actual: 39
        test/integration/microposts_interface_test.rb:28:in `block in <class:MicropostsInterfaceTest>'

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

Finished in 5.13155s
1 tests, 9 assertions, 1 failures, 0 errors, 0 skips

私の環境では、test/integration/microposts_interface_test.rbの28〜30行目は以下のコードが記述されています。

test/integration/microposts_interface_test.rb(28〜30行目)
assert_difference 'Micropost.count', -1 do
  delete micropost_path(first_micropost)
end

"Micropost.count" didn't change by -1.というのは、「正しいdeleteリクエストが送られたのに、RDB上から対象マイクロポストが削除されていない」という旨のメッセージですね。今度は、「正しくマイクロポストの削除処理が行われるdestroyアクションの実体」を実装する必要がありますね。

マイクロポストの削除処理の実体を実装する

マイクロポストの削除処理は、対象となるマイクロポストを指定したHTTPのDELETEリクエストをトリガーとして行われます。そのため、マイクロポストの削除処理の実体というのは、具体的には「Micropostsコントローラーのdestroyアクション」ということになります。

マイクロポストの削除処理に必要となる実装

繰り返しになりますが、「ユーザーは、自身が投稿したマイクロポストのみを削除することができる」という動作となることが前提です。そのため、beforeフィルターの内容も、「削除対象のマイクロポストが、現在のユーザーと紐付いたものであるか」を検査するものである必要があります。

当該beforeフィルターの具体的な動作は以下のようになります。

  1. DELETEリクエストのクエリパラメータとして与えられたマイクロポストをキーとして、現在のユーザーの全マイクロポストにfind_byメソッドで検索を行う
  2. 検索結果に該当するマイクロポストがあった場合のみ、実際にマイクロポストの削除処理を行う

検索結果に該当するマイクロポストがなかった場合は、マイクロポストの削除処理を行わずにHomeページ( / )にリダイレクトします。

beforeフィルターの内容

上記実装のうち、beforeフィルターに関する部分のみを取り出すと以下のようになります。beforeフィルターに対応するメソッドの名前は、correct_userとします。

before_action :correct_user, only: [:destroy]

private

  def correct_user
    @micropost = current_user.microposts.find_by(id: params[:id])
    redirect_to root_url if @micropost.nil?
  end

Micropostsコントローラーのdestroyアクションの内容

一方、実際にRDBからマイクロポストを削除する処理を実装するのは、Micropostsコントローラーのdestroyアクション内となります。その動作は以下の通りです。

  1. RDBから当該マイクロポストを削除する
  2. マイクロポストが削除された旨をフラッシュメッセージで表示する
  3. リダイレクトする

対応するコードは以下のようになります。

Microposts#destroy
def destroy
  @micropost.destroy
  flash[:success] = "Micropost deleted"
  redirect_to request.referrer || root_url
end

request.referrerというメソッドの意味

先程、単に「リダイレクトする」と書きました。実は、このときのリダイレクト先としては、以下の2つのURLを設定しています。

  • DELETEリクエストが発行されたページ
  • DELETEリクエストが発行されたページが特定できなかった場合)Homeページ

request.referrerというのは、上記のコードの場合、「DELETEリクエストが発行されたページ」を指します。

マイクロポストを削除する機能をMicropostsリソースに追加した時点でのテストの結果

マイクロポストを削除する機能をMicropostsリソースに追加した時点での、test/integration/microposts_interface_test.rbを対象としたテストの結果は以下のようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 75
Started with run options --seed 25888

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

Finished in 2.92451s
1 tests, 10 assertions, 0 failures, 0 errors, 0 skips

続いてはtest/controllers/microposts_controller_test.rbを対象としたテストの結果です。

# rails test test/controllers/microposts_controller_test.rb
Running via Spring preloader in process 380
Started with run options --seed 1158

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

Finished in 1.18872s
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips

今度はテストスイート全体に対してテストを実施してみましょう。

# rails test
Running via Spring preloader in process 107
Started with run options --seed 2017

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

Finished in 10.45135s
59 tests, 316 assertions, 0 failures, 0 errors, 0 skips

テストスイート全体も無事テストが成功しています。

実はこの時点でHomeページの実装が足りない

…あれ、ちょっと待ってください。Homeページの実装が足りないのにテストが全部成功してしまいました。具体的には、以下の実装が足りていません!

  • サイドバーで表示されるべきユーザー情報のパーシャル
  • マイクロポストの投稿フォーム

というわけで、上記2つの要素に対しての実装が必要となります。こちらも、Railsチュートリアル本文の記述を参考としつつ、テスト駆動で実装していきましょう。以下別記事となります。

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

Railsチュートリアル 第13章 ユーザーのマイクロポスト - 【発展】ユーザー情報のパーシャルとマイクロポスト投稿フォームを、テスト駆動開発で実装する

内容

Railsチュートリアル第13章の本文中において、テストが書かれることなしに実装されていた以下2つの要素を、テスト駆動で実装してみようというものです。

  • ユーザー情報のパーシャル
  • マイクロポスト投稿フォーム

Railsチュートリアル本文においては、「マイクロポスト作成フォームのあるホーム画面のモックアップ」として、図 13.10が挙げられています。

ユーザー情報のパーシャルの実装

ユーザー情報のパーシャルに含まれているべき要素に対するテスト

Railsチュートリアル本文によれば、サイドバーで表示されるべきユーザー情報のパーシャルには、以下の要素が含まれている必要があります。

  • gravatar_forクラスを持つimg要素
  • ユーザー名を内容とするh1要素
  • マイクロポスト数を含むspan要素

テストを追加するのはtest/integration/microposts_interface_test.rbです。追加するテストの内容は以下となります。

test/integration/microposts_interface_test.rb
  require 'test_helper'

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

    test "micropost interface" do
      log_in_as(@user)
      get root_path
+     assert_select 'img.gravatar'
+     assert_select 'h1', @user.name
+     assert_select 'span', /#{@user.microposts.count}/
      assert_select 'div.pagination'
      ...略
    end
  end

「指定の文字列が含まれる指定の要素が存在するか」というテスト

例えば「@user.microposts.count変数の内容が記述されているspan要素が存在するか」というテストであれば、以下のように記載します。

assert_select 'span', /#{@user.microposts.count}/

正規表現オブジェクトを意味する//で、変数の内容を展開する#{}を囲っているのがポイントですね。

サイドバーで表示されるべきユーザー情報のパーシャルに対しての、現時点のテスト結果

現時点でのtest/integration/microposts_interface_test.rbに対するテストの結果は、以下のようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 120
Started with run options --seed 17602

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.629997299999559]
 test_micropost_interface#MicropostsInterfaceTest (3.63s)
        Expected at least 1 element matching "h1", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:12:in `block in <class:MicropostsInterfaceTest>'

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

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

h1要素が見当たらない」というメッセージを出してテストが失敗しています。

サイドバーで表示するユーザー情報のパーシャルを実装する

まず、サイドバーで表示するユーザー情報のパーシャルの実体を実装していきます。ファイル名はapp/views/shared/_user_info.html.erbとします。

app/views/shared/_user_info.html.erb
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user %></h1>
<span><%= link_to "view my profile", current_user %><span>
<span><%= pluralize(current_user.microposts.count, "micropost") %></span>

続いて、Homeページのビューでも当該パーシャルを使用するようにします。変更するファイルはapp/views/static_pages/home.html.erbですね。

app/views/static_pages/home.html.erb
  <% provide(:title, "Home") %>
  <% if logged_in? %>
    <div class="row">
      <aside class="col-md-4">
-       <%#TODO: ユーザー情報表示部の実装 %>
+       <section class="user_info">
+         <%= render 'shared/user_info' %>
+       </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>
  <% else %>
    ...略
  <% end %>

サイドバーで表示するユーザー情報のパーシャルを実装した時点でのテストの結果

ここまでの実装が完了した時点で、test/integration/microposts_interface_test.rbを対象としてテストを実行すると、結果は以下のようになります。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 224
Started with run options --seed 14923

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

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

テストが成功していますね。ひとまず、サイドバーで表示するユーザー情報のパーシャルは正しく追加できたようです。

マイクロポスト投稿フォームの完全な実装

完全なマイクロポストの投稿フォームに含まれているべき要素に対するテスト

Railsチュートリアル本文によれば、サイドバーで表示されるべきマイクロポスト投稿フォームのパーシャルには、以下の要素が含まれている必要があります。

  • /microposts に対してPOSTアクションを行うフォーム
  • マイクロポスト本文を入力するためのテキストフィールド
test/integration/microposts_interface_test.rb
  require 'test_helper'

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

    test "micropost interface" do
      log_in_as(@user)
      get root_path
      assert_select 'img.gravatar'
      assert_select 'h1', @user.name
      assert_select 'span', /#{@user.microposts.count}/
+     assert_select 'form[action="/microposts"]'
+     assert_select 'textarea'
      assert_select 'div.pagination'
      ...略
    end
  end

マイクロポスト投稿フォームに対するテストを実装した時点でのテストの結果

上記マイクロポスト投稿フォームに対するテストを実装した時点で、microposts_interface_test.rbに対してテストを実行してみましょう。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 276
Started with run options --seed 9808

 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.1817765000014333]
 test_micropost_interface#MicropostsInterfaceTest (3.18s)
        Expected at least 1 element matching "textarea", found 0..
        Expected 0 to be >= 1.
        test/integration/microposts_interface_test.rb:15:in `block in <class:MicropostsInterfaceTest>'

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

Finished in 3.19284s
1 tests, 5 assertions, 1 failures, 0 errors, 0 skips

textarea要素が存在しない」というメッセージを出力してテストが失敗しています。

マイクロポスト投稿フォームの完全な実装

Railsチュートリアル本文によれば、マイクロポスト投稿フォームの完全な実装は以下の通りになります。

<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>

対応するソースファイルはtest/integration/microposts_interface_test.rbです。

ここまでの実装で、エラーメッセージのパーシャルが出力できるようにするための実装は完了しています。新たに追加する必要があるのは以下の2つですね。

  • マイクロポスト本文を入力するためのテキストフィールド
  • 送信ボタン

早速実装を追加していきましょう。

app/views/shared/_micropost_form.html.erb
  <%= form_for(@micropost) do |f| %>
    <%= render 'shared/error_messages', object: f.object %>
+   <div class="field">
+     <%= f.text_area :content, placeholder: "Compose new micropost..." %>
+   </div>
+   <%= f.submit "Post", class: "btn btn-primary" %>
  <% end %>

マイクロポスト投稿フォームの完全な実装を終えた時点でのテストの結果

test/integration/microposts_interface_test.rbに対し、改めてテストを実行してみましょう。

# rails test test/integration/microposts_interface_test.rb
Running via Spring preloader in process 341
Started with run options --seed 28502

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

Finished in 4.07368s
1 tests, 15 assertions, 0 failures, 0 errors, 0 skips

今度こそテストが成功しました。

テストスイート全体に対するテストの結果はどうでしょうか。

# rails test
Running via Spring preloader in process 354
Started with run options --seed 14679

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

Finished in 11.81368s
59 tests, 321 assertions, 0 failures, 0 errors, 0 skips

テストスイート全体に対しても、無事テストが成功しましたね。

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

「Bundler could not find compatible versions for gem」なとき

概要

  • rails tutorial(2020/1/3現在)にあるとおりにgemfileを編集してbundle installを実行したら、うまくいかなかったときの話。

Bundler could not find compatible versions for gem "actionpack":
  In Gemfile:
    rails (~> 6.0.2, >= 6.0.2.1) was resolved to 6.0.2.1, which depends on
      actionpack (= 6.0.2.1)

    rails-controller-testing (= 1.0.2) was resolved to 1.0.2, which depends on
      actionpack (~> 5.x, >= 5.0.1)

Bundler could not find compatible versions for gem "listen":
  In Gemfile:
    listen (>= 3.0.5, < 3.2)

    guard (= 2.13.0) was resolved to 2.13.0, which depends on
      listen (>= 2.7, <= 4.0)

    spring-watcher-listen (~> 2.0.0) was resolved to 2.0.1, which depends on
      listen (>= 2.7, < 4.0)

解決方法&参考にした情報

  • 「インストールしたいバージョンの指定を、もっとゆるくしてあげればいいんだよ」
  • 例)
    • gem 'rspec-rails', '3.3.2' ではなく
    • gem 'rspec-rails', '~> 3.3' のようにする
  • https://github.com/bundler/bundler/issues/7034

参考までに私の修正した部分

修正前

・・・
  gem 'rails-controller-testing', '1.0.2'
  gem 'minitest',                 '5.13.0'
  gem 'minitest-reporters',       '1.1.14'
・・・

修正後

・・・
  gem 'rails-controller-testing', '~> 1.0.2'
  gem 'minitest',                 '~> 5.13.0'
  gem 'minitest-reporters',       '~> 1.1.14'
・・・
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WSLでRails6の環境構築

VSCode上で環境構築します。

WSLを有効にし、Ubuntuをインストール

こちらの記事通りに進めていけば簡単にできます。画像付きですごく分かりやすかったです。
UbuntuはLTS版を入れました。

VSCodeの設定

VSCodeの拡張機能Remote Developmentをインストール。これでWSLに接続して開発ができるようになる。
インストールしたら、VSCodeの一番左下に緑のアイコンがでるのでクリック。
Remote-WSL: New Windowを選択。これで設定終了。

Ubuntuのパッケージを最新化

VScodeのターミナルを開いて、2つのコマンドを実行。かなり時間かかりました。
sudoコマンドを使うときに、Ubuntuで設定したパスワードを聞かれるかも。

パッケージの最新化
$ sudo apt update
$ sudo apt upgrade -y

rbenvのインストール

rbenvは、複数のRubyのバージョンを管理し、プロジェクトごとにRubyのバージョンを指定して使える。バージョンを変更したい場合も簡単。
参考: rbenv

rbenvのインストールと設定
#rbenvをインストール
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv

#パス指定と初期化処理を.bashrcに追加
$ echo 'export PATH="$HOME"/.rbenv/bin:$PATH' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc

#パス指定と初期化処理の反映
$ source ~/.bashrc

#バージョンが表示されれば成功
$ rbenv -v

ruby-buildのインストール

ruby-buildはRubyのインストールを簡単にしてくれる。
参考: ruby-buildruby-buildのWiki

ruby-buildのインストール、その他必要パッケージ
# ruby-buildのインストール
$ mkdir -p "$(rbenv root)"/plugins ←これいるの?やらなくてもできた。
$ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

#Rubyのインストールに必要なパッケージ。ruby-buildのWiki参照。かなり時間かかる。
$ sudo apt-get install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm5 libgdbm-dev

Rubyのインストール

ようやくrbenvを使ってRubyのインストールをします。安定版をインストールする。
恐ろしいくらい時間がかかりました。
最新の安定版の確認: こちら

Rubyのインストール
#Rubyのインストール
$ rbenv install 2.7.0

#PC全体で使うRubyのバージョンを指定する
$ rbenv global 2.7.0

$ ruby -v

RubyGemsのアップデート

RubyGemsを最新版にアップデートする。

$ gem update --system

Railsのインストール

Railsをインストールします。
最新のバージョンの確認: こちら

$ gem install rails -v 6.0.2.1
$ rails -v

Node.jsのインストール

Rails6から標準のWebpackerのインストールに必要。

# npmのインストール
$ sudo apt install -y npm

# Node.jsのインストール
$ sudo npm install n -g
$ sudo n stable

$ node -v

yarnのインストール

Webpackerのインストールに必要。
最初この方法でやったがうまくできなかった。
参考: yarn

$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
$ sudo apt-get update && sudo apt-get install yarn
$ yarn -v

#エラー出た時のメモ
$ sudo apt remove cmdtest
$ sudo apt remove yarn
$ sudo apt autoremove

SQLite3のインストール

Rails標準のデータベースなのでインストール。

$ sudo apt-get install libsqlite3-dev

アプリの作成

$ rails new sample_app
$ cd sample_app
$ rails s

その他メモ

railsインストールするときノードキュメント

# gemでライブラリーをインストールした際にドキュメントを生成すると時間がかかる。
# そこで、ドキュメントを生成しない設定にする
$ cd
$ touch .gemrc
$ echo "install: --no-document" >> .gemrc
$ echo "update: --no-document" >> .gemrc

# rbenvのアップデート
# Rubyの最新バージョンを使いたいときは、rbenvをアップデートしてから導入
$ cd ~/.rbenv
$ git pull
$ cd plugins/ruby-build
$ git pull

# 新しいコマンドを要するgemをインストールしたとき使用
$ rbenv rehash

# 新しく作ったディレクトリで作業するときは、そこで使うRubyのバージョンを指定する
$ rbenv local バージョン

感想

ほとんどエラー無くできたが、各種パッケージのインストールが長すぎて疲れました笑
それにすごい動作が遅い

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

Ruby on Rails を学ぶ _No.1

view(ビュー)

ページや「見た目」を作るためのHTMLファイルです。
ブラウザとrailsの通信でrailsからビューが返され、ページが表示されます。

controller(コントローラー)

ページを表示するときにrailsの中ではコントローラを経由してビューをブラウザに返します。
具体的には、コントローラと同じ名前のビューフォルダから、あくしょんと同じ名前のHTMLファイルを探してブラウザに返します。

■ コントローラファイルの中身
ファイルの中にtopメソッドが追加されます。コントローラの中のメソッドはアクションと呼ばれます。
→ アクションと同じ名前のHTMLファイルをブラウザに返します。

html_controller.rb
class HomeController < ApplicationController
  def top #homeコントローラのtopアクションに対応するhtmlファイルを返す
  end
end

ルーティング

ブラウザとコントローラを繋ぐ役割があります。
送信されたURLに対してどのコントローラのどのアクションで対応するかを決める対応表のような物です。

■ ルーティングファイルの中身
ルーティングはconfig/routes.rb内に定義され以下のような文法で書かれます。

routes.rb
Rails.application.routes.draw do
  get "home/top" => "home#top"
  #get "URL名" => コントローラ名#アクション名
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む