- 投稿日:2020-05-15T23:36:45+09:00
[エラー解析]デバッグして、Gemの中身を確認して、エラーの原因を特定した
わけのわからんエラーが表示された
Facebookログイン画面の操作中に、以下のOKボタンを押すと、500番エラーが表示されログインができない。
ターミナルのログには
undefined method 'load_from_provider' for #<Class:0x00007ff2e7d32a98>
というエラーメッセージが表示されている。
Facebook固有のページには遷移できているのに、何故ログインできないのか原因を掴めなかった。原因とその解決方法
結論を述べると、Userモデルの
authenticates_with_sorcery!
という、sorceryを使用する上で必要な記述がなぜか抜け漏れていたため(まさかすぎて全然気づけなかった)。
その抜け漏れを発見するまでのデバッグ処理のプロセスが勉強になったので、自分のために以下に記していく。解決手順
app/controllers/oauths_controller.rb
のcallbackアクション内でデバッグを行い、if @user = login_from(provider)
の箇所でnext
を実行してみると、そこで例外が発生し、rescueで捕捉されていることが分かった。
→
if @user = login_from(provider)
で何か問題が発生していることが分かったため、その中身を掘り下げていく。ログのエラーで
undefined
として表示されていたload_from_provider
メソッドが使用されている箇所まで辿り着いた。
→このメソッドはuser_class.load_from_provider
という形で使われている。
→user_class
がnilになっているせいで、"undefined method load_from_provider"となっているのかも?
→user_class
をデバッグしたが、ちゃんとUserモデルは入っている(nilじゃない)。
load_from_provider
自体が使えない状態になっているのかも?
→そういえばsorceryの機能使うためのauthenticates_with_sorcery!
て定義があったはず。
→Userモデルを確認すると、削除されていた。この定義を足し戻すとfacebookログインが出来た。このように問題箇所と思われる範囲を徐々に狭めながら、原因を特定する力を身につけなければと思ったのであります。。
- 投稿日:2020-05-15T22:33:20+09:00
編集画面で更新ができない!?
An invalid form control with name='◯◯[image]' is not focusable. というエラー
編集画面(edit)ではnewの画面をコピーして使用することもあるかと思います。
私は今回コピペで作成し、動作確認をしていました。通常であれば問題なく動くはず。。。なのですがなぜか更新ボタンを押しても反応しない状態状態でした。
デベロッパーツールを見てみると、
An invalid form control with name='recipe[image]' is not focusable.
というエラーが出ていました。name='recipe[image]'のformコントローラーが使えない・・・?
該当コード
= form_with model: @recipe do |f| .new__main__upper-half__left#image_input = f.label :image, {class: 'new__main__upper-half__left__label'} do = f.file_field :image, {class: 'new__main__upper-half__left__label__input',required: "required"} %pre %i.fas.fa-camera.fa-lg クリックして画像を選んでください解決方法
入力必須のオプションである required: "required" を消す、だけ
editで生成されるfile_fieldは上手く初期値が拾えていないようです。
その結果、入力必須のオプションを使用しているとrails様は
『必須になってるけどそんなんないよ!?』
と戸惑ってしまう分けです。と言うわけで、file_fieldはrequired: "required"を付けないようにしましょう
- 投稿日:2020-05-15T21:40:20+09:00
uniqueness: scope を使ったユニーク制約方法の解説
uniqueness: scope を使ったユニーク制約方法の解説
uniqueness: scopeを利用して一意性検証をする方法について解説します。
目次
動作環境
OS : macOS Mojave 10.14.6
ruby : 2.6.3p62
rails : 5.2.4
実装例
class Label < ApplicationRecord has_many :labelings, dependent: :destroy belongs_to :user, optional: true validates :name, presence: true, uniqueness: { scope: :user } endユーザはタスクに紐付けるラベルを作成することができますが、各ユーザは同じ名前のラベルを作れないようにラベルモデルのnameカラムに一意性制約をつけています。
実行結果[5]を見るとRollbackしています.
[3] pry(main)> user = User.first [4] pry(main)> user.labels.create(name:'test-label') (0.2ms) BEGIN (1.6ms) COMMIT #同じユーザで同名のラベルを作成する [5] pry(main)> user.labels.create(name:'test-label') #Rollbackする (0.3ms)ROLLBACK #違うユーザで検証 [6] pry(main)> user2 = User.last [7] pry(main)> user2.labels.create(name:'test-label') (5.7ms) BEGIN #書き込み成功 (34.0ms) COMMIT解説
scope
を付けない場合,テーブル全体で一つの名前のラベル名しか保存できません。
validates :name, uniqueness:true
つまりscopeという文字通りscopeの中での一意性制約にするオプションです。
このことによって各ユーザごとに一意となるカテゴリを作成することができます。
複数のscope
またscopeは配列により複数作成することもできます。
scope は配列にして複数指定できます。
validates :name, uniqueness: { scope: [:group_id, :user_id] }
これでname, group_id, user_id の全てが同じデータは1件しか作成できないように制約できます.データベース側の制約
また上記だけでなくデータベース側にも制約を作成する場合は、以下のように両方のカラムにuniqueインデックスを作成します.
label.rbclass AddUniqueIndexToLabels < ActiveRecord::Migration def change add_index :labels, [:name, :user_id], unique: true end endおわりに
ActiveRecordの一意性検証の範囲指定について学びました.
- 投稿日:2020-05-15T21:36:19+09:00
GitHubとアプリケーションの紐付け方-チーム開発-
概要
この記事ではGitHubとアプリケーションの紐付け方を解説します。
また、その際にGitHub Desktopを活用していきます。方法
まず、ローカルリポジトリを作成します。
GitHub Desktopを開いて左上のCurrent Ripositoryをクリックします。
クリックして開いた画面のAddをクリックします。
すると下記のような画面が出てきます。
その画面内のChoose...をクリックするとファイルが選択できる画面が出てくるので自分が作成中のアプリケーションのフォルダを選択します。
そしてAdd Repositoryボタンをクリックします。
これでローカルリポジトリの作成完了です。もしAdd repositoryが押せない場合はターミナルの自分が作成中のディレクトリで
git init
を叩いてください。
そうすればクリックできるようになると思います。次に上記画面の左下にSummary(required)とあると思いますので、そこにinitial commit(初めてのコミットという意味)と入力して、Commit to masterボタンをクリックします。
これでコミットが完了したので最後ににリモートリポジトリを作成します。画面右上にPublic repositoryがあるのでそこをクリックします。するとリポジトリ名を決められたり、コードを公開するかしないかのチェックボックスが出てきます。場合に合わせて適宜変更してください。
Publish repositoryボタンをクリックしたらリモートレポジトリの作成完了です。
ブラウザでGithubのページにアクセスして自分の作成しているアプリケーションの名前があれば作成は成功しています。ここまででGitHubとアプリケーションの紐付けに関しては完了しています。
この記事を読んでいただきありがとうございました。
- 投稿日:2020-05-15T18:35:33+09:00
rails generate migrationをdockerで立ち上げたRailsで実行したい
はじめに
dockerでRailsを立ち上げたら、Railsコマンド使えなかった。困った。
これは完全なる自分用の備忘録です。既存DBのテーブルにカラムを追加したい
docker-compose exec app rails g migration AddSomeColumnsToHoge
app
はdockerで立ち上がってるものです。app
とかweb
とかそんな名前で使われることが多い。(知らんけど)
AddSomeColumnsToHoge
のHoge
はカラムを追加したいテーブル名にしました。これで
db/migrate
に20200xxxxxx_add_some_columns_to_Hoge.rb
ファイルが作られます。
中身を見ると20200xxxxxx_add_some_columns_to_Hoge.rbclass AddSomeColumnsToHygienists < ActiveRecord::Migration[5.2] def change end endこんな感じ。
ここに追加したいカラムを書いていく。20200xxxxxx_add_some_columns_to_Hoge.rbclass AddSomeColumnsToHygienists < ActiveRecord::Migration[5.2] def change add_column :テーブル名, :カラム名, :型 end endこんな感じ。
追加したいカラムが複数ある時は下に追記していく。型
型 意味 string 文字列 text 長い文字列 integer 整数 float 浮動小数 decimal 精度の高い小数 datetime 日時 timestamp タイムスタンプ time 時間 date 日付 binary バイナリデータ boolean Boolean 以上の型が指定できるみたい。
マイグレーションを実行
追加したいカラムがかけたらマイグレーションを実行
docker-compose exec app rake db:migrate
db/schema.rb
でカラムを追加したテーブルを確認すると、カラムが追加されてます!やったね!参考文献
- https://qiita.com/pon-san/items/c54aee04e56e6bb26eff
- https://qiita.com/zaru/items/cde2c46b6126867a1a64
最後に
dockerで立ち上げたらコマンドややこしいと個人的に感じたので備忘録。
今度は削除とか変更とかで躓きそう。
その時は追記します。
殴り書きなので、不備とかアドバイスとかいただけるとありがたいです。
書いておいてアレだけど、基本的には参考文献の記事を参考にすると解決します
- 投稿日:2020-05-15T17:42:52+09:00
RailsでRSSを生成した時の備忘録(RSS2.0準拠)
これなに
RailsでRSSを生成した時にちょっと困ったりして調べたのでまとめておく。
RSS::MakerとかBuilder::XmlMarkupとかあるけど、slimで書けばいいじゃない!と思ったけど結局builderでも書きました。
たぶんSmartNewsのSmartFormat仕様書の必須項目は埋めてあるはず。サンプルコード
postsテーブルの構造はわりと一般的だと思うので割愛します。
slim
rss.xml.slimdoctype xml rss version='2.0' xmlns:content='http://purl.org/rss/1.0/modules/content/' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:media='http://search.yahoo.com/mrss/' channel title タイトル link = Settings.url description ディスクリプション language ja pubDate = Time.zone.now.rfc822 copyright Copyright My Blog, Inc. All Right Reserved. - @posts.each do |post| item title = post.title link = post_url(post.permalink) guid = post.permalink description = post.description pubDate = post.published_at.rfc822 category = post.category.name content:encoded = cdata_section(render partial: 'posts', formats: :html, locals: { post: post }) media:thumbnail url=post.eyecatch_url dc:creator = post.author.namebuilder
rss.xml.builderxml.instruct! :xml, version: 1.0 xml.rss(version: 2.0, 'xmlns:content': 'http://purl.org/rss/1.0/modules/content/', 'xmlns:dc': 'http://purl.org/dc/elements/1.1/', 'xmlns:media': 'http://search.yahoo.com/mrss/') do xml.channel do xml.title 'タイトル' xml.link Settings.url xml.description 'ディスクリプション' xml.language 'ja' xml.pubDate Time.zone.now.rfc822 xml.copyright 'Copyright My Blog, Inc. All Right Reserved.' @posts.each do |post| xml.item do xml.title post.title xml.link post_url(post.permalink) xml.guid post.permalink xml.description post.description xml.pubDate post.published_at.rfc822 xml.category post.category.name xml.tag!('content:encoded') do xml.cdata! render partial: 'posts', formats: :html, locals: { post: post } end xml.tag!('media:thumbnail', url: post.eyecatch_url) xml.tag!('dc:creator', post.author.name) end end end endメモ
- xmlでコロン付きのタグを扱いたい時はxml.tag!を使う
- cdataにしたい場合、slimではcdata_sectionヘルパー、xmlではxml.cdata!を使う
- slimはもちろんbuilderでもpartialを呼べる
- 投稿日:2020-05-15T16:33:29+09:00
railsアプリの作成手順
①rails new
rails newコマンドは、Railsアプリケーションの土台を作るためのコマンド
$ rails _5.2.4_ new sample -d postgresql
②rails server
rails severはwebサーバーを起動させるコマンド
$ rails db:create
③rails new scaffold
簡単なCRUD機能をもつアプリを作成するコマンド
$ rails generate scaffold モデル名 カラム名:データ型
作成時にGemfileからjbuilder gemを削除するとJSON関連のコードを生成しない。
gem 'jbuilder
- 投稿日:2020-05-15T16:13:33+09:00
Rails6+Reactで付箋アプリっぽいページを作ってみた。3 (UI作成編)
この記事について
前回までRailsを利用してバックエンド側を作り込んでみたので、フロントエンド側の実装に入っていきたいと思います。
果たして、分量が多くなりすぎるのではないかと心配してますが、きっと多くなります。まずは基点となるコンポーネントを追加するのじゃ。
react-railsを使う場合、viewのテンプレートで以下のようにreact_component()メソッドを呼んで、Reactコンポーネントのレンダリングを行います。
<%= react_component(コンポーネント名, {propsを表すハッシュ}) %>
ここで、呼び出すコンポーネント名ですが、うちの職場で付箋がたくさん貼ってある場所といえば、ホワイトボードなので、WhiteBoardという名前で作ることにしました。
ということで、以下のコマンドで最初のコンポーネントを作ってみます。
shell# propsとしてtitleを受け取るWhiteBoardという名前のReactコンポーネントを作ってください。という意味です。 # propsは後から、色々追加になると思いますが、最初の段階ではなかなか思いつかないもの。。 bundle exec rails g react:component WhiteBoard title:stringうまくいったら、実際に表示できるのか試しておきます。
。。。そういえば、APIのことばかりやってて、UIのためのコントローラがない!!
ので、追加します。shell# コントローラ名もWhiteBoardとし、アクション名は適当に"main"としてみました。 bundle exec rails g controller WhiteBoard main続いて、作成されたmain.html.erbを以下のように書き換えて、WhiteBoardコンポーネントを呼び出してもらうようにします。
app/views/white_board/main.html.erb<%= react_component('WhiteBoard', { title: 'You can let others do your task' }) %>ここで、一応動作確認してみます。
今まで、テストで確認してましたが、画面でみてみないと不安な事ってありますね。shell# サーバを起動 bundle exec rails s起動できたら、localhost:3000/white_board/mainにアクセスしてみます。
おぉ、ちょっと時間かかったけど表示できた。
確認できたら、Ctrl+Cでサーバを停止しちゃいます。中身を書いていきます。
先ほどまでの手順でWhiteBoardコンポーネントというのを作成しています。
が、今回はさらに、以下のコンポーネントを追加します。
コンポーネント名 説明 UserBox ユーザ毎の枠(箱?) Sticky タスクを表示するもの(付箋) それぞれ、以下のコマンドで追加できます。
bundle exec rails g react:component UserBox
bundle exec rails g react:component Sticky
WhiteBoardコンポーネントの実装
WhiteBoardでは、主にAPIとのやりとりと、UserBoxの表示までを行うようにしてみました。
実装は以下のようになりました。(長いです。)WthiteBoard.jsimport React from "react" import PropTypes from "prop-types" // 自作コンポーネントはこのように呼び出せます。 import UserBox from "./UserBox" class WhiteBoard extends React.Component { // コンストラクタ constructor(props) { // おまじない super(props); // stateの初期化 this.state = { users: {}, loading: true, dropHandlers: {}, need_render: false }; // イベントハンドラのバインド this.dropHandlerRegister = this.dropHandlerRegister.bind(this); this.onTaskDrop = this.onTaskDrop.bind(this); } // コンポーネントがマウントされたらデータの取得にいきます。 componentDidMount() { this.getData(); } // need_renderがtrueの場合だけレンダリングを行うようにしました。 shouldComponentUpdate(nextProps, nextState){ if (nextState.need_render) { return true; } console.log("** skip rendering **"); return false; } // propsで指定されたURLに向かってユーザ毎のタスク一覧をくださいとリクエストを投げます。 getData() { fetch(this.props.user_tasks_url) .then((response) => response.json()) .then((json) => { // うまくいったら表示データを更新します。 this.setState({users: json.users, loading: false, need_render: true}); }) .catch((response) => { console.log('** error **'); }) } // ユーザの変更をDBに通知します。 callSwitchUser(task_id, user_id) { var switch_info = { switch_info: { task_id: task_id, user_id: user_id } }; // APIとして作成したswitch_userアクションを呼び出します。 // propでもらったCSRFトークンをリクエストヘッダに含めることで、更新リクエストを可能としています。 // エラー処理がログ吐くだけというお粗末なものですが、すいません。 fetch(this.props.switch_user_url, { method: "PUT", headers: { "Content-Type": "application/json; charset=utf-8", "X-CSRF-Token": this.props.secure_token }, body: JSON.stringify(switch_info) }) .then(response => response.json()) .then(json => console.log(JSON.stringify(json))) .catch(error_response => console.log(error_response)); } // 各UserBoxにStickyがドロップされた際の処理を登録する処理です。 dropHandlerRegister(user_id, func) { var handlers = this.state.dropHandlers; // 該当ユーザIDのハンドラが存在しなければ、stateに追加します。 // このstate変更による再レンダリングは不要なため、need_renerにはfalseを設定しておきます。 if ( ! handlers[user_id] ) { handlers[user_id] = func; this.setState({dropHandlers: handlers, need_render: false}); } } // Stickyがドロップされた際のイベント処理です。 onTaskDrop(prev_user_id, next_user_id, task) { // 各UserBoxのハンドラを呼び出します。 Object.keys(this.state.dropHandlers).map((key) => { this.state.dropHandlers[key](prev_user_id, next_user_id, task); }); // swich_userアクションを呼んで更新を反映します。 this.callSwitchUser(task.id, next_user_id); } // レンダラーです。 // ユーザ毎にUserBoxを生成しています。 // dropHandlerRegsterはonTaskDrop時に呼び出す関数を登録してもらうための関数です。(わかりづらくてすみません。。) // onTaskDropは、UserBox内でStickyがドロップされた時に呼び出(CallBack)してもらう関数です。 // ちなみに、ループして同じコンポーネントをいくつも使う時は、key属性に一意の値を設定しなければなりませんので、ここではユーザIDを設定しています。 render () { return ( <React.Fragment> <div id="WhiteBoardTitle">{this.props.title}</div> { ! this.state.loading && this.state.users.map((user) => <UserBox user={user} key={user.id} dropHandlerRegister={this.dropHandlerRegister} onTaskDrop={this.onTaskDrop} /> )} </React.Fragment> ); } } // この下に型チェック用の記述がありましたが、削除してしまいました。 export default WhiteBoardこの実装に合わせて、viewから呼び出す際の引数も以下のように変更しました。
ユーザ一覧取得APIとユーザ切り替えAPIのURLを渡すようにしています。
さらに、CSRFトークンも渡しています。(form_authenticity_token()が出力してくれます。)app/views/white_board/main.html.erb<%= react_component('WhiteBoard', { title: 'You can let others do your task', user_tasks_url: api_users_user_task_list_url(:json), switch_user_url: api_tasks_switch_user_url(:json), secure_token: form_authenticity_token }) %>UserBoxコンポーネントの実装
続いて、ユーザ事のタスク一覧を表示するUserBoxの実装です。
ここは、付箋が貼り付けられた(Stickyがドロップされた)場合の処理がちょっとトリッキーです。app/javascript/components/UserBox.jsimport React from "react" import PropTypes from "prop-types" import Sticky from "./Sticky" // ユーザ毎の箱を表示します。 class UserBox extends React.Component { // コンストラクタです。 constructor(props) { // おまじない。 super(props); // 一つもタスクを持たないユーザの場合、user.tasksがnullになってしまうため、 // nullの場合は空のハッシュを割り当てています。 var tasks = this.props.user.tasks ? this.props.user.tasks : {}; // タスクのリストをstateに突っ込みます。 this.state = { tasks: tasks }; // イベントハンドラのバインド this.onDrop = this.onDrop.bind(this); this.updateTaskList = this.updateTaskList.bind(this); this.preventDefault = this.preventDefault.bind(this); // WhiteBoardに対して、自身のupdateTaskList関数を登録します。 // (これにより、タスクの所有者変更を通知してもらおうという算段です。) this.props.dropHandlerRegister(this.props.user.id, this.updateTaskList); } // ドラッグオーバー時の通常イベント処理を抑止するための処理です。 // これやらないとドロップできないようです。 preventDefault(event) { event.preventDefault(); } // ドロップイベント処理 onDrop(event) { // dataTransferにセットされたデータ(変更前ユーザIDと対象タスク情報)を取得します。 var dropData = JSON.parse(event.dataTransfer.getData('text/plain')); // WhiteBoardのonTaskDropを呼び出してあげます。 // こうすると、WhiteBoardからupdateTaskListが呼ばれるのでした。 this.props.onTaskDrop(dropData.now_user_id, this.props.user.id, dropData.task); } // タスク一覧更新処理 // prev_user_id: 以前のユーザID // next_user_id: 変更後のユーザID // task: 対象タスク updateTaskList(prev_user_id, next_user_id, task) { // ユーザIDが変わらない時は何もしません。 if (prev_user_id == next_user_id) { return; } // 以前のユーザIDと自分のユーザIDが等しい時。 // それは、自分からそのタスクを削除する時です。 if (prev_user_id == this.props.user.id) { // 自分のタスクを押し付けたので、自分のタスク一覧から削除しよう。 this.deleteTask(task.id); } // 変更後のユーザIDが自身のユーザIDの時。 // それはあなたに仕事が押し付けられた時です。 if (next_user_id == this.props.user.id) { // 押し付けられた仕事を自分のタスク一覧に追加しよう。 this.addTask(task); } } // タスク削除処理 deleteTask(task_id) { var tasks = this.state.tasks; // 削除対象IDのタスクをリストから削除します。 // タスク一覧をKey-Value形式で持ってたのはこのためです。 // ハッシュにしておくことで検索する手間を省いてます。 delete tasks[task_id]; // stateを更新します。(これで再描画してもらおう) this.setState({tasks: tasks}); } // タスク追加処理 addTask(task) { var tasks = this.state.tasks; // イヤイヤながらタスクを追加します。 tasks[task.id] = task; // stateを更新します。(これで再描画してもらおう) this.setState({tasks: tasks}); } // レンダラーです。 render () { return ( <React.Fragment> <div id={"user-" + this.props.user.id} className="UserBox" onDrop={this.onDrop} onDragOver={this.preventDefault} > <div className="UserName">{this.props.user.name}</div> <div className="TaskArea"> { Object.keys(this.state.tasks).map((key) => <Sticky user_id={this.props.user.id} task={ this.state.tasks[key] } key={ key } /> ) } </div> </div> </React.Fragment> ); } } export default UserBoxStickyコンポーネントの実装
最後にタスクの内容を表示するStickyの実装です。
大事なのは、ドラッグ開始時にdataTranser.setDataを行っていることです。
これにより、drop時のデータ引継ぎができます。app/javascript/components/Sticky.jsimport React from "react" import PropTypes from "prop-types" class Sticky extends React.Component { // コンストラクタです。 constructor(props) { // おまじないです。 super(props); // ドラッグ開始イベントハンドラをバインドします。 this.onDragStart = this.onDragStart.bind(this); } // ドラッグ開始イベントハンドラ onDragStart(event) { // ドラッグを開始したら、現在のpropsに設定されたユーザIDとタスク情報をJSON形式のテキストデータに直してdataTransferにセットします。 // text/plainですが、JSON.stringify()を使うことでハッシュデータを引き継ぐことができます。 // (https://stackoverflow.com/questions/9533585/drag-drop-html-5-jquery-e-datatransfer-setdata-with-json) var dragData = { now_user_id: this.props.user_id, task: this.props.task }; event.dataTransfer.setData("text/plain", JSON.stringify(dragData)); } // レンダラです。 render () { return ( <React.Fragment> <div id={"task-" + this.props.task.id} className="Sticky" draggable="true" onDragStart={this.onDragStart} > <div className="TaskTitle">{this.props.task.title}</div> <div className="TaskDescription">{this.props.task.description}</div> <div className="TaskDueDate">{this.props.task.due_date}</div> </div> </React.Fragment> ); } } export default Sticky軽く動作確認
まずは実際に画面表示してみる。
ここまでで、DBからユーザ情報とそれぞれのタスク情報を取得して画面に表示する流れと、
タスクの書かれた付箋をドラッグドロップして所有者を変更する流れが出来上がっているはずです。
ということで、動作確認用にサーバを起動(bundle exec rails s
)して、テスト用ページ(localhost:3000/white_board/main)にアクセスしてみましょう。・・・あ、味気ねぇが、一応ドラッグドロップも動きます。
seedでデータを用意しておいたので、こういうときに簡単に確認できますね。テストしてみます。
ドラッグドロップなんてテストできるの?
Rails環境にはCapybaraが住んでいるのでできます。実際のUIを使ってテストするので、systemテスト扱いかなぁ、とシステムテストを追加します。
shell# WhiteBoardのテストということで、whiteboardsにしてみました。 bundle exec rails g system_test whiteboards以下のテストを追加しました。
test/system/whiteboards_test.rbtest "sticky is able to drag and drop" do # fixutreで登録したデータを取得しておきます。 alice = users(:alice); bob = users(:bob); task2 = tasks(:task2); # divのidを設定します。(idを設定しておくことで、テストが格段に楽になりますね。) task2_id = "task-" + task2.id.to_s; bob_id = "user-" + bob.id.to_s; alice_id = "user-" + alice.id.to_s; # white_board/mainを開く。 visit white_board_main_url; # 一応各ユーザのタスク数を確認しておきます。 assert_equal(1, alice.tasks.count); assert_equal(0, bob.tasks.count); # task2を表示しているエレメントを取得 div_task2 = find(id: task2_id); # bobのUserBoxを表示しているエレメントを取得 div_bob = find(id: bob_id); # aliceのUserBoxを表示しているエレメントを取得 div_alice = find(id: alice_id); # aliceのタスクは1つ、bobのタスクはなし。 div_alice.assert_selector("div", class: "Sticky", count: 1); div_bob.assert_selector("div", class: "Sticky", count: 0); # alice said "Hey bob, I think you want to do my job 'task2'." # drag_to(ドラッグ先エレメント) div_task2.drag_to(div_bob); # タスク所有者が入れ替わったことを確認 div_alice.assert_selector("div", class: "Sticky", count: 0); div_bob.assert_selector("div", class: "Sticky", count: 1); # 本当かどうか、スクリーンショットを撮ってもらう。 take_screenshot(); # データをリロードしてDBにも反映されたことを確認 alice.tasks.reload; bob.tasks.reload; assert_equal(0, alice.tasks.count); assert_equal(1, bob.tasks.count); endUIがテストできると言うのは素晴らしいですね。
それにしても、見た目が寂しいので次回はスタイルを直してみます。
- 投稿日:2020-05-15T16:13:33+09:00
Rails6+Reactで付箋アプリっぽいページを作ってみた。3 (UI作成編1)
この記事について
前回までRailsを利用してバックエンド側を作り込んでみたので、フロントエンド側の実装に入っていきたいと思います。
果たして、分量が多くなりすぎるのではないかと心配してますが、きっと多くなります。関連する記事
書いているうちに分量がすごくなって記事を分割したので、リンク先をまとめておきます。
その1(環境構築〜モデル作成編)
その2(API作成編)
その3(UI作成編1)
その4(UI作成編2)
おまけ(モデルのテスト編)まずは基点となるコンポーネントを追加するのじゃ。
react-railsを使う場合、viewのテンプレートで以下のようにreact_component()メソッドを呼んで、Reactコンポーネントのレンダリングを行います。
<%= react_component(コンポーネント名, {propsを表すハッシュ}) %>
ここで、呼び出すコンポーネント名ですが、うちの職場で付箋がたくさん貼ってある場所といえば、ホワイトボードなので、WhiteBoardという名前で作ることにしました。
ということで、以下のコマンドで最初のコンポーネントを作ってみます。
shell# propsとしてtitleを受け取るWhiteBoardという名前のReactコンポーネントを作ってください。という意味です。 # propsは後から、色々追加になると思いますが、最初の段階ではなかなか思いつかないもの。。 bundle exec rails g react:component WhiteBoard title:stringうまくいったら、実際に表示できるのか試しておきます。
。。。そういえば、APIのことばかりやってて、UIのためのコントローラがない!!
ので、追加します。shell# コントローラ名もWhiteBoardとし、アクション名は適当に"main"としてみました。 bundle exec rails g controller WhiteBoard main続いて、作成されたmain.html.erbを以下のように書き換えて、WhiteBoardコンポーネントを呼び出してもらうようにします。
app/views/white_board/main.html.erb<%= react_component('WhiteBoard', { title: 'You can let others do your task' }) %>ここで、一応動作確認してみます。
今まで、テストで確認してましたが、画面でみてみないと不安な事ってありますね。shell# サーバを起動 bundle exec rails s起動できたら、localhost:3000/white_board/mainにアクセスしてみます。
おぉ、ちょっと時間かかったけど表示できた。
確認できたら、Ctrl+Cでサーバを停止しちゃいます。中身を書いていきます。
先ほどまでの手順でWhiteBoardコンポーネントというのを作成しています。
が、今回はさらに、以下のコンポーネントを追加します。
コンポーネント名 説明 UserBox ユーザ毎の枠(箱?) Sticky タスクを表示するもの(付箋) それぞれ、以下のコマンドで追加できます。
bundle exec rails g react:component UserBox
bundle exec rails g react:component Sticky
WhiteBoardコンポーネントの実装
WhiteBoardでは、主にAPIとのやりとりと、UserBoxの表示までを行うようにしてみました。
実装は以下のようになりました。(長いです。)WthiteBoard.jsimport React from "react" import PropTypes from "prop-types" // 自作コンポーネントはこのように呼び出せます。 import UserBox from "./UserBox" class WhiteBoard extends React.Component { // コンストラクタ constructor(props) { // おまじない super(props); // stateの初期化 this.state = { users: {}, loading: true, dropHandlers: {}, need_render: false }; // イベントハンドラのバインド this.dropHandlerRegister = this.dropHandlerRegister.bind(this); this.onTaskDrop = this.onTaskDrop.bind(this); } // コンポーネントがマウントされたらデータの取得にいきます。 componentDidMount() { this.getData(); } // need_renderがtrueの場合だけレンダリングを行うようにしました。 shouldComponentUpdate(nextProps, nextState){ if (nextState.need_render) { return true; } console.log("** skip rendering **"); return false; } // propsで指定されたURLに向かってユーザ毎のタスク一覧をくださいとリクエストを投げます。 getData() { fetch(this.props.user_tasks_url) .then((response) => response.json()) .then((json) => { // うまくいったら表示データを更新します。 this.setState({users: json.users, loading: false, need_render: true}); }) .catch((response) => { console.log('** error **'); }) } // ユーザの変更をDBに通知します。 callSwitchUser(task_id, user_id) { var switch_info = { switch_info: { task_id: task_id, user_id: user_id } }; // APIとして作成したswitch_userアクションを呼び出します。 // propでもらったCSRFトークンをリクエストヘッダに含めることで、更新リクエストを可能としています。 // エラー処理がログ吐くだけというお粗末なものですが、すいません。 fetch(this.props.switch_user_url, { method: "PUT", headers: { "Content-Type": "application/json; charset=utf-8", "X-CSRF-Token": this.props.secure_token }, body: JSON.stringify(switch_info) }) .then(response => response.json()) .then(json => console.log(JSON.stringify(json))) .catch(error_response => console.log(error_response)); } // 各UserBoxにStickyがドロップされた際の処理を登録する処理です。 dropHandlerRegister(user_id, func) { var handlers = this.state.dropHandlers; // 該当ユーザIDのハンドラが存在しなければ、stateに追加します。 // このstate変更による再レンダリングは不要なため、need_renerにはfalseを設定しておきます。 if ( ! handlers[user_id] ) { handlers[user_id] = func; this.setState({dropHandlers: handlers, need_render: false}); } } // Stickyがドロップされた際のイベント処理です。 onTaskDrop(prev_user_id, next_user_id, task) { // 各UserBoxのハンドラを呼び出します。 Object.keys(this.state.dropHandlers).map((key) => { this.state.dropHandlers[key](prev_user_id, next_user_id, task); }); // swich_userアクションを呼んで更新を反映します。 this.callSwitchUser(task.id, next_user_id); } // レンダラーです。 // ユーザ毎にUserBoxを生成しています。 // dropHandlerRegsterはonTaskDrop時に呼び出す関数を登録してもらうための関数です。(わかりづらくてすみません。。) // onTaskDropは、UserBox内でStickyがドロップされた時に呼び出(CallBack)してもらう関数です。 // ちなみに、ループして同じコンポーネントをいくつも使う時は、key属性に一意の値を設定しなければなりませんので、ここではユーザIDを設定しています。 render () { return ( <React.Fragment> <div id="WhiteBoardTitle">{this.props.title}</div> { ! this.state.loading && this.state.users.map((user) => <UserBox user={user} key={user.id} dropHandlerRegister={this.dropHandlerRegister} onTaskDrop={this.onTaskDrop} /> )} </React.Fragment> ); } } // この下に型チェック用の記述がありましたが、削除してしまいました。 export default WhiteBoardこの実装に合わせて、viewから呼び出す際の引数も以下のように変更しました。
ユーザ一覧取得APIとユーザ切り替えAPIのURLを渡すようにしています。
さらに、CSRFトークンも渡しています。(form_authenticity_token()が出力してくれます。)app/views/white_board/main.html.erb<%= react_component('WhiteBoard', { title: 'You can let others do your task', user_tasks_url: api_users_user_task_list_url(:json), switch_user_url: api_tasks_switch_user_url(:json), secure_token: form_authenticity_token }) %>UserBoxコンポーネントの実装
続いて、ユーザ事のタスク一覧を表示するUserBoxの実装です。
ここは、付箋が貼り付けられた(Stickyがドロップされた)場合の処理がちょっとトリッキーです。app/javascript/components/UserBox.jsimport React from "react" import PropTypes from "prop-types" import Sticky from "./Sticky" // ユーザ毎の箱を表示します。 class UserBox extends React.Component { // コンストラクタです。 constructor(props) { // おまじない。 super(props); // 一つもタスクを持たないユーザの場合、user.tasksがnullになってしまうため、 // nullの場合は空のハッシュを割り当てています。 var tasks = this.props.user.tasks ? this.props.user.tasks : {}; // タスクのリストをstateに突っ込みます。 this.state = { tasks: tasks }; // イベントハンドラのバインド this.onDrop = this.onDrop.bind(this); this.updateTaskList = this.updateTaskList.bind(this); this.preventDefault = this.preventDefault.bind(this); // WhiteBoardに対して、自身のupdateTaskList関数を登録します。 // (これにより、タスクの所有者変更を通知してもらおうという算段です。) this.props.dropHandlerRegister(this.props.user.id, this.updateTaskList); } // ドラッグオーバー時の通常イベント処理を抑止するための処理です。 // これやらないとドロップできないようです。 preventDefault(event) { event.preventDefault(); } // ドロップイベント処理 onDrop(event) { // dataTransferにセットされたデータ(変更前ユーザIDと対象タスク情報)を取得します。 var dropData = JSON.parse(event.dataTransfer.getData('text/plain')); // WhiteBoardのonTaskDropを呼び出してあげます。 // こうすると、WhiteBoardからupdateTaskListが呼ばれるのでした。 this.props.onTaskDrop(dropData.now_user_id, this.props.user.id, dropData.task); } // タスク一覧更新処理 // prev_user_id: 以前のユーザID // next_user_id: 変更後のユーザID // task: 対象タスク updateTaskList(prev_user_id, next_user_id, task) { // ユーザIDが変わらない時は何もしません。 if (prev_user_id == next_user_id) { return; } // 以前のユーザIDと自分のユーザIDが等しい時。 // それは、自分からそのタスクを削除する時です。 if (prev_user_id == this.props.user.id) { // 自分のタスクを押し付けたので、自分のタスク一覧から削除しよう。 this.deleteTask(task.id); } // 変更後のユーザIDが自身のユーザIDの時。 // それはあなたに仕事が押し付けられた時です。 if (next_user_id == this.props.user.id) { // 押し付けられた仕事を自分のタスク一覧に追加しよう。 this.addTask(task); } } // タスク削除処理 deleteTask(task_id) { var tasks = this.state.tasks; // 削除対象IDのタスクをリストから削除します。 // タスク一覧をKey-Value形式で持ってたのはこのためです。 // ハッシュにしておくことで検索する手間を省いてます。 delete tasks[task_id]; // stateを更新します。(これで再描画してもらおう) this.setState({tasks: tasks}); } // タスク追加処理 addTask(task) { var tasks = this.state.tasks; // イヤイヤながらタスクを追加します。 tasks[task.id] = task; // stateを更新します。(これで再描画してもらおう) this.setState({tasks: tasks}); } // レンダラーです。 render () { return ( <React.Fragment> <div id={"user-" + this.props.user.id} className="UserBox" onDrop={this.onDrop} onDragOver={this.preventDefault} > <div className="UserName">{this.props.user.name}</div> <div className="TaskArea"> { Object.keys(this.state.tasks).map((key) => <Sticky user_id={this.props.user.id} task={ this.state.tasks[key] } key={ key } /> ) } </div> </div> </React.Fragment> ); } } export default UserBoxStickyコンポーネントの実装
最後にタスクの内容を表示するStickyの実装です。
大事なのは、ドラッグ開始時にdataTranser.setDataを行っていることです。
これにより、drop時のデータ引継ぎができます。app/javascript/components/Sticky.jsimport React from "react" import PropTypes from "prop-types" class Sticky extends React.Component { // コンストラクタです。 constructor(props) { // おまじないです。 super(props); // ドラッグ開始イベントハンドラをバインドします。 this.onDragStart = this.onDragStart.bind(this); } // ドラッグ開始イベントハンドラ onDragStart(event) { // ドラッグを開始したら、現在のpropsに設定されたユーザIDとタスク情報をJSON形式のテキストデータに直してdataTransferにセットします。 // text/plainですが、JSON.stringify()を使うことでハッシュデータを引き継ぐことができます。 // (https://stackoverflow.com/questions/9533585/drag-drop-html-5-jquery-e-datatransfer-setdata-with-json) var dragData = { now_user_id: this.props.user_id, task: this.props.task }; event.dataTransfer.setData("text/plain", JSON.stringify(dragData)); } // レンダラです。 render () { return ( <React.Fragment> <div id={"task-" + this.props.task.id} className="Sticky" draggable="true" onDragStart={this.onDragStart} > <div className="TaskTitle">{this.props.task.title}</div> <div className="TaskDescription">{this.props.task.description}</div> <div className="TaskDueDate">{this.props.task.due_date}</div> </div> </React.Fragment> ); } } export default Sticky軽く動作確認
まずは実際に画面表示してみる。
ここまでで、DBからユーザ情報とそれぞれのタスク情報を取得して画面に表示する流れと、
タスクの書かれた付箋をドラッグドロップして所有者を変更する流れが出来上がっているはずです。
ということで、動作確認用にサーバを起動(bundle exec rails s
)して、テスト用ページ(localhost:3000/white_board/main)にアクセスしてみましょう。・・・あ、味気ねぇが、一応ドラッグドロップも動きます。
seedでデータを用意しておいたので、こういうときに簡単に確認できますね。テストしてみます。
ドラッグドロップなんてテストできるの?
Rails環境にはCapybaraが住んでいるのでできます。実際のUIを使ってテストするので、systemテスト扱いかなぁ、とシステムテストを追加します。
shell# WhiteBoardのテストということで、whiteboardsにしてみました。 bundle exec rails g system_test whiteboards以下のテストを追加しました。
test/system/whiteboards_test.rbtest "sticky is able to drag and drop" do # fixutreで登録したデータを取得しておきます。 alice = users(:alice); bob = users(:bob); task2 = tasks(:task2); # divのidを設定します。(idを設定しておくことで、テストが格段に楽になりますね。) task2_id = "task-" + task2.id.to_s; bob_id = "user-" + bob.id.to_s; alice_id = "user-" + alice.id.to_s; # white_board/mainを開く。 visit white_board_main_url; # 一応各ユーザのタスク数を確認しておきます。 assert_equal(1, alice.tasks.count); assert_equal(0, bob.tasks.count); # task2を表示しているエレメントを取得 div_task2 = find(id: task2_id); # bobのUserBoxを表示しているエレメントを取得 div_bob = find(id: bob_id); # aliceのUserBoxを表示しているエレメントを取得 div_alice = find(id: alice_id); # aliceのタスクは1つ、bobのタスクはなし。 div_alice.assert_selector("div", class: "Sticky", count: 1); div_bob.assert_selector("div", class: "Sticky", count: 0); # alice said "Hey bob, I think you want to do my job 'task2'." # drag_to(ドラッグ先エレメント) div_task2.drag_to(div_bob); # タスク所有者が入れ替わったことを確認 div_alice.assert_selector("div", class: "Sticky", count: 0); div_bob.assert_selector("div", class: "Sticky", count: 1); # 本当かどうか、スクリーンショットを撮ってもらう。 take_screenshot(); # データをリロードしてDBにも反映されたことを確認 alice.tasks.reload; bob.tasks.reload; assert_equal(0, alice.tasks.count); assert_equal(1, bob.tasks.count); endUIがテストできると言うのは素晴らしいですね。
それにしても、見た目が寂しいので次回はスタイルを直してみます。
- 投稿日:2020-05-15T16:08:13+09:00
Rails6+Reactで付箋アプリっぽいページを作ってみた。1 (環境構築〜モデル作成編)
手段のために目的を。。
職場で(家庭でも?)やらなきゃいけないこと(タスク)を付箋に書いて、見やすいところに貼り付けて管理している人って結構多いと思います。
そして、勝手に他人の机に自分の付箋を貼り付けて、仕事を押し付けたり、有料で取引したり。。。そんなやりとりをWebアプリでできたら、メンバーの持っているタスクなどが共有できて面白いかも?
ということで、Reactの勉強がてら実際に作ってみた過程を共有させていただきたいと思います。・・・というか、「RailsでReactを使って何かやってみたい」という手段を目的としているので、実用性は度外視。。。
関連する記事
書いているうちに分量がすごくなって記事を分割したので、リンク先をまとめておきます。
その1(環境構築〜モデル作成編)
その2(API作成編)
その3(UI作成編1)
その4(UI作成編2)
おまけ(モデルのテスト編)使用環境
データベース周りの作り込みで楽したいので、Railsを利用しました。
この記事で使った環境は、以下の通りです。
component version Rails 6.0.2 react-rails 2.6.1 Ruby 2.6.5 環境構築にあたっては、こちらを参考にしました。
GitHub:react-railsのページついでに知ったこと
JSONでのデータ出力でハマる
number型の属性を渡せなくて困るラフなモデル設計
適当ですが、こんな設計になりますかね。
ユーザがいなくなったらタスクもいなくなる形なので不自然かもしれませんが。。。
環境構築
申し訳ありませんが、インターネット接続が可能で、railsが利用できる環境であることを前提に記述させていただきます。
shell# アプリケーションを作成(今回はstickiesAppという名前にしてみました。) rails new stickiesApp # 作成されたアプリケーションディレクトリに移動 cd stickiesApp # react-railsのReadMeに従いGemfileを編集(今回はRails6だったので、gem 'react-rails'の追加のみ) # bundle installします。 bundle install # webpackerの準備(asset pipelineでのやり方をお探しの方、すみません。。) bundle exec rails webpacker:install bundle exec rails webpacker:install:react bundle exec rails generate react:installラフなUI設計
ユーザ毎に箱があって、その中に付箋があるってイメージですね。
そうそう、ちょうどこんな風に。。って、アクティビティ図かい!!(すいません、近くにastah*があったもので。。)
モデルを準備する。
先ほどのかなりラフなモデル設計から、ユーザモデルとタスクモデルを作ってみます。
ユーザモデル
どちらを先に作るか悩みましたが、ユーザから作ってみました。
ぶっちゃけ、どっちでも良いのだと思いますが。shell# 「ユーザ名」に対応する文字列型の要素を一つ持つ"User"モデルを作成します。 bundle exec rails g model User name:stringタスクモデル
続いてタスクモデルですね。
こちらは、どのユーザが所有しているかを示すための、user_id列が必要になるので、それも加味した形で作ります。shell# 「タイトル」「説明」「期限」に対応する列と、ユーザモデルと関連付けるための列を作成します。 bundle exec rails g model Task title:string description:text due_date:datetime user:belongs_toモデルの関連付けを追加
「ユーザは複数のタスクを持つ」というモデル設計に合わせて、user.rbを以下のように修正します。
app/models/user.rbclass User < ApplicationRecord has_many :tasks # taskをたくさん持ってますよと教えてあげます。 endちなみに、タスクモデルのほうは、自動的に以下のような内容になります。(便利♪)
app/models/task.rbclass Task < ApplicationRecord belongs_to :user endマイグレーションを実行
さて、準備も整ったのでマイグレーションを実行してしまいます。
shell# マイグレーションの実行です。(すぐに忘れるコマンドの一つ。。) bundle exec rails db:migrate初期データの登録
ユーザやタスクの登録処理まで書いてると長くなりすぎるので、テスト的に使えそうな初期データを予め登録してしまおう!ということで、初期データ用のseedを準備します。
db/seed.rb# ユーザの作成 user0 = User.create(name: 'Not Assigned'); user1 = User.create(name: 'User001'); user2 = User.create(name: 'User002'); # タスクの作成 Task.create(title: 'task001', description: '0001', due_date: Date.new(2020, 4, 30), user: user0); Task.create(title: 'task002', description: '0002', due_date: Date.new(2020, 4, 30), user: user1); Task.create(title: 'task003', description: '0003', due_date: Date.new(2020, 4, 30), user: user2); Task.create(title: 'task004', description: '0004', due_date: Date.new(2020, 4, 30), user: user1);準備できたらDBに投入してみます。
shell# 初期データの投入(間違ってdb:seedsと書いて失敗するのは私だけ?) bundle exec rails db:seed。。。結果がいまいち分からない(何も言わずにコマンドが終わる)ので、DBを直接確認しておきました。
shell# sqlite3でDBを開く sqlite3 db/development.sqlite3 sqlite> select * from users; 1|Not Assigned|2020-05-13 00:21:00.654477|2020-05-13 00:21:00.654477 2|User001|2020-05-13 00:21:00.662086|2020-05-13 00:21:00.662086 3|User002|2020-05-13 00:21:00.668647|2020-05-13 00:21:00.668647 sqlite> select * from tasks; 1|task001|0001|2020-04-30 00:00:00|1|2020-05-13 00:21:00.766118|2020-05-13 00:21:00.766118 2|task002|0002|2020-04-30 00:00:00|2|2020-05-13 00:21:00.772826|2020-05-13 00:21:00.772826 sqlite> .quit # おお、入ってた!モデルができたら。。テストですね!
テストについては多いのでこちらにまとめました。
ので、ご参考にしていただければ。
とにかく、言いたいことは、テスト作らずに進めると痛い目に遭うということ。
Rubyにしろ、Railsにしろバージョンアップしたいとき、テストが作られてないと影響確認すらできませんからね。
- 投稿日:2020-05-15T15:14:45+09:00
部分テンプレート
部分テンプレートとは
viewファイルで同じHTML構造の部分を共通化したもの
例えばwebサイトのヘッダー部分や、ブログ記事の一覧表示画面の各投稿のレイアウトなども多くが共通していますよね
使用するメリット
・共通化することで何度も同じコードを書く必要がない
・修正する際の修正箇所が少なく済む
・他のviewファイルで使い回すことができる
使用方法
まずは共通化している部分を部分テンプレートのファイルに書き換えます。
ファイル名の前には必ず「 _ (アンダーバー)」を書きます
「_post.html.erb」 など。
あとは呼び出しをするだけですが、この際 render メソッドを使用します。
index.html.erb<% render partial: "post" %>partial オプションは部分テンプレート名の指定をしています。
また、部分テンプレート内で変数を扱いたい場合は locals オプションを使用します。
<% render partial: "部分テンプレート名", locals: { 変数: 値 } %>
- 投稿日:2020-05-15T14:03:20+09:00
【Rails】Bundlerのバージョンを2.0.2に戻したい(開発中)
開発中にBundlerのバージョンを2.0.2に戻したいそんな時があれば、、、
下記コマンドで1発です
gem uninstall bundler && gem install bundler -v 2.0.2 && rm -rf Gemfile.lock && rm -rf vendor/bundle && bundleこのコマンドがやっていること
- 現在使用しているbundlerを削除
- 新しくbundlerのバージョン2.0.2をインストール
- Gemfile.lockを削除
- vendor/bundleを削除
- bundle install
参考
https://qiita.com/Nedward/items/ee70b8196398dc1e3017
https://www.slideshare.net/cuzic/rubygems-bundler
https://qiita.com/egopro/items/aba12261c053eecd6d19
- 投稿日:2020-05-15T14:01:02+09:00
deviseによるユーザー管理
deviceとは
Rubyのライブラリ(拡張機能)であるgemの一つ。
アプリケーションにおけるユーザ-新規登録・ログイン機能を簡単に実装できる。
導入方法
Gemfileに記載
gem 'devise'ターミナルのアプリケーションのディレクトリにて
bundle installdeviseの設定ファイルの作成
rails g devise:installdeviseのモデル作成用コマンドでuserモデルを作成
このコマンドで、ファイルの新規作成と、ユーザーのログイン・新規登録で必要なルーティングが生成される(routes.rbにdevise_for :usersが自動追記されるため)
rails g devise userマイグレーションファイルの実行
rails db:migratedeviseに対応したビューファイルの作成
これでシンプルな見た目の新規登録画面とログイン画面が生成されます
新規登録画面はapp/views/devise/registrations/new.html.erb、
ログイン画面のビューはapp/views/devise/sessions/new.html.erb が対応しています。rails g devise:views以上で新規ユーザー登録とログイン機能の基本的な実装はできます。
deviseを導入することで使用できるメソッドや、deviseによるユーザー登録情報の項目を追加したい場合(現状ではEメールと、パスワードのみの登録しかできない)は調べてみて下さい
- 投稿日:2020-05-15T13:17:32+09:00
【Rails】ActiveRecord::PendingMigrationErrorが出たときの対処法
エラー内容
ActiveRecord::PendingMigrationError
環境
- Rails 6.0.3
- psql (PostgreSQL) 12.2
概要
- このアプリケーションに対応するマイグレーションファイルをmigrateし忘れてますよ、というエラー
- 再度migrateすることで解決
原因
- db:create後にmigrateし忘れている
- すでに以前同じ名前でRailsアプリを作成とmigrateまでしていて、(何らかの事情で)アプリを消して、再度同じアプリ名で作成してmigrateしようとしたらエラーが発生
対策
1. db:create後にmigrateし忘れている
この場合は作成したアプリを
bundle exec rake db:create
でDB作成後にmigrateし忘れているだけなので(マイグレーションファイルを変更するだけではmigrateされない)、bundle exec rake db:migrateをターミナルで入力することで解決
2.すでに以前同じ名前でRailsアプリを作成とmigrateまでしていて、(何らかの事情で)アプリを消して、再度同じアプリ名で作成してmigrateしようとしたらエラーが発生
私はこのパターンで「アプリ消してるのに何でmigrateできないの?」とちょっと戸惑っていました?
Migrationファイル自体は(同じ名前なので)存在するが、今回作成したアプリとMigrationファイルが関連づけられていないことが原因なので、一度すでに作成されていたMigrationファイルを下記コマンドでリセットします。rails db:migrate:resetmigrateをリセットできたので、再度migrateします。
bundle exec rake db:migrateサーバーを再起動すれば解決です!
rails s余談ですが、
db:create
やdb:migrate
などデータベース関連の更新でうまくいかない時はサーバーをCtrl+C
で閉じてからやることをおすすめします(結構このパターンでのエラーもあるので注意)。※migrateの状態が分からない時に調べるコマンド
rails db:migrate:status
- 投稿日:2020-05-15T11:48:29+09:00
rails データの保存の書き方
書籍などは
def create @post = Post.new(post_params) if @post.save flash[:success] = "保存に成功" redirect_to posts_path else flash[:error] = "保存に失敗" redirect_to new_post_path end end実務では、調査の為の例外のログの吐き出しと、slcakなどの通知ツールする所までがセットで、保存機能が完結する
def create @post = Post.new(post_params) @post.save! # 例外を発生させる flash[:success] = "保存に成功" redirect_to posts_path rescue StandardError => e logger.fatal "#{e.class}: #{e.message}" slack_notify(e) # slackへの通知 flash[:error] = "保存に失敗" redirect_to new_post_path end end
- 投稿日:2020-05-15T11:48:29+09:00
rails データの保存の書き方 (書籍 vs 実務 )
書籍などは
def create @post = Post.new(post_params) if @post.save flash[:success] = "保存に成功" redirect_to posts_path else flash[:error] = "保存に失敗" redirect_to new_post_path end end実務では、調査の為の例外のログの吐き出しと、slcakなどの通知ツールする所までがセットで、保存機能が完結する
def create @post = Post.new(post_params) @post.save! # 例外を発生させる flash[:success] = "保存に成功" redirect_to posts_path rescue StandardError => e logger.fatal "#{e.class}: #{e.message}" slack_notify(e) # slackへの通知 flash[:error] = "保存に失敗" redirect_to new_post_path end end
- 投稿日:2020-05-15T11:45:09+09:00
検索機能の実装(form_tag・ransack)
検索機能の実装
form_tagとransackをそれぞれ用いた2種類の検索機能の実装を行いました。
特にこだわりがなければransackを用いたほうが簡単に実装できると感じました。form_tagを用いた検索機能の実装
タスク一覧が表示されるtasks#indexページに、検索フォームを実装しました。
検索フォームに入力されたキーワードがデーターベースにあるタスク名と部分一致した場合、そのタスクを表示されるようにしました。
(html.slim, bootstrapを用いました。)views/tasks/index.html.slimh1 タスク一覧 = link_to '新規登録', new_task_path, class: 'btn btn-primary' = form_with :method => "get", local: true do |f| .form-group = f.submit "Search", :name => nil ,class: "btn btn-outline-primary" = f.text_field :search .mb-3 table.table.table-hover thead.thead-default tr th= '名称' th= '詳細' tbody - @tasks.each do |task| tr td= task.name td= task.description td= task.created_at検索フォームで入力されたキーワードが引数としてparamsの中に入るように、
taskscontrollerのindexにsearch(params[:search])を追加しました。controllers/tasks_controller.rbdef index @tasks = Task.search(params[:search]) end最後にtaskモデルにsearchというメソッドを作成しました。
検索フォームで入力されたキーワードがsearchメソッドの中で処理され、
キーワードと部分一致するタスクがあれば、その結果がページに表示されます。
部分一致するタスク名がない場合は、allとして全てのタスクが一覧として表示されます。models/task.rbclass Task < ApplicationRecord def self.search(search) if search where(["name LIKE ?", "%#{search}%"]) else all end end endransackを用いた検索機能の実装
簡単なタスクアプリの一覧ページに実装しました。
(html.slim, bootstrapを用いました。)Gemfileにransackを追加し、ターミナルでbundle installしました。
Gemfilegem 'ransack' #1番下でOK検索に引っかかったタスクのみ表示するため、
indexメソッドを以下に書き換えました。controllers/tasks_controller.rbdef index @q = User.ransack(params[:q]) @users = @q.result(distinct: true) endindexのviewsを書き換えました。
views/tasks/index.html.slim= search_form_for @q, class: 'mb-5' do |f| .form-group.row = f.label :name_cont, '名称', class: 'col-sm-2 col-form-label' .col-sm-10 = f.search_field :name_cont, class: 'form-control' .form-group = f.submit class: 'btn btn-outline-primary'また、ransackにはソート機能もあるため、並べ替えも行いたい場合はransackをお勧めします。
- 投稿日:2020-05-15T11:15:51+09:00
入力不可にしたセレクト項目をRailsに送信するTips
環境
Vagrant + Ubuntu 16.04.5 LTS
Rails 5.2.4.2
jQuery
slimやった事
入力フィールドを入力不可にするには、jQueryでこのように記述する事になります。しかし、disabledにすれば、ビューからコントローラに送信されない事が判明。つまり、Railsのコントローラ側で該当のカラムが更新されない事になります。なんと!
tasks_index.js$("#task_todofuken").prop('disabled', false);そこで、次のやり方で、readonlyにすれば、サーバーへ送信されるようになります。
tasks_index.js$("#task_todofuken").attr('readonly', true);text_fieldであればこのやり方でいいのだが、今回はselectフィールドを利用しており、そもそも、selectフィールドにはreadonlyの属性を持っていない。
(ぼやき:よくよく考えれば、selectフィールドでreadonlyにする意味はないかも知れないので、このような仕様にしている理由も分からなくはないが、開発者には何かと様々な事情が出てくる事もあろう)参考 ※disabledとreadonlyの違いについて詳しい
https://www.sejuku.net/blog/36294ちなみに、disabledも、readonlyも利用せずに、このようにselectフィールドを初期化すると、プルダウン表示が復活しなくなるため、このやり方も使えない事になります。
tasks_index.js$('#task_todofuken').html("<option value=''></option>");そこで、ビューの中で、todofukenのselectフィールドとは別に、hidden_fieldという隠し項目を重複して作ってしまう。隠し項目は必ずサーバーに送信されるため、selectフィールドがdisabledになっていても更新される事になります。disabledがfalseの状態になっている場合、重複して送信される事になるが特に問題はないようです。
_form.html.slim// 都道府県 .form-group.row .col-sm-2 = f.label :todofuken, class: 'control-label' .col-sm-10 = f.select :todofuken, ['北海道','青森県','岩手県', ・・・以下略], {include_blank: true}, class: 'selectpicker form-control', id: 'task_todofuken' // 都道府県(隠し項目) .form-group .col-sm-10 = f.hidden_field :todofuken, class: 'form-control bg-warning', id: 'task_todofuken_hidden'そして、selectフィールドが選択されたタイミングで、隠しフィールドにも値を反映させるようにします。
tasks_index.js$(function(){ $('#task_todofuken').change(function() { var input = $.trim($(this).val()); $("#task_todofuken_hidden").val(input); }) })これでOK
- 投稿日:2020-05-15T10:00:17+09:00
Rails Tutorial 6`require': no implicit conversion of String into Integer (TypeError)
そもそもbootsnapとは?
https://qiita.com/Daniel_Nakano/items/aadeaa7ae4e227b73878手取り早く、解決したい方へ
console.gem list bootsnap // bootsnap (1.4.6, 1.4.4)と、バージョン差異が出ていたら、
console.gem uninstall bootsnap --version {指定したい方}と、必要のないバージョンを削除して終わりです。
そもそも
ローカルで開発する際、Gemfileをいじると思います。
私の場合、console.user-no-MacBook-Pro:user$ bundle update user-no-MacBook-Pro:user$ bundle install --without production ... Bundle complete! 31 Gemfile dependencies, 108 gems now installed. Gems in the group production were not installed. Bundled gems are installed into `./.`と、ここまで良かったのですが、
console.user-no-MacBook-Pro:user$ rails sで、サーバーを起動しようとした際、
console.Traceback (most recent call last): 24: from bin/rails:3:in `<main>' 23: from bin/rails:3:in `require_relative' 22: from /Users/user/tutorial/config/boot.rb:4:in `<top (required)>' 21: from /Users/user/tutorial/config/boot.rb:4:in `require' 20: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/setup.rb:30:in `<top (required)>' 19: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap.rb:30:in `setup' 18: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/compile_cache.rb:9:in `setup' 17: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:48:in `require_relative' 16: from /Users/user/tutorial/ruby/2.7.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `require' 15: from /Users/user/tutorial/ruby/2.7.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:291:in `load_dependency' 14: from /Users/user/tutorial/ruby/2.7.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `block in require' 13: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require' 12: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi' 11: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register' 10: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `block in require_with_bootsnap_lfi' 9: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require' 8: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/compile_cache/iseq.rb:1:in `<top (required)>' 7: from /Users/user/tutorial/ruby/2.7.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `require' 6: from /Users/user/tutorial/ruby/2.7.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:291:in `load_dependency' 5: from /Users/user/tutorial/ruby/2.7.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `block in require' 4: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require' 3: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi' 2: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register' 1: from /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `block in require_with_bootsnap_lfi' /Users/user/tutorial/ruby/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require': no implicit conversion of String into Integer (TypeError)と、グアっとエラーが出ていました。
何をしてこうなったか
・bundle install前に、元々Gemfileは存在していた。
・その元々のGemfileは、bundle installやらなんやらしていてRailsと繋がっていた。
・新たなGemfileで諸々バージョンなども変更されていてbundle update → bundle installしようとしたら、上記のエラーが出た。元々のGemfile
source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.7.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 6.0.3' # Use sqlite3 as the database for Active Record gem 'sqlite3', '~> 1.4' # Use Puma as the app server gem 'puma', '~> 4.1' # Use SCSS for stylesheets gem 'sass-rails', '>= 6' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker gem 'webpacker', '~> 4.0' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.7' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use Active Model has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use Active Storage variant # gem 'image_processing', '~> 1.2' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.2', require: false group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' gem 'listen', '~> 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' gem 'selenium-webdriver' # Easy installation and use of web drivers to run system tests with browsers gem 'webdrivers' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]新たなGemfile
source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem 'rails', '6.0.0' gem 'aws-sdk-s3', '1.46.0', require: false gem 'image_processing', '1.9.3' gem 'mini_magick', '4.9.5' gem 'active_storage_validations', '0.8.2' gem 'bcrypt', '3.1.13' gem 'faker', '2.1.2' gem 'will_paginate', '3.1.8' gem 'bootstrap-will_paginate', '1.0.0' gem 'bootstrap-sass', '3.4.1' gem 'puma', '3.12.1' gem 'sass-rails', '5.1.0' gem 'webpacker', '4.0.7' gem 'turbolinks', '5.2.0' gem 'jbuilder', '2.9.1' gem 'bootsnap', '1.4.4', require: false // ←errorで止まってしまう group :development, :test do gem 'sqlite3', '1.4.1' gem 'byebug', '11.0.1', platforms: [:mri, :mingw, :x64_mingw] end group :development do gem 'web-console', '4.0.1' gem 'listen', '3.1.5' gem 'spring', '2.1.0' gem 'spring-watcher-listen', '2.0.1' end group :test do gem 'capybara', '3.28.0' gem 'selenium-webdriver', '3.142.4' gem 'webdrivers', '4.1.2' gem 'rails-controller-testing', '1.0.4' gem 'minitest', '5.11.3' gem 'minitest-reporters', '1.3.8' gem 'guard', '2.16.2' gem 'guard-minitest', '2.4.6' end group :production do gem 'pg', '1.1.4' end解決策
問題は、bootsnapのバージョン差異にあります。
Gemfile.// 元々 gem 'bootsnap', '>= 1.4.2', require: false // 新規 gem 'bootsnap', '1.4.4'なので、この場合は元々のbootsnapのバージョンを指定し直す事でエラーが解決できます。
Gemfile.gem 'bootsnap', '>= 1.4.2', require: falseと、再指定して
console.user-no-MacBook-Pro:user$ bundle update bootsnap ... Using bootsnap 1.4.6 (was 1.4.4) ... Bundle updated! Gems in the group production were not updated. user-no-MacBook-Pro:user$ rails s => Booting Puma => Rails 6.0.0 application starting in development => Run `rails server --help` for more startup optionsとなれば、成功です。
- 投稿日:2020-05-15T09:57:32+09:00
【Rails】has_manyの数を制限するvalidate
例えば、Userが持てるPostの数を制限したいときは、Post側にvalidateを設定します。
user.rbclass User < ActiveRecord::Base has_many :posts, dependent: :destroy endpost.rbclass Post < ActiveRecord::Base MAX_POSTS_COUNT = 5 belongs_to :user validate :posts_count_must_be_within_limit private def posts_count_must_be_within_limit errors.add(:base, "posts count limit: #{MAX_POSTS_COUNT}") if user.posts.count >= MAX_POSTS_COUNT end endこれで、6個目のPostを作成しようとするとエラーになります。
ちなみに、Userにvalidateを定義した場合はUserをsaveしたときにvalidateが走ります。
つまり、Postのsave時にvalidateが効かず、6個目のPostを作れてしまうのでNGです。user.rbclass User < ActiveRecord::Base has_many :posts, dependent: :destroy validates :posts, length: { maximum: 5 } # ←これでは`user.posts.create`のときに動かず、6個目のPostを作成できてしまう end環境
- Ruby: 2.6.3
- Rails: 6.0.2.2
- 投稿日:2020-05-15T01:16:53+09:00
Railsチュートリアル学習記録(第1章)
※チュートリアル2周目。開発環境はAtomを使用しています。
※頭の整理と備忘を兼ねて、学習内容を記録しています。
※表現の厳密さや精緻さよりも、直感的に理解しやすいよう記録しています。・第1章でやりたいこと
⇨開発環境(アプリを作っている場所)で、「Hello World」という文章を表示させるプログラムを作成し、本番環境(アプリを公開する場所)に公開する。・最初にRailsをダウンロードする。「-v 5.1.6」でバージョンを指定している。バージョン毎に機能が異なったりするので気をつける。
filename.rb$ gem install rails -v 5.1.6・アプリ開発に必要なファイル等を一括ダウンロード(ここでもバージョン指定)
filename.rb$ rails _5.1.6_ new hello_app・実装したい機能に応じてgemfileを修正し、必要なgemをインストール。
※gem:ある機能のソースコードの塊。機能を実装するたびに一からコードを書くのは大変なので、よく使う機能のコードはあらかじめまとまっている。
※gemfile:「このgemは、今このバージョンになっているよ!」といった感じで、gem毎のバージョンが書いてある場所。filename.rb$ bundle install・アプリケーションとしてデータの出し入れが可能になるよう、仮置きのサーバーを起動。サーバーは開きっぱなしにすることが多いので、サーバー起動を指示する命令は、別ターミナルで打ち込んだ方が良い。
filename.rb$ rails serverhttp://localhost:3000/をブラウザで開き、「Yay! You’re on Rails!]
と表示されたらOK。・このままでは「hello, world!」と表示しないため、少し修正。
・applicationコントローラーで「helloアクション」を定義し、このアクションが呼ばれたら「hello, world」と表示されるよう設定。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception def hello render html: "hello, world!" end end・ブラウザで末尾が「/」のURLがリクエストがされたら、applicationコントローラの「helloアクション」を呼び出すよう設定。
config/routes.rbRails.application.routes.draw do root 'application#hello' endここまで終えて、再度http://localhost:3000/をブラウザで開く(末尾が「/」のURLをリクエストする)と、「Hello world!」が表示されます。
あとは、ここまでのプログラムを本番環境にアップするだけです。
(以降はチュートリアルの手順通りに粛々と進めるだけなので割愛)