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

[エラー解析]デバッグして、Gemの中身を確認して、エラーの原因を特定した

わけのわからんエラーが表示された

Facebookログイン画面の操作中に、以下のOKボタンを押すと、500番エラーが表示されログインができない。
image.png

ターミナルのログには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)で何か問題が発生していることが分かったため、その中身を掘り下げていく。

nextstepを使いながら、原因箇所を特定していく。

ログのエラーで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ログインが出来た。

このように問題箇所と思われる範囲を徐々に狭めながら、原因を特定する力を身につけなければと思ったのであります。。

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

編集画面で更新ができない!?

An invalid form control with name='◯◯[image]' is not focusable. というエラー

編集画面(edit)ではnewの画面をコピーして使用することもあるかと思います。
私は今回コピペで作成し、動作確認をしていました。

通常であれば問題なく動くはず。。。なのですがなぜか更新ボタンを押しても反応しない状態状態でした。
デベロッパーツールを見てみると、
An invalid form control with name='recipe[image]' is not focusable.
というエラーが出ていました。

スクリーンショット 2020-05-15 22.10.56.png

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"を付けないようにしましょう

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

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.rb
class AddUniqueIndexToLabels < ActiveRecord::Migration
  def change
    add_index :labels, [:name, :user_id], unique: true
  end
end

おわりに

ActiveRecordの一意性検証の範囲指定について学びました.

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

GitHubとアプリケーションの紐付け方-チーム開発-

概要

この記事ではGitHubとアプリケーションの紐付け方を解説します。
また、その際にGitHub Desktopを活用していきます。

方法

まず、ローカルリポジトリを作成します。

GitHub Desktopを開いて左上のCurrent Ripositoryをクリックします。
クリックして開いた画面のAddをクリックします。
スクリーンショット 2020-05-15 20.59.55.png
すると下記のような画面が出てきます。

スクリーンショット 2020-05-15 21.00.12.png
その画面内のChoose...をクリックするとファイルが選択できる画面が出てくるので自分が作成中のアプリケーションのフォルダを選択します。
そしてAdd Repositoryボタンをクリックします。
これでローカルリポジトリの作成完了です。

もしAdd repositoryが押せない場合はターミナルの自分が作成中のディレクトリでgit initを叩いてください。
そうすればクリックできるようになると思います。

スクリーンショット 2020-05-15 21.00.31.png

次に上記画面の左下にSummary(required)とあると思いますので、そこにinitial commit(初めてのコミットという意味)と入力して、Commit to masterボタンをクリックします。
これでコミットが完了したので最後ににリモートリポジトリを作成します。

画面右上にPublic repositoryがあるのでそこをクリックします。するとリポジトリ名を決められたり、コードを公開するかしないかのチェックボックスが出てきます。場合に合わせて適宜変更してください。
Publish repositoryボタンをクリックしたらリモートレポジトリの作成完了です。
ブラウザでGithubのページにアクセスして自分の作成しているアプリケーションの名前があれば作成は成功しています。

ここまででGitHubとアプリケーションの紐付けに関しては完了しています。





この記事を読んでいただきありがとうございました。

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

rails generate migrationをdockerで立ち上げたRailsで実行したい

はじめに

dockerでRailsを立ち上げたら、Railsコマンド使えなかった。困った。
これは完全なる自分用の備忘録です。

既存DBのテーブルにカラムを追加したい

docker-compose exec app rails g migration AddSomeColumnsToHoge

appはdockerで立ち上がってるものです。appとかwebとかそんな名前で使われることが多い。(知らんけど)
AddSomeColumnsToHogeHogeはカラムを追加したいテーブル名にしました。

これでdb/migrate20200xxxxxx_add_some_columns_to_Hoge.rbファイルが作られます。
中身を見ると

20200xxxxxx_add_some_columns_to_Hoge.rb
class AddSomeColumnsToHygienists < ActiveRecord::Migration[5.2]
  def change
  end
end

こんな感じ。
ここに追加したいカラムを書いていく。

20200xxxxxx_add_some_columns_to_Hoge.rb
class 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でカラムを追加したテーブルを確認すると、カラムが追加されてます!やったね!

参考文献

最後に

dockerで立ち上げたらコマンドややこしいと個人的に感じたので備忘録。
今度は削除とか変更とかで躓きそう。
その時は追記します。
殴り書きなので、不備とかアドバイスとかいただけるとありがたいです。

書いておいてアレだけど、基本的には参考文献の記事を参考にすると解決します

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

RailsでRSSを生成した時の備忘録(RSS2.0準拠)

これなに

RailsでRSSを生成した時にちょっと困ったりして調べたのでまとめておく。
RSS::MakerとかBuilder::XmlMarkupとかあるけど、slimで書けばいいじゃない!と思ったけど結局builderでも書きました。
たぶんSmartNewsのSmartFormat仕様書の必須項目は埋めてあるはず。

サンプルコード

postsテーブルの構造はわりと一般的だと思うので割愛します。

slim

rss.xml.slim
doctype 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.name

builder

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

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

参考記事
https://qiita.com/jnchito/items/ec070f7551c983cc5b60

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

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にアクセスしてみます。
スクリーンショット 2020-05-14 15.30.19.png
おぉ、ちょっと時間かかったけど表示できた。
確認できたら、Ctrl+Cでサーバを停止しちゃいます。

中身を書いていきます。

先ほどまでの手順でWhiteBoardコンポーネントというのを作成しています。
が、今回はさらに、以下のコンポーネントを追加します。

コンポーネント名 説明
UserBox ユーザ毎の枠(箱?)
Sticky タスクを表示するもの(付箋)

それぞれ、以下のコマンドで追加できます。
bundle exec rails g react:component UserBox
bundle exec rails g react:component Sticky

WhiteBoardコンポーネントの実装

WhiteBoardでは、主にAPIとのやりとりと、UserBoxの表示までを行うようにしてみました。
実装は以下のようになりました。(長いです。)

WthiteBoard.js
import 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.js
import 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 UserBox

Stickyコンポーネントの実装

最後にタスクの内容を表示するStickyの実装です。
大事なのは、ドラッグ開始時にdataTranser.setDataを行っていることです。
これにより、drop時のデータ引継ぎができます。

app/javascript/components/Sticky.js
import 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)にアクセスしてみましょう。

スクリーンショット 2020-05-15 14.57.48.png

・・・あ、味気ねぇが、一応ドラッグドロップも動きます。
seedでデータを用意しておいたので、こういうときに簡単に確認できますね。

テストしてみます。

ドラッグドロップなんてテストできるの?
Rails環境にはCapybaraが住んでいるのでできます。

実際のUIを使ってテストするので、systemテスト扱いかなぁ、とシステムテストを追加します。

shell
# WhiteBoardのテストということで、whiteboardsにしてみました。
bundle exec rails g system_test whiteboards

以下のテストを追加しました。

test/system/whiteboards_test.rb
  test "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);

  end

UIがテストできると言うのは素晴らしいですね。

それにしても、見た目が寂しいので次回はスタイルを直してみます。

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

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にアクセスしてみます。
スクリーンショット 2020-05-14 15.30.19.png
おぉ、ちょっと時間かかったけど表示できた。
確認できたら、Ctrl+Cでサーバを停止しちゃいます。

中身を書いていきます。

先ほどまでの手順でWhiteBoardコンポーネントというのを作成しています。
が、今回はさらに、以下のコンポーネントを追加します。

コンポーネント名 説明
UserBox ユーザ毎の枠(箱?)
Sticky タスクを表示するもの(付箋)

それぞれ、以下のコマンドで追加できます。
bundle exec rails g react:component UserBox
bundle exec rails g react:component Sticky

WhiteBoardコンポーネントの実装

WhiteBoardでは、主にAPIとのやりとりと、UserBoxの表示までを行うようにしてみました。
実装は以下のようになりました。(長いです。)

WthiteBoard.js
import 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.js
import 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 UserBox

Stickyコンポーネントの実装

最後にタスクの内容を表示するStickyの実装です。
大事なのは、ドラッグ開始時にdataTranser.setDataを行っていることです。
これにより、drop時のデータ引継ぎができます。

app/javascript/components/Sticky.js
import 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)にアクセスしてみましょう。

スクリーンショット 2020-05-15 14.57.48.png

・・・あ、味気ねぇが、一応ドラッグドロップも動きます。
seedでデータを用意しておいたので、こういうときに簡単に確認できますね。

テストしてみます。

ドラッグドロップなんてテストできるの?
Rails環境にはCapybaraが住んでいるのでできます。

実際のUIを使ってテストするので、systemテスト扱いかなぁ、とシステムテストを追加します。

shell
# WhiteBoardのテストということで、whiteboardsにしてみました。
bundle exec rails g system_test whiteboards

以下のテストを追加しました。

test/system/whiteboards_test.rb
  test "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);

  end

UIがテストできると言うのは素晴らしいですね。

それにしても、見た目が寂しいので次回はスタイルを直してみます。

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

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型の属性を渡せなくて困る

ラフなモデル設計

適当ですが、こんな設計になりますかね。
ユーザがいなくなったらタスクもいなくなる形なので不自然かもしれませんが。。。
スクリーンショット 2020-05-12 11.50.23.png

環境構築

申し訳ありませんが、インターネット接続が可能で、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*があったもので。。)
スクリーンショット 2020-05-12 15.35.00.png

モデルを準備する。

先ほどのかなりラフなモデル設計から、ユーザモデルとタスクモデルを作ってみます。

ユーザモデル

どちらを先に作るか悩みましたが、ユーザから作ってみました。
ぶっちゃけ、どっちでも良いのだと思いますが。

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.rb
class User < ApplicationRecord
  has_many :tasks  # taskをたくさん持ってますよと教えてあげます。
end

ちなみに、タスクモデルのほうは、自動的に以下のような内容になります。(便利♪)

app/models/task.rb
class 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にしろバージョンアップしたいとき、テストが作られてないと影響確認すらできませんからね。

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

部分テンプレート

部分テンプレートとは

viewファイルで同じHTML構造の部分を共通化したもの

例えばwebサイトのヘッダー部分や、ブログ記事の一覧表示画面の各投稿のレイアウトなども多くが共通していますよね

使用するメリット

・共通化することで何度も同じコードを書く必要がない

・修正する際の修正箇所が少なく済む

・他のviewファイルで使い回すことができる

使用方法

まずは共通化している部分を部分テンプレートのファイルに書き換えます。

ファイル名の前には必ず「 _ (アンダーバー)」を書きます

「_post.html.erb」 など。

あとは呼び出しをするだけですが、この際 render メソッドを使用します。

index.html.erb
<% render partial: "post" %>

partial オプションは部分テンプレート名の指定をしています。

また、部分テンプレート内で変数を扱いたい場合は locals オプションを使用します。

<% render partial: "部分テンプレート名", locals: { 変数: 値 } %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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

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

deviseによるユーザー管理

deviceとは

Rubyのライブラリ(拡張機能)であるgemの一つ。

アプリケーションにおけるユーザ-新規登録・ログイン機能を簡単に実装できる。

導入方法

Gemfileに記載

gem 'devise' 

ターミナルのアプリケーションのディレクトリにて

bundle install

deviseの設定ファイルの作成

rails g devise:install

deviseのモデル作成用コマンドでuserモデルを作成

このコマンドで、ファイルの新規作成と、ユーザーのログイン・新規登録で必要なルーティングが生成される(routes.rbにdevise_for :usersが自動追記されるため)

rails g devise user

マイグレーションファイルの実行

rails db:migrate

deviseに対応したビューファイルの作成

これでシンプルな見た目の新規登録画面とログイン画面が生成されます

新規登録画面はapp/views/devise/registrations/new.html.erb、
ログイン画面のビューはapp/views/devise/sessions/new.html.erb が対応しています。

rails g devise:views

以上で新規ユーザー登録とログイン機能の基本的な実装はできます。

deviseを導入することで使用できるメソッドや、deviseによるユーザー登録情報の項目を追加したい場合(現状ではEメールと、パスワードのみの登録しかできない)は調べてみて下さい

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

【Rails】ActiveRecord::PendingMigrationErrorが出たときの対処法

エラー内容

ActiveRecord::PendingMigrationError
image.png

環境

  • Rails 6.0.3
  • psql (PostgreSQL) 12.2

概要

  • このアプリケーションに対応するマイグレーションファイルをmigrateし忘れてますよ、というエラー
  • 再度migrateすることで解決

原因

  1. db:create後にmigrateし忘れている
  2. すでに以前同じ名前で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:reset

migrateをリセットできたので、再度migrateします。

bundle exec rake db:migrate

サーバーを再起動すれば解決です!

rails s

余談ですが、db:createdb:migrateなどデータベース関連の更新でうまくいかない時はサーバーをCtrl+Cで閉じてからやることをおすすめします(結構このパターンでのエラーもあるので注意)。

※migrateの状態が分からない時に調べるコマンド
rails db:migrate:status
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

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

検索機能の実装(form_tag・ransack)

検索機能の実装

form_tagとransackをそれぞれ用いた2種類の検索機能の実装を行いました。
特にこだわりがなければransackを用いたほうが簡単に実装できると感じました。

form_tagを用いた検索機能の実装

タスク一覧が表示されるtasks#indexページに、検索フォームを実装しました。
検索フォームに入力されたキーワードがデーターベースにあるタスク名と部分一致した場合、そのタスクを表示されるようにしました。
(html.slim, bootstrapを用いました。)

views/tasks/index.html.slim
h1 タスク一覧

= 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.rb
def index
    @tasks = Task.search(params[:search])
end

最後にtaskモデルにsearchというメソッドを作成しました。
検索フォームで入力されたキーワードがsearchメソッドの中で処理され、
キーワードと部分一致するタスクがあれば、その結果がページに表示されます。
部分一致するタスク名がない場合は、allとして全てのタスクが一覧として表示されます。

models/task.rb
class Task < ApplicationRecord
  def self.search(search)
    if search
      where(["name LIKE ?", "%#{search}%"])
    else
      all
    end
  end
end

ransackを用いた検索機能の実装

簡単なタスクアプリの一覧ページに実装しました。
(html.slim, bootstrapを用いました。)

Gemfileにransackを追加し、ターミナルでbundle installしました。

Gemfile
gem 'ransack' #1番下でOK

検索に引っかかったタスクのみ表示するため、
indexメソッドを以下に書き換えました。

controllers/tasks_controller.rb
def index
  @q = User.ransack(params[:q])
  @users = @q.result(distinct: true)
end

indexの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をお勧めします。

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

入力不可にしたセレクト項目を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

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

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

となれば、成功です。

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

【Rails】has_manyの数を制限するvalidate

例えば、Userが持てるPostの数を制限したいときは、Post側にvalidateを設定します。

user.rb
class User < ActiveRecord::Base
  has_many :posts, dependent: :destroy
end
post.rb
class 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.rb
class 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 server

http://localhost:3000/をブラウザで開き、「Yay! You’re on Rails!]
と表示されたらOK。

・このままでは「hello, world!」と表示しないため、少し修正。
・applicationコントローラーで「helloアクション」を定義し、このアクションが呼ばれたら「hello, world」と表示されるよう設定。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  def hello
    render html: "hello, world!"
  end
end

・ブラウザで末尾が「/」のURLがリクエストがされたら、applicationコントローラの「helloアクション」を呼び出すよう設定。

config/routes.rb
Rails.application.routes.draw do
  root 'application#hello'
end

ここまで終えて、再度http://localhost:3000/をブラウザで開く(末尾が「/」のURLをリクエストする)と、「Hello world!」が表示されます。

あとは、ここまでのプログラムを本番環境にアップするだけです。
(以降はチュートリアルの手順通りに粛々と進めるだけなので割愛)

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