There’s pretty good info out there about using time zones in Rails, and Rails itself does a lot of the heavy lifting. The Railscast pretty much covers it. It’s only missing a discussion of using Javascript to figure out the client browser’s time zone.
<h1 id="time-zone-from-the-browser">Time Zone from the Browser</h1> To get the time zone from the browser, use the detect_timezone_rails gem. The instructions give you what you need to know to set up a form with an input field that will return the time zone that the browser figured out. That would work perfectly if you were implementing a traditional web site sign-up/sign-in form.
However, I needed to do something different. Since I’m using third party identity providers (Google, Twitter, Facebook, etc.) via the excellent Omniauth gems, I needed to be able to put the time zone as a parameter on the URL of the identity provider’s authorization request. Omniauth arranges for that parameter to come back from the identity provider, so it’s available to my app’s controller when I set up the session.
To add the parameter, I added this jQuery script to the head of the welcome page:
<script type="text/javascript">
$(document).ready(function () {
$('a.time_zone').each(function () {
this.href =
this.href + "?time_zone=" +
encodeURIComponent($().get_timezone());
});
});
</script>
This added the time zone, appropriately escaped, to
the URL for the identity provider (the href of the <a>
elements). This
worked because I had set each of the links to the identity providers to
have class="time_zone"
, like this:
<%= link_to image_tag("sign-in-with-twitter-link.png", alt: "Twitter" ), "/auth/twitter" , class: "time_zone" %>
In the controller, I did this (along with all the other logging in stuff):
if env['omniauth.params'] && env['omniauth.params']['time_zone']
tz = Rack::Utils.unescape(env['omniauth.params']['time_zone'])
if user.time_zone.blank?
user.time_zone = tz
user.save!
flash.notice = "Your time zone has been set to #{user.time_zone}." +
" If this is wrong," +
" please click #{view_context.link_to('here', edit_user_path(user))}" +
" to change your profile."
elsif user.time_zone != tz
flash.notice = "It appears you are now in the #{tz} time zone." +
" Please click #{view_context.link_to(edit_user_path(user), 'here')}" +
" if you want to change your time zone."
end
else
logger.error("#{user.name} (id: #{user.id}) logged in with no time zone from browser.")
end
Of course, you may want to do something different in your controller.
<h1 id="testing-time-zones">Testing Time Zones</h1>
However you get your time zones, you need to be testing your app to see how it works with different time zones. YAML, at least for a Rails fixture, interprets something that looks like a date or time as UTC. So by default, that’s what you’re testing with. But that might not be the best thing.
I had read that a good trick for testing is to pick a time zone that isn’t the one your computer is in. Finding such a time zone might be hard if you have contributors around the world. I like the Samoa time zone for testing: Far away from UTC, not too many people living in the time zone, and it has DST.
If you want a particular time zone in your fixtures, you have to use ERB. For example, in my fixtures I might put this:
created_at: <%= Time.find_zone('Samoa').parse('2014-01-30T12:59:43.1') %>
And in the test files, something like this:
test 'routines layout' do
Time.use_zone('Samoa') do
expected = Time.zone.local(2014, 1, 30)
# ...
end
end
<h1 id="gotchas">Gotchas</h1>
I found a few gotchas that I hadn’t seen mentioned elsewhere:
- Rails applies the time zone magic when it queries the database, so if you change your time zone after you retrieve the data, then you have to force a requery, or the cached times will still be in the model. Shouldn’t be a problem when running tests, but is when using the console to figure things out
- You can’t use database functions to turn times into dates, as these won’t use the time zone. No
group by to_date(...)
or anything like that