20210430のRailsに関する記事は28件です。

[Rails]ransackインストール

はじめに ransackを利用したのでインストール方法を記しておきます。 ransackのインストール ransackをGemfileに追加します。 Gemfile gen 'ransack' $ bundle install
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rspecでメール内にあるURLを取得する

メールについてのRspecを書くときに詰まったので備忘録代わりに記事にします! やりたいこと SystemSpeec内でAction Mailerを使って送信されたメール本文のリンクURLを取得して利用したい ヘルパーを使ったxxx_pathをしようと思ったが、今回はパスワードリセットで、deviseの処理で生成されたパスワードリセットトークンを含んだURLがほしかった。 結論 URI.extract(str)を使って文字列からURLを抽出する https://docs.ruby-lang.org/ja/latest/method/URI/s/extract.html 実際のコード(deviseを利用したパスワードリセットのテスト) visit 'members/password/new' expect(page).to have_content("パスワードを忘れましたか?") find('#xxxxx').set('example@example.com') click_button("パスワードの再設定方法を送信する") # システム上クリックされた後にタイミングでメールが送信される expect(page).to have_content('xxxxxxx') 本題部分 # これで配信したメールのインスタンスを取得できる password_reset_mail = ActionMailer::Base.deliveries.last # 本文をエンコードして取得 mail_body = password_reset_mail.body.encoded # 文字列からURLの配列で取得。今回はURLは1つだけなので1番目を取得 password_reset_url = URI.extract(mail_body)[0] visit password_reset_url expect(page).to have_content('パスワードを変更') この後実際にフォームを入力させたりする もっといい方法があればぜひコメントいただければと思います!! 参考 Action Mailer の基礎 | Rails ガイド Mailer specs - RSpec Rails - RSpec - Relish ingleton method URI.extract
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

herokuのアプリ情報取得

heroku apps:infoを実行 ターミナル % heroku apps:info ===***-app-*****. Addons: cleardb:ignite Auto Cert Mgmt: false Dynos: web: 1 Git URL: https://git.heroku.com/***-app-*****.git Owner: sample@sample.com Region: us Repo Size: 165 KB Slug Size: 56 MB Stack: heroku-** Web URL: https:/*****-app-*****.herokuapp.com/ heroku logs --tail --app <<アプリケーション名>> 【例】heroku logs --tail --app a*-app-** ターミナル 2021-04-30T08:35:22.839682+00:00 heroku[web.1]: Starting process with command `bin/rails server -p ${PORT:-5000} -e production` 2021-04-30T08:35:30.766757+00:00 app[web.1]: => Booting Puma 2021-04-30T08:35:30.766813+00:00 app[web.1]: => Rails 6.0.3.6 application starting in production cafe***@olive******.sakura.ne.jp 2021-04-30T08:47:21.738584+00:00 app[api]: Release v14 created by user cafe***@olive******.sakura.ne.jp 2021-04-30T08:47:21.738584+00:00 app[api]: Deploy c065f55e by user cafe***@olive******.sakura.ne.jp 2021-04-30T08:47:21.960860+00:00 heroku[web.1]: Restarting 2021-04-30T08:47:21.974003+00:00 heroku[web.1]: State changed from up to starting 2021-04-30T08:47:23.387217+00:00 heroku[web.1]: Stopping all processes with SIGTERM 2021-04-30T08:47:23.489523+00:00 app[web.1]: - Gracefully stopping, waiting for requests to finish 2021-04-30T08:47:23.547863+00:00 app[web.1]: === puma shutdown: 2021-04-30 08:47:23 +0000 === 2021-04-30T08:47:23.547896+00:00 app[web.1]: - Goodbye! 2021-04-30T08:47:42.762010+00:00 app[web.1]: * Environment: production 2021-04-30T09:18:24.501762+00:00 app[web.1]: === puma shutdown: 2021-04-30 09:18:24 +0000 === cafe***@olive******.sakura.ne.jp 2021-04-30T09:53:36.022502+00:00 heroku[web.1]: State changed from down to starting 2021-04-30T09:53:39.000000+00:00 app[api]: Build succeeded 2021-04-30T09:53:45.391721+00:00 heroku[web.1]: Starting process with command `bin/rails server -p ${PO**:-*0**} -e production` 2021-04-30T09:53:54.356026+00:00 app[web.1]: => Booting Puma 2021-04-30T09:53:54.356085+00:00 app[web.1]: => Rails 6.0.3.6 application starting in production 2021-04-30T09:53:54.356085+00:00 app[web.1]: => Run `rails server --help` for more startup options 2021-04-30T09:59:29.855528+00:00 app[api]: Release v** created by user cafe***@olive******.sakura.ne.jp 2021-04-30T09:59:30.176191+00:00 heroku[web.1]: Restarting 2021-04-30T09:59:30.197190+00:00 heroku[web.1]: State changed from up to starting 2021-04-30T09:59:31.254922+00:00 heroku[web.1]: Stopping all processes with SIGTERM 2021-04-30T09:59:31.330731+00:00 app[web.1]: - Gracefully stopping, waiting for requests to finish 2021-04-30T09:59:31.341751+00:00 app[web.1]: === puma shutdown: 2021-04-30 09:59:31 +0000 === 2021-04-30T09:59:31.341754+00:00 app[web.1]: - Goodbye! 2021-04-30T09:59:42.042358+00:00 app[web.1]: => Run `rails server --help` for more startup options 2021-04-30T09:59:43.113738+00:00 app[web.1]: Puma starting in single mode... 2021-04-30T09:59:43.113772+00:00 app[web.1]: * Version 3.12.6 (ruby 2.6.5-p114), codename: L*****s in P*******s 2021-04-30T09:59:46.449298+00:00 app[api]: Release v17 created by user cafe***@olive******.sakura.ne.jp 2021-04-30T09:59:46.724617+00:00 heroku[web.1]: Restarting 2021-04-30T09:59:46.736251+00:00 heroku[web.1]: State changed from up to starting 2021-04-30T09:59:47.648532+00:00 heroku[web.1]: Stopping all processes with SIGTERM 2021-04-30T09:59:47.715254+00:00 app[web.1]: - Gracefully stopping, waiting for requests to finish 2021-04-30T09:59:47.716038+00:00 app[web.1]: === puma shutdown: 2021-04-30 09:59:47 +0000 === 2021-04-30T09:59:58.571860+00:00 app[web.1]: => Rails 6.0.3.6 application starting in production 2021-04-30T09:59:58.571860+00:00 app[web.1]: => Run `rails server --help` for more startup options 2021-04-30T09:59:59.839376+00:00 app[web.1]: Puma starting in single mode... 2021-04-30T09:59:59.839392+00:00 app[web.1]: * Version 3.12.6 (ruby 2.6.5-p114), codename: L****s in P******s 2021-04-30T09:59:59.839393+00:00 app[web.1]: * Min threads: 5, max threads: 5 2021-04-30T09:59:59.839393+00:00 app[web.1]: * Environment: production 2021-04-30T09:59:59.839622+00:00 app[web.1]: * Listening on tcp://0.0.0.0:18031 2021-04-30T09:59:59.839910+00:00 app[web.1]: Use Ctrl-C to stop 2021-04-30T10:00:00.402077+00:00 heroku[web.1]: State changed from starting to up 最新のlogは下にでます.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Basic認証について

環境変数を利用する GitHub上の公開リポジトリでソースコードを管理している場合、コードを読める何者かに、不正にBasic認証を突破されてしまいます。その対策として、コードに直接ユーザー名とパスワードを記述するのではなく、環境変数を利用する実装に切り替える方法があります。 環境変数はzsh、またはbashの設定ファイルに記載します。 zsh(ズィーシェル) 「zsh」はログインシェルと呼ばれるもので、プログラムを実行する時に、ユーザーの要求に一番最初に対応する役割を担います。隠しファイルなので、特別な設定なしではFinderなどには表示されていません。環境変数を記載する場所は、設定ファイルである「.zshrc」の中です。 bash(バッシュ) 「bash」とは、zsh同様、ログインシェルの1つです。zshとの違いは、OSがCatalina以降であれば「zsh」、Mojave以前であれば「bash」が自動で適用されます。環境変数を記載する場所は、設定ファイルである「.bash_profile」の中です。 これらの設定ファイルは、vimというコマンドを用いて編集します。 vim(ヴィム) 「vim」とは、サーバー上で使用できるテキストエディタです。vimコマンドを用いることで、指定したファイルの編集をターミナルから行うことが可能です。 以下が使用例になります。 【使用例】 terminal % vim ~/.zshrc vimには 「通常モード」と「インサートモード」があります。 通常モードは、コマンドを打つことでファイルを保存したりvimを終了したりできます。 「通常モード」のコマンドには以下のようなものがあります。 コマンド 説明 :w 作成・編集したファイルを保存します。 :q viコマンドを終了します。 :q! 編集した内容を保存しないでviコマンドを強制終了します。 :wq 編集した内容を保存してviコマンドを強制終了します。 インサートモードは、ファイルに編集を加えることができます。この場合、「i」キーを押すことで「insert(インサート)モード」になり、文字の入力が可能です。ファイル編集後は、Escキーを押すと「通常モード」に戻ります。 ファイル編集後は、sourceコマンドを実行する必要があります。 手順① 以下のコマンドを実行してください。 ターミナル % vim ~/.zshrc 手順② 「iキー」を押して、インサートモードに移行しましょう。 ターミナルの左下に「INSERT」と表示されたら成功です。 手順③ zshの内部に、以下の記述を追加しましょう。 ターミナル export BASIC_AUTH_USER='admin' export BASIC_AUTH_PASSWORD='2222' 手順④ 記述を追加したら「escキー」を押して、 「:wq」と入力しましょう。入力後、「Enterキー」を押して終了します。 手順⑤ 最後に、「sourceコマンド」を実行しましょう。 ターミナル % source ~/.zshrc
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveRecord::RecordInvalid: Validation failed: Email can't be blank

ゲストユーザーを seed.rb に入れて、必要な情報を環境変数にしていたが、Email can't be blank というエラーがでた。 結論 =(全角)を使っていた。 RAILS_GUEST_ADDRESS=[任意のアドレス] なぜ発見が遅れたのか? 言い訳をすると、見た目ではさっぱりわからなかった。 以下が、seed.rb。VSCode によって文字が彩られている。 そして、こっちが、.env。無地である。 実際に=(全角)を使ってしまったのだけれど、非常にわかりづらい。 関連ファイル一覧 Gemfile gem 'dotenv-rails' ご存知、'dotenv-rails'。環境変数を導入するGem。 RAILS_GUEST_USER=[任意の名前] RAILS_GUEST_ADDRESS=[任意のアドレス] RAILS_GUEST_KEY=[任意の文字列] 環境変数のファイル。上のコードには、=(全角)を使用している。 seed.rb User.create!( name: ENV['RAILS_GUEST_USER'], email: ENV['RAILS_GUEST_ADDRESS'], password: ENV['RAILS_GUEST_KEY'], password_confirmation: ENV['RAILS_GUEST_KEY'], admin: false ) 最後にシード。環境変数を入れ込んでいる。 以上、自戒を込めて。 エラー全文 bin/rails db:seed rails aborted! ActiveRecord::RecordInvalid: Validation failed: Email can't be blank /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/validations.rb:80:in `raise_validation_error' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/validations.rb:53:in `save!' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/transactions.rb:318:in `block in save!' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/transactions.rb:375:in `block in with_transaction_returning_status' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/abstract/database_statements.rb:280:in `block in transaction' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/abstract/transaction.rb:280:in `block in within_new_transaction' /usr/local/bundle/gems/activesupport-6.0.3.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:26:in `block (2 levels) in synchronize' /usr/local/bundle/gems/activesupport-6.0.3.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `handle_interrupt' /usr/local/bundle/gems/activesupport-6.0.3.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `block in synchronize' /usr/local/bundle/gems/activesupport-6.0.3.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt' /usr/local/bundle/gems/activesupport-6.0.3.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/abstract/transaction.rb:278:in `within_new_transaction' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/abstract/database_statements.rb:280:in `transaction' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/transactions.rb:212:in `transaction' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/transactions.rb:366:in `with_transaction_returning_status' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/transactions.rb:318:in `save!' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/suppressor.rb:48:in `save!' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/persistence.rb:55:in `create!' /app/db/seeds.rb:7:in `<main>' /usr/local/bundle/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:59:in `load' /usr/local/bundle/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:59:in `load' /usr/local/bundle/gems/railties-6.0.3.6/lib/rails/engine.rb:557:in `block in load_seed' /usr/local/bundle/gems/railties-6.0.3.6/lib/rails/engine.rb:675:in `with_inline_jobs' /usr/local/bundle/gems/railties-6.0.3.6/lib/rails/engine.rb:557:in `load_seed' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/tasks/database_tasks.rb:440:in `load_seed' /usr/local/bundle/gems/activerecord-6.0.3.6/lib/active_record/railties/databases.rake:331:in `block (2 levels) in <main>' /usr/local/bundle/gems/railties-6.0.3.6/lib/rails/commands/rake/rake_command.rb:23:in `block in perform' /usr/local/bundle/gems/railties-6.0.3.6/lib/rails/commands/rake/rake_command.rb:20:in `perform' /usr/local/bundle/gems/railties-6.0.3.6/lib/rails/command.rb:48:in `invoke' /usr/local/bundle/gems/railties-6.0.3.6/lib/rails/commands.rb:18:in `<main>' /usr/local/bundle/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require' /usr/local/bundle/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi' /usr/local/bundle/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register' /usr/local/bundle/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi' /usr/local/bundle/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require' bin/rails:4:in `<main>' Tasks: TOP => db:seed (See full trace by running task with --trace)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

A server is already running. エラーの解決策

terminal A server is already running. Check /Users/maedatakuo/projects/furima-32844/tmp/pids/server.pid. 参考:ぷりくろ.com https://purikuro.com/2020/08/13/programing_error/ 解決策1 サーバーの切り忘れ 開いてるターミナルを全部終了して、rails sでサーバー起動をする. そう言った場合は1つずつ「コントロール+C」を押して、サーバー停止させるか、ターミナルを消してしまいましょう。 解決策2 lsof -wni tcp:3000と kill terminal maedatakuo@maedatakudainoMacBook-Air furima-32844 % lsof -wni tcp:3000 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME ruby 56002 maedatakuo 12u IPv4 0x5a5a3176181f2d33 0t0 TCP 127.0.0.1:hbci (LISTEN) ruby 56002 maedatakuo 14u IPv6 0x5a5a3176211a8c53 0t0 TCP [::1]:hbci (LISTEN) ruby 56002 maedatakuo 26u IPv6 0x5a5a3176211a85f3 0t0 TCP [::1]:hbci->[::1]:65376 (CLOSE_WAIT) ruby 56002 maedatakuo 27u IPv6 0x5a5a317625f7ef93 0t0 TCP [::1]:hbci->[::1]:65368 (CLOSE_WAIT) ruby 56002 maedatakuo 30u IPv6 0x5a5a3176243d65f3 0t0 TCP [::1]:hbci->[::1]:65372 (CLOSE_WAIT) ruby 56002 maedatakuo 33u IPv6 0x5a5a317625f7e933 0t0 TCP [::1]:hbci->[::1]:65379 (CLOSE_WAIT) terminal maedatakuo@maedatakudainoMacBook-Air furima-32844 % kill -9 56002 pitというのが裏で動いているサーバー番号になります。これをkillします。 私はこれでなおりました。 解決策3 ps aux | grep rails でプロセス削除 terminal $ rails s => Booting Puma => Rails 5.0.7.2 application starting in development on http://localhost:3000 => Run `rails server -h` for more startup options A server is already running. Check プロジェクト名/tmp/pids/server.pid. Exiting $ ps aux | grep rails user 28321 s001 S+ 0:00.00 grep rails $ kill -9 28321 $ rails s → 解決 rails サーバーのプロセスを終了させる方法です。 実は、僕はこの方法で解決できたことがありません。 もしこの方法で直せた方や、原因が分かる方がいらっしゃればご教授願いますっ 解決策4 rm /tmp/pids/server.pid でパスを指定して削除 terminal $ rm /tmp/pids/server.pid $ rails s → 解決 このファイルの場所は[アプリ名]/tmp/pids/server.pidに入っているので、パスを指定して削除します。 本来、サーバーを終了するとこのファイルは削除されますが、残ったままでエラーになっている可能性があるみたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubocop のエラー解決法。Style/RedundantFetchBlock: in 'config/puma.rb' について

そもそも、RedundantFetchBlock とは? Redundant の語源 Re / d / und / ant Re (繰り返し) + und(波)+ ant (名詞化させる接尾辞) つまり「波が繰り返していること」 要するに「あふれていること」 もっとコンパクトに言うと「氾濫」 だから、RedundantFetchBlock とは、FetchBlock が「氾濫」していることを指している。 ※ d は発音をスムーズに繋げるためのもので、ここでは無視。 エラー内容 $ rubocop --auto-gen-config このコマンドを打つと、.rubocop_todo.yml に直すべき課題が与えられる。 rubocop_todo.yml # Offense count: 2 # Cop supports --auto-correct. # Configuration parameters: SafeForConstants. Style/RedundantFetchBlock: Exclude: - 'config/puma.rb' 今回は、Style/RedundantFetchBlock: という課題が与えられた。 Exclude: で示されている 'config/puma.rb' を見に行こう。 config/puma.rb max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count port ENV.fetch("PORT") { 3000 } environment ENV.fetch("RAILS_ENV") { "development" } pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' } plugin :tmp_restart 今回、「氾濫」しているというのは、2つ。 1行目 max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 4行目 port ENV.fetch("PORT") { 3000 } これらを以下のように書き換える。 1行目(変更後) max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) 4行目(変更後) port ENV.fetch('PORT', 3000) 修正したコード全文 config/puma.rb max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } threads min_threads_count, max_threads_count port ENV.fetch('PORT', 3000) environment ENV.fetch('RAILS_ENV') { 'development' } pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' } これで氾濫は治まった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Error: unrecognized cop Enabled found in .rubocop.ymlのエラー(メモ)

エラー文詳細 ターミナルにて% bundle exec rubocopを実行時に以下が出力。 Error: unrecognized cop Enabled found in .rubocop.yml .rubocop.yml内で何かが有効になっていない? 解決方法 Gemfileにrubocop-railsを追加しターミナルで% bundle installを行なった場合、 .rubocop.ymlにrequire: rubocop-railsを追加する。 以下、例 AllCops: TargetRubyVersion: 2.6.5 DisabledByDefault: true Exclude: - db/schema.rb - vendor/bundle/**/* - node_modules/**/* require: - rubocop-rails Bundler/OrderedGems: Enabled: true Layout/EmptyLines: Enabled: true Layout/TrailingEmptyLines: Enabled: true Layout/TrailingWhitespace: Enabled: true Style/MethodDefParentheses: Enabled: true Style/StringLiterals: EnforcedStyle: single_quotes Style/StringLiteralsInInterpolation: EnforcedStyle: single_quotes 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アソシエーション

アソシエーション アソシエーションとはモデルを利用したテーブル同士の関連付けのことです。 アソシエーションをモデルに定義することで、そのモデルに紐づく別のモデルの情報へアクセスできるようになります。 has_manyメソッド 投稿機能機能があるものを実装する際に、Userモデルの視点で考えると、あるユーザーの作成した投稿は複数個ある状態です。 つまり、1人のユーザーは複数の投稿を所有しています。 この状態のことを、has_manyの関係と言います。 この関連付けをするため、userと他のモデルとの間に「1対多」の繋がりを示すものが、has_manyメソッドと言います。 belongs_toメソッド 1つの投稿は、1人のユーザーが投稿したものです。 つまり、1つの投稿を複数人が投稿できないため、投稿は必ず1人のユーザーに所属します。 この状態のことをbelongs toの関係と言います。 「1対1」の具体資を似のが所属します。 has_oneメソッド テーブルのつながりが、1対1の関係の時に使用するメソッドです。 中間テーブル その名の通り2つのテーブルの中間にあるテーブルのことです。 中間テーブルは、多対多の関係にある2つのテーブルの間に挟まって、2つの組み合わせパターンだけをレコードとして保存します。 throughオプション has_manyメソッドのthroughオプションは、モデルに多対多の関連を定義するときに利用します。throughという名前のとおり、「〜を経由する」という意味です。 多対多の関係にある2つのテーブルのモデルでは、has_manyメソッドによる1対多のアソシエーションを互いに定義するのと合わせて、throughオプションによって経由する中間テーブルを指定します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails+JavaScript】残り文字を表示させる[コピペOK]

この記事の対象者 JavaScriptをRailsに組み込んでみたい方 CRUD処理は理解されている方 細かい説明が欲しい方 *最終コードは最後にに書いております。説明が必要ない方は「最終的なコード」をご覧ください。 今回作成するもの 今回作成するものは、画像のように「xxx/xxx文字」と残り文字数を伝えるものになります 今回の想定外 文字の色を変更すること 文字数を超えて入力できること CSSによる見た目の修正 準備すること フォームの作成 (サンプル) _form.html.erb <%= form_with model: @モデル名, local: true do |f| %> <%= f.text_area :content %> <% end %> *この記事では、alertで動作確認を行う記述があります。 確認できましたら、alertは削除ください。 目次 JSファイルを作成 文字数を取得する viewに表示させる 関数の作成 1.JSファイルを作成 まずは、JSファイルを作成します。名前は任意で構いません。 今回はcountLengthとします。 ターミナル touch app/javascript/packs/countLength.js app/javascript/packs/countLength.js + document.addEventListener('turbolinks:load', () => { + alert('動作確認'); //確認用です。 + }) 次に、このファイルを読み込みます application.js import "./countLength" alertが表示されればOKです!(確認後は削除してくだい。) これで、JSファイルの準備ができました! 2.文字数を取得する 次に、文字数を取得しましょう。 今回取得する必要がある文字数は、以下の3つです 最大文字数 現在入力している文字数 残り文字数 この3つを算出する必要があります。 1つずつ確認していきましょう。 1.最大文字数 「フォームで入力できる最大文字数」を取得します。 最大文字数は、自分でフォームに設定する必要があります idとmaxlength(最大文字数)を付与しましょう _form.html.erb 例 <%= f.text_area :content %> ↓ <%= f.text_area :content, id: "textarea", maxlength:"1000" %> これで、最大1000文字までしか打てないように制限がかかりました。 実際に1000文字以上打てるか確認してください。 このmaxlength: 1000をJSファイル(countLength.js)で取得していきます。 以下のコードを追加してください。 app/javascript/packs/countLength.js document.addEventListener('turbolinks:load', () => { + let textarea = document.getElementById('textarea'); //テキストエリアを取得 + let maxLength = textarea.maxLength; //最大の文字数を取得 + alert(maxLength); //確認用 }) maxLengthが表示されればOKです。 まずは、最大文字数を取得することができるようになりました。 2.現在入力している文字数 次に、現在入力している文字数を取得します。 先ほど定義した変数textareaを用いて、textarea.value.lengthで取得できます。 app/javascript/packs/countLength.js document.addEventListener('turbolinks:load', () => { let textarea = document.getElementById('textarea'); let maxLength = textarea.maxLength; + let currentLength = textarea.value.length; //テキストエリアに入力されている文字数を取得 + alert(currentLength);//確認用 }) currentLengthが表示されればOKです。(formにいくつか文字を入力しておくとわかりやすいです。) これで、現在入力している文字数を取得できました。 3.残り文字数を取得 最後に、残り何文字打てるか?を取得します。 残り文字数は、言葉にすると以下のようになります。 残り文字数 = 最大文字数 - 現在入力している文字数 これを実際にJSファイルで実行しましょう! app/javascript/packs/countLength.js document.addEventListener('turbolinks:load', () => { let textarea = document.getElementById('textarea'); let maxLength = textarea.maxLength; let currentLength = textarea.value.length; + remainingLength = maxLength - currentLength //残り文字 = 最大文字数 - 現在入力している文字数 + alert(remainingLength); //確認用 }) remainingLengthが表示されればOKです。 これで、必要な3つの要素をJSファイルで取得することができました。 3.viewに表示させる 最後に、取得した文字数を表示させましょう! まずは文字数を表示させるdivタグを作成し、id="message"を付与します。 _form.html.erb <%= form_with model: @モデル名, local: true do |f| %> <%= f.text_area :content, id: "textarea", maxlength:"1000" %> + <div id="message"></div> <% end %> そして、先ほど取得した最大文字数と残りの文字数を<div id="message"></div>に反映させます。 JavaScriptでinnnerHTMLメソッドというメソッドを使用することで、値をview側に渡すことができます。 - element.innerHTML app/javascript/packs/countLength.js document.addEventListener('turbolinks:load', () => { let textarea = document.getElementById('textarea'); let maxLength = textarea.maxLength; let currentLength = textarea.value.length; remainingLength = maxLength - currentLength; + let message = document.getElementById('message'); //メッセージを表示する要素を取得 + message.innerHTML = `${remainingLength}/${maxLength}文字`; //残りの文字/最大文字数を出力 }) これで、残り文字を出力することができます 以上で完成になりま・・・せん! これでは、ページが表示された時の情報が入るため、後から入力しても数字が変わりません。 なので、関数を作成して、入力したら文字数が反映される仕様にします。 4.関数の作成 まず、どのような条件と動作になるか言語化しておきます 言語化 「id="textarea"を持つテキストエリア」に文字を入力した時、「入力し終わった直後の残り文字を算出して、表示させる」 という風に置き換えることができます。 それでは、条件を満たす時に実行される関数を作成します。 テキストエリアに文字を入力された時に動作するイベントとして、今回'keyup'というイベントを使用します。 - Document: keyup イベント app/javascript/packs/countLength.js //テキストエリアに文字をkeyupしたときに動作する関数を作成 textarea.addEventListener('keyup', function(){   //ここに動作を書いていきます。 }) app/javascript/packs/countLength.js document.addEventListener('turbolinks:load', () => { let textarea = document.getElementById('textarea'); let maxLength = textarea.maxLength; let currentLength = textarea.value.length; remainingLength = maxLength - currentLength; let message = document.getElementById('message'); message.innerHTML = `${remainingLength}/${maxLength}文字`; //テキストエリアでkeyupイベントが発生した時に動作する関数を作成。 + textarea.addEventListener('keyup', function(){ + alert('動作確認'); + }) }) キーUPするたびにアラートが表示されればOKです! これで、関数が動作する条件を設定することができました。 最後に中身を作成します app/javascript/packs/countLength.js document.addEventListener('turbolinks:load', () => { let textarea = document.getElementById('textarea'); let maxLength = textarea.maxLength; let currentLength = textarea.value.length; remainingLength = maxLength - currentLength; let message = document.getElementById('message'); message.innerHTML = `${remainingLength}/${maxLength}文字`; textarea.addEventListener('keyup', function(){ //現在の文字数を再取得する + let currentLength = textarea.value.length //現在の文字数を再出力する + message.innerHTML = `${maxLength - currentLength }/${maxLength}文字` }) }) これで、入力(keyup)があるたびに、残り文字数を表示する関数を実装できました! 動作確認してみてください! 最終的なコード _form.html.erb <%= form_with model: @モデル名, local: true do |f| %> <%= f.text_area :content, id: "textarea", maxlength:"1000" %> <div id="message"></div> <% end %> app/javascript/packs/countLength.js document.addEventListener('turbolinks:load', () => { if (document.getElementById('textarea') !== null) { let textarea = document.getElementById('textarea'); let maxLength = textarea.maxLength; let currentLength = textarea.value.length; - remainingLength = maxLength - currentLength; //リファクタリング let message = document.getElementById('message'); + message.innerHTML = `${maxLength - currentLength}/${maxLength}文字`; //改善 textarea.addEventListener('keyup', function(){ let currentLength = textarea.value.length message.innerHTML = `${maxLength - currentLength}/${maxLength}文字` }) } }) 今後 今後、以下を実装してみたいと思っています。追加できるようになれば、この記事に追加しようと思います。 <div id = "message"></div>をJSファイルで作成する(view側で設定する必要をなくす) 残り文字数が少なくなった時、色を変化させる エラー対応 上記に説明がありませんが、document.getElementById('textarea')を取得できない場合、エラーが発生することがわかりました。 そのため、document.getElementById('textarea')で値を取得できた場合のみ、動作するように条件分岐させております。(最終的なコードに追加してあります。) app/javascript/packs/countLength.js document.addEventListener('turbolinks:load', () => { + if (document.getElementById('textarea') !== null) { let textarea = document.getElementById('textarea'); let maxLength = textarea.maxLength; let currentLength = textarea.value.length; let message = document.getElementById('message'); message.innerHTML = `${maxLength - currentLength}/${maxLength}文字`; textarea.addEventListener('keyup', function(){ let currentLength = textarea.value.length message.innerHTML = `${maxLength - currentLength}/${maxLength}文字` }) + } })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【新人プログラマ応援】開発タスクをアサインされたらどういう手順で進めるべきか

はじめに これはQiitaで開催されている「新人プログラマ応援 - みんなで新人を育てよう!」イベントの投稿記事です。 前回は「学習用のプログラムと仕事で書くプログラムは何が違うか」というタイトルで、お勉強用に作るプログラムと仕事で書くプログラムはこんなところが違うんだよ〜、というお話を書いてみました。 今回の記事ではみなさんが無事にプログラマとして就職できたと仮定して、「○○さん、このタスクお願いね」と開発タスクをアサインされたときの対応手順を説明してみます。 この記事を書いている人 仕事で20年近くプログラムを書いているプログラマ 現在は株式会社ソニックガーデンでRubyプログラマをやっている Rubyの入門書「プロを目指す人のためのRuby入門」を出版している プログラミングスクール「フィヨルドブートキャンプ」のメンターでもある 対象読者 新卒、または業界未経験の中途入社で最近プログラマになった人 その上で、開発タスクをアサインされたものの、「全然成果が出せなくて辛い・・・?」と落ち込んでいる人 僕が普段Railsを使っているため、この記事ではRailsを使う開発の現場を想定していますが、大半の内容はWeb系企業であれば言語やフレームワークを問わず参考になるはずです。 想定する開発タスク ここでは以下のような開発タスクがアサインされたと仮定します。 対象となるアプリケーションは運用されてすでに数年が経っている既存のRailsアプリケーション 商品一覧画面に「CSVダウンロードボタンを付けてほしい」というのがアサインされたタスクの内容 ローカルの開発環境はすでにセットアップが完了している TL;DR(長いので最初に結論) タスクは食べられるサイズに小さく切ってから口に運べ! アサインされたタスクをそのままほおばると喉に詰まって死ぬぞ!! ・・・と言われても何の話かわからないと思うので、さっそく本編に進みます。 なお、この手順は新人プログラマ専用の手順ではなく、僕自身も普段から実践している手順です。 手順1. 現行の仕様(システムの挙動)を確認する まずはローカル環境でRailsアプリケーションを動かしてみて、現行のアプリケーションがどういう仕様で動いているのかを確認します。 たとえば、今回の開発タスクであれば「CSVダウンロードボタン」を追加する画面にどうやってアクセスすればいいのか確認します。 手順2. 機能追加の要件や具体的な変更内容を確認する つぎに、機能追加の要件や具体的な変更内容を確認します。 「CSVダウンロードボタンを付けてCSVをダウンロードできるようにすればいいんでしょ?」と考えるだけでは甘いです。 画面のどの位置にどんなボタンを付ければいいのか? ボタンをクリックしたときの挙動はどうなるのか? 確認ダイアログを表示する必要はないか? ボタンをクリックしたあとにボタンをdisableにしなくてよいか? CSVファイルにはどんなデータを出力するのか? どのカラムにどんなデータを出力するのか? データの出力順はどうなるのか? 日付のフォーマットはどうするのか?"yyyy/mm/dd"か、"yyyy-mm-dd"か、それともまた別のフォーマットか? CSVファイルのエンコーディングは何にするのか?UTF-8でいいのか、それともShift_JISでないとダメなのか? などなど、単なるCSVダウンロード機能でも考慮すべきポイントはたくさんあります。 また、すでに他の画面にCSVダウンロードボタンが付いているのであれば、その挙動もチェックしましょう。 システムの挙動には一貫性があった方がいいので、既存のCSVダウンロード機能があるのであればその挙動にあわせる方がベターです。 正常系だけでなく異常系やセキュリティ面の仕様も検討しよう 新人プログラマのうちはついつい正常系の仕様ばかり考えてしまい、異常系の考慮が不十分なことがよくあります。 異常系というのは「場合によっては起こりうる、あまり嬉しくない動作パターン」のことです。 CSVダウンロードで言えば、次のような異常系が想定できます。 出力するデータがゼロ件だったらどうするのか?ヘッダ行だけをダウンロードさせるのか、それとも何か特別な方法でユーザーに知らせるのか? ダウンロードしようとしていたデータにカンマや改行が含まれていた場合(つまりCSVファイルのフォーマットを壊すようなデータが含まれていた場合)、どういう形式で出力するのか? 他にもログインしていないユーザーや権限がないユーザーが間違って(もしくは悪意をもって)直接ダウンロード用URLにアクセスしてきた場合など、セキュリティ面の考慮もきちんと検討しておく必要があります。 タスクの背景やユースケースも確認しよう 「何を作ればいいのか」だけではなく、「なぜそのタスクが必要になったのか」という背景までもしっかり確認しましょう。 また、それに加えて、追加する新機能がどういうユースケースで使われるのかも確認しておきましょう。すなわち、 どういう立場のユーザーが いつ、どんな頻度でその機能を使い その機能を使って何をするのか(例:ダウンロードしたCSVファイルを何に使うのか) といった点も確認しておいた方がよい、ということです。 背景やユースケースを知っておけば、「そういう目的なのであれば、こういうふうに動いた方がいいだろうな」とか、「そもそもユーザーが必要な機能はCSVダウンロード機能ではなくて、画面に表示されているデータのソート順を変更できる機能なのでは?」といった観点でタスクの内容を見直すことができます。 いわゆる「顧客が本当に必要だったもの」ってやつですね。 Image: 顧客が本当に必要だったものとは (コキャクガホントウニヒツヨウダッタモノとは) [単語記事] - ニコニコ大百科 要件や仕様で不明な点が出てきたり、提示された仕様の見直しを相談したくなったりした場合はタスクをアサインしてきた開発リーダーに質問しましょう。 手順3. 現行のロジックがどのように実装されているのかを確認する 次にソースコードを覗いて現行のロジックがどのように実装されているのかを確認します。 商品一覧画面はいったいどのように実装されているのでしょうか? サーバーサイドでRailsがHTMLをレンダリングしてレスポンスとして返しているだけ? それともReactやVue.jsといったフロントエンドフレームワークを使って描画している? その場合、APIはどこでどうやってどんなデータを返している? CSVに出力する項目はどのテーブルのどのカラムから出力する? モデルとモデルの関連はどのようになっている? 既存のCSVダウンロード機能があるのであれば、それはどのような実装になっている? 共通部品や共通ロジックがすでにあって、差分だけ実装すればいいような作りになってたりしないか? アサインされたタスクの内容や画面の動きだけ見るとすごくシンプルなのに、ロジックを覗いて見ると「げげっ、なんでこんなややこしいことやってるの!?」って思うことは結構あります。(僕は「ふたを開けたらビックリ☆パターン」と呼んでいます) 思った以上に現行ロジックが複雑でどこで何がどう動いてるのかさっぱりわからん、という場合は先輩プログラマに声をかけてそのカラクリを説明してもらってください。 本番環境のデータ量やパフォーマンス目標も確認する 今回例で挙げたCSVダウンロード機能であれば、本番環境のデータベースにはどれくらいデータがあって、一回あたり最大何件ぐらいのデータをダウンロードするのか?といった確認も必要です。 でないと、開発環境では数秒でダウンロードできたのに(=5件しかデータがなかったから)、本番環境では10分以上待ってもダウンロードが終わらない(=100万件データがあったから)、みたいな問題が起きたりします。 時間がかかりそうな場合は、 ストリーミング形式でダウンロードする 非同期でCSVファイルを生成する(あとからダウンロード用のリンクをメールで通知する等) バッチ処理で決められた時刻にCSVファイル用のデータを生成する といった方法が考えられます。 テストコードの有無も確認する 実装コードだけでなく、テストコードの有無も確認しておきましょう。 商品一覧画面にはすでにテストが用意されているか?用意されているならそこにCSVダウンロードのテストを簡単に追加できそうか? CSVダウンロードのテストは他の画面で書かれているか? 既存のテストコードがあればそのテストコードを流用して今回実装するCSVダウンロード機能のテストが書けますが、そうでない場合はゼロからテストコードを書く必要があります。 テストコードの書き方に慣れていない場合は、テストコードを書く時間も開発工数に含める必要があるでしょう。 手順4. 機能追加するにはどこをどう変更すればよいのかリストアップする(=タスクばらし) ここまでで開発に必要なインプットはそろったはずなので、具体的に何をどうやるのかをリストアップします。 「CSVダウンロードをボタンを追加してCSVをダウンロードできるようにする」みたいな粒度ではダメです。 タスクが大きすぎるので新人プログラマはタスクを喉に詰まらせて死にます。 そうではなく、もっともっと小さい単位にタスクを分解しましょう。 1つ1つのタスクは大きくても30分から1時間程度で終わる内容にしてください。 この作業を「タスクばらし」と言います。 たとえばCSVダウンロード機能であれば以下のようなタスクに分解できそうです。 CSVダウンロード用のルーティングをroutes.rbに追加する 画面上にCSVダウンロードボタンを配置してCSVダウンロード用のパス(URL)にリクエストを送れるようにする コントローラにCSVダウンロード用のアクションを追加する CSVの生成ロジックを実装する。CSVダウンロード機能はすでに共通ロジックがあるので、今回はデータの取得とファイル名作成のロジックだけを実装し、それ以外の処理は共通ロジックに任せる CSSを使ってボタンの見た目を整える CSVダウンロード機能のテストを書く タスクが小さければ小さいほど「食べやすいタスク」になります。 タスクを喉に詰まらせないよう、タスクをできるだけ小さく分解してください。 また、ここでリストアップした小さなタスクはTODOリストであり、自分自身への作業指示書でもあります。 NotionやEvernoteのようなツールでTODOリスト化して、作業が終わったらチェックを付ける、というような使い方もオススメです。 各タスクの作業時間も見積もってみよう ひととおりタスクを分解したら、それぞれのタスクにどれくらいの時間がかかりそうかざっくりと時間を見積もってみましょう。 これまでに使ったことのあるライブラリを利用する場合や、すでにお手本となるコードがある場合は比較的簡単に終わると思いますが、反対に「今まで使ったことがないライブラリを使うコード」や「お手本がなく自分がゼロから書かなければならないコード」は思った以上に時間がかかるリスクがあります。 タスクが大きいままだと不明な点も多いので見積もりの誤差も大きくなりますが、タスクを細かく分解すれば各タスクの見積もりの精度が上がり、その結果タスク全体の見積もりの精度も向上します。 手順5. タスクばらしの結果を先輩プログラマにレビューしてもらう タスクばらしが終わったら、その結果を先輩プログラマや開発リーダーにレビューしてもらいましょう。 「今回はこんな手順で、こんなふうに実装しようと思っています」という内容を先輩プログラマに伝え、認識の齟齬はないか、難しく考えすぎてないか等々、大きな手戻りが発生しそうなポイントがないことを確認してもらってください。 また、だいたいの見積もり時間も一緒に伝えてください。 明らかに手間がかかりそうなタスクや、リスクが高いタスク(例:実務でVue.jsのコードを書くのは今回が初めてですんなり実装できるか不安、等)は予め正直に伝えておくことが大事です。 もしかすると「そんなに時間がかかるならその部分だけ別タスクで対応しよう」とか、「もしハマったら手伝うから声をかけて」というように先輩プログラマからのアドバイスやサポートが受けられるかもしれません。 【重要】ここまでまだコードはひとつも書いていません! さて、現時点で手順5まで説明しましたが、実はここまでコードは一切書いていない点にお気づきでしょうか? ここまでやったのはひたすら前準備です。 「タスクをアサインされたから早くコードを書かなきゃ!」と焦ってコードを書き始めるのは間違いです。 まずは仕様や実装方法の疑問点を全部潰して、「あとは手を動かすだけ」という状態にすることが大事です。 ゴールまでの道筋がハッキリと見えていないのに慌てて出発すると、すぐに迷子になって右往左往することになります。 ただし、コードを書くなと言っても、スパイク(技術検証のためだけに使う小さなプログラム)を作るのは問題ありません。 手順6. 実装を開始する お待たせしました。ここからようやく楽しいコーディングの時間です。 先ほどのタスクばらしで作ったTODOリストに従って実装を進めていきましょう。 仕様と実装方針が事前に明確になっていれば、そこまで苦しむことなくコードを書き進められるはずです! 実装を開始したらWIP(work in progress=作業途中)のプルリクエストを作って、開発中のコードの差分を他の人が確認できるようにしておきましょう。 ハマったら制限時間を決めて、それを超えたら助けを求める そうは言ってもいざ実装を始めると思い通りに動かない部分が出てきてハマってしまうことがあるかもしれません。 そういうときは必ず時間を区切って対応するようにしてください。 目安としては30分です。 「ヤバい、これはハマってるぞ」と思ったら時計を見て、30分後にスマホのアラームをセットしましょう。 もしアラームが鳴ってもまだ問題が解決しないようであればタイムオーバーです。 先輩プログラマをつかまえて「すいません、ハマったんでちょっと助けてください」と声をかけてください。 先輩の時間を奪ってしまうかも、というような遠慮は不要です。 なぜならあなたが1人で悩みに悩んで3日かけてタスクを完了させるよりも、先輩に相談して2時間で完了させる方がチーム全体として見たときに効率がいいからです。 それでも「嫌な顔をされたらどうしよう」と不安になる人は、先ほど書いたタスクばらしのレビューのタイミングで先輩プログラマに「もしハマったら声をかけるんでよろしくお願いします」とひとこと伝えておけば大丈夫なはずです。 正常系の実装が終わったら途中経過を見てもらって手戻りを防ぐ 大きなタスクの場合は最後まで一気に完成させようとせずに、ある程度動くようになった段階でタスクをアサインしてきた開発リーダーに実際の動きを軽くレビューしてもらってください。 これは何のためかというと、手戻りを防ぐためです。 せっかく最後まで作りこんだのに仕様を勘違いしてて作りなおしになってしまうと、時間的ロスがめちゃくちゃ大きくなります。 加えて、時間的ロスだけでなく精神的な落ち込みも半端ないものになるで、手戻りはできる限り避けたいところです。 そのためにも最初は細かい点の作り込みは後回しにします。 細かい点とはたとえば入力値のバリデーションやデザインの調整、イレギュラーパターンの実装等です。 正常系の最もシンプルな処理フローが一通り動くようになったあたりで途中経過を報告するのが一番良いと思います。 手順7. 実装が終わったらプルリクエストのWIPを外して完了! タスクばらしで作ったTODOリストが全部完了済みになれば、タスクそのものの実装も完了します。 つまり、TODOリストがこの状態になればOKですね。 実装が全部終わったらプルリクエストのWIPを外して、コードレビューを依頼しましょう。 場合によっては一部コードの修正や画面の動きの修正を求められるかもしれませんが、致命的な問題はおそらくないはずです。 プルリクエストが承認され、mainブランチにマージされたらアサインされたらタスクは完了です。 どうもお疲れ様でした! まとめ というわけでこの記事では実務に入って間もない新人プログラマを想定して、「開発タスクをアサインされたらどういう手順で進めるべきか」という話を書いてみました。 この記事で述べたようにCSVダウンロード機能の実装をアサインされたからといって、「なるほどCSVダウンロード機能を作ったらいいんだな。よし!」でいきなりコードを書き始めるのはNGです!?‍♂️ そうではなく、コードを書き始めるのはしっかり事前調査をして、大きなタスクを小さく分解して、「これならもう迷うことはない。あとは手を動かすだけ!」と言えるようになってからです。 僕はかれこれ20年近くプログラマとして働いてきていますが、僕自身もこのスタイルで開発を進めています。 こうやって手順を文章化するとすごく時間がかかるように見えるかもしれませんが、「急がば回れ」で結局こういうやり方が一番早く終わります。 つまるところ、「最短経路を見つけるために時間をかけましょう。最短経路を見つけたら、その経路に沿って一気にゴールに向かいましょう。最短経路を確認しないまま歩き始めても、迷子になって余計に時間がかかるだけですよ」ということです。 特に業務で扱う大きくて複雑なプログラムになればなおさらです。 なんの準備も無く真っ暗な樹海に足を踏み込んだら二度と帰ってこれません……? 新人プログラマとして就職したけどなかなか成果が出せずに困っている人は、ぜひこの手順を実践してみてください!? おまけ プログラミングの理想と現実はこんな感じですよね〜。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

capistrano-db-tasksのdb:pullでpg_dump「サーババージョンの不整合のため処理を中断しています」を解決

この記事の意図 WebサーバーとDBサーバーが別の環境で capistrano-db-tasks の pg:pull を実行したときに pg_dump の バージョンがそれぞれ違うということでエラーになり、解決したので共有します。 環境 EC2(amazon linux)からRDS(PostgreSQL)接続のRailsアプリケーションです。 サーババージョン(RDS): 12.5、pg_dump バージョン(EC2): 9.2.24 解決方法 EC2のpg_dumpのバージョンを12にすればOKなはずです。 amazon linuxにはこの時点でpostgresqlの12は用意されていないようなので下記を参考に12をインストールしました。 今回pg_dumpさえ動けばいいのでサーバーは立てる必要はないと思います。 このままですとpg_dumpがバージョン9を見てしまうのでcapistranoでログインしているユーザーの.bashrcにパスを追加します。 export PATH=/usr/pgsql-12/bin/:$PATH .bashrcを読み直します。 $ source .bashrc これでローカルからpg:pullを実行して動きました。 注意 .bash_profileでは動きませんでした。理由は.bashrcがSHELL_VARIABLE、.bash_profileはENVIRONMENT_VARIABLE、という違いがあるからのようです。capistranoでアクセスする時はsshなのでSHELL_VARIABLEしか見てくれないという理解をしています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]Formオブジェクト内でuniquenessを使用してエラーが発生した件

はじめに 初めてパターンのエラーと本日遭遇したため記録のため記述します。 正しいかどうか大変不安ですので、間違い等ご指摘を下さると幸いです エラー内容 Formオブジェクト内でこんな感じにバリデーションを記述していたところ、エラーが発生しました。 with_options presence: true do validates :title,length:{maximum:25} validates :content validates :name,uniqueness:true end 原因 結論から言えば、uniquenessはFormオブジェクト内では使用できないからです。 Formオブジェクトはつい勘違いしてしまいますが、モデルとは違い直接モデルを触れることができないものです。ゆえに、以下のようにモジュールを読み込んでいます。 class PracticesPtag include ActiveModel::Model #省略 解消方法 私の場合は、正しいかわかりませんが、uniquenessのみ直接モデルに書いてやることでエラーを解消させました。 終わりに 正直。まだ腑に落ちない部分もあるため、より詳しい説明ができる方はコメントにて教えてくださると大喜びです!!!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]Stimulus Content Loaderを使ってあとからコンテンツを読み込む

はじめに Railsでトップページや管理画面のダッシュボードなど、表示するコンテンツが多いページを作るときに1つのコントローラーで全てを取得するように作ると物によっては遅くなってしまうのではないでしょうか。 対策としてページ表示後にコンテンツを読み込むように Stimulusを使って実装してみようと思います。 こういった手法を遅延読み込みや遅延ロードと言ったりします。英語だとlazy loadです。 よく画像の表示で使われたりします。 デモ ソースコードはこちら 実装 環境 Rails 6.1.3.1 Ruby 2.7.2 Yarn 1.22.5 Node 14 Docker 流れ !!!注意!!! 今回Dockerを使っていますが、ここではDockerについては割愛しています。なのでdocker-composeなどのコマンドを省略しています。実際に再現する場合は各自の環境に合わせてコマンドや設定を変更してください。 今回Stimulusを使うのでwebpackでインストールします。 rails new my_app --webpack=stimulus localhost:3000を確認します。 rails s この時点でapp/javascript配下に作成されたファイルを確認します。 rails newするときに--webpack=stimulusを指定したのでStimulusのcontrollerを読み込む設定が作成されています。 app/javascript/packs/application.js import Rails from "@rails/ujs" import Turbolinks from "turbolinks" import * as ActiveStorage from "@rails/activestorage" import "channels" Rails.start() Turbolinks.start() ActiveStorage.start() import "controllers" // これ app/javascript/controllers/index.js import { Application } from "stimulus" import { definitionsFromContext } from "stimulus/webpack-helpers" const application = Application.start() const context = require.context("controllers", true, /_controller\.js$/) application.load(definitionsFromContext(context)) app/javascript/controllers/hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "output" ] connect() { this.outputTarget.textContent = 'Hello, Stimulus!' } } まずはCRUD機能を作成していきます。 デモにあるように、今回は投稿とコメントの2つを作成して、投稿詳細ページを表示後にコメントを読み込むといった形を実装してみます。 $ rails g scaffold posts title body:text $ rails g model comment post:references body:text $ rails db:migrate 投稿詳細ページでコメントを閲覧・作成できるように変更します。 config/routes.rb Rails.application.routes.draw do resources :posts do resources :comments, only: %i(index create destroy) end end app/models/post.rb class Post < ApplicationRecord has_many :comments, dependent: :destroy end app/controllers/comments_controller.rb class CommentsController < ApplicationController before_action :set_post def create @comment = @post.comments.new(comment_params) respond_to do |format| if @comment.save format.html { redirect_to @post, notice: "コメントしました" } format.json { render :show, status: :created, location: @post } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @comment.errors, status: :unprocessable_entity } end end end def destroy @comment = @post.comments.find(params[:id]) @comment.destroy respond_to do |format| format.html { redirect_to @post, notice: "コメントを削除しました" } format.json { head :no_content } end end private def set_post @post = Post.find(params[:post_id]) end def comment_params params.require(:comment).permit(:body) end end app/views/posts/show.html.slim p#notice = notice p strong タイトル: p = @post.title p strong 本文: = simple_format @post.body => link_to 'Edit', edit_post_path(@post) '| =< link_to 'Back', posts_path / 以下を追加 h4 コメント - if @comments.present? ul - @comments.each do |comment| li = simple_format comment.body span = link_to "削除", [@post, comment], data: { confirm: 'Are you sure?' }, method: :delete - else p コメントはありません。 hr = form_with model: [@post, Comment.new] do |f| .field = f.label :body, "本文" br = f.text_area :body, required: true .actions = f.submit ここまでで、投稿詳細ページでコメントできるようになりました。 ただ、コメントはページ時に取得しているので、これをページ表示後に取得するようにStimulusを使っていきます。 JSを書いていってもいいですが、以下のcomponentを使えばさくっと実装できそうなのでこれを使ってみます。 $ yarn add stimulus-content-loader 次にimportします。 app/javascript/controllers/index.js import { Application } from "stimulus" import { definitionsFromContext } from "stimulus/webpack-helpers" import ContentLoader from "stimulus-content-loader" // 追加 const application = Application.start() const context = require.context("controllers", true, /_controller\.js$/) application.load(definitionsFromContext(context)) application.register("content-loader", ContentLoader) // 追加 コントローラーを変更します。 stimulus-content-loaderではページ表示後に非同期でコンテンツを取得するので、実行したいアクションを追加します。 app/controllers/comments_controller.rb # 省略... # 追加 def index render partial: 'posts/comments', locals: { post: @post, comments: @post.comments } end # 省略... app/views/posts/_comments.html.slim - if comments.present? ul - comments.each do |comment| li = simple_format comment.body span = link_to "削除", [post, comment], data: { confirm: 'Are you sure?' }, method: :delete - else p コメントはありません。 app/views/posts/show.html.slim / 省略... h4 コメント div[data-controller="content-loader" data-content-loader-url-value="#{ post_comments_path(@post) }"] p ローディング中... / 省略... これでOKです。 実行してみるとデモのような動きになるはずです。 ログを確認してみます。 最初に投稿詳細ページを表示したあとに、コメントを取得しているのが分かります。 以上で完成です。 Stimulus Content Loaderが便利でJSを書かなくても実装できました。 よかったら試してみて下さい。 補足 Stimulus Content Loaderにはdata-content-loader-refresh-interval-valueを指定することで、表示したコンテンツをN秒ごとに再ロードする設定ができるようです。 ソースコードも公開されているのでJSの勉強にも使えそうです。今回のStimulus Content Loaderのコードはこちら Stimulus Componentsには、他にも多くのcomponentがあるので便利そうです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails初学者によるRailsチュートリアル学習記録⑥ 第4章

目次 1. はじめに 2. 第4章の概要 3. 学習内容 4. 終わりに 1. はじめに この記事は、Rails初学者の工業大学三年生がRailsチュートリアルの学習記録をつけるための記事です。 筆者自体がRailsやWebについて知識が少ないので、内容の解釈などに間違いがある可能性があります。(その時はコメントで指摘してくださると助かります!) Railsチュートリアル内ではRailsの内容以外にも、gitでのバージョン管理やHerokuを使ったデプロイも学習しますが、gitに関しては既に私が学習済みのため学習記録には記述しません。 演習の記録も省略します。 2. 第3章の概要 この章ではRailsを扱う上で重要なRubyの要素について学習します。 アプリケーションへの変更はあまり多くなく、どちらかというとRubyの言語仕様をRails consoleを使用して 学ぶことが中心だったので、その内容は私が初めて知った内容や後から見返せるようにしたい内容のみを記述していきます。 カスタムヘルパーの定義 カスタムヘルパーとは タイトルが未定義だった場合の動作を定義する Rails風味のRuby クラスを定義する 3. 学習内容 1. カスタムヘルパーの定義 1-1. カスタムヘルパーとは カスタムヘルパーとは、ビュー内で使用する関数の内、開発者自身で新しく作成した関数のことです。 Railsによって自動的に作成される関数は組み込み関数と呼ばれます。  カスタムヘルパーは、コントローラの作成時に同時に作成されるヘルパーファイルに記述していきます。 1-2. タイトルが未定義だった場合の動作を定義する ここで定義するカスタムヘルパーは、タイトルが定義されていないときに、 表示されるタイトルを変えるカスタムヘルパーです。 第3章で、それぞれのページごとに自動的にタイトルが変わるようにビューの設定を行いました。 app/views/static_pages/home.html.erb <% provide(:title, "Home") %> <h1>Sample App</h1> <p> This is the home page for the <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a> sample application. </p> 前回、変更を加えたビューの1つです。 最初の行の<% provide(:title, "Home") %>で、ビューのタイトルを定義し、application.html.erbというファイルで その値を受け取ることで「Home | Ruby on Rails Tutorial Sample App」というタイトルを表示しています。 しかし、もしビューファイルにタイトルの定義が無ければ 「| Ruby on Rails Tutorial Sample App」というタイトルになってしまいます。 余分な縦棒(|)が初めに表示されてしまうため、カスタムヘルパーによってタイトルが定義されていないときは、 縦棒なしの「Ruby on Rails Tutorial Sample App」というタイトルが表示されるようにします。 カスタムヘルパーの名前はfull_titleと定義して、内容は以下のコードを使用して説明します。 app/helpers/application_helper.rb module ApplicationHelper def full_title(page_title = '') base_title = "Ruby on Rails Tutorial Sample App" if page_title.empty? base_title else page_title + " | " + base_title end end end このカスタムヘルパーはデフォルト値がnilの引数を1つ取ります。 この引数はページごとのタイトルと対応してif page_title.empty?で条件分岐に利用されます。 この条件式がtrueだった場合、つまりページごとのタイトルが定義されていない場合に、 base_titleという変数を返します。 base_titleの内容は「Ruby on Rails Tutorial Sample App」という文字列です。 そして、条件式がfalseだった場合、つまりページごとのタイトルが定義されている場合には、 ページごとのタイトル、縦棒(|)、Ruby on Rails Tutorial Sample Appの3つの文字列が結合された文字列が返されます。 このヘルパーを作成すればapplication.html.erbのtitleタグを書き換えることができます。 <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>から、 <title><%= full_title(yield(:title)) %></title>というコードになります。 変更後では、各ビューのタイトルが引数として渡されています。 2. Rails風味のRuby ここでは、Rubyの言語仕様やメソッドの使用方法について初めて知ったことや、 見返せるように記録しておきたいと思ったものを書いていきます。 ①「後置 if」でif文を一行で書く if文は通常、以下のように最低でも3行必要です。 if 条件式 処理 end しかし、Rubyの後置ifというものを使えば、処理が1行のみの場合にif文を1行で書くことができます。 処理 if 条件式 このように記述すると条件式がtrueの時のみ処理が実行されます。 後置ifは便利ですが返り値が特殊らしいので注意が必要らしいです。 参考:【Ruby】後置ifが末尾にあるメソッドの返り値はなに...? ②範囲オブジェクトの対応範囲 範囲オブジェクトとは1..10と記述することで生成される順番に値が取得できるオブジェクトです。 上記の場合1~10の整数が1つずつ取得できます。 この範囲オブジェクトは文字にも対応しており、 "a".."g"や、"あ".."お"、"一".."九"などでもオブジェクトが生成されました。内容は以下の通りです。 ・('a'..'g').to_a => ["a", "b", "c", "d", "e", "f", "g"] ・('あ'..'お').to_a => ["あ", "ぃ", "い", "ぅ", "う", "ぇ", "え", "ぉ", "お"] ・('一'..'九').to_a ["一", "丁", "丂", "七", "丄", "丅", "丆", "万", "丈", "三", "上", "下", "丌", "不", "与", "丏", "丐", "丑", "丒", "专", "且", "丕", "世", "丗", "丘", "丙", "业", "丛", "东", "丝", "丞", "丟", "丠", "両", "丢", "丣", "两", "严", "並", "丧", "丨", "丩", "个", "丫", "丬", "中", "丮", "丯", "丰", "丱", "串", "丳", " 临", "丵", "丶", "丷", "丸", "丹", "为", "主", "丼", "丽", "举", "丿", "乀", "乁", "乂", "乃", "乄", "久", "乆", "乇", "么", "义", "乊", "之", "乌", "乍", "乎", "乏", "乐", "乑", "乒", "乓", "乔", "乕", "乖", "乗", "乘", "乙", "乚", "乛", "乜", "九"] 漢数字で1~9が取得できるかと思ったら、辞書順?で漢字が取得できました。 ③ブロックとは ブロックとは配列や範囲の値を1つずつ取得して、処理を行う仕組みです。 >> (1..5).each { |i| puts 2 * i } 2 4 6 8 10 上のコードは1~5の整数を1つずつ取り出して i に代入、そして i を2倍した値を出力しています。 この i のことをブロック変数と呼びます。 ④ハッシュとシンボル ハッシュとは配列と似たオブジェクトで、キーと値がペアになったものです。 ハッシュを定義するときの書き方にはリテラル表現を使用した書き方と、シンボルを使用した書き方があります。 ・リテラル表現を使用した書き方 user = { "first_name" => "Michael", "last_name" => "Hartl" } {"last_name"=>"Hartl", "first_name"=>"Michael"} #結果 ・シンボルを使用した書き方 user = { :first_name=>"Michael", :last_name=>"Hartl" } { :first_name=>"Michael", :last_name=>"Hartl" } #結果 #シンボルは以下のように書くこともできる user = { first_name: "Michael", last_name: "Hartl" } { :first_name=>"Michael", :last_name=>"Hartl" } #結果 リテラル表現ではハッシュロケットという=>を使ってキーと値を記述します。左辺がキー、右辺が対応する値です。 シンボルを使用した表記方法は、コロンをシンボル名の前においてハッシュロケットを使用する書き方と、 シンボル名の後にコロンをおく書き方があります。 ⑤Rubyにおける関数の書き方 ここではサンプルアプリケーションのレイアウトファイルの以下の一文を用いて、Ruby特有の関数の書き方を説明します。 <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> このコードはスタイルシートを追加する「stylesheet_link_tag」というメソッドですが、書き方がRuby特有です。 1つ目のポイントは引数の丸カッコがありません。 Rubyではメソッド呼び出しの丸カッコを省略できます。 2つ目のポイントは2つ目の引数のmedia: 'all', 'data-turbolinks-track': 'reload'という部分です。 この引数はハッシュですがハッシュを表す波カッコがありません。 Rubyではハッシュが最後の引数であれば、波カッコを省略できます。 3つ目のポイントは途中に開業が含まれている点です。 Rubyは開業と空白を区別しないため、コードの途中での改行時に折り返し用の文字列を入れる必要がありません。 以上のポイントをまとめると、最初のコードは以下の全てのコードと等価と言えます。 # 元のコード stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' # 丸カッコあり stylesheet_link_tag('application', media: 'all', 'data-turbolinks-track': 'reload') # 波カッコあり stylesheet_link_tag 'application', { media: 'all', 'data-turbolinks-track': 'reload' } # 改行なし stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' # 省略、改行なし stylesheet_link_tag('application', { media: 'all', 'data-turbolinks-track': 'reload' }) 3. クラスの作成方法 Rubyではあらゆるものがオブジェクトであり、何らかのクラスに属しています。 それぞれのクラスの継承関係はsuperclassメソッドで確認できます。 ここでは自分でクラス定義のこーどについて説明します。 ここで定義するUserクラスはこの章でしか使用しないので、ルートディレクトリ直下に「examole_user.rb」というファイルを作成します。 クラス定義のコードを以下に示します。 class User attr_accessor :name, :email def initialize(attributes = {}) @name = attributes[:name] @email = attributes[:email] end def formatted_email "#{@name} <#{@email}>" end end まず、class クラス名でクラス名を定義します。この時クラス名の頭文字は大文字にします。 その次のattr_accessorではnameとemailという2つの属性に対応するアクセサーを作成しています。 アクセサーが作成されるとインスタンス変数が使用できるようになります。 つまり、@name, @emailという2つのインスタンス変数をクラスメソッドやビューで使用できるようになるということです。 その下のdefで始まる2つの文がクラスメソッドです。 1つ目のクラスメソッドはRubyの特殊なメソッドで、User.newを実行してインスタンスを作成すると 自動的に実行されるメソッドです。 このメソッドはattributesという空のハッシュを引数として持ちます。 よって、User.newを実行したときに:name. :emailとラベルを指定して値を渡すと、 userインスタンスのインスタンス変数に初期値として入力されます。 また、User.newを実行したときに値を渡さないと初期値はnilとなります。 formatted_emailというクラスメソッドはRailsチュートリアルないで学習のために作成したメソッドで、 名前とメールアドレスを式展開を使用してユーザ名とメールアドレスを一緒に返すメソッドです。 4. 終わりに 第4章では、Rubyの言語仕様を中心に学習しました。 私はRubyはProgateくらいでしか学んでいないので知らないことがまだ多く、抽象的な概念に関しては Rubyのコードを書いた経験が少ないゆえに理解が浅い状態です。 これ以降アプリケーションの機能の追加やテストの実装を行っていくので、 意味が理解できないコードは一つ一つリファレンスマニュアルを参照して、時間をかけてでも理解しながら進めていこうと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Could not find generator '○○:install'. の解決策 (springの停止)

はじめに プログラミング初学者のため、自分の理解できている範囲内で言語化しています。 何か間違っている情報や改善点などありましたら、コメントいただけますと幸いです。?‍♂️ springとは Springは、アプリケーションをバックグラウンドで実行し続けることで開発をスピードアップする Rails アプリケーションプリローダーです。これは、変更を加えるときにサーバーを再起動する必要がないことを意味します。 引用:https://pleiades.io/help/ruby/spring.html 簡単にいうと、アプリケーションを効率よくスピーディーに動かしてくれる役割をになっている。 注意点として開発環境で使うのはいいが、商用環境で使うのは公式でも推奨されていないそう、、、 実際にspringが原因でエラーになる事例もあるとか、、、 まあ、エラーが出なかったらオールオッケー?‍♂️ Could not find generator '○○:install'.エラー rspecの導入でテストコード書くためのディレクトリを生成するために ターミナル. % rails g rspec:install を行った際に ターミナル. Running via Spring preloader in process 38602 Could not find generator 'rspec:install'. Run `rails generate --help` for more options. とエラーが出た。 解決策 調べたところ、springを停止することで解決できるみたいなので ターミナル. % spring stop Spring stopped. spring stopコマンドで停止させて、インストールすることに成功した? spring stop してもどこかのタイミングで、すぐ再起動するが、 stopした直後は止まっているので、問題なくinstallできると思う *これでもうまくいかない人は、下記の参考文献の1つ目のURLをみて、springの無効化などをお試しください 補足 (springの再起動) 起動の確認 ターミナル. % spring status Spring is running など状況が記述される 再起動 ターミナル. % spring start --- springが使えるコマンドなどが表示され起動となる --- 参考文献 こちらの文献ではspringについてかなり細かく記載されているので、しっかりと理解した方は、 下記の一つ目のURLをご覧ください↓ url:https://blog.aiampy.net/20191130-ruby-on-rails-stop-spring/ url:https://pleiades.io/help/ruby/spring.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails Formオブジェクトパターン

勉強用です。 初心者なので間違えている箇所などあるかもしれないのでその時はコメントなどで教えて下さい。 今回は、フリマアプリ購入機能をつくる時に、1つのフォームから送られてきた情報を複数のテーブルに分けて保存」する必要がありました。 従来では、1つのフォームの情報を1つのテーブルに保存でしたが、この方法で複数のテーブルに保存するやり方だと バリデーションの問題などいろいろと問題があります。 そこで使うのがFormオブジェクトです。 個人的のイメージになりますが、モデル同士をくっつけるって感じです。 それをすることにより、バリデーションも簡単につけやすく、コントローラーでの定義もしやすくなります。 やり方としては、まず 新たにmodelsディレクトリ直下にファイルを作成し、クラスを定義 app/modelsディレクトリ配下に自分でファイルをつくります ファイル名.rb 作成できたら、作成したファイルに以下のように記述してクラスを定義。 今回は、donation_address.rbです. app/models/donation_address.rb class DonationAddress end Form_withメソッドに対応する機能とバリデーションを行う機能を、作成したクラスにもたせる。 DonationAddressクラスにActiveModel::Modelをincludeします。 そして保存したいカラム名を属性値として扱えるようにします。 DonationAddressクラス内でattr_accessorを使用します。 donationsテーブルとaddressesテーブルに保存したいカラム名を、すべて指定。 app/models/donation_address.rb class DonationAddress include ActiveModel::Model attr_accessor :postal_code, :prefecture, :city, :house_number, :building_name, :price, :user_id end ActiveModel::Modelとattr_accessorを活用することで、Formオブジェクトの属性をform_withメソッドの引数に指定できるようになります。 バリデーションの処理を書く Formオブジェクトのインスタンスに対してバリデーションを実行します。 そのために、Formオブジェクトにバリデーションを定義しておく必要があります。 app/models/donation_address.rb class DonationAddress include ActiveModel::Model attr_accessor :price, :user_id, :postal_code, :prefecture, :city, :house_number, :building_name with_options presence: true do validates :price, numericality: {only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 1000000, message: 'is invalid'} validates :user_id validates :postal_code, format: {with: /\A[0-9]{3}-[0-9]{4}\z/, message: "is invalid. Include hyphen(-)"} end validates :prefecture, numericality: {other_than: 0, message: "can't be blank"} end user_idには、本来belongs_to :userのアソシエーションにより、バリデーションが設定されています。 が!! Fromオブジェクトでつくったモデルには組み込まれてないので再度定義が必要。 データをテーブルに保存する処理を書く Formオブジェクトに、フォームから送られてきた情報をテーブルに保存する処理を記述します。 app/models/donation_address.rb class DonationAddress include ActiveModel::Model attr_accessor :price, :user_id, :postal_code, :prefecture, :city, :house_number, :building_name with_options presence: true do validates :price, numericality: {only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 1000000, message: "is invalid"} validates :user_id validates :postal_code, format: {with: /\A[0-9]{3}-[0-9]{4}\z/, message: "is invalid. Include hyphen(-)"} end validates :prefecture, numericality: {other_than: 0, message: "can't be blank"} def save # 寄付情報を保存し、変数donationに代入する donation = Donation.create(price: price, user_id: user_id) # 住所を保存する # donation_idには、変数donationのidと指定する Address.create(postal_code: postal_code, prefecture: prefecture, city: city, house_number: house_number, building_name: building_name, donation_id: donation.id) end end コントローラーでFormオブジェクトのインスタンスを生成する new, createアクションで、Formオブジェクトのインスタンスを生成します。 理由は2つある。 1. Formオブジェクトのインスタンスをform_withのmodelオプションに指定するため form_withのmodelオプションに指定できる。これは、個人的に有り難いです!! 自動でアクションを振り分けてくれる 2. 入力した内容やエラーメッセージをフォームで表示させるため エラーメッセージがでるおかげで、こっちも助かりました。(笑) 購入ボタンをおして保存できるか試したが、「商品の情報がない」エラーがでてなるほど!!と助かった実体験。 app/controllers/donations_controller.rb class DonationsController < ApplicationController before_action :authenticate_user!, except: :index def index end def new @donation_address = DonationAddress.new end def create @donation_address = DonationAddress.new(donation_params) if @donation_address.valid? @donation_address.save redirect_to root_path else render :new end end private def donation_params params.require(:donation_address).permit(:postal_code, :prefecture, :city, :house_number, :building_name, :price).merge(user_id: current_user.id) end あとは、viewでform_withを使って表示していく感じになると思います。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル 第6版 振り返り 第12章

はじめに このまとめ記事はRailsチュートリアルを1周終えた私が1周目で分からなかった所や記憶に残したい箇所のみをピックアップして記述しています。完全解説記事ではないので注意して下さい。 私と同じく2周目の方、たまに復習したいなと振り返りを行う方等におすすめです。 とは言いつつも、9章あたりから難しくなったのでこの章も全て書き残しています。 パスワードの再設定(12章) 全章でアカウントの有効化の実装が完了し、ユーザーのメールアドレスが本人のものである確信が得られるようになったので、れでパスワードを忘れた時のパスワードの再設定に取り組めるようになった。 本章で見ていく内容のほとんどは、アカウント有効化で見てきた内容と似通っている。 とはいえ、すべてが同じではなく、違う実装もある。 例えばアカウントの有効化のときと異なり、パスワードを再設定する場合はビューを1つ変更する必要があり、また、新しいフォームが新たに2つメールレイアウト用と新しいパスワードの送信用)必要になる。 アカウント有効化の際と似ていて、PasswordResetsリソースを作成して、再設定用のトークンとそれに対応するダイジェストを保存するのが今回の目的。 全体の流れは次のとおり。 ①ユーザーがパスワードの再設定をリクエストすると、ユーザーが送信したメールアドレスをキーにしてデータベースからユーザーを見つける。 ②該当のメールアドレスがデータベースにある場合は、再設定用トークンとそれに対応する再設定ダイジェストを生成する。 ③再設定用ダイジェストはデータベースに保存しておき、再設定用トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく。 ④ユーザーがメールのリンクをクリックしたら、メールアドレスをキーとしてユーザーを探し、データベース内に保存しておいた再設定用ダイジェストと比較する(トークンを認証する)。 ⑤認証に成功したら、パスワード変更用のフォームをユーザーに表示する。 PasswordResetsリソース(12.1) セッションやアカウント有効化のときと同様に、まずはPasswordResetsリソースのモデリングから始める。 前章と同様に、今回も新たなモデルは作らずに、代わりに必要なデータ(再設定用のダイジェストなど)をUserモデルに追加していく形で進める。 PasswordResetsもリソースとして扱っていきたいので、まずは標準的なRESTfulなURLを用意する。 有効化のときはeditアクションだけを取り扱ったが、今回はパスワードを再設定するフォームが必要なので、ビューを描画するためのnewアクションとeditアクションが必要になる。 また、それぞれのアクションに対応する作成用/更新用のアクションも最終的なRESTfulなルーティングには必要になる。 PasswordResetsコントローラ(12.1.1) 最初のステップとしてパスワード再設定用のコントローラを作成する。 今回はビューも扱うので、newアクションとeditアクションも一緒に生成。 $ rails generate controller PasswordResets new edit --no-test-framework 上のコマンドでは、テストを生成しないというオプションを指定していることに注目。 これはコントローラの単体テストをする代わりに、11章で作成した結合テストを使用するから。 また今回の実装では、新しいパスワードを再設定するためのフォームと、Userモデル内のパスワードを変更するためのフォームが必要になるので、new、create、edit、updateのルーティングも用意。 この変更は、前回と同様にルーティングファイルのresources行で行う。 config/routes.rb resources :password_resets, only: [:new, :create, :edit, :update] このコードは、RESTfulのルーティングにしたがっている。 HTPPリクエスト URL Action 名前付きルート GET /password_resets/new new new_password_reset_path POST /password_resets create password_resets_path GET /password_resets/トークン/edit edit edit_password_reset_url(token) PATCH /password_resets/トークン update password_reset_url(token) 引用:Railsチュートリアル12.1.1表12.1 次に、パスワード再せって画面へのリンクをログイン画面に追加。 app/views/sessions/new.html.erb <% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(url: login_path, scope: :session, local: true) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= link_to "(forgot password)", new_password_reset_path %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div> 新しいパスワードの設定(12.1.2) パスワード再設定のデータモデルも、アカウント有効化の場合と似ている。 記憶トークンや有効化トークンでの実装パターンに倣って、パスワードの再設定でも、トークン用の仮想的な属性とそれに対応するダイジェストを用意。 しトークンをハッシュ化せずに(つまり平文で)データベースに保存してしまうとすると、攻撃者によってデータベースからトークンを読み出されたとき、セキュリティ上の問題が生じる。 つまり、攻撃者がユーザーのメールアドレスにパスワード再設定のリクエストを送信し、このメールと盗んだトークンを組み合わせて攻撃者がパスワード再設定リンクを開けば、アカウントを奪い取ることができてしまう。 したがって、パスワードの再設定では必ずダイジェストを使うようにする。 さらにセキュリティ上の注意点はもう1つある。 それは再設定用のリンクはなるべく短時間(数時間以内)で期限切れになるようにしなければならないということ。 そのために、再設定メールの送信時刻も記録する必要がある。 以上の背景に基づいて、reset_digest属性とreset_sent_at属性をUserモデルに追加する。 $ rails generate migration add_reset_to_users reset_digest:string \ > reset_sent_at:datetime $ rails db:migrate これを追加した現在のUsersモデルがこちら。 引用:Railsチュートリアル12.1.2図12.5 新しいパスワード再設定の画面を作成するために、新しいセッションを作成するためのログインフォームを使用する。 新しいパスワード再設定フォームは多くの共通点がありますが、重要な違いとして、form_withで扱うリソースとURLが異なっている点と、パスワード属性が省略されている点が異なっている。 app/views/password_resets/new.html.erb <% provide(:title, "Forgot password") %> <h1>Forgot password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(url: password_resets_path, scope: :password_reset, local: true) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.submit "Submit", class: "btn btn-primary" %> <% end %> </div> </div> createアクションでパスワード再設定 前項で作成したフォームから送信を行なった後、メールアドレスをキーとしてユーザーをデータベースから見つけ、パスワード再設定用トークンと送信時のタイムスタンプでデータベースの属性を更新する必要がある。 それに続いてルートURLにリダイレクトし、フラッシュメッセージをユーザーに表示。 送信が無効の場合は、ログインと同様にnewページを出力してflash.nowメッセージを表示。 app/controllers/password_resets_controller.rb class PasswordResetsController < ApplicationController def new end def create @user = User.find_by(email: params[:password_reset][:email].downcase) #[:password_reset]に注意する。 if @user @user.create_reset_digest @user.send_password_reset_email flash[:info] = "Email sent with password reset instructions" redirect_to root_url else flash.now[:danger] = "Email address not found" render 'new' end end def edit end end Userモデル内のコードは、before_createコールバック内で使われるcreate_activation_digestメソッドと似ている。 app/models/user.rb class User < ApplicationRecord attr_accessor :remember_token, :activation_token, :reset_token before_save :downcase_email before_create :create_activation_digest . . . # アカウントを有効にする def activate update_attribute(:activated, true) update_attribute(:activated_at, Time.zone.now) end # 有効化用のメールを送信する def send_activation_email UserMailer.account_activation(self).deliver_now end # パスワード再設定の属性を設定する def create_reset_digest self.reset_token = User.new_token update_attribute(:reset_digest, User.digest(reset_token)) update_attribute(:reset_sent_at, Time.zone.now) end # パスワード再設定のメールを送信する def send_password_reset_email UserMailer.password_reset(self).deliver_now end この時点でのアプリケーションは、無効なメールアドレスを入力した場合に正常に動作する。 正しいメールアドレスを送信した場合にもアプリケーションが正常に動作するためには、パスワード再設定のメイラーメソッドを定義する必要がある。 パスワード再設定のメール送信(12.2) PasswordResetsコントローラで、createアクションがほぼ動作するところまで持っていった。 残すところは、パスワード再設定に関するメールを送信する部分。 既にUserメイラー(app/mailers/user_mailer.rb)を生成したときに、デフォルトのpassword_resetメソッドもまとめて生成されているはず。 パスワード再設定のメールとテンプレート(12.2.1) 11章では、UserメイラーにあるコードをUserモデルに移すリファクタリングを行った。 同様のリファクタリング作業を、パスワード再設定に対しても行っていく。 UserMailer.password_reset(self).deliver_now 上のコードの実装に必要なメソッドは、前の章で実装したアカウント有効化用メイラーメソッドとほぼ同じ。 最初にUserメイラーにpassword_resetメソッドを作成し続いて、テキストメールのテンプレートとHTMLメールのテンプレートをそれぞれ定義する。 app/mailers/user_mailer.rb class UserMailer < ApplicationMailer def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end def password_reset(user) @user = user mail to: user.email, subject: "Password reset" end end アカウント有効化メールの場合と同様、Railsのメールプレビュー機能でパスワード再設定のメールをプレビューする。 test/mailers/previews/user_mailer_preview.rb # Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/account_activation def account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) end # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset user = User.first user.reset_token = User.new_token UserMailer.password_reset(user) end end これでメールのプレビューを確認することが可能。 画像は省略しますが、しっかりできました。 さらにこの段階で正しいメールアドレスを送信したときの画面もエラーが発生しなくなる。 送信メールのテスト送信メールのテスト(12.2.2) アカウント有効化のテストと同様に、メイラーメソッドのテストを書いてみる。 test/mailers/user_mailer_test.rb require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end test "password_reset" do user = users(:michael) user.reset_token = User.new_token mail = UserMailer.password_reset(user) assert_equal "Password reset", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.reset_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end end これでテストが成功すればOK。 パスワードを再設定する(12.3) 無事に送信メールを生成が完了したので、次はPasswordResetsコントローラのeditアクションの実装を進めていく。 また、結合テストも行ってうまく動作しているかのテストも行っていく。 editアクションで再設定(12.3.1) サーバのログで確認したパスワード再設定の送信メールには、次のようなリンクが含まれていた。 https://example.com/password_resets/3BdBrXeQZSWqFIDRN8cxHA/editemail=fu%40bar.com このリンクを機能させるためにはパスワード再設定フォームを表示するビューが必要。 このビューはユーザーの編集フォームと似ているが、今回はパスワード入力フィールドと確認用フィールドだけで十分。 ただし、今回の作業は少しだけ面倒な点がある。 というのも、メールアドレスをキーとしてユーザーを検索するためには、editアクションとupdateアクションの両方でメールアドレスが必要になるから。 例のメールアドレス入りリンクのおかげで、editアクションでメールアドレスを取り出すことは問題なし。 しかしフォームを一度送信してしまうと、この情報は消えてしまう。 この値はどこに保持しておくのがよいか。 今回はこのメールアドレスを保持するため、隠しフィールドとしてページ内に保存する手法をとる。 これにより、フォームから送信したときに、他の情報と一緒にメールアドレスが送信されるようになる。 app/views/password_resets/edit.html.erb <% provide(:title, 'Reset password') %> <h1>Reset password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(model: @user, url: password_reset_path(params[:id]), local: true) do |f| %> <%= render 'shared/error_messages' %> <%= hidden_field_tag :email, @user.email %> <%= 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 "Update password", class: "btn btn-primary" %> <% end %> </div> </div> 中でもフォームタグヘルパーを使っている点に注目。 hidden_field_tag :email, @user.email これまでは f.hidden_field :email, @user.email このように書いていた。 再設定用のリンクをクリックすると、hidden_field_tagではメールアドレスがparams[:email]に保存されるが、f.hidden_fieldでは、params[:user][:email]に保存される。 今度は、このフォームを描画するためにPasswordResetsコントローラのeditアクション内で@userインスタンス変数を定義する。 アカウント有効化の場合と同様、params[:email]のメールアドレスに対応するユーザーをこの変数に保存。 続いて、params[:id]の再設定用トークンと、抽象化したauthenticated?メソッドを使って、このユーザーが正当なユーザーである(ユーザーが存在する、有効化されている、認証済みである)ことを確認。 editアクションとupdateアクションのどちらの場合も正当な@userが存在する必要があるので、いくつかのbeforeフィルタを使って@userの検索とバリデーションを行う。 app/controllers/password_resets_controller.rb class PasswordResetsController < ApplicationController before_action :get_user, only: [:edit, :update] before_action :valid_user, only: [:edit, :update] . . . def edit end private def get_user @user = User.find_by(email: params[:email]) end # 正しいユーザーかどうか確認する def valid_user unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id])) redirect_to root_url end end end 上記のコードでは次のコードを使っている。 authenticated?(:reset, params[:id]) さらに以前使ったこのコード authenticated?(:remember, cookies[:remember_token]) と authenticated?(:activation, params[:id]) この3つが11章の表11.1のauthenticationとなり、今回追加したコードで全ての実装が完了した。 これでメールのリンクを開いたときに、パスワードの再設定のフォームが出力されるようになった。 実際に確認してこの項は終了。 パスワードを更新する(12.3.2) AccountActivationsコントローラのeditアクションでは、ユーザーの有効化ステータスをfalseからtrueに変更したが、今回の場合はフォームから新しいパスワードを送信するようになっている。 したがって、フォームからの送信に対応するupdateアクションが必要となる。 このupdateアクションでは、次の4つのケースを考慮する必要がある。 ① パスワード再設定の有効期限が切れていないか ②無効なパスワードであれば失敗させる(失敗した理由も表示する) ③新しいパスワードが空文字列になっていないか(ユーザー情報の編集ではOKだった) ④新しいパスワードが正しければ、更新する 上のケースを1つずつ対応していく。 ①については、editとupdateアクションに次のようなメソッドとbeforeフィルターを用意することで対応する。 before_action :check_expiration, only: [:edit, :update] # (1)への対応案 このcheck_expirationメソッドは、有効期限をチェックするPrivateメソッドとして定義。 # 期限切れかどうかを確認する def check_expiration if @user.password_reset_expired? flash[:danger] = "Password reset has expired." redirect_to new_password_reset_url end end このcheck_expirationメソッドでは、期限切れかどうかを確認するインスタンスメソッド「password_reset_expired?」を使っている。 このメソッドの説明は後ほどとのこと。 次にupdateアクションを使用して②と④のケースを解決する。 ②は、更新が失敗したときにeditのビューが再描画され、edit.html.erbのパーシャルにエラーメッセージが表示されるようにすれば解決。 ④については、更新が成功したときにパスワードを再設定し、あとはログインに成功したときと同様の処理を進めていけば問題なし。 今回の小難しい問題点は③の、パスワードが空文字だった場合の処理。 というのも、以前Userモデルを作っていたときに、パスワードが空でも良いallow_nilを実装したから。 したがって、このケースについては明示的にキャッチするコードを追加する必要がある。 今回は@userオブジェクトにエラーメッセージを追加する方法を使う。 具体的には、次のようにerrors.addを使ってエラーメッセージを追加。 @user.errors.add(:password, :blank) このように書くと、パスワードが空だった時に空の文字列に対するデフォルトのメッセージを表示してくれるようになる。 以上をまとめると①のpassword_reset_expired?の実装を除き、すべてのケースに対応したupdateアクションが完成する。 app/controllers/password_resets_controller.rb class PasswordResetsController < ApplicationController before_action :get_user, only: [:edit, :update] before_action :valid_user, only: [:edit, :update] before_action :check_expiration, only: [:edit, :update] # (1)への対応 def new end def create @user = User.find_by(email: params[:password_reset][:email].downcase) if @user @user.create_reset_digest @user.send_password_reset_email flash[:info] = "Email sent with password reset instructions" redirect_to root_url else flash.now[:danger] = "Email address not found" render 'new' end end def edit end def update if params[:user][:password].empty? # (3)への対応 @user.errors.add(:password, :blank) render 'edit' elsif @user.update(user_params) # (4)への対応 log_in @user flash[:success] = "Password has been reset." redirect_to @user else render 'edit' # (2)への対応 end end private def user_params params.require(:user).permit(:password, :password_confirmation) end # beforeフィルタ def get_user @user = User.find_by(email: params[:email]) end # 有効なユーザーかどうか確認する def valid_user unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id])) redirect_to root_url end end # トークンが期限切れかどうか確認する def check_expiration if @user.password_reset_expired? flash[:danger] = "Password reset has expired." redirect_to new_password_reset_url end end end 上のコードでは、user_paramsメソッドを使ってpasswordとpassword_confirmation属性を精査している点に注意 @user.password_reset_expired? 上のコードを動作させるために、password_reset_expired?メソッドをUserモデルで定義。 このメソッドではパスワード再設定の期限を設定して、2時間以上パスワードが再設定されなかった場合は期限切れとする処理を行う。 これをRubyで表現すると次のようになる。 reset_sent_at < 2.hours.ago 上の < 記号を「〜より少ない」と読んでしまうと、「パスワード再設定メール送信時から経過した時間が、2時間より少ない場合」となってしまい、困惑してしまうので注意。 ここで行っている処理は、「少ない」ではなく「早い」と捉えると理解しやすい。 こうすると「パスワード再設定メールの送信時刻が、現在時刻より2時間以上前(早い)の場合」となり、 期待どおりの条件となる。 したがって、この条件が満たされるかどうかを確認するpassword_reset_expired?メソッドは、次のようになる。 app/models/user.rb class User < ApplicationRecord . . . # パスワード再設定の期限が切れている場合はtrueを返す def password_reset_expired? reset_sent_at < 2.hours.ago end private . . . end このコードを使うと、updateアクションが動作するようになる。 チュートリアルにあるように、成功と失敗の画面を確認してこの項は終了。 パスワードの再設定をテストする(12.3.3) この項では、送信に成功した場合と失敗した場合の統合テストを作成する。 まずはパスワード再設定のテストファイルを生成。 $rails generate integration_test password_resets パスワード再設定をテストする手順は、アカウント有効化のテストと多くの共通点があるが、テストの冒頭部分には次のような違いがある。 最初に「forgot password」フォームを表示して無効なメールアドレスを送信し、次はそのフォームで有効なメールアドレスを送信する。 後者ではパスワード再設定用トークンが作成され、再設定用メールが送信される。 続いて、メールのリンクを開いて無効な情報を送信し、次にそのリンクから有効な情報を送信して、それぞれが期待どおりに動作することを確認。 test/integration/password_resets_test.rb require 'test_helper' class PasswordResetsTest < ActionDispatch::IntegrationTest def setup ActionMailer::Base.deliveries.clear @user = users(:michael) end test "password resets" do get new_password_reset_path assert_template 'password_resets/new' assert_select 'input[name=?]', 'password_reset[email]' # メールアドレスが無効 post password_resets_path, params: { password_reset: { email: "" } } assert_not flash.empty? assert_template 'password_resets/new' # メールアドレスが有効 post password_resets_path, params: { password_reset: { email: @user.email } } assert_not_equal @user.reset_digest, @user.reload.reset_digest assert_equal 1, ActionMailer::Base.deliveries.size assert_not flash.empty? assert_redirected_to root_url # パスワード再設定フォームのテスト user = assigns(:user) # メールアドレスが無効 get edit_password_reset_path(user.reset_token, email: "") assert_redirected_to root_url # 無効なユーザー user.toggle!(:activated) get edit_password_reset_path(user.reset_token, email: user.email) assert_redirected_to root_url user.toggle!(:activated) # メールアドレスが有効で、トークンが無効 get edit_password_reset_path('wrong token', email: user.email) assert_redirected_to root_url # メールアドレスもトークンも有効 get edit_password_reset_path(user.reset_token, email: user.email) assert_template 'password_resets/edit' assert_select "input[name=email][type=hidden][value=?]", user.email # 無効なパスワードとパスワード確認 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "foobaz", password_confirmation: "barquux" } } assert_select 'div#error_explanation' # パスワードが空 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "", password_confirmation: "" } } assert_select 'div#error_explanation' # 有効なパスワードとパスワード確認 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "foobaz", password_confirmation: "foobaz" } } assert is_logged_in? assert_not flash.empty? assert_redirected_to user end end 今回の新しい要素はinputタグ。 assert_select "input[name=email][type=hidden][value=?]", user.email 上のコードは、inputタグに正しい名前、type="hidden"、メールアドレスがあるかどうかを確認している。 <input id="email" name="email" type="hidden" value="michael@example.com" /> 演習 1 create_reset_digestメソッドは、update_attributeを2回呼び出している。 update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみる。 app/models/user.rb def create_reset_digest self.reset_token = User.new_token update_columns(reset_digest: User.digest(reset_token),reset_sent_at: Time.zone.now) end 2 チュートリアルにあるテンプレートを埋めて、期限切れのパスワード再設定で発生する分岐を統合テストで網羅する。 test/integration/password_resets_test.rb test "expired token" do get new_password_reset_path post password_resets_path, params: { password_reset: { email: @user.email } } @user = assigns(:user) @user.update_attribute(:reset_sent_at, 3.hours.ago) patch password_reset_path(@user.reset_token), params: { email: @user.email, user: { password: "foobar", password_confirmation: "foobar" } } assert_response :redirect follow_redirect! assert_match /expired/i, response.body #←追加 end response.bodyは、そのページのHTML本文をすべて返すメソッド。 なお、この返された本文の大文字と小文字は区別されない。 3 2時間経ったらパスワードを再設定できなくする方針は、セキュリティ的に好ましいやり方だが、もっと良くする方法はまだある。 例えば、公共の(または共有された)コンピューターでパスワード再設定が行われた場合を考える。 仮にログアウトして離席したとしても、2時間以内であれば、そのコンピューターの履歴からパスワード再設定フォームを表示させ、パスワードを更新してしまうことができてしまう。 この問題解決のために、コードを追加し、パスワードの再設定に成功したらダイジェストをnilになるように変更。 app/controllers/password_resets_controller.rb class PasswordResetsController < ApplicationController . . . def update if params[:user][:password].empty? @user.errors.add(:password, :blank) render 'edit' elsif @user.update(user_params) log_in @user @user.update_attribute(:reset_digest, nil) #←追加 flash[:success] = "Password has been reset." redirect_to @user else render 'edit' end end . . . end 4 password_resets_test.rbに演習3のテストを記述する。 test/integration/password_resets_test.rb # 有効なパスワードとパスワード確認 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "foobaz", password_confirmation: "foobaz" } } assert_nil user.reload.reset_digest #追加 assert is_logged_in? assert_not flash.empty? assert_redirected_to user end 本番環境でのメール送信(12.4) あとは、development環境だけでなくproduction環境でも動くようにするだけ。 アカウント有効化と全く同じなのでセットアップが済んでれば飛ばしてよし。 $ rails test $ git add -A $ git commit -m "Add password reset" $ git checkout master $ git merge password-reset いつものようにマージ。 $ rails test $ git push && git push heroku $ heroku run rails db:migrate Herokuにデプロイ。 画像は省略しますが、実際にメールが送られて来て、パスワードの変更も完了しました。 これでこの章は終了。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでメッセージ機能と未読管理を実装する

やること 下記をrailsで実装していく。(ちなみにviewはtailwindcssを使っている メッセージを1対1で送る 未読管理をする メッセージ機能 仕様は下記 人に対してDM 1対1のみでグループでのdmなどはない メッセージ一覧ページとメッセージルームがある 一覧ページで未読のものはその未読数のバッジがつく scheme message_table id:integer content:text user_id:integer to_user_id:integer to_user_opentime:timestamp roomテーブルを設けるか迷ったがグループDMをつける予定はないので1対1のみを想定し、シンプルに。 to_user_opentimeで開封した日時を入れる。これがnilだと未読ということに routes config/routes.rb get 'messages/index/:id' => "messages#index" get 'messages/room/:user_id/:to_user_id' => "messages#roomshow" resources :messages controller app/controllers/messages_controller.rb class MessagesController < ApplicationController def index # 送られたユーザーでないと表示できないように if current_user.id.to_i == params[:id].to_i @user = User.find_by(id: params[:id])     # messageをしているユーザーのidを配列で取得その後に自分のは削除。これで一覧でどのユーザーに対してのDMかを表示させる @message_user_ids = Message.where(to_user_id: @user.id).or(Message.where(user_id: @user.id)).distinct.pluck(:user_id) @message_user_ids.delete(@user.id) else flash[:notice] = "権限がありません" redirect_to("/") end end def roomshow if current_user.id.to_i == params[:user_id].to_i @to_user_id = params[:to_user_id] @messages = Message.where(user_id: params[:user_id],to_user_id: @to_user_id).or(Message.where(user_id: params[:to_user_id],to_user_id: params[:user_id])).order(created_at: :asc) unread_messages = Message.where(to_user_opentime: nil,to_user_id: current_user.id) unread_messages.each do |unread_message| unread_message.to_user_opentime = Date.today.to_time unread_message.save end else flash[:notice] = "権限がありません" redirect_to("/") end end def create if current_user.id.to_i == params[:user_id].to_i message = Message.new(content: params[:content],user_id: params[:user_id],to_user_id: params[:to_user_id]) if message.save flash[:notice] = "送信しました!" redirect_back(fallback_location: root_path) else redirect_to("/") flash[:alert] = "投稿できませんでした" end end end end view app/views/messages/index.html.erb <% @message_user_ids.each do |message_user_id| message_user = User.find_by(id: message_user_id.user_id) %> <div class="border-gray-400 flex flex-row mb-2 bg-white"> <div class="flex flex-col w-10 h-10 justify-center items-center mr-4"> <%= image_tag('default_icon.png',class: "mx-auto object-cover rounded-full h-10 w-10",alt: "ユーザーアイコン") %> </div> <div class=""> <%= message_user.name %> </div> </div> <% end %> 上記で@message_user_idsで取得した自分以外のメッセージしているユーザーのidを使って一覧表示 app/views/messages/roomshow.html.erb <div class="flex flex-col"> <% @messages.each do |message| %> <% if message.to_user_id == current_user.id %> <div class="flex flex-row mb-8"> <div class="flex items-center justify-center h-10 w-10 rounded-full bg-white flex-shrink-0"> <%= image_tag('default_icon.png',class: "mx-auto object-cover rounded-full h-10 w-10",alt: "ユーザーアイコン") %> </div> <div class="ml-3 text-sm bg-white py-2 px-4 shadow rounded-xl"> <p> <%= message.content %> </p> </div> </div> <% else %> <div class="flex flex-row-reverse mb-8"> <div class="flex items-center justify-center h-10 w-10 rounded-full bg-white flex-shrink-0"> <%= image_tag('default_icon.png',class: "mx-auto object-cover rounded-full h-10 w-10",alt: "ユーザーアイコン") %> </div> <div class="ml-3 text-sm bg-white py-2 px-4 shadow rounded-xl"> <p> <%= message.content %> </p> </div> </div> <% end %> <% end %> </div> <%= form_with model: @message, url: messages_path do |form| %> <div class=""> <%= form.text_area :content, id: "content", rows: "4", class: "form_textarea mb-4" %> </div> <%= form.text_area :user_id, class: "hidden", value: current_user.id %> <%= form.text_area :to_user_id, class: "hidden", value: @to_user_id %> <%= form.submit "メッセージ送信", class: "btn_01" %> <% end %> dmページでは自分へのメッセージと自分が送ったメッセージを振り分けて左右対象になるように繰り返し処理を行っている。 メッセージ未読管理 未読管理でやることは下記 current_userに対して送信されているdmで未読のものをカウント 読んだらnilのものだけ取得してto_user_opentimeに日時を入れる 未読と判断するのはto_user_opentimeがnilのもの controller app/controllers/messages_controller.rb unread_messages = Message.where(to_user_opentime: nil,to_user_id: current_user.id) unread_messages.each do |unread_message| unread_message.to_user_opentime = Date.today.to_time unread_message.save end 上記をroom表示の際に記載。 unread_messagesにnilのものを配列で入れる eachでto_user_opentimeに現在時刻を入れる view app/views/messages/index.html.erb <% @message_user_ids.each do |message_user_id| message_user = User.find_by(id: message_user_id) unread_count = Message.where(user_id: message_user_id,to_user_id: @user.id,to_user_opentime: nil).count %> <a href="/messages/room/<%= current_user.id %>/<%= message_user.id %>"> <div class="border-b border-gray-200 flex py-4 bg-white items-center"> <div class="w-10 h-10 mr-4"> <img src="<%= user_icon_url(message_user) %>" class="object-cover rounded-full h-10 w-10"/> </div> <div class=""> <%= message_user.name %> </div> <% if unread_count > 0 %> <div class="rounded-full bg-green-300 ml-4 text-white w-6 h-6 text-center"> <%= unread_count %> </div> <% end %> </div> </a> <% end %> unread_countを取得し、0以上だったら表示 headerに未読バッジをつける app/controllers/application_controller.rb before_action :message_notification def message_notification @message_notification_count = Message.where(to_user_id: current_user.id,to_user_opentime: nil).count end application_controller.rbで未読数を取得 view app/views/layouts/_header.html.erb <a href="/messages/index/<%= current_user.id %>"> <div class="mr-2"> <% if @message_notification_count > 0 %> <%= image_tag('message_icon_unread.png',class: "header_icon align-middle",alt: "airteam") %> <% else %> <%= image_tag('message.png',class: "header_icon align-middle",alt: "airteam") %> <% end %> </div> </a> 未読数が0より上だったらバッジ付きの画像アイコンで0だったらバッジなし
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails6】Omniauth × devise_auth_tokenを用いたTwitterログイン機能のRequest Specを書く

Rails × React SPAの作成に当たりOmniauth × devise_auth_tokenを用いたTwitterログイン機能を実装している。 その際のRequest Specの書き方についてメモ どこをテストするか? 以下の記事内にいい感じの画像があったので引用。 https://qiita.com/hirakei1203/items/d7e03040a4f899d374c1 画像内の③から④のあたり(auth_hashを受け取ってその内容をもとにユーザーを保存する)を検証する コード内容 実際に書いたコードがこちら require 'rails_helper' RSpec.describe 'OmniauthUsers', type: :request do before do OmniAuth.config.test_mode = true # これがないと実際にツイッターと通信してしまう OmniAuth.config.mock_auth[:twitter] = nil # テストごとに認証情報を初期化する Rails.application.env_config['omniauth.auth'] = twitter_mock Rails.application.env_config['omniauth.params'] = { 'resource_class' => 'User', 'namespace_name' => 'api_v1' } # No resource_class foundというエラーを避ける end describe 'omniauthを用いたTwitterでのログイン' do context 'ログインに成功' do it 'oauthのデータが存在する場合ログインに成功する' do get '/api/v1/users/twitter/callback' expect(response).to have_http_status(200) end it 'oauthのデータが存在する場合ユーザーモデルのカウントが1増える' do expect do get '/api/v1/users/twitter/callback' end.to change(User, :count).by(1) end it 'oauthのデータが存在する場合リクエストのmockのデータに応じたレスポンスが返却される' do get '/api/v1/users/twitter/callback' json = JSON.parse(response.body) expect(json['uid']).to eq request.env['omniauth.auth']['uid'] # 念の為一意性のカラムで検証 expect(json['email']).to eq request.env['omniauth.auth']['info']['email'] # 念の為一意性のカラムで検証 expect(json['provider']).to eq request.env['omniauth.auth']['provider'] # 念の為一意性のカラムで検証 end end context 'ログイン失敗' do it "request.env['omniauth.auth']がnilの場合リクエストに失敗する" do Rails.application.env_config['omniauth.auth'] = nil expect do get '/api/v1/users/twitter/callback' end.to raise_error NoMethodError # undefined method [] for nil:NilClassと出る。auth_hashがnilのためにユーザー情報の取得に失敗している状態 end end end end 詳しく見ていこう before do ~ end before do OmniAuth.config.test_mode = true # これがないと実際にツイッターと通信してしまう OmniAuth.config.mock_auth[:twitter] = nil # テストごとに認証情報を初期化する Rails.application.env_config['omniauth.auth'] = twitter_mock Rails.application.env_config['omniauth.params'] = { 'resource_class' => 'User', 'namespace_name' => 'api_v1' } # No resource_class foundというエラーを避ける end コメントアウトで書いている通り。今回は3行目にあるtwitter_mockというのを用いてauth_hashを擬似的に生成する。 twitter_mockは以下のように作成 module OmniauthMocks def twitter_mock OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new({ 'provider' => 'twitter', 'uid' => '123456', # infoはTwitterのプロフィールと対応 'info' => { 'nickname' => 'mock_user', 'image' => 'http://mock_image_url.com', 'location' => '', 'description' => 'This is a mock user.', 'email' => 'mock@example.com', 'urls' => { 'Twitter' => 'https://twitter.com/MockUser1234', 'Website' => '' } }, 'credentials' => { 'token' => 'mock_credentails_token', 'secret' => 'mock_credentails_secret' }, 'extra' => { 'raw_info' => { 'name' => 'Mock User', 'id' => '123456', 'followers_count' => 0, 'friends_count' => 0, 'statuses_count' => 0 } } }) end end 作り方はいろいろあるだろうけど、自分の場合はsupportディレクトリ配下に配置しrails_helper.rbにて Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } RSpec.configure do |config| config.include OmniauthMocks とすることで読み込みを可能にしている。 またtwitter_mockを代入する以外にもtest_modeの有効化やmock_authをnilにする処理、Rails.application.env_config['omniauth.params'] の設定など を行った。それについてはコメントアウトで示しているとおりです。 example describe 'omniauthを用いたTwitterでのログイン' do context 'ログインに成功' do it 'oauthのデータが存在する場合ログインに成功する' do get '/api/v1/users/twitter/callback' expect(response).to have_http_status(200) end it 'oauthのデータが存在する場合ユーザーモデルのカウントが1増える' do expect do get '/api/v1/users/twitter/callback' end.to change(User, :count).by(1) end it 'oauthのデータが存在する場合リクエストのmockのデータに応じたレスポンスが返却される' do get '/api/v1/users/twitter/callback' json = JSON.parse(response.body) expect(json['uid']).to eq request.env['omniauth.auth']['uid'] # 念の為一意性のカラムで検証 expect(json['email']).to eq request.env['omniauth.auth']['info']['email'] # 念の為一意性のカラムで検証 expect(json['provider']).to eq request.env['omniauth.auth']['provider'] # 念の為一意性のカラムで検証 end end context 'ログイン失敗' do it "request.env['omniauth.auth']がnilの場合ログインに失敗する" do Rails.application.env_config['omniauth.auth'] = nil expect do get '/api/v1/users/twitter/callback' end.to raise_error NoMethodError # undefined method [] for nil:NilClassと出る。auth_hashがnilのためにユーザー情報の取得に失敗している状態 end end end end 実際のexampleはこんな感じで基本は Rails.application.env_config['omniauth.auth'] がnilかどうかで正常形と異常形を分けている。 正常形はhttpステータス、Userのカウントの増加、レスポンスをjsonで受け取り中身がmockと一致することの3つを検証 感想 参考記事がなくて大変だったけど、どの処理をテストするのか?という点が明確であればそれなりにテスト書くこともできるな、という印象でした。いい経験でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]collection_selectでクラスやイベント属性が設定できない

今回は下記を参考にさせていただきました。 Railsでcollection_selectを使ってセレクトボックスを作ろうとしたところclassやJavascriptのイベント属性が反映されなかったので解決しました。 公式ドキュメントには一切説明がなかったので戸惑いました。 参考にさせていただいた記事に感謝の意を申し上げます。 結論 <%= f.collection_select(:continent, Continent.all, :id, :name ,{prompt: "方面を選択してください"},{:onchange => "select_country($(this).val())",class: "form-control col-sm-6"})%> 上記のように第5引数にだけpromptを記述し第6引数にはonchangeとclassをを記述します。それぞれ{}で囲えば設定することができます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]text_areaのサイズ変更方法3つ

公式ドキュメントは以下になります。 サイズ変更方法 サイズ変更は以下のように記述する size <%= form.text_area :body , class: 'form-control',size: "20x10"%> cols(横幅) <%= form.text_area :body , class: 'form-control', cols="40"%> rows(縦幅) <%= form.text_area :body , class: 'form-control', rows: 25%>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ログインIDで販売済み商品選択の場合トップページへ遷移させる

この場合はログインIDの確認とpurchaseテーブルにitem_idがあるかどうかを条件分岐させるだけ purchasecontroller.rb def set_redirect_to_root2 if user_signed_in? if Purchase.exists?(item_id: params[:item_id]) redirect_to root_path end end end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フリマアプリ決済備忘録

Token (トークン)⇨カード情報を代替するトークンオブジェクトです。トークンは、カード番号やCVCなどのセキュアなデータを隠しつつも、カードと同じように扱うことができます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リクエストないのparameterについて

HTTPメソッドとしてGETを使う場合、Railsでは次のようなURL形式でパラメータをURLに含めてリクエストを送信 上のURLはfurimaアプリの1つのURLで、出品番号27番の商品の購入画面になります。 POSTを使って送信されたパラメータの取得 また購入画面なので購入フォームに入力や表示のあるものがparameterで取得できます。 Parameters: {"authenticity_token"=>"CCYTmCC7QDCwfk4HZpYRuapieE0CHaQ3ffA6YClz+deEkoo9etxfeeukyqQT/shksW+u5Vc3zUeXhuX/JgUj1g==", "address_purchase"=>{"postal_code"=>"", "prefecture_id"=>"1", "town"=>"", "address"=>"", "building"=>"", "phone_number"=>""},
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2種類のparameter

パラメータ コントローラのアクションでは、ユーザーから送信されたデータやその他のパラメータにアクセスして何か作業を行なうのが普通です。Railsに限らず、一般にWebアプリケーションでは2種類のパラメータを扱うことができます。 「クエリ文字列パラメータ」 URLの一部として送信されるパラメータで、クエリ文字列は、常にURLの"?"の後に置かれます。 「POSTデータ」 POSTデータは通常、ユーザーが記入したHTMLフォームから受け取ります。これがPOSTデータと呼ばれているのは、HTTP POSTリクエストの一部として送信されるからです。 どちらもコントローラ内ではparamsという名前のハッシュでアクセスできます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VCのまとめ

def~~~endの間にかけるアクションは1つだけ index @model複数形=モデル名.all @model複数形.each do Iモデル単数形Iで1個づつ取り出す GET newアクション フォームを表示(new.html.erb) フォームからデータ送信 GET createアクション フォーム内に保存データ入力(form_for/form_with @モデルインスタンス変数) データ保存 POST editアクション edit(new)のフォームを表示(new.html.erb) フォームからデータ送信 GET updateアクション newフォーム内に保存データ入力(form_for/form_with @モデルインスタンス変数) データ保存 PATCH destroyアクション editフォーム内に削除ボタン(@モデルインスタンス変数) データ保存 delete メソッド 役割 GET サーバーからブラウザに情報を返す。単にウェブサイトを閲覧する際など、情報を取得するために利用される。 POST ブラウザからサーバーに情報を送信し、サーバーに情報を保存する。情報を登録する際など、サーバーに情報を送信するために利用される。 DELETE ブラウザからサーバーに情報を送信し、サーバーの情報を削除する。アカウントを削除する際など、サーバー内のデータを削除するために利用される。 PATCH ブラウザからサーバーに情報を送信し、サーバー内の情報を置き換える。登録情報を更新する際など、サーバー内のデータを更新するために利用される。 railsガイド https://railsguides.jp/action_controller_overview.html newメソッドの内容が空であるにもかかわらず正常に動作するという点です。これは、Railsではnewアクションで特に指定のない場合にはnew.html.erbビューをレンダリングするようになっているからです。コントローラが使用できるビューのパスからアクション名.html.erbというビューテンプレートを探し、それを使用して自動的に出力する」というものです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

草野球の出欠確認Webアプリを作ろう! part.4

これから作っていく簡単なWebアプリの作成メモ(自分の備忘)です。 自分用なのであまり凝りすぎないように書いていきたい。 <<前回の記事 今回やったこと スケジュールのデータを作って見た目を確認 コンソール経由でSchedulesモデルのデータを準備する。 (date型やtime型は手入力できるか自信なかったので、Dateクラスなどを利用した) ※以下の記事を参考にした。 Railsタイムゾーンまとめ config/application.rb class Application < Rails::Application (略) config.time_zone = "Tokyo" config.active_record.default_timezone = :local (略) end $ rails c > date = Date.today + 365 > time1 = Time.zone.now > time2 = time1 + (3600 * 3) > time3 = time1 - 1800 > schedule1 = Schedule.new(title:"テスト予定1",date_of:date,start_time:time1,end_time:time2,meeting_time:time3) > schedule1.save (titleを変えてコンソールでの入力を繰り返すことで、スケジュールのデータを複数作れる) (私はそこまで徹底できないけど、大量のスケジュールデータを作成したい方向けにコピペできるスクリプト例も用意した) app/helpers/schedules_helper.rb module SchedulesHelper # 動作確認用のスケジュールデータを複数個作成するメソッド def create_demo_schedules(n) # 引数が数字以外だったら終了 if n =~ /^\d+$/ n = n.to_i elsif n.class == "String".class return false end count = 0 n.times do count += 1 date1 = Date.today + 364 + count time1 = Time.zone.now time2 = time1 + (3600 * 3) time3 = time1 - 1800 title = "テスト予定#{count.to_s.tr("0-9","0-9")}" demo_schedule = Schedule.new( title: title, date_of: date1, start_time: time1, end_time: time2, meeting_time: time3 ) demo_schedule.save end return true end end $ rails c > include SchedulesHelper > create_demo_schedules(100) 上のようにして100件のダミーデータを作成できる。 ※ほんとうは自動テストとして、RspecのFactoryBotで作成して動かすのがいいのでしょう。  あとでやるつもりです。 この状態で動作確認すると、以下のようになった。 時間が確認できないので、すこしviewを変更する。 時間の表示について以下の記事を参考にした。 Time型のデータの値を、時刻部分だけ表示する views/schedules/index.html.erb <table align="center"> <thead> <tr> <th>件名</th> <th>予定日</th> <th>予定の時間</th> </tr> </thead> <tbody> <% @schedules.each do |lst| %> <tr> <td><%= lst.title %></td> <td><%= lst.date_of %></td> <td><%= lst.start_time.strftime('%H:%M') %> ~ <%= lst.end_time.strftime('%H:%M') %></td> </tr> <% end %> </tbody> </table> これで以下のような見た目になる。 まあ最低限ならこれでよしとする。 つぎは、予定の新規作成や編集をさせたい。 スケジュールの新規作成   まずはviewから、つながりを用意しておく。 views/schedules/index.html.erb <h1>チームの予定一覧</h1> <div class="row_line"> <%= link_to '新規作成', new_schedule_path, class: 'btn primary-btn' %> </div> <% if @schedules.blank? %> (以下略) 新規作成側のviewも作成する。 views/shedules/new.html.erb <h1>予定の新規作成</h1> <div class="row_line"> <%= link_to '予定一覧へ', schedules_path, class: 'btn primary-btn' %> </div> <%= form_for @schedules, url: {action: "create"} do |f| %> <div class="row_line"> <label>件名:</label> <%= f.text_field :title %> </div> <div class="row_line"> <label>予定の日付:</label> <%= f.date_select :date_of %> </div> <div class="row_line"> <label>開始時間:</label> <%= f.time_select :start_time %> </div> <div class="row_line"> <label>終了時間:</label> <%= f.time_select :end_time %> </div> <div class="row_line"> <label>集合時間:</label> <%= f.time_select :meeting_time %> </div> <div class="row_line"> <%= f.submit "新規作成する" %> </div> <% end %> controllers/schedules_controller.rb def new @schedules = Schedule.new end これだと表示が落ち着かないので、さすがに私でもパッとできる程度に整える。 ※以下の記事を参考にした。 【Rails】date_selectタグの使い方メモ Rails の date_select でつくるセレクトボックスを「年」「月」「日」で区切る views/shedules/new.html.erb <%= form_for @schedules, url: {action: "create"} do |f| %> <div class="row_line"> <label>件名:</label> <%= f.text_field :title %> </div> <div class="row_line"> <label>予定の日付:</label> <%= raw sprintf( f.date_select(:date_of, use_month_numbers: true, order: [:year,:month,:day], selected: Time.zone.now, start_year: Time.zone.now.year + 5, end_year: Time.zone.now.year, date_separator: '%s' ),'年','月') + '日' %> </div> <div class="row_line"> <label>開始時間:</label> <%= f.time_select :start_time %> &nbsp;~&nbsp; <label>終了時間:</label> <%= f.time_select :end_time %> &nbsp;( <label>集合時間:</label> <%= f.time_select :meeting_time %> ) </div> <div class="row_line"> <%= f.submit "新規作成する" %> </div> <% end %> CSSやbootstrapを入れていないなりに、最初よりはマシになった(感じ方には個人差があります)。 あとはviewに入力した値をDBに登録できるようにする。 そのために以下を実施した。 ※以下は参考にしたWeb記事。 【Rails】permitメソッドを使ってストロングパラメーターにしよう controllers/schedules_controller.rb (略) def create @schedules = Schedule.new(schedules_params) if @schedules.save redirect_to schedule_path(@schedules), notice: "予定を新規作成しました。" else render :new end end (略) private def schedules_params params.require(:schedule).permit( :title, :date_of, :start_time, :end_time, :meeting_time ) end (略) せっかくなので、登録後に表示するshowアクションとそのviewも整える。 controllers/schedules_controller.rb (前後略) def show @schedules = Schedule.find(params[:id]) end views/schedules/show.html.erb <h1>予定の詳細</h1> <div class="row_line"> <%= link_to '予定一覧へ', schedules_path, class: 'btn primary-btn' %> </div> <div class="row_line"> <label>件名:</label> <%= @schedules.title %> </div> <div class="row_line"> <label>予定の日付:</label> <%= @schedules.date_of.strftime("%Y年%m月%d日") %> </div> <div class="row_line"> <label>開始時間 ~ 終了時間: </label> <%= @schedules.start_time.strftime("%H:%M") %> &nbsp;~&nbsp; <%= @schedules.end_time.strftime("%H:%M") %> &nbsp;( <label>集合時間:</label> <%= @schedules.meeting_time.strftime("%H:%M") %> ) </div> showアクションまわりの動作を確認する。 まあ動いたので、ひとまずよしとする。 (個人的には「04月」の表記が気に入らないが、修正の労力があったら機能実装に振りたい気持ち) 疲れたので今日はここまで。 (BootstrapやRspecをはやく導入しなければ...)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む