20200104のRailsに関する記事は23件です。

form_withでデフォルトのデータ送信先を変更する[Rails]

ポートフォリオアプリを作っています。
作品の一覧と各作品の詳細は誰でも見られるようにしようと思いますが、作品の投稿と編集は管理者だけができるようにしたいと思います。

管理者ユーザーの作成は、過去の自分のこちらの記事を読んで実装できたのですが、

【初心者向け】管理者ユーザーと管理者用controllerの追加方法[Ruby, Rails]

その後、管理者ユーザーの投稿機能をつけ、submitボタンを押したときに、以下のようなエラーが出ました。

Image from Gyazo

何、/worksPOSTメソッドでつながるルーティングがないそうです。
確かに、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のデフォルトの送信先を変更する方法でした^^

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

【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のくだりにはこう書かれています。

image.png

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.js
import '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.js
import 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>

※例では超シンプルに、サインアウトに必要な箇所のみ抜き出しています。

おわりに

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

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

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

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

【Rails】Rspec何言ってるか解読してみた件。コントローラー#index編

今回は、写真とタイトルを投稿するアプリケーションのtweetsコントローラーのindexアクション(トップページを表示する)が問題なく動くかどうか、Rspecという言語を使ってテスト。

テストする内容は2つ。

  1. tweets_controller.rb内でindexアクションの定義文で生成した@tweetsに、意図したものが入っているかな?

    tweets_controller.rb
    def index
    @tweets = Tweet.includes(:user).page(params[:page]).per(5).order("created_at DESC")
    end
    
  2. テスト環境上で、indexアクションを起こすリクエストを飛ばして、indexアクションに対応したビューが返ってくるかな?

コントローラーのテストの際は、2つの点に注意。

①tweets_controller.rbにおいて、テストしたいアクションに対し、before_actionが定義されていないこと。
②gem rails-controller-testingをインストールしよう。

テストで使うダミーデータの生成

また、トップページはこれまで投稿されたツイートが一覧表示される仕様。テスト環境上でのindexアクションで持ってくる、これまでに投稿されたツイートデータを、ダミーでテスト用に生成。gemのfactory_botを利用しよう。ツイート投稿の際、そのツイートは誰が作ったのかという情報も含まれるため、ダミーユーザーも作成しダミーツイートにその情報を含ませよう。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    nickname              {"mamama"} 
    email                 {Faker::Internet.email}
    password              {"00000000"}
    password_confirmation {"00000000"}
  end
end
spec/factories/tweets.rb
FactoryBot.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.rb
require '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
end

1. 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を使ったテストについてまとめてみました。指摘やご意見お待ちしてます。モデルや、コントローラーのその他アクションについても、まとめていきたいと思います。

最後までご覧いただきありがとうございました!

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

RailsでiOSアプリのURI スキームに対応する

image.png

一瞬「あれっ...」ってなったので備忘録

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.rb
direct :qa_chat do
    "instagram://qa_chat"
end

ヘルパーリンク

Railsと同一のViewでヘルパーリンクを利用する場合は、以下のように記述します。

sample.erb
<%= link_to qa_chat_url %>

ちなみにダイレクトルーティングについて、Railsガイドでは以下の部分に記載があります。
(このURIスキームについての記載はありませんが、あくまで参考までに)
image.png

小ネタ

外部URLもroutes.rbで変数として定義できる

ダイレクトルーティングといえば、こちらの使い方のほうが一般的です。

よく使う外部URLを変数定義して、Railsアプリケーション内で使うことができます。
サービスLPのURLを記述する時は、個別にではなく、routes.rbにまとめて記述するほうが管理しやすいです。

example.rb
direct :homepage do
    "http://www.rubyonrails.org"
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.rb
FactoryBot.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
end

RSpecの基礎に関しては以下の記事が非常に参考になります。
引用:使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」

Request specを書いていく

deviseで生成されたアクションの中からいくつか書いていきます。

POST #create

spec/requests/users_autentications_spec.rb
require '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
end

postリクエストのパラメータに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に移行する

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

フォームクラスを使う

※この記事は、シリーズ記事「多人数による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.rb
  def 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.rb
class Entries::Form
  include ActiveModel::Model
  include ActiveModel::Attributes

attributeメソッドでparamsで渡されるパラメータ名を並べます。それ以外の属性はattr_accessorで作ります。

app/forms/entries/form.rb
  attribute :title, :string
  attribute :body, :string
  attribute :draft, :boolean, default: false
  attribute :published_at, :datetime
  attribute :tags
  attr_accessor :user, :entry

バリデーションはモデルの中ではなく、フォームクラスの中で指定します。

app/forms/entries/form.rb
  validates :title, presence: true, length: { maximum: 255 }
  validates :body, presence: true, length: { maximum: 40000 }
  validate :check_tags

initializeでは、super(params)でパラメータを格納します。

app/forms/entries/form.rb
  def 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.rb
  def 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.rb
  def 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
  end

Entries::Formと同様に、ActiveModel::ModelとActiveModel::Attributesをインクルードして属性を設定し、initializeでパラメータを読み込みます。

app/forms/entries/search_form.rb
class 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.rb
  def 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.rb
  def 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

関連記事

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

Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストを操作する

マイクロポストリソースのルーティング

Micropostsコントローラーの内容を実装する前提条件として、まずは必要なルーティングをconfig/routes.rbに記述していかなければなりません。但し、「Micropostリソースのインターフェースへのアクセスは、プロフィールページ・Homeページを介して行われる」という前提があるため、Mircopostsコントローラーにneweditのようなアクションは必要とされません。実際に必要とされるアクションは、createdestroyの2つのみとなります。

以上を踏まえて、config/routes.rbに対して実際に行う変更の内容は、以下のようになります。

config/routes.rb
  Rails.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です。そして、UsersControllerApplicationControllerのサブクラスです。

また、今回の実装においては、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文では、どういった内容をデータベースに送っているでしょうか? 確認してみてください。

まずは、新たに実装したマイクロポストの投稿フォームに文字を入力し、実際に投稿してみます。

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

「Post」ボタンを押すと、以下のような画面が出力されます。

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

「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?」という確認メッセージが表示されます。

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

そこで「OK」をクリックすると、マイクロポストが実際に削除されます。

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

「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.rb
  class 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.rb
  require '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.rb
assert_match /1 micropost(?!s)/, response.body

「"1 microposts"でない"1 micropost"という文字列が見つからない」という趣旨のメッセージが出てテストが失敗している、ということになります。想定通りの失敗ですね。


  1. こうしたルールを利用してメソッドの定義を上書きすることを「オーバーライド」と言います。 

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

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 init

3.Gemfileの編集

以下のコメント箇所を外したくらいです

Gemfile
#gem 'rails'

4.Dockerファイルの用意

Docerfileの内容については以下の記事を参考にさせて頂きました。
https://qiita.com/togana/items/30b22fc39fe6f7a188ec

今回の主な変更点は以下の点になりました

  • yarnのリポジトリ設定(Rails6の起動にはwebpackerが必要で、Webpackerのインストールにはyarnが必要)
  • yarnのインストール(上記と同じ理由)
  • webpackerのインストール
Dockerfile
FROM 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_name

8.ブラウザからアクセスする

ブラウザから以下のURLにアクセスしてRailsの画面が表示されることを確認
http://localhost:3000/

9.DockerHubへの登録

$ docker login
$ docker push developer_name/project_name:latest

DockerHubに登録ができたかは以下URLで確認できる
https://hub.docker.com/

10.DockerHubに登録されたDockerイメージからGKEノードを作成

以下、GCP Console画面です。
Kubernetes Engine -> ワークロード -> デプロイを選択すると以下の画面が表示されます。
スクリーンショット 2020-01-04 15.27.06.jpg
以下を選択し、後はデフォルトのままとしました。

  • 「既存コンテナイメージ」を選択
  • イメージパス:developer_name/project_name:latest (上記で作成しDockerHubに登録したものと同じものを選択)

11.GKEのサービスを作成し外部公開する

ポートを3000に設定した他は前回の4の手順以降と同じです。
https://qiita.com/bleu/items/f2f16ee34fb0039e8890

参考

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

Fargate及びECSでコンテナからAWSのIAM認証を利用する

概要

AWSでコンテナを利用する場合、FargateやECSが候補に上がると思います。
コンテナからAWSの各種サービスを利用する場合、IAMのCredentialsを利用する必要があります。
いつも意識せずに利用しているIAMですが、裏ではどういう構成になっているのか確認していきます。

確認方法

Fargateでsshできるようにして、コンテナ内でCredentialsを探します。

Fargateでsshするには以下の記事を参考にしてください

結果

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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【第7章】Railsチュートリアル 5.1(第4版) ユーザー登録

大まかな流れの自己整理が目的のため、不足・誤り等あれば追記&訂正していきますのでご指摘頂けますと幸いです:bow_tone1:
なお、筆者は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#destroy

putリクエスト  → 全部(旧残り)
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
  end

rails サーバを立ち上げると、止まってる場所がわかる

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

image.png

この時点でデフォルト画像のまま表示されるが、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>

image.png

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;
}

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

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;
}

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

そのまま作るとエラーが起きるが、「newアクション移行→createアクションがないよ」という表示なので正常。
image.png

createアクション追加&renderでビュー画面newに戻り、登録情報確認(試験的にfoobar入れてます)

  def create
    @user = User.new
    render 'new'
  end

image.png

7.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

image.png

エラー登場。createの直後のparamsはユーザが送る情報なのでいろいろな情報を送る(いじる)ことができるので、今後adminなどを入れていく過程で悪意あるユーザからDB書き換えたれるなどのマスアサインメント脆弱性を回避するRails4.0移行実装の機能。paramsハッシュでは:user属性を必須とし、名前、メールアドレス、パスワード、パスワードの確認の属性をそれぞれ許可し、それ以外を許可しないようにしたいので、requireなどを追加したuser_paramsメソッドをコントローラに追加してメソッドで対応する。

users_controller.rb
   def 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)
  end

Rails 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 %>

image.png

「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;
  }
}

   
Chromeの検証でエリア選択すると
スクリーンショット 2020-01-03 20.28.58.png

<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.rb
require '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にタイプミスしてました:innocent:

 

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
  end

flash

登録完了後に表示されるページにメッセージを表示する (この場合は新規ユーザーへのウェルカムメッセージ)。
createアクション(if文直下)と、ビュー(yield直前)に追加

users_controller.rb
flash[: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は一度だけなのでリロードすればメッセージ(緑色)は消える。

スクリーンショット 2020-01-04 13.42.55.png

成功時のテスト

成功時、ユーザのカウントが1増えてればokという内容。
基本的にはshowテンプレートが表示されていれば?‍♂️

users_signup_test.rb
  test "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'
  end

問題がなければ、herokuにデプロイして本番環境で確認。
スクリーンショット 2020-01-04 13.58.58.png

URLを確認すると「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.rb
workers 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 Procfile
web: bundle exec puma -C config/puma.rb

一応テスト走らせた後、
再度herokuへデプロイ(コミットメッセージが分かるように)して終了。

$ git add -A
$ git commit -m "Use SSL and the Puma webserver in production"    

感想ほか

講義が非常に分かりやすく全く挫折することなく終えました(講師:安川さん、ありがとうございました!!)。
教材模写(受け身)が中心だったので、忘れた部分含め2周目自力で進めて公式ドキュメント漁って読み込むなどもっと頭を悩ませる必要はあるかなと。
最後までお読み頂きありがとうございました!!

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

Railsチュートリアル 第13章 ユーザーのマイクロポスト - ユーザーのフィード画面に対するテストは正しく動いているか

概要

Railsチュートリアル 第13章 ユーザーのマイクロポスト - マイクロポストを操作する - 演習「フィード画面のマイクロポストをテストする」より、演習1番目の内容です。Railsチュートリアル本文には以下のようにあります。

リスト 13.55で示した4つのコメント (「無効な送信」など) のそれぞれに対して、テストが正しく動いているか確認してみましょう。具体的には、対応するアプリケーション側のコードをコメントアウトし、テストがredになることを確認し、元に戻すとgreenになることを確認してみましょう。

なお、「それぞれの確認で変更したソースコードは、確認が終わったら変更前の状態に戻すこと」を前提とします。

無効なマイクロポストを受け付けてしまう場合

無効なマイクロポストを受け付けてしまう例としては、「Micropostモデルのバリデーションが正常に機能していない場合」が想定できます。具体的には、「Micropostモデルのcontent属性に対するバリデーションが動作していない場合」です。当該バリデーションに関するソースコードをapp/models/micropost.rbからコメントアウトしてみます。

app/models/micropost.rb
  class 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.rb
  class 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_usercorrect_userという2つのBeforeフィルターが適用されます。その実装内容に誤りがあった場合も、マイクロポストの削除はうまくいかないはずです。

今回は、correct_userフィルターの実装内容を、意図的に誤ったものにしてみます。

app/controllers/microposts_controller.rb
  class 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 skips
Expected 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

想定通りの形でテストが失敗したということですね。

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

【Rails】Rspec何言ってるか解読してみた件。コントローラー#edit編

今回は、写真とタイトルを投稿するアプリケーションのtweetsコントローラーのeditアクション(投稿したツイートの編集画面を呼ぶ)が問題なく動くかどうか、Rspecという言語を使ってテスト。

テストする内容は以下の2つ。

  1. tweets_controller.rb内でeditアクションを定義した際に生成したインスタンス変数に期待する値が入っているかな?

    tweets_controller.rb
    def edit
    @tweet = Tweet.find(params[:id])
    end
    
  2. 擬似的にeditアクションを起こすリクエストを飛ばして、ちゃんとeditアクションに対応したビューが返ってくるかな?

コントローラーのテストをする際は、2つの点に注意。
①tweets_controller.rbに、テストしたいアクションに対して、before_actionが定義されていないこと。
②gem rails-controller-testingをインストールしよう。

また、factory_botを利用して、テストにおいてeditアクションで編集するダミーのツイートデータを作成しよう。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    nickname              {"mamama"}
    email                 {"mamama@mail.com"}
    password              {"00000000"}
    password_confirmation {"00000000"}
  end
end
spec/factories/tweets.rb
FactoryBot.define do
  factory :tweet do
    text  {" I love animals"}
    image {"animals.png"}
    user  #←factory_botを利用して作った :userのこと。ツイートを投稿した際、そのツイートは誰が作ったのかも情報に含まれるため、ダミーのユーザー情報も作成しダミーのツイートを作成する際に利用している。
  end
end

では、見ていきましょう。

tweets_controller_spec.rb
require '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
end

1. 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を使ったテストについてまとめてみました。指摘やご意見お待ちしてます。モデルや、コントローラーのその他アクションについても、まとめていきたいと思います。

最後までご覧いただきありがとうございました!

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

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#new

prefix名に_pathを付け加えることによって、パスとして認識される。
prefixを確認するとルートパスを表す「/」というパスは「root」になっている為、
「root_path」を記入したら「/」で指定した記述と同じになる。

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

【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

参考

【rails5】テーブルに複数の外部キーを追加する

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のような書き方でコネコネすれば出来そうな気がします。

参考

テーブルの同じモデルの外部キーを複数もたせる

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

rails: datetime_selectをカスタマイズする方法

やりたいこと

デフォルトでdatetime_selectを使うとデザイン的にあまり美しく無いので下記のようにカスタマイズしてみた。
デフォルトだと下記のような殺風景なデザインでUX的に美しくない。

before
image.png

after
image.png

どのようにしたらできるのか?

結論から言うと、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_separatortime_separatorもあるので暇なときにそっちについても記事にしていきたいと思う。
コメントで要望があればすぐに対応しますが、多分ググればそれっぽい記事があると思うのでまずはググってみてください。

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

【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.4

jQuery Validation Pluginとは

jQuery Validation Pluginはバリデーションチェックが実装できるjQueryのプラグインです。

公式サイト:https://jqueryvalidation.org/

やりたいこと

  • 入力フォームにユーザー情報を入力する際にリアルタイムにバリデーションをチェック
  • バリデーションエラーがある場合はエラーをリアルタイムに表示
  • バリデーションがNGの場合は入力欄が赤色、OKの場合は緑色に変更

なぜjQuery Validation Pluginを選んだのか?

バリデーションチェックを実装するにあたり、実装方法を検討しました。
他にも以下の方法があります。
RailsのActive Record バリデーション
HTMLのフォーム検証機能
HTML5 + JavaScriptによるFormバリデーション

プラグインなどを有効活用でき、動的にバリデーションチェックできることを主軸に検討し、
jQueryプラグインでフロントエンドでの第1チェックを行い、Railsのバリデーションでバックエンド側の最終チェックを行う方法を選びました。

完成イメージ

 バリデーションNGの場合
f84fd46c54185fe33c48155f5c351a86.gif
 バリデーションNG→OKの場合
89c9be89713460808c27152edc4ec222.gif

 バリデーションOKの場合
e20855f932d4782f531643e312954f0a.gif

実装手順

1. プラグインの導入

CDNでも導入できるそうですが、上手くいかなかったため、下記手順で導入しました。

1.1 公式サイトにアクセス
スクリーンショット 2020-01-04 0.41.33.png
1.2 ダウンロードページへ
スクリーンショット 2020-01-04 0.42.38.png
1.3 最下段からzipファイルをダウンロード
スクリーンショット 2020-01-04 0.43.37.png
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 applications

Please 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チェックタイミングを指定する

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

【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.rb
require 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を使ったテストについてまとめてみました。指摘やご意見お待ちしてます。モデルや、コントローラーのその他アクションについても、まとめていきたいと思います。

最後までご覧いただきありがとうございました!

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

[Rails]rails sを実行した際のPG::ConnectionBadの対処法

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

rails sを実行すると画像のようなエラー画面になります

9b14f558d5741ade85174b2f5f175e78.png

この様な場合は、PostgreSQLサーバーが起動していない事が原因の様です。
PG::ConnectionBad の対処法 - Qiita

ただ、macの場合は、以下のコマンドは使えないので

$ sudo service postgresql start

以下のPostgreSQL起動コマンドを使用すればエラーが無くなりました!
ローカル Mac で PostgreSQL を起動したり停止したり - Qiita

# Start
$ postgres -D /usr/local/var/postgres
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Cloud Vision APIを使って、食品パッケージ画像からテキストを非同期で読み取りフォームに記載してみる

概要

ポートフォリオでGoogle Cloud Vision APIを使ってみたので紹介したいと思います。
ちなみに食品パッケージのサンプルは世界一おいしいアマタケのサラダチキン(ピザ味)を使用してます

Image from Gyazo

詳細

下準備

詳細は省きますが、Cloud Vision API を利用するため GCP のサービスアカウントの json ファイルをダウンロードします。gem 'google-cloud-vision'も bundle installしてください。keyファイルのPathはcredentials.yml.encで管理しました。
(時間が空いたときに詳細手順を記載します...)
参考:RailsにGoogle Cloud Vision APIを導入し、簡単に過激な画像を検知する

各種ファイルの作成

ルート

本件に関係あるものは、post "upload" だけです

route.rb
  resources :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.rb
class 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.jbuilder
json.data do |data|
  data.protein @data[:protein]
  data.fat @data[:fat]
  data.carbo @data[:carbo]
end

jQuery

再度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を学習している未経験エンジニアです。
ご不備等ありましたら、ご指摘ください。

ひとこと

最後までご覧いただきまして、ありがとうございました。
もし気に入っていただけたら、イイね・ストック・フォローご自由に!

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

【Rails】CSVを取り込み、計算し、画面に出力

運営しているシステムの大幅回収にあたり、DBを移行しました。

その際あるテーブルのIDが連番になっていなかった部分を1から連番にしたのですが、関連するテーブル(1対多)のIDを1つずつ手動で変更するには量が膨大すぎたので、IDの値を簡単に変更するためのプログラムを組んだので内容を記録します。

プログラムの内容

・webページ上でCSVファイルをアップロード
・受け取ったファイルの各行1列目(一番左の項目)を配列に保存する
・数値を確認し、値ごとに計算を行う(42~117の間の数値だった場合-10、など)
・計算後の数値を画面に出力

画面に出力された数値をコピペすることで、手元のCSVのIDを一括で修正することが出来ます。

実装

app/controllers/csv_controller.rb
class 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
end
app/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.rb
Rails.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|が使える
・コントローラー名を単数形にしてしまったが、普通に動いた。

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

Rails6 開発中のアプリに機能追加をする時は専用のブランチを作ろう

目的

  • Railsアプリに機能追加する時の筆者の手法をまとめる

実施方法概要

  1. 機能追加用ブランチの作成
  2. 機能追加を実施
  3. テスト
  4. 機能追加ブランチへの登録
  5. 機能追加ブランチをmasterブランチへmerge

実施方法詳細

  1. 機能追加用ブランチの作成

    1. 下記コマンドを実行してブランチを作成、機能追加ブランチへ移動する。

      $ cd railsアプリのルートディレクトリ
      $ git branch 任意のブランチ名
      $ git checkout 任意のブランチ名(前述の物と同じもの)
      
  2. 機能追加を実施

    1. 追加したい機能をコーディングなどを行い追加する。
  3. テスト

    1. 追加した機能が正常に動作するかブラウザでの簡単なテストを行う。
  4. 機能追加ブランチへの登録

    1. 下記コマンドを実行して追加した機能用のコードをgitに登録する。

      $ git add 修正を行なったファイル名
      $ git commit
      $ git push リモートリポジトリの別名(デフォルトならorigin) 任意のブランチ名(前述の物と同じもの)
      
  5. 機能追加ブランチをmasterブランチへmerge

    1. 下記コマンドを実行してmasterブランチに移動する。

      $ git chechkout master
      
    2. 下記コマンドを実行して機能追加用ブランチの修正内容をmasterブランチにmergeする。

      $ git merge 任意のブランチ名(前述の物と同じもの)
      
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6 開発中のアプリに機能追加をしよう

目的

  • Railsアプリに機能追加する時の筆者の手法をまとめる

実施方法概要

  1. 機能追加用ブランチの作成
  2. 機能追加を実施
  3. テスト
  4. 機能追加ブランチへの登録
  5. 機能追加ブランチをmasterブランチへmerge

実施方法詳細

  1. 機能追加用ブランチの作成

    1. 下記コマンドを実行してブランチを作成、機能追加ブランチへ移動する。

      $ cd railsアプリのルートディレクトリ
      $ git branch 任意のブランチ名
      $ git checkout 任意のブランチ名(前述の物と同じもの)
      
  2. 機能追加を実施

    1. 追加したい機能をコーディングなどを行い追加する。
  3. テスト

    1. 追加した機能が正常に動作するかブラウザでの簡単なテストを行う。
  4. 機能追加ブランチへの登録

    1. 下記コマンドを実行して追加した機能用のコードをgitに登録する。

      $ git add 修正を行なったファイル名
      $ git commit
      $ git push リモートリポジトリの別名(デフォルトならorigin) 任意のブランチ名(前述の物と同じもの)
      
  5. 機能追加ブランチをmasterブランチへmerge

    1. 下記コマンドを実行してmasterブランチに移動する。

      $ git chechkout master
      
    2. 下記コマンドを実行して機能追加用ブランチの修正内容をmasterブランチにmergeする。

      $ git merge 任意のブランチ名(前述の物と同じもの)
      
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

github actions rspec

成果物

.github/workflows/ruby.yml
name: 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 rspec
config/database.yml
default: &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

image.png


エラー2

Could not locate Gemfile

解決策

Gemfileが存在するdirectoryに移動する

        cd docker/api/api

エラ-3


戦闘

image.png

エラーですぎ。うれしい。エラーでないと死ぬ
OSが本番環境と違うと思った。とりあえずOK
https://rocky-plateau-44026.herokuapp.com/
https://github.com/kajirikajiri/rails-api

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