rosh-1228のメモ

自身の勉強したことをメモしてます。

Choices.jsでセレクトボックスを複数作る・動的に作る

概要

フィヨルドブートキャンプでChoices.jsを使ったセレクトボックスを作ることになったが、Choices.jsの記事が少なく、複数作ったり動的にセレクトボックスを作ったりしている記事がなかったので、残しておきたい。

参考

@GaramMasala29さんのqiitaの記事を参考にさせていただきました。 非常にわかりやすかったです! ありがとうございました!

あとは公式を参考にした。

Choices.jsについて

以下のような文字検索ができたり、他にもオプションをつけることで様々なことができるライブラリ。

複数のセレクトボックスにChoices.jsを適用する

複数のセレクトボックスにChoices.jsを適用する場合、私はquerySelectorAllを使ってidを全て取得し、for文を使用することで、各セレクトボックスにChoices.jsを適用できた。

f.collection_select :book_id, all_books, :first, :last, {}, { id: 'js-book-select' }
document.addEventListener('DOMContentLoaded', () => {
  const bookSelectCount = document.querySelectorAll('#js-book-select').length
  const elements = document.querySelectorAll('#js-book-select')
  for (let i = 0; i < bookSelectCount; i++) {
    // eslint-disable-next-line no-new
    new Choices(elements[i], {
      allowHTML: true,
      searchResultLimit: 20,
      searchPlaceholderValue: '検索ワード',
      noResultsText: '一致する情報は見つかりません',
      itemSelectText: '選択',
      shouldSort: false
    })
  }
})

動的にセレクトボックスを作る

動的にセレクトボックス場合もquerySelectorAllを使うことで解決できた。 動的に作る場合は、querySelectorAllで取得した最後の要素に対してChoices.jsを適用することでセレクトボックスを作ることができた。

document.addEventListener('DOMContentLoaded', () => {
    const bookSelectCount = document.querySelectorAll('#js-book-select').length
      const elements = document.querySelectorAll('#js-book-select')
      for (let i = 0; i < bookSelectCount; i++) {
        // eslint-disable-next-line no-new
        new Choices(elements[i], {
          allowHTML: true,
          searchResultLimit: 20,
          searchPlaceholderValue: '検索ワード',
          noResultsText: '一致する情報は見つかりません',
          itemSelectText: '選択',
          shouldSort: false
        })
      }
    $('.reference-books-form__add').on('cocoon:after-insert', () => {
    const elements = document.querySelectorAll('#js-book-select')
    const element = elements[elements.length - 1]
    if (element) {
      return new Choices(element, {
        allowHTML: true,
        searchResultLimit: 20,
        searchPlaceholderValue: '検索ワード',
        noResultsText: '一致する情報は見つかりません',
        itemSelectText: '選択',
        shouldSort: false
      })
    }
  })
})

出来上がったセレクトボックスはこちら。

コントローラーを分けるとシンプルに記述できて良い感じだったので習慣にしたい

今回ユーザーの就職活動中かどうかのフラグをオンオフするというissueに取り組んでいて、その際にいただいたアドバイスについてここに残しておきたい。

条件としてはユーザー情報を更新するページと記事に対してコメントするページがあったとして、記事に対してコメントするページで就職活動中かどうかのフラグをオンオフするという実装が必要だった。

私は、以下のような感じで2種類のupdate処理を1つのコントローラー内に記述してレビュー依頼を行なった。

  • user_controller(だいぶぼかしてある)
class UsersController < ApplicationController
  def update
    update_job_seek if only_job_seek?
    if @user.update(user_params)
      redirect_to users_url, notice: 'ユーザー情報を更新しました。'
    else
       ... # 何らかの処理
    end
  end

  def update_job_seek
    if @user.update(user_job_seek_params)
      ... # 何らかの処理
      return
    else
      ... # 何らかの処理
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :age, :job_seek)
  end

  def user_job_seek_params
      ... # 何らかの処理
  end

  def only_job_seek?
    ... # job_seekというパラメータのみかをチェック
  end
end

このコードをレビューいただき、アドバイスとしてDHHはどのようにRailsのコントローラを書くのかという記事をもとに別のURLを作って更新処理を行なったほうが良いとうことだった。

この記事はだいぶ前であるが、教えていただいておりすっかり忘れてしまっていた。。。(今後はしっかり覚えておく!)

こちらの記事を読み、その通りに実践したところコントローラーの記述がシンプルになり、とても読みやすいコードになった。

  • user_controller
class UsersController < ApplicationController
  def update
    if @user.update(user_params)
      redirect_to users_url, notice: 'ユーザー情報を更新しました。'
    else
       ... # 何らかの処理
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :age, :job_seek)
  end
end
  • job_seek_controller
class JobSeeksController < ApplicationController
  before_action :set_user, only: %i[update]

  def update
    if @user.update(user_params)
       ... # 何らかの処理
    else
       ... # 何らかの処理
    end
  end

  private

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

  def user_params
    params.require(:user).permit(:job_seeking)
  end
end

上記の通り、user_controlleronly_job_seek?というメソッドを使って、job_seekというパラメータだけかどうかを判断していたが、コントローラーを2つに分けることでuser_controllerjob_seek_controllerのそれぞれがすごくシンプルになった。

1つのコントローラーにたくさんの処理を任せれば任せるほどコードは読みにくくなるし、URLの設計の観点からもPUT /userjob_seekだけをupdate するかどうかわからなくなる。

そのため、コントローラー分ける(責任を分ける)ことで、コードは読みやすく、URLの設計もPUT /job_seekでわかりやすくなる。

記事の引用の引用になるけれど、以下の言葉は確かに後の自分に怒りたくなるのだろうと思った。

自分の作ったコントローラの状態を悔やむのは決まって、作ったコントローラの数が少なすぎた時です。多くの処理を任せようとしすぎてしまうんです。

Full Stack Radioのインタビュー

ここで書いたからには、忘れないようにしたいし、コントローラーに変更を加える際にもう一度読み直したい。

before_actionのifオプションとskip_before_actionで親コントローラーのbefore_actionを無効にする

before_actionの使い方

before_actionを使うと、各アクション実行前に、before_actionで指定したメソッドが実行される。

before_action :メソッド

この使い方は知っていたけど、今回はifオプションを使うと便利だったので、ifオプションについて残しておきたい。

ifオプション

今回以下のようにbefore_actionで認証系にメソッドが使われているが、特定のコントローラだけ無効にしたい時にifオプションが使えた。

class SampleController < ApplicationController
    before_action :require_login_for_api
end

以下のように条件チェック用のメソッドをSampleControllerで用意する。

class ApplicationController < ActionController::Base
    before_action :require_login_for_api, if: :check?

    def check?
        true
    end
end

そして継承先のコントローラーでもfalseになるメソッドを用意する。

class SampleController < ApplicationController
    def check?
        false
    end
end

こうすることで、SampleControllerだけ親コントローラーのrequire_login_for_apiを無効にすることができた。

skip_before_action

先ほどのifオプションでも親コントローラのbefore_actionを無効にできるが、この場合CIを通した時に tracerouteのエラーが発生した。

そこで、skip_before_actionのコールバックを使うともっとスッキリ書くことができ、CIを通すことができた。 こちらも自分のために残しておきたい。

class SampleController < ApplicationController
    skip_before_action :require_login_for_api
end

しかし、セキュリティに穴を開けるなどであれば使う時はしっかり考えたい。

railsテストのデバッグ方法をメモ

railsテストのデバッグ方法

railsのsystemテストでハマってしまい、その時に使ったデバッグ方法を残しておく。

binding.pry

binding.pryをコード中に書くと、コード実行時にbinding.pryの場所で止めてくれるgem

ブレークポイントとして機能する。

以下のように、確認したい処理の手前に書く。

def update
    binding.pry
    if @user.update(user_params)
        成功時の処理
    else
        失敗時の処理
    end
end

すると、以下のように処理が止まる。

    18: def update
    19:   binding.pry
 => 20:   if @user.update(user_params)
    21:     成功時の処理
    22:   else
    23:     失敗時の処理
    24:   end
    25: end

[1] pry(#<Controller>)>

ここで例えば以下のように@user.update(user_params)を実行した時の処理がわかる。

実際にupdateが成功しているのか失敗しているのかわからない時などに使える。

[1] pry(#<Controller>)> @user.update(user_params)
=> false

他にもこの時点のuser_paramsの内容なども確認できる。

@user.errors.full_messages

以下のように.errors.full_messagesを使うことで先程のupdateでどんなエラーが発生していたかがわかる。

今回はユーザー情報にos:があったのだが、テストデータで用意していたユーザにos:がなかったため、エラーが発生していた。

[1] pry(#<Controller>)> @user.errors.full_messages
=> [使用しているOSを選択してください。]

これはテストだけでなく、普通に実装している時も使用できる。

binding.pryも普通に実装している時も使えるが、今回のようにテストでエラーが発生した場合もデバッグに使える。

page.save_screenshot('~~~/screenshot.png')

capybaraをインストールすることで使用できる。

テストコード中に書くことで、スクリーンショットを取得できる。 以下の場合、更新ボタンをクリック後のページをスクリーンショットで取得できる。

test 'test' do
    click_on '更新'
    page.save_screenshot('~~~/screenshot.png')
end

save_and_open_page

こちらもcapybaraをインストールして使う。

こちらもテストコードの中に記述して使用する。

save_and_open_pageを書いた時点の画面の状態をブラウザで確認することができる。

おわりに

p や putsを使ってデバッグも可能だが、更新系の処理を実際に確かめたかったり、画面の状態を確かめたい場合は、これらのメソッドを使って行きたい。

日時をテストするときは、travel_toメソッドを使うと良い

モデルテストで日時をテストするときに、以下のように今日作成されたものかどうかを確認するテストを作成していた。

report = Report.create(title: 'test_title', content: 'test_content')

assert_match Time.zone.today.to_s, report.created_at.to_date.to_s

ただ、これだとReport.createが実施された時間が23:59:59.999で、assert_matchでテストした時間が24:00:00.000とかになると、日を跨いでしまい、エラーになる可能性がある。

そこで、travel_toを使うと、日時を固定できるので便利だった。

travel_to(Date.new(2022, 1, 1)) do
      report = Report.create(title: 'test_title', content: 'test_content') # <=2022/01/01で固定される
      assert_equal Date.new(2022, 1, 1).to_s, report.created_at.to_date.to_s
    end

travel_to do ~ end の中でreportを作成して、assertionで結果を確認すれば、いつテストを行っても、日が固定されているので、日を跨いだらテストにならないといったことは心配なくなる。

【参考】

ActiveSupport::Testing::TimeHelpers

Timecopを使わなくても時間を止められた話

scaffoldについて復習

Railsについて勉強したので復習のために書いていく。

scaffold

Railsドキュメントによると、以下のように記述がある。

アプリケーションの基本的な機能の一覧(index)、詳細(show)、新規作成(new/create)、編集(edit/update)、削除(destroy)するために必要なコントローラ、モデル、ビューをまとめて生成

基本的な機能としてCRUD機能を自動で作成してくれる機能みたい。

CRUD機能を持つアプリを作成したい時は、scaffoldを使うと便利そう。

試しに実行した結果を以下に掲示していく。

コマンド

まず、rails newでアプリを作成しないと使えないので、rails newでbooks_appを作成する。

$ rails new books_app

次に、作成されたbooks_appフォルダに移動し、scaffoldコマンドを実行する。

後続についていてるtitle:stringは、DBを作成するマイグレーションファイルを作成する時に使用される。

$ cd books_app/
$ rails g scaffold book title:string

この状態では、マイグレーションファイルが作られただけなので、サーバを起動してもDBにテーブルがない状態になる。 そのため、migrateしてDBにテーブルを作成する。

rails db:migrate

これでサーバを起動する準備ができたので、サーバを起動する。

rails s

http://localhost:3000/booksにアクセスすると以下のようにページが出来上がっていることがわかる。

f:id:rosh-1228:20220204104537p:plain

できたページ

先ほど表示したページでNew Bookをクリックすることで、本の新規作成ページに遷移する。

ここで、本を作成することができる。

f:id:rosh-1228:20220204104737p:plain

実際に本を作成すると、以下のように本の作成に成功したとメッセージを表示することができる。

f:id:rosh-1228:20220204104804p:plain

次にeditをクリックすると、以下のように本のタイトルを編集することができる。

f:id:rosh-1228:20220204104836p:plain

Update Bookをクリックすることで、本のタイトルを編集することができる。

編集に成功すると以下のようにメッセージが表示される。 f:id:rosh-1228:20220204104922p:plain

Backを押すとShow Edit Destroyと表示されており、Destroyを押すことで作成した本を削除するこができる。

f:id:rosh-1228:20220204104948p:plain

f:id:rosh-1228:20220204105023p:plain

アプリのファイルの内容

できたアプリの内容を確認していく。 モデルファイルのapp/models/book.rbも作成されている。

f:id:rosh-1228:20220204112816p:plain

コントローラファイルにはapp/controller/books_controller.rbが作成されており、CRUDを扱うメソッドが用意されている。

def show; endのように、使っていないメソッドを1行にすることもできる。

class BooksController < ApplicationController
  before_action :set_book, only: %i[ show edit update destroy ]

  # GET /books or /books.json
  def index
    @books = Book.all
  end

  # GET /books/1 or /books/1.json
  def show; end

  # GET /books/new
  def new
    @book = Book.new
  end

  # GET /books/1/edit
  def edit; end

  # POST /books or /books.json
  def create
    @book = Book.new(book_params)

    respond_to do |format|
      if @book.save
        format.html { redirect_to book_url(@book), notice: "Book was successfully created." }
        format.json { render :show, status: :created, location: @book }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /books/1 or /books/1.json
  def update
    respond_to do |format|
      if @book.update(book_params)
        format.html { redirect_to book_url(@book), notice: "Book was successfully updated." }
        format.json { render :show, status: :ok, location: @book }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /books/1 or /books/1.json
  def destroy
    @book.destroy

    respond_to do |format|
      format.html { redirect_to books_url, notice: "Book was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_book
      @book = Book.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def book_params
      params.require(:book).permit(:title)
    end
end

app/views/books/にはCRUDを扱うビューファイルが作成されている。

f:id:rosh-1228:20220204110447p:plain

このようにscaffoldで作成されたファイルを編集していくこと自分が作りたいアプリを作っていくことができて便利だなと思った。

Nginxで設定したVirtualHostの複数ドメインをSSLに対応させた

今回やったこと

  • VirtualHostで設定したそれぞれのドメインSSLに対応させた

    前提

  • Debian 10
  • Nginx
    • VirtualHostの設定をして、2つのドメインを使ってそれぞれ別のhtmlを表示させている

概要

公式の手順通り進めてたけど、証明書をインストールする段階でNginxのプラグインがないとログに出ており、そこで対応したことを残す。

証明書のインストール

certbotの公式を使用して進める。 SoftwareにNginx、SystemにDebian 10を選択すると手順が表示されるので、その通りに実行する。(エラーで詰まったところまでは割愛) f:id:rosh-1228:20210805154009p:plain

つまったところ

sudo certbot --nginxを実行した時に以下のエラーが表示される。 /var/log/letsencrypt/letsencrypt.logを読めと書いてあるので従う。

$ sudo certbot --nginx
Saving debug log to /var/log/letsencrypt/letsencrypt.log
The requested nginx plugin does not appear to be installed

ログを見ると、Nginxのプラグインが不足しているぽい。

ここでこちらの記事を参考にさせていただきました。

2021-08-04 15:38:00,827:DEBUG:certbot.main:certbot version: 0.31.0
2021-08-04 15:38:00,827:DEBUG:certbot.main:Arguments: ['--nginx']
2021-08-04 15:38:00,828:DEBUG:certbot.main:Discovered plugins: PluginsRegistry(PluginEntryPoint#manual,PluginEntryPoint#null,PluginEntryPoint#standalone,PluginEntryPoint#webroot)
2021-08-04 15:38:00,833:DEBUG:certbot.log:Root logging level set at 20
2021-08-04 15:38:00,833:INFO:certbot.log:Saving debug log to /var/log/letsencrypt/letsencrypt.log
2021-08-04 15:38:00,834:DEBUG:certbot.plugins.selection:Requested authenticator nginx and installer nginx
2021-08-04 15:38:00,834:DEBUG:certbot.plugins.selection:No candidate plugin
2021-08-04 15:38:00,834:DEBUG:certbot.plugins.selection:Selected authenticator None and installer None

python3-certbot-nginxをインストール。

sudo apt install python3-certbot-nginx

動いた。

$ sudo certbot --nginx
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator nginx, Installer nginx
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel):

参考

NGINXが動作するRHEL8にSSL設定をします。