Rails 3: setting the timezone to the current users timezone - ruby-on-rails

I put this in Application Controller:
before_filter :set_timezone
def set_timezone
Time.zone = current_user.time_zone
end
But I always get the error:
undefined method time_zone for #<User:0xa46e358>
and I just don't know why...
I hope someone can help

Further to Jesse's answer, I should add that you can generally avoid adding a new column in db and just create a custom method in user model and make use of cookie to get the user's
timezone:
in client-side (js):
function set_time_zone_offset() {
var current_time = new Date();
$.cookie('time_zone', current_time.getTimezoneOffset());
}
in Application Controller:
before_filter :set_timezone
def set_timezone
min = request.cookies["time_zone"].to_i
Time.zone = ActiveSupport::TimeZone[-min.minutes]
end

Max -- the ryandaigle.com article you mentioned links to this writeup where you need to create a migration to add "time_zone" as an attribute to the user
(this is from the article, in rails 2.x syntax)
$ script/generate scaffold User name:string time_zone:string
$ rake db:migrate
later
<%= f.time_zone_select :time_zone, TimeZone.us_zones %>
That's why your .time_zone is returning a method_missing -- you haven't stored the time_zone on the user yet.

function set_time_zone_offset() {
var current_time = new Date();
$.cookie('time_zone', current_time.getTimezoneOffset());
}
This is not correct, because time offset is not constant, it depends on daylight saving time periods.
Rails expects the standard time offset when calling ActiveSupport::TimeZone[-min.minutes].
ex: in France at date 09/03/2013 10:50:12 +02:00, your javascript will return -120 as offset where ActiveSupport will need -60 to resolve France timezone.
Then you need to check if this is a daylight saving time period in JS then if this is the case you will have to substract one hour to the offset to get the right value used by Rails.

Related

Is created_at in Rails by default Time.now.utc.to_date?

When Rails creates an active record and inserts it, is the created_at value practically the same as Time.now.utc.to_date?
In most cases yes, but it depends on the default timezone configuration option.
ActiveRecord::Timestamp code:
def current_time_from_proper_timezone
default_timezone == :utc ? Time.now.utc : Time.now
end
You can change timezone setting in:
config.active_record.time_zone_aware_attributes = false
If you meant to_date, then no, in the worst case it could be nearly 24 hours off.
If you meant to_datetime, then I believe it will be the same to the second. But note that if you call Time.now immediately before or after creating a record it may not match to the second. I'm curious to know why you need to convert to a DateTime though.
Just test it yourself (let's say your AR class is Post):
dtm_before = Time.now.to_datetime
post = Post.create!(attributes)
dtm_after = Time.now.to_datetime # zone does not matter!
# these differences should be tiny
dtm_before.to_time - post.created_at
dtm_after.to_time - post.created_at
I said the zone doesn't matter because when you're doing time arithmetic, zones are automatically taken into account. Example:
# let's assume your local TZ-offset isn't zero
t = Time.now
t == t.getutc # true
t - t.getutc # 0.0 (these are the exact same instant)

How can I validate and parse an ISO 8601 timestamp with user's time zone?

I need to be able to receive a user-input timestamp, with an optional time zone component, validate that is it a valid ISO 8601 time representation, and parse it according to the user's configured time zone.
I'm using Rails 4.2.6 on Ruby 2.3. I had hoped that Time.zone (ActiveSupport::TimeZone) would have an equivalent implementation to Time::iso8601 so that I could rescue ArgumentError exceptions to determine if the user input was a valid ISO 8601 time representation. Then I could do something like:
user_time_zone = 'America/Los_Angeles' # Would actually be from user's stored settings
params = {when: '2016-04-01T01:01:01'} # Would actually be from user input
# Would actually use Time::use_zone in around_action filter
Time.use_zone(user_time_zone) do
timestamp = Time.zone.iso8601 params[:when]
end
But, alas, no such method exists. And I can't find an equivalent one.
I can't use Time.zone.parse, because it treats ambiguous dates as valid (e.g. Time.zone.parse '04/11/16' # => Tue, 16 Nov 0004 00:00:00 LMT -07:52).
The best alternative I've been able to come up with so far is:
Time.use_zone(user_time_zone) do
old_tz = ENV['TZ']
ENV['TZ'] = Time.zone.name
timestamp = Time.iso8601 params[:when] # => 2016-04-01 01:01:01 -0700
ENV['TZ'] = old_tz
end
But this is ugly, messing around with an environment variable this way doesn't feel proper, and it and certainly isn't Rails-like. How can I validate and parse the time according to the user's time zone in a Rails way?
I suggest that you simply split the assignment into two steps: validate the ISO8601 format first and if valid, parse it:
user_time_zone = 'America/Los_Angeles'
params = { when: '2016-04-01T01:01:01' }
begin
Time.iso8601(params[:when]) # raises ArgumentError if format invalid
rescue ArgumentError => e
puts "invalid time format"
return
end
Time.use_zone(user_time_zone) do
timestamp = Time.zone.parse(params[:when])
end
I think you can still use an around_action for your use case. That's what I use and it works well for me.
In my ApplicationController I have:
class ApplicationController < ActionController::Base
around_action :set_time_zone
def set_time_zone
old_time_zone = Time.zone
Time.zone = user_time_zone
yield
ensure
Time.zone = old_time_zone
end
end
Any calls to Time will use the user's time zone within the scope of the request.

Ruby - Getting the next Daylight Savings change

I know there is a method to determine if a certain time is on Daylight Savings Time (Time.now.dst?) but is there a method to give us the next date when Daylight Savings will change?
For example, Google returns Sunday, November 1 as the next Daylight Savings Time change in 2015.
Since these are dates that are based on other values, like the timezone you are working with, it requires a module like ActiveSupport/TZInfo.
require 'active_support/core_ext/time/zones'
tz = TZInfo::Timezone.get('US/Pacific')
# pick a timezone to work with
tz.current_period #returns an object of the current period
=> #<TZInfo::TimezonePeriod: #<TZInfo::TimezoneTransitionDefinition:
#<TZInfo::TimeOrDateTime: 1425808800>,#<TZInfo::TimezoneOffset: -28800,3600,PDT>>,
#<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1446368400>,
#<TZInfo::TimezoneOffset: -28800,0,PST>>>
tz.current_period.local_start.to_s
# => "2015-03-08T03:00:00+00:00"
tz.current_period.local_end.to_s
# => "2015-11-01T02:00:00+00:00"
One thing I haven't figured out is that since regular Ruby Core does this:
Time.now.dst?
# => true
Where is it getting this info? I found the TZInfo classes through ActiveSupport. Is Ruby just getting a boolean value from the OS?
How about this extension of the Time class:
class Time
class << self
def next_dst_change
startdate = Date.today
enddate = Date.today.end_of_year
match = Date.today.to_time.dst? ? false : true
startdate.upto(enddate).find { |date| date.to_time if date.to_time.dst? == match }
end
end
end
Now you can do Time.next_dst_change. You can apply this on your own timezone only but it solves your problem.

Rails: Creating a Scope for Last Month

I have a Rails 3.2 app with an Article model that has a field for date. I want to create a scope that will retrieve all records from last month. The problem I'm having is that my current scope is not including the first day of the month.
# article.rb
scope :last_month, lambda { { conditions: { date: last_month_range } } }
private
def self.last_month_range
1.month.ago.beginning_of_month..1.month.ago.end_of_month
end
When I run this it does this:
SELECT * FROM 'articles' WHERE ('articles`.'date' BETWEEN '2013-07-01 07:00' AND '2013-08-01 06:59:59')
When I look at the actual results it only starts with the articles with a date of 07-02-2013.
However, if I change the code to:
def self.last_month_range
(1.month.ago.beginning_of_month - 1.day)..1.month.ago.end_of_month
end
It does this:
SELECT * FROM 'articles' WHERE ('articles`.'date' BETWEEN '2013-06-30 07:00' AND '2013-08-01 06:59:59')
In that case it pulls in articles with dates of 6/31/2013.
Can someone please recommend a fix? Thank you!
ActiveRecord does the conversion automatically to UTC, unless you have specified otherwise. You need to convert the range to UTC.
def self.last_month_range
1.month.ago.utc.beginning_of_month..1.month.ago.utc.end_of_month
end
I got bit by a bug in the accepted answer:
Time.now.utc # => 2015-05-01 01:18:57 UTC
1.month.ago.utc.beginning_of_month #=> 2015-03-01 00:00:00 UTC
The problem is that the time is being converted to utc after the month is subtracted.
Here's a correct solution:
def self.last_month_range
Time.now.utc.prev_month.beginning_of_month..Time.now.utc.prev_month.end_of_month
end

How to be sure that Date.today is in Pacific Time Zone?

I am tracking user activity:
def track
UserActivityTracker.new(date: Date.today.to_s).track
end
#application.rb
config.time_zone = 'UTC'
How to be sure that days are tracked in the Pacific Time (US & Canada)
time zone.
I don't want to change time zone in application.rb
Rails will store your data in the db using UTC (which is a good thing)
i don't think changing config.time_zone on an existing app is a good idea, the UTC default is probably best
When rails pulls data from the db using ActiveRecord, it will convert datetimes based on the Time.zone settings for that request
Date.today
# => server time, rails does not convert this (utc on a typical production server, probably local on dev machine)
Time.zone.now.to_date
# => rails time, based on current Time.zone settings
You can set the current users time zone in a before_filter on ApplicationController, then when you display datetimes use the I18n helpers
I18n.localize(user_activity_tracker.date, format: :short)
# => renders the date based on config/locals/en.yml datetime:short, add your own if you wish
# => it automatically offsets from UTC (database) to the current Time.zone set on the rails request
If you need to display a time that is not the same as the current Time.zone request setting, use Time.use_zone
# Logged on user is PST timezone, but we show local time for an event in Central
# Time.zone # => PST
<% Time.use_zone('Central Time (US & Canada)') do %>
<%= I18n.l(event.start_at, format: :time_only_with_zone) %>
<% end %>
when saving data, don't bother doing a conversion, let rails save it as UTC, you can display the value in any timezone you wish using the helpers
see also:
http://railscasts.com/episodes/106-time-zones-revised
http://guides.rubyonrails.org/i18n.html
http://api.rubyonrails.org/classes/Time.html#method-c-use_zone
http://www.ruby-doc.org/core-2.0/Time.html#method-i-strftime
Replace config.time_zone this way :
config.time_zone = 'PST'
If you don't want to change all the dates, you can use Time.zone_offset
good_date = bad_date + Time.zone_offset('PST')
You can add the offset in the initialize or in a before_xxx callback.

Resources