- 投稿日:2020-01-26T22:35:05+09:00
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.rENV['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.rbrequire '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.rbrequire '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
- 投稿日:2020-01-26T20:00:23+09:00
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/extendRails側の作り方や全体的なポイントは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.jsimport "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.jsimport 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.jsconst initialEntry = { title: '', body: '', tags: [], published_at: '', draft: false };useReducerに渡す関数です。記事全体、タグのリスト、チェックボックス、それ以外(テキスト入力欄)で場合分けして新しい記事オブジェクトを返します。
app/javascript/entries/form.jsfunction 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.jsexport 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.jsfunction 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.jsfunction 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.jsexport 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.jsfunction 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のフレームワークに関する理解が深まります。
- 投稿日:2020-01-26T19:37:55+09:00
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) SELECTusers.* FROMusersORDER BYusers.idASC 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) SELECTcategories.* FROMcategoriesORDER BYcategories.idASC 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) SELECTcategories.* FROMcategoriesWHEREcategories.id= 1 LIMIT 1
MemoRoom Create (0.2ms) INSERT INTOmemo_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が上手くいきました。初歩的なミスですが、メモを残す事によって、久しぶりにやった際に忘れないようにという思いで書きました。
- 投稿日:2020-01-26T18:32:55+09:00
deviseを用いたウィザード形式でユーザー新規登録機能を実装してやるって![実装方法総まとめ]
某フリマサイトのコピーサイトをチーム開発しており、deviseを用いたウィザード形式でのユーザー新規登録機能を実装しましたが、大分苦戦しました。。。
ということで、備忘録もかねて以下にまとめます!
*ページ内、クレジットカード登録のページございますが、今回はpay.jpなどのGemは使用しておりません。
単純に情報を登録するだけにしております。ウィザード形式とは?
そもそもウィザード形式とは、なんぞや?というところから説明します。
ウィザード形式とは、サイト利用者に一つずつ質問や設定項目を提示し、対話的に処理を進める操作方式のことを指します。
イメージとしては、以下のような感じです。
ウィザード形式でのユーザー新規登録機能を実装する時の概要
まず、会員登録の画面でユーザー情報を入力させ、それをsessionに保持させておきます。
次に住所情報を登録する画面で住所情報入力してもらい、それをまたsessionに保持させておきます。
そして次の支払い方法を登録する画面で支払い方法を入力してもらい、最後のステップでsessionに保持していたユーザー情報と、それに関連する住所情報・支払いをテーブルに保存します。この時注意する必要があるのは、ウィザードの各ページごとにテーブルを分ける必要があるということです。ウィザードが切り替わるときに、都度バリデーションのチェックを行うからです。
例えば2ページ目で記入する住所情報のカラムが、adressesテーブルではなく、usersテーブルに存在すると、1ページ目から2ページ目に切り替わるときに住所情報が入力されていないのでバリデーションに引っかかってしまいます。該当テーブル(モデル)
今回の実装で使用したテーブルは以下のようになっております。(該当箇所のみ抜き出してます)
Column Type Options nickname string null: false 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をインストールします。
gemfilegem '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/creditcardclass 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 } endmodels/addressclass 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 } endmodel/userclass 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 endviewの編集(user)
それでは、各viewを作成しましょう。
今回はrailsのverが古いのでform_forを使用しております。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)
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)
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.rbclass 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.rbdef 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 endcontroller編集(address登録)
アドレスの登録は基本的にuserと同様ですが、userの記述でaddressのインスタンス変数は作成しているので、
createアクションのみ記述します。
また、protected内に、引数の設定をしましょう。
- buildメソッド:
今回、以下のようにbuildメソッドを使用しています。
これは、親モデルに属する子モデルのインスタンスを新たに生成したい場合に使うメソッドとなっており、
このメソッドを使用することで、外部キーに値が入った状態でインスタンスが生成できます。
(親モデルと子モデルは、アソシエーション設定あり)
今回は以下のようにaddressをuserの子モデルとして、使用するために、以下のように記述しており、
addressにはuser_idのカラムに数字が与えられます。
例) @address = @user.build_addressapp/controllers/users/registrations_controller.rbdef 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) endcontroller編集(creditcard登録)
creditcardの登録に関しても、addressの記述でcreditcardのインスタンス変数は作成しているので、
createアクションのみ記述します。
ポイントは以下の通りです。
- バリデーションチェック
- バリデーションチェックが完了した情報と、sessionで保持していた情報とあわせ、ユーザー情報として保存すること
ログインをすること
- sessionを削除することapp/controllers/users/registrations_controller.rbdef 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) endrouteの編集
view、controllerの準備ができましたので、routeの編集をします。
この編集は2つのポイントがあります。
1.devise userモデルにrouteを設定
2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする1.devise userモデルにrouteを設定
devise管理下のusersコントローラを作成がしましたが、記述を変更しないと、現状deviseのコントローラーが呼ばれてしまっています。
そこで、以下のように記述をして、ルーティングを設定しましょう。route.rbRails.application.routes.draw do devise_for :users, controllers: { registrations: 'users/registrations', } end2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする
今回複数モデル(addressとcreditcard)を使用しているため、devise_scopeを使用し、各モデルのルートを設定しましょう。
route.rbRails.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/13378buildメソッドについて
https://qiita.com/tsuchinoko_run/items/d671ea840bc0bfa90186
- 投稿日:2020-01-26T18:32:55+09:00
deviseを用いたウィザード形式でユーザー新規登録機能を実装した時の話。[実装方法総まとめ]
某フリマサイトのコピーサイトをチーム開発しており、deviseを用いたウィザード形式でのユーザー新規登録機能を実装しましたが、大分苦戦しました。。。
ということで、備忘録もかねて以下にまとめます!ウィザード形式とは?
そもそもウィザード形式とは、なんぞや?というところから説明します。
ウィザード形式とは、サイト利用者に一つずつ質問や設定項目を提示し、対話的に処理を進める操作方式のことを指します。
イメージとしては、以下のような感じです。
ウィザード形式でのユーザー新規登録機能を実装する時の概要
まず、会員登録の画面でユーザー情報を入力させ、それをsessionに保持させておきます。
次に住所情報を登録する画面で住所情報入力してもらい、それをまたsessionに保持させておきます。
そして次の支払い方法を登録する画面で支払い方法を入力してもらい、最後のステップでsessionに保持していたユーザー情報と、それに関連する住所情報・支払いをテーブルに保存します。この時注意する必要があるのは、ウィザードの各ページごとにテーブルを分ける必要があるということです。ウィザードが切り替わるときに、都度バリデーションのチェックを行うからです。
例えば2ページ目で記入する住所情報のカラムが、adressesテーブルではなく、usersテーブルに存在すると、1ページ目から2ページ目に切り替わるときに住所情報が入力されていないのでバリデーションに引っかかってしまいます。該当テーブル(モデル)
今回の実装で使用したテーブルは以下のようになっております。(該当箇所のみ抜き出してます)
Column Type Options nickname string null: false 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をインストールします。
gemfilegem '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/creditcardclass 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 } endmodels/addressclass 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 } endmodel/userclass 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 endviewの編集(user)
それでは、各viewを作成しましょう。
今回はrailsのverが古いのでform_forを使用しております。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)
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)
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.rbclass 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.rbdef 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 endcontroller編集(address登録)
アドレスの登録は基本的にuserと同様ですが、userの記述でaddressのインスタンス変数は作成しているので、
createアクションのみ記述します。
また、protected内に、引数の設定をしましょう。
- buildメソッド:
今回、以下のようにbuildメソッドを使用しています。
これは、親モデルに属する子モデルのインスタンスを新たに生成したい場合に使うメソッドとなっており、
このメソッドを使用することで、外部キーに値が入った状態でインスタンスが生成できます。
(親モデルと子モデルは、アソシエーション設定あり)
今回は以下のようにaddressをuserの子モデルとして、使用するために、以下のように記述しており、
addressにはuser_idのカラムに数字が与えられます。
例) @address = @user.build_addressapp/controllers/users/registrations_controller.rbdef 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) endcontroller編集(creditcard登録)
creditcardの登録に関しても、addressの記述でcreditcardのインスタンス変数は作成しているので、
createアクションのみ記述します。
ポイントは以下の通りです。
- バリデーションチェック
- バリデーションチェックが完了した情報と、sessionで保持していた情報とあわせ、ユーザー情報として保存すること
ログインをすること
- sessionを削除することapp/controllers/users/registrations_controller.rbdef 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) endrouteの編集
view、controllerの準備ができましたので、routeの編集をします。
この編集は2つのポイントがあります。
1.devise userモデルにrouteを設定
2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする1.devise userモデルにrouteを設定
devise管理下のusersコントローラを作成がしましたが、記述を変更しないと、現状deviseのコントローラーが呼ばれてしまっています。
そこで、以下のように記述をして、ルーティングを設定しましょう。route.rbRails.application.routes.draw do devise_for :users, controllers: { registrations: 'users/registrations', } end2.devise_scopeを使用し、Deviseで複数のモデルを扱うことができるようにする
今回複数モデル(addressとcreditcard)を使用しているため、devise_scopeを使用し、各モデルのルートを設定しましょう。
route.rbRails.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/13378buildメソッドについて
https://qiita.com/tsuchinoko_run/items/d671ea840bc0bfa90186
- 投稿日:2020-01-26T18:28:58+09:00
実務未経験者が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レベルの一般的な知識がないと、この教材は難しいと思います。理由は説明が少ないからです。
講座内で解説はありますが、それでも基本を理解していないと追いつけません。作った成果物
講座内で配布されているCSSを使っていないので、UIが貧弱です。
ちなみに講座内で配布されているCSSを使うとこんな感じになるようです。配布場所はここ。
学んだこと
- hamlとscssの使い方
- gemのsimple_formでformが簡単に作れる。
- gemのdeviseでログイン認証が簡単にできる。
- gemのpaperclipで画像の扱いが簡単にできる。
- gemのact_as_votableでlike機能が簡単に実装できる。
不満なこと
- gemを用いて簡単にアプリができてしまったので、学べた感じがしない。
- レベルが高い内容ではない。
まとめ
Instagram風アプリが作りたかったので受講しましたが、そこまでレベルの高い内容ではありませんでした。
ですが無料の教材ということを踏まえれば、内容は充実していたと思います。Instagram風のアプリを作成してみたい方は利用してみてはいかがでしょうか?
- 投稿日:2020-01-26T18:21:04+09:00
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名
としてあげると新しいアプリとファイルが作成されます。
- 投稿日:2020-01-26T18:15:51+09:00
超初心者が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 curlcurlはパッケージをインストールするためのパッケージ。
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 clonegithubという賢い人たちが書いたコードが集められた場所(適当)からrbenvを自分のローカル環境にコピーする。ありがとう賢い人!引数には欲しいフォルダのURL、保存する場所の順番に記す。
cp bashrc...
.bashrcは重要な設定ファイル。これを書き換える必要がある。もし変な操作をしてしまったら怖いから。バックアップを取るよ。cpはコピーするコマンド。第一引数にコピーしたいファイル。第二引数にペースト先を指定。
echo export ...>> .bashrc
.bashrcという重要な設定ファイルがある。シェル起動時に自動的に実行されるファイルなのだが、こいつの内容を書き換えている。具体的には先ほどインストールしたrbenvをコマンドとして使えるようにするために、rbenvの実行ファイルが置いてある場所を教えてあげている。教え方は$PATHという変数(環境変数という)にrbenvの居場所を付け加えて更新している。この環境変数を書き換えるコマンドがexportです。
curl -fsSL ... | bash
curlはパッケージをインストールするファイル。ここでインストールしているのはrbenvが正しくインストールされたかを検証するファイル。-fsSLはcurlする時のオプションを決めれるよ。詳しくは$ 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 --systemとgem installの違いは以下のリンクが教えてくれる。
「gem update --system」と「gem update」の違いSQlite3のインストール
以下のコマンドでSQlite3のインストールを行う。
libsqlite3-devはsudo apt install sqlite3 libsqlite3-devrailsのインストール
$ gem install rails $ gem list railsnode.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 -vyarn インストール
$ 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 yarnrails 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 --savewebpack のインストール
$ 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 upgradepackage.jsonに記述されている範囲内での最新パッケージを取ってくる。
yarn upgadeに関しては以下のサイトが詳しく書いてある。でもcore-jsのバージョンは変わらなかった。
なぜならcore-jsはそれ単体でインストールされているわけではない。Babelやpugの一つとしてインストールされているからその親であるBabelやpugの指定するバージョン以上には上げられないのであろう。そのため、以下のコマンドでcore-js単体をインストールした。yarn add core-js -Dこれでcore-jsは最新版である core-js@3.6.4 にアップグレードされた。
- 投稿日:2020-01-26T18:07:18+09:00
Railsのルーティングを支える技術 - Journeyについて
Journeyは、Railsのルーティング処理を担当するライブラリです。Rails3で導入され、大規模なアプリケーションでの処理が高速化しました。
今回は、Journeyがどのようにしてルーティング処理を行っているかを紹介します。
TL;DR
- Railsのルーターは、ウェブから受け取ったURL(入力URL)を認識し、ルート定義と見比べマッチする処理が見つかればそれを呼び出す
- 入力URLに対応する処理を高速に探すためのライブラリがJourneyである
- Journeyはルート定義を非決定性有限オートマトン(NFA)に変換し、マッチングを行う
- Journeyは以下ステップを踏み、ルート定義をNFAに変換している
- ルート定義URLを字句解析し、トークンに分解する
- トークンから抽象構文木(AST)を構築する
- ASTからNFAに変換する
- Journeyは以下ステップを踏み、入力URLに対するマッチング処理を行っている
- 入力として与えられたURLを字句解析し、NFA入力形式に分解する
- 入力をもとにNFAのシミュレーションをする
- 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.jsonはid=12、format=jsonと認識され、articles#showにマッチします。入力URLが複数定義にマッチした場合は、先に定義した処理が呼び出されます。
例えば、入力URL/articles/newはarticles#newとarticles#showにマッチしますが、先に定義をしているnewが呼び出されます。有限オートマトン
Journeyのルートマッチング処理を理解するためには、オートマトンについて理解をしておく必要があります。
オートマトンは、計算理論で使われるモデルのことです。
今回は有限オートマトンと呼ばれる領域に絞って説明します。有限オートマトンと聞くとなんだか複雑そうですが、いたってシンプルなモデルです。有限オートマトンは現在の状態と、入力が与えられた時にどの状態へ遷移するかの規則をもっています。
初期状態から入力値に従って状態遷移し、次の状態に進みます。
すべての入力が終わり、最終的な状態が受理状態であったら、受理したという結果を返します。以下は、シンプルなオートマトンを図にしたものです。
この有限オートマトンは、入力値は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の例です。このオートマトンに入力
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がマッチした場合(受理状態)は二重丸で表しています。この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で表すと以下となります。
この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に変換しています。
- ルート定義URLを字句解析し、トークンに分解する
- トークンから抽象構文木(AST)を構築する
- ASTからNFAに変換する
また、マッチング処理をする際は、以下ステップを踏みます。
- 入力として与えられたURLを字句解析し、NFA入力形式に分解する
- 入力をもとにNFAのシミュレーションをする
- 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であると定義されます。
terminalはsymbolまたは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から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_statesとregexp_statesで表しています。
例えば、{0=>{"/"=>1}は 状態0から入力/が来たら1に遷移せよ、ということを表しています。
memosは受理した場合の処理を入れておく変数です。とりあえず今は何も使わないのでnilのままにしておきます。これでNFAが完成しました。図に表すと以下となります。
ルートの定義が複数ある場合はどうしたらよいでしょうか。これは、作成した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は図に表すと以下となります。
NFAシミュレーション - 入力URLにマッチする処理を求める
NFAが作れました。あとは入力URLをもとにNFAのシミュレーションをして、受理状態になったら対応する処理を呼び出すと、ルートのマッチング処理は完了です。
シミュレーションの処理は https://github.com/rails/rails/blob/6-0-stable/actionpack/lib/action_dispatch/journey/nfa/simulator.rb#L23 となります。
これは以下ステップを踏み、ルート定義に対応する処理を見つけます。
- 入力として与えられたURLを字句解析し、NFA入力形式に分解する
- 入力をもとにNFAのシミュレーションをする
- 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の基礎知識は大事なんだなあと改めて感じた次第です。
- 投稿日:2020-01-26T17:08:00+09:00
【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 LDFLAGS、export 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する事で、インストールが出来ました。
- 投稿日:2020-01-26T15:50:38+09:00
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をすることができます。
- 投稿日:2020-01-26T14:56:15+09:00
[strong parameters][form_with]投稿にユーザー情報を表示する
概要
目的
プロゲート以外の方法(strong parameters)で投稿詳細に投稿主(ユーザー)情報を記載すること。
(ちなみにプロゲートはこんな感じ)実現したいこと
各action すること(ポイント) new from_withを使って、投稿。 create strong parametersを使用して処理。 show 投稿にユーザー情報を引っ張ってくる。 このような流れで進めていきます。
完成イメージ
画像ではユーザー名、アイコン記載しておりますが、
今回は分かりやすく理解してもらうためにユーザー名だけの表示します。環境
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.rbdef 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%> #投稿内容を表示これで投稿にユーザー情報を紐付けることが出来ました!
最後に
まだまだ勉強不足なところがありますので、
アップデートできた知識は追加で記載していきます。もし、何か修正点とかございましたらコメント等
恐縮ですが、宜しくおねがいします。
- 投稿日:2020-01-26T12:25:25+09:00
[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_categorymigrationファイル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.rbclass Category < ApplicationRecord has_many :work_categories #1 has_many :works, through: :work_categories #2 end
#1と#2はこの順番で書かないとエラーが出ます。以下、同じです。models/work.rbclass Work < ApplicationRecord has_many :work_categories, dependent: :destroy has_many :categories, through: :work_categories accepts_nested_attributes_for :work_categories, allow_destroy: true endmodels/work_category.rbclass 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.rbdef work_params params.require(:work).permit(:name, :description, { category_ids: [] }) endWorkを作成(new)・編集(edit・update)するときに、work_categoriesテーブルにデータを入れられるよう、strong parameterに
{ category_ids: [] }でwork_categoriesの配列の入力を許可しています。viewでカテゴリー一覧をチェックボックスすで表示し、編集できるようにする
works_controllerのnewとeditで、categoryをチェックボックスで表示し、チェックを入れるとカテゴリーの値が設定・編集できるようにします。
▼作りたいものはこんな感じ
▼書いたコードはこちら
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]
さて、以上をもって多対多の関係を持ったモデル・コントローラー・ビューの実装が一通り完成しました。いざやり始めると細かい気遣いが色々必要でしたので、また他のテーブルとのリレーションを組むときに、参照していきたいです。
最後まで読んでくださり、ありがとうございました。
- 投稿日:2020-01-26T12:10:58+09:00
【Rails】 5分でFullCalendar実装する方法
はじめに
Railsアプリケーションは爆速で開発できるところがメリットですので、開発スピードに着目して記事を書きました。
5分でFullCalendarを実装するために、vim だけでも書けるようにファイルパスを全てしましたので、
vimを使ってパスをコピー&ペーストして、ファイル内のコードを変更することができます。プロトタイプを作る時などにスピード重視で作れば一目置かれる存在になれるかもしれませんので、ぜひご活用ください。
やること
scaffoldで作成したEventモデルにFullCalendarライブラリを適用する。
イメージは下図のような感じになります。コマンド
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=developmentGemFile
GemFile へ3つのgemを追加して
bundle installGemFilegem 'jquery-rails' gem 'fullcalendar-rails' gem 'momentjs-rails'$ bundle installCSS
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.jbuilderjson.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) endHTML
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をクリックして、イベントを作成。すると下記の画像のように表示されました。
以上です。
まとめ
ものすごいライブラリと出会ってしまうとすぐに開発ができてしまうため、自分が天才になったのではないかと錯覚してしまいます。
あくまでライブラリの力だと思って日々精進しないといけませんね。参考
- GitHub bokmann/fullcalendar-rails
- RailsでFullCalendarを使って予定を表示するまで
- turbolinksチートシート
- 投稿日:2020-01-26T11:17:48+09:00
AWS+Nginx+Unicornを利用してRailsアプリをデプロイしてみた。〜その1〜
はじめに
前回記事で作成したAWSの環境を利用し、Nginx+Unicornを使用してアプリをデプロイします。
次のアプリがローカル環境と同じように動作するようにデプロイします。Railsアプリケーションの実行環境
DB: MySQL5.7
言語: ruby 2.6.3
フレームワーク: rails 5.2.4
バージョン管理: gitEC2接続
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_profilebash_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です。
確認が済んだら一旦nginxは停止させておきます。
$ sudo systemctl stop nginx.service・unicornとRailsアプリケーションの接続設定
$ sudo vi /etc/nginx/nginx.confnginx.confuser 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.ymldatebase.ymldefault: &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.rbdevelopment.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アドレスにアクセスします。そもそもアクセスができていない様子なのでセキュリティグループを確認します。
nginx.confで3000番ポートを指定しているためそれをルール編集で追加します。再度、
[パブリックIPアドレス]:3000でEC2インスタンスのIPアドレスにアクセスします。接続はされましたが、ローカル環境と比較して挙動が異なっています。それ以外にもサインインやアカウント登録を行うと、エラーが生じるため修正が必要なようです。
少し長くなりましたので次回の記事で修正を行います。
- 投稿日:2020-01-26T10:57:31+09:00
ES6構文をRailsでプリコンパイル
AWSにRailsアプリをデプロイしようとした時、、、
諸々設定も終盤、EC2へSSHでログインし、Railsアプリをプリコンパイルする
はい、怒られました。
原因は
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)で有効にする必要があります。」
なるほど、、、
と言うわけで、
config/environments/production.rbconfig.assets.js_compressor = :uglifierこれを、、、
config/environments/production.rbconfig.assets.js_compressor = Uglifier.new(harmony: true)こう!
お疲れ様でした。
- 投稿日:2020-01-26T09:54:52+09:00
git push heroku masterできない時の対応
- 投稿日:2020-01-26T03:37:37+09:00
capybaraでfields_forのformの要素をあいまい検索でテストする
はじめに
最近はrspec書いてて、これどうすんねんーーーってとこあったので備忘録的な感じで書いておきます。
今回の困りごと
fields_forでformを追加した際に、formの要素をどうやって指定すればいいか困りました。
今回は画像のプラスボタンを押すと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のテストはこれでfindかallで全体から探して、そのindex番号で指定してやればいいかと思います。
なにかご指摘等あればよろしくお願いします。
- 投稿日:2020-01-26T02:00:15+09:00
困ったらググれ
どうも、momotaroです。初めまして。
TECH::EXPERT受講初めたことがきっかけで、ブログはじめました。
自己学習(アウトプット)の為に投稿していきます。今週月曜からプログラミング初心者として学習を始めて、この一週間で学んだ大きなこと。
それはタイトルにある通り、分からないコードが出てきたり、エラーが出てきたらGoogleさんに遠慮なく質問しよう!
・・・ということですね。
これはTECH::EXPERTのカリキュラムにもきちんと書いてあります。
この教えが意味するものは
『”自己解決能力”を身につける』
だと思います。大切なのは、なぜその問題が起こるのかを理解すること。そして大事なのは、いかにその問題をいち早く解決できるか・・・。
実際現場の方でもググることはよくあるそうです。なので、これからは良い検索方法やわかりやすい解説サイト、ブログなどあれば模索していきたいと思います。
また自分が新しい解説など思いついたり発見した場合は発信していきます。
最後に自分と同じくTECH::EXPERT受講はじめたばかりの人に読んで欲しいブログのリンク載せておきます。
もしよかったら『いいね』押してください。
- 投稿日:2020-01-26T01:37:08+09:00
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' endFactoryBotでテストデータを作成できるよう準備
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 endsequenceでデータを生成する毎に通し番号をふってユニークな値を作るようにする。
これが無いと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を省略してもデータを作成可能。
短いコードで書けるのは良いですよね以上です!
- 投稿日:2020-01-26T00:20:25+09:00
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よりも大きい時にエラーで特定のページにリダイレクトされるような処理などが連なって記載されている。


























