20201123のRubyに関する記事は30件です。

[初心者向け]怪奇!Herokuで画像投稿する際の謎のエラー

ローカルでは正常に動いていた。Herokuになると途端に
スクリーンショット 2020-11-23 23.43.06.png
こうなってしまいます。

エラーの正体を先にいうと、、


拡張子です。


ファイルの名前の最後についてるあれです。.pdf.png.mp3です
Herokuでは、"jpeg"が使えません。

※ちなみに"jpg"は使えます。

僕が投稿しようとしてエラーが出ていた画像は拡張子が全てjpegでした。

ログを見ても気付きにくい初心者には手強いエラーなので気をつけてください!:)

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

初期データをseeds.rbに記述して、ゲストログイン機能のエラーを解決してみた

はじめに

 ゲストログイン機能を実装したが、ユーザー登録以前に、2つの情報を登録し、ユーザーと紐づけていたので、それを解決するために、試行錯誤した結果、初期データの作成にたどり着いた。

seeds.rbとは

 Rails6.0では、(もう少し前からだとは思うが、)デフォルトで入っているファイル。dbディレクトリの配下にある。最初から、コメントアウトで色々記述されているが、説明なので、消してしまって構わない。

記述方法

 seeds.rbには、初期データとして作成しておきたい(テスト用などの目的)データを直接作る。

seeds.rb
User.create!(name: 'ゲスト', email: 'gest@sample.com')

モデル名.create!(カラム名: 値)が基本形。
create!の部分は他にも、いくつか使えるメソッドがある。(次回、紹介予定)
上記のように、書けば、いくつでも初期データを作成できる。モデル名の部分を他に変えれば、別のテーブルにも作成可能。

カラム名にidを用いることもできるので、いつもは自動で振り当てられるidについても、任意で作成可能。

ちなみに、eachメソッドやtimesメソッドを使って、繰り返し処理によって、大量の初期データを作成することも可能。

初期データ生成方法

ターミナルで、

rails db:seed

を実行。特にエラーが無ければ、特に反応なく、次の行にいき、待機状態となる。(success!みたいに、表示してくれれば安心なのに…)。テーブルで実際に保存されているか、確認するとよい。

最後に

 これで本番環境でも、生成のコマンドさえ実行すれば、無事に、ゲストログインもできるはず!

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

「N+1問題」とは??

1. 「N+1問題」とは??

アソシエーションを利用した際に、データベースへのアクセス回数が多くなってしまう問題を「N+1問題」といいます

例えば1つのツイート投稿(tweet)は1人のユーザー(user)とアソシエーションで結びついているとします。そして、データベースから全てのツイート投稿を取得するとアソシエーションによりユーザー情報も取得しようとします。
この時、次のコードだとターミナルではこのようなログを示します。

class TweetsController < ApplicationController
  def index
    @tweets = Tweet.all
  end
end

スクリーンショット 2020-11-23 22.07.24.png
水色の箇所がTweetsテーブルとUsersテーブルにアクセスしているログです。
Tweetsテーブルに対しては回で全ての情報を取得していますが、Usersテーブルには回アクセスしています。
(このときTweetsテーブルに存在するTweetのレコードは6つです。)
取得したTweet1つ1つに対して、アソシエーションを使ってユーザーのレコードを取得する処理を繰り返していることを示しています。つまり100個のツイート投稿を取得すると、100回Usersテーブルにアクセスすることになり、データ取得に時間がかかります。その結果、アプリケーションのパフォーマンスが著しく下がることになります。これが「N+1問題」です。これを解決するためには、includesメソッドを利用します。

2. includesメソッド

モデル名.includes(:アソシエーションで紐付くモデル名)

includesメソッドは、引数に指定された関連モデルを1度のアクセスでまとめて取得するメソッドです。このメソッドを用いて以下のようにコードを書き換えます。

class TweetsController < ApplicationController
  def index
    @tweets = Tweet.includes(:user)
  end
end

スクリーンショット 2020-11-23 22.07.13.png
結果、水色の箇所が2つに変わりました。Tweetsテーブルに対して回、Usersテーブルに対しても回のアクセスで全ての情報を取得できました。

3. さいごに

 私自身、今まで勉強する中で膨大なデータの使用を想定した開発をしていませんでした。そのため「N+1問題」を勉強して、アプリケーションのパフォーマンスを意識した開発する重要性を感じました。この記事を読んで、内容が良かったらLGTMお願いします!また、ご意見があれば是非コメントもお願いします。

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

[Rails]collection_selectについて勉強してみた![初心者]

はじめに

現在作成しているアプリの中で、collection_selectを使用する場面があり、備忘録のためにアウトプットします。
正直、このメソッドにたどり着くまでに、メチャクチャ時間がかかりました笑

collection_selectとは、モデルの情報を元に、セレクトボックスを生成できるメソッドです。
具体的に見ていきましょう!!

やりたかったこと

セレクトボックス内に、ユーザーの登録済住所を選択肢として用意したかったのですが、どのような記載方法が適切なのか、全然分からなくて、けっこう長い時間グーグル検索して辿り着いたのが、collection_selectです。
image.png
このセレクトボックスをクリックすると、
image.png
というように、登録している住所一覧が出るようにしたかったです!!

使い方

collection_select(オブジェクト名, メソッド名, 要素の配列, value属性の項目, テキストの項目 [, オプション or HTML属性 or イベント属性])
f.collection_select(メソッド名, オブジェクトの配列, value属性の項目, テキストの項目 [, オプション or HTML属性 or イベント属性])

使用例

<%= f.collection_select(:address_id, @addresses, :id, :order_address, prompt: "選択してください") %>

上のcollection_selectがHTMLでどうなっているのか確認すると、こうなっていました。

<select name="order[address_id]" id="order_address_id"><option value="">選択してください</option>
  <option value="1">1111111京都市田中</option>
  <option value="2">2222222アメリカ佐藤</option>
  <option value="3">3333333インド安井</option>
</select>

それぞれの引数について確認しましょう!!

第一引数 → :address_id

第一引数(プロパティ名)は、selectタグのid属性とname属性に関係しています。
今回で言うと、
name="order[〜〜〜]" id="order_〜〜〜"
という風に反映されています。

第二引数 → :@addresses

第二引数(オブジェクトの配列)は、コントローラで
@addresses = Address.where(customer_id: current_customer.id)
と定義しています。
つまり、現在ログインしているユーザー(cuurent_customer)の登録済住所を全て取得しているということです。

第三引数 → :id

第三引数(value属性の項目)は、<option value="〜〜〜">において。設定したい値のカラム名が入ります。
今回はidカラムの値を設定するので、上記記述となります。

第四引数 → :order_address

第四引数(テキストの項目)は、optionタグ内のテキスト<option>〜〜〜</option>に設定したい値のカラム名が入ります。

第五引数 → prompt: "選択してください")

最後はオプションですが、これを付け足すことにより、選択してくださいの文言が一番上に表示されるようになります。

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

�【Rails】重複した要素を取り除くメソッド!

現在作っているウェブアプリで、記事を投稿したユーザーの画像と名前をサイドバーに一覧表示したい!と思って使ったメソッドの話です。

まずやってみたのは、controllerで投稿ユーザーの情報をインスタンス変数 @post_usersに代入。

def post_users
  @post_users = Post.select(:user_id)
end

その後、viewのファイルで@post_usersから要素を取り出して表示。

しかしこのやり方だと(当然ですが)ブログの投稿の数だけユーザーの写真と名前が表示されてしまいます(写真ではuserの象が2つ出てきている)。
スクリーンショット 2020-11-23 21.58.04.png

そこで使ったのがdistinctメソッドです。Railsガイドにも記述がありました。
https://railsguides.jp/active_record_querying.html
(下記Railsガイドより)

特定のフィールドについて、重複のない一意の値を1レコードだけ取り出したい場合、distinctを使用できます。

Client.select(:name).distinct
上のコードを実行すると、以下のようなSQLが生成されます。

SELECT DISTINCT name FROM clients

「重複のない一意の値を1レコードだけ取り出したい場合」というのがいかにもRailsガイドっぽい言い回しの気がしますが、、、要するに

モデル.select(:取得する列).distinct"

の形にすれば、重複した要素をはじいてくれるようです。早速controllerにdistinctを追加。

def post_users
  @post_users = Post.select(:user_id).distinct
end

今度はうまくいきました。
スクリーンショット 2020-11-23 21.57.21.png

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

Railsアプリケーション全体のジェネレーターの設定変更方法

Railsアプリケーション全体のジェネレータの設定変更方法について、調べながら学習したのでここにアウトプットさせていただきます。

  • 開発環境
    • Ruby 2.6.4
    • Rails 5.2.3

設定方法

Railsアプリ全体のジェネレーターの設定を変更するには、config/application.rbファイルに設定コードを追記してあげると変更する事ができます。

application.rbファイルのclass Application < Rails::Applicationの記述下に、

config.generators do |g|
end

以上のコードを追記し、config.generators do |g| 〜 endの間に設定コードを追記します。

config.generators do |g|
 #ここに設定コードを追記する
end

今回はRails generate controllerでファイルを自動生成する際に、asset, helper, testファイル, ルーティングが生成されない様に制限を設けたかったので、以下の4行のコードを追記しました。

config.generators do |g|
 g.assets false #assetsを生成しない
 g.helper false #helperを生成しない
 g.test_framework false #testファイルを生成しない
 g.skip_routes true   #ルーティングを生成しない
end

これでRailsアプリケーション全体のジェネレーターの設定を変更する設定ができました。

あとはこの状態でrails g controller user show の様なジェネレーターコマンドを入力すると、

スクリーンショット 2020-11-23 21.11.00.png

上記の様に必要なディレクトリやファイルだけを生成する事ができます。

終わり

以上でアウトプットを終わります。
最後まで読んでいただきありがとうございました。
少しでもお役に立てれる内容であれば幸いです。

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

Ruby ② 配列・繰り返し処理(each文)・ハッシュ・nil(何もない)

配列

配列とは、複数の値をまとめて管理するために用いる

["John", "Mike", "Emily"]

[10, 20, 30]

値の間にはコンマで区切る
文字列には "~" をつける

配列を変数に代入

names = ["John", "Mike", "Emily"]
puts names
# John
# Mike
# Emily

インデックスで管理

names = ["John", "Mike", "Emily"]
puts names[1]
# Mike

インデックスの番号は0から始まるため
0 → John
1 → Mike
2 → Emily
となる

配列と変数展開

names = ["John", "Mike", "Emily"]
puts "私の名前は #{names[1]} です"
# 私の名前は Mike です

繰り返し処理(each文)

基礎文法

配列.each do |変数名|
 #実行したい処理
end
names = ["John", "Mike", "Emily"]

names each.do|name|
 puts name
end
# John
# Mike
# Emily

配列内の値が一つずつ変数に入る
変数が使用できる範囲はendまでの間

ハッシュ

ハッシュとは変数と同様に複数の値をまとめて管理できる
配列と違う点は
①キーを用いること
②[ ]ではなく{ }を使う

基礎文法

{キー1 => 値1 , キー2 => 値2}
{"name" => "John", "age" => 20}

ハッシュを変数に代入

user = {"name" => "John", "age" => 20}
puts user
# {"name" => "John", "age" => 20}

キーで管理

user = {"name" => "John", "age" => 20}
puts user[name]
# John

ハッシュの要素を更新

user = {"name" => "John", "age" => 20}
puts user[age]
user[age] = 21
puts user[age]
# 20
# 21

ハッシュの要素を追加

user = {"name" => "John", "age" => 20}
puts user
user["Birthplace"] = "NewYork"
puts user
# {"name" => "John", "age" => 20}
# {"name" => "John", "age" => 20, "Birthplace" => "NewYork"}

ハッシュの別の書き方

{"name" => "John", "age" => 20}
              ↓
{:name => "John", :age => 20}

文字列ではなく先頭にコロンを付けた書き方もできる
この書き方を シンボル という

ハッシュのキーにシンボルを用いる

user = {"name" => "John", "age" => 20}
puts user["name"]
user = {:name => "John", :age => 20}
puts user[:name]
# John
# John

出力結果は同じになる

ハッシュのキーにシンボルを用いた時の省略方法

user = {:name => "John", :age => 20}
                ↓
user = {name : "John", age : 20}

:name => を name: に省略可能

user = {name : "John", age : 20}
puts user[:name]

省略しても同様にシンボルで出力できる

nil (何もない)

nilとは、ハッシュから存在しないキーの値を取り出した時の「何もない」という値

user = {name : "John", age : 20}
puts user[:Birthplace]
# 表示なし (nil)

nilの回避方法

nilの回避方法としてif文を使用する

true → false と nil以外
false → false と nil

user=[name:"John",age:20]

if  puts user[:age]
# ageの値があるため true となる
 puts "#{user[:name]}さんは#{user[:age]}歳です"
else
 puts "#{user[:name]}さんの年齢は秘密です"
end

# Johnさんは20歳です
user=[name:"John"]

if  puts user[:age]
# ageの値がないため false となる
 puts "#{user[:name]}さんは#{user[:age]}歳です"
else
 puts "#{user[:name]}さんの年齢は秘密です"
end

# Johnさんの年齢は秘密です

配列の要素にハッシュを入れる

基礎文法

[ {キー1 : 値1} , {キー2 : 値2}]
[ {name : "John",age : 20} , {name : "Mike",age : 21}]

[ {name : "John",age : 20} , 
  {name : "Mike",age : 21}
]

[ ]で囲み、ハッシュをコロンで区切る
要素ごとに縦に書くこともできる

each文を使う

users=[ {name : "John",age : 20} , 
        {name : "Mike",age : 21}
      ]
users.each do|user|
 puts user[:name]
end
# John
# Mike

配列の各要素が順に変数(user)に入る

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

Ruby ② 配列・繰り返し処理(each文)・ハッシュ・nil(何もない)

配列

配列とは、複数の値をまとめて管理するために用いる

["John", "Mike", "Emily"]

[10, 20, 30]

値の間にはコンマで区切る
文字列には "~" をつける

配列を変数に代入

names = ["John", "Mike", "Emily"]
puts names
# John
# Mike
# Emily

インデックスで管理

names = ["John", "Mike", "Emily"]
puts names[1]
# Mike

インデックスの番号は0から始まるため
0 → John
1 → Mike
2 → Emily
となる

配列と変数展開

names = ["John", "Mike", "Emily"]
puts "私の名前は #{names[1]} です"
# 私の名前は Mike です

繰り返し処理(each文)

基礎文法

配列.each do |変数名|
 #実行したい処理
end
names = ["John", "Mike", "Emily"]

names each.do|name|
 puts name
end
# John
# Mike
# Emily

配列内の値が一つずつ変数に入る
変数が使用できる範囲はendまでの間

ハッシュ

ハッシュとは変数と同様に複数の値をまとめて管理できる
配列と違う点は
①キーを用いること
②[ ]ではなく{ }を使う

基礎文法

{キー1 => 値1 , キー2 => 値2}
{"name" => "John", "age" => 20}

ハッシュを変数に代入

user = {"name" => "John", "age" => 20}
puts user
# {"name" => "John", "age" => 20}

キーで管理

user = {"name" => "John", "age" => 20}
puts user[name]
# John

ハッシュの要素を更新

user = {"name" => "John", "age" => 20}
puts user[age]
user[age] = 21
puts user[age]
# 20
# 21

ハッシュの要素を追加

user = {"name" => "John", "age" => 20}
puts user
user["Birthplace"] = "NewYork"
puts user
# {"name" => "John", "age" => 20}
# {"name" => "John", "age" => 20, "Birthplace" => "NewYork"}

ハッシュの別の書き方

{"name" => "John", "age" => 20}
              ↓
{:name => "John", :age => 20}

文字列ではなく先頭にコロンを付けた書き方もできる
この書き方を シンボル という

ハッシュのキーにシンボルを用いる

user = {"name" => "John", "age" => 20}
puts user["name"]
user = {:name => "John", :age => 20}
puts user[:name]
# John
# John

出力結果は同じになる

ハッシュのキーにシンボルを用いた時の省略方法

user = {:name => "John", :age => 20}
                ↓
user = {name : "John", age : 20}

:name => を name: に省略可能

user = {name : "John", age : 20}
puts user[:name]

省略しても同様にシンボルで出力できる

nil (何もない)

nilとは、ハッシュから存在しないキーの値を取り出した時の「何もない」という値

user = {name : "John", age : 20}
puts user[:Birthplace]
# 表示なし (nil)

nilの回避方法

nilの回避方法としてif文を使用する

true → false と nil以外
false → false と nil

user=[name:"John",age:20]

if  puts user[:age]
# ageの値があるため true となる
 puts "#{user[:name]}さんは#{user[:age]}歳です"
else
 puts "#{user[:name]}さんの年齢は秘密です"
end

# Johnさんは20歳です
user=[name:"John"]

if  puts user[:age]
# ageの値がないため false となる
 puts "#{user[:name]}さんは#{user[:age]}歳です"
else
 puts "#{user[:name]}さんの年齢は秘密です"
end

# Johnさんの年齢は秘密です

配列の要素にハッシュを入れる

基礎文法

[ {キー1 : 値1} , {キー2 : 値2}]
[ {name : "John",age : 20} , {name : "Mike",age : 21}]

[ {name : "John",age : 20} , 
  {name : "Mike",age : 21}
]

[ ]で囲み、ハッシュをコロンで区切る
要素ごとに縦に書くこともできる

each文を使う

users=[ {name : "John",age : 20} , 
        {name : "Mike",age : 21}
      ]
users.each do|user|
 puts user[:name]
end
# John
# Mike

配列の各要素が順に変数(user)に入る

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

README に記述するアプリ開発の背景

はじめに

個人開発したオリジナルアプリのREADMEを記述しています。このアプリを開発した背景や問題、課題を記載しました。問題と背景の切り分け、考え方の本質を書き記します。

目次

1.問題と課題の違い
2.オリジナルアプリの開発背景から設定した問題と課題

1.問題と課題の違い

辞書などにおける意味は下記の通り。言葉そのもの意味に差異はない。

問題:批判・論争・研究などの対象となる事柄。解決すべき事柄。課題。
課題:解決しなければならない問題。果たすべき仕事。

参考:
課題
問題

しかし、ビジネスにおいては明確な違いがある。主に下記のような意味で使われる。

問題:好ましくない状態
課題:問題を解決すべく行うこと

問題と課題の因果関係は上位に問題、下位に課題となる。基本的には1つの問題に対し、複数の課題がぶら下がる。逆算的に見れば複数ある下位の課題を全て解決すると、上位の問題も解決されることになる。

オリジナルアプリの開発背景から設定した問題と課題

背景
コロナ渦をきっかけとしてリモートワークが増えており、それに伴い自宅にいる時間も増えてる。普段の職場における労働と比べ、具体的には下記2点の傾向が増加していると考えた。

・一人で部屋にこもりがち。
・仕事とプライベートの切り替えが曖昧。これまで仕事をしない時間帯や場所で仕事をする。

問題
下記2点の理由から「メンタルヘルスが悪化する可能性がある」を問題として設定した(状態)。

・人との接点が減り孤独を感じやすくなる
・デスク周りの汚れや整理整頓に対し、他者から指摘されない。部屋が汚く無意識にストレスを感じる。

課題
メンタルヘルスが悪化するという問題に対し、下記2点を課題とした(やること)。
・対面に変わる、孤独を感じない仕組みの確立
・清潔が保たれている空間の確保
image.png

そして各課題をさらに細かく分類し、具体的な機能を7つ抽出した。逆算的にみれば抽出した7つの機能を満足することにより2つの課題は解決され、最終的には問題も解決できる。
image.png

以上

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

個人アプリ開発日記 #4

では前回書いた様々なメソッドが機能してくれるのか簡単なビューを使って確かめてみましょう、、!

まずは新規登録から

このような簡単なビューで新規登録

users/new.html

<% if current_user %>
  <h1>welcome to my app</h1>
<% else %>
  <%= link_to "ログイン", login_path%>
<% end %>


<h2><%= link_to "ユーザー一覧",users_path %></h2>
<h2><%= link_to "投稿一覧", drinks_path %></h2>
<% unless  logged_in? %>
    <div class="col-md-6 col-md-offset-3">
    <%= render 'shared/form' %>
  </div>
<% end %>



  <% if current_user %>
    <p>ログインしてます</p>
    <%= link_to @current_user.nickname, user_path(@current_user) %>
    <%= link_to "ログアウト",logout_path,method: :delete %>
    <%= link_to "投稿する", new_drink_path %>
    <%= link_to "退会する", user_path(current_user),method: :delete %>
  <% else %>
    <strong>ログインしてません</strong>
  <% end %>

前回定義したcurrent_userメソッドや、logged_in?メソッドを使ってみました
スクリーンショット 2020-11-23 18.36.21.png

これが今回のルート画面です

ログインしてみます

1606132818649@2x.jpg

しっかり前回書いたコードが機能してるみたいですね、、、!!

users/show.html.erb

<h1><%= current_user.nickname%></h1>
<%= link_to "toppage", root_path %>
<%= link_to "setting", edit_user_path(current_user)%>

  <p><%= current_user.nickname %>さんの投稿一覧</p>
  <% @drinks.each do |drink| %>
      <%= drink.price%>
      <span class="name"><%= drink.name %></span>
      <p><%= drink.explain %></p>
  <% end %>

drinkリソースの実装は次回で書きます

前回書いたusers#showあたりがしっかり機能していて、sessions_helperのcurrent_userも使えます
便利メソッドはビューでも呼び出せるので、ビューでも呼び出したいメソッドはhelperメソッドに定義した方がいいのかもしれません

次はuserの更新を見てみましょう

先ほどのユーザー詳細ページにsettingというところをクリックすると

1606133218088@2x.jpg

という画面に遷移します

コードはこんな感じ

users/edit.html.erb

<h1>Update <%= @user.nickname %>'s profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'shared/form' %>
  </div>
</div>
<%= render 'shared/form' %>

部分テンプレートを使用してるのでそちらもみてみましょう

shared/_form.html.erb

<%= form_with(model: @user, local: true) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>

  <%= f.label :nickname %>
  <%= f.text_field :nickname, class: 'form-control' %>


  <%= f.label :email %>
  <%= f.email_field :email, class: 'form-control' %>

  <%unless @current_user%>
    <%= f.label :password %>
    <%= f.password_field :password, class: 'form-control' %>

    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation, class: 'form-control' %>
  <% end %>
  <%= f.submit "submit"%>
<% end %>

ここは新規登録ページのユーザーに関する入力フォームと一緒です

ですが

<%unless @current_user%>
<%end%>

とパスワードに関するフォームを囲むことにより、現在ログインしてる本人ならパスワードを改めて入力せずに
プロフィールの更新ができます。

パスワードを入力せずに値を更新するためには

user.rb

 validates :password, presence: true, length: { minimum: 6 },allow_nil: true

allow_nil: true にしなければなりません

パスワードがなくても新規登録できちゃうじゃん!

と思うかもしれませんが、has_secure_passwordをuser.rbに書いてるので、
新規登録の時はhas_secure_passwordくんがパスワードあるかどうか確かめてくれるらしいです、
賢いですね。

とりあえず「そうくん」に名前を変更しときましょう

ログインしたことによるトップページの変化

トップ.jpg

では早速ログアウトしてみましょう

トップ2.jpg

ログインしてないバージョンのtoppageに戻ります

そしてもう一回ログイン

ログイン画面.jpg

ログイン.jpg

ユーザーの詳細ページにリダイレクトされました
ユーザーが投稿したコーヒーの感想がチラッと見えてますが、これは次回投稿します

ちなみにremember me on this computerにチェックしたので、ブラウザを閉じたりしてもログイン情報は保持される仕組みです

どのような仕組みかは前回解説しております

何はともあれ前回書いたコードがしっかり機能することが確かめられました、、、!
本当はrails consoleとかでもうちょい上手く確かめられると思うのですが、、、、、

次回はコーヒーの感想を投稿したり、削除したり、一覧表示したり、誰の投稿かを定義したり、drinkリソースに関することをやっていきたいと思います

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

【RSpec】Ruby on Rails チュートリアル「第3章」のテストを RSpec で書いてみた

はじめに

Ruby on Rails チュートリアルでは「minitest」を使用してテストが実施されていますが、実際の現場では主に「RSpec」を使用してテストを実施するとのことなので、チュートリアルのテストを実際の現場に近づけるために「RSpec」で実施するようにしました。まず手初めに「第3章」のテストを RSpec で書き換えてみたので、同じようにチュートリアルのテストを RSpec で実施してみたい人の参考になれば幸いです。

対象者

  • Ruby on Rails チュートリアルのテストを Rspec で実施予定、または実施してみたい人
  • Ruby on Rails チュートリアル「第3章」のテストを Rspec で書きたいけど、書き方が分からない人

テストコード

実際にテストコードを書き換えた結果が下記になります。

Minitest

static_pages_controller_test.rb
require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  def setup
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Home | #{@base_title}"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | #{@base_title}"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | #{@base_title}"
  end
end

RSpec

static_pages_spec.rb
require "rails_helper"

RSpec.describe "StaticPages", type: :request do
  before do
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  describe "Home page" do
    it "should get home" do
      visit static_pages_home_url # /static_pages/home/ へアクセス
      expect(page.status_code).to eq(200) # HTTP ステータスコードが "200" か判定
      expect(page).to have_title "Home | #{@base_title}" # タイトルに 「Home | Ruby on Rails Tutorial Sample App」 が含まれているか判定
    end
  end

  describe "Help page" do
    it "should get help" do
      visit static_pages_help_url
      expect(page.status_code).to eq(200)
      expect(page).to have_title "Help | #{@base_title}"
    end
  end

  describe "About page" do
    it "should get about" do
      visit static_pages_about_url
      expect(page.status_code).to eq(200)
      expect(page).to have_title "About | #{@base_title}"
    end
  end
end

次回

「第4章」のテストコードを RSpec に書き換える予定です。

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

Rails 幹事向けアプリの作り方 複数レコードの同時登録編

最初に

この記事は、幹事向けのアプリを作成方法を記載します。
どんな構造を取るかアプリと言うと、2段階の複数登録を行うものです。

①複数のテーブルと複数レコードを同時登録(イベントと複数の日付の登録)

②①で登録した複数レコード(日付など)の中間テーブルレコードの一括登録(出欠状態など)

開発時に、②複数のレコードを登録する方法について
参考となる情報がなく苦労しました。
そこで、作り方を公開して、同じ悩みを持った方の
助けに慣れればと思い記載致します。

※UI部分については今回紹介していません。Gitにて確認お願いします。

目次

  1. 概要
  2. テーブルデータについて
  3. イベント(親)と複数の日程(子)、お店(子)の登録機能
  4. 参加者(親)と参加状況(子)の登録機能
  5. 参加者の参加状況の編集機能
  6. 参加状況の削除機能

1.アプリの概要

イベントを開催して、参加者の状況を管理。最終的にはイベントの日時・場所を決定するアプリです
ユーザーは2種類あり、「イベントの主催者」と「参加者」を想定しています。
Git:https://github.com/tsuyatsuya-april/ikang
HP:http://kyomo-ikang.com/events/1

機能

主催者
・イベントの概要・候補日・候補店を登録・編集・削除する機能
Image from Gyazo

参加者
・参加者の名前と参加状況の登録・編集・削除する機能
Image from Gyazo

2.テーブルデータについて

Userテーブル...主催者の名前・email・passwordを登録
Eventテーブル...イベントの名称と概要を登録
Scheduleテーブル...イベントの候補日を登録
Shopテーブル...イベントの開催場所候補を登録
Joinテーブル...参加者の名前を登録
DateAnswerテーブル...JoinとScheduleを親として、参加日程の状況を登録
ShopAnswerテーブル...JoinとShopを親として、開催場所の評価を登録

作成するアプリでは、主に2つのフォームで下記の関係での登録を行う
Event(親)、Schedule(子)、Shop(子)のイベント登録フォーム
Join(親)、DateAnswer(子)、ShopAnswer(子)の参加者登録フォーム

Image from Gyazo

3.イベント(親)と複数の日程(子)、お店(子)の登録機能

イベントの登録機能を下記の小項目に沿って説明を行う。
尚、ユーザーの登録は、メジャーなので割愛させていただきます。
また、コードの一部抜粋した形で説明させていただきます。

小項目

  1. 注目コード記載(Model,Controller,HTML,JS)
  2. fields_forメソッド
  3. name属性の修正

1.注目コード記載

model/event.rb
class Event < ApplicationRecord
  belongs_to :user
  has_many :shops, inverse_of: :event, dependent: :destroy
  accepts_nested_attributes_for :shops, allow_destroy: true
end

model/shop.rb
class Shop < ApplicationRecord
  belongs_to :event, inverse_of: :shops
  validates_presence_of :event
end
events_controller.rb
  def new
    @event = Event.new
    1.times { @event.shops.build } 
  end 

  def create
    @event = Event.new(event_params)
    @event.user_id = current_user.id
    if @event.save
      redirect_to event_path(@event.id)
    else
      render "new"
    end
  end

  private
  def event_params
    params.require(:event).permit(:name, :description, schedules_attributes: [:savedate, :savetime], shops_attributes: [:shop_name, :shop_url, :map_url, :comment])
  end

events/new.html
    <%= form_for @event,id:"event-new-form", local: true do |f| %>
      <%= render "share/error_messages", model: f.object %>
      <% 中略%>
          <div id="shop-add-btn">
            <%= link_to "お店追加", "#", class:"btn-flat-border" %>
          </div>
        </div>
        <%= f.fields_for :shops do |shop_fields| %>
          <div id="new-shop-top">
            <div id="new-shop">
              <div id="shop-name-box">
                <div id="shop-name"><p>店名(必須)</p></div>
                <%= shop_fields.text_field :shop_name, class:"shop-name-input"%>
              </div>
              <% 中略 %>
              <div id="shop-delete-box">
                <%= link_to "お店削除", "#", class:"btn-flat-border-red", id:"shop-delete" %>
              </div>
            </div>
          </div>
        <% end %>
      </div>
      <div class="submit-box">
        <%= f.submit "イベントの登録",class:"btn-flat-border-submit", id:"new-submit" %>
      </div>
    <% end %>
main.js
//お店の追加
   function newShopAdd(){
        nameNumberShopAdjust();
        const shopParent = document.getElementById("new-shop-top");
        const addShopBtn = document.getElementById("shop-add-btn");
        let currentShopLength = document.querySelectorAll("#new-shop").length;
        let nextNum = currentShopLength;
        let shopHtml = `
          <div id="new-shop">
            <div id="shop-name-box">
              <div id="shop-name"><p>店名(必須)</p></div>
              <input class="shop-name-input" type="text" name="event[shops_attributes][${nextNum}][shop_name]" id="event_shops_attributes_${nextNum}_shop_name">
            </div>
            ~<中略>~`;
        addShopBtn.onclick = function(){
          shopParent.insertAdjacentHTML("beforeend", shopHtml);
          shopDelete();
          newShopAdd();
        };

      }
   //お店の削除
      function shopDelete(){
        let shopParent = document.querySelectorAll("#new-shop");
        let shopDeleteBtn = document.querySelectorAll("#shop-delete");
        let shopParentLength = shopParent.length;
        for (let i=0; i < shopParentLength; i++){
          shopDeleteBtn[i].onclick = function(){
            let conformShopLength = document.querySelectorAll("#new-shop").length;
            if(conformShopLength != 1){  
              return shopParent[i].remove();
            }
          };
        };
      }
   //お店のname属性の値に入る数値の調整
   function nameNumberShopAdjust(){
        let saveShop = document.querySelectorAll(".shop-name-input");
        let saveShopLength = saveShop.length;
        for(let j=0; j<saveShopLength; j++){
          saveShop[j].removeAttribute("name");
          saveShop[j].setAttribute("name",`event[shops_attributes][${j}][shop_name]`);
        }
      }
    } 

2.fields_forメソッド

このメソッドは、公式ドキュメントにて下記のような説明をしている。
モデルを固定してフォームを生成
form_for内で異なるモデルを編集できるようになる。

つまり、今回でいうとEvent(親)とは別のShop(子)モデルの編集ができるようになるというものである。
ただし、fields_forを使う為にはいくつか準備が必要である。

アソシエーションの設定

model/parent.rb
親と子のモデルの双方向の関連付けができるようにする
has_many :子モデル(複数形), invers_of: :(親モデル), dependent: :destroy

子モデルが同時に登録できるようにする
accepts_nested_attributes_for :子モデル(複数形), allow_destroy: true(空白のフォームがあれば登録できないようにする)
model/child.rb
  親と双方向の関連付けしてバリデーション設定などができるようにする。(例 shops.eventなど)
  belongs_to :親モデル, inverse_of: :子モデル

コントローラの設定

controller/test.rb
def new
 @event = Event.new
 関連した子の要素を生成する時はbuild(newのエイリアス)を使用する
 1times { @event.shops.build }
 ちなみにtimesの数値の分だけ子の要素の登録フォームができる
end

通常に保存する場合と同じ
def create
  @event = 親モデル.new(event_params)
    if @event.save
      redirect_to 指定のパス
    else
      render アクション名
    end
end

private
def event_params
    params.require(:親モデル).permit(:親カラム1, :親カラム2, 
                  子モデル名(複数系)_attributes: [:子カラム1])
    .merge(user.id: :current_user.id)
end

ビューの設定

views/test.html
<%= form_for @event,id:url, local: true do |f| %>
  <%= f.text_field :親カラム名 %>
 ここで子モデルの登録ができるように設定を行う
  <%= f.fields_for :子モデル do |sf| %>
     <%= sf.text_field :子モデルカラム名 %> 
  <% end %>
  <%= f.submit "イベントの登録" %>
<% end %>

上記のビュー・モデル・コントローラーの設定を行えば、
フォームの送信ボタンを押した後にデータが下記のようなパラメータで送られます。

paramater.rb
通常の場合
親モデル名 = { :first => 1, :second => 2}

fields_forを使用した場合
親モデル名 = {:first => 1, :second => 2, 
子モデル名 => {:first => 1, :second => 2 }}

見にくいですが具体的な例が下記画像です。
Image from Gyazo

親モデルの中に子モデルがネストされてデータが受け渡され保存されるということになります。
これで複数モデルの複数レコードの保存の下地が整いました。

3. name属性の修正

2では子モデルの複数のレコードが保存できる設定の説明を行ってきた。
ただし、javascriptを用いてお店と日程フォームの増減が行えるように設定をしています。
この時に保存が上手く行かなくなることがあったので、その原因と解消方法について記載します。

問題
javascriptでフォームの追加を実行した後に全てのフォームが保存されない時があった。

前提条件
fields_forを使った時のフォームのname属性がhtml上で下記のように変換されて表示される

test.html
fields_for内のinputタグの中にあるname属性の名前
event[schedules_attributes][0][savedate]
event[schedules_attributes][1][savedate]
event[schedules_attributes][3][savedate]

公式化
親モデル名[子モデル名(複数系)_attributes][要素番号][子モデルの対象カラム名]

仮説検証
Image from Gyazo

解消方法
javascriptを使ってデータの送信前にname属性の要素番号が被らないように調整する。

main.js
   //(送信ボタンを押した時に要素番号を調整するメソッドを実行)
   function nameNumberShopAdjust(){
     //お店のフォームのセレクタを取得
        let saveShop = document.querySelectorAll(".shop-name-input");
     //要素の数がいくらあるかを取得
        let saveShopLength = saveShop.length;
     //要素数分だけループを実行
        for(let j=0; j<saveShopLength; j++){
      //フォームの既存で設定されたname属性を削除する
          saveShop[j].removeAttribute("name");
      //フォームのname属性について、現在のループ回数を要素番号として付け直す。
          saveShop[j].setAttribute("name",`event[shops_attributes][${j}][shop_name]`);
        }
      }
    } 

ここの部分に関しては、ドキュメントに記載されておらず挙動を読んで
仮説検証をたてたものですが上記方法で解決致しました。

4.参加者(親)と参加状況(子)の登録機能

大項目3で指定した日程とお店について、参加状況を登録する方法について記載する。
join(親)、shop(親)、shop_answer(子)の中間テーブル、
join(親)、schedule(親)、date_answer(子)の中間テーブル
上記2つの登録を主に行う。
尚、解説はお店の方のみさせていただく

小項目

  1. 注目コード記載(Model,Controller,HTM, routes)
  2. viewのname属性の調整
  3. 中間テーブルの親_idの格納
  4. event/showページで子のjoinコントローラにパラメータを渡す方法
  5. joinコントローラーのupdateアクション

1. 注目コード記載(Model,Controller,HTM, routes)

model/join.rb
  has_many :shop_answers, dependent: :destroy
  accepts_nested_attributes_for :shop_answers, allow_destroy: true
model/shop.rb
  has_many :shop_answesr, dependent: :destroy
model/shop_answer.rb
  belongs_to :join
  validates_presence_of :join
  belongs_to :shop
events_controller.rb
  def show
    if params[:join_id]
      set_join
    else
      @join = Join.new
      1.times { @join.shop_answers.build }
    end
  end
joins_controller.rb
  def create
    @join = Join.new(join_params)
    if @join.save
      redirect_to event_path(params[:event_id])
    else
      render "events/show"
    end
  end
  private
  def join_params
    params.require(:join).permit(:nickname, :email, 
      shop_answers_attributes: [:shop_id, :status, :vote])
      .merge(event_id: params[:event_id])
  end
events/show.html
<%= form_with model: @join, url:event_joins_path(@event.id), id:"join-form",  local: true do |f| %>
   <div id="join-box">
     <div id="join-name-label">
       <label>ユーザー名</label>
     </div>
     <div class="join-users">
      <%= f.text_field :nickname, id:"join-user",placeholder:"ニックネームを入力ください"%>
     </div>

     <div id="shop-answer">
       <h1 id="shop-answer-title">店舗選択</h1>
         <div class="circle-text">説明: お店を◯×△で評価,一番良いと思う店に一番ボタンで投票下さい</div>
           <table id="shop-answer-table">
             <tbody> 
               <% @event.shops.each do |es| %>
                  <%= f.fields_for :shop_answers do |shop_fields| %>
                    <tr id="join-shops" class="bottom-line">
                      <th class="shop-label">
                        <div>
                          <%= link_to es.shop_name, es.shop_url, target: :_blank %>
                        </div>
                      </th>
                      <td class="shop-vote-balance">
                        <%= shop_fields.hidden_field :shop_id,class:"shop-id", value: es.id %>
                        <%= shop_fields.hidden_field :status, id:"shops-status" %>
                        <%= shop_fields.hidden_field :vote, id:"shops-vote" %>
                        <div class="change-status">
                          <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow " id="shop-yes"></h1>
                          <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow choice" id="shop-delta"></h1>
                          <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow cross-vote" id="shop-no">×</h1>
                         </div>
                         <div id="shop-change-vote">
                           <h1 class="btn btn--orange btn--circle btn--circle-a btn--shadow" id="shop-vote">一番</h1>
                         </div>
                       </td>
                    </tr>
                 <% end %>
              <% end %>
            </tbody>
         </table>
      </div>
       <div id="join-submit-box">
        <%= f.submit "回答",class:"btn-flat-border", id:"join-submit-inputbox", :onclick => "return check_name()" %>
       </div>
  <% end %>
routes.rb
Rails.application.routes.draw do
  root to: "events#index"
  resources :events do
    resources :schedules
    resources :shops
    resources :joins
    resources :comments
    resources :date_decisions
    resources :shop_decisions
  end
end

2. viewのname属性の調整

大項目3で登録した複数データに対して、複数の中間テーブルの保存を行うには下記のコードの構造をとる。

events/show.html
<%= form_with model: @join, url:event_joins_path(@event.id), id:"join-form",  local: true do |f| %>
  親モデルのnicknameカラムの保存フォーム
  <%= f.text_field :nickname, id:"join-user",placeholder:"ニックネームを入力ください"%>

  イベントページに紐づいているお店のレコードを全て出力する。
  <% @event.shops.each do |es| %>

  fields_forで複数レコード登録できるようにする。
    <%= f.fields_for :shop_answers do |shop_fields| %>

    中間テーブルの親IDを登録するフォーム。事前にid番号をvalueに格納する
      <%= shop_fields.hidden_field :shop_id, value: es.id %>

    javascriptを使って、数字1(=◯),2(=△),3(=×)が格納されるようにしている
      <%= shop_fields.hidden_field :status, id:"shops-status" %>

    javascriptを使って投票数0,1を格納する。
      <%= shop_fields.hidden_field :vote, id:"shops-vote" %>

    onclickでユーザーのニックネームが格納されていない場合にフォームを送信できないようにする
      <%= f.submit "回答",:onclick => "return check_name()" %>
    <% end %>
  <% end %>
<% end %>               

Image from Gyazo
解決方法
大項目3と同様な形でjavascriptを用いて、
出力されたお店の分、name属性の要素番号を付け直す
これで出力されたお店のレコード分だけ、中間テーブルのレコードが保存できるようになった。

3. 中間テーブルの親_idの格納

中間テーブルであるshop_answerテーブルに親IDを紐付ける方法を記載する。
・1点目のjoin_idはfields_forメソッドを使用しているので親子関係となり自動的にIDが割り振られる。
・2点目のshop_idは下記の通りeachメソッドを用いて、各ID番号をshop_idのフォームに格納している。

events/show.html
<% @event.shops.each do |es| %>
  <%= f.fields_for :shop_answers do |shop_fields| %>

    先ほども記述したがes.idでshop(親)のID番号をshop_answer_idに渡している。
    <%= shop_fields.hidden_field :shop_id, value: es.id %>
  <% end %>
<% end %>               

以上で中間テーブルの保存ができる状態になった

4. events/showページで子のjoinコントローラにパラメータを渡す方法

初学者にとってevents/showページで登録を行う場合、
events_controllerにパラメータをpostするという印象が強い。
しかし今回はjoins_controllerにパラメータをpostして登録を行うので
その場合のurl指定方法を記載する。

まず下記コマンドを実行しURLの確認を行う

test.rb
 rails routes

次に確認したURLの内joinの登録であるURLをform_withメソッドの中に適応させる

events/show.html
 <%= form_with model: @join, url:event_joins_path(@event.id), id:"join-form",  local: true do |f| %>

以上を行うことで、events/showページからjoins_controllerにデータを受け渡すことができるようになる。

5.参加者の参加状況の編集機能

events/show.htmlにて参加者毎の編集フォームを表示する方法について記載を行う。
複数の情報を出力する上で

小項目

  1. 同一ページ内で参加者IDの情報をコントローラに受け渡す方法
  2. fields_forを用いた場合の編集フォームのviewを表示について。
  3. objectメソッド、親データの店名やURLを引き出す方法

1. 同一ページ内で参加者IDの情報をコントローラに受け渡す方法

まず、events/show.html上に参加者の編集フォームを出力するには、
コントローラに各参加者のjoin_idの数値を送り、データを抽出する必要がある。

その為に、link_toメソッドを用いて、join_idをパラメータとしてコントローラに引き渡す手法を用いた。

events/show.html
<% if @event.joins %>
 登録されている参加者全てを出力する
  <% @event.joins.each do |event_join| %>
  ループ中の参加者のID、join.idをjoin_idというキーに格納してコントローラに渡している
    <%= link_to event_join.nickname, event_path(@event.id,join_id: event_join.id) %>
  <% end %>
<% end %>

コントローラではjoin_idというパラメータの有無によって
編集フォームの出力か登録ページの出力かの分岐を行っている。

events_controller.rb
def show
  @joins = Join.all
  if params[:join_id]
    set_join
  else
    @join = Join.new
    1.times { @join.shop_answers.build }
  end
end

private
def set_join   
    @join = Join.find(params[:join_id])
end

編集フォームの場合は、決まった値をフォームに格納した状態で表示される。
登録フォームの場合は、新規にデータを作成できる状態で表示される
これでビューに表示できる条件が整った。

2. fields_forを用いた場合の編集フォームのviewを表示について

登録時には、shop.each doとfields_forを用いたが編集時には、fields_forのみを用いれば、選択したJoinレコードに紐づくshop_answerレコードが全て出力される。

events/show.html
<% unless params[:join_id] %>
新規登録の時の処理
<% else %>
編集時の処理
  <%= form_with model: @join, url:event_join_path(@event.id, @join.id), id:"join-edit-form" , local: true do |f| %>
   ユーザー名の編集
    <%= f.text_field :nickname,id:"join-edit-user", placeholder:"ニックネームを入力ください"%>
   fields_forを用いて、選択したJoinレコードに紐づく全てのshop_answerレコードを出力
    <%= f.fields_for :shop_answers, @join.shop_answers do |shop_edit_fields| %>
   編集ページでは新たに、joinのidカラムを格納するフォームを用意する。それ以外は登録と同じ。
      <%= shop_edit_fields.hidden_field :id,class:"shop-answers-id" %>
      <%= shop_edit_fields.hidden_field :shop_id,class:"shop-edit-id" %>   
      <%= shop_edit_fields.hidden_field :status, id:"shops-edit-status" %>
      <%= shop_edit_fields.hidden_field :vote, id:"shops-edit-vote" %>  
    <% end % >
    <%= f.submit "更新",class:"btn-flat-border", id:"join-edit-submit-inputbox", :onclick => "return check_name()" %>
  <% end %>
<% end %>

同じ躓きをする方もいると思うので私が失敗した時の場合も下記の画像にて記載します。
Image from Gyazo

3. objectメソッド、親テーブルの店名やURLを引き出す方法

小項目2では、フォームが正しく表示されたが実は1つ問題がある。
それは、fields_forでは、
「中間テーブルの子モデルに対し、親モデルのデータが引き出せない」ことである。
私のアプリでは店名やURLが表示されず、どのデータに紐づいているか判断出来なくなる。

これに対して、shops.each do を用いて、店名を引き出せば良いと考えたこともあった。
しかし、結果は小項目2の最後の画像の通り、フォームが正しく表記されない。

そこで、objectメソッドを用いることにした。
このメソッドを変数の後につけることで、変数に格納しているデータを取得できる。

test.HTML
  shop_answer = { id => 1, join_id => 1, shop_id => 1, status => 1}
  shop = { id => 1, shop_name => "吉野家", shop_url => "yoshinoya" }

  <%= f.fields_for :shop_answers, @join.shop_answers do |shop_edit_fields| %>
   snum = shop_id = 1
    <% snum = shop_edit_fields.object.shop_id%>
      sn = Shop.find(1)
     <% sn = Shop.find(snum) %>
     これでfields_for内でshop_answerの親モデルのお店情報が出力できるようになった
     sn.shop_name = 吉野家
   sn.shop_url = yoshinoya

objectメソッド導入前と導入後のイメージ
Image from Gyazo

5. joinコントローラーのupdateアクション

fields_forを用いた時は、paramsの表記方法が異なる。
登録時には子モデルのID番号が必要なかったが
更新時には小モデルのID番号が必要となる

また、:_destroyをつけることで親モデルに紐づく子モデルのデータを削除することができる。
例えば、編集時に子モデルのフォームを空白に変更して更新するとデータが削除されるなど。

joins_controller.rb
  def update
    if @join.update(join_update_params)
      redirect_to event_path(params[:event_id])
    else
      render "events/show"
    end
  end

  private
  通常時
  def join_params
    params.require(:join).permit(:nickname, :email, 
      date_answers_attributes: [:schedule_id, :status], 
      shop_answers_attributes: [:shop_id, :status, :vote])
      .merge(event_id: params[:event_id])
  end
 
 更新時
  def join_update_params
    params.require(:join).permit(:nickname, :email, 
      date_answers_attributes: [:id,:schedule_id, :status, :_destroy], 
      shop_answers_attributes: [:id,:shop_id, :status, :vote, :_destroy])
  end

6. 参加状況の削除機能

アソシエーションの設定で親レコードの削除に伴い紐づく子のレコードが削除されるようにする必要がある。
allow_destroy: trueは
親要素が削除された時、関連付けている情報もまとめて削除するためのオプションです。

model/join.rb
  has_many :date_answers, dependent: :destroy
  accepts_nested_attributes_for :date_answers, allow_destroy: true
model/shop.rb
  has_many :shop_answer, dependent: :destroy
model/shop_answer.rb
  belongs_to :join
  validates_presence_of :join
  belongs_to :shop

以上のアソシエーションをつけることで通常時と変わりなく、削除を実行することができる。
今回はビューとコントローラーの記述を割愛させていただきます。

最後に

ここまで読んで下さってありがとうございます。
はじめて個人で作成したアプリなのでアラが目立つかとは存じますが
ご容赦下さいますようお願いします。

また、何かご質問があればコメントお願いします。
お答えできる限りはお答え致します。

参考

https://qiita.com/kouuuki/items/5daf2b5f34273d8457f7

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

【ruby on rails】「devise」gemを利用したウィザード方式のユーザー登録


Toshiです!!
今日はウィザード方式でのユーザー情報の登録についてまとめます。

 この記事の対象の方

・Ruby on railsを学んでいる方
・deviseのgemの使い方がわかる方

ただ、条件関係なくその他の方も全然Welcomeです:sunglasses:

 「ウィザード形成」とは

ウィザード形式とは、対話するように順番に操作が進んでいく方式のことです。例として、ユーザー登録の際に、最初のページでユーザー情報(名前やメールアドレスなど)の登録を行い、次のページで住所などの情報を登録するように、ページを切り替えて登録を行うことが挙げられます。


ユーザーのメリットとして

1ページで縦長に全ての情報を登録するよりも、どの情報を、どのページで登録しているのか分かるので、ユーザーからすると見やすく使いやすいという特徴があります。

 STEP0 事前に準備できていること

・deviseのgemを、ruby on railsにインストールしていること

(すでにできているアプリを前提に説明をしていきます)
・ビューが完成し、ユーザー登録はできている状態。

 STEP1 ウィザード形式のイメージを掴む

1.png

画像は完全拝借物ですが、上記のようなイメージです。

 STEP2 「ウィザード形式で次の画面で登録するための情報用のモデルを作成する」

今回はユーザー情報に加えて、本人の住所情報を登録させます。

今回は「Addressモデル」という名前で、ユーザー情報登録後に登録する住所情報のモデルを作成しています。

⭐️重要ポイント①
最初のユーザー情報登録画面画面で、「user_id」が外部キーの設定によって作られる形ですが、addressesテーブルには、住所情報入力時点では登録されない!

そのため、下記のように住所の登録時にuser_id(Addressesテーブルの外部キー)が、nil(空欄)でも登録を許可するという設定を、モデルに追加します。

app/models/address.rb
class Address < ApplicationRecord
  belongs_to :user, optional: true #⭐️ここがポイント!!
  validates :postal_code, :address ,presence: true
end


次にユーザー情報と住所情報を1:1の関係なのでアソシエーションを設定します。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  validates :name, :age ,presence: true
  has_one :address #追加
end


⭐️重要ポイント②
ルーティングにて、Userモデルのコントローラーとの紐付けの設定を追加する
まず、deviseのコントローラーを作成します。作成後、「rails routes」を実行すると以下のようになります。**

ターミナル
% rails g devise:controllers users

2.png
devise管理下にusersコントローラーを作成されていることがわかります。



しかし、deviseはgemをインストールしてしまえば、ビューファイルやモデルを作ったりなどでユーザー情報管理ができてしまう反面、gemの中でほぼ完結してしまい、ウィザード形式のように追加実装がやりにくいデメリットもあります。

そこで、作成したコントローラーに紐付けの設定を行い、そこでcreateやnewといった7つのアクションを記載し実施できるようにします。ルーティングを以下のように設定します。

config/routes.rb
Rails.application.routes.draw do  #以下を変更
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  } #ここまで
  root to: "home#index" #すでに作成しているコントローラーに関するルーティングです。
end



rails routesを再度実行すると紐づくコントローラーが変更されています。
3.png
これで、users配下のコントローラーに7つのアクションを記載し処理を実装することができます。

 STEP3 「users配下のコントローラーに、ユーザー情報登録のアクションを記載する」

ユーザー新規登録ページへ遷移するnewアクションを設定します。

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
 # before_action :configure_sign_up_params, only: [:create]
 # before_action :configure_account_update_params, only: [:update]

  def new
    @user = User.new
  end

# 中略

end

Userモデルの新規インスタンスを生成しました。new.html.erbを下記のように編集します。(ビューファイルはデフォルトから変えていません)

app/views/devise/registrations/new.html.erb
<h2>ユーザー情報登録</h2>

<%= form_for(@user, url: user_registration_path) do |f| %> #変更
  <%= render "devise/shared/error_messages", resource: @user %> #変更

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>

  <div class="field">
    <%= f.label :age %><br />
    <%= f.text_field :age %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Next" %> #変更
  </div>
<% end %>

<%= render "devise/shared/links" %>

※実際作る人はここで画面の確認を。


次に保存のアクションを記載していきます。

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
 # before_action :configure_sign_up_params, only: [:create]
 # before_action :configure_account_update_params, only: [:update]

 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}
   session["devise.regist_data"][:user]["password"] = params[:user][:password]
   @address = @user.build_address
   render :new_address
 end #ここまで

 # 省略

end

⭐️重要ポイント③
画面遷移をしても情報を保持するために「session」を使う

最後のページまで遷移した後に保存させる為に、sessionを用います。

1ページ目で入力した情報のバリデーションチェックが完了したらsession["devise.regist_data"]に値を代入します。この時、sessionにハッシュオブジェクトの形で情報を保持させるために、attributesメソッドを用いてデータを整形しています。

なお、[devise.regist_data]は変数ではありますが、deviseが落ちてしまった時に自動で保持する値を削除できるといった副次的なメリットもあります。

また、paramsの中にはパスワードの情報は含まれていますが、attributesメソッドでデータ整形をした際にはパスワードの情報は含まれません。そこで、パスワードを再度sessionに代入する必要があります。これをsession["devise.regist_data"][:user]["password"]が担い、sessionに追加しています。

最後の「render :new_address」で次の画面の遷移をできるようにしています。new_addressはまだ未作成のためここで保存をするとエラーになります。


 STEP4 「住所情報を登録する、ウィザード形式の完成」

住所情報の登録のために先ほど、new_addressのビューに飛ぶ設定(renderメソッド)を追加しました。その画面で登録ページを表示させる「new_address」アクションと、登録を行う「create_address」アクションをルーティングに設定していきます。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }
  devise_scope :user do #追加
    get 'addresses', to: 'users/registrations#new_address'
    post 'addresses', to: 'users/registrations#create_address'
  end #ここまで
  root to: "home#index"
end



次に Viewを作成していきます。今回は、借り物のViewファイルを使います。

app/views/devise/registrations/new_address.html.erb
<h2>住所情報登録</h2>

<%= form_for @address do |f| %>
  <%= render "devise/shared/error_messages", resource: @address %>

  <div class="field">
    <%= f.label :postal_code %><br />
    <%= f.text_field :postal_code %>
  </div>

  <div class="field">
    <%= f.label :address %><br />
    <%= f.text_field :address %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

ここまでで住所登録までの画面遷移はできますが、まだ登録はできません。
住所情報を保存するために、コントローラーの設定を追加します。

⭐️重要ポイント④
ウィザード形式で遷移後の画面であっても、「registrations_controller.rb」(作成したコントローラーファイル)にアクションを記載していく。


create_addressアクション内でやることは、以下4点です。

・2ページ目で入力した住所情報のバリデーションチェック
・バリデーションチェックが完了した情報とsessionで保持していた情報を合わせ、ユーザー情報として保存
・sessionを削除する
・ログインをする

new_addressはビューファイルを用意し、特に情報を遷移時に表示させる必要はないのでコントローラーに記載は入りません。「registrations_controller.rb」に、「create_address」を以下のように追記していきます。

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
 # before_action :configure_sign_up_params, only: [:create]
 # before_action :configure_account_update_params, only: [:update]

 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}
   session["devise.regist_data"][:user]["password"] = params[:user][:password]
   @address = @user.build_address
   render :new_address
 end

 def create_address #追加
   @user = User.new(session["devise.regist_data"]["user"])
   @address = Address.new(address_params)
    unless @address.valid?
      render :new_address and return
    end
   @user.build_address(@address.attributes)
   @user.save
   session["devise.regist_data"]["user"].clear
   sign_in(:user, @user)
 end

 private

 def address_params
   params.require(:address).permit(:postal_code, :address)
 end #ここまで

# 省略

end 

上記の4つがどのように設定されているのか。

・2ページ目で入力した住所情報のバリデーションチェック

 @user = User.new(session["devise.regist_data"]["user"])
 @address = Address.new(address_params)
    unless @address.valid?
      render :new_address and return
    end

valid?メソッドを用いて、address.rbファイルに記述したバリデーションを実行します。この際、アプリケーションが設定したバリデーションの条件を満たしている場合はtrueを返します。その逆に、バリデーションの条件を満たしていない場合はfalseを返します。今回はunlessを用いているため、falseの場合にunless内に記述した処理が実行されます。

render :new_addressと記述しているため、設定したバリデーションの条件を満たさない場合は、renderメソッドによってnew_addressアクションに画面遷移します。

このとき、and returnと記述することで処理を中断させています。もしこのand returnがない場合はunless以降の記述も実行され、ユーザーの情報は保存されてしまいます。これを防ぐためにand returnを記述して処理を中断しています。

・バリデーションチェックが完了した情報とsessionで保持していた情報を合わせ、ユーザー情報として保存

@user.build_address(@address.attributes)
@user.save

同コントローラーにある「create」アクションにて、「@address」の変数を@user.build_addressと、@user変数のbuild_address要素としてあとで格納できるようにしています。
現在セッションの情報が格納されている@userに対して、「build_address」の要素を使い代入する形で住所情報を格納しています。saveメソッドを用いてテーブルに保存します。

・sessionを削除する

session["devise.regist_data"]["user"].clear

「.clear」メソッドを用いて、sessionの情報を削除します。

・ログインをする

sign_in(:user, @user)

情報が保存できてもログインできているわけではないので、sign_inメソッドでログインをします。

create_address.html.erbのビューファイルを作成し完成です

app/views/devise/registrations/create_address.html.erb
<h2>登録が完了しました</h2>
<%= link_to "トップへ戻る", root_path%>

4.gif

最後に

最初はどうしても拝借気味にはなりますが、自分なりの理解している内容などを盛り込むのも大事ですね。引き続きよろしくお願いします!!

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

【超絶初心者向け】プログラミングをかじってみる

0. はじめに

この記事はプログラミング初心者が挫折せずに学習を始められるよう、なるべく分かりやすい構成にしました。(したつもりです。)
色んなプログラミングスクールが乱立する昨今、どこに通えばいいか分からず、通ったら通ったで思った以上に難易度が高く、覚えること多過ぎて無事死亡…なんてことが多発しているようです。知らんけど。
高いお金払ってスクールに通わなくても、今の時代は無料(もしくは少額)で多くのことを学ぶことが出来ます。
無料で独学するもよし、まとまったお金を払って集中的に学ぶもよし。

どちらにしてもスタートで挫折する人がとても多いようです。
なので、まずはこの記事でプログラミングの触りについて学んでみて、自分に適性があるかどうか確認してみてはどうでしょうか。
適性がないと思ったらスッパリと諦め別の道を探しましょう。
プログラミング以上に面白くて稼げてやり甲斐があって世の中の役に立つ仕事なんてこの世にゴマンとあるので。知らんけど。

まずはこの記事の目次を確認して、やるべきことの概要をざっくり把握してください。

1. PCを準備する

当たり前ですがPCがないとプログラミングは出来ません。
今どき売られているPCであればどれでも構いません。
個人的にはMacBookをオススメしてますが、Windowsでももちろん大丈夫です。
むしろつよつよエンジニア(スキルの高いエンジニア)はWindowsを好んで使っているような気もします。

2. Visual Studio Code(略してVScode)をダウンロードする

スクリーンショット 2020-11-23 16.54.18.png

Visual Studio Code はPCでプログラミングをするために必要なツール(道具)だと思ってください。

Visual Studio Codeをインストールする

3. Visual Studio Code の説明

ダウンロードが出来たら Visual Studio Code を開いてください。
多少の違いはあれど、同じような見た目になってるかと思います。

スクリーンショット 2020-11-23 17.26.07.png

上記画面を確認出来たら、下の画面のように ターミナル > 新しいターミナル を選択してください。

スクリーンショット 2020-11-23 17.26.51.png

すると画面の下半分に新たな枠が表示されました。
画面の上半分のエリアを テキストエディタ、下半分のエリアを ターミナル と呼びます。

スクリーンショット_2020-11-23_17_40_34.png

ひとまずこの状態で次の手順に進みましょう。

4. Rubyをインストールする

世界には色んな言語がありますよね?
日本語、英語、中国語、スペイン語など。
それと同様にプログラミング言語にもたくさんの種類があります。
今回は初心者でも学習しやすい Ruby を使用します。

Ruby を使うには下準備が必要です。
MacにはRubyが標準でインストールされているのでこの手順を飛ばしてください。
Windowsには こちらの記事 を参考にインストールしてください。

5. プログラムを書く

Visual Studio Code のテキストエディタに下記コードをコピペしてください。

puts "Hello World!"

コピペ出来たらファイルを保存しましょう。
ファイル名は sample.rb、保存場所は デスクトップ にしてから保存します。

スクリーンショット 2020-11-23 17.37.22.png

無事保存出来たらターミナルで下記のコマンドをコピペしてEnterを押してください。
※ Windowsユーザーの方は別のコマンドになると思います。。。

cd Desktop

スクリーンショット 2020-11-23 17.51.56.png

6. プログラムを実行する

ではターミナルで下記コマンドをコピペしてEnterを押してください。

ruby sample.rb

するとターミナルに Hello World! と表示されたら成功です!
あなたはプログラミングの道に一歩踏み出したのです。

これが一体なんの役に立つのか?
それはこれから分かります。

次章:Rubyで簡単なプログラムを書いてみる(今後記事作成予定)

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

GitHub Actions を使った Rails アプリの自動デプロイを10分で理解する

こんにちは、駆け出しエンジニアのよしこ @k2_yoshikoukiです

CI/CDってカッコいいですよね。なんかこう、カッコいいんですよね(語彙力)

しかし「どうやって実装したらいいのかワケワカメ\(^o^)/」というエンジニアの方は多いと思います。でも実際はとても簡単なんだよということを知ってほしかったので、10分で読める記事で CI/CD のうちのCD(自動デプロイ)を実装していきたいと思います。

実作業時間は詰まらなかったら30分かからないくらいです。

ゴール

CI/CD をやってみたいがやったことがないし何から手を付けたら良いか分からないエンジニア向けに、Rails アプリ (6系) で自動デプロイを最速で実装する。

結果、GitHub Actions を使ったCI/CDの大枠を把握できて自力で実装できるようになる

実装フローを見ていく

  1. Rails アプリを作る
  2. Heroku に手動デプロイして動作確認
  3. GitHub Actions の初期設定
    • ./.github/workflows/{好きな名前}.yml の作成
    • 必要な鍵などの取得・GitHubへ設定

すでにアプリケーションがある方は 3 からです。たった2ステップで自動デプロイが実装できます。早速やっていきましょう

手順

Railsアプリの準備

  • rails new sample-rails-with-gha

    ~/tmp/sample-rails-with-gha
    ❯ rails -v
    Rails 6.0.3.4
    
    ~/tmp/sample-rails-with-gha
    ❯ ruby -v
    ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin19]
    # 後から2.7.2にアップデートします(デバックのため)
    
    bundle install
    rails server
    

    image.png

リポジトリの準備

  • GitHub上にリポジトリを作る
    https://github.com/new
  • ローカルにリポジトリを登録

    git remote add origin https://github.com/yoshikouki/sample-rails-with-gha.git
    

    image.png

    • アップロード

      git add -A
      git commit -m "rails new"
      git push -u origin master
      

デプロイの初期設定と初回デプロイ

  • 今回はデプロイ先に Heroku を使う(簡単なので)

    ~/tmp/sample-rails-with-gha master
    ❯ heroku --version
    
    zsh: command not found: heroku
    
  • Heroku 入ってなかった・・・

    brew tap heroku/brew && brew install heroku
    # https://devcenter.heroku.com/articles/heroku-cli
    
  • Heroku アカウントを作っておく
    https://signup.heroku.com/

  • Heroku にログイン(ターミナル上)

    heroku login --interactive
    
  • Heroku 用のアプリケーションを作る(アップロードする場所を作る)

    heroku create
    # URL を控えておく
    # https://sleepy-beyond-32826.herokuapp.com/
    # `$ heroku open` でも開ける
    

    image.png

  • Heroku は sqlite3 に対応していないため、./Gemfile を操作する

    image.png

  • Heroku へデプロイする

    git push heroku master
    
  • あらら

    image.png

    • エラーログ
    $ heroku logs
    2020-11-22T14:39:05.610528+00:00 app[web.1]: I, [2020-11-22T14:39:05.610419 #4]  INFO -- : [a6b71e5b-f13a-4224-a857-9b791da3ecc4] Started GET "/" for 133.32.232.41 at 2020-11-22 14:39:05 +0000
    2020-11-22T14:39:05.611222+00:00 app[web.1]: F, [2020-11-22T14:39:05.611152 #4] FATAL -- : [a6b71e5b-f13a-4224-a857-9b791da3ecc4]
    2020-11-22T14:39:05.611223+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] ActionController::RoutingError (No route matches [GET] "/"):
    2020-11-22T14:39:05.611224+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4]
    2020-11-22T14:39:05.611225+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
    2020-11-22T14:39:05.611225+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
    2020-11-22T14:39:05.611226+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/rack/logger.rb:37:in `call_app'
    2020-11-22T14:39:05.611226+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/rack/logger.rb:26:in `block in call'
    2020-11-22T14:39:05.611226+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/tagged_logging.rb:80:in `block in tagged'
    2020-11-22T14:39:05.611227+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/tagged_logging.rb:28:in `tagged'
    2020-11-22T14:39:05.611227+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/tagged_logging.rb:80:in `tagged'
    2020-11-22T14:39:05.611228+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/rack/logger.rb:26:in `call'
    2020-11-22T14:39:05.611228+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
    2020-11-22T14:39:05.611228+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/request_id.rb:27:in `call'
    2020-11-22T14:39:05.611229+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] rack (2.2.3) lib/rack/method_override.rb:24:in `call'
    2020-11-22T14:39:05.611229+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] rack (2.2.3) lib/rack/runtime.rb:22:in `call'
    2020-11-22T14:39:05.611230+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] activesupport (6.0.3.4) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
    2020-11-22T14:39:05.611230+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/executor.rb:14:in `call'
    2020-11-22T14:39:05.611231+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/static.rb:126:in `call'
    2020-11-22T14:39:05.611231+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] rack (2.2.3) lib/rack/sendfile.rb:110:in `call'
    2020-11-22T14:39:05.611232+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] actionpack (6.0.3.4) lib/action_dispatch/middleware/host_authorization.rb:76:in `call'
    2020-11-22T14:39:05.611232+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] railties (6.0.3.4) lib/rails/engine.rb:527:in `call'
    2020-11-22T14:39:05.611232+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/configuration.rb:228:in `call'
    2020-11-22T14:39:05.611233+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/server.rb:713:in `handle_request'
    2020-11-22T14:39:05.611233+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/server.rb:472:in `process_client'
    2020-11-22T14:39:05.611233+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/server.rb:328:in `block in run'
    2020-11-22T14:39:05.611234+00:00 app[web.1]: [a6b71e5b-f13a-4224-a857-9b791da3ecc4] puma (4.3.6) lib/puma/thread_pool.rb:134:in `block in spawn_thread'
    2020-11-22T14:39:05.612663+00:00 heroku[router]: at=info method=GET path="/" host=sleepy-beyond-32826.herokuapp.com request_id=a6b71e5b-f13a-4224-a857-9b791da3ecc4 fwd="133.32.232.41" dyno=web.1 connect=1ms service=4ms status=404 bytes=1902 protocol=https
    2020-11-22T14:39:05.858618+00:00 heroku[router]: at=info method=GET path="/favicon.ico" host=sleepy-beyond-32826.herokuapp.com request_id=831fe7f9-bc01-496d-a9ac-380c9c89dd3c fwd="133.32.232.41" dyno=web.1 connect=1ms service=2ms status=200 bytes=143 protocol=https
    
    • なるほどねえ(分かっていない) ActionController::RoutingError (No route matches [GET] "/"):
        # ./config/routes
        Rails.application.routes.draw do
          # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
        end
    
    • ないものは作ればいいんだよ(ドヤ顔)

      rails generate controller Home index
      
    • ルーティング ./config/routes もイジる

      image.png

    • よしよし(手動デプロイで動作を確認)

      image.png

GitHub Actions を設定していく

  • 空の設定ファイルを作る

    mkdir -p ./.github/workflows
    tocuh -p ./.github/workflows/sample_cd.yml
    
  • 設定していく

    • 設定ファイル

      name: Deploy to sample-rails-with-gha
      
      on:
        # master ブランチにプッシュされた場合に動作する
        push:
          branches:
            - master
      
      jobs:
        deploy:
          runs-on: ubuntu-latest
          steps:
            # GitHub リポジトリからコードを引っ張ってくる
            - name: Checkout
              uses: actions/checkout@v2
            # 前回のコンテナキャッシュがあれば使用する。なければキャッシュを作る
            - name: Cache multiple paths
              uses: actions/cache@v2
              with:
                path: vendor/bundle
                key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
                restore-keys: |
                  ${{ runner.os }}-gems-
            # Ruby をインストールする
            - name: Set up Ruby 2.7
              uses: actions/setup-ruby@v1
              with:
                ruby-version: 2.7
            # バンドラーをインストールし、初期化する
            - name: Bundle install
              run: |
                gem install bundler
                bundle config path vendor/bundle
                bundle install --jobs 4 --retry 3
            # ヘロクにデプロイする
            - uses: akhileshns/heroku-deploy@v3.6.8 # This is the action
              with:
                heroku_api_key: ${{secrets.HEROKU_API_KEY}}
                heroku_app_name: "sleepy-beyond-32826"
                heroku_email: "yoshikouki@gmail.com"
      
    • 簡単な設定ファイルなので説明はファイル内のコメントを参考にしてください。

      • アクションという Ruby でいう Gem のような機能を使用しています。
    • ドキュメント

    • secrets.HEROKU_API_KEY が必要なので Heroku で取得する

    • 取得した Key をGitHub で使用できるように設定する

      • sample-rails-with-gha のリポジトリ > Settings > Secrets
      • GitHub Actions で使用する非公開環境変数の設定画面になる > New repository secret
      • Name に HEROKU_API_KEY Value に先程Herokuで取得したキーをセット > Add secret
  • 準備は整ったので、master/main ブランチにプッシュしてみる

    デバッグ

    • actions/cache のドキュメントは bundle がインストールしてある前提だったので gem install bundler を追加した
    • Gemfile に記述されている ruby-version と GitHub Actions のコンテナ内のRubyを一致させること

      uses: actions/setup-ruby@v1
      with:
        ruby-version: 2.7
      
      • GitHub Actions setup-ruby ではマイナーバージョンまでしか指定できないみたいなので、手元Rubyのメンテナンス (ビルド) バージョンをあげることになるかもしれない
      • マイナーバージョンが上がるわけではないので、素直に上げておけばいいと思う

そして...

image.png

  • 最後の push を済ませれば・・・

    image.png

  • 感極まる?

    image.png

まとめ

以上で Rails アプリ (6系) で自動デプロイまでを最速で実装しました。

以後は、master (main) ブランチへ push するだけで heroku へ自動デプロイされます。これで2〜3コマンド分の実行の手間が省けたことになりますし、あなたはもう完全に CI/CD を習得したエンジニアです。

(実際のところ、今回はCI部分を何も扱っていませんし、デプロイに関してもデータベースを触っていないので中途半端な状態ではあるのですが、最低限は実装できたので良しとしましょう。してください。)

参考URL

https://github.co.jp/features/actions

https://docs.github.com/ja/free-pro-team@latest/actions

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

【Rails】formの検索ボタンを虫眼鏡のアイコンにする

Ruby on RailsでFont-Awesomeを初めて使ったの投稿しました。

Font Awesome HP
https://fontawesome.com/icons?d=gallery

ポイント:使いたいアイコンのUnicodeの前に \u をつける

1 . CDNでfont-awesomeを読み込む

<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

2 . submitの記述の後を下記に変更。

<%= f.submit "\uF002" %>

3 . cssで下記を追加

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

【Rails / FontAwesome】formの検索ボタンを虫眼鏡のアイコンにする

Ruby on RailsでFont-Awesomeを初めて使ったの投稿しました。

Font Awesome HP
https://fontawesome.com/icons?d=gallery

ポイント:使いたいアイコンのUnicodeの前に \u をつける

1 . CDNでfont-awesomeを読み込む

<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

2 . submitの記述の後を下記に変更。

<%= f.submit "\uF002" %>

3 . cssで下記を追加

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

【Rails】form_withの使い方について解説【初心者向け】

はじめに

Rails開発をする中で、form_withを使う機会は非常に多いですが、

  • 結局form_withとは何なのか?
  • form_withにはどんな役割があるのか?
  • form_withはどのように用いたらいいのか?

といった悩みを抱えている方は多いと思います。

本記事はそのような方達に向けて、書いた記事となります。

ぜひ最後までご覧下さい。

使用技術

Ruby on Rails 5.1〜
Ruby 2.6〜

筆者の自己紹介

  • テックキャンプ86期生(2020/9/21~)
  • 執筆時点で学習開始から約9週間経過
  • 約6週間で最終課題(メルカリクローンアプリの作成)を終了

本記事の内容

1.form_withとはそもそも何者なのか?
2.form_withはどのような役割を果たしているのか?
3.form_withの具体的な記述法
4.まとめ
5.最後に
6.参照サイト

1.form_withとはそもそも何者なのか?

form_withは①ヘルパーメソッドの一種で、ビューファイル上で②フォーム入力欄を作成できるメソッドのことを言います。

①ヘルパーメソッド

ヘルパーメソッドとはRails内部で定義されているメソッドのことで、ビューファイルにおける記述をシンプルにするためのものです。

ビューファイルではERBタグ(本記事ではビューファイルにテンプレートエンジンとしてerbを使用)を使えば、直接Rubyの記述を書くことはできますが、DRYの視点から見た場合そのようなコードは綺麗なコードとは言えません。

そういったことを防ぐためにヘルパーメソッドは使用されます。

ヘルパーメソッドを使用しない場合
<%= form action="/posts" method="post" %>
  <%= input type="text" name="content" %>
  <%= input type="submit" value="投稿する" %>
<%= /form %>
ヘルパーメソッドを使用した場合
<%= form_with url: "/posts", method: :post, local: true do |form| %>
  <%= form.text_field :content %>
  <%= form.submit '投稿する' %>
<% end %>



またヘルパーメソッドはform_withの他にも様々なものが存在していて、有名なものでいえばlink_toメソッドが存在します。

※他のヘルパーメソッドが気になる方は下記で詳細を確認してみてください
ActionView::Helpers

②フォーム入力欄を作成できる

またヘルパーメソッドはビューファイル上で用いられ、HTMLの代わりのような役割を果たします。

具体的にはHTMLタグを出現させたり、テキストの加工を行うことが可能です。

form_withの場合は、HTMLにおけるinputタグにあたるものを生成できます。

つまりフォーム入力欄が作成できることになります。

HTMLのフォーム記述
<form action="/posts" method="post">
  <input type="text" name="content">
  <input type="submit" value="投稿する">
</form>
ヘルパーメソッドを用いたフォーム記述
<%= form_with url: "/posts", method: :post, local: true do |form| %>
<%= form.text_field :content %>
<%= form.submit '投稿する' %>
<% end %>

Image from Gyazo

③まとめ

  • ヘルパーメソッドとはRails内部で定義されているメソッドのことで、ビューファイルでの記述をシンプルにするためのもの
  • form_withはヘルパーメソッドの一種である
  • form_withはHTMLでinputタグにあたるものを作成し、フォーム入力欄を作成することができる

2. form_withはどのような役割を果たしているのか?

次にform_withメソッドは実際どのような役割を果たしているのか?について考えていきます。

結論からいうと、form_withメソッドは

ユーザーからサーバー側に情報を送信する

という役割を果たしています。

これだけでは少しわかりづらいので、実際のアプリケーションであるTwitterを例に考えてみましょう

Twitterでform_withがどういう役割を果たしているのか?考えてみる

Twitterには様々な機能がありますが、その中でも今回は投稿機能に着目して考えてみます

Twitterの投稿機能では

①ユーザーが投稿フォームに値を入力し、ボタンをクリックする
②その投稿がサーバー側に送られる
③サーバーに送られた投稿内容が投稿一覧表示画面に反映される

という流れで処理が行われています。

この際にform_withがどのような動きをしているのか?を考えてみましょう。


①ユーザーが投稿フォームに値を入力し、ボタンをクリックする

まず最初に、form_withで作成された投稿フォームにユーザーが値を入力し、送信(投稿)ボタンをクリックします。

作成した投稿フォーム
<%= form_with url: "/posts", method: :post, local: true do |form| %>
<%= form.text_field :content %>
<%= form.submit '投稿する' %>
<% end %>

Image from Gyazo

②その投稿がサーバー側に送られる

次に、送信(投稿)ボタンを押したため、先ほど作成したツイートの内容が送信されます。

ここで大半の人が疑問に思っていることが

「ツイートが送信されるのは感覚的にはわかるが、どこへ送信されているのか?」
「ツイートはどのようにして送信されているのか?」

だと思います。

なので、この送信される過程についてもう少し詳しく見ていきましょう。

この過程は大きく分けると、次の3つに分かれます。

  1. リクエストが送信される
  2. ルーティングによって、「どのコントローラーのどのアクションを行うのか?」が決められる
  3. 2で指定されたコントローラーへツイートの内容が渡される

1. リクエストが送信される

まず送信ボタンをクリックすると、使用しているブラウザからサーバー側にリクエストが送られます。

リクエストに関してわからない人がいれば、「HTTPリクエスト」と検索してみてください。
簡単にいうと、ユーザーからサーバーへの「〜して欲しい!」という要望みたいなものです。

そしてこの時リクエストには パラメーターと呼ばれるものが含まれます

※ パラメーター

パラメーターとはリクエストに含まれてサーバーの外部から渡されるデータのことを指します。

わかりやすく言い換えると、外から入ってくる値のことです。


少し身近な例に例えて考えてみましょう。

例えば、自動販売機を想像してみてください。

自販機は100円を入れたら、「100」と料金表示の欄に表示されます。また1000円札を入れれば、「1000」と表示されます。

この時、自販機の料金表示機能をプログラムと見立てた場合に、自販機に入金したお金というのは自販機側からは外から入ってくるものであるので、パラメータになります。

これがパラメータのイメージです。

そしてパラメータにはいくつか種類があります。

例えば、URLに含まれるパラメータはURLパラメータと呼ばれたりします。これは http://tweets.jp/tweets/1/1ように、URLの末尾に文字列が含まれているものを指します。

2. ルーティングによって、「どのコントローラーのどのアクションを行うか」が決定される

次に送信されたリクエストは、サーバー側のルーターに遷移します。

このルーターでは受け取ったリクエストを見て、「どのコントローラのなんの処理を行うか」を決定します。

このように、「このリクエストに対してはこのコントローラーのこの処理を行う」みたいなルールのことをルーティングと言います。

ここでは詳細の説明は省きます。

また「コントローラーやアクション」などの詳しい説明に関しても、今回は省かせていただきます。
RailsにおけるMVCの理解は最低限できているものとして本記事は進ませていただきますので、ご注意ください。

3. 2で指定されたコントローラーへツイート内容(パラメーター)が渡される

最後に、2で指定されたコントローラーへリクエストに含まれるパラメーターが渡されます

ただこの時、パラメーターには様々な情報があるため、そのまま渡されると非常にわかりづらくなってしまいます。

そこで、パラメータはparamsと呼ばれるものに一度格納されてからコントローラーへ渡されます。

※ params

paramsとはハッシュのような構造を取るパラメーターを格納するためのものです。

実際には以下のような形をとっています。

Image from Gyazo

しかし、これだと少しわかりづらい方もいらっしゃると思うので、「paramsはパラメータを格納するための箱のようなもの」と考えると、イメージしやすいかもしれません。

以下のようなイメージです。


1~3をまとめると

フォームに入力された値は、リクエストの中に含まれるパラメータとしてリクエストと共にサーバー側のルーターに送信され、そこでparamsに格納されてサーバー側のコントローラーへ送信される

ということになります。

③サーバーに送られた投稿内容が投稿一覧表示画面に反映される

次にサーバーに送られた投稿内容は、コントローラーによって処理が行われます。

今回だと、投稿一覧画面に投稿内容を反映させるために、送られてきた投稿内容を保存するという処理が行われます。

具体的には

tweets_controller.rb
  def create
    Tweet.create(tweet_params)
  end

のような処理が行われ、投稿一覧画面に反映されます。


Twitterの投稿機能の場合は、このように処理が行われます。

ではこの時、form_withはどのような役割を果たしていたのか?考えてみましょう。

form_withは、上の一連の処理の中では①で登場しました。

つまり、サーバーに投稿内容を送信するという役割を果たしていることがここでわかるかと思います。

3. form_withの具体的な記述法

form_withの表記の仕方は大きく2つに分かれます。

①基本形

基本形
<%= form_with url: "パス" do |form| %>
  フォーム内容
<% end %>

urlにはリクエスト先のパス(URLと考えてもらって大丈夫です)が入ります。

そして、この基本形の場合はオプションをつけることが可能です。

※オプションについて

オプションとはメソッドに補足的に付けられるもので、メソッドの情報を補足する働きがあります。

基本形の場合は以下のようなオプションをつけることが可能です。

  • methodオプション
    form_withで送信されるHTTPリクエストのデフォルトはPOSTであり、methodオプションはそのHTTPリクエストを変更したい場合に用います

  • localオプション
    form_withから送られたリクエストはデフォルトではHTTP通信が行われていないため、それをHTTP通信にする場合localオプションを用います

オプションを付けた場合
<%= form_with url: "/posts", method: :post, local: true do |form| %>
 フォーム内容
<% end %>

②応用形

①のようにurlにリクエスト先を指定したり、methodオプションでHTTPメソッドを指定したりするのが面倒な場合はそれらを省略して、以下のように記述することができます

応用形
<%= form_with model: モデルクラスのインスタンス do |form| %>
  フォーム内容
<% end %>

そして、この応用形には他にも様々なメリットがあるためform_withを用いる場合はこちらがよく使われる傾向にあります。

4. まとめ

  • form_withはヘルパーメソッドの一種で、ビューファイル上でフォーム入力欄を作成できるメソッドのことを言います。
  • form_withメソッドはユーザーからサーバー側に情報を送信するという役割を果たしている
  • form_withの表記は大きく分けると、urlオプションとmomdelオプションを使う場合の2種類が存在する

5. 最後に

本記事の内容がみなさんの参考になれば嬉しいです。

最後までご覧いただきありがとうございました。

6. 参照サイト

https://qiita.com/snskOgata/items/44d32a06045e6a52d11c
https://qiita.com/tomoharutt/items/46e6358acc8b45cd7db1
https://qiita.com/hayulu/items/5bf26656d7433d406ede

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

JavaScript基礎まとめ

JavaScriptの基礎を自分の備忘録&アウトプットも兼ねて記述。

そもそも、、JavaScriptはWEBページ内でページ遷移や更新をせずに画面を表示できる。
つまり、読み込む時間がないのでユーザーはストレスなく使えるということ。

検証ツール(ディベロッパーツール)で確認ができる(Rubyでいうirb的なやつ)
右クリック+検証、または⌘ + option + Cで出せる

1.変数定義
var(ほとんど使わないらしいから省略)
const
let の3種類

・const 再代入、再定義NG

const sample = "Hello"
sample = "Hello" 
再代入NG
const sample ="こんにちは"
再定義もNG

・let

const sample = "Hello"
sample = "Hello" 
再代入OK
const sample ="こんにちは"
再定義はNG

2.テンプレートリテラル
文字の中に変数を入れる

rubyだと 

name = "yamada"
puts #{name}さん

だったのが

const name = "yamada"
console.log(`${name}さん`)

この``で囲うのがテンプレートリテラル

3.条件分岐

if (条件式1) {
  // 条件式1がtrueのときの処理
} else if (条件式2) {
  // 条件式1がfalseで条件式2がtrueのときの処理
} else {
  // 条件式1も条件式2もfalseのときの処理
}

rubyとの違いは
条件式を()で囲ったり、条件式の後の処理は{}で囲うこと、
elsif ではなく else ifぐらい、、?

4.繰り返し(for文)

for ([初期化式]; [条件式]; [加算式]) {
  // 繰り返す処理の内容
}

①初期化式
変数の定義をする
②条件式 
処理内容を何回繰り返すか
③加算式
①で定義をした変数の増減を書く

let name = ['A','B','C','D','E']
for (let i = 0; i <= name.length; i ++) {
  console.log(name[i])
}

①②③で考えると
①変数 i = 0
②配列の中から i 番目を取り出す(現時点ではi=0としている)
このi番目の長さがnameの配列の個数以上になるまで繰り返す
③繰り返す度にiに1ずつ加えていく

、、、もし間違えている点があればコメントいただきたいです!よろしくお願いいたします。

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

【Rails6】Active Job + Sidekiqを動かしてみた

Railsの非同期処理をActive Job + Sidekiqで実装したのでメモを残します。

※RailsアプリケーションはDocker環境で構築済みの前提です。環境構築はこちら

※Active Jobとバックエンドの比較はこちら

環境

  • Ruby 2.7.2
  • Rails 6.0.3.4
  • MySQL 8.0.20
  • Redis 6.0.9
  • Sidekiq 6.1.2
  • Docker version 19.03.13

1. Redisの導入

まずredisコンテナを用意します。
ポート番号はdocker-compose.override.ymlで指定していますが、下記で設定して問題ないと思います。

docker-compose.yml
version: '3.7'

services:
  db:
    image: mysql:8.0.20
    volumes:
      - mysql:/var/lib/mysql:delegated
    command: --default-authentication-plugin=mysql_native_password
    env_file: .env

  web:
    build:
      context: .
      dockerfile: Dockerfile
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    tty: true
    stdin_open: true
    env_file: .env
    depends_on:
      - db
      - chrome
      - redis
    volumes:
      - .:/app:cached
      - bundle:/usr/local/bundle:delegated
      - node_modules:/app/node_modules

  chrome:
    image: selenium/standalone-chrome:3.141.59
    volumes:
      - /dev/shm:/dev/shm

  redis:
    image: redis:6.0.9
    env_file: .env
    command: redis-server --appendonly yes
    volumes:
      - redis:/data

volumes:
  mysql:
  bundle:
  node_modules:
  redis:
docker-compose.override.yml
version: '3.7'

services:
  db:
    ports:
      - 3306:3306
  web:
    ports:
      - 3000:3000
  chrome:
    ports:
      - 4444:4444
  redis:
    ports:
      - 6379:6379

Redisの設定ファイルを追加します。
host名に注意してください。

config/redis.yml
default: &default
  db:
    sidekiq: 0
    # cache:   1
    # session: 2

development:
  <<: *default
  host: redis

test:
  <<: *default
  host: redis

2. Sidekiqの設定

まずはGemを追加します。
ruby:Gemfile
gem 'sidekiq'

bundle installを実行し、設定ファイルを追加します。
詳しくは、こちらを参照してください。

config/sidekiq.yml
:verbose: false
:max_retries: 1
:concurrency: 10
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
:queues:
  - development_default

続いてSidekiqとRedisの接続情報も追加します。

config/initializers/sidekiq.rb
redis_config = YAML.load_file('config/redis.yml')[Rails.env]
redis_config['db'] = redis_config['db']['sidekiq']

Sidekiq.configure_server do |config|
  config.redis = {
    url: "redis://#{redis_config['host']}/#{redis_config['db']}"
  }
end

Sidekiq.configure_client do |config|
  config.redis = {
    url: "redis://#{redis_config['host']}/#{redis_config['db']}"
  }
end

ダッシュボードのルーティングも設定します。

config/routes.rb
require 'sidekiq/web'

Rails.application.routes.draw do
  mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
end

3. Active Jobの設定

Active Jobで非同期処理を実装していきます。

config/application.rb
require "active_job/railtie"

module App
  class Application < Rails::Application
    config.active_job.queue_adapter = :sidekiq
    config.active_job.queue_name_prefix = Rails.env # これは任意
  end
end

4. ジョブの作成と動作確認

ようやくジョブを作成します。

rails g job sample
app/jobs/sample_job.rb
class SampleJob < ApplicationJob
  queue_as :default

  def perform
    puts '--------------------------------'
    puts '------------  Test  ------------'
    puts '--------------------------------'
  end
end

続いて動作確認を行います。
dockerコンテナを起動し、RailsとRedisが動いていることを確認します。
コンテナ内でsidekiqを起動します。

$ bundle exec sidekiq -C config/sidekiq.yml

2020-11-23T07:06:57.513Z pid=74 tid=9om INFO: Booting Sidekiq 6.1.2 with redis options {:url=>"redis://redis/0"}


               m,
               `$b
          .ss,  $$:         .,d$
          `$$P,d$P'    .,md$P"'
           ,$$$$$b/md$$$P^'
         .d$$$$$$/$$$P'
         $$^' `"/$$$'       ____  _     _      _    _
         $:     ,$$:       / ___|(_) __| | ___| | _(_) __ _
         `b     :$$        \___ \| |/ _` |/ _ \ |/ / |/ _` |
                $$:         ___) | | (_| |  __/   <| | (_| |
                $$         |____/|_|\__,_|\___|_|\_\_|\__, |
              .d$$                                       |_|


2020-11-23T07:06:57.938Z pid=74 tid=9om INFO: Booted Rails 6.0.3.4 application in development environment
2020-11-23T07:06:57.939Z pid=74 tid=9om INFO: Running in ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]
2020-11-23T07:06:57.939Z pid=74 tid=9om INFO: See LICENSE and the LGPL-3.0 for licensing details.
2020-11-23T07:06:57.939Z pid=74 tid=9om INFO: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org
2020-11-23T07:06:57.946Z pid=74 tid=9om INFO: Starting processing, hit Ctrl-C to stop

別タブのターミナルでRailsコンソールからSampleJobをキューイングしてみます。

$ rails c
Loading development environment (Rails 6.0.3.4)
> SampleJob.set(wait: 5.second).perform_later

Enqueued SampleJob (Job ID: fdcf5c60-3542-4b26-bfd2-9662ffafada9) to Sidekiq(development_default) at 2020-11-23 07:08:49 UTC
=> #<SampleJob:0x00005567e535e4c0
 @arguments=[],
 @exception_executions={},
 @executions=0,
 @job_id="fdcf5c60-3542-4b26-bfd2-9662ffafada9",
 @priority=nil,
 @provider_job_id="7c42f602bd499e75efecad26",
 @queue_name="development_default",
 @scheduled_at=1606115329.3860393>
>

キューイングすると先ほど起動したSidekiq側でジョブが実行されたことが確認できました。

2020-11-23T07:08:52.800Z pid=74 tid=b6y class=SampleJob jid=7c42f602bd499e75efecad26 INFO: start
--------------------------------
------------  Test  ------------
--------------------------------
2020-11-23T07:08:53.067Z pid=74 tid=b6y class=SampleJob jid=7c42f602bd499e75efecad26 elapsed=0.267 INFO: done
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【自分メモ】モデル/コントローラー/ビューの役割

モデル…コントローラーで定義したインスタンス変数にデータを入れるため、コントローラーからモデルへデータを取ってくるよう指示が出る(モデル名.all)→指示を受けたモデルはデータベースからデータを取得後、コントローラーのインスタンス変数へ入れる。
またコントローラーから指示を受けて新しいオブジェクトを作成する(モデル名.new)。

コントローラー…インスタンス変数に入れるデータをモデルに持ってくるよう指示を出したり、そのインスタンス変数をビューに渡したりする。

ビュー…コントローラーから受け取ったインスタンス変数を元にhtmlファイルを作成する。

※自分の考えをメモとして残していますので、間違っていたり修正した方が良い点がございましたら、ドシドシつっこみいただけると嬉しいです!

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

[Ruby]ハマりを最小限に抑えるseleniumの導入方法

はじめに

seleniumは、Webアプリケーションのテストを自動化するためのツールであり、プログラムからWebブラウザの自動操作を行うことが出来ます。
自分が導入した際につまづいた点を振り返りながら、初心者なりに考え、仮にエラーが出たとしても調査しやすいような手順にまとめましたので、これから導入される方のお役に立てればと思います。(フォルダ構成が変わるだけで、WinもMacも作業は同じです)

selenium導入手順 目次

0_webスクレイピングで気をつけること
1_確認作業用フォルダの作成
2_chromedriverのダウンロード
3_seleniumのインストール
4_中間動作確認
5_chromedriverの移動
6_最終動作確認

0_webスクレイピングで気をつけること

seleniumを使ってWebアプリケーションの自動テストの他に、webスクレイピングも出来ます。スクレイピングの際、やって良いこと/悪いことがありますので、これから始めるという方は下記の記事を読まれることをおすすめします。

- Webスクレイピングの注意事項一覧
- WEBスクレイピングの技術と懸念事項

1_確認作業用フォルダの作成

確認作業のためにデスクトップにフォルダを作り、中にhtmlファイルとRubyファイルを作成します。
コンソールで下記を実行
mkdir /Users/ユーザーネーム/Desktop/sele_work
cd /Users/ユーザーネーム/Desktop/sele_work
touch sample.html
touch selenium_test.rb

ファイルを編集

sample.html
<!DOCTYPE html>
<html>
    <head>
        <title>テスト用</title>
    </head>
    <body>
        <h1>ハンバーガーショップ</h1>
        <div>
            メニュー:
            <select name="menu">
            <option value="">メニューを選択してください</option>
            <option value="normal">ハンバーガー</option>
            <option value="cheese">チーズバーガー</option>
            <option value="double">ダブルバーガー</option>
            </select>
            <br>
            サブメニュー:
            <input type=checkbox name="submenu" value="drink">飲み物
            <input type=checkbox name="submenu" value="potato">ポテト
            <input type=checkbox name="submenu" value="nugget">ナゲット
            <br>
            お持ち帰り:
            <input type=radio name="takeout" value="yes">yes
            <input type=radio name="takeout" value="no">no
            <br>
            メモ:
            <input type=text name="memo">
        </div>
        <input type=submit value="OK">
        <input type=reset value="キャンセル">
    </body>
</html>
selenium_test.rb
require "selenium-webdriver"

url = "file:///Users/ユーザーネーム/Desktop/sele_work/sample.html"

# chromeドライバは作業フォルダに仮置中なので作業フォルダを指定する
chromedriver_path = "/Users/ユーザーネーム/Desktop/sele_work/chromedriver"
Selenium::WebDriver::Chrome::Service.driver_path = chromedriver_path
driver = Selenium::WebDriver.for :chrome

# urlへアクセス
driver.get(url)

p driver.page_source

driver.quit

2_chromedriverのダウンロード

Google Chromeのバージョンを確認してから、自分のchromeのバージョンに合ったドライバをこちらからダウンロードし、解凍したchromedriver/Users/ユーザーネーム/Desktop/sele_workに置きます。

【Google Chromeのバージョンチェック】
01_chormeバージョンチェック.png

3_seleniumのインストール

コンソールで下記を実行
gem install selenium-webdriver

4_中間動作確認

ここで一旦、ruby selenium_test.rbを実行し、sample.htmlのsourceが取得できていることを確認します。セキュリティーのエラーが出たら、左上のりんごマーク > システム環境設定 > セキュリティーとプライバシーに進み許可を選択します。

セキュリティーブロックを解除してもエラーが発生する場合は、以下が考えられます。
- Google Chromeとchromedriverのバージョンが合っていない
- 誤字やファイル指定のミスが有る

【セキュリティーエラーの対処】
02_セキュリティー解除.png
【ソース取得成功】
03_実行結果1.png

5_chromedriverの移動

chromedriverの動作確認が出来たので、本来置くべき場所の/usr/local/binへ移動します。
コンソールで下記を実行し、ちゃんと移動できたかも確認。
sudo mv chromedriver /usr/local/bin/
which chromedriver
PATHが通ってるか確認
echo $PATH
もし通っていないならPATHを追加
export PATH=“/usr/local/bin:$PATH”

【chromedriverの移動、PATHの確認】
05_ドライバを移動.png

6_最終動作確認

最後に、selenium_test.rbを修正し、再度ruby selenium_test.rbを実行する。
中間動作確認と同じ結果が得られれば任務完了。
ここでエラーが出る場合は、PATHが通っていないことが考えられます。

selenium_test.rb
require "selenium-webdriver"

url = "file:///Users/ユーザーネーム/Desktop/sele_work/sample.html"

# chromeドライバは作業フォルダに仮置中なので作業フォルダを指定する

# コメントアウト ここから
# chromedriver_path = "/Users/ユーザーネーム/Desktop/sele_work/chromedriver"
# Selenium::WebDriver::Chrome::Service.driver_path = chromedriver_path
# ここまで コメントアウト

driver = Selenium::WebDriver.for :chrome

# urlへアクセス
driver.get(url)

p driver.page_source

driver.quit

【最終動作確認結果】
06_実行結果2.png

【おまけ pythonからも動作確認】
07_おまけ.png

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

Enumerator::Lazy でエラトステネスの篩

Enumerator::Lazy は無限数列を扱うことができるが、それを使って「エラトステネスの篩」をちょっとおもしろく実装できることに気づいた。何はともあれコードである。

lazy_prime.rb
prime_seq = Enumerator.new do |y|
  sieve = 2.step.lazy
  loop do
    a = sieve.first
    y << a
    sieve = sieve.reject {|x| (x % a).zero?}
  end
end

prime_seq.take(10)    #=>[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

 
Enumerator::Lazy はsieve = 2.step.lazyのところで生成している。これの先頭aは必ず素数なので、素数列として Enumerator で取り出す(y << a)。そして、sieveからaの倍数をrejectですべて篩い落とし(Lazy だから可能なのである)、そうされたものを新しいsieveとする。以下、繰り返し、というわけである。

これは子供の頃に紙と鉛筆でやってみた「エラトステネスの篩」のやり方そのものである。小さい方から素数に丸でも打っておいて、その倍数を斜線で消していく…。実装としては、素直といえば素直だ。

しかし、これ、めちゃめちゃに遅いのはすぐわかる。素数ひとつ得るたびに、Enumerator::Lazy オブジェクトを作り直しているのだから、どれほど遅いかというと、例えば最初の 500個の素数を得るのに、この方法だとわたしの環境でなんと 2.8秒ほどもかかる。標準添付ライブラリの prime を使うと 0.0002秒ほどだから、比較にもならない。

でもまあ、ちょっとおもしろかったので書いてみた。

なお、単なる Enumerator ももちろん無限数列を扱うことができるが、mapselect、それにここで使ったrejectなどの結果が有限数列(Array)になってしまう。なので、Enumerator::Lazy を使ったのである。為念。

以上はブログ記事の転載です。

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

自分のメモrubyhttp/httpsリクエスト

いつもhttpリクエストでどういう実装するのか悩むのでコピペですぐ使えるようにする。
faradayを経験してみる

/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/net/protocol.rb:44:in `connect_nonblock': SSL_connect returned=1 errno=0 state=SSLv3 read server hello A: sslv3 alert handshake failure (OpenSSL::SSL::SSLError)

ruby version

$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]

rubyのマニュアルはどこにあるの?

ruby マニュアルで検索
https://docs.ruby-lang.org/ja/
https://docs.ruby-lang.org/ja/2.6.0/doc/index.html

httpはネットワークのところにあった。
https://docs.ruby-lang.org/ja/2.6.0/library/index.html
https://docs.ruby-lang.org/ja/2.6.0/library/net=2fhttp.html

マニュアルに沿って試してみる

例1: GET して 表示するだけ。httpでアクセスしていると思う

$ cat test.rb
require 'net/http'
Net::HTTP.get_print 'jsonplaceholder.typicode.com', '/todos/1'

$ ruby test.rb
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false

例2: URI を使う

$ cat test.rb
require 'net/http'
require 'uri'
Net::HTTP.get_print URI.parse('http://jsonplaceholder.typicode.com/todos/1') # httpsにすると動かなかった

$ ruby test.rb
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false

例3: より汎用的な例

require 'net/http'
require 'uri'

url = URI.parse('http://jsonplaceholder.typicode.com')
res = Net::HTTP.start(url.host, url.port) {|http|
  http.get('/todos/1')
}
puts res.body

$ ruby test.rb
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

例4: 上の例よりさらに汎用的な例

require 'net/http'

url = URI.parse('http://www.example.com/index.html')
req = Net::HTTP::Get.new(url.path)
res = Net::HTTP.start(url.host, url.port) {|http|
  http.request(req)
}
puts res.body

結果は省略、動いた

リクエストヘッダを指定する方法、portも変える

https://qiita.com/takano-h/items/dd10818eb7e09161bc29
https://qiita.com/mogulla3/items/a4bff2e569dfa7da1896

require "net/http"

uri = URI.parse("http://jsonplaceholder.typicode.com/todos/1")
http = Net::HTTP.new(uri.host, uri.port) # port指定したければuri.portをport番号に置き換える
# puts uri.port # httpなら80、httpsなら443がデフォルト値になる
http.use_ssl = uri.scheme === "https" # uri.schemeがhttp or httpsだと思われ。一致したらtrueをhttp.use_sslを代入する。一致しなければfalseを代入。trueだとsslを使用するという設定になる

headers = { "Content-Type" => "application/json" } # ヘッダーの設定
req = Net::HTTP::Get.new(uri.path)
req.initialize_http_header(headers)

response = http.request(req) # リクエストを投げる

puts response.code # status code
puts response.body # response body

最終形、リクエストを投げて、JSONオブジェクトをHashへパースする

https://qiita.com/mogulla3/items/a4bff2e569dfa7da1896

require 'net/http'
require 'uri'
require 'json'
require 'logger'

# [ロガー]
# カレントディレクトリのwebapi.logというファイルに出力
# (DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN)
logger = Logger.new('./webapi.log')
logger.info("start")

# [クエリパラメータ]
# URI.encode_www_formを使って「application/x-www-form-urlencoded」形式の文字列に変換
# 文字列はURLエンコードされた形式に変換(半角スペースの"+"への変換等)
#
# (変換例)
# 'bar baz' => 'bar+baz'
# 'あ' => '%E3%81%82'
# params = URI.encode_www_form({ param1: 'foo', param2: 'bar baz' , param3: 'あ' })

# [URI]
# URI.parseは与えられたURIからURI::Genericのサブクラスのインスタンスを返す
# -> 今回はHTTPプロトコルなのでURI::HTTPクラスのインスタンスが返される
#
# オブジェクトからは以下のようにして構成要素を取得できる
# uri.scheme => 'http'
# uri.host   => 'mogulla3.com'
# uri.port   => 4567
# uri.path   => ''
# uri.query  => 'param1=foo&param2=bar+baz&param3=%E3%81%82'

# uri = URI.parse("http://mogulla3.com:4567?#{params}")
uri = URI.parse("http://jsonplaceholder.typicode.com/todos/1")

begin
  # [GETリクエスト]
  # Net::HTTP.startでHTTPセッションを開始する
  # 既にセッションが開始している場合はIOErrorが発生
  response = Net::HTTP.start(uri.host, uri.port) do |http|
    # Net::HTTP.open_timeout=で接続時に待つ最大秒数の設定をする
    # タイムアウト時はTimeoutError例外が発生
    http.open_timeout = 5

    # Net::HTTP.read_timeout=で読み込み1回でブロックして良い最大秒数の設定をする
    # デフォルトは60秒
    # タイムアウト時はTimeoutError例外が発生
    http.read_timeout = 10

    # Net::HTTP#getでレスポンスの取得
    # 返り値はNet::HTTPResponseのインスタンス
    http.get(uri.request_uri)
  end

  # [レスポンス処理]
  # 2xx系以外は失敗として終了することにする
  # ※ リダイレクト対応できると良いな..
  #
  # ステータスコードに応じてレスポンスのクラスが異なる
  # 1xx系 => Net::HTTPInformation
  # 2xx系 => Net::HTTPSuccess
  # 3xx系 => Net::HTTPRedirection
  # 4xx系 => Net::HTTPClientError
  # 5xx系 => Net::HTTPServerError
  case response
  # 2xx系
  when Net::HTTPSuccess
    # [JSONパース処理]
    # JSONオブジェクトをHashへパースする
    # JSON::ParserErrorが発生する可能性がある
    # {"userId"=>1, "id"=>1, "title"=>"delectus aut autem", "completed"=>false}
    # p JSON.parse(response.body)
    result = JSON.parse(response.body)
    p result
    p result["userId"]
  # 3xx系
  when Net::HTTPRedirection
    # リダイレクト先のレスポンスを取得する際は
    # response['Location']でリダイレクト先のURLを取得してリトライする必要がある
    logger.warn("Redirection: code=#{response.code} message=#{response.message}")
  else
    logger.error("HTTP ERROR: code=#{response.code} message=#{response.message}")
  end

# [エラーハンドリング]
# 各種処理で発生しうるエラーのハンドリング処理
# 各エラーごとにハンドリング処理が書けるようにrescue節は小さい単位で書く
# (ここでは全て同じ処理しか書いていない)
rescue IOError => e
  logger.error(e.message)
rescue TimeoutError => e
  logger.error(e.message)
rescue JSON::ParserError => e
  logger.error(e.message)
rescue => e
  logger.error(e.message)
ensure
  logger.info("end")
end
----------------
$ ruby test.rb
{"userId"=>1, "id"=>1, "title"=>"delectus aut autem", "completed"=>false}
1

httpsでGETする。postが必要になったらまた調査しよう

https://qiita.com/katu_/items/a116c261c8616fd01fca

require 'net/https'
require 'uri'
require 'json'
require 'logger'

# [ロガー]
# カレントディレクトリのwebapi.logというファイルに出力
# (DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN)
logger = Logger.new('./webapi.log')
logger.info("start")

# [クエリパラメータ]
# URI.encode_www_formを使って「application/x-www-form-urlencoded」形式の文字列に変換
# 文字列はURLエンコードされた形式に変換(半角スペースの"+"への変換等)
#
# (変換例)
# 'bar baz' => 'bar+baz'
# 'あ' => '%E3%81%82'
# params = URI.encode_www_form({ param1: 'foo', param2: 'bar baz' , param3: 'あ' })

# [URI]
# URI.parseは与えられたURIからURI::Genericのサブクラスのインスタンスを返す
# -> 今回はHTTPプロトコルなのでURI::HTTPクラスのインスタンスが返される
#
# オブジェクトからは以下のようにして構成要素を取得できる
# uri.scheme => 'http'
# uri.host   => 'mogulla3.com'
# uri.port   => 4567
# uri.path   => ''AA
# uri.query  => 'param1=foo&param2=bar+baz&param3=%E3%81%82'
uri = URI.parse("https://jsonplaceholder.typicode.com/todos/1")

begin
  # [GETリクエスト]
  # Net::HTTP.startでHTTPセッションを開始する
  # 既にセッションが開始している場合はIOErrorが発生
  # httpのようにブロックを使ったら動かなかった
  # HTTP.startは使えない。startを使うと、すぐにsessionを貼るので、https.use_ssl=falseができない
  https = Net::HTTP.new(uri.host, uri.port)
  https.use_ssl = uri.scheme === "https"
  https.verify_mode = OpenSSL::SSL::VERIFY_NONE # テストのときはこれでよい。社内テストでエラーのときは対応が必要。ssl証明書を取得してあげる?
  # Net::HTTP.open_timeout=で接続時に待つ最大秒数の設定をする
  # タイムアウト時はTimeoutError例外が発生
  https.open_timeout = 5

  # Net::HTTP.read_timeout=で読み込み1回でブロックして良い最大秒数の設定をする
  # デフォルトは60秒
  # タイムアウト時はTimeoutError例外が発生
  https.read_timeout = 10

  req = Net::HTTP::Get.new(uri.request_uri)
  response = https.request(req)
  # [レスポンス処理]
  # 2xx系以外は失敗として終了することにする
  # ※ リダイレクト対応できると良いな..
  #
  # ステータスコードに応じてレスポンスのクラスが異なる
  # 1xx系 => Net::HTTPInformation
  # 2xx系 => Net::HTTPSuccess
  # 3xx系 => Net::HTTPRedirection
  # 4xx系 => Net::HTTPClientError
  # 5xx系 => Net::HTTPServerError

  case response
  # 2xx系
  when Net::HTTPSuccess
    # [JSONパース処理]
    # JSONオブジェクトをHashへパースする
    # JSON::ParserErrorが発生する可能性がある
    # {"userId"=>1, "id"=>1, "title"=>"delectus aut autem", "completed"=>false}
    # p JSON.parse(response.body)
    result = JSON.parse(response.body)
    p result
    p resul["userId"]
  # 3xx系
  when Net::HTTPRedirection
    puts "baaa"
    # リダイレクト先のレスポンスを取得する際は
    # response['Location']でリダイレクト先のURLを取得してリトライする必要がある
    logger.warn("Redirection: code=#{response.code} message=#{response.message}")
  else
    logger.error("HTTP ERROR: code=#{response.code} message=#{response.message}")
  end

# [エラーハンドリング]
# 各種処理で発生しうるエラーのハンドリング処理
# 各エラーごとにハンドリング処理が書けるようにrescue節は小さい単位で書く
# (ここでは全て同じ処理しか書いていない)
rescue IOError => e
  logger.error(e.class)
  logger.error(e.message)
  logger.error(e.backtrace)
rescue TimeoutError => e
  logger.error(e.class)
  logger.error(e.message)
  logger.error(e.backtrace)
rescue JSON::ParserError => e
  logger.error(e.class)
  logger.error(e.message)
  logger.error(e.backtrace)
rescue => e
  logger.error(e.class)
  logger.error(e.message)
  logger.error(e.backtrace)
ensure
  logger.info("end")
end

今度これをやってみよう

https://qiita.com/toshihirock/items/19fc868d3c4c52411aa9
postがわかる気がする

faraday

https://qiita.com/YumaInaura/items/fb18e3d56dced19cbd28
http://nekorails.hatenablog.com/entry/2018/09/28/152745
使いこなせなかった

require 'faraday'
require 'json'
require 'logger'
# gem install faraday が必要

# シンプル1
# p Faraday.get("https://jsonplaceholder.typicode.com/todos/1", ssl: { verify: false })

# シンプル2
# connection = Faraday.new("https://jsonplaceholder.typicode.com/todos/1", ssl: { verify: false })
# p connection.get

# シンプル3
# connection = Faraday.new(url: "https://jsonplaceholder.typicode.com/todos/1", ssl: { verify: false })
# p connection.get


# シンプル4
connection = Faraday.new(url: "https://jsonplaceholder.typicode.com",ssl: { verify: false })
response =  connection.get "/todos/1" do |request|
  request.headers["Content-Type"] = "application/json"
  # ssl を設定しようとしたがエラーになる
end

#p response
p response.body     # レスポンスbody
result = JSON.parse(response.body)
p result
p result["userId"]
#p response.headers  # レスポンスheader
#p response.status   # ステータスコード
#p response.success? # リクエストは成功か?(ステータスコードが200番台か?)

エラーハンドリング

https://qiita.com/dany1468/items/2d5e18dee84225ede77d

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

自分のメモrubyhttpリクエスト

いつもhttpリクエストでどういう実装するのか悩むのでコピペですぐ使えるようにする。
httpsエラーになったの諦めた。ssl証明書のせいかも。理解できなかった

/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/net/protocol.rb:44:in `connect_nonblock': SSL_connect returned=1 errno=0 state=SSLv3 read server hello A: sslv3 alert handshake failure (OpenSSL::SSL::SSLError)

ruby version

$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]

rubyのマニュアルはどこにあるの?

ruby マニュアルで検索
https://docs.ruby-lang.org/ja/
https://docs.ruby-lang.org/ja/2.6.0/doc/index.html

httpはネットワークのところにあった。
https://docs.ruby-lang.org/ja/2.6.0/library/index.html
https://docs.ruby-lang.org/ja/2.6.0/library/net=2fhttp.html

マニュアルに沿って試してみる

例1: GET して 表示するだけ。httpでアクセスしていると思う

$ cat test.rb
require 'net/http'
Net::HTTP.get_print 'jsonplaceholder.typicode.com', '/todos/1'

$ ruby test.rb
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false

例2: URI を使う

$ cat test.rb
require 'net/http'
require 'uri'
Net::HTTP.get_print URI.parse('http://jsonplaceholder.typicode.com/todos/1') # httpsにすると動かなかった

$ ruby test.rb
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false

例3: より汎用的な例

require 'net/http'
require 'uri'

url = URI.parse('http://jsonplaceholder.typicode.com')
res = Net::HTTP.start(url.host, url.port) {|http|
  http.get('/todos/1')
}
puts res.body

$ ruby test.rb
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

例4: 上の例よりさらに汎用的な例

require 'net/http'

url = URI.parse('http://www.example.com/index.html')
req = Net::HTTP::Get.new(url.path)
res = Net::HTTP.start(url.host, url.port) {|http|
  http.request(req)
}
puts res.body

結果は省略、動いた

リクエストヘッダを指定する方法、portも変える

https://qiita.com/takano-h/items/dd10818eb7e09161bc29
https://qiita.com/mogulla3/items/a4bff2e569dfa7da1896

require "net/http"

uri = URI.parse("http://jsonplaceholder.typicode.com/todos/1")
http = Net::HTTP.new(uri.host, uri.port) # port指定したければuri.portをport番号に置き換える
# puts uri.port # httpなら80、httpsなら443がデフォルト値になる
http.use_ssl = uri.scheme === "https" # uri.schemeがhttp or httpsだと思われ。一致したらtrueをhttp.use_sslを代入する。一致しなければfalseを代入。trueだとsslを使用するという設定になる

headers = { "Content-Type" => "application/json" } # ヘッダーの設定
req = Net::HTTP::Get.new(uri.path)
req.initialize_http_header(headers)

response = http.request(req) # リクエストを投げる

puts response.code # status code
puts response.body # response body

最終形、リクエストを投げて、JSONオブジェクトをHashへパースする

いつかhttpsに対応させる

https://qiita.com/mogulla3/items/a4bff2e569dfa7da1896

require 'net/http'
require 'uri'
require 'json'
require 'logger'

# [ロガー]
# カレントディレクトリのwebapi.logというファイルに出力
# (DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN)
logger = Logger.new('./webapi.log')
logger.info("start")

# [クエリパラメータ]
# URI.encode_www_formを使って「application/x-www-form-urlencoded」形式の文字列に変換
# 文字列はURLエンコードされた形式に変換(半角スペースの"+"への変換等)
#
# (変換例)
# 'bar baz' => 'bar+baz'
# 'あ' => '%E3%81%82'
# params = URI.encode_www_form({ param1: 'foo', param2: 'bar baz' , param3: 'あ' })

# [URI]
# URI.parseは与えられたURIからURI::Genericのサブクラスのインスタンスを返す
# -> 今回はHTTPプロトコルなのでURI::HTTPクラスのインスタンスが返される
#
# オブジェクトからは以下のようにして構成要素を取得できる
# uri.scheme => 'http'
# uri.host   => 'mogulla3.com'
# uri.port   => 4567
# uri.path   => ''
# uri.query  => 'param1=foo&param2=bar+baz&param3=%E3%81%82'

# uri = URI.parse("http://mogulla3.com:4567?#{params}")
uri = URI.parse("http://jsonplaceholder.typicode.com/todos/1")

begin
  # [GETリクエスト]
  # Net::HTTP.startでHTTPセッションを開始する
  # 既にセッションが開始している場合はIOErrorが発生
  response = Net::HTTP.start(uri.host, uri.port) do |http|
    # Net::HTTP.open_timeout=で接続時に待つ最大秒数の設定をする
    # タイムアウト時はTimeoutError例外が発生
    http.open_timeout = 5

    # Net::HTTP.read_timeout=で読み込み1回でブロックして良い最大秒数の設定をする
    # デフォルトは60秒
    # タイムアウト時はTimeoutError例外が発生
    http.read_timeout = 10

    # Net::HTTP#getでレスポンスの取得
    # 返り値はNet::HTTPResponseのインスタンス
    http.get(uri.request_uri)
  end

  # [レスポンス処理]
  # 2xx系以外は失敗として終了することにする
  # ※ リダイレクト対応できると良いな..
  #
  # ステータスコードに応じてレスポンスのクラスが異なる
  # 1xx系 => Net::HTTPInformation
  # 2xx系 => Net::HTTPSuccess
  # 3xx系 => Net::HTTPRedirection
  # 4xx系 => Net::HTTPClientError
  # 5xx系 => Net::HTTPServerError
  case response
  # 2xx系
  when Net::HTTPSuccess
    # [JSONパース処理]
    # JSONオブジェクトをHashへパースする
    # JSON::ParserErrorが発生する可能性がある
    # {"userId"=>1, "id"=>1, "title"=>"delectus aut autem", "completed"=>false}
    # p JSON.parse(response.body)
    result = JSON.parse(response.body)
    p result
    p result["userId"]
  # 3xx系
  when Net::HTTPRedirection
    # リダイレクト先のレスポンスを取得する際は
    # response['Location']でリダイレクト先のURLを取得してリトライする必要がある
    logger.warn("Redirection: code=#{response.code} message=#{response.message}")
  else
    logger.error("HTTP ERROR: code=#{response.code} message=#{response.message}")
  end

# [エラーハンドリング]
# 各種処理で発生しうるエラーのハンドリング処理
# 各エラーごとにハンドリング処理が書けるようにrescue節は小さい単位で書く
# (ここでは全て同じ処理しか書いていない)
rescue IOError => e
  logger.error(e.message)
rescue TimeoutError => e
  logger.error(e.message)
rescue JSON::ParserError => e
  logger.error(e.message)
rescue => e
  logger.error(e.message)
ensure
  logger.info("end")
end
----------------
$ ruby test.rb
{"userId"=>1, "id"=>1, "title"=>"delectus aut autem", "completed"=>false}
1

今度これをやってみよう

https://qiita.com/toshihirock/items/19fc868d3c4c52411aa9
postがわかる気がする

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

Rails + Docker でhello worldを表示するまでを簡単に

アプリ作成時に毎回調べている気がするので、備忘録的にまとめてみました。
下記のapp名の箇所には適宜アプリ名を入力して下さい。

※間違いがありましたら変更しますのでコメント頂けると嬉しいです^^

新規ディレクトリ作成 〜 hello world!!まで

まず、アプリの土台となるディレクトリを作ります。
さらに、touchコマンドで、2つの空ファイルを作成します。

$ mkdir app名 && cd app名
$ touch Gemfile Gemfile.lock

私は、VScodeを使用して開発しているので
code コマンドを使用して起動しています。
ちなみに、code コマンドは起動と作成をしてくれます。

Gemfileを編集します。

$ code Gemfile
Gemfile
source 'https://rubygems.org'
gem 'rails', '~>5.2'

Dockerfileを作成して編集します。

$ code Dockerfile
Dockerfile
FROM ruby:2.5
RUN apt-get update
RUN apt-get install -y \ 
    build-essential \
    libpq-dev \
    nodejs \
    postgresql-client \
    yarn \
    vim

WORKDIR /app名
COPY Gemfile Gemfile.lock /app名/
RUN bundle install

docker-compose.ymlファイルを作成して編集します。

$ code docker-compose.yml
docker-compose.yml
version: "3"

volumes:
  db-data:

services:
  web:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ".:/app名"
    environment:
      - "DATABASE_PASSWORD=postgres"
    tty: true
    stdin_open: true
    depends_on:
      - db
    links:
      - db

  db:
    image: postgres
    volumes:
      - "db-data:/var/lib/postgresql/data"
    environment:
      - "POSTGRES_HOST_AUTH_METHOD=trust"
      - "POSTGRES_USER=postgres"
      - "POSTGRES_PASSWORD=postgres"

コンテナの起動を行い、webコンテナに入って、rails new します。

$ docker-compose up --build -d
$ docker-compose exec web bash
$ rails new . --force --database=postgresql

rails new で作成された database.yml ファイルに追記します。

database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db  #追記
  user: postgres  #追記
  port: 5432  #追記
  password: <%= ENV.fetch("DATABASE_PASSWORD") %>  #追記
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
$ rails db:migrate
$ rails s -b 0.0.0.0

Chromeの検索バーに、localhost:3000 と入力してアクセスするとhello worldが表示されているかと思います!

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

【自分用】マイグレーション関連【ロールバック、カラムの追加、型の変更】

完全に自分用の書き殴りです。

誤り等ございましたら、コメント下さい!

ロールバック

  • rails db:migrate:down VERSION=20190611235049
    • VERSIONで指定した箇所まで戻れる
  • rails db:rollback STEP=3
    • 現在の migration ファイルから遡って、STEPで指定した回数戻れる

カラムの追加

  1. rails g migration Addカラム名Toテーブル名 @@@:integer @@@:string
  2. rails db:migrate
  • Add 以下はキャメルケースで記載
  • カラム名のところは何を書いてもいいが、追加したいカラム名の複数形に統一しておく
  • テーブル名にはカラムを追加するテーブル名の複数形を書く
  • @@@はカラム名を単数形で記入

以下は user テーブルにふりがなを追加したときの例。

class AddKanaToUser < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :kana, :string
  end
end

カラム型の変更

rails g migration change_data_<カラム名>_to_<テーブル名の複数形>

class ChangeColumnToUsers < ActiveRecord::Migration[5.2]
  def change
  end
end

上記のように空の migration ファイルが作成されるので、下記のように編集

class ChangeColumnToUser < ActiveRecord::Migration[5.2]
  def up
    change_column :users, :detail, :text
  end

  def down
    change_column :users, :detail, :string
  end
end

注意点

カラムの追加カラム型の変更では、方法がやや異なっていて、migration ファイルを触る時は、追加(up)するだけでなく、削除(down)するときのことも考えなければならない。

つまり、カラムの追加では、change で書いても rollback 時など down する際にもよしなにしてくれるが、カラム型の変更で change を使ってしまうと、up 時は良くても、down 時に rollback できないというようなことが起こってしまう。ということ。

簡単なことだが、ここを理解したときに、migration ファイルに対する怖さが一気に消えた。

おまけ

これからは上記の様に書くとして、これまでに書いてしまった箇所を rollback するさいはどうすればいいのか...

これは簡単で、今ある migration ファイルの change と記載している箇所を、up に上書き変更して、その下に down 時の動作を追記すればよい。down 時の動作は変更前のカラム型のこと。

なんとなく、migration ファイルを上書きするのは怖かったが、これで問題なく動作した。

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

Docker×Rails6(メモ)

事前準備

環境構築

Docker 関連ファイルを用意

FROM ruby:2.6.3-alpine

ENV LANG=ja_JP.UTF-8
ENV TZ=Asia/Tokyo
ENV ROOT=/myapp \
    GEM_HOME=/bundle \
    BUNDLE_PATH=$GEM_HOME
ENV BUNDLE_BIN=$BUNDLE_PATH/bin
ENV PATH /app/bin:$BUNDLE_BIN:$PATH


WORKDIR $ROOT

RUN apk update && \
    apk upgrade && \
    apk add --no-cache \
        gcc \
        g++ \
        libc-dev \
        libxml2-dev \
        linux-headers \
        make \
        nodejs \
        postgresql \
        postgresql-dev \
        tzdata \
        imagemagick \
        yarn && \
    apk add --virtual build-packs --no-cache \
        build-base \
        curl-dev

COPY Gemfile $ROOT
COPY Gemfile.lock $ROOT

RUN bundle install -j4
# 不要ファイル削除
RUN rm -rf /usr/local/bundle/cache/* /usr/local/share/.cache/* /var/cache/* /tmp/* && \
apk del build-packs

COPY . $ROOT

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["sh", "/usr/bin/entrypoint.sh"]
EXPOSE 3000
docker-compose.yml
version: "3.8"

services:
  db:
    image: postgres:11.0-alpine
    volumes:
      - postgres:/var/lib/postgresql/data:cached
    ports:
      - "5432:5432"
    environment:
      PGDATA: /var/lib/postgresql/data/pgdata
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
      TZ: Asia/Tokyo
  app:
    build: .
    command: ash -c "rm -f tmp/pids/server.pid && ./bin/rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp:cached
      - rails_cache:/myapp/tmp/cache
      - node_modules:/myapp/node_modules:cached
      - bundle:/bundle:cached
    tmpfs:
      - /tmp
    tty: true
    stdin_open: true
    ports:
      - "3000:3000"
    environment:
      RAILS_ENV: development
      NODE_ENV: development
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_USER: postgres
      DATABASE_PASSWORD: password
      WEBPACKER_DEV_SERVER_HOST: webpacker
    depends_on:
      - db
      - webpacker

  webpacker:
    build: .
    command: ./bin/webpack-dev-server
    volumes:
      - .:/myapp:cached
      - node_modules:/myapp/node_modules:cached
    environment:
      RAILS_ENV: development
      NODE_ENV: development
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
    tty: false
    stdin_open: false
    ports:
      - "3035:3035"

volumes:
  rails_cache:
  node_modules:
  postgres:
  bundle:
source 'https://rubygems.org'
gem 'rails',      '6.0.3'
gem 'devise'

# to upload images
gem 'carrierwave', '~> 2.0'
gem "mini_magick"
Gemfile.lock(empry)
```entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

cmd

docker-compose run app rails new . --force --no-deps --database=postgresql --skip-bundle

Gemfile を編集する

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.3'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.3', '>= 6.0.3.4'
# Use postgresql as the database for Active Record
gem 'pg', '>= 0.18', '< 2.0'
# 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'

gem 'devise'
# 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]

# to upload images
gem 'carrierwave', '~> 2.0'

gem "mini_magick"

その後のコマンド

docker-compose run app bundle update
docker-compose run app rails webpacker:install

database.yml を修正

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password: password
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
...

Troubleshooting

up 時に check_yarn_integrity関連 というエラーが出たら webpacker.yml を修正

config/webpacker.yml
...
development:
  <<: *default
  compile: true 

  # Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules
  check_yarn_integrity: false # true -> falseに変更
...
docker-compose build
docker-compose up -d
docker-compose run app rake db:create

また up 時に "webpack-dev-server" not found が出たら

docker-compose run app yarn add webpack-dev-server
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyで文字列を大文字(upcase)にする、又は小文字にする(downcase)方法

Ruby 2.7.0の記事です

puts "STring".upcase # => "STRING"
puts "STring".downcase # => "string"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue(Nuxt)+Rails APIで、ネストした配列で画像とJSONのパラメーターを送る方法

なにこれ

Vue(Nuxt)で、子テーブルへ画像データとJSONを配列にしてパラメーターで送りたい!って時にかなりつまづいたので、備忘録として書き残しておきます。

この記事で得られること

1.Vue(Nuxt)で画像+JSONの形式でPOSTする方法
2.ネストさせた配列で、画像とJSON形式のパラメーターを送る方法

大事なポイント

1.FormData型の変数を宣言して、その変数にappendしていく。

test.js
      const req = new FormData()
      req.append(`name`, this.product.name)

2.ネストした配列で送りたい時はこうする。配列を明示してその中にオブジェクトを生成&appendする。

test.js
          req.append(`product_sub_attributes[][image]`, subs[i].image)

3.axiosでリクエストを投げる時、'Content-Type': 'multipart/form-data'にすること。

test.js
        const response = await axios.post('/products', req, {
          headers: {
            'Content-Type': 'multipart/form-data'
          }
        })

備考

  • バックエンドはRails APIモードを仕様しています
  • 画像アップ方法は、フロントはvue-cropeer、バックエンドはcarrierwaveを使用していますが、まだ理解しきれてない箇所があるのでそちらも後日記事にします。 なので、vue-cropperとcarrierwaveの説明は割愛します。今はJSON配列で送れるんだなーと知っていただければ幸いです。
  • バックエンドはみんな大好きgem 'active_model_serializers’を使用しています。
  • 一部vuetify、bootstrap、axiosを使っています。

注意事項

今回はタイトルの方法を紹介するのがメインになるので、かなり端折っています。
全部解説すると膨大な量になってしまうので。
雰囲気を掴んでいただければ、と思っているのでご了承ください。
既存のコードをQiita用に抽出して載せているので、一部間違いがあるかも知れません。

前提条件

テーブル構造はこんな感じです。
productsテーブルproducts_subsテーブルがあって、1:Nの関係です。
両方とも画像投稿用のimageカラムを持っています。
登場する全てのカラムはstring型です。

product.rb
class Product < ApplicationRecord
  has_many :products_subs
  accepts_nested_attributes_for :products_subs
end
product_sub.rb
class ProductSub < ApplicationRecord
  mount_uploader :image, ProductSubUploader

  belongs_to :product

  validates :name, presence: true
  validates :image, presence: true
end

やりたいこと

productsテーブルへcreateしたい時に、子テーブルのproducts_subsへ同時にデータを保存させたい。
そのためにはネストして送る必要がある。

早速解説します。

画像アップのおおまかな流れ

プレビュー表示用のimgタグと画像アップ用のinputタグを用意して、
imgタグをクリックすると$refsでinputを参照してクリックしたことにします。

配列なので$refs.subImageに[i]をつけて、番号を参照しています。
inputタグはクリックされるとsetSubImageメソッドを呼び出します。

setSubImageで画像を読み込んだ後にvue-cropperに投げて、
らcropSubImageメソッドでsubImageCropperを参照してプレビューを表示する変数に突っ込んだり、画像格納用のオブジェクトに代入したりしてます。
余談だけど、このvue-cropperさんの動きが理解できてなくて、たまに$refsで参照できないバグが生まれます。笑

test.vue
<template v-for="(sub, i) in product.productsSubAttributes">

  <div>
    <v-textarea
      v-model="sub.name"
    >
    </v-textarea>
    <img
      :src="sub.imageSrc ? sub.ImageSrc : '' "
      @click.prevent="$refs.subImage[i].click()"
    >
    <input
      ref="subImage"
      class="d-none"
      type="file"
      name="image"
      accept="image/*"
      @change="setSubImage($event)"
    />
    <v-card>
      <vue-cropper
        ref=“subImageCropper"
        :src="imgSrc"
      />
      <button @click="cropSubImage(sub), i)">
        保存
      <button>
    </v-card>
  </div>
</template>

<script>
export default {
  methods: {
    setSubImage(e) {
      const reader = new FileReader()
      reader.onload = (e) => {
        this.imgSrc = e.target.result
      }
      reader.readAsDataURL(e.target.files[0])
    },
    async cropSubImage(sub, i) {
      // imageSrcがプレビュー表示用のプロパティです。
      sub.imageSrc = this.$refs.subImageCropper[0].getCroppedCanvas().toDataURL()
        this.$refs.subImageCropper[0].getCroppedCanvas().toBlob((blob) => {
          sub.image = blob
        })
      }
    }
  }
}
</script>

本題の画像とJSONオブジェクトをパラメーターで送る方法

今回はaxiosを使います。
その際に、configを設定してmultipart/form-data
という形式に変換します。

【axios】HTTPリクエストメソッド別の引数一覧表(エイリアスを使用した場合)
この記事がよくまとまってたのでリンク貼っておきます。

axios公式ドキュメント

multipart/form-dataとは、画像ファイル(Blob型)を送信できるようにするHTTPリクエストメソッドです。
application/jsonだと画像データが送れないです。

パラメーターで送るための方法

細かく解説するためにコメント式にしました。

test.js
    async onClickCreate() {
      // FormData型の変数reqを定義して、そちらにappendしていきます。
      const req = new FormData()
      // 今回は使用しませんが、こんな感じでバックエンドのカラム名に合わせて
      // オブジェクトを生成してappendすることで、パラメーターを送れます。
      req.append('name', this.product.name)
      // 定数subにアップした画像や文字列のデータを代入して、for文で回します。
      // for文を使っている理由は複数ありますが、今回は省略。状況によってはforEachでも代用可能です。
      const subs = this.product.productSubAttributes
      for(let i = 0; i < subs.length; i++){
        // 送りたいパラメーターに[]をつけることで、0から順番に配列で送ることが出来ます。
        // ただ、配列にインデックスを指定する方法が分からないです。
        req.append(`product_sub_attributes[][name]`, subs[i].name)
        // subs[i].imageがBlob型である=先程vue-cropeerで整形したデータなので、直接カラムに代入します。
        if (subs[i].image instanceof Blob) {
          req.append(`product_sub_attributes[][image]`, subs[i].image)
        // 画像がない場合、番号を指定できないので、空文字を代入しないと順番が狂います。
        // どういうことかと言うと、配列内のオブジェクトが2個以上あった場合、0番目から探して空いているカラムに代入されます。
        // ちなみにここでかなりハマりました。
        } else {
          req.append(`product_sub_attributes[][image]`, '')
        }
      }
      try {
        // 先程代入していった変数reqをパラメーターで送ります。
        // 余談ですが、Railsの場合全てキー名をスネークケースにする必要があります。
        const response = await axios.post('/products', req, {
          headers: {
            'Content-Type': 'multipart/form-data'
          }
        })
      } catch (error) {
        console.error(error.response)
      }
    }

ちなみにバックエンドはこんな感じ

products_controller.rb
 def product_params
    params.permit(
      :id,
      :name,
      {
        product_sub_attributes: [
          :id,
          :name,
          :image,
        ]
      }
    )
  end

おわり

こんな感じでVue(Nuxt)からパラメーターで画像を配列+JSONで送ることが出来ます。

自分が実装しようとした時にこの方法にたどり着くまで苦労したので、

個人的な感想

やり方(How)を覚えるのは良いことなんですけど、
裏側のなぜこの方法なのか(How)の方を理解していきたいです。

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