Accepting timestamp as an integer in Rails - ruby-on-rails

I am trying to POST a json object to the rails app.
One of the field of this object is a a pretty much timestamp. It's saved in a 'timestamp' member in a model.
Rails handles well a lot of datetime formats represented by a string (as example I can send "December 24, 2015 at 9:46:24 PM PST" and it will work).
However, rails will reject an object if I try to send an integer (unix time) timestamp.
Is this standard behavior (or am I missing something)

We can easily do this by defining like a proxy attribute in your model
attr_accessible :integer_timestamp
def integer_timestamp
timestamp.to_time.to_i
end
def integer_timestamp=(value)
self.timestamp = value.blank? ? nil : Time.at(value.to_i)
end

You need to convert your epoch time explicitly.
It can be:
class MyModel < ActiveRecord::Base
def my_attr_name=(timestamp)
dt = begin
timestamp.to_datetime
rescue ArgumentError
DateTime.strptime(timestamp,'%s')
end
write_attribute :my_attr_name, dt
end
end

Related

Overriding an active support method with a ruby refinement

I'd like to use a Ruby Refinement to monkey patch an ActiveSupport method for outputting time in a specific format.
My ultimate goal is to have JSON.pretty_generate(active_record.as_json) print all timestamps in UTC, iso8601, 6 decimals. And I want to have all other timestamp printing behave normally.
This is what I have so far:
module ActiveSupportExtensions
refine ActiveSupport::TimeWithZone do
def to_s(_format = :default)
utc.iso8601(6)
end
end
end
class Export
using ActiveSupportExtensions
def export
puts JSON.pretty_generate(User.last.as_json(only: [:created_at]))
end
end
Export.new.export
Which outputs the following (not what I want).
{
"created_at": "2022-04-05 14:36:07 -0700"
}
What's interesting, is if I monkey patch this the regular way:
class ActiveSupport::TimeWithZone
def to_s
utc.iso8601(6)
end
end
puts JSON.pretty_generate(User.last.as_json(only: [:created_at]))
I get exactly what I want:
{
"created_at": "2022-04-05T21:36:07.878101Z"
}
The only issue is that this overrides the entire applications TimeWithZone class, which is not something I want to do for obvious reasons.
Thanks to Lam Phan comment, it's not possible via a refinement unfortunately.
However I was able to do it by override the default timestamp format.
# save previous default value
previous_default = ::Time::DATE_FORMATS[:default]
# set new default to be the utc timezone with iso8601 format
::Time::DATE_FORMATS[:default] = proc { |time| time.utc.iso8601(6) }
puts JSON.pretty_generate(User.last.as_json(only: [:created_at]))
# set the default back if we have one
if previous_default.blank?
::Time::DATE_FORMATS.delete(:default)
else
::Time::DATE_FORMATS[:default] = previous_default
end

append '01-' to strong parameter on rails controller

I have a form that uses jQuery datepicker for picking just the month and year.
$(".date-input").datepicker({
format: 'mm-yyyy',
startView: "months",
minViewMode: "months"
});
I have postgreSQL db on the backend with datetime column storing this date which apparently needs date in full format 'dd-mm-yyyy' to save it. While POSTing the form to my controller, this is what i came across:
Experience.update(date_from: '02-2016') updates the table with nil value for date_from column
So, the question is how can i append '01-' into this date_from param that i receive from POST request on the controller as Strong parameters.
Use the ISO8601 Date format (2016-10-21).
$(".date-input").datepicker({
format: 'yyyy-mm-dd',
startView: "months",
minViewMode: "months"
});
You can parse it with Date.iso8601(str) or DateTime.iso8601(str).
I would create a model level setter which handles this:
class Experience < ActiveRecord::Base # or ApplicationRecord
def date_from=(val)
super( val.is_a?(String) ? Date.iso8601(val) : val )
end
end
This means you don't have to worry about the params on the controller level. Just whitelist it like any other scalar value and let your model do the work.
I would just store the values as a DATE type (as the first day of the month). Thats gives you the maximum flexibility when comes to DB querying and also a sane format when you pull records so that you don't have to parse a string again.
This has nothing to do with strong parameters. When you get the data from the backend using strong params, you can access the information in the controller. You can try something like this -
def create
datestr = date_params[:datestr]
date = Date.strptime("01-".concat(datestr), "%d-%m-%Y")
Experience.update(date_from: date)
end
private
def date_params
params.require(:dateinfo).permit(:datestr)
end

How can I validate and parse an ISO 8601 timestamp with user's time zone?

I need to be able to receive a user-input timestamp, with an optional time zone component, validate that is it a valid ISO 8601 time representation, and parse it according to the user's configured time zone.
I'm using Rails 4.2.6 on Ruby 2.3. I had hoped that Time.zone (ActiveSupport::TimeZone) would have an equivalent implementation to Time::iso8601 so that I could rescue ArgumentError exceptions to determine if the user input was a valid ISO 8601 time representation. Then I could do something like:
user_time_zone = 'America/Los_Angeles' # Would actually be from user's stored settings
params = {when: '2016-04-01T01:01:01'} # Would actually be from user input
# Would actually use Time::use_zone in around_action filter
Time.use_zone(user_time_zone) do
timestamp = Time.zone.iso8601 params[:when]
end
But, alas, no such method exists. And I can't find an equivalent one.
I can't use Time.zone.parse, because it treats ambiguous dates as valid (e.g. Time.zone.parse '04/11/16' # => Tue, 16 Nov 0004 00:00:00 LMT -07:52).
The best alternative I've been able to come up with so far is:
Time.use_zone(user_time_zone) do
old_tz = ENV['TZ']
ENV['TZ'] = Time.zone.name
timestamp = Time.iso8601 params[:when] # => 2016-04-01 01:01:01 -0700
ENV['TZ'] = old_tz
end
But this is ugly, messing around with an environment variable this way doesn't feel proper, and it and certainly isn't Rails-like. How can I validate and parse the time according to the user's time zone in a Rails way?
I suggest that you simply split the assignment into two steps: validate the ISO8601 format first and if valid, parse it:
user_time_zone = 'America/Los_Angeles'
params = { when: '2016-04-01T01:01:01' }
begin
Time.iso8601(params[:when]) # raises ArgumentError if format invalid
rescue ArgumentError => e
puts "invalid time format"
return
end
Time.use_zone(user_time_zone) do
timestamp = Time.zone.parse(params[:when])
end
I think you can still use an around_action for your use case. That's what I use and it works well for me.
In my ApplicationController I have:
class ApplicationController < ActionController::Base
around_action :set_time_zone
def set_time_zone
old_time_zone = Time.zone
Time.zone = user_time_zone
yield
ensure
Time.zone = old_time_zone
end
end
Any calls to Time will use the user's time zone within the scope of the request.

Rails: active record saving time:type with unwanted format

I have a table call periodo with the attribute hour. i pass my time param in this way
hour = Time.parse( splitLine[1] ) #where splitLine[1] is my time but in string
periodo = Periodo.new(:hour => hour.strftime("%H:%M"))
periodo.save
but active record save the records in this way hour: "2000-01-01 07:00:00" , I already set the format in /config/initializers/time_formats.rb
Time::DATE_FORMATS[:default] = "%H:%M"
Date::DATE_FORMATS[:default] = "%H:%M"
and in en.yml too
en:
hello: "Hello world"
time:
formats:
default: "%H:%M"
date:
formats:
default: "%H:%M"
but my records are still saving the year and month :( what i have to do to save just the hour and minutes ???
greetings
Date formats are only valid within your application, not within the database - they are only responsible for the way time objects are displayed to your users and will not affect the way data is stored.
Unfortunately, there is no such a concept like time only in the database (at least I haven't heard about any, and trust me I did search for it as I need it in my current project)
Simplest solution
However, in many cases it makes sense to store only the time of the event. In current project we decided to store it in format of integer 60 * hour + minutes. This is unfortunately where we stopped in the project. :(
One step further
You can then create a helper class (just a scaffold - more work needed like validations casting etc):
class SimpleTime
attr_accessor :hour, :minute
def initialize(hour, minute)
#hour, #minute = hour, minute
end
def self.parse(integer)
return if integer.blank?
new(integer / 60, integer % 60)
end
def to_i
60 * #hour + #minute
end
end
and then override a setter and getter:
class Model < ActiveRecord::Base
def time
#time ||= SimpleTime.parse(super)
end
def time=(value)
super(value.to_i)
end
end
Further fun
Now there are more things you could do. You can for example write extension simple_time to active_record which will automatically redefine setters and getters for a list of passed attributes. You can even wrap it in a small gem and make it real-world-proof (missing validations, string format parsers, handling _before_type_cast values etc).
You have to do nothing. That is activerecord convention on time storing. So if you want to have automatically parsed time in your model from sql database you have to store it in the way AR does. But if you really want to store only hours and minutes, you should change your scheme and use just string instead of datetime in AR migration. So you can store your time like that. and in the model class you can override the attribute getter like:
def some_time
Time.parse(#some_time)
end
Then you can get parsed time object when you call attribute. But that is a bad way actually.

Using a duration field in a Rails model

I'm looking for the best way to use a duration field in a Rails model. I would like the format to be HH:MM:SS (ex: 01:30:23). The database in use is sqlite locally and Postgres in production.
I would also like to work with this field so I can take a look at all of the objects in the field and come up with the total time of all objects in that model and end up with something like:
30 records totaling 45 hours, 25 minutes, and 34 seconds.
So what would work best for?
Field type for the migration
Form field for the CRUD forms (hour, minute, second drop downs?)
Least expensive method to generate the total duration of all records in the model
Store as integers in your database (number of seconds, probably).
Your entry form will depend on the exact use case. Dropdowns are painful; better to use small text fields for duration in hours + minutes + seconds.
Simply run a SUM query over the duration column to produce a grand total. If you use integers, this is easy and fast.
Additionally:
Use a helper to display the duration in your views. You can easily convert a duration as integer of seconds to ActiveSupport::Duration by using 123.seconds (replace 123 with the integer from the database). Use inspect on the resulting Duration for nice formatting. (It is not perfect. You may want to write something yourself.)
In your model, you'll probably want attribute readers and writers that return/take ActiveSupport::Duration objects, rather than integers. Simply define duration=(new_duration) and duration, which internally call read_attribute / write_attribute with integer arguments.
In Rails 5, you can use ActiveRecord::Attributes to store ActiveSupport::Durations as ISO8601 strings. The advantage of using ActiveSupport::Duration over integers is that you can use them for date/time calculations right out of the box. You can do things like Time.now + 1.month and it's always correct.
Here's how:
Add config/initializers/duration_type.rb
class DurationType < ActiveRecord::Type::String
def cast(value)
return value if value.blank? || value.is_a?(ActiveSupport::Duration)
ActiveSupport::Duration.parse(value)
end
def serialize(duration)
duration ? duration.iso8601 : nil
end
end
ActiveRecord::Type.register(:duration, DurationType)
Migration
create_table :somethings do |t|
t.string :duration
end
Model
class Something < ApplicationRecord
attribute :duration, :duration
end
Usage
something = Something.new
something.duration = 1.year # 1 year
something.duration = nil
something.duration = "P2M3D" # 2 months, 3 days (ISO8601 string)
Time.now + something.duration # calculation is always correct
I tried using ActiveSupport::Duration but had trouble getting the output to be clear.
You may like ruby-duration, an immutable type that represents some amount of time with accuracy in seconds. It has lots of tests and a Mongoid model field type.
I wanted to also easily parse human duration strings so I went with Chronic Duration. Here's an example of adding it to a model that has a time_spent in seconds field.
class Completion < ActiveRecord::Base
belongs_to :task
belongs_to :user
def time_spent_text
ChronicDuration.output time_spent
end
def time_spent_text= text
self.time_spent = ChronicDuration.parse text
logger.debug "time_spent: '#{self.time_spent_text}' for text '#{text}'"
end
end
I've wrote a some stub to support and use PostgreSQL's interval type as ActiveRecord::Duration.
See this gist (you can use it as initializer in Rails 4.1): https://gist.github.com/Envek/7077bfc36b17233f60ad
Also I've opened pull requests to the Rails there:
https://github.com/rails/rails/pull/16919

Resources