管理画面の作成

[管理画面の作成]

管理画面を作成するのに今回は、AdminLTEというパッケージ管理ツールを用いる。
[AdminLTE]をインストールするのにyarnからインストールする。
Gemからもインストールできるが、Gemだとメンテナンスが追いついていなく、バージョンも古いバージョンしかインストールできない為。
JavascriptパッケージのリポジトリRailsに限らず、様々な言語で利用されるためメンテナンスも頻繁に行われる。
Gemとパッケージ管理ツールで利用されている数を比較するのにGitHubの右上の星の数を比較するのが良い。
Image from Gyazo

※ ・yarn: node.jsのパッケージマネージャ
・node.js: サーバーサイドで使えるJavaScriptの1種
・パッケージマネージャ: 言語の各パッケージのインストール・アンインストールを行うツール。インストールやアップグレードの際には、依存関係にあるパッケージも自動でインストール・アップグレートしてくれる。(Gemは、Rubyのパッケージマネージャ)
・AdminLTE: Bootstrap3をベースにした、管理画面等のテンプレートテーマ

[AdminLTE]のインストール手順と使い方

下記で[AdminLTE]をインストールする。
$ yarn add admin-lte@^3.0 ※yarnを使ってのインストールになる為、yarnをインストールしていない時はyarnのインストールが必要になる。


上記で[AdminLTE]をインストールすると下記のような[node_modules]ディレクトリ以下にファイルがダウンロードされる。
Image from Gyazo


今回は、管理画面へのログイン画面とログイン後の画面を作成する。
上記で[AdminLTE]をインストールして、ディレクトリやファイルがたくさんあるが[node_modules/admin-lte]ディレクトリに[AdminLTE]関連のファイルがある。
下記のようなログイン後の管理画面のHTMLは、[node_modules/admin-lte/starter.html]に記載がある。

Image from Gyazo

管理画面のイメージ図は、https://adminlte.io/themes/v3/starter.htmlから確認できる。


管理系の事は、管理ディレクトリで管理したい為、ネームスペースを使ってコントローラを作成する
$ rails g controller admin::base $ rails g controller admin::dashboards $ rails g controller admin::user_sessions 上記で管理画面全体の設定をする[app/controllers/admin/base_controller.rb]
管理画面へのログイン処理をする[app/controllers/admin/user_sessions_controller.rb]
管理画面ログイン後の画面の処理をする[app/controllers/admin/dashboards_controller.rb]
を作成する。


[app/controllers/admin/base_controller.rb]は、以下のように記述した。

[app/controllers/admin/base_controller.rb]

class Admin::BaseController < ApplicationController
  layout 'admin/layouts/application'
  before_action :check_admin

  private

  def not_authenticated
    redirect_to admin_login_url, info: (t 'defaults.please_login')
  end

  def check_admin
    return if current_user.admin?

    redirect_to root_path, warning: (t 'admin.user_sessions.create.no_admin_message')
  end
end

[app/controllers/admin/base_controller.rb]は、以下のApplicationControllerの内容を継承している。
[not_authenticated]メソッド(sorcery Gemのログインしていなかった時に行われるメソッド)は、親クラスと同じメソッド名でオーバーライドしているので、管理画面でログインしていなかった時は、[app/controllers/admin/base_controller.rb]の[not_authenticated]メソッドが実行される。
管理画面へのログインは、通常のログインと異なり管理者でなければログインできないようにしなければいけないため、[check_admin]メソッドで現在のユーザーが管理者かどうか判定している。

[app/controllers/application_controller.rb]

class ApplicationController < ActionController::Base
  add_flash_types :success, :info, :warning, :danger
  before_action :require_login

  private

  def not_authenticated
    redirect_to login_url, info: (t 'defaults.please_login')
  end
end

管理画面へのログイン処理をするコントローラは、以下のように記述した。

[app/controllers/admin/user_sessions_controller.rb]

class Admin::UserSessionsController < Admin::BaseController
  skip_before_action :require_login, only: %i[new create]
  skip_before_action :check_admin, only: %i[new create]
  layout 'admin/layouts/login'
  def new; end

  def create
    @user = login(params[:email], params[:password])
    if @user
      redirect_to admin_root_url, success: (t 'admin.user_sessions.create.success_message') # 管理者か一般ユーザーかの判定無しに、管理画面へリダイレクトしているが管理画面へは[base_controller.rb](管理者かどうかを判定している)を継承している[dashboards_controller.rb]が担当しているので、一般ユーザーがログインに成功して管理者画面へリダイレクトしても[dashboards_controller.rb]で管理者かどうかを最初に判定しているので、弾かれる。
    else
      flash.now[:danger] = (t 'admin.user_sessions.create.fail_message')
      render :new
    end
  end

  def destroy
    logout
    redirect_to admin_login_url, success: (t 'admin.user_sessions.destroy.message')
  end
end

[app/controllers/admin/user_sessions_controller.rb]は、[app/controllers/admin/base_controller.rb]を継承している。
管理画面へのログイン画面の表示は、一般ユーザーだろうが管理者だろうが表示できるようにしたいので、[skip_before_action]を設定している。

管理画面の中の処理をするコントローラは、以下のように記述した。

[app/controllers/admin/dashboards_controller.rb]

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

[app/controllers/admin/dashboards_controller.rb]は、[app/controllers/admin/base_controller.rb]を継承している。
管理画面の中へは、管理者であって且つログインしている状態でないと入れないようにしたい為、before_action :require_loginbefore_action :check_adminを設定している。

管理者画面は、一般ユーザーと画面のデザインが違うので一般ユーザーとは違うレイアウトファイルを使用するため、下記のように記載している。

[app/controllers/admin/base_controller.rb]

before_action :check_admin

管理者用のログイン画面のデザインは、管理者画面の中のデザインと異なるため、レイアウトファイルをオーバーライドする事でレイアウトファイルを変えている。

[app/controllers/admin/user_sessions_controller.rb]

layout 'admin/layouts/login'

管理画面の中のデザイン
Image from Gyazo

管理者用のログイン画面
Image from Gyazo

・管理者用のログイン画面で一般ユーザーがログイン: 一般ユーザー用のトップページへリダイレクト
・管理者用のログイン画面で管理者がログイン: 管理者画面の中へリダイレクト
・管理者用のログイン画面でログインが失敗: 管理者用のログイン画面へレンダー
・未ログインの状態で管理者画面の中へ入るのに直接URLを入力: 管理者用のログイン画面へリダイレクト

※ レイアウトファイル: 各ビューファイルにはbodyタグの中身を記述するだけでWEBページを表示できるようにしているのがレイアウトファイル。レイアウトファイルのbodyタグの中のyieldで各ビューファイルの中身を読み込み画面を表示している。レイアウトファイルにheadタグなどを記載している。


レイアウトファイルは、以下のように記述している。

[app/views/admin/layouts/application.html.erb]

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta lang='ja'>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title><%= page_full_title(yield(:title), admin: true) %></title>
  <%= csrf_meta_tags %>     # クロスサイトリクエストフォージェリ対策で記述されている。
  <%= csp_meta_tag %>       # コンテンツセキュリティポリシー(Content Security Policy: CSP)を実装
  <%= stylesheet_link_tag    'admin', media: 'all' %>
  <%= javascript_include_tag 'admin' %>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
</head>

<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
  <%= render 'admin/shared/header' %>
  <%= render 'admin/shared/menu' %>

  <!-- Content Wrapper. Contains page content -->
  <div class="content-wrapper">
    <%= render 'shared/flash_message' %>
    <%= yield %>
  </div>
  <!-- /.content-wrapper -->

  <%= render 'admin/shared/footer' %>
</div>

</body>
</html>
[app/views/admin/layouts/login.html.erb]

<html>
  <head>
    <meta charset="utf-8">
    <title><%= page_full_title(yield(:title), admin: true) %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'admin', media: 'all' %>
    <%= javascript_include_tag 'admin' %>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
  </head>

  <body>
    <div class="hold-transition login-page">
      <%= render 'shared/flash_message' %>
      <%= yield %>
    </div>
  </body>
</html>

下記記載は、マニフェストファイル[app/assets/stylesheets/admin.scss]に記載したものを読み込んでいる。

<%= stylesheet_link_tag    'admin', media: 'all' %>
[app/assets/stylesheets/admin.scss]

@import "admin-lte/plugins/fontawesome-free/css/all.min.css";     # AdminLTEの[starter.html]や[login.html]に記載があるため。
@import "admin-lte/plugins/icheck-bootstrap/icheck-bootstrap.min.css";    # AdminLTEの[login.html]に記載があるため。
@import "admin-lte/dist/css/adminlte.min.css";    # AdminLTEの[starter.html]や[login.html]に記載があるため。

下記記載は、マニフェストファイル[app/assets/javascripts/admin.js]に記載したものを読み込んでいる。

<%= javascript_include_tag 'admin' %>
[app/assets/javascripts/admin.js]

//= require jquery3   #  Bootstrapと依存関係にあるため。
//= require popper   #  Bootstrapと依存関係にあるため。
//= require bootstrap-sprockets   #  Bootstrapと依存関係にあるため。
//= require rails-ujs   # link_toのmethodオプションが使えなくなるため。(これにハマったので注意!!)
//= require admin-lte/plugins/jquery/jquery.min.js   # # AdminLTEの[starter.html]や[login.html]に記載があるため。
//= require admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js   # # AdminLTEの[starter.html]や[login.html]に記載があるため。
//= require admin-lte/dist/js/adminlte.min.js   # # AdminLTEの[starter.html]や[login.html]に記載があるため。

※ポイント
マニフェストファイルに記載した[admin-lte/dist/js/adminlte.min]をどのように探しているのか?(通常は、[node_modules/admin-lte/dist/js/adminlte.min]ではないのか)
これにはRails.application.config.assets.pathsが関係している。
上記コマンドをrails cした画面で打ってみると以下のように出てくる。
[node_modules/]のパスも以下に出てきている。
Image from Gyazo 上記の出力されたパスの先頭から順に検索([admin-lte/dist/js/adminlte.min]があるか?)する。
上記のパス指定は、[config/initializers/assets.rb]で以下のように記述して追加している。

[config/initializers/assets.rb]

Rails.application.config.assets.paths << Rails.root.join('node_modules')
[app/assets/javascripts/application.js]

//= require jquery3   #  Bootstrapと依存関係にあるため。
//= require popper   #  Bootstrapと依存関係にあるため。
//= require bootstrap-sprockets  #  Bootstrapと依存関係にあるため。
//= require rails-ujs    # link_toのmethodオプションが使えなくなるため。(これにハマったので注意!!)
//= require activestorage
//= require_tree .    # 同じ階層にあるファイルを全て読み込む(読み込む順序は指定できない為、利用しているJSファイルが特定の読み込み順に依存している場合不都合が生じる。)

[app/assets/javascripts/application.js]に上記のように記載されているが、今回は使用するレイアウトファイル毎に読み込むJavaScriptsファイルを変えたいため、下記のように[require_tree .]の=を外してコメントアウトにする。

[app/assets/javascripts/application.js]

//= require jquery3
//= require popper
//= require bootstrap-sprockets
//= require rails-ujs
//= require activestorage
// require_tree .

Image from Gyazo

[require_tree .]の自動読み込みを使わないことにより手動でのプリコンパイルが必要になります。
手動でのプリコンパイルの設定は、以下のようになります。
下記記述のようにプリコンパイルするマニフェストファイル([admin.js],[admin.css])を記載するのは、デフォルトだと[application.scss]や[application.js]のマニフェストファイルしかプリコンパイルされないため。

[config/initializers/assets.rb]

Rails.application.config.assets.precompile += %w[admin.js admin.css]

※ ・マニフェストファイル: [application.css]、[application.js]の様な、どのアセットを読み込むか指定するファイル
・アセット: [javascript]、[スタイルシート]、[画像ファイル]といったHTML に付随したファイル
・プリコンパイル: コンパイルできる状態にして配置する的な意味


以下の管理画面とログイン画面をベースに作成したいので、管理画面は、[node_modules/admin-lte/starter.html]をコピーして不要な部分を削除し、ヘッダー、フッター、サイドバーの部分を共通の部分テンプレートとして切り分け、レイアウトファイル[app/views/admin/layouts/application.html.erb]から部分テンプレートを読み込む。
管理画面の中の本文部分は、[app/views/admin/dashboards/index.html.erb]に作成する。
ログイン画面は、[node_modules/admin-lte/pages/examples/login.html]を[app/views/admin/user_sessions/new.html.erb]にコピーして不要な部分を削除し、[app/views/admin/layouts/login.html.erb]のレイアウトファイルから読み込む。
各部分テンプレートとビューファイルは、以下のようになる。

管理画面
Image from Gyazo

ログイン画面
Image from Gyazo

[app/views/admin/dashboards/index.html.erb]

<% content_for(:title, (t 'admin.dashboards.index.title')) %>
<div class="container">
  <div class="row">
    ダッシュボードです
  </div>
</div>
[app/views/admin/shared/_header.html.erb]

<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="#" role="button"><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 %>
  </li>
</ul>
</nav>
[app/views/admin/shared/_header.html.erb]

<footer class="main-footer">
  <strong>Copyright &copy</strong> All rights reserved.
</footer>
[app/views/admin/shared/_menu.html.erb]

<aside class="main-sidebar sidebar-dark-primary elevation-4">
<!-- Brand Logo -->
<a href="index3.html" class="brand-link">
  <img src="https://runteq-board.herokuapp.com/assets/AdminLTELogo-92af06833886bd48cb14b00faa6d70220b3eb7a651f12c5d6f38501ac910dd6b.png" alt="AdminLTE Logo" class="brand-image img-circle elevation-3" style="opacity: .8">
  <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.profile_image.url, class: "img-circle elevation-2", alt: "User Image" %>
    </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">
      <!-- Add icons to the links using the .nav-icon class
           with font-awesome or any other icon font library -->
      <li class="nav-item menu-open">
        <ul class="nav nav-treeview">
          <li class="nav-item">
            <a href="#" class="nav-link">
              <i class="far fa-file nav-icon"></i>
              <p>掲示板一覧</p>
            </a>
          </li>
        </ul>
      </li>
      <li class="nav-item">
        <a href="#" class="nav-link">
          <i class="nav-icon far fa-user"></i>
          <p>
            ユーザー一覧
          </p>
        </a>
      </li>
    </ul>
  </nav>
  <!-- /.sidebar-menu -->
</div>
<!-- /.sidebar -->
</aside>
[app/views/admin/user_sessions/new.html.erb]

<% content_for(:title, (t 'admin.user_sessions.new.title')) %>
  <div class="login-box">
    <div class="login-logo">
      <a href="../../index2.html"><h1><%= (t "defaults.login") %></h1></a>
    </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.email_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>
            <%= f.submit (t "defaults.login"), class: "btn btn-primary btn-block" %>
        <% end %>
      </div>
    </div>
  </div>


Userモデルに管理者か一般ユーザーかの判断するカラムを追加する(カラムは、integer型でenumを利用する)

$ rails g migration AddRoleUsers


以下のようにマイグレーションファイルを作成

[app/db/migrate ファイル]

  def change
    add_column :users, :role, :integer, default: 0, null: false   # [null: false]で空値を許容していないので、[default: 0]でデフォルトの設定をする
  end


$ rails db:migrate


管理者か一般ユーザーかの判定カラムは、enumを使うので以下のように設定する。

[app/models/user.rb]

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

enumは、上記の場合DBに実際に保存されるのは[0と1]だがインスタンスの内容は、[generalとadmin]になる
DBに[general]や[admin]のハッシュのkeyを保存しようとすると、実際にはDBにはハッシュのvalueが保存される
enumには、ハッシュで設定した値以外が保存されないというメリットもある
Image from Gyazo


タイトルの表示を管理者画面か一般ユーザー画面かで切り替えるのに以下のように記述している

[app/helpers/application_helper.rb]

  def page_full_title(page_title = '', admin = false)
    base_title = if admin
                   'RUNTEQ BOARD APP(管理画面)'
                 else
                   'RUNTEQ BOARD APP'
                 end
    page_title.empty? ? base_title : page_title + ' | ' + base_title
  end
[app/views/admin/layouts/application.html.erb]

  <title><%= page_full_title(yield(:title), admin: true) %></title>


ルーティングは、以下のように設定している

[config/routes.rb]

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

Image from Gyazo

※注意
ビュー毎にレイアウトファイルを変えて、指定したビューファイルがレンダーされなくてハマったこと

[app/controllers/admin/user_sessions_controller.rb]

class Admin::UserSessionsController < Admin::BaseController
  skip_before_action :require_login, only: %i[new create]
  skip_before_action :check_admin, only: %i[new create]
  def new
    render layout: 'admin/layouts/login.html.erb'  ②
  end

  def create
    @user = login(params[:email], params[:password])
    unless @user
      flash.now[:danger] = (t 'admin.user_sessions.create.fail_message')
      render :new  ①
      return
    end
    if @user&.role == 'admin'
      redirect_to admin_url, success: (t 'admin.user_sessions.create.success_message')
    elsif @user&.role == 'general'
      redirect_to root_url
    end
  end

  def destroy
    logout
    redirect_to admin_login_url, success: (t 'admin.user_sessions.destroy.message')
  end
end
[app/controllers/admin/base_controller.rb]

class Admin::BaseController < ApplicationController
  layout 'admin/layouts/application'
  add_flash_types :success, :info, :warning, :danger
  before_action :check_admin

  private

  def not_authenticated
    redirect_to admin_login_url
  end

  def check_admin
    unless current_user.admin?
      redirect_to root_path
    end
  end
end

上記のように記述していて①のログインした時にログイン情報に該当するユーザーが存在せず、ログイン画面(newアクション)にレンダーを記述しているのに管理画面の中に入れてしまう
Image from Gyazo

ログを確認すると以下のようになっている。

  Rendering admin/user_sessions/new.html.erb within admin/layouts/application
  Rendered shared/_flash_message.html.erb (0.4ms)
  Rendered admin/user_sessions/new.html.erb within admin/layouts/application (22.8ms)
  Rendered admin/shared/_header.html.erb (1.3ms)
  Rendered admin/shared/_menu.html.erb (0.6ms)
  Rendered shared/_flash_message.html.erb (0.1ms)
  Rendered admin/shared/_footer.html.erb (0.3ms)

ログからも分かるように、レイアウトファイルがapplicationファイルが読み込まれている。(ログイン画面のレイアウトファイルは、②でも指定しているようにloginなはず)
これは、ログイン画面(newアクション)をレンダー(レンダーは、アクションを通らない)しているのでアクションを通らない事により、[Admin::UserSessionsControlle]は[Admin::BaseController]を継承しているので、[Admin::BaseController]ではレイアウトファイルを[application.html.erb]に設定しているので、こちらが適用されるため。

※ポイント

[app/views/admin/layouts/application.html.erb]で、AdminLteにタグに[layout-fixed]クラスが入っていないのに、タグに[layout-fixed]クラスを付け加えたのは、下記のようにスライダーが途中で途切れてしまうため。
[layout-fixed]クラスは、admin-lteというbootstrapのtemplateに設定されているクラス(サイドバーを固定できるクラス)
下記ドキュメント参照

Layout | AdminLTE v3 Documentation

Image from Gyazo

参考記事:

【Rails】AdminLTE3.0.0-alpha.2の利用|Artefact|note

Introduction | AdminLTE 3 Documentation

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

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

[Rails入門] layoutsファイルを分かりやすく解説! | 押さえておきたいWeb知識

コンテンツセキュリティポリシー (CSP) - HTTP | MDN

【Rails】 layoutメソッドの使い方と使い所とは? | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

Railsアプリで Bootstrap 4 を利用する - Qiita

link_toで突然methodが効かなくなって困ってるあなたへ - Qiita

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

Railsのマニフェストファイルを使ったJsとStylesheetの呼び出し - Qiita

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

Railsで特定のページのみJavaScriptを読み込む方法を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン

Rails 必要なJavaScriptのみを読み込む | | KeruuWeb

https://www.ryotaku.com/entry/2019/02/19/215229

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

【Rails】 enumチュートリアル | Pikawaka - ピカ1わかりやすいプログラミング用語サイト