20200209のRailsに関する記事は20件です。

Rails6アップデートをproduction環境へリリースしたらインスタンスごと殺してしまった話

あまり目立っていない、地味に追加された機能によって殺されてしまいました :knife:

よし!Rails6アップデートリリースするぞ! :rocket:

bundle update railsを実行し、テストが全て通るよう修正し、Warningを潰し、stagingで入念に動作確認しました。

そして、もう大丈夫!productionリリースしてもいいよね! と、リリースへの機運が高まりきったところで事件は起きました。

productionだけど社内用のクローズドな環境へリリース :rocket:

全体のproductionへリリースする前に、社内用の隔離されたproduction環境へリリースしてみました。

無事リリースが成功し、「ふんふん、まあ大丈夫そうかな??」と触っていたら、急に503エラーが発生してうんともすんともいわなくなりました。

エラー監視(Sentry)を見ても、特にアプリケーションエラーが発生したような痕跡はなさそう。
何が起きたのかと思って急いでアプリケーションサーバへsshしてアプリケーションログを見てみました。
が、特にめぼしいエラーは見当たらず。。 :thinking:

原因を探っていると、急にsshが遮断されました。
どうやらインスタンスがterminateされた模様。。

え?なんかヤバイことしちゃった? :scream:

さすがに焦りました。
社内用のクローズドな環境なので影響範囲は社内に限られるのが不幸中の幸いでした。
社員のみなさんにごめんなさい:sob:して、調査を継続しました。

AWSコンソールからterminateされたインスタンスの情報を見てみても、CPU使用率やメモリ使用率などが異常に上がっているということはなさそうでした。

そして、「おっ復旧したみたい」という声が社内から上がってきました。

えっ?何もしてないけど??? :thinking:

原因も分かってないし、何もしてないのに、確かに復旧していました。

どうやらterminateされたインスタンスの代わりに新たなインスタンスが立ち上がったようです。
新たなインスタンスが元気にリクエストを捌いていました。
新しいインスタンスはRais6アップデート前の状態でデプロイされています。

Auto Scaling?

勝手に立ち上がるってことは、Auto Scalingかな?
と思い、Auto Scalingを見てみました。

確かにアクティビティ履歴にはTerminatingの履歴がありました。
原因を見ると、
image.png

system health check failure.

どうやら、Auto Scaling(ELB)からのヘルスチェックに応答しなかったせいで、unhealty扱いされてインスタンスterminate、という流れだったようです。

ふーん、なるほど。。でもなんで?? :thinking:

nginxのアクセスログを見る

アプリケーションエラーが原因ではなさそう、ということは分かっていました。
では、何が原因か?

ヘルスチェックに応答できないって、そもそもヘルスチェックのリクエストに何返してるの??と思い、nginxのアクセスログを見てみました。

新たに立ち上がった元気なインスタンスへsshしてnginxのアクセスログを見てみました。

time:<アクセス時間> host:192.168.0.54       uri:/health     status:200      size:33 reqsize:-       method:GET      referer:-       ua:ELB-HealthChecker/2.0        reqtime:0.008   apptime:0.008   upstream:127.0.0.1:8080 vhost:192.168.8.12
time:<アクセス時間> host:192.168.2.116      uri:/health     status:200      size:33 reqsize:-       method:GET      referer:-       ua:ELB-HealthChecker/2.0        reqtime:0.013   apptime:0.012   upstream:127.0.0.1:8080 vhost:192.168.8.12
time:<アクセス時間> host:192.168.1.89       uri:/health     status:200      size:33 reqsize:-       method:GET      referer:-       ua:ELB-HealthChecker/2.0        reqtime:0.007   apptime:0.000   upstream:127.0.0.1:8080 vhost:192.168.8.12

こんな感じでヘルスチェックのリクエストが来ていました。
status:200 を返していますね。

ちなみに、Railsアプリケーションの動作確認を目的にヘルスチェックのリクエストを受けるエンドポイントを用意しています。

config/routes.rb
get '/health' => 'health_statuses#show'

この状態で、Rails6アップデートを再度リリースしてみました :rocket:

おっ、やっぱりまた落ちた

無事リリースが成功すると、nginxのアクセスログが、

time:<アクセス時間> host:192.168.0.54       uri:/health     status:403      size:1187       reqsize:-       method:GET      referer:-       ua:ELB-HealthChecker/2.0        reqtime:0.024   apptime:0.024   upstream:127.0.0.1:8080 vhost:192.168.8.12
time:<アクセス時間> host:192.168.2.116      uri:/health     status:403      size:1187       reqsize:-       method:GET      referer:-       ua:ELB-HealthChecker/2.0        reqtime:0.005   apptime:0.008   upstream:127.0.0.1:8080 vhost:192.168.8.12
time:<アクセス時間> host:192.168.1.89       uri:/health     status:403      size:1187       reqsize:-       method:GET      referer:-       ua:ELB-HealthChecker/2.0        reqtime:0.002   apptime:0.000   upstream:127.0.0.1:8080 vhost:192.168.8.12

おっと、status:403 に変わりました。
この状態で数十秒経過したら、先ほどと同じくインスタンスterminateされてsshが切断されました。

terminateされてhealthyなインスタンスが無くなってしまうのでELB側で503を返していた、ということだったんですね。

Rails6で403エラーを返す変更といえば・・

そう、 ActionDispatch::HostAuthorization ですね。

production環境には、以下のようにconfig.hostsを設定していました。

config.hosts << '.example.com'

ELBからのヘルスチェックは、host:192.168.0.54 のようにIPアドレスを指定した形式なので ActionDispatch::HostAuthorization のチェックに引っかかって403エラーを返してしまっていたようです。

どう回避したか

以下の通りconfig.hostsの設定をクリアしました。

config.hosts.clear

これで、 ActionDispatch::HostAuthorization のチェックを無効にできます。(せっかく追加された機能を無効化するのは心苦しいですが・・:cry:

あとで調べたら、そもそもconfig.hostsを設定しなければ、デフォルトでdevelopment環境以外ではチェックされないようですね。

https://github.com/rails/rails/blob/v6.0.2.1/railties/CHANGELOG.md#rails-600beta1-january-18-2019

In other environments Rails.application.config.hosts is empty and no Host header checks will be done.

ひとまず、これでヘルスチェックに応答するようになり、社内用production環境へリリースできました。
その後、無事全体のproductionへもリリースできました :rocket:

まとめ

ELBからのヘルスチェックを使用している場合はActionDispatch::HostAuthorization に注意してください。

今回、ELBがあるのはproductiondだけというインフラ構成の違いによって、stagingでは気づくことができませんでした。

環境ごとの違いをしっかり考慮しておく必要がありますね。
本来は、production と staging はインフラ構成も完全に一致させておくべきです。
しかし、様々な事情によってできないケースもあると思うので難しいところですね。

あと、stagingとproductionの間にもう1クッションとして、社内用のクローズドなproduction環境があるのは本当に助かりました。
もし、いきなり全体productionへリリースしていたら全インスタンス死亡でサービス停止という最悪の事態になるところでした:fearful:

先人の知恵、努力に感謝です :pray:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails 環境構築チートシート

アプリ開発をスタートする際の環境構築について

1からアプリ開発をスタートさせる場合(git cloneからスタートしない場合)

前段のPCの環境構築はこちら
https://qiita.com/kenkentarou/items/e592a10a54c9c7e110b9
上記が終わっている前提で進めます!

  • 新アプリ名のディレクトリを作成
  • $ cd ~/<ディレクトリ名>コマンドで、アプリディレクトリに移動
  • $bundle init →Gemfileを作成
  • $ ruby -vで現状のrubyのバージョンを確認
  • Gemfileでrailsのバージョンを確認

rbenvでrubyのバージョン設定

  • ターミナルで $rbenv versions を実行し、rbenvで管理されているrubyのバージョンを確認

→開発で使用したい該当のバージョンがなければインストールする必要あり。
$ rbenv install 2.6.5(バージョンは一例)

rubyのインストール時にエラーが出た場合

If you don't have the version you need, try upgrading ruby-build: brew update && brew upgrade ruby-build

このエラーが出れば、指摘通り以下のコマンド実行

$brew update

$brew upgrade ruby-build

その後、もう一度トライすると上手くいく

$rbenv install 2.6.5

今回の開発で使うrubyのバージョンを指定

$ rbenv global 2.6.5
以下のコマンドで反映
$ rbenv rehash

gemのインストール

$ bundle install --path vendor/bundle

※以下のようなbundlerのバージョンエラーが出たら

To update to the latest version installed on your system, run `bundle update --bundler`.
To install the missing version, run `gem install bundler:2.0.2

$ gem install bundlerで最新のbundlerをインストール
$ rbenv rehash
$ bundle -vでbundlerのバージョン確認
→その後もう一度
$ bundle install --path vendor/bundle

rails new

$ cd ~/<ディレクトリ名> で、アプリのディレクトリに移動

$ bundle exec rails new . -d mysql --skip-turbolinks --skip-test --skip-coffee
→.はカレントディレクトリを意味する
→gemfileの上書きを聞かれた場合は、yes入力でgemfileを上書き
→うまく行かなければ
$ bundle update

mysqlエラー出た場合
以下をやってみる
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"
$ bundle install

nodeを使う場合(nodeを使わないのであれば飛ばしてください)

nodeのバージョンなどをいじる場合は以下の記事を参照
https://qiita.com/kenkentarou/items/f27bd4e49af6fe429429

nodeとは
関係性
ruby <-> node
rbenv <-> nodebrewやnvm

nodeは一つの言語(node.jsと同義)で、nodeは、言語としても扱うし、サーバーサイドで動くためにコンパイルするシステムとしても使われる

データベースの設定

データベースの設定ファイルを作成

通常の現場では、datebase.yml.defaultというファイルを作成して、設定情報はrootなどセキュリティー上影響ない初期値を入れておき、こちらをgit管理します。

ローカルで作業する際には、datebase.yml.defaultをコピーして、database.ymlというファイルを作成し、自身のPC上の設定に合わせ修正したものを使います。自身の設定に合わせたdatabase.ymlを作成しないと、ローカルでデータベースを走らせることができません。

以下のコマンドを実行し、database.yml.defaultをコピーして、自身の環境に合わせたdatabase.ymlを作成しましょう。

$ cp config/database.yml.default config/database.yml

ローカル上にデータベーステーブル作成

$ bundle exec rails db:create
→テーブル作成
$bundle exec rails db:migrate
→migrationが必要なファイルがあれば実行

$ bundle exec rails s

git flowでの管理の場合

git flowがPCに入っている前提で、

$git flow init
→アプリのディレクトリ全体がgit管理対象になる
→→ローカルにmasterとdevelopがbranchが出来る

プッシュする前に確認すること

.gitignoreに入れておかなければいけないファイル、ディレクトリが無いかどうか

ex)
/vender等

リモートリポジトリにプッシュする

$ git add .
$ git commit -m "inital commit"

$ git remote add origin https://github.com/hoge/hoge.git

$git push --all
→ローカルにあるdevelopとmasterの両方をリモートにあげる

レビューをしやすくするためにrails newの段階でgit flow initをしてmasterとdevelopブランチを自身のパブリックリポジトリに対してプッシュしています!

(PCにgit flow入れてなければ$ brew install git-flow)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails+Redis+Docker】RailsアプリにRedis導入&ランキングの実装

Redisの導入

今回使うRedisはブログサイトでよくあるランキングを表示するために使います。

まずRailsとRedisの接続からしないといけないのですが、ここで2日くらい躓きました。色んなサイトを見まくって試したけど全然繋がらない状態が続き地獄でした。

解決方法

基本的にredisはlocalhost:6379に繋ぐのが普通なのですが、開発段階だとRails自体をlocalhostに繋いでるので混同しちゃって上手く繋がらなくなってしまっていたということでした。

なのでredisの設定をlocalhost→redisといった感じに名前を変更して行えばすんなり上手くいった感じです。

出来てしまえば簡単なことだったと思うのですが、やはり初心者には結構辛いところでした。コード打っててエラーならまだしも繋がらなくてエラーは精神的にかなりきます。

以下は変更点と参考サイトです。

https://stackoverflow.com/questions/34729752/sidekiq-error-connecting-to-redis-on-127-0-0-16379-errnoeconnrefused-on-doc】

ちなみにdocker-compose.ymlはこんな感じです。

docker-compose.yml
version: '3'

 services:
   db:
     image: mysql:8.0.17
     command: mysqld --default-authentication-plugin=mysql_native_password
     volumes:
       - ./db/mysql_data:/var/lib/mysql 
     environment:
       MYSQL_ROOT_PASSWORD: root
       MYSQL_DATABASE: root
     ports:
       - "4306:3306"

   web:
     build: .
     command: bundle exec rails s -p 3000 -b '0.0.0.0'
     volumes:
       - .:/app_name
     ports:
       - "3000:3000"
     depends_on:
       - db
       - redis
     tty: true
     stdin_open: true
     links:
       - db
     environment:
       REDIS_HOST: redis
       REDIS_PORT: 6379

   redis:
     image: redis:5.0.5
     ports:
       - 6379:6379
     volumes:
       - ./redis:/data
     command: redis-server --appendonly yes

Redisでランキング機能の実装

無事redisは導入出来ましたが、「導入出来てしまえばこっちのもん!」というわけではありません。

ランキングを表示しないといけないのでこれまたredisの基礎とcontroller、viewを見直さないといけません。

これも「Rails redis ランキング」と調べたら結構参考サイトは出てくるのですが、仕組みの理解が乏しいので基礎の見直しが必要でした。

redisの特徴としては以下のような感じです。

  • インメモリアルデータベース(すごく早い!)…ランキングなどに向いてる
  • 永続化(定期的にディスクに書き出す)
  • データ構造サーバー

そんなredisをRailsで使うには、gemでredisを導入してREDISメソッドを利用する必要があります。

gemの導入が完了したらconfig/initialize以下にredis.rbを作成して以下を記入。

config/initialize/redis.rb
require 'redis'

uri = URI.parse(ENV["REDIS"])
REDIS = Redis.new(host: uri.host, port: uri.port)
post_controller.rb
  def index
     @posts = Post.all
     ids = REDIS.zrevrangebyscore "posts/daily/#{Date.today.to_s}", "+inf", 0,limit:[0,3]
     @ranking_posts = ids.map{ |id| Post.find(id) }
   end

   # GET /posts/1
   # GET /posts/1.json
   def show
     REDIS.zincrby "posts/daily/#{Date.today.to_s}", 1, @post.id
   end 

今回はredisのソート済みセットを使ってpv数ランキングを実装していきました。
pv数の表示はviewに直接以下のように書けば表示されます。

index.rb
<ul>
   <% @ranking_posts.each do |ranking_post| %>
       <li>
         <%= link_to(ranking_post.title,"/posts/#{ranking_post.id}") %>
         (<%= REDIS.zscore("posts/daily/#{Date.today.to_s}", ranking_post.id).to_i %>PV)
       </li>
   <% end %>
</ul>

アクションに設置する方法がないか考えたのですが、これしか方法がわからなかったです。ちょっと見苦しいですがとりあえずこれでPV数が表示されます。

まとめ

Redisの理解はまだまだ乏しいのですが、触ってると結構楽しかったのでまた利用してみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 6 + RSpec 3.7~でスクリーンショットが真っ白になる問題の要点とその解決法

要点

  • RSpec3.7で追加されたSystemSpecはスクショを自動で保存してくれる機能がある
  • Rails6ではRSpec4系の使用が推奨されている
  • 解決方法はrails_helper.rbパッチをあてる
  • なぜ?を追ってみたけど、わからなかった
  • 他の理由で失敗するケースもある模様

System Specのスクリーンショット保存機能

RSpec 3.7から追加された System Specでは、テスト失敗時に自動でスクリーンショットを保存してくれます。

この機能はRailsのシステムテストに組み込まれているので、feature specのようにcapybara-screenshotなどによるサポートを手動で設定する必要がありません。
Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)|TechRacho

Rails6ではRSpec4の使用が推奨されている

rspec-railsのイシューにもおなじ問題が上がっていました。このスレッドで解決できる :thinking:?

Teardown hooks are called in the wrong order, resulting in a blank screenshot in Rails 6. · Issue #2153 · rspec/rspec-rails

とおもったのですが、「rspec-rails4.0で試してみてほしい」「完璧に動いたよ、サンキュー!」でおわっていました。Rails6にはRSpec4という組み合わせが推奨のようですね。

解決方法

以下の記事で、原因と解決するためのパッチが説明されていました。

rails 6.0.0 と rspec-rails 3.8.2 の組み合わせだと Capybara.reset_sessions! を呼んだ後に save_screenshot が呼ばれるので、スクリーンショットの内容が真っ白になります。
Rails アプリケーションの不安定なテストを撲滅したい 〜system spec のデバッグ方法とテストを不安定にさせる要因〜 - あらびき日記

rails_helper.rbにパッチをあてる

さきほどの記事にはパッチコードはあったのですが、「どこに書けばいいか」は明記されておらず、迷いました :sweat_drops:

rails_helper.rbに書いてあげると動作することがわかりました。

rails_helper.rb
module SystemHelper
  extend ActiveSupport::Concern

  included do |example_group|
    # Screenshots are not taken correctly
    # because RSpec::Rails::SystemExampleGroup calls after_teardown before before_teardown
    example_group.after do
      take_failed_screenshot
    end
  end

  def take_failed_screenshot
    return if @is_failed_screenshot_taken
    super
    @is_failed_screenshot_taken = true
  end
end

RSpec.configure do |config|
  config.include SystemHelper, type: :system
end

なぜ?を追ってみたが…

パッチコードをみたときに、「うーん :thinking: ?」何をしているのか、なぜ直るのかがわかりませんでした。「Capybara.reset_sessions! を呼んだ後に save_screenshot が呼ばれる」というキーワードでしらべてみました。

順序はただしいように見える
def before_teardown
  take_failed_screenshot
ensure
  super
end

def after_teardown
  Capybara.reset_sessions!
ensure
  super
end

TestHelpers::SetupAndTeardownモジュールにたどりついて、それでも :thinking:?で力尽きました(笑)

理由が気になる…。

まとめ

  • RSpec4はbetaだし、あげると既存のテストが壊れる可能性もある。
  • スクショが保存できればそれでいい。
  • RSpec4が来るまで動いてくれたらOK!
  • 上記にあてはまるようであれば、今回の対応でよいのではとおもいました。

他の理由で失敗するケースもある

遭遇はしたことないのですが、パスの長さがエラーの問題になるケースもあるようでした。(詳細未調査)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コード書いたことないPdMやPOに捧ぐ、Rails on Dockerハンズオン vol.6 - Model validation -

この記事はなにか?
この記事はが社内のプログラミング未経験者、ビギナー向けに開催しているRuby on Rails on Dockerハンズオンの内容をまとめたものです。ていうかこの記事を基にそのままハンズオンします。ハンズオンは
1回の内容は喋りながらやると大体40~50分くらいになっています。お昼休みに有志でやっているからです。
現在進行形なので週1ペースで記事投稿していけるように頑張ります。
ビギナーの方のお役にたったり、同じように有志のハンズオンをしようとしている人の参考になれば幸いです。

他のハンズオンへのリンク
Vol.1 - Introduction -
Vol.2 - Hello, Rails on Docker -
Vol.3 - Scaffold, RESTful, MVC -
Vol.4 - Static pages -
Vol.5 - Model and CRUD -
・ Vol.6 - Model validation -

$, #, >について
$: ローカルでコマンドを実行するときは、頭に$をつけています。
#: コンテナの中でコマンドを実行するときは、頭に#をつけています。
>: Rails console内でコマンド(Rubyプログラム)を実行するときは、頭に>をつけています。

はじめに

第6回目となる今回は、前回作成したUserモデルにバリデーションを付けていきます。
バリデーションとは、日本語では『検証』と訳されますが、モデルの保存条件のようなもので、例えばnamenilじゃダメ、とかemail@が含まれていないとダメ、とかそういうやつです。

Validationをつけてみる

早速Validationを付けてみます。いよいよModelファイルをいじる時がきました。

app/models/user.rb
class User < ApplicationRecord
  validates :name,
    presence: true,
    length: { maximum: 50 }

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email,
    presence: true,
    length: { maximum: 255 },
    format: { with: VALID_EMAIL_REGEX },
    uniqueness: { case_sensitive: false }
end

Validationはvalidates [attribute_name], [validations]の形式で定義することができます。
一つの属性に対して複数のValidationを一気に定義していますね。
どんなValidationが定義されているのか紹介していきます!

presence

presenceは『存在性』のValidationです。presence: trueなので『存在しなければならない』ことを検証します。
ユーザー情報としてnameemailが不足しているのはおかしいですよね。
なのでnameemail両方にPresence validationを与えています。

動作を確認してみましょう。Rails consoleで確認していくのでまずはコンテナ&Rails consoleの起動から。

$ docker-compose up -d
$ docker-compose exec web ash
# rails c
> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

> user.save
   (0.3ms)  BEGIN
  User Exists? (6.7ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT $1  [["LIMIT", 1]]
   (0.5ms)  ROLLBACK
=> false

おっと、saveメソッドでfalseが返却されていますね。前回も少しお話しましたが、saveメソッドはデータのDB保存に失敗する場合(validationで引っかかる場合)、falseを返却するようになっています。

エラーの内容はuser.errorsに格納されます。中でもuser.errors.full_messagesを見れば、ユーザー向けのエラーメッセージが格納されているのでエラー理由が一目瞭然です。

> user.errors.full_messages
=> ["Nameを入力してください", "Emailを入力してください", "Emailは不正な値です"]

『を入力してください』がPresence validationに違反した場合のエラーメッセージです。

属性の日本語化

...
validationは日本語化されていますが属性は英語になっていますね...
それもそのはず。属性の表現の仕方を定義していないのですから!

ということで初期設定の時にi18n化対応したのと同じように、localesファイルを編集して『Name』を『お名前』、『Email』を『メールアドレス』と日本語化してあげましょう。

config/locales/ja.yml
ja:
  activerecord:
    attributes:
      user:
        name: "お名前"
        email: "メールアドレス"
    errors:
      ...

属性の名称はactiverecord.attributes.[model_name].[attribute_name]で定義します。
一度Rails consoleをリロードして、もう一度エラーを起こして確認してみましょう!

> reload!
Reloading...
=> true

> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

> user.save
   (0.3ms)  BEGIN
  User Exists? (3.0ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT $1  [["LIMIT", 1]]
   (1.6ms)  ROLLBACK
=> false

> user.errors.full_messages
=> ["お名前を入力してください", "メールアドレスを入力してください", "メールアドレスは不正な値です"]

属性も日本語化されたエラーメッセージに変わりました!

length

lengthは属性値の長さを検証するvalidationです。
maximumで最大文字数(最大桁数)、minimumで最小文字数(最小桁数)、inで最小と最大の範囲、isで特定の文字数(桁数)を検証します。
今回は、nameには最大50文字、emailには最大255文字のvalidationを定義しているので、エラーの確認をするために51文字のnameと256文字のemailを持つUserモデルをsaveしてみましょう。

> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

> user.name = "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

> user.email = "b" * 245 + "@sample.com"
=> "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb@sample.com"

> user.save
   (2.8ms)  BEGIN
  User Exists? (11.7ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb@sample.com"], ["LIMIT", 1]]
   (0.4ms)  ROLLBACK
=> false

> user.errors.full_messages
=> ["お名前は50文字以内で入力してください", "メールアドレスは255文字以内で入力してください"]

文字数について検証してくれていることが確認できました!

format

formatは正規表現とマッチするかを検証するvalidationです。
今回のケースでは、正規表現として/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/iを与えています。これはメールアドレスの正規表現として一般的なもので、簡単に言えば「【何か文字列】@【何か文字列】.【何か文字列】」を表しています。
正規表現の表現方法については今回は端折ります。例えば「Ruby 正規表現の使い方 - Qiita」などを参考に勉強してみてください。常に知っておく必要はあんまりないと思いますが、必要になったときに調べながらでも書けるようになっているのが望ましいでしょう。

では、早速このフォーマットバリデーションが正しく動作するかを確認します。例えば、「@」を抜いた「taro.com」なんていかがでしょうか?絶対メールアドレスじゃないのでちゃんとエラーになってほしいですよね。

> user = User.new(name: "taro", email: "taro.com")
=> #<User id: nil, name: "taro", email: "taro.com", created_at: nil, updated_at: nil>

> user.save
   (0.4ms)  BEGIN
  User Exists? (2.1ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "taro.com"], ["LIMIT", 1]]
   (0.3ms)  ROLLBACK
=> false

> user.errors.full_messages
=> ["メールアドレスは不正な値です"]

メールアドレスのフォーマットエラーになりました。

uniqueness

uniquenessは一意性を検証するvalidationです。
case_sensitivefalseに設定すると大文字小文字の区別をしないで一意性を検証するようになります。emailで「taro@sample.com」と「TARO@sample.com」は同じメールアドレスですのでこのオプションを付与してます。

では検証してみましょう。

> User.create(name: "taro", email: "taro@sample.com")
   (0.5ms)  BEGIN
  User Exists? (3.7ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "taro@sample.com"], ["LIMIT", 1]]
  User Create (2.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "taro"], ["email", "taro@sample.com"], ["created_at", "2020-01-30 13:58:08.104803"], ["updated_at", "2020-01-30 13:58:08.104803"]]
   (2.0ms)  COMMIT
=> #<User id: 2, name: "taro", email: "taro@sample.com", created_at: "2020-01-30 04:58:08", updated_at: "2020-01-30 04:58:08">

> user = User.create(name: "taro", email: "TARO@sample.com")
   (0.5ms)  BEGIN
  User Exists? (3.0ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "TARO@sample.com"], ["LIMIT", 1]]
   (1.1ms)  ROLLBACK
=> #<User id: nil, name: "taro", email: "TARO@sample.com", created_at: nil, updated_at: nil>

> user.errors.full_messages
=> ["メールアドレスはすでに存在します"]

emailの一意性チェックでエラーになったことがわかります。また、大文字小文字を区別せずに一意性チェックをしてくれていることもわかりました。

DBの制約

ここまでモデル側、つまりアプリ側にvalidationをかけてきました。
このような制約はDB側でもかけることができますし、その方が安全だ!という考え方もあります。

Railsでは、デザインパターンとしてActiveRecordを採用しています。ActiveRecordでは「制約をかけるのはモデルの仕事」とされているため、上のようにモデル側で制約をかけています。
こうすることで、制約の内容が変わったとしてもDB側の設定を変えることなくアプリ側だけの改修で柔軟に対応をすることができます。

一方で一意性に関しては、複数のアプリが並列で処理を行う構成を考えるとDB側で制御されている方がよいとされています。

今回は一意性(uniqueness)に関して、DB側にも制約をかけます。

まず、マイグレーションファイルを生成します。

# rails g migration add_index_to_user
Running via Spring preloader in process 194
      invoke  active_record
      create    db/migrate/YYYYMMDDhhmmss_add_index_to_user.rb

これでほとんど空のマイグレーションファイルが生成されますので、中身を書いていきます。

db/migrate/YYYYMMDDhhmmss_add_index_to_user.rb
class AddIndexToUser < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email, unique: true
  end
end

uniquenessの制約はadd_index [table (model)], [column (attribute)], unique: trueで設定します。
ではマイグレーションファイルを適用していきましょー。

# rails db:migrate
== 20200130053807 AddIndexToUser: migrating ===================================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0942s
== 20200130053807 AddIndexToUser: migrated (0.0947s) ==========================

DBの一意性チェックは利用するDBによりますが大文字小文字を区別してしまう可能性があります。そのため、モデル側でDBに保存する前に強制的にemailを小文字化する処理を入れます。

app/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  ...
end

user.saveをするときに、RailsではCallbacksといいますがシーケンシャルな処理が存在します。まずvalidationが実行されsaveが実行されcommitが実行されるといった流れです。
before_saveはその名前から分かるとおり、validationが通った後、saveが始まる前に処理を挟み込むことを意味しています。before_saveの後の{}の中身が処理になりますが、email.downscaleで現在のemailを小文字化して再度self.email、つまり自分の属性値に代入しています。

さて、ここまでやるとDBでも一意制約を入れることができている状態になります。

一度モデル側のuniqueness validationを外しておき、DBだけで一意制約を担保できるか確認してみましょう。

app/models/user.rb
class User < ApplicationRecord
  ...
  validates :email,
    presence: true,
    length: { maximum: 255 },
    format: { with: VALID_EMAIL_REGEX }#,
    # uniqueness: { case_sensitive: false }
end
> reload!
Reloading...
=> true

> user = User.create(name: "taro", email: "TARO@sample.com")
   (0.5ms)  BEGIN
  User Create (13.3ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "taro"], ["email", "taro@sample.com"], ["created_at", "2020-01-30 16:12:37.414147"], ["updated_at", "2020-01-30 16:12:37.414147"]]
   (0.6ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):40
ActiveRecord::RecordNotUnique (PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "index_users_on_email")
DETAIL:  Key (email)=(taro@sample.com) already exists.

> user.errors.full_messages
=> []

今回もROLLBACKが走っているので失敗していることがわかります。
違う点としては、ROLLBACKの理由がモデルのvalidationではなくActiveRecord::RecordNotUnique例外であるということがコンソールから読み取れると思います。モデルでのエラーではないので、user.errors.full_messagesには何も格納されていません。

さて、DB側の制約でも一意性を担保できることを確認できたので、モデルのコメントアウトを元に戻しておきます。

app/models/user.rb
class User < ApplicationRecord
  ...
  validates :email,
    presence: true,
    length: { maximum: 255 },
    format: { with: VALID_EMAIL_REGEX },
    uniqueness: { case_sensitive: false }
end

後片付け

じゃあ、またデータ消します。

$ docker-compose down
$ docker-compose run --rm web rails db:migrate:reset

まとめ

今回は、Modelにvalidationを付与してみました。

ふんふん。ちゃんとvalidationをつけれましたね。
validationは他にもいろいろあります。
Active Record バリデーション - Railsガイド
自分のアプリケーションに合致するバリデーションを見つけましょう!

次回は、Userモデルにセキュアなパスワードを付与して行こうと思います。
パスワードを平文で持つのはやっぱりNG。Railsは簡単にセキュアなパスワードを実装できるんです!

では、次回も乞うご期待!ここまでお読みいただきありがとうございました!

Reference

P.S. 間違っているところ、抜けているところ、説明の仕方を変えるとよりわかりやすくなるところなどありましたら、優しくアドバイスいただけると助かります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

$ is not defined rails

gemfile
gem 'jquery-rails'

gemを追加して読み込んでおくこと。



app>assets>javascripts>application.jsにて

//= require jquery       ←これを追加
//= require jquery_ujs  ←これを追加
//= require_tree .    ←必ずこれより先に記載。



  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【rails tutorial 13.4.4】「ArgumentError: Missing required arguments: aws_access_key_id, aws_secret_access_key」エラーの対処法

経緯

railsチュートリアル「13.4.4 本番環境での画像アップロード」において、
AWSの「Simple Storage Service (S3)」との接続がうまくできませんでした。

本来スキップしても良い項目でしたが、
カリキュラム通り進めて
IAMの設定やS3との接続もできた(と思っていた)のですが

$ git push heroku

をすると、
「ArgumentError: Missing required arguments: aws_access_key_id, aws_secret_access_key」

及び
「remote:! Precompiling assets failed.」

というエラーが出てしまいましたので、その原因と対処法を記載します。

結論

heroku config:setがされていなかった

AWSのS3を使う際には、登録したデータの紐付けが必要になります。
その際のコマンドに""(ダブルクォーテーション)を不必要に付けていたことが原因のようでした。

$ heroku config:set S3_ACCESS_KEY="ココに先ほどメモしたAccessキーを入力"
$ heroku config:set S3_SECRET_KEY="同様に、Secretキーを入力"
$ heroku config:set S3_BUCKET="Bucketの名前を入力"
$ heroku config:set S3_REGION="Regionの名前を入力"

↑こちらはチュートリアルから引用してきたコマンドです。
""は含まずに代入してください。

代入できたか確認するコマンドが以下になります

$ heroku config:get S3_ACCESS_KEY
$ heroku config:get S3_SECRET_KEY
$ heroku config:get S3_BUCKET
$ heroku config:get S3_REGION

setがgetになっただけです。
入力値が不安であれば確認の時に使ってください。

これで問題なく$ git push herokuはできるようになりました。

別件の問題が

git push herokuをやりすぎてlimitいっぱいになってしまいました。
herokuはgit push回数に制限があるんですね。知りませんでした。

そこで、別のherokuをcreateしました。

git push herokuは無事できるようになりました。

しかし、
今度はアプリのsigninができなくなりました。

signinできない原因

認証に使っているedit_account_activation_urlというリンクに入っている名前の中に古い方のheroku.app名が入っていました。

対策

production環境のメール設定のhostを新しい方のherokuへ切り替える

config/environments/production.rb
 host = 'https://自分のherokuアプリ名.herokuapp.com/'

これで本番環境でのsigninができました。
ちょっと脱線してしまい、すみませんでした。

まとめ

・$ heroku config:setコマンドには""は必要ない
・$ heroku config:getで反映できているか確認できる
・production.rbにherokuのhost情報を入れている

参考

https://qiita.com/fuku_tech/items/b21e49ff49cb19f04838
https://310nae.com/es6-compile/
https://pg-happy.jp/rails-aws-s3-upload.html
https://qiita.com/kazukimatsumoto/items/a0daa7281a3948701c39

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails チュートリアル】一度作成したherokuアプリを作り直すときの復旧手順

はじめに

Ruby on Rails チュートリアル を進めている最中、何らかの理由でherokuアプリを再作成したくなったとき(謎のエラーに苦しんだ時とか)の手順をまとめました

以下、筆者はリモートリポジトリとしてBitbucketではなくGithubを使用していますが、それ以外の開発環境はRails Tutorialに準拠しています。

既存のherokuアプリ名を確認

リモート先に設定しているURLから、アプリ名を確認します。

Console-input
$ git remote -v
Console-result
heroku  ssh://git@heroku.com/<アプリ名>.git (fetch)
heroku  ssh://git@heroku.com/<アプリ名>.git (push)
origin  git@github.com:<ユーザー名>/<リポジトリ名>.git (fetch)
origin  git@github.com:<ユーザー名>/<リポジトリ名>.git (push)

リモートリポジトリとしてBitbucketを使用している人は、ここの表記が少し異なります。また、リモートの設定次第では、各URLの先頭が「https:~」で始まる人もいると思います。

いずれにせよ、heroku のリモート先に設定されているURL(上2行)のうち、「.git」の直前の文字列が<アプリ名>になりますので、これをコピーして控えておきましょう

既存のherokuアプリを削除

herokuの無料枠では、5つまでアプリをデプロイすることができます。

しかし、デプロイ枠が余っている場合でも、デプロイ先が複数存在してしまうことは初学者にとって思わぬ形での混乱を招きかねません。

そのため、既存のアプリは一旦消してしまうことをオススメします(余りの無料枠で新規作成する場合は、本章はスキップして頂いて構いません)

Console-input
$ heroku apps:destroy --app <アプリ名>
Console-result
 ▸    WARNING: This will delete ⬢ <アプリ名> including all add-ons.
 ▸    To proceed, type <アプリ名> or re-run this command with --confirm <アプリ名>

<アプリ名>の箇所を、先ほどコピーしたアプリ名をに書き換えてください。

その後、上記のように「本当に消して良いですか?本当に消すなら、アプリ名をもう一回入力してね」という旨のメッセージがきますので、Consoleに<アプリ名>を再入力します

Console-input
> <アプリ名>

これで、一度デプロイしたherokuアプリを消せました。

新規herokuアプリの作成

Console-input
$ heroku create

ここでアプリ名を指定することもできますが、デフォルトだとheroku側で空いているアプリ名を自動で生成してくれます。

console画面から、「https://git.heroku.com/<新規アプリ名>.git」をと書かれている箇所を見つけ、コピーして控えておきましょう

リモート先の再設定

旧アプリ自体は既に削除済みですが、gitのリモート先が旧アプリのURLのままなので、新アプリのURLに変更します。

Console-input
$ git remote remove heroku
$ git remote set-url heroku https://git.heroku.com/<新規アプリ名>.git

これで、リモート先の再設定ができました。念のため確認しておきましょう

Console-input
$ git remote -v
Console-output
heroku  https://git.heroku.com/<新規アプリ名>.git (fetch)
heroku  https://git.heroku.com/<新規アプリ名>.git (push)
origin  git@github.com:<ユーザー名>/<リポジトリ名>.git (fetch)
origin  git@github.com:<ユーザー名>/<リポジトリ名>.git (push)

herokuのリモート先のURLが、新規アプリ名のものになっていれば、OKです。

※任意ですが、リモートリポジトリにgithubを選択肢しており、herokuへのアクセスをHTTPS経由ではなくSSH経由にしたい場合は、リモート先の設定を下記のように描いてください

Console-input
$ git remote set-url heroku ssh://git@heroku.com/<新規アプリ名>.git

確認すると、

Console-input
$ git remote -v
Console-output
heroku  https://git.heroku.com/<新規アプリ名>.git (fetch)
heroku  https://git.heroku.com/<新規アプリ名>.git (push)
origin  git@github.com:<ユーザー名>/<リポジトリ名>.git (fetch)
origin  git@github.com:<ユーザー名>/<リポジトリ名>.git (push)

これでSSH経由のURLになりました

本番環境でのメール設定

「第11章 アカウントの有効化」で「11.4 本番環境でのメール送信 」まで進んでいる人は、SendGridの再設定が必要です(第11章まで進んでいない人はスキップ)

まずは、SendGridのアドオンを再インストールします。

Console-input
$ heroku addons:create sendgrid:starter

その後、本番環境の設定ファイル内の「host = ~」の箇所が旧アプリのホストのままになっているので、修正します。

config/environments/production.rb
Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  host = '<新規アプリ名>.herokuapp.com'
  config.action_mailer.default_url_options = { host: host }
  ActionMailer::Base.smtp_settings = {
    :address        => 'smtp.sendgrid.net',
    :port           => '587',
    :authentication => :plain,
    :user_name      => ENV['SENDGRID_USERNAME'],
    :password       => ENV['SENDGRID_PASSWORD'],
    :domain         => 'heroku.com',
    :enable_starttls_auto => true
  }
  .
  .
  .
end

最後に、heroku config内部における、SendGridのユーザー名とパスワードを更新して完了です。

Console-input
$ heroku config:get SENDGRID_USERNAME
$ heroku config:get SENDGRID_PASSWORD

本番環境での画像アップロード

「第13章 ユーザーのマイクロポスト」内の、「13.4.4 本番環境での画像アップロード
」まで進めていた人は、heroku configにおけるS3に関する情報の再設定が必要です(第13章まで進んでいない人はスキップ)

Console-input
$ heroku config:set S3_ACCESS_KEY="Accessキーを入力"
$ heroku config:set S3_SECRET_KEY="Secretキーを入力"
$ heroku config:set S3_BUCKET="Bucketの名前を入力"
$ heroku config:set S3_REGION="Regionの名前を入力"

各パラメータ値を忘れてしまった場合は、【Railsチュートリアル】S3に画像をアップロードする設定【13章課題】を参考に、AWSコンソールより取得してください。

デプロイ

最後、新規URLへデプロイをして完了です。

Console-input
$ rails test
$ git add -A
$ git commit -m "Recreate heroku app" 

まだ第11章まで進んでいなかった人は、今回の一連の作業の中でファイルを一切編集していないはずです。

そのため下記のように「ファイルの変更がないため、commitしません」と言う表示が出るかもしれません。

Console-result
On branch master
Your branch is up-to-date with 'origin/master'.

nothing to commit, working tree clean

この場合は、そのまま下へ進んでください。

Console-input
$ git push
$ git push heroku

第6章 ユーザーのモデルを作成する」まで進めている人は、heroku上でのmigrationも走らせておきましょう

Console-input
$ heroku run rails db:migrate

加えて、「第10章 ユーザーの更新・表示・削除」まで進めている人は、忘れずにseedファイルを本番環境に流し込んでおきましょう

Console-input
$ heroku run rails db:seed
$ heroku restart

おわりに

以上で、完了です!予期せぬエラーが発生して抜け出せなくなった時は、アプリごと作り直すことでエラーが解消されるケースもあります。よければ参考にしてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsAdminをインストールして簡単に管理画面を作成する方法

Ruby on Rails には管理画面をテンプレート的に作成できるgemがいくつかあります。
今回は、カスタマイズ性はそこまで高くはないもののセットアップが簡単な「RailsAdmin」を使って管理画面を作成します。

管理画面を作成する

最初にgemをインストールします。
Gemfileに記述し、bundel installを実行します。

gem 'rails_admin', '~> 2.0.0.beta'
$ bundle install
$ rails g rails_admin:install

管理画面へアクセス

まずはサーバを起動させます。

$ rails s

ブラウザから、railsの基本画面に "/admin" を後付けしてアクセスします。
初期設定であれば "http://localhost:3000/admin" をアドレスバーに入力します。

インストールに成功していれば、次のような画面が表示されます。
(初回は少し時間がかかります)

スクリーンショット 2020-02-09 15.08.40.png

すでに作成したテーブルを作成している場合にはそれが表示されます。
私の環境の場合、Questions というテーブルを作成しているので表示されていますね。

スクリーンショット 2020-02-09 15.10.45.png

テーブル名を選択すると、編集画面に飛べます。
"+Add new" からレコードを追加したり、外部へ出力したりできます。

BASIC認証をするよう制限をかける

デフォルトだとアドレスバーから誰でも管理画面にアクセスできてしまうので、セキュリティ的に問題があります。
BASIC認証(ユーザー名、パスワード認証)をかけましょう。

config/initializers/rails_admin.rb
# 一番上に追加
  config.authenticate_with do
    authenticate_or_request_with_http_basic('Site Message') do |username, password|
      username == 'admin' && password == 'admin'
    end
  end

サーバを起動しなおして、"http://localhost:3000/admin" にアクセスすると認証ダイアログが表示されます。

スクリーンショット 2020-02-09 15.15.39.png

上記ダイアログが表示されれば成功です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsが動いてくれない。

rails newはなんとか動く。モデル作成その他が動かない。なんでや!

で、rails newした時によくよく見てみるとこんなんがある。

  • bin/rake: Spring inserted
  • bin/rails: Spring inserted

なんぞこれ。

調べてみると以前サーバーが起動しなかった時と同じ感じらしい。

$ ps ax | grep spring
11588 ?        Sl     0:00 spring server | anilova | started 1 hour ago
12708 pts/1    S+     0:00 grep --color=auto spring

11588が何故か動いてるのでこいつを消す。
一旦以下のコードで確認。

$spring status
Spring is running:

11588 spring server | anilova | started 1 hour ago
12283 [ruby] <defunct>
$spring status
Spring stopped.

これで11588は止まったはず。
もう一度確認。

$ spring status
Spring is not running.
$ ps -ef | grep spring
vagrant  12728 12631  0 05:53 pts/1    00:00:00 grep --color=auto spring

これで正常に戻った!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで新しくテーブルを作成するときの基本的な流れ

Railsで新しくモデルを作成するときの備忘メモ

マイグレーションファイルを作成

$ rails g model Test

マイグレーションファイルの編集

マイグレーションファイルを「rails g model Test」で作成すると、次のようなファイルが作成される。

yyyymmddHHMMSS_create_tests.rb
class CreateQuestions < ActiveRecord::Migration[5.2]
  def change
    create_table :tests do |t|

      t.timestamps
    end
  end
end

基本としては、create_table :tests do |t| から end の間に追加したいカラムの情報をつけていく。

ここでは、string型のtitleカラムと、inter型の No というカラムを追加させる。
なお、255文字以下の文字列ならば string 型、それ以上であれば text 型を使います。

yyyymmddHHMMSS_create_tests.rb
class CreateQuestions < ActiveRecord::Migration[5.2]
  def change
    create_table :tests do |t|
      t.string :title
      t.integer :No
      t.timestamps
    end
  end
end

これであとは「rails db:migrate」:を実行すれば、titleカラムとNoカラムとtimestampsカラムを持つ「Test」というテーブル(表のようなもの)が作成される。

マイグレーションファイルをデータベースに反映

$ rails db:migrate

モデルに制約をつけていく

通常、作成したモデルに入れる情報(ここではテーブルに入るデータのようなイメージ)には、好き勝手な情報を入れられないように制約(validates)をつけてあげます。
ちなみにモデルファイルを編集したあとは、特にコマンドを実行する必要はありません。

次の例は、空のタイトルが入力されないように Test モデルに制約(バリデーションといいます)をつける例です。

¥app¥models¥test.rb
class Question < ApplicationRecord
  validates :title, presence: true
end

なお、複数のバリデーションをもたせることもできます。
例えばタイトルの長さを100文字以下にしてほしい場合は次のように記述します。

¥app¥models¥test.rb
class Question < ApplicationRecord
  validates :title,
    presence: true,
    length: { maximum: 100 }
end

以下ではよく使うバリデーションを整理

空の値ではないこと

validates :title, presence: true

一意性(かぶらないようにしたいとき)

validates :title, uniqueness: true

長さ

validates :title,    length: { minimum: 10 }       # 「10文字以上」
validates :title,    length: { maximum: 100 }      # 「100文字以下」
validates :title,    length: { in: 10..100 }        # 「10文字以上100文字以下」
validates :title, length: { is: 8 }            # 「8文字のみ」

数値であること

例えばinteger型で書いてほしいとき

validates :No, numericality: true

メールアドレス形式

正規表現を駆使していて少し難しいと思うので、コピペが楽

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, uniqueness: true, format: { with: VALID_EMAIL_REGEX }

これで「@が入っているか」「一意性があるか」「空ではないか」といった、メールアドレスのデータとして必要な要件を包含できているので便利です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

refileで投稿した画像が表示されない

初投稿です。至らない点が多いとは思いますがよろしくお願いします!

refileで実装した画像投稿機能。投稿はできるものの投稿した画像の表示がどうもおかしい。
投稿画像はどこへやら、表示されるのは穏やかな山と雲のアイコン。困ったのでその時の解決方法をまとめます。

結論から言うと自分の計画性のなさからマイグレーションファイルにimage_idカラムを2つ記述していました。
その結果、image_idカラムのデータ型がstringではなくinteger型になっていたのが原因でした。

現状の問題点...refileで画像投稿後、表示されない

スクリーンショット 2020-02-09 12.00.36.png

新規投稿、一覧表示はどちらも機能しているが、画像の表示だけどうもおかしい。

原因の特定に色々やってみる

そもそも投稿した時に画像がきちんと保存できているのか?
byebugでデバックして投稿した画像がどうなっているのかを確認してみる。

posts_controller.rb
 def create
    @post = Post.new(post_params)
    @post.user_id = current_user.id
    if @post.save
      byebug
      redirect_to posts_path
    else
      redirect_to new_post_path
    end
 end

vagrant環境で動かし新規投稿、その後ターミナルで確認↓
スクリーンショット 2020-02-09 12.39.39.png

image_id:に一応値は入ってるみたい。
.....あれ、image_id: 599?image_idってstring型じゃなかったっけ。画像のurlが入るはずなんだけどな。
とりあえず、schemaでpostsのカラムを確認してみる。

schema.rb
 create_table "posts", force: :cascade do |t|
    t.text "comment"
    t.integer "image_id"
    t.integer "rating"
    t.integer "user_id"
    t.integer "shop_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

integer形になってるこれはおかしい。次にpostsのマイグレーションファイルを確認してみる。

..._create_posts.rb
 class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.text :comment
      t.string :image_id
      t.integer :rating
      t.integer :user_id
      t.integer :shop_id
      t.integer :image_id
      t.timestamps
    end
  end
 end

:image_idが2つ!?
どうやら設計の段階で複数画像を投稿できるようにpost_imagesテーブルで保存しようとしていたのを途中でpostテーブルで保存しようとカラムを追加したのでimage_idが2つという現象になってた。これが原因で画像の保存が上手くいってなかった。
2つある場合はエラーとかではなく、最後の記述が反映されるんですね。
というかめんどくさがって、addやchangeを使わず直接createファイルに記述を追加してrails db:migrate:resetで作り直すというやり方自体に問題ありなんでしょうね。

カラムの記述被りを解消

原因がわかったので後はマイグレーションファイルの操作でt.integer :image_idを消して記述の被りを解消。:image_idのデータ型をstring型に変更。

スクリーンショット 2020-02-09 13.23.43.png

被りを解消してpostsのカラムをstring型に変更したらちゃんと表示されました。
計画性のない予定変更ダメ絶対。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsの環境構築時にLibrary not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)で苦しんだ

Railsの環境構築時に表題のエラーで苦しんだ

おなじみのopensslのエラー。どうせすぐ解決できるやろって思ってたら意外とハマったので備忘録として残しておきます。

bundleを叩くとコケる

諸々の環境構築終了後によしローカルサーバー立ち上げるぞ!と意気揚々と

$ bundle exec rails s

を叩くと

Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
  Referenced from: /usr/local/opt/mysql@5.6/lib/libmysqlclient.18.dylib
  Reason: image not found - /Users/~~/~~/~~/vendor/bundle/ruby/2.5.0/gems/mysql2-0.4.10/lib/mysql2/mysql2.bundle

あれ、opensslで怒られてるぞ…

https://qiita.com/YoshiyukiKato/items/e4f67c588d2943c1253d
この方の記事の通り、

$ brew install openssl
$ brew link openssl --force
$ rbenv uninstall 2.5.5
$ rbenv install 2.5.5

とかでいけるやろ… いけー!!

Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
  Referenced from: /usr/local/opt/mysql@5.6/lib/libmysqlclient.18.dylib
  Reason: image not found - /Users/~~/~~/~~/vendor/bundle/ruby/2.5.0/gems/mysql2-0.4.10/lib/mysql2/mysql2.bundle

え…同じじゃん…

というわけで少しハマってしまいました…

解決法

https://qiita.com/utsu_jimmy/items/a35437faea1c0d2f357d
この方の記事も参考にしながら、rbenvでrubyごと再インストールしたり、brew upgrade試したり一通りはやったのですが、エラー内容は変わらず…

もしかしてopensslのversionがおかしい?と思って、

$ brew switch openssl ~~

みたいにopensslのversionを切り替えようとすると

Error: openssl does not have a version ~~ in the Cellar.
openssl's installed versions: 1.0.2s

あ、~~なんてversionなかった。。。じゃあ1.0.2s指定したったらどうなるんやろ

$ brew switch openssl 1.0.2s

を実行すると、、

Cleaning /usr/local/Cellar/openssl/1.0.2s
Opt link created for /usr/local/Cellar/openssl/1.0.2s

ん?もしかしてうまくいった?
じゃあこれで

$ bundle exec rails s

実行すると

=> Booting WEBrick
=> Rails 5.1.7 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options

ふつうに立ち上がったあああ!

結局opensslのversionが問題だったようです…

記事にするとしょうもないけど、ハマるときはハマりますよね…

この記事が誰かの役に立てば幸いです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(メモ)【Rails勉強会 第7回】アソシエーンション Part1(2020/2/9)

※こちらの記事は,「人生逆転サロン共同開発参加者限定の勉強会」で使用したコードのメモ書きです。

人生逆転サロンの共同開発について知りたい方はこちらをご覧下さい。
http://yanbaru-spike.com/l/c/yE5eqgMh/NuSs3Jlb

0. 内容

【開催日】 2/9(日) 19:30〜20:30

「メッセージ投稿アプリ」を作成する場合,「どのユーザーのメッセージであるか」を判別できる形でデータベースに保存しなければなりません。

Railsアプリを開発する上で,このような関連付け(アソシエーション)は避けて通れないのですが,最初はなかなか分かりづらい内容です。

そのアソシエーションを基礎から順番に解説していきます。

1. 基礎の復習

1.1 アプリの作成

  • アプリの作成
ターミナル
rails new association_tutorial_app
Gemfile
group :development do
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
  # ********** 以下を追加 **********
  # デバッグ用
  gem 'pry-byebug'
  # ********** 以上を追加 **********
end

rails sでサーバーを起動し,http://localhost:3000にアクセスしたときに,Yay! You’re on Rails!が表示されることを確認して下さい。

1.2 ユーザー一覧の表示

ターミナル
rails g model User name:string age:integer
rails db:migrate
  • 初期データ その1
seeds.rb
User.create!(name: '大地', age: 53)
User.create!(name: '前野', age: 30)
User.create!(name: '原田', age: 54)
ターミナル
rails db:seed
  • コンソールの起動
ターミナル
rails c
  • control + Dでコンソールから抜けられる(1文字でも入力していたら使えない)

  • usersテーブル

id name age created_at updated_at
1 大地 53
2 前野 30
3 原田 54
  • ユーザー一覧を表示しよう
ターミナル
rails g controller users index
app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end
end
app/views/users/index.html.erb
<% @users.each do |user| %>
  【名前】 <%= user.name %> 【年齢】 <%= user.age %>
  <hr>
<% end %>

http://localhost:3000/users/indexにアクセス

2. アソシエーション(1:多)

どのユーザーが投稿したメッセージであるかを判断するためには通常「ログイン機能」を利用しますが,複数の内容をまとめて話すと話がややこしくなりますので,今回はアソシエーションのみに焦点を当てることにします。

2.1 データベースの正規化

投稿したユーザーが分かる形でメッセージを保存する場合,どのようにデータベースを設計すればよいでしょうか。

  • 悪い例1

usersテーブル

id name age messages
1 大地 53 ["おはよう", "こんばんは", "おやすみ"]
2 前野 30 ["こんにちは"]
3 原田 54 ["テスト"]
  • 悪い例2

usersテーブル

id name age message
1 大地 53 おはよう
2 前野 30 こんにちは
3 大地 53 こんばんは
4 原田 54 テスト
5 大地 53 おやすみ
  • 正しい例

usersテーブル

id name age
1 大地 53
2 前野 30
3 原田 54

messagesテーブル

id user_id message
1 1 おはよう
2 2 こんにちは
3 1 こんばんは
4 3 テスト
5 1 おやすみ
ターミナル
rails g model Message content:string user:references
rails db:migrate
  • 初期データ その2
seeds.rb
# 元の内容は全て削除
Message.create!(user_id: 1, content: 'おはよう')
Message.create!(user_id: 2, content: 'こんにちは')
Message.create!(user_id: 1, content: 'こんばんは')
Message.create!(user_id: 3, content: 'テスト')
Message.create!(user_id: 1, content: 'おやすみ')

2.2 メッセージの一覧表示,新規投稿機能

ターミナル
rails g controller messages index new create
config/routes.rb
Rails.application.routes.draw do
  root to: "messages#index"
  resources :messages
  get 'users/index'
end
app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index
    @messages = Message.all
  end

  def new
    @message = Message.new
    @users = User.all
  end

  def create
    Message.create!(message_params)
    redirect_to root_path
  end

  private

  def message_params
    params.require(:message).permit(:user_id, :content)
  end
end
app/views/messages/index.html.erb
<%= link_to '新規投稿', new_message_path %>
<hr>
<% @messages.each do |message| %>
  【投稿者名】 <%= message.user.name %> 【内容】 <%= message.content %>
  <hr>
<% end %>
app/views/messages/new.html.erb
<%= form_with model: @message, local: true do |form| %>
  【ユーザー】 <%= form.collection_select :user_id, @users, :id, :name %>
  【内容】 <%= form.text_field :content %>
  <%= form.submit '投稿' %>
<% end %>

【問題】 現状では「一覧表示ページ」でとある問題が起こります。それは何でしょうか?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsアプリをherokuにデプロイするまでの大まかな流れとハマったところ

Railsで簡単なセリフ検索アプリを作りました。
昔から『おおきく振りかぶって』という漫画が大好きで考察もしていたのでその効率化のためにも作ったアプリです。
https://oofurisearch.herokuapp.com/

初めての公開なのでherokuを使ったのですが、結構時間がかかってしまったのでデプロイまでの流れと初心者がハマりやすいところをメモしときたいと思います。

前提条件

  • railsアプリがすでにできている
  • herokuの登録は完了している
  • herokuコマンドが使える

環境

  • Mac
  • docker
  • MySQL

herokuにファイルをアップロードする

herokuにアプリをアップロードするためにはgitを使います。

まずはターミナルでherokuにログイン。

heroku login

Enterを押すと勝手にブラウザが開くのでloginボタンを押します。

ログインできたら次はアプリを作ります。

heroku create アプリ名

アプリ名を入力しなかったら勝手に名前がつきます。
また、同じ名前のアプリ名があるとエラーが出ます。

アプリを作ったらgitでファイルをアップしましょう。

git init
git add .
git commit -am "first commit"
git push heroku master

これでファイルのアップロードはおkです。

DB周辺の設定

DB設定が結構ハマるポイントだと思いますので丁寧に進めていくことをおすすめします。

MySQLを利用したいので、まずはherokuでクレジットカード情報を登録しときます。お金はかかりません。登録ができたら以下を実行。

$ heroku addons:create cleardb:ignite

これでMySQLの設定はできましたが、railsはmysql2を利用していると思うので(gemファイルで確認できます)mysql2にも繋ぎます。

heroku config

CREATE_DATABASE_URL:   mysql://~~~~~~~

「heroku config」で出たURL(上みたいなやつ)をmysql2に設定します。

heroku config:add DATABASE_URL=mysql2://~~~~~~~~

heroku config:set DATABASE_URL='mysql2://~~~~~~~~'

これでmysql2にも設定できました。

最後に、

heroku run rake db:migrate

したらおk。

これであとは「OPENAPP」押したら(コマンドならheroku open)アプリは立ち上がります。ここまでは特に問題ないのですが、めんどくさいのが初期データを投入する場合です。

初期データを投入するときは、まずアプリに以下のファイルを作り記述を行います。

lib/tasks/seed.rb
Dir.glob(File.join(Rails.root, 'db', 'seeds', '*.rb')).each do |file|
  desc "Load the seed data from db/seeds/#{File.basename(file)}."
  task "db:seed:#{File.basename(file).gsub(/\..+$/, '')}" => :environment do
    load(file)
  end
end
db/seeds.rb
User.create!([
  {name: "test", email: "sample@email.com",password:"testtesttest"}
])

初期データを記入したらherokuにpushしときます。そのあとに以下のコマンドを打つと初期データが投入されます。

heroku run rake db:seed

ただここで気をつけないといけないのは、例えばログイン機能などをControllerを経由して入るように設定していると入れません。
どうやら初期データはControllerを経由しないらしく、ただデータベースに放り込まれるだけらしいのでそれでログインはできません。なので、ユーザー情報とかはアプリで新規登録するのがいいと思います。

また、複数の同じモデルのデータを投入したい場合はvalidatesをかけてると一番上のデータ以降読み込んでくれなくなってしまうので外しときましょう。

ちなみに、複数の別のモデルを投入したい場合は以下のようにファイルを分けたらいいと思います。

db/seeds/users.rb
User.create!([
  {name: "test", email: "sample@email.com",password:"testtesttest"}
])
db/seeds/items.rb
Item.create!([
  {item:"*********"}
])
db/seeds.rb
require './db/seeds/users.rb'
require './db/seeds/items.rb'

これで複数モデルを読み込むことができます。

まとめ

これでherokuへのデプロイができます。私みたいな初心者が躓きやすそうなところなので参考になれば嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CircleCI 2.1 設定サンプル(Rails + Rspec + Rubocop + Jest + ESLint + Yarn + Postgres)

環境

  • Circle CI 2.1
  • Rails 5 + PostgresSQL 9.6 + Yarn
    • 主にCircle CIの設定のため、上記環境には大きく依存していません。

やっていること

  • テストの実行(rspecjest
  • Linterの実行(rubocopeslint
    • RAILS_ENV: testで実行しているため、Gemfile内でrubocopgroup :development, :testに定義されている必要があります。
  • デプロイ
    • developmasterブランチへマージされた時に、capistranoを使って各staging、production環境にデプロイ。
    • 結果をslackに通知。
    • capistranoの設定ファイル(config/deploy/staging.rb等)で、set :branch, ENV['BRANCH'] || 'develop'等として、ブランチの設定がされている必要があります。
  • 上記に付随する処理(ソースコード、ライブラリのインストール及びキャッシング)

出来ていないこと

  • fingerprintを環境変数で設定すべき

設定例

.circleci/config.yml
version: 2.1

orbs:
  slack: circleci/slack@3.3.0

executors:
  default:
    working_directory: ~/repo
    docker:
      - image: circleci/ruby:2.6.3-node
        environment:
          BUNDLE_PATH: vendor/bundle
          RAILS_ENV: test
  extended:
    working_directory: ~/repo
    docker:
      - image: circleci/ruby:2.6.3-node
        environment:
          BUNDLE_PATH: vendor/bundle
          RAILS_ENV: test
          PGHOST: 127.0.0.1
          PGUSER: root
      - image: circleci/postgres:9.6.2-alpine
        environment:
          POSTGRES_USER: root
          POSTGRES_DB: circle-test

commands:
  restore_source_code:
    steps:
      - restore_cache:
          name: Restore source code cache
          keys:
            - v1-source-code-{{ .Environment.CIRCLE_SHA1 }}
            - v1-source-code-
  save_source_code:
    steps:
      - save_cache:
          key: v1-source-code-{{ .Environment.CIRCLE_SHA1 }}
          paths:
            - ~/repo
  restore_node_dependencies:
    steps:
      - restore_cache:
          name: Restore node dependencies cache
          keys:
            - v1-node-dependencies-{{ checksum "yarn.lock" }}
            - v1-node-dependencies-
  install_node_dependencies:
    steps:
      - run:
          name: Install node dependencies
          command: yarn install --frozen-lockfile
  save_node_dependencies:
    steps:
      - save_cache:
          name: Save node dependencies cache
          key: v1-node-dependencies-{{ checksum "yarn.lock" }}
          paths:
            - ~/node_modules
  restore_bundle_dependencies:
    steps:
      - restore_cache:
          name: Restore bundle dependencies cache
          keys:
            - v1-bundle-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-bundle-dependencies-
  install_bundler:
    steps:
      - run:
          name: Install bundler
          # Without the following two lines, `echo ... source $BASH_ENV`, you see the following error.
          #   Could not find rake-12.3.2 in any of the sources
          # see: https://discuss.circleci.com/t/using-bundler-2-0-during-ci-fails/27411
          command: |
            echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV
            source $BASH_ENV
            gem install bundler
  install_bundle_dependencies:
    steps:
      - run:
          name: Install bundle dependencies
          command: bundle install --jobs=4 --retry=3 --path vendor/bundle
  save_bundle_dependencies:
    steps:
      - save_cache:
          key: v1-bundle-dependencies-{{ checksum "Gemfile.lock" }}
          paths:
            - ~/vendor/bundle
  wait_for_db_start_up:
    steps:
      - run:
          name: Wait for db start up
          command: dockerize -wait tcp://127.0.0.1:5432 -timeout 1m
  set_up_database:
    steps:
      - run:
          name: Set up database
          command: |
            bin/rails db:create --trace
            bin/rails db:migrate --trace
  run_rubocop:
    steps:
      - run:
          name: Run rubocop
          command: bundle exec rubocop
  run_rspec:
    steps:
      - run:
          name: Run rspec
          command: bundle exec rspec --format progress --format RspecJunitFormatter -o test_results/rspec.xml
  run_eslint:
    steps:
      - run:
          name: Run eslint
          command: yarn lint --format junit -o test_results/eslint.xml
  run_jest:
    steps:
      - run:
          name: Run jest
          command: yarn test --maxWorkers=2 --testResultsProcessor="jest-junit"
          environment:
            JEST_JUNIT_OUTPUT: ~/test_results/jest.xml

jobs:
  checkout_code:
    executor: default
    steps:
      - checkout
      - save_source_code
  node_dependencies:
    executor: default
    steps:
      - restore_source_code
      - restore_node_dependencies
      - install_node_dependencies
      - save_node_dependencies
  bundle_dependencies:
    executor: default
    steps:
      - restore_source_code
      - restore_bundle_dependencies
      - install_bundler
      - install_bundle_dependencies
      - save_bundle_dependencies
  rubocop_job:
    executor: default
    steps:
      - restore_source_code
      - restore_bundle_dependencies
      - install_bundler
      - run_rubocop
  eslint_job:
    executor: default
    steps:
      - restore_source_code
      - restore_node_dependencies
      - run_eslint
  jest_job:
    executor: default
    steps:
      - restore_source_code
      - restore_node_dependencies
      - run_jest
  rspec_job:
    executor: extended
    steps:
      - restore_source_code
      - restore_bundle_dependencies
      - install_bundler
      - wait_for_db_start_up
      - set_up_database
      - run:
          name: Copy application.yml.sample
          command: cp -p config/application.yml.sample config/application.yml
      - run_rspec
  deploy:
    executor: default
    parameters:
      env:
        type: enum
        enum: ['staging', 'production']
    steps:
      - restore_source_code
      - restore_bundle_dependencies
      - install_bundler
      - add_ssh_keys:
          fingerprints: <your_fingerprint_comes_here>
      - run:
          name: Deploy to << parameters.env >>
          command: bundle exec cap << parameters.env >> deploy
      - slack/status:
          mentions: channel

workflows:
  build_test_and_deploy:
    jobs:
      - checkout_code
      - node_dependencies:
          requires:
            - checkout_code
      - bundle_dependencies:
          requires:
            - checkout_code
      - rubocop_job:
          requires:
            - bundle_dependencies
      - eslint_job:
          requires:
            - node_dependencies
      - jest_job:
          requires:
            - node_dependencies
      - rspec_job:
          requires:
            - bundle_dependencies
      - deploy:
          name: Deploy to staging
          env: staging
          requires:
            - rubocop_job
            - eslint_job
            - jest_job
            - rspec_job
          filters:
            branches:
              only: develop
      - deploy:
          name: Deploy to production
          env: production
          requires:
            - rubocop_job
            - eslint_job
            - jest_job
            - rspec_job
          filters:
            branches:
              only: master

ポイント

参照先

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails アプリを作成する際に出たエラー

目的

  • アプリ作成コマンド$ rails new アプリ名を実行した際に出力されたエラーを解決したときの話をまとめる

エラー

  • 下記にエラー内容を記載する。
[10:51:19]MacBook-miriwo~/workspace/study/rails$ rails new tropical_fish_sns --database=mysql
      create  
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
      create  Gemfile
         run  git init from "."
Initialized empty Git repository in /Users/admin/workspace/study/rails/tropical_fish_sns/.git/
      create  package.json
      create  app
      create  app/assets/config/manifest.js
      create  app/assets/stylesheets/application.css
      create  app/channels/application_cable/channel.rb
      create  app/channels/application_cable/connection.rb
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/javascript/channels/consumer.js
      create  app/javascript/channels/index.js
      create  app/javascript/packs/application.js
      create  app/jobs/application_job.rb
      create  app/mailers/application_mailer.rb
      create  app/models/application_record.rb
      create  app/views/layouts/application.html.erb
      create  app/views/layouts/mailer.html.erb
      create  app/views/layouts/mailer.text.erb
      create  app/assets/images/.keep
      create  app/controllers/concerns/.keep
      create  app/models/concerns/.keep
      create  bin
      create  bin/rails
      create  bin/rake
      create  bin/setup
      create  bin/yarn
      create  config
      create  config/routes.rb
      create  config/application.rb
      create  config/environment.rb
      create  config/cable.yml
      create  config/puma.rb
      create  config/spring.rb
      create  config/storage.yml
      create  config/environments
      create  config/environments/development.rb
      create  config/environments/production.rb
Traceback (most recent call last):
        47: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/bin/rails:23:in `<main>'
        46: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/bin/rails:23:in `load'
        45: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/exe/rails:10:in `<top (required)>'
        44: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
        43: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
        42: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/cli.rb:18:in `<top (required)>'
        41: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/command.rb:46:in `invoke'
        40: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/command/base.rb:69:in `perform'
        39: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor.rb:392:in `dispatch'
        38: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/invocation.rb:127:in `invoke_command'
        37: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/command.rb:27:in `run'
        36: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/commands/application/application_command.rb:26:in `perform'
        35: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/base.rb:485:in `start'
        34: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/group.rb:232:in `dispatch'
        33: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/invocation.rb:134:in `invoke_all'
        32: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/invocation.rb:134:in `map'
        31: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/invocation.rb:134:in `each'
        30: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/invocation.rb:134:in `block in invoke_all'
        29: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/invocation.rb:127:in `invoke_command'
        28: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/command.rb:27:in `run'
        27: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/generators/rails/app/app_generator.rb:319:in `create_config_files'
        26: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/generators/app_base.rb:156:in `build'
        25: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/generators/rails/app/app_generator.rb:106:in `config'
        24: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/generators/rails/app/app_generator.rb:19:in `inside'
        23: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions.rb:187:in `inside'
        22: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/2.5.0/fileutils.rb:122:in `cd'
        21: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/2.5.0/fileutils.rb:122:in `chdir'
        20: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions.rb:187:in `block in inside'
        19: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/generators/rails/app/app_generator.rb:115:in `block in config'
        18: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/generators/rails/app/app_generator.rb:19:in `directory'
        17: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/directory.rb:52:in `directory'
        16: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions.rb:93:in `action'
        15: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/directory.rb:66:in `invoke!'
        14: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/directory.rb:80:in `execute!'
        13: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/directory.rb:80:in `each'
        12: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/directory.rb:92:in `block in execute!'
        11: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/file_manipulation.rb:122:in `template'
        10: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/create_file.rb:25:in `create_file'
         9: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions.rb:93:in `action'
         8: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/create_file.rb:60:in `invoke!'
         7: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/empty_directory.rb:117:in `invoke_with_conflict_check'
         6: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/create_file.rb:63:in `block in invoke!'
         5: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/create_file.rb:63:in `open'
         4: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/create_file.rb:63:in `block (2 levels) in invoke!'
         3: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/create_file.rb:53:in `render'
         2: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/thor-1.0.1/lib/thor/actions/file_manipulation.rb:131:in `block in template'
         1: from /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/2.5.0/erb.rb:885:in `result'
/Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/2.5.0/erb.rb:885:in `eval': /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/railties-6.0.2.1/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt:10: syntax error, unexpected ')', expecting end-of-input (SyntaxError)
);  if spring_install? 

原因

  • Railsのバージョンを指定せず現時点で最新のものを使用し作成したため発生したエラーである。

解決方法

  • $ rails new アプリ名の実行時にrailsのバージョンを指定することで解決した。
  • 下記に問題を解決したときのコマンドを記載する。
$ rails _6.0.0_ new tropical_fish_sns --database=mysql

付録

  • 下記に正常に$ rails new アプリ名が実行できたときのターミナル出力を記載する。
[10:54:05]MacBook-miriwo~/workspace/study/rails$ rails _6.0.0_ new tropical_fish_sns --database=mysql
      create  
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
      create  Gemfile
         run  git init from "."
Initialized empty Git repository in /Users/admin/workspace/study/rails/tropical_fish_sns/.git/
      create  package.json
      create  app
      create  app/assets/config/manifest.js
      create  app/assets/stylesheets/application.css
      create  app/channels/application_cable/channel.rb
      create  app/channels/application_cable/connection.rb
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/javascript/channels/consumer.js
      create  app/javascript/channels/index.js
      create  app/javascript/packs/application.js
      create  app/jobs/application_job.rb
      create  app/mailers/application_mailer.rb
      create  app/models/application_record.rb
      create  app/views/layouts/application.html.erb
      create  app/views/layouts/mailer.html.erb
      create  app/views/layouts/mailer.text.erb
      create  app/assets/images/.keep
      create  app/controllers/concerns/.keep
      create  app/models/concerns/.keep
      create  bin
      create  bin/rails
      create  bin/rake
      create  bin/setup
      create  bin/yarn
      create  config
      create  config/routes.rb
      create  config/application.rb
      create  config/environment.rb
      create  config/cable.yml
      create  config/puma.rb
      create  config/spring.rb
      create  config/storage.yml
      create  config/environments
      create  config/environments/development.rb
      create  config/environments/production.rb
      create  config/environments/test.rb
      create  config/initializers
      create  config/initializers/application_controller_renderer.rb
      create  config/initializers/assets.rb
      create  config/initializers/backtrace_silencers.rb
      create  config/initializers/content_security_policy.rb
      create  config/initializers/cookies_serializer.rb
      create  config/initializers/cors.rb
      create  config/initializers/filter_parameter_logging.rb
      create  config/initializers/inflections.rb
      create  config/initializers/mime_types.rb
      create  config/initializers/new_framework_defaults_6_0.rb
      create  config/initializers/wrap_parameters.rb
      create  config/locales
      create  config/locales/en.yml
      create  config/master.key
      append  .gitignore
      create  config/boot.rb
      create  config/database.yml
      create  db
      create  db/seeds.rb
      create  lib
      create  lib/tasks
      create  lib/tasks/.keep
      create  lib/assets
      create  lib/assets/.keep
      create  log
      create  log/.keep
      create  public
      create  public/404.html
      create  public/422.html
      create  public/500.html
      create  public/apple-touch-icon-precomposed.png
      create  public/apple-touch-icon.png
      create  public/favicon.ico
      create  public/robots.txt
      create  tmp
      create  tmp/.keep
      create  tmp/cache
      create  tmp/cache/assets
      create  vendor
      create  vendor/.keep
      create  test/fixtures
      create  test/fixtures/.keep
      create  test/fixtures/files
      create  test/fixtures/files/.keep
      create  test/controllers
      create  test/controllers/.keep
      create  test/mailers
      create  test/mailers/.keep
      create  test/models
      create  test/models/.keep
      create  test/helpers
      create  test/helpers/.keep
      create  test/integration
      create  test/integration/.keep
      create  test/channels/application_cable/connection_test.rb
      create  test/test_helper.rb
      create  test/system
      create  test/system/.keep
      create  test/application_system_test_case.rb
      create  storage
      create  storage/.keep
      create  tmp/storage
      create  tmp/storage/.keep
      remove  config/initializers/cors.rb
      remove  config/initializers/new_framework_defaults_6_0.rb
         run  bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies......
Using rake 13.0.1
Using concurrent-ruby 1.1.5
Using i18n 1.8.2
Using minitest 5.14.0
Using thread_safe 0.3.6
Using tzinfo 1.2.6
Using zeitwerk 2.2.2
Using activesupport 6.0.2.1
Using builder 3.2.4
Using erubi 1.9.0
Using mini_portile2 2.4.0
Using nokogiri 1.10.7
Using rails-dom-testing 2.0.3
Using crass 1.0.6
Using loofah 2.4.0
Using rails-html-sanitizer 1.3.0
Using actionview 6.0.2.1
Using rack 2.1.2
Using rack-test 1.1.0
Using actionpack 6.0.2.1
Using nio4r 2.5.2
Using websocket-extensions 0.1.4
Using websocket-driver 0.7.1
Using actioncable 6.0.2.1
Using globalid 0.4.2
Using activejob 6.0.2.1
Using activemodel 6.0.2.1
Using activerecord 6.0.2.1
Using mimemagic 0.3.4
Using marcel 0.3.3
Using activestorage 6.0.2.1
Using mini_mime 1.0.2
Using mail 2.7.1
Using actionmailbox 6.0.2.1
Using actionmailer 6.0.2.1
Using actiontext 6.0.2.1
Using public_suffix 4.0.3
Using addressable 2.7.0
Using bindex 0.8.1
Using msgpack 1.3.3
Using bootsnap 1.4.5
Using bundler 2.0.2
Using byebug 11.1.1
Using regexp_parser 1.6.0
Using xpath 3.2.0
Using capybara 3.31.0
Using childprocess 3.0.0
Using ffi 1.12.2
Using jbuilder 2.9.1
Using rb-fsevent 0.10.3
Using rb-inotify 0.10.1
Using ruby_dep 1.5.0
Using listen 3.1.5
Using method_source 0.9.2
Using mysql2 0.5.3
Using puma 3.12.2
Using rack-proxy 0.6.5
Using thor 1.0.1
Using railties 6.0.2.1
Using sprockets 3.7.2
Using sprockets-rails 3.2.1
Using rails 6.0.2.1
Using rubyzip 2.2.0
Using sass-listen 4.0.0
Using sass 3.7.4
Using tilt 2.0.10
Using sass-rails 5.1.0
Using selenium-webdriver 3.142.7
Using spring 2.1.0
Using spring-watcher-listen 2.0.1
Using turbolinks-source 5.2.0
Using turbolinks 5.2.1
Using web-console 4.0.1
Using webdrivers 4.2.0
Using webpacker 4.2.2
Bundle complete! 17 Gemfile dependencies, 75 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
         run  bundle binstubs bundler
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
         run  bundle exec spring binstub --all
* bin/rake: Spring inserted
* bin/rails: Spring inserted
       rails  webpacker:install
      create  config/webpacker.yml
Copying webpack core config
      create  config/webpack
      create  config/webpack/development.js
      create  config/webpack/environment.js
      create  config/webpack/production.js
      create  config/webpack/test.js
Copying postcss.config.js to app root directory
      create  postcss.config.js
Copying babel.config.js to app root directory
      create  babel.config.js
Copying .browserslistrc to app root directory
      create  .browserslistrc
The JavaScript app source directory already exists
       apply  /Users/admin/.anyenv/envs/rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/webpacker-4.2.2/lib/install/binstubs.rb
  Copying binstubs
       exist    bin
      create    bin/webpack
      create    bin/webpack-dev-server
      append  .gitignore
Installing all JavaScript dependencies [4.2.2]
         run  yarn add @rails/webpacker@4.2.2 from "."
yarn add v1.19.1
info No lockfile found.
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...
success Saved lockfile.
warning Your current version of Yarn is out of date. The latest version is "1.22.0", while you're on "1.19.1".
info To upgrade, run the following command:
$ brew upgrade yarn
success Saved 610 new dependencies.
info Direct dependencies
├─ @rails/actioncable@6.0.2
├─ @rails/activestorage@6.0.2
├─ @rails/ujs@6.0.2
├─ @rails/webpacker@4.2.2
└─ turbolinks@5.2.0
info All dependencies
├─ @babel/code-frame@7.8.3
├─ @babel/core@7.8.4
├─ @babel/helper-builder-binary-assignment-operator-visitor@7.8.3
├─ @babel/helper-call-delegate@7.8.3
├─ @babel/helper-compilation-targets@7.8.4
├─ @babel/helper-create-class-features-plugin@7.8.3
├─ @babel/helper-define-map@7.8.3
├─ @babel/helper-explode-assignable-expression@7.8.3
├─ @babel/helper-wrap-function@7.8.3
├─ @babel/helpers@7.8.4
├─ @babel/highlight@7.8.3
├─ @babel/plugin-proposal-async-generator-functions@7.8.3
├─ @babel/plugin-proposal-class-properties@7.8.3
├─ @babel/plugin-proposal-dynamic-import@7.8.3
├─ @babel/plugin-proposal-json-strings@7.8.3
├─ @babel/plugin-proposal-nullish-coalescing-operator@7.8.3
├─ @babel/plugin-proposal-object-rest-spread@7.8.3
├─ @babel/plugin-proposal-optional-catch-binding@7.8.3
├─ @babel/plugin-proposal-optional-chaining@7.8.3
├─ @babel/plugin-proposal-unicode-property-regex@7.8.3
├─ @babel/plugin-syntax-top-level-await@7.8.3
├─ @babel/plugin-transform-arrow-functions@7.8.3
├─ @babel/plugin-transform-async-to-generator@7.8.3
├─ @babel/plugin-transform-block-scoped-functions@7.8.3
├─ @babel/plugin-transform-block-scoping@7.8.3
├─ @babel/plugin-transform-classes@7.8.3
├─ @babel/plugin-transform-computed-properties@7.8.3
├─ @babel/plugin-transform-destructuring@7.8.3
├─ @babel/plugin-transform-dotall-regex@7.8.3
├─ @babel/plugin-transform-duplicate-keys@7.8.3
├─ @babel/plugin-transform-exponentiation-operator@7.8.3
├─ @babel/plugin-transform-for-of@7.8.4
├─ @babel/plugin-transform-function-name@7.8.3
├─ @babel/plugin-transform-literals@7.8.3
├─ @babel/plugin-transform-member-expression-literals@7.8.3
├─ @babel/plugin-transform-modules-amd@7.8.3
├─ @babel/plugin-transform-modules-commonjs@7.8.3
├─ @babel/plugin-transform-modules-systemjs@7.8.3
├─ @babel/plugin-transform-modules-umd@7.8.3
├─ @babel/plugin-transform-named-capturing-groups-regex@7.8.3
├─ @babel/plugin-transform-new-target@7.8.3
├─ @babel/plugin-transform-object-super@7.8.3
├─ @babel/plugin-transform-parameters@7.8.4
├─ @babel/plugin-transform-property-literals@7.8.3
├─ @babel/plugin-transform-regenerator@7.8.3
├─ @babel/plugin-transform-reserved-words@7.8.3
├─ @babel/plugin-transform-runtime@7.8.3
├─ @babel/plugin-transform-shorthand-properties@7.8.3
├─ @babel/plugin-transform-spread@7.8.3
├─ @babel/plugin-transform-sticky-regex@7.8.3
├─ @babel/plugin-transform-template-literals@7.8.3
├─ @babel/plugin-transform-typeof-symbol@7.8.4
├─ @babel/plugin-transform-unicode-regex@7.8.3
├─ @babel/preset-env@7.8.4
├─ @babel/runtime@7.8.4
├─ @rails/actioncable@6.0.2
├─ @rails/activestorage@6.0.2
├─ @rails/ujs@6.0.2
├─ @rails/webpacker@4.2.2
├─ @types/parse-json@4.0.0
├─ @types/q@1.5.2
├─ @webassemblyjs/floating-point-hex-parser@1.8.5
├─ @webassemblyjs/helper-code-frame@1.8.5
├─ @webassemblyjs/helper-fsm@1.8.5
├─ @webassemblyjs/helper-wasm-section@1.8.5
├─ @webassemblyjs/wasm-edit@1.8.5
├─ @webassemblyjs/wasm-opt@1.8.5
├─ @xtuc/ieee754@1.2.0
├─ abbrev@1.1.1
├─ acorn@6.4.0
├─ aggregate-error@3.0.1
├─ ajv-errors@1.0.1
├─ ajv-keywords@3.4.1
├─ ajv@6.11.0
├─ amdefine@1.0.1
├─ ansi-styles@3.2.1
├─ anymatch@2.0.0
├─ are-we-there-yet@1.1.5
├─ argparse@1.0.10
├─ arr-flatten@1.1.0
├─ array-find-index@1.0.2
├─ asn1.js@4.10.1
├─ asn1@0.2.4
├─ assert@1.5.0
├─ assign-symbols@1.0.0
├─ async-each@1.0.3
├─ async-foreach@0.1.3
├─ asynckit@0.4.0
├─ atob@2.1.2
├─ autoprefixer@9.7.4
├─ aws-sign2@0.7.0
├─ aws4@1.9.1
├─ babel-loader@8.0.6
├─ babel-plugin-macros@2.8.0
├─ base@0.11.2
├─ base64-js@1.3.1
├─ bcrypt-pbkdf@1.0.2
├─ big.js@5.2.2
├─ binary-extensions@1.13.1
├─ bindings@1.5.0
├─ block-stream@0.0.9
├─ bluebird@3.7.2
├─ boolbase@1.0.0
├─ brace-expansion@1.1.11
├─ braces@2.3.2
├─ browserify-aes@1.2.0
├─ browserify-cipher@1.0.1
├─ browserify-des@1.0.2
├─ browserify-sign@4.0.4
├─ browserify-zlib@0.2.0
├─ buffer-xor@1.0.3
├─ buffer@4.9.2
├─ builtin-status-codes@3.0.0
├─ cache-base@1.0.1
├─ caller-callsite@2.0.0
├─ caller-path@2.0.0
├─ callsites@2.0.0
├─ camelcase-keys@2.1.0
├─ caniuse-lite@1.0.30001025
├─ case-sensitive-paths-webpack-plugin@2.3.0
├─ caseless@0.12.0
├─ chokidar@2.1.8
├─ chownr@1.1.3
├─ chrome-trace-event@1.0.2
├─ class-utils@0.3.6
├─ clean-stack@2.2.0
├─ cliui@5.0.0
├─ clone-deep@4.0.1
├─ coa@2.0.2
├─ code-point-at@1.1.0
├─ collection-visit@1.0.0
├─ color-convert@1.9.3
├─ color-name@1.1.3
├─ color-string@1.5.3
├─ color@3.1.2
├─ combined-stream@1.0.8
├─ commander@2.20.3
├─ compression-webpack-plugin@3.1.0
├─ concat-map@0.0.1
├─ concat-stream@1.6.2
├─ console-browserify@1.2.0
├─ console-control-strings@1.1.0
├─ constants-browserify@1.0.0
├─ convert-source-map@1.7.0
├─ copy-concurrently@1.0.5
├─ copy-descriptor@0.1.1
├─ core-js-compat@3.6.4
├─ core-js@3.6.4
├─ core-util-is@1.0.2
├─ create-ecdh@4.0.3
├─ create-hmac@1.1.7
├─ cross-spawn@6.0.5
├─ crypto-browserify@3.12.0
├─ css-blank-pseudo@0.1.4
├─ css-color-names@0.0.4
├─ css-declaration-sorter@4.0.1
├─ css-has-pseudo@0.10.0
├─ css-loader@3.4.2
├─ css-prefers-color-scheme@3.1.1
├─ css-select-base-adapter@0.1.1
├─ css-select@2.1.0
├─ css-unit-converter@1.1.1
├─ css-what@3.2.1
├─ cssdb@4.4.0
├─ cssnano-preset-default@4.0.7
├─ cssnano-util-raw-cache@4.0.1
├─ cssnano-util-same-parent@4.0.1
├─ cssnano@4.1.10
├─ csso@4.0.2
├─ currently-unhandled@0.4.1
├─ cyclist@1.0.1
├─ dashdash@1.14.1
├─ debug@2.6.9
├─ decamelize@1.2.0
├─ decode-uri-component@0.2.0
├─ deep-extend@0.6.0
├─ delayed-stream@1.0.0
├─ delegates@1.0.0
├─ des.js@1.0.1
├─ detect-file@1.0.0
├─ detect-libc@1.0.3
├─ diffie-hellman@5.0.3
├─ dom-serializer@0.2.2
├─ domain-browser@1.2.0
├─ domelementtype@1.3.1
├─ domutils@1.7.0
├─ dot-prop@4.2.0
├─ duplexify@3.7.1
├─ ecc-jsbn@0.1.2
├─ electron-to-chromium@1.3.345
├─ emoji-regex@7.0.3
├─ emojis-list@2.1.0
├─ enhanced-resolve@4.1.0
├─ entities@2.0.0
├─ errno@0.1.7
├─ error-ex@1.3.2
├─ es-to-primitive@1.2.1
├─ escape-string-regexp@1.0.5
├─ eslint-scope@4.0.3
├─ esprima@4.0.1
├─ esrecurse@4.2.1
├─ estraverse@4.3.0
├─ events@3.1.0
├─ execa@1.0.0
├─ expand-brackets@2.1.4
├─ expand-tilde@2.0.2
├─ extend@3.0.2
├─ extglob@2.0.4
├─ extsprintf@1.3.0
├─ fast-deep-equal@3.1.1
├─ fast-json-stable-stringify@2.1.0
├─ file-loader@4.3.0
├─ file-uri-to-path@1.0.0
├─ fill-range@4.0.0
├─ find-cache-dir@2.1.0
├─ findup-sync@3.0.0
├─ flatted@2.0.1
├─ flatten@1.0.3
├─ flush-write-stream@1.1.1
├─ for-in@1.0.2
├─ forever-agent@0.6.1
├─ form-data@2.3.3
├─ from2@2.3.0
├─ fs-minipass@2.1.0
├─ fs.realpath@1.0.0
├─ fsevents@1.2.11
├─ fstream@1.0.12
├─ gauge@2.7.4
├─ gaze@1.1.3
├─ gensync@1.0.0-beta.1
├─ get-caller-file@2.0.5
├─ get-stream@4.1.0
├─ getpass@0.1.7
├─ glob-parent@3.1.0
├─ glob@7.1.6
├─ global-modules@2.0.0
├─ global-prefix@3.0.0
├─ globule@1.3.0
├─ har-schema@2.0.0
├─ har-validator@5.1.3
├─ has-ansi@2.0.0
├─ has-unicode@2.0.1
├─ has-value@1.0.0
├─ hash.js@1.1.7
├─ hex-color-regex@1.1.0
├─ hmac-drbg@1.0.1
├─ hosted-git-info@2.8.5
├─ hsl-regex@1.0.0
├─ hsla-regex@1.0.0
├─ html-comment-regex@1.1.2
├─ http-signature@1.2.0
├─ https-browserify@1.0.0
├─ iconv-lite@0.4.24
├─ icss-utils@4.1.1
├─ ieee754@1.1.13
├─ ignore-walk@3.0.3
├─ import-cwd@2.1.0
├─ import-fresh@2.0.0
├─ import-from@2.1.0
├─ import-local@2.0.0
├─ in-publish@2.0.0
├─ indent-string@4.0.0
├─ infer-owner@1.0.4
├─ inflight@1.0.6
├─ ini@1.3.5
├─ interpret@1.2.0
├─ invert-kv@2.0.0
├─ is-absolute-url@2.1.0
├─ is-accessor-descriptor@1.0.0
├─ is-arrayish@0.2.1
├─ is-binary-path@1.0.1
├─ is-callable@1.1.5
├─ is-color-stop@1.1.0
├─ is-data-descriptor@1.0.0
├─ is-date-object@1.0.2
├─ is-descriptor@1.0.2
├─ is-directory@0.3.1
├─ is-extglob@2.1.1
├─ is-finite@1.0.2
├─ is-obj@1.0.1
├─ is-plain-obj@1.1.0
├─ is-plain-object@2.0.4
├─ is-regex@1.0.5
├─ is-resolvable@1.1.0
├─ is-stream@1.1.0
├─ is-svg@3.0.0
├─ is-symbol@1.0.3
├─ is-typedarray@1.0.0
├─ is-utf8@0.2.1
├─ is-windows@1.0.2
├─ is-wsl@1.1.0
├─ isarray@1.0.0
├─ isexe@2.0.0
├─ isstream@0.1.2
├─ jest-worker@25.1.0
├─ js-base64@2.5.1
├─ js-tokens@4.0.0
├─ jsesc@2.5.2
├─ json-schema-traverse@0.4.1
├─ json-schema@0.2.3
├─ json-stringify-safe@5.0.1
├─ json5@2.1.1
├─ jsprim@1.4.1
├─ last-call-webpack-plugin@3.0.0
├─ lcid@2.0.0
├─ leven@3.1.0
├─ lines-and-columns@1.1.6
├─ load-json-file@1.1.0
├─ loader-runner@2.4.0
├─ locate-path@3.0.0
├─ lodash.get@4.4.2
├─ lodash.has@4.5.2
├─ lodash.memoize@4.1.2
├─ lodash.template@4.5.0
├─ lodash.templatesettings@4.2.0
├─ lodash.uniq@4.5.0
├─ lodash@4.17.15
├─ loose-envify@1.4.0
├─ loud-rejection@1.6.0
├─ make-dir@2.1.0
├─ mamacro@0.0.3
├─ map-age-cleaner@0.1.3
├─ map-obj@1.0.1
├─ map-visit@1.0.0
├─ mdn-data@2.0.4
├─ mem@4.3.0
├─ memory-fs@0.4.1
├─ meow@3.7.0
├─ merge-stream@2.0.0
├─ miller-rabin@4.0.1
├─ mime-db@1.43.0
├─ mime-types@2.1.26
├─ mimic-fn@2.1.0
├─ mini-css-extract-plugin@0.8.2
├─ minimalistic-assert@1.0.1
├─ minimalistic-crypto-utils@1.0.1
├─ minimatch@3.0.4
├─ minimist@1.2.0
├─ minipass-collect@1.0.2
├─ minipass-flush@1.0.5
├─ minipass-pipeline@1.2.2
├─ minipass@3.1.1
├─ minizlib@1.3.3
├─ mississippi@3.0.0
├─ mixin-deep@1.3.2
├─ mkdirp@0.5.1
├─ ms@2.1.2
├─ nan@2.14.0
├─ nanomatch@1.2.13
├─ needle@2.3.2
├─ nice-try@1.0.5
├─ node-gyp@3.8.0
├─ node-libs-browser@2.2.1
├─ node-pre-gyp@0.14.0
├─ node-releases@1.1.47
├─ node-sass@4.13.1
├─ nopt@3.0.6
├─ normalize-package-data@2.5.0
├─ normalize-range@0.1.2
├─ normalize-url@1.9.1
├─ npm-bundled@1.1.1
├─ npm-packlist@1.4.8
├─ npm-run-path@2.0.2
├─ npmlog@4.1.2
├─ nth-check@1.0.2
├─ num2fraction@1.2.2
├─ oauth-sign@0.9.0
├─ object-assign@4.1.1
├─ object-copy@0.1.0
├─ object-inspect@1.7.0
├─ object-keys@1.1.1
├─ object.getownpropertydescriptors@2.1.0
├─ object.values@1.1.1
├─ optimize-css-assets-webpack-plugin@5.0.3
├─ os-browserify@0.3.0
├─ os-homedir@1.0.2
├─ os-locale@3.1.0
├─ os-tmpdir@1.0.2
├─ osenv@0.1.5
├─ p-defer@1.0.0
├─ p-finally@1.0.0
├─ p-is-promise@2.1.0
├─ p-limit@2.2.2
├─ p-locate@3.0.0
├─ p-map@3.0.0
├─ p-try@2.2.0
├─ pako@1.0.11
├─ parallel-transform@1.2.0
├─ parent-module@1.0.1
├─ parse-json@4.0.0
├─ parse-passwd@1.0.0
├─ pascalcase@0.1.1
├─ path-browserify@0.0.1
├─ path-complete-extname@1.0.0
├─ path-dirname@1.0.2
├─ path-exists@3.0.0
├─ path-key@2.0.1
├─ path-parse@1.0.6
├─ path-type@4.0.0
├─ performance-now@2.1.0
├─ pinkie@2.0.4
├─ pnp-webpack-plugin@1.6.0
├─ posix-character-classes@0.1.1
├─ postcss-attribute-case-insensitive@4.0.2
├─ postcss-calc@7.0.1
├─ postcss-color-functional-notation@2.0.1
├─ postcss-color-gray@5.0.0
├─ postcss-color-hex-alpha@5.0.3
├─ postcss-color-mod-function@3.0.3
├─ postcss-color-rebeccapurple@4.0.1
├─ postcss-colormin@4.0.3
├─ postcss-convert-values@4.0.1
├─ postcss-custom-media@7.0.8
├─ postcss-custom-properties@8.0.11
├─ postcss-custom-selectors@5.1.2
├─ postcss-dir-pseudo-class@5.0.0
├─ postcss-discard-comments@4.0.2
├─ postcss-discard-duplicates@4.0.2
├─ postcss-discard-empty@4.0.1
├─ postcss-discard-overridden@4.0.1
├─ postcss-double-position-gradients@1.0.0
├─ postcss-env-function@2.0.2
├─ postcss-flexbugs-fixes@4.2.0
├─ postcss-focus-visible@4.0.0
├─ postcss-focus-within@3.0.0
├─ postcss-font-variant@4.0.0
├─ postcss-gap-properties@2.0.0
├─ postcss-image-set-function@3.0.1
├─ postcss-import@12.0.1
├─ postcss-initial@3.0.2
├─ postcss-lab-function@2.0.1
├─ postcss-load-config@2.1.0
├─ postcss-loader@3.0.0
├─ postcss-logical@3.0.0
├─ postcss-media-minmax@4.0.0
├─ postcss-merge-longhand@4.0.11
├─ postcss-merge-rules@4.0.3
├─ postcss-minify-font-values@4.0.2
├─ postcss-minify-gradients@4.0.2
├─ postcss-minify-params@4.0.2
├─ postcss-minify-selectors@4.0.2
├─ postcss-modules-extract-imports@2.0.0
├─ postcss-modules-local-by-default@3.0.2
├─ postcss-modules-scope@2.1.1
├─ postcss-modules-values@3.0.0
├─ postcss-nesting@7.0.1
├─ postcss-normalize-charset@4.0.1
├─ postcss-normalize-display-values@4.0.2
├─ postcss-normalize-positions@4.0.2
├─ postcss-normalize-repeat-style@4.0.2
├─ postcss-normalize-string@4.0.2
├─ postcss-normalize-timing-functions@4.0.2
├─ postcss-normalize-unicode@4.0.1
├─ postcss-normalize-url@4.0.1
├─ postcss-normalize-whitespace@4.0.2
├─ postcss-ordered-values@4.1.2
├─ postcss-overflow-shorthand@2.0.0
├─ postcss-page-break@2.0.0
├─ postcss-place@4.0.1
├─ postcss-preset-env@6.7.0
├─ postcss-pseudo-class-any-link@6.0.0
├─ postcss-reduce-initial@4.0.3
├─ postcss-reduce-transforms@4.0.2
├─ postcss-replace-overflow-wrap@3.0.0
├─ postcss-safe-parser@4.0.1
├─ postcss-selector-matches@4.0.0
├─ postcss-selector-not@4.0.0
├─ postcss-svgo@4.0.2
├─ postcss-unique-selectors@4.0.1
├─ prepend-http@1.0.4
├─ private@0.1.8
├─ process-nextick-args@2.0.1
├─ process@0.11.10
├─ prr@1.0.1
├─ pseudomap@1.0.2
├─ psl@1.7.0
├─ public-encrypt@4.0.3
├─ pumpify@1.5.1
├─ punycode@1.4.1
├─ q@1.5.1
├─ qs@6.5.2
├─ query-string@4.3.4
├─ querystring-es3@0.2.1
├─ querystring@0.2.0
├─ randomfill@1.0.4
├─ rc@1.2.8
├─ read-cache@1.0.0
├─ read-pkg@1.1.0
├─ readdirp@2.2.1
├─ redent@1.0.0
├─ regenerate-unicode-properties@8.1.0
├─ regenerator-runtime@0.13.3
├─ regenerator-transform@0.14.1
├─ regexpu-core@4.6.0
├─ regjsgen@0.5.1
├─ regjsparser@0.6.2
├─ remove-trailing-separator@1.1.0
├─ repeat-element@1.1.3
├─ repeating@2.0.1
├─ request@2.88.0
├─ require-main-filename@2.0.0
├─ resolve-cwd@2.0.0
├─ resolve-dir@1.0.1
├─ resolve-url@0.2.1
├─ resolve@1.15.1
├─ ret@0.1.15
├─ rgb-regex@1.0.1
├─ rgba-regex@1.0.0
├─ rimraf@2.7.1
├─ run-queue@1.0.3
├─ safer-buffer@2.1.2
├─ sass-graph@2.2.4
├─ sass-loader@7.3.1
├─ sax@1.2.4
├─ scss-tokenizer@0.2.3
├─ semver@5.7.1
├─ set-value@2.0.1
├─ setimmediate@1.0.5
├─ shallow-clone@3.0.1
├─ shebang-command@1.2.0
├─ shebang-regex@1.0.0
├─ simple-swizzle@0.2.2
├─ snapdragon-node@2.1.1
├─ snapdragon-util@3.0.1
├─ sort-keys@1.1.2
├─ source-list-map@2.0.1
├─ source-map-resolve@0.5.3
├─ source-map-support@0.5.16
├─ source-map-url@0.4.0
├─ spark-md5@3.0.0
├─ spdx-correct@3.1.0
├─ spdx-exceptions@2.2.0
├─ split-string@3.1.0
├─ sprintf-js@1.0.3
├─ sshpk@1.16.1
├─ ssri@7.1.0
├─ stable@0.1.8
├─ static-extend@0.1.2
├─ stdout-stream@1.4.1
├─ stream-browserify@2.0.2
├─ stream-each@1.2.3
├─ stream-http@2.8.3
├─ strict-uri-encode@1.1.0
├─ string_decoder@1.1.1
├─ string.prototype.trimleft@2.1.1
├─ string.prototype.trimright@2.1.1
├─ strip-bom@2.0.0
├─ strip-eof@1.0.0
├─ strip-indent@1.0.1
├─ strip-json-comments@2.0.1
├─ style-loader@1.1.3
├─ stylehacks@4.0.3
├─ supports-color@6.1.0
├─ svgo@1.3.2
├─ tar@2.2.2
├─ terser-webpack-plugin@2.3.4
├─ terser@4.6.3
├─ through2@2.0.5
├─ timers-browserify@2.0.11
├─ timsort@0.3.0
├─ to-arraybuffer@1.0.1
├─ to-fast-properties@2.0.0
├─ to-object-path@0.3.0
├─ to-regex-range@2.1.1
├─ tough-cookie@2.4.3
├─ trim-newlines@1.0.0
├─ true-case-path@1.0.3
├─ ts-pnp@1.1.5
├─ tslib@1.10.0
├─ tty-browserify@0.0.0
├─ tunnel-agent@0.6.0
├─ turbolinks@5.2.0
├─ tweetnacl@0.14.5
├─ typedarray@0.0.6
├─ unicode-canonical-property-names-ecmascript@1.0.4
├─ unicode-match-property-ecmascript@1.0.4
├─ unicode-match-property-value-ecmascript@1.1.0
├─ unicode-property-aliases-ecmascript@1.0.5
├─ union-value@1.0.1
├─ unique-slug@2.0.2
├─ unquote@1.1.1
├─ unset-value@1.0.0
├─ upath@1.2.0
├─ uri-js@4.2.2
├─ urix@0.1.0
├─ url@0.11.0
├─ use@3.1.1
├─ util-deprecate@1.0.2
├─ util.promisify@1.0.1
├─ util@0.11.1
├─ uuid@3.4.0
├─ v8-compile-cache@2.0.3
├─ validate-npm-package-license@3.0.4
├─ vendors@1.0.4
├─ verror@1.10.0
├─ vm-browserify@1.1.2
├─ watchpack@1.6.0
├─ webpack-assets-manifest@3.1.1
├─ webpack-cli@3.3.10
├─ webpack@4.41.5
├─ which-module@2.0.0
├─ which@1.3.1
├─ wide-align@1.1.3
├─ worker-farm@1.7.0
├─ wrap-ansi@5.1.0
├─ xtend@4.0.2
├─ yallist@3.1.1
├─ yaml@1.7.2
├─ yargs-parser@13.1.1
└─ yargs@13.2.4
✨  Done in 49.66s.
Installing dev server for live reloading
         run  yarn add --dev webpack-dev-server from "."
yarn add v1.19.1
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
warning " > webpack-dev-server@3.10.3" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
[4/4] ?  Building fresh packages...
success Saved lockfile.
success Saved 100 new dependencies.
info Direct dependencies
└─ webpack-dev-server@3.10.3
info All dependencies
├─ @types/events@3.0.0
├─ @types/glob@7.1.1
├─ @types/minimatch@3.0.3
├─ @types/node@13.7.0
├─ accepts@1.3.7
├─ ansi-colors@3.2.4
├─ ansi-html@0.0.7
├─ array-flatten@1.1.1
├─ array-union@1.0.2
├─ array-uniq@1.0.3
├─ async-limiter@1.0.1
├─ async@2.6.3
├─ batch@0.6.1
├─ body-parser@1.19.0
├─ bonjour@3.5.0
├─ buffer-indexof@1.1.1
├─ cliui@4.1.0
├─ compressible@2.0.18
├─ compression@1.7.4
├─ connect-history-api-fallback@1.6.0
├─ content-disposition@0.5.3
├─ cookie-signature@1.0.6
├─ cookie@0.4.0
├─ deep-equal@1.1.1
├─ default-gateway@4.2.0
├─ del@4.1.1
├─ destroy@1.0.4
├─ detect-node@2.0.4
├─ dns-equal@1.0.0
├─ dns-packet@1.3.1
├─ dns-txt@2.0.2
├─ ee-first@1.1.1
├─ eventemitter3@4.0.0
├─ eventsource@1.0.7
├─ express@4.17.1
├─ faye-websocket@0.10.0
├─ finalhandler@1.1.2
├─ follow-redirects@1.10.0
├─ forwarded@0.1.2
├─ globby@6.1.0
├─ handle-thing@2.0.0
├─ hpack.js@2.1.6
├─ html-entities@1.2.1
├─ http-deceiver@1.2.7
├─ http-parser-js@0.4.10
├─ http-proxy-middleware@0.19.1
├─ http-proxy@1.18.0
├─ internal-ip@4.3.0
├─ ip-regex@2.1.0
├─ ip@1.1.5
├─ ipaddr.js@1.9.1
├─ is-absolute-url@3.0.3
├─ is-arguments@1.0.4
├─ is-path-cwd@2.2.0
├─ is-path-in-cwd@2.1.0
├─ is-path-inside@2.1.0
├─ json3@3.3.3
├─ killable@1.0.1
├─ loglevel@1.6.6
├─ media-typer@0.3.0
├─ merge-descriptors@1.0.1
├─ methods@1.1.2
├─ mime@2.4.4
├─ multicast-dns-service-types@1.1.0
├─ multicast-dns@6.2.3
├─ negotiator@0.6.2
├─ node-forge@0.9.0
├─ object-is@1.0.2
├─ obuf@1.1.2
├─ on-headers@1.0.2
├─ opn@5.5.0
├─ original@1.0.2
├─ p-retry@3.0.1
├─ path-is-inside@1.0.2
├─ path-to-regexp@0.1.7
├─ portfinder@1.0.25
├─ proxy-addr@2.0.5
├─ querystringify@2.1.1
├─ raw-body@2.4.0
├─ regexp.prototype.flags@1.3.0
├─ retry@0.12.0
├─ select-hose@2.0.0
├─ selfsigned@1.10.7
├─ serve-index@1.9.1
├─ serve-static@1.14.1
├─ sockjs-client@1.4.0
├─ sockjs@0.3.19
├─ spdy-transport@3.0.0
├─ spdy@4.0.1
├─ thunky@1.1.0
├─ type-is@1.6.18
├─ unpipe@1.0.0
├─ utils-merge@1.0.1
├─ wbuf@1.7.3
├─ webpack-dev-middleware@3.7.2
├─ webpack-dev-server@3.10.3
├─ websocket-extensions@0.1.3
├─ ws@6.2.1
├─ yargs-parser@11.1.1
└─ yargs@12.0.5
✨  Done in 14.68s.
Webpacker successfully installed ? ?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Choices.js + fetchAPIでフィルタ付き動的セレクトボックス [脱jQuery]

概要

件名のモノが必要になった時
ググって出てきたのはajaxやcoffee script、select2にchosenと
内容が古かったり環境制約で使えないものだったりで苦しめられたので
rails6とpure javascriptで動くサンプルを遺します↓


親カテゴリの選択に応じて動的に子の選択肢をセットする
Screen Shot 2020-02-09 at 2.18.54.png


フィルタ検索機能
Screen Shot 2020-02-09 at 4.01.08.png


Sample app source code

  • Ruby 2.7.0
  • Rails 6.0.2.1
  • Choices.js
  • Fetch API

Choices.js

素のjavascriptで書かれた軽量custom select box
jQueryに依存せずにselect2やchosenのようなフィルタ機能を実現します
option設定やメソッドの詳細はGitHubで確認出来ます
公式動作デモ


Fetch API

Webブラウザの新しめの標準APIで、非同期通信処理をカンタンに書けます
従来のjQuery.ajax、XHR(XMLHttpRequest)を代替するとかしないとか。


How does it work?

大まかな処理の流れは以下になります

  1. ユーザが親select boxからカテゴリを選択する
  2. 親select boxのchangeイベントが発火
  3. javascriptがfetchでserverのtile一覧apiを叩く
  4. serverがtile一覧をjsonで返す
  5. javascriptがjsonを受け取る
  6. javascriptがchoicesメソッドを経由して子select boxにjsonの内容をセットする

以下具体的なコードです


javascript

  • /javascript/tiles.js
document.addEventListener('DOMContentLoaded', function() {
  /* apply choices to select boxes */
  const choicesOptions = {
    shouldSort: false,
    removeItemButton: true,
    searchResultLimit: 9,   // default: 4
    searchFields: ['label'] // default: ['label', 'value']
  };
  const selects = document.querySelectorAll('select');
  selects.forEach(function(select) {
    select.choices = new Choices(select, choicesOptions);
  });

  /* add event listener to first select */
  const firstSelect = document.getElementById('first_select')
  if (firstSelect != null) {
    firstSelect.addEventListener('change', setSecondChoices); // type: 'input' is not effective.
  }

  /* get tiles json by fetch api */
  async function getTiles(tile_category_id) {
    const params = new URLSearchParams();
    params.set('tile_category_id', tile_category_id);
    const apiURL = `/api/tiles?${params}`;
    const response = await fetch(apiURL);
    const json = await response.json();
    return json;
  }

  /* set values to second select */
  function setSecondChoices() {
    const secondSelect = document.getElementById('second_select');
    const choices = secondSelect.choices;
    /* clear current values & selected item */
    choices.clearStore();
    const tile_category_id = firstSelect.value;
    getTiles(tile_category_id).then(json => {
      // setChoices(choices, value, label, replaceChoices);
      choices.setChoices(json, 'index', 'display_name', true);
    });
  }
});

要点を抜粋解説します

序盤の以下の部分で素のselect要素に対してchoicesを適用しています

const selects = document.querySelectorAll('select');
selects.forEach(function(select) {
  select.choices = new Choices(select, choicesOptions);
});

choices適用後はカスタムタグが挿入されて見た目が変わり、フィルタが使えるようになります
select boxに対する項目の追加や削除はchoicesメソッドを通す必要があるので注意して下さい


続くaddEventListenerで親select boxのchangeイベントを拾い、setSecondChoices()を呼びます

firstSelect.addEventListener('change', setSecondChoices);

setSecondChoices()内
choices適用時にselect.choices = new Choices...という風に書いたので
以下のようにselect boxのchoicesにアクセス出来ます

const choices = secondSelect.choices;

親カテゴリが変更された時、変更前に選んだ子が残っていると宜しくないのでクリアします

choices.clearStore();

getTiles()は非同期処理で/api/tiles?を叩きます
async/await+fetchAPIを使用します

/* get tiles json by fetch api */
async function getTiles(tile_category_id) {
  const params = new URLSearchParams();
  params.set('tile_category_id', tile_category_id);
  const apiURL = `/api/tiles?${params}`;
  const response = await fetch(apiURL);
  const json = await response.json();
  return json;
}

ES6縛りでasync/await記法が使えない場合は以下をお使い下さい

/* set values to second select */
function setSecondChoices() {
  const secondSelect = document.getElementById('second_select');
  const choices = secondSelect.choices;
  /* clear current values & selected item */
  choices.clearStore();
  const tile_category_id = firstSelect.value;
  const params = new URLSearchParams();
  params.set('tile_category_id', tile_category_id);
  const url = `/api/tiles?${params}`;
  /* get tiles JSON by fetch api */
  fetch(url).then(function(response) {
    return response.json();
  }).then(function(json) {
    // setChoices(choices, value, label, replaceChoices);
    choices.setChoices(json, 'index', 'display_name', true);
  });
}

apiを叩いて返ってくるjsonの構造は以下のような形式です

[
  {"display_name":"","index":41},
  {"display_name":"","index":42},
  {"display_name":"","index":43}
]

setChoices()の第二, 第三引数で
choicesのvalueとlabelに対応するjson中のhashのkeyを指定します(value, labelの順)

// setChoices(choices, value, label, replaceChoices);
choices.setChoices(json, 'index', 'display_name', true);

これで子select boxの選択肢が書き換わります


  • /javascript/packs/application.js
    webpackerにpackしてもらう為、↑のtiles.jsをimportしてるだけ
import '../tiles.js'

routes.rb

  • select boxesを表示: / (root)
  • カテゴリ毎のtile一覧を取得するapi: /api/tiles?tile_category_id=xxx
Rails.application.routes.draw do
  root controller: :top_page, action: :show
  namespace :api do
    resources :tiles, only: [:index]
  end
end

model

Model定義と一覧取得は今回の本筋ではないのでhashでベタ書きしました (DB不要です)
RailsユーザならActiveRecordに置き換えるのは造作も無い事でしょう
indexは各牌識別用の通し番号です
rubyのsymbolって漢字使えたんですね…:scream_cat:


  • /models/concerns/tiles_identifiable.rb
module TilesIdentifiable
  extend ActiveSupport::Concern

  included do
    def get_tiles(tile_category_id:)
      tile_category = tile_categories.key(tile_category_id)
      case tile_category
      when :萬子 then characters
      when :筒子 then dots
      when :索子 then bamboos
      when :風牌 then winds
      when :三元牌 then dragons
      else all_tiles
      end
    end

    def tile_categories
      {
        萬子: 0,
        筒子: 1,
        索子: 2,
        風牌: 3,
        三元牌: 4,
      }
    end

    def numbers
      1..9
    end

    def chinese_numerals
      ['一', '二', '三', '四', '五', '六', '七', '八', '九']
    end

    def to_chinese_numerals(number)
      chinese_numerals[number - 1]
    end

    # tile: { display_name:, index: }
    def characters(base_index: 10 * 0)
      numbers.map { | number | { display_name: "#{to_chinese_numerals(number)}萬", index: base_index + number } }
    end

    def dots(base_index: 10 * 1)
      numbers.map { | number | { display_name: "#{to_chinese_numerals(number)}筒", index: base_index + number } }
    end

    def bamboos(base_index: 10 * 2)
      numbers.map { | number | { display_name: "#{to_chinese_numerals(number)}索", index: base_index + number } }
    end

    def winds(base_index: 10 * 3)
      ['東', '南', '西', '北'].map.with_index(1) {
        | wind, index | { display_name: wind, index: base_index + index } }
    end

    def dragons(base_index: 10 * 4)
      ['白', '發', '中'].map.with_index(1) {
        | dragon, index | { display_name: dragon, index: base_index + index } }
    end

    def all_tiles
      characters | dots | bamboos | winds | dragons
    end
  end
end

controller

  • controllers/top_page_controller.rb
class TopPageController < ApplicationController
  include TilesIdentifiable
  before_action :set_values

  private
    def set_values
      @first_choices = tile_categories.to_a
    end
end

  • controllers/api/tiles_controller.rb
    指定されたカテゴリに対応する牌一覧をjsonで返すapi
class Api::TilesController < ApplicationController
  include TilesIdentifiable

  def index
    tile_category_id = params[:tile_category_id]
    if tile_category_id.blank?
      render json: all_tiles
    else
      render json: get_tiles(tile_category_id: tile_category_id.to_i)
    end
  end
end

view

素のerbです

  • views/top_page/show.html.erb
<div class='form_wrapper'>
  <%= form_with do | form | %>
    <%= form.label 'tile categories' %>
    <%= form.select :first_select, @first_choices, { include_blank: true }, {} %>
    <%= form.label 'tiles' %>
    <%= form.select :second_select, [], { include_blank: true }, {} %>
  <% end %>
</div>

  • views/layouts/application.html.erb
    choicesのjsとcss、whatwg-fetchのjsをCDNでお手軽導入
<head>
  ...
  <!-- Include Choices CSS -->
  <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css'/>
  <!-- Include Choices JavaScript (latest) -->
  <script src='https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js'></script>
  <!-- Include whatwg-fetch -->
  <script src='https://cdn.jsdelivr.net/npm/whatwg-fetch@3.0.0/dist/fetch.umd.min.js'></script>
</head>

最後に

誰かの何らかの助けになれば幸いです
フィルタの日本語動作の確認で漢字を使う麻雀牌(Tile)にしてみたけど
牌一覧用意するのが無駄に面倒でもうやらないと思います:dizzy_face:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

世はまさに Web アプリケーションフレームワーク戦国時代?

Web アプリケーションフレームワークを選ぶ基準って、人それぞれ、色々とあるとは思いますが、やっぱり流行も気になってしまうのが Web エンジニア心というものではないでしょうか?

それに、よく使われているということは、

  • 当然、フレームワークとしての成熟度は高いはずですし、
  • サードパーティのライブラリも充実していて、
  • 比較的、Web サイトや書籍などから得られる情報量も多い

と、考えられます。

初めて Web アプリを作成するという人はもちろん、

ある程度、知識のある方でも、軽い気持ちで新しいフレームワークを学んでみたいというのであれば、流行りに乗っておくのが無難かもしれません。

というわけで、ここでは、

  • Ruby
  • Python
  • PHP
  • JavaScript

で、実装されている「 人気の Web アプリケーションフレームワーク 」について、興味本位でまとめてみましたので、良かったら何かの参考にでもして頂ければ幸いです。

Ruby on Rails vs. Sinatra

Ruby の Web アプリケーションフレームワークと言えば、Ruby on Rails ですね。

GitHub のスター数は、現在、45k となっています。

https://github.com/rails/rails

異論は認めますが、比較するとしたら、Sinatra くらいしかないでしょうか?

GitHub のスター数は、現在、10.9k となっています。

https://github.com/sinatra/sinatra

Google トレンド のグラフを見ると、Ruby の Web フレームワーク自体、徐々に人気が下がってきているようでもありますが、あとで他の言語とも比較してみましょう。

Django vs. Flask vs. Tornado

Google トレンド によると、Python では、やっぱり Django が人気ですが、Flask の人気もかなり上がってきているように見えます。

単純に考えれば、Python 自体の人気が急上昇していることが要因でしょうか。

GitHub のスター数はと言うと、現在、Django が 47.1k で、Flask が 48.8k となっています。

https://github.com/django/django
https://github.com/pallets/flask

比較対象として、もう一つ、Tornado という Web フレームワークを挙げておきましょう。

こちらは、GitHub のスター数が、現在、18.8k となっています。

https://github.com/tornadoweb/tornado

Laravel vs. Symfony vs. CodeIgniter

PHP では、Laravel が人気を博しているようですね。

GitHub のスター数は、現在、57.5k となっています。

https://github.com/laravel/laravel

何となくのイメージですが、昔から、Web アプリ作成のフレームワークと言えば PHP みたいなところもあるので、やはり定番なのでしょうか。

今回、Laravel と比較するのは、SymfonyCodeIgniter です。

GitHub のスター数は、現在、Symfony が 22.7k で、CodeIgniter が 17.9k となっています。

https://github.com/symfony/symfony
https://github.com/bcit-ci/CodeIgniter

Google トレンド によると、他のフレームワークの人気がなくなってきていると言うよりは、純粋に Laravel が強すぎる感じですかね。

Vue.js vs. React vs. AngularJS

JavaScript に関しては、そもそも他の言語と同列に扱っていいのか、ちょっと微妙なところもあるかもしれませんが、

ともかく、Google トレンド のグラフを見ると、React の勢いが凄いです。

GitHub のスター数は、現在、143k となっています。

https://github.com/facebook/react

もちろん、GitHub のスター数が全てではありませんし、条件が同じとも言い難いですが、他の言語の Web フレームワークと、桁が違いますね。笑

しかし、スター数で言えば、実は、Vue.js の方が多く、現在、157k となっています。

https://github.com/vuejs/vue

また、AngularJS はと言うと、GitHub のスター数は、現在、59.6k となっていますが、それでも多いので、全く侮れません。

https://github.com/angular/angular.js

ただ、Google トレンド によると、Vue.js の人気も、上がってはいるのですが、他の二つに比べるとちょっと低すぎるように見えます。

一体、どういうことでしょうか?

ここへきて、この人気調査?の信憑性が危ぶまれる事態とも言えますが、これは「 一つの指標だけに囚われてはいけない 」ということの好例、ということにしておきましょう。

Ruby on Rails vs. Django vs. Laravel vs. React

まぁ、Google トレンド については、ものによって、キーワードの指定や検索の仕方で結果が大きく変わってしまう場合もあるので、自分でも試してみることをオススメします。

ちなみに、今回は、国やカテゴリなどは絞り込まず、同形異義語を排除するために、プログラミング言語名を一緒に検索してみました。

他にも、人気を判断するために、こんな指標があるよ!というような情報がありましたら、是非、教えて頂ければと思います。

話は逸れましたが、最後に、懲りもせず Google トレンド で、

  • Ruby on Rails
  • Django
  • Laravel
  • React

の、各言語で人気らしい Web フレームワークの比較もしてみましょうか。

結果は、こんな感じになりました。
スクリーンショット 2020-02-09 0.00.30.png
URL は、https://trends.google.co.jp/trends/explore?date=2016-01-01%202019-12-31&q=rails%20ruby,django%20python,laravel%20php,react%20javascript です。

このグラフによれば、Rails の人気が下がってきて、Django と Laravel は少しずつ上昇し、React は急上昇したことにより、

世はまさに「 Web アプリケーションフレームワーク戦国時代 」に突入した!と、言えるのかもしれません。笑

特にどの言語に媚びることもない、こんな平和な?結論が、果たしてあり得るのかどうかはともかく、たとえこれが事実だったとしても、ここを分岐点として、これから、どのフレームワークが生き残っていくのか、頭一つ抜けるのか、見ものではありますよね。

しばらくしたら、また調べてみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初めてのWebアプリ制作 - ruby on rail - 4日目

はじめに

せき風邪をひいて4日程寝込んでおり、更新が止まってしまってしまいました・・・(´・ω・`)
新型コロナが流行ってる最中なので、病院ヘはいかず自宅療養してました
皆様も今の風邪にはご注意ください!

4日目

それでは、4日目についてまとめて行こうと思います。

まず最初に、風邪で寝込んでる最中もスマホを使って、
rails関連の記事などを読んでいました。
そんな中で下記記事に行きつき、衝撃を受けます・・・。

Herokuでアップロードした画像が時間経つと消える問題

Herokuで画像アップロード機能を備えたWebアプリをデプロイした場合、
画像の保存自体はできるが、一定時間で消えてしまうという内容の記事でした。

Herokuで画像アップロード機能を利用したWebアプリをデプロイする場合は、
画像をクラウドストレージなどを利用する必要があるそうです。

そのため、画像アップロード機能自体を見直すことになりました。

画像アップロード機能の見直し

まず、Herokuに画像アップロード機能を備えたWebアプリをデプロイする場合、
画像アップロードする方法については、以下の2つの選択肢があるようです。

①クラウドストレージ(AWS S3 / Cloudinary など)にアップロードし、
 データベース上には、アップロード先のURLを保存する方法

②画像データをバイナリ化して、データベースに保存する方法

結論としては、今回はの方法を選択。
クラウドストレージには、AWS S3を利用する事にしました。

理由としては、以下の通り。


  1. 画像バイナリ化については、メリットが少なくデメリットが多い事
    (下記リンクの回答がすごい参考になりました)
     データベースに画像を保存するのはありでしょうか?

  2. クラウドストレージは今後も利用する場面が多くありそうと感じた


使用するGem

クラウドストレージを使うにあたっては、参考記事が多かった下記Gemを利用しようと思います。

・carrierwave
・Fog

まだ、利用方法がよくわかっていないので、今日の残りの時間と、明日の時間を利用して、
実際にアップロード機能の実装を適当なプロジェクトを作成して試してみようと思います。

風邪に続いてメインの実装部分が止まってしまいますが、何とか1日使って覚えようと思います!

5日目の目標

5日目の目標については、以下の通り!
-テストアプリを作成し、carrierwave + fog を利用したクラウドストレージへの画像アップロード機能の実装

4日目の目標だった以下については、まんま6日目の目標へ

-ログイン機能の実装
-セッションコントローラの作成
-イメージポストモデルの作成
-画像投稿機能の実装

正直、どんどん先延ばしになっていってる感じが否めないですが、
とにかく完成に向かって頑張ろうと思います・・・!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む