前提
ユーザーがイベントの内容(タイトル、日時など)を投稿できる SNS を作成しております。
投稿機能は完成しており、その中に「昨日以前の日付を選択した場合はエラーメッセージを出力する」と言う記述があります。
「昨日以前の日付」を date < Date.current
と記述しているのですが、今日を選択しても post.rb
で設定した "Date must be greater than today" のエラーがなぜか出てしまいます。明日以降を選択すると無事投稿できます。
そしてこの記述を date <= Date.yesterday
に変えるとエラーはなくなります。上のコードと意味は同じだと思うのですが、なぜこのような挙動になるのか不思議です。
そしてこのエラーが発生する原因は、恐らくタイムゾーンが関係していると言う所まで分かりました。
User
モデルには time_zone
カラムも持たせており、ユーザーはタイムゾーンを選択できるようになっております。
UTC (GMT+00:00) を含み UTC よりも遅いタイムゾーン (メキシコ GMT-06:00 など) をユーザーが選択している場合は問題ないのですが、UTC よりも早いタイムゾーン (東京 GMT+09:00 など) を選択した場合、ユーザーがイベント日時を「今日」と選択したにも関わらず上記のエラーがなぜか作動してしまします。
本日4月28日を選択して保存を試みた時のパラメーターですが、ちゃんと4月28日として認識はされているようです…。
Started POST "/posts/create" for ::1 at 2022-04-28 16:27:04 +0900 Processing by PostsController#create as HTML Parameters: {"utf8"=>"✓", "authenticity_token"=>"省略", "date"=>"2022-04-28", "commit"=>"Save"}
またタイムゾーンの取り扱いにつきましてはtime_zone_options_for_selectを使用しております。
画像のようなセレクトボックスで、保存される値は Tokyo
と言った地名のみで (GMT+09:00)
などの文言は保存されません。
[1] pry(main)> User.find(1).time_zone User Load (3.2ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 => "Tokyo"
実現したいこと
- エラーの原因を究明したい。
- どのタイムゾーンを選択しても、日時選択時に今日は今日として判定して欲しい。
該当のソースコード
users/new.html.erb
Ruby
1<%= form_with url: users_create_path, local: true do |form| %> 2 <div>Time zone</div> 3 <div> 4 <%= form.select :time_zone, time_zone_options_for_select(@user.time_zone, nil, ActiveSupport::TimeZone) %> 5 </div> 6<% end %>
users_controller.rb
Ruby
1def create 2 @user = User.new( 3 # 他は省略 4 time_zone: params[:time_zone] 5 ) 6 if @user.save 7 session[:user_id] = @user.id 8 redirect_to("/users/#{@user.id}") 9 else 10 render("users/new") 11 end 12end
posts/new.html.erb
Ruby
1<%= form_with url: "/posts/create", local: true do |form| %> 2 <div>Schedule</div> 3 <div><%= form.datetime_field :date, type: "date", id: "inputDate", value: @post.date&.strftime("%Y-%m-%d"), placeholder: "Y-m-d" %></div> 4<%= form.submit id: "button", type: "submit", value: "Save", onclick: "myfunk()" %> 5<% end %>
posts_controller.rb
Ruby
1def create 2 @post = Post.new( 3 # 他は省略 4 date: params[:date] 5 user_id: @current_user.id 6 ) 7 if @post.save 8 redirect_to("/posts/#{@post.id}") 9 else 10 render("posts/new") 11 end 12end
post.rb
Ruby
1validate :pretend_ago 2 3def pretend_ago 4 errors.add(:date, 'must be greater than today') if date.present? && date < Date.current 5end
config/application.rb
Ruby
1module アプリ名 2 class Application < Rails::Application 3 # config.time_zone = 'Asia/Tokyo' 4 config.active_record.default_timezone = :local 5 end 6end
scheme.rb
Ruby
1create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t| 2 # 他は省略 3 t.datetime "created_at", null: false 4 t.string "time_zone" 5end 6 7create_table "posts", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t| 8 # 他は省略 9 t.datetime "created_at", null: false 10 t.datetime "date" 11end
タイムゾーンのデータ型は string ですので、time_zone_options_for_selectにて言及されている以下の文言も満たしているかと思います。
The selected parameter must be either nil, or a string that names an ActiveSupport::TimeZone.
試したこと
attributes_before_type_cast
メソッドを使い、以下の2点を型が変換される前と後でどのように変わっているかを調べてみました。
- ユーザーが新規作成された日時
- 投稿が新規作成された日時
# ユーザーが新規作成された日時 [1] pry(main)> User.find(1).created_at User Load (1.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 => Thu, 28 Apr 2022 05:50:11 UTC +00:00 [2] pry(main)> User.find(1).created_at_before_type_cast User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 => 2022-04-28 14:50:11 +0900
# 投稿が新規作成された日時 [3] pry(main)> Post.find(1).created_at Post Load (0.6ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1 LIMIT 1 => Thu, 28 Apr 2022 06:08:25 UTC +00:00 [4] pry(main)> Post.find(1).created_at_before_type_cast Post Load (0.6ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1 LIMIT 1 => 2022-04-28 15:08:25 +0900
UTC から JST に変換され、書式も変わっています。しかしこれは以下の2点が動いた結果かと思いますので、問題はないのではないかと思いました。
- application.rb で指定したタイムゾーン:
config.active_record.default_timezone = :local
- schema.rb にある datetime 型:
t.datetime "created_at", null: false
こちら数日前に調べた時は created_at
は JST、 created_at_before_type_cast
は +0900
で表示されていたのですが、今日調べると created_at
はなぜか UTC で表示されていました…。
またサーバーの再起動、 rails tmp:clear
、 Rails.cache.clear
も試しましたが挙動は変わりませんでした。
どなたかご助言を頂けますと有難いです。
補足情報(FW/ツールのバージョンなど)
ruby 2.6.8p205 (2021-07-07 revision 67951) [x64-mingw32]
RubyGems 3.0.3.1
Rails 5.2.6
mysql Ver 14.14 Distrib 5.7.36, for Win64 (x86_64)
夜中の 00:00 を回って明らかになった点
1. ビューのエラーメッセージに関する場合分け
- GMT+01:00よりも早いタイムゾーンを選択した場合、いかなるタイムゾーンであっても「今日」を選択すると
Date must be greater than today
のエラーが出る。 4月28日23:59までは「28日」を選択するとエラーが発生。4月29日00:00 (夜中) を回ると「28日」ではエラーが消えるが「29日」を選択するとエラーが発生するようになる(GMT+01:00 でも GMT+09:00 でも同じ)。 - GMT+00:00よりも遅いタイムゾーンを選択した場合、いかなるタイムゾーンであっても「今日」を選択しても問題なく動く。4月29日00:00 (夜中) を回った後「28日」は「昨日」になるので、これを選択するとエラーが発生するかと思いきや、
Date must be greater than today
が出ない。何時を過ぎるとエラーが出るようになるのか、要確認。
2. Date.current がタイムゾーン毎にどのような表示になっているか
タイムゾーンに東京(GMT+09:00)を選択した場合(01:00AM に確認)。
[1] pry(#<Post>)> Date.current => Fri, 29 Apr 2022 [2] pry(#<Post>)> Time.current => Fri, 29 Apr 2022 01:00:39 JST +09:00 [3] pry(#<Post>)> Time.now => 2022-04-29 01:00:43 +0900
タイムゾーンにモスクワ(GMT+03:00)を選択した場合(01:02AM に確認)。
[1] pry(#<Post>)> Date.current => Thu, 28 Apr 2022 [2] pry(#<Post>)> Time.current => Thu, 28 Apr 2022 19:02:28 MSK +03:00 [3] pry(#<Post>)> Time.now => 2022-04-29 01:02:32 +0900
これを見ると Date.current
は Rails sysem の timezone +09:00 で動いている訳ではなく、各タイムゾーンのリアルな時間(モスクワであれば +03:00)で判定されているようです。
ですが以下が希望の仕様ですので、これは希望していた動きです。
海外にいるユーザーはそれぞれのタイムゾーンの時間で(UTC や JST を気にすることなく)イベント開始時間を設定出来る。他のユーザーにはタイムゾーン毎に変換された日時で各々のビューに表示される。
3. タイムゾーンを東京 (GMT+09:00) に設定してイベントを投稿してみた結果
データベースには UTC で保存されておりました。
# イベントを作成した日時 [1] pry(main)> Post.find(1).created_at Post Load (1.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1 LIMIT 1 => Thu, 28 Apr 2022 15:43:13 UTC +00:00
↑ 日本時間で 4月29日0:43 にイベントを作成したので、9時間遅い UTC に直すと 4月28日15:43 という表示で合っています。
# イベントが開始される日時 [2] pry(main)> Post.find(1).date Post Load (0.7ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1 LIMIT 1 => Fri, 29 Apr 2022 15:00:00 UTC +00:00
↑ 日本時間 (GMT+09:00) で 4月30日6:00(AM) にイベントを開始すると投稿したので、9時間遅い UTC に直すと 4月29日21:00(PM) になるはず…。15:00 とは何の時間なのかが分かりません。
タイムゾーンに関する設定
タイムゾーンに関しては以下の流れで実装しております。
こちらの記事を参考に記述致しました。
user.rb
Ruby
1 validates :time_zone, inclusion: { in: ActiveSupport::TimeZone.all.map(&:name) }, presence: true, on: :create
application_controller.rb
Ruby
1class ApplicationController < ActionController::Base 2 before_action :set_current_user 3 4 # タイムゾーン関連。 5 around_action :user_time_zone, if: @current_user 6 7 def set_current_user 8 @current_user = User.find_by(id: session[:user_id]) 9 end 10 11 # タイムゾーン関連。 12 private 13 def user_time_zone(&block) 14 Time.use_zone(@current_user.time_zone, &block) 15 end 16end
タイムゾーンでの日付の確認 (2)
- (GMT-01:00) で 4/29 になるタイムゾーン:Cape Verde (GMT-01:00)
(日本時間 4/30 09:06 に確認、現地時間は 4/29 23:06 (PM))
[1] pry(#<Post>)> Date.current => Fri, 29 Apr 2022 [2] pry(#<Post>)> Date.today => Sat, 30 Apr 2022 [3] pry(#<Post>)> Date.yesterday => Thu, 28 Apr 2022 [4] pry(#<Post>)> Time.current => Fri, 29 Apr 2022 23:06:53 -01 -01:00
- (GMT+01:00) で 4/30 になるタイムゾーン:Amsterdam (GMT+01:00)
(日本時間 4/30 09:09 に確認、現地時間は 4/30 01:09 (AM))
*サマータイム中で実際は (GMT+2:00) の為、現地時間は 4/30 02:09 (AM)。
[1] pry(#<Post>)> Date.current => Sat, 30 Apr 2022 [2] pry(#<Post>)> Date.today => Sat, 30 Apr 2022 [3] pry(#<Post>)> Date.yesterday => Fri, 29 Apr 2022 [4] pry(#<Post>)> Time.current => Sat, 30 Apr 2022 02:09:21 CEST +02:00
- (GMT-04:00) で 4/29 になるタイムゾーン:Samara (GMT-04:00)
(日本時間 4/30 09:16 に確認、現地時間は 4/30 20:16 (PM))
[1] pry(#<Post>)> Date.current => Fri, 29 Apr 2022 [2] pry(#<Post>)> Date.today => Sat, 30 Apr 2022 [3] pry(#<Post>)> Date.yesterday => Thu, 28 Apr 2022 [4] pry(#<Post>)> Time.current => Fri, 29 Apr 2022 20:16:06 -04 -04:00
- (GMT+04:00) で 4/30 になるタイムゾーン:Samara (GMT+04:00)
(日本時間 4/30 09:17 に確認、現地時間は 4/30 04:17 (AM))
[1] pry(#<Post>)> Date.current => Sat, 30 Apr 2022 [2] pry(#<Post>)> Date.today => Sat, 30 Apr 2022 [3] pry(#<Post>)> Date.yesterday => Fri, 29 Apr 2022 [4] pry(#<Post>)> Time.current => Sat, 30 Apr 2022 04:17:58 +04 +04:00
- (GMT-09:00) で 4/29 になるタイムゾーン:Alaska (GMT-09:00)
(日本時間 4/30 09:19 に確認、現地時間は 4/29 15:19 (PM))
*サマータイム中で実際は (GMT-8:00) の為、現地時間は 4/29 16:19 (PM)。
[1] pry(#<Post>)> Date.current => Fri, 29 Apr 2022 [2] pry(#<Post>)> Date.today => Sat, 30 Apr 2022 [3] pry(#<Post>)> Date.yesterday => Thu, 28 Apr 2022 [4] pry(#<Post>)> Time.current => Fri, 29 Apr 2022 16:19:52 AKDT -08:00
- (GMT+09:00) で 4/30 になるタイムゾーン:Tokyo (GMT+09:00)
(日本時間 4/30 09:22 に確認、現地時間は 4/30 09:22 (AM))
[1] pry(#<Post>)> Date.current => Sat, 30 Apr 2022 [2] pry(#<Post>)> Date.today => Sat, 30 Apr 2022 [3] pry(#<Post>)> Date.yesterday => Fri, 29 Apr 2022 [4] pry(#<Post>)> Time.current => Sat, 30 Apr 2022 09:22:22 JST +09:00

回答1件
あなたの回答
tips
プレビュー