404、500エラー発生時のSlack通知

どうもどハマりエンジニアです。

 

目的

slack-notifier

exception_notification

上記の2つのgemを追加・適切な記述をし、Slackの該当チャンネルに通知できるようにします。Slackの通知設定でWebhookURLに指定するURL情報は、credentials(日本語だと信任状)を使って設定してください。通知確認後は、production環境のみで通知がいくようにします。

404エラーに関してはエラーハンドリング処理を実装し、public/404.htmlを表示させ、500エラーに関してはエラーハンドリング処理を実装し、ログに「エラーメッセージ」と「エラーのバックトレース」を出力させ、public/500.htmlを表示させます。

 

gemのインストール

$slack-notifier

&exception_notification なんかしらのエラーが発生すると、メールを自動的に送付してくれる gem

gemfileに上記を記載し$bundle install

 

slackのwebhook_urlを取得

  1. https://任意のslackワークスペース名.slack.com/services/newでアクセス
  2. 検索フィールドからIncoming WebHooksを検索
  3. Slackに追加をクリックして通知したいチャンネル名を選択して保存
  4. webhook_URLをコピー

 

通知設定

$ bundle exec rails g exception_notification:install

exception_notification.rb
require 'exception_notification/rails'

ExceptionNotification.configure do |config|
 config.ignore_if do
  !Rails.env.production?
 end
 config.add_notifier :slack, {
 channel: '#基礎編通知'
}
end
env.production?のところで、エラーが発生した際にどの環境でエラーをslack に送るか設定します。確認の段階だけdevelopmentにします。
webhook_url と channel では、さきほど取得したurlとチャンネル名を記載します。

 

コントローラーでのrescue_formによるエラー補足

application_controller.rb

 

unless Rails.env.development?
rescue_from StandardError, with: :render_500
rescue_from ActiveRecord::RecordNotFound, with: :render_404
end
 
private
 
def render_404
render file: Rails.root.join('public/404.html'), layout: false, status: :not_found
end

def render_500(error = nil)
logger.error(error.message)
logger.error(error.backtrace.join('\n'))
render file: Rails.root.join('public/500.html'), layout: false, status: :internal_server_error
end

動的なエラーページを表示する方法としては、おそらくApplicationControllerで下記のようにするのが最もポピュラーで手軽な方法です。ここで例外が発生した際にrenderさせる設定を記載します。

<注意>

明示的にrescue_from ActiveRecord::RecordNotFound, with: :render_404のように例外を捕捉した場合はconsider_all_requests_localの設定にかかわらず必ずwith:の後のメソッドが呼ばれます。

その中のメソッドでは render file: Rails.root.join('public/404.html'), layout: false, status: :not_foundと記述しているので何が何でも404.htmlが表示されます。

ですので unless Rails.env.development? と記載し、開発環境意外でrenderさせるようにします。でないと、開発環境でなんのエラーが出ているのか分からなくなってしまいます。

 

あとは404.html、500.htmlを設定すれば完了です。

 

rescue_fromで補足しているエラーはSlack通知がいかないので気をつけましょう。

 

 

 

 

掲示板/ユーザのCRUD機能の作成(管理者画面)

どうもどハマりエンジニアです。

 

本日は前回の引き続きで、管理者画面の掲示板及びユーザーのCRUD機能を作成していきます。

 

ユーザーと掲示板のコントローラー作成

$rails g controller admin::users

$rails g controller admin::boards

コントローラーは普通のCRUD機能ですので、一般での時と同じように作成します。

admin/user_controller.rb

class Admin::UsersController < Admin::BaseController
before_action :set_user, only: %i[show edit update destroy]
def index
@search = User.ransack(params[:q])
@user = @search.result(distinct: true).order(created_at: :desc).page(params[:page])
end

def show; end

def edit; end

def update
if @user.update(user_params)
redirect_to admin_user_path(@user), success: 'ユーザーを更新しました'
else
flash.now[:danger] = '更新できませんでした'
render 'edit'
end
end

def destroy
@user.destroy!
redirect_to admin_users_path, success: 'ユーザーを削除しました'
end

private

def set_user
@user = User.find(params[:id])
end

def user_params
params.require(:user).permit(:email, :first_name, :last_name, :abatar)
end
end

 

admin/boards_controller.rb

class Admin::BoardsController < Admin::BaseController
before_action :set_board, only: %i[show edit update destroy]
def index
@search = Board.ransack(params[:q])
@board = @search.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
end

def show; end

def edit; end

def update
if @board.update(board_params)
redirect_to admin_board_path(@board), success: '掲示板を更新しました'
else
flash.now[:danger] = '更新できませんでした'
render 'edit'
end
end

def destroy
@board.destroy!
redirect_to admin_boards_path, success: '掲示板を削除しました'
end

private

def set_board
@board = Board.find(params[:id])
end

def board_params
params.require(:board).permit(:title, :body, :board_image, :board_image_cache)
end
end

 

ルーティングの設定

routes.rb

namespace :admin do
root 'dashboards#index', as: :root
get 'login', to: 'user_sessions#new'
post 'login', to: 'user_sessions#create'
delete 'logout', to: 'user_sessions#destroy'
resources :users, only: %i[index show edit update destroy] 追加
resources :boards, only: %i[index show edit update destroy] 追加
end

 

ビューの作成及び検索機能(日付)の設定等

まず、ユーザー一覧に管理者で検索できるようにしていきます。

 

admin/users/index.html.erb (検索部分のみ)

<%# 検索機能 %>
<%= search_form_for @search, url: admin_users_path do |f| %>
<div class="row">
<div class="form-inline align-items-center mx-auto">
<div class="col-auto">
<%= f.search_field :first_name_or_last_name_cont, class: 'form-control',
           placeholder: '検索ワード' %>
</div>
<div class="col-auto">
<%= f.select :role_eq, User.roles_i18n.invert.map
          { |key, value| [key, User.roles[value]] },
          { include_blank: '指定なし' }, { class: 'form-control' } %>
</div>
<div class="col-auto">
<%= f.submit value: '検索', class: 'btn btn-primary' %>
</div>
</div>
</div>
<% end %>

 

f.selectは、

<%= f.select 属性, 選択肢の集合, {オプション}, {HTMLオプション} %>

と書くか、あるいは、

<%= f.select( 属性, 選択肢の集合, {オプション}, {HTMLオプション} ) %>

と括弧を使って書きます。

 

オプションは、

{ include_blank: '指定なし' }

のようにします。この例では、何も選択されていない時は、 ‘指定なし’ という項目が選択されるという定義です。セレクトボックスの1番目の選択肢に’指定なし’ が追加されます。

 ransackを使うとboardsテーブルに対して:title_eqと記述するだけでSQLを内部で走らせてくれます。
上記のeqの部分をPredicateと呼び、Predicateを変更することで様々な条件のデータを検索することができます。

 

User.roles_i18n.invert.map i18nを使えば、選択部分を日本語に変えることができます。inverでkeyとvalueの関係を逆にして、mapで表示させる設定をしています。例えば、コンソールを開いて下記を実行します。そうするとkeyがgeneral、valueが一般になっています。なので表示させたいのは一般なので、invertを使い反転させます。あとは、mapをつかい、表示の設定をしてあげます。

irb(main):001:0> User.roles_i18n

=> {"general"=>"一般", "admin"=>"管理者"}

 

views/ja.yml

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

 

i18nでは上記のように記載してあげます。

以上で管理者権限での検索が可能になります。

<参考記事>

https://310nae.com/rails-selectbox/

 

 

次に掲示板一覧にて日付で検索をできるようにしていきます。

 

admin/boards.index.html.erb(検索機能のみ)

<%# 検索機能 %>
<%= search_form_for @search, url: admin_boards_path do |f| %>
<div class="row">
<div class="form-inline align-items-center mx-auto">
<div class="col-auto">
<%= f.search_field :title_or_body_cont, class: 'form-control',
placeholder: '検索ワード' %>
</div>
<div class="col-auto">
<%= f.date_field :created_at_gteq, class: 'form-control' %>
<span></span>
<%= f.date_field :created_at_lteq_end_of_day, class: 'form-control' %>
</div>
<div class="col-auto">
<%= f.submit value: '検索', class: 'btn btn-primary' %>
</div>
</div>
</div>
<% end %>

 

ransackで oooo/oo/oo から xxxx/xx/xx までというふうに日付検索ができるようにしていたのですが、そのままだと xxxx/xx/xx の部分の時間が00:00:00となってしまう事で、指定した日付の前日までのデータしか取得できませんでした。

日付の検索についてはいつからいつまでといった感じにするためにransackをカスタマイズする必要があります。そのためransack.rbというファイルを作り、そこに設定を加えていきます。ちなみにgteq(以上)、lteq(以下)というpredicate(述語)でransackをインストールすることで使えるものです。

 

config/initializers/ransack.rb

Ransack.configure do |config|
config.add_predicate 'lteq_end_of_day',
arel_predicate: 'lteq', # カスタマイズしたいpredicateを指定
formatter: proc { |v| v.end_of_day } # どのようにフォーマットするか指定
end

 

これできちんと指定した日付のデータも検索結果に含まれるようになりました!

SQLを確認してみてもきちんと 2019-03-01 00:00:00 から 2019-03-01 23:59:59.999999 までというようにデータを検索してくれております。

 

これで日付での検索機能は完了です!

 

掲示板のメニューをアクティブに

どいうことかというと、例えば掲示板一覧にいるときは、メニューのボタンの色をかえるということ。これは直接ビューに書き込んでもいいのですが、ヘルパーに書き込んだ方が便利なので、今回はヘルパー に記載していきます。

 

app/helpers/application_helper.rb

def active?(controller_name)
return 'active' if controller_name == params[:controller]
end

 

 

admin/shared/_sidebar.html.erb

<%= link_to admin_boards_path, class: "nav-link #{ active?("admin/boards") }" do %>

 

へるぱーで定義したメソッドをつかえば、これだけで掲示板メニューをアクティブにすることができます。

 

以上で、おおまかな掲示板/ユーザのCRUD機能の作成(管理者画面)の説明を終わります。

 

 

 

 

AdminLTE 3を使って簡単な管理者ページを実装

どうもどハマりエンジニアです。

 

本日は管理画面へのログイン機能、管理画面トップページの作成 を作成していきます。

 

 

AdminLTE3のインストール

まず今回はyarnを使ってAdminLTEをインストールしていきます。

yarnとはJavaScriptのパッケージマネージャーのことで、yarnとnpmの2種類ありますが、今回はyarnを使っていきます。

$yarn add admin-lte

これでこれでnode_modulespackage.jsonyarn.lockというファイルがインストールされます。 node_modules/admin-lteディレクトリにデフォルトテンプレートが記載されているので今回はその中のstarter.htmlとlogin.htmlを使っていきます。

 

管理者ページ用のマニフェストファイルを作成

今回は一般ページと管理者ページでは見た目が大きく変わってくるので、一般ページと分けて管理者ページ専用のマニフェストファイルを作成していきます。なので一般ページはjacascripts/application.js、管理者ページはjavascripts/admin.jsに記載していくことになります。管理者ページではしっかりとjquery3やrails-ujsを指定しないとlink_toのヘルパーメソッドのルーティンでdeleteメソッドを指定出来ないなどの問題が起きてしまいます。

 

app/assets/javascripts/admin.js

//= require jquery3
//= require rails-ujs
//= require admin-lte/plugins/bootstrap/js/bootstrap.bundle.min
//= require admin-lte/dist/js/adminlte

 

このままだと一般ページにもadmin.jsが読み込まれてしまいます。require=treeがjavascriptディレクトリ階層全てのファイルを読み込むので、個別に記載していく必要があります。

 

app/assets/javascripts/application.js

//= require edit_comment

これはコメント編集の時に使ったjsです。require=treeを消して個別に書いていきます。

 

app/assets/stylesheets/admin.scss

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

ここには、starter.htmlにlinkでここのcssを使用してあると記載してあるので、こちらで専用にimportしてあげる必要があります。

 

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

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

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 << Rails.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 admin.css]
上記がコメントアウトになっているので外してあげる。

 

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

$rails g migration AddRoleToUsers role:integer

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

一般権限となるdefaultは0としてAdmin権限は1とします。

$rails db:migrate

 

enumの追加をする。enumとはモデルの数値カラムに対して文字列の名前を定義することができます。 今回は一般管理者をgeneral、管理者権限をadminとして定義していきます。

user.rb

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

 

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

$rails g controller admin::base

こうすることでcontorollersの中にadmminディレクトリが作成され、そのなかに新たなコントローラーが作成されます。base_controllerには、application_controller.rbのように、管理者のためのコントローラの基盤となる処理を実装していきます。なので、admin以下のコントローラーは全てbaseコントローラーが継承される記載をしていく必要があります。

admin/base_controller.rb

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

private

def not_authenticated
redirect_to admin_login_path, warning: 'ログインしてください'
end

def check_admin
redirect_to root_path, warning: '権限がありません' unless current_user.admin?
end
end

layoutで読み込みたいレイアウトを指定します。この作業はレイアウトは後ほど作成します。 check_adminメソッドでadmin権限でない場合はトップページに遷移させるようにします。

 

$rails g controller admin::dashboards

$rails g controller admin::user_sessions

 

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/admin_login'

def new; end

def create
@user = login(params[:email], params[:password])
if @user
redirect_to admin_root_path, success: 'ログインしました'
else
flash.now[:danger] = 'ログインに失敗しました'
render 'new'
end
end

def destroy
logout
redirect_to admin_login_path, success: 'ログアウトしました'
end
end

classはbase_contoroller.rbから継承されるように書き直します。ログインのフォーマットは特別にlayoutを変えます。node_modules/admin-lte/dist/pages/examplesにあるlogin.htmlを参考に新たにフォーマットを作成します。

 

admin/dashboards.rb

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

 

 

ルーティングの設定

routes.rb

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

今回は/adminから始まるURLにしたいので、namespaceを使っていきます。これを使うと一般ページと分けて管理することが出来ます。

rootの設定のところでas: :rootを付けないとエラーが出てくるのでつけるようにします。

 

ビューの設定

admin/layoutディレクトリを作成して、その中にapplication.html.erbとadmin_login.html.erbを作成していきます。

 

view/admin/layout/application.html.erb

<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta lang="ja">
<title><%= page_title(yield(:title), admin: true) %></title>
<%= csrf_meta_tags %>
<%# ここでadmin.scssを読み込む %>
<%= stylesheet_link_tag 'admin', media: 'all' %>
</head>

<body class="hold-transition sidebar-mini">
<div class="wrapper">
<%= render 'admin/shared/header' %>
<%= render 'admin/shared/sidebar' %>
<div class="content-wrapper">
<%= render 'shared/flash_message' %>
<%= yield %>
</div>
<%= render 'admin/shared/footer' %>
</div>
<%# admin.jsを読み込む %>
<%= javascript_include_tag 'admin' %>
</body>
</html>

 

view/admin/layout/admin_login.html.erb

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <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' %>
</head>

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

 

admin/shared配下にてヘッダー、フッター、サイドバーのパーシャルの作成とログイン画面とトップページの作成をしていきます。(ここは省略)

 

application/helper.rb

module ApplicationHelper
def page_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
end

 

管理者ログイン用のアカウントを作成

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

 

以上で完成になります。

 

sorceryのreset_passwordモジュールを使用したパスワードリセット機能

どうもどハマりエンジニアです。

 

本日はパスワードのリセット機能について実装していきます。

使う概念

  • soceryのreset_passwordモジュール
  • letter_opener_web
  • config

sorceryのreset_passwowrdモジュールをインストールする


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

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

そうすると上記のmigrationが作成されます。

rails db:migrate 実行

Mailerの作成

$rails g mailer UserMailer reset_password_email

(user_mailer.rb)

class UserMailer < ApplicationMailer
  default from: 'from@example.com' 
  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
  • default from: 'from@example.com'でメールの送信元のアドレスを指定できる
  • mail(to: user.email,subject: 'パスワードリセット')でメールの宛先、件名を指定

 

(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
  end

  config.user_class = 'User'
end

sorcery.rbにreset_passwordサブモジュールを追加し、パスワードリセットに使用するActionMailerとしてUserMailerを定義

 

 

コントローラー・ビューの作成

$rails g controller password_resets

(password_resets_controller.rb)

class PasswordResetsController < ApplicationController
  skip_before_action :require_login
  def new; end

  def create
    @user = User.find_by(email: params[:email])
    if @user
      @user.deliver_reset_password_instructions!
    redirect_to login_path, success: 'パスワードリセット手順を送信しました'  
    else
    render 'new'
    end
  end

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

  def update
    @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: 'パスワードを変更しました'
    else
      flash.now[:danger] = 'パスワードの変更に失敗しました'  
      render :edit
    end
  end
end

 

create()では送信されてきたメールに対応するUserを検索し、「deliver_reset_password_instructions!」を呼び出すことでトークンの発行とメール送信を行っています。

edit()はトークンの存在チェックです。

update()では「change_password!」を呼び出してパスワードの変更を行っています。APIであるため、それぞれのアクションではHTTPステータスコードを状態に応じて返しています。

 

(routes.rb) ルーティングの設定

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

 

(new.html.erb)リセット申請画面

<%= form_with url: password_resets_path, local: true, method: :post do |f| %>
  <div class="field">
    <%= f.label :email, t(User.human_attribute_name(:email)) %><br />
    <%= f.text_field :email, class: 'form-control' %>
    <%= f.submit '送信', class:'btn btn-primary' %>
  </div>
<% end %>

 

(edit.html.erb)リセット編集画面

<%= form_with model: @user, url: password_reset_path(@token), method: :patch, local: true do |f| %>
<div class="field">
  <%= f.label :email %>
  <%= @user.email %>
</div>
<div class="field">
  <%= f.label :password %>
  <%= f.password_field :password, class: 'form-control' %>
</div>
<div class="field">
  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation, class: 'form-control' %>
</div>
<div class="actions">
  <%= f.submit '更新する', class: 'btn btn-primary' %>
</div>
<% end %>

 

メール文作成

(reset_password_email.html.erb)

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

@urlは先ほどmailer.rbで作成

 

(reset_password_email.text.erb)

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

 

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

下記のgemをインストールする
'letter_opener_web'

(routes.rb)設定を加える
mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?

 

configのgemを追加

rails g config:install
config/settings.yml
config/settings.local.yml
config/settings/development.yml
config/settings/production.yml
config/settings/test.yml

上記のファイルが作成されるので、各ファイルに必要な記述をしていきます。

 

(config/environments/development.rb)

config.action_mailer.perform_caching = false
config.action_mailer.default_url_options = { host: Settings.host }
config.action_mailer.delivery_method = :letter_opener_web

 

(settings/development.yml)

host: 'localhost:3000'

 

上記でパスワードリセットの実装は完了です。

実際にパスワードのリセットを申請すると、http://localhost:3000/letter_opener にメールが届くので確認してください。

 

以上で本日の実装は終了です。

 

 

 

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

どうもどハマりエンジニアです

 

 

gemの導入

gem 'ransack' → $bundle install

 

要件

  • 掲示板のタイトルor本文の部分一致検索ができること
  • ブックマーク一覧画面で検索した場合はブックマークした掲示板のみが検索対象であること。

 

検索フォームのテンプレートを作成

(boards/index.html.erb)

<%= search_form_for @search do |f| %>
<div class='form-group mb-3'>
  <%= f.search_field :title_or_body_cont,
         class: 'form-control', placeholder: '検索ワード' %>
  <div class='input-group-append'>
    <%= f.submit value: '検索', class: 'btn btn-primary' %>
  </div>
</div>
<% end %>

 

  • viewからsearch_form_forへ検索ワード入力してEnterを押すと、指定したpathへ送信
  • bords_controllerのindexアクションへ送信して検索する
  • :title_or_body_contが検索条件を指定している部分で、マッチャーと呼ばれ、gemでいくつもの種類が用意されている。今回は一部分でも文字が含まれている場合はヒットさせたいので、contを使用している。

 

controllerを実装

(boards_controller)

def index
# @boards = Board.all.includes(:user)
           .order(created_at: :desc).page(params[:page])
  @search = Board.ransack(params[:q])
  @boards = @search.result(distinct: true).includes(:users)
             .order(created_at: :desc).page(params[:page])
end
  • フォームから飛んできたデータをparams[:q]で受けて、@searchへ代入(ransackの仕様上、検索パラメータはqを付けて飛ばすようになっている)
  • @searchオブジェクトに対して、resultメソッドで検索結果を表示
  • 通常indexページは@boards = Board.all.includes(:user).page(params[:page])などと書いていることが多いが、こちら記載は不要になる。

以上で掲示板一覧の検索機能は完成!

 

 

ブックマーク一覧での検索

こちらも掲示板一覧とほぼ同じであるが、ここではブックマークをしている掲示板の中から検索したい。

 

(bookmarks.html.erb)

<%= search_form_for @search, url: bookmarks_boards_path do |f| %>
<div class='form-group mb-3'>
  <%= f.search_field :title_or_body_cont,
          class: 'form-control', placeholder: '検索ワード' %>
  <div class='input-group-append'>
    <%= f.submit value: '検索', class: 'btn btn-primary' %>
  </div>
</div>
<% end %>

ここではurlを指定してあげ、boards_controllerのbookmarksアクションにいくようにする。それ以外は同じ!

 

(boards_controller.rb)

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

ブックマークした掲示板のなかからデータをもってきたいので、@searchはcurrent_user.bookmark_boardsのなかから検索するようにする。

掲示板一覧と同様に最初に記入してあった「

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

この部分は不要になる。

 

以上でブックマーク一覧の検索も完了!

 

これ以外にも様々な検索が可能なので、場面に応じて適図使用を変えなければいなけい。

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

どうもどハマりエンジニアです。

 

今回は掲示板の下部にページネーションを付け加える実装を行っていきます。

 

①gem 'kaminari' → bundle install

 

rails g kaminari:config

ここで設定等を決めることができるファイルが作成させます

 

rails g kaminari:views bootstrap4

見た目をよくするためにbootstrapを使いいます

これだけで見た目がよくなります

 

<kaminari_config.rb>

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

ここで表示される数の上限を決めます。

 

<boards_controller.rb>

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

.page(params[:page])を加える。

 

<bookmarks.html.erb>

<%= paginate @boards %>

あとは好きなとろこに差し込むだけです!

 

以上で簡単にページネーションを実装することができました。

さらにカスタム等したい場合は、下記のサイトを参考にすると良いと思います!

https://qiita.com/rio_threehouse/items/313824b90a31268b0074

 

 

コメント(編集)のAjax化について

どうもどハマりエンジニアです。

 

今回はコメント(編集)のAjax化について行っていきます。

 

流れとして下記の4つを実装していきます。

機能を四つに分けて実装していきます。
1. 編集ボタンクリックでラベル非表示、コメントエリアとボタン表示
2. キャンセルボタンクリックでラベル表示、コメントエリアとボタン非表示
3. 更新ボタンクリッ成功でDB更新、ラベル表示、コメントエリアとボタン非表示
4. 更新ボタンクリック失敗でエラーメッセージ表示

 

そもそもrailsに渡していた投稿内容の情報をjavascriptにどうやって渡すのか?

railsで定義した変数をJavaScriptに渡すためにはjson形式に変換して渡す必要があります。

 

まずは、編集ボタンクリックでラベル非表示、コメントエリアとボタンの表示とその逆の実装をしていく。

(comments/_comment.html.erb)

<td>
  <h3 class="small"><%= comment.user.decorate.full_name %></h3>
  <div id="js-comment-<%= comment.id %>">
    <p><%= simple_format(comment.body) %></p>
  </div>
  <div id="js-textarea-comment-box-<%= comment.id %>"
       style="display: none;">
    <textarea id="js-textarea-comment-<%= comment.id %>"
       class="form-control mb-1"><%= comment.body %></textarea>
    <button class="btn btn-light js-button-edit-comment-cancel"
       data-comment-id="<%= comment.id %>">キャンセル</button>
    <button class="btn btn-success js-button-comment-update"
       data-comment-id="<%= comment.id %>">更新</button>
  </div>
</td>

class="js-button-comment-update" → jsに使う

data-comment-id  → dataは属性として使う。また一つ一つにコメントのID必要が あるためIDをひっぱてくる

 

(comments_controller)

def update
  @comment = current_user.comments.find(params[:id])
  if @comment.update(comment_update_params)
    render json: { comment: @comment }, status: :ok
  else
    render json: { comment: @comment, errors:
            { messages: @comment.errors.full_messages } },
            status: :bad_request
  end
end
 
 
private 
 
def comment_update_params
  params.require(:comment).permit(:body)
end

   

 

(assets/javascripts/edti_comments.js) 

$(function() {
 
  $(document).on("click", '.js-edit-comment-button', function(e) {
  e.preventDefault();
  const commentId = $(this).data("comment-id")
  switchToEdit(commentId)
  })

  $(document).on("click", '.js-button-edit-comment-cancel', function() {
  clearErrorMessages()
  const commentId = $(this).data("comment-id")
  switchToLabel(commentId)
   })

  $(document).on("click", '.js-button-comment-update', function() {
  clearErrorMessages()
  const commentId = $(this).data("comment-id")
  submitComment($("#js-textarea-comment-" + commentId).val(), commentId)
   .then(result => {
  $("#js-comment-" + result.comment.id).html(result.comment.body.replace(/\r?\n/g, '<br>'))
  switchToLabel(result.comment.id)
  })
   .catch(result => {
  const commentId = result.responseJSON.comment.id
  const messages = result.responseJSON.errors.messages
  showErrorMessages(commentId, messages)
   })
   })
 
  function switchToLabel(commentId) {
  $("#js-textarea-comment-box-" + commentId).hide()
  $("#js-comment-" + commentId).show()
  }

  function switchToEdit(commentId) {
  $("#js-comment-" + commentId).hide()
  $("#js-textarea-comment-box-" + commentId).show()
   }

  function showErrorMessages(commentId, messages) {
  $('<p class="error_messages text-danger">' + messages.join('<br>') + '</p>').insertBefore($("#js-textarea-comment-" + commentId))
   }

  function submitComment(body, commentId) {
  return new Promise(function(resolve, reject) {
  $.ajax({
  type: 'PATCH',
  url: '/comments/' + commentId,
  data: {
  comment: {
  body: body
   }  
   }
   }).done(function (result) {
  resolve(result)
   }).fail(function (result) {
  reject(result)
  });
  })
  }

  function clearErrorMessages() {
  $("p.error_messages").remove()
  }
});

function()関数の中で、Promiseオブジェクトを生成しています。Promiseの引数にresolveとrejectがありますが、これが非同期処理が実行された場合のコールバック関数となります。

 resolveは処理が正常に終了した時に呼ばれるコールバック関数です。また上記では記述されていませんが、rejectは処理が失敗した時に呼ばれるコールバック関数となります。resolveに引数がありますが、このように任意のオブジェクトを渡すことができます。

 then関数とcatch関数が記述されています。thenは、処理が正常に実行された時、すわなちresolveが実行された時に呼ばれ、resolveの引数が渡されます。catch関数は上記では記述されていませんが、rejectが実行された時に呼ばれます。

 

replace(/\r?\n/g, '');

これは改行コード(\r\nまたは\n)をすべて削除するには次のように記述します。

 

 

<参考資料> 

 http://js.studio-kingdom.com/javascript/function/

 https://qiita.com/hiro266/items/160a4f9290ecd9f27d15

 https://pikawaka.com/rails/json

https://noumenon-th.net/programming/2018/12/14/promise1/ 

https://qiita.com/aiandrox/items/f612b9b8503785ed18bf