We all have to deal with timezones in our Rails app sooner or later (probably sooner). In this blog post I want to share some of the tricks I have learned to deal with timezones effectively in Rails.
Do it sooner
Unless you are confident that you will never need to deal with timezones, you should think about them sooner rather than later, building your application in a way that accommodates for them from the beginning. This is one of those things that can be very difficult to include later, so I don’t think it is a premature optimisation.
Config
Some tutorials and blogposts suggest that you set the timezone in your app configuration to something known e.g. config.time_zone = 'UTC'
. Unless you are only going to be dealing with only that timezone all the time I think this is unnecessary and even misleading. I suggest you don’t worry about setting this at all and leave it to the server timezone.
Use datetime for storing dates
When storing dates in your database always try to use absolute time, e.g. datetime in rails. Don’t use just dates (e.g. 2012-01-12 without the time part) unless you are certain that is what you need, i.e when the date is relative to the user. Add a timezone to the users
Every user should have a timezone attribute, most likely in the database, but in early stages you can just create a method in the User class that returns a known value. Showing the time / dates in the user’s timezone
This is one of the most crucial things, each time you need to show a date / time to a user, you should convert that time to the user’s timezone. For example:
ruby
Time.now.in_time_zone(user.timezone).strftime(....
I18n.l(Time.now.in_time_zone(user.timezone).beginning_of_day)
Just remember to convert the timezone by using in_time_zone()
before anything else.
Querying information relative to the user’s timezone
The same is true for queries that should be relative to the user’s timezone. For example, let’s say we want to find today’s appointments for a user (today as in their timezone):
ruby
starts_at = Time.now.in_time_zone(user.time_zone).beginning_of_day
ends_at = Time.now.in_time_zone(user.time_zone).end_of_day
Appointment.where(starts_at: starts_at).where(ends_at: ends_at)
Or use an around filter.
Another strategy you can use is to always convert the timezone in each request to the user’s timezone. You can do this with an around filter in your application controller.
ruby
class ApplicationController < ActionController::Base
around_filter :set_time_zone
def set_time_zone(&block)
time_zone = current_user.try(:time_zone) || 'UTC'
Time.use_zone(time_zone, &block)
end
end
In this way you don’t need to convert the timezone inside your controllers, although I like the explicitness of using in_time_zone()
.
What if I don’t have logged in users
If your are building a site that needs to show times and you don’t have logged in users, setting the server time zone is not a solution either. You have the following options:
- Use Javascript on the front end to figure out the correct time to display, based on the user’s browser
- Try this gem https://github.com/scottwater/detect_timezone_rails
- Use the user’s IP address to infer the timezone, this can be a hit or miss
Instead of having current_user as nil consider setting current_user to a special user object that represents a non logged in user, in that way this object can respond to normal user methods e.g.
user.time_zone
.
Querying from the front end
When you need to make ajax request where times are relevant the most reliable way is to use the epoch timestamps, this the number of second since January 1st, 1970 in UTC. So this is an absolute number regardless of the browser timezone.
js
var epoch = (new Date()).getTime() / 1000;
The in Rails convert that value to a time:
ruby
Time.at(params[:epoch])
##Testing
If you are building an application that uses server-generated views, testing is very straightforward. You can test that your timezones are working correctly by changing the timezone for the current user and checking that your views show the correct date/time for their timezone.
Integration testing with client side JS
However if you are doing integration testing that involves client side JavaScript, things get more difficult. For example the client side could get the timezone from the browser and then use that for querying the server, this is really hard to mock.
So the best strategy is to use the same timezone across the stack i.e. Browser, user model and server. To do this you need to set the application timezone in your tests to the timezone of the server running the CI. A particular gem that is very useful is timezone_local. If you are using factories you can then set the user timezone to the server timezone like this:
ruby
User.blueprint do
time_zone { TimeZone::Local.get().name }
end
##Conclusion
- In general you don’t need to worry about setting the server timezone.
- You also don’t need to worry about which methods in rails use timezones and which don’t
- Add a timezone property to the user
- Convert the timezone when dealing with users (using in_time_zone)
- Use a consistent timezone across the stack for integration testing that involves client side JS