20200126のRailsに関する記事は21件です。

Rails Minitest こんな時どうする?テストクラス毎にfixureを分けたい

概要

テストクラス毎にfixureを分けたい。

詳細

Minitestではテストデータをfixtureフォルダにyaml形式のファイルで配置しておかなければならない。yamlファイル名はモデル名であり、アプリケーション全体で1モデル=1fixutureファイルが基本である。

しかし、アプリケーションが複雑になってくると1モデル=1ファイルですべてのテストケースを網羅するようなテストデータを用意するのが難しくなることがある。そこでテストクラス毎にfixureファイルを分けることでテストケースを網羅するテストデータを用意し易くする。

対応

アプリケーション全体で1モデル=1ファイルを一度に読み込むようになっているのを止める。
それぞれのテストクラスで任意のfixtureを読み込むようにする。

サンプルコード

サンプルでは2つのテストクラス book_test.rb と user_test.rb があり、それぞれのテストクラスでBookモデルのテストデータを別々のfixtureから読み込む。

アプリケーション全体で1モデル=1ファイルを一度に読み込むようになっているのを止める。
通常はtest/fixuresフォルダ以下のfixureファイルを読み込む。

test/test_helper.r
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'

class ActiveSupport::TestCase
  # ここでfixtureを一度に読み込むようになっているのでコメントアウトする
  # fixtures :all
end

このテストクラスでは test/fixtures/book_test フォルダにある books.yaml をBookモデルに読み込むようにする。

test/book_test.rb
require 'test_helper'

class BookTest < ActiveSupport::TestCase

  fixture_directory = Rails.root.join("test", "fixtures", "book_test")
  fixture_files = [:books]
  fixture_models = {:book => Book}
  ActiveRecord::FixtureSet.create_fixtures(fixture_directory, fixture_files, fixture_models, ActiveRecord::Base)

  test "テスト" do
    # テストコード
  end

end

こちらのテストクラスでは test/fixtures/user_test フォルダにある books.yaml をBookモデルに読み込むようにする。このサンプルでは別のモデルであるUserのfixureも読み込んでいる。

test/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  fixture_directory = Rails.root.join("test", "fixtures", "user_test")
  fixture_files = [:books, :users]
  fixture_models = {:book => Book, :user => User}
  ActiveRecord::FixtureSet.create_fixtures(fixture_directory, fixture_files, fixture_models, ActiveRecord::Base)

  test "テスト" do
    # テストコード
  end

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

Rails+Reactによるフォームの作例

Rails+Vue.jsによるフォームの作例のサンプルをReactに移植してみました。

サンプルプログラムはこちら。簡単なブログアプリケーションです。masterブランチでは、Hooksを使っています。
https://github.com/kazubon/blog-rails6-react
別ブランチでは class extends React.Component を使ったものも作りました。
https://github.com/kazubon/blog-rails6-react/tree/extend

Rails側の作り方や全体的なポイントはVue版と同じですので、Vue版の記事を参照してください。

React歴2週間ですので、ヘンなところがあればご指摘ください。

環境

  • Rails 6.0、Webpacker 4.2、React 16.12。
  • 非SPA、Turbolinksあり。
  • jQueryとBootstrapあり。

application.js

packs下のapplication.jsはこんな感じです。SPAではないので、ページ遷移するたびにReactコンポーネントを初期化します。HTML要素をid属性で探して、対応するReactコンポーネントをReactDOM.renderします。

Vueと違い、Turbolinksとの相性の悪さは今のところなさそうです。

app/javascript/packs/application.js
import "core-js/stable";
import "regenerator-runtime/runtime";

require("@rails/ujs").start();
require("turbolinks").start();

import React from 'react';
import ReactDOM from 'react-dom';

import EntryIndex from '../entries/index';
import EntryForm from '../entries/form';
import EntryStar from '../entries/star';
import Flash from '../flash';

document.addEventListener('turbolinks:load', () => {
  Flash.show();

  let apps = [
    { elem: '#entry-index', object: EntryIndex },
    { elem: '#entry-form', object: EntryForm },
    { elem: '#entry-star', object: EntryStar }
  ];

  let props = window.jsProps || {};
  apps.forEach((app) => {
    if($(app.elem).length) {
      ReactDOM.render(React.createElement(app.object, props), $(app.elem)[0]);
    }
  });
});

Reactを埋め込むHTMLのテンプレートでは、Reactコンポーネントのpropsに渡すために、グローバル変数jsPropsを作っています。

app/views/entries/edit.html.erb
<script>
var jsProps = <%= { entryId: @entry.try(:id) }.to_json.html_safe %>;
</script>
<div id="entry-form"></div>

編集ページのフォーム

記事の編集フォームだけかいつまんで紹介します。

データの持ち方

コンポーネントは関数コンポーネントにします。記事のデータを扱うために、Hooksの機能useReducerを使ってます。アラートメッセージ用にはuseStateを使ってます。

app/javascript/entries/form.js
import React, { useEffect, useReducer, useState } from 'react';
(中略)
export default function (props) {
  const [entry, updateEntry] = useReducer(entryReducer, initialEntry);
  const [alert, setAlert] = useState('');
(中略)
}

useReducerに渡す記事データの初期値です。

app/javascript/entries/form.js
const initialEntry = {
  title: '',
  body: '',
  tags: [],
  published_at: '',
  draft: false
};

useReducerに渡す関数です。記事全体、タグのリスト、チェックボックス、それ以外(テキスト入力欄)で場合分けして新しい記事オブジェクトを返します。

app/javascript/entries/form.js
function entryReducer(entry, action) {
  switch(action.name) {
    case 'entry':
      return action.entry;
    case 'tag':
      let tags = entry.tags.map(t => ({ name: t.name }));
      tags[action.index] = { name: action.value };
      return { ...entry, tags };
    case 'draft':
      return { ...entry, draft: action.checked };
    default:
      return { ...entry, [action.name]: action.value };
  }
}

テンプレート

関数コンポーネントが返すテンプレートです。この関数は、データに変更があるたびに呼び出されるので、return (<...>) 以外の部分はなるべく小さめにしてみました。

フォームを送信するhandleSubmitやデータを読み込むgetEntryでは、関数の引数に関数を渡しています。useReducerが返したupdateEntryや、useStateが返したsetAlertです。

このような書き方でよいのかはよくわからないところです。

app/javascript/entries/form.js
export default function (props) {
  const [entry, updateEntry] = useReducer(entryReducer, initialEntry);
  const [alert, setAlert] = useState('');

  useEffect(() => {
    getEntry(props, updateEntry);
  }, []);

  function handleChange(e) {
    updateEntry({name: e.target.name, value: e.target.value,
      checked: e.target.checked });
  }

  return (
    <form onSubmit={e => handleSubmit(e, props.entryId, entry, setAlert)}>
      {alert && <div className="alert alert-danger">{alert}</div>}
      <div className="form-group">
        <label htmlFor="entry-title">タイトル</label>
        <input type="text" id="entry-title" name="title" className="form-control"
          required="" maxLength="255" pattern=".*[^\s]+.*"
          value={entry.title} onChange={handleChange} />
      </div>
      <div className="form-group">
        <label htmlFor="entry-body">本文</label>
        <textarea id="entry-body" name="body" cols="80" rows="15"
          className="form-control" required="" maxLength="40000"
          value={entry.body} onChange={handleChange} />
      </div>
      <div className="form-group">
        <label htmlFor="entry-tag0">タグ</label>
        <div>
          <TagList tags={entry.tags}
            onChange={(idx, value) => updateEntry({ name: 'tag', index: idx, value: value })} />
        </div>
      </div>
      <div className="form-group">
        <label htmlFor="entry-published_at">日時</label>
        <input type="text" id="entry-published_at" name="published_at"
          className="form-control"
          pattern="\d{4}(-|\/)\d{2}(-|\/)\d{2} +\d{2}:\d{2}"
          value={entry.published_at} onChange={handleChange} />
      </div>
      <div className="form-group mb-4">
        <input type="checkbox" id="entry-draft" name="draft" value="1"
          checked={entry.draft} onChange={handleChange} />
        <label htmlFor="entry-draft">下書き</label>
      </div>
      <div className="row">
        <SubmitButton entryId={props.entryId} />
        {props.entryId &&
          <DeleteButton
            onClick={() => handleDelete(props.entryId, setAlert)} />}
      </div>
    </form>
  );
}

子コンポーネントとして使うタグ入力欄のリスト、送信ボタン、削除ボタンです。

app/javascript/entries/form.js
function TagList(props) {
  return props.tags.map((tag, idx) => (
    <input key={idx}
      value={tag.name}
      onChange={e => props.onChange(idx, e.target.value)}
      className="form-control width-auto d-inline-block mr-2"
      style={{width: '17%'}} maxLength="255" />
  ));
}

function SubmitButton(props) {
  let text = props.entryId ? '更新' : '作成';
  return (
    <div className="col">
      <button type="submit" className="btn btn-outline-primary">{text}</button>
    </div>
  );
}

function DeleteButton(props) {
  return (
    <div className="col text-right">
      <button type="button" className="btn btn-outline-danger"
        onClick={props.onClick}>削除</button>
    </div>
  );
}

データの読み込み

useEffectを使い、コンポーネントがマウントされたときにAjaxでデータを読み込みます。useReducerが返すupdateEntryを呼び出す→entryReducerが呼ばれる→useReducerが返すentryが更新される、となります。

Reactでは、入力欄の値がnullだとエラーになるので、entry.title || ''のようなことをしています。

app/javascript/entries/form.js
function getEntry(props, updateEntry) {
  let path = props.entryId ? `/entries/${props.entryId}/edit` : '/entries/new';

  Axios.get(path + '.json').then((res) => {
    let entry = res.data.entry;
    updateEntry({
      name: 'entry',
      entry: {
        title: entry.title || '',
        body: entry.body || '',
        tags: initTags(entry.tags),
        published_at: entry.published_at,
        draft: entry.draft
      }
    });
  });
}
(中略)
export default function (props) {
(中略)
  useEffect(() => {
    getEntry(props, updateEntry);
  }, []);
(中略)
}

入力欄の値のバインディング

Vueのv-modelのようなものはReactにありません。入力欄の値が変更されたときに記事データを更新するには、inputにvalueとonChangeをセットで加えます。onChangeから呼び出すhandleChangeでは、useReducerが返す関数updateEntryを呼び出します。

なお、Reactのテンプレートでは、閉じタグのないHTML要素は <input /> のように書かないとなりません。

app/javascript/entries/form.js
export default function (props) {
(中略)
  function handleChange(e) {
    updateEntry({name: e.target.name, value: e.target.value,
      checked: e.target.checked });
  }

  return (
(中略)
        <input type="text" id="entry-title" name="title" className="form-control"
          required="" maxLength="255" pattern=".*[^\s]+.*"
          value={entry.title} onChange={handleChange} />
(中略)
  );
}

フォームの送信

フォームを送信する部分です。useReducerによって更新されたentryをそのままRailsに送ります。成功したときは指定のパスにリダイレクトします。失敗したときはアラート表示部にメッセージを入れます。

app/javascript/entries/form.js
function handleSubmit(e, entryId, entry, setAlert) {
  e.preventDefault();
  if(!validate(entry, setAlert)) { return }

  let path = entryId ? `/entries/${entryId}` : '/entries';
  Axios({
    method: entryId ? 'patch' : 'post',
    url: path + '.json',
    headers: {
      'X-CSRF-Token' : $('meta[name="csrf-token"]').attr('content')
    },
    data: { entry: entry }
  }).then((res) => {
    Flash.set({ notice: res.data.notice });
    Turbolinks.visit(res.data.location);
  }).catch((error) => {
    if(error.response.status == 422) {
      setAlert(error.response.data.alert);
    }
    else {
      setAlert(`${error.response.status} ${error.response.statusText}`);
    }
    window.scrollTo(0, 0);
  });
}
(中略)
export default function (props) {
(中略)
  return (
    <form onSubmit={e => handleSubmit(e, props.entryId, entry, setAlert)}>
(中略)
    </form>
  );
}

Vue.jsと比べて

  • Reactには、Vueと違ってHTMLファイルの中の要素を取り出してテンプレートとして使う機能はありません。必ずJavaScript内のテンプレートを使います。
  • ReactはVueよりソースコードの文字数が多くなります。
  • Reactは素のJavaScriptに近いので、だいたい予想通りに動作します。Vueのように想定外の動きをすることはなさそうです。
  • で、お仕事のプロジェクトでReactとVueどっちを採用するかというと、Vueとなります。Vueは本当は難しいのですが、少なくともとっつきやすい見た目をしています。スキルがバラバラなメンツを集めたプロジェクトでReactを使うと、どこから手を付けてよいやら途方に暮れる人が出そう。JavaScript大好きっ子が集まるプロジェクト(現実にあるのか?)なら、Reactはありでしょう。
  • 個人的なプロジェクトでは、好きなほう使えばいいんじゃないでしょうか。というか、両方やればJavaScriptのフレームワークに関する理解が深まります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails consoleで別のテーブルの_idとの結び付け

今回はRailsコンソール

を使ってトークルームにカテゴリーを紐付ける際に、初歩的なミスで試行錯誤するはめになってしまったので、メモとして残したいと思います。

まずは、コンソールで入力した結果を先に表示したいと思います。

2.5.3 :001 > user = User.first
(7.5ms) SET NAMES utf8, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
User Load (0.2ms) SELECT users.* FROM users ORDER BY users.id ASC LIMIT 1
=> #
2.5.3 :002 > memo_room = user.memo_rooms.build(title: 'hello')
=> #
2.5.3 :003 > category = Category.first
Category Load (0.2ms) SELECT categories.* FROM categories ORDER BY categories.id ASC LIMIT 1
=> #
2.5.3 :004 > memo_room = user.memo_rooms.build(title: 'hello', category_id: category.id)
=> #
2.5.3 :005 > memo_room.save
(0.3ms) BEGIN
Category Load (0.3ms) SELECT categories.* FROM categories WHERE categories.id = 1 LIMIT 1
MemoRoom Create (0.2ms) INSERT INTO memo_rooms (title, user_id, category_id, created_at, updated_at) VALUES ('hello', 1, 1, '2020-01-26 10:24:56', '2020-01-26 10:24:56')
(2.7ms) COMMIT
=> true
2.5.3 :006 >


・まず最初にuser = User.firstで 最初のユーザーを表示します。

・次にmemo_room ←トークルームと考えてください。にuser.memo_rooms.buildでタイトルhelloを作成します。

・これで、memo_roomのタイトルが作成されました。

・次に、このmemo_roomにはcategoryが紐づいているので、今の状態だとcategoryが紐付けされていないのでsaveは出来ません。
errorメッセージ表示をすると、categoryが必須です。と出ている状態です。(memo_room.errors.full_messages←で設定してあるのでコンソールに入れるとcategoryが必要な表示が出ます。)

・次にcategoryが作成されていなかったので、category(カラムはtitle) ←カテゴリを同じように作成します。(流れ同じなので略)

・そしてmemo_room = user.memo_rooms.build(title: 'hello', category_id: category.id)
memo_roomにcategory_idを入れてあげます。 category_idは数字であり文字列ではないので''は使いません。

上記の時に、memo_room ←の変数に代入を忘れていました。

ただcategory_idを入れてるだけを繰り返し。無意味に悩んでいたのです。

user.memo_rooms.build(title: 'hello', category_id: category.id) ←変数なしでただ作ってるという意味ない事をしてました。

変数にきちんと入れれば、 usersテーブル、memo_roomsテーブル、categoriesテーブルの3つのidが入り。
きちんと紐付けされて結果saveが上手くいきました。

初歩的なミスですが、メモを残す事によって、久しぶりにやった際に忘れないようにという思いで書きました。

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

deviseを用いたウィザード形式でユーザー新規登録機能を実装してやるって![実装方法総まとめ]

某フリマサイトのコピーサイトをチーム開発しており、deviseを用いたウィザード形式でのユーザー新規登録機能を実装しましたが、大分苦戦しました。。。
ということで、備忘録もかねて以下にまとめます!
*ページ内、クレジットカード登録のページございますが、今回はpay.jpなどのGemは使用しておりません。
単純に情報を登録するだけにしております。

ウィザード形式とは?

そもそもウィザード形式とは、なんぞや?というところから説明します。
ウィザード形式とは、サイト利用者に一つずつ質問や設定項目を提示し、対話的に処理を進める操作方式のことを指します。
イメージとしては、以下のような感じです。
d103dd1497df787e7afc015db8e1b3cb.png

ウィザード形式でのユーザー新規登録機能を実装する時の概要

まず、会員登録の画面でユーザー情報を入力させ、それをsessionに保持させておきます。
次に住所情報を登録する画面で住所情報入力してもらい、それをまたsessionに保持させておきます。
そして次の支払い方法を登録する画面で支払い方法を入力してもらい、最後のステップでsessionに保持していたユーザー情報と、それに関連する住所情報・支払いをテーブルに保存します。

この時注意する必要があるのは、ウィザードの各ページごとにテーブルを分ける必要があるということです。ウィザードが切り替わるときに、都度バリデーションのチェックを行うからです。
例えば2ページ目で記入する住所情報のカラムが、adressesテーブルではなく、usersテーブルに存在すると、1ページ目から2ページ目に切り替わるときに住所情報が入力されていないのでバリデーションに引っかかってしまいます。

該当テーブル(モデル)

今回の実装で使用したテーブルは以下のようになっております。(該当箇所のみ抜き出してます)

Column Type Options
nickname string null: false
email string null: false, unique:true
password string null: false
first_name string null: false
last_name string null: false
first_name_kana string null: false
last_name_kana string null: false
user_id references foreign_key:true
birthday integer null: false

Association
- has_one: address
- has_one : credit_cards

Column Type Options
user_id references null: false, foreign_key: true
card_company string null: false
card_number string null: false
card_year integer null: false
card_month integer null: false
card_pass integer null: false

Association
- belongs_to : user

Column Type Options
user_id references null: false, foreign_key: true
postal_code varcar(7) null: false
prefecture integer null: false
city string null: false
address string null: false
apartment string

Association
- belongs_to : user

実装の流れ

今回の実装の流れは以下のようになっております。
- gemインストール
- 各モデルの準備
- view編集
- controller編集
- route編集

gemインストール

まず、今回の機能実装にあたり"devise"を使用するので、gemをインストールします。

gemfile
gem 'devise'
ターミナル
$ bundle install

#アプリケーション内でdeviseを使えるようにするため、下記のコマンドを実行します。
$ rails g devise:install

各モデルの準備

それでは、今回使用するモデルの準備をしていきます。
今回、userモデルはdeviseを使用して登録されるため、devise管理下のモデルを作成します。

ターミナル
#User(devise管理下)作成
$ rails g devise user

#マイグレーションファイル編集後、migrateを忘れずに。
$ rails db:migrate

これでマイグレーションファイルが出来上がりますので、上記テーブルを元にマイグレーションファイルを編集します。
*emailとpasswordはdeviseが元々持っているので、それ以外を追記します。

application_controller編集

追加したカラムがある場合は、以下のようにapplication_controllerを編集します。

app/controllers/application_controller.rb
#例(nicknameだけ、カラム追加した場合)
  protected
    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
    end

モデル編集

作成したモデルを編集してvalidationなどを必要に応じてかけましょう。

app/models/user.rb
#例(nicknameだけ、カラム追加した場合)
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  validates :nickname ,presence: true
end

*その他モデルについては、通常通り「$ rails g model モデル名」で作成し、
マイグレーションファイルなどを編集しましょう。

以下、各モデルの記述です。
ポイント
- creditcardモデルとaddressモデルはenumを使用しています。
- creditcardモデルとaddressモデルはoptional: trueを記述してます。
*この記述がないと、creditcardモデルとaddressモデル内のカラム(user_id)に対して、エラーが発生するので、必ず記述しましょう。

models/creditcard
class Creditcard < ApplicationRecord
  belongs_to :user, optional: true
  validates :card_number, :card_company, :card_year, :card_month,:card_pass, presence: true

  enum card_company:{
    VISA:1,Mastercard:2,セゾンカード:3,JCB:4,アメリカンエキスプレス:5,ダイナーズ:6,ディスカバー:7
  }
end
models/address
class Address < ApplicationRecord
  belongs_to :user, optional: true
  validates :postal_code, :prefecture, :city, :address, presence: true

  enum prefecture:{
    北海道:1,青森県:2,岩手県:3,宮城県:4,秋田県:5,山形県:6,福島県:7,
    茨城県:8,栃木県:9,群馬県:10,埼玉県:11,千葉県:12,東京都:13,神奈川県:14,
    新潟県:15,富山県:16,石川県:17,福井県:18,山梨県:19,長野県:20,
    岐阜県:21,静岡県:22,愛知県:23,三重県:24,
    滋賀県:25,京都府:26,大阪府:27,兵庫県:28,奈良県:29,和歌山県:30,
    鳥取県:31,島根県:32,岡山県:33,広島県:34,山口県:35,
    徳島県:36,香川県:37,愛媛県:38,高知県:39,
    福岡県:40,佐賀県:41,長崎県:42,熊本県:43,大分県:44,宮崎県:45,鹿児島県:46,沖縄県:47
  }
end
model/user
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  validates :nickname, presence: true
  has_one :address
  has_one :creditcard
end

viewの編集(user)

それでは、各viewを作成しましょう。
今回はrailsのverが古いのでform_forを使用しております。

完成イメージ1e6f07b58919c11ee9f7ab262ba0cf5b.png
6d28ee1c1e57038bf074ce08cad0eeff.png

viewファイルは以下のように作成しています。

view
.single-container
  %header.single-header
    %h1.single-header__logo
      =link_to image_tag("fmarket_logo_red.svg",width:"134",height:"36"), root_path  
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text--active{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round     
        %li.single-header__progress__text
          支払い方法
          .single-header__progress__round    
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
.single-main__container__form__frame
  = form_for(@user, url: user_registration_path) do |f|
    = render "devise/shared/error_messages", resource: @user
    .form-group
      = f.label :ニックネーム
      %span.form-group__require 必須
      = f.text_field :nickname, {placeholder: "例) メルカリ太郎",class:'form-group__input'}
    .form-group
      = f.label :メールアドレス
      %span.form-group__require 必須
      = f.email_field :email, {autofocus: true, autocomplete: "email", placeholder: "PC・携帯どちらでも可",class:'form-group__input'}
    .form-group
      = f.label :パスワード
      %span.form-group__require 必須
      = f.password_field :password,{autocomplete: "new-password",placeholder: "7文字以上の半角英数字",class:'form-group__input',id:"password"}
      %p.form-group__info ※ 英字と数字の両方を含めて設定してください
      .form-password-revelation-toggle
        .checkbox-default
          %input#reveal_password{type: "checkbox",class:"icon-check"}
          %label{for: "reveal_password"} パスワードを表示する
        .form-password-revelation-revealed-password-container
          %span.form-password-revelation-revealed-password
    .form-group
      %label.form-group-text-title 本人確認
      %p.form-group__info 安心・安全にご利用いただくために、お客さまの本人情報の登録にご協力ください。他のお客さまに公開されることはありません。
    .form-group
      = f.label :"お名前(全角)"
      %span.form-group__require 必須
    = f.text_field :last_name, {placeholder:"例) 山田",class:'form-group__input--half'}
    = f.text_field :first_name, {placeholder:"例) 彩",class:'form-group__input--half'}
    .form-group
      = f.label :"お名前カナ(全角)"
      %span.form-group__require 必須
    = f.text_field :last_name_kana, {placeholder:"例) ヤマダ",class:'form-group__input--half'}
    = f.text_field :first_name_kana, {placeholder:"例) アヤ",class:'form-group__input--half'}
    .form-group
      = f.label :"生年月日"
      %span.form-group__require 必須
      %br
      .birthday-select-wrap
        != sprintf(f.date_select(:birthday, prefix:'birthday',with_css_classes:'XXXXX', prompt:"--",use_month_numbers:true, start_year:Time.now.year, end_year:1900, date_separator:'%s'),'年','月')+'日'
      .clearfix
    .form-group
      %p.form-group__text--center
        「次へ進む」のボタンを押すことにより、
        = link_to "利用規約", "#" , target:"_blank"
        に同意したものとみなします
      = f.submit '次へ', class: "btn-default btn-red", url: "address_path"
  = render "registration_footer"

viewの編集(address)

d43f2201d8164b186cc41b855da68991.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to "#" do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text
          支払い方法
          .single-header__progress__round
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        住所入力
      .single-main__container__form__frame
        =form_for(@address, url: addresses_path, method: :post) do |f|
          =render "devise/shared/error_messages", resource: @address
          .form-group
            = f.label :郵便番号
            %span.form-group__require 必須
            = f.text_field :postal_code,{autofocus: true, placeholder: "例)123-4567", class: 'form-group__input'}
          .form-group
            = f.label :都道府県
            %span.form-group__require 必須
            = f.select :prefecture, Address.prefectures.keys, {}, {class: 'form-group__input'}
          .form-group
            = f.label :市町村
            %span.form-group__require 必須
            = f.text_field :city, autofocus: true, placeholder: "例)札幌市", class: 'form-group__input'
          .form-group
            = f.label :番地
            %span.form-group__require 必須
            = f.text_field :address, autofocus: true, placeholder: "例)青葉1-1-1", class: 'form-group__input'
          .form-group
            = f.label :建物名
            %span.form-group__optional 任意
            = f.text_field :apartment, autofocus: true, placeholder: "例)柳ビル103", class: 'form-group__input'
          .form-group
            = f.submit '次へ', class: "btn-default btn-red", url: "creditcard_path"
  = render "registration_footer"

viewの編集(creditcard)

612316d11d112f7fb4f670ef83a440ec.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to root_path do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          支払い方法
          .single-header__progress__round--red
            .single-header__progress__round--red-long{ id: "long" }
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        支払い方法

.single-main__container__form
  .single-main__container__form__frame
    = form_for(@creditcard, url: creditcards_path,method: :post) do |f|
      = render "devise/shared/error_messages", resource: @creditcard
      .form-group
        = f.label :カード番号
        %span.form-group__require 必須
        = f.text_field :card_number, {placeholder: "半角数字のみ", class: "form-group__input"}
      .form-group
        = f.label :カード会社
        %span.form-group__require 必須
        = f.select :card_company, Creditcard.card_companies.keys, {}, {class: 'form-group__input'}
        %ul.signup__card--list
          %li.icon--visa
            = image_tag("visa.svg", id:"icon--visa")
          %li.icon--master
            = image_tag("master-card.svg", id:"icon--master")
          %li.icon--saison
            = image_tag("saison-card.svg", id:"icon--saison")
          %li.icon--jcb
            = image_tag("jcb.svg", id:"icon--jcb")
          %li.icon--americanexpress
            = image_tag("american_express.svg", id:"icon--americanexpress")
          %li.icon--diners
            = image_tag("dinersclub.svg", id:"icon--diners")
          %li.icon--discover
            = image_tag("discover.svg", id:"icon--discover")
      .form-group
        = f.label :有効期限
        %span.form-group__require 必須
        %br
        = f.select :card_year, options_for_select(["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
        = f.select :card_month, options_for_select(["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
      .form-group
        = f.label :セキュリティコード, class: "label"
        %span.form-group__require 必須
        = f.text_field :card_pass, placeholder: "カード背面4桁もしくは3桁の番", class: "form-group__input"

      .form-group__add
        .form-group__add--question ?
        %p.form-group__text--right--blue
          カード裏面の番号とは?
      .form-group
        = f.submit "登録する", class: "btn-default btn-red", url: "creditcards_path", method: :post
  = render "/registration/registration_footer"

  f.submit "Sign up"

controllerの準備+編集

今回は、devise管理下のuserモデルを使用しているため、controllerもdeviseの管理下に作成します。

ターミナル
$ rails g devise:controllers users

作成が完了しましたら、該当のコントローラーを編集します。

controller編集(user登録)

まずはuserの登録ができるようにコントローラを編集します。
ファイル内に元々記述されているコメントアウトされている箇所は、すでにDevise::RegistrationsControllerで定義されているものです。
コメントアウト部分は外して上書きすることができます(メソッドのオーバーライド)。
また、superはスーパークラス(今回であればDevise)のメソッドを呼び出しています。
*スーパークラスについての詳細は、まずはこちらのRuby公式ドキュメントを確認しましょう。
https://docs.ruby-lang.org/ja/2.5.0/doc/spec=2fcall.html#super

それでは、まずnewアクションを編集します。

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController

#以下、superクラスの呼び出しを削除して、User.newで新規インスタンスを生成することもできますが、最初にdeviseとUserモデルが紐づくように設定してあるので、superで呼び出してもdeviseは同様の操作を行ってくれます。

 def new
    super
 end

続いて、createアクションを編集します。
ポイントは以下の通りです。
- 1ページ目で入力した情報のバリデーションチェック
- 1ページで入力した情報をsessionに保持させること
*passwordはuser.attributeに入っていないので、個別で入れてあげます。
*session["devise.regist_data"]の"devise.regist_data"、こちらは基本的には変数ですが、
"devise.regist_data"はdeviseが落ちたときに自動的にセッションを切る機能を持ってます。
- 次の住所情報登録で使用するインスタンスを生成、当該ページへ遷移すること

app/controllers/users/registrations_controller.rb
 def create
    @user = User.new(sign_up_params)
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
  #この記述でもOKです。 session["devise.regist_data"] [user]= @user.attributes
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @address = @user.build_address
    render :new_address
 end

controller編集(address登録)

アドレスの登録は基本的にuserと同様ですが、userの記述でaddressのインスタンス変数は作成しているので、
createアクションのみ記述します。
また、protected内に、引数の設定をしましょう。
- buildメソッド:
今回、以下のようにbuildメソッドを使用しています。
これは、親モデルに属する子モデルのインスタンスを新たに生成したい場合に使うメソッドとなっており、
このメソッドを使用することで、外部キーに値が入った状態でインスタンスが生成できます。
(親モデルと子モデルは、アソシエーション設定あり)
今回は以下のようにaddressをuserの子モデルとして、使用するために、以下のように記述しており、
addressにはuser_idのカラムに数字が与えられます。
例) @address = @user.build_address

app/controllers/users/registrations_controller.rb
  def create_address
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(address_params)
    unless @address.valid?
      flash.now[:alert] = @address.errors.full_messages
      render :new_address and return
    end
    @user.build_address(@address.attributes)
    session["address"] = @address.attributes
    @creditcard = @user.build_creditcard
    render :new_credit_card
  end

  protected

  def address_params
    params.require(:address).permit(:address,:postal_code, :prefecture, :city,:apartment)
  end

controller編集(creditcard登録)

creditcardの登録に関しても、addressの記述でcreditcardのインスタンス変数は作成しているので、
createアクションのみ記述します。
ポイントは以下の通りです。
- バリデーションチェック
- バリデーションチェックが完了した情報と、sessionで保持していた情報とあわせ、ユーザー情報として保存すること
ログインをすること
- sessionを削除すること

app/controllers/users/registrations_controller.rb
  def create_creditcard
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(session["address"])
    @creditcard = Creditcard.new(creditcard_params)
    unless @creditcard.valid?
      flash.now[:alert] = @creditcard.errors.full_messages
      render :new_credit_card and return
    end
    @user.build_address(@address.attributes)
    @user.build_creditcard(@creditcard.attributes)
    @user.save
    sign_in(:user, @user)
  end

routeの編集

view、controllerの準備ができましたので、routeの編集をします。
この編集は2つのポイントがあります。
1.devise userモデルにrouteを設定
2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする

1.devise userモデルにrouteを設定

devise管理下のusersコントローラを作成がしましたが、記述を変更しないと、現状deviseのコントローラーが呼ばれてしまっています。
そこで、以下のように記述をして、ルーティングを設定しましょう。

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

2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする

今回複数モデル(addressとcreditcard)を使用しているため、devise_scopeを使用し、各モデルのルートを設定しましょう。

route.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    }
  devise_scope :user do
    get 'addresses', to: 'users/registrations#new_address'
    post 'addresses', to: 'users/registrations#create_address'
    get 'creditcards', to: 'users/registrations#new_creditcard'
    post 'creditcards', to: 'users/registrations#create_creditcard'
  end
end

これで今回使用するルーティングの設定が完了しました。

以上でdeviseを用いたウィザード形式でユーザー新規登録機能が実装されております。

最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

https://docs.ruby-lang.org/ja/2.5.0/doc/spec=2fcall.html#super

devise_parameter_sanitizerメソッドとはなにか
https://aliceblog1616.com/devise_parameter_sanitizer%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%81%AF%EF%BC%9F/

rails devise完全入門!結局deviseって何ができるの?
https://www.sejuku.net/blog/13378

buildメソッドについて
https://qiita.com/tsuchinoko_run/items/d671ea840bc0bfa90186

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

deviseを用いたウィザード形式でユーザー新規登録機能を実装した時の話。[実装方法総まとめ]

某フリマサイトのコピーサイトをチーム開発しており、deviseを用いたウィザード形式でのユーザー新規登録機能を実装しましたが、大分苦戦しました。。。
ということで、備忘録もかねて以下にまとめます!

ウィザード形式とは?

そもそもウィザード形式とは、なんぞや?というところから説明します。
ウィザード形式とは、サイト利用者に一つずつ質問や設定項目を提示し、対話的に処理を進める操作方式のことを指します。
イメージとしては、以下のような感じです。
d103dd1497df787e7afc015db8e1b3cb.png

ウィザード形式でのユーザー新規登録機能を実装する時の概要

まず、会員登録の画面でユーザー情報を入力させ、それをsessionに保持させておきます。
次に住所情報を登録する画面で住所情報入力してもらい、それをまたsessionに保持させておきます。
そして次の支払い方法を登録する画面で支払い方法を入力してもらい、最後のステップでsessionに保持していたユーザー情報と、それに関連する住所情報・支払いをテーブルに保存します。

この時注意する必要があるのは、ウィザードの各ページごとにテーブルを分ける必要があるということです。ウィザードが切り替わるときに、都度バリデーションのチェックを行うからです。
例えば2ページ目で記入する住所情報のカラムが、adressesテーブルではなく、usersテーブルに存在すると、1ページ目から2ページ目に切り替わるときに住所情報が入力されていないのでバリデーションに引っかかってしまいます。

該当テーブル(モデル)

今回の実装で使用したテーブルは以下のようになっております。(該当箇所のみ抜き出してます)

Column Type Options
nickname string null: false
email string null: false, unique:true
password string null: false
first_name string null: false
last_name string null: false
first_name_kana string null: false
last_name_kana string null: false
user_id references foreign_key:true
birthday integer null: false

Association
- has_one: address
- has_one : credit_cards

Column Type Options
user_id references null: false, foreign_key: true
card_company string null: false
card_number string null: false
card_year integer null: false
card_month integer null: false
card_pass integer null: false

Association
- belongs_to : user

Column Type Options
user_id references null: false, foreign_key: true
postal_code varcar(7) null: false
prefecture integer null: false
city string null: false
address string null: false
apartment string

Association
- belongs_to : user

実装の流れ

今回の実装の流れは以下のようになっております。
- gemインストール
- 各モデルの準備
- view編集
- controller編集
- route編集

gemインストール

まず、今回の機能実装にあたり"devise"を使用するので、gemをインストールします。

gemfile
gem 'devise'
ターミナル
$ bundle install

#アプリケーション内でdeviseを使えるようにするため、下記のコマンドを実行します。
$ rails g devise:install

各モデルの準備

それでは、今回使用するモデルの準備をしていきます。
今回、userモデルはdeviseを使用して登録されるため、devise管理下のモデルを作成します。

ターミナル
#User(devise管理下)作成
$ rails g devise user

#マイグレーションファイル編集後、migrateを忘れずに。
$ rails db:migrate

これでマイグレーションファイルが出来上がりますので、上記テーブルを元にマイグレーションファイルを編集します。
*emailとpasswordはdeviseが元々持っているので、それ以外を追記します。

application_controller編集

追加したカラムがある場合は、以下のようにapplication_controllerを編集します。

app/controllers/application_controller.rb
#例(nicknameだけ、カラム追加した場合)
  protected
    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
    end

モデル編集

作成したモデルを編集してvalidationなどを必要に応じてかけましょう。

app/models/user.rb
#例(nicknameだけ、カラム追加した場合)
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  validates :nickname ,presence: true
end

*その他モデルについては、通常通り「$ rails g model モデル名」で作成し、
マイグレーションファイルなどを編集しましょう。

以下、各モデルの記述です。
ポイント
- creditcardモデルとaddressモデルはenumを使用しています。
- creditcardモデルとaddressモデルはoptional: trueを記述してます。
*この記述がないと、creditcardモデルとaddressモデル内のカラム(user_id)に対して、エラーが発生するので、必ず記述しましょう。

models/creditcard
class Creditcard < ApplicationRecord
  belongs_to :user, optional: true
  validates :card_number, :card_company, :card_year, :card_month,:card_pass, presence: true

  enum card_company:{
    VISA:1,Mastercard:2,セゾンカード:3,JCB:4,アメリカンエキスプレス:5,ダイナーズ:6,ディスカバー:7
  }
end
models/address
class Address < ApplicationRecord
  belongs_to :user, optional: true
  validates :postal_code, :prefecture, :city, :address, presence: true

  enum prefecture:{
    北海道:1,青森県:2,岩手県:3,宮城県:4,秋田県:5,山形県:6,福島県:7,
    茨城県:8,栃木県:9,群馬県:10,埼玉県:11,千葉県:12,東京都:13,神奈川県:14,
    新潟県:15,富山県:16,石川県:17,福井県:18,山梨県:19,長野県:20,
    岐阜県:21,静岡県:22,愛知県:23,三重県:24,
    滋賀県:25,京都府:26,大阪府:27,兵庫県:28,奈良県:29,和歌山県:30,
    鳥取県:31,島根県:32,岡山県:33,広島県:34,山口県:35,
    徳島県:36,香川県:37,愛媛県:38,高知県:39,
    福岡県:40,佐賀県:41,長崎県:42,熊本県:43,大分県:44,宮崎県:45,鹿児島県:46,沖縄県:47
  }
end
model/user
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  validates :nickname, presence: true
  has_one :address
  has_one :creditcard
end

viewの編集(user)

それでは、各viewを作成しましょう。
今回はrailsのverが古いのでform_forを使用しております。

完成イメージ1e6f07b58919c11ee9f7ab262ba0cf5b.png
6d28ee1c1e57038bf074ce08cad0eeff.png

viewファイルは以下のように作成しています。

view
.single-container
  %header.single-header
    %h1.single-header__logo
      =link_to image_tag("fmarket_logo_red.svg",width:"134",height:"36"), root_path  
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text--active{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round     
        %li.single-header__progress__text
          支払い方法
          .single-header__progress__round    
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
.single-main__container__form__frame
  = form_for(@user, url: user_registration_path) do |f|
    = render "devise/shared/error_messages", resource: @user
    .form-group
      = f.label :ニックネーム
      %span.form-group__require 必須
      = f.text_field :nickname, {placeholder: "例) メルカリ太郎",class:'form-group__input'}
    .form-group
      = f.label :メールアドレス
      %span.form-group__require 必須
      = f.email_field :email, {autofocus: true, autocomplete: "email", placeholder: "PC・携帯どちらでも可",class:'form-group__input'}
    .form-group
      = f.label :パスワード
      %span.form-group__require 必須
      = f.password_field :password,{autocomplete: "new-password",placeholder: "7文字以上の半角英数字",class:'form-group__input',id:"password"}
      %p.form-group__info ※ 英字と数字の両方を含めて設定してください
      .form-password-revelation-toggle
        .checkbox-default
          %input#reveal_password{type: "checkbox",class:"icon-check"}
          %label{for: "reveal_password"} パスワードを表示する
        .form-password-revelation-revealed-password-container
          %span.form-password-revelation-revealed-password
    .form-group
      %label.form-group-text-title 本人確認
      %p.form-group__info 安心・安全にご利用いただくために、お客さまの本人情報の登録にご協力ください。他のお客さまに公開されることはありません。
    .form-group
      = f.label :"お名前(全角)"
      %span.form-group__require 必須
    = f.text_field :last_name, {placeholder:"例) 山田",class:'form-group__input--half'}
    = f.text_field :first_name, {placeholder:"例) 彩",class:'form-group__input--half'}
    .form-group
      = f.label :"お名前カナ(全角)"
      %span.form-group__require 必須
    = f.text_field :last_name_kana, {placeholder:"例) ヤマダ",class:'form-group__input--half'}
    = f.text_field :first_name_kana, {placeholder:"例) アヤ",class:'form-group__input--half'}
    .form-group
      = f.label :"生年月日"
      %span.form-group__require 必須
      %br
      .birthday-select-wrap
        != sprintf(f.date_select(:birthday, prefix:'birthday',with_css_classes:'XXXXX', prompt:"--",use_month_numbers:true, start_year:Time.now.year, end_year:1900, date_separator:'%s'),'年','月')+'日'
      .clearfix
    .form-group
      %p.form-group__text--center
        「次へ進む」のボタンを押すことにより、
        = link_to "利用規約", "#" , target:"_blank"
        に同意したものとみなします
      = f.submit '次へ', class: "btn-default btn-red", url: "address_path"
  = render "registration_footer"

viewの編集(address)

d43f2201d8164b186cc41b855da68991.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to "#" do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text
          支払い方法
          .single-header__progress__round
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        住所入力
      .single-main__container__form__frame
        =form_for(@address, url: addresses_path, method: :post) do |f|
          =render "devise/shared/error_messages", resource: @address
          .form-group
            = f.label :郵便番号
            %span.form-group__require 必須
            = f.text_field :postal_code,{autofocus: true, placeholder: "例)123-4567", class: 'form-group__input'}
          .form-group
            = f.label :都道府県
            %span.form-group__require 必須
            = f.select :prefecture, Address.prefectures.keys, {}, {class: 'form-group__input'}
          .form-group
            = f.label :市町村
            %span.form-group__require 必須
            = f.text_field :city, autofocus: true, placeholder: "例)札幌市", class: 'form-group__input'
          .form-group
            = f.label :番地
            %span.form-group__require 必須
            = f.text_field :address, autofocus: true, placeholder: "例)青葉1-1-1", class: 'form-group__input'
          .form-group
            = f.label :建物名
            %span.form-group__optional 任意
            = f.text_field :apartment, autofocus: true, placeholder: "例)柳ビル103", class: 'form-group__input'
          .form-group
            = f.submit '次へ', class: "btn-default btn-red", url: "creditcard_path"
  = render "registration_footer"

viewの編集(creditcard)

612316d11d112f7fb4f670ef83a440ec.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to root_path do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          支払い方法
          .single-header__progress__round--red
            .single-header__progress__round--red-long{ id: "long" }
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        支払い方法

.single-main__container__form
  .single-main__container__form__frame
    = form_for(@creditcard, url: creditcards_path,method: :post) do |f|
      = render "devise/shared/error_messages", resource: @creditcard
      .form-group
        = f.label :カード番号
        %span.form-group__require 必須
        = f.text_field :card_number, {placeholder: "半角数字のみ", class: "form-group__input"}
      .form-group
        = f.label :カード会社
        %span.form-group__require 必須
        = f.select :card_company, Creditcard.card_companies.keys, {}, {class: 'form-group__input'}
        %ul.signup__card--list
          %li.icon--visa
            = image_tag("visa.svg", id:"icon--visa")
          %li.icon--master
            = image_tag("master-card.svg", id:"icon--master")
          %li.icon--saison
            = image_tag("saison-card.svg", id:"icon--saison")
          %li.icon--jcb
            = image_tag("jcb.svg", id:"icon--jcb")
          %li.icon--americanexpress
            = image_tag("american_express.svg", id:"icon--americanexpress")
          %li.icon--diners
            = image_tag("dinersclub.svg", id:"icon--diners")
          %li.icon--discover
            = image_tag("discover.svg", id:"icon--discover")
      .form-group
        = f.label :有効期限
        %span.form-group__require 必須
        %br
        = f.select :card_year, options_for_select(["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
        = f.select :card_month, options_for_select(["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
      .form-group
        = f.label :セキュリティコード, class: "label"
        %span.form-group__require 必須
        = f.text_field :card_pass, placeholder: "カード背面4桁もしくは3桁の番", class: "form-group__input"

      .form-group__add
        .form-group__add--question ?
        %p.form-group__text--right--blue
          カード裏面の番号とは?
      .form-group
        = f.submit "登録する", class: "btn-default btn-red", url: "creditcards_path", method: :post
  = render "/registration/registration_footer"

  f.submit "Sign up"

controllerの準備+編集

今回は、devise管理下のuserモデルを使用しているため、controllerもdeviseの管理下に作成します。

ターミナル
$ rails g devise:controllers users

作成が完了しましたら、該当のコントローラーを編集します。

controller編集(user登録)

まずはuserの登録ができるようにコントローラを編集します。
ファイル内に元々記述されているコメントアウトされている箇所は、すでにDevise::RegistrationsControllerで定義されているものです。
コメントアウト部分は外して上書きすることができます(メソッドのオーバーライド)。
また、superはスーパークラス(今回であればDevise)のメソッドを呼び出しています。
*スーパークラスについての詳細は、まずはこちらのRuby公式ドキュメントを確認しましょう。
https://docs.ruby-lang.org/ja/2.5.0/doc/spec=2fcall.html#super

それでは、まずnewアクションを編集します。

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController

#以下、superクラスの呼び出しを削除して、User.newで新規インスタンスを生成することもできますが、最初にdeviseとUserモデルが紐づくように設定してあるので、superで呼び出してもdeviseは同様の操作を行ってくれます。

 def new
    super
 end

続いて、createアクションを編集します。
ポイントは以下の通りです。
- 1ページ目で入力した情報のバリデーションチェック
- 1ページで入力した情報をsessionに保持させること
*passwordはuser.attributeに入っていないので、個別で入れてあげます。
*session["devise.regist_data"]の"devise.regist_data"、こちらは基本的には変数ですが、
"devise.regist_data"はdeviseが落ちたときに自動的にセッションを切る機能を持ってます。
- 次の住所情報登録で使用するインスタンスを生成、当該ページへ遷移すること

app/controllers/users/registrations_controller.rb
 def create
    @user = User.new(sign_up_params)
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
  #この記述でもOKです。 session["devise.regist_data"] [user]= @user.attributes
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @address = @user.build_address
    render :new_address
 end

controller編集(address登録)

アドレスの登録は基本的にuserと同様ですが、userの記述でaddressのインスタンス変数は作成しているので、
createアクションのみ記述します。
また、protected内に、引数の設定をしましょう。
- buildメソッド:
今回、以下のようにbuildメソッドを使用しています。
これは、親モデルに属する子モデルのインスタンスを新たに生成したい場合に使うメソッドとなっており、
このメソッドを使用することで、外部キーに値が入った状態でインスタンスが生成できます。
(親モデルと子モデルは、アソシエーション設定あり)
今回は以下のようにaddressをuserの子モデルとして、使用するために、以下のように記述しており、
addressにはuser_idのカラムに数字が与えられます。
例) @address = @user.build_address

app/controllers/users/registrations_controller.rb
  def create_address
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(address_params)
    unless @address.valid?
      flash.now[:alert] = @address.errors.full_messages
      render :new_address and return
    end
    @user.build_address(@address.attributes)
    session["address"] = @address.attributes
    @creditcard = @user.build_creditcard
    render :new_credit_card
  end

  protected

  def address_params
    params.require(:address).permit(:address,:postal_code, :prefecture, :city,:apartment)
  end

controller編集(creditcard登録)

creditcardの登録に関しても、addressの記述でcreditcardのインスタンス変数は作成しているので、
createアクションのみ記述します。
ポイントは以下の通りです。
- バリデーションチェック
- バリデーションチェックが完了した情報と、sessionで保持していた情報とあわせ、ユーザー情報として保存すること
ログインをすること
- sessionを削除すること

app/controllers/users/registrations_controller.rb
  def create_creditcard
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(session["address"])
    @creditcard = Creditcard.new(creditcard_params)
    unless @creditcard.valid?
      flash.now[:alert] = @creditcard.errors.full_messages
      render :new_credit_card and return
    end
    @user.build_address(@address.attributes)
    @user.build_creditcard(@creditcard.attributes)
    @user.save
    sign_in(:user, @user)
  end

routeの編集

view、controllerの準備ができましたので、routeの編集をします。
この編集は2つのポイントがあります。
1.devise userモデルにrouteを設定
2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする

1.devise userモデルにrouteを設定

devise管理下のusersコントローラを作成がしましたが、記述を変更しないと、現状deviseのコントローラーが呼ばれてしまっています。
そこで、以下のように記述をして、ルーティングを設定しましょう。

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

2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする

今回複数モデル(addressとcreditcard)を使用しているため、devise_scopeを使用し、各モデルのルートを設定しましょう。

route.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    }
  devise_scope :user do
    get 'addresses', to: 'users/registrations#new_address'
    post 'addresses', to: 'users/registrations#create_address'
    get 'creditcards', to: 'users/registrations#new_creditcard'
    post 'creditcards', to: 'users/registrations#create_creditcard'
  end
end

これで今回使用するルーティングの設定が完了しました。

以上でdeviseを用いたウィザード形式でユーザー新規登録機能が実装されております。

最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

https://docs.ruby-lang.org/ja/2.5.0/doc/spec=2fcall.html#super

devise_parameter_sanitizerメソッドとはなにか
https://aliceblog1616.com/devise_parameter_sanitizer%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%81%AF%EF%BC%9F/

rails devise完全入門!結局deviseって何ができるの?
https://www.sejuku.net/blog/13378

buildメソッドについて
https://qiita.com/tsuchinoko_run/items/d671ea840bc0bfa90186

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

実務未経験者がUdemyの講座を参考にして、RailsでInstagram風アプリを作ったので感想をまとめます

現在22歳の大学生です。就職先としてweb形自社開発企業を目指し、ポートフォリオ作成と就活を頑張っています。

今回はUdemyの講座を参考にInstagram風アプリを作成したので、感想などを書いていきます。

参考にした教材

今回はUdemyにある「Beautiful Ruby on Rails Apps in 30 Days & TDD - Immersive」を使用しました。

こちらの教材はInstagram、Evernote、Tumblrなどの有名アプリを含めた、8つのアプリの作り方をRuby on Railsで学ぶという盛りだくさんの内容です。

しかもこの教材、無料です。ただその代わり、教材の内容は2014年ぐらいと少し古いです。
あと解説は英語で日本語字幕などはありません。まあ内容は難しくないので大丈夫です。

難易度はProgate以上、Railsチュートリアル未満

Railsチュートリアルができるなら、この教材は難しくありません。ただhamlやscss、新しいgemなどの使い方は覚える必要があります。

逆にProgateレベルの一般的な知識がないと、この教材は難しいと思います。理由は説明が少ないからです。
講座内で解説はありますが、それでも基本を理解していないと追いつけません。

作った成果物

Screen Shot 2020-01-26 at 17.21.42.png

講座内で配布されているCSSを使っていないので、UIが貧弱です。

ちなみに講座内で配布されているCSSを使うとこんな感じになるようです。配布場所はここ。

Screen Shot 2020-01-26 at 18.08.56.png

学んだこと

  • hamlとscssの使い方
  • gemのsimple_formでformが簡単に作れる。
  • gemのdeviseでログイン認証が簡単にできる。
  • gemのpaperclipで画像の扱いが簡単にできる。
  • gemのact_as_votableでlike機能が簡単に実装できる。

不満なこと

  • gemを用いて簡単にアプリができてしまったので、学べた感じがしない。
  • レベルが高い内容ではない。

まとめ

Instagram風アプリが作りたかったので受講しましたが、そこまでレベルの高い内容ではありませんでした。
ですが無料の教材ということを踏まえれば、内容は充実していたと思います。

Instagram風のアプリを作成してみたい方は利用してみてはいかがでしょうか?

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

rails newする際のエラー(can't find gem railties)の解決法

新しいアプリを作成する際のrails ○○○○ new で困ったので解決法を共有します。

rails ○.○.○.○ new app名 ○.○.○.○ はrails -vした際のバージョンと同じにする必要があります。

私の場合ですと、rails -vをするとRails 6.0.2.1というバージョンが出てくるので、

rails _6.0.2.1_ new app名
としてあげると新しいアプリとファイルが作成されます。

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

超初心者がUbuntu 18.04 にRailsをインストールする&vue.jsのインストール&脱webpacker

はじめに

UbuntuにRailsをインストールする。これに関しては他に記事がアップされているので、基本的にはそちらを閲覧したほうが良いかと。

この記事は上記の記事で何をやっているのかわからない僕がインストールの手順を理解するために調べたものを残す自分用メモです。
特に、ここではUbuntu18.04にRails6をwebpackerを使わずインストールすることを目的としています。

前提

僕のスペック

  • プログラミング学習を始めて1ヶ月
  • HTML/CSS/Javascript/Ruby/Railsをprogateで一応学習済
  • Ruby on rails でポートフォリオを作成中

開発環境

  • 端末 : LENOVO ideapad 530S-14ARR
  • OS : Ubuntu 18.04
  • シェル : bash 4.4.20
  • git : 2.9.0
  • ruby : 2.7.0
  • rails : 6.0.2

railsインストールまでの道のり

rbenvのインストール

まずはrubyのバージョンを管理してくれるrbenvをインストール
そのためにはgitとcurlが必要。それをインストールしていない人は以下のコマンドで。

sudo apt install git
sudo apt install curl

curlはパッケージをインストールするためのパッケージ。
gitについては後で説明するが、githubとやり取りするときに使うためのパッケージ。

そして、rbenvのインストール。以下のコマンドをターミナルに入力。

# githubからrbenvをインストール
$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
$ cp .bashrc ~/.bashrc.back

# pathを通す。
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> .bashrc
$ echo 'eval "$(rbenv init -)"' >> .bashrc
$ source .bashrc

# rubyenv が正しくインストールされたか確認
$ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bash

使っているコマンドを説明していく。

git clone

githubという賢い人たちが書いたコードが集められた場所(適当)からrbenvを自分のローカル環境にコピーする。ありがとう賢い人!引数には欲しいフォルダのURL、保存する場所の順番に記す。

cp bashrc...

.bashrcは重要な設定ファイル。これを書き換える必要がある。もし変な操作をしてしまったら怖いから。バックアップを取るよ。cpはコピーするコマンド。第一引数にコピーしたいファイル。第二引数にペースト先を指定。

echo export ...>> .bashrc

.bashrcという重要な設定ファイルがある。シェル起動時に自動的に実行されるファイルなのだが、こいつの内容を書き換えている。具体的には先ほどインストールしたrbenvをコマンドとして使えるようにするために、rbenvの実行ファイルが置いてある場所を教えてあげている。教え方は$PATHという変数(環境変数という)にrbenvの居場所を付け加えて更新している。この環境変数を書き換えるコマンドがexportです。

curl -fsSL ... | bash

curlはパッケージをインストールするファイル。ここでインストールしているのはrbenvが正しくインストールされたかを検証するファイル。-fsSLcurlする時のオプションを決めれるよ。詳しくは$ man curlと入力すると、マニュアルがあるからそこで見れます!|はパイプと言います。この棒の左側のコードの結果を右側のコードの引数として与えます。この場合はcurlでインストールしたファイルを実行しています。(bashはコードを実行します!というコマンド)

ruby-buildをインストール

$ mkdir .rbenv/plugins
$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
$ echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
$ sudo ~/.rbenv/plugins/ruby-build/install.sh
$ source .bashrc

#ruby-build を実行するのに必要なパッケージをインストール
$ sudo apt install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm5 libgdbm-dev

いよいよrubyをインストール

$ rbenv install 2.7.0
$ rbenv global 2.7.0
$ ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]

インストールされたgemを更新

gem update --system
gem install bundler

ちなみに、gem update --systemgem installの違いは以下のリンクが教えてくれる。
「gem update --system」と「gem update」の違い

SQlite3のインストール

以下のコマンドでSQlite3のインストールを行う。
libsqlite3-devは

sudo apt install sqlite3 libsqlite3-dev

railsのインストール

$ gem install rails  
$ gem list rails

node.js をインストール

以下の記事に沿ってインストールを行いました。
Ubuntuに最新のNode.jsを難なくインストールする
記事に沿って以下のコマンドを打ち込む。

$ sudo apt install -y nodejs npm
$ sudo npm install n -g
$ sudo n stable
$ sudo apt purge -y nodejs npm
$ exec $SHELL -l
$ node -v

yarn インストール

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

rails new tweet_app

Gemfileからgem webpackerを削除して以下を実行。

$ bundle install
$ yarn remove @rails/webpacker
$ yarn remove webpack-dev-server

参考:Rails環境でJS , CSSをwebpackで完全に管理する

以下のファイルを削除

  • bin/webpack
  • bin/webpack-dev-server
  • config/webpack/development.js
  • config/webpack/environment.js
  • config/webpack/loaders/vue.js
  • config/webpack/production.js
  • config/webpack/test.js
  • config/webpacker.yml
  • config/environment/development.rb
  • production.rb/config.webpacker.check_yarn_integrity

vue のインストール

#yarn init で package.json にversionやlisenceを追加
$ yarn init

# vueをインストール
$ yarn add vue --save

webpack のインストール

$ yarn add webpack webpack-cli -D
$ yarn add @babel/core @babel/polyfill @babel/preset-env babel-loader -D
$ yarn add css-loader file-loader mini-css-extract-plugin pug pug-plain-loader sass-loader vue-loader vue-style-loader vue-template-compiler webpack-manifest-plugin -D

これでwebpackをインストール完了!
あとは、webpack.config.jsを設定すれば普通に使えるはずだ!
webpack.config.jsとは?という方は↓が参考になります。
webpack4入門

おまけ

webpackインストール時に以下の警告が出て、これを消したい人向けの話

warning @babel/polyfill > core-js@2.6.11: core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core-js@3.

warning pug > pug-code-gen > constantinople > babel-types > babel-runtime > core-js@2.6.11: core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core-js@3.

両方共言っていることは同じ。core-jsをアップグレードしてくれとのこと。どうやら今使っているのはメンテナンス対象外とのことだ。
core-jsってなんだ?

core-js?

core-jsはポリフィルのライブラリだ。
ポリフィルとは

JavaScriptはバージョンが更新されるたびに新しい構文や仕様が追加されるため、ブラウザによって対応状況が大きく変わってしまいます。
そこで、利用したい機能に対応していないブラウザでも使えるように、同等の機能をJavaScriptで自ら供給するというのがポリフィルの基本的な考え方です。一般的には、JavaScriptライブラリのようにさまざまなポリフィルが公開されています。

引用元:JavaScriptのブラウザごとの互換性をポリフィル・Babelで解決する!

ちなみにBabalとPolyfillとの違いは多分以下の通り。

  • Babel トランスパイルするやつ
  • Polyfill ないものを保管するやつ

このPlyfillの仲間にcore-jsがいるようで。こいつの役割は以下の通りみたい。

ファイルを読み込むだけでECMAScript 5th Edition〜の機能(およびProposals)が補完されます。

引用元:そのコード、開発環境と異なるブラウザでも動作しますか?

core-js のupgradeを試みる。

yarn upgrade

package.jsonに記述されている範囲内での最新パッケージを取ってくる。
yarn upgadeに関しては以下のサイトが詳しく書いてある。

参考:yarn upgradeのあれこれ

でもcore-jsのバージョンは変わらなかった。
なぜならcore-jsはそれ単体でインストールされているわけではない。Babelやpugの一つとしてインストールされているからその親であるBabelやpugの指定するバージョン以上には上げられないのであろう。そのため、以下のコマンドでcore-js単体をインストールした。

yarn add core-js -D

これでcore-jsは最新版である core-js@3.6.4 にアップグレードされた。

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

Railsのルーティングを支える技術 - Journeyについて

Journeyは、Railsのルーティング処理を担当するライブラリです。Rails3で導入され、大規模なアプリケーションでの処理が高速化しました。

今回は、Journeyがどのようにしてルーティング処理を行っているかを紹介します。

TL;DR

  • Railsのルーターは、ウェブから受け取ったURL(入力URL)を認識し、ルート定義と見比べマッチする処理が見つかればそれを呼び出す
  • 入力URLに対応する処理を高速に探すためのライブラリがJourneyである
  • Journeyはルート定義を非決定性有限オートマトン(NFA)に変換し、マッチングを行う
  • Journeyは以下ステップを踏み、ルート定義をNFAに変換している
    1. ルート定義URLを字句解析し、トークンに分解する
    2. トークンから抽象構文木(AST)を構築する
    3. ASTからNFAに変換する
  • Journeyは以下ステップを踏み、入力URLに対するマッチング処理を行っている
    1. 入力として与えられたURLを字句解析し、NFA入力形式に分解する
    2. 入力をもとにNFAのシミュレーションをする
    3. NFAが受理状態となった場合、対応する処理を呼び出す
  • NFAでのマッチングを行うことで、高速なルーティングを実現している

Journeyを理解するために必要な基本知識

Journeyの内部実装を説明するまえに、まずは理解が必要な項目の解説を行います。

  • Railsのルート定義について
  • 有限オートマトンとはなにか

Railsのルート定義

Railsでは、以下文法でルーティングの定義を行います。

get "/articles(.:format)",          to: "articles#index"
get "/articles/new(.:format)",      to: "articles#new"
get "/articles/:id/edit(.:format)", to: "articles#edit"
get "/articles/:id(.:format)",      to: "articles#show"

これは例えばGETリクエストで /articles/12 が入力URLとして与えられた場合、処理articles#showを呼び出します。

:(コロン)で始まる項目はパラメーターで、URLで利用できる英数字記号ならなんでもよいです。
()で始まる部分は任意の項目です。例えば、/articles/12.jsonid=12format=json と認識され、articles#show にマッチします。

入力URLが複数定義にマッチした場合は、先に定義した処理が呼び出されます。
例えば、入力URL/articles/newarticles#newarticles#show にマッチしますが、先に定義をしているnewが呼び出されます。

有限オートマトン

Journeyのルートマッチング処理を理解するためには、オートマトンについて理解をしておく必要があります。

オートマトンは、計算理論で使われるモデルのことです。
今回は有限オートマトンと呼ばれる領域に絞って説明します。有限オートマトンと聞くとなんだか複雑そうですが、いたってシンプルなモデルです。

有限オートマトンは現在の状態と、入力が与えられた時にどの状態へ遷移するかの規則をもっています。
初期状態から入力値に従って状態遷移し、次の状態に進みます。
すべての入力が終わり、最終的な状態が受理状態であったら、受理したという結果を返します。

以下は、シンプルなオートマトンを図にしたものです。

automaton01.png

この有限オートマトンは、入力値は0,1いずれかを取ります。q0は初期状態で、q0,q1,q2は各種状態を表しています。二重丸q2は、受理状態を表しています。

このオートマトンに入力1を与えると、状態はq0からq1に遷移します。さらに入力1を与えると、状態はq1からq2に遷移します。その後入力がなければ、受理された状態となります。二重丸q2まで遷移しなかったものは、受理されず拒否状態となります。

上記オートマトンに以下の入力を与えた場合、受理状態となります。

11
011
000000011
110
111

以下入力では拒否となります。

000
001
0010
00100

有限オートマトンには、決定性と非決定性のものがあります。
上記のオートマトンは、入力に対して遷移先が一意に決まるため、決定性有限オートマトン(DFA; Deterministic Finite Automaton) と呼びます。

DFAに対し、入力に対して遷移先が一意に決まらないものは、非決定性有限オートマトン(NFA; Nondeterministic Finite Automaton) と呼びます。
例えば、以下はNFAの例です。

automaton02.png

このオートマトンに入力1を与えると、状態はq1とq2に同時に遷移します。
NFAの場合、現在の状態を複数取ります。
さらに入力1を与えると、q1,q4へ遷移します。その後入力がなければ、q4は受理するので受理状態となります。

上記オートマトンに以下入力を与えた場合は、受理状態となります。

110      ---> q3
111      ---> q4
0101   ---> q4
01000010 ---> q3

以下入力では拒否となります。

0000
0001

ここから先は完全に余談ですが、NFAとDFAは互いに同じモデルを表現できます(等価性)。そして任意のNFAはDFAに変換することができます。
NFAは遷移中の状態を複数持たないといけないので、遷移の処理効率が悪くなる可能性があります。
正規表現のエンジンは有限オートマトンを使ってマッチング処理をするものがありますが、それらエンジンではNFAはDFAに変換してから処理をすることがあります。
またDFAの場合、状態を最小化するアルゴリズムも存在します。
ただし、NFA/DFA変換も万能かといえばそうではなくて、DFAにすることで状態が膨大になってしまう可能性もあります。
このあたりの話は割愛します。興味がある方はぜひ調べてみてください。

Journeyのルートマッチング処理の流れ

Journeyのルートマッチング処理は非決定性オートマトン(NFA) を用いて行います。
例えば、以下ルート定義を考えます。

get "/articles/new(.:format)",      to: "articles#new"

このURLの定義を、以下6つのトークンに分解(字句解析)します。

/
articles
/
new
.
正規表現[^./?]+ (これは . または / または ? 以外の1文字以上の繰り返しを意味する。?-mix: rubyの正規表現オプションなので一旦無視してください)

トークンに分解後、各種状態のノードとエッジとしNFAを作ります。図に表すと以下状態になります。
URLがマッチした場合(受理状態)は二重丸で表しています。

automaton03.png

このNFAに対して、例えば以下入力を与えた場合は受理となります。
受理した場合は articles#newにマッチしたことになります。

/          (状態が0から1に遷移)
articles   (状態が1から2に遷移)
/          (状態が2から3に遷移)
new        (状態が3から4に遷移、入力はこれ以上ないので受理)
/          (状態が0から1に遷移)
articles   (状態が1から2に遷移)
/          (状態が2から3に遷移)
new        (状態が3から4に遷移)
.          (状態が4から5に遷移)
json       (状態が5から6に遷移、入力はこれ以上ないので受理)

もう少し複雑なルート定義を見てみます。
例えば以下のような定義があるとします。

get "/articles(.:format)",          to: "articles#index"
get "/articles/new(.:format)",      to: "articles#new"
get "/articles/:id/edit(.:format)", to: "articles#edit"
get "/articles/:id(.:format)",      to: "articles#show"

この定義をNFAで表すと以下となります。

journey_route01.png

このNFAに対し、以下入力を与えると、2つの受理状態となります。

/           (状態が0から1に遷移)
articles    (状態が1から2に遷移)
/           (状態が2から4に遷移)
new         (状態が4から6に遷移し受理 かつ 4から7に遷移し受理)

状態6で受理した場合はarticles#newを、状態7で受理した場合はarticles#show でマッチしたことを表します。
2つの受理状態となった場合は、先に定義したほうを勝ちとします。

JourneyのNFAシミュレーターが http://tenderlove.github.io/fsmjs/ にあります。
入力した文字列に対し、受理したかどうかをシミュレーションできます。このシミュレーターで色々な入力パターンを試すと理解が深まります。

Journeyの内部処理を覗く

JourneyがNFAを用いてマッチング処理を行っていることがわかりました。
次はJourneyが実際に行っている内部処理を覗いてみます。

Journeyは以下ステップを踏み、ルート定義をNFAに変換しています。

  1. ルート定義URLを字句解析し、トークンに分解する
  2. トークンから抽象構文木(AST)を構築する
  3. ASTからNFAに変換する

また、マッチング処理をする際は、以下ステップを踏みます。

  1. 入力として与えられたURLを字句解析し、NFA入力形式に分解する
  2. 入力をもとにNFAのシミュレーションをする
  3. NFAが受理状態となった場合、対応する処理を呼び出す

ルート定義URLを字句解析し、トークンに分解する

ルート定義 /articles/new(.:format) は、以下6つのトークンに分解(字句解析)します。

/
articles
/
new
.
正規表現[^./?]+ (これは . または / または ? 以外の1文字以上の繰り返しを意味する)

入力をトークンに分解するのは、スキャナが担当しています。
Journeyの場合、Scannerの定義は https://github.com/rails/rails/blob/v6.0.2/actionpack/lib/action_dispatch/journey/scanner.rb にあります。

これは単純で、入力に対して以下のようなトークンに分解します。

pry(main)> scanner = ActionDispatch::Journey::Scanner.new
=> #<ActionDispatch::Journey::Scanner:0x00007ff1ce901498 @ss=nil>

pry(main)> scanner.scan_setup("/articles/:id(.:format)")
=> #<StringScanner 0/23 @ "/arti...">

pry(main)> scanner.next_token
=> [:SLASH, "/"]

pry(main)> scanner.next_token
=> [:LITERAL, "articles"]

pry(main)> scanner.next_token
=> [:SLASH, "/"]

pry(main)> scanner.next_token
=> [:SYMBOL, ":id"]

pry(main)> scanner.next_token
=> [:LPAREN, "("]

pry(main)> scanner.next_token
=> [:DOT, "."]

pry(main)> scanner.next_token
=> [:SYMBOL, ":format"]

pry(main)> scanner.next_token
=> [:RPAREN, ")"]

pry(main)> scanner.next_token
nil

定義 /articles/:id(.:format) は、以下トークンとなりました。

[:SLASH, "/"]
[:LITERAL, "articles"]
[:SLASH, "/"]
[:SYMBOL, ":id"]
[:LPAREN, "("]
[:DOT, "."]
[:SYMBOL, ":format"]
[:RPAREN, ")"]

トークンから抽象構文木(AST)を構築する

NFAって、よく見ると木構造みたいなものですよね。
次はNFAに変換する前処理として、トークンを一度木構造に変換します。
この処理はパーサーが担当します。

パーサーの定義は https://github.com/rails/rails/blob/v6.0.2/actionpack/lib/action_dispatch/journey/parser.y にあります。
このパーサーは、与えられたルート定義をスキャナーを使いトークンに分解後、ASTを組み立てます。
このパーサーはRacc(yaccのruby版)を使って作られています。

Racc(yaccの定義)について、簡単に説明します。
例えば以下定義があるとします。

expressions : expression expressions  | expression;
expression  : terminal;
terminal    : symbol | dot;
symbol      : SYMBOL;
dot         : DOT;

Raccは:で区切った右辺と左辺でみます。例えば、expressionsは expression expressions または expression で構成されるという定義です。
expressionは、さらにterminal であると定義されます。
terminalsymbol または dot でできていて、symbolはSYMBOL、dotはDOTであると定義されます。大文字で表した定義が終端となります。

Raccはさらに、処理にマッチしたときのアクションを定義できます。右辺の一番右にブロックを書くことでアクションを定義できます。
例えば、以下定義でDOTという定義をすると、DOTにマッチしたらDotのインスタンスにする(マッチした定義はvalで取れる)ということが可能です。

dot         : DOT       { Dot.new(val[0]) }

JourneyのRacc定義を見てみましょう。以下のようになっています。

  expressions
    : expression expressions  { Cat.new(val.first, val.last) }
    | expression              { val.first }
    | or
    ;
  expression
    : terminal
    | group
    | star
    ;
  group
    : LPAREN expressions RPAREN { Group.new(val[1]) }
    ;
  or
    : expression OR expression { Or.new([val.first, val.last]) }
    | expression OR or { Or.new([val.first, val.last]) }
    ;
  star
    : STAR       { Star.new(Symbol.new(val.last)) }
    ;
  terminal
    : symbol
    | literal
    | slash
    | dot
    ;
  slash
    : SLASH              { Slash.new(val.first) }
    ;
  symbol
    : SYMBOL             { Symbol.new(val.first) }
    ;
  literal
    : LITERAL            { Literal.new(val.first) }
    ;
  dot
    : DOT                { Dot.new(val.first) }
    ;

DOTノードがでてきたらDotインスタンスにする、SymbolインスタンスがでてきたらSymbolにする。
expressionsにヒットしたらCatノードにして、ヒットしたexpression(すなわちval[0])とexpressions(すなわちval[1])をCatノードの初期値として渡すといった意味になります。

このあたりのRaccの使い方について、詳しくは http://i.loveruby.net/ja/projects/racc/doc/usage.html を参照ください。

/articles/:id(.:format) という定義をパーサーに通してみます。パーサーは内部的にトークンに変換後、以下ASTを構築します。

pry(main)> parser = ActionDispatch::Journey::Parser.new
=> #<ActionDispatch::Journey::Parser:0x00007ff1c7f32698 @scanner=#<ActionDispatch::Journey::Scanner:0x00007ff1c7f32670 @ss=nil>>

pry(main)> parser.parse("/articles/:id(.:format)")

=> #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7f0bbb0
 @left=#<ActionDispatch::Journey::Nodes::Slash:0x00007ff1c7f101d8 @left="/", @memo=nil>,
 @memo=nil,
 @right=
  #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7f0bc00
   @left=#<ActionDispatch::Journey::Nodes::Literal:0x00007ff1c7f10138 @left="articles", @memo=nil>,
   @memo=nil,
   @right=
    #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7f0bc50
     @left=#<ActionDispatch::Journey::Nodes::Slash:0x00007ff1c7f100c0 @left="/", @memo=nil>,
     @memo=nil,
     @right=
      #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7f0bca0
       @left=#<ActionDispatch::Journey::Nodes::Symbol:0x00007ff1c7f10020 @left=":id", @memo=nil, @name="id", @regexp=/[^\.\/\?]+/>,
       @memo=nil,
       @right=
        #<ActionDispatch::Journey::Nodes::Group:0x00007ff1c7f0bd40
         @left=#<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7f0bd90 @left=#<ActionDispatch::Journey::Nodes::Dot:0x00007ff1c7f0bef8 @left=".", @memo=nil>, @memo=nil, @right=#<ActionDispatch::Journey::Nodes::Symbol:0x00007ff1c7f0be58 @left=":format", @memo=nil, @name="format", @regexp=/[^\.\/\?]+/>>,
         @memo=nil>>>>>

これを図に表すと、以下となります。

ast.png

ASTからNFAに変換する

最後にルート定義をASTに変換したものから、NFAの状態遷移表を作成します。
これは GTG::Builder( https://github.com/rails/rails/blob/6-0-stable/actionpack/lib/action_dispatch/journey/gtg/builder.rb )が処理を担当します。

pry(main)> ast = parser.parse("/articles/:id(.:format)")

=> #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7a42fc0
 @left=#<ActionDispatch::Journey::Nodes::Slash:0x00007ff1c9974a88 @left="/", @memo=nil>,
 @memo=nil,
 @right=
  #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7a43150
   @left=#<ActionDispatch::Journey::Nodes::Literal:0x00007ff1c7a43fb0 @left="articles", @memo=nil>,
   @memo=nil,
   @right=
    #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7a432b8
     @left=#<ActionDispatch::Journey::Nodes::Slash:0x00007ff1c7a43dd0 @left="/", @memo=nil>,
     @memo=nil,
     @right=
      #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7a43308
       @left=#<ActionDispatch::Journey::Nodes::Symbol:0x00007ff1c7a43a38 @left=":id", @memo=nil, @name="id", @regexp=/[^\.\/\?]+/>,
       @memo=nil,
       @right=
        #<ActionDispatch::Journey::Nodes::Group:0x00007ff1c7a43510
         @left=#<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7a43560 @left=#<ActionDispatch::Journey::Nodes::Dot:0x00007ff1c7a438a8 @left=".", @memo=nil>, @memo=nil, @right=#<ActionDispatch::Journey::Nodes::Symbol:0x00007ff1c7a436c8 @left=":format", @memo=nil, @name="format", @regexp=/[^\.\/\?]+/>>,
         @memo=nil>>>>>


ActionDispatch::Journey::GTG::Builder.new(ast).transition_table
=> #<ActionDispatch::Journey::GTG::TransitionTable:0x00007ff1c9d07cb8 
  @accepting={4=>true, 6=>true}, 
  @memos={4=>[nil], 6=>[nil]}, 
  @regexp_states={3=>{/[^\.\/\?]+/=>4}, 5=>{/[^\.\/\?]+/=>6}}, 
  @string_states={0=>{"/"=>1}, 1=>{"articles"=>2}, 2=>{"/"=>3}, 4=>{"."=>5}}
>

acceptingは受理状態を表しています。この場合、状態4または6に到達すれば受け入れとなります。

@accepting={4=>true, 6=>true}, 

状態遷移はstring_statesregexp_statesで表しています。
例えば、 {0=>{"/"=>1} は 状態0から入力/が来たら1に遷移せよ、ということを表しています。
memosは受理した場合の処理を入れておく変数です。とりあえず今は何も使わないのでnilのままにしておきます。

これでNFAが完成しました。図に表すと以下となります。

nfa_result.png

ルートの定義が複数ある場合はどうしたらよいでしょうか。これは、作成したASTをORノードの子とすることで表現できます。

pry(main)> ast_1 = parser.parse("/articles/:id(.:format)")
=>  省略

pry(main)> ast_2 = parser.parse("/articles/new(.:format)")
=>  省略

pry(main)> root = ActionDispatch::Journey::Nodes::Or.new([ast_1, ast_2])
=>  省略

pry(main)> ActionDispatch::Journey::GTG::Builder.new(root).transition_table
=> #<ActionDispatch::Journey::GTG::TransitionTable:0x00007ff1c8f4e4f0 

@accepting={4=>true, 5=>true, 8=>true, 9=>true}, 
@memos={4=>[nil], 5=>[nil], 8=>[nil], 9=>[nil]}, 
@regexp_states={3=>{/[^\.\/\?]+/=>4}, 6=>{/[^\.\/\?]+/=>8}, 7=>{/[^\.\/\?]+/=>9}}, 
@string_states={0=>{"/"=>1}, 1=>{"articles"=>2}, 2=>{"/"=>3}, 3=>{"new"=>5}, 4=>{"."=>6}, 5=>{"."=>7}}
>

上記NFAは図に表すと以下となります。

or_nfa.png

NFAシミュレーション - 入力URLにマッチする処理を求める

NFAが作れました。あとは入力URLをもとにNFAのシミュレーションをして、受理状態になったら対応する処理を呼び出すと、ルートのマッチング処理は完了です。

シミュレーションの処理は https://github.com/rails/rails/blob/6-0-stable/actionpack/lib/action_dispatch/journey/nfa/simulator.rb#L23 となります。
これは以下ステップを踏み、ルート定義に対応する処理を見つけます。

  1. 入力として与えられたURLを字句解析し、NFA入力形式に分解する
  2. 入力をもとにNFAのシミュレーションをする
  3. NFAが受理状態となった場合、対応する処理を呼び出す

NFA::Simulatorは、先程作成した状態遷移表(TransitionTable)を使い、シミュレーションを行います。

pry(main)> parser = ActionDispatch::Journey::Parser.new

=> #<ActionDispatch::Journey::Parser:0x00007ff1c7fa3578 @scanner=#<ActionDispatch::Journey::Scanner:0x00007ff1c7fa3550 @ss=nil>>

pry(main)> ast_1 = parser.parse("/articles/:id(.:format)")

=> #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7f78418
 @left=#<ActionDispatch::Journey::Nodes::Slash:0x00007ff1c7f78a08 @left="/", @memo=nil>,
 @memo=nil,
 @right=
  #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7f78468
   @left=#<ActionDispatch::Journey::Nodes::Literal:0x00007ff1c7f78968 @left="articles", @memo=nil>,
   @memo=nil,
   @right=
    #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7f784b8
     @left=#<ActionDispatch::Journey::Nodes::Slash:0x00007ff1c7f788f0 @left="/", @memo=nil>,
     @memo=nil,
     @right=
      #<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7f78508
       @left=#<ActionDispatch::Journey::Nodes::Symbol:0x00007ff1c7f78850 @left=":id", @memo=nil, @name="id", @regexp=/[^\.\/\?]+/>,
       @memo=nil,
       @right=
        #<ActionDispatch::Journey::Nodes::Group:0x00007ff1c7f785a8
         @left=#<ActionDispatch::Journey::Nodes::Cat:0x00007ff1c7f785f8 @left=#<ActionDispatch::Journey::Nodes::Dot:0x00007ff1c7f78760 @left=".", @memo=nil>, @memo=nil, @right=#<ActionDispatch::Journey::Nodes::Symbol:0x00007ff1c7f786c0 @left=":format", @memo=nil, @name="format", @regexp=/[^\.\/\?]+/>>,
         @memo=nil>>>>>

pry(main)> table = ActionDispatch::Journey::GTG::Builder.new(ast_1).transition_table

=> #<ActionDispatch::Journey::GTG::TransitionTable:0x00007ff1c7f42ed0 
@accepting={4=>true, 6=>true}, 
@memos={4=>[nil], 6=>[nil]}, 
@regexp_states={3=>{/[^\.\/\?]+/=>4}, 5=>{/[^\.\/\?]+/=>6}}, 
@string_states={0=>{"/"=>1}, 1=>{"articles"=>2}, 2=>{"/"=>3}, 4=>{"."=>5}}
>

pry(main)> table.memos[4] = "State4 Match"
=> "State4 Match"
# これはstate4で受け入れた場合のロジックをいれます。

pry(main)> table.memos[6] = "State6 Match"
=> "State6 Match"
# これはstate6で受け入れた場合のロジックをいれます。

[35] pry(main)> simulator = ActionDispatch::Journey::NFA::Simulator.new(table)

=> #<ActionDispatch::Journey::NFA::Simulator:0x00007ff1c8593848 @tt=#<ActionDispatch::Journey::GTG::TransitionTable:0x00007ff1c7f42ed0 @accepting={4=>true, 6=>true}, @memos={4=>"State4 Match", 6=>"State6 Match"}, @regexp_states={3=>{/[^\.\/\?]+/=>4}, 5=>{/[^\.\/\?]+/=>6}}, @string_states={0=>{"/"=>1}, 1=>{"articles"=>2}, 2=>{"/"=>3}, 4=>{"."=>5}}>>

[36] pry(main)> simulator.simulate("/articles/3")
=> #<ActionDispatch::Journey::NFA::MatchData:0x00007ff1c8edb7e8 @memos=["State4 Match"]>

[37] pry(main)> simulator.simulate("/articles/3.json")
=> #<ActionDispatch::Journey::NFA::MatchData:0x00007ff1c8eafe68 @memos=["State6 Match"]>

[38] pry(main)> simulator.simulate("/articles/3/hello")
=> nil

/articles/:id(.:format) の定義に対し、 入力 /articles/3/articles/3.json がマッチしていることがわかります。

おわりに

今回Journeyの内部実装を見てみました。一見難しそうに見えますが、NFAについて理解できれば何てことはないです。

私は学生時代、オートマトンの授業はあまり得意ではありませんでした。
今回Journeyの調査にあたり各種テキストを見ていたのですが、そういえばこんなこと勉強したなぁ...くらいでおぼろげな記憶しかありませんでした。
まさかRailsの内部でNFAにお目にかかるとは。CSの基礎知識は大事なんだなあと改めて感じた次第です。

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

【Rails】MySQL2がbundle installした時エラーになったので対処法を調べてみた

スクールの課題でGitからアプリをクローンし、
自身のPCで環境構築する際にbundle installしたら、エラーを出したので、対処法を調べて見ました。
以下、エラー時の状況と対処手順です。

エラー内容

gemのインストールをする為、bundle installを行ったところエラー発生

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /Users/motoike/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.4.10/ext/mysql2
/Users/motoike/.rbenv/versions/2.5.1/bin/ruby -r ./siteconf20200126-32549-89onao.rb extconf.rb
checking for rb_absint_size()... yes
checking for rb_absint_singlebit_p()... yes
checking for ruby/thread.h... yes
r ruby/thread.h... yes
checking for rb_thread_call_without_gvl() in ruby/thread.h... yes
checking for rb_thread_blocking_region()... no
checking for rb_wait_for_single_fd()... yes
checking for rb_hash_dup()... yes
checking for rb_intern3()... yes
checking for rb_big_cmp()... yes
-----
Using mysql_config at /usr/local/opt/mysql@5.6/bin/mysql_config
-----
checking for mysql.h... yes
checking for errmsg.h... yes
checking for SSL_MODE_DISABLED in mysql.h... no
checking for MYSQL_OPT_SSL_ENFORCE in mysql.h... no
checking for MYSQL.net.vio in mysql.h... yes
checking for MYSQL.net.pvio in mysql.h... no
checking for MYSQL_ENABLE_CLEARTEXT_PLUGIN in mysql.h... yes
-----
Don't know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load
-----
-----
Setting libpath to /usr/local/opt/mysql@5.6/lib
-----
creating Makefile

current directory: /Users/motoike/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.4.10/ext/mysql2
make "DESTDIR=" clean

current directory: /Users/motoike/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.4.10/ext/mysql2
make "DESTDIR="
compiling client.c
compiling infile.c
compiling mysql2_ext.c
compiling result.c
result.c:326:40: warning: incompatible pointer types assigning to 'my_bool *' (aka 'char *') from 'bool *' [-Wincompatible-pointer-types]
    wrapper->result_buffers[i].is_null = &wrapper->is_null[i];
                                       ^ ~~~~~~~~~~~~~~~~~~~~
result.c:328:40: warning: incompatible pointer types assigning to 'my_bool *' (aka 'char *') from 'bool *' [-Wincompatible-pointer-types]
    wrapper->result_buffers[i].error   = &wrapper->error[i];
                                       ^ ~~~~~~~~~~~~~~~~~~
2 warnings generated.
compiling statement.c
linking shared-object mysql2/mysql2.bundle
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1

make failed, exit code 2

Gem files will remain installed in /Users/motoike/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.4.10 for inspection.
Results logged to /Users/motoike/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/extensions/x86_64-darwin-19/2.5.0/mysql2-0.4.10/gem_make.out

An error occurred while installing mysql2 (0.4.10), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.4.10' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  mysql2

解決する為の手順

ターミナルでbrew info opensslを実行します。

openssl@1.1 is keg-only, which means it was not symlinked into /usr/local,
because openssl/libressl is provided by macOS so don't link an incompatible version.

If you need to have openssl@1.1 first in your PATH run:
  echo 'export PATH="/usr/local/opt/openssl@1.1/bin:$PATH"' >> ~/.zshrc

For compilers to find openssl@1.1 you may need to set:
  export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"
  export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"

For pkg-config to find openssl@1.1 you may need to set:
  export PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig"

export LDFLAGSexport CPPFLAGSに着目します。

$ bundle config --local build.mysql2"--with-cppflags=-I/usr/local/opt/openssl@1.1/include"
Settings for `build.mysql2--with-cppflags=-I/usr/local/opt/openssl@1.1/include` in order of priority. The top value will be used

と実行してから、

$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"
You are replacing the current local value of build.mysql2, which is currently nil

と実行後、再度bundle installする事で、インストールが出来ました。

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

gem install ovirt-engine-sdk -v '4.2.4'がインストールできない

gem install ovirt-engine-sdk -v '4.2.4'をbundle install しようとするとエラーが発生する。

エラー内容

Building native extensions. This could take a while...
ERROR: Error installing ovirt-engine-sdk:
ERROR: Failed to build gem native extension.

currentdirectory:
/home/vagrant/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/ovirt-engine-sdk-4.2.4/ext/ovirtsdk4c
/home/vagrant/.rbenv/versions/2.4.0/bin/ruby -r ./siteconf20180522-11143-c35v2a.rb extconf.rb
checking for xml2-config... no
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers. Check the mkmf.log file for more details. You may
need configuration options.

Provided configuration options:
--with-opt-dir
--without-opt-dir
--with-opt-include
--without-opt-include=${opt-dir}/include
--with-opt-lib
--without-opt-lib=${opt-dir}/lib
--with-make-prog
--without-make-prog
--srcdir=.
--curdir
--ruby=/home/vagrant/.rbenv/versions/2.4.0/bin/$(RUBY_BASE_NAME)
--with-libxml2-config
--without-libxml2-config
--with-pkg-config
--without-pkg-config
extconf.rb:29:in `': The "libxml2" package isn't available. (RuntimeError)

To see why this extension failed to compile, please check the mkmf.log which can be found here:

/home/vagrant/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/extensions/x86_64-linux/2.4.0-static/ovirt-engine-sdk-4.2.4/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /home/vagrant/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/ovirt-engine-sdk-4.2.4 for inspection.
Results logged to /home/vagrant/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/extensions/x86_64-linux/2.4.0-static/ovirt-engine-sdk-4.2.4/gem_make.out

解決方法

①sudo yum --enablerepo=epel,remi,rpmforge install libxml2 libxml2-develをインストール

②sudo yum -y install curl をインストール
③sudo yum -y install libcurl libcurl-devel をインストールすると
gem install ovirt-engine-sdk -v '4.2.4'
が通使えるようになり、bundle installをすることができます。

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

[strong parameters][form_with]投稿にユーザー情報を表示する

概要

目的

プロゲート以外の方法(strong parameters)で投稿詳細に投稿主(ユーザー)情報を記載すること。
(ちなみにプロゲートはこんな感じ

実現したいこと

各action すること(ポイント)
new from_withを使って、投稿。
create strong parametersを使用して処理。
show 投稿にユーザー情報を引っ張ってくる。

このような流れで進めていきます。

完成イメージ

スクリーンショット 2020-01-25 15.47.36.png
画像ではユーザー名、アイコン記載しておりますが、
今回は分かりやすく理解してもらうためにユーザー名だけの表示します。

環境

Ruby 2.5.6
Rails 5.2.3

前提

  • userのデーブルがあり、「name」カラムがあること。
  • 投稿モデルは今回commentモデルであること。

- deviseを導入していること。(current_userを使用している為)

strong parametersとは?

一言でいうと
「Web画面上から内部へ流れてきた値を安全に受け取る仕組み」のことです。
もう少し噛み砕いて簡単に説明すると、
入力フォームで記述した情報(名前、メールアドレス等)を安全にバックエンドに渡す仕組みのことです。

今回そのストロングパラメーターを使用します。

参考URL
Rails初学者がつまずきやすい「ストロングパラメータの仕組み」
【Rails入門】params使い方まとめ

new(form_withを使って投稿)

ここでの目的はfrom_withを使って、投稿出来るようにします。

comments/new.html.erb
<%= form_with model: @comment, local: true  do |form| %>
 <%= form.text_area :content,class:"new_text" %>
<% end %>

このようにform_withを使用して、入力された情報をストロングパラメーターで受け取れるようにします。

controller(strong parametersを使用して処理)

app/controllers/comments_controller.rb
    def new
    @comment = Comment.new
    end

    def create
      @comment = Comment.create params.require(:comment).permit(:content).merge(user_id: current_user.id)

    end

    def show
      @comment = Comment.find(params[:id])
      @user = User.find_by(id: @comment.user_id)
    end

★解説★

ここでのポイントはcreateアクションの時に、
newで入力された特定の情報(今回はcontent)しか
情報は受け取りませんよ〜!っていう設定をしています。

★ポイント★

(1)ログインしているユーザーIDの読み取り

createアクションを実行した時に入力されたcontent以外にもその時に
ログインしているユーザーIDを受け取る設定しています。
current_userはストロングパラメーターでは適用対象外なので、
mergeメソッドを使用して受け取れるようにしました。

(2)ログインしているユーザーIDの受け取り

showアクションを実行した時に、
createアクションで読み取ったユーザーIDを紐付けています。

参考URL
【Ruby on Rails】ストロングパラメータって何なの?

show(投稿にユーザー情報を引っ張る)

app/views/comments/show.html.erb
<%= @user.name%> #名前を表示
<%=@comment.content%> #投稿内容を表示

これで投稿にユーザー情報を紐付けることが出来ました!

最後に

まだまだ勉強不足なところがありますので、
アップデートできた知識は追加で記載していきます。

もし、何か修正点とかございましたらコメント等
恐縮ですが、宜しくおねがいします。

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

[Rails]多対多のモデルの実装、コントローラー、ビューの書き方

はじめに

Railsで多対多のモデルを作り、コントローラー・ビューを実装しました。過去にもやったことがあったので、できるかなーと思ったら、かなり迷ってしまったので、手順をメモしておきます。

データベース設計

今回のテーブルはこんな感じ。作品(works)は複数のカテゴリー(categories)をもち、それぞれのカテゴリーにも複数の作品が属しています。

worksテーブル

column type
id integer
name string
description string
... ...

categoriesテーブル

column type
id integer
name string

work_categoriesテーブル(中間テーブル)

column type
id integer
category_id integer
work_id integer

ところで、現在、特に中間テーブルに命名規則はないようなのですが(※)。2つのテーブル名を連結した名前をつける場合には、アルファベット順につけるのが慣習のようです。

私は知らずにwork_categoryテーブルという名前にしてしまったので、以下の記述でもそのまま書かせていただきます。。。

has_and_belongs_to_manyを使っていた頃には命名規則があったみたいです。今は、極力意味のあるテーブル名にするのがルールということです。

migrationファイルの記述

中間テーブルのmigrationファイルを作るときには若干ポイントがありました

$ rails g model work_category
migrationファイル
def change
  create_table :work_categories do |t|
    t.references :work, index: true
    t.references :category, index: true, foreign_key: true
    t.timestamps
  end
end

この記述で、work_idとcategory_idカラムがそれぞれ生成されます。各項にindex: trueオプションをつけて、検索を高速化。foreign_key: trueオプションをつけて、categoryテーブルにないカテゴリーが追加されないようにしています。

ただし、foreign_key: trueをつけると依存関係にある他のテーブルのカラムを自由に編集できなくなるので、今回の練習用のアプリのように、考えながら作る場合は、最後に外部キー制約をつけても良いのかもしれません。。。

▼参考リンク
外部キーの概要と制約を使うことのメリット・デメリット

modelにアソシエーションを追記

work, category, work_categoryのモデルに、それぞれ以下のようにアソシエーションを追記します。

単数形、複数形の気遣いが必要で、Railsガイドの多対多のリレーションの項を参照しました。

models/category.rb
class Category < ApplicationRecord
  has_many :work_categories #1
  has_many :works, through: :work_categories #2
end

#1#2はこの順番で書かないとエラーが出ます。以下、同じです。

models/work.rb
class Work < ApplicationRecord
  has_many :work_categories, dependent: :destroy
  has_many :categories, through: :work_categories
  accepts_nested_attributes_for :work_categories, allow_destroy: true
end
models/work_category.rb
class WorkCategory < ApplicationRecord
  belongs_to :work
  belongs_to :category
end

dependent: :destroyは、依存関係にあるデータも削除するオプションで、もし、あるworkが削除されたら、それに関連するwork_categoriesテーブルのデータも削除されます。

accepts_nested_attributes_forは、関連する項目も含めて一気に更新、削除する際の設定項目です。

dependent: :destroyがあればいらないのでは??と思うけれども、カテゴリーの内容が変更されたときに、これがあると便利なのだろうか??ちょっと検証が必要な部分です。(後日追記するかもしれません)

なお、この辺りに関しては、こちらの記事が大変ためになりました。

▼参照した記事
railsで多対多な関係を実装する時のポイント(加筆修正するかも)

controllerのwork_paramsの編集

controllers/works_controller.rb
def work_params
  params.require(:work).permit(:name, :description,  { category_ids: [] })
end

Workを作成(new)・編集(edit・update)するときに、work_categoriesテーブルにデータを入れられるよう、strong parameterに{ category_ids: [] }でwork_categoriesの配列の入力を許可しています。

viewでカテゴリー一覧をチェックボックスすで表示し、編集できるようにする

works_controllerのnewとeditで、categoryをチェックボックスで表示し、チェックを入れるとカテゴリーの値が設定・編集できるようにします。

▼作りたいものはこんな感じ

Image from Gyazo

▼書いたコードはこちら

controllers/works_controller.rb
= collection_check_boxes(:work, :category_ids, Category.all, :id, :name ) do |t|
      =t.label { t.check_box + t.text }

こちらのコードの意味に関しては、以前書いた記事があるので、拙著ですがこちらをご参照ください。

【初心者向け】チェックボックスの書き方あれこれ[Ruby][Rails]

さて、以上をもって多対多の関係を持ったモデル・コントローラー・ビューの実装が一通り完成しました。いざやり始めると細かい気遣いが色々必要でしたので、また他のテーブルとのリレーションを組むときに、参照していきたいです。

最後まで読んでくださり、ありがとうございました。

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

【Rails】 5分でFullCalendar実装する方法

はじめに

Railsアプリケーションは爆速で開発できるところがメリットですので、開発スピードに着目して記事を書きました。
5分でFullCalendarを実装するために、vim だけでも書けるようにファイルパスを全てしましたので、
vimを使ってパスをコピー&ペーストして、ファイル内のコードを変更することができます。

プロトタイプを作る時などにスピード重視で作れば一目置かれる存在になれるかもしれませんので、ぜひご活用ください。

やること

scaffoldで作成したEventモデルにFullCalendarライブラリを適用する。
イメージは下図のような感じになります。

image.png

コマンド

Rails のバージョンは5.2.4を指定しました。
また、scaffold で Eventモデルを作成しています。

// Rails アプリケーション作成
$ rails _5.2.4_ new five_min_fullcalendar
$ cd five_min_fullcalendar

// scaffold で Event モデルを作成
$ rails g scaffold event title:string body:string start_date:datetime end_date:datetime
$ rails db:migrate RAILS_ENV=development

GemFile

GemFile へ3つのgemを追加してbundle install

GemFile
gem 'jquery-rails'
gem 'fullcalendar-rails'
gem 'momentjs-rails'
$ bundle install

CSS

fullcalendar の部分を追加。

app/assets/stylesheets/application.css
 *= require_tree .
 *= require_self
 *= require fullcalendar
 */

JavaScript

丸々コピーして、javascripts/application.jsへ貼り付けをしてください。

app/assets/javascripts/application.js
//= require jquery
//= require moment
//= require fullcalendar

$(function () {
    // 画面遷移を検知
    $(document).on('turbolinks:load', function () {
        if ($('#calendar').length) {

            function Calendar() {
                return $('#calendar').fullCalendar({
                });
            }
            function clearCalendar() {
                $('#calendar').html('');
            }

            $(document).on('turbolinks:load', function () {
                Calendar();
            });
            $(document).on('turbolinks:before-cache', clearCalendar);

            //events: '/events.json', 以下に追加
            $('#calendar').fullCalendar({
                events: '/events.json',
                //カレンダー上部を年月で表示させる
                titleFormat: 'YYYY年 M月',
                //曜日を日本語表示
                dayNamesShort: ['', '', '', '', '', '', ''],
                //ボタンのレイアウト
                header: {
                    left: '',
                    center: 'title',
                    right: 'today prev,next'
                },
                //終了時刻がないイベントの表示間隔
                defaultTimedEventDuration: '03:00:00',
                buttonText: {
                    prev: '',
                    next: '',
                    prevYear: '前年',
                    nextYear: '翌年',
                    today: '今日',
                    month: '',
                    week: '',
                    day: ''
                },
                // Drag & Drop & Resize
                editable: true,
                //イベントの時間表示を24時間に
                timeFormat: "HH:mm",
                //イベントの色を変える
                eventColor: '#87cefa',
                //イベントの文字色を変える
                eventTextColor: '#000000',
                eventRender: function(event, element) {
                    element.css("font-size", "0.8em");
                    element.css("padding", "5px");
                }
            });
        }
    });
});

JSON File

JSONファイルを作成して、FullCalendarのライブラリへそのJSONファイルを渡すことで、カレンダーを表示することができます。

app/views/events/index.json.jbuilder
json.array!(@events) do |event|
  json.extract! event, :id, :title, :body
  json.start event.start_date
  json.end event.end_date
  json.url event_url(event, format: :html)
end

HTML

id が calendar の div 要素を追加すると、ここに fullCalendarが表示されます。

app/views/events/index.html.erb
<div id="calendar"></div>

turbolinks対策で必要です。

app/views/layouts/application.html.erb
  <!-- body タグに data-turbolinks 属性を追加 -->
  <body data-turbolinks="false">
    <%= yield %>
  </body>

開発環境で表示してみる

$ rails s

次のURLへアクセス。localhost:3000/events

New Eventをクリックして、イベントを作成。

すると下記の画像のように表示されました。

image.png

以上です。

まとめ

ものすごいライブラリと出会ってしまうとすぐに開発ができてしまうため、自分が天才になったのではないかと錯覚してしまいます。
あくまでライブラリの力だと思って日々精進しないといけませんね。

参考

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

AWS+Nginx+Unicornを利用してRailsアプリをデプロイしてみた。〜その1〜

はじめに

前回記事で作成したAWSの環境を利用し、Nginx+Unicornを使用してアプリをデプロイします。
次のアプリがローカル環境と同じように動作するようにデプロイします。

スクリーンショット 2020-01-25 6.40.58.png

Railsアプリケーションの実行環境

DB: MySQL5.7
言語: ruby 2.6.3
フレームワーク: rails 5.2.4
バージョン管理: git

EC2接続

EC2を起動し、SSH接続を行います。

$ ssh -i aws-test.pem ec2-user@[パブリックIPアドレス]
The authenticity of host '[パブリックIPアドレス]' can't be established.
ECDSA key fingerprint is SHA256:/gw4yP+4cWnIB4GU4UATDndTtYg+BAX7XaYtL6Ic46Q.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[パブリックIPアドレス]' (ECDSA) to the list of known hosts.
Last login: Sun Jan  5 09:04:35 2020 from softbank060135156151.bbtec.net

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
6 package(s) needed for security, out of 31 available
Run "sudo yum update" to apply all updates.

yumアップデートの指示が出ているので実行しておきます。

$ sudo yum update -y
:
:
完了しました!

EC2インスタンスの環境構築

次に、Railsアプリケーションを動かすためにツールやライブラリのインストールを行います。

・Git

$ sudo yum install git -y
:
:
完了しました!

・nodejs-10.3.0

$ sudo rpm -Uvh https://rpm.nodesource.com//pub_10.x/el/6/x86_64/nodejs-10.3.0-1nodesource.x86_64.rpm
:
:
 1:nodejs-2:10.3.0-1nodesource      ################################# [100%]

・dependencies for rails

$ sudo yum install gcc gcc-c++ libyaml-devel libffi-devel libxml2 libxslt libxml2-devel libslt-devel -y
:
:
完了しました!

・yarn

$ sudo npm install yarn  -g
:
:
added 1 package in 0.499s

・yarnのcheck-files

$ sudo yarn install --check-files
:
:
success Saved lockfile.
Done in 0.10s.

・git-core

$ sudo yum install git-core
:
:
完了しました!

・rbenv

$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
:
:
Resolving deltas: 100% (1756/1756), done.

・rbenvのpath設定

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ vi ~/.bash_profile
bash_profile.
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"           ← #追加
$ source ~/.bash_profile

・Avoid to Install rb-docs

$ echo 'gem: --no-document' >> ~/.gemrc

・ruby-build

$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
:
:
Resolving deltas: 100% (6879/6879), done.

・dependencies for ruby-build

$ sudo yum install bzip2 gdbm-devel openssl-devel libffi-devel libyaml-devel ncurses-devel readline-devel zlib-devel -y
:
:
完了しました!

・Ruby

$ RUBY_CONFIGURE_OPTS=--disable-install-doc ~/.rbenv/bin/rbenv install 2.6.3
:
:
Installed ruby-2.6.3 to /home/ec2-user/.rbenv/versions/2.6.3

・Set default Ruby version

$ rbenv global 2.6.3 && rbenv rehash

・bundle and so on

$ gem install bundler -v 2.1.0
Fetching bundler-2.1.0.gem
Successfully installed bundler-2.1.0
1 gem installed

・rbenv-rehash

$ gem install rbenv-rehash
Fetching rbenv-rehash-0.3.gem
Successfully installed rbenv-rehash-0.3
1 gem installed

・rails

$ gem install rails -v 5.2.4
:
:
Successfully installed rails-5.2.4

・インストール確認

$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]
$ rails -v
Rails 5.2.4
$ bundler -v
Bundler version 2.1.0

・nginx

WEBサーバーとして使うためにnginxをインストールします。

$ sudo yum install http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm -y
:
:
完了しました!

・nginx package

リポジトリがない状態だとインストールできないので、リポジトリをインストールしてからnginxをインストールします。

$ sudo yum install nginx -y
:
:
完了しました!

・nginxの接続確認

$ sudo systemctl status nginx.service #接続状況(Active: inactive (dead))
● nginx.service - nginx - high performance web server
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
   Active: inactive (dead)
     Docs: http://nginx.org/en/docs/
$ sudo systemctl start nginx.service #接続開始
$ sudo systemctl status nginx.service #接続状況(Active: active (running))
● nginx.service - nginx - high performance web server
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
   Active: active (running) since 日 2020-01-19 03:41:12 UTC; 1min 0s ago
     Docs: http://nginx.org/en/docs/
:
:

EC2パブリックIPでアクセスした際に次の画面が表示されれば接続確認OKです。

スクリーンショット 2020-01-19 12.43.48.png

確認が済んだら一旦nginxは停止させておきます。

$ sudo systemctl stop nginx.service 

・unicornとRailsアプリケーションの接続設定

$ sudo vi /etc/nginx/nginx.conf
nginx.conf
user  nginx;
    worker_processes  1;

    error_log  /var/log/nginx/error.log warn;
    pid        /var/run/nginx.pid;


    events {
        worker_connections  1024;
    }

    http {
        include       /etc/nginx/mime.types;
        default_type  application/octet-stream;

        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for"';

        access_log  /var/log/nginx/access.log  main;

        sendfile        on;
        #tcp_nopush     on;

        keepalive_timeout  65;

        #gzip  on;

        # include /etc/nginx/conf.d/*.conf; 
        
        upstream unicorn {
        server unix:/var/www/[アプリケーション名]/tmp/sockets/unicorn.sock;
        }
    server {
          listen 3000;
          server_name [EC2パブリックIPアドレス];
          access_log /var/log/nginx/app_access.log;
          error_log /var/log/nginx/app_error.log;
          try_files $uri/index.html @unicorn;

          root home/ec2-user/var/www/[アプリケーション名]/public;
          client_max_body_size 15M;
          client_body_temp_path /tmp/client_body;

          location @unicorn {
            proxy_pass http://unicorn;
          }

          location ~ ^/assets/ {
            root /var/www/[アプリケーション名]/public;
          }
          error_page 404 /404.html;
            location = /40x.html {
          }
          error_page 500 502 503 504 /50x.html;
            location = /50x.html {
          }
      }
    }

・アプリケーション用のフォルダを作成しフォルダ移動

$ sudo mkdir -p /var/www/
$ cd /var/www/

・アプリケーションのGitClone実行

GitHubでClone with HTTPSのURLをコピーします。

$ sudo git clone [対象アプリケーションのリモートリポジトリのURL]  
:
:
Resolving deltas: 100% (1193/1193), done.

・アプリケーションのディレクトリ内の権限変更

$ sudo chown ec2-user:ec2-user [アプリケーション名] 
$ cd [アプリケーション名]
$ sudo chown ec2-user:ec2-user vendor/
$ sudo chown -R ec2-user .
$ bundle install --path vendor/bundle
:
:
Bundle complete! 23 Gemfile dependencies, 93 gems now installed.
Bundled gems are installed into `./vendor/bundle`

・railsアプリとRDSの接続設定

$ vi config/database.yml 
datebase.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: [対象RDSのエンドポイント]
  username: [RDS作成時の名称]
  password: [RDS作成時のパスワード]
  socket: /tmp/mysql.sock

development:
  <<: *default
  database: [アプリケーション名]_development

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: [アプリケーション名]_test
#
production:
  <<: *default
  database: [アプリケーション名]_production
  username: [アプリケーション名]
  password: <%= ENV['PROJECT_DATABASE_PASSWORD'] %>

・DBの作成

$ bundle exec rails db:create
Created database '[アプリケーション名]_development'
Created database '[アプリケーション名]_test'
$ bundle exec rails db:migrate
== 20200117122858 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0163s
:
:
:
:
$ bundle exec rails db:seed

・unicornのインストール

$ vi Gemfile
  gem "unicorn"  #追加
$ bundle install --path vendor/bundle
:
:
Bundled gems are installed into `./vendor/bundle`

・unicornの設定

$ sudo mkdir -p config/unicorn
$ sudo vi config/unicorn/development.rb
development.rb
# paths
    app_path = "/var/www/[アプリケーション名]"
    pid_path = "#{app_path}/tmp/pids/unicorn.pid"
    listen_path = "#{app_path}/tmp/sockets/unicorn.sock"
    # unicorn paths
    working_directory app_path
    pid pid_path

    # listen
    listen listen_path, backlog: 64

    # logging
    stderr_path "#{app_path}/log/unicorn.stderr.log"
    stdout_path "#{app_path}/log/unicorn.stdout.log"

    # workers
    worker_processes 2

    # Time-out
    timeout 30

    # use correct Gemfile on restarts
    before_exec do |_server|
      ENV["BUNDLE_GEMFILE"] = "#{app_path}/current/Gemfile"
    end

    # preload
    preload_app true

    before_fork do |server, _worker|
      # the following is highly recomended for Rails + "preload_app true"
      # as there's no need for the master process to hold a connection
      if defined?(ActiveRecord::Base)
        ActiveRecord::Base.connection.disconnect!
      end

      # Before forking, kill the master process that belongs to the .oldbin PID.
      # This enables 0 downtime deploys.
      # rubocop: disable Lint/HandleExceptions
      old_pid = "#{server.config[:pid]}.oldbin"
      if File.exist?(old_pid) && server.pid != old_pid
        begin
          Process.kill("QUIT", File.read(old_pid).to_i)
        rescue Errno::ENOENT, Errno::ESRCH
          # someone else did our job for us
        end
      end
      # rubocop: enable Lint/HandleExceptions
    end

    after_fork do |_server, _worker|
      if defined?(ActiveRecord::Base)
        ActiveRecord::Base.establish_connection
      end
    end

・アクセスログとエラーログのファイル作成

$ touch log/unicorn.stderr.log
$ touch log/unicorn.stdout.log 
$ mkdir -p tmp/pids
$ mkdir -p tmp/sockets

・ unicornの起動

$ bundle exec unicorn_rails -E development -c config/unicorn/development.rb -D 
$ ps -ef | grep unicorn | grep -v grep  #起動確認
ec2-user  3457     1  0 01:24 ?        00:00:00 unicorn_rails master -E development -c config/unicorn/development.rb -D
ec2-user  3464  3457  0 01:24 ?        00:00:00 unicorn_rails worker[0] -E development -c config/unicorn/development.rb -D
ec2-user  3465  3457  0 01:24 ?        00:00:00 unicorn_rails worker[1] -E development -c config/unicorn/development.rb -D

・nginxの起動

$ sudo systemctl start nginx.service 
$ ps aux | grep nginx #起動確認
root      3476  0.0  0.1  46176  1008 ?        Ss   01:25   0:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx     3477  0.0  0.2  46588  2892 ?        S    01:25   0:00 nginx: worker process
ec2-user  3486  0.0  0.0 119436   940 pts/0    S+   01:27   0:00 grep --color=auto nginx

最後、[パブリックIPアドレス]:3000でEC2インスタンスのIPアドレスにアクセスします。

スクリーンショット 2020-01-25 11.23.24.png

そもそもアクセスができていない様子なのでセキュリティグループを確認します。

スクリーンショット 2020-01-25 11.01.54.png

nginx.confで3000番ポートを指定しているためそれをルール編集で追加します。

スクリーンショット 2020-01-25 11.04.04.png

再度、[パブリックIPアドレス]:3000でEC2インスタンスのIPアドレスにアクセスします。

スクリーンショット 2020-01-25 11.18.51.png

接続はされましたが、ローカル環境と比較して挙動が異なっています。それ以外にもサインインやアカウント登録を行うと、エラーが生じるため修正が必要なようです。
少し長くなりましたので次回の記事で修正を行います。

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

ES6構文をRailsでプリコンパイル

AWSにRailsアプリをデプロイしようとした時、、、

諸々設定も終盤、EC2へSSHでログインし、Railsアプリをプリコンパイルする

スクリーンショット 2020-01-26 10.46.06.png

はい、怒られました。

原因は

Uglifier::Error: Unexpected token: punc ()). To use ES6 syntax, harmony mode must be enabled with Uglifier.new(:harmony => true).

この部分に書いてありますね。
google翻訳先生に聞くと、

「Uglifier :: Error:予期しないトークン:punc())。 ES6構文を使用するには、ハーモニーモードを>Uglifier.new(:harmony => true)で有効にする必要があります。」

なるほど、、、

と言うわけで、

スクリーンショット 2020-01-26 10.48.51.png

config/environments/production.rb
config.assets.js_compressor = :uglifier

これを、、、

スクリーンショット 2020-01-26 10.49.04.png

config/environments/production.rb
 config.assets.js_compressor = Uglifier.new(harmony: true)

こう!

お疲れ様でした。

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

git push heroku masterできない時の対応

問題

リポジトリから読み取ることができないと言ってそう。。

$ git push heroku master
fatal: 'heroku' does not appear to be a git repository
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

解決策

リンク先のやり方で解決しました!!

参考

作成したRuby on RailsプロジェクトをHerokuを使ってデプロイしてみる

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

capybaraでfields_forのformの要素をあいまい検索でテストする

はじめに

最近はrspec書いてて、これどうすんねんーーーってとこあったので備忘録的な感じで書いておきます。

今回の困りごと

fields_forformを追加した際に、formの要素をどうやって指定すればいいか困りました。
スクリーンショット 2020-01-26 2.22.58.png

今回は画像のプラスボタンを押すとformが追加されるようになっており、値を入力して保存できるかどうかをチェックしたいのですが、追加したformのindex番号はランダムで生成されるため、

<input type="text" name="user[users_attributes][1579974088711][name]" id="users_attributes_1579974088711_name">

上記のような[1579974088711]が数字が生成されています。
なので、

fill_in 'user[users_attributes][2][name]', with: '夜勤'

というふうにindex番号の指定ができないです。

解決策

指定ができない場合どうやって空白のformに文字入力したらいい?
- ページ内のformを全部検索してきて、最後の要素を指定してやればいいです。
全体から検索してくるには、

all("input[name$='[name]']")[2].set("夜勤")

のようにすれば入力してくれるようになります。

find("input[name$='[name]']")[0].set("夜勤")

でもいけるようですが、自分は動きませんでしたのでallで全体から探すようにしています。

おわりに

今回findで動かなかったがすごい気持ちわるいなーと思ったんですが、無事テストはできたんでよかったです。
なので、動的に追加したformのテストはこれでfindallで全体から探して、そのindex番号で指定してやればいいかと思います。
なにかご指摘等あればよろしくお願いします。

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

困ったらググれ

どうも、momotaroです。初めまして。

TECH::EXPERT受講初めたことがきっかけで、ブログはじめました。
自己学習(アウトプット)の為に投稿していきます。

今週月曜からプログラミング初心者として学習を始めて、この一週間で学んだ大きなこと。
それはタイトルにある通り、

分からないコードが出てきたり、エラーが出てきたらGoogleさんに遠慮なく質問しよう!

・・・ということですね。

これはTECH::EXPERTのカリキュラムにもきちんと書いてあります。

この教えが意味するものは

『”自己解決能力”を身につける』

だと思います。大切なのは、なぜその問題が起こるのかを理解すること。そして大事なのは、いかにその問題をいち早く解決できるか・・・。
実際現場の方でもググることはよくあるそうです。

なので、これからは良い検索方法やわかりやすい解説サイト、ブログなどあれば模索していきたいと思います。

また自分が新しい解説など思いついたり発見した場合は発信していきます。

最後に自分と同じくTECH::EXPERT受講はじめたばかりの人に読んで欲しいブログのリンク載せておきます。

#Rails入門者のエラー対処

もしよかったら『いいね』押してください。

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

RSpecで利用するFactoryBotの導入

FactoryBot

FactoryBotはテスト用データの作成をサポートするgemであり、利用するとテスト用のデータを簡単に作成し、テストから呼び出すことができる。

早速Gemfileに'factory_bot_rails'を記述しbundle install を実行しよう

# Gemfile
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails', '~> 4.11'
end

FactoryBotでテストデータを作成できるよう準備

Userのファクトリを作成

一例としてuserのファクトリを作成していきます。

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "tester#{n}@example.com" } #重複を防ぐためsequenceを使う
    password { 'password' }
    password_confirmation { 'password' }
  end
end

sequenceでデータを生成する毎に通し番号をふってユニークな値を作るようにする。
これが無いとuserが複数回生成された時にemailが重複してしまう。
知らなかった時苦戦した。。。

実際にデータを作成

※ RSpecのコードを意識してletで定義している

#specを書くファイル
let(:user) { FactoryBot.create(:user) }

letで定義したuserが呼び出されるとファクトリ通りのuserのデータが作成される。

省略形

# spec/rails_helper.rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods

こうすることで、、、

{ create(:user) }

FactoryBotを省略してもデータを作成可能。 
短いコードで書けるのは良いですよね

以上です!

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

Rails6 decimal型のカラムに格納されたデータの文字数を取得する

目的

  • devimal型のカラムに格納されたデータの文字数を取得する方法をまとめる。

経緯

  • Rails6のアプリでツイート用のテンプレートを作成したい。
  • そのテンプレート内容は140文字以内に制限したい。
  • そのテンプレート内容はデータベースの複数の特定カラムに格納されているもので構成される。
  • 複数の特定カラムのデータ型はtext型とdecimal型が混在している。

結論

  • decimal型のデータの文字数は.to_sで一旦文字列に変更してから.lengthで文字数を抽出する。

詰まったところ

  • decimal型の値を.lengthのみで文字数を抽出しようとした。
  • そもそも数値に.lengthメソットは使用できない。

解決方法

  • decimal型の値の文字列は.to_iで一旦文字列に変更してから.lengthで文字数を抽出した。
  • 下記に文字数の抽出方法を記載する。

    decimal型の値.to_s.length
    

今回の具体例

  • 下記にテンプレートのビューファイルの内容を記載する。(下記に記載される内容を全て含めて140文字以内にしたい。説明のために空行を入れているため改行は考慮しない。)

    <p>
    <!-- postsテーブルのcontentカラムの内容(text型) -->
    <%= @post.content %><br>
    
    <!-- postsテーブルのtoday_study_timeの内容(decimal型) -->
    today: <%= @post.today_study_time %> h<br>
    
    <!-- postsテーブルのstudy_timeの内容(decimal型) -->
    total: <%= @post.study_time %> h<br>
    
    <!-- postsテーブルのfree_commentの内容(text型) -->
    <%= @post.free_comment %><br>
    
    <!-- postsテーブルのhash_tagの内容(text型) -->
    #<%= @post.hash_tag %>
    </p>
    
  • コントローラファイルで前述のデータベースから取得した値の文字列のトータルを140文字以下になっているかの処理を記載した。

  • 下記に処理を記載する。(今回の件を説明することに最小限のコードを記載する)

    #ビューから受け取った情報を@postに格納
    @post = Post.new(content: params[:content], 
                     study_time: params[:'study_time'],
                     today_study_time: params[:'study_time'], 
                     hash_tag: params[:hash_tag], 
                     user_id: @current_user.id, 
                     free_comment: params[:free_comment]
    )
    
    #変数word_countに各値の文字数を格納する。
    word_count = @post.content.length  + @post.hash_tag.length  + @post.free_comment.length  + @post.study_time.to_s.length + @post.today_study_time.to_s.length
    
    #word_countが140よりも大きい時にエラーで特定のページにリダイレクトされるような処理などが連なって記載されている。
    
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む