- 投稿日:2019-12-01T23:55:25+09:00
【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で確認
- 投稿日:2019-12-01T23:44:42+09:00
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バージョンアップ後は不要な処理になるので忘れずに削除しましょう。
- 投稿日:2019-12-01T23:25:22+09:00
【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
上の質問と回答をもとに解決することができました。ありがとうございます。さいごに
初心者らしく「なぜ?」からはじまり、その解決策もシンプルすぎるがゆえいろいろ戸惑ったので残しておきます。今後はじめてデプロイに臨まれる方の参考になれば幸いです。
万が一、誤りや解釈が十分でないところがあれば、ご指摘いただけると嬉しいです。
- 投稿日:2019-12-01T22:55:23+09:00
【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
gem
gon
とは?シンプルにRailsアプリ内でJavaScriptに変数を渡すことが出来るgemです。
RSpecにも変数を渡せたりと、便利。
GitHub - gazay/gon: Your Rails variables in your JSIf 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に何かデータを送る必要があるなら、面倒くさいビューとかパースとかやめてこれを使っちゃいなよ!
と紹介されています。インストール
Gemfilegem '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.jsalert(gon.your_int) alert(gon.your_other_int) alert(gon.your_array) alert(gon.your_hash)先程コントローラー側で定義したものがそのまま使えます。
※aleat
はテキトーです。当然ですが、例えば
current_user
を渡していた場合は、current_user.jsalert(gon.current_user.name) alert(gon.current_user.email) alert(gon.current_user.id)のようにすると名前やメールアドレスなど、欲しいキーを指定すれば値が取り出せます。
非常に分かりやすくて便利です。
おわりに
最後まで読んで頂きありがとうございました
他にも
input
タグ経由で変数を渡したり、JSONを使ったりして変数を渡している記事を見つけましたが、gon
の方がシンプルに変数を渡せるので楽ですね参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2019-12-01T22:43:03+09:00
【初心者向け】「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.html
や500.html
などRailsを使用しない静的ページや画像を格納する場所。
デプロイ時とかでお世話になるかも。/storage
Active Storage等を使用した際にlocalでデフォルトで投稿されたファイルが保存される場所。10
デフォルトでは中身はない。/test
作ったアプリケーションが正しく動作するのかという確認するファイルをまとめたフォルダ。
テストを行うことで正しい動作を保証し、品質の高いアプリケーションを仕上げる事につながる。
コントローラーやモデルが作られると同時にコントローラー、モデル名の名前のファイルが作成される。
Minitestなどを使いテストをしていくのが主流。Rspecは/specが別途作られる。/tmp
一時ファイルを保存するためのファイルをまとめたフォルダ。
/vendor
vendorは自分が開発しているものではないサードパーティのライブラリ(jsフレームワークやcssフレームワークなど)を格納する場所。
デフォルトでは中身はない。
ここにライブラリを置いて、require
で呼び出す。11Gemfile
現在のアプリケーションで使うgemをまとめているファイル。
このファイルに追加したいgemを記入し、bundle install
を実行するとgemがinstallされる。Gemfile.lock
Gemfileを元に依存関係にあるgemのバージョンと取得先が記録される。
実際にインストールしたgemのリストという認識。
bundle install
やbundle update
で更新される。おわりに
この記事書くだけでもかなり勉強になりました。アウトプットはやはりよい。
かなりざっくりしていて、全てのファイルは紹介しきれていませんが、ふんわりこんな内容なんだと掴んでいただければ幸いです。
間違っていたりしたら報告していただけると泣いて喜びます。
私の他にもDMM WEBCAMPのメンターや社員さんが記事を書かれているので、よければご覧になってください。
rubyのライブラリの総称( https://www.ruby-lang.org/ja/libraries/ ) ↩
https://github.com/rails/webpacker, https://qiita.com/ttiger55/items/e45ec0febba3a412507e ↩
https://qiita.com/yukiyoshimura/items/f0763e187008aca46fb4 ↩
https://railsguides.jp/routing.html#crud%E3%80%81%E5%8B%95%E8%A9%9E%E3%80%81%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3 ↩
https://qiita.com/takehanKosuke/items/79a66751fe95010ea5ee ↩
- 投稿日:2019-12-01T21:20:07+09:00
#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: :optionsrspec でのテスト
こんなんで
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
- 投稿日:2019-12-01T21:20:06+09:00
#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: :allor 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: :optionsrspec 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 endREf
Railsの ActionController::RoutingError は ApplicationController での rescue_from で捕まえられない - Qiita
Original by Github issue
- 投稿日:2019-12-01T20:28:47+09:00
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.rbmodule 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.rbmodule 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.rbclass 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::CreateUserMutation
のargument
でrequired: 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 のみで実装したアプリケーションを書いてみたい気持ち。
- 投稿日:2019-12-01T17:51:03+09:00
ぼくのかんがえたさいきょうのAPIドキュメント運用
ドキュメントちゃんと保守できてますか?
API開発とドキュメントの保守は切っても切れない問題です。
仕様の記述はもちろんのこと、サンプルを試せるAPIクライアントや、仕様に則った実装になっているかテストも自動化したいですよね。
本記事では、現在開発中のAPIアプリケーションで、実際に僕が試行錯誤していく中でたどり着いたベストプラクティスを紹介しようと思います。
アーキテクチャ
- iOSアプリのバックエンドとしてJSONを返すAPIサーバー
- Rails6 × MySQL5.7 on Docker
いつもの というお買い物アプリです。
ドキュメント何で書いてますか?
- Excel => つらい
- Markdown => つらい
- 何らかのDSLを用いて生成するツール =>
素でマークダウンを書くのはつらみが深いので何かしらツールを使いましょう。
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ファイルで環境のやりとりするみたいなイメージに近いそして錚々たる大企業たちがスポンサーとなっているため、業界の標準となっていくことは確実です。
OpenAPIの記述に慣れておくことは、エンジニアとして必要なスキルになってくるかと思っています。実際に使ってみよう
今回実現したいことはこちらです。
- ドキュメントをブラウザで手軽に確認したい
- ドキュメントを楽に記述したい
- ドキュメントをローカルサーバーのHTTPクライアントとして使いたい
- レスポンスがドキュメントに則っているか自動テストしたい
これ全てOpenAPIでできます。
ただ実際にやろうとするとツールが豊富で選択肢が多い割に、3.0に対応していないものが多かったり、情報がまとまっていなかったり、ベストプラクティスにたどり着くまでに苦労したので、この記事が何かの助けになれば幸いです。
ちなみにですが、2.xと3.xでは破壊的な変更があるので、2系のツールで3系を動かすのは無理があります。
多くのツールで3に対応するissueが上がっているのですが、長い間放置されているものが多いため、3を使おうとすると選択肢は結構狭まります。ドキュメントをブラウザで手軽に確認したい
まずはサンプルとなるエンドポイントを実装します。
config/routes.rbRails.application.routes.draw do resources :users endapp/controllers/users_controller.rbclass 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.ymlopenapi: 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.ymlversion: '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でドキュメントが表示されます。▼それぞれ対応するセクションがこうなっている。
▼パスをクリックするとアコーディオンが開いて仕様が表示される。
▼上の画像は
Example Value
を表示したもので、Schema
をクリックすると、プロパティの説明、型、requiredの有無、nullableの有無など詳細が表示される。ドキュメントサーバーとAPIサーバーの立ち上げをいっぺんに管理できるのが便利
ドキュメントを楽に記述したい
エディタごとにプラグインがそれぞれあるかと思います。
僕は普段 VS Code を使っているのでこれを入れています。あとはプレビュー用として Swagger Viewer を入れておいてもいいかと思います。
ホットリロードで確認しながらできるので便利です。
一つだけ注意点があって、たまに表示がおかしくなったり、正しく表示されないことがあります($ref
が展開されないなど)。
なので僕は書き方に慣れるまではこれを使っていましたが、いまはブラウザでまとめてチェックしています。あとは書き方のTipsですが、
components
を使って共通化していくと見通しが良くなります。doc/openapi.umlpaths: /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"ドキュメントをローカルサーバーのHTTPクライアントとして使いたい
せっかく docker-compose してるんだから、ローカルのサーバーに実際につないで動かしたいですよね?
API開発時のHTTPクライアントにはずっと Postman や、
最近だと REST Client なんかを使っていましたが、ドキュメントの変更に対する反映が面倒だったりするのが難点です。
OpenAPIを組み込めればそんな手間もなくなります。SwaggerUI には
Try it out
というボタンがついています。
このままでは利用できないので設定を行います。
servers
というセクションを追記してください。doc/openapi.ymlservers: - url: "http://localhost:3000" description: "local api server"これがボタンを押した時のリクエスト先のベースURLとなります。
▼トップに
servers
の設定が反映されました。複数設定することができ、切り替えられるようになっています。▼ボタンを押すとパラメータが編集できるようになり、
Execute
ボタンが現れます。
(今回はパラメータないが適当に書くとこんな感じ。よしなにクエリパラメータに突っ込んでくれる。)もう一つ設定が必要で、このままリクエストを送ってもこのようなエラーが出ます。
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.rbconfig.middleware.insert_before 0, Rack::Cors do allow do origins "localhost:8080" resource "*", :headers => :any, :methods => :any end endこれでサーバーを再起動すれば、リクエストできるようになります。
▼生成されたcurlコマンドと、レスポンスが表示されました。
いちいち Postman 開いてリクエスト書く必要がないので幸せ
レスポンスがドキュメントに沿っているか自動テストしたい
これだけでもだいぶ開発が捗るようになったんですが、テストまでいきたいです。
ドキュメントで Integer になってるプロパティが String で返ってきたり、
required なプロパティがレスポンスになかったら落ちるようにしたいですよね。rspecに組み込んでみます。
Gemfilegem 'rspec-rails'docker-compose run web bundle docker-compose run web rails generate rspec:installspec/requests/users_spec.rbrequire "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 endcommittee という gem を使います。
さらにそれを Rails 用にラップした committee-rails も使います。Gemfilegem 'committee' gem 'committee-rails'設定を追加。
spec/rails_helper.rbconfig.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.rbclass UsersController < ApplicationController def index user = { :name => "sakuraya", :age => "26" } render :json => user end endテストをこう書きます。
spec/requests/users_spec.rbrequire "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テストまでかけるとか最高か
普段の開発フローとしては、まずドキュメントを記述して仕様をレビュー => テスト書く => 実装というドキュメント&テスト駆動開発でやっています。
まとめ
今回は既存のプロジェクトに後から組み込んだので使ってませんが、
ドキュメントからモックサーバーを立ち上げたり、コードを生成したりするツールも言語ごとにいろいろあります。
うまく活用してAPIドキュメント運用のつらみから解放されましょうOpenAPI の記述方法については、OpenAPI-Specification を見ながら覚えていくのがオススメです。
[追記]
今回使ったサンプルコード
- 投稿日:2019-12-01T17:34:32+09:00
Rails+heroku+LINE Messager APIで秘書的なLINEbotを作ってみた(ゴミ出しの通知編)
作るもの
毎日決まった時間になるとゴミ出しについて通知してくれる簡単なLINEbot。
ゆくゆくは天気/交通情報/イベントなども共有してくれるbotにしたいと思っています。
アイアンマンに出てくる人工知能ジャーヴィスをお節介おばさんにしたイメージです。使用した言語/フレームワーク/ライブラリ/サービス
-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 DevelopersAPIの準備
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:createLine APIを使う為のgemをインストールします。
Gemfile.rb#LINE API用 gem 'line-bot-api' #access tokenなどを管理する用 gem 'dotenv-rails'$bundle installLINE API登録時に控えた情報をアプリのルート直下に.envファイルを作成して保存
#.env LINE_CHANNEL_SECRET=XXXXXX LINE_CHANNEL_TOKEN=XXXXXXコントローラーの準備をしてきます。
$rails g controller Linebot
linbot_controller.rbclass 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 endconfig/route.rbRails.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コードを使うと簡単です。初期メッセージはLINE Developersから編集できます。
STEP2 LINEにメッセージpushする
STEP1では、自分のメッセージをトリガーに、LINE Message APIを操作しました。
今回は、チャンネルがこちらのアクションなしで、テキストを送信できるよう実装していきます。ユーザーモデルの追加
messageをAPIから一方的に送りつけるためには、rails側で送り先のuserIDを知っている必要があります。
送るユーザーが決まっていればIDをメモして、環境変数に設定してもいいと思いますが、今回はお友達登録でユーザーIDを保存できるように、実装します。$rails g model user uid:string $rails db:migratecontrollerに追記
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.rakenamespace :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.rbclass 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.rbclass 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/TokyoherokuのURLをAPIのWebhookURLに設定するのを忘れないでください。
定期実行の設定
通常であれば、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の秘書力を高めて行きたいです。
- 投稿日:2019-12-01T17:25:47+09:00
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アプリケーションのサンプルである
公式リポジトリ
こんなアプリケーションにおすすめ
- あまり多くのJavaScriptのコードを含まない
- スタイルシートのビルド環境は
sprockets-rails
で十分- 使わない
npm
パッケージはインストールしない- 必要なパッケージを即座に更新できるようにするために、
npm
パッケージは可能な限り個別のものとして管理する既存アプリケーションへの設定
minimum_javascript_on_rails
ではプルリクエスト形式で設定のカスタマイズ例を示してくれているので、細かい実装の仕方を知りたい方や、順を追って設定していきたい方はリポジトリからPRを追うのが一番早いです環境
- Ruby ->
2.6.3
- Ruby on Rails ->
6.0.1
- Node.js ->
12.6.0
流れ
npm run build
が動くようにするnpm run watch
が動くようにする- クロスブラウザで新しいECMAScript機能を有効にする
- JSファイルの移行
- その他設定
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.jsconst 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.rbmodule ApplicationHelper def client_side_javascript_tag path = ClientSideSupporter.webpacked_javascript_path javascript_include_tag(path).html_safe end endapp/helpers/application_helper/client_side_supporter.rbmodule 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 endconfig/initializers/client_side_supporter.rbwebpack_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.js
とwatch-for-development.config.js
は共通部分が多いので、こちらのPRを参考にまとめてみてください)client/webpack/watch-for-development.config.jsconst 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.rbconfig.x.client_side_supporter.update_webpack_manifest_on_reloading = trueconfig/initializers/client_side_supporter.rbif 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@3
とregenerator-runtime
をインストールし、index.js
(エントリーポイント)でインポートします。npm install core-js@3 regenerator-runtimeclient/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すれば完成です!お疲れ様でした〜〜
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のレールに乗るよりはるかに勉強になったなと感じています。
個人的には初心者の方にこそおすすめだなと感じているので、ぜひ気になった方はリポジトリ覗いてみてください〜!
- 投稿日:2019-12-01T16:49:50+09:00
#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
- 投稿日:2019-12-01T16:35:59+09:00
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 のバージョンを指定してインストールできてなかった...?のかな..?
- 投稿日:2019-12-01T16:35:29+09:00
Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #15 投稿機能, Active Storage編
こんな人におすすめ
- プログラミング初心者でポートフォリオの作り方が分からない
- Rails Tutorialをやってみたが理解することが難しい
前回:#14 ユーザ投稿表示, ページネーション編
次回:準備中今回の流れ
- ルーティングと各アクションを整える
- 投稿フォームをつくる
- 画像を投稿できるようにする
- 投稿編集・削除機能をつくる
- 投稿時の不具合を解消する
- テストをつくる
今回は投稿機能を実装します。
(投稿のためのモデルが必要なので未読の方は#14を参照下さい。)1. ルーティングと各アクションを整える
投稿フォームはshow.html.erbと同じビューを使用します。
投稿編集と削除の際には別途ビューを生成します。
以上から必要なアクションはcreate、edit、update、destroyとなります。
よって、ここでの手順は以下の通りです。
- ルーティングを行う
- 作成済みのメソッドを移動する
- 各アクションを整える
ルーティングを行う
まずはルーティングを行いましょう。
config/routes.rbRails.application.routes.draw do # 中略 resources :microposts, only: [:create, :edit, :update, :destroy] end作成済みのメソッドを移動する
各アクションを整える前に、1つ変更を加えます。
logged_in_userメソッドはMicropostsコントローラでも使用します。
先にこちらはApplicationコントローラに移動しましょう。app/controllers/application_controller.rbclass 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.rbclass 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.erbapp/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>まず説明する点は、このような記述についてです。
<%= 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を使用する権限を与えます。
以下の手順を行いましょう。
- ルートユーザでAWSにログインする
- IAMを開く
- ポートフォリオ用ユーザを選択する
- 「アクセス権限の追加」を押す
- 「既存のポリシーを直接アタッチ」からS3を検索する
- 「AmazonS3FullAccess」をチェックし確認する
分からない場合はこちらをご覧ください↓
【AWS】【S3】作成手順 & アップロード手順 & アクセス権限設定手順アクセスキーを作成する
続いて後述で必要になるアクセスキーを作成します。
このキーを作成することでRailsとS3を紐づけることが可能です。
以下の手順を行いましょう。
- ルートユーザでAWSにログインする
- IAMを開く
- ポートフォリオ用ユーザを選択する
- 「認証情報」タブから「アクセスキーの作成」を押す
- アクセスキーとシークレットアクセスキーをメモする (一度しか表示されないので注意)
バケットを作成する
最後にポートフォリオ用ストレージとなるバケットを作成します。
以下の手順を行いましょう。
- IAMユーザでAWSにログインする
- S3を開く
- 「バケットを作成する」を押す
- バケット名を入力し東京リージョンを選択する
- 「次へ」を押し続け「バケットを作成」を押す
分からない場合はこちらをご覧ください↓
【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.rbhas_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 = :amazonconfig/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:editVimは癖があるので、下記に簡単な操作を載せておきます。
- 入力開始:i
- 入力終了:esc
- 保存終了:ZZ
- 保存せず終了::q!
aws: access_key_id: # ここにアクセスキーを入力 secret_access_key: # ここにシークレットアクセスキーを入力以上で紐づけは完了です。
画像をリサイズする
このままではユーザが投稿する画像サイズに制限がありません。
画像のリサイズにはMiniMagickというGemを使用しましょう。
ここでの手順は以下の通りです。
- Gemを追加する
- ImageMagickをインストールする
- リサイズのメソッドを定義する
- ビューで表示する
Gemを追加する
gemfile+ gem 'mini_magick'
bash$ bundle installImageMagickをインストールする
MiniMagickの使用にはImageMagickのインストールが必要です。
こちらも加えて行いましょう。bash$ sudo yum install -y ImageMagickリサイズのメソッドを定義する
画像のリサイズを行うよう、モデルにメソッドを定義します。
app/models/micropost.rbdef 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 %>フォーム入力↓
ログ表示↓
以上で画像のリサイズは完了です。4. 投稿編集・削除機能をつくる
編集と削除のためのアクションはすでに終えました。
よって、ここでの手順はビューをつくるのみです。bash$ touch app/views/microposts/edit.html.erbapp/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>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分」と表示↓
(少々デザインも変更しています)
6. テストをつくる
残るはテストのみです。
ここではModel specとRequest specのテストを行います。Model specでのテスト
このテストでは以下を確認します。
- pictureのみ存在するMicropostモデルは有効か
- 5MBを超える画像は無効か
- 画像以外のファイルは無効か
その前に5MB以上の画像、画像以外のファイルを以下のフォルダに準備しましょう。
bash$ mkdir spec/fixtures/images# 実際にファイルを追加する
その後、テストを記述します。
spec/models/micropost_spec.rbrequire '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.rbrequire '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を使う手順は以下の通りです。
- 環境設定を行う
- FactoryBotを整える
- テストを書く
環境設定を行う
まずは、ダミーでアップロードを行う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_attachedFactoryBotを整える
続いてFactoryBotを整えます。
spec/factories/microposts.rbFactoryBot.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.rbrequire '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 ユーザ投稿表示, ページネーション編
次回:準備中
- 投稿日:2019-12-01T16:33:11+09:00
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.rbenum 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.ymlja: 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からデータを取って来た時に数字で表示されてしまって、
これは何とかしないといけないなと思ったことがスタートでした。皆様のお役に立てれば幸いです
- 投稿日:2019-12-01T16:33:11+09:00
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.rbenum 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.ymlja: 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からデータを取って来た時に数字で表示されてしまって、
これは何とかしないといけないなと思ったことがスタートでした。皆様のお役に立てれば幸いです
- 投稿日:2019-12-01T15:58:47+09:00
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.rbclass 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.rbclass User < ApplicationRecord endUserクラスが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") => trueupdate_attributeはvalidationを介さずにDBに登録をすることができるという特徴がある。
ユーザーを検証する
モデルの場合、テストコードを先に書いて、あとでvalidationを書いていったほうが早い
モデルのテストを見ていこう
test/models/user_test.rbrequire '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.rbrequire '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.rbclass User < ApplicationRecord validates :name, presence: true endvalidationはmodelのファイルに書く。
次は長さを検証してみよう
test/models/user_test.rbrequire '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.rbclass User < ApplicationRecord validates :name, presence: true, length: { maximum: 50 } validates :email, presence: true, length: { maximum: 255 } endとすることで、文字数のバリデーションを定義できる。
適切なメールアドレスかチェックする
test/models/user_test.rbclass 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.rbclass 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 } endformatオプションで設定すると、その型通りじゃないとvalidにならないようになる。
ちなみにVALID_EMAIL_REGEXは定数である。一意性を知ろう。
同じメールアドレスがDB内に複数あると困る。
テストを見てみよう
test/models/user_test.rbclass 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 enddupは複製するメソッド。
@userがDBに保存された状態で、duplicate_userという複製されたインスタンスはvalidですか?というテストである。このテストは落ちる。じゃあ、一意性を担保するには?
app/models/user.rbclass 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.rbrequire '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.rbclass 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 } enduniqueness: 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.rbclass 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.ymlone: 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.rbclass 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.rbclass 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.rbrequire '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.rbclass 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
を忘れずに!!!
- 投稿日:2019-12-01T15:57:23+09:00
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-railsdeviseを導入
ユーザ管理にはdeviseを使います。こういった「よく使う機能を手軽に実現できるgem」が充実しているのも「Railsのいいとこ」と思います。
Gemfilegem '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.rbconfig.action_mailer.default_url_options = { host: 'localhost', port: 3000 }config/routes.rbroot 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テーブルには以下の列が生成されています。
- encrypted_password
- reset_password_token
- reset_password_sent_at
- remenber_created_at
大丈夫ですね。
すでにdeviseを入れてあるので、http://localhost:3000/users/sign_in からログイン画面にアクセスできます。
この時点では、サインアップしようとしても roots.rb に記述した root to: "home#index" をまだ作成していないので、Routing Errorになります。
routes.rbを書き換えます。
routes.rbRails.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マイグレーションはしたのですが、、、
と思ってコンソールを見直したら、マイグレーションファイルの中に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で作成した画面が表示できました。
新機能を作成したので、featureブランチを作成して、そこにpushしておきます。
$ git checkout -b create-contents $ git add . $ git commit -m "create contents scaffold" $ git push origin create-contents試しに動かしてみる
一応動くものができているはずなので、試しに動かしてみましょう。ここでは、データを3件登録してみました。
scaffoldのままなのでドシンプルですが、正しく画面表示できています。
最低限のテスト(RSpec)
APIを作る前に、ここまでの内容に対して最低限のテストを書いておきます。
RSpecとFactoryBotを導入します。
Gemfilegroup :test do # 中略 gem 'rspec-rails', '~> 3.8' gem 'factory_bot_rails', '~> 5.1.0' endconsole$ bundle install $ rails g rspec:installこれにより、specフォルダが作成されます。
minitest用のフォルダを削除します。
$ rm -r ./testHeadless Chromeを使用するように設定を入れます。
spec/spec_helper.rbrequire 'capybara/rspec' RSpec.configure do |config| config.before(:each, type: :system) do driven_by :selenium_chrome_headless end end下記設定がデフォルトではコメントアウトされていますが、コメントを外しておきます。
rails_driver.rbDir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }console$ mkdir ./spec/factories./spec/factories/contents.rb を作成します。
./spec/factories/contents.rbFactoryBot.define do factory :content do title { 'テストタイトル' } detail { 'テストコンテンツの明細です。' } end endconsole$ mkdir ./spec/system./spec/system/contents_spec.rb を作成します。
./spec/system/contents_spec.rbrequire '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 endconsole$ 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/supportspec/support/javascript_driver.rb# During the cache time, Webdrivers won't check to update Chrome. Webdrivers.cache_time = 1.month.to_iWARNが出なくなりました✨
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 failuresAPIを生やす
さて、ここからようやく本題です。APIを生やして行きます。
元々
http://localhost:3000/contents.json
へアクセスしたらJSONで結果が返ってきますが5、ここではせっかくなのでもう少しAPIっぽく、
http://localhost:3000/api/contents
でアクセスしたらJSONで結果が返ってくるようにしてみます。routes.rbに以下の設定を追加します。APIからはindexとshowのアクションだけを呼べるようにしたいので、resouresは使わず、個別に設定しています。
routes.rbscope '/api' do get '/contents', to: 'contents#index', defaults: { format: :json } get '/contents/:id', to: 'contents#show', defaults: { format: :json } endscope を設定することで、URLに
/api
が入っていても/api/contents
ではなく/contents
にアクセスしたかのような動きになり、
各ルーティングの defaults に format の設定を入れることで、/contents.json
ではなく/contents
と書くだけでJSONが自動的にフォーマットとして指定されるようになります。
同様に、特定のコンテンツの明細も以下のように取得できます。6
たったこれだけで、APIを作ることができました。
なお、今回は一般公開する機能だけをAPI化したので、APIキーによるアクセス制限は実施していません。
編集機能や特定ユーザだけに公開する情報をAPI化する時は、何らか7のアクセス制御を組み込む必要があります。編集系の機能をログイン必須にする
今回は自分以外は編集できないようにする計画ですが、ここまでの内容のままでは誰でもコンテンツを編集できてしまいます。
最初にdeviseを入れてありますので、それを利用してログイン状態でなければ追加・編集・削除を実施できないようにします。controllerの
before_action
を使って、ログイン済みの場合だけ表示を認可します。
現時点では複雑な権限設定は持たせていないので、単純に「ログイン済みであれば誰でもOK」という仕組みです。8contentsは、indexアクションは誰でもOKなので、それ以外のアクションに認可をかけています。
contents_controller.rbbefore_action :authenticate_user!, only: [:show, :new, :edit, :create, :update, :destroy]認可されていないアクションを実行しようとすると、ログイン画面にリダイレクトされます。
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 failuresfeatureブランチにcommit&push。9
$ git add . $ git commit -m "add auth to contents" $ git pushこれで、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サイト
執筆者の皆様、本当にありがとうございます!
- 「現場で使える Ruby on Rails 5 速習実践ガイド」https://www.amazon.co.jp/dp/4839962227
- https://www.gitignore.io/api/rails
- https://railsguides.jp/routing.html
- https://bokunonikki.net/post/2018/0804_rails5_devise/
- https://qiita.com/Hal_mai/items/350c400e8763ce0487a3
- https://qiita.com/tatsurou313/items/c923338d2e3c07dfd9ee
- https://bloggie.io/@kinopyo/migrate-from-chromedriver-helper-to-webdrivers
- https://qiita.com/Kokudori/items/d2bcb6fd24c662e73a33
- https://qiita.com/ttiger55/items/d144b8094d61b70955bf
- https://qiita.com/ebi_death/items/3912630e32268c9cce46
- https://qiita.com/tobita0000/items/866de191635e6d74e392
- https://solodev.io/git-flow/
初代は静的サイトとして構築したので、コンテンツを更新するときにサイト自体をデプロイし直す必要がありました。今度はDBと連携させて、コンテンツの更新を容易にしようと考えています。ちなみに初代を構築した時の記事はこちら。https://qiita.com/shozzy/items/dadea4181d6219d2d326 ↩
少し前にRails6がリリースされていますが、Rails5.xで開発を進めてきたのでまだバージョンアップしていません。 ↩
ここまではmasterに直接コミットしていますが、この先はそういうことはしません。 ↩
ここでは db/migrate/20190923152109_create_contents.rb でした。 ↩
https://qiita.com/ttiger55/items/d144b8094d61b70955bf にあるように、デフォルトのJSON生成機構では速度が遅いようですが、それは今後の課題としてここでは触れません。 ↩
このサンプルでは、一覧に全ての情報が含まれているので、明細を取得しても意味がありません。むしろ、URLなど不要な情報も含んでいるので、必要なデータだけ返すように改善が必要ですね。 ↩
APIキーとリファラの組み合わせをチェックするとか。 ↩
自分用のアカウントだけ発行する想定なのでこれで十分という判断です。アカウントごとに権限レベルによる画面制御を細かく掛けるなら、もっとしっかり作り込む必要があります。 ↩
実際には、featureブランチにはもう少しこまめにコミット&プッシュしていました。1機能分の進捗があった時と、作業が途切れるタイミングで。 ↩
- 投稿日:2019-12-01T15:56:43+09:00
初心者 データベース基礎 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
アカウントによって権限があり、アクセスできるアカウントもあれば、できないアカウントもあるような
制御を行う。
- 投稿日:2019-12-01T14:33:33+09:00
【Rails】Couldn't find [model_name]without an というエラーが出た時の解決策
- 投稿日:2019-12-01T14:29:34+09:00
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/DockerfileFROM 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 . /railsdocker-composeはこの記事が非常にわかりやすいです
docker-compose.ymlの書き方について解説してみた
docker-compose 解説 version docker-composeの文法はバージョンごとにことなるので指定が必要 servise 動かすアプリケーションの指定。ここでは、webとdb。 他 Service設定する際の項目について docker-compose.ymlversion: '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.rbRails.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 updatewebpack設定
$ 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 //コンテナ起動参考
Rails で postgresql を使う(インストールからマイグレーションまで)
Docker初心者がRails + PostgreSQL or MySQLで仮想環境構築した手順を丁寧にまとめる
既存のRailsアプリにReactを導入する方法
- 投稿日:2019-12-01T14:24:42+09:00
Railsチュートリアル 第10章 ユーザーの更新・表示・削除 - ユーザーを削除する
何をするか
- ユーザー削除が可能な権限を持つ管理ユーザーのクラスの実装
- ユーザーを削除するためのリンクの追加
- RDBからユーザーを削除する動作の実装
ここまでの実装が完了すれば、Userリソースに対し、RESTが求めるすべての動作の実装が完了することになります。
Railsチュートリアル本文においては、ユーザーを削除するためのリンクを追加したサイトレイアウトのモックアップは、図 10.13に示されています。
管理ユーザー
Userモデルに、boolean型の値を取る
admin
という属性を追加します。特権を持つ管理ユーザーを識別するために用いる属性です。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.rbclass 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.rbUser.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=1UNIX系OSのroot権限もそうですが、「任意のユーザーが、任意の属性を変更可能である」というのは非常に危険な状態です。なんとしても「一般ユーザーが編集できてはいけない属性を編集できないようにする」という仕組みを実装する必要があります。
4.0以降のRailsにおいては、「一般ユーザーが編集できてはいけない属性を編集できないようにする」という要求を実現する仕組みとして、Strong Parametersという機能が実装されています。Railsチュートリアル本文では、節7.3.2で言及されていました。
Railsチュートリアルをここまで順番通りに進めていれば、「Strong Parametersによって
admin
属性を保護する」という仕組みは、サンプルアプリケーションにすでに実装されているはずです。コードは以下です。app/controllers/users_controller.rbclass 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.rbclass 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 ...略 end1.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#updatedef 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.rbclass 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
(あるいはPATCH
やPUT
)リクエストを発行することはできません。そのため、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に表示されるようになる
ここまで実装が完了すれば、(管理ユーザーでログインしている場合に)deleteリンクがindexに表示されるようになるようになります。
destroy
アクションの実装deleteリンクをクリックして実際にRDBからユーザーが削除されるようにするためには、Usersコントローラーに
destroy
アクションを実装する必要があります。
destroy
アクションの実装内容は以下のとおりになります。UsersController#destroydef destroy User.find(params[:id]).destroy flash[:success] = "User deleted" redirect_to users_url end
User.find.destroy
というメソッドチェーンが「RDBからユーザーを削除する」という動作の実体ですね。以降、「フラッシュメッセージの定義」からの「indexページへのリダイレクト」へと続きます。
destroy
アクションをbeforeフィルターの対象にする
destroy
アクションも、edit
やupdate
と同様に「ログインユーザーのみが実行できる」ようにする必要があります。コードは以下です。ログインユーザーでなければ
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.rbclass 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#destroydef 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.rbrequire '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.rbclass 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そのため、テストが失敗するのです。
- 投稿日:2019-12-01T14:17:28+09:00
【Rails】add_indexについて
- 投稿日:2019-12-01T11:59:19+09:00
dependentオプションまとめ
はじめに
userが複数のpostを持っていると仮定する
class User < ActiveRecord::Base has_many :posts endclass 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する)
参考情報
- 投稿日:2019-12-01T11:40:50+09:00
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 -vbundlerはgemを管理するためのツールで、そのアプリケーションで使われるパッケージやバージョンを管理してくれます。
複数人で開発をするときは、他のPCでもバージョンは揃えないといけないので、その役割をbundlerが担っているというわけです。
https://qiita.com/jnchito/items/99b1dbea1767a5095d85逆にgemは、パッケージの形式だと考えてください。
bundlerもgemのひとつなので、次のコマンドを使ってインストールできます。
$ gem install bundler $ bundle -vbundlerにインストールが終わったら、次のコマンドで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/5bfe5b322872c61a6896Gemfileを編集したら、いよいよrailsのインストールです。
次のコマンドでrailsをインストールしましょう$ bundle install --path=vendor/bundle
bundle install
は、Gemfileの中身を見て色々インストールしてくれます。
なので先程railsのコメントアウトを外しましたので、このコマンドでrailsがインストールされます。また、
--path
オプションでは、今インストールしたパッケージの保管場所を指定しています。
これをつけないとPC全体に影響が出るグローバルな位置にインストールされます。つけなくても問題ないとする声もありますが、同一サーバで複数のrailsを扱う場合はつけたほうがいいといった声もありますので、役割だけでも覚えておきましょう。
railsのインストールが完了したら、次にrailsプロジェクトを作成します。
$ bundle exec rails new samplesampleの部分はそのプロジェクトの名前に適宜変えてください。
プロジェクトの作成が終わったら、cd
コマンドで移動し、railsサーバーを起動します$ cd sample $ rails sブラウザから
localhost:3000
にアクセスしてWelcomeページが表示されたら成功です!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
- 投稿日:2019-12-01T11:36:43+09:00
RUNTEQの講師をやってみてわかった初学者にありがちなパターン20選(前編)
はじめに
今年の8月からプログラミングスクールの講師として初学者にRailsを教えてきました。またMENTAでも100名弱の方にプログラミングを教えてきました。それくらい教えると初学者がどんなところでハマりがちでどんな知識が不足しがちなのかが大体わかってきます。要は初心者あるあるですね。
それを今回Advent Calendar一発目に書くことにしました。ではさっそくいってみましょう!
開発全般編
1. 問題がおきた場合に一気に色々と試しがち
一気に色々と試すと原因の切り分けができなくなります。
例えば「ログインができない」という問題があった時、初学者がやりがちなのはこのようなログイン処理のコードとずーーーっとにらめっこすることです。sessions_controller.rbdef create @user = login(params[:email], params[:password]) if @user redirect_to root_path, success: '成功' else flash.now[:danger] = '失敗' render :new end end色々な要素が入り混じっているのでこのコードだけを見てもどこに問題があるのか切り分けができません。
なので例えばですがこのようなアプローチで進めていけば良いのでしょう。
①そもそもこのコントローラのこのアクションが動いているか
これを確認するためにはこのように書けば良いですね。
sessions_controller.rbdef create p '通ってる?' end標準出力に何も出力されていなかったらルーティングまたはフォームの書き方に問題がある可能性が高いです。
いくらこのアクションの中の実装が正しくてもそもそもこのアクションが動いていなければ話になりませんよね。そこが問題なければ次に進みます。
②クライアントから正しくパラメータが送られてきてるか
②-1
params
を使っているところをハードコーディングしてみるsessions_controller.rbdef create @user = login('example@example.com', '12345678') # ハードコーディング if @user redirect_to root_path, success: '成功' else flash.now[:danger] = '失敗' render :new end endこれでもしログインできたら『クライアントから正しくパラメータが送られていなかった』もしくは『送られてきたパラメータをコントローラ側でうまく扱えていなかった』ということになります。
②-2
params
の中身を確認するsessions_controller.rbdef create p params end# 説明の便宜上色々省略してます <ActionController::Parameters {"user_sessions"=>{"email"=>"example@example.com", "password"=>"12345678"}>フォームに入力した値が格納されていれば一旦問題ないと言えます。それでもログインできない場合はコントローラ側でのパラメータの扱いが怪しそうです。
②-3
params[:email]
という書き方があっているかを確認するsessions_controller.rbdef 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.rbdef mine?(comment) # ロジックは一旦置いといて # trueかfalseを返せれば良い end「コメントのオブジェクトを引数として受け取り、boolean型の値を返す関数」ですね。
それができたらようやくロジックを考えられるようになります。
完成形の一例です。
user.rbdef mine?(comment) id == comment.user_id endこのロジック自体はここでは重要ではありません。重要なのは入出力が決まって初めて実際のロジックを考えられるようになるということです。その逆は有り得ません。
余談ですがスペックを書くとこの辺りの力が鍛えられると思ってるので、初学者こそスペックを書いて欲しいです。
3. ブラウザの開発者ツールを使わない
サーバログを見る人はちらほらいますがブラウザの開発者ツールは見ないという人がほとんどでした。
不具合が起きた際のヒントが何かしらあることが多いので開発者ツールは使うことをおすすめします。
特にJSが絡んでくるとサーバとクライアントのどちらに問題があるのかの見極めが重要になってくるので開発者ツールのコンソールは個人的にはよく見ます。500番台のエラーならすぐにサーバサイドに問題があることがわかります。その他、ネットワークタブも個人的にはよく見ますね。
httpプロトコルの勉強にもなって一石二鳥です。
4. ログを見てググらない/見ない
ログを見て断念する。もしくはそもそも見ない人が一定数います。エラーログは解決のためのヒントが書いてあるので必ず見てググってもらいたいです。
ただこれは難しい問題で、周辺知識がないとエラーログはどうしても読めないと自分は考えてます。
このエラーを見た時にある程度経験のある人は(実装上どこに問題があるかはどうあれ)根本の原因はわかると思います。
一方で知識がまだ少ない初学者にとってはundefined method user
はundefined method user
でしかありません。そこから何か得ることはできないでしょう。『テレビが映らない』という問題に対して『テレビは電気で動いている』という知識がなければ『コンセントはちゃんと入っているかな?』という発想にはそもそも至らないんですね。
プログラミングはそういった周辺知識の積み重ねが重要で、先の例でいうと
- undefinedというのは変数や関数が定義されていないことを意味する
- methodというのは関数である
- 定義されていないというのはオブジェクトに当該変数や関数が書かれていない
という知識がないとどこに問題があるのか察しがつかないでしょう。
そういった知識は一朝一夕で身に付くものではないので地道に積み上げていくことが重要です。
モデル編
5. rails consoleを使って試そうとしない
『ユーザーが作成できない!助けてください!』と言われた時には『まずコンソールで作れるか確認してください』と伝えてます。
これも原因の切り分けの一環です。
ブラウザでぽちぽち操作してユーザーが作れないという場合、モデル・ビュー・コントローラ・ルーティングその他諸々が複合的に絡み合ってるので原因の切り分けがしづらくなります。
なので一旦rails consoleを使ってとりあえずDBに保存できるか、ということを試してもらってます。User.create(email="example@example.com", password="12345678")こんな風に書いてたとしたらそれは当然ユーザーも作れないですね。
Railsには色々なクエリインターフェースがあるので色々と試してみると良いと思います。
6. クラスとインスタンスのイメージを持てていない
User.nameや
user.find(params[:id])と書く人がちらほらいましたが、クラスとインスタンスの違いをイメージだけでも持っていればこんなミスはしないはずです。
ざっくりとしたイメージはこうです。
クラスは設計図
インスタンスは実態「人間」という設計図には具体的なメールアドレスなんて存在しえないですよね。なので『「たろう」さんのメールアドレス』や『「はなこ」さんのメールアドレス』とするのは正しいですが、『「人間」のメールアドレス』とするのは誤りということが感覚でおかしいなと気づくはずです。
イメージを持ち、違和感を感じる感覚を養うことが大事です。
ルーティング・コントローラ編
7. リクエストからレスポンスの流れの理解が曖昧
リンクを踏んでからブラウザに表示されるまでに一体何が行われているのかのイメージがついていない人ほど課題に行き詰まる傾向にあるような気がしています。
このフローを理解していないと実装してても腹落ちしないはずです。
8. RESTの理解が曖昧
カンペを見なくても答えられるくらいこの表が頭の中に入っていないとRailsでの開発は厳しいです。
初学者はこの表をベースに「この機能を実現するにはどういったURLにすれば良いのだろうか?」を考えて試行錯誤してそのURLを出力させることに注力すると良いと思います。Webアプリは原則ユーザーの操作を起点に動くものなので何はともあれまずはビューです。詰まった時は
link_to
だろうがform_with
だろうが自分が期待するURLをなんとかして出力することを意識すると良いと思います。言っちゃえば最初の動作確認の段階ではlink_to
もform_with
も使わなくてもいいんです。コメントを作りたいのであれば先の表で言うと/comments
に対してPOST
すれば良いのでフォームのアクション属性に直接/comments
を書いても良いんですし、同様に、ログイン機能を作りたいのであれば/sessions
に対してPOST
すれば良いのでフォームに直接/sessions
を指定しちゃっていいんです。comments_path
やuser_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個書きます...!!
- 投稿日:2019-12-01T11:05:18+09:00
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の略。「設定」という意味。
- 投稿日:2019-12-01T10:48:37+09:00
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_self1行目はマニフェストファイル(上記のファイル)内のcssの記述をまとめる。
2行目はマニフェストファイル以下のファイルもまとめるという意味。3.プリプロセッサエンジン
指示に従いブラウザに配信できるように結合する。
foobar.js.coffee
foobar.js.erb.coffee
1行目の場合、coffee.script → JavaScriptの順で外側からコンパイルされる。なぜアセットパイプライン?
パソコン的には複数のファイルを読み込むより、1つのファイルで改行や空白を無くしたものの方がレスポンスが早いから。また開発者的にも複数のファイルの方が開発がしやすく、それを一つにまとめてくれるなんて最高!ってこと。
名前付きルートについて
config/routes.rbRails.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.rbRails.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.rbrequire '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.rbrequire '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.rbRails.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.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest test "should get new" do get signup_path assert_response :success end end名前付きルートを設定したら、必ずテストコードも書き換えるようにする。
- 投稿日:2019-12-01T10:15:48+09:00
アクションと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
- 投稿日:2019-12-01T08:16:22+09:00
いいね機能
ツイートアプリにいいね機能をつけていきます。
【その1】 jQueryを入れる
gem 'jquery-rails'
を加え、bundle install
Gemfilegem '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.rbresources :tweets do resources :likes, only: [:create, :destroy] end【その3】コントローラー
ターミナルrails g controller likeslikes_controller.rbclass 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.rbclass Tweet < ApplicationRecord has_many :likes #追記 enduser.rbclass User < ApplicationRecord has_many :likes #追記 end
Like
モデルとlikes
テーブル作成ターミナル$ rails g model like user:references tweet:references $ rails db:migratelike.rbclass 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
ではまた!