Ruby on Rails Learning Diary

学習したことの整理用です。

CI環境と開発環境でDBパスワードが異なる場合

CI上のパスワードは空でOK, 開発環境上はcredentialでパスワードを設定しています。

以前は開発環境だけで読み込めるようにgem 'dotenv-rails'を使用してパスワードを設定し、Github上にはそのパスワード環境変数をpushしないことでCI上にはパスワードが空になるようにしていました。
酷い力技だったので、わかりやすいように修正しました。

jobs:
  build:
    docker:
      - image: circleci/ruby:2.7.2-node-browsers-legacy
        environment:
          - BUNDLER_VERSION: 2.1.4
          - RAILS_ENV: 'test'
          - DB_HOST: 127.0.0.1
          - LANG: 'ja_JP.UTF-8'
          - TZ: "Japan"
      - image: circleci/mysql:5.7
        environment:
          - MYSQL_ALLOW_EMPTY_PASSWORD: 'true'  # ここ
          - MYSQL_USER: root
          - MYSQL_DB: ci_test
# 略

test:
  <<: *default
  database: ci_test
  host: <%=ENV['DB_HOST'] || '127.0.0.1' %>
  password: <%= Rails.application.credentials.development[:password] %> ##ここ

対処法

database.yml.ciを用意

# 略

test:
  <<: *default
  database: ci_test
  host: <%=ENV['DB_HOST'] || '127.0.0.1' %>
  password: "" #空にする

本来使用するdatabase.ymlからdatabase.yml.ciを入れ替え

      # DBのセットアップ
      - run:
          name: Database Setup
          command: |
            rm ./config/database.yml # ここ
            mv ./config/database.yml.ci ./config/database.yml # ここ
            bundle exec rake db:create
            bundle exec rake db:schema:load

参考

CircleCIを使ってみた(RailsのRSpecとデプロイのサンプル) | RE:ENGINES

【Rails】rails-adminを使って簡単に管理画面を作る

簡単に管理画面をつくりたい

ユーザー情報を一覧で見られる単純な管理画面があればいいので、一番簡単に管理画面ができそうなrails-adminを使いました。
rails-admin × sorceryの参考記事が見つからなかったのでブログに残しておきます。

環境

Rails 6.0.3 ruby 2.7.2

認証はgem 'sorcery'を使用しています。
Userモデルがあり、roleカラムで管理者と一般ユーザーを分類しています。

実装の流れ

  • gem 'rails-admin'のインストール
  • 認証用 sorceryの設定
  • 認可用gem 'cancancan'のインストール

rails-adminのインストール

公式に沿っていきます。
GitHub - sferik/rails_admin: RailsAdmin is a Rails engine that provides an easy-to-use interface for managing your data

  1. Gemfileに追記 gem 'rails_admin', '~> 2.0'

  2. Run bundle install

  3. ターミナルで実行rails g rails_admin:install

  4. 名前空間を聞かれるので、/adminでよければ、Enterキーを押す

  5. サーバを再起動してから、/adminにアクセスする。

これでもう既に形はできている。簡単。

翻訳ファイル

Japanese translation for RailsAdmin · GitHub
これをコピペしてconfig/locales/views/rails_admin_ja.ymlに貼り付け。

認証機能を追加

今はログインしていなくても誰でも管理画面にアクセスできる状態なので、認証機能を追加していきます。 認証はsorceryを使用しているので、公式のwikiのsorcery設定のページを参考にします。

Sorcery · sferik/rails_admin Wiki · GitHub

(略)
RailsAdmin.config do |config|

  config.authenticate_with do
    #sorceryのメソッド
    require_login
  end
  config.current_user_method(&:current_user)

  config.parent_controller = 'ApplicationController' #追記
end
(略)
class ApplicationController < ActionController::Base
  before_action :require_login

  private

  def not_authenticated
    flash[:info] = 'ログインしてください'
    redirect_to main_app.login_path #main_appのプレフィックスをつける
  end
end

最初、main_appの意味がわからなかったので、省略していたら、No route matches~のエラーが出てしまいました。
調べてみると、アプリケーションのルーティングと明記する必要があった。

ルーティングプロキシメソッド呼び出しを省略したアプリケーション側のルーティングヘルパーメソッドを、エンジン内でレンダリングされるテンプレートから呼び出そうとすると、未定義メソッド呼び出しエラーが発生することがあります。
このような問題が発生した場合は、アプリケーション側のルーティングメソッドをエンジンから呼びだすときに、main_appというプレフィックスを付け忘れていないかどうかを確認してください。

Rails エンジン入門 - Railsガイド

認可機能の追加

管理者以外は、アクセスできないようにします。
gem 'cancancan'を使います。
rails-adminの公式がこちらを勧めている&ここ以外で使う予定がないので素直に従います。

gem 'cancancan'

追記してbundle install

rails g cancan:ability

公式に設定方法があるのでそのとおりに実装していきます。Lintは多少修正。 Authorization · sferik/rails_admin Wiki · GitHub

CanCanCan · sferik/rails_admin Wiki · GitHub

class Ability
  include CanCan::Ability

  def initialize(user)
    return unless user && user&.admin? # 管理者じゃなかったらこのメソッドを抜ける

    can :access, :rails_admin # 管理者画面のアクセス許可
    can :manage, :all # 管理権限許可
  end
end

更に、先程のrails_admin側の設定ファイルに追記する。

RailsAdmin.config do |config|
  config.authenticate_with do
    require_login

    redirect_to root_path unless current_user.admin? # 追記
  end
  config.current_user_method(&:current_user)

  ## == CancanCan ==
  config.authorize_with :cancancan # コメントアウトを外す

(略)
  end
  config.parent_controller = 'ApplicationController'
end

完成

f:id:Study-Diary:20210105211615p:plain
rails-admin

【エラー解消メモ】[BUG] Segmentation fault

エラー

ローカルではRSpecが通るのに、CircleCI上で落ちてしまう。

環境

Rails 6.0.3

Dockerのimageは以下のとおり

jobs:
  build:
    docker:
      - image: circleci/ruby:2.7.2-node-browsers-legacy

エラー内容

f:id:Study-Diary:20210105193245p:plain
[BUG] Segmentation fault

今まで遭遇したことのないSegmentation fault エラー文をダウンロードして見てみる。

f:id:Study-Diary:20210105193550p:plain
エラー文(抜粋)

..*..................../home/circleci/repo/vendor/bundle/ruby/2.7.0/gems/sassc-2.4.0/lib/sassc/engine.rb:43: [BUG] Segmentation fault at 0x0000000000000000
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]

sasscが問題のよう。 公式のissueを見てみると、同じエラーの人がいて、解決方法も載っていました。

segfault on 2.3.0 · Issue #197 · sass/sassc-ruby · GitHub

Same here at sassc-2.3.0/lib/sassc/engine.rb:43: [BUG] Segmentation fault at 0x0000000000000000 ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]

解決方法

以下を追加

Rails.application.config.assets.configure do |env|
  env.export_concurrent = false
end

やっと通った

f:id:Study-Diary:20210105195817p:plain
success!

原因

sprocketsとwebpackerのコンパイルが並行して行われているため、問題が起こる…らしいので、先の解決方法のコードで並行処理をしないように設定する。
正直、よくわからない

gem 'sprockets' 4.0での並列処理でデッドロックしてしまう問題について - Qiita

chartkick × Highchartsでドーナツグラフを作る

実装したいこと

  • ドーナツグラフの真ん中に合計額
  • ラベルはグラフ上に表示
  • ラベルが細かくて重なってしまう場合は非表示
  • 凡例を非表示

chartkickを使用したら、簡単に実装できました。
ただ、chartkick × Highchartsについての日本語記事がほぼない&各オプションの指定が公式から探しにくく、苦労しました。

chartkickでは、chart-js Google Charts Highchartsを使用することができます。
最初、chart-jsを使用していたのですが、細かい設定はできない&ラベル表示がデフォルトでないため扱いが難しかったので、途中でHighchartsに変更しました。見た目もHighchartsのほうが綺麗な気がします。

f:id:Study-Diary:20201230183715p:plain
ドーナツグラフ

環境

  • rails (6.0.3.4)

  • "chartkick": "^3.2.1",

  • "highcharts": "^8.2.2",

chartkickのインストール

GitHub - ankane/chartkick: Create beautiful JavaScript charts with one line of Ruby

gem "chartkick"

Rails6なので、

yarn add chartkick highcharts
require("chartkick").use(require("highcharts"))

View

line_chart(表示したいデータ) pie_chart(表示したいデータ)のように、表示させたいviewファイルに記述するだけでグラフを生成できます。
今回は円グラフを作りたいので、pie_chartを選択します。 (テンプレートエンジンはslimを使用)

#公式の例
= pie_chart Goal.group(:name).count 

グローバルオプションの追加

今回は1種類のグラフしか使用しないので、全体に適用するグローバルオプションを指定していきます。
Chartkickで使用できるオプションと、Highchartsから使うオプションで記述方法が異なります。
Highchartsのオプションは、library以下に記述していきます。

Chartkick.options = {
  donut: true, # ドーナツグラフ
  width: '400px',
  colors: [ "#769fcd",
            "#b9d7ea",
            "#d6e6f2",
            "#f7fbfc",
          ],
  message: {empty: "データがありません"},
  thousands: ",", 
  suffix: "円",
  legend: false, # 凡例非表示
  library: { # ここからHighchartsのオプション
    title: { # タイトル表示(ここでは、グラフの真ん中に配置して,viewでデータを渡しています。*後述)
      align: 'center',
      verticalAlign: 'middle',
    },
    chart: {
      backgroundColor: 'none',
      plotBorderWidth: 0, 
      plotShadow: false
    },
    plotOptions: {
      pie: {
        dataLabels: {
          enabled: true, 
          distance: -40, # ラベルの位置調節
          allowOverlap: false, # ラベルが重なったとき、非表示にする
          style: { #ラベルフォントの設定
            color: '#555', 
            textAlign: 'center', 
            textOutline: 0, #デフォルトではラベルが白枠で囲まれていてダサいので消す
          }
        },
        size: '110%',
        innerSize: '60%', # ドーナツグラフの中の円の大きさ
        borderWidth: 0,
      }
    },
  }
}

Highchartsの公式でDemoが見られるので、これをcodepenで操作しながらオプションを試しました。 annotations.labelOptions.shadow | Highcharts JS API Reference

View

これは力技で無理やり実装したので、多分もっといい方法がありそう
コントローラーで定義したインスタンス変数@expenses @sum_of_expenditureを取得して表示しています。(内容は省略)

= pie_chart @expenses,library: {title: {text: "支出<br> #{@sum_of_expenditure)"}}

グラフ内に表示したいタイトルはグローバルオプションではなく、ローカルオプションで渡します。

【Rails】 default値の変更

ちょっと詰まったのでメモ フォーム入力画面でplaceholderを表示させたいけど、デフォルト値を設定していたため、表示できない

良し悪しは置いといて、default値を削除したい。

DBはMySQL Rails 6.0.3.4

試したけど、できなかったmigrationスクリプト

class ChangeUserProfileColumn < ActiveRecord::Migration[6.0]
  def up
    change_column :user_profiles, :age, :integer
    change_column :user_profiles, :job, :string, limit: 20
  end
  def down
    change_column :user_profiles, :age, :integer, default: 0
    change_column :user_profiles, :job, :string, default: '', limit: 20
  end
end
class ChangeUserProfileColumnDefault < ActiveRecord::Migration[6.0]
  def change
    change_column_default :user_profiles, :age, from: true, to: false
    change_column_default :user_profiles, :job, from: true, to: false
  end
end

成功したmigrationスクリプト

class ChangeUserProfileColumnDefault < ActiveRecord::Migration[6.0]
  def change
    change_column_default :user_profiles, :age, nil
    change_column_default :user_profiles, :job, nil
  end
end

change_column_default (ActiveRecord::ConnectionAdapters::SchemaStatements) - APIdock

【CircleCI】エラー

モデルスペックは通るのに、システムスペックを追加したら、ローカルではパスするのにCircleCI上で落ちるようになった。 半日苦しんだので、メモ

ローカルでjobを実行できない

❯ circleci local execute --job build
Docker image digest: sha256:ed0cf0f38d357f2599fb3872009de30d79664d48b6fb7167c54867befbc0d794
docker: Error response from daemon: dial unix docker.raw.sock: connect: connection refused.
See 'docker run --help'.

docker起動してなかった。。。

f:id:Study-Diary:20201209194655p:plain
docker-desktop

Systemスペックが落ちる

モデルスペックは問題なし。システムスペックだけ、ローカルでは大丈夫なのにCircleCI上で落ちる。

f:id:Study-Diary:20201209194726p:plain
CIrcleCIエラー

Error reading historical timing data: file does not exist
Requested weighting by historical based timing, but they are not present. Falling back to weighting by name.
DEPRECATION WARNING: Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload ActionText::ContentHelper, for example,
the expected changes won't be reflected in that stale Module object.

These autoloaded constants have been unloaded.

Please, check the "Autoloading and Reloading Constants" guide for solutions.
 (called from <top (required)> at /home/circleci/repo/config/environment.rb:5)
..........Since there is no EDITOR or BETTER_ERRORS_EDITOR environment variable, using Textmate by default.
FFFFFFFF

Failures:

  1) UserSessions ログイン機能 メールアドレスとパスワードが一致しているとき ログインに成功する
     Failure/Error: fill_in 'メールアドレス', with: user.email
     
     Capybara::ElementNotFound:
       Unable to find field "メールアドレス" that is not disabled
     
     [Screenshot]: /home/circleci/repo/tmp/screenshots/failures_r_spec_example_groups_user_sessions_nested_nested_ログインに成功する_166.png

     
     # ./spec/system/user_sessions_spec.rb:10:in `block (4 levels) in <main>'

日本語フォントに対応していない??と思い、フォントをダウンロードしたり色々試したけどできず。
テスト結果のスクリーンショットを見てみる(見方は後述)※色々試すより、先にこの設定をすればよかった。

f:id:Study-Diary:20201209194757p:plain
テスト結果スクリーンショット

Webpacker::Manifest::MissingEntryError

ということなので、yarn installを設定ファイルに追加

    steps:
      - checkout

      # yarnインストール
      - run:
          name: yarn Install
          command: yarn install

やっとできた!

f:id:Study-Diary:20201209194826p:plain
解決!

デバッグ

CircleCIでエラーがでたときのデバッグ方法がわからずに無駄に色々試してしまった

テスト結果を保存する設定を追記する。
これで、CircleCI上のArtifactsのタブからスクリーンショットを確認できる。

      # テスト結果の保存(CircleCI上で見れる)
      - store_test_results:
          path: /tmp/test-results
          destination: test-results
      - store_artifacts:
          path: tmp/screenshots
          destination: test-screenshots

has_oneでbuildメソッドを使うときの書き方

アソシエーションは以下のとおりで、1対1の関係

class User < ApplicationRecord
  has_one :user_profile, dependent: :destroy
end
class UserProfile < ApplicationRecord
  belongs_to :user
end
class UserProfilesController < ApplicationController
# 略
  def create
    @user_profile = current_user.user_profile.build(user_profile_params)
 # 略

こんな感じで、ログインしているユーザー(current_user)が、プロフィール(UserProfile)をbuild…と記述したらエラー。

NoMethodError at /user_profiles
undefined method `build' for nil:NilClass

Hint: Something is `nil` when it probably shouldn't be.

has_oneで関連付けをしているとき、 build_associationという書き方になる。

    @user_profile = current_user.build_user_profile(user_profile_params)

Active Record の関連付け - Railsガイド