git, github超基礎

git、githubについてあまり分かってないまま学習してきたので、かなり初歩的な内容になると思いますがまとめてみます。

gitとは

gitとは分散型バージョン管理システムのことです。
分散型と集中型がありますが集中型についてはここでは扱いません。
バージョン管理とはいつ、誰が、どんな編集をしたか、その履歴を管理することです。
履歴ごとにセーブポイントを作って、どの地点にも戻ることができます。

githubとは

ソースコードを管理するWEBサービスです。

どのように管理するか

gitで作った履歴をgithubでオンラインで管理する、ということがわかりました。
では、もう少し具体的に見ていきたいと思います。

リポジトリ

ファイルの保管場所のことをリポジトリと言います。
プロジェクト単位でリポジトリを作ることが一般的なようです。
リポジトリ

に分かれます。
gitが用意してくれているサーバーに対してファイルをアップロード、ダウンロードすることでファイルの共有ができますが、このインターネット上に保存したリポジトリリモートリポジトリと言います。
githubアカウントでリモートリポジトリを作成し、自分のPCにコピーしたものがローカルリポジトリです。

クローン

リモートからローカルにリポジトリをコピーすることをクローンと言います。

git clone [URL]

でクローンできます。

コミット

クローンしたファイルで開発を進め、きりのいいところでセーブします。
このセーブのことをコミットと言います。

git status

というコマンドで変更したファイルの一覧が表示されるので、

git add [ファイル名]

で指定したファイルがgitの管理下に置かれます。この状態をステージングと言います。
ステージング状態ではまだセーブされていません。セーブするには

git commit -m 'コミットメッセージ '

とします。
コミットにはどんな変更を行ったかを端的に表すコミットメッセージを付けておきます。
これがないと後から戻ろうとしてもどのセーブポイントに戻ればいいのか分からなくなります。
また、コミット履歴を確認するには

git log

コマンドを実行します。

プッシュ

ローカルの変更箇所をセーブできましたが、この時点ではまだリモートリポジトリには反映されていません。
リモートに反映させることをプッシュと言います。

git push origin

originとはリモートリポジトリのことです。
このコマンドでリモートにも自分がコミットした内容が反映されます。

プル

最新のリモートリポジトリの情報をダウンロードすることをプルと言います。

git pull origin

このコマンドで、他の開発者がプッシュした内容をプルしてローカルに持ってきます。

ブランチ

ブランチとは作業場所のようなものです。
gitの変更履歴を枝分かれさせ、新しい作業場所を作ることをブランチを切る、と言います。
元となる作業場所はマスターと言います。

masterーーcommitーーcommitーーcommitーーcommitーーcommitーー
branch └ーーcommitーーcommit--commitーー

git checkout -b ブランチ名 master

checkout自体はブランチを移動するコマンドです。
-bと付けることで新しいブランチを作り、ブランチ名 master でmasterから新しいブランチに移動できます。
現在のブランチを確認するには

git branch

を実行します。

プルリクエス

ブランチを切って開発を進め、コミット、プッシュまでしたらgithubプルリクエストを行います。
プルリクエストとは、他の開発者にレビューしてもらい、変更内容がOKならマージしてください、という依頼です。

マージ

ブランチをマスターに合流させることをマージと言います。
マスターブランチにマージする際はマスターに移動して

git marge 合流させるブランチ名

とします。
マージするまではブランチのコミットはマスターには反映されません。
実務では初めのうちはマージすることはなさそうな気がしますがどうなんでしょう。
僕はまだ学習中なのでわかりませんが、マージ怖い、という印象です。

大まかな流れはこんな感じかな、と思います。

最後に

厳密には違う表現もあるかも知れませんが、大体のイメージは掴めるんじゃないかなと思います。
必須知識らしいのでちゃんと身につけたいですね。
では今回は以上です。ありがとうございました。

参考サイト

【Git入門】Git + Github使い方入門講座🐒Gitの仕組みや使い方を完全解説!パーフェクトGit入門! - YouTube

GitHubとは?覚えておくべき7つの基礎知識や使い方を解説 – IT業界、エンジニア、就活生、第二新卒、転職者、20代向け情報サイト

JavaScript DOM操作練習してみた

今回はJavaScriptのDOM操作を練習してみます。

DOMとは

DOM (Document Object Model) とは、すべての HTML または XML 文書を表現・操作する API です。 DOM はブラウザーで文書構造をノードのツリーとして読み込み、それぞれのノードを文書の一部 (例えば要素、テキスト文字列、コメント) として表します。引用元

この仕組みを使ってJavaScriptを使ってHTMLの要素を取得、操作します。

実践

早速やってみます。
まず適当にファイルを作りHTMLを書いていきます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Practice</title>
</head>
<body>

<p id="text">ここが変わります</p>
<button id="button">クリック</button>

</body>
</html>

ブラウザで開いてみるとこんな感じです。

Image from Gyazo

こうなりました。
まだhtmlしか書いてないので当然クリックしても何も起きません。

ここから、ボタンをクリックしたら「ここが変わります」の部分が変わるようにしていきます。
とりあえずJavaScriptのコードを書き込む場所を作ります。

<body>

<p id="text">ここが変わります</p>
<button id="button">クリック</button>

  <script>

  </script>
</body>

ちょっと練習する用なのでbodyタグの一番下にscriptタグを書いて直接htmlファイルに書きます。
script内をこのようにします。

  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const text = document.getElementById("text");
      const button = document.getElementById("button");

      button.addEventListener('click', () => {
        text.innerText = ('変わった?');
      });
    });
  </script>

すると

Image from Gyazo

変わりましたね。
ではscriptタグの中を見ていきましょう。
documentには現在表示しているページのすべてのHTMLやCSSJavaScriptなどの文章が入っています。
試しにscript内にconsole.log(document);を入れてみると

Image from Gyazo

コンソールにファイルの内容が表示されました。
addEventListenerはイベントを実行するメソッドです。
引数のDOMContentLoadedイベントは HTML ページの読み込みが完了しHTML のパース(解析)が完了してDOM ツリーの構築が完了した時点で発生するイベントです。
正直、まだあまり理解できていないのでおまじないだと思ってます。

さらに中を見ていきます。
getElementByIdでhtml要素を取得します。
何を頼りに要素を探すかというと、その名の通りIdです。
そのためhtml要素にはidを設定している必要があります。
const text = document.getElementById("text");で、textというidが付いている要素を取得してきて、textという定数に代入しています。
const button = document.getElementById("button");も同様です。
button.addEventListener('click', () => {定数buttonをクリックしたときにイベントが起きます。
text.innerHTML = ('変わった?');定数textを`innerHTMLで要素を置き換えています。

流れとしてはこんな感じです。
他にもドットインストールで簡単なゲームを作ったりすればどんなことができるかわかりやすいと思います。

最後に

初めは何をやってるのかさっぱりでしたがなんとか動きました。基本は取得→操作の流れなのでしっかり考えれば色々遊べそうですね。
では今回は以上です。ありがとうございました。

参考サイト

【JavaScript】DOMを理解してHTMLを操作してみよう | RAKUMAオンラインスクール
JavaScript入門 | Let'sプログラミング
【JavaScript入門】addEventListener()によるイベント処理の使い方! | 侍エンジニアブログ
MDN Web Docs
はじめてのJavaScript (全11回) - プログラミングならドットインストール

現在のページに応じてactiveクラスを付与する

メニューリストとかで現在のページに応じて色がついてたらサイトが見やすくなりますよね。

Boardsページにいる時

Image from Gyazo

Usersページにいる時

Image from Gyazo

こんな感じにしたいです。

今回はこれを実装していきます。
前回作ったアプリにBootstrapを導入してBoardのCRUD機能をつけておきました。
viewも最低限整えてます。

どう実装するかですが、現在のページに応じて、というところが肝になりそうです。
では何で判断すればいいか。
そのページがどのコントローラを通っているか、を判断できるようなメソッドを作れば実装できそうです。

ヘルパーメソッドを作成

app/helpers/application_helper.rbにヘルパーメソッドを定義します。

module ApplicationHelper
  def add_active(controller_name)
    if controller_name == params[:controller]
      return 'active'
    end
end

もしparamsで受け取っているcontrollerが引数で渡したcontroller_nameと一致していたら、文字列'active'を返す。
というメソッドです。

viewに記述

_menu.html.erb

<ul class="nav nav-pills">
  <li>
    <%= link_to 'User List', root_path, class: "nav-link #{add_active('users')}" %> <%# users_controllerを通っていたらadd_activeメソッドを適用 %>
  </li>
  <li>
    <%= link_to 'Board List', boards_path, class: "nav-link #{add_active('boards')}" %> <%# boards_controllerを通っていたらadd_activeメソッドを適用 %>
  </li>
</ul>

こんな感じです。
細かいですが変数展開を使っているのでclassの要素はダブルクォーテーション("")で囲みます。
僕はこれで30分程溶けました。

Image from Gyazo

できました。
ログと検証ツールでも

users_controllerを通っている時

Image from Gyazo

Image from Gyazo

boards_controllerを通っている時

Image from Gyazo

Image from Gyazo

想定通りになりました。

最後に

こういう便利ヘルパーメソッドを作ったり、リファクタリングしたりが苦手です。今回もいろいろ参考にさせてもらいました。引き出しを増やして最適なものを組み合わせる、という方向でも伸ばしたいです。
では今回は以上です。ありがとうございました。

参考サイト

Railsでアクティブなページに対応するタブのスタイル変える場合 - なんかの備忘録

コントローラ(controller) | Railsドキュメント

ransackの検索機能

今回はgem ransackで検索機能を実装します。
以前の記事でフリーワード検索を実装したのですが、今回は作成日での検索とセレクトボックスでの検索を実装していきます。
1つ前の記事で作ったscaffoldアプリを元にやっていきます。
このアプリではi18nenumenum_helpを導入済みです。
ではいってみましょう。

ransackをインストール

gemfileに

gem 'ransack'

として

bundle install

します。

コントローラー編集

コントローラーで検索結果を受け取れるようにします。

  def index
    # @users = User.all これを削除
    @q = User.ransack(params[:q]) #この2行を
    @users = @q.result(distinct: true) #追加します
  end

ransackメソッド送られてきたパラメーターを元にテーブルからデータを検索するメソッド
(params[:q])検索パラメータを取得
resultメソッドransackメソッドで取得したデータをオブジェクトに変換するメソッド
(distinct: true)検索結果の重複を取り除く

viewの編集

ついでにviewをちょっと見やすく編集します。

users/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Gender</th>
      <th>Created_at</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <%= render @users %>
  </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>
users/_user.html.erb

<tr>
  <td><%= user.name %></td>
  <td><%= user.gender_i18n %></td>
  <td><%= user.created_at %></td>
  <td><%= link_to 'Show', user %></td>
  <td><%= link_to 'Edit', edit_user_path(user) %></td>
  <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>

ユーザー情報部分を部分テンプレートとして切り出して、作成日時も表示されるようにしました。

Image from Gyazo
こんな感じです。

検索フォームの作成

セレクトボックスで検索

まずはセレクトボックスで、性別で検索できるようにします。

users/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Users</h1>

<%= search_form_for @q do |f| %>
  <%= f.select :gender_eq, User.genders_i18n.invert.map{|key, value| [key, User.genders[value]]} %>
  <%= f.submit %>
<% end %>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Gender</th>
      <th>Created_at</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <%= render @users %>
  </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>

search_form_forransackが提供するメソッド
f.selectのところがややこしいので1つ1つ見ていきます。
まず第一引数の:gender_eqですが、選択した性別と等しいものを探すという意味です。eqはイコール(equal)の略ですね。
そしてややこしい第二引数。
User.genders_i18n.invertまでは前回の記事を参照してください。

irb(main):001:0> User.genders_i18n.invert
=> {"男性"=>"male", "女性"=>"female", "秘密"=>"secret"}
irb(main):002:0> User.genders_i18n.invert.map{|key,value|[key,User.genders[value]]}
=> [["男性", 0], ["女性", 1], ["秘密", 2]]

この2つを比べてみます。
違いは

  • ハッシュから配列に変わっている
  • male, female, secretから0, 1, 2に変わっている

の2点、違いがあります。
ハッシュから配列に変わっているのはmapメソッドによるものです。
mapメソッドは、配列orハッシュ.map {|変数名| 実行する処理 }で戻り値を配列で返します。
今回のややこしいところに当てはめてみると、

irb(main):001:0> User.genders_i18n.invert
=> {"男性"=>"male", "女性"=>"female", "秘密"=>"secret"}

これを配列の形になおして欲しい。
配列の中身は|key, value|という形にしてくれ。
keyはそのままkey("男性"=>"male"の"男性")、
valueはUser.genders[value]つまり

irb(main):003:0> User.genders
=> {"male"=>0, "female"=>1, "secret"=>2}

valueの部分("male"=>0の0)
つまり最終的な形が

irb(main):002:0> User.genders_i18n.invert.map{|key,value|[key,User.genders[value]]}
=> [["男性", 0], ["女性", 1], ["秘密", 2]]

になる、という流れです。
これでセレクトボックスによる検索が実装できました。
Image from Gyazo

最後に指定なしでも検索できるように

<%= f.select :gender_eq, User.genders_i18n.invert.map{|key, value| [key, User.genders[value]]}, include_blank: '指定なし' %>

include_blankを設定しておきます。
複数条件で検索する時に必要です。

作成日で検索

次は作成日での検索です。
例えば5/1の00:00から5/10の23:59で検索したいとします。

<%= search_form_for @q do |f| %>
  <%= f.select :gender_eq, User.genders_i18n.invert.map{|key, value| [key, User.genders[value]]}, include_blank: '指定なし' %>
  <%= f.date_field :created_at_gteq %>
  <span></span>
  <%= f.date_field :created_at_lteq %>
  <%= f.submit %>
<% end %>

date_field日付の入力欄を生成
gteqgreater than equalの略
lteqless than equalの略
created_at_gteqで指定した日付以降
created_at_lteqで指定した日付まで

Image from Gyazo

こんな感じで日付で検索できます。
しかし実際に検索してみると

Image from Gyazo

5/10の15:00:00に作成したyamadaさんが表示されていません。
created_at_lteqだと、その日の00:00までの範囲で検索してしまうからです。
これを23:59までにするにはpredicateのカスタマイズが必要です。

predicateのカスタマイズ

まずconfig/initializers/ransack.rbを作成します。

Ransack.configure do |config|
  config.add_predicate 'lteq_end_of_day', # 名前を付ける
                       arel_predicate: 'lteq', # 'lteq'をカスタマイズしますよ
                       formatter: proc { |v| v.end_of_day } # end_of_dayメソッドを実行
end

これでカスタマイズできたので検索フォームを修正してみます。

<%= search_form_for @q do |f| %>
  <%= f.select :gender_eq, User.genders_i18n.invert.map{|key, value| [key, User.genders[value]]}, include_blank: '指定なし' %>
  <%= f.date_field :created_at_gteq %>
  <span></span>
  <%= f.date_field :created_at_lteq_end_of_day %>
  <%= f.submit %>
<% end %>

これで

Image from Gyazo

想定通りの検索ができました。

最後に

自分的には難しい内容でした。コンソールを使ったりして流れを追って、なんとかやってることはわかった、ぐらいの感じです。
このロジックを自分で考えるのはちょっと今の理解度では厳しいな、と思いました。アウトプットを続けて知識を定着させていきたいです。
あと参考にしたサイトとかいつも載せてなかったんですが載せないとダメですよね、忘れてました。すいません。
今日は疲れたので次回から載せます…
最後まで読んでいただきありがとうございました。間違いなどありましたらコメントいただけますと幸いです。

enum_help導入

制作中の掲示板アプリにenum_helpを導入しました。
ちゃんと定着させるためにもう一度実際に手を動かしながらやってみようと思います。

今回やりたいのは

フォームにセレクトボックスを作り選択肢を日本語で表示する

です。

Image from Gyazo
こんな感じの作ります。
i18nenumenum_helpを使って実装したいと思います。
rails newからやったので順番にいきましょう。

新規アプリ作成

rails new sample_app

名前は適当にsample_appで作成します。

cd sample_app

で移動して

rails g scaffold user name:string gender:integer

scaffoldで雛形を作り、userモデルを作成。カラムは名前と性別を作ります。
性別(gender)はinteger型にしておきます。
enum用のカラムはinteger型かboolean型かのどちらかです。
integerなら整数、booleanなら真偽値(true, false)でDBに保存されます。
選択肢が2択ならboolean、それより多い、今後増える、とかの場合はintegerがいいのかなと思います。
マイグレーションファイルを

class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.integer :gender, default: 0, null: false

      t.timestamps
    end
  end
end

このように編集して

rails db:migrate

します。

gemのインストール

必要なgemをインストールしましょう。
gemfileに

gem 'rails-i18n'
gem 'enum'
gem 'enum_help'

として

bundle install

これでインストールできたかと思います。

enumの定義

userモデルでenumの定義をします。

# user.rb

enum gender: { male: 0, female: 1, secret: 2 }

i18nの設定

config/application.rbに必要な設定を記述します。

require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module SampleApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.

    # これを追加
    config.i18n.default_locale = :ja #デフォルト言語を日本語に設定
  end
end

config/locales/ja.ymlを作成し、

ja:
  enums:
    user:
      gender:
        male: 男性
        female: 女性
        secret: 秘密

翻訳情報を作成します。

viewを整形

viewを整えます。

users/_form.html.erb

<%= form_with(model: user, local: true) do |form| %>
  <% if user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

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

  <div class="field">
    <%= form.label :gender %>
    <%= form.number_field :gender %> <%#この行を削除 %>
    <%= form.select :gender, User.genders_i18n.invert %> <%# この行を追加 %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

User.gendersでuserモデルのgenderの値をハッシュで取得します。

irb(main):001:0> User.genders
=> {"male"=>0, "female"=>1, "secret"=>2}

これをUser.genders_i18nとすると

irb(main):002:0> User.genders_i18n
=> {"male"=>"男性", "female"=>"女性", "secret"=>"秘密"}

ja.ymlファイルで設定した日本語が適用されました。
このカラム名(複数形)_i18nenum_helpで使えるようになります。
しかしこれだと
Image from Gyazo
選択肢は英語のままになっています。
ハッシュのkeyの方が取得されていますね。これに

irb(main):003:0> User.genders_i18n.invert
=> {"男性"=>"male", "女性"=>"female", "秘密"=>"secret"}

invertメソッドを付けるとkeyとvalueを入れ替えることができます。
これで再度見てみると
Image from Gyazo
日本語になりました。

最後に

無事実装できました。使用頻度が高そうな気がするのでしっかり押さえたいです。
以上となります。読んでいただきありがとうございました。

Admin-LTEで管理者用ページを実装

今回はAdmin-LTEを使って管理者用ページを作りたいと思います。
Admin-LTEとは管理画面に特化したBootstrapベースのCSSフレームワークです。
管理者用なので凝ったデザインにする必要はないですよね。なのでフレームワークを使ってサクッと作ってしまいたいです。

インストール・設定

yarnでインストールしました。

yarn add admin-lte

インストールするとnode_modulesというディレクトリが作成されます。
今回はこの中のadmin-lte/starter.htmlを見本に作ってみます。
Admin-LTEのページでいろんなテンプレートが見れるのでここから好みのデザインを選ぶのもいいと思います。
Demoでテンプレートを開いて検証ツールを開きます。
CSSheadタグの中 Image from Gyazo JavaScriptbodyタグの中 Image from Gyazo
にあるので、app/assets内で必要なものを読み込みます。
管理者用ページとユーザーが使うページのデザインが異なるのでapplication.jsやapplication.scssではなく、新たに管理者用ページのマニフェストファイルadmin.jsadmin.scssを作成し、そこに記述します。

// app/assets/javascript/admin.js

//= require jquery3
//= require rails-ujs
//= require admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js
//= require admin-lte/dist/js/adminlte.min.js
/* app/assets/stylesheets/admin.scss */

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

application.js内の//= require_tree .は全てのjsファイルを読み込んでしまうので、view毎にデザインを変えたい場合は削除します。

また、application.js、application.scssはデフォルトで読み込んでくれますが、それ以外のマニフェストファイルは設定しないといけないので

# config/initializers/assets.rb

# 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 admin.css]  # コメントアウトを外す

とします。

Controllerを作成

管理機能を持つコントローラを作ります。
管理用に階層を分けた方が管理しやすそうです。

rails g controller Admin::Base

でApplicationControllerを継承したAdmin::BaseControllerを作成し、Admin系コントローラの基幹とします。
さらにこのAdmin::BaseControllerを継承したコントローラで管理者ページの機能を作ります。
Image from Gyazo
階層はこんな感じです。
admin/user_sessions_controller.rbとadmin/dashboards_controller.rbは

class Admin::UserSessionsController < Admin::BaseController
 略
end
class Admin::DashboardsController < Admin::BaseController
 略
end

として基幹となるAdmin::BaseControllerを継承します。
また、一般ユーザー用のページとは異なるデザインを当てるため、

class Admin::BaseController < ApplicationController
  layout 'admin/layouts/application'
 略
end

layoutメソッドで使用するlayoutファイルを指定します。

Viewを作成

viewの中身は省略しますが、前述の通りnode_modules/admin-lte/starter.htmlをコピペして不要な部分を削除しました。 layoutファイルでは

<%= stylesheet_link_tag 'admin', media: 'all' %>
<%= javascript_include_tag 'admin' %>

読み込むCSSJavaScriptadminと指定します。

ルーティングの設定

管理者用ページのpathはadmin/loginのようにしたいので

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

namespaceを使います。すると
Image from Gyazo
ルーティングは/admin/○○のようになります。

enumの定義・管理者かどうかの判別をするカラムを作成

enumとは簡単に言うと名前に数値を割り当てることです。
user.rbでUserモデルにenumの定義をしておきます。

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

これだけです。

Userモデルで管理者かそうでないかの判別をするroleカラムを作ります。roleとは役割という意味です。

rails g migration add_role_to_users

で作られたマイグレーションファイルに

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

としてrails db:migrateします。
enumの定義で数字を割り当てたので型はintegerです。
一般か管理者か、必ずどちらか割り当てるためnull: false
デフォルトは一般にしたいのでdefault: 0
とします。
僕はnullとdefaultのオプションを付けずに作ってしまって、後から

rails g migration change_column_to_users
class ChangeColumnToUsers < ActiveRecord::Migration[5.2]
  def change
    change_column_null :users, :role, false
    change_column_default :users, :role, from: nil, to: "0"
  end
end

としようとしたらエラーが出て追加できませんでした。
レコードを全削除したら無事追加できたんですが良い手ではないのでまた調べたいです。
まだ機能が全然ないですがとりあえず管理者用ページができました。

最後に

change_columnできなかったところや管理ツール(yarnなど)のこと、アセットパイプラインなどやればやるほど分からないことが出てきて沼です。
ブログのネタができて嬉しいなぁ…1つ1つ、潰していきたいです。

以上です、ありがとうございました。

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

ユーザー登録ができるサービスには必ず付いていると言っても過言ではないであろう機能、パスワードリセット機能を実装していきます。

今回はgem sorceryのモジュールであるreset_passwordを導入します。sorceryの導入を前提で進めていきます。

パスワードリセットの流れ

  • パスワードリセット申請ページからメールアドレスを送信
  • メールを受信しリンクをクリック
  • パスワードリセットページでパスワードを更新

という感じです。
開発環境ではメールを実際には送らないようletter_opener_webというgemを使ってますがそちらは割愛させていただきます。
にではいってみましょう。

reset_passwordモジュールのインストール

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

でインストール。
マイグレーションファイルが作成されます。
トークンは一意でなければいけないのでreset_password_tokenカラムにindex制約をかけておきます。 トークンとはワンタイムパスワードのようなものです。

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_index :users, :reset_password_token # これを追加
  end
end

として

rails db:migrate

DBに反映。

モデルの方でもvalidationを設定しておきます。

# user.rb

validates :reset_password_token, uniqueness: true, allow_nil: true

allow_nilオプションは、対象の値がnilの場合にバリデーションをスキップします。
パスワードが変更される際にreset_password_tokenは削除されnilになります。
allow_nil: trueがないとユニーク制約にに引っかかってしまうのでこちらも設定します。

Mailerの作成

メイラーとはRailsからメールを送信する機能です。
まずは

$ rails g mailer UserMailer reset_password_email

でUserMailerを作成します。
rails g mailer メーラー名 メソッド名です。
共通の設定

# application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  default from: 'example@example.com' # メールの送信元
  layout 'mailer' # mailer.html.erb、mailer.text.erbをメールのレイアウトとして使用する
end


メール送信のメソッドを定義します。

# user_mailer.rb

class UserMailer < ApplicationMailer
  def reset_password_email(user)
    @user = User.find(user.id)
    @url = edit_password_reset_url(@user.reset_password_token)
    mail to: user.email,
         subject: 'パスワードリセット'
  end
end

mailメソッド送信先、メールのタイトルを設定しています。

sorcery.rbの設定もしておきます。

Rails.application.config.sorcery.submodules = [:reset_password] # サブモジュールとしてreset_passwordを設定

Rails.application.config.sorcery.configure do |config|
  config.user_config do |user|
    user.reset_password_mailer = UserMailer # パスワードリセット用のメーラーにUserMailerを指定
  end
end

メール本文を作成

views/user_mailer/reset_password_email.text.erbviews/user_mailer/reset_password_email.html.erbに実際のメールの内容を記述します。
一部のメールソフトではhtml形式のメールが受け取れない(ガラケーとか?)などがあるようで、その対策としてtext形式とhtml形式の2つで作成します。
Railsではマルチパートメールという仕組みでtext形式とhtml形式の両方をメール送信して、メールクライアント側で自動判別してメールを表示してくれるそうです。
また、メンテナンス等の際、両方ともメンテしなくてもいいようにactionmailer-textというgemもあるようです。
ここでは取り扱わないので興味のある方は調べてみてください。

コントローラーの設定

# app/controllers/password_resets_controller.rb

class PasswordResetsController < ApplicationController
  skip_before_action :require_login
    
  def create 
    @user = User.find_by_email(params[:email])
        
    @user.deliver_reset_password_instructions! if @user
        
    redirect_to(root_path, :notice => 'Instructions have been sent to your email.')
  end
    
  def edit
    @token = params[:id]
    @user = User.load_from_reset_password_token(params[:id])

    if @user.blank?
      not_authenticated
      return
    end
  end
      
  def update
    @token = params[:id]
    @user = User.load_from_reset_password_token(params[:id])

    if @user.blank?
      not_authenticated
      return
    end

    @user.password_confirmation = params[:user][:password_confirmation]
    if @user.change_password(params[:user][:password])
      redirect_to(root_path, :notice => 'Password was successfully updated.')
    else
      render :action => "edit"
    end
  end
end

フォームの作成

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

<%= form_with url: password_resets_path, local: true do |f| %>
    <%= f.label :email, 'メールアドレス' %>
    <%= f.email_field :email, class: 'form-control' %>

  <%= f.submit class: 'btn btn-primary' %>
<% end %>

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

<%= form_with model: @user, url: password_reset_path(@token), local: true do |f| %>
    <%= f.label :email %><br>
    <%= @user.email %>

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

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

      <%= f.submit class: 'btn btn-primary ' %>
<% end %>

ルーティングの設定

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

を追加。 以上です。

最後に

今まで使ったことなかったMailerとかが出てきて難しかったです。
githubからコピペしたのでコントローラ内のコードの流れが正直まだわかってません。 また振り返って流れを追いたいと思います。
間違いなどありましたらコメントいただけると幸いです。
読んでいただきありがとうございました。