Saving a Rails Record with a specific Timezone - ruby-on-rails

How to save an event in Berlin with its own specific timezone and the next event in Tijuana with a different one?
The user is asked to choose a City for the event, as a source for e.g. +02:00.
I would like to receive a time code like this:
eventstart = "2011-07-22T18:00:00+02:00"
How would you go about creating that form?
UPDATE:
Realized saving as standard UTC is fine for many reasons. So now I am altering a time string in the view to present a *distance_to_time_in_words* for eventstart, depending on the user's local time.
Event in Tijuana, viewing from Berlin time:
old_zone = Time.zone
#=> "Berlin"
Time.zone = "Tijuana"
t = Time.zone.parse(eventstart.to_s[0..-7])
#=> 2011-07-22 18:00:00 +08:00
Time.zone = old_zone
#=> "Berlin"
distance_of_time_in_words(Time.now.in_time_zone, t)
#=> "9 hours"
cumbersome, but works for now. Improvement ideas welcome!

Add a time_zone column to your database and use time_zone_select in your form to let user select the time_zone for which he is creating event.
And in the model you can convert the datetime to zone specific datetime and store utc in the database. You can use helper something like below
def local_time(date)
Time.use_zone(self.time_zone) do
Time.zone.at(date.to_i)
end
end

Related

Rails and Postgres problem querying record that was stored in UTC; app Timezone is UTC but record is not being found

I have a Rails 5 app with default Timezone settings (UTC).
I have a record that was created on Thu, 08 Aug 2019 02:12:56 UTC +00:00 but the app is being accessed by someone located at Central Time which means the record was created for them on Wed, 07 Aug 2019 20:12:56 CST -06:00; The app provides a filter and this user is trying to retrieve records from 08/07/2019 and is expecting that this record would be part f the resulting set, date filter is being passed as a Date object.
I have tried several forms to convert this Date object into a form that when passed down to the query it successfully returns the expected results. Note: filter.end_date is an instance of Date with value "Wed, 07 Aug 2019"
Query:
Record.where("records.created_at BETWEEN ? AND ?", filter.start_date.beginning_of_day, filter.end_date.end_of_day)
SQL:
Record Load (4.2ms) SELECT "records".* FROM "records" WHERE (records.created_at BETWEEN '2019-08-07 00:00:00' AND '2019-08-07 23:59:59.999999')
The only way that I have achieved this is when I set Timezone to central time and then pass the query:
This is working
Time.use_zone("Central America") { Record.where("records.created_at BETWEEN ? AND ?", date_filter.end_date.beginning_of_day, date_filter.end_date.end_of_day) }
SQL:
Record Load (4.2ms) SELECT "records".* FROM "records" WHERE (records.created_at BETWEEN '2019-08-07 06:00:00' AND '2019-08-08 05:59:59.999999')
I am wondering if there is a way to avoid the necessity to set a Timezone per user within the app and just do the correct conversion when passing down the date provided in the filter.
Unfortunately there isn't a simple way to ignore the timezone.
If you are getting the users timezone on sign up you could add something like Time.zone(current_user.timezone) if current_user into a before action callback or some other initial startup sequence. The has the disadvantage of making the value mostly static, ie if they are traveling and are in a different timezone your results will still be for your "home" timezone unless you add that as an option in their profile etc.
Alternatively you could set the current timezone dynamically using javascript:
window.addEventListener("submit", function(event) { // listen to all submit events
var form = event.target.closest('form');
var timezoneInput = form.querySelector('input[name=timezone]');
if (timezoneInput) {
timezoneInput.value = moment.tz.guess(); // assuming you are using a time library
}
});
And handle that in your controller:
def index
Time.use_zone(params[:timezone]) do
#records = Record.where("records.created_at BETWEEN ? AND ?", date_filter.end_date.beginning_of_day, date_filter.end_date.end_of_day)
end
end
For a database solution (which is commendable since you use timestamp with time zone), just set the timezone parameter for the database session to the current time zone of the user. Then everything should work as expected.
Whether or not you should store user's timezone in the DB depends on how often you will need to show times in a specific timezone. Best practice is to set your application to use the time zone where most of your users are. If you have not already, you probably want to set this application.rb for example:
config.time_zone = 'Eastern Time (US & Canada)'
For complete list you can run rake time:zones:all
Now make sure you always use zone. Anywhere in your application you will want to replace Time.now with Time.zone.now and Date.today with Date.current or better Time.zone.now.to_date.
If in your case you just want to filter records timestamps (i.e. created_at) I'm guessing you use some kind of datetime picker on the front end in a form etc. There is likely an option to add the users's browser's time offset. Here's an example of using jQuery datetimepicker. The additional format option is O
$(function() {
$("#datetimepicker").datetimepicker({
timepicker: true,
format: 'm/d/Y H:i O',
maxDate: '0'
});
})
Now you will have the datetime with timezone offset value which you can use in your database query. You'll be able to do something like this rough example.
start_time = Time.zone.parse(params[:start_time].to_s)
end_time = Time.zone.parse(params[:end_time].to_s)
Record.where('created_at > ? and created_at < ?', start_time, end_time)
I would also recommend checking out this Railscast, old but relevant.

Convert time zone of datetime returned by f.datetime_select

When I use f.datetime_select in a rails form, it returns a datetime to my controller in UTC, e.g.
2014-06-18T11:00:00+00:00
My local time zone is set to Melbourne (+10) as specified in my application.rb file:
config.time_zone = 'Melbourne'
So when I retrieve datatimes from my database they are automatically converted to Melbourne (+10) timezone, e.g.
2014-06-17 19:00:00 +1000
I want to compare the datetime returned by f.datetime_select with a field in my database. How can I do this?
i.e. how can i change the time zone of the datetime returned by f.datetime select to 'Melbourne' (+10) without changing the actual time? i.e. convert:
2014-06-18T11:00:00+00:00
to
2014-06-18T11:00:00+10:00
All dates stored in database are in UTC time. So when your app get new date from 'params' you basically have 2 options: to save it to the ActiveRecord model, and during that AR will perform all heavylifting of deciding of what timezone was meant.
If you don't want to save data to the model, you have to deal with it yourself. Date select control return just 3 specially formatted strings in params hash.
Let's say I named field in my form 'birthdate'. Controller will get something like:
"<your_model_name>" => {... , "birthdate(3i)" => "<day>", "birthdate(2i)" => "<month>", "birthdate(1i)" => "<year>", ...}
So you could deal with that info something like:
Time.zone.parse("#{ params[:model]['birthdate(3i)'] }-#{ params[:model]['birthdate(2i)'] }-#{ params[:model]['birthdate(1i)'] }")
And yeah I know that it looks ugly, and after some research I surprised that there is no any 'out of the box' solution )

Manipulating TimeWithZone in Rails

I have this rails application that needs to capture a time date and a time zone in one of the forms, for example:
Time zone: 'Hong Kong' as string
Date time: '31/12/2013 23:59' as string
The default time zone for the application is set to 'Melbourne', the application currently accept the date time and automatically convert to '2013-12-31 23:59:00 +1100', which +1100 is the Melbourne time zone offset with daylight saving.
I want the application to:
1) Take '31/12/2013 23:59:00' as a time of the selected time zone, i.e. Hong Kong
2) Convert the Hong Kong time to Melbourne time, i.e. '31/12/2013 23:59:00 +0800' to '01/01/2014 02:59 +1100' and persist into the database.
3) During the conversion in 2), daylight saving will need to be taken care of.
I have written up the following code would do exactly what i want it to do ONLY works in the controller. However, it doesn't work when I move it to the model with before_create filter
time_zone = ActiveSupport::TimeZone.new(params[:time_zone])
date_time = DateTime.strptime(params[:date_time], '%d/%m/%Y %H:%M')
new_date_time = date_time.change(offset: time_zone.formatted_offset(false))
melbourne_date_time = new_date_time.in_time_zone(Time.zone)
The reason it doesn't work in the model is that in the controller, I manually parse the date time string to a date time object. But in the model, rails automatically convert the date time string into a TimeWithZone object... therefore, can't really alter the object...
I have done fair a bit of googling and still can't work out the exact solution.
Any help will be appreciated!
P.S. The application is on Rails 3.2.12. I am planning to run the conversion within a method attached with the before_create filter of a model.
Cheers
You can simply set default time_zone for your rails application.
Open up config/application.rb
set default time_zone by using this
class Application < Rails::Application
config.time_zone = 'Hong Kong'
config.active_record.default_timezone = 'Hong Kong'
end
I have found a (dirty?) way to archive what I want in the model.
User input from the form:
Time Zone - 'Hong Kong' as String,
Date Time - '31/12/2013 23:59:00' as String
config.time_zone = 'Melbourne'# in config/application.rb
self.time_zone # 'Hong Kong' as String
self.date_time # '31/12/2013 23:59:00 +1100' as TimeWithZone
class MyClass < ActiveRecord::Base
before_create :populate_date_time_with_zone
def populate_date_time_with_zone
original_tz = Time.zone # Capture the system default timezone, 'Melbourne'
Time.zone = self.time_zone # Change the timezone setting for this thread to 'Hong Kong'
dt = Time.zone.parse(self.date_time.strftime('%d/%m/%Y %I:%M%p')) #Dirty??
self.date_time = dt.in_time_zone(original_tz) # '01/01/2014 02:59:00 +1100' as TimeWith Zone
end
end
Is there a better way of doing it than printing the date time into a string than parse it again with an altered Time.zone?

How to present Rails form datetime select in different time zone?

I would like to present a datetime select to the user in their preferred time zone but store the datetime as UTC. Currently, the default behavior is to display and store the datetime field using UTC. How can I change the behavior of this field without affecting the entire application (i.e. not changing the application default time zone)?
Update: This is not a per-user timezone. I don't need to adjust how times are displayed. Only these specific fields deal with a different time zone, so I would like the user to be able to specify the time in this time zone.
Here's how you can allow the user to set a date using a specific time zone:
To convert the multi-parameter attributes that are submitted in the form to a specific time zone, add a method in your controller to manually convert the params into a datetime object. I chose to add this to the controller because I did not want to affect the model behavior. You should still be able to set a date on the model and assume your date was set correctly.
def create
convert_datetimes_to_pdt("start_date")
convert_datetimes_to_pdt("end_date")
#model = MyModel.new(params[:my_model])
# ...
end
def update
convert_datetimes_to_pdt("start_date")
convert_datetimes_to_pdt("end_date")
# ...
end
def convert_datetimes_to_pdt(field)
datetime = (1..5).collect {|num| params['my_model'].delete "#{field}(#{num}i)" }
if datetime[0] and datetime[1] and datetime[2] # only if a date has been set
params['my_model'][field] = Time.find_zone!("Pacific Time (US & Canada)").local(*datetime.map(&:to_i))
end
end
Now the datetime will be adjusted to the correct time zone. However, when the user goes to edit the time, the form fields will still display the time in UTC. To fix this, we can wrap the fields in a call to Time.use_zone:
Time.use_zone("Pacific Time (US & Canada)") do
f.datetime_select :start_date
end
There are a couple of options:
Utilize the user's local timezone when displaying data to them. This is really easy with something like the browser-timezone-rails gem. See https://github.com/kbaum/browser-timezone-rails. It is essentially overriding the application timezone for each request based on the timezone detected from the browser. NOTE: it only uses the OS timezone, so it's not as accurate as an IP/geo based solution.
Setup your application timezone so that it is consistent with the majority of your user base. For example: config.time_zone = 'Mountain Time (US & Canada)'. This is a very standard thing to do in rails. Rails will always store the data in the DB as UTC, but will present / load it using the application timezone.
Create a timezone for your user model. Allow users to set this value in their account settings. And, then use a similar approach to that of the above gem does in the application_controller.

How to display the time in user's timezone

I am using rails 3.0.5 and I have created_at and updated_at stored in UTC. Now I want to display the created_at time in users' timezone. I believe it is possible to pick user's timezone from the browser and then convert time to user's timezone.
I am sure rails will have a gem/plugin to take care of something like this. Is there something?
Rails by default converts every date into UTC before storing the value into the database. This means that, regardless the server timezone, you always have UTC dates in your database.
In order to convert the dates into your user's timezone you have basically two possibilities:
server-side approach
client-side approach
Server-side approach
If your site allows registration, you can store the user timezone as user preference. In the user table, store the user timezone. Then create a custom helper you can use to format any date/time into the proper timezone using the in_time_zone method.
> t = Time.current
# => Mon, 23 Dec 2013 18:25:55 UTC +00:00
> t.zone
# => "UTC"
> t.in_time_zone("CET")
# => Mon, 23 Dec 2013 19:25:55 CET +01:00
Your helper may looks like
def format_time(time, timezone)
time.in_time_zone(timezone)
end
I normally also like to output a standard format, using the I18n.l helper
def format_time(time, timezone)
I18n.l time.to_time.in_time_zone(timezone), format: :long
end
Client-side approach
If your site has no registration or you don't want to ask your users for their timezone or you simply want to use the user system timezone, then you can use JavaScript.
My suggestion is to create a custom helper that will print out every time in a proper way so that you can create a generic JavaScript function to convert the values.
def format_time(time, timezone)
time = time.to_time
content_tag(:span, I18n.l(time, format: :long), data: { timezone: timezone, time: time.iso8601 })
end
Now, create a JavaScript function that is performed on DOM load and that will select all the HTML tags with data-time attribute. Loop them and update the value inside the span tag with the proper time in the given timezone.
A simple jQuery example would be
$(function() {
$("span[data-time]").each(function() {
// get the value from data-time and format according to data-timezone
// write the content back into the span tag
});
});
I'm not posting the full code here, since there are plenty of JavaScript time formatters available with a simple search. Here's a few possible solutions
Convert date to another timezone in JavaScript
Convert date in local timezone using javascript
There is a nice gem by Basecamp called local_time for client side rendering - https://github.com/basecamp/local_time. It's great for applications where user is not signed in and it's caching friendly.
You can add this to your application controller to convert all times to the User's timezone:
class ApplicationController < ActionController::Base
around_filter :user_time_zone, :if => :current_user
def user_time_zone(&block)
Time.use_zone(current_user.timezone_name, &block)
end
end
You just have to capture the user's timezone
Assuming that the value you want displayed is coming from the database, :ie started_at and is (as is the default) stored in UTC.
If you have the user's timezone as an offset you can also localize the time by doing:
started_at.in_time_zone(-2)
=> Mon, 24 Feb 2014 23:07:56 GST -02:00
Which then can be munged in all sorts of way to get the parts you want:
started_at.in_time_zone(-2).yesterday
=> Sun, 23 Feb 2014 23:07:56 GST -02:00
started_at.in_time_zone(-2) + 3.days
=> Thu, 27 Feb 2014 23:07:56 GST -02:00
http://api.rubyonrails.org/classes/Time.html#method-c-use_zone
This is what you're looking for :
Time.use_zone(#user.timezone) do
blah blah blah
end
If you'd like to convert your date to a specific Timezone:
deadline.in_time_zone(time_zone)
Here deadline is a date.
In addition, you can find Universal time through your local machine Timezone plus local time and vice verse, like in Karachi - +05:00, you can simply add it to value in Universal time to find time in your time zone or get Universal time from your local time by subtraction of Timezone difference (05:00 in our case) from your local time
My jquery is a rusty, so it took me a little while to figure out how to implement the client-side approach of the accepted answer above.
Here's my solution:
HTML:
<span data-time="<%= last_message_at %>"> </span>
Jquery/Javascript:
<script type="text/javascript">
$(document).ready($(function() {
$("span[data-time]").each(function() {
var timestamp = $(this).attr("data-time");
var localeTimestamp = new Date(timestamp).toLocaleString();
$(this).html(localeTimestamp);
});
}));
</script>
You might want to take a look at these links in addition to the one Ben posted:
http://railscasts.com/episodes/106-time-zones-in-rails-2-1

Resources