- 投稿日:2020-01-04T23:15:00+09:00
form_withでデフォルトのデータ送信先を変更する[Rails]
ポートフォリオアプリを作っています。
作品の一覧と各作品の詳細は誰でも見られるようにしようと思いますが、作品の投稿と編集は管理者だけができるようにしたいと思います。管理者ユーザーの作成は、過去の自分のこちらの記事を読んで実装できたのですが、
【初心者向け】管理者ユーザーと管理者用controllerの追加方法[Ruby, Rails]
その後、管理者ユーザーの投稿機能をつけ、submitボタンを押したときに、以下のようなエラーが出ました。
何、
/works
にPOSTメソッドでつながるルーティングがないそうです。
確かに、rake routes
で確認してみると、管理者ユーザーが画像を投稿するときのルーティングはadmin_works_path
で、URLも/admin/works
です。works GET /works(.:format) works#index work GET /works/:id(.:format) works#show admin_works GET /admin/works(.:format) admin/works#index POST /admin/works(.:format) admin/works#createところが、form_withの記述を見直してみても、以下の通り。
= form_with model: @work, local: true do |f| # 中略 =f.submitそうでした、form_withは投稿先のメソッドに応じて、よしなにURLを変更してくれるのでした。ちなみに、これで
POST(create)
にもPATCH(edit)
にも対応してくれます。わー、便利。ですが、今回はこちらのデフォルトのURLを変更しなければいけません。
form_withのAPIドキュメントを翻訳してくれた方がいたので、こちらの記事を参照しました。Rails 5.1〜: ‘form_with’ APIドキュメント完全翻訳
こちらの記事によると、「ルーティングをadmin_post_urlのように名前空間化する場合は以下のようにします。」とのこと。
<%= form_with(model: [ :admin, @post ]) do |form| %> ... <% end %>こちらを参考に、hamlに合わせて、以下のように記述を変更しました。
第二引数にurlを追記しています。= form_with model: [ :admin, @work ], local: true do |f|・・・そしたら、、、動いた!!
以上、簡単ですが、form_withのデフォルトの送信先を変更する方法でした^^
- 投稿日:2020-01-04T22:36:19+09:00
【Vuex】Rails(devise_token_auth)+Vue.jsのSPAでサインアウトするサンプルコード
はじめに
Rails + Vue.jsのSPAでdevise_token_authを使ってサインアウトするサンプルコードを残します。
リクエストヘッダ情報の保存にVuexを活用しました。
環境
OS: macOS Catalina 10.15.1 Ruby: 2.6.5 Rails: 6.0.2.1 Vue: 2.6.10 vuex: 3.1.2 axios: 0.19.0考え方
devise_token_authの公式ドキュメントの、sign_outのくだりにはこう書かれています。
Use this route to end the user's current session. This route will invalidate the user's authentication token. You must pass in uid, client, and access-token in the request headers.
リクエストヘッダに
uid
,client
,access-token
を含めて、DELETEリクエストを投げる必要があります。このヘッダ情報を保管するためにVuexを使用します。
コード
store.jsimport 'babel-polyfill' import Vue from 'vue' import Vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) const store = new Vuex.Store({ state: { //ヘッダの入れ物を用意。 headers: null, }, mutations: { //サインイン時のレスポンスヘッダから情報を抜き出して保存しておく。 signIn(state, payload) { state.headers = { "access-token": payload["access-token"], "client": payload["client"], "uid": payload["uid"], }; }, //サインアウトしたらヘッダを空にしておく。 signOut(state) { state.headers = null; }, }, actions: { //paramsはemailなどのユーザー情報が入っていると思って下さい。 //(paramsへの情報の入れ方は今回割愛。) signIn(context, params) { axios .post('/api/v1/auth/sign_in', params) .then(function (response) { //ここでレスポンスヘッダを受け取る。 context.commit('signIn', response.headers); }) }, signOut(context) { axios //ここでヘッダ情報を呼び出してDELETEリクエストに含める .delete('/api/v1/auth/sign_out', { headers: context.state.headers }) .then(function () { context.commit('signOut'); }) }, }, }) export default store※本来は
currentUser
のようなstate
も用意してすべきかと思いますが、話を超シンプルにするためにカットしています。呼び出し方
Webpackのエントリーポイントとなるファイルで先程の
store.js
を読み込みます。main.jsimport Vue from 'vue' import App from '../App.vue' import store from './store.js' //こちら document.addEventListener('DOMContentLoaded', () => { const app = new Vue({ store, //こちら render: h => h(App) }).$mount() document.body.appendChild(app.$el) })↓
store.js
を読み込めば任意の単一ファイルコンポーネント内にて、以下のようにしてsignOut
を呼び出せます。Anything.vue<template> <div> <button @click="signOut">ログアウト</button> </div> </template> <script> export default { methods: { signOut() { this.$store.dispatch('signOut') } } } </script>※例では超シンプルに、サインアウトに必要な箇所のみ抜き出しています。
おわりに
最後まで読んで頂きありがとうございました
どなたかの参考になれば幸いです
参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2020-01-04T22:21:31+09:00
【Rails】Rspec何言ってるか解読してみた件。コントローラー#index編
今回は、写真とタイトルを投稿するアプリケーションのtweetsコントローラーのindexアクション(トップページを表示する)が問題なく動くかどうか、Rspecという言語を使ってテスト。
テストする内容は2つ。
tweets_controller.rb内でindexアクションの定義文で生成した@tweetsに、意図したものが入っているかな?
tweets_controller.rbdef index @tweets = Tweet.includes(:user).page(params[:page]).per(5).order("created_at DESC") endテスト環境上で、indexアクションを起こすリクエストを飛ばして、indexアクションに対応したビューが返ってくるかな?
コントローラーのテストの際は、2つの点に注意。
①tweets_controller.rbにおいて、テストしたいアクションに対し、before_actionが定義されていないこと。
②gem rails-controller-testingをインストールしよう。テストで使うダミーデータの生成
また、トップページはこれまで投稿されたツイートが一覧表示される仕様。テスト環境上でのindexアクションで持ってくる、これまでに投稿されたツイートデータを、ダミーでテスト用に生成。gemのfactory_botを利用しよう。ツイート投稿の際、そのツイートは誰が作ったのかという情報も含まれるため、ダミーユーザーも作成しダミーツイートにその情報を含ませよう。
spec/factories/users.rbFactoryBot.define do factory :user do nickname {"mamama"} email {Faker::Internet.email} password {"00000000"} password_confirmation {"00000000"} end endspec/factories/tweets.rbFactoryBot.define do factory :tweet do text {"I love animals"} image {"animals.png"} created_at { Faker::Time.between(from: DateTime.now - 1, to: DateTime.now) } user end end
email{Faker::Internet.email}
tweets_controller.rb内indexアクション定義文のインスタンス変数には、複数のツイートデータが入る。テストで、インスタンス変数に意図したデータが入っているかを調べる際、factory_bot :tweetで生成したツイートデータを複数個一旦登録しておく。そして、テスト環境上で起こすindexアクションで、登録したデータ達を持って来て、インスタンス変数にそれらが入っているか確かめる。しかしemail {mmmm@e.mail}
などでは、テスト上で登録するツイート達の投稿者(user)のemailが全て被ってしまう。今回アプリの仕様上、同じメールアドレスは登録できないようにしてあるので、Fakerというgemをインストールしてemailを自動でランダムに設定させよう。
created_at { Faker::Time.between(from: DateTime.now - 1, to: DateTime.now) }
tweets_controller.rb内indexアクション定義文に、order("created_at DESC”)
とある。ツイートが投稿された日時(created_at)を大きい方から小さい方へ(DESC)、つまり新しいツイート順に並べる(order)という意味。何も指定しないと、ダミーツイートデータのcreated_atは全て同じになるので、意図した順番になっているかテストできない。バラバラな日時を設定する必要があるので、Fakerというgemをインストールし、現在の日時の1日前(DateTime.now - 1)から現在の日時(DateTime.now)の間で、created_atを自動でランダムに設定させよう。
user
factory_botを利用して作った :userのこと。それでは、見ていきましょう。
テストコードを解読。
tweets_controller_spec.rbrequire 'rails_helper' describe TweetsController, type: :controller do describe 'GET #new' do #「【Rails】Rspec何言ってるか解読してみた件。コントローラー#new編」を参照。 end describe 'GET #edit' do #「【Rails】Rspec何言ってるか解読してみた件。コントローラー#edit編」を参照。 end describe 'GET #index' do # 1. tweets_controller.rb内でindexアクションの定義文で生成した@tweetsに、意図したものが入っているかな? it "populates an array of tweets ordered by created_at DESC" do dummyTweets = create_list(:tweet, 3) get :index expect(assigns(:tweets)).to match(dummyTweets.sort{|a, b| b.created_at <=> a.created_at }) end # 2. テスト環境上で、indexアクションを起こすリクエストを飛ばして、indexアクションに対応したビューが返ってくるかな? it "renders the :index template" do get :index expect(response).to render_template :index end end end1. tweets_controller.rb内でindexアクションの定義文で生成した@tweetsに、意図したものが入っているかな?
require 'rails_helper'
RailsでRspecを利用する時の共通の設定が記載されているrails_helper.rbを読み込む。これがなきゃ始まらない。
describe TweetsController, type: :controller do end
tweetsコントローラーのテストをするよ。 type: :controllerこれは書かなきゃいけないみたいですね。
describe 'GET #index' do end
GETメソッドのindexアクションに関するテストだよ。
it "populates an array of tweets ordered by created_at DESC" do end
do - endの間に書かれている、実際に動くテストコードの説明。投稿日時で降順(created_at DESC)となっているtweetsの配列(an array of)があるか(populates)と言っている。日本語でも良いらしい。ちなみに、it do endのセット1つで、1exampleと呼ぶ。
dummyTweets = create_list(:tweet, 3)
factory_botで生成したダミーツイートを3つ登録するという意味。それをdummyTweetに入れる。ちなみにこれら3つのダミーツイートの作成者はemailだけが異なっている。
get :index
テスト環境上でリクエストを起こすコード。get :index
でアクション名indexをそのリクエストに情報として渡す。
expect(assigns(:tweets)).to match(dummyTweets.sort{|a, b| b.created_at <=> a.created_at })
インスタンス変数に意図したものが入っているかな?というテストコード。assignsは、インスタンス変数の中身をチェックしてくれるやつ。今回は、tweets_controller.rb内で定義した@tweetsが対象なので、(:tweets)と書こう。コードの意味は、@tweetsの中身は、一旦登録させてindexアクションで持ってこさせたdummyTweetsと一緒でかつ並びもcreated_atの降順(.sort{|a, b| b.created_at <=> a.created_at })である(.to match)と期待する(expect)。tweets_controller_spec.rb#省略 describe 'GET #index' do # 1. tweets_controller.rb内でindexアクションの定義文で生成した@tweetsに、意図したものが入っているかな? it "populates an array of tweets ordered by created_at DESC" do dummyTweets = create_list(:tweet, 3) get :index expect(assigns(:tweets)).to match(dummyTweets.sort{|a, b| b.created_at <=> a.created_at }) end # 2. テスト環境上で、indexアクションを起こすリクエストを飛ばして、indexアクションに対応したビューが返ってくるかな? it "renders the :index template" do get :index expect(response).to render_template :index end end #省略2. テスト環境上で、indexアクションを起こすリクエストを飛ばして、indexアクションに対応したビューが返ってくるかな?
it "renders the :index template" do end
do - endの間に書かれている、実際に動くテストコードの説明。indexアクションに対応したビューファイル(template)が呼び出される(renders)ことをテストしますと言っている。
get :index
テスト環境上でリクエストを起こすコード。get :index
でアクション名indexをそのリクエストに情報として渡す。
expect(response).to render_template :index
ちゃんと期待するビューが返ってくるかな?というテストコード。resposeが、indexが起きた時に呼ばれるrender_template(ビューである)と期待する(expect)。テストを実施。
ターミナルにて
% bundle exec rspec spec/controllers/tweets_controller_spec.rb
を実行しよう。
初学者の視点からRailsアプリケーションのRspecを使ったテストについてまとめてみました。指摘やご意見お待ちしてます。モデルや、コントローラーのその他アクションについても、まとめていきたいと思います。
最後までご覧いただきありがとうございました!
- 投稿日:2020-01-04T21:45:35+09:00
RailsでiOSアプリのURI スキームに対応する
一瞬「あれっ...」ってなったので備忘録
TL; DR
routes.rb
内でダイレクトルーティングで定義すればOKバージョン
- Ruby:2.6.1
- Rails:5.2.3
タイトルにiOSと書いてますがAndroidも対応できます。ただのRoutingの話なので。
やりたいこと
- RailsがWebとモバイルアプリの両方のサーバサイドの役割を担っている
- モバイルアプリ内で、特定の画面に遷移させるURIスキームを使いたい
- メニューなどで表示されていないページへの遷移など
- 上記の例としてInstagramに「運営からのコメント」があり、チャットで操作説明を受ける画面に遷移させたいような目的があったとする
- ヘルパーリンク:
qa_chat
- URIスキーム:
instagram://qa_chat
解決策
routes.rb
routes.rb
で、以下のようなダイレクトルーティングを設定してあげれば良いです。routes.rbdirect :qa_chat do "instagram://qa_chat" endヘルパーリンク
Railsと同一のViewでヘルパーリンクを利用する場合は、以下のように記述します。
sample.erb<%= link_to qa_chat_url %>ちなみにダイレクトルーティングについて、Railsガイドでは以下の部分に記載があります。
(このURIスキームについての記載はありませんが、あくまで参考までに)
小ネタ
外部URLもroutes.rbで変数として定義できる
ダイレクトルーティングといえば、こちらの使い方のほうが一般的です。
よく使う外部URLを変数定義して、Railsアプリケーション内で使うことができます。
サービスLPのURLを記述する時は、個別にではなく、routes.rb
にまとめて記述するほうが管理しやすいです。example.rbdirect :homepage do "http://www.rubyonrails.org" end
- 投稿日:2020-01-04T19:00:58+09:00
RSpec初心者のdevise認証システムテスト
はじめに
RSpecの機能や構文を全く知らない状態からユーザー登録や編集のRequest specを行った際の備忘録です。
初心者なので細かい記述や命名は参考にしないことをお勧めします。環境
・Rails 5.2.4.1
・Ruby 2.6.5
・rspec-rails 3.9.0
・factory_bot_rails 5.1.1
deviseで登録/ログインシステムを構築しています。RSpecとFactory_botの導入についてはこちらが参考になりました。
参考記事:RailsアプリへのRspecとFactory_botの導入手順事前準備
テスト環境でログインを実行するためにはヘルパーを用意する必要があります。
rails_helper.rbにコンフィグを追加することにより、spec/requests/以下でsign_inヘルパーを使えるようにしておきます。rails_helper.rb... RSpec.configure do |config| config.include Devise::Test::IntegrationHelpers, type: :request #sign_inヘルパーを提供してくれます config.include FactoryBot::Syntax::Methods #ついでにFactoryBotもincludeしておきます endこれでRequestテストの際にsign_inヘルパーが使えるようになりました。
factoriesフォルダでテストデータを作る
factoriesフォルダ内でFactory_bot使ってテストに使うユーザーのデータを作成します。
emailにはデータを生成する毎に通し番号をふってユニークな値を作るようにしています。spec/factories/users.rbFactoryBot.define do factory :user do #factory :testuser, class: User do のようにクラスを明示すればモデル名以外のデータも作れます。 name { "test" } sequence(:email) { |n| "TEST#{n}@example.com" } password { "testuser" } end endこのようにテストデータを定義することでspecテスト内でテスト用データを生成することができるようになります。
# DBへレコード生成 @user = create(:user) # モデルのみの作成 @user = build(:user) # userを生成しておく let(:user) { create(:user) } # ハッシュとして使えるパラメータuser_paramsを生成しておく let(:user_params) { attributes_for(:user) } # 先ほど config.include FactoryBot::Syntax::Methods を追加したことでFactoryBot.を指定せずにcreateなどが使えるようになっています。参考記事:RailsアプリへのRspecとFactory_botの導入手順
テストの基本構造
describeでテストをグループ化、contextで条件を分け、itで条件ごとの結果を検査する、という流れがRSpecテストの基本構造です。
結果の検査にはexpectという機能を使って期待する値と実際の結果が等しいかを比較します。
例を載せておきます。RSpec.describe User do describe '#greet' do context '12歳以下の場合' do it 'ひらがなで答えること' do user = User.new(name: 'たろう', age: 12) expect(user.greet).to eq 'ぼくはたろうだよ。' end end context '13歳以上の場合' do it '漢字で答えること' do user = User.new(name: 'たろう', age: 13) expect(user.greet).to eq '僕はたろうです。' end end end endRSpecの基礎に関しては以下の記事が非常に参考になります。
引用:使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」Request specを書いていく
deviseで生成されたアクションの中からいくつか書いていきます。
POST #create
spec/requests/users_autentications_spec.rbrequire 'rails_helper' RSpec.describe "UserAuthentications", type: :request do let(:user) { create(:user) } let(:user_params) { attributes_for(:user) } let(:invalid_user_params) { attributes_for(:user, name: "") } describe 'POST #create' do before do ActionMailer::Base.deliveries.clear end context 'パラメータが妥当な場合' do it 'リクエストが成功すること' do post user_registration_path, params: { user: user_params } expect(response.status).to eq 302 end it '認証メールが送信されること' do post user_registration_path, params: { user: user_params } expect(ActionMailer::Base.deliveries.size).to eq 1 end it 'createが成功すること' do expect do post user_registration_path, params: { user: user_params } end.to change(User, :count).by 1 end it 'リダイレクトすること' do post user_registration_path, params: { user: user_params } expect(response).to redirect_to root_url end end context 'パラメータが不正な場合' do it 'リクエストが成功すること' do post user_registration_path, params: { user: invalid_user_params } expect(response.status).to eq 200 end it '認証メールが送信されないこと' do post user_registration_path, params: { user: invalid_user_params } expect(ActionMailer::Base.deliveries.size).to eq 0 end it 'createが失敗すること' do expect do post user_registration_path, params: { user: invalid_user_params } end.to_not change(User, :count) end it 'エラーが表示されること' do post user_registration_path, params: { user: invalid_user_params } expect(response.body).to include 'prohibited this user from being saved' end end end endpostリクエストのパラメータにletで用意したものを使ったり、httpステータスからリクエストの成功を確認したり、ActionMailerの機能で認証メールが送信されたことを確認したりしています。
$ rspec コマンドを実行し、テストが通りました。GET #edit
spec/requests/users_autentications_spec.rb... describe 'GET #edit' do subject { get edit_user_registration_path } context 'ログインしている場合' do before do user.confirm sign_in user end it 'リクエストが成功すること' do is_expected.to eq 200 end end context 'ゲストの場合' do it 'リダイレクトされること' do is_expected.to redirect_to new_user_session_path end end end ...注意点
deviseでメール認証機能をつけている場合はサインインの前に user.confirm を行い認証を済ませておく必要があります。
GET #show
エラーの捉え方
get "/users/#{user_id}" で存在しないユーザーのプロフィールを見ようとした場合、エラーが発生したとします。
spec/requests/users_autentications_spec.rb... describe 'GET #show' do ... context 'ユーザーが存在しない場合' do it 'エラーが発生すること' do user_id = user.id user.destroy expect{ get "/users/#{user_id}" }.to raise_error ActiveRecord::RecordNotFound end end end ...上のコードではexpectに渡すgetリクエストを{}で囲んでブロックとしているため、検査対象がブロック内の返り値となりエラーを捉えることができます。
普通にexpect()として渡した場合はexpectメソッドに渡される前にgetリクエストが実行されエラーが発生するため検査することが出来ません。
参考記事:RSpec でエラーを捉えらんないアレなミス最後に
今回は認証システムのRequest specを通じてRSpecの基礎について学習しましたが、この他にも様々な機能や構文があるので引き続き学んでいこうと思います。
余談ですが最初IntegrationHelpersの存在に気付かずsupportフォルダでsign_inヘルパーを自作するという遠回りをしてしまいました。。。
参考記事:type: :requestのテストでsign_in/sign_outする
Rails5でコントローラのテストをController specからRequest specに移行する
- 投稿日:2020-01-04T17:42:32+09:00
フォームクラスを使う
※この記事は、シリーズ記事「多人数によるRailsアプリケーション開発」の1つにする予定です。
ここでは、フォームクラスの使い方について考えます。フォームから送信されたデータを保存するときに、ActiveRecordのモデルではなく、ActiveModel::Modelをインクルードしたクラスを使い、バリデーションやコールバックをそちらに移してしまう、というものです。
私はフォームクラスと呼んでいますが、ネット上ではフォームオブジェクト(Form Object)と呼んでいる人が多いので、検索するときはそちらで。
サンプル: https://github.com/kazubon/blog-rails6-vuejs
環境: Rails 5.2以上メリット
- 機能1つ(画面1つやフォーム1つ)をクラス1つに対応させることで、ソースコードのどこに実装があるか分かりやすくなる。別のプログラマが改修する際に、あちこちのファイルを探し回らなくてよい。
- Fat Controllerを防ぐとともに、モデルクラスをすっきりしたものにできる。たとえば、モデルクラスの中に特定の画面でしか使わない機能が長々と書かれている、という状態を解消できる。
- has_manyの関係にあるデータの配列など、複数のモデルがからむデータを保存するときは、フォームクラスの中でパラメータの処理と保存をベタに書けば、Railsに慣れていない人にもわかりやすい。
- テストコードが書きやすい。パラメータをいろいろ変えながらフォームクラスのテストを念入りにすれば、E2Eテストを最小限にしてテストの実行時間を減らせる。
- モデルからバリデーションやコールバックを外すと、開発用のシードデータやテスト用のFactoryBotが書きやすくなる。
新規作成・更新用のフォーム
サンプルを元にフォームクラスの流れをざっと紹介します。上記のサンプルの中でブログ記事を保存するのに使っているEntries::Formです。
コントローラはこんな感じです。なるべくRailsの基本的な型を守りつつ、モデルクラスをフォームクラスに置き換えた形にします。
app/controllers/entries_controller.rbdef create @entry = Entry.new @form = Entries::Form.new(current_user, @entry, entry_params) if @form.save render json: { location: entry_path(@entry), notice: '記事を作成しました。' } else render json: { alert: '記事を作成できませんでした。' }, status: :unprocessable_entity end endフォームクラスには、2つのモジュールActiveModel::ModelとActiveModel::Attributesをインクルードします。ActiveModel::Modelで使えるようになるものは、おもに次の機能です。
- assign_attributesメソッド
- バリデーション
- errorsオブジェクト
ActiveModel::Attributesでは、attributeメソッドが使えるようになります。ActiveModel::Attributesを使うを参照してください。
app/forms/entries/form.rbclass Entries::Form include ActiveModel::Model include ActiveModel::Attributesattributeメソッドでparamsで渡されるパラメータ名を並べます。それ以外の属性はattr_accessorで作ります。
app/forms/entries/form.rbattribute :title, :string attribute :body, :string attribute :draft, :boolean, default: false attribute :published_at, :datetime attribute :tags attr_accessor :user, :entryバリデーションはモデルの中ではなく、フォームクラスの中で指定します。
app/forms/entries/form.rbvalidates :title, presence: true, length: { maximum: 255 } validates :body, presence: true, length: { maximum: 40000 } validate :check_tagsinitializeでは、
super(params)
でパラメータを格納します。app/forms/entries/form.rbdef initialize(user, entry, params = {}) @user = user @entry = entry super(params) endこの
super(params)
は次のように置き換えても同じです。@attributes = self.class._default_attributes.deep_dup assign_attributes(params)フォームクラスにsaveメソッドを用意します。ActiveRecordのモデルと同様にバリデーションに失敗したらfalseを返します。複数のモデルがからむときは、トランザクションの中でモデルを使ってレコードを保存します。
モデルにはコールバックを書かずに、保存前後の処理はこの中に入れます。
app/forms/entries/form.rbdef save return false unless valid? # バリデーション実行 set_published_at # before_saveに当たるメソッド entry.user = user ActiveRecord::Base.transaction do entry.update!(attributes.except('tags')) # モデルを使って保存 save_tags! # after_saveに当たるメソッド end true end必要に応じて、assign_attributesやvalid?を上書きしてもよいです。
検索フォーム
上記のサンプルでは、検索用のフォームでもフォームクラスを使っています。ブログ記事を検索するEntries::SearchFormです。コントローラはこんな感じです。
app/controllers/entries_controller.rbdef index @user = User.active.find(params[:user_id]) if params[:user_id].present? @form = Entries::SearchForm.new(current_user, @user, search_params) respond_to :html, :json endEntries::Formと同様に、ActiveModel::ModelとActiveModel::Attributesをインクルードして属性を設定し、initializeでパラメータを読み込みます。
app/forms/entries/search_form.rbclass Entries::SearchForm include ActiveModel::Model include ActiveModel::Attributes attribute :title, :string attribute :tag, :string attribute :offset, :integer attribute :sort, :string attr_accessor :current_user, :user, :entries, :entries_count def initialize(current_user, user, params = {}) @current_user = current_user @user = user super(params) end記事の配列を取り出すメソッドと記事数を取り出すメソッドです。
app/forms/entries/search_form.rbdef entries @entries ||= relation.preload(:user, :tags).order(sort_key => :desc) .limit(20).offset(offset) end def entries_count @entries_count ||= relation.count endクエリーを組み立てるメソッドです。モデルの中にスコープやクエリーを返すメソッドを書かずに、フォームクラスに記述します。
app/forms/entries/search_form.rbdef relation return @relation if @relation rel = if user user == current_user ? user.entries : user.entries.published else Entry.joins(:user).merge(User.active).published end if title.present? rel = rel.where('title LIKE ?', '%' + title + '%') end if tag.present? rel = rel.joins(:tags).where('LOWER(tags.name) = LOWER(?)', tag) end @relation = rel endモデルから移すもの
バリデーション
私の観察するところでは、モデルでのバリデーションはたいてい次のどちらかになります。
- バリデーションと保存は特定のフォームでしか行われない。
- 複数のフォームで保存が行われるが、バリデーションの仕方がそれぞれ違う。
どちらの場合でも、フォームクラスの中にバリデーションを移すほうが「どこで何やってるのか」が明確になります。特にバリデーションの仕方が違う場合は、
if
オプションをやたらと生やすよりも、別々のフォームクラスで別々のバリデーションを書くほうがよいです。必要があればモデルのほうにバリデーションを残してもかまいません。フォームとモデルで同じバリデーションを二重にやってもたいてい害はありません。
コールバック
before_validationやafter_saveなどのモデルのコールバックはなるべく避けます。コールバックではレコード保存時の処理の流れがわかりにくくなりますし、バリデーションと同様に画面によってやることが変わったりします。上記のフォームクラスのsaveメソッドのようにベタに書くほうが楽です。
スコープ
モデルクラスの中に大量のスコープを書いたり、
->{ }
の中に長々としたクエリーメソッドを書かなくても、上記のSearchFormのように検索用のクラスの中にクエリーを書けば、たいていはそれで済みます。ただし、
scope :active, ->{ where(deleted_at: nil) }
のような、ソースコードの各所から頻繁に使われる簡潔なスコープは、モデルクラスの中に書くほうがよいでしょう。Nested attributesは使わない
あるモデルにぶら下がっている複数のモデル(has_manyの関係にあるモデル)を保存するときは、Nested attributes(accepts_nested_attributes_forやfields_for)のことは忘れたほうがよいです。
Nested attributesはプログラマの頭を痛め、Railsのコードを取っ付きにくいものにします。上記のサンプルでは、記事のタグを保存するときは、save_tags!メソッドで単純にTagモデルの配列を保存しています。
そのほか補足
i18nでパラメータ名を追加する
errorsに加えたエラーのメッセージを表示したいときは、config/ja.ymlで activemodel -> attributes -> フォームクラス名の下にパラメータ名を記述します。
activemodel: attributes: entries/form: title: タイトルフォームクラスが長くなったときは
フォームクラスが長大化してきたら、適宜Serviceクラスを作って処理を任せます。たとえば、このサンプルのsave_tags!メソッドが長いものになったら、EntryTagsSaver(名前はてきとうです)を作ってレコード保存用のコードを移します。
def save_tags! service = EntryTagsSaver.new(entry, tags) service.perform endテンプレートで使うメソッド
Fat Controllerを避けるには、コントローラ内でインスタンス変数をいくつも作ったりせずに、フォームクラスにヘルパーメソッドを書きます。たとえば、このサンプルのフォームで、ユーザーがよく使うタグを選ばせる機能を追加するときは、Entris::Formクラスにメソッドを足します。
def my_tags # よく使うタグ名の配列を作成 endテンプレート(この場合はjson)でそのメソッドを呼び出します。
json.entry do # そのほかのデータ... json.my_tags @form.my_tags end関連記事
- 投稿日:2020-01-04T17:30:44+09:00
Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストを操作する
マイクロポストリソースのルーティング
Micropostsコントローラーの内容を実装する前提条件として、まずは必要なルーティングを
config/routes.rb
に記述していかなければなりません。但し、「Micropostリソースのインターフェースへのアクセスは、プロフィールページ・Homeページを介して行われる」という前提があるため、Mircopostsコントローラーにnew
やedit
のようなアクションは必要とされません。実際に必要とされるアクションは、create
とdestroy
の2つのみとなります。以上を踏まえて、
config/routes.rb
に対して実際に行う変更の内容は、以下のようになります。config/routes.rbRails.application.routes.draw do get 'password_resets/new' get 'password_resets/edit' 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' post '/signup', to: 'users#create' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users resources :account_activations, only: [:edit] resources :password_resets, only: [:new, :create, :edit, :update] + resources :microposts, only: [:create, :destroy] end
上述
config/routes.rb
を前提とした場合、Micropstsリソースが提供するRESTfulルートの内訳は以下のようになります。
HTTPリクエスト URL アクション 名前付きルート POST
/microposts create
microposts_path
DELETE
/microposts/1 destroy
micropost_path(micropost)
マイクロポストのアクセス制御
長くなりましたので、別記事で解説します。
演習 - マイクロポストのアクセス制御
1. なぜUsersコントローラ内にある
logged_in_user
フィルターを残したままにするとマズイのでしょうか? 考えてみてください。「スーパークラスで定義された名前と同じ名前のメソッドをサブクラスで定義すると、当該サブクラスにおいては、当該メソッドの定義は上書きされる」というのが、オブジェクト指向一般のルール1です。そして、
UsersController
はApplicationController
のサブクラスです。また、今回の実装においては、
ApplicationController#logged_in_user
メソッドの実装をUsersController
で上書きしなければならない理由はありません。そのような状況で無駄にlogged_in_user
メソッドの実装を上書きすることは、以下のような問題があります。
- 存在しなくてもいい重複したコードがプログラム中に存在することになる
- 無駄なコードは、無駄にバグを埋め込む原因となる
マイクロポストの作成・フィードの実装・マイクロポストの削除・フィード画面のマイクロポストに対するテスト
「テストから先に書き、テストの内容を網羅していく」という方法を取った結果、内容が非常に長くなりました。別記事で解説します。
演習 - マイクロポストを作成する
1. Homeページをリファクタリングして、
if-else
文の分岐のそれぞれに対してパーシャルを作ってみましょう。まずはログイン済みユーザーのHomeページに対応するパーシャルを作成します。ファイル名は
app/views/shared/_home_logged_in.erb
とします。内容は以下のようになります。app/views/shared/_home_logged_in.erb<div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= render 'shared/user_info' %> </section> <section class="micropost_form"> <%= render 'shared/micropost_form' %> </section> </aside> <div class="col-md-8"> <h3>Micropost Feed</h3> <%= render 'shared/feed' %> </div> </div>続いて、未ログインユーザーのHomeページに対応するパーシャルを作成します。ファイル名は
app/views/shared/_home_not_logged_in.erb
とします。内容は以下のようになります。app/views/shared/_home_not_logged_in.erb<div class="center jumbotron"> <h1>Welcome to the Sample App</h1> <h2> This is the home page for the <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a> sample application. </h2> <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %> </div> <%= link_to image_tag("rails.png", alt: "Rails logo"), 'http://rubyonrails.org/' %>続いて、新たに作成したパーシャルを元々のHomeページのビューで使うようにします。新たな
app/views/static_pages/home.html.erb
の内容は以下のようになります。app/views/static_pages/home.html.erb<% provide(:title, "Home") %> <% if logged_in? %> <%= render 'shared/home_logged_in' %> <% else %> <%= render 'shared/home_not_logged_in' %> <% end %>最後に、テストスイート全体を実行して問題がないことを確認しておきましょう。
# rails test Running via Spring preloader in process 419 Started with run options --seed 35036 60/60: [=================================] 100% Time: 00:00:14, Time: 00:00:14 Finished in 14.10136s 60 tests, 323 assertions, 0 failures, 0 errors, 0 skips演習 - フィードの原型
1. 新しく実装したマイクロポストの投稿フォームを使って、実際にマイクロポストを投稿してみましょう。Railsサーバーのログ内にある
INSERT
文では、どういった内容をデータベースに送っているでしょうか? 確認してみてください。まずは、新たに実装したマイクロポストの投稿フォームに文字を入力し、実際に投稿してみます。
「Post」ボタンを押すと、以下のような画面が出力されます。
「Micropost created!」というフラッシュメッセージもきちんと表示されていますね。
このときの
POST
リクエストに対して出力されたRailsサーバーのログは、以下のようになっています。Started POST "/microposts" for 172.17.0.1 at 2020-01-03 05:04:24 +0000 Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by MicropostsController#create as HTML Parameters: {...略, "micropost"=>{"content"=>"Est vitae enim nihil qui voluptates nemo quia."}, "commit"=>"Post"} User Load (7.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] (0.2ms) begin transaction SQL (15.6ms) INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "Est vitae enim nihil qui voluptates nemo quia."], ["user_id", 1], ["created_at", "2020-01-03 05:04:24.981222"], ["updated_at", "2020-01-03 05:04:24.981222"]] (13.1ms) commit transaction Redirected to http://localhost:8080/ Completed 302 Found in 55ms (ActiveRecord: 36.7ms)特にSQLの
INSERT
文のみを見ると、以下のようになっています。INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "Est vitae enim nihil qui voluptates nemo quia."], ["user_id", 1], ["created_at", "2020-01-03 05:04:24.981222"], ["updated_at", "2020-01-03 05:04:24.981222"]]2. コンソールを開き、user変数にデータベース上の最初のユーザーを代入してみましょう。その後、
Micropost.where("user_id = ?", user.id)
とuser.microposts
、そしてuser.feed
をそれぞれ実行してみて、実行結果がすべて同じであることを確認してみてください。ヒント:
==
で比較すると結果が同じかどうか簡単に判別できます。# rails console Running via Spring preloader in process 454 Loading development environment (Rails 5.1.6) >> user = User.first User Load (2.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] >> Micropost.where("user_id = ?", user.id) == user.microposts Micropost Load (9.4ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC [["user_id", 1]] Micropost Load (3.3ms) SELECT "microposts".* FROM "microposts" WHERE (user_id = 1) ORDER BY "microposts"."created_at" DESC => true >> Micropost.where("user_id = ?", user.id) == user.feed => true >> user.microposts == user.feed Micropost Load (6.6ms) SELECT "microposts".* FROM "microposts" WHERE (user_id = 1) ORDER BY "microposts"."created_at" DESC => true
Micropost.where("user_id = ?", user.id) == user.microposts
Micropost.where("user_id = ?", user.id) == user.feed
user.microposts == user.feed
以上3つの条件文は、いずれも
true
を返しています。演習 - マイクロポストを削除する
1. マイクロポストを作成し、その後、作成したマイクロポストを削除してみましょう。次に、Railsサーバーのログを見てみて、DELETE文の内容を確認してみてください。
マイクロポストの「delete」リンクをクリックすると、まず以下のように「You sure?」という確認メッセージが表示されます。
そこで「OK」をクリックすると、マイクロポストが実際に削除されます。
「Micropost deleted」というフラッシュメッセージも表示されています。
Started POST "/microposts" for 172.17.0.1 at 2020-01-03 05:28:46 +0000 Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by MicropostsController#create as HTML Parameters: {...略, "micropost"=>{"content"=>"Fantastic Wooden Shirt"}, "commit"=>"Post"} User Load (3.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] (0.2ms) begin transaction SQL (25.1ms) INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "Fantastic Wooden Shirt"], ["user_id", 1], ["created_at", "2020-01-03 05:28:46.381435"], ["updated_at", "2020-01-03 05:28:46.381435"]] (14.8ms) commit transaction Redirected to http://localhost:8080/ Completed 302 Found in 72ms (ActiveRecord: 43.8ms) ...略 Started DELETE "/microposts/304" for 172.17.0.1 at 2020-01-03 05:29:04 +0000 Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by MicropostsController#destroy as HTML Parameters: {"authenticity_token"=>"7eAZU7veT9D+/vpKM8o9evxc9AF0EKF1aMXomohNHiPbnBWkcEODxVMeIlQ2ThxDQn2AQWcW+aBy0cE3CTsMbA==", "id"=>"304"} User Load (5.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Micropost Load (2.1ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ? [["user_id", 1], ["id", 304], ["LIMIT", 1]] (0.2ms) begin transaction SQL (33.6ms) DELETE FROM "microposts" WHERE "microposts"."id" = ? [["id", 304]] (16.6ms) commit transaction Redirected to http://localhost:8080/ Completed 302 Found in 73ms (ActiveRecord: 58.4ms)削除リンクの
href
属性には、例えば /microposts/304 のようなパスへのリンクが設定されています。SQLの
DELETE
文の内容のみを見ると、以下のようになります。DELETE FROM "microposts" WHERE "microposts"."id" = ? [["id", 304]]なお私は、以下のtypoにより、最初「deleteリンクをクリックしたときに、確認メッセージが表示されることなくマイクロポストが削除されてしまう」という不具合を発生させてしまいました。
- <%= link_to "delete", micropost, method: :delete, data: { confierm: "You sure?" } %> + <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %>2.
redirect_to request.referrer || root_url
の行をredirect_back(fallback_location: root_url)
と置き換えてもうまく動くことを、ブラウザを使って確認してみましょう。(このメソッドはRails 5から新たに導入されました)
まずは、
app/controllers/microposts_controller.rb
の内容を以下のように変更してみます。app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController ...略 def destroy @micropost.destroy flash[:success] = "Micropost deleted" - redirect_to request.referrer || root_url + redirect_back(fallback_location: root_url) end ...略 endテストスイート全体が正常に完了することを確認します。
# rails test Running via Spring preloader in process 492 Started with run options --seed 51584 60/60: [=================================] 100% Time: 00:00:10, Time: 00:00:10 Finished in 10.53625s 60 tests, 323 assertions, 0 failures, 0 errors, 0 skipsテストスイート全体が正常に完了しましたね。
続いて、ブラウザでマイクロポストの新規投稿・削除操作を行い、そのときのRailsサーバーのログを確認してみます。
Started POST "/microposts" for 172.17.0.1 at 2020-01-03 05:28:46 +0000 Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by MicropostsController#create as HTML Parameters: {...略, "micropost"=>{"content"=>"Fantastic Wooden Shirt"}, "commit"=>"Post"} User Load (3.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] (0.2ms) begin transaction SQL (25.1ms) INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "Fantastic Wooden Shirt"], ["user_id", 1], ["created_at", "2020-01-03 05:28:46.381435"], ["updated_at", "2020-01-03 05:28:46.381435"]] (14.8ms) commit transaction Redirected to http://localhost:8080/ Completed 302 Found in 72ms (ActiveRecord: 43.8ms) ...略 Started DELETE "/microposts/304" for 172.17.0.1 at 2020-01-03 05:29:04 +0000 Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by MicropostsController#destroy as HTML Parameters: {...略, "id"=>"304"} User Load (5.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Micropost Load (2.1ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ? [["user_id", 1], ["id", 304], ["LIMIT", 1]] (0.2ms) begin transaction SQL (33.6ms) DELETE FROM "microposts" WHERE "microposts"."id" = ? [["id", 304]] (16.6ms) commit transaction Redirected to http://localhost:8080/ Completed 302 Found in 73ms (ActiveRecord: 58.4ms)マイクロポストの投稿・削除ともに正常に行えているようですね。
演習 - フィード画面のマイクロポストをテストする
1. リスト 13.55で示した4つのコメント (「無効な送信」など) のそれぞれに対して、テストが正しく動いているか確認してみましょう。具体的には、対応するアプリケーション側のコードをコメントアウトし、テストが
red
になることを確認し、元に戻すとgreen
になることを確認してみましょう。長くなりましたので、別記事で解説します。
2. サイドバーにあるマイクロポストの合計投稿数をテストしてみましょう。このとき、単数形 (micropost) と複数形 (microposts) が正しく表示されているかどうかもテストしてください。
ヒント: リスト 13.57を参考にしてみてください。
test/integration/microposts_interface_test.rb
の変更内容は以下のようになります。test/integration/microposts_interface_test.rbrequire 'test_helper' class MicropostsInterfaceTest < ActionDispatch::IntegrationTest def setup @user = users(:rhakurei) + @other_user = users(:skomeiji) end ...略 + + test "micropost sidebar count" do + log_in_as(@user) + get root_path + assert_match "#{@user.microposts.count} microposts", response.body + # まだマイクロポストを投稿していないユーザー + log_in_as(@other_user) + get root_path + assert_match "0 microposts", response.body + @other_user.microposts.create!(content: "A micropost") + get root_path + assert_match /1 micropost(?!s)/, response.body + end endなお、以下の正規表現は、「"1 micropost"には一致するが、"1 microposts"には一致しない」という動作を想定しています。
/1 micropost(?!s)/
ここまでの実装内容に問題がなければ、現時点でこのテストは成功するはずです。
# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 707 Started with run options --seed 57726 2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.85516s 2 tests, 21 assertions, 0 failures, 0 errors, 0 skipsサイドバーで単数形と複数形が正しく表示されていない場合
全て単数形で表示されている場合
app/views/shared/_user_info.html.erb
の内容が以下のようになっていた場合、今回実装したテストの結果はどうなるでしょうか。app/views/shared/_user_info.html.erb<%= link_to gravatar_for(current_user, size: 50), current_user %> <h1><%= current_user.name %></h1> <span><%= link_to "view my profile", current_user %><span> - <span><%= pluralize(current_user.microposts.count, "micropost" %></span> + <span><%= "#{current_user.microposts.count} micropost") %># rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 733 Started with run options --seed 53872 FAIL["test_micropost_sidebar_count", MicropostsInterfaceTest, 3.921162500002538] test_micropost_sidebar_count#MicropostsInterfaceTest (3.92s) Expected /34\ microposts/ to match "...略 <span>34 micropost\n</span> ...略". test/integration/microposts_interface_test.rb:45:in `block in <class:MicropostsInterfaceTest>'私の環境では、現時点で
test/integration/microposts_interface_test.rb
の45行目は以下のようになっています。test/integration/microposts_interface_test.rb(45行目)assert_match "#{@user.microposts.count} microposts", response.body(
@user
の対象となるユーザーは複数のマイクロポストに紐付けされているのに)「microposts」という文字列がHTML中にない、ということですね。想定通りの失敗です。全て複数形で表示されている場合
今度は、
app/views/shared/_user_info.html.erb
の内容が以下のようになっていた場合、今回実装したテストの結果はどうなるでしょうか。app/views/shared/_user_info.html.erb<%= link_to gravatar_for(current_user, size: 50), current_user %> <h1><%= current_user.name %></h1> <span><%= link_to "view my profile", current_user %><span> - <span><%= pluralize(current_user.microposts.count, "micropost" %></span> + <span><%= "#{current_user.microposts.count} microposts") %># rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 940 Started with run options --seed 25315 FAIL["test_micropost_sidebar_count", MicropostsInterfaceTest, 3.437450199999148] test_micropost_sidebar_count#MicropostsInterfaceTest (3.44s) Expected /1 micropost(?!s)/ to match "...略 <span>1 microposts\n</span> ...略". test/integration/microposts_interface_test.rb:52:in `block in <class:MicropostsInterfaceTest>' 2/2: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.44661s 2 tests, 21 assertions, 1 failures, 0 errors, 0 skips私の環境では、
test/integration/microposts_interface_test.rb
の52行目は以下のようになっています。test/integration/microposts_interface_test.rbassert_match /1 micropost(?!s)/, response.body「"1 microposts"でない"1 micropost"という文字列が見つからない」という趣旨のメッセージが出てテストが失敗している、ということになります。想定通りの失敗ですね。
こうしたルールを利用してメソッドの定義を上書きすることを「オーバーライド」と言います。 ↩
- 投稿日:2020-01-04T16:08:58+09:00
GKEで自作コンテナのRails6を起動する
はじめに
Rails6プロジェクトの新規作成からGKE上で動作させるまでを試した結果となります。
今回試したのは以下3点です。
- Dockerイメージ(Ruby公式 2.7.0)から新たにRails6用Dockerイメージを作成する
- 作成したDockerイメージをDockerHubへ登録
- DockerHubに登録したイメージを使ってGKEのノードを作成し起動する手順
1.Dockerイメージの取得
$ docker pull ruby:2.7.0
2.プロジェクトディレクトリ、Gemfileの作成
$ mkdir project_name $ cd project_name $ docker run --rm -v "$PWD":/usr/src/project_name -w /usr/src/project_name ruby:2.7.0 bundle init3.Gemfileの編集
以下のコメント箇所を外したくらいです
Gemfile#gem 'rails'4.Dockerファイルの用意
Docerfileの内容については以下の記事を参考にさせて頂きました。
https://qiita.com/togana/items/30b22fc39fe6f7a188ec今回の主な変更点は以下の点になりました
- yarnのリポジトリ設定(Rails6の起動にはwebpackerが必要で、Webpackerのインストールにはyarnが必要)
- yarnのインストール(上記と同じ理由)
- webpackerのインストール
DockerfileFROM ruby:2.7.0 ENV APP_ROOT /usr/src/rails6-base WORKDIR $APP_ROOT # yarnのリポジトリ設定(Rails6の起動の為に必要) RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list # packageリストの更新 RUN apt-get update # packageからのインストール RUN apt-get install -y \ nodejs \ default-mysql-client \ sqlite3 \ yarn \ --no-install-recommends # packageインデックスファイルの削除 RUN rm -rf /var/lib/apt/lists/* # Gemのインストール COPY Gemfile $APP_ROOT COPY Gemfile.lock $APP_ROOT RUN \ echo 'gem: --no-document' >> ~/.gemrc && \ cp ~/.gemrc /etc/gemrc && \ chmod uog+r /etc/gemrc && \ bundle config --global build.nokogiri --use-system-libraries && \ bundle config --global jobs 4 && \ bundle install && \ rm -rf ~/.gem # webpackerがないとRails6を起動できないので RUN bundle exec rails webpacker:install COPY . $APP_ROOT # 3000ポートをListern状態で起動 EXPOSE 3000 CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]5. railsプロジェクトの新規作成
Dockerイメージを作成する時にプロジェクトのファイルをイメージにコピーするので必要。
$ docker run --rm -it -v "$PWD":/usr/src/sample-ruby-gke developer_name/project_name bundle exec rails new .6.Dockerイメージの生成
$ docker build -t developer_name/project_name .7.Dockerイメージからコンテナの生成と起動
$ docker run -d -p 3000:3000 developer_name/project_name8.ブラウザからアクセスする
ブラウザから以下のURLにアクセスしてRailsの画面が表示されることを確認
http://localhost:3000/9.DockerHubへの登録
$ docker login $ docker push developer_name/project_name:latestDockerHubに登録ができたかは以下URLで確認できる
https://hub.docker.com/10.DockerHubに登録されたDockerイメージからGKEノードを作成
以下、GCP Console画面です。
Kubernetes Engine -> ワークロード -> デプロイを選択すると以下の画面が表示されます。
以下を選択し、後はデフォルトのままとしました。
- 「既存コンテナイメージ」を選択
- イメージパス:developer_name/project_name:latest (上記で作成しDockerHubに登録したものと同じものを選択)
11.GKEのサービスを作成し外部公開する
ポートを3000に設定した他は前回の4の手順以降と同じです。
https://qiita.com/bleu/items/f2f16ee34fb0039e8890参考
- 投稿日:2020-01-04T15:40:11+09:00
Fargate及びECSでコンテナからAWSのIAM認証を利用する
概要
AWSでコンテナを利用する場合、FargateやECSが候補に上がると思います。
コンテナからAWSの各種サービスを利用する場合、IAMのCredentialsを利用する必要があります。
いつも意識せずに利用しているIAMですが、裏ではどういう構成になっているのか確認していきます。確認方法
Fargateでsshできるようにして、コンテナ内でCredentialsを探します。
Fargateでsshするには以下の記事を参考にしてください
- https://qiita.com/pocari/items/3f3d77c80893f9f1e132
- https://qiita.com/ryurock/items/fa18b25b1b38c9a0f113
- https://qiita.com/fnaoto/items/c980efa39c2b1597331e
結果
Fargateでは
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
という環境変数にCredentialsへのファイルファイルパスが入っているようです。
- 詳細はこの記事を参考にしてください
rootユーザでコンテナにログインしても環境変数が設定されていない
実は、この環境変数はPIDが1のプロセスに設定されているようです
確認方法
$ strings /proc/1/environ
例えば、Dockerfileで以下のようになっている場合。
この場合、環境変数はPIDが1の/bin/sh
に設定されます。
そして、bundle exec rails s
には親プロセスの/bin/sh
から環境変数が引き継がれます。CMD ["/bin/sh", "-c", "bundle exec rails s"]ちなみに、以下のようにデーモンプロセスを動かす場合。
この場合は起動するデーモンの親プロセスは/bin/sh
にはならないので環境変数は引き継がれません。CMD ["/bin/sh", "-c", "service ssh start"]環境変数に設定されているIAMはどこから設定するのか
このIAMはTask Roleを参照しているようです。
なので、taskRoleArn
に設定しているIAMから権限を変更可能です。RubyライブラリでIAM認証を使う場合
aws-sdk-rubyでは以下のように、
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
があればECSCredentials.new
するようにしているみたいです。
ちなみに、instance_profile_credentialsはprivateなので外から呼び出しはできないみたいです。
普段、ライブラリを使う場合は特に意識することなくことなく、Credentialsが使われているようですね。# https://github.com/aws/aws-sdk-ruby/blob/704c4a2afbee1bb20a6c47545305684a92771f7e/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb#L110 def instance_profile_credentials(options) if ENV["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"] ECSCredentials.new(options) else InstanceProfileCredentials.new(options) end end
- 投稿日:2020-01-04T15:04:57+09:00
【第7章】Railsチュートリアル 5.1(第4版) ユーザー登録
大まかな流れの自己整理が目的のため、不足・誤り等あれば追記&訂正していきますのでご指摘頂けますと幸いです
なお、筆者はYassLabさんの動画版で学んでいるため、本記事は「チュートリアル sample_app」+「他補足」個人的に「電子ページ以上に分かりやすい!」と感じた解説部分+参考記事を整理してみようと試みた劣化の内容寄りになってます。7.1 ユーザーを表示する
params
Railsで送られてきた値を受け取るためのメソッド(その後Railsが保存を行う)。中身はハッシュ。
主に、以下の2つ
・投稿フォームなどPOSTで送信されたデータ
・検索フォームなどGETで送信されURLにクエリとして入るデータex.params[:id] → ユーザなどのid
<参考>
【Rails】paramsについて徹底解説!
【Rails入門】params使い方まとめdebug(メソッド)
paramsの中身(表示画面)を出力してくれる。putsのような扱い。
本番環境に展開したアプリケーションではデバッグ情報を表示したくないので、後置if文でデベロッパー確認を追加する。
(後置if文は1行で済むときによく使用される)app/views/layouts/application.html.erb 内に下記追加
<%= debug(params) if Rails.env.development? %>補足
Railsにはテスト環境 (test)、開発環境 (development)、そして本番環境 (production) の3つの環境がデフォルトで装備されている。Rails consoleのデフォルトの環境はdevelopment。
app/assets/stylesheets/custom.scssに
デバッグ表示を整形するための追加と、Sassのミックスインを追加する(ここでは省略)
/users/1 のURLを有効にするため、routes(るーつ、らうつ)ファイルsignup下に追加resources :users
確認(今回注目するのはnewアクションとshowアクション)$ rails routes Prefix Verb URI Pattern Controller#Action root GET / static_pages#home static_pages_home GET /static_pages/home(.:format) static_pages#home help GET /help(.:format) static_pages#help about GET /about(.:format) static_pages#about contact GET /contact(.:format) static_pages#contact sighup GET /sighup(.:format) users#new users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroyputリクエスト → 全部(旧残り)
patchリクエスト → 欲しい情報の一部(現在主流)
Usersコントローラにshowアクションを追加する(showアクションはGET /users/:id と連動)。挿入場所(順番)は可読性を考慮し出来ればRESTfulなルート順(表)が望ましい。
@user
はインスタンス変数とされ、showアクション外(例えばviewであるshowテンプレート: => app/views/users/show.html.erb)からも呼び出すことが出来るため、「paramsで持ってきたユーザid(リクエスト値)を@user
に格納する」ような流れ。def show @user = User.find(params[:id]) end
HTTPリクエスト URL アクション 名前付きルート 用途 GET /users index users_path すべてのユーザーを一覧するページ GET /users/1 show user_path(user) 特定のユーザーを表示するページ GET /users/new new new_user_path ユーザーを新規作成するページ (ユーザー登録) POST /users create users_path ユーザーを作成するアクション GET /users/1/edit edit edit_user_path(user) id=1のユーザーを編集するページ PATCH /users/1 update user_path(user) ユーザーを更新するアクション DELETE /users/1 destroy user_path(user) ユーザーを削除するアクション
サンプルアプリケーションを既にHeroku上にデプロイしている場合は、heroku run rails consoleというコマンドを打つことで、本番環境を確認することができます。$ heroku run rails console >> Rails.env => "production" >> Rails.env.production? => trueデバッガー(debugger)
追記して稼働させるとゆっくり読み込み中になる
def show @user = User.find(params[:id]) # => app/views/users/show.html.erb debugger endrails サーバを立ち上げると、止まってる場所がわかる
def show 4: @user = User.find(params[:id]) 5: # => app/views/users/show.html.erb 6: debugger => 7: end 8: 9: def new 10: end 11: end (byebug)コマンドはlist,nextなど。exit、ctrl+cで終了。
使い終わったらコメントアウト。
show.html.erbにgravatar_forヘルパーメソッドを使い、WordPress系アバター画像のGravatarの画像を利用できるようにする。<% provide(:title, @user.name) %> <h1> <%= gravatar_for @user %> <%= @user.name %> </h1>ヘルパーメソッドとしてusers_helper.rbに追加する。
最後の行のimage_tag(url、alt&class属性付与)がgravatar_forに返される。
なお、gravatar_forメソッドでのemailは(has_secure_passwordのように)Digest::MD5によってハッシュ化されている。module UsersHelper # 引数で与えられたユーザーのGravatar画像を返す def gravatar_for(user) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end endこの時点でデフォルト画像のまま表示されるが、show.html.erbにasideタグ(サイドバーなど補足として付け加えたいときに便利なもの)を付け加える。
<% provide(:title, @user.name) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <h1> <%= gravatar_for @user %> <%= @user.name %> </h1> </section> </aside> </div>saasに追記してフォーマット等調整。
/* sidebar */ aside { section.user_info { margin-top: 20px; } section { padding: 10px 0; margin-top: 20px; &:first-child { border: 0; padding-top: 0; } span { display: block; margin-bottom: 3px; line-height: 1; } h1 { font-size: 1.4em; text-align: left; letter-spacing: -1px; margin-bottom: 3px; margin-top: 0px; } } } .gravatar { float: left; margin-right: 10px; } .gravatar_edit { margin-top: 15px; }7.2 ユーザー登録フォーム
コントローラのnewアクションに空のオブジェクトを作成して@user変数を追加する
def new @user = User.new end
view画面のnewに追記。
form_forはブロック付き引数で、ブロック内部では例として「email」というキーの中にユーザのバリューが入るイメージ。
「f.label」はラベルのなので削っても問題ないが、あるとユーザ視点で何を入力していいか分かりやすい。(変更イメージの参考:[rails]ActiveModelを使ったフォームのラベル名を変更する)
「f.submit」は"Create my account"というボタンからこれらのデータをnewアクション→createアクションへ送る(次のアクションへ)。<h1>Users#new</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= f.label :name %> <%= f.text_field :name %> <%= f.label :email %> <%= f.email_field :email %> <%= f.label :password %> <%= f.password_field :password %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation %> <%= f.submit "Create my account", class: "btn btn-primary" %> <% end %> </div> </div>formのscss追記
/* forms */ input, textarea, select, .uneditable-input { border: 1px solid #bbb; width: 100%; margin-bottom: 15px; @include box_sizing; } input { height: auto !important; }そのまま作るとエラーが起きるが、「newアクション移行→createアクションがないよ」という表示なので正常。
createアクション追加&renderでビュー画面newに戻り、登録情報確認(試験的にfoobar入れてます)
def create @user = User.new render 'new' end7.3 ユーザー登録失敗
ユーザー登録の失敗に対応できるcreateアクション
オプション引数について
ハッシュのハッシュなので、userの中に[:name],[:email],[:password]があるので:userで省略。
def create # オプション引数はキーワードにシンボルが入ってバリューに値が入ってる集合体 # → User.new(name: ..., email:, ...) # @user.name = params[:user][:name] # @user.user = params[:user][:email] # @user.password = params[:user][:password] # 1行で完結 @user = User.new(params[:user]) if @user.save #=> Validation # Sucess else #Failure render 'new' end endエラー登場。createの直後のparamsはユーザが送る情報なのでいろいろな情報を送る(いじる)ことができるので、今後adminなどを入れていく過程で悪意あるユーザからDB書き換えたれるなどのマスアサインメント脆弱性を回避するRails4.0移行実装の機能。paramsハッシュでは:user属性を必須とし、名前、メールアドレス、パスワード、パスワードの確認の属性をそれぞれ許可し、それ以外を許可しないようにしたいので、requireなどを追加したuser_paramsメソッドをコントローラに追加してメソッドで対応する。
users_controller.rbdef create @user = User.new(user_params) if @user.save #=> Validation # Sucess else #Failure render 'new' end end def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) endRails consoleからメソッド「.errors.full_messages」でエラー詳細表示
空のユーザ作成後save失敗から.full_messagesメソッドでエラーの詳細表示ができる。
(今回はいろいろ空だったことが原因)2.6.3 :002 > @user = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil> 2.6.3 :003 > @user.valid? User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT ? [["LIMIT", 1]] => false 2.6.3 :004 > @user.save (0.1ms) begin transaction User Exists (0.4ms) SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT ? [["LIMIT", 1]] (0.1ms) rollback transaction => false 2.6.3 :005 > @user.errors.full_messages => ["Name can't be blank", "Email can't be blank", "Email is invalid", "Password can't be blank", "Password can't be blank", "Password is too short (minimum is 6 characters)"]ユーザ登録時にエラーメッセージが出るよう追記する。sharedディレクトリもviewディレクトリ下に作る。
<h1>Users#new</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Create my account", class: "btn btn-primary" %> <% end %> </div> </div>
パーシャル(Viewでの似たようなコードを一つでまとめてるもの)を作るがアンダーバーなので注意$ cd app/views/ $ mkdir shared $ c9 open _error_messages.html.erb
「.errors.eny?」 → save,validなど何か実行されてエラーがなければfalse(何も表示しない)、「.count」あるので1個以上あればtrueを返す。pluralizeは"error"を個数に応じて単数・複数形で判断してくれる。_error_messages.html.erb<% if @user.errors.any? %> <div id="error_explanation"> <div class="alert alert-danger"> The form contains <%= pluralize(@user.errors.count, "error") %>. </div> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %>「Password can't be blank」が2つ出る理由は、「has_secure_password」と「validates presence」で引っかかっているためのバグ。
応急処置として、has_secure_password内に下記を追記するなどがある。allow_nil: true
エラー表示のSCSS追加#error_explanation { color: red; ul { color: red; margin: 0 0 30px 0; } } .field_with_errors { @extend .has-error; .form-control { color: $state-danger-text; } }<form class="new_user" id="new_user" action="/users" accept-charset="UTF-8" method="post">とあり、
アクションが「/users」、メソッドが「post」から、「postリクエストを/usersに送りつける」ことが分かる。
ポストリクエストの流れは下記から確認でき、
「POST」リクエストが「/users」に送られると、「users」コントローラの「create」アクションが反応する。$ rails routes Prefix Verb URI Pattern Controller#Action root GET / static_pages#home static_pages_home GET /static_pages/home(.:format) static_pages#home help GET /help(.:format) static_pages#help about GET /about(.:format) static_pages#about contact GET /contact(.:format) static_pages#contact signup GET /signup(.:format) users#new users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy「assert_no_difference」を使った、失敗時のテスト
インテグレーションテスト(結合テスト)から
$ rails generate integration_test users_signup Running via Spring preloader in process 4226 invoke test_unit create test/integration/users_signup_test.rb
自動生成されたものに追記。「signup_path」にgetリクエストを送りつける。
postリクエストをusers_pathに送り、その時にパラメータ:{ user: { name:,email:,password:,password_confirmation}}(オプション引数)を送りつける。
assert_no_differenceは呼び出す前後で値に違いがないことを主張するテストで、引数に(User.count)が入っていることから、「ユーザ数を覚えた後にデータを投稿してみて、ユーザ数が変わらないかどうかを検証するテスト」になる(公式より)。users_signup_test.rbrequire 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, params: { user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } } end assert_template 'users/new' end endこの時点でテスト(通過)
$ rails t補足(エラー 「undefined local variable or method `signup_path'」)
筆者の場合、この時点で下記のようなエラーが出ました。
$ rails t Running via Spring preloader in process 4176 Run options: --seed 61612 # Running: .E.......... Finished in 0.618448s, 19.4034 runs/s, 30.7221 assertions/s. 1) Error: UsersSignupTest#test_invalid_signup_information: NameError: undefined local variable or method `signup_path' for #<UsersSignupTest:0x00000000065b4838> test/integration/users_signup_test.rb:6:in `block in <class:UsersSignupTest>' 12 runs, 19 assertions, 0 failures, 1 errors, 0 skips
「自分でアカウント追加して遊んだから講義とユーザ数ずれちゃったせいかな?」と考えたりしてましたが(このテストでは前後数のみで関係ない)
ルート(routes.rb)、ビュー(home.html.erb)でsignup_pathがsighup_pathにタイプミスしてました
7.4 ユーザー登録成功
createアクションにリダイレクトを追加する(省略過程あり)。
def create @user = User.new(user_params) # Sucess # redirect_to user_path(@user.id) # user_pathの引数デフォルトがidなので「.id」省略可、 # redirect_to user_path(@user) # さらにredirect_toのデフォルト挙動としてユーザオブジェクトを渡すとuser_pathになるので redirect_to @user #GETリクエスト(が右にいく) => "/users/#{@user.id}" => showアクションが動く else #Failure render 'new' end endflash
登録完了後に表示されるページにメッセージを表示する (この場合は新規ユーザーへのウェルカムメッセージ)。
createアクション(if文直下)と、ビュー(yield直前)に追加users_controller.rbflash[:success] = "Welcome to the Sample App!"application.html.erb<% flash.each do |message_type, message| %> <div class="alert alert-<%= message_type %>"><%= message %></div> <% end %>↑(ビュー側のerb埋め込みruby)の補足
ハッシュはキー+バリューの仕組みになっていて、
eachメソッドで呼び出すと、message_type(キー:success)とmessage(バリュー:"Welcome to the Sample App!")の関係になっている。
また、classの中にmessageを埋め込んでいる。
登録動作を確認してみると、5番目のユーザで作成できてる(成功!)。
flashは一度だけなのでリロードすればメッセージ(緑色)は消える。成功時のテスト
成功時、ユーザのカウントが1増えてればokという内容。
基本的にはshowテンプレートが表示されていれば?♂️users_signup_test.rbtest "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end follow_redirect! assert_template 'users/show' endURLを確認すると「https://〇〇.herokuapp.com/
users/1
」となっており、
本番環境(heroku)では1人目のユーザであることが分かる。7.5 プロのデプロイ
本番環境でのSSL
herokuのサブドメイン「https://〇〇
.herokuapp.com(2番目)
/users/1」を使っている内はサービス上SSL化(?Secure)されている。(Googleなどではhttpより検索順位が上位になったりするので重要であり、将来的に独自ドメインを使っていく際は自分で証明書の発行が必要になる。)https(SSL)通信を本番環境で強制するやり方として、production(本番環境).rbに下記を追記(コメントアウト解除してtrueに)する。
これにより、もしhttpでアクセスしても問答無用でhttpsに切り替わる。config/environments/production.rb# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true
本番環境に適したWebサーバーを構築する
heroku推奨の設定ファイルをpumaに書き換える。
config/environments/production.rbworkers Integer(ENV['WEB_CONCURRENCY'] || 2) threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5) threads threads_count, threads_count preload_app! rackup DefaultRackup port ENV['PORT'] || 3000 environment ENV['RACK_ENV'] || 'development' on_worker_boot do # Worker specific setup for Rails 4.1+ # See: https://devcenter.heroku.com/articles/ # deploying-rails-applications-with-the-puma-web-server#on-worker-boot ActiveRecord::Base.establish_connection end
Pumaがheroku上で使うようにプロックファイル(./Procfile)(はデフォルトでないので)作成し、定義する。/sample_app $ c9 open Procfileweb: bundle exec puma -C config/puma.rb一応テスト走らせた後、
再度herokuへデプロイ(コミットメッセージが分かるように)して終了。$ git add -A $ git commit -m "Use SSL and the Puma webserver in production"感想ほか
講義が非常に分かりやすく全く挫折することなく終えました(講師:安川さん、ありがとうございました!!)。
教材模写(受け身)が中心だったので、忘れた部分含め2周目自力で進めて公式ドキュメント漁って読み込むなどもっと頭を悩ませる必要はあるかなと。
最後までお読み頂きありがとうございました!!
- 投稿日:2020-01-04T14:58:35+09:00
Railsチュートリアル 第13章 ユーザーのマイクロポスト - ユーザーのフィード画面に対するテストは正しく動いているか
概要
「Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストを操作する - 演習「フィード画面のマイクロポストをテストする」より、演習1番目の内容です。Railsチュートリアル本文には以下のようにあります。
リスト 13.55で示した4つのコメント (「無効な送信」など) のそれぞれに対して、テストが正しく動いているか確認してみましょう。具体的には、対応するアプリケーション側のコードをコメントアウトし、テストが
red
になることを確認し、元に戻すとgreen
になることを確認してみましょう。なお、「それぞれの確認で変更したソースコードは、確認が終わったら変更前の状態に戻すこと」を前提とします。
無効なマイクロポストを受け付けてしまう場合
無効なマイクロポストを受け付けてしまう例としては、「Micropostモデルのバリデーションが正常に機能していない場合」が想定できます。具体的には、「Micropostモデルの
content
属性に対するバリデーションが動作していない場合」です。当該バリデーションに関するソースコードをapp/models/micropost.rb
からコメントアウトしてみます。app/models/micropost.rbclass Micropost < ApplicationRecord belongs_to :user default_scope -> { order(created_at: :desc) } validates :user_id, presence: true - validates :content, presence: true, length: { maximum: 140 } + # validates :content, presence: true, length: { maximum: 140 } endこの状態で、
test/integration/microposts_interface_test.rb
を対象にテストを実行してみましょう。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 513 Started with run options --seed 28697 FAIL["test_micropost_interface", MicropostsInterfaceTest, 4.152912300000025] test_micropost_interface#MicropostsInterfaceTest (4.15s) "Micropost.count" didn't change by 0. Expected: 38 Actual: 39 test/integration/microposts_interface_test.rb:18:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04 Finished in 4.15470s 1 tests, 7 assertions, 1 failures, 0 errors, 0 skips"Micropost.count" didn't change by 0. Expected: 38 Actual: 39上記は「(当該テストテストコードの時点で変化してはならない)
Micropost.count
の値が変化してしまっている」という趣旨のメッセージですね。また、私の環境では、
test/integration/microposts_interface_test.rb
の17行目から20行目のソースコードは以下のようになっています。test/integration/microposts_interface_test.rb(17〜20行目)# 無効な送信 assert_no_difference 'Micropost.count' do post microposts_path, params: { micropost: { content: "" } } end想定通りの形でテストが失敗したということですね。
有効なマイクロポストが受け付けられない場合
逆に「無効なマイクロポストは受け付けられないが、有効なマイクロポストが受け付けられない」という場合です。
例えば「Strong Parameters機能の利用に際し、受け入れる属性の名前を間違えている」という場合は、このケースに該当するはずです。というわけで、
app/controllers/microposts_controller.rb
を以下のように変更した上で保存します。app/controllers/microposts_controller.rb(抜粋)class MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] ...略 private def micropost_params - params.require(:micropost).permit(:content) + params.require(:micropost).permit(:id) endこの状態で、
test/integration/microposts_interface_test.rb
を対象にテストを実行してみましょう。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 526 Started with run options --seed 19620 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.3941545999987284] test_micropost_interface#MicropostsInterfaceTest (3.39s) "Micropost.count" didn't change by 1. Expected: 39 Actual: 38 test/integration/microposts_interface_test.rb:24:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.40014s 1 tests, 9 assertions, 1 failures, 0 errors, 0 skips"Micropost.count" didn't change by 1. Expected: 39 Actual: 38上記は「(当該テストテストコードの時点で1増えていなければならない)
Micropost.count
の値が1増えていない」という趣旨のメッセージですね。また、私の環境では、
test/integration/microposts_interface_test.rb
の22行目から26行目のソースコードは以下のようになっています。test/integration/microposts_interface_test.rb(22〜26行目)# 有効な送信 content = "This micropost really ties the room together" assert_difference 'Micropost.count', 1 do post microposts_path, params: { micropost: { content: content } } end想定通りの形でテストが失敗したということですね。
投稿が削除されない場合
「投稿が削除されない」という事態が発生する原因はいくつか考えられます。
マイクロポストをRDBから削除する処理の実装が抜けた場合
マイクロポストをRDBから削除する処理の実装が抜けた場合について確認してみましょう。一番単純なケースですね。
app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] before_action :correct_user, only: [:destroy] ...略 def destroy - @micropost.destroy + # @micropost.destroy flash[:success] = "Micropost deleted" redirect_back(fallback_location: root_url) end private ...略 end上記変更を適用した状態で、
test/integration/microposts_interface_test.rb
に対してテストを実行してみましょう。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 552 Started with run options --seed 40696 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.7064526000031037] test_micropost_interface#MicropostsInterfaceTest (3.71s) "Micropost.count" didn't change by -1. Expected: 38 Actual: 39 test/integration/microposts_interface_test.rb:33:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.71107s 1 tests, 14 assertions, 1 failures, 0 errors, 0 skips"Micropost.count" didn't change by -1. Expected: 38 Actual: 39上記は「(当該テストテストコードの時点で1減っていなければならない)
Micropost.count
の値が1減っていない」という趣旨のメッセージですね。また、私の環境では、
test/integration/microposts_interface_test.rb
の30行目から35行目のソースコードは以下のようになっています。test/integration/microposts_interface_test.rb(30〜35行目)# 投稿を削除する assert_select 'a', text: 'delete' first_micropost = @user.microposts.paginate(page: 1).first assert_difference 'Micropost.count', -1 do delete micropost_path(first_micropost) end想定通りの形でテストが失敗したということですね。
Beforeフィルターの実装内容に誤りがあった場合
今回実装したMicropostsコントローラーでは、マイクロポストの削除に際し、
logged_in_user
とcorrect_user
という2つのBeforeフィルターが適用されます。その実装内容に誤りがあった場合も、マイクロポストの削除はうまくいかないはずです。今回は、
correct_user
フィルターの実装内容を、意図的に誤ったものにしてみます。app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] before_action :correct_user, only: [:destroy] ...略 def destroy @micropost.destroy flash[:success] = "Micropost deleted" redirect_back(fallback_location: root_url) end private ...略 def correct_user @micropost = current_user.microposts.find_by(id: params[:id]) - redirect_to root_url if @micropost.nil? + redirect_to root_url unless @micropost.nil? end end上記変更を適用した状態で、
test/integration/microposts_interface_test.rb
に対してテストを実行してみましょう。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 578 Started with run options --seed 23555 FAIL["test_micropost_interface", MicropostsInterfaceTest, 4.179720099997212] test_micropost_interface#MicropostsInterfaceTest (4.18s) "Micropost.count" didn't change by -1. Expected: 38 Actual: 39 test/integration/microposts_interface_test.rb:33:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:04, Time: 00:00:04 Finished in 4.20035s 1 tests, 14 assertions, 1 failures, 0 errors, 0 skips前述「マイクロポストをRDBから削除する処理が抜けた場合」と同様の形でテストが失敗しています。
自身のページにマイクロポストの削除リンクが表示されない場合
app/views/microposts/_micropost.html.erb<li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <span class="user"><%= link_to micropost.user.name, micropost.user %></span> <span class="content"><%= micropost.content %></span> <span class="timestamp"> Posted <%= time_ago_in_words(micropost.created_at) %> ago. - <% if current_user?(micropost.user) %> + <%# <% if current_user?(micropost.user) %> %> - <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %> + <%# <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %> %> - <% end %> + <%# <% end %> %> </span> </li>上記変更を適用した状態で、
test/integration/microposts_interface_test.rb
に対してテストを実行してみましょう。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 617 Started with run options --seed 35207 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.800804300000891] test_micropost_interface#MicropostsInterfaceTest (3.80s) <delete> expected but was <sample app>.. Expected 0 to be >= 1. test/integration/microposts_interface_test.rb:31:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.81553s 1 tests, 13 assertions, 1 failures, 0 errors, 0 skips<delete> expected but was <sample app>.. Expected 0 to be >= 1.「deleteというリンクテキストの
a要素
が、1つ以上存在するはずなのに1つも存在しない」という趣旨のメッセージですね。また、私の環境では、
test/integration/microposts_interface_test.rb
の30行目から31行目のソースコードは以下のようになっています。test/integration/microposts_interface_test.rb(30〜31行目)# 投稿を削除する assert_select 'a', text: 'delete'想定通りの形でテストが失敗したということですね。
違うユーザーのページにマイクロポストの削除リンクが表示される場合
app/views/microposts/_micropost.html.erb<li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <span class="user"><%= link_to micropost.user.name, micropost.user %></span> <span class="content"><%= micropost.content %></span> <span class="timestamp"> Posted <%= time_ago_in_words(micropost.created_at) %> ago. - <% if current_user?(micropost.user) %> + <%# <% if current_user?(micropost.user) %> %> + <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %> - <% end %> + <%# <% end %> %> </span> </li>上記変更を適用した状態で、
test/integration/microposts_interface_test.rb
に対してテストを実行してみましょう。# rails test test/integration/microposts_interface_test.rb Running via Spring preloader in process 604 Started with run options --seed 24198 FAIL["test_micropost_interface", MicropostsInterfaceTest, 3.710591699997167] test_micropost_interface#MicropostsInterfaceTest (3.71s) Expected exactly 0 elements matching "a", found 2.. Expected: 0 Actual: 2 test/integration/microposts_interface_test.rb:38:in `block in <class:MicropostsInterfaceTest>' 1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03 Finished in 3.72132s 1 tests, 15 assertions, 1 failures, 0 errors, 0 skipsExpected exactly 0 elements matching "a", found 2.. Expected: 0 Actual: 2「1つも存在しないはずの(deleteというリンクテキストの)
a
要素が2つ存在する」というメッセージが表示されています。また、私の環境では、
test/integration/microposts_interface_test.rb
の36行目から38行目のソースコードは以下のようになっています。test/integration/microposts_interface_test.rb(36〜38行目)# 違うユーザーのプロフィールにアクセス(削除リンクがないことの確認) get user_path(users(:mkirisame)) assert_select 'a', text: 'delete', count: 0想定通りの形でテストが失敗したということですね。
- 投稿日:2020-01-04T14:12:58+09:00
【Rails】Rspec何言ってるか解読してみた件。コントローラー#edit編
今回は、写真とタイトルを投稿するアプリケーションのtweetsコントローラーのeditアクション(投稿したツイートの編集画面を呼ぶ)が問題なく動くかどうか、Rspecという言語を使ってテスト。
テストする内容は以下の2つ。
tweets_controller.rb内でeditアクションを定義した際に生成したインスタンス変数に期待する値が入っているかな?
tweets_controller.rbdef edit @tweet = Tweet.find(params[:id]) end擬似的にeditアクションを起こすリクエストを飛ばして、ちゃんとeditアクションに対応したビューが返ってくるかな?
コントローラーのテストをする際は、2つの点に注意。
①tweets_controller.rbに、テストしたいアクションに対して、before_actionが定義されていないこと。
②gem rails-controller-testingをインストールしよう。また、factory_botを利用して、テストにおいてeditアクションで編集するダミーのツイートデータを作成しよう。
spec/factories/users.rbFactoryBot.define do factory :user do nickname {"mamama"} email {"mamama@mail.com"} password {"00000000"} password_confirmation {"00000000"} end endspec/factories/tweets.rbFactoryBot.define do factory :tweet do text {" I love animals"} image {"animals.png"} user #←factory_botを利用して作った :userのこと。ツイートを投稿した際、そのツイートは誰が作ったのかも情報に含まれるため、ダミーのユーザー情報も作成しダミーのツイートを作成する際に利用している。 end endでは、見ていきましょう。
tweets_controller_spec.rbrequire 'rails_helper' describe TweetsController, type: :controller do describe 'GET #new' do #「【Rails】Rspec何言ってるか解読してみた件。コントローラー#new編」を参照。 end describe 'GET #edit' do # 1. tweets_controller.rb内でeditアクションを定義した際に生成したインスタンス変数に期待する値が入っているかな? it "assigns the requested tweet to @tweet" do dummyTweet = create(:tweet) get :edit, params: { id: dummyTweet } expect(assigns(:tweet)).to eq dummyTweet end # 2. 擬似的にeditアクションを起こすリクエストを飛ばして、ちゃんとnewアクションに対応したビューが返ってくるかな? it "renders the :edit template" do dummyTweet = create(:tweet) get :edit, params: { id: dummyTweet } expect(responce).to render_template :edit end end end1. tweets_controller.rb内でeditアクションを定義した際に生成したインスタンス変数に期待する値が入っているかな?
require 'rails_helper'
RailsでRspecを利用する時の共通の設定が記載されているrails_helper.rbを読み込む。これがなきゃ始まらない。
describe TweetsController, type: :controller do end
tweetsコントローラーのテストをするよ。 type: :controllerこれは書かなきゃいけないみたいですね。
describe 'GET #edit' do end
GETメソッドのeditアクションに関するテストだよ。
it "assigns the requested tweet to @tweet" do end
do - endの間に書かれている、実際に動くテストコードの説明。呼ばれたtweet(the requested tweet)が@tweetにアサインされているかをテストしますと言っている。日本語でも良いらしい。ちなみに、it do endのセット1つで、1exampleと呼ぶ。
dummyTweet = create(:tweet)
factory_botで生成したtweetをcreateアクションを起こさせて、一旦登録。それをdummyTweetに入れる。
get :edit, params: { id: dummyTweet }
擬似的なリクエストを起こすコード。get :edit
でアクション名editを、params: { id: dummyTweet }
でdummyTweetがTweetsテーブルの何番目のカラムにあるのかを示すidを、そのリクエストに情報として渡す。
expect(assigns(:tweet)).to eq dummyTweet
インスタンス変数に意図したものが入っているかな?というテストコード。assignsは、インスタンス変数の中身をチェックしてくれるやつ。今回は、tweets_controller.rb内で定義した@tweetが対象なので、(:tweet)と書こう。@tweetの中身は、一旦登録させてeditアクションで持ってこさせたdummyTweetと一緒だと(.to eq)と期待する(expect)。tweets_controller_spec.rb#省略 describe 'GET #edit' do # 1. tweets_controller.rb内でeditアクションを定義した際に生成したインスタンス変数に期待する値が入っているかな? it "assigns the requested tweet to @tweet" do dummyTweet = create(:tweet) get :edit, params: { id: dummyTweet } expect(assigns(:tweet)).to eq dummyTweet end # 2. 擬似的にeditアクションを起こすリクエストを飛ばして、ちゃんとnewアクションに対応したビューが返ってくるかな? it "renders the :edit template" do dummyTweet = create(:tweet) get :edit, params: { id: dummyTweet } expect(responce).to render_template :edit end end #省略2. 擬似的にeditアクションを起こすリクエストを飛ばして、ちゃんとnewアクションに対応したビューが返ってくるかな?
it "renders the :edit template" do end
do - endの間に書かれている、実際に動くテストコードの説明。editアクションに対応したビューファイル(template)が呼び出される(renders)ことをテストしますと言っている。
get :edit, params: { id: dummyTweet }
擬似的なリクエストを起こすコード。get :edit
でアクション名editを、params: { id: dummyTweet }
でdummyTweetがTweetsテーブルの何番目のカラムにあるのかを示すidを、そのリクエストに情報として渡す。
expect(response).to render_template :edit
ちゃんと期待するビューが返ってくるかな?というテストコード。resposeが、editが起きた時に呼ばれるrender_template(ビューである)と期待する(expect)。
ターミナルにて
% bundle exec rspec spec/controllers/tweets_controller_spec.rb
を実行してテストを実施しよう。
初学者の視点からRailsアプリケーションのRspecを使ったテストについてまとめてみました。指摘やご意見お待ちしてます。モデルや、コントローラーのその他アクションについても、まとめていきたいと思います。
最後までご覧いただきありがとうございました!
- 投稿日:2020-01-04T13:47:05+09:00
link_toの使い方
link_toとは
link_toはリンクを作成できるrailsのヘルパーメソッドの事。
ビューファイルに記述できるメソッドになる。link_toの種類と使い方
例<%= link_to "テキスト", "リンク先のパス" %>例<%= link_to "Yahoo!", "https://www.yahoo.co.jp/" %>method指定
HTTPメソッドを指定する時に使い、何も指定しない時は「get」になる。
destroyアクションを動かしたい時のHTTPメソッドはdelete。
下記のコードを記述。<%= link_to "削除", "/users/1", method: :delete %>画像リンク作成
link_toと同じヘルパーメソッドであるimage_tagを使用。
<%= link_to image_tag('test.jpg'), 'パス' %>内部リンク作成
内部リンクはPrefixを使用して作成する。
トップページ作成の場合<%= link_to "トップページ", "/" %>Prefixを使った書き方<%= link_to "トップページ", root_path %>Prefixとは?
ターミナルでrails routesコマンドを実行したらルーティングを表示がさせる事が出来る。
ターミナルPrefix Verb URI Pattern Controller#Action root GET / posts#index new_post GET /posts/new(.:format) posts#newprefix名に_pathを付け加えることによって、パスとして認識される。
prefixを確認するとルートパスを表す「/」というパスは「root」になっている為、
「root_path」を記入したら「/」で指定した記述と同じになる。
- 投稿日:2020-01-04T13:07:28+09:00
【Rails】1つのテーブルに複数の外部キーを設定
複数のテーブルを参照
モデル生成時に設定する場合
$ rails g model Song user:references mc:references song
migrationファイルclass CreateSongs < ActiveRecord::Migration[6.0] def change create_table :songs do |t| t.references :user, foreign_key: true t.references :mc, null: false, foreign_key: true t.string :song t.timestamps end end end後から追加する場合
add_foreign_key :参照元のテーブル名, :参照したいテーブル名, column: :参照元につける外部キー名migrationファイルadd_foreign_key :songs, :users, column: :user_id add_foreign_key :songs, :mcs, column: :mc_id参考
1つのテーブルを2つのカラムで参照
モデル作成時のコマンドでは外部キーを指定せず、作成されたマイグレーションファイルを手動で変更しました。
rails g model Video url
migrationファイルclass CreateVideos < ActiveRecord::Migration[6.0] def change create_table :videos do |t| t.references :mc1 t.references :mc2 t.string :url t.timestamps end add_foreign_key :videos, :mcs, column: :mc1_id add_foreign_key :videos, :mcs, column: :mc2_id end end試していませんが、既存のテーブルに外部キーを追加する時にも
add_foreign_key :videos, :mcs, column: :mc1_id
のような書き方でコネコネすれば出来そうな気がします。参考
- 投稿日:2020-01-04T11:52:59+09:00
rails: datetime_selectをカスタマイズする方法
やりたいこと
デフォルトで
datetime_select
を使うとデザイン的にあまり美しく無いので下記のようにカスタマイズしてみた。
デフォルトだと下記のような殺風景なデザインでUX的に美しくない。どのようにしたらできるのか?
結論から言うと、
datetime_separator
を弄ることでafter後のデザインに出来る。
初めは、文字しか書けないんだろうなと思ったらhtml
も書き込むことができた。= f.datetime_select :date, {use_month_numbers: true, start_year: Date.today.year, datetime_separator: '<h4 class="fb-txt font-weight-bold my-2">時間</h4>'},{class: "form-control", id: 'ymd-date'}ちなみに
datetime_separator
以外にdate_separator
とtime_separator
もあるので暇なときにそっちについても記事にしていきたいと思う。
コメントで要望があればすぐに対応しますが、多分ググればそれっぽい記事があると思うのでまずはググってみてください。
- 投稿日:2020-01-04T11:50:47+09:00
【jQuery】RailsでValidation Pluginを使った動的なバリデーションチェックの実装その1
はじめに
本記事がQiitaでの初投稿となります。
プログラミングスクールで開発中のECサイトアプリでjQueryのプラグインを使って
動的なバリデーションチェック機能を実装しました。
実装手順とRailsで使用する場合の注意点をシェアします。開発環境
Ruby 2.5.1
Rails 5.0.7.2
jQuery 3.4.1
jQuery Validation Plugin 1.19.1
Haml 5.1.2
Sass 3.7.4jQuery Validation Pluginとは
jQuery Validation Pluginはバリデーションチェックが実装できるjQueryのプラグインです。
公式サイト:https://jqueryvalidation.org/
やりたいこと
- 入力フォームにユーザー情報を入力する際にリアルタイムにバリデーションをチェック
- バリデーションエラーがある場合はエラーをリアルタイムに表示
- バリデーションがNGの場合は入力欄が赤色、OKの場合は緑色に変更
なぜjQuery Validation Pluginを選んだのか?
バリデーションチェックを実装するにあたり、実装方法を検討しました。
他にも以下の方法があります。
①RailsのActive Record バリデーション
②HTMLのフォーム検証機能
③HTML5 + JavaScriptによるFormバリデーションプラグインなどを有効活用でき、動的にバリデーションチェックできることを主軸に検討し、
jQueryプラグインでフロントエンドでの第1チェックを行い、Railsのバリデーションでバックエンド側の最終チェックを行う方法を選びました。完成イメージ
実装手順
1. プラグインの導入
CDNでも導入できるそうですが、上手くいかなかったため、下記手順で導入しました。
1.1 公式サイトにアクセス
1.2 ダウンロードページへ
1.3 最下段からzipファイルをダウンロード
1.4 下記のJSファイルをアプリのディレクトリに保存
- jquery-validation-1.19.1/dist/jquery.validate.min.js (※1)
- jquery-validation-1.19.1/src/localization/messages_ja.js (※2)
※1 : 必須
※2 : 標準メッセージを日本語化したい場合のみ2. HTMLファイルの作成
下記のように入力フォームのform_forとf.text_filedなどの各入力欄にidを割り振ります。
(例: id: "signup-form", id: "name" etc.)
BEMが長くて読みにくいですが、ご容赦ください。Railsでの注意点:HTMLではname属性値を付けないようにしましょう。(理由は後述します。)
私は最初、name属性値を指定していてバリデーションチェックが動作しませんでした。
registration.html.haml= form_for(@user, url: phone_signup_index_path, method: :get, html: {class: "registration__main__form", id: "signup-form"}) do |f| %div.registration__main__form__content %div.registration__main__form__content__group = f.label :name, "ニックネーム", class: "registration__main__form__content__group__label" %span.registration__main__form__content__group__require 必須 = f.text_field :name, class: "registration__main__form__content__group__input", placeholder:"例)テスト太郎", id: "name" %div.registration__main__form__content__group = f.label :email, "メールアドレス", class: "registration__main__form__content__group__label" %span.registration__main__form__content__group__require 必須 = f.email_field :email, class:"registration__main__form__content__group__input", placeholder:"PC・携帯どちらでも可", id: "email" %div.registration__main__form__content__group = f.label :password, "パスワード", class: "registration__main__form__content__group__label" %span.registration__main__form__content__group__require 必須 = f.password_field :password, class:"registration__main__form__content__group__input", placeholder:"7文字以上の半角英数字", id: "password" %p.registration__main__form__content__group__input__info ※ 英字と数字の両方を含めて設定してください。 %div.registration__main__form__content__group = f.label :password_confirmation, "パスワード(確認)", class: "registration__main__form__content__group__label" %span.registration__main__form__content__group__require 必須 = f.password_field :password_confirmation, class:"registration__main__form__content__group__input", placeholder:"7文字以上の半角英数字", id: "password_confirmation"3. Javascriptファイルの作成
次に下記のようにJavascriptファイルを作成します。
ファイル名は任意です。jquery.validate.handler.user.js$(function () { // メソッドの定義 var methods = { email: function (value, element) { // メールアドレスの正規表現 return this.optional(element) || /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/i.test(value); }, password: function (value, element) { // パスワードの正規表現 return this.optional(element) || /^(?=.*?[a-z])(?=.*?\d)[a-z\d]{8,100}$/i.test(value); }, } // メソッドの追加 $.each(methods, function (key) { $.validator.addMethod(key, this); }); // バリデーションの実行 $("#signup-form").validate({ // ルール設定 rules: { "user[name]": { required: true // ニックネームの入力有無チェック }, "user[email]": { required: true, // メールアドレスの入力有無チェック email: true // メールアドレスの正規表現チェック }, "user[password]": { required: true, // パスワードの入力有無チェック password: true // メールアドレスの正規表現チェック }, }, // エラーメッセージの定義 messages: { "user[name]": { required: "ニックネームを入力してください" }, "user[email]": { required: "メールアドレスを入力してください", email: "フォーマットが不適切です" }, "user[password]": { required: "パスワードを入力してください", password: "英字と数字両方を含むパスワードを入力してください" }, }, errorClass: "invalid", // バリデーションNGの場合に追加するクラス名の指定 errorElement: "p", // エラーメッセージの要素種類の指定 validClass: "valid", // バリデーションOKの場合に追加するクラス名の指定 } }); // 入力欄をフォーカスアウトしたときにバリデーションを実行 $("#name, #email, #password").blur(function () { $(this).valid(); }); });基本的な記述方法は下記の記事を参考にさせていただきました。
【jQuery入門】validate()の使い方と独自ルールの設定方法!
jQuery Validation 簡単にフォームをチェック バリデーション
フォームの入力値を検証するjQuery Validation Pluginの使い方メールアドレスとパスワードの正規表現はデフォルトのルールでも実装可能です。
Railsでの注意点
通常の使い方だとrules:の中にname属性値を指定しますが、下記の記事の通り、Railsの場合は少し通常と異なります。
[Rails]「jQueryValidationプラグイン」を使用してフォームのバリデーションを実装ページのソースを確認するとname属性値は"user[name]"となっています。
このname属性値をrules:の中で指定するとバリデーションチェックが動作します。<input class="クラス名" placeholder="例)メルカリ太郎" id="name" type="text" name="user[name]" /> <input class="クラス名" placeholder="PC・携帯どちらでも可" id="email" type="email" name="user[email]" /> <input class="クラス名" placeholder="7文字以上の半角英数字" id="password" type="password" name="user[password]" />このRails特有のname属性値の指定の仕方についての記事が少なく途中で手詰まりました。
いろいろ調べたところ以下記事で明確な記載がありました。
JQuery validate in Rails applicationsPlease note that in rails, name of an element is created as form[name] instead of name only. If you are creating a form for creating a new user and fields in this form are Email first_name, last_name etc then name of these fields is being created as user[emal], user[first_name], user[last_name] resp. While using these name of an elements as key you have to keep in mind to use them in qoutes i.e
“user[email]” or your jquery will be unable to understand what this name belongs to.'user[name]': {required : true}一部を訳すとRailsではname属性値がuser[email]となっているため、"user[email]"としないとjQueryがそのname属性が何に属しているかを認識しないと記述がありました。
入力欄をフォーカスアウトした時に動作させる
通常の使い方ですとsubmitボタンが押されたタイミングでバリデーションチェックが動作します。
入力欄をフォーカスアウトした時などに動作させたい場合は、下記のように実行のタイミングを指定します。// 入力欄をフォーカスアウトしたときにバリデーションを実行 $("#name, #email, #password").blur(function () { $(this).valid(); });下記の記事を参考させていただきました。
jquery.validate.jsでvalidateチェックタイミングを指定する4. CSSファイル作成
バリデーションNGもしくはOKの時に追加されるクラス名を独自に指定できるので、
CSSで枠線を指定します。errorClass: "invalid", // バリデーションNGの場合に追加するクラス名の指定 validClass: "valid", // バリデーションOKの場合に追加するクラス名の指定registration.scss.registration { &__main { // 〜省略〜 &__form { // 〜省略〜 &__content { // 〜省略〜 &__group { // 〜省略〜 &__input.invalid { // バリデーションNGの場合に枠線を赤色に変更 border: #ea352d 1px solid; } &__input.valid { border: seagreen 1px solid; // バリデーションNGの場合に枠線を緑色に変更 } } } } } } // エラーメッセージ #name-error, #email-error, #password-error { color: #ea352d; line-height: 1.5; font-size: 14px; margin-top: 8px; } // 入力欄をクリック時に枠線を水色に変更 input[type=text]:focus, input[type=email]:focus, input[type=password]:focus { outline: none; border-color: #0098E8; }まとめ
jQuery Validation Pluginを用いて実行タイミングを指定すると動的なバリデーションチェックが実装可能です。
他にもselectタグのバリデーションなども実装しましたので別記事でまとめたいと思います。
記載内容に誤りなどございましたら、ご指摘いただけますと幸いです。参考URL
https://jqueryvalidation.org/
https://www.sejuku.net/blog/44470
https://sys-guard.com/post-14579/
https://into-the-program.com/jquery-validation-plugin/
[Rails]「jQueryValidationプラグイン」を使用してフォームのバリデーションを実装
jquery.validate.jsでvalidateチェックタイミングを指定する
- 投稿日:2020-01-04T11:28:30+09:00
【Rails】Rspec何言ってるか解読してみた件。コントローラー#new編
今回は、写真とタイトルを投稿するアプリケーションのtweetsコントローラーのnewアクション(新規ツイート投稿画面を呼ぶ)が問題なく動くかどうか、Rspecという言語を使ってテスト。
spec/controllers/tweets_controller_spec.rbに、擬似的にnewアクションを起こすリクエストを飛ばして、ちゃんとnewアクションに対応したビューが返ってくるかな?というテストコードを記述。
コントローラーのテストをする際は、2つの点に注意。
①tweets_controller.rbに、テストしたいアクションに対して、before_actionが定義されていないこと。
②gem 'rails-controller-testing'をインストールしよう。では、見ていきましょう。
tweets_controller_spec.rbrequire ‘rails_helper' describe TweetsController, type: :controller do describe 'GET #new' do it "renders the :new template" do get :new expect(response).to render_template :new end end end
require 'rails_helper'
RailsでRspecを利用する時の共通の設定が記載されているrails_helper.rbを読み込む。
これがなきゃ始まらない。
describe TweetsController, type: :controller do end
tweetsコントローラーのテストをするよ。 type: :controllerこれは書かなきゃいけないみたいですね。
describe 'GET #new' do end
GETメソッドのnewアクションに関するテストだよ。
it "renders the :new template" do end
do - endの間に書かれている、実際に動くテストコードの説明。
newアクションに対応したビューファイル(template)が呼び出される(renders)ことをテストしますと言っている。日本語でも良いらしい。ちなみに、it do endのセット1つで、1exampleと呼ぶ。
get :new
擬似的なリクエストを起こすテストコード。そのリクエストに、アクション名newを情報として渡す。
expect(response).to render_template :new
ちゃんと期待するビューが返ってくるかな?というテストコード。
resposeが、newが起きた時に呼ばれるrender_template(ビューである)と期待する(expect)。ターミナルにて
% bundle exec rspec spec/controllers/tweets_controller_spec.rb
実行すると以下の表示が。TweetsController GET #new renders the :new template Finished in 0.13692 seconds (files took 3.21 seconds to load) 1 example, 0 failures
TweetsController
GET #new
describeに書いたテストの概要説明。
renders the :new template
itに書いた、実際に動くテストコードの説明。
1 example, 0 failures
1exampleあって、0個の失敗。つまりテストは成功し、問題なかったということ。
初学者の視点からRailsアプリケーションのRspecを使ったテストについてまとめてみました。指摘やご意見お待ちしてます。モデルや、コントローラーのその他アクションについても、まとめていきたいと思います。
最後までご覧いただきありがとうございました!
- 投稿日:2020-01-04T10:53:18+09:00
[Rails]rails sを実行した際のPG::ConnectionBadの対処法
最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!rails sを実行すると画像のようなエラー画面になります
この様な場合は、PostgreSQLサーバーが起動していない事が原因の様です。
PG::ConnectionBad の対処法 - Qiitaただ、macの場合は、以下のコマンドは使えないので
$ sudo service postgresql start
以下のPostgreSQL起動コマンドを使用すればエラーが無くなりました!
・ローカル Mac で PostgreSQL を起動したり停止したり - Qiita# Start $ postgres -D /usr/local/var/postgres
- 投稿日:2020-01-04T10:46:29+09:00
Google Cloud Vision APIを使って、食品パッケージ画像からテキストを非同期で読み取りフォームに記載してみる
概要
ポートフォリオでGoogle Cloud Vision APIを使ってみたので紹介したいと思います。
ちなみに食品パッケージのサンプルは世界一おいしいアマタケのサラダチキン(ピザ味)を使用してます詳細
下準備
詳細は省きますが、Cloud Vision API を利用するため GCP のサービスアカウントの json ファイルをダウンロードします。gem 'google-cloud-vision'も bundle installしてください。keyファイルのPathはcredentials.yml.encで管理しました。
(時間が空いたときに詳細手順を記載します...)
参考:RailsにGoogle Cloud Vision APIを導入し、簡単に過激な画像を検知する
各種ファイルの作成
ルート
本件に関係あるものは、post "upload" だけです
route.rbresources :foods, only: %i[index show new create] do collection do get "search" post "upload" endビュー
3行目のinput行で発火させます。
views/foods/new.html.haml= form_with model: @food , data: { remote: false }, class: "common-form" do |food| .common-form__upload-image %input#image-upload.common-form__upload-image--input{name: "image", type: "file"} %label.common-form__image--icon{for: "image-upload"} .common-form__upload-image--icon-wrap %i.fas.fa-image .common-form__upload-image--icon-text 画像からPFCを取得する #~中略~ .plan-form__pfc .plan-form__pfc--protein = food.number_field :protein, step:"0.1", min:0 = food.label 'たんぱく質(g)' .plan-form__pfc--protein--description 画像を挿入すると自動で数値が入力されます。 .plan-form__pfc--fat = food.number_field :fat, step:"0.1", min:0 = food.label '脂質(g)' .plan-form__pfc--fat--description 画像を挿入すると自動で数値が入力されます。 .plan-form__pfc--carbo = food.number_field :carbo, step:"0.1", min:0 = food.label '炭水化物(g)' .plan-form__pfc--carbo--description 画像を挿入すると自動で数値が入力されます。 #~後略~jQuery
FormDataでコントローラーに送信します。
formData.append("image",$('input[type=file]')[0].files[0]);
が個人的に気づきにくかったところです。
こうすることで後述のコントローラーでデータを受け取れます。upload.js$(function(){ if(document.URL.match(/\/foods\/new/)) { //~中略~ $(document).on('change', 'input[type= "file"]',function(e) { //~中略~ e.preventDefault(); var formData = new FormData(); formData.append("image",$('input[type=file]')[0].files[0]); $.ajax({ type:'POST', url: '/foods/upload', data: formData, dataType:'json', processData: false, contentType: false }) //~後略~ }) } })コントローラー
image = params[:image].pathでjQuery側で設定したformDataを受け取れます。
正規表現で抽出した文字列を加工してます。1例なので、食品パッケージのバリエーションの種類分作る必要があります。(まだやってない...)foods_controller.rbclass FoodsController < ApplicationController #~中略~ def upload require 'google/cloud/vision' ENV["GOOGLE_CLOUD_PROJECT"] = Rails.application.credentials.google[:GOOGLE_CLOUD_PROJECT] ENV["GOOGLE_CLOUD_KEYFILE"] = if Rails.env.production? Rails.application.credentials.google[:GOOGLE_CLOUD_KEYFILE_PRO] else Rails.application.credentials.google[:GOOGLE_CLOUD_KEYFILE_DEV] end image = params[:image].path image_annotator_client = Google::Cloud::Vision::ImageAnnotator.new image_text = (image_annotator_client.document_text_detection image: image).to_s if image_text.include?("たんぱく質") @protein = image_text[/たんぱく質(.{0,3}\d+\.?\d+)/][/\d+\.?\d+/].to_f @fat = image_text[/脂質(.{0,3}\d+\.?\d+)/][/\d+\.?\d+/].to_f @carbo = image_text[/炭水化物(.{0,3}\d+\.?\d+)/][/\d+\.?\d+/].to_f @data = { protein: @protein, fat: @fat, carbo: @carbo } end respond_to do |format| format.html format.json end end #~後略~json.jbuilder
上記のコントローラーで処理したデータをjQueryで使えるようにします。
upload.json.jbuilderjson.data do |data| data.protein @data[:protein] data.fat @data[:fat] data.carbo @data[:carbo] endjQuery
再度jQueryファイルに戻ります。
data.data.protein等にするとデータを取り出せます。upload.js$(function(){ if(document.URL.match(/\/foods\/new/)) { //~中略~ $(document).on('change', 'input[type= "file"]',function(e) { //~中略~ e.preventDefault(); var formData = new FormData(); formData.append("image",$('input[type=file]')[0].files[0]); $.ajax({ type:'POST', url: '/foods/upload', data: formData, dataType:'json', processData: false, contentType: false }) .done(function(data){ $('#food_protein').val(data.data.protein) $('#food_fat').val(data.data.fat) $('#food_carbo').val(data.data.carbo) alert(`画像の読み込みに成功しました! たんぱく質:${data.data.protein}g 脂質:${data.data.fat}g 炭水化物:${data.data.carbo}g`) }) .fail(function(){ alert('画像の読み込みに失敗しました') }) }) } })完成です
あとがき
この記事を書いた目的
・自分なりに工夫した点をアウトプットして、理解を深める。
・あわよくば有識者にフィードバックをもらいたい。
・私と同じ初学者からも奇譚のない意見をもらいたい。(自分だったらどうこうする的な)筆者について
TECH::EXPERTにて4月27日より52期夜間・休日コースでruby/railsを学習している未経験エンジニアです。
ご不備等ありましたら、ご指摘ください。ひとこと
最後までご覧いただきまして、ありがとうございました。
もし気に入っていただけたら、イイね・ストック・フォローご自由に!
- 投稿日:2020-01-04T10:42:14+09:00
【Rails】CSVを取り込み、計算し、画面に出力
運営しているシステムの大幅回収にあたり、DBを移行しました。
その際あるテーブルのIDが連番になっていなかった部分を1から連番にしたのですが、関連するテーブル(1対多)のIDを1つずつ手動で変更するには量が膨大すぎたので、IDの値を簡単に変更するためのプログラムを組んだので内容を記録します。
プログラムの内容
・webページ上でCSVファイルをアップロード
・受け取ったファイルの各行1列目(一番左の項目)を配列に保存する
・数値を確認し、値ごとに計算を行う(42~117の間の数値だった場合-10、など)
・計算後の数値を画面に出力画面に出力された数値をコピペすることで、手元のCSVのIDを一括で修正することが出来ます。
実装
app/controllers/csv_controller.rbclass CsvController < ApplicationController require "csv" def input; end def output file = params[:file] @a = Array.new CSV.foreach(file) do |i| b = i[0].to_i if b @a.push(b) end end @a.each_with_index do |value, index| if value.between?(5,8) @a[index] -= 4 elsif value.between?(12,28) @a[index] -= 7 elsif value.between?(31,40) @a[index] -= 9 elsif value.between?(42,117) @a[index] -= 10 elsif value.between?(119,300) @a[index] -= 11 end end end endapp/views/csv/input.html.erb<%= form_with url: csv_path, local: true do |f| %> <%= f.file_field :file %> <%= f.submit %> <% end %>app/views/csv/output.html.erb<% @a.each do |i| %> <p><%= i %></p> <% end %>app/config/routes.rbRails.application.routes.draw do get '/csv', to:'csv#input' post '/csv', to:'csv#output' . .学んだこと
・
require "csv"
をコントローラー内に書き込むことで、CSV.foreach
等のメソッド?群が使えるようになる。
・between?(12,28)
というメソッドで、変数に格納されている数値が12以上28以下かどうかを確認することができる
・ファイルの送信はf.file_field :file
、受け取りはparams[:file]
でできる。
→Strong Parametersは、DBに値を保存するとき以外は、使わなくてもエラーは発生しない
・配列内の値を編集し、再度代入する時にはeach_with_index do |value, index|
が使える
・コントローラー名を単数形にしてしまったが、普通に動いた。
- 投稿日:2020-01-04T10:36:25+09:00
Rails6 開発中のアプリに機能追加をする時は専用のブランチを作ろう
目的
- Railsアプリに機能追加する時の筆者の手法をまとめる
実施方法概要
- 機能追加用ブランチの作成
- 機能追加を実施
- テスト
- 機能追加ブランチへの登録
- 機能追加ブランチをmasterブランチへmerge
実施方法詳細
機能追加用ブランチの作成
下記コマンドを実行してブランチを作成、機能追加ブランチへ移動する。
$ cd railsアプリのルートディレクトリ $ git branch 任意のブランチ名 $ git checkout 任意のブランチ名(前述の物と同じもの)機能追加を実施
- 追加したい機能をコーディングなどを行い追加する。
テスト
- 追加した機能が正常に動作するかブラウザでの簡単なテストを行う。
機能追加ブランチへの登録
下記コマンドを実行して追加した機能用のコードをgitに登録する。
$ git add 修正を行なったファイル名 $ git commit $ git push リモートリポジトリの別名(デフォルトならorigin) 任意のブランチ名(前述の物と同じもの)機能追加ブランチをmasterブランチへmerge
下記コマンドを実行して
master
ブランチに移動する。$ git chechkout master
下記コマンドを実行して機能追加用ブランチの修正内容を
master
ブランチにmergeする。$ git merge 任意のブランチ名(前述の物と同じもの)
- 投稿日:2020-01-04T10:36:25+09:00
Rails6 開発中のアプリに機能追加をしよう
目的
- Railsアプリに機能追加する時の筆者の手法をまとめる
実施方法概要
- 機能追加用ブランチの作成
- 機能追加を実施
- テスト
- 機能追加ブランチへの登録
- 機能追加ブランチをmasterブランチへmerge
実施方法詳細
機能追加用ブランチの作成
下記コマンドを実行してブランチを作成、機能追加ブランチへ移動する。
$ cd railsアプリのルートディレクトリ $ git branch 任意のブランチ名 $ git checkout 任意のブランチ名(前述の物と同じもの)機能追加を実施
- 追加したい機能をコーディングなどを行い追加する。
テスト
- 追加した機能が正常に動作するかブラウザでの簡単なテストを行う。
機能追加ブランチへの登録
下記コマンドを実行して追加した機能用のコードをgitに登録する。
$ git add 修正を行なったファイル名 $ git commit $ git push リモートリポジトリの別名(デフォルトならorigin) 任意のブランチ名(前述の物と同じもの)機能追加ブランチをmasterブランチへmerge
下記コマンドを実行して
master
ブランチに移動する。$ git chechkout master
下記コマンドを実行して機能追加用ブランチの修正内容を
master
ブランチにmergeする。$ git merge 任意のブランチ名(前述の物と同じもの)
- 投稿日:2020-01-04T08:55:01+09:00
github actions rspec
成果物
.github/workflows/ruby.ymlname: Ruby on: [push] jobs: build: runs-on: ubuntu-latest services: postgres: image: postgres:10.3 ports: ["5432:5432"] options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v1 - name: Set up Ruby 2.5 uses: actions/setup-ruby@v1 with: ruby-version: 2.5 - name: Install PostgreSQL client run: | sudo apt-get -yqq install libpq-dev dialog apt-utils - name: Build App env: RAILS_ENV: test DATABASE_URL: postgresql://postgres@localhost:5432/postgres?encoding=utf8&pool=5&timeout=5000 run: | gem install bundler:1.16.1 cd docker/api/api bundle install --quiet --jobs 4 --retry 3 bin/rails db:migrate:reset RAILS_ENV=test - name: Build and test with Rspc env: RAILS_ENV: test DATABASE_URL: postgresql://postgres@localhost:5432/postgres?encoding=utf8&pool=5&timeout=5000 run: | cd docker/api/api bundle exec rspecconfig/database.ymldefault: &default adapter: postgresql encoding: unicode host: db username: postgres password: pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default database: myApi_development test: <<: *default database: postgres_test url: <%= ENV['DATABASE_URL'].gsub('?', '_test?' ) %> production: <<: *default database: myApi_production username: myApi password: <%= ENV['MYAPI_DATABASE_PASSWORD'] %> url: <%= ENV['DATABASE_URL'] %>
はまった
エラー1
PG::ConnectionBad: could not translate host name "postgres" to address: Temporary failure in name resolution
役に立った
https://stackoverflow.com/a/20722229
postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]
stackoverflowを見る限り、localhostにしておけばいいっぽい。
てことで、これでなおったbefore env: RAILS_ENV: test DATABASE_URL: postgresql://postgres@postgres:5432/postgres?encoding=utf8&pool=5&timeout=5000 ---------- after env: RAILS_ENV: test DATABASE_URL: postgresql://postgres@localhost:5432/postgres?encoding=utf8&pool=5&timeout=5000
エラー2
Could not locate Gemfile
解決策
Gemfileが存在するdirectoryに移動する
cd docker/api/apiエラ-3
戦闘
エラーですぎ。うれしい。エラーでないと死ぬ
OSが本番環境と違うと思った。とりあえずOK
https://rocky-plateau-44026.herokuapp.com/
https://github.com/kajirikajiri/rails-api