RSpecメモ(2) Capybara
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の構文まとめ | プログラミングマガジン
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は一意である必要がないので、そのまま打ち込む
associationでuserとの関連を記載することで、データ作成時にuserを記載する必要がなくなる(後述)。
上記のsequenceの書き方について
連番データを作成することができる。ユニークな値を作りたい場合に用いる。
sequence(:title) { |n| "title_#{n}" }
の書き方の方がどういう動きか見えやすい気がするが…
sequence(:title, "title_1")
は何が起きているのか分からない。
二つ目の書き方では何が起こっているのか。
こちらのサイトによると、「ブロックを渡さずに第二引数を渡すと、.next(Rubyメソッド)
が呼ばれるようになっている」ので
このように異なる連番の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
JSとDOM
DOM(Document Object Model)
ドキュメントを物として扱うモデル。 JSからHTMLにアクセスする仕組みのこと。この仕組みによって文書構造、スタイル、内容を変更することができる。DOMを操作して画面をちょこちょこ変えるのがJSの役割(ただし、ひと昔前の内容らしい)
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名から要素を取得するメソッド
button-deleteというIDが振られた要素が取得されている。
単なる文字列ではなくこれがDOMであり、オブジェクト。
削除ボタンだけ赤になった。
上記のような方法で取得したオブジェクトを介してメソッドを実行することができる。
document.getElementById('ID名')で取得した要素を定数に代入し、定義した定数に対してinnerTextメソッドを用いるとタグに囲まれたテキストが表示された。
上記のように新たに文字列を代入するとボタンの文字が変更された。
このようにコンソールに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’, () => { // クリックされた時に動く処理 }) });
↑クリックイベントを仕掛けた後、実際にボタンをクリックすると、こんな感じで画面上部にポップアップが出現する。
演習の考え方まとめ
(なぜか画像が陰った・・・)
↑アラートボタンをクリックするとテキストエリア内に記載した文字がポップアップとして出現する
やってみたこと(失敗)
<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) }) });
ボタンが反応しなかった・・・
原因はよく分からないけど、パソコン再起動で解決した
検証でコードを確認した
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) }) });
できた!
考え方
- テキストエリア内の内容を特定したいので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
全く分からなかったので、検証で調べた。
<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
何も分からない笑
<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>
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や何やは一気に考えるとどんどん迷い込みそうだったので省いて考えました。proc
やProc.new
以外にもProcオブジェクトを作る方法はあるとだけ覚えておきます。。
あと今はぼんやりと理解できたようなできていないような、数日後にはまた訳分からなくなってそうな程度の理解度です。難しい🥲
参考にしたサイト
RubyのProcとは?ブロック・Proc・lamdaの違いをマスター | ポテパンスタイル
権限のプルダウン(メモ書き)
権限のプルダウンを作るために調べたことまとめ
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が便利すぎた - ひよっこエンジニアの雑多な日記
enumをI18n対応させるために用いる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 %>
<%= l @board.created_at, format: :long %>
参考にしたサイト
セレクトボックス
【Rails】完全理解 formでセレクトボックスをつくるselectの使い方 | WEB屋のメモ帳
ransack
【Rails】 ransackを使って検索機能がついたアプリを作ろう! | Pikawaka
enumのi18n対応
【Rails】enumをI18n対応させるenum_helpが便利すぎた - ひよっこエンジニアの雑多な日記
localizeメソッドについて
管理画面を作りたい(メモ書き)
量が多いのでひとまず細かい手順は記載しません。
課題の反省点
- タスクを上手く分解できなかった
- 何をすべきかは分かっていても、どうすべきなのか検討がつかなかった
大まかな実装の流れ
- yarnでAdminLTE3をインストールする
- コントローラ、ルーティング作成
- admin-lteから使いたいテンプレートをそのままコピペする
- マニフェストファイルに読み込むべきファイルを記載する
- コピペしたテンプレ内に作成したマニフェストファイルを読み込む記載をする
- ビューを分解し、共通レイアウトでレンダリングさせる
- 動的な
<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
- 管理画面は
/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のインストール、マニフェストファイルについて
マニフェストファイルに読み込み設定
アセットパイプラインに「node_modules」のpathが通っているの確認
% rails c > Rails.application.config.assets.paths
↓なんかモリモリ出てきた😨
#<Pathname:/Users/user_name/workspace/***/rails_2/5***/node_modules>
とりあえずこんな表示が入っていればパスが通っているらしい。
パスが通っていない場合はconfig/initializers/assets.rb
に下記パスを記載
↑自分のは元々記載されていた。
管理者判定用のroleカラム(enum型)をusersテーブルに追加する
Usersテーブルにroleカラムをenum(列挙)型で追加する
enum型とは?
一つのカラムに指定した複数個の定数を保存できるようにするためのもの。
今回の例で言えば、role(役割)というカラムにgeneral(一般)とadmin(管理者)という二つの定数を保存したい。
特徴
- 指定した複数個の定数以外は保存できなくなる
- カラムに指定した定数が入っているレコードを取り出すのが容易になる
- enumはinteger型(0,1,2...)、もしくはboolean型(true,flase)としてDBに保存される
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 %>
の記述がないの下記エラーが発生するらしい。
ただ自分は<%= 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の解決方法 - 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が存在せず、エラーになってしまいます。
プリコンパイルとは
コンパイラが言語の翻訳ができるように、事前に準備することです。コンパイラに翻訳をお願いする前に、もっとコンピュータの分かりやすい言語にしてあげることです。
他のマニフェストや、個別のスタイルシート/JavaScriptファイルをインクルードしたい場合は、config/initializers/assets.rbのprecompileという配列を使う。
application以外のマニフェストファイルを個別に読み込みたい場合はプリコンパイルの設定をしないと、ファイルが対象外とされエラーが出てしまうらしい。
# コメントアウトされているのでシャープを外す Rails.application.config.assets.precompile += %w( admin.js admin.css )
宣言したら(decleared)サーバーを再起動する。