How convert a string into a ActiveSupport::TimeWithZone? - ruby-on-rails

I need to update a field (called updated_at). The field in MySQL is of type datetime, and the class is ActiveSupport::TimeWithZone. But the dates are strings like "10/17/2008". I used "10/17/2008".to_date (And I intend .to_time and .to_datetime), and even if in console the ActiveRecord class save successfully, the field in the database still is the current date.

OK.. let's take them one at the time.
First, it is not recommended to set a field name updated_at, since this is a "magic" field that is automatically populated by Rails.
If you want to disable this functionality, you may:
class Foo < ActiveRecord::Base
self.record_timestamps = false
end
in your class, but this will also disable created_at fields.
The best option is to add a new field (e.g. my_updated_at) as date in the database, and then Rails will automatically handle conversions, meaning that the next snippet will work:
Foo.new({:my_updated_at => "10/17/2008"})
Second, the answer on how to parse a string to ActiveSupport::TimeWithZone is:
ActiveSupport::TimeZone['UTC'].parse("10/17/2008")
but I don't think this will help you (of course, change UTC with your current date/time).

Simply
date_as_string = "2008-10-17"
ActiveSupport::TimeZone['UTC'].parse(date_as_string)
# => Fri, 17 Oct 2008 00:00:00 UTC +00:00
And just to confirm that it worked..
ActiveSupport::TimeZone['UTC'].parse(date_as_string).class
# => ActiveSupport::TimeWithZone

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 )

Why does Ruby parse 'foo' into 01.01.1970 within a Date type

I do have a model within a Rails application with the following definition (I am using Mongoid):
field :date, type: Date
I included the field within the view as a text_field (f.text_field). Everything works fine, if I do enter a valid date. But when I enter some text in this field (e.g. 'foo'), then the date gets parsed as 01.01.1970. I am not parsing the date manually, rather I use the following code:
#offer = Offer.new(offer_params)
I also checked on the Ruby console:
>> offer = Offer.new
...
>> offer.date = 'foo'
"foo"
>> offer.date
Thu, 01 Jan 1970
>> offer.date.is_a? Date
true
How can I prevent this behavior so that the record can't be saved and a validation error gets shown to the user?
Thanks for your help!
You can modify the Offer initializer so that it will use the Date::parse method for parsing your date. This will throw an ArgumentError, when the parameter you supply to it is not a valid date format.
You may have to convert that to seconds since 1.1.1970 as that seems to be the format used for storing your dates in your database. Have a look at Date#strftime for that.

Displaying invalid attribute before type cast

I have a model with date attribute. I built a custom validation using [attribute]_before_type_cast so that a date like '31.02.2013' make validation failing.
The problem is that:
#invoice.order_date = '31.02.2013'
#invoice.order_date #=> Sun Mar 03 00:00:00 +0100 2013
So on the view layer user gets message about invalid date but the invalid attribute is automatically changed to 03.03.2013 which is infact a proper date so it is kind of non-sense.
Is there a way to stop that type_cast and display that attribute with invalid date?
I think you can create a custom setter method to prevent the changing of the attribute.
def order_date=(date)
date = your_validate_and_format_method(date)
self.send(:write_attribute, :order_date, date)
end
In the your_validate_and_format_method you can do something like validate, format and you can determine to keep the old value or use the new one.

Saving a Rails Record with a specific Timezone

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

Resources