What is the best way with Rails to have a “time” attribute (selected by the user) which is supposed to always be displayed as the same “static” time value?
(Meaning: It should always show the same time, for example “14:00”, completely independently of any user’s time zone and/or DST value.)
Until now, I have tried the following setup:
In the MySQL database, I use a field of the type time (i.e. with the format: 14:00:00)
In the Rails view, I use the helper time_select (because it’s really handy)
However, it seams that with this approach, Rails’ ActiveRecord will treat this time value as a full-blown Ruby Time object, and therefor convert the value (14:00:00) to the default time zone (usually set to ‘UTC’) for storage and then convert it back to the user’s time zone, for the view. And if I’m not mistaken, this also means that the fluctuating DST value will make the displayed time value fluctuate throughout the year (and the same happens if the user moves to another time zone).
So what is the best way to manage a simple “static” time attribute with Rails?
If you don't want any time related functionality, why not save it as an string field. Since from your question description its evident that functionalities such as timezone doesn't effect your use case, so just make it a normal VARCHAR(8) and save the value as a string and parse it such as Time.now.strftime("%H:%M:%S") before saving it to the database, you can also write this logic inside your ActiveRecrd model class
def static_time=(value)
super(value.strftime("%H:%M:%S"))
end
you can somewhere in the code say model_object.static_time=Time.now and this will automatically parse it, if you want to get the time as a ruby object retaining the format you can simply do it defining a custom getter.
def static_time
current_time = Time.now
time_keys = [:hour, :min, :sec]
current_time.change(**Hash[time_keys.zip(super.split(":"))])
end
Related
PostgreSQL supports several "Special Date/Time Inputs", strings that it interprets upon execution. Eg, 'now' means "current transaction's start time", and 'infinity' means "later than all other timestamps".
ActiveRecord does not seem to understand these - eg, SomeRecord.update!(updated_at: DateTime.current) works, but SomeRecord.update!(updated_at: 'now') tries to execute the UPDATE query with a NULL.
These special strings do work in Rails fixtures, because they go straight to the database. But is there a way to use them with an instantiated model?
You can set the value with raw_write_attribute(:updated_at, 'now')
Use .update_column
Eg, some_record.update_column(:updated_at, 'now').
Warning - this bypasses validations, callbacks, and normal setting of timestamps; it just issues an immediate UPDATE query.
Despite that, ActiveRecord contributor Sean Griffin said "update_column is probably the best 'hacky' answer" and that special string values like this aren't really well-supported and maybe shouldn't be. (I think I agree - it would make validation really awkward, both for ActiveRecord and in your own models.)
A compromise to get validations, etc
Given that this is a "direct to database" method, one possible compromise to avoid skipping validations and callbacks would be:
ActiveRecord::Base.transaction do
record.updated_at = Time.current # temporary value
record.save! # with validations, etc
record.update_column(:updated_at, 'now') # final value
end
Caveats for the console
A couple of other caveats I noticed for doing this in the console:
Inspecting the record afterwards, I saw 'now' as the value for created_at. Only after doing some_record.reload did I see the timestamp.
Doing this repeatedly didn't seem to give a new timestamp. I think that's because 'now' is "current transaction's start time", and maybe each rails console session uses one transaction. If I started a new console, I got a new value for 'now'
I want to initialize a variable in rails and use it in a controller's action, but the variable should have a fixed value when the server starts. I have tried defining it inside a controller's action and it get's the same initialized value for every request. For example, i want to initialize a date.now and have the same date after 15 days also.
Update
I am implementing a coming soon page in which a timer is shown 15 days from now. If i implement it in a action inside a controller, it shows new date every time the action is invoked.
Please Help
If you want to create a CONSTANT in rails then you can simply put it into the initializer file. For eg, create a file name constants.rb inside initializer:
#config/initializers/constants.rb
TEST_VALUE = 10
And to access this CONSTANT from your controller, just call for TEST_VALUE. for eg,
#controllers_path/some_controller.rb
.....
def some_def
#value = TEST_VALUE # this will be enough to fetch the constants.
end
But, make sure you restart your server after changing the intializer.
You're looking to create a constant, which is basically a variable which doesn't change its value
You'd typically set these with initializers:
#config/initializers/your_initializer.rb
YOUR_CONSTANT = your_date
To maintain a persistent date, you'll have to give some more context on what you're using it for. It will be difficult to create this each time Rails loads (how to know which Time.now to use), so giving us more info will be a good help
You can also use an opposite approach. Assuming you should know the date when the new feature comes (for example 2014-04-04 18:00) you can just find a number of seconds left till the target date:
#seconds_left = Time.parse('2014-04-04 18:00').to_i - Time.now.to_i
then pass it to client side and implement a timer. So you'll just need to store a string representation of a target date.
Obvious disadvantage of this approach is that you should adjust that target time each time you want to introduce a new feature.
I'm having an issue with a date format. I have a time picker that has the date in a funky format (well, it's a nice format, actually, but not to the computer). I'm trying to have Chronic parse the date so that it can be saved properly.
At first, I was doing, in the create action of my controller:
params[:event][:start] = Chronic.parse(params[:event][:start])
but if and when validation fails, it sends the parsed value back to the view, and my datetimepicker is all botched, then.
So, I thought... callback? In my model, I added:
private
def date_change
self.start = Chronic.parse(self.start)
end
I tried before_save, before_validation, after_validation... but nothing seems to get that date formatted correctly.
As it stands, I keep getting ArgumentError in EventsController#create - Argument out of range. I assume that's because the database is expecting a properly formatted datetime object.
Any idea on how I can accomplish my goal, here, of not changing the params, but still being able to save a properly formatted object?
I'm guessing that the problem is occurring the the start= mutator method that ActiveRecord supplies. If you're doing things like this in your controller:
#event.update_attributes(params[:events])
#event = Event.create(params[:event])
#...
then create and update_attributes should call start= internally. That should allow you to put the Chronic stuff in your own start=:
def start=(t)
super(Chronic.parse(t))
end
You might need to adjust that for non-String ts, I'm not sure what Chronic.parse(Time.now), for example, would do. You could also call write_attribute(:start, Chronic.parse(t)) or self[:start] = Chronic.parse(t) if you didn't want to punt to super.
Note that before_validation and similar handlers will be called too late to bypass whatever default string-to-timestamp conversion ActiveRecord is doing but a mutator override should happen at the right time.
Alternatively, you could parse the time in the controller with something like this:
event = params[:events].dup
events[:start] = Chronic.parse(events[:start])
#event = Event.create(event)
Assumption is the mother of all mess ups :)
are you sure the callback is hit? Because if it would, and the error occurred (like it did), wouldn't it still send back the incorrect data (because parsed) back to the view? In case of doubt: log something to make sure it is hit.
are you sure which field causes the Argument out of range error.
Most cases bugs are so hard to find/fix because we assume we know the error, but we are looking at the error in the wrong way.
Easy ways to test which attribute causes the error:
open rails console, build an object with the parameters, save it, and ask the errors. Something like
e = Event.new(params[:event]) # copy params verbatim from your logfile
e.save
e.errors
and that will display which field causes the error.
Alternatively: use pry and add a line binding.pry just after the save, so you inspect the errors (more info)
Answer (assuming your assumption was correct)
I see two options to do what you want:
use the after_validation callback, if you are sure the data will always be correct, and correctly parsed by Chronic. This way if validation is passed, then convert the field and normally nothing can go wrong anymore, and the value is never sent to the browser again.
Note: if some other attribute is causing the error, this callback is never hit, of course. Because it does not pass the validation.
use a virtual attribute, e.g. start_str, which is a visual representation of your start, and
before_save convert it to start. It does not really matter that much here, because if validation fails, you just show start_str and not the "real" start field.
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.
I have to compare the date in rails to get the values after that date I have send the date as "2013-03-04T06:26:25Z"but actually the record in the db contains date as follows 2013-03-04 06:26:25.817149 so when i check with the date it also returns that record but i want records after that date. how can i remove the milliseconds from the db? please help me.
I had a similar problem
Update your time object like this before sending it to the database :
time.change(:usec => 0)
Since ruby 1.9 you can use round
t = Time.now.round
t.usec # => 0
I was also having this problem, however when I was applying the change as indicated in #Intrepidd's answer, it wasn't affecting the microseconds of my time object.
Please note that (at least currently) the :usec key only works with the Time#change method, and not with the DateTime#change method.
The DateTime#change method ignores the keys that it doesn't accept, so you wouldn't be able to tell that your attempted change of the microseconds didn't work unless you inspected the object further (such as with DateTime#rfc3339(9)).
So before you attempt this change, make sure that you are working with a Time object, not a DateTime object.