rosh-1228のメモ

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

フィヨルドブートキャンプのシステム開発プラクティスにて、今までのコミット数ランキング化したアプリ「FjordBootCamp Contributors」をリリースしました

はじめに

フィヨルドブートキャンプの最終課題である自作サービスとして、bootcampアプリにPRを送ってマージされているかをランキング形式で確認できるFjordBootCamp Contributorsをリリースしました。 今までPRを送ってマージされたことのある人達のコミット数を期間別にランキング形式で表示されています。

github.com

目次

自己紹介

rosh-1228と申します。前職はSESの会社にて、ITインフラの運用/保守とSAP BASISをメインに設計/構築/運用/保守など仕事としておりました。

プログラミングをメインに仕事をしたいと思い、FjordBootCamp(フィヨルドブートキャンプ)というプログラミングスクールにて勉強をしております。

サービスについて

FjordBootCamp Contributorsは、元となったサービスがあり、Rails Contributersというものがあります。

こちらは、Railsに対して、PRを送った人をランキング形式で表示しており、期間別に今月 今年 今週 今日 コミットした日から今までの期間で表示されてます。

FjordBootCampでもRails Contributorsのように誰がどれくらいbootcampアプリにPRを送ってマージされているか確認されたいとあり、私がサービスを作らせていただきました。

FjordBootCamp Contributorsも、今月 今年 今週 今日 コミットした日から今までの期間の5つの期間別に、FjordBootCampAppにてマージされたPRに含まれるコミットの数をランキング形式で表示しております。

FjordBootCampAppの開発を行なっている人(システム開発ラクティスに参加されている生徒やメンターの方々)向けのサービスになります。

過去にコミットを行ったことがある人も、All Timeで自分がどのくらいコミットしたのか確認できます。

ちなみにAll Timeで自分は21位でしたw

また、各コントリビューターの今までのコミット一覧を確認することができます。

自分が今までどのようなコミットメッセージをしたのかを確認し、今後もっとわかりやすいメッセージに修正しようと見直すこともできます。

このランキングで、他の人よりも技術力がある・なしを図るものではないということだけ留意いただきたいです。

どこで使用するのか?

それでは、どういった場面を想定しているかというと、以下の使用場面を想定しています。

  • システム開発ラクティスで自分の今月や今週のコミットランクを確認して、やる気をあげる時
  • 就職活動で企業へ自分のコミット(どのようなコードを書いて貢献したのか)を見せる時
  • FjordBootCampで行われるイベントで、参加者全員でランキングを見て、誰がどんなコミットをしたのかなど話して盛り上がる

contribute数の集計時間

1日4回(0時/6時/12時/18時)Githubからコミット数を取得し、ランキングを表示します。

Github Actionsを使用しているため、タイムラグが生じる可能性もあります。参考

技術スタック

開発中に苦労したこと

触ったことがあるものはRuby on Railsのみで、他の技術スタックは0からのスタートでしたので、かなり時間がかかりました。

プログラミングより環境を整えることに悩み時間をかけたと思います。

Githubから欲しい情報を入手すること

1つ目に、GithubからFjordBootCampAppのコントリビューターとコミットを全て取得する必要があり、これをGraphQLを使って実現しようとしました。

しかし、1つのリポジトリに所属しているコントリビューター全員のコミットを取得するといったことにはあまり関心を持っている方はおられず、調べても参考になりそうなものはなく、書き方だけ学びました。

そのため、Githubの公式リファレンスを参考にexplorerで何回も試しました。 ここに時間を最も使ったと思います。

また、Githubapiは1度に100件しかコミット情報を取得することができず、FjordBootCampAppは10000件を超えるコミット数でした。

全コミット数/100回も繰り返しでGithubへ問い合わせすることになり、これで本当に良いのかと悩み他の方法を調べることにも時間を費やすほどでした。

https://docs.github.com/ja/graphql/overview/resource-limitations

今回はバックグラウンドで処理を行うので、サービスの操作が重くなるようなことはありませんが、ユーザーが何らかの登録や表示といった操作を行うサービスを作る際は、操作性について必ず考慮する必要があるなと思いました。

React.jsを初めて使った

今回、FjordBootCampのカリキュラムで勉強したVue.jsではなく、React.jsを使うことにしました。 これは、Vue.jsとReact.jsを比較して使うことを決めたとかはなく、React.jsを使ったことがないので、使ってみようという興味から決めたことです。 ただ、React.jsも使ったことがなかったので、どう書けば良いのかなど初めはわからず苦労しました。 特に、webpackerを使ってビルドする際に足りないモジュールがいくつかあったりして、システムテストでReact.jsを使ったコンポーネントが画面に描画されずに悩みました。

ホスティングサービス選択

Herokuが有料になるということもあり、代わりのホスティングサービスを探すことも苦労しました。

blog.heroku.com

今回はお金をかけずにサービスを作りたいと思っていましたので、Herokuが使えないとなると完全に代替できるサービスはありませんでした。

調べるとFly.ioRailwayのどちらかが有力な候補となりそうということがわかりました。

初めはFly.ioの方が無料で提供してくれるサーバーのスペックが良さそうだと思い、試しにデプロイを行なってみましたがここで挫折して使うことをやめました。

fly.io

なぜなら、Rails.application.credentialsを使用していたのですが、Fly.io側でmaster.keyを登録しているにも関わらずデプロイ時にRails.application.credentialsがnilになってしまいデプロイがいつまでもできませんでした。

解決のための調査にも時間をかけたのですが、解決せず焦りばかりが募りました。

そこで、Railway.appを使ってみると、非常に簡単にデプロイすることができました。

railway.app

Railway.appはブラウザ上でGithubと連携するだけでデプロイをしてくれる上に、登録したmaster.keyの情報もしっかり読み込み余計な時間をかけずに済みました。

Railway.appの無料プランは、毎月5$のリソースを与えてくれるのでそれを消費しきるか、約20日連続稼働させるかすると、システムがダウンするようでした。

ただ、developerプランという1つ上のプランでは、毎月5$のリソースをくれた上でそれ以上のリソースを消費した場合に初めて課金されるというものでした。

また、連続稼働時間の制限はなくなるものでしたので、システムがダウンするよりは良いかと考え、developerプランに加入しました。

無料プランよりも良いリソースを提供されているので、それもプラスでよかった点です。

最後に

webに公開して誰かに使ってもらう事を意識してサービスを作る経験ができてとても良かったと思います。

前職でお客様へシステムを提供していましたが、すでに出来上がっているパッケージを導入したり、使いにくい・値段が高いと言われ本当にお客様が欲しいものを提供できているのかずっと疑問でした。

そのため、自作サービス進捗会で、デモを動かした時にkomagataさん、machidaさんに自分達にとって価値があるものができたと仰っていただいた時、誰かの役に立つものを提供することができた!と嬉しくなりました。

受託開発の会社に就職したいと考えている自分としては、komagataさんmachidaさんの要望に合わせて、開発を行いデモで実際の使用感の感想をいただいたり、修正を行なったりできたことも良い経験でした。

やはり、誰かの需要に応えられるエンジニアになりたいと思えましたし、これからも応えられるようにスキルを上げていきたいと改めて思いました。

このサービスがFjordBootCampAppの開発に携わる人の一助となれば幸いです。

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で作成されたファイルを編集していくこと自分が作りたいアプリを作っていくことができて便利だなと思った。