20201025のRubyに関する記事は23件です。

意外と簡単にできた。パスワードを入力させずにユーザー情報を更新する方法

概要

今回の記事は表題の通り「パスワードを入力させずにユーザー情報を更新する方法」です。
Railsでとあるアプリケーションを作成中に、どうにかしてパスワードを入力させずにユーザー情報を更新できないものかと考えました。
updateメソッドではパスワードの入力を必須としているようです。
で、よく考えてみたらSNSやECサイトなどの多くのWEBアプリケーションは、ユーザー名やプロフィールなどをパスワードを入力せずとも更新可能です。
updateメソッドをオーバーライドとかしなくても、意外と簡単に出来るのでは...と思い調べてみました。

実際にやったこと

update_without_passwordという、そのまんまな名前をしているメソッドがありました。
これを用いて今回は下記のように実装しました。
単純にパラメータに含まれるパスワードが空かどうかで、使用するメソッドを振り分けています。

# パラメータに含まれるパスワードが空の場合、
if params[:password].blank?
  # パスワードなしでユーザー情報を更新
  @user.update_without_password(user_params)

# パラメータにパスワードが含まれていた場合
else
  @user.update(user_params)

注意点

モデルに以下のような記載があると、update_without_passwordメソッドをつかった際にエラーになります。
パスワードの入力を必須にする、という意味のバリデーションですね!

app/models/user.rb

validates :password, presence: true

onオプションでバリデーションをかけるアクションを指定しましょう。
以下のようにすれば、ユーザーを作成する際のみバリデーションがかかります。

validates :password, presence: true, on: :create
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]Modelの関連付け(アソシエーション)

Railsのモデルの(アソシエーション)の種類の忘備録です
参考資料:Railsガイド
https://railsguides.jp/association_basics.html

関連付け(アソシエーション)を行う理由

2つのActive Recordモデルの繋がりを関連付け(アソシエーション)という。
以下、アソシエーションは関連付けでと統一して記述していく。
関連付けをする理由は
・モデル間の共通操作を可能とし、コードの記述がシンプルで簡単になる
・上記により、コードの見通しが良くなる
為である。
※主キー、外部キーの詳細は省く。

Example

簡単なタスク管理アプリを例にして、ユーザー(User)とタスク(Task)の関連付けを書いていく。(Railsガイドの沿って記述していく)

class User < ApplicationRecord
end

class Task < ApplicationRecord
end

ユーザーは新しいタスクを追加する場合とユーザーを削除する場合を関連付け無しで記述すると以下のような実装となると思われる。

#新しいタスクの追加
@task = Task.create(task: "買い物", user_id: @user.id)

#ユーザーの削除(この場合、削除されるユーザーのタスクも一緒に全て削除しなければ、いつまでもDBに意味のないデータが残されてしまう)
@tasks = Task.where(user_id: @user.id)
@tasks.each do |task|
  task.destroy
end
@user.destroy

Railsのモデルに明示的に関連付けを追加することで、より簡潔にコードを記述することが可能となる。
まず、モデルの関連付けを定義する。

class User < ApplicationRecord
 #ユーザーは複数のタスクを持つよ, ユーザーが削除されたら、tasksも全て削除してねと定義
  has_many :tasks, dependent: :destroy
end

class Task < ApplicationRecord
 #タスクは1人のユーザーから生み出されるよ
  belong_to :user
end

これで関連付けは完了である。
has_manyとbelong_toを、かなりざっくりと解説すると

Userから見ると、Task_a,Task_b,Task_cと複数タスクを持てるけど、
Taskから見ると、Userは1人しかいない
1(User)対多(Task)の関係だとRailsに定義した。

ちなみに、dependent: :destroyオプションは、
ユーザーが削除されたら、そのユーザーのタスクも漏れなく全て削除してねという意味である。

上記のように関連付けを行ったことにより、
新しいタスクの追加とユーザーの削除は下記のよう簡潔に記述できるようになった。

#新しいタスクの追加
@task = @user.tasks.create(task: "買い物")

#ユーザーの削除(dependent: :destroyオプションでユーザーのタスクも一緒に削除される。)
@user.destroy

特に削除の部分は5行が1行で済む。
コードを見ても、すぐに何をしているのか理解が可能で見通しも良くなった。

関連付けの種類

belongs_to
has_one
has_many
has_many :through
has_one :through
has_and_belongs_to_many

説明

belong_to

1対1の関連付けが設定される。
宣言を行ったモデルのすべてのインスタンスは、他方のモデルのインスタンスに「従属(belongs to)」する。
Exampleの章を例にすると1つのTaskに対して1人のユーザーを割り当てる関係を表現している。
belongs_to関連付けで指定するモデル名は必ず「単数形」にしなければならない。

 #ユーザーは1人しかいない為、単数形出なければならない。
 #Railsの自動推測でエラーとなる。
class Task < ApplicationRecord
  belong_to :user
end

has_one

1対1の関連付けが設定される。belong_toとの違いは、
宣言が行われているモデルのインスタンスが、他方のモデルのインスタンスを「まるごと含んでいる」または「所有している」ことを示す。
国民健康保険の保険証を例にとる(分かりづらい(笑)?)

1人の人が1つの保険証を所有している。
class people < ApplicationRecord
  has_one :insurance_card
end

1人の人に保険証は所有されている。
class insurance_card < ApplicationRecord
  belong_to :people
end

has_many

「1対多」のつながりがあることを示す。
has_many関連付けが使われている場合、そのモデルのインスタンスは、反対側のモデルの「0個以上の」インスタンスを所有する。
Exampleの章を例にすると1人のユーザーが複数のタスクを持っている関係を表現できる。
has_many関連付けを宣言する場合、相手のモデル名は「複数形」にする必要がある。

 #ユーザー1人に対して、タスクは複数所有できる為、複数形にて記述しなければならない。
 #Railsの自動推測でエラーとなる。
class User < ApplicationRecord
  has_many :tasks
end

has_many :through

「多対多」のつながりを設定する場合によく使われる。
この関連付けは、2つのモデルの間に「第3のモデル(中間モデル)」を作成する。それによって、相手モデルの「0個以上」のインスタンスとマッチする。
複数の授業と複数の生徒から、特定の授業に出る生徒を限定することが可能となる。

生徒は、複数の授業を受けている。
class Student < ApplicationRecord
  has_many :members
  has_many :class_works, through: :members
end

複数の授業と、複数の生徒のidを保存することで、特定の授業に出てる生徒を限定することが出来る。
class Member < ApplicationRecord
  belongs_to :student
  belongs_to :class_work
end

授業は複数の生徒が受けている。
class Class_work < ApplicationRecord
  has_many :members
  has_many :students, through: :members
end

has_one :through

「1対1」のつながりを設定する。
この関連付けは、2つのモデルの間に「第3のモデル(中間モデル)」を作成する。
それにより、相手モデルの1つのインスタンスとマッチする。
思いつかないのでRailsガイドをそのまま例にとる。

1人の提供者(supplier)が1つのアカウントに関連付けられ、さらに1つのアカウントが1つのアカウント履歴に関連付けられる場合、supplierモデルは以下のような感じになります。

#Supplierはaccountを持ち、accountを通じてaccout_historyを持つ。
class Supplier < ApplicationRecord
  has_one :account
  has_one :account_history, through: :account
end

#accountはsupplierに属していて、account_historyを一つ持つ
class Account < ApplicationRecord
  belongs_to :supplier
  has_one :account_history
end

#AccountHistoryはaccoutに属している
class AccountHistory < ApplicationRecord
  belongs_to :account
end

has_and_belongs_to_many

「多対多」のつながりを作成する。しかしthrough:を指定した場合と異なり、第3のモデル(中間モデル)がない。
完成車(assembly)と部品(part)があり、1つの完成車に多数の部品が対応し、逆に1つの部品にも多くの完成車が対応するのであれば、モデルの宣言は以下のようになります。

#完成車はたくさんの部品(parts)が取り付けられて車となる。
class Assembly < ApplicationRecord
  has_and_belongs_to_many :parts
end

#部品は複数あり、複数のの完成車に取り付けられている。
class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング学習歴1ヶ月半からWebアプリを開発してみた

1.はじめに

 2020年8月からプログラミング勉強を勉強していますが、約1ヶ月かけてWebアプリを開発しました。勉強する前はこんな状態でした。

・HTML、CSSはうっすらと名前を聞いたことがある
・現職ではWord、Excelぐらいしか使用していない
・寿司打で3000円コースすらクリアできない

この状態から1ヶ月半学習しほぼ独学でWebアプリを開発をしました。

(一応10月から侍エンジニア塾にレッスンを受けていますが、アプリ開発前の基礎的な部分は独学で学習したこと、アプリ開発においては技術的な質問の回答、コードのレビューをインストラクターにしていただきましたが、アプリの方向性など大半は独自で考えたことを考慮してほぼ独学という言葉を使いました。)

 学習歴1ヶ月半の実力を記録として残したくて記事にしました。

2.自己紹介

  • 年齢:32歳

  • 職業:プラント設計

  • 未経験からエンジニア転職目指して勉強中。

  • 2020年4月コロナ禍の中、一児の父となる。生まれた直後2ヶ月間、育児休暇を取得。

  • プログラミング学習より育児を優先している。離乳食の裏ごしで腕を痛める。

学習履歴

2020年8月1日~9月18日

3.アプリ概要

育児の悩み、不安を救うための育児特化型のQ&Aアプリです。

【Home画面】
スクリーンショット 2020-10-25 8.33.22.png
スクリーンショット 2020-10-25 14.15.10.png

Herokuで公開しています→ーSUKUSUKUー(PC&Chrome推奨)

Github → https://github.com/SHOGOHORI/myapp

使い方

  • ログインなしでも、質問一覧、質問詳細の閲覧、キーワード検索、タグ検索が可能

  • ゲストユーザーで簡単ログイン(ゲストユーザーとしてログインボタンをクリック)

  • 質問を投稿するボタンをクリックし、投稿フォーム画面へ

  • 質問詳細から回答可能

4.作成目的

 長男が生まれた直後、妻が感染症により高熱を出し、入院する、しないの騒ぎになりました。結局入院はしませんでしたが1〜2週間ほどずっと熱は下がらず寝たきりになり、私一人で育児をしていました。
 生まれたばかりの一つ命を預かるという、とてつもない責任を感じていました。孤独と不安で押しつぶされそうな状況でした。その時の経験から、同じような状況にいるママ、パパの不安を少しでも埋めたいという思い、SNSに近い形のQ&Aアプリを作成しました。

作成期間

2020年9月19日〜10月20日(31日間)

一日6〜8時間ぐらい作成に費やしていましたので、作成時間は180〜200時間ぐらいです。

5.設計

ER図

ER図.png

サイトマップ

サイトマップ.png

ワイヤーフレーム

名称未設定ファイル.png

6.使用技術

フレームワーク:Rails6.0.3
フロント:HTML、Sass、JavaScript(jQuery)
サーバーサイド言語:Ruby2.7.1
データベース:PostgreSQL
テストフレームワーク:Minitest → Rspec
サーバー構築:puma
開発環境:Docker 19.3.12 → ローカル環境
本番環境:Heroku
検索機能:ransack
バージョン管理:Git hub
ページネーション機能:kaminari
使用マシン:Mac Catalina(10.15.7)
エディタ:VSCode

7.機能

ユーザー機能

 ・新規登録、ログイン、ログアウト、ゲストユーザーログイン機能
 ・ログイン保持機能
 ・ユーザープロフィール編集機能
 ・ユーザーマイページが投稿した質問、回答の一覧表示

投稿機能

 ・一覧表示、記事詳細表示、投稿、画像アップロード、編集、削除機能
 ・回答投稿機能

質問検索機能

 ・キーワード検索、タグ検索機能

ページネーション機能

 ・Ajax対応

テスト機能

 ・単体、統合テスト機能

8.苦労した点

Dockerによる開発環境構築

 当初開発環境にDockerを導入しましたが、非常に重く、rails db:migrateやテストを実行する際にすごく時間がかかってしまいました。軽くする方法を色々調べましたが、状況は改善されず結局ローカル環境に切り替えました。
 後述するVer.2ではもう一度チャレンジしたいです。

RSpecによるテスト記載

 RailsチュートリアルではMinitestを使用していたので、RSpecについて勉強しました。Rspecの文法理解、Capybaraの使い方等、苦労しました。

苦労した経験のアウトプット→RSpecのディレクトリ構成とspecごと役割について

Ajax対応

 JavaScriptの知識が不足していたのと、そもそもAjaxがどういう仕組なのか理解していなかった為、そもそもAjaxとは?JavaScriptでなにができるのか?jQueryの基本的な書き方・文法から勉強しました。

苦労した経験のアウトプット→【Rails】ページネーションをAjax対応にする

ビューのコーディング

フロント部分、デザインとHTML、Sass、Bootstrap使い方について学習していなかったため、苦労しました。

9.反省点

Railsチュートリアルを起点にアプリ開発をスタートした

  • Railsチュートリアルの復習という意味で勉強になりましたが、必要ない機能も追加してしまいました。(ユーザー登録にメールを使用する機能、パスワード再設定機能)

 解決策:要件定義の段階で必要な機能を洗い出す。

  • Railsチュートリアルで使用しているGemしかよく知りませんでした。(アプリ作成当初、devise、kaminari、carrierwave、RSpec、RuboCop等よく使われるGemについて知らなかった)

 解決策:必要な機能に対してどのGemが適切か選定する。選定の際に評価と更新履歴を確認する。

  • レイアウトにRailsチュートリアル感が出てしまいました。

 解決策:ワイヤーフレーム作成の段階でどの層に向けてのWebアプリなのか意識してデザインする。

計画性がなかった

  • 要件定義を厳密にすると時間がかかると考え、簡単なER図とワイヤーフレームを作成した後、とりあえず手を動かしながら場当たり的に計画していったことから、ゴールがよくわからなくなってしまいました。逆に時間がかかりすぎたように思います。

 解決策:まず作成完了の期限を決める。機能ごとにGithubのissueを作成して、branchをきる。

コードが煩雑になってしまった

  • コードをインストラクターにレビューしてもらった際、trailing whitespace(末尾のスペース)や無駄な改行、無駄なファイルが散見されました。

 解決策:開発前にVScodeをコーディング規約の守れる設定にする。RuboCopで細かく静的コード解析を実行する。こまめに使用していないファイルを調べ削除する。

ただのQ&Aサイトになってしまった

  • 初めて独自のアプリを作るため、挫折しないよう機能は最小限にとどめました。その結果、あまり独自性のないアプリになってしまいました。

 解決策:後述するリーンキャンバスを作成し、同じテーマの他のサービスとの独自性を図る。

10.Ver.2に向けて

 別のWebアプリを作成することも考えましたが、子育てしている経験をWebアプリ開発に活かしたいのと、「新米ママ、パパの不安を少しでも埋めたい」という思いから、今のアプリをブラッシュアップしたVer.2を作成したいと思います。今回の反省を活かし、Ver.2でやりたいことをまとめました。(全部できるかわかりませんが。。。)

要件定義を具体的に行う

 Ver.1を作成して、計画性、サービスの独自性、デザイン等全ておいて要件定義の重要性を認識しました。Ver.2ではリーンキャンバスを利用して要件定義を具体的に行おうと思います。そして、あの技術を使いたい!よりも、この要件を満たすためにはこの技術が必要、という形で技術を選びたいです。
 また、現職では設計の工程管理をしていたので、その強みを生かして開発の工程表を作成したいです。

【リーンキャンバス】
スクリーンショット 2020-10-25 12.38.14.png

要件定義~システム設計ができる人材になれる記事

実務を意識してGit、Githubを使用する

 Ver.1ではbranchをきって、git pushするぐらいしかしていなかったので、GithubのissueやGit-flowの活用、疑似共同開発を意識してプルリクしたりと、実務を意識してGit、Githubのバージョン管理をしたいです。

Git-flowって何?
GitHub Cheat Sheet(日本語訳)

AWSへデプロイ、CI/CDパイプラインの構築

 実際に運用することを想定してVer.2を作成したいので、AWSへデプロイしてドメイン取得までしたいです。あとネットワークの勉強も兼ねて。
 また、後の作業効率を考えてCI/CDパイプラインの構築したいです。

想定する顧客や専門家に話を聞く

 新米ママ、パパに実際にサービスを使用してもらって、ヒヤリングしてブラッシュアップしたり、専門家に意見を聞いたりしたいです。

その他実装したい機能

 ・ユーザー登録にdeviseを使用
 ・パフォーマンスを考慮し、テンプレートエンジンをhamlかslimにする
 ・マイページに子ども情報(年齢、性別)を追加
 ・回答の返信機能
 ・ミニ日記機能
 ・ブックマーク機能
 ・フォロー、アンフォロー機能
 ・アンケート機能
 ・SNSシェア機能
 ・デザインを充実させる
 ・データベースをMySQL

11.まとめ

 Webアプリを開発してみて、下記のことがわかりました。

 ・よく言われる通り、アウトプットしつつのインプットが成長につながる
 ・1つのエラーに何時間も費やす経験も良かったが、質問できるような環境構築も大事
 ・要件定義に時間をかけることの重要性

この経験を今後に活かしたいです。
何かWebアプリに問題点、指摘がありましたらどしどしコメントください!

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

【Rails】amCharts4を用いたグラフ描画における第2縦軸の作成及び日本語化 他

はじめに

本記事では、JavaScriptのグラフ描画ライブラリのamChartを用いて、複数軸の線グラフの実装する際の第2縦軸の作成方法や日本語化などのカスタム方法を共有します。

開発環境

Ruby 2.5.1
Rails 5.2.4.4

amChartsとは

amChartsは、javascriptのグラフ描画ライブラリで、様々な種類の高機能なグラフを描画することができます。

公式リファレンス:https://www.amcharts.com/docs/v4/

日本語の情報が少ないため、カスタマイズする際は、公式リファレンスを参照することをおすすめします。

完成イメージ

現在、体重と体脂肪率を記録してグラフ化する機能を持つアプリを開発中で、
1つのグラフに横軸を日付、第1縦軸に体重、第2縦軸に体脂肪率を描画するため、amCharts4を使用しました。

下図のグラフが完成イメージとなります。
ezgif-6-ef32c8681700.gif

  • 横軸:日付、第1縦軸:体重、第2縦軸:体脂肪率
  • カーソル上のデータをTooltipで表示
  • スクロールバーで拡大
  • タイトルをマウスホバーするとTooltipを表示

実装手順/解説

amChartsの導入

amChartsの導入方法はこちらの記事が参考になります。
amcharts 4 Demos を使ってグラフを作成

横軸の値が不連続な場合のグラフの作成

本記事の横軸の値が不連続になる場合のグラフの作成は、下記の記事を参考にさせていただきました。
Railsにて不連続な間隔(日付など)で投稿された値をamChartsを使って折れ線グラフを作成する。

デモデータの準備

csvファイルをseedして以下のようなデモデータを準備します。

id date weight body_fat_percentage
1 2020/06/08 72 15
2 ・・・ ・・・ ・・・

完成サンプルコード

解説の前にサンプルコードを貼っておきます。
Rails側の記述は今回割愛します。

record.html.erb
<style>
  #chartdiv {
    width: 700px;
    height: 300px;
  }
</style>

//必要なJSファイルの読み込み
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>
<script src="//www.amcharts.com/lib/4/lang/ja_JP.js"></script>

<script>
am4core.ready(function() {

am4core.useTheme(am4themes_animated);

var chart = am4core.create("chartdiv", am4charts.XYChart);
chart.dateFormatter.language = new am4core.Language();
chart.dateFormatter.language.locale = am4lang_ja_JP;
chart.language.locale["_date_day"] = "MMMdd日";
chart.language.locale["_date_year"] = "yyyy年";

const weights = <%== JSON.dump(@weights) %>;
const body_fat_percentages = <%== JSON.dump(@body_fat_percentages) %>;
const dates = <%== JSON.dump(@dates) %>;

var firstDate = new Date(dates[0])
var lastDate = new Date(dates.slice(-1)[0])
var termDate = (lastDate - firstDate) / 1000 / 60 / 60 / 24 + 1

function generateChartData() {
  var chartData = [];
  for (var j = 0; j < weights.length; j++ ) {
    for (var i = 0; i < termDate; i++ ) {
      var newDate = new Date(firstDate)
      newDate.setDate(newDate.getDate() + i);
      if ((new Date(dates[j])) - (newDate) == 0 ){
        weight = weights[j]
        body_fat_percentage = body_fat_percentages[j]
        chartData.push({
          date1: newDate,
          weight: weight,
          date2: newDate,
          body_fat_percentage: body_fat_percentage
        });
      }
    }
  }
  return chartData;
}

chart.data = generateChartData();

//グラフタイトルの設定
var title = chart.titles.create();
title.text = "体重・体脂肪率の推移"; //グラフタイトルの設定
title.fontSize = 15; //グラフタイトルのフォントサイズの設定
//タイトルをマウスホバーした際に表示させるTooltipの表示内容設定
title.tooltipText = "スクロールバーで拡大できます。"; 

//第1横軸の設定
var dateAxis = chart.xAxes.push(new am4charts.DateAxis());
dateAxis.renderer.grid.template.location = 0;
dateAxis.renderer.labels.template.fill = am4core.color("#ffffff");

//第2横軸の設定
var dateAxis2 = chart.xAxes.push(new am4charts.DateAxis());
dateAxis2.tooltip.disabled = true; //Tooltipの非表示設定
dateAxis2.renderer.grid.template.location = 0;
dateAxis2.renderer.labels.template.fill = am4core.color("#000000");

//第1縦軸の設定
var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
valueAxis.tooltip.disabled = true;
valueAxis.renderer.labels.template.fill = am4core.color("#e59165");
valueAxis.renderer.minWidth = 60;
valueAxis.renderer.labels.template.adapter.add("text", function(text) {
  return text + "kg";
});
valueAxis.renderer.fontWeight = "bold"; //軸の値を太字に変更

//第2縦軸の設定
var valueAxis2 = chart.yAxes.push(new am4charts.ValueAxis());
valueAxis2.tooltip.disabled = true;
valueAxis2.renderer.grid.template.strokeDasharray = "2,3";
valueAxis2.renderer.labels.template.fill = am4core.color("#dfcc64");
valueAxis2.renderer.minWidth = 60;
valueAxis2.renderer.labels.template.adapter.add("text", function(text) {
  return text + "%";
});
valueAxis2.renderer.opposite = true; //第2縦軸を右側に設定
valueAxis2.renderer.fontWeight = "bold"; //軸の値を太字に変更

//第1縦軸用の値の設定
var series = chart.series.push(new am4charts.LineSeries());
series.name = "体重";
series.dataFields.dateX = "date1";
series.dataFields.valueY = "weight";
series.tooltipText = "{valueY.value}kg";
series.fill = am4core.color("#e59165");
series.stroke = am4core.color("#e59165");
series.smoothing = "monotoneX";
series.strokeWidth = 2;

//系列のポイントの設定(第1縦軸)
var bullet = series.bullets.push(new am4charts.Bullet());
var circle = bullet.createChild(am4core.Circle);
circle.width = 5;
circle.height = 5;
circle.horizontalCenter = "middle";
circle.verticalCenter = "middle";

//第1縦軸用の値の設定
var series2 = chart.series.push(new am4charts.LineSeries());
series2.name = "体脂肪率";
series2.dataFields.dateX = "date2";
series2.dataFields.valueY = "body_fat_percentage";
series2.yAxis = valueAxis2;
series2.xAxis = dateAxis2;
series2.tooltipText = "{valueY.value}%"; //ツールチップの表示設定
series2.fill = am4core.color("#dfcc64"); //ツールチップの色
series2.stroke = am4core.color("#dfcc64"); //グラフの線の色
series2.smoothing = "monotoneX";
series2.strokeWidth = 2;

//系列のポイントの設定(第2縦軸)
var bullet2 = series2.bullets.push(new am4charts.Bullet());
var circle2 = bullet2.createChild(am4core.Circle);
circle2.width = 5;
circle2.height = 5;
circle2.horizontalCenter = "middle";
circle2.verticalCenter = "middle";

chart.scrollbarX = new am4core.Scrollbar(); //スクロールバーの設定

//カーソルの設定
chart.cursor = new am4charts.XYCursor();
chart.cursor.xAxis = dateAxis2;

//凡例の設定
chart.legend = new am4charts.Legend();
chart.legend.parent = chart.plotContainer;
chart.legend.zIndex = 100;
chart.legend.position = "top";
chart.legend.contentAlign = "right";

//グリッド線の設定
valueAxis2.renderer.grid.template.strokeOpacity = 0.07;
dateAxis2.renderer.grid.template.strokeOpacity = 0.07;
dateAxis.renderer.grid.template.strokeOpacity = 0.07;
valueAxis.renderer.grid.template.strokeOpacity = 0.07;

});
</script>

<div id="chartdiv"></div>

解説

複数縦軸の設定

サンプルコードの通り、複数軸のグラフの場合、各軸及び各値の設定が必要になります。
それぞれの使用するデータなどの設定を行います。

  • 第1横軸:dateAxis
  • 第2横軸:dateAxis2
  • 第1縦軸:valueAxis
  • 第2縦軸:valueAxis2
  • 体重:series
  • 体脂肪率:series2

日本語化

横軸が日付の場合、デフォルトの表記が米国式のため下図のように英語表記になります。
スクリーンショット 2020-10-25 22.02.52.png

そのままでも問題は無いのですが、もし「◯月◯日」という表記にしたい場合は、以下の設定を追加します。
標準の翻訳設定では、例えば「Aug」を「8月」に翻訳はできますが、何日の方は「〇〇日」とは翻訳されないため、独自ルールを以下のように追加します。年も同様に行えます。

<script src="//www.amcharts.com/lib/4/lang/ja_JP.js"></script> //localeファイルの呼び出し
<script>
//中略
chart.dateFormatter.language = new am4core.Language(); //標準の翻訳設定
chart.dateFormatter.language.locale = am4lang_ja_JP; //標準の翻訳設定
chart.language.locale["_date_day"] = "MMMdd日"; 独自ルールで上書き
chart.language.locale["_date_year"] = "yyyy年"; 独自ルールで上書き
//中略
</script>

【参考リンク】
 https://www.amcharts.com/docs/v4/concepts/locales/
 https://github.com/amcharts/amcharts4/blob/master/src/lang/ja_JP.ts

第2縦軸の設定

デフォルトの設定ですと第1縦軸と第2縦軸は両方左側にあります。
スクリーンショット 2020-10-25 22.14.34.png
少し見にくいので、第2縦軸を右側に変更したい時は、以下の設定を追加します。

valueAxis2.renderer.opposite = true; //第2縦軸を右側に設定

他の追加設定を紹介

  • データポイントの設定

各系列毎にデータポイントの設定が行えます。circleをsquareに変えると四角に変更できます。

var bullet = series.bullets.push(new am4charts.Bullet());
var circle = bullet.createChild(am4core.Circle);
circle.width = 5;
circle.height = 5;
circle.horizontalCenter = "middle";
circle.verticalCenter = "middle";

【参考リンク】
 https://www.amcharts.com/docs/v4/concepts/bullets/

  • グラフの線を曲線に変更
series.smoothing = "monotoneX";
  • 色の変更
series2.fill = am4core.color("#dfcc64"); //ツールチップの色
series2.stroke = am4core.color("#dfcc64"); //グラフの線の色

まとめ

amChartsを使うと高機能なグラフを描画できます。
折れ線グラフ以外にも様々なグラフを作ることができます。
日本語の情報が少ないので、カスタマイズしたい場合は、公式リファレンスを参照することをおすすめします。

参考URL

amcharts 4 Demos を使ってグラフを作成
Railsにて不連続な間隔(日付など)で投稿された値をamChartsを使って折れ線グラフを作成する。

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

問題解決するには2つの記事を参考にすると解決できる!!!

どうも、三町哲平です!!

プロミングをしているととにかく分からない事が出まくりますよね!?
初めて行う実装やエラー解決の為にあれやこれやすると思いますが、そういう時は、本で調べたり、人に聞いたりすることがあるでしょうが、やっぱり一番するのって、ググって調べる事じゃないでしょうか...?

少なくとも私は9割以上の問題に対し、ググって解決しています。

しかし...しかしですよ。

例えばRuby on Railsで出た問題に対してあれやこれや調べたとしても開発環境が違ったり、データベースが違ったり、バージョンが違ったり、アプリ名が違ったりと全く一緒の条件でアプリ開発している可能性なんて限りなく0です。必ずどこかに違いがあります。

そんな微妙な違いによってただコピペしただけだは、中々実装できなかったり、エラー解決できなかったりして最終的には嫌になって止めてしまう...。

そうならない為にも問題に対峙する時にこれに気を付ければ上手く解決できるかもね...というやり方を一つ見つけたので、そのご紹介です。

問題解決するには2つの記事を参考にすると解決できる!!!

タイトル通りなのですが、答えは、
「問題解決するには2つの記事を参考にすると解決できる!!!」です。
もうこれが今回の結論なのですが、これだけじゃ分かりにくいので参考例をどうぞ!

1. やり方が分からない...。

まず、ググって調べるに当たって直面している壁といったらとにかくやり方が分からないって所です。

  • 何で、このエラーは出たのだろう?
  • どうやってこの機能を実装すればいいのだろう?   など、など...

その疑問に対して調べまくった結果、訳わからんって状態になる中で今回は、ja.ymlの書き方が分からないという問題の解決方法を模索していました。

スクリーンショット 2020-10-20 11.52.42.png

状態としては、投稿フォームで、紹介文というテキスト欄が空欄だった場合に表示するエラーメッセージを全て日本語表示にしたい。

つまり、Contentを入力してくださいContentを日本語にしたいという話です。

ちなみに、Contetは保存したいデータベース(postテーブル)のカラム名になります。

2. ググって行き着いた記事

ググると沢山似たような記事が検索に引っかかる事があります。その中で自分自身にとって分かりやすく、状況が似ている記事を参考にしていく中で私は、
Railsのバリデーションエラーのメッセージの日本語化 - Qiita
この記事を参考にさせて頂きました。


参考にした結果↑この様にja.ymlを作成して、入力しました。

これは、Railsのバリデーションエラーのメッセージの日本語化 - Qiita
カラム名の日本語化のコードをコピペしただけです。

この結果が実は先ほどお見せした投稿フォームの画像になります。↓再掲しています。
スクリーンショット 2020-10-20 11.52.42.png

まあ...つまりは、コードをコピペしただけでは、今回のカラムを日本語化したいという問題は、解決しなかった訳ですね。

3. ja.ymlの書き方が分からないからまたググってみる

ja.ymlの何処かが間違っている...何が違うんだ...!?
そんな疑問の中、エラーメッセージの日本語化を再度調べていく中で、
ActiveRecordのvalidatesで表示されるエラーメッセージのフォーマットを変更する - Qiita
こちらの記事を発見!!

そして、ja.ymlで、

config/local/models/ja.yml
ja:
  activerecord:
    models:
      user: ユーザー
    attributes:
      user:
        name: 名前

↑上記のコードを発見!

config/local/models/ja.yml
ja:
  activerecord:
    models:
      event: イベント
    attributes:
      event:
        name: イベント名
        place: 開催場所
        content: イベント内容

↑これが現在使用しているコードです。

ここで、タイトルにもどります。
問題解決するには2つの記事を参考にすると解決できる!!!
に戻ります。

2つのコードを見ている中で、これって、

config/local/models/ja.yml
ja:
  activerecord:
    models:
      user: ユーザー
    attributes:
      user:
        name: 名前

↑このコードだと、user:が、

config/local/models/ja.yml
ja:
  activerecord:
    models:
      event: イベント
    attributes:
      event:
        name: イベント名
        place: 開催場所
        content: イベント内容

↑このコードだと、event:の部分が、テーブル名だと気づいた私は...

[Before]


[After]

この様に変更して...

サーバーを再起動した結果は、
スクリーンショット 2020-10-20 11.56.58.png

無事、エラーメッセージを全て日本語化できました!!

結果

繰り返しになりますが、
「問題解決するには2つの記事を参考にすると解決できる!!!」はこれにて完了です。

...ご参考までに。

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

mergeメソッドについて改めて理解を深めた

はじめに

 formオブジェクトを用いて、フォームから複数のテーブルに情報を保存する機能を実装する過程で、いちばん悩んだエラーについて、忘れないために記録しておく。

想定している場面

 ユーザーが商品を購入する。formに入力したものをデータベースに保存すると同時に、どの商品をどのユーザーが購入したかも保存する。つまり、フォームで「購入」を押した時に、2つのテーブルに保存される。

mergeメソッドについて

 ストロングパラメーターを設定するときに、使うメソッド。

使用例

controllers.rb
private
  def user_order_params
    params.require(:user_order).permit(:postal_code, :prefecture_id).merge(user_id: current_user.id, item_id: params[:item_id])
  end

user_id: current_user.idのcurrent_userメソッドが使えるのは、deviseのGemを導入しているため。
item_id: params[:item_id]で値を入れることができるのは、ルーティングをネストし、URLにitem_idを含めているため。
ストロングパラメーターはprivateメソッド以下に記述する。

requireの引数は、モデル名。
permitの引数は、DBのカラム名。

ターミナルで確認できるパラメーター

 "user_order"=>{"hoge"=>"", "postal_code"=>"", "prefecture_id"=>"1"}, "commit"=>"購入", "controller"=>"orders", "action"=>"create", "item_id"=>"7"}

mergeメソッドを使う場面

 form_withでユーザーが記入した内容はハッシュの中に、キーと一緒に入っているが、ユーザーが記入しない内容も保存したい時。
例えば、ユーザーのidやその商品のidについては、ユーザーが直接入力することはないが、パラメーターに含めて、DBの保存したい。そのような時に、mergeメソッドを使って、パラメーターに含めたいキーと値を記述する。
上記の例では、user_idをcrrent_user.idから、item_idをURLに含めたparamsから取ってきて、パラメーターに含めている。

最後に

 エラーが解決できた時、マージかぁと一人呟いたとさ…。

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

「API開発 + Swagger UIを利用したAPI検証」な環境をDockerで構築する

Swaggerを利用することでREST APIの仕様をドキュメント化できます。
SwaggerではREST APIの仕様をドキュメントしたファイルをSwagger Specと呼びます。

Swagger UIとはSwagger Specの情報を反映させた静的ページを生成するツールのことを言います。
Swagger UIはSwagger Specの情報を可視化するだけでなく、画面上からREST APIを実行する機能も提供しています。

今回は「開発中のAPIをSwagger Specでドキュメント化 → Swagger Specの情報が反映されたSwagger UIの画面からAPIリクエストの検証」という一連の作業が行えるDocker環境の構築手順について紹介します。

今回作成するDocker環境について

仕様は以下の通りです。

  • docker-compose upだけで環境が準備できる
  • 「/swagger-ui」でSwagger UIの画面が表示される
  • 「/swagger-ui」以外はAPI用のエンドポイントとする
  • Swagger Spec編集後、リロードでSwagger UIに変更内容が反映される
  • APIはRuby on RailsのAPIモードで作成する
  • DBはMySQLを利用

nginxをリバースプロキシとして利用することでAPIの開発環境とSwagger UIを組み合わせます。
図で表現すると以下のようになります。

スクリーンショット 2020-10-25 20.17.16.png

今回の利用する各種バージョンは以下の通りです。

  • Ruby on Rails: 6.0.3.2
  • Ruby: 2.7.1
  • MySQL: 8.0.21
  • nginx: 1.19.3

API開発環境をDockerに作成する

APIモードで作成する『Rails 6 x MySQL 8』Docker環境構築手順を参考に、RailsのAPIモードを利用してAPI開発環境を作成します。

Dockerfileとdocker-compose.ymlは以下の通りです。

Dockerfile
FROM ruby:2.7.1

# 作業ディレクトリを/rails_api_swaggerに指定
WORKDIR /rails_api_swagger

# ローカルのGemfileをDokcerにコピー
COPY Gemfile* /rails_api_swagger/

# /rails_api_swaggerディレクトリ上でbundle install
RUN bundle install
docker-compose.yml
version: '3'
services:
  api: # Ruby on Railsが起動するコンテナ
    build: .
    ports:
      - '3000:3000' # localhostの3000ポートでアクセスできるようにする
    volumes:
      - .:/rails_api_swagger # アプリケーションファイルの同期
    depends_on:
      - db
    command: ["./wait-for-it.sh", "db:3306", "--", "./start.sh"]
  db: # MySQLが起動するコンテナ
    image: mysql:8.0.21
    volumes:
      - mysql_data:/var/lib/mysql # データの永続化
      - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    command: --default-authentication-plugin=mysql_native_password # 認証方式を8系以前のものにする。
    environment:
      MYSQL_USER: 'webuser'
      MYSQL_PASSWORD: 'webpass'
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'rails_api_swagger_development'
volumes:
  mysql_data: # データボリュームの登録

RailsのAPIモードでは静的ページを生成する機能は除外されているため、Swagger UIを直接Railsアプリケーションに組み込むことはできませんが、今回の方法を利用すればAPIモードでもSwagger UIを利用できます。

サンプルとなるAPIを作成します。

# コンテナをバックグランドで起動
$ docker-compose up -d

# Eventを操作する機能(モデル、ビュー、コントローラー)を一括作成
$ docker-compose exec api rails g scaffold event title:string

# eventsテーブルを作成
$ docker-compose exec api rails db:migrate

# rails consoleでeventsのレコードを作成
$ docker-compose exec api rails c
> event = Event.new(title: 'サンプルイベント')
> event.save

localhost:3000/eventsにアクセスして以下のようなレスポンスが返ってくればOKです。

リバースプロキシの設定を行い、nginx経由でAPIにアクセスできるようにする

nginx経由でRailsアプリケーションにアクセスできるようリバースプロキシの設定を行ます。

default.conf
server {
  listen 80;
  server_name  localhost;

  # "/"にアクセスがあったときの処理
  location / {
    proxy_set_header Host localhost; # アクセス元のホストをlocalhostにする
    proxy_pass http://api:3000; # apiコンテナの3000ポートにリクエストを送る
  }
}

nginxの設定ファイルは/etc/nginx/nginx.confです。
設定ファイルにinclude /etc/nginx/conf.d/*.conf;という記述があることからも分かるように、設定ファイルでは/etc/nginx/conf.d配下の.confという拡張子の設定も読み込んでいます。

つまり、/etc/nginx/conf.d配下に今回作成した設定ファイルを配置することで、nginxコンテナをリバースプロキシとして利用できます。

docker-compose.ymlにnginxコンテナを追加します。

docker-compose.yml
services:
  api:
    (略)
  db:
    (略)
  nginx:
    image: nginx:1.19.3
    ports:
      - '80:80'
    command: [nginx-debug, '-g', 'daemon off;']
    volumes:
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - api
volumes:
  mysql_data:

[nginx-debug, '-g', 'daemon off;']はデバッグモードによる起動方法です。1

なお、swagger-uiのDockerイメージもnginxを利用していますが、nginx.confinclude /etc/nginx/conf.d/*.conf;の記述がありません。
ですので、swagger-uiのDockerイメージを利用する場合だと今回のアプローチはうまくいかないので注意してください。

コンテナ起動後、localhost:80/eventsにアクセスして以下のようなレスポンスが返ってくればOKです。

Swagger UIをnginxに組み込む

/swagger-uiにアクセスをしたらSwagger UIの画面が表示されるようにnginxにSwagger UIを組み込んでいきます。

Swagger UIの画面はswagger-ui/distによって構成されています。

dist配下のファイルをローカルにコピーし、nginxコンテナにバインドマウントすることで、Swagger UIをnginxに組み込みます。

dist配下のファイルをすべてコピーしてきてもよいのですが、unpkgを利用することでindex.htmlのみをコピーするだけでSwagger UIの画面が作成できます。 2

index.html
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Swagger UI</title>
-   <link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
+   <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css" >
    <style>
      html
      {
        box-sizing: border-box;
        overflow: -moz-scrollbars-vertical;
        overflow-y: scroll;
      }

      *,
      *:before,
      *:after
      {
        box-sizing: inherit;
      }

      body
      {
        margin:0;
        background: #fafafa;
      }
    </style>
  </head>

  <body>
    <div id="swagger-ui"></div>

-   <script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
+   <script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js" charset="UTF-8"> </script>
-   <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
+   <script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>

    <script>
    window.onload = function() {
      // Begin Swagger UI call region
      const ui = SwaggerUIBundle({
        url: "https://petstore.swagger.io/v2/swagger.json",
        dom_id: '#swagger-ui',
        deepLinking: true,
        presets: [
          SwaggerUIBundle.presets.apis,
          SwaggerUIStandalonePreset
        ],
        plugins: [
          SwaggerUIBundle.plugins.DownloadUrl
        ],
        layout: "StandaloneLayout"
      })
      // End Swagger UI call region

      window.ui = ui
    }
  </script>
  </body>
</html>

docker-compose.ymlを修正し、作成したindex.htmlをnginxのデフォルトの公開ディレクトリである/usr/share/nginx/html配下に配置します。

docker-compose.yml
services:![スクリーンショット 2020-10-25 18.23.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140792/22cf7096-e237-7c2e-3aa5-0959d4776657.png)

  api:
    (略)
  db:
    (略)
  nginx:
    image: nginx:1.19.3
    ports:
      - '80:80'
    command: [nginx-debug, '-g', 'daemon off;']
    volumes:
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
+     - ./nginx/html/swagger-ui:/usr/share/nginx/html/swagger-ui
    depends_on:
      - api
volumes:
  mysql_data:

/swagger-uiにアクセスしたらindex.htmlが表示されるようnginxの設定を追記します。

default.conf
server {
  listen 80;
  server_name  localhost;

  location / {
    proxy_set_header Host localhost;
    proxy_pass http://api:3000;
  }

  # "swagger-ui"にアクセスがあったときの処理
  location /swagger-ui {
    alias /usr/share/nginx/html/swagger-ui;
  }
}

コンテナ起動後、localhost:80/swagger-uiにアクセスして以下のような画面が表示されればOKです。

スクリーンショット 2020-10-25 18.23.07.png

ローカルのSwagger SpecがSwagger UI上に反映されるようにする

Swagger UIのurlを変更することで参照するSwagger Specを変更できます。

ローカルのSwagger Specを参照するように変更します。

index.html
- url: "https://petstore.swagger.io/v2/swagger.json",
+ url: "./api.yml",

上記の変更でローカル環境に配置された./nginx/html/swagger-ui/api.ymlの内容がSwagger UIへ反映されます。

なお、./nginx/html/swagger-ui/ディレクトリはバインドマウントされているので、ローカルでSwagger Spec編集後、リロードすればコンテナのSwagger UIに変更内容が反映されます。

サンプルとして作成したGET /eventsを実行するSwagger Specは以下の通りです。

api.yml
openapi: 3.0.2
info:
  title: サンプルAPI
  version: 1.0.0
servers:
  - url: http://localhost:3000
tags:
  - name: イベント
paths:
  /events:
    get:
      tags:
        - イベント
      description: イベント一覧取得
      responses:
        200:
          description: 成功
          content:
            application/json:
              schema:
                type: array
                description: イベントの配列
                items:
                  $ref: "#/components/schemas/Event"
components:
  schemas:
    Event:
      type: object
      properties:
        id:
          description: ID
          type: integer
          format: int64
          example: 1
        title:
          description: タイトル
          type: string
          example: サンプルイベント
        created_at:
          description: 作成日
          type: string
          format: date-time
          example: 2020-04-01 10:00
        updated_at:
          description: 更新日
          type: string
          format: date-time
          example: 2020-04-01 10:00

コンテナ起動後、以下のような画面が表示されればOKです。

スクリーンショット 2020-10-25 18.24.54.png

CORSの設定をする

Swagger UIはlocalhost:80、APIはlocalhost:3000で起動しています。
この状態でSwagger UIからAPIにリクエストを送るとオリジンをまたがっているためAccess to fetch at 'http://localhost:3000/events' from origin 'http://localhost' has been blocked by CORS policyというエラーが発生します。

スクリーンショット 2020-10-25 18.37.42.png

CORSの設定を行い、Swagger UIからAPIリクエストが送れるようにします。
今回はrack-corsを利用してCORSの設定を行ます。

Gemfile
gem 'rack-cors'
config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  unless Rails.env.production?
    allow do
      origins(['localhost', /localhost:\d+\Z/])

      resource '*',
        headers: :any,
        methods: [:get, :post, :put, :patch, :delete, :options, :head]
    end
  end
end

コンテナ起動後、リクエストが正常に返ってくればOKです。

スクリーンショット_2020-10-25_18_39_30.png

参考: CRUD操作を行うSwagger Spec

  • GET /events
  • POST /events
  • GET /events/{id}
  • PATCH /events/{id}
  • DELETE /events/{id}

上記のエンドポイントに関するSwagger Specは以下の通りです。

api.yml
openapi: 3.0.2
info:
  title: サンプルAPI
  version: 1.0.0
servers:
  - url: http://localhost:3000
tags:
  - name: イベント
paths:
  /events:
    get:
      tags:
        - イベント
      description: イベント一覧取得
      responses:
        200:
          description: 成功
          content:
            application/json:
              schema:
                type: array
                description: イベントの配列
                items:
                  $ref: "#/components/schemas/Event"
    post:
      tags:
        - イベント
      description: イベント登録
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  example: サンプルイベント
      responses:
        201:
          description: 作成
  /events/{event_id}:
    get:
      tags:
        - イベント
      description: イベント詳細
      parameters:
        - name: event_id
          in: path
          description: イベントID
          required: true
          schema:
            type: integer
            format: int64
            example: 1
      responses:
        200:
          description: 成功
          content:
            application/json:
              schema:
                type: object
                $ref: "#/components/schemas/Event"
        404:
          description: event not found
    patch:
      tags:
        - イベント
      description: イベント更新
      parameters:
        - name: event_id
          in: path
          description: id
          required: true
          schema:
            type: integer
            format: int64
          example: 1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  example: サンプルイベント
      responses:
        200:
          description: 成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  activity:
                    $ref: "#/components/schemas/Event"
    delete:
      tags:
        - イベント
      description: イベント削除
      parameters:
        - name: event_id
          in: path
          description: id
          required: true
          schema:
            type: integer
            format: int64
            example: 1
      responses:
        204:
          description: No Content
components:
  schemas:
    Event:
      type: object
      properties:
        id:
          description: ID
          type: integer
          format: int64
          example: 1
        title:
          description: タイトル
          type: string
          example: サンプルイベント
        created_at:
          description: 作成日
          type: string
          format: date-time
          example: 2020-04-01 10:00
        updated_at:
          description: 更新日
          type: string
          format: date-time
          example: 2020-04-01 10:00

画面は以下のようになります。

スクリーンショット 2020-10-25 18.44.00.png

まとめ

以上でAPIとSwagger UIを統合した開発環境の構築手順の紹介を終わります。

  • nginxを利用することでSwagger UIとAPIを組み合わせる
  • nginxのリバースプロキシ設定は『/etc/nginx/conf.d』配下に作成
  • オリジンをまたがるリクエストをする際はCORSの設定が必要になる
  • 自作のSwagger SpecをSwagger UIに反映させるにはindex.htmlのurlを変更する

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

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

【3分でわかる】Rubyの継承とは?わかりやすく要点のみ解説!

はじめに

Rubyを学び始めると「継承」についての理解が必要になります。難しく考えなくても、なーんだそんなことか。となるように3分でまとめます。

結論:重複しているところをまとめてくっつけるだけ

下記の中で、[attr_accessor :a]と[def aa]が被っていて不効率だなと思う時ありますよね。そういう時に、被っている[attr_accessor :a]と[def aa]をまとめたclass Cを作ります。

class A

  attr_accessor :a :b :c

    def aa
    end

    def aaa
    end
end

class B

  attr_accessor :c :d :e

    def aa
    end

    def bbb
    end
end


クラスCを作ってAとBに継承させる

class C
#何度も書いていた2つを新たなクラスCに記載
  attr_accessor :c

  def aa
  end
end

class A < C #<Cと記載することで最初と同じ機能

  attr_accessor :b :c

    def aaa
    end
end

class B < C #<Cと記載することで最初と同じ機能

  attr_accessor :d :e

    def bbb
    end
end


今回、新しく作ったclass Cは親クラス、継承するclass Aとclass Bはサブクラスと呼ばれるようなので覚えておきましょう。

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

【並び替え】あるユーザーのフォロー、フォロワーをフォローした、フォローされた順(降順)に並び替える!

概要

あるユーザーのフォロー、フォロワーをフォローした、フォローされた順(降順)に並び替えた時のことを備忘録として記録します。

環境

・ruby '2.5.7'
・rails '5.2.3'

前提

・ユーザーのフォロー機能は実装済であること

【参考】
第14章 ユーザーをフォローする - Railsチュートリアル

過程

1.実装することの確認

「あるユーザー(@user)のフォロー、フォロワーを(@users)フォローした、フォローされた順(降順)に並び替える(order("relationships.created_at DESC"))」

これを具体的にコードにしていきます!

2.following,followersアクションを定義する

users_controllerにfollowing,followersアクションを定義していきます。

controllers/users_controller.rb
class UsersController < ApplicationController
(省略)

  def following
    @title = "フォロー"
    @user  = User.find(params[:id])

    get_follower_user_ids = Relationship.where(follower_id: @user.id).pluck(:followed_id)
    @users = User.includes(:passive_relationships).where(id: get_follower_user_ids).order("relationships.created_at DESC").paginate(page: params[:page])

    render 'show_follow'
  end

  def followers
    @title = "フォロワー"
    @user  = User.find(params[:id])

    get_followed_user_ids = Relationship.where(followed_id: @user.id).pluck(:follower_id)
    @users = User.includes(:active_relationships).where(id: get_followed_user_ids).order("relationships.created_at DESC").paginate(page: params[:page])

    render 'show_follow'
  end

(省略)
end

コードを順番に説明していきます!(followingアクションのみ説明します)

① @user = User.find(params[:id])で表示されているユーザーを@userに代入します。

② get_follower_user_ids = Relationship.where(follower_id: @user.id).pluck(:followed_id)@userにフォローされているユーザーのidをget_follower_user_idsに代入します。

③ @users = User.includes(:passive_relationships).where(id: get_follower_user_ids).order("relationships.created_at DESC").paginate(page: params[:page])@userにフォローされているユーザーを@usersに代入します。

 ここで、includes(:passive_relationships)とすることで、Relationshipモデルを参照できるようになり、order("relationships.created_at DESC")で並び替えることができます。

結果

これで、あるユーザーのフォロー、フォロワーをフォローした、フォローされた順(降順)に並び替えることができました!

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

配列の中に配列がある場合の値の出力の仕方

普通の配列

colors =["white", "black", "red", "green", "blue"]

colorsという配列の中に、5つの要素が入っています。
先頭から順番に0、1、2、3…とインデックス番号が割り当てられています。
※1、2、3…ではないので注意!

出力する際は、

puts colors[2]

とすると、3番目のredが取り出せます。

配列の中に配列

animals = [["tuna", "octopus", "shark"], ["dog", "cat", "pig"], ["crow", "swan", "eagle"]]

こんな感じのやつです。
animalsという配列の中に、海の動物、陸の動物、空の動物をそれぞれ3種類ずつ入れた配列が入っています。

中身を全てまとめて出力する

puts animals

<出力結果>
tuna
octopus
shark
dog
cat
pig
crow
swan
eagle

海の動物だけを出力する

海の動物は0というインデックス番号が割り当てられています。

puts animals[0]

<出力結果>
tuna
octopus
shark

タコだけを出力する

インデックス番号0という海の動物の中で、
さらにインデックス番号1が割り当てられています。

puts animals[0][1]

<出力結果>
octopus

指定して全て出力したもの

=> は出力されたものを表しています。

puts animals[0][0]
=> tuna
puts animals[0][1]
=> octopus
puts animals[0][2]
=> shark
puts animals[1][0]
=> dog
puts animals[1][1]
=> cat
puts animals[1][2]
=> pig
puts animals[2][0]
=> crow
puts animals[2][1]
=> swan
puts animals[2][2]
=> eagle

さらに複雑な配列

構造が複雑になっても、同じような感じで取り出すことができます。

countries = [["Japan", "America"], [["Brazil", "Russia"],["China", "India"]]]

countriesという配列の中に2つの配列があり、
2つ目の配列の中にはさらにもう1つの配列が入っています。

puts countries[0]

<出力結果>
Japan
America
puts countries[1]

<出力結果>
Brazil
Russia
China
India
puts countries[1][0]

<出力結果>
Brazil
Russia

指定して全て出力したもの

puts countries[0][0]
=> Japan
puts countries[0][1]
=> America
puts countries[1][0][0]
=> Brazil
puts countries[1][0][1]
=> Russia
puts countries[1][1][0]
=> China
puts countries[1][1][1]
=> India

ここからさらに四重とかになったとしても、同様の記述で値を取り出せます。

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

【Rails】パフォーマンス改善にすぐに役立つTips集

はじめに

Railsにおけるパフォーマンス改善に役立つTipsを集めてみました。
すぐに使えるものから、少し改善に時間がかかるものまで幅広く集めています。

開発中のアプリのパフォーマンス改善のお役に立てれば幸いです。

【Tips1】 N+1を改善する

何はともあれN+1が発生していたら、それを解消するようにしましょう。
大抵の場合、そこがアプリのパフォーマンスのボトルネックになっているはずです。

books_controller.rb
class BooksController < ApplicationController
  def index
    @book = Book.all
  end
end
index.html.erb
<% @book.each do |book|
  <%= book.title %>
  <%= book.user.name %> # ここでN+1が起きている
<% end %>     

上のコード例だとbookの関連先のuserを読み込むところでN+1が起きています。

下記のようにコントローラーを書き換えましょう。

books_controller.rb
class BooksController < ApplicationController
  def index
    @book = Book.includes(:user)
  end
end

モデル.allで全てのモデルを取得してきている場所はN+1が起きている可能性が高くなり、大抵allは使わなくなることが多いです。
N+1を検知するためにgemのbulletをアプリへ導入するのもおすすめです。

https://github.com/flyerhzm/bullet

また、上の例だとincludesを使用していますが、preloadeager_loadを使い分けられるようになるといいですね。

ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い

【Tips2】 countではなくてsizeを使用する

countを使用するとSQLを発行してしまいます。
そのため、モデルの数などを調べたい時はsizeなどで代用しましょう。

意外とやってしまいがちなケースですが、countsizeへ置き換えるだけなので、手軽にできます。

countを使用した場合

user = User.all
user.count
# count関数を用いたSELECT文が発行されてしまう
   (4.6ms)  SELECT COUNT(*) FROM `users`
=> 100

sizeを使用した場合

user = User.all
user.size
# SQLクエリは発行されない
=> 100

【Tips3】exist?の使用を控える

countと同じくexist?はモデルオブジェクトに使用すると、sqlを発行してしまいます。
存在するかを確認したい場合などはpresent?などで代用しましょう。

exsit?を使用した場合

user = User.where(deleted: true)
user.exist?
# SQLが発行される
  User Load (5.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`deleted` = TRUE
=> []

present?を使用した場合

user = User.where(deleted: true)
user.present?
# SQLは発行されない
=> []

【Tips4】allで取得した結果をeachで回さない

全ユーザーをallで取得してそれをeachで回す・・・のようなパターンです。
バッチ処理とかにありがちなパターンですね。

User.all.each do |user|
  # 何かuserのオブジェクトを使用して処理をするコード
end

N+1のTipsでも触れましたが、にallが処理に入ってきたときは一度そのコードを疑ってかかりましょう。

上述の実装だとUserの全件をメモリに展開してから、ひとつひとつの処理をeachで行っていくため、メモリ消費が激しくなります。

allのかわりにfind_eachを使いましょう。

User.find_each do |user|
  # 何かuserのオブジェクトを使用して処理をするコード
end

find_eachはレコードを1000件取得ずつ取得し、その取得したレコードを1件ずつ処理してきてくれます。

1000件取得し終わったらまた、次の1000件を取得してきて・・・の繰り返しとなります。

ちなみにレコード展開数を指定したかったらその姉妹メソッドであるfind_in_batchesを使用しましょう。メソッドの引数で、レコード数を指定できます。

【Tips5】不必要なActive Recordオブジェクトの生成

上述のN+1問題や、eachパターンほどではないですが、これも気をつけないとやってしまいがちな実装となります。

user_names = User.all.map(&:name)

上述のmapを使用したやり方は不必要なActive Recordオブジェクトを生成してしまい、パフォーマンスがあまりよくありません。

Active Recordオブジェクトは膨大な数のモジュールやメソッドをラップしているので、生成コストが高く、それだけでメモリを費やしてしまいます。

Active Recordオブジェクトを生成しなくてもよいやり方があるならば、そちらのやり方を考えましょう。

user_names = User.pluck(:name)

pluckを使用することで、不要なオブジェクト生成を回避できました。

【Tips6】 キャッシュや非同期処理の導入を検討する

パフォーマンス悪化箇所にはキャッシュを使ったり、処理を非同期にする方法もあります。

キャッシュを使う
Redisなどの導入を検討する。マスター系のデータ読み込みなどに検討してみるといいかも。
ただ、導入箇所をよく検討しないとバグの原因になりがちです。

処理を非同期にする
gemのsidekiqdelayed jobなどで重い処理を非同期にしてしまう。
メール送信処理を非同期にする方法をよく見かけます。

終わりに

いかがだったでしょうか。
以上、Railsでコーディングする上で意識するだけで簡単にパフォーマンス改善ができるTipsを紹介しました。

他にもこんなやり方あるよだったり、ここが間違っているよなどありましたらコメント欄でそっとお知らせください笑

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

【Ruby on Rails】URLのidをカラム名に変更

はじめに

Railsでは、routes.rbでresourcesメソッドを使用すると、通常:idパラメータがデフォルトで使われますが、
場合によっては表示を変更したい場合もあるかと思います。
今回はidをnameカラムに変更する方法を記載します。

目標

スクリーンショット 2020-10-25 16.32.12.png

スクリーンショット 2020-10-25 16.31.38.png

開発環境

ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina

前提

※ ▶◯◯ を選択すると、説明等が出てきますので、
  よくわからない場合の参考にしていただければと思います。

【Ruby on Rails】gemのdeviseを使用し、名前とパスワードのみでログインする方法
こちらをベースにURLを変更していきます。

準備

前提記事では、URLがmypageになっているため、
今回はあえてidをつけたURLにしていきます。
※変更点のみ記述します。

config/routes.rb
get 'mypage', to: 'homes#mypage'

get 'mypage/:id', to: 'homes#mypage', as: 'mypage'
app/controllers/users/registrations_controller.rb
def after_sign_up_path_for(resource)
  mypage_path
end

def after_sign_up_path_for(resource)
  mypage_path(@user)
end
app/controllers/users/sessions_controller.rb
def after_sign_in_path_for(resource)
  mypage_path
end

def after_sign_in_path_for(resource)
  mypage_path(@user)
end
app/controllers/homes_controller.rb
def mypage
end

def mypage
  @user = User.find(params[:id])
end

これでidがついたURLができました。

idをnameカラムに変更

同一の名前のURLをなくす

URLをカラムにするためには、同一のnameをなくす必要がありまあす。
そこでvalidationをかけます。

モデルのバリデーション

app/models/user.rb
validates :name, uniqueness: true

DBのバリデーション

ターミナル
$ rails g migration add_index_users_name
db/migrate/xxxxxxxxxxxxx_add_index_users_name.rb
class AddIndexUsersName < ActiveRecord::Migration
  def change
    add_index :users, :name, unique: true
  end
end
ターミナル
$ rails db:migrate

実際にidをnameに変更

findをfind_byにするのが肝です。

config/routes.rb
get 'mypage/:id', to: 'homes#mypage', as: 'mypage'

get 'mypage/:name', to: 'homes#mypage', as: 'mypage'
app/controllers/homes_controller.rb
def mypage
  @user = User.find(params[:id])
end

def mypage
  @user = User.find_by(name: params[:id])
end

加えてリンク先もnameで指定するために
to_paramメソッドを使用します、

app/models/user.rb
validates :name, uniqueness: true

# ↓追加
def to_param
  name
end

これで目標と同じになるはずです。

resourcesを使用する場合

resourcesに下記のように追記すればOKです。
resources :users, param: :name

これでリンク先もnameで表示されるはずです。

まとめ

このように数字ではなく、文字列にすることで、
どのページにいるかがわかりやすく、利便性が高まります。
また、SNSのようなサービスで数字のIDを使用してしまうと、
今何人の登録者がいるかわかってしまうため、あまり推奨できる方法ではないです。
その時はこの方法を試してみてください。

またtwitterではQiitaにはアップしていない技術や考え方もアップしていますので、
よければフォローして頂けると嬉しいです。
詳しくはこちら https://twitter.com/japwork

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

Rubyでheavy(蛇)!?

0.恐竜×スキルチェック

ここのところ激務につき、ほとんどスキルアップできていなかった。
念願のテレワークになっているのに何故か、テレワーク前より忙しくなっている。
そして何より、恐竜ちゃんを出すプログラムのアイデアがなくなりつつある。
さらに最近知ったのだが、

「恐竜は爬虫類である」

そこでスキルチェックで見つけてしまったのが「へび」。

つけたタイトルも何気なく気に入っているし運命を感じたので、
解くためのプロセスを書いてみた。

そしてランクが上がるほど、問題文が長くなり、『理解度とその世界観についていけるか』の勝負になる。
『問題を解けることが目的』なのであって、
コードの美しさ、短さを求める方には、ここから先の文章は不要です。

最後に汚いコードを載せてみる。
そして、『どこか懐かしさと寒さを感じさせるタイトルはパリピには受けない事』
を反省しつつ始めてみる。

1.問題「へび」

問題を画像で貼り付けてみる。

D7BA2F36-25C6-42F5-9BC6-C268DDF6C63E.jpeg

43193582-B07E-404F-82A8-37E5032D640E.jpeg

2.移動先の境界チェック

2.1移動範囲

一行目に表示される情報として、ヘビの移動できる縦幅と横幅がある。
この座標縦をy、横をxとすると
0<=y<=H-1,0<=x<=W-1が移動可能範囲となる。

2.2移動先に壁がないか

壁をあらわすのは#で、.の場合は移動可能。
ここでわかるのは、2.1を満たさない場合がNG,
2.2を満たさない場合もNG,両方満たす場合がOKとなる。

※余談だが、インドミナスレックスなどの恐竜様は壁ごとぶち破るので
このルールは通用しない。(俺様がルールだという奴でしょう。)

上記を判定する関数として作成したのがcheck_limit関数

3.方向と遷移する座標

ここでは、方角(向いている方角)と進む向きが与えられた時に、
遷移先の座標が現在の座標と比較して、行方向と列方向にどのくらい
移動するか、次の方角を確認する。

図で表すと下記の関係性になる

C53E1D06-9E48-4A54-A78A-5B3DADA94193.jpeg

3.1向いている方角が北(N)の時

右向きに進む時、今いる場所から右に、つまり0行1列移動する。
次の方角は、東になる。
左向きに進む場合、今いる場所から左に移動、つまり0行−1列移動する。
次の方角は西になる。
向きが指定されていない場合(方向転換する時刻でない場合がここにあたり、noneとする)、今いる場所から上に移動、つまり−1行0列移動する。
次の方角は、北になる。

3.2向いている方角が東(E)の時

右向きに進む時、今いる場所から1行(下)0列移動する
次の方角は、南になる。
左向きに進む場合、今いる場所から−1行(上)0列移動する。
次の方角は、北になる。
向きがnoneの時は、今いる場所から0行1列(右)移動する
次の方角は東になる。

この情報を元に作成したのがcheck_direction関数
(checkというよりgetがいい事に今気がつく。)

4.座標重複チェック

一度通った座標は既に蛇の胴体があるので進めない。
との事。どうやらアナコンダのような巨大な蛇を想定しているらしい。
遷移先が既に通った座標の場合、移動がストップ。

5.コード例

全体的な流れ
(1)入力値を取得
(2)方向転換する時刻配列(tarr配列)とそれに対応する方向配列(lrarr)
や座標データ取得(arr2配列)を整理する処理

(3)座標の行き止まり判定や、移動座標や移動方角取得関数の作成
(4)時刻0から99までの間、以下を繰り返す
(ⅰ)現在の座標(sy,sx)と向いてる方角と進む方向に対し、遷移先座標(ny,nx)と次の方角(next_dir)を求める
(ⅱ)遷移先座標が行き止まりかチェック(行き止まりの場合ループを抜ける)
(ⅲ)行き止まりでない場合、今まで通った座標でないかチェックする
(ⅳ)今まで通った座標でない場合、この座標を(sy,sx)とし、snake_arrに格納する。
(ⅴ)今まで通った座標の場合(snake_arrに登録されている座標の場合)ループを抜ける
(5)snake_arr配列に登録された座標のarr2データに*を代入する
(6)arr2を出力

# 自分の得意な言語で
# Let's チャレンジ!!

in1 = gets.chomp!
#puts(in1)
arr1=in1.split(' ')
#print(arr1)


row1=arr1[0].to_i
col1=arr1[1].to_i
sy=arr1[2].to_i
sx=arr1[3].to_i
num1=arr1[4].to_i


arr2 = Array.new(row1) { Array.new(col1,"") }

#print(arr2)

for i in 1..row1 do
    tmp1=gets.chomp.to_s
    for j in 1..col1 do
        arr2[i-1][j-1]=tmp1[j-1...j]
    end
end

tarr=[]
lrarr=[]


for i in 1..num1 do
    tmp1=gets.chomp.to_s
    arr3=tmp1.split(" ")
    tarr.push(arr3[0].to_i)
    lrarr.push(arr3[1])
    #in3.push(tmp1)
end



def check_limit(arr0,i,j,row1,col1)
    if i<0 or i>row1-1
        flg=1
    elsif j<0 or j>col1-1
        flg=1
    elsif arr0[i][j]=="#"
        flg=1
    else
        flg=0
    end
    return flg
end 


def check_direction(p_dir,rlstr)
    retstr=""
    if p_dir=="N"
        if rlstr=="R"
            retstr="0,1,E"
        elsif rlstr=="L"
            retstr="0,-1,W"
        else
            retstr="-1,0,N"
        end
    elsif p_dir=="S"
        if rlstr=="R"
            retstr="0,-1,W"
        elsif rlstr=="L"
            retstr="0,1,E"
        else
            retstr="1,0,S"
        end
    elsif p_dir=="E"
        if rlstr=="R"
            retstr="1,0,S"
        elsif rlstr=="L"
            retstr="-1,0,N"
        else
            retstr="0,1,E"
        end
    else
        if rlstr=="R"
            retstr="-1,0,N"
        elsif rlstr=="L"
            retstr="1,0,S"
        else
            retstr="0,-1,W"
        end
    end
    return retstr
end


def find_lr(sarr,retarr,i2,num1)
    rnum=-1
    for i in 0..num1-1 do
        if sarr[i]==i2
            rnum=i
            break
        end
    end
    return retarr[rnum]
end



snake_arr=[]
snk_str=arr1[2]+" "+arr1[3]
snake_arr.push(snk_str)
#print(snake_arr)
p_dir='N'
flg_e=0
for i in 0..99 do
    if tarr.count(i)==0
        retstr=check_direction(p_dir,"none")
        tmparr=retstr.split(",")
        ny=tmparr[0].to_i+sy
        nx=tmparr[1].to_i+sx
        next_dir=tmparr[2]
        flg=check_limit(arr2,ny,nx,row1,col1)

    else
        lrstr=find_lr(tarr,lrarr,i,num1)
        retstr=check_direction(p_dir,lrstr)
        tmparr=retstr.split(",")
        ny=tmparr[0].to_i+sy
        nx=tmparr[1].to_i+sx
        next_dir=tmparr[2]
        flg=check_limit(arr2,ny,nx,row1,col1)
    end    

    if flg==0
        sy=ny
        sx=nx
        p_dir=next_dir
        str1=sy.to_s+" "+sx.to_s
        if snake_arr.count(str1)==0
            snake_arr.push(str1)
        else
            flg_e=1
            #puts(1)
        end
    else
        flg_e=1
        #puts(2)
    end    
    if flg_e==1
        break
    end    
end    


#print(snake_arr)
#print(snake_arr.length)


for i in 0..snake_arr.length-1 do
    tmp1=snake_arr[i].to_s.split(" ")
    arr2[tmp1[0].to_i][tmp1[1].to_i]="*"
end


str1=""

for i in 0..row1-1 do
    str1=""
    for j in 0..col1-1 do
        str1=str1+arr2[i][j]
    end
    puts(str1)
end    

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

[Rails]Google Maps APIによるGoogle Mapの表示と複数地点間のルート検索

はじめに

参考になる記事がとても少なく、ポートフォリオ作成で一番苦戦した部分なので、自分の学習のためアウトプットとして残す、とともにだれかの役に立てればいいなと思ったので書きました!
初学者なりにこの記述の意味はなんだ?と思う部分はしっかり説明したつもりです。
当たり前だろ!と思う部分も多々あると思いますがご了承ください。

目標

Google Maps APIでGoogle Mapを表示させるとともに、マーカーの吹き出しから任意にルート検索リストに追加でき、複数地点のルートを検索する機能を目標とします。
ezgif com-optimize-2

開発環境

・Ruby: 2.5.1
・Rails: 5.2.1
・OS: macOS

前提

・Slimの導入
・公式のGoogle Maps Platformで以下のAPIの有効化
 ・Maps JavaScript API
  → GoogleMapの表示
 ・Geocoding API
  → 住所から緯度経度の算出
 ・Directions API
  → ルート検索

設定

1. 必要なgemをインストール

Gemfile
gem 'dotenv-rails' # APIキーを環境変数化
gem 'gon' # コントローラーで定義したインスタンス変数をJavaScript内で使用出来るようにする。
gem 'geocoder' # 住所から緯度経度を算出する。
ターミナル
$ bundle install

2. APIキーを環境変数化

アプリケーション直下に「.env」ファイルを作成

ターミナル
$ touch .env 

自身のAPIキーを' 'の中に記述

.env
GOOGLE_MAP_API = '自身のコピーしたAPIキー'
.gitignore
/.env

3. turbolinksの無効化

Gemfile
gem 'turbolinks' # この行を削除
app/assets/javascripts/application.js
//= require turbolinks // この行を削除

data-turbolinks-track':'reload'属性の削除

app/views/layouts/application.html.slim
= stylesheet_link_tag    'application', media: 'all'
= javascript_include_tag 'application'

4. Geocoding APIを使用できるようにする

geocorderの設定ファイルを作成し、編集

ターミナル
$ touch config/initializers/geocoder.rb
config/initializers/geocoder.rb
# 追記
Geocoder.configure(
  lookup: :google,
  api_key: ENV['GOOGLE_MAP_API']
)

これで設定は終了です。ここからGoogleMapを表示していく実装に入ります。

GoogleMapの表示

1. 追加したいモデルにカラムを追加

自分のアプリの場合はPlaceモデルにaddressカラムを追加します。
latitude, longitudeカラムはGeocoding APIによってaddressカラムの値から算出された経度・緯度の値です。小数の値なので型はfloatを使います。

ターミナル
$ rails g migration AddColumnsToPlaces address:string latitude:float longitude:float
ターミナル
$ rails db:migrate

2. モデルを編集

models/place.rb
  # 追記
  geocoded_by :address # addressカラムを基準に緯度経度を算出する。
  after_validation :geocode # 住所変更時に緯度経度も変更する。

3. コントローラーを編集

controllers/places_controller.rb
def index
  @place = Place.all
  gon.place = @place # 追記
end

private
  def place_params
    # ストロングパラメーターに「address」を追加
    params.require(:place).permit(:name, :description, :image, :address)
  end

4. ビューを編集

①application.html.slimを編集
CSSとJavaScriptより先に、gonを読み込むよう記述します。

views/layouts/application.html.slim
doctype html
html
  head
    title
      | app_name
    = csrf_meta_tags
    = csp_meta_tag
    = include_gon # 追記
    = stylesheet_link_tag    'application', media: 'all'
    = javascript_include_tag 'application'

②新規登録画面に住所入力フォームを追加

views/places/new.html.slim
= f.label :address, '住所'
= f.text_field :address, class: 'form-control'

③GoogleMapを表示するファイルに記述

views/places/index.html.slim
div id = 'map_index' # idを付与, この部分にjsファイルで記述したGoogle Mapが埋め込まれる
- google_api = "https://maps.googleapis.com/maps/api/js?key=#{ ENV['GOOGLE_MAP_API'] }&callback=initMap".html_safe
script{ async src = google_api }

.map-route
  < ルート検索リスト >
  ul id = "route-list" class = "list-group" # jsファイルで吹き出しの追加ボタンによってその場所がli要素に追加される


div id = 'directions-panel' # 距離・時間が埋め込まれる
  < 各地点間の距離・時間 >
  ul id = "display-list" class = "display-group"

.map-search
   = button_tag "ルート検索", id: "btn-search", class: "btn btn-primary", onclick:     "search()" # クリック処理でsearch()関数を呼び出す

[ google_api = 〜〜〜〜の部分について ]
→ callback処理で読み込み時にinitMap関数を呼び出す。
→ .html_safeはエスケープ処理
→ async属性によって非同期でJavaScriptを読み込みレンダリングを早くする。

④GoogleMapで表示したいサイズをscssに記述

stylesheets/application.scss
#map_index{
  height: 400px;
  width: 400px; 
}

5. JavaScriptのファイルを編集

ここが肝です。
assets/javascripts直下に新たなファイルを作成し、記述します。
だいぶ長く見にくいかと思いますが、変数を定義したのち、関数の定義をそれぞれ行っているだけです。
関数は、
・initMap
・markerEvent( i )
・addPlace(name, lat, lng, number)
・search()
の順で4つがあります。
わかりにくい部分やポイントは、コメントアウトで説明していますので参考にしてください。

assets/javascripts/googlemap.js
var map
var geocoder
var marker = [];
var infoWindow = [];
var markerData = gon.places; // コントローラーで定義したインスタンス変数を変数に代入
var place_name = [];
var place_lat = [];
var place_lng = [];

// GoogleMapを表示する関数(callback処理で呼び出される)
function initMap(){
    geocoder = new google.maps.Geocoder()
    // ビューのid='map_index'の部分にGoogleMapを埋め込む
    map = new google.maps.Map(document.getElementById('map_index'), {
      center: { lat: 35.6585, lng: 139.7486 }, // 東京タワーを中心
      zoom: 9,
    });

    // 繰り返し処理でマーカーと吹き出しを複数表示させる
    for (var i = 0; i < markerData.length; i++) {
      // 各地点の緯度経度を算出
      markerLatLng = new google.maps.LatLng({
        lat: markerData[i]['latitude'],
        lng: markerData[i]['longitude']
      });

      // マーカーの表示
      marker[i] = new google.maps.Marker({
        position: markerLatLng,
        map: map
      });

      // 吹き出しの表示
      let id = markerData[i]['id']
      place_name[i]= markerData[i]['name'];
      place_lat[i]= markerData[i]['latitude'];
      place_lng[i]= markerData[i]['longitude'];
      infoWindow[i] = new google.maps.InfoWindow({
        // 吹き出しの中身, 引数で各属性の配列と配列番号を渡す
        content: `<a href='/places/${ id }'>${ markerData[i]['name'] }</a><input type="button" value="追加" onclick="addPlace(place_name, place_lat, place_lng, ${i})">`
      });
      markerEvent(i);
    }
  }
}

// マーカーをクリックしたら吹き出しを表示
function markerEvent(i) {
  marker[i].addListener('click', function () {
    infoWindow[i].open(map, marker[i]);
  });
}

// リストに追加する
function addPlace(name, lat, lng, number){
  var li = $('<li>', {
    text: name[number],
    "class": "list-group-item"
  });
  li.attr("data-lat", lat[number]); // data-latという属性にlat[number]を入れる
  li.attr("data-lng", lng[number]); // data-lngという属性にlng[number]を入れる
  $('#route-list').append(li); // idがroute-listの要素の一番後ろにliを追加
}

// ルートを検索する
function search() {
  var points = $('#route-list li');

  // 2地点以上のとき
  if (points.length >= 2){
      var origin; // 開始地点
      var destination; // 終了地点
      var waypoints = []; // 経由地点

      // origin, destination, waypointsを設定する
      for (var i = 0; i < points.length; i++) {
          points[i] = new google.maps.LatLng($(points[i]).attr("data-lat"), $(points[i]).attr("data-lng"));
          if (i == 0){
            origin = points[i];
          } else if (i == points.length-1){
            destination = points[i];
          } else {
            waypoints.push({ location: points[i], stopover: true });
          }
      }
      // リクエストの作成
      var request = {
        origin:      origin,
        destination: destination,
        waypoints: waypoints,
        travelMode:  google.maps.TravelMode.DRIVING
      };
      // ルートサービスのリクエスト
      new google.maps.DirectionsService().route(request, function(response, status) {
        if (status == google.maps.DirectionsStatus.OK) {
          new google.maps.DirectionsRenderer({
            map: map,
            suppressMarkers : true,
            polylineOptions: { // 描画される線についての設定
              strokeColor: '#00ffdd',
              strokeOpacity: 1,
              strokeWeight: 5
            }
          }).setDirections(response);//ライン描画部分

            // 距離、時間を表示する
            var data = response.routes[0].legs;
            for (var i = 0; i < data.length; i++) {
                // 距離
                var li = $('<li>', {
                  text: data[i].distance.text,
                  "class": "display-group-item"
                });
                $('#display-list').append(li);

                // 時間
                var li = $('<li>', {
                  text: data[i].duration.text,
                  "class": "display-group-item"
                });
                $('#display-list').append(li);
            }
            const route = response.routes[0];
            // ビューのid='directions-panel'の部分に埋め込む
            const summaryPanel = document.getElementById("directions-panel");
            summaryPanel.innerHTML = "";

            // 各地点間の距離・時間を表示
            for (let i = 0; i < route.legs.length; i++) {
              const routeSegment = i + 1;
              summaryPanel.innerHTML +=
                "<b>Route Segment: " + routeSegment + "</b><br>";
              summaryPanel.innerHTML += route.legs[i].start_address + "<br>" + "" + "<br>";
              summaryPanel.innerHTML += route.legs[i].end_address + "<br>";
              summaryPanel.innerHTML += "<" + route.legs[i].distance.text + ",";
              summaryPanel.innerHTML += route.legs[i].duration.text + ">" + "<br>";
            }
        }
      });
  }
}



吹き出しの内容のcontent部分の補足:(データの受け渡しの方法で苦戦したので)

content: `<a href='/places/${ id }'>${ markerData[i]['name'] }</a><input type="button" value="追加" onclick="addPlace(place_name, place_lat, place_lng, ${i})">`

addPlace(place_name, place_lat, place_lng, ${i})
この関数の呼び出しでは前の3つの引数は配列として渡しています。4つ目の引数は配列の中でどの情報かを表すための番号(インデックスと呼びます。)を式展開したものです。JavaScriptでの式展開はこの形だそうです。
このような引数を用意することで、関数addPlace(name, lat, lng, number)は正常にどのデータであるかという情報を処理できるのです。

最後に

最後まで読んでくださり、ありがとうございます。
自分自身、現在ポートフォリオが完成に近づき就職活動を本格的に始め出したような状態です!
目標を持ってポートフォリオ作成、転職活動など行っている方を心から応援しています、共に頑張りましょう!!

参考

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

Railsチュートリアル第4版:第2章 Toyアプリケーション

Railsチュートリアル第4版:第2章 Toyアプリケーション

2.1 アプリケーションの計画

Terminal
-> % rails new toy_app 
      create  
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
      create  Gemfile
         run  git init from "."
Initialized empty Git repository in /Users/**********/environment_2/toy_app/.git/
      create  package.json
      create  app
      create  app/assets/config/manifest.js
      create  app/assets/stylesheets/application.css
      create  app/channels/application_cable/channel.rb
      create  app/channels/application_cable/connection.rb
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/javascript/channels/consumer.js
      create  app/javascript/channels/index.js
      create  app/javascript/packs/application.js
      create  app/jobs/application_job.rb
      create  app/mailers/application_mailer.rb
      create  app/models/application_record.rb
      create  app/views/layouts/application.html.erb
      create  app/views/layouts/mailer.html.erb
      create  app/views/layouts/mailer.text.erb
      create  app/assets/images
      create  app/assets/images/.keep
      create  app/controllers/concerns/.keep
      create  app/models/concerns/.keep
      create  bin
      create  bin/rails
      create  bin/rake
      create  bin/setup
      create  bin/yarn
      create  config
      create  config/routes.rb
      create  config/application.rb
      create  config/environment.rb
      create  config/cable.yml
・
・
・
├─ retry@0.12.0
├─ select-hose@2.0.0
├─ selfsigned@1.10.8
├─ serve-index@1.9.1
├─ serve-static@1.14.1
├─ sockjs-client@1.4.0
├─ sockjs@0.3.20
├─ spdy-transport@3.0.0
├─ spdy@4.0.2
├─ strip-eof@1.0.0
├─ thunky@1.1.0
├─ type-is@1.6.18
├─ unpipe@1.0.0
├─ utils-merge@1.0.1
├─ wbuf@1.7.3
├─ webpack-dev-middleware@3.7.2
├─ webpack-dev-server@3.11.0
└─ ws@6.2.1
✨  Done in 10.52s.
Webpacker successfully installed ? ?
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', '>= 6.0.3.4'
# 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]
Terminal
-> % bundle install --without production
[DEPRECATED] The `--without` flag is deprecated because it relies on being remembered across bundler invocations, which bundler will no longer do in future versions. Instead please use `bundle config set without 'production'`, and stop using this flag
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Using rake 13.0.1
Using concurrent-ruby 1.1.7
Using i18n 1.8.5
Using minitest 5.14.2
Using thread_safe 0.3.6
Using tzinfo 1.2.7
Using zeitwerk 2.4.0
Using activesupport 6.0.3.4
・
・
・
Using webdrivers 4.4.1
Using webpacker 4.3.0
Bundle complete! 17 Gemfile dependencies, 74 gems now installed.
Gems in the group production were not installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

Githubに新規リポジトリを追加

Terminal
-> % git init
Reinitialized existing Git repository in /Users/**********/environment_2/toy_app/.git/

-> % git add -A

-> % git commit -m "Initialize repository"
[master (root-commit) 28e8b26] Initialize repository
 93 files changed, 9253 insertions(+)
 create mode 100644 .browserslistrc
 create mode 100644 .generators
 create mode 100644 .gitignore
 create mode 100644 .ruby-version
 create mode 100644 Gemfile
 create mode 100644 Gemfile.lock
 create mode 100644 README.md
 create mode 100644 Rakefile
 create mode 100644 app/assets/config/manifest.js
 create mode 100644 app/assets/images/.keep
 create mode 100644 app/assets/stylesheets/application.css
 create mode 100644 app/channels/application_cable/channel.rb
 create mode 100644 app/channels/application_cable/connection.rb
 create mode 100644 app/controllers/application_controller.rb
 create mode 100644 app/controllers/concerns/.keep
 create mode 100644 app/helpers/application_helper.rb
 create mode 100644 app/javascript/channels/consumer.js
・
・
・
 create mode 100644 test/models/.keep
 create mode 100644 test/system/.keep
 create mode 100644 test/test_helper.rb
 create mode 100644 tmp/.keep
 create mode 100644 tmp/pids/.keep
 create mode 100644 vendor/.keep
 create mode 100644 yarn.lock
image.png
Terminal
-> % git remote add origin https://github.com/**********/toy_app.git

-> % git push origin master
Enumerating objects: 105, done.
Counting objects: 100% (105/105), done.
Delta compression using up to 4 threads
Compressing objects: 100% (87/87), done.
Writing objects: 100% (105/105), 149.29 KiB | 4.82 MiB/s, done.
Total 105 (delta 3), reused 0 (delta 0)
remote: Resolving deltas: 100% (3/3), done.
To https://github.com/**********/toy_app.git
 * [new branch]      master -> master
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  def hello
    render html: "hello, world!"
  end
end
config/routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root 'application#hello'
end
Terminal
-> % heroku create
Creating app... done, ⬢ dry-headland-50008
https://dry-headland-50008.herokuapp.com/ | https://git.heroku.com/dry-headland-50008.git

-> % git push heroku master
Enumerating objects: 112, done.
Counting objects: 100% (112/112), done.
Delta compression using up to 4 threads
Compressing objects: 100% (94/94), done.
Writing objects: 100% (112/112), 149.82 KiB | 3.12 MiB/s, done.
Total 112 (delta 7), reused 0 (delta 0)
remote: Compressing source files... done.
・
・
・
remote:        An error occurred while installing sqlite3 (1.4.2), and Bundler cannot continue.
remote:        Make sure that `gem install sqlite3 -v '1.4.2' --source 'https://rubygems.org/'`
remote:        succeeds before bundling.
remote:        
remote:        In Gemfile:
remote:          sqlite3
remote: 
remote:  !
remote:  !     Failed to install gems via Bundler.
remote:  !     Detected sqlite3 gem which is not supported on Heroku:
remote:  !     https://devcenter.heroku.com/articles/sqlite3
remote:  !
remote:  !     Push rejected, failed to compile Ruby app.
remote: 
remote:  !     Push failed
remote: Verifying deploy...
remote: 
remote: !       Push rejected to dry-headland-50008.
remote: 
To https://git.heroku.com/dry-headland-50008.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/dry-headland-50008.git'

2.1.1 ユーザーのモデル設計

2.1.2 マイクロポストのモデル設計

2.2 Usersリソース

Terminal
-> % bin/rails g scaffold User name:string email:string
Running via Spring preloader in process 17885
      invoke  active_record
      create    db/migrate/20201023134008_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      invoke    test_unit
      create      test/controllers/users_controller_test.rb
      create      test/system/users_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder
      create      app/views/users/_user.json.jbuilder
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/users.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss
Terminal
-> % bin/rails db:migrate
== 20201023134008 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0026s
== 20201023134008 CreateUsers: migrated (0.0026s) =============================
Terminal
-> % bin/rails s
=> Booting Puma
=> Rails 6.0.3.4 application starting in development 
=> Run `rails server --help` for more startup options

2.2.1 ユーザーページを探検する

3ef79966e175698eb5e88c3abb248bf6.gif
  • 演習
  • 1. CSSを知っている読者へ: 新しいユーザーを作成し、ブラウザのHTMLインスペクター機能を使って「User was successfully created.」の箇所を調べてみてください。ブラウザをリロードすると、その箇所はどうなるでしょうか?
image.png
dd72ec4a45ece740d44f7fcb8367c0b6.gif
  • 2. emailを入力せず、名前だけを入力しようとした場合、どうなるでしょうか?
e956f9c922e2ce988624009b0016d953.gif

これは何を意図した問題だ?てっきり入力してくださいアラート出るのかの思った。

  • 3. 「@example.com」のような間違ったメールアドレスを入力して更新しようとした場合、どうなるでしょうか?
442322433a5dfe5fe515fc455ca5f41d.gif
  • 4. 上記の演習で作成したユーザーを削除してみてください。ユーザーを削除したとき、Railsはどんなメッセージを表示するでしょうか? "Are you sure?"
e6bf3b6aa2d55c14b4d5983736a8c10b.gif

2.2.2 MVCの挙動

config/routes.rb
Rails.application.routes.draw do
  resources :users
  root 'users#index'
end
  • 演習
    • 1. 図 2.11を参考にしながら、/users/1/edit というURLにアクセスしたときの振る舞いについて図を書いてみてください。
      省略
    • 2. 図示した振る舞いを見ながら、Scaffoldで生成されたコードの中でデータベースからユーザー情報を取得しているコードを探してみてください。
      app/controllers/users_controller.rb@users = User.allとか@user = User.find(params[:id])かな、自信ないけど。
    • 3. ユーザーの情報を編集するページのファイル名は何でしょうか?
      app/views/users/edit.html.erb

一旦コミットしたよ

Terminal
-> % git add .

-> % git commit -m "Usersリソース"
・
・
・
-> % git push origin master

コミット

2.2.3 Usersリソースの欠点

2.3 Micropostsリソース

2.3.1 マイクロポストを探検する

Terminal
-> % bin/rails g scaffold Micropost content:text user_id:integer
Running via Spring preloader in process 19890
      invoke  active_record
      create    db/migrate/20201024030447_create_microposts.rb
      create    app/models/micropost.rb
      invoke    test_unit
      create      test/models/micropost_test.rb
      create      test/fixtures/microposts.yml
      invoke  resource_route
       route    resources :microposts
      invoke  scaffold_controller
      create    app/controllers/microposts_controller.rb
      invoke    erb
      create      app/views/microposts
      create      app/views/microposts/index.html.erb
      create      app/views/microposts/edit.html.erb
      create      app/views/microposts/show.html.erb
      create      app/views/microposts/new.html.erb
      create      app/views/microposts/_form.html.erb
      invoke    test_unit
      create      test/controllers/microposts_controller_test.rb
      create      test/system/microposts_test.rb
      invoke    helper
      create      app/helpers/microposts_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/microposts/index.json.jbuilder
      create      app/views/microposts/show.json.jbuilder
      create      app/views/microposts/_micropost.json.jbuilder
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/microposts.scss
      invoke  scss
   identical    app/assets/stylesheets/scaffolds.scss
Terminal
-> % bin/rails db:migrate
== 20201024030447 CreateMicroposts: migrating =================================
-- create_table(:microposts)
   -> 0.0032s
== 20201024030447 CreateMicroposts: migrated (0.0033s) ========================
  • 演習
  • 1. CSSを知っている読者へ: 新しいマイクロポストを作成し、ブラウザのHTMLインスペクター機能を使って「Micropost was successfully created.」の箇所を調べてみてください。ブラウザをリロードすると、その箇所はどうなるでしょうか?
822193d18c0d841706431a011d209a83.gif
image.png
  • 2. マイクロポストの作成画面で、ContentもUserも空にして作成しようとするどうなるでしょうか?
    保存できちゃう。
af19e44f42c0cabfbc5ca7058f98b649.gif
  • 3. 141文字以上の文字列をContentに入力した状態で、マイクロポストを作成しようとするとどうなるでしょうか? (ヒント: WikipediaのRubyの記事にある設計思想の引用文が140文字を超えているので、これをコピペしてみましょう)
9da2106e05a4419cac4a00399e472ebd.gif
  • 4. 上記の演習で作成したマイクロポストを削除してみましょう。
610307aa8eeb4688aefc6ccc1a38cd92.gif

一旦コミットしたよ

Terminal
-> % git add .

-> % git commit -m "Micropostsリソース"
・
・
・
-> % git push origin master

コミット

2.3.2 マイクロポストをマイクロにする

app/models/micropost.rb
class Micropost < ApplicationRecord
  validates :content, length: { maximum: 140 }
end
  • 演習
  • 1. 先ほど2.3.1.1の演習でやったように、もう一度Contentに141文字以上を入力してみましょう。どのように振る舞いが変わったでしょうか?
d5d73bc81a6a5364812fb506680e30fe.gif
  • 2. CSSを知っている読者へ: ブラウザのHTMLインスペクター機能を使って、表示されたエラーメッセージを調べてみてください。
image.png

一旦コミットしたよ

Terminal
-> % git add .

-> % git commit -m "文字数のvalidation追加"
・
・
・
-> % git push origin master

コミット

2.3.3 ユーザーはたくさんマイクロポストを持っている

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts # <= 追加
end

app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user # <= 追加
  validates :content, length: { maximum: 140 }
end
  • 演習
  • 1. ユーザーのshowページを編集し、ユーザーの最初のマイクロポストを表示してみましょう。同ファイル内の他のコードから文法を推測してみてください (コラム 1.1で紹介した技術の出番です)。うまく表示できたかどうか、/users/1 にアクセスして確認してみましょう。
app/controllers/users_controller.rb
  # GET /users/1
  # GET /users/1.json
  def show
    @first_micropost = @user.microposts.first # <= 追加
  end
app/views/users/show.html.erb
<p>
  <strong>Email:</strong>
  <%= @user.email %>
</p>

# 以下を追加
<p>
  <strong>Micropost:</strong>
  <%= @first_micropost.content %>
</p>

<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>
image.png
  • 2. リスト 2.16は、マイクロポストのContentが存在しているかどうかを検証するバリデーションです。マイクロポストが空でないことを検証できているかどうか、実際に試してみましょう (図 2.16のようになっていると成功です)。
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :content, length: { maximum: 140 }, presence: true
end
beb6294845b268ba61132c580cee7ae8.gif

コミット

  • 3. リスト 2.17のFILL_INとなっている箇所を書き換えて、Userモデルのnameとemailが存在していることを検証してみてください (図 2.17)。
app/models/user.rb
class User < ApplicationRecord
  has_many :microposts
  validates :name, presence: true
  validates :email, presence: true
end

コミット

beb6294845b268ba61132c580cee7ae8.gif

2.3.4 継承の階層

  • 演習
  • 1. Applicationコントローラのファイルを開き、ApplicationControllerがActionController::Baseを継承している部分のコードを探してみてください。
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base # <= ここのはず
  def hello
    render html: "hello, world!"
  end
end
  • 2. ApplicationRecordがActiveRecord::Baseを継承しているコードはどこにあるでしょうか? 先ほどの演習を参考に、探してみてください。ヒント: コントローラと本質的には同じ仕組みなので、app/modelsディレクトリ内にあるファイルを調べてみると...?)
app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base # <= ここのはず
  self.abstract_class = true
end

2.3.5 アプリケーションをデプロイする

ビューを一部修正。
コミット

Terminal
-> % git add .

-> % git commit -m "first_micropost部分をコメントアウト"
[master 59064e0] first_micropost部分をコメントアウト
 1 file changed, 4 insertions(+), 4 deletions(-)

-> % git push origin master
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 4 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 531 bytes | 531.00 KiB/s, done.
Total 6 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), completed with 5 local objects.
To https://github.com/**********/toy_app.git
   71fcd86..59064e0  master -> master
Terminal
-> % git push heroku
Enumerating objects: 215, done.
Counting objects: 100% (215/215), done.
Delta compression using up to 4 threads
Compressing objects: 100% (192/192), done.
Writing objects: 100% (215/215), 161.74 KiB | 3.94 MiB/s, done.
Total 215 (delta 55), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
・
・
・
remote:        Gem files will remain installed in
remote:        /tmp/build_ef1fbbcc/vendor/bundle/ruby/2.7.0/gems/sqlite3-1.4.2 for inspection.
remote:        Results logged to
remote:        /tmp/build_ef1fbbcc/vendor/bundle/ruby/2.7.0/extensions/x86_64-linux/2.7.0/sqlite3-1.4.2/gem_make.out
remote:        
remote:        An error occurred while installing sqlite3 (1.4.2), and Bundler cannot continue.
remote:        Make sure that `gem install sqlite3 -v '1.4.2' --source 'https://rubygems.org/'`
remote:        succeeds before bundling.
remote:        
remote:        In Gemfile:
remote:          sqlite3
remote: 
remote:  !
remote:  !     Failed to install gems via Bundler.
remote:  !     Detected sqlite3 gem which is not supported on Heroku:
remote:  !     https://devcenter.heroku.com/articles/sqlite3
remote:  !
remote:  !     Push rejected, failed to compile Ruby app.
remote: 
remote:  !     Push failed
remote: Verifying deploy...
remote: 
remote: !       Push rejected to dry-headland-50008.
remote: 
To https://git.heroku.com/dry-headland-50008.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/dry-headland-50008.git'

gemを修正しわすれていたので、git push herokuできなかったので修正。ここ修正忘れててつまずく人でてくるかも。(参考

Gemfile
group :production do
  gem 'pg'
end

https://github.com/fukadashigeru/toy_app/commit/1d0d5c80762438ef72c2c507b240dda9819edf2a

Terminal
-> % git push heroku
Enumerating objects: 215, done.
Counting objects: 100% (215/215), done.
Delta compression using up to 4 threads
Compressing objects: 100% (192/192), done.
Writing objects: 100% (215/215), 161.75 KiB | 3.76 MiB/s, done.
Total 215 (delta 55), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote: 
・
・
・
remote: ###### WARNING:
remote: 
remote:        There is a more recent Ruby version available for you to use:
remote:        
remote:        2.7.2
remote:        
remote:        The latest version will include security and bug fixes. We always recommend
remote:        running the latest version of your minor release.
remote:        
remote:        Please upgrade your Ruby version.
remote:        
remote:        For all available Ruby versions see:
remote:          https://devcenter.heroku.com/articles/ruby-support#supported-runtimes
remote: 
remote: ###### WARNING:
remote: 
remote:        No Procfile detected, using the default web server.
remote:        We recommend explicitly declaring how to boot your server process via a Procfile.
remote:        https://devcenter.heroku.com/articles/ruby-default-web-server
remote: 
remote: 
remote: -----> Discovering process types
remote:        Procfile declares types     -> (none)
remote:        Default types for buildpack -> console, rake, web
remote: 
remote: -----> Compressing...
remote:        Done: 77.8M
remote: -----> Launching...
remote:        Released v6
remote:        https://dry-headland-50008.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... done.
To https://git.heroku.com/dry-headland-50008.git
 * [new branch]      master -> master

アクセスすると、なんかわからんけど落ちてるくさい。

image.png
Terminal
-> % heroku run rails db:migrate
Running rails db:migrate on ⬢ dry-headland-50008... up, run.3253 (Free)
D, [2020-10-25T05:13:31.252614 #4] DEBUG -- :    (13.5ms)  CREATE TABLE "schema_migrations" ("version" character varying NOT NULL PRIMARY KEY)
D, [2020-10-25T05:13:31.264484 #4] DEBUG -- :    (8.8ms)  CREATE TABLE "ar_internal_metadata" ("key" character varying NOT NULL PRIMARY KEY, "value" character varying, "created_at" timestamp(6) NOT NULL, "updated_at" timestamp(6) NOT NULL)
D, [2020-10-25T05:13:31.302300 #4] DEBUG -- :    (1.3ms)  SELECT pg_try_advisory_lock(2791976884009269180)
D, [2020-10-25T05:13:31.321970 #4] DEBUG -- :    (2.6ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
I, [2020-10-25T05:13:31.323672 #4]  INFO -- : Migrating to CreateUsers (20201023134008)
== 20201023134008 CreateUsers: migrating ======================================
-- create_table(:users)
D, [2020-10-25T05:13:31.328518 #4] DEBUG -- :    (1.2ms)  BEGIN
D, [2020-10-25T05:13:31.337948 #4] DEBUG -- :    (9.1ms)  CREATE TABLE "users" ("id" bigserial primary key, "name" character varying, "email" character varying, "created_at" timestamp(6) NOT NULL, "updated_at" timestamp(6) NOT NULL)
   -> 0.0114s
== 20201023134008 CreateUsers: migrated (0.0115s) =============================

D, [2020-10-25T05:13:31.348980 #4] DEBUG -- :   primary::SchemaMigration Create (1.4ms)  INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version"  [["version", "20201023134008"]]
D, [2020-10-25T05:13:31.352253 #4] DEBUG -- :    (2.9ms)  COMMIT
I, [2020-10-25T05:13:31.352397 #4]  INFO -- : Migrating to CreateMicroposts (20201024030447)
== 20201024030447 CreateMicroposts: migrating =================================
-- create_table(:microposts)
D, [2020-10-25T05:13:31.356681 #4] DEBUG -- :    (1.2ms)  BEGIN
D, [2020-10-25T05:13:31.365711 #4] DEBUG -- :    (8.8ms)  CREATE TABLE "microposts" ("id" bigserial primary key, "content" text, "user_id" integer, "created_at" timestamp(6) NOT NULL, "updated_at" timestamp(6) NOT NULL)
   -> 0.0123s
== 20201024030447 CreateMicroposts: migrated (0.0124s) ========================

D, [2020-10-25T05:13:31.367853 #4] DEBUG -- :   primary::SchemaMigration Create (1.2ms)  INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version"  [["version", "20201024030447"]]
D, [2020-10-25T05:13:31.370689 #4] DEBUG -- :    (2.5ms)  COMMIT
D, [2020-10-25T05:13:31.381377 #4] DEBUG -- :   ActiveRecord::InternalMetadata Load (1.3ms)  SELECT "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = $1 LIMIT $2  [["key", "environment"], ["LIMIT", 1]]
D, [2020-10-25T05:13:31.394744 #4] DEBUG -- :    (1.1ms)  BEGIN
D, [2020-10-25T05:13:31.396634 #4] DEBUG -- :   ActiveRecord::InternalMetadata Create (1.5ms)  INSERT INTO "ar_internal_metadata" ("key", "value", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "key"  [["key", "environment"], ["value", "production"], ["created_at", "2020-10-25 05:13:31.392814"], ["updated_at", "2020-10-25 05:13:31.392814"]]
D, [2020-10-25T05:13:31.399052 #4] DEBUG -- :    (2.1ms)  COMMIT
D, [2020-10-25T05:13:31.400599 #4] DEBUG -- :    (1.3ms)  SELECT pg_advisory_unlock(2791976884009269180)

https://dry-headland-50008.herokuapp.com/

image.png
  • 演習
  • 1. 本番環境で2〜3人のユーザーを作成してみましょう。
30d78f8d09f6afdf93d714c8d9518c6d.gif
  • 2. 本番環境で最初のユーザーのマイクロポストを作ってみましょう

https://dry-headland-50008.herokuapp.com/microposts

07913684b165d7f2d84d91a9b6770143.gif
  • 3. マイクロポストのContentに141文字以上を入力した状態で、マイクロポストを作成してみましょう。リスト 2.13で加えたバリデーションが本番環境でもうまく動くかどうか、確認してみてください。
b408a9607ef2bc24808fdc90d6b4f4f4.gif

2.4 最後に

省略

2.4.1 本章のまとめ

省略

質問ある方どうぞ

駆け出しエンジニアの方が多いかと思います。私でもお力になれることもあるかもしれないので、何か質問ありましたら聞いて下さい。

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

【Rails】sessionによるデータの一時保持

はじめに

sessionは「ウィザード形式」等でデータを保持させた状態で遷移先に移動する時に使用します。

目次

  1. セッションについて
  2. 実装例
  3. 最後に

1. セッションについて

セッションは情報を一時的に記憶しておく仕組みです。
Railsにおいてセッションは、sessionというオブジェクトにハッシュのような形でデータが格納されます。

例えば、会員登録の際ページが切り変わって進んでいくようなサイトを想定します。
この場合、セッションを使い入力した情報を一旦sessionに格納して、次のページに遷移してそこで展開させる必要があります。

2実装例

前提

前提としてdeviseを用いてページを遷移しながら会員情報を保存する事を想定してます。

開発環境

ruby 2.6.5
rails 6.0.0
devise 4.7.3

2.1 データの保持

それでは実装してみますー

registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController

  def new
    @user = User.new
  end

  def create
    @user = User.new(sign_up_params)
      unless @user.valid?
        render :new and return
      end
    session["devise.regist_data"] = {user: @user.attributes}
    @address = @user.build_address
    render "new_address"
  end

他いろいろ記述してますがsessionに注目します。

session["devise.regist_data"] = {user: @user.attributes} 

{user: @user.attributes}はsessionにハッシュオブジェクトの形で情報を保持させたい時、attributesメソッドを用いてデータを整形しています。

つまり、attributesされた@userのデータがハッシュの状態でsessionに入ってます。
これで、session["devise.regist_data"]に入力情報が代入され、保持さてた状態になります。

2.2 展開させる

次に、データを保持した状態でページを遷移して展開します。

registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController

#省略
 def create_address
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(useraddress_params)
      if @address.valid?
        @user.build_address(@address.attributes)
        @user.save
          session["devise.regist_data"]["user"].clear
          sign_in(:user, @user)
      else
        render "new_address"
      end
  end

こちらもいろいろ記述してますが一番上の@userに注目。

@user = User.new(session["devise.regist_data"]["user"])

session["devise.regist_data"]["user"]について説明します。
{user: 〇〇}でデータを持すと ["user"]を使う必要がある。
また、sessionは配列で情報を入れるので以下のように情報を持す事もできます。
session["devise.regist_data"] = {user: @user.attributes, address: @user.address}

これで無事遷移先のページでsessionが展開できて@userに代入ができました。

あとは@userに代入されてるデータを使って保存などが可能です。 

まとめ

今回は「ウィザード形式」でdeviseを用いた際のsessionの使いた方を紹介しました。

最後に

私はプログラミング初学者ですが、同じ様に悩んでる方々の助けになればと思い、記事を投稿しております。
それでは、また次回お会いしましょう〜

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

rubyで簡単に基本型と参照型を感じる2

rubyで簡単に基本型と参照型を感じる のメソッドを使わないパターンです。

macのターミナルでRubyを起動

   irb

参照型の値のもつ基本型の値を変更した場合

temp=[1,2]
temp2=temp
temp2[0]=11
temp #[11, 2]

tempとtemp2の持つ値(参照値)は同じ

参照型の値そのものを変更した場合

temp=[1,2]
temp2=temp
temp2=11
temp #[1, 2]

tempとtemp2の持つ値(参照値)は異なる

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

Unicorn環境でRubyをアップデートする

Unicorn と capistrano を使った Railsアプリケーション で ruby を 2.3 から2.5にアップデートしたので、
その手順をまとめてみました。

1. rbenv の更新

updateしたい対象のrubyのバージョンをinstall し、global で対象のバージョンを指定しておく

$ rbenv install  2.5.8
$ rbenv global 2.5.8

rbenv install --list でアップデートしたい対象のバージョンが出てこない時は、
rbenv install ができないので、下記の手順でrbenvを更新すると、installできるはずです。

$ cd ~/.rbenv/plugins/ruby-build
$ git pull

2. bundler の install と bundle install をしておく

デプロイ時に bundle install でこけないように予め bundler と 他のライブラリを install しておく

# gemfile.lock を確認して、同じ version の bundler を指定する
$ gem install bundler -v 1.17.3
$ bundle install

3. デプロイ

capistrano で通常通りデプロイする
しかし、この時必要なのが、 ruby のバージョンを切り替えるには、再起動が必要であり、
一度 unicorn を kill して立ち上げ直す必要があった。

preload_app: true の設定をしている型は注意が必要です

$ kill -QUIT `cat /path/to/unicorn.pid`
$ bundle exec unicorn_rails -E production -D
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル 第11章 SendGridでの送信者ID未認証による、メールが送信できない事象と対応

事象

Rails tutorialの「11.4 本番環境でのメール送信」において、herokuからユーザ登録のためのメール送信を実行してみたところ、「We're sorry, but something went wrong.」と表示された。

heroku logsでログを確認すると、以下のようなエラーログが表示されており、
メールの内容等の情報が出力されていたので、SendGridによる送信処理の部分にエラーがありそう。

Net::SMTPFatalError (550 The from address does not match a verified Sender Identity. Mail cannot be sent until this error is resolved. Visit https://sendgrid.com/docs/for-developers/sending-email/sender-identity/ to see the Sender Identity requirements)

調査

現在チュートリアルの記載そのままで設定しているので、送信元として設定している「noreply@example.com」が認証されず、SendGridからメールが届かない?
ググってみるものの、Railsチュートリアルで同様の事象は見つけられなかった。

試しにheroku run rails consoleより以下コマンドを実行するが、上記と同じエラーが発生し、メールが届かなかった。

ActionMailer::Base.mail(from: "noreply@example.com", to: "<受信用メールアドレス>", subject: "subject", body: "body").deliver_now

ログに示されたURL等を見て色々調べてみたが、SendGridでの送信元の認証が必要であるよう。
https://sendgrid.com/docs/for-developers/sending-email/sender-identity/
https://sendgrid.com/docs/ui/sending-email/sender-verification/

ただ、「noreply@example.com」は自分で作成したドメインであるわけでもなく、このアドレスでの認証の仕方が不明なので、今回は個人用のアドレスを送信元に設定し、送信することとした。

対応

個人で使用しているメールアドレスを送信用メールアドレスとして、SendGridで送信元として認証し、使用する。

以下の記事を参考に、SendGridへログインし、「Single Sender Verification」を設定。
https://sendgrid.kke.co.jp/docs/Tutorials/B_Marketing_Mail/marketing_campaigns1.html

認証後、以下のコマンドを実行すると、送信用メールアドレスから受信用メールアドレスにメールが届いた。

ActionMailer::Base.mail(from: "<送信用メールアドレス>", to: "<受信用メールアドレス>", subject: "subject", body: "body").deliver_now

そのため、メール送信部の処理は下記の通り変更し、テスト部の送信元も併せて変更したところ、heroku上で事象が解決し、メール送信が問題なく動作した。

/sample_app/app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: "<送信用メールアドレス>"
  layout 'mailer'
end

私見

実際のwebサービスでは、所有するメールアドレスやドメインを送信元として使用すると思われるため、所有しない「noreply@example.com」による送信は、解決を見送りました。

ただ、所有しないアドレスでの送信の可否等、まだまだ理解が追いついていないので、何か見つけた時は追記します。

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

rubyで簡単に基本型と参照型を感じる

macのターミナルでRubyを起動

   irb

#基本型 メソッドの引数の持つ値が変わらない場合

a=1
def method_primitive(a)
   a=2
end
method_primitive a #2
a #1 #変わってない

#参照型 メソッドの引数の持つ値が変わる場合

temp={}
temp[1]=1
def method_reference(temp)
    temp[1]=2
end
method_reference temp #2
temp[1] #2 #変わっている
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsのvalid?とinvalid?

Ruby on Rails学習における忘備録です。
参照記事:Railsガイド
https://railsguides.jp/active_record_validations.html

バリデーション

バリデーションは正しいデータをDBに保存するために行われる。
RailsはオブジェクトをActiveRecordオブジェクトに保存する前にバリデーションを実行する。
そこでエラーが発生するとオブジェクトは保存されない。
要は、DBの制約に対して保存条件を満たしていますか?のチェックをDBレベルで保存前に行う仕組みである。

valid?とinvalid?

valid?はバリデーションを手動でトリガすることができる。
保存するオブジェクトにエラーがない場合はtrueを返し
エラーの際はfalseを返す。

class User < ApplicationRecord
    # バリデーション(nameが空は許容しない)
    validates :name, presence: true
end

#これはtrue(条件を満たしている)
User.create(name: "Gonshiba").valid? 

#これはfalse(条件を満たしていない→nameが空である)
User.create(name: "").valid?

invalid?はvalid?の逆チェックになっており、帰ってくるBool値が逆になるだけである。
ちなみに、createメソッドはオブジェクト作成から保存までを一括で行ってくれる。

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

いいね機能 非同期にする上で詰まった部分

対象者

いいね機能を実装している方で同期処理はできており、非同期処理の挙動とならない方
リロードしないと「いいねしている状態といいねしていない状態の切り替え」ができない方
Rails6におけるJQuery導入方法(①参照)やデバッグがうまくできていない(②参照)方

環境

Rails6 環境
Turbolinks導入済み

記事の目的

いいね機能において同期処理は実現できたが、非同期にする上で詰まった部分を記しておきたいと思います。

いいね機能の記事は良質なものがあるので(ただRails5の環境が多いかも?)そちらを基本にして頂き、本記事では初学者が詰まった時に参考になる部分があればいいなという思いで記事にしました。

また、初学者の為、間違いがあれば、ご指摘頂けると幸いです。

①Rails5とRails6におけるJQuery導入方法の違いを理解していなかった(環境設定ミス)

現在出ている記事ではRails5環境のものが多いのですが、Rails6環境での設定方法と異なることに注意しましょう。

Rails5の場合

Rails5系までは、「アセットパイプライン」を使用して、JavaScriptを管理します。

gem 'jquery-rails'
application.js
//= require jquery
//= require rails-ujs

以上のようにjquery-railsを導入することでRailsアプリケーション開発にJQueryを用いることができます。
その後、app/assets/javasctipts/application.jsなどのマニフェストファイル上でJQueryをrequireします。
私は、Rails6を導入していたのにも関わらず、このRails5の方法で記述してしまいました。
この部分に限らず、Railsのバージョンに気をつけて記事を参照して下さい。

Rails6の場合

Rails6以降では「Webpacker」という機能で管理します。
私の場合、application.jsの配置場所はapp/javascript/packs/application.jsとなっていました。

application.js
require("@rails/ujs").start()
require("turbolinks").start()
require('jquery')

導入に関しては以下の記事を参考にしました。
https://techtechmedia.com/jquery-webpacker-rails/
加えてenvironment.jsの編集、JQuery導入の確認方法についてもこちらの記事で触れています。
JQuery導入の確認方法については、以下で少し触れます。

JQuery導入の確認方法

application.jsに以下のコードを入れ、

application.js
require("@rails/ujs").start()
require("turbolinks").start()
require('jquery')
document.addEventListener("turbolinks:load", () => {
   console.log($.fn.jquery)
})

remote:trueの記述によるJavascriptのリクエストによって呼び出されるjs.erbファイルにconsole.log($.fn.jquery)を追加。

create.js.erb
console.log($.fn.jquery)
$("#like_<%= @review.id%>").html("<%= j(render partial: "reviews/likes", locals: { review: @review, book: @book, likes: @likes })%>");

そうすると、デベロッパーツールのconsoleの部分でJQueryのバージョンが表示されます。
JavaScriptで非同期処理を実装する際に確認する場所となるので覚えておきましょう。
consoleの部分に「3.5.1」のようにバージョンが表示されていれば、導入ができてるということになります。

スクリーンショット 2020-10-25 0.06.01.png

②変数定義のエラーに気づかなかった(rails sによるエラーのログを確認しなかった)

create.js.erb
$("#like_<%= @review.id%>").html("<%= j(render partial: "reviews/likes", locals: { review: @review, book: @book, likes: @likes })%>");

エラー出ていないと勘違いしていたのですが、rails sで出力されるログを確認しておりませんでした。
いいねボタンを押すとrails sのログが出力され、一番最後にidが未定義ですとエラーが出ていました。
rails sのログは、以下の通りです。

Started POST "/books/2/reviews/3/likes" for ::1 at 2020-10-24 09:41:59 +0900
Processing by LikesController#create as JS
・
.

(省略)
・
.
ActionView::Template::Error (undefined method `id' for nil:NilClass):
    1: $("#like_<%= @review.id %>").html("<%= j(render partial: "reviews/likes", locals: { review: @review, book: @book, likes: @likes })%>");

一番下にエラーがでています。
私の場合は、コントローラ内の@reviewの定義方法の誤りで値が入っていないのが理由で、@reviewを含むcreate.js.erbが動作せず、非同期の挙動になりませんでした。
このエラーに対する適切なデバック(変数の正しい定義)を行ったら、リロードしないと「いいねしている状態といいねしていない状態の切り替え」ができなかった問題が解決され、非同期処理の挙動を実現することができました。

同じような環境・部分で詰まっている方の参考になれば幸いです。

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

Rails学習 1日目その3

Ruby on Rails速習実践ガイド chapter3

3-1-4 データベースの環境ごとの使い分け

データベースには3種類の環境がある。
それぞれやることに対応してデータベースも割り振られている。

環境の種類 環境システム名 用途
開発環境 development 開発時の動作確認を行う
テスト環境 test 自動テストを行う
本番環境 production ユーザーが利用可能な形で稼働させる

開発時はdevelopment環境とtest環境を使っていく。この二つのファイルはdb:createを実行した時に自動的に作成されるようになっている。

3-1-5 ビュー層を効率良く書くためにslimを使えるようにする(slimの使い方)

デフォルトではerbで行われているテンプレートエンジンをslimというテンプレートエンジンに変えると簡単にコードを書けるようになる。

slimの書き方
 

erbの書き方 slimの書き方
<% name %> - name
<%= name %> = name
#コメント /コメント
<p> こんにちは </p> p こんにちは
<a href='//example.com">image</a> a href="//example.com"image
<div class="profile name"> .profile.name
<div id="pam"> #pam

slimは慣れると便利なので慣れるまでは苦労しそうだ。

3-2-2 タスクモデルのひな形を作成する

・モデルを作成する
モデルを作成するにはrails g の後に作成したいものを記入する(今回はmodel)

$ rails g model Task name:string description:text

こうすることで
・Taskモデルができる
・name:string description:textのテーブルを作成できる設計図のマイグレーションファイルができる
・モデルの自動テスト用のファイルができる

テーブルを作るにはマイグレーションファイルをdb:migrateをするとテーブルが作られる。

3-3 コントローラーとビュー

・ルーティング
ルーティングはURLとHTTPメソッドを見てコントローラーの各アクションに割り振る。

URLの例 HTTPメソッド アクション名 機能名 役割
/tasks GET index 一覧表示 全タスクを表示する
/tasks/17 GET show 詳細表示 特定のidのタスクを表示する
/tasks/new GET new 新規登録画面 新規登録画面を表示する
/tasks POST create 登録 登録処理を行う
/tasks/17/edit GET edit 編集画面 編集画面を表示する
/tasks/17 PATCH,PUT update 更新 更新処理を行う
/tasks/17 DELETE destory 削除 削除処理を行う

HTTPメソッドは重要!!

・ルーティングを1つにまとめる方法

get 'tasks/index'
get 'tasks/show'
get 'tasks/new'
get 'tasks/edit'

上のルーティングをresourcesを使ってまとめる

resource :tasks

resourcesメソッドはindex,show,new,create,edit,update,destroyの7つのアクションを一つにまとめることができる

get "/" => "tasks#index"

root to: 'tasks#index'

またrailsの初期画面もroot toで指定するとそのアクションに連携したビューが初期画面になる。

・URLヘルパーメソッド
/tasks/newや/tasks/editなどもヘルパーメソッドに置き換えることができる

URLの例 HTTPメソッド URLパターン名 URLヘルパーメソッド
/tasks GET tasks tasks_path
/tasks/17 GET task task_path
/tasks/new GET new_task new_task_path
/tasks POST tasks tasks_path
/tasks/17/edit GET edit_task edit_task_path
/tasks/17 PATCH、PUT task task_path
/tasks/17 DELETE task task_path

3-3-1-5 新規登録画面のビューを実装する

新規登録画面

app/views/tasks/new.html.slim
h1 タスクの新規登録

.nav.justify-content-end
  = link_to '一覧', tasks_path, class: 'nav-link'

=form_with model:@task,local:true do |f|
  .form-group
    =f.label :name 
    =f.text_field :name, class: 'form-control', id: 'task_name'

  .form-group
    =f.label :name 
    =f.text_area :description, rows: 5, class: 'form-control', id: 'task_description'
  =f.submit nil, class: 'btn btn-primary'

form_withはフォームを作成するためのもの。フォームで必要なものをf.を使って記入していく。
・解説
.nav.justify-content-end:Bootstrapで用意されたコード
f.label :name :入力欄に対応するラベルを表示本来はnameだが日本語翻訳されているので名称という名前になる
=f.text_field :name:テキストを入れる場所
class: 'form-control:bootstrapに元々ついているクラス

3-3-1-6 登録アクションを実装する

app/controllers/tasks_controller.rb
def create
   task = Task.new(task_params)
   task.save!
   redirect_to tasks_url, notice: "タスク「#{task.name}」を登録しました"
end



private

def task_params
   params.require(:task).permit(:name,:description)
end

require(:task).permit(:name,:description)は「nameとdiscriptionの情報だけ許可して新しくtaskを作ります」という意味。paramsでその作ったパラメーターを取得する。

3-3-3 詳細表示機能を実装する

tasks/[タスクのid]はURLヘルパーメソッドを用いるとtask_path(task)と表すことができる。idはrailsの方で引数taskから自動的に判定されるようになっている。

3-3-3-2 詳細画面にタスクの属性情報を表示する

simple_format(h(@task.description),{},sanitize: false, wrapper_tag: "div")

descriptionのように説明が入る文章は長くなり改行する必要が出てくる。simple_formatを用いることで簡単に改行が行われる。

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