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

【AWS EC2】自動デプロイ設定後に、修正ファイルを再デプロイする手順

①まずデスクトップアプリでgithubのmasterに編集ファイルをpushしてマージする。

②EC2でログインして該当ディレクトリまで移動

EC2にログイン

$ cd .ssh
$ ssh -i chat-space.pem ec2-user@[生成したElastic IP]

アプリまで移動

$ cd /var/www/app/

③②の位置でマスターをpull

$ git pull origin master

これで変更がEC2サーバー上にきたか確認

④念の為プロセスを切る

$ ps aux | grep unicorn
$ kill プロセス番号

⑤ローカルで自動デプロイする。

$ bundle exec cap production deploy

⑥デプロイしたIPで確認

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

rails6.0ではsend_dataでファイル名を明示的にエンコードしなくても文字化けしなくなってる

タイトルの通り、rails6.0以前ではsend_fileでファイルダウンロード機能を実装しているとIE, edgeの場合にファイル名が文字化けしてしまうので、明示的にエンコード処理をして対策を行う必要がありました。
rails6.0ではこの対応が不要になるコミットがされています。

rails6.0以前の場合

これだと、IE、edgeの場合にファイル名が文字化けしてしまいます。

data = "XXX"
filename = "サンプルファイル.txt"
send_data(data, filename: filename)

その為、以下の様に明示的にエンコードしてあげる必要がありました。

data = "XXX"
filename = "サンプルファイル.txt"
+ encorded_filename = ERB::Util.url_encode(filename)
- send_data(data, filename: filename)
+ send_data(data, filename: encorded_filename)

rails6.0では

詳細はPRに書いてありますが、上記のエンコード処理をrailsがしてくれる様になったので、
明示的にエンコード処理は不要になりました。
また、rails6.0以前のアプリの為にバックポートgemも用意されています。(PR内に書かれています。)

https://github.com/rails/rails/pull/33829

現在rails5系で動いていて、明示的にエンコードしているアプリでは
railsバージョンアップ後は不要な処理になるので忘れずに削除しましょう。

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

【Heroku】デプロイ時の"Missing encryption key to decrypt file with."を乗り越える

はじめてのHerokuデプロイ。
下記のQiita記事にならって進めていました。

【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】
(丁寧にまとめていただき、本当にありがとうございます。)

おかげさまで順調に進み、デプロイ間近でわくわくしていたところ以下のエラーと遭遇しました。

Missing encryption key to decrypt file with. Ask your team for your master key and write it to ~~~~~~/config/master.key or put it in the ENV['RAILS_MASTER_KEY'].

要するに、"復号するためのキーが見つからない"ということです。

なぜ?

と、思いましたが、たしかにそうですね。

GitHubと連携してデプロイを試みていましたが、リモートリポジトリにmaster.keyはありません。通常はgitignoreの監視下なのでGitHubにはあがっていないはずです。そのため、これを別途読み取らせる工程が必要です。

解決策

ということで、ちょっと迷いましたが一手で解決することができました。

heroku config:set RAILS_MASTER_KEY=`rake secret`

以上のコマンドを実行したあとで、ようやくデプロイが通るようになりました。

参考

stackoverflow:Ask your team for your master key and put it in ENV[“RAILS_MASTER_KEY”] on heroku deploy
上の質問と回答をもとに解決することができました。ありがとうございます。

さいごに

初心者らしく「なぜ?」からはじまり、その解決策もシンプルすぎるがゆえいろいろ戸惑ったので残しておきます。今後はじめてデプロイに臨まれる方の参考になれば幸いです。

万が一、誤りや解釈が十分でないところがあれば、ご指摘いただけると嬉しいです。

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

【Rails】Rails側で定義した変数をJavaScriptに簡単に渡せるgem 「gon」を使ってみた

はじめに

Railsアプリケーションを作成中、JavaScriptにRails側で定義した変数を渡したくなり、調べたところgonというかなり使い勝手のいいgemがあったので導入してみました。

この記事が役に立つ方

  • Rails側で定義した変数をJavaScript側でも使いたい方

この記事のメリット

  • gonを使ってRailsで定義した変数をJavascriptに渡せるようになる

環境

  • macOS Catalina 10.15.1
  • zsh: 5.7.1
  • Ruby: 2.6.5
  • Rails: 5.2.3
  • Docker: 19.03.5
  • docker-compose: 1.24.1
  • gon: 6.3.2

gemgonとは?

シンプルにRailsアプリ内でJavaScriptに変数を渡すことが出来るgemです。
RSpecにも変数を渡せたりと、便利。

スクリーンショット 2019-12-01 22.05.52.png
GitHub - gazay/gon: Your Rails variables in your JS

If you need to send some data to your js files and you don't want to do this with long way through views and parsing - use this force!

(ざっくり)JavaScriptに何かデータを送る必要があるなら、面倒くさいビューとかパースとかやめてこれを使っちゃいなよ!
と紹介されています。

インストール

Gemfile
gem 'gon'

bundle install

使用方法

Usage example · gazay/gon Wiki · GitHub

1. Viewで読み込み

app/views/layouts/application.html.erb
<head>
  <title>some title</title>
  <%= include_gon %>
  <!-- include your action js code -->

titleタグの下で、javascript_include_tagよりは上。

※公式Wikiであった、以下方法ではうまくいきませんでした。

app/views/layouts/application.html.erb
<head>
  <title>some title</title>
  <%= Gon::Base.render_data %> 
  <!-- include your action js code -->

2. Controllerで使っている変数をgonにセット。

any_controller.rb
@your_int = 123
@your_array = [1,2]
@your_hash = {'a' => 1, 'b' => 2}

# 上記の変数をJavaScriptで呼び出したいなら
# 以下のように頭に`gon`をつけて変数定義する
gon.your_int = @your_int
gon.your_array = @your_array
gon.your_hash = @your_hash

# `gon`をつけた後に別の変数定義に活用することも可能。
gon.your_other_int = 345 + gon.your_int

# `gon`をつけたもの同士で配列に追加も可能。
gon.your_array << gon.your_int
gon.your_array # > [1, 2, 123]

# `all_variables`で`gon`をつけた全ての変数がハッシュで取り出せる
gon.all_variables # > {:your_int => 123, :your_other_int => 468, :your_array => [1, 2, 123], :your_hash => {'a' => 1, 'b' => 2}}

# `clear`で全変数をクリアできる
gon.clear # gon.all_variables now is {}

gonをつけるだけでいいのでシンプルですね。


3. JavaScriptで呼び出し

any.js
alert(gon.your_int)
alert(gon.your_other_int)
alert(gon.your_array)
alert(gon.your_hash)

先程コントローラー側で定義したものがそのまま使えます。
aleatはテキトーです。

当然ですが、例えばcurrent_userを渡していた場合は、

current_user.js
alert(gon.current_user.name)
alert(gon.current_user.email)
alert(gon.current_user.id)

のようにすると名前やメールアドレスなど、欲しいキーを指定すれば値が取り出せます。

非常に分かりやすくて便利です。

おわりに

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

他にもinputタグ経由で変数を渡したり、JSONを使ったりして変数を渡している記事を見つけましたが、gonの方がシンプルに変数を渡せるので楽ですね:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

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

【初心者向け】「rails new」で生成されるフォルダ、ファイルをざっくり解説してみた。

はじめに

この記事はDMM WEBCAMP アドベントカレンダー2日目の記事です。

こんにちは。DMM WEBCAMPのメンターをしてます。このアドベントカレンダーのいいだしっぺです。

今回は、Railsを触る上で外せないrails newコマンドで生成されるフォルダ、ファイルについて、解説していきたいと思います。

また、Rails5と6のファイル構成の違いも軽く触れてあります。

対象者

  • Rails触りたての方、触ったことのない方。
  • Railsを使ってるけど、具体的にこのフォルダで何ができるのかイマイチな方。
  • 他言語のMVCフレームワークを触っていて、Railsを理解したい方。

環境

macOS Catalina 10.15.1
Rails: 6.0.1
Ruby: 2.6.5

そもそもrails newとは

rails newコマンドは、Railsを使う上で必要不可欠なファイルを一度に作成してくれるコマンドです。
むしろこれを打たないと始まんないじゃないかってぐらい大事なコマンドです。
実行したらたくさんのファイルを生成したあと、デフォルトのGemfileに記載されているgem 1 のinstallが始まります。(Gemfileについては後述)
また、Rails6だとその後webpacker 2 のインストール(rails webpacker:install)が実行されます。

生成されるフォルダ/ファイルの解説

ここからはrails newを叩いたら、生成されるフォルダ、ファイルの解説をしていきます。
主要なものだけざっくり解説するのでもっと細かい説明はRailsガイドを参照してください。

/app

MVC関連やjs、cssを格納するフォルダなど、Railsの中枢に関わるフォルダ。
rails5だとassetsの中に/javascriptsファイルが入っていたが、rails6だとappの中に入っている。

/assets

/images(画像)や/stylesheets(CSS)など、ページを装飾するものをまとめたフォルダ。

/controller

MVCのCの部分。人間で言うところの「脳」であるコントローラーをまとめたフォルダ。
rails g controller hogesのコマンドを叩くと、このフォルダにhoges_controller.rbが作成される。

/helper

helperとは、主にviewをシンプルにするために、ちょっとした処理をやりたいときに使うメソッド3
それらを書くファイルをまとめたフォルダ。
コントローラーが作られると同時にコントローラー名の名前のファイルが作成される。

/models

MVCのMの部分。Railsでは主にデータベースとのやりとりや制約などを記述するモデルをまとめたフォルダ。
使用しているデータベースのテーブル毎にモデルが用意される。
rails g model Hogeのコマンドを叩くと、このフォルダにHoge.rbが作成される。

/views

MVCのVの部分。ページの見た目を作るためのERBファイルをまとめたフォルダ。
コントローラーが作られると同時にコントローラー名の名前のフォルダが作成される。
rails g controller hoges index とコマンドを打てば該当のファイルにindex.html.erbが作成される。

/bin

サーバを起動したり、テストをしたり、アプリケーションを管理する様々なスクリプトファイルをまとめるフォルダ。4
各プロジェクトのRailsのバージョンが違っていた場合、bin/rails sと/binを指定してコマンド実行するとそのバージョンでのコマンドで実行されるのでおすすめ。

/config

RailsやルーティングやDBなど、Railsの様々な設定ファイルをまとめてあるフォルダ。
Rails6ではwebpack関連の設定ファイルもある。

database.yml

データベース設定ファイル。YAML5というデータ形式で書かれている。
開発、テスト、本番環境別のDBサーバやパスワード等を記述する。

routes.rb

取得したURLを適切なコントローラー内のアクションなどに割り当てるためのルーティングファイル。
Routing Errorの原因はだいたいここ。
rails routesで割り当てているルーティングの一覧が見れる。
resourceがとっても便利。(RESTfulなリソースにしてくれる。)6

/db

データベース関連の情報をまとめたフォルダ。
デフォルトだとseeds.rbしかない。

/migrate

モデル作成時やrails g migration hogehogeでマイグレーションファイルがこのファイルの直下に作成される。
rails db:migrateを実行すると生成したテーブルやカラムなどがマイグレーションファイルを参考にデータベースへ内容が保存される。

schema.rb

rails db:migrate実行後に生成される、実行結果(実際にデータベースに保存されているテーブル等)が反映されているファイル。
デフォルトだとここを弄ってもテーブルの内容はかわらない。

seeds.rb

既存のテーブルにデータを格納するために設定するファイル。
記述し、rails db:seedを実行するとデータベースにデータが格納される。7

/lib

自作のモジュール8を置く場所。
ここにモジュールを置いて、requireで呼び出す。9

/log

いわゆるログファイル。デフォルトでは中身はない。
logger.debugを使うことで、log/development.logにlogを出力できる。

/public

404.html500.htmlなどRailsを使用しない静的ページや画像を格納する場所。
デプロイ時とかでお世話になるかも。

/storage

Active Storage等を使用した際にlocalでデフォルトで投稿されたファイルが保存される場所。10
デフォルトでは中身はない。

/test

作ったアプリケーションが正しく動作するのかという確認するファイルをまとめたフォルダ。
テストを行うことで正しい動作を保証し、品質の高いアプリケーションを仕上げる事につながる。
コントローラーやモデルが作られると同時にコントローラー、モデル名の名前のファイルが作成される。
Minitestなどを使いテストをしていくのが主流。Rspecは/specが別途作られる。

/tmp

一時ファイルを保存するためのファイルをまとめたフォルダ。

/vendor

vendorは自分が開発しているものではないサードパーティのライブラリ(jsフレームワークやcssフレームワークなど)を格納する場所。
デフォルトでは中身はない。
ここにライブラリを置いて、requireで呼び出す。11

Gemfile

現在のアプリケーションで使うgemをまとめているファイル。
このファイルに追加したいgemを記入し、bundle installを実行するとgemがinstallされる。

Gemfile.lock

Gemfileを元に依存関係にあるgemのバージョンと取得先が記録される。
実際にインストールしたgemのリストという認識。
bundle installbundle updateで更新される。

おわりに

この記事書くだけでもかなり勉強になりました。アウトプットはやはりよい。
かなりざっくりしていて、全てのファイルは紹介しきれていませんが、ふんわりこんな内容なんだと掴んでいただければ幸いです。
間違っていたりしたら報告していただけると泣いて喜びます。
私の他にもDMM WEBCAMPのメンターや社員さんが記事を書かれているので、よければご覧になってください。

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

#Rails で 存在しない全てのパスへの GET / POST / PUT / PATCH / DELETE / OPTIONS リクエストで 404 NotFound を返すようにエラーハンドリングする

注意

結構怖い
非推奨

方法

routes.rb の 最下部 でこうだ
上の方に書くと優先マッチしてダークホールになってしまうかも

match '*path', to: 'errors#not_found', via: :all

一個ずつメソッドを書く場合はこう

  get '*path', to: 'errors#not_found'
  post '*path', to: 'errors#not_found'
  put '*path', to: 'errors#not_found'
  patch '*path', to: 'errors#not_found'
  delete '*path', to: 'errors#not_found'
  match '*path', to: 'errors#not_found', via: :options

rspec でのテスト

こんなんで

require 'rails_helper'

describe 'not found path', type: :request do
  describe 'get' do
    subject { get "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end

  describe 'post' do
    subject { post "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end

  describe 'put' do
    subject { put "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end

  describe 'patch' do
    subject { patch "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end

  describe 'delete' do
    subject { delete "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end

  context 'options' do
    subject { process :options, "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end
end

多分これで行けるはず

REf

Railsの ActionController::RoutingError は ApplicationController での rescue_from で捕まえられない - Qiita

Original by Github issue

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

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

#Rails error handling : routing not existence all path with request method GET / POST / PUT / PATCH / DELETE / OPTIONS and return 404 not found

Warning

do not use easy

How

match '*path', to: 'errors#not_found', via: :all

or describe each request types

  get '*path', to: 'errors#not_found'
  post '*path', to: 'errors#not_found'
  put '*path', to: 'errors#not_found'
  patch '*path', to: 'errors#not_found'
  delete '*path', to: 'errors#not_found'
  match '*path', to: 'errors#not_found', via: :options

rspec test example

require 'rails_helper'

describe 'not found path', type: :request do
  describe 'get' do
    subject { get "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end

  describe 'post' do
    subject { post "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end

  describe 'put' do
    subject { put "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end

  describe 'patch' do
    subject { patch "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end

  describe 'delete' do
    subject { delete "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end

  context 'options' do
    subject { process :options, "/path/to/not/existence/" }

    before { subject }

    it { expect(response.status).to eq 404 }
  end
end

REf

Railsの ActionController::RoutingError は ApplicationController での rescue_from で捕まえられない - Qiita

Original by Github issue

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

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

GraphQLのmutationでargumentにオブジェクトを渡す

graphql-ruby で Mutation を書いていて、 argument にスカラ型ではなくオブジェクトを渡す時に variables を併用する場合の書き方で詰まったのでメモを残しておく。

環境

  • Rails 6.0.0
  • graphql-ruby 1.9.14

目標

  • createUser という mutation の argument にオブジェクトを渡す
  • クライアント側のクエリでは variables を用いる

手順

MutationType にフィールドを追加する

app/graphql/types/mutation_type.rb
module Types
  class MutationType < Types::BaseObject
    field :createUser, mutation: Mutations::CreateUserMutation
  end
end
  • mutation_type.rb に createUser というフィールドを追加し、 Mutations::CreateUserMutation を紐付ける

CreateUserMutation を実装する

app/graphql/mutations/create_user_mutation.rb
module Mutations
  class CreateUserMutation < GraphQL::Schema::RelayClassicMutation
    graphql_name "CreateUser"

    field :user, Types::UserType, null: false

    argument :user, Types::Attributes::UserInput, required: true

    def resolve(user:)
      created_user = User.create(user.to_h.transform_keys {|key| key.to_s.underscore })

      { user: created_user }
    end
  end
end
  • field :user は、この mutation 実行後のレスポンスとして取得できるフィールド。
  • Types::UserType は普段 query で使用する BaseObject なので内容は割愛。
  • argument :user が本題。 user というリクエストパラメータを受け取ることを宣言。そのオブジェクトの方を Types::Attributes::UserInput というクラスで定義している(詳細は後述)。
  • 上記で宣言したリクエストパラメータを resolve メソッドのキーワード引数で受け取る。

ちなみに user の中身はハッシュではなく Types::Attributes::UserInput のインスタンスになっている。
そのため中のプロパティにアクセスするには下記の二通りある。

user.postal_code # インスタンスメソッドを通してアクセス
user[:postalCode] # ハッシュのキーを通してアクセス(この時、プロパティ名はキャメルケースにする)

単純に to_h するだけだとキーがキャメルケースのままなので、 to_h.transform_keys {|key| key.to_s.underscore } という長ったらしい変換処理が必要になる(他に良い方法は無いものだろうか?)

Types::Attributes::UserInput を実装する

app/graphql/types/attributes/user_input.rb
class Types::Attributes::UserInput < Types::BaseInputObject
  argument :name, String, required: true
  argument :gender, Integer, required: true
  argument :profile, String, required: true
  argument :postal_code, String, required: true
end
  • 先述した resolve メソッドで受け取るキーワード引数の型を下記のように定義する。型の書き方などは query_types 等と同様。
  • さらにオブジェクトをネストしたい時は Types::BaseInputObject を継承する別のクラスを指定するのかな?(試してない)

以上でサーバー側の実装は完了。

クライアント側から送信する query を書く

mutation registerUser(
  $user: UserInput!
) {
  createUser(input: { user: $user }) {
    user { id postalCode profile }
  }
}
  • 1行目の registerUser はこのクエリに付けた適当な名前なので変えても動く。
  • 2行目の UserInput!! がキモ。 Mutations::CreateUserMutationargumentrequired: true を指定した場合、この ! を付けないと Nullability mismatch on variable $user というエラーが出る(ここで1時間くらいハマった)。
  • 4行目の createUser が Types::MutationType で定義した mutation 名。
  • 5行目の user { id postalCode profile } が mutation 実行後のレスポンスボディに入れてほしいオブジェクトとフィールドの指定。

クエリを実行

query = <<-QUERY
  mutation registerUser(
    $user: UserInput!
  ) {
    createUser(input: { user: $user }) {
      user { id postalCode profile }
    }
  }
QUERY
variables = {
  karte: {
    name: "midwhite",
    gender: 1,
    profile: "I am a software engineer."
    postalCode: "000-0000"
  }
}
AppSchema.execute(query, variables: variables).to_h
  • variables に user オブジェクトとして渡したいパラメータをハッシュで記述する。

実際にはフロントから Apollo とか使って query や variables を送信するんだと思うけど、 query の形式さえ分かっていれば特に迷わないだろうと思うのでその部分は割愛。
ここまで分かれば mutation を実用レベルで書けそう。そろそろエンドポイントを GraphQL のみで実装したアプリケーションを書いてみたい気持ち。

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

ぼくのかんがえたさいきょうのAPIドキュメント運用

ドキュメントちゃんと保守できてますか?

API開発とドキュメントの保守は切っても切れない問題です。

仕様の記述はもちろんのこと、サンプルを試せるAPIクライアントや、仕様に則った実装になっているかテストも自動化したいですよね。

本記事では、現在開発中のAPIアプリケーションで、実際に僕が試行錯誤していく中でたどり着いたベストプラクティスを紹介しようと思います。

アーキテクチャ

  • iOSアプリのバックエンドとしてJSONを返すAPIサーバー
  • Rails6 × MySQL5.7 on Docker

いつもの というお買い物アプリです。

 2019-12-01 13.34.00.png

ドキュメント何で書いてますか?

  • Excel => つらい
  • Markdown => つらい
  • 何らかのDSLを用いて生成するツール => :innocent:

素でマークダウンを書くのはつらみが深いので何かしらツールを使いましょう。
apiary, api blueprint, APIDOC, etc. 
いろいろありますが、僕が激しくおすすめするのは OpenAPI です。

理由としては、

  • Linux Foundation、Google、IBM、Microsoftなどが協力して仕様の策定に関わっていること
  • 歴史が長く周辺ツールが豊富で、拡張性があること
  • 表現力豊かな記述方法

が挙げられます。

Swagger? OpenAPI?

もともと Swagger という名前だったものが、 OpenAPIと名前を変えてバージョン3.0がリリースされました。
Swaggerと聞けば馴染みのある方も多いと思います。
基本的にSwaggerとOpenAPIを読み替えても問題はないのですが、(ドメインとか残ってるし => https://swagger.io/specification/)
Swaggerは2.xまでで、OpenAPIは3.0からになるので、Swaggerのバージョン3というものは厳密には存在しません。

OpenAPIが優れている理由

使ったことがない人向けに説明をしておくと、
OpenAPI とは、RESTful API を記述するためのフォーマットの標準であり、
その標準を用いて様々なことを解決するためのヘルパーツール群を提供するものです。

ツールは大きく3種類に分けられます。

  • Swagger Codegen
    • スタブサーバーとクライアントSDKの生成
  • Swagger Editor
    • 定義ファイルの編集が行えるリッチエディタ
    • シンタックスのチェックや補完、ホットリロードでのプレビューをサポート
  • Swagger UI
    • HTMLとしてドキュメントをビジュアライズする
    • 定義されたホストに対してリクエストを送信するAPIクライアントとしても使用できる(Postman的な)

この3つのオープンソースツールを拡張する形で、各言語ごとのラッパーやフレームワークへの組み込みをサポートするライブラリが数多く作成されています。(ex. swagger-blocks

自分が実現したいことに合わせて柔軟にモジュールを組み込むことができるので、どのプロジェクトにも導入がしやすいです。

また、先に述べたように、OpenAPI とはただ単にフォーマットを標準化した仕様なので、
ツールを通さなくてもその仕様に則って記述した yaml ファイルをただ共有するだけみたいな使い方もできます。
yamlさえあれば、受け取った人は何かのツールを使って自由に拡張利用できるので、非常にポータビリティが高いと言えます。Dockerファイルで環境のやりとりするみたいなイメージに近い :thinking:

そして錚々たる大企業たちがスポンサーとなっているため、業界の標準となっていくことは確実です。
OpenAPIの記述に慣れておくことは、エンジニアとして必要なスキルになってくるかと思っています。

実際に使ってみよう

今回実現したいことはこちらです。

  • ドキュメントをブラウザで手軽に確認したい
  • ドキュメントを楽に記述したい
  • ドキュメントをローカルサーバーのHTTPクライアントとして使いたい
  • レスポンスがドキュメントに則っているか自動テストしたい

これ全てOpenAPIでできます。

ただ実際にやろうとするとツールが豊富で選択肢が多い割に、3.0に対応していないものが多かったり、情報がまとまっていなかったり、ベストプラクティスにたどり着くまでに苦労したので、この記事が何かの助けになれば幸いです。

ちなみにですが、2.xと3.xでは破壊的な変更があるので、2系のツールで3系を動かすのは無理があります。
多くのツールで3に対応するissueが上がっているのですが、長い間放置されているものが多いため、3を使おうとすると選択肢は結構狭まります。

ドキュメントをブラウザで手軽に確認したい

まずはサンプルとなるエンドポイントを実装します。

config/routes.rb
Rails.application.routes.draw do
  resources :users
end
app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    user = {
      :name => "sakuraya",
      :age => 26
    }
    render :json => user
  end
end
$ curl localhost:3000/users
{"name":"sakuraya","age":26}

これを OpenAPI のドキュメントとして記述します。
yaml と json がサポートされていますが、特に理由がなければ yaml を使うことをお勧めします。

ファイル名は何でもいいんですが、 openapi.yml がスタンダードです。

doc/openapi.yml
openapi: 3.0.2
info:
  title: "ぼくのかんがえたさいきょうのAPIドキュメント運用"
  description: "サンプルアプリ"
  version: "1.0.0"
tags:
  - name: "users"
    description: "ユーザーAPI"
paths:
  /users:
    get:
      summary: "ユーザーを取得"
      description: "ユーザーを取得"
      tags:
        - "users"
      responses:
        200:
          description: "成功時"
          content:
            application/json:
              schema:
                type: "object"
                properties:
                  name:
                    description: "名前"
                    type: "string"
                    example: "sakuraya"
                  age:
                    description: "年齢"
                    type: "integer"
                    example: 26
                required:
                  - "name"
                  - "age"

最低限これだけ書けばOKです。

これをブラウザで見るためには、SwaggerUIというツールを使います。
SwaggerUIで調べると Node.js を使ってサーバーを立ち上げる例ばかりが出てきますが、
別で立てるのは面倒なので docker-compose で一緒に立ち上げてしまいます。
イメージが公開されているのでこれをベースにします。

docker-compose.yml
version: '3'

services:
  web: &app_base
    build:
      context: .
    ports:
      - 3000:3000
    command: bundle exec rails s -p 3000 -b 0.0.0.0
    volumes:
      - .:/myapp
      - bundle:/usr/local/bundle
    depends_on:
      - db
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password
      TZ: Asia/Tokyo
    command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
    volumes:
      - ./vendor/docker/db/data:/var/lib/mysql
      - ./vendor/docker/db/conf.d:/etc/mysql/conf.d
      - ./vendor/docker/db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    ports:
      - 3306:3306
  doc:
    image: swaggerapi/swagger-ui
    volumes:
      - ./doc/openapi.yml:/usr/share/nginx/html/openapi.yml
    environment:
      API_URL: openapi.yml
    ports:
      - 8080:8080
volumes:
  bundle:

doc の部分が SwaggerUI です。
doc 配下に置いたドキュメントファイルをマウントして、ファイル名を環境変数で指定することで、ドキュメントサーバーが立ち上がるようになっています。
これで http://localhost:8080 にアクセスすると、インタラクティブなUIでドキュメントが表示されます。

 2019-12-01 15.09.43.png

▼それぞれ対応するセクションがこうなっている。

2019-12-01_15_09_43.png

▼パスをクリックするとアコーディオンが開いて仕様が表示される。

 2019-12-01 15.09.53.png

▼上の画像は Example Value を表示したもので、 Schema をクリックすると、プロパティの説明、型、requiredの有無、nullableの有無など詳細が表示される。

 2019-12-01 15.10.01.png

ドキュメントサーバーとAPIサーバーの立ち上げをいっぺんに管理できるのが便利 :innocent:

ドキュメントを楽に記述したい

エディタごとにプラグインがそれぞれあるかと思います。
僕は普段 VS Code を使っているのでこれを入れています。

OpenAPI(Swagger)Editor

サイドバーから特定の場所にジャンプできる機能が重宝します。
OpenAPI Explorer.png

あとはプレビュー用として Swagger Viewer を入れておいてもいいかと思います。

 2019-12-01 15.38.55.png

ホットリロードで確認しながらできるので便利です。
一つだけ注意点があって、たまに表示がおかしくなったり、正しく表示されないことがあります($ref が展開されないなど)。
なので僕は書き方に慣れるまではこれを使っていましたが、いまはブラウザでまとめてチェックしています。

あとは書き方のTipsですが、 components を使って共通化していくと見通しが良くなります。

doc/openapi.uml
paths:
  /users:
    get:
      summary: "ユーザーを取得"
      description: "ユーザーを取得"
      tags:
        - "users"
      responses:
        200:
          description: "成功時"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
components:
  schemas:
    User:
      description: "ユーザー"
      type: "object"
      properties:
        name:
          description: "名前"
          type: "string"
          example: "sakuraya"
        age:
          description: "年齢"
          type: "integer"
          example: 26
      required:
        - "name"
        - "age"

 2019-12-01 15.44.28.png

ドキュメントをローカルサーバーのHTTPクライアントとして使いたい

せっかく docker-compose してるんだから、ローカルのサーバーに実際につないで動かしたいですよね?
API開発時のHTTPクライアントにはずっと Postman や、
最近だと REST Client なんかを使っていましたが、ドキュメントの変更に対する反映が面倒だったりするのが難点です。
OpenAPIを組み込めればそんな手間もなくなります。

SwaggerUI には Try it out というボタンがついています。
2019-12-01_15_09_53.png

このままでは利用できないので設定を行います。
servers というセクションを追記してください。

doc/openapi.yml
servers:
  - url: "http://localhost:3000"
    description: "local api server"

これがボタンを押した時のリクエスト先のベースURLとなります。

▼トップに servers の設定が反映されました。複数設定することができ、切り替えられるようになっています。

 2019-12-01 15.57.28.png

▼ボタンを押すとパラメータが編集できるようになり、 Execute ボタンが現れます。
(今回はパラメータないが適当に書くとこんな感じ。よしなにクエリパラメータに突っ込んでくれる。)

 2019-12-01 16.01.41.png

もう一つ設定が必要で、このままリクエストを送ってもこのようなエラーが出ます。

Access to fetch at 'http://localhost:3000/users?some_condition=true' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.Access to fetch at 'http://localhost:3000/users?some_condition=true' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Cross Origin の制約を回避するために、Rails側に設定が必要です。
rack-cors という gem を使います。
ドキュメントサーバーを使うのは開発環境だけなので、 development.rb に記述します。

config/environments/development.rb
config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "localhost:8080"
    resource "*", :headers => :any, :methods => :any
  end
end

これでサーバーを再起動すれば、リクエストできるようになります。

▼生成されたcurlコマンドと、レスポンスが表示されました。
 2019-12-01 16.58.41.png

いちいち Postman 開いてリクエスト書く必要がないので幸せ:innocent:

レスポンスがドキュメントに沿っているか自動テストしたい

これだけでもだいぶ開発が捗るようになったんですが、テストまでいきたいです。
ドキュメントで Integer になってるプロパティが String で返ってきたり、
required なプロパティがレスポンスになかったら落ちるようにしたいですよね。

rspecに組み込んでみます。

Gemfile
gem 'rspec-rails'
docker-compose run web bundle
docker-compose run web rails generate rspec:install
spec/requests/users_spec.rb
require "rails_helper"

RSpec.describe UsersController, :type => :request do
  describe "#index" do
    let(:path) { users_path }  
    it  do
      get path
      expect(JSON.parse(response.body)).to match(
        # いい感じにドキュメントのスキーマを検証したい
      )
    end
  end 
end

committee という gem を使います。
さらにそれを Rails 用にラップした committee-rails も使います。

Gemfile
gem 'committee'
gem 'committee-rails'

設定を追加。

spec/rails_helper.rb
config.add_setting :committee_options
config.committee_options = { :schema_path => Rails.root.join("doc", "openapi.yml").to_s }
include ::Committee::Rails::Test::Methods

テストを落とすために、 Integer であるはずの年齢を String に書き換えます。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    user = {
      :name => "sakuraya",
      :age => "26"
    }
    render :json => user
  end
end

テストをこう書きます。

spec/requests/users_spec.rb
require "rails_helper"

RSpec.describe UsersController, :type => :request do
  describe "#index" do
    let(:path) { users_path }  
    it do
      get path
      assert_request_schema_confirm
      assert_response_schema_confirm
    end
  end 
end

実行すると。。。

F

Failures:

  1) UsersController#index
     Failure/Error: assert_response_schema_confirm

     Committee::InvalidResponse:
       #/components/schemas/User/properties/age expected integer, but received String: 26
     # /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3/operation_wrapper.rb:35:in `rescue in validate_response_params'
     # /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3/operation_wrapper.rb:30:in `validate_response_params'
     # /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3/response_validator.rb:20:in `call'
     # /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3.rb:38:in `response_validate'
     # /usr/local/bundle/gems/committee-3.3.0/lib/committee/test/methods.rb:27:in `assert_response_schema_confirm'
     # ./spec/requests/users_spec.rb:9:in `block (3 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # OpenAPIParser::ValidateError:
     #   #/components/schemas/User/properties/age expected integer, but received String: 26
     #   /usr/local/bundle/gems/openapi_parser-0.6.1/lib/openapi_parser/schema_validator.rb:62:in `validate_data'

Finished in 0.11266 seconds (files took 3.96 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/requests/users_spec.rb:6 # UsersController#index

無事落ちました!

▼requiredな値がないときはこうなったりします。

 1) UsersController#index
     Failure/Error: assert_response_schema_confirm

     Committee::InvalidResponse:
       #/components/schemas/User missing required parameters: age

テストまでかけるとか最高か:innocent:

普段の開発フローとしては、まずドキュメントを記述して仕様をレビュー => テスト書く => 実装というドキュメント&テスト駆動開発でやっています。

まとめ

今回は既存のプロジェクトに後から組み込んだので使ってませんが、
ドキュメントからモックサーバーを立ち上げたり、コードを生成したりするツールも言語ごとにいろいろあります。
うまく活用してAPIドキュメント運用のつらみから解放されましょう:innocent:

OpenAPI の記述方法については、OpenAPI-Specification を見ながら覚えていくのがオススメです。

[追記]
今回使ったサンプルコード

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

Rails+heroku+LINE Messager APIで秘書的なLINEbotを作ってみた(ゴミ出しの通知編)

IMG_7844.PNG

作るもの

毎日決まった時間になるとゴミ出しについて通知してくれる簡単なLINEbot。
ゆくゆくは天気/交通情報/イベントなども共有してくれるbotにしたいと思っています。
アイアンマンに出てくる人工知能ジャーヴィスをお節介おばさんにしたイメージです。

dims.jpg

使用した言語/フレームワーク/ライブラリ/サービス

-Ruby
-Ruby On Rails6
-Heroku
-LINE Messager API

大まかな手順

1.メッセージをオウム返しするLINEbotを作る
2.LINEにメッセージをpushしてみる
3.Herokuにデプロイして定期実行する

STEP1 メッセージをオウム返しするLINEbotを作る

LINE MessagerAPI

作成したbotのメッセージ周りのやり取りを担当してくれるのがLINEMessage APIです。
まず、自分のメッセージの内容をそのまま返してくれるbotを作ります。
LINE Messager APIの概要はDeveloperページを確認。
LINE Developers

APIの準備

APIを使用する前にやるべきことがいくつかあります。ざっくりとこんな感じ。
1. ビジネスアカウントの登録
2. プロバイダーの作成
3. チャンネルの作成
4. グループ・複数人チャットへの参加を許可するにチェック
5. Messaging APIの「Channel access token」と「channel Secret」を控える

LINEのBot開発 超入門(前編) ゼロから応答ができるまで」 登録周りを丁寧に説明しているので、わからなくなったらこっちを参考にしてください。

Rails側の実装

ここからは、「今更ながらRails5+line-bot-sdk-ruby+HerokuでLineBot作成してみたら、色々詰まったのでまとめました。」を参考に実装します。

まずRailsのプロジェクトを作ります。herokuがデフォルトでpostgresqlなので、合わせます。

$Rails new mother_bot --database=postgresql
$rails db:create

Line APIを使う為のgemをインストールします。

Gemfile.rb
#LINE API用
gem 'line-bot-api'

#access tokenなどを管理する用
gem 'dotenv-rails' 
$bundle install

LINE API登録時に控えた情報をアプリのルート直下に.envファイルを作成して保存

#.env
LINE_CHANNEL_SECRET=XXXXXX
LINE_CHANNEL_TOKEN=XXXXXX

コントローラーの準備をしてきます。

$rails g controller Linebot
linbot_controller.rb
class LinebotController < ApplicationController
  require 'line/bot'  # gem 'line-bot-api'

  # callbackアクションのCSRFトークン認証を無効
  protect_from_forgery :except => [:callback]

  def client
    @client ||= Line::Bot::Client.new { |config|
      config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
      config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
    }
  end

  def callback
    body = request.body.read

    signature = request.env['HTTP_X_LINE_SIGNATURE']
    unless client.validate_signature(body, signature)
      head :bad_request
    end

    events = client.parse_events_from(body)

    events.each { |event|
      case event
      when Line::Bot::Event::Message
        case event.type
        when Line::Bot::Event::MessageType::Text
          message = {
            type: 'text',
            text: event.message['text']
          }
        end
      end
    }

    head :ok
  end
end
config/route.rb
Rails.application.routes.draw do
  post '/callback' => 'linebot#callback'
end

ローカルでデバッグしてみる

herokuにpushしておしまい!のような記事が多いので、ローカルのデバッグで苦労しました。
ローカルからAPIを操作するためにはまずngrokを使う必要があります。参考記事を読みながら、デバッグ環境を構築してください。

公式サイト
https://ngrok.com/

参考記事(ローカルで動かす セクションからが重要)
LINE Botをローカル環境で動かしたりデバッグしたりする方法

TIPS

  • ngrokuを導入する際は、公式サイトからemailを登録してインストラクションに剃ってダウンロード、コマンドを叩いてくと正常に動くようになります。
  • LINE DevelopersからWebhook URLを設定を正しくするというのが、一番のミソだと思います。
  • ngrokuは無料プランだと起動するたびにエンドポイントが変わるので、Webhook URLを毎回変えましょう。

STEP1終了
これでうまく実装できていれば、自分のLINEから作成したアカウントを友達登録して、メッセージを投げると同じ内容のメッセージを投げ返してくれます。友達登録は、LINE DeveloperのチャンネルページのQRコードを使うと簡単です。

IMG_7845.PNG

初期メッセージはLINE Developersから編集できます。

STEP2 LINEにメッセージpushする

STEP1では、自分のメッセージをトリガーに、LINE Message APIを操作しました。
今回は、チャンネルがこちらのアクションなしで、テキストを送信できるよう実装していきます。

ユーザーモデルの追加

messageをAPIから一方的に送りつけるためには、rails側で送り先のuserIDを知っている必要があります。
送るユーザーが決まっていればIDをメモして、環境変数に設定してもいいと思いますが、今回はお友達登録でユーザーIDを保存できるように、実装します。

$rails g model user uid:string
$rails db:migrate

controllerに追記

linebot_controller.rb
        #省略 
        #29行目ぐらい
        when Line::Bot::Event::MessageType::Text
          message = {
            type: 'text',
            text: event.message['text']
          }
          client.reply_message(event['replyToken'], message)
        when Line::Bot::Event::MessageType::Follow #友達登録イベント
          userId = event['source']['userId'] 
          User.find_or_create_by(uid: userId)
        when Line::Bot::Event::MessageType::Unfollow #友達削除イベント
          userId = event['source']['userId']  
          user = User.find_by(uid: userId)
          user.destroy if user.present?
        end

タスクを追加

決まった時間になったら、メッセージを送りつける為のタスクを作成します。

lib/tasks/push.rake
namespace :push_line do 
    desc "LINEBOT:ゴミ出しの通知" 
    task push_line_message_trash: :environment do
        trash_day = TrashDay.new
        message = {
            type: 'text',
            text: trash_day.text
        }
        client = Line::Bot::Client.new { |config|
            config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
            config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
        }
        User.all.each do |user|
            client.push_message(user.uid, message)
        end
    end
end

曜日によってメッセージが違うので、メソッドを切り出しました。必要ない人は無視で!

trash.rb
class TrashDay
    def text
      date = Date.today

      case date.strftime('%a')
        when "Mon"
            "ちょっとあんた!今日は月曜日、普通ゴミの日だわ!" + metal_text
        when "Tue"
            "ちょっとあんた!今日は火曜日、空き缶・ペットボトル・空き瓶・使用済み乾電池の日だわ!"
        when "Wed"
            "ちょっとあんた!今日は水曜日、プラスチック製容器包装の日だわ!"
        when "Thu"
            "ちょっとあんた!今日は木曜日、普通ゴミの日だわ!"
        when "Fri"
            "HEY BUDDY! It's Friday. Party hard. Don't drink too much.(ゴミ無しの日)"
        when "Sat"
            "ちょっとあんた!今日は土曜日、ミックスペーパーの日だわ!"
        else
            ""
        end
    end

    def metal_text
        date = Date.today
        week = DateHelper.new.week_of_month_for_date(date)
        day = date.strftime('%a')
        if week == 2 || week == 4
            "2・4回目の月曜日だから小物金属も忘れちゃダメよ!"
        else
            ""
        end
    end
end

隔週で回すタスク用のメソッドです。

date_helper.rb
class DateHelper
    def week_of_month_for_date(date)
        my_date = Time.zone.parse(date.to_s)
        week_of_target_date = my_date.strftime("%U").to_i
        week_of_beginning_of_month = my_date.beginning_of_month.strftime("%U").to_i
        week_of_target_date - week_of_beginning_of_month + 1
    end
end

タスクが登録されたか確認

$rails -T

実行してみる

rails push_line:push_line_message_trash

メッセージが届いたら成功です!
STEP2終了

STEP3 Herokuにデプロイして定期実行する

これで9割完成です。あとはherokuにアップして、定期実行の設定をしてみましょう。

まずherokuのアカウントとアプリを作成します。
この辺は、記事を参考に進めてください。

HerokuにRailsアプリをデプロイする手順
https://qiita.com/NaokiIshimura/items/eee473675d624a17310f

一通りherokuデプロイの手順が終わったら、LINEAPIの環境変数を設定していきます。

$heroku config:set LINE_CHANNEL_SECRET=xxxx
$heroku config:set LINE_CHANNEL_TOKEN=xxx

#herokuの時間を日本時間に合わせます
$heroku config:add TZ=Asia/Tokyo

herokuのURLをAPIのWebhookURLに設定するのを忘れないでください。
スクリーンショット 2019-12-01 16.16.48.png

定期実行の設定

通常であれば、wheneverみたいなgemを使ってcronを操作すると思いますが、今回はherokuでのデプロイなので、heroku schedulerというaddonを使って実装します。無料です。

参考記事
Herokuでスケジューラ(cron)を設定する方法【Heroku Scheduler】
https://reasonable-code.com/heroku-cron/

#addonの追加
$heroku addons:create scheduler:standard --app アプリ名

#heroku上でタスクが動くか確認
$heroku run bundle exec rails push_line:push_line_message_trash --app アプリ名

実際に定期実行してみる

#GUI上でスケジュールを設定
$heroku addons:open scheduler

#実行されてるか確認
$heroku logs --app アプリ名

以上です。これで決まった時間にメッセージが送られてきたら成功です!
お疲れ様でした。

感想

簡単なLINEbotでしたが、とても楽しかったです。機会があればgoogle calendar APIなどと連携してbotの秘書力を高めて行きたいです。

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

WebpackerなしでNode.jsベースの最小のJavaScriptビルド環境を設定する「minimum_javascript_on_rails」

はじめに

事の始まりはかれこれ1年程運用している個人プロダクトにWebpackerを導入しようとしたことです。
Webpackerを使うという選択は正直「なんとなく」でした。しかしWebpackerに関する様々な記事を読んでいたところ、pixivさんの今日から簡単!Webpacker 完全脱出ガイドやMisocaさんのWebpackerを導入してから外すまでをふりかえるをはじめとした脱Webpacker系記事がいくつか見受けられたことや、「なんとなく」Webpackerを使うよりは自分で設定した方がはるかに勉強になると思い、結果的にWebpackerの使用はやめました。

本記事では、minimum_javascript_on_railsを参考に最小のビルド環境を設定する手順を説明していきたいと思います!

minimum_javascript_on_railsとは

WebpackerなしでNode.jsベースの最小のJavaScriptビルド環境を設定した、Ruby on Railsアプリケーションのサンプルである

公式リポジトリ

minimum_javascript_on_rails

こんなアプリケーションにおすすめ

  • あまり多くのJavaScriptのコードを含まない
  • スタイルシートのビルド環境はsprockets-railsで十分
  • 使わないnpmパッケージはインストールしない
  • 必要なパッケージを即座に更新できるようにするために、npmパッケージは可能な限り個別のものとして管理する

既存アプリケーションへの設定

minimum_javascript_on_railsではプルリクエスト形式で設定のカスタマイズ例を示してくれているので、細かい実装の仕方を知りたい方や、順を追って設定していきたい方はリポジトリからPRを追うのが一番早いです:dash:

環境

  • Ruby -> 2.6.3
  • Ruby on Rails -> 6.0.1
  • Node.js -> 12.6.0

流れ

  1. npm run buildが動くようにする
  2. npm run watchが動くようにする
  3. クロスブラウザで新しいECMAScript機能を有効にする
  4. JSファイルの移行
  5. その他設定

1. npm run buildが動くようにする

まずはGitの管理対象から外したいファイルを.gitignoreに追加します

.gitignore
/client/tmp/
/node_modules/
/public/client/
npm-debug.log

次に、以下コマンドでjsファイルをビルドするためのnpmパッケージを準備します

npm install -D webpack webpack-cli @babel/core @babel/preset-env babel-loader assets-webpack-plugin

次に、npm run buildを実行するとpublic/client/webpacked-{hash}.jsにjsファイルが生成され、client/src/index.js(エントリーポイント)に関連するすべてのソースを再帰的にバンドル&コンパイルされるように設定していきます。
また、assets-webpack-plugin(アセットパスを含むJSONファイルを生成するプラグイン)と、毎ビルド前に古いファイルを消去する設定(webpacked-{hash}.jsは互いに異なるハッシュを持っているため、古いファイルを削除しないと残り続けてしまいます)もしておきます。

client/webpack/production-build.config.js
const AssetsPlugin = require('assets-webpack-plugin');
const path = require('path');

const javaScriptRoot = path.join(__dirname, '../src'); // -> "{project-root}/client/src"
const publicationRoot = path.join(__dirname, '../../public/client'); // -> "{project-root}/public/client"
const temporaryFilesRoot = path.join(__dirname, '../tmp'); // -> "{project-root}/client/tmp"

module.exports = {
  mode: 'production',
  entry: {
    'webpacked': path.join(javaScriptRoot, 'index.js'),
  },
  output: {
    filename: '[name]-[chunkhash].js',
    path: publicationRoot,
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
          },
        ],
      },
    ],
  },
  plugins: [
    new AssetsPlugin({
      filename: 'webpack-manifest.json',
      path: temporaryFilesRoot,
    }),
  ],
};
package.json
"scripts": {
  "build": "npm run clean && npm run webpack",
  "clean": "if [ -e client/tmp ]; then rm -r client/tmp; fi && if [ -e public/client ]; then rm -r public/client; fi",
  "test": "echo \"Error: no test specified\" && exit 1",
  "webpack": "$(npm bin)/webpack --config client/webpack/production-build.config.js"
},

最後に、client_side_javascript_tag(Webpackerでいうとjavascript_pack_tag)を定義すれば完成です!

app/helpers/application_helper.rb
module ApplicationHelper
  def client_side_javascript_tag
    path = ClientSideSupporter.webpacked_javascript_path
    javascript_include_tag(path).html_safe
  end
end
app/helpers/application_helper/client_side_supporter.rb
module ApplicationHelper
  # A module for cooperation with the client side
  module ClientSideSupporter
    class << self
      # Returns a value that is used for the "src" attribute on script tag.
      def webpacked_javascript_path
        "#{base_publication_path}/#{webpack_manifest['webpacked']['js']}"
      end

      private

      def base_publication_path
        # If you have specified an external host in `config.action_controller.asset_host`,
        #   you need to consider it here.
        '/client'
      end

      def webpack_manifest
        Rails.application.config.x.client_side_supporter.webpack_manifest
      end
    end
  end
end
config/initializers/client_side_supporter.rb
webpack_manifest_json_path = Rails.root.join('client', 'tmp', 'webpack-manifest.json')

unless File.exist?(webpack_manifest_json_path)
  raise 'Please execute `npm run build` command before operating the Rails.'
end
Rails.application.config.x.client_side_supporter.webpack_manifest = JSON.parse(File.read(webpack_manifest_json_path))
app/views/layouts/application.html.erb
<%= client_side_javascript_tag %> # add
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> # delete

npm run buildの後にrails serverを実行して確認してみてください!

2. npm run watchが動くようにする

まずは以下コマンドでclean-webpack-pluginというビルド時に出力先フォルダの中身を空にするプラグインを入れます

npm install -D clean-webpack-plugin

次に、npm run watchが動くようにしていきます。
(先ほど作成したproduction-build.config.jswatch-for-development.config.jsは共通部分が多いので、こちらのPRを参考にまとめてみてください)

client/webpack/watch-for-development.config.js
const AssetsPlugin = require('assets-webpack-plugin');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const path = require('path');
const webpack = require('webpack');

const javaScriptRoot = path.join(__dirname, '../src'); // -> "{project-root}/client/src"
const publicationRoot = path.join(__dirname, '../../public/client'); // -> "{project-root}/public/client"
const temporaryFilesRoot = path.join(__dirname, '../tmp'); // -> "{project-root}/client/tmp"

module.exports = {
  mode: 'none',
  watch: true,
  entry: {
    'webpacked': path.join(javaScriptRoot, 'index.js'),
  },
  output: {
    filename: '[name]-[chunkhash].js',
    path: publicationRoot,
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
          },
        ],
      },
    ],
  },
  plugins: [
    // This is similar to `npm run clean`.
    // If `npm run clean` does not needed to be independent, it can be included in `npm run build`.
    new CleanWebpackPlugin(),
    // This sets an environment variable that is enabled in build process on webpack.
    // Many npm packages refer to the `NODE_ENV` value to change the build behavior.
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('development'),
    }),
    new AssetsPlugin({
      filename: 'webpack-manifest.json',
      path: temporaryFilesRoot,
    }),
  ],
};
package.json
"watch": "$(npm bin)/webpack --config client/webpack/watch-for-development.config.js", # Add under `scripts`

次に、監視対象のファイルが更新された場合にreloaderを実行する処理を書きます

config/environments/development.rb
config.x.client_side_supporter.update_webpack_manifest_on_reloading = true
config/initializers/client_side_supporter.rb
if Rails.application.config.x.client_side_supporter.update_webpack_manifest_on_reloading == true
  reloader = Rails.application.config.file_watcher.new([webpack_manifest_json_path]) do
    if File.exist?(webpack_manifest_json_path)
      Rails.application.config.x.client_side_supporter.webpack_manifest =
        JSON.parse(File.read(webpack_manifest_json_path))
    end
  end

  Rails.application.reloaders << reloader

  ActiveSupport::Reloader.to_prepare do
    reloader.execute_if_updated
  end
end

これでブラウザをリロードするとソースの変更が反映されるようになったと思います!

3. クロスブラウザで新しいECMAScript機能を有効にする

できるだけ多くのブラウザで動作するようにforceAllTransformsを追加し、production-build.config.js, watch-for-development.config.jsのmodule > rules > use配下に以下を追加します

options: {
  presets: [
    [
      '@babel/preset-env',
      {
        // This "forceAllTransforms" transforms sources for working on many browsers as possible.
        //
        // Normally, "@babel/preset-env" performs only the minimum necessary conversions
        //   for the supported browsers specified by the "targets" option or the ".browserslistrc" file.
        // So this option slows down the transpiling and increases the file size of the built ".js".
        // But if you don't have to write a lot of JavaScript, you don't have to worry about it.
        forceAllTransforms: true,
      },
    ],
  ],
},

最後に以下コマンドでcore-js@3regenerator-runtimeをインストールし、index.js(エントリーポイント)でインポートします。

npm install core-js@3 regenerator-runtime
client/src/index.js
// This polyfill way is the easiest way, but it has the largest file size and has some global side effects.
// If you want to know different ways, you may want to read from the following article.
// https://babeljs.io/blog/2019/03/19/7.4.0#core-js-3-7646-https-githubcom-babel-babel-pull-7646
import 'core-js/stable';
import 'regenerator-runtime/runtime';

4. JSファイルの移行

あとはひたすらapp/assets/javascripts配下にあるjsファイルをclient/src配下に移行し、index.js(エントリーポイント)から各ファイルをimportすれば完成です!

お疲れ様でした〜〜 :smile:

5. その他設定

上記の1〜4では最小限の設定を紹介してきましたが、他にもPRが出されているので紹介しておきます

BabelからTypeScriptへ変更する
ユニットテストをNode.js/Jestで動かす
bin/setupに設定を適用
production-build.config.jsとwatch-for-development.config.jsの共通処理をまとめる
Please execute npm run build command before operating the Rails.エラーを抑止する

おわりに

いかがでしたでしょうか!
実際に個人プロダクトで設定してみて、「なんとなく」Webpackerのレールに乗るよりはるかに勉強になったなと感じています。
個人的には初心者の方にこそおすすめだなと感じているので、ぜひ気になった方はリポジトリ覗いてみてください〜!

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

#Rails での ransackable_scopes の使い方と SQLの例

class User
  def full_name
    "#{first_name} #{last_name}"
  end

  private

  def self.ransackable_scopes(auth_object = nil)
    %i(full_name_like)
  end
end


こんな使い方してます

他のDBカラムからの検索と、つじつまをあわせて、実装的にレールに乗るために

User.full_name_like('A').to_sql
=> "SELECT `users`.* FROM `users` WHERE (((last_name LIKE '%A%') OR (first_name LIKE '%A%')) OR (CONCAT(`last_name`, `first_name`) LIKE 'A'))"

すごく頑張ってる感

ありがとうransack

おめでとうransack

Original by Github issue

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

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

Rails find_spec_for_exe': can't find gem railties (= 5.0.7.2)

rails _5.0.7.2_ new DataBaseDesignSample -d mysql

ターミナル で実行した所、

find_spec_for_exe': can't find gem railties (= 5.0.7.2) with executable rails (Gem::GemNotFoundException)

とエラーが出てきました

解決策

 % gem install rails -v 5.0.7.2

原因
Rails のバージョンを指定してインストールできてなかった...?のかな..?

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

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #15 投稿機能, Active Storage編

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#14 ユーザ投稿表示, ページネーション編
次回:準備中

今回の流れ

  1. ルーティングと各アクションを整える
  2. 投稿フォームをつくる
  3. 画像を投稿できるようにする
  4. 投稿編集・削除機能をつくる
  5. 投稿時の不具合を解消する
  6. テストをつくる

今回は投稿機能を実装します。
(投稿のためのモデルが必要なので未読の方は#14を参照下さい。)

1. ルーティングと各アクションを整える

投稿フォームはshow.html.erbと同じビューを使用します。
投稿編集と削除の際には別途ビューを生成します。
以上から必要なアクションはcreate、edit、update、destroyとなります。
よって、ここでの手順は以下の通りです。

  • ルーティングを行う
  • 作成済みのメソッドを移動する
  • 各アクションを整える

ルーティングを行う

まずはルーティングを行いましょう。

config/routes.rb
Rails.application.routes.draw do
  # 中略
  resources :microposts, only: [:create, :edit, :update, :destroy]
end

作成済みのメソッドを移動する

各アクションを整える前に、1つ変更を加えます。
logged_in_userメソッドはMicropostsコントローラでも使用します。
先にこちらはApplicationコントローラに移動しましょう。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper

  private
    def logged_in_user
      unless logged_in?
        store_location
        flash[:warning] = 'ログインしてください'
        redirect_to login_url
      end
    end
end

各アクションを整える

create、edit、update、destroyといった4つのアクションを整えましょう。

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

  def create
    @user = current_user
    @micropost = current_user.microposts.build(micropost_params) if logged_in?
    @microposts = @user.microposts.page(params[:page]).per(10)

    if @micropost.save
      redirect_to current_user
    else
      render 'users/show'
    end
  end

  def edit
    @micropost = current_user.microposts.find_by(id: params[:id]) || nil
    if @micropost.nil?
      flash[:warning] = "編集権限がありません"
      redirect_to root_url
    end
  end

  def update
    @micropost = current_user.microposts.find_by(id: params[:id])
    @micropost.update_attributes(micropost_params)
    if @micropost.save
      flash[:success] = "編集が完了しました"
      redirect_to current_user
    else
      render 'microposts/edit'
    end
  end

  def destroy
    @micropost.destroy
    flash[:success] = "ログが削除されました"
    redirect_to current_user
  end

  private
    def micropost_params
      params.require(:micropost).permit(:memo, :time, :picture)
    end

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

このポートフォリオはTutorialと異なり、user_pathにフォームを置いています。
この場合、フォーム入力失敗時のパスがmicroposts_pathとなります。

その結果、フォーム入力失敗時に/user/show.html.erbをrenderするため、Micropostsコントローラのcreateアクションにuserとmicroposts変数を渡す必要があります。

2. 投稿フォームをつくる

投稿フォームをつくりましょう。
パーシャルを生成し、そこに投稿フォームを作ります。

bash
$ touch app/views/layouts/_micropost_form.html.erb
app/views/layouts/_micropost_form.html.erb
<div class="container micropost-container">
  <h1>Form</h1>
  <%= form_with(model: @micropost, url: microposts_path, local: true) do |form| %>
    <%= render 'shared/error_messages', object: form.object %>
    <div class="form-group">
      <%= form.number_field :time, class: 'form-control', placeholder: "時間(分)を入力してください" %>
    </div>
    <div class="form-group">
      <%= form.text_area :memo, class: 'form-control', placeholder: "メモを加えてください" %>
    </div>
    <div class="form-group">
      <%= form.file_field :picture %>
    </div>
    <div class="form-group">
      <%= form.submit "記録する", class: 'btn btn-info btn-lg form-submit' %>
    </div>
  <% end %>
</div>

lantern_form.png

まず説明する点は、このような記述についてです。

<%= render 'shared/error_messages', object: form.object %>

これによりerror_messages.html.erbで、userやmicropostなどを引き渡すオブジェクト名としてobjectを使用できます。
エラーを表示する他のビューに対しても、同様に記述しましょう。

加えてerror_messages.html.erbはこのように変更しましょう。

app/views/shared/_error_messages.html.erb
<% if object.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger alert-form-extend" role="alert">
      <%= object.errors.count %>個のエラーがあります
    </div>
    <ul>
      <% object.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>

参考になりました↓
form_for の f.object って何だ?(Rails)

続いてこの部分ですが、現状では動作しません。

<%= form.file_field :picture %>

こちらは下記の画像投稿を可能にすることで動作します。

3. 画像を投稿できるようにする

画像投稿にはRails5.2から対応のActive Storageを使用します。
また画像をアップロードするストレージとしてS3を使用します。
まずはActive Storageについて理解しましょう。

Active Storageを理解する

Active Storageとはファイルをアップロードする機能のことです。
以下の記事が分かりやすいですので一読してみましょう。
【Rails 5.2】Active Storageの使い方

加えて今回はアップロード時の保存先にS3を使用します。
S3とはAWSが提供するストレージのことです。
S3の使用にはGemの追加と設定の編集が必要です。

以上を踏まえてActive Storageを使用するにはこのような手順が必要です。

  • S3(バケット)を作成する
  • Active Storageを用意する
  • Active StorageとS3を紐づける

S3(バケット)を作成する

まずはS3(バケット)を作成しましょう。
ここでの手順は以下の通りです。

  • IAMユーザに権限を与える
  • アクセスキーを作成する
  • バケットを作成する

IAMユーザに権限を与える

ポートフォリオ用のユーザにS3を使用する権限を与えます。
以下の手順を行いましょう。

  1. ルートユーザでAWSにログインする
  2. IAMを開く
  3. ポートフォリオ用ユーザを選択する
  4. 「アクセス権限の追加」を押す
  5. 「既存のポリシーを直接アタッチ」からS3を検索する
  6. 「AmazonS3FullAccess」をチェックし確認する

分からない場合はこちらをご覧ください↓
【AWS】【S3】作成手順 & アップロード手順 & アクセス権限設定手順

アクセスキーを作成する

続いて後述で必要になるアクセスキーを作成します。
このキーを作成することでRailsとS3を紐づけることが可能です。
以下の手順を行いましょう。

  1. ルートユーザでAWSにログインする
  2. IAMを開く
  3. ポートフォリオ用ユーザを選択する
  4. 「認証情報」タブから「アクセスキーの作成」を押す
  5. アクセスキーとシークレットアクセスキーをメモする (一度しか表示されないので注意)

バケットを作成する

最後にポートフォリオ用ストレージとなるバケットを作成します。
以下の手順を行いましょう。

  1. IAMユーザでAWSにログインする
  2. S3を開く
  3. 「バケットを作成する」を押す
  4. バケット名を入力し東京リージョンを選択する
  5. 「次へ」を押し続け「バケットを作成」を押す

分からない場合はこちらをご覧ください↓
【AWS】【S3】作成手順 & アップロード手順 & アクセス権限設定手順
以上でS3(バケット)の作成は完了です。

Active Storageを用意する

次にActive Storageを用意しましょう。
ここでの手順は以下の通りです。

  • Active Storageをインストールする
  • モデル(今回はMicropostモデル)を編集する

Active Storageをインストールする

まずはインストールを行いDBをマイグレートします。

bash
$ rails active_storage:install
$ rails db:migrate

モデル(今回はMicropostモデル)を編集する

モデルの編集はいたって簡単です。
app/models/micropost.rbに以下を加えるだけで成立します。

app/models/micropost.rb
has_one_attached :picture

ここで宣言した名前(今回はpicture)がモデルのカラムとして機能します。
以上でActive Storageの用意は完了です。

Active StorageとS3を紐づける

最後にActive StorageとS3を紐づけましょう。
ここでの手順は以下の通りです。

  • 必要なGemを追加する
  • 設定を編集する

必要なGemを追加する

S3との紐づけにはaws-sdk-s3が必要です。
Gemfileに追加しましょう。

gemfile
+ gem 'aws-sdk-s3'

設定を編集する

Active Storageの保存先をS3にするため:localから:amazonに変更します。

config/environments/development.rb
  # 中略
  # Store uploaded files on the local file system (see config/storage.yml for options)
  config.active_storage.service = :amazon
config/environments/production.rb
  # 中略
  # Store uploaded files on the local file system (see config/storage.yml for options)
  config.active_storage.service = :amazon

続いてstorage.ymlにてamazon:の部分をこのように変更します。

config/storage.yml
# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: ap-northeast-1
  bucket: #S3で作成したバケット名(今回はlantern-lantern-s3)

最後に、先ほどメモしたアクセスキーとシークレットアクセスキーを入力します。
この2つはセキュリティ上Rails Credentialsを使用します。
この機能により直接キーを入力することを回避し暗号化します。

それではRails Credentialsを編集しましょう。
エディターにはVimを使用します。

bash
$ EDITOR=vim rails credentials:edit

Vimは癖があるので、下記に簡単な操作を載せておきます。

  • 入力開始:i
  • 入力終了:esc
  • 保存終了:ZZ
  • 保存せず終了::q!
aws:
 access_key_id: # ここにアクセスキーを入力
 secret_access_key: # ここにシークレットアクセスキーを入力

以上で紐づけは完了です。

画像をリサイズする

このままではユーザが投稿する画像サイズに制限がありません。
画像のリサイズにはMiniMagickというGemを使用しましょう。
ここでの手順は以下の通りです。

  • Gemを追加する
  • ImageMagickをインストールする
  • リサイズのメソッドを定義する
  • ビューで表示する

Gemを追加する

gemfile
+ gem 'mini_magick'
bash
$ bundle install

ImageMagickをインストールする

MiniMagickの使用にはImageMagickのインストールが必要です。
こちらも加えて行いましょう。

bash
$ sudo yum install -y ImageMagick

リサイズのメソッドを定義する

画像のリサイズを行うよう、モデルにメソッドを定義します。

app/models/micropost.rb
def resize_picture
  return self.picture.variant(resize: '100x100').processed
end

ビューで表示する

リサイズされた画像を表示するにはこのように記述します。

app/views/layouts/_log.html.erb
<% @microposts.each do |micropost| %>
<!-- 省略 -->
  <% if micropost.picture.attached? %>
  <div class="log-picture">
    <%= image_tag micropost.resize_picture %>
  </div>
  <% end %>
<!-- 省略 -->
<% end %>

フォーム入力↓
lantern_input_form.png
ログ表示↓
lantern_output_form.png
以上で画像のリサイズは完了です。

4. 投稿編集・削除機能をつくる

編集と削除のためのアクションはすでに終えました。
よって、ここでの手順はビューをつくるのみです。

bash
$ touch app/views/microposts/edit.html.erb
app/views/microposts/edit.html.erb
<% provide(:title, 'メモ編集') %>
<div class="container micropost-edit-container">
  <div class="edit-titles">
    <%= image_tag 'edit_02.png', class: 'edit-img' %>
    <h1 class="title edit-micropost-title">メモ編集</h1>
  </div>
  <%= form_with(model: @micropost, url: micropost_path, local: true) do |form| %>
    <%= render 'shared/error_messages', object: form.object %>
    <div class="form-group">
      <%= form.label :time, '時間' %>
      <%= form.number_field :time, class: 'form-control', placeholder: @micropost.time %>
    </div>
    <div class="form-group">
      <%= form.label :memo, 'メモ' %>
      <%= form.text_area :memo, class: 'form-control', placeholder: @micropost.memo %>
    </div>
    <div class="form-group">
      <%= form.label :picture, '画像' %>
      <%= form.file_field :picture, class: 'form-control-file', placeholder: @micropost.picture %>
    </div>
    <div class="row">
      <div class="col">
        <div class="form-group">
          <%= link_to "削除", micropost_path, method: :delete, data: { confirm: "本当に削除しますか?" }, class: 'btn btn-lg btn-danger btn-edit-user' %>
        </div>
      </div>
      <div class="col">
        <div class="form-group">
          <%= form.submit "編集", class: 'btn btn-info btn-lg btn-edit-user' %>
        </div>
      </div>
    </div>
    <div class="form-group">
      <%= link_to "戻る", current_user, class: 'btn btn-lg btn-edit-user btn-back' %>
    </div>
  <% end %>
</div>

メモ編集画面↓
lantern_edit_micropost.png

5. 投稿時の不具合を解消する

このままではフォームの時間が空の時、ログが「分」のみで出力されてしまいます。
それを避けるために簡単な条件を書き、ログを正しく出力しましょう。

app/views/layouts/_log.html.erb
<!-- 省略 -->
<% @microposts.each do |micropost| %>
    <li id ="micropost-<%= micropost.id %>">
      <span class="row log-list">
        <span class="col-2 log-timestamp d-none d-md-inline-block log-timestamp-block">
          <span class="log-timestamp"><%= time_ago_in_words(micropost.created_at) %></span>
        </span>
        <span class="col-md-10 col-log-memos">
          <div class="log-time-and-edit">
            <div class="row">
              <span class="log-time col-3">
                <% if micropost.time.nil? %>
                  0分
                <% else %>
                  <%= micropost.time %><% end %>
              </span>
              <span class="col-7 log-timestamp log-timestamp-inline"><%= time_ago_in_words(micropost.created_at) %></span>
              <span class="log-edit col-2"><%= link_to image_tag('edit.png', class: "log-edit-image"), edit_micropost_path(micropost) %></span>
            </div>
          </div>
          <% if micropost.memo.present? %>
            <div class="log-memo"><%= micropost.memo %></div>
          <% end %>
          <% if micropost.picture.attached? %>
            <div class="log-picture"><%= image_tag micropost.resize_picture %></div>
          <% end %>
        </span>
      </span>
    </li>
<% end %>
<!-- 省略 -->

時間が空の場合「0分」と表示↓
(少々デザインも変更しています)
lantern_log_correction.png

6. テストをつくる

残るはテストのみです。
ここではModel specとRequest specのテストを行います。

Model specでのテスト

このテストでは以下を確認します。

  • pictureのみ存在するMicropostモデルは有効か
  • 5MBを超える画像は無効か
  • 画像以外のファイルは無効か

その前に5MB以上の画像、画像以外のファイルを以下のフォルダに準備しましょう。

bash
$ mkdir spec/fixtures/images
# 実際にファイルを追加する

その後、テストを記述します。

spec/models/micropost_spec.rb
require 'rails_helper'

RSpec.describe Micropost, type: :model do

  let(:user) { create(:user) }
  let(:micropost) { user.microposts.build(time: 240, memo: "Lorem ipsum", user_id: user.id) }

  # 中略

  describe "picture" do
    it "should be valid if all columns are nil except picture" do
      micropost.update_attributes(time: nil, memo: nil, user_id: user.id)
      expect(micropost).to be_invalid
      micropost.picture.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'images', 'test.jpg')), filename: 'test.jpg', content_type: 'image/jpg')
      expect(micropost).to be_valid
    end

    it "should not be over than 5MB" do
      micropost.picture.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'images', 'test_5mb.jpg')), filename: 'test_5mb.jpg', content_type: 'image/jpg')
      expect(micropost).to be_invalid
    end

    it "should be only images file" do
      micropost.picture.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'images', 'test.pdf')), filename: 'test.pdf', content_type: 'application/pdf')
      expect(micropost).to be_invalid
    end
  end
end

参考にさせていただきました↓
Active Storageの簡単なバリデーションの実装とテスト
ActiveStorageでattachできるものについて調べてみた
Ruby IOクラスについて学ぶ
Railsのファイルパスの操作のメモ

Request specでのテスト

このテストでは以下を確認します。

  • ログインしていないユーザの投稿が無効か
  • フォームが全て空欄のユーザ投稿が無効か
  • ログインしているユーザの投稿が有効か
  • ログインしていないユーザの投稿削除が無効か
  • 他ユーザの投稿削除が無効か
  • ログインしているユーザの投稿削除が有効か
  • ログインしていないユーザの投稿編集が無効か
  • 他ユーザの投稿編集が無効か
  • フォームが全て空欄の投稿編集が無効か
  • ログインしているユーザの投稿編集が有効か
spec/requests/microposts_spec.rb
require 'rails_helper'

RSpec.describe "Microposts", type: :request do

  let(:user) { create(:user) }
  let(:other_user) { create(:other_user) }

  def post_valid_information
    post microposts_path, params: { micropost: { memo: "aaa" } }
  end

  def post_invalid_information
    post microposts_path, params: { micropost: { memo: nil } }
  end

  def patch_valid_information
    patch micropost_path, params: { micropost: { memo: "bbb" } }
  end

  def patch_invalid_information
    patch micropost_path, params: { micropost: { memo: nil } }
  end

  describe "POST /microposts" do
    it "does not add a micropost when not logged in" do
      expect{ post_valid_information }.not_to change(Micropost, :count)
      follow_redirect!
      expect(request.fullpath).to eq '/login'
    end

    it "does not add a micropost when the form has no information" do
      log_in_as(user)
      get user_path(user)
      expect{ post_invalid_information }.not_to change(Micropost, :count)
    end

    it "succeeds to add a micropost" do
      log_in_as(user)
      get user_path(user)
      expect(request.fullpath).to eq '/users/1'
      expect{ post_valid_information }.to change(Micropost, :count).by(1)
      follow_redirect!
      expect(request.fullpath).to eq '/users/1'
    end
  end

  describe "DELETE /micropost" do
    it "does not destroy a micropost when not logged in" do
      delete micropost_path(1)
      follow_redirect!
      expect(request.fullpath).to eq '/login'
    end

    it "does not destroy a micropost when other users logged in" do
      log_in_as(user)
      get user_path(user)
      post_valid_information
      follow_redirect!
      delete logout_path
      log_in_as(other_user)
      get user_path(other_user)
      expect(request.fullpath).to eq '/users/2'
      post_valid_information
      expect{ delete micropost_path(1) }.not_to change(Micropost, :count)
      expect{ delete micropost_path(2) }.to change(Micropost, :count).by(-1)
    end

    it "succeeds to destroy a micropost" do
      log_in_as(user)
      get user_path(user)
      expect{ post_valid_information }.to change(Micropost, :count).by(1)
      follow_redirect!
      expect{ delete micropost_path(1) }.to change(Micropost, :count).by(-1)
      follow_redirect!
      expect(request.fullpath).to eq '/users/1'
      expect(flash[:success]).to be_truthy
    end
  end

  describe "GET /microposts/:id/edit" do
    it "does not edit a micropost when not logged in" do
      log_in_as(user)
      get user_path(user)
      post_valid_information
      follow_redirect!
      delete logout_path
      follow_redirect!
      get edit_micropost_path(1)
      follow_redirect!
      expect(request.fullpath).to eq '/login'
    end

    it "does not edit a micropost when other users logged in" do
      log_in_as(user)
      get user_path(user)
      post_valid_information
      follow_redirect!
      delete logout_path
      follow_redirect!
      log_in_as(other_user)
      get edit_micropost_path(1)
      follow_redirect!
      expect(request.fullpath).to eq '/'
    end

    it "does not edit a micropost when the form has no information" do
      log_in_as(user)
      get user_path(user)
      post_valid_information
      follow_redirect!
      get edit_micropost_path(1)
      expect(request.fullpath).to eq '/microposts/1/edit'
      patch_invalid_information
      expect(request.fullpath).to eq '/microposts/1'
    end

    it "succeeds to edit a micropost" do
      log_in_as(user)
      get user_path(user)
      post_valid_information
      follow_redirect!
      get edit_micropost_path(1)
      expect(request.fullpath).to eq '/microposts/1/edit'
      patch_valid_information
      follow_redirect!
      expect(request.fullpath).to eq '/users/1'
    end
  end
end

以上でテストは終了です。
お疲れさまでした。

備考:Active Storageを伴うテストにFactoryBotを使いたい

Micropostモデルを検証する際、FactoryBotを使ったテストを行いたかったのですが、断念しました。一応調べた分を共有します。

Active Storageを伴うFactoryBotを使う手順は以下の通りです。

  1. 環境設定を行う
  2. FactoryBotを整える
  3. テストを書く

環境設定を行う

まずは、ダミーでアップロードを行うfixture_file_uploadメソッドを使うために、環境設定をしましょう。

spec/rails_helper.rb
# 中略
RSpec.configure do |config|
  # 中略
  # ファイルアップロードのテストに使用する
  config.include ActionDispatch::TestProcess
  # factoryBot内での呼び出し
  FactoryBot::SyntaxRunner.class_eval do
    include ActionDispatch::TestProcess
  end
  # fixtureのパス指定(テスト時のパスをfixtures以下から省略できる)
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
# 中略
end

参考にさせていただきました↓
parperclipでファイルアップロードをRspecでテスト w/ factory_girl
Factory Bot trait for attaching Active Storange has_attached

FactoryBotを整える

続いてFactoryBotを整えます。

spec/factories/microposts.rb
FactoryBot.define do
  factory :micropost do
    trait :memo_1 do
      time { 240 }
      memo { "I just ate an orange!" }
      user_id { 1 }
      created_at { 10.minutes.ago }
      picture { fixture_file_upload('/images/test.jpg', 'image/jpg') }
    end
    # 中略
    association :user
  end
end

参考にさせていただきました↓
Factory Bot trait for attaching Active Storange has_attached

テストを書く

残るはletでMicropostモデルを定義するだけです。

spec/models/micropost_spec.rb
require 'rails_helper'

RSpec.describe Micropost, type: :model do
  let(:micropost) { create(:micropost, :memo_1) }
  # 中略
end

しかしテスト時に画像を追加・削除する際、Active Storageで生成される他の2つのモデルとの関連づけがないためエラーが発生します。

# どれもエラーが発生する
micropost.picture = nil
micropost.save!

micropost.update_attribute(:picture, nil)

micropost.picture.attach(nil)
ActiveRecord::RecordNotSaved:
       Failed to save the new associated picture_attachment.

強引に2つのクラスを生成するという方法もあるのですが、断念しました。
シンプルにテストを通す方法をご存知の方は、ご教示お願いします。

参考にさせていただきました↓
ruby on rails 画像 fixture_file_uploadに{file}が存在しないというエラーがあります
ruby on rails rails_blob_path ActiveStorageでモデルテストを正しく行うには?


前回:#14 ユーザ投稿表示, ページネーション編
次回:準備中

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

enumの実装と、日本語化について

enumの実装と日本語化

某プログラミングスクールの課題で、
enumの実装と、対応する日本語化の機能を実装しましたので、
そのやり方を投稿させていただきます。
<railsでの開発、haml記法を想定しています。>

※本記事が初投稿です
わかりにくい点が見受けられるかと思いますが、ご容赦ください。。。

enum実装の経緯

productsテーブル:statusカラム(integer型)の場合
statusカラムに保存されている情報(1、2など)を、変数を用いて表示させたかったが、

 → DBに格納されている数字で表示されてしまう

例)viewで「@product.status」と記載 → 1 が表示される・・・
これを、「@product.status」 → ”新品、未使用” などの日本語へ変換したい

実装方法

<前提として>
ruby 2.5.1
rails 5.2.3
なお、enum実装のメリットとしては、下記の2点が大きいと思います
・コードが読みやすくなる
・データの管理がやりやすい 

それでは、段階を踏んで実装をしていきます。

1. gem の導入

gem 'rails-i18n'
gem 'enum_help' 

bundle install の実行

2.モデル(product)にenumを記載

product.rb
 enum status:{
    '---':           0, #---
    unused:          1, #新品、未使用
    nearly_unused:   2, #未使用に近い
    not_injured:     3, #目立った傷や汚れなし
    bit_injured:     4, #やや傷や汚れあり
    injured:         5, #傷や汚れあり
    bad:             6,  #全体的に状態が悪い
  }

今回はselectboxを作成するため、1~6の選択肢として記載。
選択されたstatusは、DBには1~6として格納される。
(記載した英語は、自由で大丈夫です)

3. ja.ymlに変換したい日本語を記載

※  ja.yml がない場合は、 config>localsの下に作成してください。
ja.yml
ja:
  enums:
    product:
      status:
        '---': "---"
        unused: "新品、未使用"
        nearly_unused: "未使用に近い"
        not_injured: "目立った傷や汚れなし"
        bit_injured: "やや傷や汚れあり"
        injured: '傷や汚れあり'
        bad: '全体的に状態が悪い'

階層が綺麗になっていないと、日本語化出来ないので要注意

4. viewに記載

new.html.haml
= f.select  :status, Product.statuses.keys.map {|k| [I18n.t("enums.product.status.#{k}"), k]}

Product : モデル名
statsues : カラム名(複数形にしてください)
keys.map {|k| [I18n.t("enums.product.status.#{k}"), k]}
→ 選択肢を1つ1つ取り出して、日本語に変換して並び替えるイメージです。

5. 変数を表示する場合

statusに1が格納されている前提で、最後に'_i18n'を記載する

ruby.sample.haml
(enum 実装ナシ)            @product.status       → " 1 "       
(enum実装アリ:日本語変換ナシ)        〃          →  ”unused”   
(enum実装アリ:日本語変換アリ)  @product.status_i18n  → ”新品、未使用”

最後に

DB設計の段階で、どのようにデータを管理するか、しっかり確認・詰めておくべきでした。。。
最初は、statusカラムの型をstringにしてしまったため、苦戦しました。
チーム開発半ばで、DBからデータを取って来た時に数字で表示されてしまって、
これは何とかしないといけないなと思ったことがスタートでした。

皆様のお役に立てれば幸いです

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

rails での enumと、日本語化のやり方

enumの実装と日本語化

某プログラミングスクールの課題で、
enumの実装と、対応する日本語化の機能を実装しましたので、
そのやり方を投稿させていただきます。
<railsでの開発、haml記法を想定しています。>

※本記事が初投稿です
わかりにくい点が見受けられるかと思いますが、ご容赦ください。。。

enum実装の経緯

productsテーブル:statusカラム(integer型)の場合
statusカラムに保存されている情報(1、2など)を、変数を用いて表示させたかったが、

 → DBに格納されている数字で表示されてしまう

例)viewで「@product.status」と記載 → 1 が表示される・・・
これを、「@product.status」 → ”新品、未使用” などの日本語へ変換したい

実装方法

<前提として>
ruby 2.5.1
rails 5.2.3
なお、enum実装のメリットとしては、下記の2点が大きいと思います
・コードが読みやすくなる
・データの管理がやりやすい 

それでは、段階を踏んで実装をしていきます。

1. gem の導入

gem 'rails-i18n'
gem 'enum_help' 

bundle install の実行

2.モデル(product)にenumを記載

product.rb
 enum status:{
    '---':           0, #---
    unused:          1, #新品、未使用
    nearly_unused:   2, #未使用に近い
    not_injured:     3, #目立った傷や汚れなし
    bit_injured:     4, #やや傷や汚れあり
    injured:         5, #傷や汚れあり
    bad:             6,  #全体的に状態が悪い
  }

今回はselectboxを作成するため、1~6の選択肢として記載。
選択されたstatusは、DBには1~6として格納される。
(記載した英語は、自由で大丈夫です)

3. ja.ymlに変換したい日本語を記載

※  ja.yml がない場合は、 config>localsの下に作成してください。
ja.yml
ja:
  enums:
    product:
      status:
        '---': "---"
        unused: "新品、未使用"
        nearly_unused: "未使用に近い"
        not_injured: "目立った傷や汚れなし"
        bit_injured: "やや傷や汚れあり"
        injured: '傷や汚れあり'
        bad: '全体的に状態が悪い'

階層が綺麗になっていないと、日本語化出来ないので要注意

4. viewに記載

new.html.haml
= f.select  :status, Product.statuses.keys.map {|k| [I18n.t("enums.product.status.#{k}"), k]}

Product : モデル名
statsues : カラム名(複数形にしてください)
keys.map {|k| [I18n.t("enums.product.status.#{k}"), k]}
→ 選択肢を1つ1つ取り出して、日本語に変換して並び替えるイメージです。

5. 変数を表示する場合

statusに1が格納されている前提で、最後に'_i18n'を記載する

ruby.sample.haml
(enum 実装ナシ)            @product.status       → " 1 "       
(enum実装アリ:日本語変換ナシ)        〃          →  ”unused”   
(enum実装アリ:日本語変換アリ)  @product.status_i18n  → ”新品、未使用”

最後に

DB設計の段階で、どのようにデータを管理するか、しっかり確認・詰めておくべきでした。。。
最初は、statusカラムの型をstringにしてしまったため、苦戦しました。
チーム開発半ばで、DBからデータを取って来た時に数字で表示されてしまって、
これは何とかしないといけないなと思ったことがスタートでした。

皆様のお役に立てれば幸いです

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

rails-tutorial第6章

そもそもなんでmodelが必要なの?

・永続的な情報を保存したいけど普通の変数だと実現できない。
・永続的な情報の保存にはDBを使わないといけない。
・ActiveRecordを使うと変数のようにDBに保存をすることができる。
・モデルはActiveRecordを継承したApplicationRecordを継承している。
・つまり、RubyとDBの橋渡しをしてくれるから。

Userモデルの作成

$ rails generate model User name:string email:string

モデルの作成時はUserのように単数形で書く。
name:string はnameカラムでデータ形式はstringだよーって意味。

ちなみに id:integer created_at:datetime updated_at:datetimeはデフォルトで入っている。

このコマンドにより、テストファイルやマイグレーションファイルが作成される。
以下は作成されたマイグレーションファイル

db/migrate/[timestamp]_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end

これで $ rails db:migrateをすると、Userリソースを保存するためのテーブルが作成される。
rails g modelコマンドだけではテーブルは作成されないから注意が必要。

$ rails db:rollback

このコマンドによってrails db:migrateの更新を戻すことができる。

modelファイルを見てみよう

app/models/user.rb
class User < ApplicationRecord
end

UserクラスがApplicationRecordを継承していることがわかる。
この継承によって、Userクラスのインスタンスにsaveメソッドが使えるようになり、DBに保存することができる。(findメソッドとかallメソッドとか色々使える。)またマジックカラム(id ,created_at, updated_at)はDBに保存されて初めて値が埋まる。

User.create

User.create(name: "A Nother", email: "another@example.org")
user.new, user.saveなどが面倒なときは、User.createでいきなりDBベースに保存することができる。
また、

u = User.create(name: "A Nother", email: "another@example.org")

createメソッドはUserインスタンスを返すので、上記でDBに保存し、かつローカル変数uに代入することができる。

findメソッドとfind_byメソッドの違い

findメソッドは見つからなかったときに例外を出すのに対して、find_byは見つからなかったときにnilを返す。

update_attributes

update_attributesはcreateと似ていて、更新のショートカットを可能にする。

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

このように1行で書ける。

update_attribute

update_attributeは二つの引数を使って更新する。

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

update_attributeはvalidationを介さずにDBに登録をすることができるという特徴がある。

ユーザーを検証する

モデルの場合、テストコードを先に書いて、あとでvalidationを書いていったほうが早い

モデルのテストを見ていこう

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

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

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

上記のsetupメソッドはその下の test do endが実行される直前に実行されるという特徴がある。

また、modelだけテストをしたいときは、

$ rails test:models

とすると良い。
このテストの場合、バリデーションが設定されてないのでテストは通る。

では次のテストはどうだろうか?

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

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

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

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

この場合、user.nameが空文字の際に、@userはnot validじゃなきゃいけないよね?っていうテスト。
この場合、バリデーションが設定されていないので、テストは失敗する。

validationを設定しよう

app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
end

validationはmodelのファイルに書く。

次は長さを検証してみよう

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

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

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

@user.nameはsetupメソッドでテスト直前に定義されるから問題ない。
"a" * 51とすることで、aを50回打つなどの面倒を避ける。

この場合、バリデーションを定義していないのでテストは失敗してしまう。
なので、

app/models/user.rb
class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end

とすることで、文字数のバリデーションを定義できる。

適切なメールアドレスかチェックする

test/models/user_test.rb
class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "email validation should accept valid addresses" do
    valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org
                         first.last@foo.jp alice+bob@baz.cn]
    valid_addresses.each do |valid_address|
      @user.email = valid_address
      assert @user.valid?, "#{valid_address.inspect} should be valid"
    end
  end

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

現在、メールアドレスは存在性と長さしかバリデーションを設定してないので、2つ目のテストで失敗してしまう。

なので、正規表現でバリデーションを設定しよう。

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

formatオプションで設定すると、その型通りじゃないとvalidにならないようになる。
ちなみにVALID_EMAIL_REGEXは定数である。

一意性を知ろう。

同じメールアドレスがDB内に複数あると困る。

テストを見てみよう

test/models/user_test.rb
class UserTest < ActiveSupport::TestCase

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

dupは複製するメソッド。
@userがDBに保存された状態で、duplicate_userという複製されたインスタンスはvalidですか?というテストである。このテストは落ちる。

じゃあ、一意性を担保するには?

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

このようにuniqueness: trueとすると一意性が担保される確率が上がる。

まだ、このままだとメールアドレスの大文字小文字を区別できない。
そのため、

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

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

最後のテスト、@userがDBに登録され、またメールアドレスが大文字になったインスタンスはnot validになるか?というテストなのだが、落ちてしまう。これはuniqueness: trueがデフォルトで大文字小文字を区別するようになっているためだ。

そこで

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

uniqueness: trueからuniqueness: { case_sensitive: false }に変えてあげる。
case_sensitive: falseは大文字小文字を区別しなくてもいいよーって意味。
これで大文字小文字関係なく、スペルが同じアドレスは登録できなくなり、一意性が担保されるようになる?

いや、まだだ。

全く同じメールアドレスが全く同じ時間に登録されたらどうなるか?
なんと、どちらも登録されてしまう。。。

なので、DBにも一意性を担保してもらうようにお願いをしなければいけない。
具体的には片方のアドレスが登録されるまで次の登録を待ってもらう。

その際はマイグレーションファイルを使う

$ rails generate migration add_index_to_users_email

できたマイグレーションファイルに

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

このように書く。
unique: trueは一意性をDB側でも担保してくださいねーってお願い。

で、 rails db:migrateをする。

これでOKか?
いや、このままだと、テストが全て通らなくなってしまう。

これは以下が原因となっている。

test/fixtures/users.yml
one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString

テスト用データベースの中に、MyStringというメールアドレスが2つあり、一意性に引っかかってしまったのだ。

解決策としては、このファイルの内容を全て消してあげれば良い。

セキュアなパスワードを設定する。

まずは散らばった文字列のパスワードのハッシュ値を入れる場所を作る。

$ rails generate migration add_password_digest_to_users password_digest:string

ちなみにマイグレーションファイル名を add_カラム名toテーブル名とするとRailsが勝手に判断して以下のようにコーディングしてくれる。

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

あとは、上のファイルが自分のやりたいことと一致するか確認して、
$ rails db:migrateを実行する。

これでパスワードのハッシュ値を保存する場所ができた。

bcrypt

次に、パスワードをハッシュ化するためのgem bcryptをインストールする。
gem 'bcrypt', '3.1.12'
bundle

そして、モデルファイルに has_secure_passwordと書けば完了。以下

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

ただ、この状態だとテストが落ちてしまう。
理由はtestファイルのsetupメソッドにpassword属性とpassword_confirmation属性の値を指定していないためらしい。

ちなみにpasswordとpassword_confirmationは仮想的な属性で、実際にDBに保存されるのはpassword_digestだけ。

そこで、

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

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

というように、仮想的な属性を指定してあげる。これで一応テストは通る。

あとは、パスワードの文字数を6文字以上とかにするバリデーションを設定してあげる。

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

パスワードの存在性と長さ6文字以上を指定。

これで一応セキュアなパスワードは実装完了。

パスワードの認証について

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

マイグレーションファイルを色々設定したあとは、
$ heroku run rails db:migrate
を忘れずに!!!

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

RailsとVue.js(SPA)を「いいとこ取り」。API連携で開発するハンズオン。(その1:Rails編)

はじめに

DBの操作と管理画面はRailsで作成しつつ、一般ユーザ向けの画面はSPAとかPWAにしたいというシーンは結構あるかと思います。既存アプリがRailsで動いているのを活かしながら、フロント側はSPA化するとか。基本はSPAなんだけど、管理画面はscaffoldでサクッと作って済ませたいとか。

今回はそんなことを考えながら、Rails(管理画面&API)Vue.js&Nuxt.js(SPA)という構成のアプリを作ってみました。題材は、ちょうど作り直したいと考えていた自分のポートフォリオサイトです。1

以下の役割分担で作っていきます。

Rails(View) Rails(API) Vue&Nuxt
コンテンツの表示(一般公開)
コンテンツの編集(管理者限定)

この記事では、Rails側で画面とAPIを作成するところまで掲載します。
SPA側の作成と、本番環境へのデプロイは、別途記事化します。

Rails側の準備

Railsに持たせる機能がAPIだけだったらrails new projectname --apiで良いのですが、これだとview関連のコードが作成されません。
今回は、管理画面は「Railsのいいとこ」を活かして作りつつ、SPA向けのAPIも作るので、このオプションは使わずに進めていきます。

以下、Rails5.2の環境が作成済みである前提です。2

rails new

まずは普通に rails new

$ rails new portfolio-rails
$ cd portfolio-rails

deviseを導入

ユーザ管理にはdeviseを使います。こういった「よく使う機能を手軽に実現できるgem」が充実しているのも「Railsのいいとこ」と思います。

Gemfile
gem 'devise'
console
$ bundle install
$ rails g devise:install

続けて、rails g devise:installをした時に表示されるSome setup you must do manually if you haven't yet:の下に書かれている1〜3を実施します。

config/environments/development.rb
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config/routes.rb
  root to: "home#index"
app/views/layouts/application.html.erb
    <p class="notice"><%= notice %></p>
    <p class="alert"><%= alert %></p>
console
$ rails g devise user
$ rails db:migrate
$ rails g devise:views
$ rails g devise:controllers users

この時点で、usersテーブルには以下の列が生成されています。

  • email
  • encrypted_password
  • reset_password_token
  • reset_password_sent_at
  • remenber_created_at

ここで一度rails sして最低限の動作を確認しました。
スクリーンショット 2019-09-22 22.49.11.png

大丈夫ですね。

すでにdeviseを入れてあるので、http://localhost:3000/users/sign_in からログイン画面にアクセスできます。
スクリーンショット 2019-09-22 22.40.28.png

この時点では、サインアップしようとしても roots.rb に記述した root to: "home#index" をまだ作成していないので、Routing Errorになります。

routes.rbを書き換えます。

routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    sessions: 'users/sessions'
  }
end

これで、サインアップしようとした際のRouting Errorはなくなりました。

ユーザ管理(認証・認可)周りはまだやることがありますが、一旦置いておき、他の作業に進みます。

Githubへ初期コミット

生成されたコードや設定への変更点が増える前に、Githubへの初期コミットをしておきます。
Github側でリポジトリを作成し、そこの指示通りに作業します。
今回は「portfolio-rails」というリポジトリにしました。
https://github.com/shozzy/portfolio-rails

$ git init
$ git add README.md
$ git commit -m "first commit"
$ git remote add origin 
https://github.com/shozzy/portfolio-rails.git
$ git push -u origin master

これだけだとREADME.mdだけがpushされた状態なので、続けてここまでに生成されたコードや設定ファイルもpushしようと思いますが、その前に.gitignoreの内容を見直しておきます。
正直詳しくないので、 gitignore.io で取得した内容をそのまま適用します。
https://www.gitignore.io/api/rails

これで公開してはいけないファイルを誤って公開してしまう可能性は低減できたでしょう。

それでは、コミットを実施します。3

$ git add .
$ git commit -m "second commit"
$ git push

コンテンツ用のmodel, view, controllerを作成

ここからは、ポートフォリオのコンテンツ用のmodel, view, controllerを作成します。
今回は、scaffoldを使って、最低限のCRUDをざっくり作成してしまいます。

$ rails g scaffold Content title:String detail:String
$ rails db:migrate

rails sして確認すると、エラーになりました。
スクリーンショット 2019-09-24 0.26.32.png

マイグレーションはしたのですが、、、

と思ってコンソールを見直したら、マイグレーションファイルの中にtypoがありました。フィールドの型をstringと書くべきところ、うっかりキャメルケースでStringと書いていました。

NoMethodError: private method `String'

マイグレーションに失敗した状態なので、マイグレーションファイル4を修正してから再度マイグレーションします。

$ rails db:migrate
== 20190923152109 CreateContents: migrating ===================================
-- create_table(:contents)
   -> 0.0015s
== 20190923152109 CreateContents: migrated (0.0016s) ==========================

成功しました。

rails s

まだ中身のデータは入っていませんが、scaffoldで作成した画面が表示できました。

スクリーンショット 2019-09-24 0.36.26.png

新機能を作成したので、featureブランチを作成して、そこにpushしておきます。

$ git checkout -b create-contents
$ git add .
$ git commit -m "create contents scaffold"
$ git push origin create-contents

試しに動かしてみる

一応動くものができているはずなので、試しに動かしてみましょう。ここでは、データを3件登録してみました。
scaffoldのままなのでドシンプルですが、正しく画面表示できています。
スクリーンショット 2019-09-25 12.20.30.png

最低限のテスト(RSpec)

APIを作る前に、ここまでの内容に対して最低限のテストを書いておきます。

RSpecとFactoryBotを導入します。

Gemfile
group :test do
  # 中略
  gem 'rspec-rails', '~> 3.8'
  gem 'factory_bot_rails', '~> 5.1.0'
end
console
$ bundle install
$ rails g rspec:install

これにより、specフォルダが作成されます。

minitest用のフォルダを削除します。

$ rm -r ./test

Headless Chromeを使用するように設定を入れます。

spec/spec_helper.rb
require 'capybara/rspec'

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
end

下記設定がデフォルトではコメントアウトされていますが、コメントを外しておきます。

rails_driver.rb
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
console
$ mkdir ./spec/factories

./spec/factories/contents.rb を作成します。

./spec/factories/contents.rb
FactoryBot.define do
  factory :content do
    title { 'テストタイトル' }
    detail { 'テストコンテンツの明細です。' }
  end
end
console
$ mkdir ./spec/system

./spec/system/contents_spec.rb を作成します。

./spec/system/contents_spec.rb
require 'rails_helper'

describe 'コンテンツ管理機能', type: :system do
  describe '一覧表示機能' do
    context '1件だけデータがある場合' do
      before do
        # コンテンツを1件作成
        FactoryBot.create(:content, title:"テストコンテンツ1", detail:"コンテンツ1の明細")
        visit contents_path
      end
      it '1件のコンテンツが表示される' do
        # 表示内容を確認
        expect(page).to have_content 'テストコンテンツ1'
      end
    end
  end
end
console
$ bundle exec rspec spec/system/contents_spec.rb
Capybara starting Puma...
* Version 3.12.1 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:55559
2019-09-28 15:15:03 WARN Webdrivers Driver caching is turned off in this version, but will be enabled by default in 4.x. Set the value with `Webdrivers#cache_time=` in seconds
.

Finished in 5.84 seconds (files took 2.37 seconds to load)
1 example, 0 failures

テストは無事にパスしました。

Set the value with `Webdrivers#cache_time=` in secondsのWARNが気になるので、下記対処をしておきます。

console
$ mkdir ./spec/support
spec/support/javascript_driver.rb
# During the cache time, Webdrivers won't check to update Chrome.
Webdrivers.cache_time = 1.month.to_i

WARNが出なくなりました✨

console
$ bundle exec rspec spec/system/contents_spec.rb
Capybara starting Puma...
* Version 3.12.1 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:55767
.

Finished in 2.39 seconds (files took 2.34 seconds to load)
1 example, 0 failures

APIを生やす

さて、ここからようやく本題です。APIを生やして行きます。

元々
http://localhost:3000/contents.json
へアクセスしたらJSONで結果が返ってきますが5、ここではせっかくなのでもう少しAPIっぽく、
http://localhost:3000/api/contents
でアクセスしたらJSONで結果が返ってくるようにしてみます。

routes.rbに以下の設定を追加します。APIからはindexとshowのアクションだけを呼べるようにしたいので、resouresは使わず、個別に設定しています。

routes.rb
  scope '/api' do
    get '/contents', to: 'contents#index', defaults: { format: :json }
    get '/contents/:id', to: 'contents#show', defaults: { format: :json }
  end

scope を設定することで、URLに/apiが入っていても/api/contentsではなく/contentsにアクセスしたかのような動きになり、
各ルーティングの defaults に format の設定を入れることで、/contents.jsonではなく/contentsと書くだけでJSONが自動的にフォーマットとして指定されるようになります。
スクリーンショット 2019-10-05 15.40.58.png

同様に、特定のコンテンツの明細も以下のように取得できます。6

スクリーンショット 2019-10-06 14.46.44.png

たったこれだけで、APIを作ることができました。

なお、今回は一般公開する機能だけをAPI化したので、APIキーによるアクセス制限は実施していません。
編集機能や特定ユーザだけに公開する情報をAPI化する時は、何らか7のアクセス制御を組み込む必要があります。

編集系の機能をログイン必須にする

今回は自分以外は編集できないようにする計画ですが、ここまでの内容のままでは誰でもコンテンツを編集できてしまいます。
最初にdeviseを入れてありますので、それを利用してログイン状態でなければ追加・編集・削除を実施できないようにします。

controllerのbefore_actionを使って、ログイン済みの場合だけ表示を認可します。
現時点では複雑な権限設定は持たせていないので、単純に「ログイン済みであれば誰でもOK」という仕組みです。8

contentsは、indexアクションは誰でもOKなので、それ以外のアクションに認可をかけています。

contents_controller.rb
  before_action :authenticate_user!, only: [:show, :new, :edit, :create, :update, :destroy]

認可されていないアクションを実行しようとすると、ログイン画面にリダイレクトされます。
スクリーンショット 2019-11-30 16.09.58.png

masterブランチへマージする

最低限ではありますが、作りたかった機能が出来上がったので、自分にプルリクを出してmasterブランチへマージします。

まず、テストを流して問題が発生していないことを確認。

$ bundle exec rspec spec/system/contents_spec.rb
(中略)
Finished in 4.7 seconds (files took 1.95 seconds to load)
1 example, 0 failures

featureブランチにcommit&push。9

$ git add .
$ git commit -m "add auth to contents"
$ git push

GitHub側で、プルリクエストを出します。
スクリーンショット 2019-12-01 15.12.46.png

マージします。
スクリーンショット 2019-12-01 15.16.04.png

これで、masterブランチに開発内容を入れることができました。

最後にローカル側で、今後の作業に備えてmasterブランチをcheckoutしておきます。うっかりfeatureブランチからさらに別のfeatureブランチを派生させないために。個人開発なので、master+feature1本ずつで運用する方針です。

$ git fetch
$ git checkout master
Switched to branch 'master'
Your branch is behind 'origin/master' by 8 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)
$ git pull

まとめ

この記事では、contentsの編集画面をRailsのscaffold機能でサクッと作った上で、contentsの一覧と詳細をREST API経由でJSONとして取得できるところまでを作りました。

次の記事(鋭意制作&執筆中)では、Vue.js&Nuxt.jsで作成したSPAから、このAPIを叩いて取得した情報をいい感じに表示するところを作ります。

自己最長記事なので推敲にかなり時間をかけましたが、おかしいところがありましたら教えて頂けますと幸いです。

参考にした書籍・Webサイト

執筆者の皆様、本当にありがとうございます!


  1. 初代は静的サイトとして構築したので、コンテンツを更新するときにサイト自体をデプロイし直す必要がありました。今度はDBと連携させて、コンテンツの更新を容易にしようと考えています。ちなみに初代を構築した時の記事はこちら。https://qiita.com/shozzy/items/dadea4181d6219d2d326 

  2. 少し前にRails6がリリースされていますが、Rails5.xで開発を進めてきたのでまだバージョンアップしていません。 

  3. ここまではmasterに直接コミットしていますが、この先はそういうことはしません。 

  4. ここでは db/migrate/20190923152109_create_contents.rb でした。 

  5. https://qiita.com/ttiger55/items/d144b8094d61b70955bf にあるように、デフォルトのJSON生成機構では速度が遅いようですが、それは今後の課題としてここでは触れません。 

  6. このサンプルでは、一覧に全ての情報が含まれているので、明細を取得しても意味がありません。むしろ、URLなど不要な情報も含んでいるので、必要なデータだけ返すように改善が必要ですね。 

  7. APIキーとリファラの組み合わせをチェックするとか。 

  8. 自分用のアカウントだけ発行する想定なのでこれで十分という判断です。アカウントごとに権限レベルによる画面制御を細かく掛けるなら、もっとしっかり作り込む必要があります。 

  9. 実際には、featureブランチにはもう少しこまめにコミット&プッシュしていました。1機能分の進捗があった時と、作業が途切れるタイミングで。 

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

初心者 データベース基礎 Rails

いろいろ調べて自分なりにまとめました。

リレーショナル型データベース(RDB)とは
リレーショナル型データベース(以下、リレーショナル・データベース)は、データを行(レコード)と列(カラム)から構成される2次元の表形式で表します。列は各項目を表し、行はデータのエントリー(レコード)を表します。データ同士は複数の表と表の関係によって関連付けられ、SQL(構造化問い合わせ言語)によりユーザーの目的に応じて自由な形式で簡単に操作できます。そして、リレーショナル・データベースは、重複排除や一元管理の為のルールももっている。

①データベースの役割
データベース基本操作 (CRUD)
Create 作成・保存
Read 取得
Update 更新
Delete 削除

②SQLとは
SQLは、RDBMSのデータ操作や定義を行うための言語。

SQLの3つの役割

1.データ操作のSQL
DML(Data Manipulation Language)
INSERT | Create | 作成・保存 |
SELECT | Read | 取得 |
UPDATE | Update | 更新 |
DELETE | Delete | 削除 |

2.データを定義するDDL(Data Definition Language)
(データを格納するためのデータベースやテーブルを作成する)

CREATE | 新しいデータベース、テーブルを作成する
ALTER | データベースやテーブルの更新
DROP | 既に存在するデータベースや、テーブルを削除する

3.データ制御のSQL
アカウントによって権限があり、アクセスできるアカウントもあれば、できないアカウントもあるような
制御を行う。

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

【Rails】Couldn't find [model_name]without an というエラーが出た時の解決策

Screen Shot 0001-12-01 at 14.25.22.png

こんなエラーが出た

解決策

rails routesでルート確認
config/routes.rbで確認

私の場合、config/routes.rbの
resources :consignment
resource :consignmentになってました

rails route

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

Docker/Rails/ReactをつかってHelloWorld!

初めまして。

プログラミングを始めてからあと四ヶ月で一年が経とうとしている。

本当に時間が立つのは早い...

今回は、RailsとReactを使って、HelloWorldをしてみる。

Railsを主にバックエンド、Reactをフロント、データベースはPostgresSQLを使用する。

1.Dockerで環境構築
2.RailsとReactの導入

  • RailsTutorialを完走し、簡易的なアプリ開発経験があるレベル。
  • ReactTutorial
  • Dockerインストール

追記(注意事項)

記事を書き終わった後に、RailsへのReact導入の方法がこれがベストではない感じがしてきました。

https://qiita.com/ry_2718/items/9b824a3f9ca750ce403e

rails new . --skip-coffee --skip-turbolinks --skip-sprockets --webpack=vue

rails 5.1からはこれでAssetpiplineの代わりにwebpackを導入することができるみたい。
情報収拾の仕方を改めて考えさせられました。最近になってからは公式リファレンスや、検索機能に1ヶ月以内などを指定して、英語の記事でもGoogle翻訳を駆使しながら頑張って読んでいる...。

Dockerで環境構築をする

Dockerとは?なぜDockerか。 (読まなくていい)(間違ってたらすいません)

そもそもOSとは

http://www.toha-search.com/it/os.htm

OSとはOperation System(オペレーティング・システム)の略で、アプリやデバイスを動作させるための基本となるソフトウェアのことです。 具体的には、キーボードやマウス・タッチパッドなどのデバイスから入力した情報をアプリケーションに伝え、またソフトウェアとハードウェアの連携を司る中枢的な役割を果たします。

つまり、OSはハードウェアや入力デバイス出力デバイス、アプリケーションなどを容易に操作するためのもの。

それで、このOSの上でどんな感じで仮想環境を作るかで違いがでる。

https://udemy.benesse.co.jp/development/web/docker.html

ハードウェアを仮想化し、複数のサーバを構築できる仕組みは変わりません。ただ、コンテナは1つのOSを共有して利用しているのに対し、仮想マシンはサーバごとにOSをインストールし動かしていきます

つまり、

  • 仮想マシンはホストOSの上でもう一つのOS(ゲストOS)を起動すること。(VirtualBoxとか)
    virtual boxとかを使ったことがある人はわかると思うが、仮想化させたいOSイメージを指定した後、設定で仮想化したOSが使用するハードディスクやメモリの分割を行う。<-結果的にゲストOSとホストOSが同時にメモリを占有するので処理が重たい

  • コンテナは仮想化をホストOSの上で行う(Dockerとか)
    コンテナでは、ホストOSの上で直接仮想化する(ゲストOSを建てない)ので非常に動作が軽い。Docker上であれば基本的に環境の差異による影響を受けない
    また、DockerにはDockerHubというのがあり、そこからすでに環境が構築されたテンプレートや、MySQLやRubyなどのツールや言語をDocker上にイメージとしてインストールしてくれる。

Dockerの基本コマンド

とりあえず目を通して、どんな動作を行うコマンドがあるかみてください。
初心者用Docker基本コマンド一覧(新旧スタイル対応)
DockerComposeの基本

Dockefileとdocker-compose.ymlの設定

まずはDockerで環境構築

$ mkdir myblog
$ cd myblog
$ touch {Dockerfile,docker-compose.yml}

Dockerfileはこの記事が非常にわかりやすいです。
Docker初心者がRails + PostgreSQL or MySQLで仮想環境構築した手順を丁寧にまとめる

Dockerfile 解説
FROM dockerhubからイメージをダウンロード
WORKDIR 作業ディレクトリの指定
RUN コマンドの実行
COPY 引数1を引数2にコピー

yarnインストール参考docker for macでrails × yarn × webpackerのfront環境を整える

myblog/Dockerfile
FROM ruby:2.5.5
RUN apt-get update && apt-get install -y build-essential nodejs libpq-dev 

#yarnインストール webpackで必要になります。
RUN curl apt-transport-https wget && \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && apt-get install -y yarn

RUN mkdir /rails
WORKDIR /rails
COPY Gemfile /rails/Gemfile
COPY Gemfile.lock /rails/Gemfile.lock
RUN bundle install
COPY . /rails

docker-composeはこの記事が非常にわかりやすいです
docker-compose.ymlの書き方について解説してみた

docker-compose 解説
version docker-composeの文法はバージョンごとにことなるので指定が必要
servise 動かすアプリケーションの指定。ここでは、webとdb。
Service設定する際の項目について
docker-compose.yml
version: '3' 
services: 

  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/rails
    ports:
      - "3000:3000" #ポート3000番を開けてコンテナの3000番に転送
    depends_on:
      - db

  db:
    image: postgres
    volumes:
      - datavol:/var/lib/postgresql/data

volumes:
  datavol:

Railsアプリを作る。

以下のコマンドを入力

$ touch {Gemfile,Gemfile.lock}

$ echo "source 'https://rubygems.org' 
gem 'rails','5.1.4'
gem 'pg', '~> 0.20.0'" > Gemfile

$ docker-compose run web bundle exec rails new . --force --database=postgresql

$ docker-compose build

ここまででRailsサーバーを立ち上げる準備が整っているはずなので立ち上げてみる。

$ docker-compose up -d //サーバー起動
$ docker-compose run web rake db:create //db作成

ここにアクセス
みなさんは成功したでしょうか??....

Reactを導入

とりあえずRailsの初期画面から変更を行う。

コントローラーを作ろう

$ rails g controller StaticPages home about contact

ルートの設定

routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
  get '/about', to: 'static_pages#about'
  get '/contact', to: 'static_pages#contact'
end

これでReactでviewに変更を加える準備ができました。

gem追加

$ echo "gem 'webpacker'
gem 'react-rails'">>Gemfile

$ docker-compose run web bundle update

webpack設定

$ docker-compose run web rails webpacker:install 
$ docker-compose run web rails webpacker:install:react

ここまでくると、app/assets/javascriptというファイルが作成される。
この中のファイルがreactファイルになっている。

試しにrailsのviewに呼び出したいころではあるが、railsサーバーを再起動しないと反映されないのでrailsコンテナを再起動。

$ docker ps //稼働中のコンテナの表示
0739cbd77243        170064292a20        "bundle exec rails s…"   12 hours ago        Up 12 hours         0.0.0.0:3000->3000/tcp   myblog_web_1
0d302bae2084        postgres            "docker-entrypoint.s…"   13 hours ago        Up 13 hours         5432/tcp                 myblog_db_1

上の場合だと

0739cbd77243これがrailsコンテナのIDになるので、このIDを指定してコンテナの再起動をする

$ docker restart 0739cbd77243 //コンテナ起動

こうなったら成功です!おつかれさまでした!
スクリーンショット 2019-11-30 13.58.04.png

参考

Rails で postgresql を使う(インストールからマイグレーションまで)

Docker初心者がRails + PostgreSQL or MySQLで仮想環境構築した手順を丁寧にまとめる
既存のRailsアプリにReactを導入する方法

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

Railsチュートリアル 第10章 ユーザーの更新・表示・削除 - ユーザーを削除する

何をするか

  • ユーザー削除が可能な権限を持つ管理ユーザーのクラスの実装
  • ユーザーを削除するためのリンクの追加
  • RDBからユーザーを削除する動作の実装

ここまでの実装が完了すれば、Userリソースに対し、RESTが求めるすべての動作の実装が完了することになります。

Railsチュートリアル本文においては、ユーザーを削除するためのリンクを追加したサイトレイアウトのモックアップは、図 10.13に示されています。

管理ユーザー

Userモデルに、boolean型の値を取るadminという属性を追加します。特権を持つ管理ユーザーを識別するために用いる属性です。

User_full.png

boolean型のadmin属性をUserモデルに追加すると、Railsによって、Userモデルのadmin?というメソッドが自動で追加されます。

マイグレーションの生成と修正

続いて、マイグレーションを生成します。

# rails generate migration add_admin_to_users admin:boolean
Running via Spring preloader in process 12240
      invoke  active_record
      create    db/migrate/[timestamp]_add_admin_to_users.rb

db/migrate/[timestamp]_add_admin_to_users.rbというマイグレーションが生成されました。ただ、生成されたマイグレーションに若干の修正を加える必要があります。

db/migrate/[timestamp]_add_admin_to_users.rb
  class AddAdminToUsers < ActiveRecord::Migration[5.1]
    def change
-     add_column :users, :admin, :boolean
+     add_column :users, :admin, :boolean, default: false
    end
  end

default: falseという引数を与えています。「デフォルトでは管理権限はない」ということを明示するためです。

後はマイグレーションを実行すれば、Userモデルにadmin属性が実装され、admin?メソッドも使えるようになります。

# rails db:migrate
== [timestamp] AddAdminToUsers: migrating ==================================
-- add_column(:users, :admin, :boolean)
   -> 0.0152s
== [timestamp] AddAdminToUsers: migrated (0.0186s) =========================

admin?メソッドを試してみる

# rails console --sandbox

>> user = User.first
  User Load (4.6ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, ..., admin: nil>
>> user.admin?
=> false
>> user.toggle!(:admin)
   (0.2ms)  SAVEPOINT active_record_1
  SQL (37.0ms)  UPDATE "users" SET "updated_at" = ?, "admin" = ? WHERE "users"."id" = ?  [["updated_at", "2019-11-28 21:42:04.018070"], ["admin", "t"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
>> user.admin
=> true

ここでは、toggle!メソッドを使って、id=1のユーザーのadmin属性値をfalseからtrueに反転しています。follow_redirect!find_by!等の!とは異なり、この場合の!は「破壊的代入」という意味ですね。

id=1のユーザーのみ、デフォルトで管理者とする

まず、db/seeds.rbの内容を変更し、id=1のユーザーをデフォルトで管理者とするようにサンプルデータ生成タスクの内容を変更します。

db/seeds.rb
  User.create(name:                  "Example User",
              email:                 "example@railstutorial.org",
              password:              "foobar",
-             password_confirmation: "foobar")
+             password_confirmation: "foobar",
+             admin: true)

  99.times do |n|
    name = Faker::Name.name
    email = "example-#{n+1}@railstutorial.org"
    password = "password"
    User.create!( name:                  name,
                  email:                 email,
                  password:              password,
                  password_confirmation: password)
  end

次に、データベースをリセットし、サンプルデータを再生成します。

# rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
...略

# rails db:seed

rails db:seedは、正常に完了した場合、シェルには何も表示されません。

なお、私はここで少々つまづきました。顛末はdb/seeds.rbの中身を変更したらrails db:seedできない。そんなときにありがちな原因に記述ています。

Strong Parametersにより、admin属性を保護する

特に対策をしていない場合、例えば以下のようなPATCHリクエストにより、任意のユーザーのadmin属性の値を変更することができてしまいます。

patch /users/17?admin=1

UNIX系OSのroot権限もそうですが、「任意のユーザーが、任意の属性を変更可能である」というのは非常に危険な状態です。なんとしても「一般ユーザーが編集できてはいけない属性を編集できないようにする」という仕組みを実装する必要があります。

4.0以降のRailsにおいては、「一般ユーザーが編集できてはいけない属性を編集できないようにする」という要求を実現する仕組みとして、Strong Parametersという機能が実装されています。Railsチュートリアル本文では、節7.3.2で言及されていました。

Railsチュートリアルをここまで順番通りに進めていれば、「Strong Parametersによってadmin属性を保護する」という仕組みは、サンプルアプリケーションにすでに実装されているはずです。コードは以下です。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  # ...略
  private

    def user_params
      params.require(:user).permit(:name, :email, :password, :password_confirmation)
    end
    # ...略
end

user_paramsメソッド内のparams.require(:user).permitの引数に:adminが含まれていないことがポイントです。この実装により、「任意のユーザーが自分に管理者権限を与えること」は防止されています。

演習 - 管理ユーザー

1.1. Web経由でadmin属性を変更できないことを確認するテストを実装するために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
    ...略

    private

      def user_params
-       params.require(:user).permit(:name, :email, :password, :password_confirmation)
+       params.require(:user).permit(:name, :email, :password, :password_confirmation, :admin)
      end
      ...略
  end

1.2. Web経由でadmin属性を変更できないことを確認してみましょう。

具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL (/users/:id) に送信するテストを作成してみてください。

以下のようなテストをtest/controllers/users_controller_test.rbに追加します。

test "should not allow the admin attribute to be edited via the web" do
  log_in_as(@other_user)
  assert_not @other_user.admin?
  patch user_path(@other_user), params: { user: { password:              'password',
                                                  password_confirmation: 'password',
                                                  admin: true } }
  assert_not @other_user.reload.admin?
end

ポイントは以下です。

  • 以下の値は、@other_user.passwordとするとupdateが正常に完了しない
    • params[:user][:password]
    • params[:user][:password_confirmation]
  • reloadにより、RDBに保存された@other_userを再度読み込む必要がある

特にparams[:user][:password]およびparams[:user][:password_confirmation]の値については、1時間くらい悩んでしまいました。

このようなときに決め手となるのはやはりdebuggerですね。

app/controllers/users_controller.rb#update
  def update
+   debugger
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
(byebug) user_params
<ActionController::Parameters {"password"=>nil, "password_confirmation"=>nil, "admin"=>"true"} permitted: true>
"password"=>nil, "password_confirmation"=>nil

ここを見てようやく気づきました。params[:user][:password]params[:user][:password_confirmation]には、生文字列を与えなければダメなのだと…。

テストが正しい振る舞いをしているかどうか確信を得るために、最初のテストの結果はredになるはずです。

# rails test test/controllers/users_controller_test.rb:35
Running via Spring preloader in process 12914
Started with run options --seed 32377

 FAIL["test_should_not_allow_the_admin_attribute_to_be_edited_via_the_web", UsersControllerTest, 0.8165606999828015]
 test_should_not_allow_the_admin_attribute_to_be_edited_via_the_web#UsersControllerTest (0.82s)
        Expected true to be nil or false
        test/controllers/users_controller_test.rb:41:in `block in <class:UsersControllerTest>'

  7/7: [===================================] 100% Time: 00:00:00, Time: 00:00:00

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

悩み悩んだ挙げ句、ようやくテストが失敗してくれました。

test/controllers/users_controller_test.rb(41行目)
assert_not @other_user.reload.admin?

現在の私の環境では、test/controllers/users_controller_test.rbの41行目は上記コードです。というわけで、テスト失敗時のメッセージの内容も、:admin属性の値がtrueになっていますという内容です。ここまで長かった。

user_paramsの内容を本来の実装に戻して、もう一度テスト

app/controllers/users_controller.rb
  class UsersController < ApplicationController
    ...略

    private

      def user_params
-       params.require(:user).permit(:name, :email, :password, :password_confirmation, :admin)
+       params.require(:user).permit(:name, :email, :password, :password_confirmation)
      end

      ...略
  end
# rails test test/controllers/users_controller_test.rb:35
Running via Spring preloader in process 12927
Started with run options --seed 30515

  7/7: [===================================] 100% Time: 00:00:00, Time: 00:00:00

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

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

destroyアクション

indexビュー用のuserパーシャルに、ユーザー削除用のリンクを追加する

続いて編集するのは、indexビュー用のuserパーシャルです。追加するのはユーザー削除用のリンクで、「管理者のみに表示される」という要件を満たす必要があります。

ユーザー削除用のリンクの実体

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

重要と思われるポイントは以下です。

  • if文について
    • 「管理者のみに表示される」という要件を満たすために、current_user.admin?という記述がなされている
    • !current_user?(user)というのは、「現在ログイン中のユーザーが自分自身を削除することはできない」ことを意味する
  • DELETEリクエストを発行するリンクの生成は、link_toの引数にmethod: :deleteという記述があることがポイントとなる
  • 確認メッセージを表示するようにしている

2019年現在のHTMLの仕様では、HTMLのフォームは直接DELETE(あるいはPATCHPUT)リクエストを発行することはできません。そのため、Railsその他Webフレームワークでは、フレームワーク内部で実装された何らかの仕組みによってDELETEリクエストを発行するようにしています。

実際にindexビュー用のuserパーシャルの内容を変更する

app/views/users/_user.html.erbに、以下の変更を加えていきます。

app/views/users/_user.html.erb
  <li>
    <%= gravatar_for user, size: 50 %>
    <%= link_to user.name, user %>
+   <% if current_user.admin? && !current_user?(user) %>
+     | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %>
+   <% end %>
  </li>

(管理ユーザーでログインしている場合)deleteリンクがindexに表示されるようになる

スクリーンショット 2019-11-30 19.52.43.png

ここまで実装が完了すれば、(管理ユーザーでログインしている場合に)deleteリンクがindexに表示されるようになるようになります。

destroyアクションの実装

deleteリンクをクリックして実際にRDBからユーザーが削除されるようにするためには、Usersコントローラーにdestroyアクションを実装する必要があります。

destroyアクションの実装内容は以下のとおりになります。

UsersController#destroy
def destroy
  User.find(params[:id]).destroy
  flash[:success] = "User deleted"
  redirect_to users_url
end

User.find.destroyというメソッドチェーンが「RDBからユーザーを削除する」という動作の実体ですね。以降、「フラッシュメッセージの定義」からの「indexページへのリダイレクト」へと続きます。

destroyアクションをbeforeフィルターの対象にする

destroyアクションも、editupdateと同様に「ログインユーザーのみが実行できる」ようにする必要があります。コードは以下です。

ログインユーザーでなければdestroyアクションを実行できない

before_action :logged_in_user, only: [:index, :edit, :update, :destroy]

管理ユーザーでなければdestroyアクションを実行できない

さらに、「destroyアクションは管理ユーザーのみが実行できる」という保護も必要となります。「cURLなどのツールを使って、直接DELETEリクエストを対象リソースに送りつける」という想定外のアクセスを防ぐ必要があるためです。コードは以下です。

before_action :admin_user, only: :destroy

:admin_userを第一引数とするbefore_actionにおいて、:onlyをキーとするハッシュに与えているのは配列ではありません。:destroyというシンボルを直接与えています。この点は、:logged_in_user:correct_userを第一引数とするbefore_actionとは異なっています。

admin_userメソッドそのものの定義

admin_userメソッドそのものの定義は以下です。

def admin_user
  redirect_to(root_url) unless current_user.admin?
end

「管理ユーザーでなければ / にリダイレクトされる」という動作になります。

以上の実装をapp/controllers/users_controller.rbに反映する

app/controllers/users_controller.rb全体の変更内容は以下のとおりです。

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

    ...略
+
+   def destroy
+     User.find(params[:id]).destroy
+     flash[:success] = "User deleted"
+     redirect_to users_url
+   end

    private

      ...略
+
+     # 管理者かどうか確認
+     def admin_user
+       redirect_to(root_url) unless current_user.admin?
+     end
  end

演習 - destroyアクション

1. 管理者ユーザーとしてログインし、試しにサンプルユーザを2〜3人削除してみましょう。ユーザーを削除すると、Railsサーバーのログにはどのような情報が表示されるでしょうか?

Started DELETE "/users/100" ...略
Processing by UsersController#destroy as HTML
  Parameters: {"authenticity_token"=>"rhQj0KvhGPwbu2uDnnux9jBIBpt6RC00ly1hoqSWPiE3EHv2wyrDvy003EsFWoANTfm79aAsGcTQbHQUpIO9ng==", "id"=>"100"}
  User Load (2.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (2.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 100], ["LIMIT", 1]]
   (1.2ms)  begin transaction
  SQL (11.4ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 100]]
   (8.3ms)  commit transaction
Redirected to http://localhost:8080/users
Completed 302 Found in 119ms (ActiveRecord: 39.5ms)


Started GET "/users" ...略
Processing by UsersController#index as HTML
  User Load (2.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Rendering users/index.html.erb within layouts/application
   (2.5ms)  SELECT COUNT(*) FROM "users"
  User Load (2.3ms)  SELECT  "users".* FROM "users" LIMIT ? OFFSET ?  [["LIMIT", 30], ["OFFSET", 0]]
  ...略
Completed 200 OK in 370ms (Views: 342.7ms | ActiveRecord: 7.2ms)
  • /users/100 へのDELETEリクエストに対して
    • idが100であるユーザーを対象として、SQLのDELETE文が発行されている
    • 最終的には /users へのリダイレクトでDELETEリクエストが完了している
  • /usersへのGETリクエストに対して
    • 特に変わった動作はない

フラッシュメッセージの内容

UsersController#destroy
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
+   debugger
    redirect_to users_url
  end

以上のようにして、flash[:success]の内容を確認してみましょう。

(byebug) flash[:success]
"User deleted"

フラッシュメッセージの内容にも問題ないようですね。

ユーザー削除のテスト

fixture内で最初に登場するユーザーを管理ユーザーとする

ユーザー削除、すなわちdestroyアクションの動作をテストするためには、fixtureにも管理ユーザーが必要となります。最初に出てくるユーザー、ここでは:rhakureiさんを管理ユーザーとしましょう。

  rhakurei:
    name: Reimu Hakurei
    email: rhakurei@example.com
    password_digest: <%= User.digest('password') %>
+   admin: true

  mkirisame:
    name: Marisa Kirisame
    email: example.example@example.org
    password_digest: <%= User.digest('password') %>

  skomeiji:
    name: Satori Komeiji
    email: example_example@example.net
    password_digest: <%= User.digest('password') %>

  rusami:
    name: Renko Usami
    email: example0@example.com
    password_digest: <%= User.digest('password') %>

  <% 30.times do |n| %>
  user_<%= n %>:
    name:  <%= "User #{n}" %>
    email: <%= "user-#{n}@example.com" %>
    password_digest: <%= User.digest('password') %>
  <% end %>

管理ユーザーでないユーザーがdestroyアクションを実行しようとした場合に対するテスト

表題のような操作は、Usersコントローラーによって拒否されます。そのため、テストの実装箇所はtest/controllers/users_controller_test.rbとなります。

非ログインユーザーがdestroyアクションを実行しようとした場合に対するテスト

test "should redirect destroy when not logged in" do
  assert_no_difference 'User.count' do
    delete user_path(@user)
  end
  assert_redirected_to login_url
end

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

  • @userに対してDELETEリクエストを発行し、前後でユーザー数が変わっていなければOK
  • @userに対してDELETEリクエストを発行した後に、 /login にリダイレクトされればOK

ログイン済みの非管理ユーザーがdestroyアクションを実行しようとした場合に対するテスト

test "should redirect destroy when logged in as a non-admin" do
  log_in_as(@other_user)
  assert_no_difference 'User.count' do
    delete user_path(@user)
  end
  assert_redirected_to root_url
end

こちらのテストの内容は以下となります。

  • @userに対してDELETEリクエストを発行し、前後でユーザー数が変わっていなければOK
  • @userに対してDELETEリクエストを発行した後に、 / にリダイレクトされればOK

ここまでのテストを実装する

上記の記述を踏まえ、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 destroy when not logged in" do
+     assert_no_difference 'User.count' do
+       delete user_path(@user)
+     end
+     assert_redirected_to login_url
+   end
+
+   test "should redirect destroy when logged in as a non-admin" do
+     log_in_as(@other_user)
+     assert_no_difference 'User.count' do
+       delete user_path(@user)
+     end
+     assert_redirected_to root_url
+   end
  end

実装内容に問題がなければ、この時点でテストは成功するはずです。

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

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

Finished in 2.68050s
9 tests, 18 assertions, 0 failures, 0 errors, 0 skips

管理ユーザーがdestroyアクションを実行する場合に対するテスト、および、indexビューにおける"delete"リンクの表示に関するテスト

表題のパターンではdestroyアクションが成功します。そのため、テストによる影響範囲は、「Usersコントローラーの動作」のみならず、「Userモデルの内容」にまで及びます。コントローラーとモデルの双方に影響範囲が及ぶテストなので、実装箇所は統合テストとなります。今回は「/index 上のリンクからdestroyアクションを呼び出す」という動作に対するテストなので、より詳細な実装箇所はtest/integration/users_index_test.rbです。

また、test/integration/users_index_test.rbに実装するテストには、「indexビューに削除リンクが表示されるか否か」というテストも含まれます。「管理ユーザーであれば、indexビューで、自身以外の各ユーザーの削除リンクが表示される。管理ユーザーでなければ、indexビューに削除リンクは表示されない。」というのが正しい実装となります。

管理ユーザーに対する統合テスト

以下の動作に対するテストが必要となります。

  • 一覧内の、自身以外のユーザーに対して削除リンクが表示される
  • @non_adminというユーザーに対してDELETEリクエストが送信された場合、実際にRDBから当該ユーザーが削除される

対応するテストのコードは以下となります。

test "index as admin including pagination and delete links" do
  log_in_as(@admin)
  get users_path
  assert_template 'users/index'
  assert_select 'div.pagination'
  first_page_of_users = User.paginate(page: 1)
  first_page_of_users.each do |user|
    assert_select 'a[href=?]', user_path(user), text: user.name
    unless user == @admin
      assert_select 'a[href=?]', user_path(user), text: 'delete'
    end
  end
  assert_difference 'User.count', -1 do
    delete user_path(@non_admin)
  end
end

なお、このテストには、「indexビューに実装された、ページネーション機能に対するテスト」も含まれます。個人的には、「一つのテストコードで多くの機能に対するテストを詰め込みすぎではないか」という気はします。

非管理ユーザーのindexビューに対するテスト

非管理ユーザーのindexビューに対しては、以下の動作に対するテストが必要となります。

  • 一覧内に削除リンクが表示されない

対応するテストのコードは以下になります。

test "index as non-admin" do
  log_in_as(@non_admin)
  get users_path
  assert_select 'a', text: 'delete', count: 0
end

個人的には、テストの粒度はこれくらいが丁度いい気がします。

test/integration/users_index_test.rbの内容

差分ではなく、test/integration/users_index_test.rb全体を丸ごと書き換えてしまいます。コードは以下のとおりです。

`test/integration/users_index_test.rb`
require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest
  def setup
    @admin = users(:rhakurei)
    @non_admin = users(:mkirisame)
  end

  test "index as admin including pagination and delete links" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    first_page_of_users = User.paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
  end

  test "index as non-admin" do
    log_in_as(@non_admin)
    get users_path
    assert_select 'a', text: 'delete', count: 0
  end
end

この時点でテストは成功する

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

# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 13117
Started with run options --seed 23568

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

Finished in 2.94128s
2 tests, 63 assertions, 0 failures, 0 errors, 0 skips

ここまでの実装内容に問題がなければ、テストは成功するはずです。

演習 - ユーザー削除のテスト

1. 試しにリスト 10.59にある管理者ユーザーのbeforeフィルターをコメントアウトしてみて、テストの結果がredに変わることを確認してみましょう。

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

    ...略
  end
# rails test
Running via Spring preloader in process 13156
Started with run options --seed 29997

 FAIL["test_should_redirect_destroy_when_logged_in_as_a_non-admin", UsersControllerTest, 3.8563491000095382]
 test_should_redirect_destroy_when_logged_in_as_a_non-admin#UsersControllerTest (3.86s)
        "User.count" didn't change by 0.
        Expected: 34
          Actual: 33
        test/controllers/users_controller_test.rb:68:in `block in <class:UsersControllerTest>'

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

Finished in 4.65755s
43 tests, 182 assertions, 1 failures, 0 errors, 0 skips

テストは確かに失敗します。test/controllers/users_controller_test.rbの「should redirect destroy when logged in as a non-admin」というテストで失敗しているようですね。当該テストのコードは以下です。

test "should redirect destroy when logged in as a non-admin" do
  log_in_as(@other_user)
  assert_no_difference 'User.count' do
    delete user_path(@user)
  end
  assert_redirected_to root_url
end

当該テストの要求は、「非管理ユーザーがDELETEリクエスト(→destroyアクション)を送出しても、RDBのレコード件数は変化してはならない」というものです。コードは以下です。

assert_no_difference 'User.count' do
  delete user_path(@user)
end

しかしながら、管理者ユーザーのbeforeフィルターがコメントアウトされていると、「非管理ユーザーがDELETEリクエスト(→destroyアクション)を送出しても、RDBのレコード件数が変化する」という事態が発生してしまいます。

"User.count" didn't change by 0.
  Expected: 34
    Actual: 33

そのため、テストが失敗するのです。

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

【Rails】add_indexについて

indexってなんだ

特定のカラムからデータを取得する際に、テーブルの中の特定のカラムのデータを複製し検索が行いやすいようにしたもの。

Usersテーブルのnameカラムにindexを貼ることで、アルファベット順にnameを並べ替え検索しやすいようにしてくれる

メリットとデメリット

メリット:データの読み込み・取得が早くなる。
デメリット: 書き込みの速度が倍かかる

ある程度多くのデータを格納するテーブルの、格納される値がその値がそれぞれ異なるようなカラムの中で、検索がよく行われるカラムに対して春と効果的

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

dependentオプションまとめ

はじめに

userが複数のpostを持っていると仮定する

class User < ActiveRecord::Base
  has_many :posts
end
class Post < ActiveRecord::Base
  belongs_to :user
end

ここで、userが削除された場合、userに紐づいているpostsをどうするかをdependentオプションで指定できる

userと一緒にpostsを削除する場合

パターン1
class User < ActiveRecord::Base
  has_many :posts, :dependent => :destroy
end
  • 基本これを覚える
  • ActiveRecordを介して削除(コールバック処理が実行される)
  • クエリがuserに紐づいているpostsの数だけ実行される
パターン2
class User < ActiveRecord::Base
  has_many :posts, :dependent => :delete_all
end
  • SQLを直接実行して削除(コールバック処理は実行されない)
  • クエリが1回実行される

userだけ削除する(postsは削除しない)場合

class User < ActiveRecord::Base
  has_many :posts, :dependent => :nullify
end
  • postsレコードのuser_idをnull更新する

例外やエラーを発生させる場合

例えば、「postsを持っているuserなので、退会できません」などのエラーメッセージを出したい時などに便利

パターン1
class User < ActiveRecord::Base
  has_many :posts, :dependent => :restrict_with_error
end
  • restrict_with_error:エラーとなる。(ActiveRecordのerrorとして扱われる)
  • userレコードにエラー情報が付加される
パターン2
class User < ActiveRecord::Base
  has_many :posts, :dependent => :restrict_with_exception
end
  • 例外を発生させる。(DeleteRestrictionErrorがraiseする)

参考情報

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

RubyonRails 環境構築 【Mac版】

はじめに

OCAではRailsを中心にプログラミングの学習を行っていただきますが、そもそもプログラミング初心者が最初に躓くことってなんだと思いますか?
アルゴリズムだとか、変数がわからないとか、それよりも前に『開発環境が作れない』ということが多々あります。
特にWeb系の言語は、初学者でも入りやすい言語だとは思いますが、環境を作るにはコマンドを使ったりしないといけなかったりで、導入のハードルは少し高く感じます。
そこでOCAでは、dockerを使って開発環境を用意することで、環境構築でつまずいてしまうという事態を回避しています。

この記事では、一度回避した環境構築に立ち返り、より理解を深めようという内容です。

環境

今回作成する環境は次の通りです

  • Homebrew 2.2.0
  • ruby 2.6.3
  • Rails 6.0.1

Homebrewのインストール

まずはHomebrewをインストールします。
HomebrewはMacOSのパッケージ管理ツールです。
Homebrewは色々なタイミングで使うことがあるので、入れていない人はこの機会にいれておきましょう

まずは以下のコマンドを叩いてHomebrewがインストールされていないことを確認してください。
この記事に出るコマンドすべてに言えることですが、頭の$はコマンドであることを示しているため、打たなくて大丈夫です

$ brew -v

さて、まずはHomebrewのインストールと言いましたが、あれは嘘です
以下のコマンドを入力し、コマンドライン・デベロッパーツールをインストールします。
AppstoreからXCodeをインストールしてもOKな様子

$ xcode-select --install

ダイアログが出たりすると思いますが気にせず進めましょう。

インストールが終わったら、今度こそHomebrewをインストールします

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

インストールが終わったら、正常にインストールが完了したか確認します。

$ brew docker

さて、ここで勘のいい方ならあることに気づくかと思います。

「あれ、rubyのコマンド使ってない?」

そうです。今からrubyを入れようとしているのに、rubyのコマンドを使用しました。

実はmacにはrubyが標準搭載されています。
ですが、バージョンが古かったり、別バージョンを使う上で管理が面倒なので、別の方法でrubyを入れ直すというわけです。

rubyをインストール

次に、rubyをインストールするためのツールをインストールします。

以下のコマンドを実行します。

$ brew install rbenv ruby-build

rbenv はrubyをインストールするためのツールであり、rubyのバージョンを切り替えるためのツールでもあります。

インストールが終わったら、rbenvのパスを通します。
簡単に説明すると、パスを通すと、ディレクトリのどの位置にいてもコマンドのフルパスを書かずともそのコマンドを実行できるというメリットがあります。

$ echo 'export PATH="~/.rbenv/shims:/usr/local/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile

パスを通し終わったら、以下のコマンドを実行してみます。

$ rbenv install --list

今のコマンドは、rbenvを使ってインストールできるrubyのツールが表示されます。
ここでは最新版である 2.6.3 があるか確認しましょう。
他にインストールしたいバージョンが決まっていれば、そのバージョンを探してください。

バージョンが決まれば、以下のコマンドを順番に実行してrubyをインストールします。

$ rbenv install 2.6.3
$ rbenv global 2.6.3
$ rbenv rehash

インストールが完了したら、以下のコマンドで確認しましょう。

$ ruby -v

ここで表示されたrubyのバージョンが今インストールしたバージョンと同一か確認してください。
もし違う場合は、以下のコマンドを実行してインストールされているかを確認します。

$ rbenv versions

rbenv globalコマンドを使えば指定のバージョンに切り替えることが可能です。

Railsをインストール

まずは作業ディレクトリを決めましょう
今の場所を確認するときは、pwdコマンドを使います。
今の実行場所が嫌な人はcdコマンドを使って移動しましょう。

作業ディレクトを決めたら、次のコマンドを実行します。

$ rbenv local 2.6.3

このコマンドを使うと、今いるディレクトリで作業するときには、rubyのバージョンが2.6.3に固定されます。
これにより、他のプロジェクトで違うrubyバージョンを扱うことになっても、影響がなくなります。

次に、bundlerをインストールします。
すでにインストールされていないか、次のコマンドで確認してください。

$ bundle -v

bundlerはgemを管理するためのツールで、そのアプリケーションで使われるパッケージやバージョンを管理してくれます。
複数人で開発をするときは、他のPCでもバージョンは揃えないといけないので、その役割をbundlerが担っているというわけです。
https://qiita.com/jnchito/items/99b1dbea1767a5095d85

逆にgemは、パッケージの形式だと考えてください。

bundlerもgemのひとつなので、次のコマンドを使ってインストールできます。

$ gem install bundler
$ bundle -v

bundlerにインストールが終わったら、次のコマンドでGemfileを作成します。

$ bundle init

作成されたGemfileを編集します。

せっかくなのでvimを使って編集しましょう。

$ vi Gemfile

最初はこの様になっていると思います。

# frozen_string_literal: true

source "https://rubygems.org"

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

# gem "rails"

この一番下にある # gem "rails" のコメントアウトを解除します。

矢印キーで一番下まで移動し、xキーを二回押して#と余分なスペースを消します

これで編集完了ですので、その状態で:wqと順番に入力してエンターキーを押すとvimを終了させることが出来ます。

ちなみにvimを使って何かを書き足したいときは、iキーを押して挿入モードにします。
挿入モードを解除するときは、escキーを押します。

もし万が一間違った操作をしてしまった場合は、:q!と順番に入力してエンターキーを押せば、保存せずに終了することが出来ます。
https://qiita.com/hide/items/5bfe5b322872c61a6896

Gemfileを編集したら、いよいよrailsのインストールです。
次のコマンドでrailsをインストールしましょう

$ bundle install --path=vendor/bundle

bundle installは、Gemfileの中身を見て色々インストールしてくれます。
なので先程railsのコメントアウトを外しましたので、このコマンドでrailsがインストールされます。

また、--pathオプションでは、今インストールしたパッケージの保管場所を指定しています。
これをつけないとPC全体に影響が出るグローバルな位置にインストールされます。

つけなくても問題ないとする声もありますが、同一サーバで複数のrailsを扱う場合はつけたほうがいいといった声もありますので、役割だけでも覚えておきましょう。

railsのインストールが完了したら、次にrailsプロジェクトを作成します。

$ bundle exec rails new sample

sampleの部分はそのプロジェクトの名前に適宜変えてください。
プロジェクトの作成が終わったら、cdコマンドで移動し、railsサーバーを起動します

$ cd sample
$ rails s

ブラウザからlocalhost:3000にアクセスしてWelcomeページが表示されたら成功です!

Image from Gyazo

rails s が失敗する

Webpacker configuration file not found

といったメッセージでrails sが失敗する場合があるようです。
下記コマンドを実行することで解消します。

$ rails webpacker:install

yarnを入れろと怒られた場合はこちら

$ brew install yarn

私の場合はnode.jsのバージョンが低すぎると怒られたので下記の記事を参考に修正しました。
https://qiita.com/tonkotsuboy_com/items/5322d226b6783d25b5df

おわり

こうして文章にしてみると、結構やることあるんだなと再認識しました。
スムーズに行く場合もありますが、最後のように既存の環境が原因でうまく行かないなんてこともよくあります。

エラーが起きたときは英語ばかりで何をかいてあるのかさっぱりでお手上げになりがちですが、よく読むとこうしてくださいと指示があったり、エラーの内容が書いてあったりするので、それを頼りにググればきちんと解決できます。

慌てず冷静に、素敵なコーディングライフを祈ってます

手順の参考はこちら
https://qiita.com/TAByasu/items/47c6cfbeeafad39eda07

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

RUNTEQの講師をやってみてわかった初学者にありがちなパターン20選(前編)

はじめに

今年の8月からプログラミングスクールの講師として初学者にRailsを教えてきました。またMENTAでも100名弱の方にプログラミングを教えてきました。それくらい教えると初学者がどんなところでハマりがちでどんな知識が不足しがちなのかが大体わかってきます。要は初心者あるあるですね。
それを今回Advent Calendar一発目に書くことにしました。

ではさっそくいってみましょう!

開発全般編

1. 問題がおきた場合に一気に色々と試しがち

一気に色々と試すと原因の切り分けができなくなります。
例えば「ログインができない」という問題があった時、初学者がやりがちなのはこのようなログイン処理のコードとずーーーっとにらめっこすることです。

sessions_controller.rb
def create
  @user = login(params[:email], params[:password])

  if @user
    redirect_to root_path, success: '成功'
  else
    flash.now[:danger] = '失敗'
    render :new
  end
end

色々な要素が入り混じっているのでこのコードだけを見てもどこに問題があるのか切り分けができません。

なので例えばですがこのようなアプローチで進めていけば良いのでしょう。

①そもそもこのコントローラのこのアクションが動いているか

これを確認するためにはこのように書けば良いですね。

sessions_controller.rb
def create
  p '通ってる?'
end

標準出力に何も出力されていなかったらルーティングまたはフォームの書き方に問題がある可能性が高いです。
いくらこのアクションの中の実装が正しくてもそもそもこのアクションが動いていなければ話になりませんよね。

そこが問題なければ次に進みます。

②クライアントから正しくパラメータが送られてきてるか

②-1paramsを使っているところをハードコーディングしてみる
sessions_controller.rb
def create
  @user = login('example@example.com', '12345678') # ハードコーディング

  if @user
    redirect_to root_path, success: '成功'
  else
    flash.now[:danger] = '失敗'
    render :new
  end
end

これでもしログインできたら『クライアントから正しくパラメータが送られていなかった』もしくは『送られてきたパラメータをコントローラ側でうまく扱えていなかった』ということになります。

②-2paramsの中身を確認する
sessions_controller.rb
def create
  p params
end
# 説明の便宜上色々省略してます
<ActionController::Parameters {"user_sessions"=>{"email"=>"example@example.com", "password"=>"12345678"}>

フォームに入力した値が格納されていれば一旦問題ないと言えます。それでもログインできない場合はコントローラ側でのパラメータの扱いが怪しそうです。

②-3params[:email]という書き方があっているかを確認する
sessions_controller.rb
def create
  p params[:email]
end
=> nil

もしこれでnilが出力されたらparams[:email]という書き方がおかしいということになります。
つまり今回のケースではクライアントからサーバへのデータの送り方を変えるか(name=user_sessions[:email]からname=emailに変えるか)、コントローラ側でparamsの扱い方を変えるか(params.dig(:user_sessions, :email)のように変えるか)で解決できそうです。

繰り返しになりますが一気に色々とやろうとするとどこに原因があるかがわからなくなるので、必ず問題を細分化して考えるべきです。

2. 入力と出力を意識していない

プログラムは全てインプットとアウトプットの組み合わせといっても過言ではありません。何を入力したら何が出力されるのかを意識することは非常に重要です。

例えば『自分が投稿したコメントかどうかを判定したい』というメソッドを作るとします。
その際いきなりロジックを考え始める人が初学者には非常に多いと感じました。

ロジック云々の前に考えるべきなのは

  • 何を入力して
  • どんな出力を期待するのか

という入出力です。
やり方は色々あるとは思いますが一例としてはこうでしょう。

  • 何を入力して => 判定したいコメントのオブジェクトを入力する
  • どんな出力を期待するのか => trueかfalse

メソッドとしてはこのようになるはずです。

user.rb
def mine?(comment)
  # ロジックは一旦置いといて
  # trueかfalseを返せれば良い
end

「コメントのオブジェクトを引数として受け取り、boolean型の値を返す関数」ですね。

それができたらようやくロジックを考えられるようになります。

完成形の一例です。

user.rb
def mine?(comment)
  id == comment.user_id
end

このロジック自体はここでは重要ではありません。重要なのは入出力が決まって初めて実際のロジックを考えられるようになるということです。その逆は有り得ません。

余談ですがスペックを書くとこの辺りの力が鍛えられると思ってるので、初学者こそスペックを書いて欲しいです。

3. ブラウザの開発者ツールを使わない

サーバログを見る人はちらほらいますがブラウザの開発者ツールは見ないという人がほとんどでした。
不具合が起きた際のヒントが何かしらあることが多いので開発者ツールは使うことをおすすめします。
特にJSが絡んでくるとサーバとクライアントのどちらに問題があるのかの見極めが重要になってくるので開発者ツールのコンソールは個人的にはよく見ます。500番台のエラーならすぐにサーバサイドに問題があることがわかります。

image.png

一方こういうのはJS側のエラーですね。
image.png

その他、ネットワークタブも個人的にはよく見ますね。

  • そもそも通信が走っているのか
  • 送ろうとしているデータが本当にリクエストに載ってるか(FormData)
  • etc image.png

httpプロトコルの勉強にもなって一石二鳥です。

4. ログを見てググらない/見ない

ログを見て断念する。もしくはそもそも見ない人が一定数います。エラーログは解決のためのヒントが書いてあるので必ず見てググってもらいたいです。

ただこれは難しい問題で、周辺知識がないとエラーログはどうしても読めないと自分は考えてます。

image.png

このエラーを見た時にある程度経験のある人は(実装上どこに問題があるかはどうあれ)根本の原因はわかると思います。
一方で知識がまだ少ない初学者にとっては undefined method userundefined method user でしかありません。そこから何か得ることはできないでしょう。

『テレビが映らない』という問題に対して『テレビは電気で動いている』という知識がなければ『コンセントはちゃんと入っているかな?』という発想にはそもそも至らないんですね。

プログラミングはそういった周辺知識の積み重ねが重要で、先の例でいうと

  • undefinedというのは変数や関数が定義されていないことを意味する
  • methodというのは関数である
  • 定義されていないというのはオブジェクトに当該変数や関数が書かれていない

という知識がないとどこに問題があるのか察しがつかないでしょう。

そういった知識は一朝一夕で身に付くものではないので地道に積み上げていくことが重要です。

モデル編

5. rails consoleを使って試そうとしない

『ユーザーが作成できない!助けてください!』と言われた時には『まずコンソールで作れるか確認してください』と伝えてます。
これも原因の切り分けの一環です。
ブラウザでぽちぽち操作してユーザーが作れないという場合、モデル・ビュー・コントローラ・ルーティングその他諸々が複合的に絡み合ってるので原因の切り分けがしづらくなります。
なので一旦rails consoleを使ってとりあえずDBに保存できるか、ということを試してもらってます。

User.create(email="example@example.com", password="12345678")

こんな風に書いてたとしたらそれは当然ユーザーも作れないですね。

Railsには色々なクエリインターフェースがあるので色々と試してみると良いと思います。

Active Record クエリインターフェイス

6. クラスとインスタンスのイメージを持てていない

User.name

user.find(params[:id])

と書く人がちらほらいましたが、クラスとインスタンスの違いをイメージだけでも持っていればこんなミスはしないはずです。

ざっくりとしたイメージはこうです。
クラスは設計図
インスタンスは実態

「人間」という設計図には具体的なメールアドレスなんて存在しえないですよね。なので『「たろう」さんのメールアドレス』や『「はなこ」さんのメールアドレス』とするのは正しいですが、『「人間」のメールアドレス』とするのは誤りということが感覚でおかしいなと気づくはずです。

イメージを持ち、違和感を感じる感覚を養うことが大事です。

ルーティング・コントローラ編

7. リクエストからレスポンスの流れの理解が曖昧

リンクを踏んでからブラウザに表示されるまでに一体何が行われているのかのイメージがついていない人ほど課題に行き詰まる傾向にあるような気がしています。

image.png

このフローを理解していないと実装してても腹落ちしないはずです。

8. RESTの理解が曖昧

Railsガイドから引用
image.png

カンペを見なくても答えられるくらいこの表が頭の中に入っていないとRailsでの開発は厳しいです。
初学者はこの表をベースに「この機能を実現するにはどういったURLにすれば良いのだろうか?」を考えて試行錯誤してそのURLを出力させることに注力すると良いと思います。

Webアプリは原則ユーザーの操作を起点に動くものなので何はともあれまずはビューです。詰まった時はlink_toだろうがform_withだろうが自分が期待するURLをなんとかして出力することを意識すると良いと思います。言っちゃえば最初の動作確認の段階ではlink_toform_withも使わなくてもいいんです。コメントを作りたいのであれば先の表で言うと/commentsに対してPOSTすれば良いのでフォームのアクション属性に直接/commentsを書いても良いんですし、同様に、ログイン機能を作りたいのであれば/sessionsに対してPOSTすれば良いのでフォームに直接/sessionsを指定しちゃっていいんです。comments_pathuser_sessions_pathなど難しいことは一旦置いといていいんです。あとから書き直せばいいんです。

余談ですが、癖をつけると言う意味で初心者のうちはRESTにとことん忠実になった方が良いと考えてます。デフォルトで生成される7つのアクション以外を自前で作り始めるとカオスになりがちなので。

この辺りを参考にすると良いです。
DHHはどのようにRailsのコントローラを書くのか
DHH流のルーティングで得られるメリットと、取り入れる上でのポイント

9. Comment.find(params[:id])とやりがち

コメントの編集機能を実装するときにこのように書く人が多いです。

Comment.find(params[:id])

これでも動きますが、/comments/:id/edit のid部分をユーザーに書き換えられたら他人のコメントも更新できてしまうという問題点があります。

なのでこちらの書き方の方がベターです。

current_user.comments.find(params[:id]

RailsBestPracticeにも書いてあります。
Use scope access

他にもRailsBestPracticeには為になることが書いてあるので読んでみると良いです。

ビュー編

10. コレクションをeachで回してパーシャルをレンダーしがち

@users.each do |user|
  render 'user', user: user
end

ユーザーの一覧を表示する際にこのような書き方をする人が多いです。間違いではないですがもっとスッキリする書き方があるのでそちらを使おうという話です。

render @users

自分では検証していませんが、レンダリングコストも低くなっているそうです。

RailsBestPracticeにも書いてあります。
Simplify render in views

まとめ

開発全般編の話なんかはRailsに限らず他の言語でもいえる話ですね。というか原因の切り分けの話はプログラミング以外でも通ずる話ですよね。
iPhoneが充電できなかった時ってiPhoneが悪いのか充電器が悪いのかをまず調べるために、違うiPhoneを繋いでみたりしませんか?
プログラミングも結局はそういうことです。

とりあえず今回は前半ということで10個書きましたが、近いうち後半編であと10個書きます...!!

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

Railsでweb制作をする際に使ったコマンド一覧&用語解説(第一章)

セットアップ

$ bundle install --without production
Gemfileで指定したgemをインストール(本番環境でしかつかさないgemは排除)
$ bundle update
エラーが出た時用

Git

$ git config --global user.name "あなたの名前"
$ git config --global user.email あなたのメールアドレス
gitの設定(一回のみでOK)
$ git init
新規リポジトリの初期化
$ git add -A
プロジェクトのファイルをステージングエリアに追加
$ git status
gitの状態を見ることが出来る。赤文字で何か書かれていたらステージングに未追加。緑文字で何か書かれていたらステージングに追加済み
$ git commit -m "変更メッセージ"
リポジトリに反映
$ git log
上の変更メッセージの履歴を見ることが出来る
$ ls ファイル
ファイルを確認(他にも色々な使い方あり)
$ rm ファイル
ファイルの削除
$ git checkout -f
全ての変更を元に戻す
$ cat ~/.ssh/id_rsa.pub
公開鍵の出力
$ git remote add origin git@bitbucket.org:ユーザー名/プロジェクト名.git
$ git push -u origin --all
プロジェクトのリポジトリへの追加とプッシュ(bitbucket版)
$ git remote add origin git@github.com:ユーザー名/プロジェクト名.git
$ git push -u origin master
上のgithub版

Heroku

$ source <(curl -sL https://cdn.learnenough.com/heroku_install)
herokuのインストール
$ heroku --version
herokunoバージョン確認
$ heroku login --interactive
herokuにターミナル上でログイン
$ heroku keys:add
herokuにSSHキーを追加
$ heroku create
herokuに新しいアプリを作成
$ git push heroku master
プロジェクトをherokuにpush(masterは省略可)
$ heroku rename ○○
herokuにデプロイしたアプリケーションの名前を変更

用語解説

アーキテクチャパターン

問題解決をする時のパターン・マニュアルみたいなもの。RailsではWebを閲覧する際のコンピュータ内の手順をMVCというアーキテクチャパターンによって行っている。

GUI

マウスを使って操作できる画面のこと。キーボードでしか操作できない画面はCUI。

config

configurationの略。「設定」という意味。

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

rails-tutorial第5章

headタグの中身について

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                               'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                               'data-turbolinks-track': 'reload' %>
    <!--[if lt IE 9]>
      <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
      </script>
    <![endif]-->
  </head>

<%= csrf_meta_tags %>はcsrf対策のコードでクラッキングからサイトを守る。

<%= stylesheet_link_tag    'application', media: 'all',
                               'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application',
                               'data-turbolinks-track': 'reload' %>

上記はcssに何か書かれていたら反映するよー。jsに何か書かれてたら反映するよーって意味。

<!--[if lt IE 9]>
      <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
      </script>
    <![endif]-->

上記はIEが9以下?の場合、HTML5のheaderタグなどが使えないため、JSでなんとかするよーってコード。

なんでaタグじゃなくてlink_toメソッドを使うの?

<header class="navbar navbar-fixed-top navbar-inverse">
      <div class="container">
        <%= link_to "sample app", '#', id: "logo" %>
        <nav>
          <ul class="nav navbar-nav navbar-right">
            <li><%= link_to "Home",   '#' %></li>
            <li><%= link_to "Help",   '#' %></li>
            <li><%= link_to "Log in", '#' %></li>
          </ul>
        </nav>
      </div>
    </header>

別に以下のコードでもよくね?って思うけど

<a href = # id = logo>sample app</a>

aタグだとhelperメソッドや変数を呼び出すことができないという欠点がある。

<%= link_to #{example}, static_pages_url, id: "logo" %>

そのため上記のようにurlや表示する文字にRubyのメソッドを使いたいがためにlink_toメソッドを使うのである。
id: logo はlink_toメソッドのオプションとしてハッシュの形で書かれている。
idは元から決められているがhogehoge: 'foobar'というオリジナルのオプションも与えられる。
オプション引数と呼ばれている。

Bootstrap

先に書いたnavbar や containerはbootstrapでもともと決められたクラス名。
ちなみにbootstrapは公式サイトからダウンロードする必要はなく、

gem 'bootstrap-sass', '3.3.7'

$bundle installで準備ok。

$ touch app/assets/stylesheets/custom.scss
でファイルを作り

app/assets/stylesheets/custom.scss
@import "bootstrap-sprockets";
@import "bootstrap";

を記入する。

image_tagについて

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

app/assets/images/に画像ファイルを置いておく(画像ファイルはここにダウンロードする)ことで、image_tagヘルパーによって画像を探してくれる。

ちなみにalt属性(オルト属性)とは、HTMLのimg要素の中に記述される画像の代替となるテキスト情報です。 ... 従って、そうした「画像が閲覧できない環境下でも、その情報が正しく理解される」ような代替テキスト情報がalt属性には記述されなければならないらしい。

パーシャルについて

yieldの時のように自分で自分のテンプレートを作りたい!そんなときに使うのがパーシャル。

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                               'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                               'data-turbolinks-track': 'reload' %>
    <%= render 'layouts/shim' %>
  </head>
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>

<%= render 'layouts/header' %>はヘッダーを別ファイルに分けて、「ヘッダーいじりたい人はlayouts/header見てねー」という感じになっている。これにより散らかったコードをdryにできる。

もちろん今までのコードを別ファイルにまとめる必要があるが、その際のファイル名はパーシャルとわかるように_から始まるファイル名にする。

app/views/layouts/_shim.html.erb
<!--[if lt IE 9]>
  <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
  </script>
<![endif]-->

つまり、<%= render 'layouts/shim' %>の場合は_shim.html.erbというファイル名にする。
逆に慣れてきたら最初からパーシャルを作ってコーディングしていけば早い。

アセットパイプラインとは

CSS JS imageなどの管理機能。

1.アセットディレクトリ

静的ファイルを目的別に分類する。

2.マニフェストファイル

1つのファイルにまとめる方法をrailsに指示する。

app/assets/stylesheets/application.css
*= require_tree .
*= require_self

1行目はマニフェストファイル(上記のファイル)内のcssの記述をまとめる。
2行目はマニフェストファイル以下のファイルもまとめるという意味。

3.プリプロセッサエンジン

指示に従いブラウザに配信できるように結合する。

foobar.js.coffee
foobar.js.erb.coffee
1行目の場合、coffee.script → JavaScriptの順で外側からコンパイルされる。

なぜアセットパイプライン?

パソコン的には複数のファイルを読み込むより、1つのファイルで改行や空白を無くしたものの方がレスポンスが早いから。また開発者的にも複数のファイルの方が開発がしやすく、それを一つにまとめてくれるなんて最高!ってこと。

名前付きルートについて

config/routes.rb
Rails.application.routes.draw do
  get 'static_pages/home'

  get 'static_pages/help'
  get 'static_pages/about'
  get 'static_pages/contact'
  root 'static_pages#home'
end

この状態だと、urlにいちいちstatic_pagesが出てきて、わかりづらい.
そもそも、上記は「このurlにリクエストされたらこのコントローラのこのアクションを実行してください」という情報を1つにまとめてしまっている。
もうちょっとわかりやすくできないものかと。

config/routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
end

このように書き換えることによってヘルパーを使えるようになる。
例えば、/helpだったらhelp_path /aboutなら about_path
root なら root_pathという感じだ。これが名前付きルート。

名前付きルートをテストで使うと以下のようになる。

test/controllers/static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get root_path
    assert_response :success
    assert_select "title", "Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get help_path
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get about_path
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end

  test "should get contact" do
    get contact_path
    assert_response :success
    assert_select "title", "Contact | Ruby on Rails Tutorial Sample App"
  end
end

名前付きルートを使う注意点としては、使うときに''をつけないこと。urlをそのまま入れるときは''必要だけど、名前付きルートはそのまま書かないと逆にエラーになってしまう。

統合テスト (Integration Test)

統合テストは、ページから別のページに飛ぶなどurlがちゃんと機能してるか?を確かめるときに使う。
アプリケーションの動作を端から端まで (end-to-end) シミュレートしてテストすることができる。

$ rails generate integration_test site_layout

上記のコマンドでテストファイルができる。

test/integration/site_layout_test.rb
require 'test_helper'

class SiteLayoutTest < ActionDispatch::IntegrationTest

  test "layout links" do
    get root_path
    assert_template 'static_pages/home'
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
  end
end

その中でも

assert_select "a[href=?]", about_path

上記はRailsは自動的にはてなマーク "?" をabout_pathに置換しています (このとき "about_path" 内に特殊記号があればエスケープ処理されます)。これにより、次のようなHTMLがあるかどうかをチェックすることができます。

<a href="/about">...</a>

一方で、ルートURLへのリンクは2つあることを思い出してください (1つはロゴに、もう1つはナビゲーションバーにあります)。このようなとき、

assert_select "a[href=?]", root_path, count: 2

上記のようにそのページにいくつ指定したurlリンクが存在するかもテストすることができる。

Users controller作ってみる。

$ rails generate controller Users new

config/routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
  get  '/signup',  to: 'users#new'
end

/signupでアクセスできるようにして、signup_pathという名前ルートも使えるようにする。コントローラーを作成すると、テストも自動作成されるよね。でも上記で名前付きルート設定しちゃったからテストが通らない。なので、、

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  test "should get new" do
    get signup_path
    assert_response :success
  end
end

名前付きルートを設定したら、必ずテストコードも書き換えるようにする。

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

アクションとHTTPメソッド

アクション       HTTPメソッド 役割       URLのパス       
index get 一覧表示 /works
show get 詳細表示 /works/1
new get 新規作成 /works/new
create post 登録 /works/create
edit get 編集 /works/1/edit
update patch/put(post) 更新 /works/1/update
destroy delete(post) 削除 /works/1/destroy
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

いいね機能

ツイートアプリにいいね機能をつけていきます。

【その1】 jQueryを入れる

gem 'jquery-rails'を加え、bundle install

Gemfile
gem 'jquery-rails'
ターミナル
bundle install



application.js//= require jqueryを追記。

app/assets/javascripts/application.js
//= require jquery  #一番上に書く
//= require rails-ujs
//= require_tree .

【その2】ルーティング

createアクションとdestroyアクションの2つ

routes.rb
resources :tweets do
  resources :likes, only: [:create, :destroy]
end

【その3】コントローラー

ターミナル
rails g controller likes
likes_controller.rb
class LikesController < ApplicationController
  before_action :set_tweet

  def create
    @like = Like.create(user_id: current_user.id, tweet_id: @tweet.id)
  end

  def destroy
    @like = Like.find_by(user_id: current_user.id, tweet_id: @tweet.id)
    @like.destroy
  end

  private
    def set_tweet
      @tweet = Tweet.find(params[:tweet_id])
    end
end

【その4】モデル

tweet.rb
class Tweet < ApplicationRecord
  has_many :likes  #追記
end

user.rb
class User < ApplicationRecord
  has_many :likes  #追記
end



Likeモデルとlikesテーブル作成

ターミナル
$ rails g model like user:references tweet:references
$ rails db:migrate
like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :tweet

  validates :user_id, presence: true
  validates :tweet_id, presence: true
  validates_uniqueness_of :tweet_id, scope: :user_id
end

【その5】ビュー

部分テンプレートとして作成↓

app/views/likes/_like.html.haml
- if Like.find_by(user_id: current_user.id, tweet_id: tweet.id)
  = link_to tweet_like_path(tweet_id: tweet.id, id: tweet.likes[0].id), method: :delete, remote: true do
    %i{class: "fas fa-heart"}
- else
  = link_to tweet_likes_path(tweet), method: :post, remote: true do
    %i{class: "far fa-heart"}
= tweet.likes.length

font-awesomeのアイコンを使っています。
使い方の解説はこちらへ
https://qiita.com/ITmanbow/items/2679109dd886dd5e6844



実際に置く場所↓

app/views/tweets/index.html.haml

#好きな場所へ配置
%div{id: "like-#{tweet.id}"}
  = render "likes/like", tweet: @tweet

【その4】jsファイルを作成

erbの場合

app/views/likes/create.js.erb
$("#like-<%= @dream.id %>").html("<%= j(render partial: 'users/like', locals: { dream: @dream }) %>");
app/views/likes/destroy.js.erb
$("#like-<%= @dream.id %>").html("<%= j(render partial: 'users/like', locals: { dream: @dream }) %>");



hamlの場合

app/views/likes/create.js.haml
$("#like-#{@tweet.id}").html("#{j(render partial: 'like', locals: { tweet: @tweet })}");
app/views/likes/destroy.js.haml
$("#like-#{@tweet.id}").html("#{j(render partial: 'like', locals: { tweet: @tweet })}");



Qiitaの記事をいくつか参考にしてなんとかできました。
先人は偉大です。。。いつも本当にありがとうございます。。。

https://qiita.com/fumikao/items/373caa60b77f27f2dbdd
https://qiita.com/shiro-kuro/items/f017dce3d199f06d1dcd



ではまた!

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