20190524のRailsに関する記事は27件です。

HomesteadでRuby on Railsの環境構築

Homesteadはlaravelの開発環境を簡単に構築できるパッケージだが、Ruby on Railsで必要なソフトも入っているため、Homesteadを使用したRuby on Railsの環境を構築していく。

今回はMacで、使用するDBはMySQLとする。

Homesteadとは?

Laravel公式のboxで、Vgrantを使用することで仮想マシンにLaravelの開発環境を簡単に構築することができるパッケージです。
PHPやWebサーバ、その他のサーバソフトウェアをローカルマシンにインストールする必要なく、開発環境を構築できます。

導入されているソフトウェア

公式より以下のソフトが既に含まれています。(2019/5 現在)

  • Ubuntu 18.04
  • Git
  • PHP 7.3
  • PHP 7.2
  • PHP 7.1
  • Nginx
  • MySQL
  • lmmによるMySQLとMariaDBデータベーススナップショット
  • Sqlite3
  • PostgreSQL
  • Composer
  • Node (Yarn、Bower、Bower、Grunt、Gulpを含む)
  • Redis
  • Memcached
  • Beanstalkd
  • Mailhog
  • avahi
  • ngrok
  • Xdebug
  • XHProf / Tideways / XHGui
  • wp-cli
  • Minio

オプションで導入できるソフトウェア

  • Apache
  • Crystal & Lucky Framework
  • Dot Net Core
  • Elasticsearch
  • Go
  • MariaDB
  • MongoDB
  • Neo4j
  • Oh My Zsh
  • Ruby & Rails
  • Webdriver & Laravel Dusk Utilities
  • Zend Z-Ray

VirtualBoxとVagrantのインストール

VirtualBoxをインストール

仮想化ソフトは他にもいくつかあるそうですが「VirtualBox」を使用

インストールはこちらから >> https://www.virtualbox.org/

自分の使用しているOSを選択

スクリーンショット 2019-05-22 22.13.38.png

【注意】
「機能拡張がブロックされました」と表示されたら「システム環境設定」→「セキュリティとプライバシー」から読み込みを許可しましょう

Vagrantをインストール

Vagrantとは仮想化ソフト(VirtualBox)を使用し、コマンドを入力することで仮想環境を管理したり構築したりすることができるツールです。

インストールはこちらから >> https://www.vagrantup.com/

自分の使用しているOSを選択

スクリーンショット 2019-05-22 22.19.47.png

ダウンロードしたらバージョンを確認してみる。

$ vagrant --version
 Vagrant 2.2.4

これで2.2.4のVagrantがインストールされました。

Homesteadのboxをインストール

boxとはOSのディスクイメージファイルで、これを追加することで仮想環境を作成できる。

https://app.vagrantup.com/laravel/boxes/homestead

上記のリンクの公式を確認すると「laravel/homestead」があるのでこれを取得する。

VagrantCloudにあるboxを追加する場合は名前だけでも良い。

$ vagrant box add laravel/homestead

実行するとproviderは何かと聞かれるので「virtualbox」の「3」を選択する。
結構時間がかかるので終わるまで待つ。

$ vagrant box add laravel/homestead
==> box: Loading metadata for box 'laravel/homestead'
    box: URL: https://vagrantcloud.com/laravel/homestead
This box can work with multiple providers! The providers that it
can work with are listed below. Please review the list and choose
the provider you will be working with.

1) hyperv
2) parallels
3) virtualbox
4) vmware_desktop

Enter your choice: 3
==> box: Adding box 'laravel/homestead' (v7.2.1) for provider: virtualbox
    box: Downloading: https://vagrantcloud.com/laravel/boxes/homestead/versions/7.2.1/providers/virtualbox.box
    box: Download redirected to host: vagrantcloud-files-production.s3.amazonaws.com
==> box: Successfully added box 'laravel/homestead' (v7.2.1) for 'virtualbox'!

Successfulluyとなったので、以下のコマンドでboxがちゃんと追加されたか確認する。

$ vagrant box list
laravel/homestead (virtualbox, 7.2.1)

Homesteadのファイルをダウンロード

ダウンロードしたファイルは任意のディレクトで良いが、今回はホームディレクトリの直下にダウンロードするので移動しておく。

$ cd
$ pwd
/Users/ユーザー名

以下のコマンドでgithubからリポジトリをクローンします。

$ git clone https://github.com/laravel/homestead.git Homestead
Cloning into 'Homestead'...
remote: Enumerating objects: 59, done.
remote: Counting objects: 100% (59/59), done.
remote: Compressing objects: 100% (46/46), done.
remote: Total 3606 (delta 37), reused 22 (delta 13), pack-reused 3547
Receiving objects: 100% (3606/3606), 782.03 KiB | 863.00 KiB/s, done.
Resolving deltas: 100% (2190/2190), done.

Homesteadの初期化

HomesteadはHomestead.yamlというファイルに色々書くことで、任意の設定をすることができる。
Homestead.yamlは初期化することで生成される。

ダウンロードしたHomesteadディレクトリに移動する。
中に色々なファイルが入っているが、初期化をしていないのでHomestead.yamlが生成されていない。

$ cd Homestead/
$ ls
CHANGELOG.md        Vagrantfile     composer.lock       phpunit.xml.dist    scripts
Homestead.yaml.example  bin         init.bat        readme.md       src
LICENSE.txt     composer.json       init.sh         resources       tests

以下のコマンドで初期化する。

$ bash init.sh
Homestead initialized!
$ ls
CHANGELOG.md        LICENSE.txt     aliases         composer.lock       phpunit.xml.dist    scripts
Homestead.yaml      Vagrantfile     bin         init.bat        readme.md       src
Homestead.yaml.example  after.sh        composer.json       init.sh         resources       tests

Homestead initialized!と表示されたので、中を確認するとHomestead.yamlが生成されている。

SSH鍵のファイルの作成

ホストOSとゲストOSの通信はSSHで行うので、それに必要な鍵を作成する。

まずはホームディレクトリに移動して既に鍵があるかを確認。

$ cd
$ ls -la | grep .ssh

id_rsaid_rsa.pubが表示されれば、既にSSH鍵ファイルはあります。

無い場合は以下のコマンドで作成する。

$ ssh-keygen -t rsa

途中でEnter file in which to save the key (/Users/ユーザー名/.ssh/id_rsa):と保存するディレクトリを聞かれるので、そのままEnterを押す。

次にEnter passphraseとパスフレーズを求められるので任意のパスフレーズを設定する。
Enter same passphrase again:と再度確認を求められるので設定したパスフレーズを入力する。

$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/ユーザー名/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/ユーザー名/.ssh/id_rsa.
Your public key has been saved in /Users/ユーザー名/.ssh/id_rsa.pub.
The key fingerprint is:
...(以下省略)...

ファイルキーが作成されたので、先ほどのコマンドで確認する。

$ ls -la | grep .ssh

Homesteadの設定

Homestead(仮想マシン)の各設定は、初期化した時に生成したhomestead.yamlに記述します。

Homesteadのディレクトリに移動し、vimでHomestead.yamlを編集。

$ cd Homestead/
$ vim Homestead.yaml

下記がHomestead.yamlのデフォルトのです。

Homestead.yaml
---
ip: "192.168.10.10"     # IPアドレス
memory: 2048            # 割り当てるメモリ
cpus: 2                 # 割り当てるCPU
provider: virtualbox    # 使用するプロバイダ

# SSH公開鍵のパス
authorize: ~/.ssh/id_rsa.pub

# SSH秘密鍵のパス
keys:
    - ~/.ssh/id_rsa

# 共有ディレクトリの設定
# map: ホストマシン側で共有したいディレクトリ
# to: ゲストマシン側で共有したいディレクトリ
folders:
    - map: ~/code
      to: /home/vagrant/cod


# Nginxサイトの設定
# map: アクセスするドメイン名の設定
# to: ゲストマシンNginxのドキュメントルートの設定
sites:
    - map: homestead.test
      to: /home/vagrant/code/public

# 使用するDB名
databases:
    - homestead

# ports:
#     - send: 50000
#       to: 5000
#     - send: 7777
#       to: 777
#       protocol: udp

# blackfire:
#     - id: foo
#       token: bar
#       client-id: foo
#       client-token: bar

rails serverは3000なので、コメントアウトを外して新しく記述する。

ports:
    - send: 3000
      to: 3000

それ以外はデフォルトのままで使用しますが、共有ディレクトリの設定や、ドキュメントルートの設定などの変更が可能です。

デフォルトのfoldersだとホストマシンのホームディレクトリ直下にcodeディレクトリが指定されていますが、現在codeディレクトリがないので作成しておきます。

今後プロジェクトフォルダを作成したときは、このcodeディレクトリのなかに入ることになります。

$ mkdir ~/code

【注意】
sitesプロパティをHomestead boxのプロビジョニング後に変更した場合、仮想マシンのNginx設定を更新するため、vagrant reload --provisionを再実行する必要があります。

ホスト名の設定

IPアドレス192.168.10.10homestead.testというドメイン名で設定してあり、hostsファイルに追記することで、Webブラウザでhttp://homestead.testにアクセスすることができる。

MacとLinuxでは/etc/hostsにファイルがあるので追加していく。
アクセス権がないと保存できないので、sudoをつけて192.168.10.10 homestead.appを追記する。

$ sudo vim /etc/hosts

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1       localhost
255.255.255.255 broadcasthost
::1             localhost

192.168.10.10 homestead.test

Vagrant Boxの実行

各種設定が終わったら仮想マシンを起動します。

Homesteadのディレクトリ内にVagrantfileがあるので、違うディレクトリにいる場合は移動します。

$ pwd
/Users/ユーザー名/Homestead

下記のコマンドで仮想マシンを起動する。

$ vagrant up
Bringing machine 'homestead-7' up with 'virtualbox' provider...
==> homestead-7: Importing base box 'laravel/homestead'...
==> homestead-7: Matching MAC address for NAT networking...
==> homestead-7: Checking if box 'laravel/homestead' version '7.2.1' is up to date...
==> homestead-7: Setting the name of the VM: homestead-7
==> homestead-7: Clearing any previously set network interfaces...
...(以下省略)...

以下のコマンドで起動できているかを確認。

$ vagrant status
Current machine states:

homestead-7               running (virtualbox)

The VM is running. To stop this VM, you can run `vagrant halt` to
shut it down forcefully, or you can run `vagrant suspend` to simply
suspend the virtual machine. In either case, to restart it again,
simply run `vagrant up`.

running (virtualbox)となっているので起動できていることが確認できます。

あとはsshコマンドで仮想マシンに入ります。

$ vagrant ssh
Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 4.15.0-47-generic x86_64)

Thanks for using
 _                               _                 _
| |                             | |               | |
| |__   ___  _ __ ___   ___  ___| |_ ___  __ _  __| |
| '_ \ / _ \| '_ ` _ \ / _ \/ __| __/ _ \/ _` |/ _` |
| | | | (_) | | | | | |  __/\__ \ ||  __/ (_| | (_| |
|_| |_|\___/|_| |_| |_|\___||___/\__\___|\__,_|\__,_|

* Homestead 8.4.0 released!
* Settler v7.2.1 released! Make sure you update

0 packages can be updated.
0 updates are security updates.


Last login: Thu May 24 08:54:14 2019 from 10.0.2.2
vagrant@homestead:~$ 

無事に仮想マシンに入れたので、あとはRailsの環境を構築していきます。

Ruby on Railsの環境を構築

Homesteadが立ち上がったのであとは必要なソフトなどを入れていくが、既にあるのがほとんどなのでrbenv等を入れていく。

必要なライブラリをインストール

Ubuntu自体をアップデート(最新化)します。

$ sudo apt update 

今回はMysqlを使用するので、必要な関連パッケージをインストール

$ sudo apt install mysql-server mysql-client
$ sudo apt install libmysqlclient-dev

Rubyインストール

rbenvをインストール

rbenvとは、Rubyの「インストール」と「バージョン切替」を簡単におこなえるようにするためのツールです。

Gitを使ってrbenvをインストールします

$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
Cloning into '/home/vagrant/.rbenv'...
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 2759 (delta 4), reused 8 (delta 4), pack-reused 2744
Receiving objects: 100% (2759/2759), 528.92 KiB | 874.00 KiB/s, done.
Resolving deltas: 100% (1724/1724), done.

インストールできたらrbenvコマンドが使えるように設定する

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ exec $SHELL -l

rbenvコマンドが使えるようになったか確認する

$ rbenv --version
rbenv 1.1.2-2-g4e92322

Rubyをビルドするために使うプラグインの「ruby-build」をインストール

$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
Cloning into '/home/vagrant/.rbenv/plugins/ruby-build'...
remote: Enumerating objects: 38, done.
remote: Counting objects: 100% (38/38), done.
remote: Compressing objects: 100% (25/25), done.
remote: Total 9750 (delta 14), reused 29 (delta 8), pack-reused 9712
Receiving objects: 100% (9750/9750), 2.08 MiB | 2.10 MiB/s, done.
Resolving deltas: 100% (6340/6340), done.

インストールできるRubyのバージョンを確認する

$ rbenv install --list
Available versions:
  1.8.5-p52
  1.8.5-p113
  1.8.5-p114
  1.8.5-p115
  1.8.5-p231
  1.8.6
  1.8.6-p36
  1.8.6-p110
  1.8.6-p111
  1.8.6-p114

...(途中省略)...

  2.5.4
  2.5.5
  2.6.0-dev
  2.6.0-preview1
  2.6.0-preview2
  2.6.0-preview3
  2.6.0-rc1
  2.6.0-rc2
  2.6.0
  2.6.1
  2.6.2
  2.6.3
  2.7.0-dev
  jruby-1.5.6
  jruby-1.6.3

...(以下省略)...

バージョンを指定してRubyをインストール。
終わったら仮想マシンにインストールされているRubyのバージョンを確認します。

$ rbenv install -v 2.6.3
/tmp/ruby-build.20190524115043.18071 ~
Downloading ruby-2.6.3.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.3.tar.bz2
Installing ruby-2.6.3...
/tmp/ruby-build.20190524115043.18071/ruby-2.6.3 /tmp/ruby-build.20190524115043.18071 ~
checking for ruby... /home/vagrant/.rbenv/shims/ruby
config.guess already exists
config.sub already exists
checking build system type... x86_64-pc-linux-gnu

...(以下省略)...

$ rbenv versions
* system (set by /home/vagrant/.rbenv/version)
  2.6.3

インストールされているのを確認できたら、仮想マシン全体で有効にするRubyのバージョンを指定する。

ディレクトリごとに指定する場合はglobalではなく、有効にしたいディレクトリでlocalを指定する。

$ rbenv global 2.6.3
$ rbenv versions
  system
* 2.6.3 (set by /home/vagrant/.rbenv/version)

アスタリスクが移動しました。
ちゃんと最新バージョンがインストールされたかを確認

$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]

bundlerのインストール

bundlerはプロジェクトごとで使うgemを、インストールしたり使用したりする。
どのバージョンが必要なのかは、プロジェクトフォルダにあるGemfileを読み取りることで実行される。

bundlerはRubyGemsからインストールできるのでgemコマンドでインストールする。
bundleコマンドが使えるようになるので確認する。

$ gem install bundler
Fetching bundler-2.0.1.gem
Successfully installed bundler-2.0.1
Parsing documentation for bundler-2.0.1
Installing ri documentation for bundler-2.0.1
Done installing documentation for bundler after 2 seconds

$ bundle -v
Bundler version 2.0.1

Ruby on Railsのインストール

Ruby on Railsは、gemコマンドでインストールする。
最新ではないバージョンを指定したい場合は-vオプションをつけて指定。

インストールが終わったらrailsコマンドが使用できるので確認する。

$ gem install rails
Fetching i18n-1.6.0.gem
Fetching rack-2.0.7.gem

...(途中省略)...

Removing ruby (1:2.5.1) ...
Removing ruby2.5 (2.5.1-1ubuntu1.2) ...
Removing libruby2.5:amd64 (2.5.1-1ubuntu1.2) ...
Removing rake (12.3.1-1) ...
Removing ruby-test-unit (3.2.5-1) ...
Processing triggers for libc-bin (2.27-3ubuntu1) ...
Processing triggers for man-db (2.8.3-2ubuntu0.1) ...

$ rails -v
Rails 5.2.3

Railsアプリを作成

Homestead.yamlで共有ディレクトリの設定がすでにできているので、codeディレクトリ内にファイルを作る。

$ cd
$ cd code/

今回は例としてtest_appというプロジェクトファイルを作成します。

プロジェクトファイルはrails newというコマンドで作成でき、-dオプションで使用するデータベースを指定できます。

オプションを指定しない場合は「SQLite」というデータベースがデフォルトで使用されますが、今回は「Mysql」を使用するので以下のコマンドでファイルを作成します。

$ rails new test_app -d 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 /home/vagrant/code/test_app/.git/
      create  package.json
      create  app

...(途中省略)...

Using spring-watcher-listen 2.0.1
Using turbolinks-source 5.2.0
Using turbolinks 5.2.0
Using uglifier 4.1.20
Using web-console 3.7.0
Bundle complete! 18 Gemfile dependencies, 78 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
         run  bundle exec spring binstub --all
* bin/rake: spring inserted
* bin/rails: spring inserted

$ ls
test_app

確認するとtest_appというファイルが作成されています。

Railsサーバーの起動

Railsサーバーはプロジェクトフォルダに移動してrails sコマンドで起動できます。

しかしVagrant環境で仮想マシンを起動している場合は、rails s -b 0.0.0.0というコマンドでRailsサーバーを起動させます。

localホスト以外からはアクセスできないため、-b 0.0.0.0オプションをつけて全てのIPアドレスを許可します。

$ cd test_app/
$ rails s -b 0.0.0.0
=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

このように表示されれば無事にサーバーが起動したことになります。
サーバーを停止したいときはCtrl + cで停止することできます。

ブラウザでアクセス

ドメイン名の設定もデフォルトでしてあるので、ブラウザにはhomestead.test:3000でアクセスできます。

Mysqlのパスワードを設定

ブラウザにアクセスすると以下のようなエラーが出ました。
スクリーンショット 2019-05-24 22.06.15.png

これはRailsアプリがMysqlにhomesteadユーザーでログインしようとした結果、「パスワードがない」と言われていることが原因です。

  • データベース : homestead
  • ユーザー名 : homestead
  • パスワード : secret

Homesteadでは上記のようなデフォルトの設定があるので、データベースの設定ファイルにパスワードを記述します。

【注意】
サーバーを起動しているタブではコマンド操作ができないので、新たにタブを増やし再度sshで仮想マシンに接続します。

データベースの設定ファイルはconfigディレクトリの中にdatabase.ymlという名前で入っています。

$ ls config/
application.rb  credentials.yml.enc  environments  master.key  spring.rb
boot.rb     database.yml         initializers  puma.rb     storage.yml
cable.yml   environment.rb       locales       routes.rb

$ vim config/database.yml

vimで開きpasswordの所にsecretを追記して保存します。

database.yml
# MySQL. Versions 5.1.10 and up are supported.
#
# Install the MySQL driver
#   gem install mysql2
#
# Ensure the MySQL gem is defined in your Gemfile
#   gem 'mysql2'
#
# And be sure to use new-style password hashing:
#   https://dev.mysql.com/doc/refman/5.7/en/password-hashing.html
#
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: secret
  socket: /var/run/mysqld/mysqld.sock

development:
  <<: *default
  database: test_app_development

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: test_app_test

保存したらサーバーを一度停止し、再起動してからブラウザにアクセスしてみます。

データベース作成

再びアクセスしてみると先ほどのエラーはなくなりましたが、違うエラーが出てきました。
スクリーンショット 2019-05-24 22.22.00.png

これはUnknown database 'test_app_development'とでていて、test_app_developmentというデータベースが無いよと注意されています。

Railsにはrails db:createというコマンド存在していて、database.ymlに記述されている情報を元に、データベースを作成してくれます。

これらはデフォルトで

  • プロジェクトフォルダ名_development
  • プロジェクトフォルダ名_test

という2つのデータベース名が記述されているので特に変更はせずに、先ほどのrails db:createコマンドを実行して上記の2つのデータベースを作成します。

$ rails db:create
Created database 'test_app_development'
Created database 'test_app_test'

データベースが作成されたので再度ブラウザにアクセスしてみます。

スクリーンショット 2019-05-24 22.42.57.png

上記のように表示されれば成功です。これで問題なくアクセスができました。

あとはアプリケーション制作

これでHomesteadを使用したRuby on Railsの環境を構築できました。
あとは作成したフォルダでアプリケーションを作ってみてください。

参考記事

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

Railsで基本情報技術者試験の過去問題サイトを作る(3:親子関係、登録編)

はじめに

ゆる〜く学ぶ。みんなのWeb勉強コミュニティー。 「にゅ〜ぶる会」を運用中です。
https://newburu.github.io/

そこで、何か教育用のコンテンツが欲しいなぁ〜と思い立ち、今回の企画をスタートしました!

Railsで基本情報技術者試験の過去問題サイトを作ります!

最終目標

  • 問題・回答の登録は、Scaffoldで簡易でOK
  • APIを用意して、ランダムに問題を抽出する機能を追加する
  • TwitterBOT、LINEBOT、SlackBOTが出来たら良いな

履歴

1:構築編
  https://qiita.com/newburu/items/ed59f47ac645b19620f6
2:日本語化(i18n)編
  https://qiita.com/newburu/items/4f12fdb61bf6cd601545/
3:親子関係、登録編
  本ページ

今回やる事

  • 親子関係を設定し、登録しやすくする

※レイアウトをやろうと思いましたが、こちらの方が優先なので、予定を変更させて頂きました。

親子関係を設定し、登録しやすくする

1. gem 'cocoon', gem 'jquery-rails'を追加します。

Rails5.1からjQueryがなくなったため、jquery-railsも追加する必要があります。

./Gemfile
gem 'cocoon'
gem 'jquery-rails'
bundle-install
$ bundle install

2. Modelに、親子関係を設定します。

・親(question)には、「has_many :answers」を追加します。
・親が削除された時に、一緒に子も削除されるように、「dependent: :destroy」をつけます。
・accepts_nested_attributes_forを使うと、親と子を一緒にcreate/update出来るようにします。
・削除できるよう「allow_destroy: true」をつけます。

app/models/question.rb
  has_many :answers, dependent: :destroy
  accepts_nested_attributes_for :answers, allow_destroy: true

子(models/answer.rb)は、作成時にreferencesとしているため、自動で設定されています。

app/models/answers.rb
  belongs_to :question

3. Viewに、親子同時に登録・更新出来るように設定します。

application.jsに以下を追加します。

app/javascripts/application.js
//= require jquery
//= require cocoon

viewを変更します。

app/views/questions/_form.html.slim
  .answers
    = f.fields_for :answers do |answer|
      = render 'answer_fields', f: answer
    = link_to_add_association "追加", f, :answers

子供の情報登録用フォームになるviewファイルを新規作成します。

app/views/questions/_answer_fields.html.slim
.nested-fields
  .field
    = f.label :msg
    = f.text_area :msg
  .field
    = f.label :correct
    = f.check_box :correct

  = link_to_remove_association "削除", f

4. コントローラーで登録可能にします。

以下のように子のパラメータを受け取れるようにしましょう。

app/controllers/questions_controller.rb
    def question_params
      params.require(:question).permit(:category1, :category2, :category3, :msg, answers_attributes: [:id, :msg, :correct, :_destroy])
    end

5. 確認します。

新規作成画面はこんな感じになります。
新規作成画面

「追加」ボタンを押すたびに、子供の入力エリアが追加されます。
追加

今回はここまで

ありがとうございました!
次回は、参照画面などに、子供の情報を追加していこう。

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

Ajaxによる非同期通信で、データベースに二重に投稿される問題

問題

form_forでデータを受け取り、Ajaxによる非同期通信でHTMLを差し替えて投稿する際、
投稿内容がデータベースに二重に投稿される問題が起きる。

原因

form_forにて投稿する際、controllerのcreateアクションとformのPOST、両方がデータベースに投稿してしまう。

※このへん理解浅いので、理解が間違っていたらご指摘いただけると助かります。

解決法

jsファイルの非同期通信の関数の最後に return false; を記述しておく。
formのPOSTがキャンセルされるため、二重投稿されなくなる。

補足

かなりピンポイントな問題と対策な気がしますが、検索しても解決方法が出てこなかったので記載しておきます。
なんとなくの理解ですので、言い方や理解の仕方が間違ってる可能性あります。

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

Heroku 本番環境 [Mysql2::Error::ConnectionError: Access denied for user root@localhost (using password: NO)] エラー

注)本ページに出てくる、usernameや、password、host値は存在しない値です。

問題:

rails c production で User.createをしようとしたら、
Mysql2::Error::ConnectionError: Access denied for user 'b2a63e570bc849'@'52.204.50.03' (using password: NO)
と言われてしまいました。

ターミナル
$ heroku config #mysql周辺情報をチェックします。
# 注意 'mysql2://[ユーザ名]:[パスワード]@[ホスト名]/[スキーマ名]?reconnect=true' です。

CLEARDB_DATABASE_URL:     mysql://kefbwfffn349:31w1dd54@us-cdbr-iron-east-04.cleardb.net/heroku_sufbd6sjdhsb5d?reconnect=true
DATABASE_PASSWORD:        31w1dd54
DATABASE_URL:             mysql2://kefbwfffn349:31w1dd54@us-cdbr-iron-east-04.cleardb.net/heroku_sufbd6sjdhsb5d?reconnect=true

$ mysql -u kefbwfffn349 -h us-cdbr-iron-east-04.cleardb.net -p  #mysqlに入れるか?
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 
...
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> exit # 入れました。

host,username,passwordはあってるのに、、、
となると,

検証

原因はエラー文の中のusing password: NOかな???と思い、

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  username: root
  password: 
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

production:
  <<: *default
  database: heroku_sufbd6sjdhsb5d
  username: kefbwfffn349
  host: us-cdbr-iron-east-04.cleardb.net
  password: <%= ENV['DATABASE_PASSWORD'] %>
  socket: /var/lib/mysql/mysql.sock

password: <%= ENV['DATABASE_PASSWORD'] %>の部分を直接パスワードに置き換えることにしました。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  username: root
  password: 
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

production:
  <<: *default
  database: heroku_sufbd6sjdhsb5d
  username: kefbwfffn349
  host: us-cdbr-iron-east-04.cleardb.net
  password: 31w1dd54
  socket: /var/lib/mysql/mysql.sock

そしたら、mysqlに繋がった、、、とりあえずは助かった、、、
本来なら、password: <%= ENV['DATABASE_PASSWORD'] %>でheroku:configのDATABASE_PASSWORD(キー)が読み込まれて、31w1dd54(値)が取得されるはずなのに、、、
色々調べてみます。

注)本ページに出てくる、usernameや、password、host値は存在しない値です。

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

bundle installで /vender/bundle にgemインストールされるのを解除する

問題

bundle installすると、/vender/bundle にgemインストールされてしまう。
デフォルトの場所にインストールされるように戻したい。

原因

bundle installをいつの間にかパス指定して実行してしまっていた。

ターミナル
$ bundle install --path vendor/bundle

一度パス指定して実行すると、以降はパス指定しなくても、
そのパスがデフォルトのインストール先になってしまう?

パス変更方法

ホームディレクトリの
/.bundle/config のファイルにパスが書いてあるので、
それを変更 or 削除する。
削除すると何もパス指定されてない状態に戻る。

/.bundle/config
BUNDLE_PATH: "vendor/bundle"

.bundle は隠しファイルなので、
shift + command + .(ドット)
のコマンドで隠しファイルの表示/非表示を切り替えられる。

参考

以下の状況で問題が発生:
最初はデフォルト状態でbundle installしていたプロジェクトで、
途中からbundle installするといつの間にか/vender/bundleにインストールされるようになってしまっていた。
GithubDesktopでコミットしようとすると以下エラーが出てコミットできない状態になった。
パス指定を削除することにより解決。
最初からパス指定してプロジェクトを立ち上げている場合は問題ないと思われる。

GithubDesktop
commit failed - exit code 1 received github
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rubocop について

rubocop とは

コーディング規約に準拠してるかチェックするgemです!
簡単に言いますと、インデントやメソッド名、改行などのチェックをしてくれるものです。
今回は、このrubocopさんについてを会社のプロダクトにいれた時(後入れ)のやり方にも交えて書きます。

流れ

  • $gem install rubocop or Gemfile にかいて $bundle install
  • $rubocop で実行(たぶんめちゃめちゃ怒られます。。。)
  • 怒られた箇所修正!!!

.rubocop.yml を作成!

プロジェクト直下に「.rubocop.yml」を作成!!

rubocop.yml
Rails:
  Enabled: true

AllCops:
  Exclude:
    - 'db/schema.rb'
    - 'vendor/**/*'

# 日本語でのコメントを許可
AsciiComments:
  Enabled: false

このようなrubocop に対して制御するファイルを作成することで、ひとつひとつを修正していける!
(実際はその人や会社によってもっと設定がいることもあります。)

と簡単そうですが、後入れの場合、まぁーめちゃくちゃ怒られます!

そこで、私が実際にやったやり方は、

$bundle exec rubocop -R --auto-gen-config --exclude-limit 999999

オプションつけてrubocop コマンドを叩くと、「.rubocop_todo.yml」が作られます!!
このファイルは、現コードに対する一時回避の設定をしてくれます!これによって、怒られなくなります。
つまり、このファイルがなくてもエラーが起きないことがゴールです〜

このファイルをもとに「.rubocop.yml」を設定していきます!


実際の流れ

0.はじめに

rubocop_todo.yml
inherit_from: .rubocop.yml

これを記述して、両方のファイルが参照されるようにしておく!
or
.rucocop_todo.ymlの内容を.rubocop.ymlに全コピー

  1. copを一個コメントアウト(削除)

  2. $rubocop何で怒られているかチェック
    -> 直したくない(これからもこのcop に対しては無視したい)->3A
    -> 直さないといけない->3B

3A.
例: AsciiCommentsに関しては、全ページで許可したい!

rubocop.yml
# cop でどのようなことが怒られたかとか、どうしたいかを明記しておいたほうが良さそう
AsciiComments:
  Enabled: false

を追記
もしくは、

rubocop.yml
AsciiComments:
  Exclude:
    - 'app/models/hoge.rb'

3B.
$rubocop -a自動で修正してくれる(これがめちゃ便利)単純にコードミスや可読性の高いコードにも直してくれる!
注:メソッドの名前などは直してくれない!!

ちなみに、私は$rubocop回した段階で何で怒られているのかがわからないやつは、とりあえず$rubocop -aを叩きました。
それで、diff でみてどう変更されたかを確認してました!!

流れに関してはこんなもんです!ひたすら、怒られているところの確認-> 修正 or 「cop」 無視

.rubocop.ymlの書き方について

rubocopの対象から除外するファイル指定

rubocop.yml
AllCops:
  Exclude:
    - db/schema.rb
    - 'vendor/**/*'

copの無効化・有効化

rubocop.yml
Rails:
  Enabled: true
rubocop.yml
Lambda:
  Enabled: false

基本的には、cop は有効化されているのでEnabled: trueはそこまで使わないかも
Enabled: falseはcop 自体を無視したいときに使います!

warning のみを検知

rubocop.yml
Bundler/OrderedGems:
  Severity: warning

warning レベルから取得

自動化

rubocop を最大活用するためには、自動化するしかないと思いまして、CircleCIと連携することにしました!
CircleCIに関しては、ここでは飛ばします笑

circleci/config.yml
jobs:
  build:
    steps:
      - run: bundle exec rubocop

と設定するだけです!
自動で回してくれます!
github と CircleCI の連携と CircleCI ファイルの中にrubocop のコマンドを設定することでgithub 上で結果を確認できます!

まとめ

そもそも導入した経緯-> レビューする人が忙しい!!細かい修正で何度もやり取りするのが無駄!

  • チームみんなのコードで既存のcop の設定とは違う物が来たときなどのメンテナンスは、必要かなと。
  • 私自身、プロダクトに導入したばかりで結果がまだわかっていないので、便利なのか、あまり良くないかはまだわかっていません(今更ですが)

非常に良かった点

  • まだ、コードを書き始めて長くない私にとって、$rubocop -aによって修正されたり、怒られたりすることでコードの違った書き方を勉強するいい機会になった!(実際これが一番でかい)

参考

https://qiita.com/kyohei_shimada/items/e739dec967eb5e61721c
https://blog-ja.sideci.com/entry/2015/03/12/160441

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

よく見かけるattr_accessorについて

はじめてのqiita投稿ですが、がんばりたいと思います。

1.ゲッターとセッターについて

railsを学び始めて早4ヶ月。
ただrailsのコードを書くだけではなく、rubyの基礎を学ぼうと思いました。
そこで気になったのがattr_accessor、これはよく見かけるがなんだろうと。

さて本題。

ゲッターくんとセッターくんの役割を実際のコードで見てみようと思います。

class Book
  def book=(book)
    @book = book
  end

  def book
    @book 
  end
end

1つ目のメソッドは引数で受け取ったデータをインスタンス変数に代入します。
このインスタンス変数を代入するためのメソッドのことを「セッター」と呼びます。

2つ目のメソッドではbookメソッドの中身を変更して、設定した名前を返しています。
このインスタンス変数の内容を参照するためのメソッドを「ゲッター」と呼びます。

2.attr_accessorについて

先程のゲッターとセッターを毎回毎回設定するのは大変ですよね
しかしrubyには便利なメソッドがあります。
それがattr_accessorという訳です。

class Book
  attr_accessor :book
end

というようにすっきり書けたのでは無いでしょうか。
attr_accessorメソッドを用いると、シンボルで :book と書けるようになりました。

3.さいごに

ちなみに、ゲッターのみの場合はattr_reader,セッターのみの場合はattr_writerと書けます。

という風に、普段何気なく使っているメソッドでもこれだけの仕事をしていたとは驚きですね。

これからも気になったら、どんどんqiitaでアウトプットしようと思います。

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

Railsコマンド

はじめに

備忘録としてまとめていきます。
随時追記していきます。

Railsプロジェクトを作成
$ rails new [プロジェクト名] -d [データベース名]
データベース作成
$ rails db:create
サーバー起動
$ rails server
コントローラー作成
$ rails generate controller [コントローラー名(複数形)][アクション名・アクション名..][オプション名]
モデル作成
$ rails generate model [モデル名(単数形)][属性名:データ型][属性名:データ型]
マイグレーション
$ rails db:migrate
テスト検証
$ rails test
integration作成
$ rails g integration_test [テストファイル名]
統合テスト検証
$ rails test integration
indexを追加
$ rails g migration add_index_to_[テーブル名]_[カラム名]
$ rails db:migrate             #最後にDBをマイグレートする。 
カラム追加
$ rails g migration add_[カラム名]_to_[テーブル名] [カラム名][データ型]

DBリセット

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

RailsとLivedoor Weather Web Serviceを使って各地点の天気を表示させてみた

はじめに

前回の記事で、Rubyから天気情報を取得することができました。
RubyでWeather Hacksを使って天気を取得してみた
この取得処理とRailsを使って、Web上に天気情報を表示します。

環境

macOS Mojave(10.14.4)
Ruby 2.6.3
Rails 5.2.3

取得処理

以下の順で各地点の天気情報を取得します。

  1. 表示させたい地点名を読み込む
  2. Livedoor Weather Web Serviceで提供されている都市と1を使って、地点のidを取り出す
  3. 各地点の天気情報を取得する

表示させたい地点名を読み込む

テレビの全国天気で、よく表示される各地点のjsonファイルを作成します。

main_city.json
{
  "city":[
    {
      "name": "釧路"
    },
    {
      "name": "旭川"
    },
・・・略・・・
    {
      "name": "那覇"
    },
    {
      "name": "石垣島"
    }

これを読み込む処理を作成します。

  def read
    file_path = File.expand_path('config/main_city.json', __dir__)
    @city_list = []
    File.open(file_path, 'r') do |text|
      @parse_text = JSON.parse(text.read)
      @parse_text['city'].each { |city|
        @city_list.push(city['name'])
      }
    end
  end

Livedoor Weather Web Serviceで提供されている都市と1を使って、各地点のidを取り出す

提供されている地点のIDをjsonファイルで書いておきます。

location_id.json
{
    "area":
    [
        {
            "name": "北海道",
            "prefs":[
                {
                    "name": "北海道",
                    "city":[
                        {
                            "name": "稚内",
                            "id": "011000"
                        },
                        {
                            "name": "旭川",
                            "id": "012010"
                        },
・・・略・・・
                        {
                            "name": "石垣島",
                            "id": "474010"
                        },
                        {
                            "name": "与那国島",
                            "id": "474020"
                        }
                    ]
                }
            ]
        }
    ]
}

このjsonファイルを読みこみ、各地点のidを取得します。
単純にforループで回すだけですね。
もっといい方法がありそうですが、今はこの方法でやります。

  def read_main_location_id
    parse_text = read_location_id

    location_list = LocationList.new
    reader = MainCityReader.new
    reader.read
    area = parse_text['area']
    for area_no in 0..area.count - 1
      prefs = area[area_no]['prefs']
      for pref_no in 0..prefs.count - 1
        city = prefs[pref_no]['city']
        for city_no in 0..city.count - 1
          city_name = city[city_no]['name']
          if reader.contain?(city_name)
            location = Location.new
            location.area_name = area[area_no]['name']
            location.pref_name = prefs[pref_no]['name']
            location.location_name = city_name
            location.id = city[city_no]['id']
            location_list.add(location)
          end
        end
      end
    end
    return location_list
  end

  private

  def read_location_id
    file_path = File.expand_path('config/location_id.json', __dir__)
    parse_text = ''
    File.open(file_path, 'r') do |text|
      parse_text = JSON.parse(text.read)
    end
    return parse_text
  end

各地点の天気情報を取得する

idを使ってURLを作成します。

  BASE_URL = 'http://weather.livedoor.com/forecast/webservice/json/v1?city='.freeze
  def create(location_id)
    return BASE_URL + location_id.to_s
  end

URLから天気情報を持ったjsonファイルを取得します。

  def read(url)
    response = URI.open(url)
    @parse_text = JSON.parse(response.read)
  end

あとはjson内から天気情報を取得して終わりです。

表示結果

全国の5/24の天気を簡単に表示させた結果です。
スクリーンショット 2019-05-24 16.51.53.png

5/24は全国的に晴れのようですね。

今後

次にやっていきたいのは以下の3つですね。

  • 最高気温、最低気温が取得できるようなので画面に追加
  • 各地点をクリックすると、その地方の詳細な天気が見れるようにする
  • ログイン機能を持たせ、ログインすると明日、明後日の天気も表示できるようにする

独学でRubyを書いているので、指摘事項ありましたらコメントに記載をお願いします。

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

Ruby on rails: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)となった時の対処法

前提

僕のようなプログラミング初心者に良くあることで、例えばMacのアップデートするときに再起動するとローカル環境で立ち上げていたMySQLサーバーが切れるのですが、それを知らず何気なくrails sとするとタイトルのようなエラーメッセージが表示されるかと思います。
その際の対処法として、備忘録として残しておきます。そんなの知っているよっている人はスルーしていただければ幸いです。

対処法

ターミナルで下記のコードを打ち込んでください。これだけで解決します。

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

RailsアプリケーションのRubyとBundlerのバージョンをアップデートする

環境

$ cat /etc/system-release
Amazon Linux AMI release 2018.03

# プロジェクトのディレクトリ内にて
$ rbenv -v
rbenv 1.1.1-30-gc8ba27f

$ rbenv versions
* 2.5.3 (set by /var/www/myapp/.ruby-version)

$ bundle -v
Bundler version 1.17.1

# すでにアップデートされたRailsアプリケーション
$ bin/rails -v
Rails 5.2.3

はじめに

Railsアプリケーションをアップデートするにあたり、RubyとBundlerのバージョンもアップデートする必要があったので備忘録として残しておきます。

Rubyのバージョンを確認

rbenvにインストールしたいバージョンが存在するか確認します。

$ rbenv install -l

以上のコマンドで該当バージョンが出てこなければ、ruby-buildをアップデートする必要があります。

$ cd /usr/local/rbenv/plugins/ruby-build && git pull && cd -

アップデートしたいRubyのインストール

# ruby2.6.2をインストール
$ rbenv install 2.6.2

# ruby2.6.2に切り替え
$ rbenv global 2.6.2

# 再読み込みし、切り替えの反映
$ rbenv rehash

# 反映されたことを確認
$ rbenv versions
  system
  2.5.3
* 2.6.2 (set by /var/www/myapp/.ruby-version)

トラブルシューティング

インストールしたRubyのバージョンに切り替わらない

# 正しい参照先
$ which ruby
/usr/local/rbenv/shims/ruby

rbenvでRubyをインストールした場合、以上のように正しい参照先は/.rbenv/shims/rubyとなります。それ以外だと参照先が間違っている可能性があるため、PATHを通します。EC2では全てのユーザーがログイン時にrbenvを使えるようにするために、/etc/profile.d/rbenv.shにPATHを通している場合があります。

# /etc/profile.d/rbenv.shにPATHの記載があるかもしれない場合は要確認!
$ cat /etc/profile.d/rbenv.sh
export RBENV_ROOT="/usr/local/rbenv"
export PATH="${RBENV_ROOT}/bin:${PATH}"
eval "$(rbenv init -)"

# 以上にPATHがなければ、~/.bash_profileに記述
$ echo 'export PATH=~/.rbenv/bin:$PATH' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

# ~/.bash_profileの変更を反映
$ source ~/.bash_profile

Rubyのバージョンをアップデートしたものの、bundle installができない

Gemfile.lockに記載されているBUNDLE_WITHとbundlerのバージョンが異なる場合、bundle installをした時に以下のようなエラーが出てしまいます。

$ bundle install --path vendor/bundle -j4
・・・
find_spec_for_exe': can't find gem bundler (>= 0.a) (Gem::GemNotFoundException)
・・・

まずはGemfile.lockにて、BUNDLED WITHを確認します。確認したバージョンのbundlerをインストールします。
bundlerの参照先を確認し、/.rbenv/shims/bundlerとなっていればOKです。

# BUNDLE_WITHでbundlerのバージョンを確認
$ vim Gemfile.lock
・・・
BUNDLED WITH
   1.17.3

# Gemfile.lockに記載されているバージョンと同じbundlerを指定してインストール
$ rbenv exec gem install bundler -v 1.17.3

# 参照先の確認
$ which bundler
/usr/local/rbenv/shims/bundler
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jsフレームワークstimulusでyahoo地図を操作してみた(その二)

前回の続き。
今回参照したサイトはこちら……というか今回は、このサイトの記述を
【stimulus使用+「googlemap→yahoo地図」】
に書き換えただけです。

やったこと、やる予定のこと

0. railsアプリへのstimulus適用(以前書いた記事)

1. stimulusでyahoo地図を表示(前回)

2. 表示した地図上で、指定座標に移動(←今ここ)

……今回は「指定座標」はコードに直接書いています。
 座標取得の方法は「3」以降でやる予定。

3. 住所から座標を取得し、それをDBに登録(こんど書く)

4. 「3」で登録した情報を選択し、yahoo地図上でその場所に移動(そのうち書く)

ということで、前回作成した地図上を移動できるようにしてみます。

まずは、移動用のボタンを用意する。

app/views/users/test.html.erb
<div data-controller="moving">

<!-- 中略 -->
  <input type="button" id="tokyo" value="東京" data-action="click->moving#tokyo">

  <input type="button" id="shinbashi" value="新橋" data-action="click->moving#sinbasi">

  <input type="button" id="shinagawa" value="品川" data-action="click->moving#sinagawa">


</div>

ボタンを三個用意して、それぞれにデータアクション名を設定……「data-action="click->moving#tokyo"」(又は「sinbasi」「sinagawa」)
なおこの記述は、【「data-controller="moving"」であるdiv要素内で行う。

このアクション名設定により、ボタンクリックするとstimulusのアクションが発生するので……

アクション発生時の処理をstimulus用jsファイルに記述

……前回作成したjsファイルに、

app/javascript/controllers/moving_controller.js
  initialize() {

       this.map = new Y.Map(this.mapTarget.id);
       this.map.drawMap(new Y.LatLng(35.66572, 139.73100), 17, Y.LayerSetId.NORMAL);

       var center = new Y.CenterMarkControl
       var control = new Y.LayerSetControl();
       this.map.addControl(center);
       this.map.addControl(control);

  }

//↓追記
  tokyo() {
    this.map.panTo(new Y.LatLng(35.680865,139.767036), true);
  }

  sinbasi() {
    this.map.panTo(new Y.LatLng(35.666397,139.758153), true);
  }

  sinagawa() {
    this.map.panTo(new Y.LatLng(35.629867,139.74015), true);
  }
//↑追記

「35.680865,139.767036」などの座標は、参照サイトから写してきた実際の位置情報です。

これで実行してみると……
スクリーンショット 2019-05-24 14.04.00.png

「東京」「新橋」「品川」のボタンが追加され、クリックするとそれぞれの場所に移動できるようになりました。

とりあえずは今回はここまで、続きは次回以降(の、予定)。

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

Rails minitest のアサーション

はじめに

備忘録としてまとめていきます。
随時追記していきます。

assert_select        #アクション実行の結果として描写されるHTMLの内容の検証

assert_template    #そのアクションで指定されたテンプレートが描写されているかの検証
assert               #真であることを主張する
assert_not           #偽であることを主張する 
assert_no_difference #式で評価した結果の数値は、ブロックで渡されたものを呼び出す前と後で違いがないと主張する

    assert_no_difference 'User.count' do
      post users_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
follow_redirect!     #リクエストを送信した結果を見て、指定されたリダイレクト先に移動するメソッド

 get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!

参考リンク・資料

Rails テスティングガイド

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

Rails minitest コマンド

はじめに

備忘録としてまとめていきます。
随時追記していきます。

assert_select        #アクション実行の結果として描写されるHTMLの内容の検証

assert_template    #そのアクションで指定されたテンプレートが描写されているかの検証
assert               #testはtrueであると主張する
assert_not           #testはfalseであると主張する 
assert_no_difference #式で評価した結果の数値は、ブロックで渡されたものを呼び出す前と後で違いがないと主張する

    assert_no_difference 'User.count' do
      post users_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
follow_redirect!     #リクエストを送信した結果を見て、指定されたリダイレクト先に移動するメソッド

 get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!

参考リンク・資料

Rails テスティングガイド

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

form_withでvalidationエラーが出ない原因と対処法

はじめに

Rails5.1からform_forとform_tag非推奨になり、form_withが推奨になりました。
form_forの感覚でform_withを使ってハマったのでまとめていきます。

問題: validationエラーがviewに表示されない

下記のようにコーディングしており、
validationエラーがnewのviewに発生するはずなのにエラーメッセージが発生しない。

new.html.erb
<% provide(:title, "Sign up")%>
  <h1>Sign up</h1>

  <div class="container">
    <div class="row">
      <div class="col-md-6 col-md-offset-3">
        <%= form_with model: @user do |f| %>
        <%= render 'shared/errors_messages' %>

        <%= f.label :name %>
        <%= f.text_field :name, class: 'form_control' %>

        <%= f.label :email %>
        <%= f.email_field :email, class: "form_control" %>

        <%= f.label :password %>
        <%= f.password_field :password, class: 'form_control' %>

        <%= f.label :password_confirmation, "Confirmation" %>
        <%= f.password_field :password_confirmation, class: 'form_control' %>

        <%= f.submit "Create my account", class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>
</div>
_errors_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

原因: form_withはデフォルトオプションがremote:trueになっている

Ajaxを行うときに、form_tagやform_forはオプションでremote: trueで対応していたが、
form_withではデフォルトでremote: trueになっている(デフォルトでAjaxになっている)。

解決: form_withのオプションをlocal: trueを指定する

formでAjaxを使用しない時はオプションlocal: trueを指定する。
form_withのオプションでlocal: trueを指定するとvalidationエラーをviewに表示できる。

参考リンク・資料

Rails 5.1のform_withでViewにvalidationエラー表示
ActionView::Helpers::FormHelper
actionview/lib/action_view/helpers/form_helper.rb

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

js.erbファイルにコメントを書くときは//を使わない

修正前

index.js.erb
// コメントが見えてるよ!
$("#ajax_panel").html("<%= escape_javascript(render partial: "ajax_panel") %>");

結果

ツールを使ってファイルを見ると...
(画像はChromeのデベロッパーツールのNetworkパネルから確認)
スクリーンショット 2019-05-24 14.01.35.png

コメントに重要な内容が含まれていると、敵の攻撃を手助けしてしまう可能性があり危険です。

修正

erbファイルのコメントの書き方に修正します。

index.js.erb
<%# コメントが見えないだと... %>
$("#ajax_panel").html("<%= escape_javascript(render partial: "ajax_panel") %>");

修正後

コメントが表示されなくなりました。
スクリーンショット 2019-05-24 14.04.56.png

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

Railsアプリ作成手順まとめ

はじめに

備忘録としてまとめていきます。
随時追記していきます。

Railsプロジェクトを作成
$ rails new <プロジェクト名> -d <データベース>
ローカルリポジトリを作成、リモートリポジトリにプッシュ
$ git init
$ git add 
$ git commit -m "Initial commit"
$ git remote add origin https://github.com.ユーザー名.リポジトリ名
$ git push -u origin master
データベース作成
$ rails db:create
Herokuへデプロイ
$ heroku create
$ git push heroku master
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsのCI環境でChildProcess::Errorが発生する場合の対処法

結構長いこと悩まされていたエラーだったのですが、原因がわかりましたので共有します。

CircleCIが安定しない

テストは全て通っているのですが、時々というか結構な頻度で以下のようなエラーが出てCIがコケていました。

/home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/childprocess-1.0.1/lib/childprocess/abstract_process.rb:188:in assert_started': process not started (ChildProcess::Error)
6: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/selenium-webdriver-3.141.5926/lib/selenium/webdriver/common/platform.rb:150:in
block in exit_hook'
5: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/selenium-webdriver-3.141.5926/lib/selenium/webdriver/common/service.rb:110:in stop'
4: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/selenium-webdriver-3.141.5926/lib/selenium/webdriver/common/service.rb:110:in
ensure in stop'
3: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/selenium-webdriver-3.141.5926/lib/selenium/webdriver/common/service.rb:163:in stop_process'
2: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/selenium-webdriver-3.141.5926/lib/selenium/webdriver/common/service.rb:180:in
process_exited?'
1: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/childprocess-1.0.1/lib/childprocess/unix/process.rb:31:in exited?'
/home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/childprocess-1.0.1/lib/childprocess/abstract_process.rb:188:in
assert_started': process not started (ChildProcess::Error)

このエラーメッセージ等でググっていたのですが、解決方法が見当たらず…。時々失敗するけれど時々成功するため、ときどき調査しようとしては諦めていました。

他のエラーメッセージに気づく

弊社の他のプロジェクトでも同様の症状が出ており、同僚が調査していたのですが、そのときに見つけたメッセージがこれ。

Text file busy - /home/circleci/.webdrivers/chromedriver

私はエラーメッセージのほうばかりを見ていて気づいてなかったのですが、RSpecが落ちたテストをリトライするところに出ていました。

こちらでググると、Rails 6.0系のissueとPRがヒットしました。

https://github.com/rails/rails/pull/36292

Rails 6系ではデフォルトで並列テストをサポートするという認識ですが、弊社のCIもparallel_testsでテストを回していて、同じ症状のようです。

症状の詳細は、webdriversがchromedriverのアップデートをしようとするが、並列でそれが行われてしまい、片方のプロセスが起動できなかったということです。

そこで、並列テストが実行される前にchromedriverのアップデートをしておけば、この問題は発生しなくなると考えました。(上記のPRもそういうことをやっていますが)

修正方法

並列テストが実行される前にchromedriverを更新するよう.circleci/config.ymlを修正しました。parallel_tests等でテストを起動するとその時点で並列化されているから、その前にやっておきます。

.circleci/config.yml
steps:
  # 略。ただし、DB作成後でないとrails runnerが失敗するので注意。
  - run:
      name: Update chromedriver
      command: env RAILS_ENV=test bin/rails runner "Webdrivers::Chromedriver.update"
  # 略。テストを実行

結果

10回連続で同じテストを実行しましたが、全部成功しました:v:

もし並列テストが不安定だ〜という方はこれを追加してみましょう!

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

RubyのJITコンパイラを理解したい!rubykaigiの香りを添えて

まえがき (ポエムなので読み飛ばせます)

image.png

「Rubyは遅い!」と言われがちですが、来たる Ruby3.0 に向けて、今までよりも3倍の高速化を図る試み、 Ruby 3x3が進行しています。
先月行われたrubykaigi2019でもその取り組みが発表され、多くの期待の眼差しが向けられていました。

とりわけ、高速化の肝になるであろう JITコンパイラはセッションだけでなくキーノートでも触れられ、大きな注目を集めていました。

しかし、電子計算機の仕組みを独学で(しかも基本情報技術者試験のため)ふわっと勉強した筆者(非情報系卒)にとっては、いささか難しい内容で、オープンされるスライドのたびに変わる会場の空気を「完全に理解した」とは口が避けても言えませんでした。

それでも、JITコンパイラをはじめとする数々の試みが、Rubyエンジニアにとって、とても興味深く、同時に楽しいことであることは十分に伝わってきました
今回はrubykaigi2019の残り香を楽しみつつ、多くのセッションでも取り上げたれたJITコンパイラについて、詳しく見ていきたいと思います。

この記事では、JITの概念をふわっと理解するために、ふわっと理解しようとしている筆者が書いています。
もし、誤りや怪しい部分がございましたら、忌憚ないご意見をいただければ幸いです! (>_<)

JITコンパイラ is なに?

JITコンパイラは Ruby2.6 からオプションで追加された、Rubyを高速に実行しようとする仕組みです。
JIT(Just-In-Time Compiler)とは、コードがまさに実行されるそのときにコンパイルされる仕組みのことで、RubyだけでなくJavaの実行環境でも取り入れられている仕組みです。

JITコンパイルという用語は、ソフトウェアを構成するモジュールやクラス、関数などの、ある単位のコードがまさに実行されるその時に、コンパイルされることから「Just In Time」の名前が付けられた
wikipediaより https://ja.wikipedia.org/wiki/%E5%AE%9F%E8%A1%8C%E6%99%82%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9

RubyのJITはMJITという名前がよく出ていますが、しくみを理解する上ではRubyのJITの実装周りの総称として覚えておいて良さそうです。
参考:https://k0kubun.hatenablog.com/entry/ruby26-jit

これまでとの処理の違い

Ruby1.9~Ruby2.5 ,デフォルト設定のRuby2.6での実行

スクリーンショット 2019-05-24 13.21.04.png

Rubyはインタープリタ型言語なので、コンパイルして直接機械語に変換されるわけではありません。どうやって実行されているというと、Rubyのコードは字句・構文解析を経てYARVバイトコードというものに変換されます。バイトコードはプログラム言語と機械語との中間にあたるようなコードです。

でもYARVバイトコードはあくまでバイトコードであって機械語ではないので、CPUはこの YARVバイトコード を直接解釈して実行することはできません。
そこで、CPUに変わってYARVバイトコードを解釈し、CPUに命令を発行してくれるのがバーチャルマシン(VM)であるYARVです。

こうしてRubyのコードは、明示的な機械語へのコンパイルを必要とせずに、実行することができます。

MJITを有効にしたRuby2.6

(これらの理解の拠り所として Cコンパイラを利用したRubyのJITコンパイラ を参考にさせていただきました。)

YARVバイトコード が生成されるところまではこれまでと変わりません。
そして、バーチャルマシンであるYARVもこれまで通り登場しますが、JITコンパイラーという役者が増えています。

image.png

あるプログラムが実行されたとき、YARV(以下VM) は生成された YARVバイトコードを解釈し、CPUが理解できる命令を発行し、実行してくれます。

ここで、あるメソッドが5回以上呼ばれたとします。そのときVMのスレッドは、JITのキューに、このメソッドを積みます。
JITはVMとは別のスレッドで動いて、積まれたメソッドをYARVバイトコードからCのコードに変換します。
生成されたCのコードはやがて機械語に変換され.soファイルが生成されます。
.soファイルの中身はバイナリコード(=機械語)です。これは動的にVMから呼ばれるようにリンクされます。

なので、もし次のタイミングでVMが処理しようとしているYARVバイトコードの中に、先ほどJITコンパイラが処理したのと同じメソッドがあった場合、
VMはYARVバイトコードを解釈してメソッドを実行するのではなく、機械語にコンパイル済みのメソッド.soファイルを関数ポインタを通じて読み込むことで、より高速に同メソッドを実行することができます。

現状での速さ/ベンチマーク

JITの仕組みによりRubyの高速化が図られたわけですが、いったいどれだけ早くなったのかというデータは検証方法によってバラツキがあるようです。

これはCコードを生成する際の最適化、Cコードから機械語に翻訳する際の最適化など、様々な要素が絡みあっているからのようで、単純に「何倍早くなった!」と言えるわけではないようでした。

また、JITを有効にしてRuby on Railsで作成したWebアプリケーションを実行すると、かえって遅くなるようなデータもrubykaigiでは示されていました。
ただ最新の実験ではJITを有効化しても、無効化時と同程度のスコアが出るようにはなったそうです....ここからが本番。(2019/4 rubykaigi)

(これは筆者の感覚的理解ですが、単純に処理が増えたのだからそれは遅くなっても仕方なさそうだし、プログラムの実行時間が長くなり機械語にコンパイル済みのメソッドが増えれば増えるほど高速化してゆくようなパラダイムでもあるし....納得、という感じです。)

さらに、JIT以外の速度改善についても、「rubyのインタープリタをrubyで書く」といった内容があり、非常に興味のそそるものでした。RubyKaigi 2019: Write a Ruby interpreter in Ruby for Ruby 3

おわりに

rubykaigiを振り返ると自分はいかにRubyを知らなかったのか思い知らされます。日常的にRailsに触っていると、Railsが行ってくれている魔術が当たり前になってしまうことがあったかもしれません。
一方、周りを見渡すと、Railsを使いながらもRubyで内製ツールを作って開発効率を上げている例が多くあることを知り、そういった活動が組織の技術力を作り、またOSSの活動へと広がってゆくことを実感しました。

Rubyというプログラミング言語を通し、技術に向き合うとうことを再確認したrubykaigiの3日間でした。

参考にた書籍/Webページ

Rubyのしくみ -Ruby Under a Microscope-
Ruby 2.6にJITコンパイラをマージしました|k0kubun's blog
プロと読み解く Ruby 2.6 NEWS ファイル|クックパッド開発者ブログ
Cコンパイラを利用したRubyのJITコンパイラ / Programming Symposium 60


LITALICOではエンジニアを積極採用中です。
新卒・第二新卒(未経験含)/中途採用、いずれも行なっていますので、ご興味のある方は下記URLをご確認ください。
https://www.wantedly.com/projects/309158

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

devise で基本的なアカウント機能のみを実装してみた

はじめに

アカウント機能を容易に実装することができる gem である devise の基本的な使用方法を
数回に分けて投稿します。

devise gem には様々な機能がありますが、
今回はあえて基本的なアカウント機能のみを実装してみようと思います。

動作対象
・Ruby 2.5
・Rails 5.1
・SQLite3

今回実装する機能
・アカウント作成機能
・ログイン及びログアウト機能
・アカウント編集機能

devise とは?

devise はログイン・ログアウト機能やアカウント作成機能などを簡単に実装できる gem です。
機能がモジュール化されているので、管理しやすいのが特徴です。
今回は2018/3/18にリリースされた4.4.3を使っていきます。

devise のインストール

まずは雛形を作成しましょう。
私はいつもディレクトリを真っ先に作成して、そのディレクトリに移動して、
Gemfile を作成するという手順を取っています。

今回は devise_learning というアプリを開発していきます。

# 任意のディレクトリに移動して、devise_learning ディレクトリを作成する
$ mkdir devise_learning

# devise_learning ディレクトリに移動する
$ cd devise_learning

# Gemfile を作成する
$ bundle init

次に、Gemfile に今回使用する gem を貼り付けます。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'rails', '~> 5.1.5'
gem 'turbolinks', '5.2.0'
gem 'puma', '~> 3.7'

# database
gem 'sqlite3', '~> 1.3.6'

# devise
gem 'devise', '4.4.3'
# gem 'bcrypt', git: 'https://github.com/codahale/bcrypt-ruby.git', require: 'bcrypt'

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '3.1.5'
end

# windows 環境の方は以下のコメントを外してください
# gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

devise はパスワードを暗号化する際に bcrypt という gem を使用しています。
ruby のバージョンが 2.3 系の場合、あるいは Windows 環境で本アプリを作成する場合は、
デフォルトでインストールされる bcrypt が動作しない場合があります。
動作しない場合はこのコメントを外してください。

Gemfile
# gem 'bcrypt', git: 'https://github.com/codahale/bcrypt-ruby.git', require: 'bcrypt'

完了したら bundle install をしましょう。

$ bundle install

ここでアプリケーションの雛形を作成します。
既に gem はインストールしたので、-B を付属して bundle install をスキップします。

$ rails new ./ -B

Gemfile の対応について尋ねられると思いますが、もちろん n と答えてください。
アプリケーションの雛形が完成したら、とりあえずアプリを起動してみましょう。

$ rails server

「Yay! You’re on Rails!」と表示されていれば OK です。

devise のインストール

早速、devise を雛形にインストールしましょう。
rails g devise:install を実行すると devise の導入手順が出力されるので、これを元に進めていきます。

$ rails g devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================

先ほどのコマンドで 2 つのファイルが作成されましたが、これらのファイルの使い方は後ほど解説します。
まずは 1 番目の説明を読んでみましょう。

devise の導入手順の和訳 (1)
環境ファイルにデフォルトの URL オプションが定義されていることを確認してください。
config/environments/development.rb の開発環境に適した default_url_options の例を次に示します:

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

プロダクション環境では、host: はアプリケーションの実際のホストに設定する必要があります。

config/environment/development.rb に defalut_url_options を設定しましょう。
default_url_options については Rails チュートリアルの 11.2.2 でも取り扱われています。
私はローカル環境で開発しているので、devise の導入手順の通りに記述します。

config/environment/development.rb
Rails.application.configure do
  # .
  # .
  # .
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
  # .
  # .
  # .
end

メール認証機能は今回実装しないので、これ以上の設定は行いません。
終わったら 2 番目の説明を読んでみましょう。

devise の導入手順の和訳 (2)
config/routes.rb に何らかの root_url を定義していることを確認してください。
例えば:

root to: "home#index"

config/routes.rb に root_url を定義しましょう。
まずは controller を定義します。
今回は devise の導入手順に示されている例のとおり、
ホーム画面用の home コントローラと index ページを用意します。

$ rails g controller home index

次は config/routes.rb に root_url を設定します。
自動で作成されるルーティングを次のように書き換えてください。

config/routes.rb
Rails.application.routes.draw do
  root to: 'home#index'
end

以上で root_url の定義は完了です。
終わったら 3 番目の説明を読んでみましょう。

devise の導入手順の和訳 (3)
app/views/layouts/application.html.erb にフラッシュメッセージがあることを確認してください。
例えば:

<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

任意のフラッシュメッセージを定義していきましょう。
といっても、今回は app/views/layouts/application.html.erb に、
導入手順にかかれているコードをそのまま貼り付けてしまいます。

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>DeviseLearning</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <div class="flash">
      <p class="notice"><%= notice %></p>
      <p class="alert"><%= alert %></p>
    </div>
    <%= yield %>
  </body>
</html>

bootstrap を使用したスタイルの調整などは今回行わないので、以上で任意のフラッシュメッセージの定義は完了です。
終わったら 4 番目の説明を読んでみましょう。

devise の導入手順の和訳 (4)
以下のコマンドを実行することによって、
Deviseビュー(カスタマイズ用)をアプリにコピーすることができます:

rails g devise:views

Devise ビュー(カスタマイズ用)をアプリにインストールしてみましょう。
導入手順に記されているコマンドを実行すると大量の Devise ビューが作成されます。

$ rails g devise:views
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_links.html.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/email_changed.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb

作成されたファイルで、ログインやアカウント作成時などで使用される初期ビューを変更することができます。
今回は変更しません。
以上で devise の初期設定は完了です。これでアカウント機能をもつテーブルを作成する手順が整いました。

devise を用いてアカウント機能を実装

devise を用いてアカウント機能をもつ users モデルを作成していきます。
devise でアカウントを持つモデルを作成する場合は次のコマンドを実行します。

$ rails g devise user
      invoke  active_record
      create    db/migrate/xxxxxxxxxxxxxx_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users

まずは、route devise_for :users からみていきます。

config/routes.rb が次の通りになっていることを確認してください。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'home#index'
end

devise_for :users というルートが設定されていることが確認できます。
どんなルートが作成されているのか確認してみましょう。

$ rails routes
                  Prefix Verb   URI Pattern                    Controller#Action
        new_user_session GET    /users/sign_in(.:format)       devise/sessions#new
            user_session POST   /users/sign_in(.:format)       devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy
       new_user_password GET    /users/password/new(.:format)  devise/passwords#new
      edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit
           user_password PATCH  /users/password(.:format)      devise/passwords#update
                         PUT    /users/password(.:format)      devise/passwords#update
                         POST   /users/password(.:format)      devise/passwords#create
cancel_user_registration GET    /users/cancel(.:format)        devise/registrations#cancel
   new_user_registration GET    /users/sign_up(.:format)       devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)          devise/registrations#edit
       user_registration PATCH  /users(.:format)               devise/registrations#update
                         PUT    /users(.:format)               devise/registrations#update
                         DELETE /users(.:format)               devise/registrations#destroy
                         POST   /users(.:format)               devise/registrations#create
                    root GET    /                              home#index

devise_for :users は使用されているモジュールに応じて、devise の機能に必要なルートを設定します。
使用するモジュールは app/models/user.rb で設定します。
次は create app/models/user.rb をみてみましょう。
devise のモジュールはこのファイルで管理します。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end

devise に実装されている各モジュールの機能をここに記載します。

devise modules 機能
database_authenticatable サイン時にパスワードを暗号化してDBに登録
registerable ユーザーが自身のアカウントの編集と削除を可能にする
recoverable パスワードのリセットを可能にする
rememberable Remember Me 機能を有効化する
trackable サインインの回数やIPアドレスなどを記録
validatable メールとパスワードのバリデーションを行う
confirmable メール認証機能を有効化
lockable 規定回数ログインに失敗したらアカウントをロックする
timeoutable 一定時間でセッションを破棄する
omniauthable Twitter や Facebook など、外部サービスのアカウントで認証を可能にする

今回は現時点で使用しないモジュールを全てコメントアウトします。

app/models/user.rb
class User < ApplicationRecord
  # Not use devise modules are:
  # :recoverable, :trackable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable, :rememberable, :validatable
end

使用するモジュールを 4 つに絞りました。
この 4 つのモジュールだけで Rails チュートリアルの 10 章 までの機能を実装することができます。
ここでもう一度ルートを確認してみてください。先ほど存在していたルートの一部がなくなっているはずです。

$ rails routes
                  Prefix Verb   URI Pattern               Controller#Action
        new_user_session GET    /users/sign_in(.:format)  devise/sessions#new
            user_session POST   /users/sign_in(.:format)  devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
cancel_user_registration GET    /users/cancel(.:format)   devise/registrations#cancel
   new_user_registration GET    /users/sign_up(.:format)  devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)     devise/registrations#edit
       user_registration PATCH  /users(.:format)          devise/registrations#update
                         PUT    /users(.:format)          devise/registrations#update
                         DELETE /users(.:format)          devise/registrations#destroy
                         POST   /users(.:format)          devise/registrations#create
                    root GET    /                         home#index

次に進む前に、ログインやログアウトなどに必要なリンクを作成しておきましょう。
rails routes の出力からリンクを作成します。
app/wiews/layouts/_session.html.erb を新たに作成して次のように記述してください。

app/wiews/layouts/_session.html.erb
<% if user_signed_in? %>
  <p><%= link_to "アカウント編集", edit_user_registration_path %>
  <p><%= link_to "ログアウト", destroy_user_session_path, method: "delete" %></p>
<% else %>
  <p><%= link_to "ログイン", new_user_session_path %></p>
  <p><%= link_to "アカウント作成", new_user_registration_path %></p>
<% end %>

user_signed_in? はセッションを登録しているかどうかを真理値で返してくれる、
devise が提供するメソッドです。
user の箇所は作成したテーブル名で変化するので注意してください。

app/views/layouts/application.html.erb にこのパーシャルを追記しましょう。

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>DeviseLearning</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <div class="flash">
      <p class="notice"><%= notice %></p>
      <p class="alert"><%= alert %></p>
    </div>
    <%= render 'layouts/session' %>
    <%= yield %>
  </body>
</html>

次は create db/migrate/xxxxxxxxxxxxxx_devise_create_users.rb を見ていきましょう。
※ xxxxxxxxxxxxxxには作成日時が入ります。

このマイグレーションファイルには、アカウント機能で使用されるテーブルが用意されています。

db/migrate/xxxxxxxxxxxxxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

今回は app/models/user.rb で設定した 4 つのモジュールに必要なカラムだけを作成します。
現時点で使用しないカラムをコメントアウトしましょう。

db/migrate/xxxxxxxxxxxxxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      # t.string   :reset_password_token
      # t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    # add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

この作業が完了したらデータベースを作成して、マイグレートしましょう。

$ rails db:migrate

db/schema.rb をみると、
アカウント機能を実現するのに必要な最低限のカラムが用意されていることがわかります

以上で基本的なアカウント機能は全て実装できました

「え?これだけ?」
はい、本当にこれだけです。
これだけでアカウント登録・編集・削除、ログイン・ログアウト、Remember Me 機能の実装が完了です。
サーバを起動してみて、実際に挙動を確認してみてください。

さいごに

今回は基本的なアカウント機能を devise を使って実装しました。
devise を扱う際、必要以上にカラムを作らないように気をつけましょう。

参照・参考

Rails チュートリアル 11.2.2
Rails チュートリアル 10章
STEP21:Rails5にdeviseでログイン機能を実装しよう! #Rails #Ruby | TickleCode

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

jsフレームワークstimulusでyahoo地図を操作してみた(その一)

「railsアプリで、stimulusを使ってyahoo地図を操作する方法」についての纏めーー
当初はgooglemapで試してみるつもりだったが、料金請求が怖そうなのでyahoo地図に切り替えた(操作自体は似ているので、ちょっと書き換えればgooglemapにも簡単に切り替えられるはず。実際、【googlemap操作について書かれたサイト】も複数参照して書いています)。

やったこと、やる予定のこと

0. railsアプリへのstimulus適用(以前書いた記事)

1. stimulusでyahoo地図を表示(←今ここ)

2. 表示した地図上で、指定座標に移動

……この段階では「指定座標」はコードに直接書いています。
 座標取得の方法は「3」以降でやる予定。

3. 住所から座標を取得し、それをDBに登録(そのうち書く)

4. 「3」で登録した情報を選択し、yahoo地図上でその場所に移動(そのうち書く)

【事前準備】railsにstimulusを適用し、yahooのアプリケーションIDも取得しておく

stimulus適用はこちら(自記事)やこちら、アプリケーションID取得はこちらを参照。

とりあえず、地図を表示させてみる。

公式サイトを参照つつ……

まずはアプリケーションビューで、

app/views/layouts/application.html.erb
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application' %>

    <%# ↓これを追記↓ %>
    <script type="text/javascript" charset="utf-8" src="https://map.yahooapis.jp/js/V1/jsapi?appid=【取得した自分のID】"></script>
    <%# ↑これを追記↑ %>

と【yahooのAPIを使用する】旨を記述
……APIを使用するビューだけに個別に書いても問題ない(というか、アクセス回数制限を考え得るならそちらのほうがいい)のだろうけど、今回は面倒なので全部に適用させることにした。

で、地図を表示したい場所(ビュー、今回はusers/testを作ってそこでやりました)に

app/views/users/test.html.erb
<!-- ↓①↓ -->
<div data-controller="moving">

  <p>Google Maps APIを使ったサンプルです。</p>

 <!-- ↓②↓ -->
  <div id="idmeihatekitoudemoiiyo" style="width:500px; height:300px"  data-target="moving.map" ></div>


</div>

と記述。
①stimulus用に【div要素の枠、「data-controller="moving"」】を作成。
②「①」の枠の中に配置した【ヤフー地図を表示するためのdiv要素……「div id="map"」】に【「map」というターゲット名……「data-target="moving.map"」】を設定する。
(stimulusだと要素はデータターゲット名で指定可能なので、「②」のdiv要素のid名は適当でも問題なくなります(いやでも、これは適当すぎだろう)。

最後に、対応するstimulusファイルを作成。

↑で「data-controller="moving"」としているので、ファイル名は「moving_controller.js」となります。

app/javascript/controllers/moving_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "map" ]

  initialize() {//←①

       this.map = new Y.Map(this.mapTarget.id);//←②
       this.map.drawMap(new Y.LatLng(35.66572, 139.73100), 17, Y.LayerSetId.NORMAL);//←③

    //↓④
       var center = new Y.CenterMarkControl
       var control = new Y.LayerSetControl();
       this.map.addControl(center);
       this.map.addControl(control);

  }
}

①イニシャライズ時に、
②【ビュー側で「data-target="moving.map"」としている要素】のidを使って、「this.map」を設定
 ……ここはid名を直接記入しても、問題はないです。ただ要素の指定はターゲット名で統一したほうが、なんかカッコイイ気がする(いや、id名が異なる違う要素にも使い回せますし)。
③「this.map」にヤフー地図を書き込む(「drawMap」。「35.66572, 139.73100」が初期表示する座標、お好みで変更してください。
④お好みでレイヤーを設定( 公式サイトにある「レイヤーセットID」をご参照ください)。

これで、地図が画面に表示されるように……

スクリーンショット 2019-05-24 11.46.42.png

  なりました。

とりあえずは今回はここまで。続きはこちらです。

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

ドット記法で値にアクセスできない…

RailsのActive Recordにおいて、例えば、Userモデルのインスタンスはドット記法を用いてその属性値にアクセスできることはご存知かと思います。

例)
>> user = User.new(name: "hoge", email: "hoge@example.com")
>> user.name
=> "hoge"

ですが、ドット記法を用いて属性値にアクセスできないことがあったので、シェアしたいと思います。

バージョン

Rails:5.1.4
ruby:ruby 2.6.0p0

状況

例えば、勤怠を管理するモデルでAttendanceモデルがあるとする。

id time_in time_out time_in_change time_out_change
1 09:00 18:00 10:00 19:00

この時、

例)
>> attendance = Attendance.find(1)
>> attendance.time_in
=> "09:00"

となり、ドット記法でアクセスできるが、time_in_change及びtime_out_changeカラムの場合は、

例)
>> attendance = Attendance.find(1)
>> attendance.time_in_change
=> nil

でドット記法で属性値にアクセスできない現象が発生しました。

似たような現象が起こっている記事を発見し、→ 参考記事

試しに、

例)
>> attendance = Attendance.find(1)
>> attendance.attributes['time_in_change']
=> "10:00"

でドット記法で値にアクセスできるが、なぜなのか原因が分からず…

さらに、試みて

time_in_change → change_time_in
time_out_change → change_time_out
とカラム名を変更すると、

例)
>> attendance = Attendance.find(1)
>> attendance.change_time_in
=> "10:00"

となり、attributesを使用しなくても値にアクセスできるようになりました。
カラム名の命名に問題があるのでしょうか?

最後に

今回の現象について、原因が何であるか分からないため、この記事を読んでくださった方で
何か原因が思い当たるようでしたら、アドバイス頂けると幸いです。
よろしくお願いします。

@sakuro さん
@scivola さん
よりご指摘頂きました。

どうやら、
今回の場合、time_inカラムが存在していて、
.time_in_changeとすると、.time_in_changeカラムの属性値に
アクセスするのではなく、ActiveModel::Dirtyモジュールに定義されている
time_in_changeメソッドが呼び出されるみたいです。
※カラム名_changeで呼び出される。

今回の属性値にアクセスできるか否かの件とは関係ありませんが、
ActiveModel::Dirtyは、変更前の値を追跡できたりするので、
かなり便利ですね!

参考記事

@sakuro さん
@scivola さん
ご指摘ありがとうございました!

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

carrierwave+S3で本番環境への画像アップロード機能実装

はじめに

プロフィール画像をアップロードする機能をcarrierwavemini_magickfog-awsを用いてAWS S3にアップロードするまでの設定方法を書いていく。
本記事では前提としてユーザー管理にdevise、ビューにはSlimを使用しており、 AWS S3でバケットの作成が済んでいる状態で進める。

準備

あらかじめ本記事で用いるgemをインストールしておく。

Gemfile
gem 'carrierwave'
gem 'mini_magick'
gem 'fog-aws'
Terminal
bundle

また、Userモデルにプロフィール画像保存用のカラムとして、avatarカラムを追加しておく。
コマンドでbin/rails g migration AddAvatarToUsers avatar:stringを入力し、マイグレーションファイルを生成。

2019***********_add_avatar_to_users.rb
class AddAvatarToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :avatar, :string
  end
end
Terminal
bin/rails db:migrate

手順

carrierwaveの設定

まずは画像のアップローダーを作成します。
コマンドでbin/rails g uploader Avatarと入力。
ここでは最低限の設定をしていきます。

app/uploaders/avatar_uploader.rb
class AvatarUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  # 環境毎の画像保存先
  if Rails.env.development?
    storage :file
  elsif Rails.env.test?
    storage :file
  else
    storage :fog
  end

  # S3のディレクトリ名
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # 許可する画像の拡張子
  def extension_whitelist
     %w(jpg jpeg gif png)
  end

  # 保存するファイルの命名規則
  def filename
     "#{secure_token}.#{file.extension}" if original_filename.present?
  end
end

また、user.rbに以下のコードを追記し、Avatarカラムとアップローダーを紐づけ。

user.rb
class User < ApplicationRecord
  mount_uploader :avatar, AvatarUploade

続いてconfig/initializers/carrierwave.rbを作成し、AWS S3の設定書いていく。
credentialの設定方法については、credentials.yml.encでシークレットキーを管理にまとめた。

config/initializers/carrierwave.rb
if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_provider = 'fog/aws'
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
      aws_secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
      #S3のリージョン #ap-northeast-1はアジアパシフィック(東京)
      region: 'ap-northeast-1'
    }
    # S3のバケット名
    config.fog_directory  = 'hogehoge'
    # S3に保存しておく期間
    config.fog_attributes = { cache_control: "public, max-age=#{365.days.to_i}" }
  end
end

ストロングパラメータの設定

avatarに要素が入った状態でUserモデルが更新されるのを許可するために、ストロングパラメータの設定をする。

application_controller.rb
  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
    devise_parameter_sanitizer.permit(:account_update, keys: [:username, :description, :avatar])
  end

ビューの設定

app/views/devise/registrations/edit.html.slim
 .circle-avatar.field
   label for="user_avatar"
     | プロフィール画像
   #img_field onclick="$('#file').click()"
     - if current_user.persisted? && current_user.avatar?
       = image_tag current_user.avatar.to_s
       = f.file_field :avatar, style: "display:none;"
     - else
       = image_tag "no_avatar.png"
       = f.file_field :avatar, style: "display:none;"

current_userdeviseの独自メソッド。
ログインユーザーがプロフィール画像を設定している場合はそれを表示し、設定していない場合に表示する画像ファイル(ここではno_avatar.pngはあらかじめ用意する
display:none;とすることで、プロフィール画像をクリックすると画像選択ができるようにする。

app/assets/stylesheets/users.scss
.circle-avatar.field img {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  object-fit: cover;
}

#img_field:hover {
  transition: 0.5s ease-out;
  opacity: 0.5;
}

プロフィール画像を丸く表示し、ホバー時に半透明になるよう設定する。

最後に選択された画像を表示するための設定をする。

users.js
$(document).on("turbolinks:load", function(){
  $fileField = $('#file')
  $($fileField).on('change', $fileField, function(e) {
    file = e.target.files[0]
    reader = new FileReader(),
    $preview = $("#img_field");

    reader.onload = (function(file) {
      return function(e) {
        $preview.empty();
        $preview.append($('<img>').attr({
          src: e.target.result,
          width: "100%",
          class: "preview",
          title: file.name
        }));
      };
    })(file);
    reader.readAsDataURL(file);
  });
});

以上でアップロード昨日の実装完了。
test1.gif

参考

【Rails5】Deviseのregistrations#editで画像をアップロードする
Railsでcarrierwaveを使ってAWS S3に画像をアップロードする手順を画像付きで説明する!

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

Rails6 のちょい足しな新機能を試す23(I18n fallbacks編)

はじめに

Rails 6 に追加されそうな新機能を試す第23段。 今回のちょい足し機能は、 I18n fallbacks 編です。
Rails 6.0 では、 config.i18n.fallbacks の設定で、明示的に I18n.default_locale を fallback として指定していないとDEPRECATION WARNING が出ます。fallbacks の挙動は、Rails 6.0 と Rails 6.1 で違いがあると思われます。

Ruby 2.6.3, Rails 6.0.0.rc1 で確認しました。Rails 6.0.0.rc1 は gem install rails --prerelease でインストールできます。

$  rails --version
Rails 6.0.0.rc1

Rails プロジェクトを作る

$ rails new rails6_0_0rc1
$ cd rails6_0_0rc1

Controller と View を作る

今回はモデルなしで、試します。

$ bin/rails g controller i18n_fallbacks index

db:create をしておく

$ bin/rails db:create

development.rb に fallbacks の設定を追加する

config/environments/development.rb に fallbacks を設定します。

config/environments/development.rb
Rails.application.configure do
  ...
  config.i18n.fallbacks = [{de: :ja}]
end

locale 変換用のファイルを用意する

英語(en)、日本語(ja)、ドイツ語(de) の3つを用意します。
1つの yml ファイルに全部の訳語が揃ってしまうと fallbacks の動作を確認できないので、揃わないようにします。

config/locales/en.yml
en:
  morning: "Good Morning"
config/locales/ja.yml
ja:
  afternoon: こんにちは
config/locales/de.yml
de:
  night: Gute Nacht

index.html.erb を編集する

本来、 with_locale は、ApplicationController で使うべきだと思いますが、今回は手抜きで View だけでやります。

app/views/i18n_fallbacks/index.html.erb
<% I18n.with_locale(:de) do %>
  <h1>I18n Fallbacks Test</h1>
  <h2>I18n settings</h2>
  <ul>
    <li>I18n.default_locale = <%= I18n.default_locale %></li>
    <li>I18n.locale = <%= I18n.locale %></li>
    <li>I18n.fallbacks = <%= I18n.fallbacks %></li>
  </ul>

  <h2>I18n translations</h2>
  <ul>
    <li>morning=<%= t(:morning) %></li>
    <li>afternoon=<%= t(:afternoon) %></li>
    <li>night=<%= t(:night) %></li>
  </ul>
<% end %>

rails server を実行してブラウザで表示する

rails server を実行して、 http://localhost:3000/i18n_fallbacks/index にアクセスします。
ログに DEPRECATION WARNING が表示されます。

DEPRECATION WARNING: Using I18n fallbacks with an empty `defaults` sets the defaults to include
the `default_locale`. This behavior will change in Rails 6.1. If you desire the default locale 
to be included in the defaults, please explicitly configure it with `config.i18n.fallbacks.defaults 
= [I18n.default_locale]` or `config.i18n.fallbacks = [I18n.default_locale, {...}]`. If you want 
to opt-in to the new behavior, use `config.i18n.fallbacks.defaults = [nil, {...}]`. 
(called from <main> at /app/config/environment.rb:5)

ブラウザの表示はこんな感じになります
2019-05-24-083631_383x299_scrot.png

fallbacks の設定を変更する

fallbacks の設定を変更してみます。明示的に I18n.default_locale を追加します。

config/environments/development.rb
Rails.application.configure do
  ...
  config.i18n.fallbacks = [I18n.default_locale, {de: :ja}]
end

再度 rails server を起動し直して、ブラウザでページを表示すると今度は、 DEPRECATION WARNING が表示されません。
ブラウザの表示内容は変わりません。
DEPRECATION WARNING の意味するところは、「 I18n.default_locale を設定していなくても、Rails 6.0 では、 Rails内部で I18n.default_locale を追加するけど、Rails6.1 では挙動が変わるので、明示的に I18n.default_locale を追加するようにしてね」ということみたいです。

fallbacks の設定を再度変更する

DEPRECATION WARNING の最後に

If you want to opt-in to the new behavior, use `config.i18n.fallbacks.defaults = [nil, {...}]

とありますので、恐らくこれが、Rails 6.1 での挙動になると思われます。設定を変更して試してみます。

config/environments/development.rb
Rails.application.configure do
  ...
  config.i18n.fallbacks = [nil, {de: :ja}]
end

ブラウザの表示内容が以下のように変わります。
en (I18n.default_locale) が fallback のリストから消えています。
また、 morningGoog Morning に変換されていません。
2019-05-24-090101_369x290_scrot.png

fallbacks の設定を再度変更する

自動生成された config/environments/production.rb では、

config/environments/production.rb
  config.i18n.fallbacks = true

となっているので、 fallbacks が true のときにどうなるのか確認します。

このときは、 I18n.default_locale が fallbacks に含まれています。
2019-05-24-090406_349x282_scrot.png

まとめ

Rails 6.0 で DEPRECATION WARNING が出た場合は、 I18n.default_locale を設定するのが良さそうです。

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try023_i18n_fallbacks

参考情報

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

「Rails」タグについて(タグシノニム: 「RubyOnRails」「Ruby-Rails」「rails」「ROR」「#rubyonrails」)

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

「Rails」タグについて(タグシノニム: 「RubyOnRails」「Ruby-Rails」「rails」「ROR」)

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

Rails コマンドの実行の流れをたどる旅

Rails コマンド群がどのように呼び出されているのか気になったので、ソースコードを追いながらその流れを整理してみました。うーんとっても長い。

とっても長いので、ざっくり要約を載せておきます。

  • Rails コマンドはそれぞれコマンド名に対応したファイルを実行する
    • 例) rails generate → /rails/railties/lib/rails/commands/generate/generate_command.rb

もう、これだけ!後はホントに沼なので書く気にならん!w

さて、ここから先は自分の頭の中の思考をダダ漏らしにした感じでお送りするので、暇な方で覗いてみてください。

まずは rails コマンドの実行パスを確認します。こいつが何をしてるかっていう話ですよね。

$ which rails
=> /usr/local/bundle/bin/rails

実行パスが分かったので cat /usr/local/bundle/bin/rails を実行して中身を確認します。

/usr/local/bundle/bin/rails
#!/usr/bin/env ruby
#
# This file was generated by RubyGems.
#
# The application 'railties' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0.a"

str = ARGV.first
if str
  str = str.b[/\A_(.*)_\z/, 1]
  if str and Gem::Version.correct?(str)
    version = str
    ARGV.shift
  end
end

if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('railties', 'rails', version)
else
gem "railties", version
load Gem.bin_path("railties", "rails", version)
end
  • Gem.activate_bin_path('railties', 'rails', version)
  • Gem.bin_path("railties", "rails", version)

このファイルでは上記のどちらかを実行して終わってるんですけど、どちらも同じ値 "/usr/local/bundle/gems/railties-5.2.3/exe/rails" を返してます。

Gem.activate_bin_path('railties', 'rails', '>= 0.a')
# => "/usr/local/bundle/gems/railties-5.2.3/exe/rails"
Gem.bin_path('railties', 'rails', '>= 0.a')
# => "/usr/local/bundle/gems/railties-5.2.3/exe/rails"

ということなので、このパスでロードして読み込まれるコードの中身を見てみます。

/railties/exe/rails
#!/usr/bin/env ruby
# frozen_string_literal: true

git_path = File.expand_path("../../.git", __dir__)

if File.exist?(git_path)
  railties_path = File.expand_path("../lib", __dir__)
  $:.unshift(railties_path)
end
require "rails/cli"
/railties/lib/rails/cli.rb
# frozen_string_literal: true

require "rails/app_loader"

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppLoader.exec_app

require "rails/ruby_version_check"
Signal.trap("INT") { puts; exit(1) }

require "rails/command"

if ARGV.first == "plugin"
  ARGV.shift
  Rails::Command.invoke :plugin, ARGV
else
  Rails::Command.invoke :application, ARGV
end

ここでコメントアウトに

If we are inside a Rails application this method performs an exec and thus
the rest of this script is not run.

と書いてあるので、 Rails アプリケーション内から読み出した場合は、 Rails::AppLoader.exec_app 以降のコードは実行されないようです。ので、この #exec_app メソッドの中身を見てみます。

/railties/lib/rails/app_loader.rb
# frozen_string_literal: true

require "pathname"
require "rails/version"

module Rails
  module AppLoader # :nodoc:
    extend self

    RUBY = Gem.ruby
    EXECUTABLES = ["bin/rails", "script/rails"]
    BUNDLER_WARNING = <<EOS
…(長いので省略)…
EOS

    def exec_app
      original_cwd = Dir.pwd

      loop do
        if exe = find_executable
          contents = File.read(exe)

          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV
            break # non reachable, hack to be able to stub exec in the test suite
          elsif exe.end_with?("bin/rails") && contents.include?("This file was generated by Bundler")
            $stderr.puts(BUNDLER_WARNING)
            Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
            require File.expand_path("../boot", APP_PATH)
            require "rails/commands"
            break
          end
        end

        # If we exhaust the search there is no executable, this could be a
        # call to generate a new application, so restore the original cwd.
        Dir.chdir(original_cwd) && return if Pathname.new(Dir.pwd).root?

        # Otherwise keep moving upwards in search of an executable.
        Dir.chdir("..")
      end
    end

    def find_executable
      EXECUTABLES.find { |exe| File.file?(exe) }
    end
  end
end

ここちょっとビックリしたんですけど、 extend self を書いとくと自身に特異メソッドを生やすことできるっぽい。だから Rails::AppLoader.exec_app を直接実行できるのね。へー…。

ここでは、 EXECUTABLES = ["bin/rails", "script/rails"] このどちらかのパスが存在したらそれを実行というロジックになってます。 bin/rails の中身を見てみます。

/path/to/work_dir/bin/rails
#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

みたところ、 APP_PATHENGINE_PATH がなかった場合でも定数 APP_PATH の初期化と config/application.rbconfig/boot.rb の読み込み、そして最終的に rails/commands を読み込んでいるっぽい。

/railties/lib/rails/app_loader.rb
Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
require File.expand_path("../boot", APP_PATH)
require "rails/commands"

ということで、 rails/commands の中身を見てみます。

/railties/lib/rails/commands.rb
# frozen_string_literal: true

require "rails/command"

aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner",
  "t"  => "test"
}

command = ARGV.shift
command = aliases[command] || command

Rails::Command.invoke command, ARGV

うおお、ここで Rails コマンドのエイリアスが出てきたぞ…。ここで ARGV に含まれていた値を取り出して、それを引数に Rails::Command.invoke command, ARGV を実行してます。

ここで言う ARGV は「 Ruby スクリプトに与えられた引数を表す配列」とのことで、実行ファイル名より後の値を配列でファイル内に渡してます。
https://docs.ruby-lang.org/ja/2.6.0/method/Object/c/ARGV.html

たとえば、

$ rails generate model

の実行時の引数 ARGV は以下のように保存されます。

['generate', 'model'] 

さて、 Rails::Command.invoke command, ARGV の中身を見てみます。
たとえば、 rails generate model を実行する場合、

full_namespace
# => 'generate'
args
# => ['model']

がそれぞれ代入されているはずです。
なお、これ以上の処理は、すべて以下のコマンドを実行する場合を前提とします。

$ rails generate model Foo foo:string

よし、 #invoke メソッドの中身見ていきます。

/railties/lib/rails/command.rb
def invoke(full_namespace, args = [], **config)
  namespace = full_namespace = full_namespace.to_s

  if char = namespace =~ /:(\w+)$/
    command_name, namespace = $1, namespace.slice(0, char)
  else
    command_name = namespace
  end

  command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
  command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)

  command = find_by_namespace(namespace, command_name)
  if command && command.all_commands[command_name]
    command.perform(command_name, args, config)
  else
    find_by_namespace("rake").perform(full_namespace, args, config)
  end
end

前段にいろいろとコマンド名の処理がありますが、最終的には command.perform(command_name, args, config) の中身が分かればいいので、まずは変数 command に代入している #find_by_namespace メソッドを見ます。

/railties/lib/rails/command.rb
def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end

冒頭 3 行の処理で変数 lookups には以下の値が代入され、 #lookup メソッドに渡されます。

lookups
# => ["generate", "generate:generate", "rails:generate", "rails:generate:generate"]

それでは #lookup メソッドの中身を見てみます。どうやらここでは namespaces を元に各コマンドの処理が入ってるファイルを require してるようです。なるほど、メソッド名通りの処理だ。

/railties/lib/rails/command/behavior.rb
def lookup(namespaces)
  paths = namespaces_to_paths(namespaces)

  paths.each do |raw_path|
    lookup_paths.each do |base|
      path = "#{base}/#{raw_path}_#{command_type}"

      begin
        require path
        return
      rescue LoadError => e
        raise unless e.message =~ /#{Regexp.escape(path)}$/
      rescue Exception => e
        warn "[WARNING] Could not load #{command_type} #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
      end
    end
  end
end

ここでは、 #namespaces_to_paths / #lookup_paths / #command_type 3 つのプライベートメソッドが呼ばれています。各メソッドの中身は単純なので割愛しますが、それぞれ以下の値を返します。

namespaces_to_paths
# => ["generate/generate", "generate", "generate/generate/generate", "rails/generate/generate", "rails/generate", "rails/generate/generate/generate"]
lookup_paths
# => ["rails/commands", "commands"]
command_type
# => "command"

ほんで、 require が true を返すのは "rails/commands/generate/generate_command" の時ですね。なんなんだろう、すごいトリッキーなことやってる気がするw

そうこうして #find_by_namespace メソッドに戻ってきました。

/railties/lib/rails/command.rb
def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end

さて、次に実行される #subclasses メソッドが謎を呼ぶんですが、中身はこうなっています。

def subclasses
  @subclasses ||= []
end

これ pry でデバッグ中に試しに実行してみると分かるんですが、なんともう値 [Rails::Command::GenerateCommand] が入ってます。い、いつ代入されたの…??????って話なんですが、実は require path の時点で代入されていました。

そもそも xxxx_command.rb の中身はざっくり以下のような構成になっており、必ず Rails::Command::Base クラスを継承するようになっています。

module Rails
  module Command
    class HogehogeCommand < Base
    end
  end
end

この < Base のタイミングで Rails::Command::Base.inherited が実行されます。メソッドの中身を見ると分かりますが、 @subclassesbase が追加されてるのが分かるかと思います。

module Rails
  module Command
    class Base < Thor
      class << self
        
        def inherited(base) #:nodoc:
          super
          if base.name && base.name !~ /Base$/
            Rails::Command.subclasses << base
          end
        end
     …
      end
    end
  end
end

ソースコードリーディングっていろんなファイルを飛び回るからアタマ混乱するなあ…。
さて、その @subclasses に #index_by をかけて Hash 化します。

/railties/lib/rails/command.rb
def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end

ここで最後の処理に使う namespaceslookups の値を確認しておきましょう。現時点でこんな感じになっています。

namespaces
# => {"rails:generate"=>Rails::Command::GenerateCommand}
lookups
# => ["generate", "generate:generate", "rails:generate", "rails:generate:generate"]

なので、最後の行の返り値はこんな感じになります。

namespaces[(lookups & namespaces.keys).first]
# => Rails::Command::GenerateCommand

やっと #find_by_namespace(namespace, command_name) の返り値がわかったので、 #invoke メソッドに戻ります。今まで見てきたのは変数 command にどんな値が代入されるかということでした。上記で見た通り、 Rails::Command::GenerateCommand が代入されることが分かりました。

/railties/lib/rails/command.rb
def invoke(full_namespace, args = [], **config)
  namespace = full_namespace = full_namespace.to_s

  if char = namespace =~ /:(\w+)$/
    command_name, namespace = $1, namespace.slice(0, char)
  else
    command_name = namespace
  end

  command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
  command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)

  command = find_by_namespace(namespace, command_name)
  if command && command.all_commands[command_name]
    command.perform(command_name, args, config)
  else
    find_by_namespace("rake").perform(full_namespace, args, config)
  end
end

仮に command が nil だった場合は Rails::Command::RakeCommand を元に rake コマンドが走るようですが一旦それは置いておきます。 command.perform(command_name, args, config) の中身を見ていきます。なお引数の値はこんな感じになっています。

command_name
# => "generate"
args
# => ["model"]
config
# => {}
/railties/lib/rails/command/base.rb
def perform(command, args, config) # :nodoc:
  if Rails::Command::HELP_MAPPINGS.include?(args.first)
    command, args = "help", []
  end

  dispatch(command, args.dup, nil, config)
end

ここの #dispatch は Rails ではなく Thor のメソッドです。Thor 内部の動きについてはまた色々ありますが、ここでは割愛します。なんだかんだあった後に Rails::Command::GenerateCommand#perform メソッドが呼び出されます。

/railties/lib/rails/commands/generate/generate_command.rb
def perform(*)
  generator = args.shift
  return help unless generator

  require_application_and_environment!
  load_generators

  ARGV.shift

  Rails::Generators.invoke generator, args, behavior: :invoke, destination_root: Rails::Command.root
end

さて、ここから沼に入っていきます。しばらく自分がどこにいるか分からなくなります。ちょっと最後の方まで書いてたんですが説明するのがめんどくさくなったので、興味のある方は覗いてみてください。 Rails って大きいなあ(小並)ってのがひしひしと分かります。

要するに Rails コマンドとそれを実行するファイル名は対応していて、 rails generate なら generate_command.rb、 rails console なら console_command.rb を見に行けばなんとなく中で何をやっているかが分かる、というしくみです。
いつもやっているソースコードリーディングの思考をそのまま書いたみたいな感じでまとまりないのは申し訳ないですが、備忘録程度に書いてるので、まあそんなもんだと思ってください。

Thor について(おまけ)

なお、各コマンドのオプションについては、 class_option で大半を定義しています。これは Thor の機能で http://whatisthor.com/#class-options 、Thor クラスを継承したクラスにこのメソッドを書くと、クラス全体で定義しておきたいオプションを設定することができる、というものです。

たとえば、

sample.rb
#!/usr/bin/env ruby
require 'thor'

class Nya < Thor
  class_option :nyanchu, type: :boolean, default: false

  desc 'hello NAME', 'say hello to NAME'
  def hello(name)
    if options[:nyanchu]
      puts "#{name}, nyanchu~!"
    else
      puts "#{name}, nya~!"
    end
  end
end

Nya.start(ARGV)

としておくと、勝手に [--nyanchu], [--no-nyanchu] オプションをつけてくれるようになります。コマンド名を指定せずにファイルを実行すると、よく見かける README を出力してくれます。なにこれ便利。

root@10161f5ac926:/hoge# ./bin/sample
Commands:
  sample hello NAME      # say hello to NAME
  sample help [COMMAND]  # Describe available commands or one specific command

Options:
  [--nyanchu], [--no-nyanchu]  

なので、このオプションにしたがって以下のように実行してみると、ちゃんと引数を解釈してくれます。

root@10161f5ac926:/hoge# ./bin/sample hello Waku --nyanchu
Waku, nyanchu~!

もっと詳しく Thor について知りたい方は公式ドキュメントか GitHub のリポジトリを見に行っても良いかもしれません。
http://whatisthor.com/
https://github.com/erikhuda/thor

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