Rails Diary

プログラミングの学習記録です。

RSpecメモ(2) Capybara

qiita.com

Capybara

  • Webアプリ用のE2E(end to end…始めから終わりまでテストすること)テストフレームワーク
  • 複数あるドライバ(動作システム)の中から使いたいものを選択できる
    →Capybaraのテスト実行環境(Headlessブラウザ)を選択

※ ヘッドレスブラウザとはGUIを持たないブラウザ。人が操作する目的のものではない。
GUI…マウスや指などで操作できる画面を持たないブラウザのこと。テストの実行環境。 https://wa3.i-3-i.info/word1371.html

Capybaraを使うことで、Webアプリのブラウザでの自動実行、JavaScriptの動作確認、Headlessブラウザ(GUIのないブラウザ)を操作できる

RSpecファイル作成、使用するドライバ設定

❶ テストファイル作成

bundle exec rails g rspec:system User, UserSessions, Task

❷ 作成したスペックファイルにテストを記載

require 'rails_helper'

RSpec.describe "Users", type: :system do
# デフォルトではRack::Testというドライバが設定されている
  before do
    driven_by(:rack_test)
  end

  pending "add some scenarios (or delete) #{__FILE__}"

Rack::Test…デフォルトのドライバ。高速だがJSを使えない。 Rackとは

Seleniumドライバ

JavaScriptもサポートしているseleniumドライバを使う場合、デフォルトでFirefoxを使ってテストする様に設定されているが、互換性の問題でChromeを使うように設定しておいた方がいいとのこと。Chromeを使うためにはwebdriversジェムを追加する。webdriversはライブラリの依存関係上、selenium-webdriverも一緒にインストールしてくれるので、明示的に書かなくて済むそうな。

他にも、Webkitドライバ(Seleniumより高速)、Poltergeistなどがある

ドライバの設定

通常テストで複数スペックを作成するので、各ファイルに自動的に記載されているドライバの設定は削除し、自作したsupportディレクトリにまとめておく

# spec/support/capybara.rb supportディレクトリを自分で作成
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium, using: :headless_chrome
  end
end

こっちの書き方でもOKだった

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by(:selenium_chrome_headless)
  end
end

末尾の_headlessを外すことで実際のブラウザでの動作が見られる

rails_helper.rbなどにsupport配下のファイルを読み込む設定を記載

これにより上で作成したsupport配下の設定ファイルが読み込まれる

# spec/rails_helper.rbの下記のコメントアウトを外す
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

capybaraの構文

describeやit等の起点はRSpecのもの、vistやfill_in '', with: ''というのは、Capybara独自の起点

.rspec

下記のようにルートディレクトリに.rspecファイルを作成し、下記の様に記述することでテスト結果を見やすくすることができる。

--format documentation
--color

RSpecで特定のテストを実行する方法

# ファイル名と行数指定してテストが実行できる
$ rspec spec/models/user_spec.rb:12

もしくは
spec_helper.rbに以下の記述を追加し、focus: trueを使える様にする

RSpec.configure do |config|
  config.filter_run_when_matching :focus
end

テストファイルのdescribe, context, itの内、実行したいテストブロックの先頭にfをつけてfdescribe, fcontext, fitのようにすることで特定のテストのみ実行ができる。

fの消し忘れ対策として

Rubocopでclass: RuboCop::Cop::RSpec::Focusを設定しておくと消し忘れた場合に検知できる。 pre-commitやCIを使って自動でRubocopを実行する様にしておくと便利とのこと。

参考にしたサイト

【Rspec】Capybaraについて | プログラミングマガジン

【Rspec】Capybaraの構文まとめ | プログラミングマガジン

Selenium + Capybara + RSpecで自動テストしてみる : 準備編

RSpecで特定のテストを実行する方法

Rackとは何か - Qiita

Capybaraチートシート - Qiita

RSpecメモ(1)

RSpecを書く際のポイントメモ

■ エラーチェック

task = user.tasks.new(title: “”, content: “aaa”, status: :todo)
expect(task.valid?).to be(false)
expect(task.errors).not_to be_empty
expect(task.errors[:status]).to eq [“can’t be blank”] 
  • errorが空出ないことの確認だけではなく、エラー文に想定する文言が含まれているかもテストする

■ タイトルの重複チェック

it ‘is invalid with a duplicate title’ do
  user = User.create(email: “user@example.com”, password: “password”)
  task1 = user.tasks.create(title: “aaa”, content: “aaa”, status: :todo)
  task2→task_with_duplicated_title = user.tasks.build(title: “aaa”, content: “aaa”, status: :todo)
  expect(task2.valid?).to be(false)
  expect(task2.errors[:title]).to eq [“has already been taken”]
end
  • 一つ目のタスクを適当に作成し、二つ目を一つ目と同じtask.titleにすることでtitleの重複状態を作る
  • 一つ目をcreateで作成し、二つ目をbuildで未saveにしているのは二つ目がtitleの重複によってsaveできないことを検証するテストだから
  • 基準(?)となる一つ目のtaskオブジェクトは他で使わないので変数に入れる必要はない
  • タスクの変数をtaskやtask1と書くだけだとイメージしづらいので、task_without_titleやtask_with_duplicated_titleの様に具体的な名前にすると良い

■ be_validやbe_invaidマッチャを使う

expect(task.valid?).to be(false)
# 書き換え
expect(task).to be_valid
expect(task.invalid?).to be(true)

expect(task).to be_invalid

# 良いか悪いかはさておき(分からない)、こっちでも通った
expect(task).not_to be_valid

FactoryBotについて

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "user_#{n}@example.com" }
    password { "password" }
    password_confirmation { "password" }
  end
end
# spec/factories/tasks.rb
FactoryBot.define do
  factory :task do
    sequence(:title, "title_1")
    content { "content" }
    status { :todo }
    deadline { 1.week.from_now }
    association :user
  end
end
  • contentとstatusは一意である必要がないので、そのまま打ち込む
  • 今現在から一週間の間でランダムに値を作ってくれているらしい
    https://i.gyazo.com/bb59c984c46158a9574a26ac25b34aa7.png

  • associationでuserとの関連を記載することで、データ作成時にuserを記載する必要がなくなる(後述)。

上記のsequenceの書き方について

連番データを作成することができる。ユニークな値を作りたい場合に用いる。

sequence(:title) { |n| "title_#{n}" }

の書き方の方がどういう動きか見えやすい気がするが…

sequence(:title, "title_1")

は何が起きているのか分からない。
二つ目の書き方では何が起こっているのか。

こちらのサイトによると、「ブロックを渡さずに第二引数を渡すと、.next(Rubyメソッド)が呼ばれるようになっている」ので

https://i.gyazo.com/7b3ea94000ab4c5a5eb8998b201d985c.png

このように異なる連番のtitleを作り出せる(ちなみにテストをカタカナで書くと上手くいかない)

emailのsequenceは変えたい番号が文字列の途中に挟まっているので、nextメソッドで上手く変更を入れることができない。ブロックを使う。

FactoryBotに記載したデータを使う

user = User.create(email: "test@example.com", password: "password")
task = user.tasks.build(title: "test1", content: "test1")
expect(task).to be_valid

みたいな形でexample(テストの1単位)を作ってきたが、FactoryBotを追加することで、記載が楽になる。


確かにごちゃごちゃしていない

user = FactoryBot.create(:user)
task = FactoryBot.build(:task, user: user)


さらに、先ほどspec/factories/tasks.rbにassociation :userというuserとの関連を記載したが、アレによってuserの記載が省略できる

task = FactoryBot.build(:task)

一層シンプルに!
さらに…

rails_helper.rb内のconfigブロック内に以下を記載

# spec/rails_helper.rb
config.include FactoryBot::Syntax::Methods

これにより、データ作成の先頭にあったFactoryBotをも省略できる

task = build(:task)

もはや原型がない。DRYすぎて返って分かりづらい気もするけど、テスト数が増えてくればこの簡素な感じが役に立つのかなと思う。や

所感

こうやって順を追って、「こういう理由だからこの記載を省略できる」と分かりやすいのかもしれない。最初から洗練された簡素なコードを見てしまうと、情報量の少なさにどうしたら良いのか分からなくなった。

書き方に全く馴染みがなかったのもあるけれど、いきなりFactoryBotでテスト用のダミーデータ作成、RSpecのテスト構文を色々試してみるだと混乱するだけなので、まずはFactoryBotを使わず、慣れないマッチャを使わず、RSpecのDRY回避をせず、少しずつ試してみれば良かったなと思います。

よく分からないままlet(:user) { emai: "", password: "password"}みたいなことをしていたので、与えられた条件の中で上手く工夫することが苦手だなぁと・・・。 あとは本来アプリ上の不具合を予防するためにテストを書くはずなのに、そのテスト自体でエラーが出ちゃう状態に苦笑いでした笑

参考にしたサイト

RSpec

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita

使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita

後々参考にしたいサイト

使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita

FactoryBot

FactoryBot(FactoryGirl)チートシート - Qiita

Factorybotを使ったテストデータの作成方法 - Qiita

Sequence

Rspec,FactoryBotのsequence - Qiita

FactoryBot (旧FactoryGirl) の sequence と .next - Qiita

JSとDOM

DOM(Document Object Model)

ドキュメントを物として扱うモデル。 JSからHTMLにアクセスする仕組みのこと。この仕組みによって文書構造、スタイル、内容を変更することができる。DOMを操作して画面をちょこちょこ変えるのがJSの役割(ただし、ひと昔前の内容らしい)

https://www.w3schools.com/js/pic_htmltree.gif
DOMのツリー構造 JavaScript HTML DOM

DOMは、文書をノードとオブジェクトで表現する。ノードとは、上記画像における一つ一つの要素のことをそう呼ぶ。HTMLにおけるエレメントやタグのこと。

ウェブページは文書です。この文書はブラウザーのウィンドウに表示されるかHTMLソースとして表示することが可能です。しかし両方の場合においてもそれは同じ文書です。ドキュメントオブジェクトモデル(DOM)は、その同じ文書を表現、保存する方法です。DOMはウェブページの完全なオブジェクト指向の表現で、JavaScriptのようなスクリプト言語から変更できます。

ID名からノードを取得し、操作する

<button id="button-delete">削除ボタン</button>
<button id="button-update">更新ボタン</button>
コンソールに以下を打ち込む

document.getElementById("id名")

ID名から要素を取得するメソッド

https://i.gyazo.com/c31ea2c554e2578879034df97f7622ea.png

button-deleteというIDが振られた要素が取得されている。
単なる文字列ではなくこれがDOMであり、オブジェクト。

https://i.gyazo.com/0483e3e6edbf8597e322ef7c7249ac9e.png

https://i.gyazo.com/e6bc4e4cd025568099d938f46585aab6.png

削除ボタンだけ赤になった。

上記のような方法で取得したオブジェクトを介してメソッドを実行することができる。

https://i.gyazo.com/4697dced0afe428243edd730e353fd9b.png

document.getElementById('ID名')で取得した要素を定数に代入し、定義した定数に対してinnerTextメソッドを用いるとタグに囲まれたテキストが表示された。

https://i.gyazo.com/ed38480e9a0266e1df1a901d32ece6d3.png

上記のように新たに文字列を代入するとボタンの文字が変更された。

https://i.gyazo.com/2563cc55ec90814661bf83f381845156.png

このようにコンソールにJSを打ち込むことでDOM(ドキュメントオブジェクトモデル)の取得ができる。実際はイベントドリブンと呼ばれる「何かをしたら何かが起きる」という処理を追加することで動かす。

  • あるボタンをクリックしたらポップアップを出す
  • マウスポインターがある文字の上に乗ったらその文字を色付ける
  • あるテキストエリアの内容が変更されたらそれらの文字をプレビュー表示する

イベントは下記のように設定する

DOMに対してaddEventListenerを設定することでイベントを仕掛けられる

// 一番上の行は「ブラウザがDOMの解析をし終わったら〜」というような一種のイベント設定処理
// この記述がないと、DOMの解析がし終わる前に、削除ボタンのDOMを取得しようとするなど上手く動かないことがある
document.addEventListener('DOMContentLoaded', () => {
  console.log("DOMContentLoaded")

  // 削除ボタンのDOMを取得
  const buttonDelete = document.getElementById("button-delete")
  // 更新ボタンのDOMを取得
  const buttonUpdate = document.getElementById("button-update")
  
  // 削除ボタンにクリックイベントを仕掛ける
  buttonDelete.addEventListener('click', () => {
    // クリックされた時に動く処理
    alert("削除!")
  })

  // 更新ボタンにクリックイベントを仕掛ける
  buttonUpdate.addEventListener('click’, () => {
    // クリックされた時に動く処理
  })
});

https://i.gyazo.com/70b742cfd76215f7ec8d189e4be853bb.png

↑クリックイベントを仕掛けた後、実際にボタンをクリックすると、こんな感じで画面上部にポップアップが出現する。

演習の考え方まとめ

(なぜか画像が陰った・・・)

https://i.gyazo.com/d113b002035105bba0b6df03e89834ce.png

↑アラートボタンをクリックするとテキストエリア内に記載した文字がポップアップとして出現する

やってみたこと(失敗)

<p>テキストエリアの内容をアラートで出す</p>
<textarea id="textarea"></textarea>
<button id="button">アラート</button>
document.addEventListener('DOMContentLoaded', ()=>{
  console.log("DOMContentLoaded")
  
  const buttonAlert = document.getElementById('button')
  const textarea = document.getElementById('textarea').innerText

  buttonAlert.addEventListener('click', ()=>{
    alert(textarea)
  })
});

ボタンが反応しなかった・・・
原因はよく分からないけど、パソコン再起動で解決した

検証でコードを確認した

https://i.gyazo.com/2c48c5b42c1260669e1aa74252af56f6.png

innerTextメソッドを使ってしまったので、タグに囲まれた内容を表示しようとしているのが間違いだったのかも?

検証の内容を参考に下記のように書き換える(成功)

参考というか検証のカンニングです。

<textarea id="textarea"></textarea>
<button id="button">アラート</button> 
document.addEventListener('DOMContentLoaded', () => {
    console.log("DOMContentLoaded")
    const button = document.getElementById("button")
    button.addEventListener('click', () => {
      const text = document.getElementById('textarea')
      alert(text.value)
    })
});

https://i.gyazo.com/576803e82e7fe487c1e6a03b9a2b08a1.png

https://i.gyazo.com/9d99317c38474417384dbdad212c1ec7.png

できた!

考え方

  • テキストエリア内の内容を特定したいのでidを指定
  • ボタンも特定したいのでidを指定

  • アラートを押した時に何かしたい

  • まずアラートのDOMを取ってくる
    const buttonAlert = document.getElementById('button')

↑これに対してイベントを設定したい

const text = document.getElementById('textarea');
buttonAlert.addEventListener('click', ()=>{
  const value = text.value // 一々定数に入れなくてもOK
  alert(value)
});

最初からalert(text.value)でいい。

演習2

全く分からなかったので、検証で調べた。

https://i.gyazo.com/b695bfc5467caa4a498ca5a373097222.png

<label for="amount">数値</label>
<input type="number" name="amount" id="js-amount">
<button id="js-add-button">増やす</button>
<button id="js-minus-button">減らす</button>

<p>結果</p>
<p id="js-result">0</p>

まず、何はともあれDOMを取得しておく

const jsAddButton = document.getElementById('js-add-button')
const jsMinusButton = document.getElementById('js-minus-button')
const jsAmount = document.getElementById('js-amount')
const jsResult = document.getElementById('js-result')

jsAddButton.addEventListener('click', ()=>{
  // 試しにここでalert('jsAmount.value')など入れてみてちゃんと動くか、またどう表示されるのか確認する
  // 少しずつ少しずつ実装しよう◎
  // jsResultのinnerTextに対してjsAmountの値を入れている
  jsResult.innerText = jsAmount.value
})

この時点で、inputに入力した内容を結果に表示させることには成功

https://i.gyazo.com/a769ee62b6a2b218a10c2c77e1061572.mp4

const jsAddButton = document.getElementById('js-add-button')
const jsMinusButton = document.getElementById('js-minus-button')
const jsAmount = document.getElementById('js-amount')
const jsResult = document.getElementById('js-result')

// 現在値を取得。最初は0で設定する
var currentValue = 0

jsAddButton.addEventListener('click', ()=>{
  jsResult.innerText = currentValue + jsAmount.value
})

↑のようにしてみると・・・

https://i.gyazo.com/ceb18d08737aeb5c198d7a1523aee632.mp4

上手くいかない。
jsAmount.valueはあくまでも文字列として取得した値なので、文字列のまま足されてしまい、思う結果にならない。このため、文字列を数値に変換する作業が必要。

parseIntを使う

currentValue = currentValue + parseInt(jsAmount.value)
jsResult.innerText = currentValue

https://i.gyazo.com/cf6fd88f196ab22f0d32b17735e48600.mp4

できた!感動😳

減らすボタンは増やすボタンをコピペして、AddをMinusに、+をーに変更すればOK

https://i.gyazo.com/4460af2e2b31fe9aad50b447207e5b0d.mp4

演習3

何も分からない笑

https://i.gyazo.com/ce9f8167da9d585e5e6b4f5da5041584.png

<input type="text" placeholder="GitHubアカウント名" id="github-name">
<button onclick="fetchRepositories()">リポジトリ取得</button>
<ul id="repository-lists">
  <!-- 例 -->
  <!-- <li><a href="url************">repo-name</a></li> -->
</ul>

https://i.gyazo.com/37254cced380bb39bc2e549e9a7667de.png

const fetchRepositories = async () => {
  const githubName = document.querySelector("#github-name").value
  try}

感想

一気にやろうとすると死ぬので、まず一つ決めてどう記載すれば実現できるのか、細かく分けて考える。分からないことだらけなので、タスクを細分化するのがまず難しいです。慣れるしかなさそう。とはいえ、仕組みは解説動画を見て腑に落ちました👏

Procとは

Procとは?

  • Procとはブロックを持ち運びに便利なオブジェクトにしたものです。
  • ProcはクラスなのでProc.newでオブジェクトを作る事が出来ます。
  • Proc.newによって作成されたProcオブジェクトはcallで呼び出すことが出来ます。 https://qiita.com/k-penguin-sato/items/7f98335ef631ea5ce7ad

ブロックはdo ~ end{...}の塊のこと。これ自体はオブジェクトではないらしい。オブジェクト指向Rubyにおいてオブジェクト以外のものは存在できないらしい。
(正直ここには未だにピンときていない)

調べれば調べるほどよく理解できずに迷宮に迷い込んでいたけれど、ブロックは確かに持ち運びづらそう。ブロックをprocでオブジェクト化して持ち運びやすくしよう!みたいな説明が一番分かりやすかった。

Procの使用例

# ブロックをProcオブジェクト化して変数に代入している
sample_proc_1 = proc { |n| n * n }
sample_proc_2 = Proc.new { |n| n ** 3 } 

# Procオブジェクトはcallメソッドで呼び出せる
sample_proc_1.call(2) #=> 4
sample_proc_2.call(2) #=> 8

持ち運びしやすい!!!

メソッドから呼び出すとは?🙄

def sample_proc(&my_proc)
# (&引数名)のようにメソッドの引数を宣言する
# ② 呼び出し元で指定されたブロックがProcオブジェクトに変換されてる

  puts my_proc.call(2)
# ③ 渡されたブロック引数をcallメソッドで呼び出すことができる
end

sample_proc { |n| n * 2 }
# ① sample_procメソッドを呼び出し時にブロック{ |n| n * 2 }を引数として渡している

なるほどeachメソッドやmapメソッドもこんなふうに定義されているのかなと思った。

def each(&引数名)
  # 何らかの処理
end

配列.each { ブロック }

実際の内部構造を見てみたかったけれど、調べても出てこなかったので合っているのかは分かりません。

感想

ransackのカスタム述語に出てきたprocが何か分からずに調べましたが、かなり難しく感じました。Rubyのオブジェクトについて勉強する時、実際の使用例が見えづらいのが理解に苦しむ要因だなと思います。

yieldやIambdaや何やは一気に考えるとどんどん迷い込みそうだったので省いて考えました。procProc.new以外にもProcオブジェクトを作る方法はあるとだけ覚えておきます。。

あと今はぼんやりと理解できたようなできていないような、数日後にはまた訳分からなくなってそうな程度の理解度です。難しい🥲

参考にしたサイト

【Ruby】eachとmapの違い - Qiita

RubyのProcとは?ブロック・Proc・lamdaの違いをマスター | ポテパンスタイル

【ブロック?yield?】RubyのProcオブジェクトをしっかり理解する | shin>>media

【Ruby】ブロック・Proc・lambda を理解する - Qiita

権限のプルダウン(メモ書き)

権限のプルダウンを作るために調べたことまとめ

ransack軽く復習

ransackは検索機能を実装できるgem

コントローラ

# ビューファイルから送られてくるパラメータqをもとにテーブルからデータを検索する
# whereメソッドのransack版なイメージ
@q = User.ransack(params[:q])

# ransackメソッドで取得したデータをresultメソッドを用いてActiveRecord_Relationのオブジェクトに変換する
@results = @q.result

ビュー

<!-- ransackで定義されているsearch_form_forメソッドを用いる -->
<%= search_form_for @q, url: ○○_path do |f| %>
  <%= f.label :name_cont, 'ユーザー名'%>
  <%= f.search_field :name_cont %>
<!-- 文字入力であればtext_field、数値入力であればnumber_fieldでも可 -->

  <%= f.submit '検索'%>
<% end %>

_contメソッド

ransackで用意されているメソッド
検索したワードが含まれているレコードを取得するためのメソッド。

name_contと、_の前に指定したカラムに対してあいまい検索(検索したワードが含まれたデータが取得)される

_eq

完全に一致する検索結果を表示したい場合に用いる。

name_eqのように記述

詳細に検索をかける

<%= search_form_for @q, url: ○○_path do |f| %>
  <%= f.label :name_cont, 'ユーザー名'%>
  <%= f.search_field :name_cont %>
<!-- 年齢を検索するフォームも追加 -->
  <%= f.label :age_eq, '年齢' %>
  <%= f.number_field :age_eq %>
  <%= f.submit '検索'%>
<% end %>

セレクトボックスを作成する

f.seletcでセレクトボックスを作ることができる

<%= form_with model: @user, url: ○○_path, do |f| %>
 <%= f.select :role_eq, User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}, { include_blank: t('defaults.unspecified') }, { class: 'form-control mr-1' } %>

どう使うのか、
見慣れないものがたくさんついている。。。

まず、_18nって何?

【Rails】enumをI18n対応させるenum_helpが便利すぎた - ひよっこエンジニアの雑多な日記

enumI18n対応させるために用いるenum_helpジェムをインストールすることで使える

enum
class User < ActiveRecord::Base
  enum role: { general: 0, admin: 1 }
end
# roleカラムに入っている数値によって値が定数名が表示される
# roleが0の場合
user.role
=> "general"

この文字列を日本語で表示させたい時に用いるのがコレ!

gem 'enum_help'

翻訳ファイルに追加

ja:
  enum:
    user:
      role:
        general: '一般'
        admin: '権限'

呼び出す際、末尾に_18nを付ける

# roleカラムの値が0の場合
user.role_i18n
=> "一般"
invert

enum_helpによって使えるヘルパー

User.roles_i18n.invert
=> {"一般"=>"general", "管理者"=>"admin"}

以上を踏まえてセレクトボックスについて

<%= f.select :role_eq, User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}, { include_blank: t('defaults.unspecified') }, { class: 'form-control mr-1' } %>
<!-- <%= f.select :カラム名, セレクトボックス表示に使うデータ配列orハッシュ, {オプション},{HTMLオプション}%> -->

セレクトボックスの表示に使うデータ配列orハッシュ

% rails c
irb(main):009:0> User.roles
=> {"general"=>0, "admin"=>1}
irb(main):010:0> User.roles_i18n
=> {"general"=>"一般", "admin"=>"権限"}
irb(main):011:0> User.roles_i18n.invert
=> {"一般"=>"general", "権限"=>"admin"}
irb(main):012:0> User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}
=> [["一般", 0], ["権限", 1]]

↑二次元配列というやつが出来上がった。

セレクトボックスの表示に使うデータはこの二次元配列を使う。必ず配列かハッシュを指定する。

オプション

セレクトボックスのオプション

{ include_blank: true, selected: 0 }
  • include_blank: 先頭を空の選択肢にする
  • selected: デフォルトで選択しておきたい値を設定

include_blank: ここに文字を入れると先頭の選択肢名が入る https://i.gyazo.com/de33f577dd6d24f59976464920db3311.mp4

HTMLオプション

selectタグにid, classオプションを指定したい時に使う

{ class: 'form-control mr-1' }

※ 必ず通常のオプションを設定しない場合でも空の波括弧{},をオプション部分に設定すること
※ これがないと、HTMLオプションが無視される

localizeメソッド (i18n主要メソッド)

日時や時刻を設定した言語のフォーマットに変換する https://pikawaka.com/rails/i18n

rails-18nのgemをインストール済みなので、所定の翻訳を参照してくれる

irb(main):001:0> Date.today
=> Sat, 12 Mar 2022
irb(main):002:0> Time.now
=> 2022-03-12 20:44:53 +0900
irb(main):003:0> I18n.l(Date.today)
=> "2022/03/12"
irb(main):004:0> I18n.l(Time.now)
=> "2022年03月12日(土) 20時45分24秒 +0900"

default以外のフォーマットを指定する場合はI18n.l()の第二引数にformat: :○○と指定する。

ja:
  activerecord:
    errors:
      messages:
       # 中略
  date:
       # 中略
    formats:
      default: "%Y/%m/%d"
      long: "%Y年%m月%d日(%a)"
      short: "%m/%d"

https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml

irb(main):005:0> I18n.l(Date.today, format: :long)
=> "2022年03月12日(土)"
irb(main):006:0> I18n.l(Date.today, format: :short)
=> "03/12"

irb(main):007:0> l(Date.today, format: :short)
Traceback (most recent call last):
        1: from (irb):7
NoMethodError (undefined method `l' for main:Object)

※ コンソールでは先頭のi18nの記載が必要だが、コントローラやビューファイルでは省略可◎

<%= @board.created_at %>

https://i.gyazo.com/5e3adf75542e3d7f46efb5a79a5c3f1e.png
直感的に分かりづらい表示

<%= l @board.created_at, format: :long %>

https://i.gyazo.com/92495cac717b322b3f2ba07f9cd0c0fa.png
見やすい!!

参考にしたサイト

セレクトボックス

【Rails】完全理解 formでセレクトボックスをつくるselectの使い方 | WEB屋のメモ帳

ransack

【Rails】 ransackを使って検索機能がついたアプリを作ろう! | Pikawaka

enumi18n対応

【Rails】enumをI18n対応させるenum_helpが便利すぎた - ひよっこエンジニアの雑多な日記

localizeメソッドについて

【Rails】 I18n入門書~日本語化対応の手順と応用的な使い方 | Pikawaka

管理画面を作りたい(メモ書き)

量が多いのでひとまず細かい手順は記載しません。

課題の反省点

  • タスクを上手く分解できなかった
  • 何をすべきかは分かっていても、どうすべきなのか検討がつかなかった

大まかな実装の流れ

  1. yarnでAdminLTE3をインストールする
  2. コントローラ、ルーティング作成
  3. admin-lteから使いたいテンプレートをそのままコピペする
  4. マニフェストファイルに読み込むべきファイルを記載する
  5. コピペしたテンプレ内に作成したマニフェストファイルを読み込む記載をする
  6. ビューを分解し、共通レイアウトでレンダリングさせる
  7. 動的な<title>の作成、翻訳追加

ユーザー管理用のコントローラ作成

  • application_controllerを継承するadmin/base_controllerを作成
    →base_controllerには管理系共通の処理を記載
    Admin::BaseControllerという名前をつけることで、Adminというモジュールの名前空間の中にbase_controllerというクラスを定義することになる
  • Railsではモジュール階層を、コードを保存するためのディレクトリ階層に対応させているため、上記の記述によりapp/controllers/admin/base_controller.rbというファイルが対応することになる
  • 管理系の機能を足したいときに、Admin::のついたコントローラを追加していけば、コードがadminディレクトリの下にまとまって分かりやすい
class Admin::BaseController < ApplicationController 

↑BaseControllerはApplicationControllerを継承

管理系の各コントローラはこのBaseControllerを継承する設計にする。

作成したいコントローラ
  • admin/dashboards_controller.rb(管理画面トップページへ)
  • admin/user_sessions_controller(管理者ログインフォーム)
  • 管理者用ログイン画面の立ち位置について
    →管理者用のログイン画面なだけであってまだこの時点では管理画面ではない(ログイン状態にない)という解釈からビューをviews/layouts配下に置くか、管理画面に関する画面と解釈し、views/admin/layouts配下に置くのか、解釈によって分ける

管理用コントローラとルーティング

2.6 コントローラの名前空間とルーティング | Railsガイド

コントローラは名前空間でグループ化することができる。

管理系のコントローラはAdmin::名前空間の中に作成しているため、ルーティングはこの名前空間でグループ化することができる。

以下のようにnamescpaceブロックを使ってグループ化する

namespace :admin do
  resources :articles, :comments
end

https://i.gyazo.com/e55e02fd34c6ecda02658542e3b8231b.png

  • 管理画面は/adminのみでアクセスできるようにroot to:を使用
  • user_sessionsは一般ユーザーのルーティングを参考に以下の通り
namespace :admin do
  root to: 'dashboards#index'
  get '/login', to: 'user_sessions#new'
  post '/login', to: 'user_sessions#create'
  delete '/logout', to: 'user_sessions#destroy'
end

rails consoleで仮の権限持ちユーザーを作っておく

seedsファイルを使った作成方法にまだ行きついていないので仮に作っておく

User.create!(first_name: "admin", last_name: "admin", email: "admin@example.com", password: "password", password_confirmation: "password", role: 1 )

これでログインできるか確かめる

AdminLTEのインストール、マニフェストファイルについて

okmt-aya-26.hatenablog.com

マニフェストファイルに読み込み設定

アセットパイプラインに「node_modules」のpathが通っているの確認

% rails c
> Rails.application.config.assets.paths

↓なんかモリモリ出てきた😨

#<Pathname:/Users/user_name/workspace/***/rails_2/5***/node_modules>

とりあえずこんな表示が入っていればパスが通っているらしい。

パスが通っていない場合はconfig/initializers/assets.rbに下記パスを記載 f:id:okmt_Aya_26:20220304170016p:plain ↑自分のは元々記載されていた。

管理者判定用のroleカラム(enum型)をusersテーブルに追加する

Usersテーブルにroleカラムをenum(列挙)型で追加する

enum型とは?

一つのカラムに指定した複数個の定数を保存できるようにするためのもの。
今回の例で言えば、role(役割)というカラムにgeneral(一般)とadmin(管理者)という二つの定数を保存したい。

特徴

  • 指定した複数個の定数以外は保存できなくなる
  • カラムに指定した定数が入っているレコードを取り出すのが容易になる
  • enumはinteger型(0,1,2...)、もしくはboolean型(true,flase)としてDBに保存される

enumを使うためには?

  1. テーブルにenum用のカラムを用意する
  2. モデルにenumの定数を定義をする
①テーブルにenum用カラムを追加
bundle exec rails g migration AddRoleToUsers
② changeメソッドを編集
class AddRoleToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :role, :integer, default: 0, null: false
# 必ず一般ユーザーか権限ユーザーに振り分けるのでNOTNULL制約を追加
# generalは0、adminは1が入るようにuser.rbに後ほど記載
  end
end
enumのカラムに複数個の定数を紐づけていく
# user.rb
enum role: { general: 0, admin: 1 }

# 定数の数が多い場合、配列(シンボルor文字列)で書くとスッキリする
# 配列で定義するとインデックスが自動で定数と紐づいてくれる
enum role: [ :general, :admin ]
enum role: [ "general",  "admin" ]

※注意点

  • enumを使うならboolean型はあまり使わない方が良い。
  • enumの魅力は複数の定数を定義できること、そもそもtrueかfalseの二択で良いのなら、booleanだけで十分。enumを使う必要はない。
  • Rails5.2系でfalseにupdateする際にnullに更新しようとするバグが発生している。

エラー

admin_login.html.erbに<%= csrf_meta_tags %>の記述がないの下記エラーが発生するらしい。

https://i.gyazo.com/adb60fe1f8ba8e63fc91320328214ad6.png

ただ自分は<%= csrf_meta_tags %>を追加してもエラーが解決しなかった。Ajaxで送信するフォームだと<%= csrf_meta_tags %><head>内で呼び、X_CSRF_Tokenをリクエストヘッダーに付与することでCSRF対策ができるそうな。 RailsのPOSTでCSRF TokenがVerifyできないときに確認したいこと - Qiita

formはlocal: trueのまま変更していないから、タグを追加しても変わらなかったのか🤔??

class ApplicationController < ActionController::Base
  protect_from_forgery # 追加

↑の記載を追加することでエラーが解決 【Rails API】ActionController::InvalidAuthenticityTokenの解決方法 - Qiita

このあとbase_controllerに記載したcheck_adminの判定ロジックにおいてnilに対して呼び出しているエラーが出たため、ぼっち演算子に変更したところ問題なく動作した。

参考にしたサイト

enum

【Rails】 enumチュートリアル | Pikawaka

AdminLTE

[管理画面]Rails 5 に yarnでインストールした「AdminLTE3.0.0-alpha.2」 を適用させる方法 - Qiita

AdminLTEを使って管理者用機能を実装する(トップページ) - Programming Learning Diary

AdminLTE 3を使って管理者ページを実装しよう - プログラミングの備忘録

CSRF(クロスサイトリクエストフォージェリー)対策

ActionController::InvalidAuthenticityTokenの解消(解決済み) - Blogメモφ(..)

ActionController::InvalidAuthenticityTokenのエラーが出る|simesime|note

【Rails API】ActionController::InvalidAuthenticityTokenの解決方法(CSRF対策、protect_from_forgeryメソッド)|TechTechMedia

【Rails API】ActionController::InvalidAuthenticityTokenの解決方法 - Qiita

RailsのPOSTでCSRF TokenがVerifyできないときに確認したいこと - Qiita

アセットのプリコンパイル

アセット

https://wa3.i-3-i.info/word18324.html

「資産」、「財産」といった意味合いの言葉。リソースで良いじゃんと思った。 別にIT用語というわけではないらしい。

railsでは意味をさらに絞ってCSS,JavaScriptなどの静的なファイルに限定しているような感じがします。Ruby on Rails プリコンパイル という言葉の定義は?

コンパイル

https://wa3.i-3-i.info/word186.html

プログラムを作るとき、
まず、人間の言葉でプログラムの元ネタ(ソースコード)を書く。

しかし、機械は人間の言葉がわからないので書いたソースコード機械語に翻訳する作業が必要になってくる。この作業をコンパイルと言う。

ソースコード機械語に変換したものがバイナリコード

つまりコンパイルとは、ソースコードをバイナリコードに変換する作業と言える。

では、プリコンパイルとは?

プリコンパイルで検索すると、「プリコンパイラ」みたいな言葉が多く出てきた。

「事前に(pre)」コンパイルを行うってことですね。 コンパイルというのは、この場面では、CoffeeScriptとか、SCSSとか、ブラウザが直接は読めない形式のファイルを、JSやCSSに変換してあげるということです。

Ruby on Rails プリコンパイル という言葉の定義は?

何に対して「事前」かというと、「rails serverが走るよりも前」ということです。Rubyインタープリター方式の言語で、物事を解釈しながら進むのですが、アセットパイプラインは、最初に一気にやってしまうわけですね。precompileをかけない状態でサーバーをproductionモードで起動すると、JSやCSSが存在せず、エラーになってしまいます。

コンパイルの流れをざっくり掴んでいく - Qiita

プリコンパイルとは
コンパイラが言語の翻訳ができるように、事前に準備することです。コンパイラに翻訳をお願いする前に、もっとコンピュータの分かりやすい言語にしてあげることです。

4.1 アセットをプリコンパイルする | Railsガイド

他のマニフェストや、個別のスタイルシート/JavaScriptファイルをインクルードしたい場合は、config/initializers/assets.rbのprecompileという配列を使う。

f:id:okmt_Aya_26:20220308192655p:plain

application以外のマニフェストファイルを個別に読み込みたい場合はプリコンパイルの設定をしないと、ファイルが対象外とされエラーが出てしまうらしい。

# コメントアウトされているのでシャープを外す
Rails.application.config.assets.precompile += %w( admin.js admin.css )

宣言したら(decleared)サーバーを再起動する。