- 投稿日:2019-05-24T22:07:32+09:00
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も追加する必要があります。
./Gemfilegem 'cocoon' gem 'jquery-rails'bundle-install$ bundle install2. Modelに、親子関係を設定します。
・親(question)には、「has_many :answers」を追加します。
・親が削除された時に、一緒に子も削除されるように、「dependent: :destroy」をつけます。
・accepts_nested_attributes_forを使うと、親と子を一緒にcreate/update出来るようにします。
・削除できるよう「allow_destroy: true」をつけます。app/models/question.rbhas_many :answers, dependent: :destroy accepts_nested_attributes_for :answers, allow_destroy: true子(models/answer.rb)は、作成時にreferencesとしているため、自動で設定されています。
app/models/answers.rbbelongs_to :question3. Viewに、親子同時に登録・更新出来るように設定します。
application.jsに以下を追加します。
app/javascripts/application.js//= require jquery //= require cocoonviewを変更します。
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 "削除", f4. コントローラーで登録可能にします。
以下のように子のパラメータを受け取れるようにしましょう。
app/controllers/questions_controller.rbdef question_params params.require(:question).permit(:category1, :category2, :category3, :msg, answers_attributes: [:id, :msg, :correct, :_destroy]) end5. 確認します。
「追加」ボタンを押すたびに、子供の入力エリアが追加されます。
今回はここまで
ありがとうございました!
次回は、参照画面などに、子供の情報を追加していこう。
- 投稿日:2019-05-24T21:58:05+09:00
[Ruby][Git] git log -L で特定の関数 (メソッド) の変更履歴が見られるらしいので Ruby でも試してみた
概要
次のツイートを見て便利そうだと思った。
これは本当にとても便利なgitのログ機能なんですが、
— ほぼろ (@rhoboro) May 22, 2019git log -L :関数名:ファイル名
でその関数の履歴が見れます pic.twitter.com/wcul1OYr0Wどうやら
git log -L :<関数名>:<ファイルパス>という形式で特定の関数の変更履歴が見られるらしい。
このツイートでは Python の関数の変更履歴を表示していたが、Ruby のメソッドでもできるか試してみた。例
試しに Active Support コア拡張機能の String#truncate の変更履歴を確認してみる。ローカルに Rails のリポジトリを git clone して、次の git コマンドを実行する。
git log -L :truncate:activesupport/lib/active_support/core_ext/string/filters.rb本当に truncate のログと差分だけ表示された!メソッドの変更を追跡するのにファイルごとだと粒度が大きいので、メソッドごとに表示できるのは便利だ ☺️
- 投稿日:2019-05-24T18:25:53+09:00
rubocop について
rubocop とは
コーディング規約に準拠してるかチェックするgemです!
簡単に言いますと、インデントやメソッド名、改行などのチェックをしてくれるものです。
今回は、このrubocopさんについてを会社のプロダクトにいれた時(後入れ)のやり方にも交えて書きます。流れ
$gem install rubocop
or Gemfile にかいて$bundle install
$rubocop
で実行(たぶんめちゃめちゃ怒られます。。。)- 怒られた箇所修正!!!
.rubocop.yml を作成!
プロジェクト直下に「.rubocop.yml」を作成!!
rubocop.ymlRails: 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.ymlinherit_from: .rubocop.ymlこれを記述して、両方のファイルが参照されるようにしておく!
or
.rucocop_todo.yml
の内容を.rubocop.yml
に全コピー
copを一個コメントアウト(削除)
$rubocop
何で怒られているかチェック
-> 直したくない(これからもこのcop に対しては無視したい)->3A
-> 直さないといけない->3B3A.
例: AsciiCommentsに関しては、全ページで許可したい!rubocop.yml# cop でどのようなことが怒られたかとか、どうしたいかを明記しておいたほうが良さそう AsciiComments: Enabled: falseを追記
もしくは、rubocop.ymlAsciiComments: Exclude: - 'app/models/hoge.rb'
3B.
$rubocop -a
自動で修正してくれる(これがめちゃ便利)単純にコードミスや可読性の高いコードにも直してくれる!
注:メソッドの名前などは直してくれない!!ちなみに、私は
$rubocop
回した段階で何で怒られているのかがわからないやつは、とりあえず$rubocop -a
を叩きました。
それで、diff でみてどう変更されたかを確認してました!!流れに関してはこんなもんです!ひたすら、怒られているところの確認-> 修正 or 「cop」 無視
.rubocop.ymlの書き方について
rubocopの対象から除外するファイル指定
rubocop.ymlAllCops: Exclude: - db/schema.rb - 'vendor/**/*'copの無効化・有効化
rubocop.ymlRails: Enabled: truerubocop.ymlLambda: Enabled: false基本的には、cop は有効化されているので
Enabled: true
はそこまで使わないかも
Enabled: false
はcop 自体を無視したいときに使います!warning のみを検知
rubocop.ymlBundler/OrderedGems: Severity: warningwarning レベルから取得
自動化
rubocop を最大活用するためには、自動化するしかないと思いまして、CircleCIと連携することにしました!
CircleCIに関しては、ここでは飛ばします笑circleci/config.ymljobs: 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
- 投稿日:2019-05-24T17:44:32+09:00
よく見かけるattr_accessorについて
はじめてのqiita投稿ですが、がんばりたいと思います。
1.ゲッターとセッターについて
railsを学び始めて早4ヶ月。
ただrailsのコードを書くだけではなく、rubyの基礎を学ぼうと思いました。
そこで気になったのがattr_accessor、これはよく見かけるがなんだろうと。さて本題。
ゲッターくんとセッターくんの役割を実際のコードで見てみようと思います。
class Book def book=(book) @book = book end def book @book end end1つ目のメソッドは引数で受け取ったデータをインスタンス変数に代入します。
このインスタンス変数を代入するためのメソッドのことを「セッター」と呼びます。2つ目のメソッドではbookメソッドの中身を変更して、設定した名前を返しています。
このインスタンス変数の内容を参照するためのメソッドを「ゲッター」と呼びます。2.attr_accessorについて
先程のゲッターとセッターを毎回毎回設定するのは大変ですよね
しかしrubyには便利なメソッドがあります。
それがattr_accessorという訳です。class Book attr_accessor :book endというようにすっきり書けたのでは無いでしょうか。
attr_accessorメソッドを用いると、シンボルで :book と書けるようになりました。3.さいごに
ちなみに、ゲッターのみの場合はattr_reader,セッターのみの場合はattr_writerと書けます。
という風に、普段何気なく使っているメソッドでもこれだけの仕事をしていたとは驚きですね。
これからも気になったら、どんどんqiitaでアウトプットしようと思います。
- 投稿日:2019-05-24T17:03:29+09:00
Railsコマンド
はじめに
備忘録としてまとめていきます。
随時追記していきます。Railsプロジェクトを作成
$ rails new [プロジェクト名] -d [データベース名]データベース作成
$ rails db:createサーバー起動
$ rails serverコントローラー作成
$ rails generate controller [コントローラー名(複数形)][アクション名・アクション名..][オプション名]モデル作成
$ rails generate model [モデル名(単数形)][属性名:データ型][属性名:データ型]マイグレーション
$ rails db:migrateテスト検証
$ rails testintegration作成
$ rails g integration_test [テストファイル名]統合テスト検証
$ rails test integrationindexを追加
$ rails g migration add_index_to_[テーブル名]_[カラム名] $ rails db:migrate #最後にDBをマイグレートする。カラム追加
$ rails g migration add_[カラム名]_to_[テーブル名] [カラム名][データ型]DBリセット
$ rails db:migrate:reset
- 投稿日:2019-05-24T16:59:27+09:00
RailsとLivedoor Weather Web Serviceを使って各地点の天気を表示させてみた
はじめに
前回の記事で、Rubyから天気情報を取得することができました。
RubyでWeather Hacksを使って天気を取得してみた
この取得処理とRailsを使って、Web上に天気情報を表示します。環境
macOS Mojave(10.14.4)
Ruby 2.6.3
Rails 5.2.3取得処理
以下の順で各地点の天気情報を取得します。
- 表示させたい地点名を読み込む
- Livedoor Weather Web Serviceで提供されている都市と1を使って、地点のidを取り出す
- 各地点の天気情報を取得する
表示させたい地点名を読み込む
テレビの全国天気で、よく表示される各地点の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 endLivedoor 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 endURLから天気情報を持ったjsonファイルを取得します。
def read(url) response = URI.open(url) @parse_text = JSON.parse(response.read) endあとはjson内から天気情報を取得して終わりです。
表示結果
5/24は全国的に晴れのようですね。
今後
次にやっていきたいのは以下の3つですね。
- 最高気温、最低気温が取得できるようなので画面に追加
- 各地点をクリックすると、その地方の詳細な天気が見れるようにする
- ログイン機能を持たせ、ログインすると明日、明後日の天気も表示できるようにする
独学でRubyを書いているので、指摘事項ありましたらコメントに記載をお願いします。
- 投稿日:2019-05-24T16:06:04+09:00
Ruby on rails: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)となった時の対処法
- 投稿日:2019-05-24T15:42:56+09:00
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/rubyrbenvで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_profileRubyのバージョンをアップデートしたものの、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
- 投稿日:2019-05-24T15:31:13+09:00
google-drive-permission-searchを使うまでのエラー対処メモ
会社で使用しているGoogleドライブの各ファイルのオーナーをエクスポートしたく、
google-drive-permission-searchを使用してみることに。詳しい使い方はこちらインストール
こちらの記事を参考に、事前にGoogle API ConsoleからDriveAPIのOAuthクライアントIDを発行し、
ファイル名をclient_secrets.json
で保存しておく。言われた通りインストール手順を踏む。
$ git clone https://github.com/morimorihoge/google-drive-permission-search.git $ cd google-drive-permission-search $ cp ~/client_secrets.json ./ $ bundleエラー発生
errorFetching gem metadata from https://rubygems.org/........ Using i18n 0.6.11 Fetching json 1.8.1 Installing json 1.8.1 with native extensions Gem::Ext::BuildError: ERROR: Failed to build gem native extension. current directory: /Users/*****/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/json-1.8.1/ext/json/ext/generator /Users/*****/.rbenv/versions/2.6.2/bin/ruby -I /Users/*****/.rbenv/versions/2.6.2/lib/ruby/2.6.0 -r ./siteconf20190524-91989-tn4e6c.rb extconf.rb creating Makefile current directory: /Users/*****/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/json-1.8.1/ext/json/ext/generator make "DESTDIR=" clean current directory: /Users/*****/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/json-1.8.1/ext/json/ext/generator make "DESTDIR=" compiling generator.c In file included from generator.c:1: ./../fbuffer/fbuffer.h:175:47: error: too few arguments provided to function-like macro invocation VALUE result = rb_str_new(FBUFFER_PAIR(fb)); ^ /Users/*****/.rbenv/versions/2.6.2/include/ruby-2.6.0/ruby/intern.h:814:9: note: macro 'rb_str_new' defined here #define rb_str_new(str, len) RB_GNUC_EXTENSION_BLOCK( \ ^ In file included from generator.c:1: ./../fbuffer/fbuffer.h:175:11: warning: incompatible pointer to integer conversion initializing 'VALUE' (aka 'unsigned long') with an expression of type 'VALUE (const char *, long)' (aka 'unsigned long (const char *, long)') [-Wint-conversion] VALUE result = rb_str_new(FBUFFER_PAIR(fb)); ^ ~~~~~~~~~~ generator.c:840:25: error: use of undeclared identifier 'rb_cFixnum' } else if (klass == rb_cFixnum) { ^ generator.c:842:25: error: use of undeclared identifier 'rb_cBignum' } else if (klass == rb_cBignum) { ^ 1 warning and 3 errors generated. make: *** [generator.o] Error 1 make failed, exit code 2 Gem files will remain installed in /Users/*****/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/json-1.8.1 for inspection. Results logged to /Users/*****/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/extensions/x86_64-darwin-17/2.6.0-static/json-1.8.1/gem_make.out An error occurred while installing json (1.8.1), and Bundler cannot continue. Make sure that `gem install json -v '1.8.1' --source 'https://rubygems.org/'` succeeds before bundling. In Gemfile: activesupport was resolved to 4.1.6, which depends on json以下のログに注目
errorInstalling json 1.8.1 with native extensions Gem::Ext::BuildError: ERROR: Failed to build gem native extension. ~~ In Gemfile: activesupport was resolved to 4.1.6, which depends on json
activesupport
とjson
の依存関係について言われているので、bundle update
で解消する。$ bundle update
再度インストール
$ bundle
エラーも出ず、インストールが成功した。
いざ使用
書き出し実行!
$ bundle exec ./google-drive-permission-search.rb --verbose --type excelまたエラー発生
errorbundler: failed to load command: ./google-drive-permission-search.rb (./google-drive-permission-search.rb) LoadError: cannot load such file -- google/api_client /Users/~~/google-drive-permission-search/google-drive-permission-search.rb:4:in `require' /Users/~~/google-drive-permission-search/google-drive-permission-search.rb:4:in `<top (required)>'LoadError: cannot load such file -- google/api_client
google/api_client
のバージョンがいけないようなので使用できた報告のある0.7に指定する。Gemfilegem 'google-api-client', '0.7'インストール
$ bundle
再度実行するもまたエラー
errorbundler: failed to load command: ./google-drive-permission-search.rb (./google-drive-permission-search.rb) LoadError: cannot load such file -- retriableLoadError: cannot load such file -- retriable
retriableが入っていなかったのでインストール。
こちらも使えたと報告のあるバージョン1.4.1に指定してインストールGemfilegem 'retriable', '1.4.1'参考:
https://github.com/googleapis/google-api-ruby-client/issues/132
https://github.com/googleapis/google-api-ruby-client/issues/107もう一度実行するとGoogleのOAuth確認画面がブラウザで表示された。
しかし惜しいところでエラー発生
errorArgumentError: header User-Agent has field value "google-drive-permission-search/1.0.0 google-api-ruby-client/0.7.0 Mac OS X/10.13.6\n (gzip)", this cannot include CR/LFrubyのバージョンが
2.4.3
だと動くと書いてあったのでバージョンを下げてみる(今まで2.6.2)$ rbenv install 2.4.3 $ rbenv local 2.4.3 $ ruby -v # 2.4.3
$ bundle
を実行しようとしたらエラーrbenv: bundle: command not found The `bundle' command exists in these Ruby versions: 2.6.2下記を実行すればいいらしい。
$ rbenv exec gem install bundle参考:
https://bugs.ruby-lang.org/issues/14664
https://qiita.com/_am_/items/c1dbeb11f40bbbac8fd9そろそろいってくれー!
$ bundle exec ./google-drive-permission-search.rb --verbose --type excelログが出てきた!!成功!!
- 投稿日:2019-05-24T14:43:27+09:00
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
- 投稿日:2019-05-24T14:27:57+09:00
[Ruby] fizzbuzzの複数コード紹介
概要
fizzbuzz問題はいろんな解法があるな、と思ったので少しまとめ。
今回、3つのパターンを紹介しますが、結果はどれも同じになります。パターン1
i = 1 while i <= 30 if i % 15 == 0 puts "FizzBuzz" elsif i % 3 == 0 puts "Fizz" elsif i % 5 == 0 puts "Buzz" else puts i end i += 1 end素直につくったらこうなりますよね。
数字を配列にせず、1を変数に代入したあと、繰り返しのなかで自己代入演算子を用いてひとつずつ数字を増やしていくパターンです。パターン2
num = 1 while num <= 30 str = "" if num % 3 == 0 str = str + "fizz" end if num % 5 == 0 str = str + "buzz" end if str == "" str = str + num.to_s end puts str num += 1 endこちらはelsifを使わないスタイルです。
空の変数を宣言し、各条件に応じてその変数に文字や数値を付け足しています。パターン3
(1..30).each do |i| number = "" number = "fizz" if i % 3 == 0 number << "buzz" if i % 5 == 0 number = i unless i % 3 == 0 || i % 5 == 0 puts number endこちらは配列になっているので、eachで繰り返しを行なっています。
それ以外はパターン2に似ていますが、文字列と数値をくっつけることを避けているため、to_sメソッドを使わずに済みました。ただ、unlessはあまり使いたくないですね。
まとめ
私ごとですが、はじめパターン1の方法でfizzbuzz問題を解き、その模範回答がパターン2だったので、それを改良した(つもり)パターン3をつくりました。
もっとおすすめのfizzbuzzコードがあれば教えていただきたいです。
追加
コメントより教えていただいたパターンを追加します。
puts (1..30).map {|n| case [n % 3, n % 5] in [0, 0] then 'FizzBuzz' in [0, _] then 'Fizz' in [_, 0] then 'Buzz' else n end }.join "\n"ruby 2.7にするとパターンマッチングがつかえるようになりました。
- 投稿日:2019-05-24T14:13:51+09:00
js.erbファイルにコメントを書くときは//を使わない
修正前
index.js.erb// コメントが見えてるよ! $("#ajax_panel").html("<%= escape_javascript(render partial: "ajax_panel") %>");結果
ツールを使ってファイルを見ると...
(画像はChromeのデベロッパーツールのNetworkパネルから確認)
コメントに重要な内容が含まれていると、敵の攻撃を手助けしてしまう可能性があり危険です。
修正
erbファイルのコメントの書き方に修正します。
index.js.erb<%# コメントが見えないだと... %> $("#ajax_panel").html("<%= escape_javascript(render partial: "ajax_panel") %>");修正後
- 投稿日:2019-05-24T14:00:19+09:00
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:createHerokuへデプロイ
$ heroku create $ git push heroku master
- 投稿日:2019-05-24T13:53:15+09:00
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)
block in exit_hook'
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
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:instop'
ensure 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
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:instop_process'
process_exited?'
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
1: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/childprocess-1.0.1/lib/childprocess/unix/process.rb:31:inexited?'
assert_started': process not started (ChildProcess::Error)
/home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/childprocess-1.0.1/lib/childprocess/abstract_process.rb:188:inこのエラーメッセージ等でググっていたのですが、解決方法が見当たらず…。時々失敗するけれど時々成功するため、ときどき調査しようとしては諦めていました。
他のエラーメッセージに気づく
弊社の他のプロジェクトでも同様の症状が出ており、同僚が調査していたのですが、そのときに見つけたメッセージがこれ。
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.ymlsteps: # 略。ただし、DB作成後でないとrails runnerが失敗するので注意。 - run: name: Update chromedriver command: env RAILS_ENV=test bin/rails runner "Webdrivers::Chromedriver.update" # 略。テストを実行結果
10回連続で同じテストを実行しましたが、全部成功しました
もし並列テストが不安定だ〜という方はこれを追加してみましょう!
- 投稿日:2019-05-24T13:47:08+09:00
RubyのString#%について
はじめに
以下のようなRubyのコードを見かけました。
sample.rbnames = ['Jon', 'Bob', 'Alice'] puts "Their names are %s, %s and %s" % names #=> Their names are Jon, Bob and Alice文字列中に含まれている%sやその後の%namesが何を意味しているのか分からなかったため、調べてみました。
String#%
リファレンスを確認したところ、Stringクラスのインスタンスメソッドで%がありました。
https://docs.ruby-lang.org/ja/2.5.0/method/String/i/=25.htmlself % args -> String
printf と同じ規則に従って args をフォーマットします。
args が配列であれば Kernel.#sprintf(self, *args) と同じです。 それ以外の場合は Kernel.#sprintf(self, args) と同じです。引数argsを変換してくれるようです。
文字列 % argsというのが最初に見たコードの"Their names are %s, %s and %s" % namesの部分と一致してそうですが、イマイチよく分かりません。
printfと同じ規則に従ってとは?
printfって何だっけ?
ということで調べてみました。printfとは
こちらもリファレンスを確認すると、Karnelモジュールのモジュール関数に定義されていました。
https://docs.ruby-lang.org/ja/2.5.0/method/Kernel/m/printf.htmlC 言語の printf と同じように、format に従い引数を文字列に変換してportに出力します。
どうやらprintfは元々C言語の関数で、ある規則に従って引数を変換し、文字列として出力してくれるようです。C言語は全く分からないのでスルーします。
つまり、String#%はprintfで使われている規則に従って引数のargsを変換してくれるメソッドのようです。
では、printfで使われている規則とは何か?
sprintf フォーマットとは
String#%のリファレンスに規則について記載されていました。
Rubyの場合はC言語のものと少し違うが、ほとんど一緒のようです。
このsprintfフォーマットの中で指示子という項目があり、その中に文字列を表す指示子としてsがありました。指示子は引数の型の解釈を示します。指示子を省略することはできません。 指示子には大きく分けて
・文字列を表す指示子: c, s, p
・整数を表す指示子: d, i, u, b, B, o, x, X,
・浮動小数点数を表す指示子: f, g, e, E, G
がありますこちらのサイトが分かりやすかったです。
http://www9.plala.or.jp/sgwr-t/c/sec05.htmlまとめ
以上の知識を得てレベルアップしたため、最初に書いたコードを再度見てみると、以下の事が分かりました。
names = ['Jon', 'Bob', 'Alice'] "Their names are %s, %s and %s" % names
- 二行目でString#%メソッドが使用されて、 self % args の形になっている。
- 一行目で定義された配列 names が%メソッドの引数(args)になっている。
- self(文字列)の中で文字列を表す指示子sが使用されている。
したがって、%メソッドの引数に指定された配列namesの各要素が、文字列中の%s(指示子)により、文字列として表示されているようです。(間違っていたらすいません)
おまけ
今回の例のように配列を引数にする場合、配列の要素数に対して文字列中の指示子の数が多いと、引数が不足しているというエラーになりました。
names = ['Jon', 'Bob', 'Alice'] puts "Their names are %s, %s, %s and %s" % names # 配列の要素数は3に対して、%sを4つ使用しています。 #=> too few arguments (ArgumentError)逆に、配列の要素数が多い場合は余剰分が無視されます。
names = ['Jon', 'Bob', 'Alice'] puts "Their names are %s and %s" % names # 配列の要素数は3に対して、%sを2つ使用しています。 #=> Their names are Jon and Bob #余計な存在の'Alice'は無視されます。
- 投稿日:2019-05-24T13:25:13+09:00
RubyのJITコンパイラを理解したい!rubykaigiの香りを添えて
まえがき (ポエムなので読み飛ばせます)
「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%A9RubyのJITは
MJIT
という名前がよく出ていますが、しくみを理解する上ではRubyのJITの実装周りの総称として覚えておいて良さそうです。
参考:https://k0kubun.hatenablog.com/entry/ruby26-jitこれまでとの処理の違い
Ruby1.9~Ruby2.5 ,デフォルト設定のRuby2.6での実行
Rubyはインタープリタ型言語なので、コンパイルして直接機械語に変換されるわけではありません。どうやって実行されているというと、Rubyのコードは字句・構文解析を経て
YARVバイトコード
というものに変換されます。バイトコードはプログラム言語と機械語との中間にあたるようなコードです。でも
YARVバイトコード
はあくまでバイトコードであって機械語ではないので、CPUはこのYARVバイトコード
を直接解釈して実行することはできません。
そこで、CPUに変わってYARVバイトコード
を解釈し、CPUに命令を発行してくれるのがバーチャルマシン(VM)であるYARV
です。こうしてRubyのコードは、明示的な機械語へのコンパイルを必要とせずに、実行することができます。
MJITを有効にしたRuby2.6
(これらの理解の拠り所として Cコンパイラを利用したRubyのJITコンパイラ を参考にさせていただきました。)
YARVバイトコード
が生成されるところまではこれまでと変わりません。
そして、バーチャルマシンであるYARV
もこれまで通り登場しますが、JITコンパイラー
という役者が増えています。あるプログラムが実行されたとき、
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
- 投稿日:2019-05-24T12:37:17+09:00
devise で基本的なアカウント機能のみを実装してみた
はじめに
アカウント機能を容易に実装することができる gem である devise の基本的な使用方法を
数回に分けて投稿します。devise gem には様々な機能がありますが、
今回はあえて基本的なアカウント機能のみを実装してみようと思います。動作対象
・Ruby 2.5
・Rails 5.1
・SQLite3今回実装する機能
・アカウント作成機能
・ログイン及びログアウト機能
・アカウント編集機能devise とは?
devise はログイン・ログアウト機能やアカウント作成機能などを簡単に実装できる gem です。
機能がモジュール化されているので、管理しやすいのが特徴です。
今回は2018/3/18にリリースされた4.4.3を使っていきます。Rails のインストール
まずは雛形を作成しましょう。
私はいつもディレクトリを真っ先に作成して、そのディレクトリに移動して、
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 ./ -BGemfile の対応について尋ねられると思いますが、もちろん
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.rbRails.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.rbRails.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.rbRails.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.rbclass User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable enddevise に実装されている各モジュールの機能をここに記載します。
devise modules 機能 database_authenticatable サイン時にパスワードを暗号化してDBに登録 registerable ユーザーが自身のアカウントの編集と削除を可能にする recoverable パスワードのリセットを可能にする rememberable Remember Me 機能を有効化する trackable サインインの回数やIPアドレスなどを記録 validatable メールとパスワードのバリデーションを行う confirmable メール認証機能を有効化 lockable 規定回数ログインに失敗したらアカウントをロックする timeoutable 一定時間でセッションを破棄する omniauthable Twitter や Facebook など、外部サービスのアカウントで認証を可能にする 今回は現時点で使用しないモジュールを全てコメントアウトします。
app/models/user.rbclass 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.rbclass 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.rbclass 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
- 投稿日:2019-05-24T12:05:25+09:00
歴史から知る「なぜ絵文字対応は面倒なのか」
はじめに
企画さんから、
「名前入力の際に絵文字を * でエスケープしてほしい(例:ハッピー→ハッピー*)」
という要望がきたので、それに対応するまでの流れをまとめたいと思います。(対応したとはとは言ってない)Unicodeに絵文字が導入されるまで
こういった一部の文字をエスケープしたい場合、for文でcharをチェックするなり正規表現で弾くなりすると思いますが、絵文字の場合はそう上手くはいきません。
これには Unicode が絵文字を表示する仕組みが関係します。
絵文字が導入されるまでを時系列で振り返りながら、仕組みを知っていきましょう。1990年代前半
制作当初、Unicode では1文字を2バイト固定で表現しようと計画されていました。
これによって、収録できる文字が最大65536文字になりますが、
- Shift_JIS などの経験から可変長文字コードが面倒なことがわかっていたこと
- 当時、中国・日本・韓国などの文字をまとめても20000個程度だったこと
などが理由で、問題ないだろうと判断されていました。
実際、Unicode 1.0 が策定された際は65536個以内に収まっていました。これ以降、2バイト固定長、のちに UTF-16 と呼ばれるルールはC#やJavaなどのプログラミング言語に採用されるようになります。(char を2バイトとして扱う)
1990年代後半
当初、2バイト(65536文字)もあれば十分だろうと判断されていた Unicode ですが、技術者だけでなく言語学者も参画してもらったところ、65536文字では足りないことが明らかになります。
そこで、2文字1組(4バイト)として扱うサロゲートペアというルールが定められました。
これによって、扱える文字は増えたわけですが、当初定めていた2バイト固定長が崩れる形となり、UTF-16 は1文字2バイトまたは4バイトの可変長となってしまいます。この結果、発生したのがUnicodeを使用したプロジェクトでのバグです。
当初、2バイトで1文字を扱っていたことで、サロゲートペアなどの4バイト文字がきた時に対処できなくなってしまいました。
しかし、サロゲートペアで表現する必要がある文字は一般人が知らないようなマイナー文字がほとんどだったため、特に対策されないプロジェクトが存在する形となりました。2010年代前半
この状況に一石を投じたのが絵文字でした。
もともと日本の携帯電話などで浸透していた絵文字は、その扱いやすさから Emoji として世界でも扱われるようになります。(大統領からも Emoji を生み出したことに感謝の意を示されたらしいです)
絵文字は4バイトで表現されますが、上記の通り世界各国で使用されているため、技術者はサロゲートペアに対応せざるを得なくなりました。
これによって、今まで対応されていなかったマイナー文字も対応される形となり、結果的にバグ対策がなされる形となっていきます。なぜ絵文字対応は面倒なのか
今回の記事のタイトルでもある、「なぜ絵文字対応は面倒なのか」
これは、絵文字は4バイトで表現されますが、上記の通り世界各国で使用されているため、技術者はサロゲートペアに対応せざるを得なくなりました。
が理由となっていることがわかりました。
プログラミング言語的に置き換えると、charが2バイトで扱われている場合、絵文字などの4バイトは正しく1文字と判定されなくなるなどの問題が発生します。なので、絵文字対応をするためには、この4バイト文字を正しく1文字扱いできればいいことがわかります。
幸い、IsSurrogate
などのサロゲートペアを検知する関数が各言語に存在するかと思われるので、そちらで判定すれば、上記の問題は解決します。これで絵文字対応は完了...ということにはなりませんでした。
これには絵文字が導入されてからの歴史が関係します。絵文字が導入されてから
もともとは日本の携帯電話だけにあったものが、iPhone の登場を筆頭に絵文字は世界的に人気となりました。
人気のコンテンツはより人の目を集め、それによって起こる問題も存在します。
主な例としては、人種差別やLGBT問題などが挙げられるでしょう。参考記事:「絵文字に平等をサポートしてください」人種差別の指摘にゆれるUnicode
ただ、こういった「複数の肌色を用意してほしい」「男と男の絵文字も用意してほしい」といった要望は、日本の携帯電話での使用をベースに作られた絵文字には含まれていませんでした。
この解決策として、これまでの経緯を振り返ると、新たに4バイト文字として肌色別の絵文字を追加するという方法が考えられますが、これでは絵文字を検索する際に面倒になってしまいます。
そこで取られたのが、複数の絵文字を組み合わせて一つの文字を作るという方法でした。
複数の絵文字からなる絵文字
これまでの話では、たとえ1文字が2バイトであろうと4バイトであろうと1コードポイントで表現されていました。(異体字セレクタだと2コードポイントになりますが...)
ここでいうコードポイントとは、文字の集合の中でどこに配置されているかの位置になります。
これは、Unicode だと、U+○○○ のような形で表現され、単純計算で U+0000 ~ U+10FFFF が存在します。
例えば、「?」だと U+1F389 に該当します。さて、先ほど挙げた「複数の絵文字からなる絵文字」ですが、これは複数のコードポイントから1文字として扱われます。
肌色違いなどの絵文字を表現する場合は、人の顔を表現する絵文字に色を表現する絵文字を追加することで表現できます。
また、この画像では2文字の絵文字を結合していますが、3文字4文字を結合して1文字を表現することもあります。(「????」4人家族など)この現象は簡単に確認ができます。実際に肌色違いを試してみましょう。
?+ ?=??
こちらは上記の画像の上の肌色違い生成式を文字に起こしたものです。
「?」をコピーして検索バーなどの適当の場所に貼り付けた後、「?」をコピーして貼り付けた後ろに「 ?」を貼り付けてください。
肌色違いが生成されたでしょうか?このように、ある決まった文字が連続で続いた場合、1文字として扱う仕組みがあります。
それが書記素クラスタです。なお、今回は絵文字を例に挙げていますが、書記素クラスタは「が」などにも利用されています。「か」に濁点の文字が結合されているのです。(1コードポイントの「が」も存在します)
# Ruby irb(main):001:0> "が".length => 2 # C# > WriteLine("が".Length); 2Rubyはコードポイント、C#は2バイト単位でカウントするので、「が」は2バイト文字が2つ並んでいることがわかります。(
U+304B U+3099
)つまり、真に絵文字に対応するということは、こういった1コードポイントを超えた文字にも対応することになります。
これが面倒な理由になります。各言語での挙動
1コードポイントを超えた文字に対応するためには、前述の通り書記素クラスタを用いる必要があります。
これが使われていなかったり、バージョン違いだったりすると正しく対応することができません。言語別に挙動を見ていきましょう。
Ruby
まずは Ruby 2.3.8 を試してみます。
補足
Ruby のstring.length
ではコードポイントを数えてしまいます(例:肌色違いならU+1F471 U+1F3FB
のため2文字扱い)。
なので、結合してできる肌色違いなどの文字を1文字扱いする正規表現\X
を使用しています。irb(main):001:0> RUBY_VERSION => "2.3.8" irb(main):002:0> "が".scan(/\X/).count => 1 irb(main):003:0> "????".scan(/\X/).count => 7どうやら正しく文字数をカウントすることができなかったようです。
これは Ruby 2.3.8 での正規表現の
\X
がこれらの絵文字に対応していないことが原因となります。
\X
が絵文字に対応するのは Ruby 2.4.0 以降になります。参考:https://docs.ruby-lang.org/ja/latest/doc/news=2f2_4_0.html
そのため、このバージョンで書記素クラスタを用いるためにはActiveSupportを使う必要があります。
irb(main):001:0> require 'active_support/multibyte/unicode' => true irb(main):002:0> ActiveSupport::Multibyte::Unicode.unpack_graphemes("が").count => 1 irb(main):002:0> ActiveSupport::Multibyte::Unicode.unpack_graphemes("????").count => 4今度は、書記素クラスタを用いた文字数カウントになりますが、一部変更はかかっているものの、やはりおかしなカウントがされています。
これは、今回使用した ActiveSupport 5.0 が Unicode 8.0.0 以下のバージョンに基づいて文字数カウントを行なうためです。正しくカウントするためには、Unicode 9.0.0 以降に対応した Ruby か ActiveSupport が必要になります。
なお、Unicode 9.0.0 に対応したのも Ruby 2.4.0 以降になります。次は Ruby 2.6.2 での挙動を見ていきましょう。
irb(main):001:0> RUBY_VERSION => "2.6.2" irb(main):002:0> "が".scan(/\X/).count => 1 irb(main):002:0> "????".scan(/\X/).count => 1Ruby 2.6.2 では正規表現
\X
が絵文字に対応しており、Unicode 9.0.0 にも対応しているので、正しく絵文字をカウントできているようです。C#
次は C# の挙動を見ていきましょう。
補足
C# のstring.Length
では char の数を数えています。(例:肌色違いならU+1F471 U+1F3FB
で、どちらも4バイト文字なので、2+2で4文字扱い)
なので、4バイト文字や結合してできる肌色違いなどの文字を1文字扱いするStringInfo
を使用します。> System.Console.WriteLine(new System.Globalization.StringInfo("が").LengthInTextElements); 1 > System.Console.WriteLine(new System.Globalization.StringInfo("????").LengthInTextElements); 7結果が Ruby 2.3.8 で正規表現
\X
を使用した時と同じ挙動ですね...
StringInfoのリファレンスを見てみましょう。リファレンスを見た限り、特に絵文字に関する記述はありません。
ただ、.NET Framework 4.6.2 では、文字の分類に基づくUnicode 標準、バージョン 8.0.0します。 .NET Framework 4.6.1 から .NET Framework 4 向けに基づくはUnicode 標準、バージョン 6.3.0します。 .NET Core でに基づくはUnicode 標準、バージョン 8.0.0します。
ということなので、どちらにせよ
StringInfo
でも正しく絵文字を取り扱うことが難しそうです。では、C#ではどうやって絵文字を扱うのか。
厳密にやるためには、外部のライブラリを頼る方法が一番かと思います。++C++; // 未確認飛行 C さんのサイトで公開されているGraphemeSplitterを使うなどがいいかもしれません。
参考:https://ufcpp.net/blog/2017/10/graphemesplitter/
まとめ
絵文字の歴史と対応方法を紹介しました。
こういった絵文字の記事、閲覧環境によっては正しく表示されないことがあるので辛いところです...おまけ
冒頭で企画さんから依頼されたもので、
「名前入力の際に絵文字を * でエスケープしてほしい(例:ハッピー→ハッピー*)」
こちらは最終的に、絵文字は入力を受け付けないという仕様に変更されたため、
System.Globalization.UnicodeCategory
にて絵文字関連の文字は弾くという対応になりました。今回はUnityでの絵文字対応で、最終的にはこういったコードに落ち着きました。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class NewBehaviourScript : MonoBehaviour { [SerializeField] InputField _inputField; private void Start() { _inputField.onValidateInput += ValidateInput; } private char ValidateInput(string text, int charIndex, char addedChar) { var category = char.GetUnicodeCategory(addedChar); switch (category) { case System.Globalization.UnicodeCategory.Surrogate: /* case System.Globalization.UnicodeCategory.Control: case System.Globalization.UnicodeCategory.OtherNotAssigned: など、その他必要なものがあれば追加 */ return '\0'; } return addedChar; } }参考文献
- 投稿日:2019-05-24T10:29:00+09:00
carrierwave+S3で本番環境への画像アップロード機能実装
はじめに
プロフィール画像をアップロードする機能を
carrierwave
とmini_magick
、fog-aws
を用いてAWS S3
にアップロードするまでの設定方法を書いていく。
本記事では前提としてユーザー管理にdevise
、ビューにはSlim
を使用しており、AWS S3
でバケットの作成が済んでいる状態で進める。準備
あらかじめ本記事で用いる
gem
をインストールしておく。Gemfilegem 'carrierwave' gem 'mini_magick' gem 'fog-aws'Terminalbundle
また、
Userモデル
にプロフィール画像保存用のカラムとして、avatarカラム
を追加しておく。
コマンドでbin/rails g migration AddAvatarToUsers avatar:string
を入力し、マイグレーションファイルを生成。2019***********_add_avatar_to_users.rbclass AddAvatarToUsers < ActiveRecord::Migration[5.2] def change add_column :users, :avatar, :string end endTerminalbin/rails db:migrate
手順
carrierwaveの設定
まずは画像のアップローダーを作成します。
コマンドでbin/rails g uploader Avatar
と入力。
ここでは最低限の設定をしていきます。app/uploaders/avatar_uploader.rbclass 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.rbclass User < ApplicationRecord mount_uploader :avatar, AvatarUploade続いて
config/initializers/carrierwave.rb
を作成し、AWS S3
の設定書いていく。
credentialの設定方法については、credentials.yml.encでシークレットキーを管理にまとめた。config/initializers/carrierwave.rbif 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.rbprotected 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_user
はdevise
の独自メソッド。
ログインユーザーがプロフィール画像を設定している場合はそれを表示し、設定していない場合に表示する画像ファイル(ここでは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); }); });参考
【Rails5】Deviseのregistrations#editで画像をアップロードする
Railsでcarrierwaveを使ってAWS S3に画像をアップロードする手順を画像付きで説明する!
- 投稿日:2019-05-24T09:26:05+09:00
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.rc1Rails プロジェクトを作る
$ rails new rails6_0_0rc1 $ cd rails6_0_0rc1Controller と 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.rbRails.application.configure do ... config.i18n.fallbacks = [{de: :ja}] endlocale 変換用のファイルを用意する
英語(en)、日本語(ja)、ドイツ語(de) の3つを用意します。
1つの yml ファイルに全部の訳語が揃ってしまうと fallbacks の動作を確認できないので、揃わないようにします。config/locales/en.ymlen: morning: "Good Morning"config/locales/ja.ymlja: afternoon: こんにちはconfig/locales/de.ymlde: 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)fallbacks の設定を変更する
fallbacks の設定を変更してみます。明示的に
I18n.default_locale
を追加します。config/environments/development.rbRails.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.rbRails.application.configure do ... config.i18n.fallbacks = [nil, {de: :ja}] endブラウザの表示内容が以下のように変わります。
en (I18n.default_locale) が fallback のリストから消えています。
また、morning
がGoog Morning
に変換されていません。
fallbacks の設定を再度変更する
自動生成された
config/environments/production.rb
では、config/environments/production.rbconfig.i18n.fallbacks = trueとなっているので、 fallbacks が true のときにどうなるのか確認します。
このときは、
I18n.default_locale
が fallbacks に含まれています。
まとめ
Rails 6.0 で DEPRECATION WARNING が出た場合は、
I18n.default_locale
を設定するのが良さそうです。試したソース
試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try023_i18n_fallbacks参考情報
- 投稿日:2019-05-24T01:28:22+09:00
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_PATH
かENGINE_PATH
がなかった場合でも定数APP_PATH
の初期化とconfig/application.rb
とconfig/boot.rb
の読み込み、そして最終的にrails/commands
を読み込んでいるっぽい。/railties/lib/rails/app_loader.rbObject.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.rbdef 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.rbdef 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.rbdef 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.rbdef 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 が実行されます。メソッドの中身を見ると分かりますが、@subclasses
にbase
が追加されてるのが分かるかと思います。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.rbdef 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ここで最後の処理に使う
namespaces
とlookups
の値を確認しておきましょう。現時点でこんな感じになっています。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.rbdef 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.rbdef 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.rbdef 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