RSpec編 1

2021.4.17

RSpec編 1

 

<Everyday Railsから>

describeとcontextについて

describe: (日本語で「〜を述べる」 「〜を説明する」)

テストのグループ化を宣言している。ネストさせることも可能。

context: (日本語で「文脈」「状況」)

スペックを読みやすくするためにある。テストをグループ化する。「12歳以下の場合」「13歳以下の場合」みたいな感じ

before: 

テストを実行する前の共通処理やデータのセットアップ等を行う

使われる場所によって対象が変わるので気をつける

sequence: ファクトリからオブジェクトを作成するたびに、カウンタの値を一つずつ増やしながらユニークにならなければならない属性に値を設定する。

Capibara: ブラウザの操作をシュミレートすることができる

 

<fork cloneの流れ>

参照

【Git初心者向け】fork(フォーク)からプルリクエストを送るまでの手順を簡単に解説|TechTechMedia

リポジトリのcloneとforkの違い - Qiita

clone: リモートリポジトリをローカルリポジトリに複製すること

fork: 他人のリポジトリを自分のアカウントのリモートリポジトリにコピーする

forkした場合そのリポジトリを所有する開発者に通知される

ex)sampleというリポジトリをforkする

1.sampleリポジトリをfork
2. forkしたリポジトリを自分のローカルにclone
3. cloneしたローカルリポジトリで開発し、コミット、プッシュ
4. forkしたsampleリポジトリの管理者にPull Requestを送信

 

<迷ったところ>

vendor/bundle以下をgitignoreせずにコミットしてしまった

 

.gitignoreに

# Ignore installed gem files.
/vendor/bundle

と入力し、

$ git add .

$ git sommit -m ''

$ git push

を実行し、

$ git rm -r --cached vendor/bundle

を使用して、コミット済みのvendor/bundle以下のファイルをGitの対象から外す。

 

だが、そうとしたところエラー

fatal: pathspec 'vendor/bundle' did not match any files

と表示された。確認したところ、vendor/bundleが存在していなかった。

原因:bundli install時に --path vendor/bundleをつけずに実行すると、自分のマシン上にgemがインストールされる形になるので、vendor/bundleフォルダは作成されないから。

参照

https://qiita.com/jnchito/items/99b1dbea1767a5095d85

 今回はこのままでいいので、PRを作成し、マージする。

 

PR: コードの変更をビュワーに通知し、マージを依頼する機能のこと。コードのレビューを受けることで1人で作ると気づかないミスを発見することができる。

 

 

 

 

 

 

 

 

基礎編23

[管理画面]掲示板/ユーザのCRUD機能の作成

参考

ransackで検索機能を実装 - Ruby on Railsの備忘録

Admin-LTE3を使用した管理画面の実装③(掲示板/ユーザーのCRUD) - プログラミング学習 備忘録

 

 

enum_heplを使用し、セレクトボックスを実装する

参照: 

Admin-LTE3を使用した管理画面の実装③(掲示板/ユーザーのCRUD) - プログラミング学習 備忘録

enum0を一般、1を管理者 のように数字を何かしらの値と紐付け流ことができる。

enum role: { general: 0, admin: 1 }

とすることで、

f:id:mmm_st:20210413150008p:plain

このような形で使用することができる。

 

enum_help:  ↑をgeneralを日本語に対応させる

❶ Gem.fileに記入

❷ $ bundle install

❸ localファイルに記入

(config/locales/activerecord/ja.yml)

ja:
enums:
user:
role:
general: '一般'
admin: '管理者'

❹呼び出す

f:id:mmm_st:20210413150551p:plain

このような形で使用できる。

 

*便利ヘルパーを使用することができるようになる 下で詳しく解説

f:id:mmm_st:20210413150941p:plain

 

<今回の実装>

権限選択画面について分解して考える

<%= f.select :role_eq, User.roles_i18n.invert.
map{|key, value| [key, User.roles[value]]},
{include_blank: '選択しない'}, {class: 'form-control mr-1'} %>

 

❶ここの部分について

User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}

 

まずenum_helpを使用する前のコード

<%= f.select :role_eq, User.role.values %>

f:id:mmm_st:20210414093121p:plain

 

enum_helpを使用することで...

<%= f.select :role_eq, User.roles_i18n.values %>

日本語化することができる

f:id:mmm_st:20210414093427p:plain

だが...このコードだと、発行されるSQLが常に "role" = 0となってしまい、正常に作動しない。

原因→ransackがそもそもenumに対応していないから。

解決法→invertメソッドとmapメソッドを組み合わせて正しく動きようにする。

 

f:id:mmm_st:20210414094359p:plain

invertメソッド: キーと値を組み替える

[Ruby] 便利な組み込みクラスのメソッド達(Hash編) - Qiita

 

f:id:mmm_st:20210414094409p:plain

一般管理者keygeneral adminvalueにそれぞれ代入され、mapメソッドによって処理されている。

mapの中の|key, value| [key, User.roles[value]]によって

key = key →key = 一般 管理者

value= User.roles[value] → value = 0,1 ↓下の画像を見ればわかると思う。

f:id:mmm_st:20210414095210p:plain

 

この結果一般タブが選択されると、calue="0"管理者タブが選択されるとvalue="1"が選択され、正常に動くようになる!

 

❷オプションについて

:include_blank

値をtrueにすると空白になり、文字列にするとその文字列が使用される。

{include_blank: '選択しない'}

参照

[Rails 4.x] FormのSelect プルダウンメニューの項目をDBから引っ張ってくる方法 - Qiita

 

 

 

ransackの検索機能

掲示板一覧画面に日付の検索機能を追加したい。

参照

ransackを使って日付検索&プルダウン選択する - Ruby on Rails Learning Diary

ransackで検索機能を実装 - Ruby on Railsの備忘録

「2020年4月1日から2020年4月3日までに作成された掲示板」のように日付を指定して掲示板を検索したい。

ransackのpredicateとは?

github.com

gteq : 以上

lteq : 以下

これを使用すれば実装できそうだが、これで実装するとこのような形になるが、

<%= f.date_field :created_at_gteq %>
<%= f.date_field :created_at_lteq %>

「4月1日00時から2020年4月3日00時」となり、前日分までの検索しかできない。

「4月1日00時から2020年4月3日23時59分599999...」のような日付設定にしたい。

 

--カスタムする

github.com

上のサイトに型が載っているのでそれに合わせて書く

(config/initializers/ransack.rb)

Racsack.configure do |config|
config.add_predicate 'lteq_end_of_day' #設定するpredicateに名前をつける
arel_predicate: 'lteq' #使いたいpredicateを指定
formatter: proc { |v| v.end_of_day } #受け取った値をどうフォーマットするか
end

end_of_day: もともとあるメソッドで、1日の終わりを23:59:59にする。

end_of_day (Date) - APIdock

 

--controllerにransack用の記述をする。

(controllers.admin.boards_controller.rb)

def index
@q = Board.ransack(params[:q])
@boards = @q.result(distinct: true).includes(user).order
(created_at: :desc).page(params[:page])
end
end

 

 

--view作成

(views/admin/boards/index.html.erb)

<%= search_form_for @q, url: admin_boards_path, do |f| %>
<div class="row">
<div class="form-inline algin-items-center mx-auto">
<div class = "col-auto">
<%= f.search_field :title_or_body_cont, class: 'form-control', placehodler:
'検索' %>
#タイトルと本文から検索
</div>
<div class = "col-auto">
 
---- 今回のところ ----
#(ransack.rb)で定義したlteq_end_of_day
元からあるgteqcreated_atに結合させて使用する。
date_fieldはdata型で日付検索ができるフィールド
<%= f.date_field :created_at_gteq, include_blank: true, class: 'form-control' %>
<span>〜</span>
<%= f.date_field :created_at_lteq_end_of_day, include_blank: true,
class: 'form-control' %>
-- ここまで --
 
</div>
<div class = "col-auto">
<%= f.submit class: 'btn btn-primary', value: '検索' %>
</div>
</div>
</div>
<% end %>

 

 

日時表示

lをつけ忘れないようにする

<td><%= l user.created_at, format: :long %></td>

 

Railsはどのようにpostとpatchを判断するのか?

<%= form_with model: @board, url: admin_board_path(@board),
local: true do |f| %>

 form_withに渡しているmodelインスタンスの状態を見ている。

persisted?メソッド: 永久化されている場合はtrueを返す。= 新しいインスタンスでもなく、削除もされていなければtrueを返す。

を使用し、

true なら patch  

false  なら   post

と判断する。

 

参照

rails/persistence.rb at 8bec77cc0f1fd47677a331a64f68c5918efd2ca9 · rails/rails · GitHub

 

 

 

 

 

 

 

 

 

基礎編22

2021.4.6

[管理画面] 管理画面へのログイン機能、管理画面トップページの作成

<今回出たエラー>

❶ BootstrapよりjQueryを先に記述してくださいという意味

Uncaught TypeError: Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.
と
Uncaught TypeError: Cannot read property 'fn' of undefined

原因 

・application.scssで@import 'adminを記述していたので削除

adimは管理者用なので。

application.jsに//= require jquery3がなかった。

 

❷コンフリクトが起きた

*masterブランチで解消せずに作業ブランチで解消commit/pushを行う

参照ページ

gitでリモートのブランチにローカルを強制一致させたい時 - Qiita

Git コンフリクト解消手順 - Qiita

git checkout master #masterブランチに切り替え
git pull origin master  #リモートの状態を反映させ、最新にする。
git checkout 〇〇 #作業ブランチに移動する。
git merge master #masterブランチをマージする

原因

リモートのmasterブランチとローカルのmasterブランチの状態に差があった。

git co master
git fetch origin
git reset --hard origin/master

これで差をなくし、

git co 22_admin_login
git merge master

で移動し、マージしここで起きたコンフリクトを解消する。

 

git reset --hard HEAD^: 前回のcommitの直後まで戻す。addとかも全て戻ってる。

 

<今回調べたこと>

パッケージマネージャー

大量のプログラムをひとまとめにしたものをパッケージと呼び、そのパッケージを管理するツールのこと

これを使用することで、インストールなどが高速に正確に行うことができる。

 ex)rubyのライブラリ管理を行うGemMacOSに導入するライブラリ管理ツールであるHomebrewなどなど

フロントエンド関連の現在主流のパッケージマネージャー

Bower→npm→yarn という感じの流れできている。

今回はyarnを使用する。

 

enum(イーナム)

列挙型とも呼ばれる。一連の整数値に対して複数の変数名をつけることができる仕組み。

この3つを読めばなんとなくわかる。

【Ruby on Rails】enumとは?enumを使って可読性をあげてみよう - SakuraWi - BLog

【Rails】Enumってどんな子?使えるの? - Qiita

Active Record クエリインターフェイス - Railsガイド

 

ルーティングのnamespace

コントローラを名前空間によってグループ分けをすることができる。

Rails のルーティング - Railsガイド

 

継承

他のクラスをベースとして新しいクラスを作成することをクラスの継承と呼ぶ。継承先のクラスを子クラスサブクラスと呼び、継承元のクラスを親クラスと呼ぶ。

複数のクラスに共通する項目を別クラスに切り出し、そのクラスを継承することによって再利用が簡単にできたり、記述箇所を一箇所にまとめることが可能。

親クラスの記述を変更したいときは、こクラスで同じメソッド名を使用してあげることで、親クラスの内容をオーバーライドすることができる。

 

コールバック

特定のタイミングで実行されるアクションのこと。

Active Record コールバック - Railsガイド

 

AdminLTE

AdminLTEはBootstrapベースで作られた管理画面作成などに特化したCSSフレームワーク(枠組み、土台)のこと。

骨組みはBootstrapで、それにスタイリングを加え、サンプルも提供してくれる。

俺たちは雰囲気でAdminLTEを使っている - Qiita

管理画面を作る:AdminLTE 基本編 - Qiita

 

マニフェストファイル

RailsでcssファイルとJavascriptファイルをマニフェストファイルから読み込む | Boys Be Engineer 非エンジニアよ、エンジニアになれ

 

アセットパイプライン

一言で言うと...

複数のディレクトリやファイルに分かれたassetsディレクトリ内のファイルをひとつに連結・圧縮する機能

参照

こっちで仕組みを理解して、

DIVE INTO CODE | 「アセットパイプライン」を学ぼう

細かい補足はこっち

Rails初学者がつまずきやすい「アセットパイプライン」

Webブラウザ上に画面が表示されるのは、”WebブラウザがHTMLとCSSJavaScriptを認識する” 必要がある。

Ruby on Rails では、この仕組みがアセットパイプラインによって実現されるようになっている

Webブラウザには、ひとつのWebページを表示するために複数のファイルを結合する機能は備わっていない。そのため、開発者が作業をしやすいようにファイルをたくさんのディレクトリに分けてしまうと、それらのディレクトリをそのままひとつのWebページとして扱うことができなくなってしまいまう為アセットパイプラインがある。

 

キャッシュとは

Railsアプリケーションを高速化するもの

リクエスト・レスポンスなどのサイクルの中で作成されたコンテンツを保存しておき、次回同じようなリクエストが発生したときのレスポンスでそのコンテンツを再利用すること。

 

参照

CDNってそもそも何?なんかサーバの負荷が下がるって聞いたんだけど!〜Web制作/運営の幅が広がるCDNを知ろう第1回〜 | さくらのナレッジ

すごくわかりやすいのがこの部分

キャッシュとは複雑なデータベースリクエストなどで作成された完成品を一時的に置いておくもの。→カレー屋さんでカツカレーのカツをあらかじめあげておくようなもの。とんかつを揚げるのには7分程度かかるが作り置きしておけばカレーをかけるだけで出来上がるので高速でカツカレーを提供できる。

 

 

<流れ>

参照

AdminLTE 3を使って管理者ページを実装しよう - Ruby on Railsの備忘録

・AdminLTE3をインストールする

・管理者ページようにマニフェストファイルを作成、記述し読み込みの設定を行う

・Userモデルに管理者かどうかを判別させるroleカラムを追加する

・↑enumを追加する

・管理者用のコントローラーの作成

・管理者用のルーティングの設定

・管理者用のページレイアウトファイルの作成

・管理者用のビューの作成

 

admin

--adminをインストールする

yarn add admin-lte@^3.0

node_modulespackage.jsonyarn.lockというファイルがインストールされる。

node_modules/admin-lteディレクトリにデフォルトテンプレートが記載されているその中から

今回は、starter.html を使用していく。

 

--検証を使用して、取り込むファイル確認する

一般のページと管理者用のページの見た目が大きく変わるため、別々で管理する。

f:id:mmm_st:20210411151828p:plain

一般→(app/assets/javascripts/application.js)(app/assert/stylesheets/application.scss)

管理者→(app/assets/javascripts/admin.js)

(app/assert/stylesheets/admin.scss)

 

このリンクから管理画面のサイトイメージが見れる

https://adminlte.io/themes/v3/starter.html

検証を開いて使用されているファイルを自分のほうでも読み込む。

JavaScriptについては

JavaScriptタグ内の--REQUIRED SCRIPTS--を確認。

(app/assets/javascripts/admin.js)

//= require jquery3 #jQueryを導入
//= require rails-ujs #rails-ujs導入
//= require admin-lte/plugins/bootstrap/js/bootstrap.bundle.min
//= require admin-lte/dist/js/adminlte

 

CSSについては

headタグ内をみる。GoogleAPIFontはyarnで取得したパッケージではないので無視。

(app/assert/stylesheets/admin.scss)

 
 
@import 'admin-lte/plugins/fontawesome-free/css/all.min.css';
@import 'admin-lte/dist/css/adminlte.css';

 

--application.jsの設定を変更

以前はappliction.jsファイル内で//= require tree .javascriptディレクトリ階層全ファイルを読み込んでいた。

だが、せっかく管理者用のファイルを別に作成したのにadmin.jsとaplication.jsが同じ階層にある為、このままでは全てのファイルを読み込んでしまう新しく作成した管理者用ファイル以外のファイルを個別で読み込む。

(app/assert/javascripts/application.js)

//= require jquery3
//= require popper #bootstrapのため
//= require bootstrap-sprockets#bootstrapのため
//= require rails-ujs
//= require activestorage #ファイルアップロードの昨日のため
 

 

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

プリコンパイルの設定をする。

application.jsまたは.css 以外のファイルを個別に呼び出したい(<%= stylesheet_link_tag 'application'... %>のような感じで<head>タグで呼び出す。)場合はプリコンパイルの設定を行わないと対象外とされ、エラーが起こることがある。

(config/initializars/assets.rb)

# Be sure to restart your server when you modify this file.

# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = '1.0'

# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Add Yarn node_modules folder to the asset load path.
Rails.application.config.assets.paths << Ra
ils.root.join('node_modules')
#ここでアセットパイプラインのpathの設定が行われている
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
Rails.application.config.assets.precompile += %w[admin.js ad
min.css]
コンパイルの設定。コメントアウトを外した。
 

 

Userモデルに管理者かどうかを判別させるroleカラムを追加する enumを使用

一般権限はdefaultは0 管理者権限は1とする。

$rails g migration add_role_to_users

 
class AddRoleToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :role, :integer, null: false, default: 0
end
end

$ rails db:migrate

enumの追加

モデルの数値カラムに対して文字列の名前を定義することができる。

一般をgenetal

管理者をadminとして定義する

(user.rb)

enum role:
{
general: 0,
admin: 1
}
 

 

管理者用Controllerを作成する

アプリケーション全体:  ApplicationController

一般ユーザー関係:  ApplicationControllerを継承したController

管理ユーザー関係に共通する基盤:  Admin::BaseController

他の管理ユーザー関係: Admin::BaseControllerを継承したController

 

--管理者用のコントローラーの基礎となるadmin/base_controller.rbを作成

$ rails g controller admin::base

 

--admin/base_controllerにAdmin::BaseControllerを継承したControllerの基盤になる処理を追加する

(admin/base_controller)

class Admin::BaseController < ApplicationController
before_action :check_admin
layout 'admin/layouts/application' #読み込みたいファイルを指定

private

def not_authenticated
flash[:warning] = t('defaults.message.require_login')
redirect_to admin_login_path
end

def check_admin
redirect_to root_path, warning: t('defaults.not_authorized') unless
current_user.admin? #user.rbで記述したものを使用
admin権限でない場合はトップページへ遷移
end
end

 

--管理画面のトップページへ遷移するコントローラー

$ rails g controller admin::dashboards

(admin/base_controller)を継承しているので、レイアウトの宣言は不要

(admin/dashboards_controller)

class Admin::DashboardsController < ApplicationController
def index; end
end

 

--管理画面へのログインの為のコントローラー

$ rails g controller admin::user_sessions

一般ユーザーと同様に管理画面へのログインフォームを設定する。app/controllers/user_sessions_controllerと同じような形で記述。

(admin/user_sessions_controller)

class Admin::UserSessionController < ApplicationController
skip_before_action: check_admin, only: %i[new create]
skip_before_action: require_login, only: %i[new create]
layout: 'admin/layouts/admin_login'
#ログインページ用のレイアウトを指定

def new; end

def create
@user = login(params[:email], params[:password])
if @user
redirect_back_or_to boards_path, success: t('.success')
else
flash.now[:danger] = t('.fail')
render :new
end
end

def destroy
logout
redirect_to root_path, success: t('.success')
end
end

 

ルーティングの設定

参照

Rails のルーティング - Railsガイド

--namespaceを使用して、/adminから始まるURLにする。

namespace :admin do
root to: 'dashboards#index'
get 'login', to: 'user_sessions#new'
get 'login', to: 'user_sessions#create'
delete 'logout', to: 'user_sessions#destroy'
end

 

ビューの設定

 

スタイルシートを指定する

stylesheet_link_tag 'スタイルシートへのパス' ,HTML属性 or イベント属性

media: 関連ファイルの出力メディアのリンクタイプ

application.html.erbと、adminディレクトリ以下にない(view/layouts/admin_login.html.erb)で呼び出し。

参照

stylesheet_link_tag | Railsドキュメント

 

javaScriptをインクルードする

javascript_include_tag 'javaScriptファイルへのパス' (,オプション)

application.html.erbのみで一度だけ呼び出し

参照

javascript_include_tag | Railsドキュメント

 

--レイアウトファイルの作成

controllerのlayout: で宣言したファイルをviews/admin/layoutsディレクトリとviews/layoutsディレクトリの作成、配置する。

 

パーシャルも使用する

(view/admin/layouts/application.html.erb )

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta lang='ja'>
<meta name="robots" content="noindex, nofollow">
<title><%= page_title(yield(:title), admin: true) %></title>
#helperメソッドで定義しているので、(管理画面)がついた表示がされる。
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'admin', media: 'all' %>
#admin.scssを読み込む
</head>
 
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
 
<%= render 'admin/shared/header' %>
<%= render 'admin/shared/sidebar' %>
 
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<%= render 'shared/flash_message' %>
<%= yield %>
</div>
<!-- /.content-wrapper -->
 
<%= render 'admin/shared/footer' %>
 
</div>
<%= javascript_include_tag 'admin' %>
#admin.jsを読み込む
</body>
</html>
 

(view/admin/layout/shared/_header.html.erb )

<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#"><i class="fas fa-bars">
</i></a>
</li>
</ul>

<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- Navbar Search -->
<li class="nav-item">
<%= link_to t('defaults.logout'), admin_logout_path, method: :delete, class: 'nav-link' %>
</li>
</ul>
</nav>

(view/admin/shared/_sidebar.html.erb )

<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-dark-primary elevation-4">
<!-- Brand Logo -->
<a href="index3.html" class="brand-link">
<%= image_tag 'AdminLTELogo.png', class: 'brand-image img-circle elevation-3' %>
<span class="brand-text font-weight-light">AdminLTE 3</span>
</a>
 
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar user panel (optional) -->
<div class="user-panel mt-3 pb-3 mb-3 d-flex">
<div class="image">
<%= image_tag current_user.avatar_url, class: 'img-circle elevation-2' %>
</div>
<div class="info">
<a href="#" class="d-block"><%= current_user.decorate.full_name %></a>
</div>
</div>
 
<!-- Sidebar Menu -->
<nav class="mt-2">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<%= link_to '#', class: "nav-link" do %>
<i class="nav-icon far fa-file"></i>
<p>
掲示
</p>
<% end %>
</li>
<li class="nav-item">
<%= link_to '#', class: "nav-link" do %>
<i class="nav-icon far fa-user"></i>
<p>
ユーザー
</p>
<% end %>
</li>
</ul>
</nav>
<!-- /.sidebar-menu -->
</div>
<!-- /.sidebar -->
</aside>

(view/admin/shared/_footer.html.erb )

 
<footer class="main-footer">
<strong>Copyright &copy; 2019 RUNTEQ.</strong>
All rights reserved.
</footer>

(view/layouts/admin_login.html.erb)*admin/下にないこと注意

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="robots" content="noindex, nofollow">
<title><%= page_title(yield(:title), admin: true) %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'admin', media:'all' %>
#admin.scss読み込み
</head>
<body class="hold-transition login-page">
<div>
<%= render 'shared/flash_message' %>
<%= yield %>
</div>
</body>
</html>

(admin/user_sessions/new.html.erb)

<% content_for(:title, t('.title')) %>
<div class="login-box">
<div class="login-logo">
<h1><%= t('.title') %></h1>
</div>
<!-- /.login-logo -->
<div class="card">
<div class="card-body login-card-body">
 
<%= form_with url: admin_login_path, local: true do |f| %>
<%= f.label :email, User.human_attribute_name(:email) %>
<div class="input-group mb-3">
<%= f.text_field :email, class: 'form-control', placeholder: :email %>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-envelope"></span>
</div>
</div>
</div>
 
<%= f.label :password, User.human_attribute_name(:password) %>
<div class="input-group mb-3">
<%= f.password_field :password, class: 'form-control', placeholder: :password %>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
</div>
</div>
</div>
 
<div class="row">
<div class="col-12">
<%= f.submit (t 'defaults.login'), class: 'btn btn-block btn-primary' %>
</div>
</div>
<% end %>
</div>
</div>
</div>

(admin/dashboards/index.html.erb)

<% content_for(:title, t('.title')) %>
<div class="container">
<div class="row">
ダッシュボードです
</div>
</div>

 

タイトルの設定

--ヘルパーメソッドを作成

module ApplicationHelper
def page_title(page_title = '', admin = false)
#defaultをadmin = falseにしておく
#user.rbに定義したadminをここでも使用
base_title = if admin
'RUNTEQ BOARD APP(管理画面)'
else
'RUNTEQ BOARD APP'
end

page_title.empty? ? base_title : page_title + ' | ' + base_title
end
end

 

 

 

 

基礎編21

2021.4.5

パスワードリセット機能の実装

 

sorceryのreset_passwordモジュールを使用する。

(イメージ)

ログイン画面のパスワードおお忘れの方(リンク)からパスワード変更の申請ページへ

パスワード変更申請ページからメールアドレスを入力→

メールアドレスにメールが届く→メールのURLを開くとパスワード申請ページが表示され、パスワードを変更できる。

 

参照

【Rails】パスワード変更(トークンがどのように使用されているのか) - Qiita

sorceryのパスワードリセットを実装(reset_passwordモジュール) - Qiita

Action Mailer の基礎 - Railsガイド

 

<エラー>

$ rails db:mograte:resetを実行したいときに、まず現状を確認したく

$rails db:migrate:statusを実行したところ、

Schema migrations table does not exist yet. (=スキーママイグレーション・テーブルがまだ存在しません。)

と出てしまった。

rails db:migrate を実行すれば存在するのか?と実行したところ、

 Multiple migrations have the name SorceryResetPassword.(=複数のマイグレーションにSorceryResetPasswordという名前がついています。)と教えてくれた。

原因:内容が全く同じmigeationファイルが2つあった。1つを削除したところ、実行できた。

参考

Ruby on Rails - rake db:migrateが実行されない|teratail

 

❷ 

$bundle exec rspecを実行すると

Capybara::ElementNotFound:

Unable to find field "メールアドレス" that is not disabledが表示される。

(原因)inputとlabelが紐づいていなくて、具体的にはlabelのfor属性とinputのnameやidなどが一致してない時に出る。

form_withを正しく使えていればRailsがうまいこと紐づけてくれるはずなのでform_withの使い方が何かおかしい...

(元々)

<div class="form-group">
<%= f.label :メールアドレス %>
<%=f.text_field :email, class:"form-control" %>
</div>

f:id:mmm_st:20210406101210p:plain

(変更後)

f:id:mmm_st:20210406101239p:plain

 

このように変更したが...エラーに変更なし!

よく考えてみると、「メールアドレス」と表示されていない。ので指定。

<div class="form-group">
<%= f.label :email, t(User.human_attribute_name(:email)) %><br />
<%= f.email_field :email, class: 'form-control' %>
</div>

 

f:id:mmm_st:20210406101922p:plain

 

これでOK

 

参照

<LABEL>-HTMLタグリファレンス

 

<流れ>

sorceryのreset_passwordモジュールを使用する

参照

Reset password · Sorcery/sorcery Wiki · GitHub

--導入する

$ rails g sorcery:install reset_password --only-submodules

--migrationファイルが作成される

class SorceryResetPassword < ActiveRecord::Migration[5.2]
def change
add_column :users, :reset_password_token, :string, default: nil
add_column :users, :reset_password_token_expires_at, :datetime, default: nil
add_column :users, :reset_password_email_sent_at, :datetime, default: nil
add_column :users, :access_count_to_reset_password_page, :integer, default: 0
 
add_index :users, :reset_password_token
end
end

 $ rails db:mograte

 

tokenにユニーク制約とallow_nil: trueを追加

--パスワードを変更した際に、reset_password_tokennilになるのでユニーク制約にひっかる。ここでallow_nil: trueを追加せることでnilを許可する。

allow_nil: true: 対象の値がnilの時にバリテーションをスキップする

(user.rb)

validates :reset_password_token, uniqueness: true, allow_nil: true

 

UserMailerという名前のパスワードリセットメール用のMailerを作成

--パスワードリセットに使用するメイラーUserMailerを作成

$ rails g mailer User.Mailer reset_password_email

 

--sorceryのパスワードリセットに使用するActionMailerとして、UserMailerを指定する。

ActionMailerRuby on Rails に組み込まれているメール送信機能

(1)からapp/mailers/user_mailer.rbが推測される

(config/initializers/sorcery.rb)

Rails.application.config.sorcery.submodules = [:reset_password]
 
Rails.application.config.sorcery.configure do |config|
config.user_config do |user|
user.reset_password_mailer = UserMailer (1)
end
 
end

 

--パスワードリセット用のメソッドを記述

・パスワードリセット用メールを送信するためのメソッドを作成し(1)、引数にuserパラメータを追加する

・メールに表示させる情報やメールの送信先を設定(2)

・メールのviewで使用する(3)

(app/mailers/user_mailer.rb)

class UserMailer < ApplicationMailer
def reset_password_email(user) (1)
@user = User.find(user.id) (3)
@url = edit_password_reset_url(@user.reset_password_token) (3)
mail(to: user.email, subject: t('defaults.password_reset')) (2)
end
end

Mailerクラス#メソッドはController#アクションと似た動きをする

今回は)

user_mailerクラス#reset_password_emailメソッド という形で

user_mailer/reset_password_email.html.erb(text.html.erbも)というメイラーviewを推測し呼び出す。

 

--送信元のメールアドレスと宛名を指定する

(app/mailers/application_mailer.rb)

class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
end

 

--メイラーviewを設定する

上で定義したインスタンス変数(@user.@url)を使用している

(app/views/user_mailer/reset_password_email.html.erb)

<p><%= @user.decorate.full_name %></p>
<p>===============================================</p>
 
<p>パスワード再発行のご依頼を受け付けました。</p>
 
<p>こちらのリンクからパスワードの再発行を行ってください。</p>
<p><a href="<%= @url %>"><%= @url %></a></p>

app/views/user_mailer/reset_password_email.text.erb)

<%= @user.decorate.full_name %>
===============================================
 
パスワード再発行のご依頼を受け付けました。
 
こちらのリンクからパスワードの再発行を行ってください。
<%= @url %>

 

routes.rbに 記述

(routes.rb)

resources :password_resets, only: %i[create new edit update]

 

パスワードアクションを処理するコントローラーを作成する

--controller作成

$ rails g controller PasswordReset create edit update new

 

--controller内容解説

 

(1)newアクション: 

パスワードリセット申請用フォーム用のアクション

 

 

(2) createアクション: 

パスワードリセットをリクエスする。ユーザーがパスワードのリセットフォームにemailを入力し、送信した時にこのアクションが実行される。(=パスワードリセット対象のメールアドレスを申請し、リセット対象を作成する)

❶(views/passeord_resets/new.html.erb)のform_withで送られてきたemailをparamsで受け取る

❷DBにデータがあれば、メールをユーザに送信する(URL付き)

(@user.deliver_reset_password_instructions! if @user と同じ)

❸ 入力したemailがDBに存在するか否かを問わず、リダイレクトし成功メッセージを表示させる。悪意ある第三者にemailがDBにあるかどうかを確認させないため。

 

 

(3)editアクション:

ユーザーがメールのURLをクリック→パスワードリセットフォームページ(views/passeord_resets/edit.html.erb)へ→新しいパスワードを入力。

❶ メールからpostされてきた値を取得

❷ リクエストで送信されてきたトークンを使用し、ユーザの検索、有効期限のチェックを行う。

トークンが見つかり、有効であればそのユーザオブジェクトを@userに格納する。

❸ @userがnilまたは空の場合、not_authenticatedメソッドを実行する。

not_authenticatedメソッド: applicatio_controller.rbに入力してある

 

 

(4) updateアクション:

(views/passeord_resets/edit.html.erb)でユーザーが「更新する」ボタンでパスワードを変更した時に動く。

❶❷❸はeditと同じ感じ

❹パスワード確認の検証を行う。

@user.password_confirmationにparams[:user][:password_confirmation]を代入している...よね?

❺ change_passwordメソッド: パスワードリセットに使用したトークンを削除し、パスワードを更新する。

 

(app/controllers/password_resets_controller.rb)

class PasswordResetsController < ApplicationController
skip_before_action :require_login

def new; end (1)

def create (2パスワードリセットをリクエストする)
@user = User.find_by(email: params[:email]) ❶
@user&.deliver_reset_password_instructions! ❷
redirect_to login_path, success: t('.submit_message') ❸
end

def edit (3 パスワードリセットフォームページへ遷移する)
@token = params[:id] ❶
@user = User.load_from_reset_password_token(params[:id]) ❷
return not_authenticated if @user.blank? ❸
end

def update (4 ユーザーが新パスワードを入力した時に実行される)
@token = params[:id] ❶
@user = User.load_from_reset_password_token(@token) ❷
return not_authenticated if @user.blank? ❸

@user.password_confirmation = params[:user][:password_confirmation] ❹
if @user.change_password(params[:user][:password]) ❺
redirect_to login_path, success: t('.success')
else
flash.now[:danger] = t('.fail')
render :edit
end
end
end

 

パスワードリセット用のフォーム作成

--パスワードリセット申請フォーム

(views/passeord_resets/new.html.erb)

<% content_for(:title, t('.title')) %>
<div class="container">
<div class="row">
<div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
<h1><%= t('.title') %></h1>
<%= form_with url: password_resets_path, local: true do |f| %>
<div class="form-group">
<%= f.label :email, t(User.human_attribute_name(:email)) %><br />
<%= f.email_field :email, class: 'form-control' %>
</div>
<%= f.submit t('defaults.submit'), class: 'btn btn-primary' %>
<% end %>
</div>
</div>
</div>

--パスワードリセット用フォーム

(views/passeord_resets/edit.html.erb)

<% content_for(:title, t('.title')) %>
<div class="container">
<div class="row">
<div class="col col-md-10 offset-md-1 col-lg-8 offset-lg-2">
<h1><%= t('.title') %></h1>
<%= form_with model: @user, url: password_reset_path(@token), local: true do |f| %>
<%= render 'shared/error_messages', object: f.object %>
 
<div class="form-group">
<%= f.label :email %>
<%= @user.email %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
</div>
<div class="actions">
<p class="text-center">
<%= f.submit class: 'btn btn-primary' %>
</p>
</div>
<% end %>
</div>
</div>
</div>

 

letter_opener_webを追加し、開発環境では実際のメールは送られないように設定する

--Gemfileに追加する

group :development do
省略
 
gem 'letter_opener_web', '~> 1.4.0'
省略
 
end

$ bundle install

 

--routes.rbにLetterOpenerWebにアクセスするために必要な記述を追記

(config/routes.rb)

mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?

 

--config/environments/development.rbに設定を追加

(config/environments/development.rb)

Rails.application.configure do
省略
config.action_mailer.delivery_method = :letter_opener_web
config.action_mailer.default_url_options = Settings.default_url_options.to_h
end

 

--メールが送れたか確認する

http://localhost:3000/letter_opener

ここにアクセセスする。

 

host情報について

--configというgemを使用してsettings/development.ymlに記載する。

今回は開発環境での設定をする為、(settings/development.yml)に記載する。

テスト環境であれば、(settings/test.yml)に記載。configフォルダ以下に管理するとメンテナンスが楽になるから。環境ごとに値が変わらない場合は、(config/settings.yml)に記述するといい。

GitHub - rubyconfig/config: Easiest way to add multi-environment yaml settings to Rails, Sinatra, Pandrino and other Ruby projects.

host : サービスする役割のコンピュータのこと

 

(Gemfile)

gem 'config'

$ bundle install

カスタマイズ可能な構成ファイル(settings/development.yml)とデフォルト設定ファイルが生成される。

 

--(settings/development.yml)の設定を定数で置き換える

default_url_options:
host: 'localhost:3000'

 

(config/environments/development.rb)に

config.action_mailer.default_url_options = { host: 'localhost:3000' }

が入る。

 

基礎編20

2021.4.3

 プロフィール編集機能の実装

 

ユーザーのプロフィール画面の編集機能を実装する

 

参照

プロフィール編集機能の実装 - Qiita

resourceを使ったプロフィール編集機能の実装 - olive_miuのブログ

 

<引っかかったエラー>

プロフィールを編集したときに「メールアドレスを入力してください」等の個別のエラーメッセージが表示されない。

f:id:mmm_st:20210404114557p:plain

原因:updateできなかった時にrender先をshowにしていた。

→render: editに変更した。

renderを使用すればコントローラを経由せずにページを描画することができるため、エラーメッセージを含んだ@userがviewに渡され、エラーメッセージを表示することができる

flash.now[]とrender => 次のページへリダイレクトした時点でflashは消える為。

 

(profiles_controller.rb)

def update
@user = User.find(current_user.id)
if @user.update(user_params)
redirect_to profile_path, success: t('defaults.message.profiles_updated')
else
flash.now['danger'] = t('defaults.message.profiles_notupdated')
render :edit
end
end

 

<ポイント>

・resourceを使用し、ユーザープロフィールのURLはidを参照しないことが望ましい

ユーザープロフィールは1つしか存在しない

他のユーザーのプロフィールを編集することはない

→idを表示するメリットがない

 

基礎編19

2021.4.3

掲示板の検索機能を実装

ransackを使用して、掲示板の検索機能を実装する

 

参照

ransackを使った検索機能の実装 - Qiita

Ransackを使って検索機能を実装してみた - プログラミング学習 備忘録

【Rails】検索機能の実装手順 | たみずブログ

 

<ポイント>

① 検索フォームでurlを指定しないと、ブックマーク一覧の検索フォームでリクエストするurlが/boadsになってしまうので気をつける。

これだとBoardsController#indexにルーティングされてしまう。

②search_field :title_or_body_cont

タイトル(title)と本文(body)カラムに対して、最後の_contでLIKE句( =あいまい検索ができる)を利用した部分一致検索ができるようにしている。

(app/views/boards/_search_form.html.erb)

<%= search_form_for @q, url: url do |f| %>①
<div class='input-group mb-3'>
<%= f.search_field :title_or_body_cont, placeholder: t('defaults.search_word')
, class: 'form-control' %>②
<div class='input-group-append'>
<%= f.submit value: "検索", class: 'btn btn-primary' %>
</div>
</div>
<% end %>

 

② distinct: true 

「関連する子テーブルの情報を条件に絞り込んで、親テーブルの検索結果を表示するとき」に使用する

ex)「〇〇○というコメントがついている掲示板を取得する」場合にdistinct: true がないとおかしくなる

掲示板Aに対して「〇〇○が好き」「〇〇○は楽しい」というコメントがあった時

distinct: true がないと、「〇〇○」を検索した場合に掲示板Aが2回取得されて検索結果が2件になってしまう。

→なぜ起こるのか...

関連するテーブルをjoinすると一つの掲示板につきコメントの数だけ行が出来てしまう。これらを検索するとどちらの行も該当するので「掲示板A」の情報が2件取得される。distinct: true を使用して重複を取り除く

id   title      body  id  board_id     body

 掲示板 A     ▲▲▲  1                  〇〇○が好き

 掲示板 A     ▲▲▲  1                  〇〇○は楽しい

 

メソッドについて

参照

【Rails】ransackを使って検索機能がついたアプリを作ろう! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

def index
@q = Board.ransack(params[:q])
@boards = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
end

 params[:q] : viewファイルから送られてくるパラメータ

ransackメソッド :送られてきたパラメータを元にテーブルからデータを検索するメソッド。(whereメソッドのransack版みたいな)

resultメソッド :ransackメソッドで取得したデータをActiveRecord_Relationのオブジェクトに変換するメソッド

 

<流れ>

❶ コントローラーを修正

(app/controllers/boards_controller.rb)

class BoardsController < ApplicationController

def index
@q = Board.ransack(params[:q])
@boards = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
end

def bookmarks
@q = current_user.bookmark_boards.ransack(params[:q])
@bookmark_boards = @q.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
end

 
end

 

@q = Board.ransack(params[:q])

の部分でransackというgemを利用してparamsで値を受け取っているが、この値が空である時はransackの機能でよしなに全件取得する。検索ランが空白の時は検索ボタンを押しても全件表示される。

 

❷ 検索フォームを作成

(app/views/boards/_search_form.html.erb)

<%= search_form_for @q, url: url do |f| %>
<div class='input-group mb-3'>
<%= f.search_field :title_or_body_cont, placeholder: t('defaults.search_word')
, class: 'form-control' %>
<div class='input-group-append'>
<%= f.submit value: "検索", class: 'btn btn-primary' %>
</div>
</div>
<% end %>

 

❸ index.html と bookmarks.html.erb で呼び出す

ローカル変数に

q : 検索オブジェクト (今回は@qを省略)

url: リクエストするURL

(app/views/boards/index.html.erb)

<!-- 検索フォーム -->
<%= render 'search_form', url: boards_path %>

 

(app/views/boards/bookmarks.html.erb)

<%= render 'search_form', url: bookmarks_boards_path %>

 

 

 

 

 

 

 

基礎編18

2021.4.2

掲示板のページネーション

kaminariを使用し、掲示板とブックマーク一覧にページネーションを実装する

1ページあたり20件を表示する

bootstrap4を使用する

参照

【Rails初心者】ページネーションを実装して自分好みにデザインを変える - Qiita

 

<ポイント>

自分で記載した時は、コントローラに直書きしてしまったが、kaminariの設定ファイルにページネーションのデフォルト値を記載した方が良い。

 

自分で記載した形✖️

(app/controller/boards_controller.rb)

@bookmark_boards = current_user.bookmark_boards.page(params[:page]).per(20)

 

良い形○

(app/controller/boards_controller.rb)

@bookmark_boards = current_user.bookmark_boards.includes(:user)
.order(created_at: :desc).page(params[:page])

(config/initializers/kaminari_config.rb)

Kaminari.configure do |config|
config.default_per_page = 20
 
end

 

<流れ>

❶ Gemfileにkaminariを追加し、インストールする。

 

「$ rails g kaminari:config 」コマンドでkaminariの設定を生成する

参照

【Rails】kaminariを使用してページネーション機能を実装 - Qiita

  • default_per_page
    • 1ページあたりの表示件数(デフォルトは25レコード)
  • max_per_page
    • 1ページあたりの最大表示件数(デフォルトはnil。つまり無限)
  • max_pages
    • 最大ページ数(デフォルトはnil
  • window
    • 現在のページから、左右何ページ分のリンクを表示させるか(デフォルトは4件)
  • outer_window
    • 最初(First)と最後(Last)のページから、左右何ページ分のリンクを表示させるか(デフォルトは0件)
  • left
    • 最初(First)のページから、何ページ分のリンクを表示させるか(デフォルトは0件)
  • right
    • 最終(Last)ページから、何ページ分のリンクを表示させるか(デフォルトは0件)
  • page_method_name
    • モデルに追加されるページ番号を指定するスコープの名前
  • param_name
    • ページ番号を渡すために使用するパラメータ名(デフォルトはpage)
    • @boards = Board.all.includes(:user).order(created_at: :desc)
      .page(params[:page])
      このようにparamsメソッドで取得できる。

 

❸ Bootstrapに合わせる

「$ kaminari:views bootstrap4 」 コマンドを実行する

 

❹コントローラーを修正する

    pageを使用する

(app/controller/boards_controller.rb)

class BoardsController < ApplicationController

def index
@boards = Board.all.includes(:user).order(created_at: :desc).page(params[:page])
end

def bookmarks
@bookmark_boards = current_user.bookmark_boards.includes(:user)
.order(created_at: :desc).page(params[:page])
end
 
end

 

❺ ページネーションの表示

   paginateを追加する

  (app/views/boards/bookmarks.html.erb)

<!-- 掲示板一覧 -->
<div class="row">
<div class="col-12">
<div class="row">
<% if @bookmark_boards.present? %>
<%= render @bookmark_boards %>
<% else %>
<p><%= t('.no_result') %></p>
<% end %>
</div>
<!--ページネーション-->
<%= paginate @bookmark_boards %>
</div>
</div>

 

(app/views/boards/index.html.erb)

<!-- 掲示板一覧 -->
<div class="row">
<div class="col-12">
<div class="row">
<div class="col-sm-12 col-lg-4 mb-3">
<% if @boards.present? %>
<%= render @boards %>
<% else %>
<p><%= t('.no_result') %></p>
<% end %>
</div>
<!--ページネーション-->
<%= paginate @boards %>
</div>
</div>
</div>