20201025のRailsに関する記事は20件です。

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

概要

今回の記事は表題の通り「パスワードを入力させずにユーザー情報を更新する方法」です。
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で続きを読む

Spree:: Taxonのカラムを解説

solidus使っていると初心者にとっては「。。。ん?」みたいな状態ですよね。
この記事を読まれていると言うことは少し全体像が掴めて、「Spree::Taxon」にはどう言ったカラムが存在するんだ?」と言うところなのかなと思います。:point_up:
そこで今回は「Spree::Taxon」内のカラムについて簡潔に紹介と説明をしていきます。

Solidusのバージョン

solidus 2.0.9

カラムの紹介と説明

Spree::Taxonのid=1をのぞいてみましょう。
スクリーンショット 2020-10-25 19.05.59.png

それではここにあるカラムについて説明します。

id

id。

parent_id

スクリーンショット 2020-10-25 22.37.52.png
例えばこのような入れ子構造になっていた場合、以下のtaxonのparent_idをそれぞれ示します。
Category ▶︎ parent_id:nil
Clothing,Shoes ▶︎ parent_id:1
T-shirts,Shirts,Socks ▶︎ parent_id:2
Boots,Sandals,Sneakers ▶︎ parent_id:3
つまり、その商品が所属する一つ上のtaxonのid。

position

画像リスト中のImageの配置場所を設定する。
例として,「2」の値では,「1」の値の後で表示される。

name

分類(taxon)名。

permalink

その商品のtaxon内でのurl。

taxonomy_id

スクリーンショット 2020-10-25 22.41.42.png
所属するtaxonomyのid。
Clothing,Shoesを含めそれ以下 ▶︎ taxonomy_id:1(Category)
NIKE,Ferrari,GUUCI ▶︎ taxonomy_id:2(Brands)

lft

商品の階層構造の中にある位置。
awesome_nested_set-Gem参照。

rgt

商品の階層構造の中にある位置。
awesome_nested_set-Gem参照。

icon_file_name

調査中。

icon_content_type

調査中。

icon_file_size

調査中。

icon_updated_at

調査中。

description

分類(taxon)紹介文。

create_at

分類(taxon)が作られた時間。

update_at

分類(taxon)が更新された時間。

meta_title

HTMLのタグ。空の場合はnameが使用される。

meta_description

SEO用で、サーチエンジン向けの説明。

meta_keywords

SEO用で、サーチエンジン向けのキーワード。

depth

そのtaxonの所属するディレクトリの深さ。
例えば
Category,Brands ▶︎ depth:0>
Clothing,Shoes,NIKE,Ferrari,GUCCI ▶︎ depth:1>

駆け出しsolidus開発者にとってSpree::Taxonモデルを理解する糧になれば幸いです!

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

[Rails]例外処理を書く方法とは?

例外処理とは、、?

例外処理とは、エラーがでたときの処理を行うことです。
エラー原因の特定やシステム側で不具合を起こさないためにも例外処理を書けば便利です。

例外処理の書き方

begin,rescueの基本形

エラーの対象となりそうな箇所をbeginで囲い、エラーが発生した場合の処理をrescueの中に書く。

begin
  100 / 0
rescue
  p "0で割れません"
end

puts "おはようございます"

ワンライナーで書く

ワンライナーで書くことも可能です。

sample_1 = 10 / 0 rescue 0

sample_2 = 10 / nil rescue 0

puts sample_1 #=>0
puts sample_2 #=>0

エラー内容を取得する(e)

また、rescueの後に引数を指定してあげて、変数の中にエラー内容を格納できます。

begin
  10 / 0
rescue => e
  puts e #=> divided by 0
end

エラーごとに条件分岐する

他にも、rescueの後にエラーメッセージを指定することで、
どのエラーが出た時にどの処理をするか,というのを条件分岐ができます。

begin
  10 / 0
rescue NoMethodError
  puts "メソッドはありません"
rescue ZeroDivisionError
  puts "0で割れません"
end

raiseの使い方

パラメータが想定されたものではない時や、不正なアクセスがきたといった場合に、明示的にエラーを発生させ、処理を中断させたいときに使います。

begin
  raise NoMethodError # 発生させたい例外のクラス
rescue => e
  puts e
end

エラーメッセージを出力することも可能です。

begin
  raise RuntimeError, "実行時エラーです"
rescue => e
  puts e
end

retryの使い方

エラーが起きても、再度beginに戻って実行することができます。

num = 0
begin
  puts 10 / num
rescue ZeroDivisionError => e
  puts e
  num = 1
  retry #beginブロックを再度実行
end

puts "終了しました"

ensureの使い方

例外が発生してもしなくても実行される処理を書くことができる。

begin
  puts "例外なし"
rescue => e
  puts e
ensure
  puts "ここは絶対実行する!"
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[linux]プロセスを終了するkillコマンド

killコマンドとは?

指定したプロセスIDのプロセスを終了させるコマンド。

$ kill プロセス番号(PID

使い方

1.以下のコマンドを実行してプロセスIDを調べる。

$ ps # 現在起動しているプロセスを確認する

$ ps ax | grep gedit # プロセスに”gedit”の名称がついたプロセスのみを表示
16619 ?        Sl     0:01 gedit

2.killコマンドでプロセスIDを指定してプログラムを終了する。

$ kill 16619

3.強制終了したい場合は以下のコマンドを実行する。

$ kill -9 16619

主なオプション

コマンド 概要
-s シグナル 指定したシグナル名またはシグナル番号を送信する
-シグナル 指定したシグナル名またはシグナル番号を送信する
-l [] シグナル名とシグナル番号の対応を表示する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像が表示出来ない時の意外な落とし穴

作業内容

投稿された画像を表示したい

画像以外は、表示されるという状況でエラーに悩んでました

構文エラー

ActionView::SyntaxErrorInTemplate in PrototypesController#index

エラー箇所

エラー部のみ記載
<%= image_tag prototype.image, if prototype.image.attached? %>

最初はもう少し記述がありましたが、わすれました。

参考にした記述が

<%= image_tag message.image, class: 'message-image' if message.image.attached? %>

違いはクラスが入っているという所です。

クラス抜いたバージョンで記述したのですが、エラーがでました。

なぜ、エラーがでたかわかりますか?

スペルが違うわけでも、ありません。

解決方法

どこが違うかわかりますか?

<%= image_tag prototype.image if prototype.image.attached? %>

エラー文の原因は単純な事が多いので、解決した時はなーんだという感じですが

悩んでいる時は、正直つらいですよね。

でも、乗り越えたら成長できるので!

頑張っていきましょう。

「,」がいらなかったんですね。
If文の時は使わない、クラスを使う時に必要だったんですね。

「、」は盲点でした。

必要だと思っていたので、必要だと思ったやつでも外してみたりすることも大切だと学ばせていただきました。

ありがとうございます!

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

画像プレビュー機能の実装

railsでの画像プレビュー機能の実装

環境
Rails 5.2.4.4

前提
userテーブルにimage_idカラム

手順
①image_tagにidを与える
JavaScriptで処理するためにidを与える。
ここではid: "img_prev"とする。

<%= attachment_image_tag @user, :image, size: "300x300", fallback: "no_profile.jpg", size: "300x300" , id: "img_prev"%>

②同ページの下部にJavaScriptの記述をする
画像ファイルフィールドの値が変化したときに、
image_tagで画像ファイルのURLを読み込み、画像を表示をする。

<script>
$(function(){                                         
    $('#user_image').on('change', function (e) {        #idからの情報取得
    var reader = new FileReader();            #既存の画像urlの取得
    reader.onload = function (e) {
        $(".image").attr('src', e.target.result);
    }                            #ここまでが画像取得のため
    reader.readAsDataURL(e.target.files[0]);        #取得したurlにアップロード画像のurlを挿入
});
});

</script>

以上で画像プレビュー機能の実装ができた。

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

Impressionistを用いてPV数(閲覧数)取得~Rails

目標

この記事ではImpressionistというgemを用い、投稿のPV(page view)数取得とPV数ランキングを実装します。

詳しくはこちら~

開発環境

・Ruby: 2.6.2
・Rails: 6.0.3
・OS: windows

前提

Tweetモデルにて、投稿機能一式作成済み。

実装

1.impressionistを導入

gemfileに下記一行を追加します。

Gemfile
gem 'impressionist'
ターミナル
$ bundle install

次にPV数をカウントするテーブルを作成します。

ターミナル
$ rails g impressionist
ターミナル
$ rails db:migrate

以下のようなimperssionsテーブルができます。

schema.rb
create_table "impressions", force: :cascade do |t|
  t.string "impressionable_type"
  t.integer "impressionable_id"
  t.integer "user_id"
  t.string "controller_name"
  t.string "action_name"
  t.string "view_name"
  t.string "request_hash"
  t.string "ip_address"
  t.string "session_hash"
  t.text "message"
  t.text "referrer"
  t.text "params"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["controller_name", "action_name", "ip_address"], name: "controlleraction_ip_index"
  t.index ["controller_name", "action_name", "request_hash"], name: "controlleraction_request_index"
  t.index ["controller_name", "action_name", "session_hash"], name: "controlleraction_session_index"
  t.index ["impressionable_type", "impressionable_id", "ip_address"], name: "poly_ip_index"
  t.index ["impressionable_type", "impressionable_id", "params"], name: "poly_params_request_index"
  t.index ["impressionable_type", "impressionable_id", "request_hash"], name: "poly_request_index"
  t.index ["impressionable_type", "impressionable_id", "session_hash"], name: "poly_session_index"
  t.index ["impressionable_type", "message", "impressionable_id"], name: "impressionable_type_message_index"
  t.index ["user_id"], name: "index_impressions_on_user_id"
end

2.Tweetsテーブルにカウント数のカラムを追加

ターミナル
$ rails g migration AddImpressionsCountToTweets impressions_count:integer

以下のmigrationファイルにdefault: 0を追記します。

~_add_impressions_count_to_tweets.rb
class AddImpressionsCountToTweets < ActiveRecord::Migration[6.0]
  def change
    # 「default: 0」を追記
    add_column :users, :impressions_count, :integer, default: 0
  end
end
ターミナル
$ rails db:migrate

3.モデルを編集

tweet.rb
# 追記
is_impressionable counter_cache: true

is_impressionable
➡︎ Tweetモデルでimpressionistを使用できるようにします。

counter_cache: true
➡︎ impressions_countカラムがupdateされるようにします。

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

tweets_controller.rb
def index
  @tweets = Tweet.all
  @rank_tweets = Tweet.order(impressions_count: 'DESC') # ソート機能を追加
end

def show
  @tweet = User.find(params[:id])
  impressionist(@tweet, nil, unique: [:ip_address]) # 追記
end

Tweet.order(impressions_count: 'DESC')
➡︎ tweet一覧をPV数の多い順に並び替える。

impressionist(@user, nil, unique: [:ip_address])
➡︎ tweet詳細ページにアクセスするとPV数が1つ増える。

※自主的にPV数を伸ばす事が出来ないように、今回はip_addressにてPV数をカウントします。

※rails sしてlocalhostで試す場合、ip_addressはデフォルトの::1が入るため、PV数は1を超えません。しかしデプロイ後はきちんと動作することが確認できていますので、ご安心ください。

5.ビューを編集

app/tweets/index.html.erb
<h3> 投稿一覧 </h3>
<% @tweets.each do |t| %>
  <%= t.~ %>

  # PV数
  <%= t.impressions_count %>
<% end %>


<h3> PV数ランキング </h3>
<% @rank_tweets.each do |t| %>
  <%= t.~ %>

  # PV数
  <%= t.impressions_count %>
<% end %>

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

【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で続きを読む

[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で続きを読む

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で続きを読む

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で続きを読む