I'm using rails app as iOS backend. There is small problem - how to render timestamps with user's timezone in json? I'm using default UTC timezone. How to converted it for example to +2? Maybe there is way how to override as_json method to convert timestamps before rendering?
Thanks for help!
You can convert the time to user's timezone before rendering json, then the timezone will be included.
1.9.3p125 :006 > t = Time.now.utc
=> 2012-10-20 13:49:12 UTC
1.9.3p125 :007 > {time: t}.to_json
=> "{\"time\":\"2012-10-20T13:49:12Z\"}"
1.9.3p125 :008 > t = Time.now.in_time_zone("Beijing")
=> Sat, 20 Oct 2012 21:49:19 CST +08:00
1.9.3p125 :009 > {time: t}.to_json
=> "{\"time\":\"2012-10-20T21:49:19+08:00\"}"
UPDATE: To convert the time into user's timezone for serialization, you can override this method read_attribute_for_serialization. See lib/active_model/serialization.rb of ActiveModel:
# Hook method defining how an attribute value should be retrieved for
# serialization. By default this is assumed to be an instance named after
# the attribute. Override this method in subclasses should you need to
# retrieve the value for a given attribute differently:
#
# class MyClass
# include ActiveModel::Validations
#
# def initialize(data = {})
# #data = data
# end
#
# def read_attribute_for_serialization(key)
# #data[key]
# end
# end
#
alias :read_attribute_for_serialization :send
Related
My problem
Oracle 'DATE' columns actually store time as well, just with less precision than 'TIMESTAMP' (seconds vs picoseconds). I need my application to interact with this legacy schema as if the Date was a DateTime. Because rails thinks of this field as a date, its truncating the time.
Example:
2.4.1 :003 > m.send_after_ts = Time.now
=> 2018-03-15 11:45:50 -0600
2.4.1 :004 > m.send_after_ts
=> Thu, 15 Mar 2018
Config Data:
The result of #columns:
#<ActiveRecord::ConnectionAdapters::OracleEnhancedColumn:0x00000005501658
#collation=nil,
#comment=nil,
#default=nil,
#default_function=nil,
#name="send_after_ts",
#null=true,
#object_type=false,
#returning_id=false,
#sql_type_metadata=
#<ActiveRecord::ConnectionAdapters::SqlTypeMetadata:0x00000005501720
#limit=nil,
#precision=nil,
#scale=nil,
#sql_type="DATE",
#type=:date>,
#table_name="mail",
#virtual=false>,
Versions:
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-darwin17]
Rails 5.1.5
activerecord (5.1.5)
activerecord-oracle_enhanced-adapter (1.8.2)
ruby-oci8 (2.2.5.1)
I assume there must be a mapping someplace that governs this relationship? How can I make rails cast this field as a timestamp?
UPDATE
Adding attribute :send_after_ts, :datetime to my model allows rails to treat the field as a DateTime, but causes an exception while trying to write to the db:
SQL (4.4ms) INSERT INTO "MAIL" ("SEND_AFTER_TS", "ID") VALUES (:a1, :a2) [["send_after_ts", "2018-03-15 12:58:02.635810"], ["id", 6778767]]
ActiveRecord::StatementInvalid: OCIError: ORA-01830: date format picture ends before converting entire input string: INSERT INTO "MAIL" ("SEND_AFTER_TS", "ID") VALUES (:a1, :a2)
from (irb):3
I assume this is caused by the extra precision (fractional seconds), but I don't see a way to define that as part of the attribute setting.
I'm able to get around this for now by writing this field as a string, eg:
2.4.1 :013 > m.send_after_ts = Time.now.to_s
=> "Mar 15, 2018 12:48"
2.4.1 :014 > m.save
=> true
I'm still looking for a real solution.
You could use the virtual attribute pattern as your interface:
def send_after_ts_datetime
send_after_ts.to_datetime
end
def send_after_ts_datetime=(datetime)
self.send_after_ts = datetime.to_s
end
Now you'll read from and write to the attribute using the _datetime methods, but your data will be stored in the original send_after_ts column.
>> m.send_after_ts_datetime = Time.now
#> "2018-03-15 15:15:49 -0600"
>> m.send_after_ts_datetime
#> Thu, 15 Mar 2018 15:15:49 -0600
Old question, but I just had to deal with this myself today, so I thought I'd share my hacky solution.
OracleEnhancedAdapter extends ActiveRecord::ConnectionAdapters::AbstractAdapter. During its initialization, it calls its initialize_type_map method where it maps these types. OracleEnhancedAdapter contains an override for this, which in turn calls the superclass method inside AbstractAdapter. In here is where it maps the undesired date type to the oracle DATE type:
register_class_with_precision m, %r(date)i, Type::Date
In my "fix" for this, I created an initializer in rails that overrides this method:
ActiveSupport.on_load(:active_record) do
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
# ... some other method customization
def initialize_type_map(m)
# other code from the AbstractAdapter implementation
# actually make the mapping to the DateTime that you really want
register_class_with_precision m, %r(date)i, ActiveRecord::Type::DateTime
# ... code from the OracleEnhancedAdapter implementation
end
end
end
The result is that oracle DATE types are now seen in rails as datetimes.
Add this to your code to force Oracle "DATE" to use DateTime ruby type
require 'active_record/connection_adapters/oracle_enhanced_adapter'
module ActiveRecord
module ConnectionAdapters
class OracleEnhancedAdapter
alias :old_initialize_type_map :initialize_type_map
def initialize_type_map(m = type_map)
old_initialize_type_map(m)
m.register_type "DATE", Type::DateTime.new
end
end
end
end
I'm writing a simple class to parse strings into relative dates.
module RelativeDate
class InvalidString < StandardError; end
class Parser
REGEX = /([0-9]+)_(day|week|month|year)_(ago|from_now)/
def self.to_time(value)
if captures = REGEX.match(value)
captures[1].to_i.send(captures[2]).send(captures[3])
else
raise InvalidString, "#{value} could not be parsed"
end
end
end
end
The code seems to work fine.
Now when I try my specs I get a time difference only in year and month
require 'spec_helper'
describe RelativeDate::Parser do
describe "#to_time" do
before do
Timecop.freeze
end
['day','week','month','year'].each do |type|
it "should parse #{type} correctly" do
RelativeDate::Parser.to_time("2_#{type}_ago").should == 2.send(type).ago
RelativeDate::Parser.to_time("2_#{type}_from_now").should == 2.send(type).from_now
end
end
after do
Timecop.return
end
end
end
Output
..FF
Failures:
1) RelativeDate::Parser#to_time should parse year correctly
Failure/Error: RelativeDate::Parser.to_time("2_#{type}_ago").should == 2.send(type).ago
expected: Wed, 29 Aug 2012 22:40:14 UTC +00:00
got: Wed, 29 Aug 2012 10:40:14 UTC +00:00 (using ==)
Diff:
## -1,2 +1,2 ##
-Wed, 29 Aug 2012 22:40:14 UTC +00:00
+Wed, 29 Aug 2012 10:40:14 UTC +00:00
# ./spec/lib/relative_date/parser_spec.rb:11:in `(root)'
2) RelativeDate::Parser#to_time should parse month correctly
Failure/Error: RelativeDate::Parser.to_time("2_#{type}_ago").should == 2.send(type).ago
expected: Sun, 29 Jun 2014 22:40:14 UTC +00:00
got: Mon, 30 Jun 2014 22:40:14 UTC +00:00 (using ==)
Diff:
## -1,2 +1,2 ##
-Sun, 29 Jun 2014 22:40:14 UTC +00:00
+Mon, 30 Jun 2014 22:40:14 UTC +00:00
# ./spec/lib/relative_date/parser_spec.rb:11:in `(root)'
Finished in 0.146 seconds
4 examples, 2 failures
Failed examples:
rspec ./spec/lib/relative_date/parser_spec.rb:10 # RelativeDate::Parser#to_time should parse year correctly
rspec ./spec/lib/relative_date/parser_spec.rb:10 # RelativeDate::Parser#to_time should parse month correctly
The first one seems like a time zone issue but the other one is even a day apart? I'm really clueless on this one.
This is a fascinating problem!
First, this has nothing to do with Timecop or RSpec. The problem can be reproduced in the Rails console, as follows:
2.0.0-p247 :001 > 2.months.ago
=> Mon, 30 Jun 2014 20:46:19 UTC +00:00
2.0.0-p247 :002 > 2.months.send(:ago)
DEPRECATION WARNING: Calling #ago or #until on a number (e.g. 5.ago) is deprecated and will be removed in the future, use 5.seconds.ago instead. (called from irb_binding at (irb):2)
=> Wed, 02 Jul 2014 20:46:27 UTC +00:00
[Note: This answer uses the example of months, but the same is true for the alias month as well as year and years.]
Rails adds the month method to the Integer class, returning an ActiveSupport::Duration object, which is a "proxy object" containing a method_missing method which redirects any calls to the method_missing method of the "value" it is serving as a proxy for.
When you call ago directly, it's handled by the ago method in the Duration class itself. When you try to invoke ago via send, however, send is not defined in Duration and is not defined in the BasicObject that all proxy objects inherit from, so the method_missing method of Rails' Duration is invoked which in turn calls send on the integer "value" of the proxy, resulting in the invocation of ago in Numeric. In your case, this results in a change of date equal to 2*30 days.
The only methods you have to work with are those defined by Duration itself and those defined by BasicObject. The latter are as follows:
2.0.0-p247 :023 > BasicObject.instance_methods
=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]
In addition to the instance_eval you discovered, you can use __send__.
Here's the definition of method_missing from duration.rb
def method_missing(method, *args, &block) #:nodoc:
value.send(method, *args, &block)
end
value in this case refers to the number of seconds in the Duration object. If you redefine method_missing to special case ago, you can get your test to pass. Or you can alias send to __send__ as follows:
class ActiveSupport::Duration
alias send __send__
end
Here's another example of how this method_missing method from Duration works:
macbookair1:so1 palfvin$ rails c
Loading development environment (Rails 4.1.1)
irb: warn: can't alias context from irb_context.
2.0.0-p247 :001 > class ActiveSupport::Duration
2.0.0-p247 :002?> def foo
2.0.0-p247 :003?> 'foobar'
2.0.0-p247 :004?> end
2.0.0-p247 :005?> end
=> nil
2.0.0-p247 :006 > 2.months.foo
=> "foobar"
2.0.0-p247 :007 > 2.months.respond_to?(:foo)
=> false
2.0.0-p247 :008 >
You can call the newly defined foo directly, but because BasicObject doesn't implement respond_to?, you can't "test" that the method is defined there. For the same reason, method(:ago) on a Duration object returns #<Method: Fixnum(Numeric)#ago> because that's the ago method defined on value.
I have a form that accepts a date from a text field in the format (m/d/yy).
When the date gets saved (using ActiveRecord), it gets parsed as the year "0014" instead of "2014." In this field's case, I'd like it to always guess the century as 2000.
Here's a [failing] test in the console:
2.0.0-p247 :021 > g.update_attributes(:date_completed => "3/4/14")
(0.3ms) BEGIN
SQL (0.7ms) UPDATE `goals` SET `date_completed` = '0014-03-04', `updated_at` = '2014-06-30 21:36:06' WHERE `goal_tracker_goals`.`id` = 10
(1.7ms) COMMIT
=> true
2.0.0-p247 :023 > g.date_completed
=> Sun, 04 Mar 0014
I assume there's an ActiveRecord method to overwrite somewhere or a configuration value to set.
Thanks!
Try:
g.date_completed=Date.parse("3/4/14")
To bypass default locale, try: [ after having updated the question this is the right answer ]
g.date_completed=Date.strptime("3/4/14","%m/%d/%y") # %m: month, %d: day, %y: 2-digit year
Bold solution, but it's the only one that covers all cases. Anywhere in the app, when an attribute of type date is set, it will send it through Date.parse() before it writes.
# initializers/active_record_extension_parse_date_writer.rb
module ActiveRecordExtensionParseDateWriter
private
def write_attribute(attr_name, value)
if self.class.column_types.fetch(attr_name.to_s).type == :date
d = (Date.parse(value.to_s)) rescue nil
super(attr_name, d)
else
super
end
end
end
# include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtensionParseDateWriter)
With the following in my rails_defaults.rb:
Date::DATE_FORMATS[:default] = '%m/%d/%Y'
Time::DATE_FORMATS[:default]= '%m/%d/%Y %H:%M:%S'
Why do the following results differ:
ruby-1.9.2-p180 :005 > MyModel.find(2).to_json(:only => :start_date)
=> "{\"start_date\":\"2012-02-03\"}"
ruby-1.9.2-p180 :006 > MyModel.find(2).start_date.to_s
=> "02/03/2012"
And more importantly, how do I get to_json to use %m/%d/%Y?
Because the standard JSON format for a date is %Y-%m-%d and there's no way to change it unless you override Date#as_json (don't do so or your application will start misbehaving).
See https://github.com/rails/rails/blob/master/activesupport/lib/active_support/json/encoding.rb#L265-273
class Date
def as_json(options = nil) #:nodoc:
if ActiveSupport.use_standard_json_time_format
strftime("%Y-%m-%d")
else
strftime("%Y/%m/%d")
end
end
end
I'm building a json object in rails as follows:
#list = Array.new
#list << {
:created_at => item.created_at
}
end
#list.to_json
Problem is this gets received by the browser like so:
"created_at\":\"2000-01-01T01:31:35Z\"
Which is clearly not right, in the DB it has:
2011-06-17 01:31:35.057551
Why is this happening? Any way to make sure this gets to the browser correctly?
Thanks
You need to do some testing / debugging to see how that date is coming through.
For me, in Rails console (Rails 3.0.9, Ruby 1.9.2)
ruby-1.9.2-p180 :014 > d = Date.parse("2011-06-17 01:31:35.057551")
=> Fri, 17 Jun 2011
ruby-1.9.2-p180 :015 > #list = {:created_at => d}
=> {:created_at=>Fri, 17 Jun 2011}
ruby-1.9.2-p180 :016 > #list.to_json
=> "{\"created_at\":\"2011-06-17\"}"
i.e. it's just fine. Can you see whether the date is really ok?
The trouble lies with the way to_json escapes characters. There is a very good post on the subject here:
Rails to_json or as_json
You may need to look into overriding as_json.