I have a failing test for a GET request — the issue lies with the way ActiveRecord's timestamps are formatted in the response. The RSpec test expects the data format to be Mon, 29 Dec 2014 13:37:09 UTC +00:00,, but it instead receives 2014-12-29T13:37:09Z. Is there any way to change the formatting of the ActiveRecord timestamp, or what the RSpec test expects?
Edit:
Here's the failing test:
describe 'GET /v1/tasks/:id' do
it 'returns an task by :id' do
task = create(:task)
get "/v1/tasks/#{task.id}"
expect(response_json).to eq(
{
'id' => task.id,
'title' => task.title,
'note' => task.note,
'due_date' => task.due_date,
'created_at' => task.created_at,
'updated_at' => task.updated_at
}
)
end
end
The test fails on created_at and updated_at. response_json is a helper method that parses the JSON response.
Here's where the test fails:
expected: {"id"=>1, "title"=>"MyString", "note"=>"MyText", "due_date"=>Mon, 29 Dec 2014, "created_at"=>Mon, 29 Dec 2014 13:37:09 UTC +00:00, "updated_at"=>Mon, 29 Dec 2014 13:37:09 UTC +00:00}
got: {"id"=>1, "title"=>"MyString", "note"=>"MyText", "due_date"=>"2014-12-29", "created_at"=>"2014-12-29T13:37:09Z", "updated_at"=>"2014-12-29T13:37:09Z"}
The solution was embarrassing simple — I had specify that the dates should be formatted using ISO 8601 using the to_formatted_s() method. Below is the correct, passing version:
require 'spec_helper'
describe 'GET /v1/tasks/:id' do
it 'returns an task by :id' do
task = create(:task)
get "/v1/tasks/#{task.id}"
expect(response_json).to eq(
{
'id' => task.id,
'title' => task.title,
'note' => task.note,
'due_date' => task.due_date.to_formatted_s(:iso8601),
'created_at' => task.created_at.to_formatted_s(:iso8601),
'updated_at' => task.updated_at.to_formatted_s(:iso8601)
}
)
end
end
The problem is that the date columns are being 'jsonified' in a format that you may or may not have specified. This value is of a different format and a string; therefore matching it with a date value fails.
To resolve this you could either adjust how the created_at value is 'jsonified' (format it) or you could just do this in your test:
columns = %w{id title note due_date created_at updated_at}
# get task as json string:
task_json_string = task.to_json(only: columns)
# turn json into hash
task_json = ActiveSupport::JSON.decode(task_json_string)
json_attributes = task_json.values.first
expected_hash = columns.each_with_object({}) do |attr, hash|
# ensuring that the order of keys is as specified by 'columns'
hash[attr] = json_attributes[attr]
end
expect(response_json).to eq expected_hash
That should work (minor debugging might be required though).
To go the former route, add an instance method like the following to your Task model:
def as_json( *args )
super(*args).tap do |hash|
attributes_hash = hash.values.first
%w{created_at updated_at}.each do |attr|
if attributes_hash.has_key?(attr)
# Use strftime time instead of to_s to specify a format:
attributes_hash[attr] = self.send(attr).to_s
end
end
end
end
Then in your test your task json could simply be:
task_json = task.as_json.values.first
Or after adding the add_json method and keeping the to_s there, in your test all you will need to do is add a .to_s to your date method e.g. task.created_at.to_s; the values will match.
Thereby removing the object-to-json-to-hash step and doing object-to-hash directly.
The following Date and DateTime formatting should correct the problem.
{
'id' => task.id,
'title' => task.title,
'note' => task.note,
'due_date' => task.due_date.strftime("%Y-%m-%d %H:%M:%S %Z"),
'created_at' =>task.created_at.strftime("%Y-%m-%d %H:%M:%S %Z"),
'updated_at' =>task.updated_at.strftime("%Y-%m-%d %H:%M:%S %Z")
}
EDIT
Correction on date format.
As #Humza mentioned, the problem stems from how values are "jsonified".
tl;dr -- just change your spec to expect 'created_at' => task.created_at.as_json
The Long Version
In ActiveSupport (5.1.4), all values are run through ActiveSupport::JSON::Encoding#jsonify:
def jsonify(value)
case value
when String
EscapedString.new(value)
when Numeric, NilClass, TrueClass, FalseClass
value.as_json
when Hash
Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
when Array
value.map { |v| jsonify(v) }
else
jsonify value.as_json
end
end
As you can see, there's no special case for ActiveSupport::TimeWithZone, it just calls ActiveSupport::TimeWithZone#as_json. So let's see what that looks like:
[1] pry(#<DataModelRowSerializer>)> show-source ActiveSupport::TimeWithZone#as_json
From: /Users/stevehull/.rbenv/versions/2.4.2/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/time_with_zone.rb # line 165:
Owner: ActiveSupport::TimeWithZone
Visibility: public
Number of lines: 7
def as_json(options = nil)
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
%(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
end
end
So if you wanted to, you could expect 'created_at' => task.created_at.xmlschema(ActiveSupport::JSON::Encoding.time_precision), however it's much more compact to use as_json ;)
Related
Is there a way to automatically parse string parameters representing dates in Rails? Or, some convention or clever way?
Doing the parsing manually by just doing DateTime.parse(..) in controllers, even if it's in a callback doesn't look very elegant.
There's also another case I'm unsure how to handle: If a date field in a model is nullable, I would like to return an error if the string I receive is not correct (say: the user submits 201/801/01). This also has to be done in the controller and I don't find a clever way to verify that on the model as a validation.
If you're using ActiveRecord to back your model, your dates and time fields are automatically parsed before being inserted.
# migration
class CreateMydates < ActiveRecord::Migration[5.2]
def change
create_table :mydates do |t|
t.date :birthday
t.timestamps
end
end
end
# irb
irb(main):003:0> m = Mydate.new(birthday: '2018-01-09')
=> #<Mydate id: nil, birthday: "2018-01-09", created_at: nil, updated_at: nil>
irb(main):004:0> m.save
=> true
irb(main):005:0> m.reload
irb(main):006:0> m.birthday
=> Tue, 09 Jan 2018
So it comes down to validating the date format, which you can do manually with a regex, or you can call Date.parse and check for an exception:
class Mydate < ApplicationRecord
validate :check_date_format
def check_date_format
begin
Date.parse(birthday)
rescue => e
errors.add(:birthday, "Bad Date Format")
end
end
end
I am trying to get a date from a user and send it inside an email as plain text in the following format: "07/30/2015".
In order to do that, if the output I am getting is a string, I could just do:
Date.parse("2015-07-30").strftime("%m/%d/%Y")
The problem is, I am getting a FixNum.
The issues are many:
If I try to convert to a string to parse it with Date.parse, it becomes "2001".
If I apply the code I just wrote, Date.parse... it will throw 'invalid date'.
For instance:
(2016-02-13).to_s #=> "2001"
(2016-02-13).to_date #=> NoMethodError: undefined method `to_date' for 2001:Fixnum
Date.parse("2001").strftime("%m/%d/%Y") #=> invalid date
So if I can convert 2015-07-30 into "2015-07-30", it would work:
Date.parse("2015-07-30").strftime("%m/%d/%Y") #=> "07/30/2015"
Then I tried using date_select instead of date_field, but now the message arrives with those fields empty.
Any suggestions?
Here is my form:
= form_for #contact do |f|
= f.text_field :product_name
= f.date_field :purchase_date
= f.submit
Here is my code:
<%= message.subject %>
<% #resource.mail_form_attributes.each do |attribute, value|
if attribute == "mail_subject"
next
end
%>
<%= "#{#resource.class.human_attribute_name(attribute)}: #{Date.parse(value).class == Date ? Date.parse(value).strftime("%m/%d/%Y") : value}" %>
<% end %>
My controller:
class ContactsController < ApplicationController
before_action :send_email, except: [:create]
def create
#contact = Contact.new(params[:contact])
#contact.request = request
if #contact.deliver
#thank = "Thank you for your message!"
#message = "We have received your inquiry and we'll be in touch shortly."
else
#error = "Cannot send message. Please, try again."
end
end
def contact_page
end
def product_complaint
#the_subject = "Product Complaint Form"
end
private
def send_email
#contact = Contact.new
end
end
My model:
class Contact < MailForm::Base
# all forms
attribute :mail_subject
attribute :first_name, validate: true
attribute :last_name, validate: true
# product-complaint
attribute :best_by, validate: true, allow_blank: true # date
attribute :bag_code, validate: true, allow_blank: true
attribute :purchase_date, validate: true, allow_blank: true # date
attribute :bag_opened, validate: true, allow_blank: true # date
attribute :problem_noticed, validate: true, allow_blank: true # date
# all forms
attribute :message, validate: true
attribute :nickname, captcha: true
def headers
{
content_type: "text/plain",
subject: %(#{mail_subject}),
to: "xxxxx#xxxxxxx.com",
# from: %("#{first_name.capitalize} #{last_name.capitalize}" <#{email.downcase}>)
from: "xxx#xxxxx.com"
}
end
end
(2016-02-13).to_date #=> NoMethodError: undefined method `to_date' for 2001:Fixnum
youre getting this error because you dont have quotes around the value. i.e. its not a string, its a number that is having subtraction applied to it. this is being interpreted as
2016 - 2
2014 - 13
2001.to_date
it needs to be ('2016-02-13').to_date
if youre unable to get it as a string, can you post how you're getting it from the user to begin with? (a date field ought to be sending you a string to your controller, not a series of numbers)
You're not understanding something about receiving values from forms: You can NOT receive an integer, a fixnum or anything else other than strings. So, you can't have received 2016-02-13. Instead you got "2016-02-13" or "2016", "02" or "2" and "13" depending on the form. If you're running under Rails, then it got the strings, and through its meta-data understands you want an integer (which really should probably be defined as a string), and it converts it to an integer for you.
Either way, when you write:
(2016-02-13).to_s
(2016-02-13).to_date
you're propagating that misunderstanding into your testing. This is how it MUST be written because you need to be working with strings:
require 'active_support/core_ext/string/conversions'
("2016-02-13").to_s # => "2016-02-13"
("2016-02-13").to_date # => #<Date: 2016-02-13 ((2457432j,0s,0n),+0s,2299161j)>
You can create dates without them being strings though: Ruby's Date initializer allows us to pass the year, month and day value and receive a new Date object:
year, month, day = 2001, 1, 2
date = Date.new(year, month, day) # => #<Date: 2001-01-02 ((2451912j,0s,0n),+0s,2299161j)>
date.year # => 2001
date.month # => 1
date.day # => 2
Moving on...
Parsing dates in Ruby quickly demonstrates it's not a U.S.-centric language. Americans suppose all dates of 01/01/2001 are in "MM/DD/YYYY" but that's a poor assumption because much of the rest of the world uses "DD/MM/YYYY". Not knowing that means that code written under that assumption is doing the wrong thing. Consider this:
require 'date'
date = Date.parse('01/02/2001')
date.month # => 2
date.day # => 1
Obviously something "wrong" is happening, at least for 'mericans. This is very apparent with:
date = Date.parse('01/31/2001')
# ~> -:3:in `parse': invalid date (ArgumentError)
This occurs because there is no month "31". In the previous example of '01/02/2001', that misunderstanding means the programmer thinks it should be "January 2" but the code thinks it's "February 1", and work with that. That can cause major havoc in an enterprise system, or anything dealing with financial calculations, product scheduling, shipping or anything else that works with dates.
Because the code is assuming DD/MM/YYYY format for that sort of string, the sensible things to do are:
KNOW what format your users are going to send dates in. Don't assume, ever. ASK them and make your code capable of dealing with alternates, or tell them what they MUST use and vet out their data prior to actually committing it to your system. Or, provide a GUI that forces them to pick their dates from popups and never allows them to enter it by hand.
Force the date parser to use explicit formats of dates so it can always do the right thing:
Date.strptime('01/31/2001', '%m/%d/%Y') # => #<Date: 2001-01-31 ((2451941j,0s,0n),+0s,2299161j)>
Date.strptime('31/01/2001', '%d/%m/%Y') # => #<Date: 2001-01-31 ((2451941j,0s,0n),+0s,2299161j)>
The last point is the crux of writing code: We're telling the language what to do, not subjecting ourselves, and our employers, to code that's guessing. Give code half a chance and it'll do the wrong thing, so you control it. That's why programming is hard.
I created a REST API with rails and use .to_json to convert the API output to json response. E.g. #response.to_json
The objects in the response contains created_at and updated_at fields, both Datetime
In the JSON response, both these fields are converted to strings, which is more costly to parse than unixtimestamp.
Is there a simple way I can convert the created_at and updated_at fields into unixtimestamp without having to iterate through the whole list of objects in #response?
[updated]
danielM's solution works if it's a simple .to_json. However, if I have a .to_json(:include=>:object2), the as_json will cause the response to not include object2. Any suggestions on this?
Define an as_json method on your response model. This gets run whenever you return an instance of the response model as JSON to your REST API.
def as_json(options={})
{
:id => self.id,
:created_at => self.created_at.to_time.to_i,
:updated_at => self.updated_at.to_time.to_i
}
end
You can also call the as_json method explicitly to get the hash back and use it elsewhere.
hash = #response.as_json
Unix timestamp reference: Ruby/Rails: converting a Date to a UNIX timestamp
Edit: The as_json method works for relationships as well. Simply add the key to the hash returned by the function. This will run the as_json method of the associated model as well.
def as_json(options={})
{
:id => self.id,
:created_at => self.created_at.to_time.to_i,
:updated_at => self.updated_at.to_time.to_i,
:object2 => self.object2,
:posts => self.posts
}
end
Furthermore, you can pass in parameters to the method to control how the hash is built.
def as_json(options={})
json = {
:id => self.id,
:created_at => self.created_at.to_time.to_i,
:updated_at => self.updated_at.to_time.to_i,
:object2 => self.object2
}
if options[:posts] == true
json[:posts] = self.posts
end
json
end
hash = #response.as_json({:posts => true})
I'm working with a User model that includes booleans for 6 days
[sun20, mon21, tue22, wed23, thur24, fri25]
with each user having option to confirm which of the 6 days they are participating in.
I'm trying to define a simple helper method:
def day_confirmed(day)
User.where(day: true).count
end
where I could pass in a day and find out how many users are confirmed for that day, like so:
day_confirmed('mon21')
My problem is that when I use day in the where(), rails assumes that I'm searching for a column named day instead of outputting the value that I'm passing in to my method.
Surely I'm forgetting something, right?
This syntax:
User.where( day: true )
Is equivalent to:
User.where( :day => true )
That's because using : in a hash instead of =>, e.g. { foo: bar }, is the same as using a symbol for the key, e.g. { :foo => bar }. Perhaps this will help:
foo = "I am a key"
hsh = { foo: "bar" }
# => { :foo => "bar" }
hsh.keys
# => [ :foo ]
hsh = { foo => "bar" }
# => { "I am a key" => "bar" }
hsh.keys
# => [ "I am a key" ]
So, if you want to use the value of the variable day rather than the symbol :day as the key, try this instead:
User.where( day => true )
If these are your column names, [sun20, mon21, tue22, wed23, thur24, fri25]
And you are calling day_confirmed('mon21') and trying to find the column 'mon21' where it is true, you can use .to_sym on the date variable
def day_confirmed(day)
User.where(day.to_sym => true).count
end
the .to_sym will get the value of date, and covert it to :mon21
I have form fields where the user enters in:
percents: 50.5%
money: $144.99
dates: Wednesday, Jan 12th, 2010
...
The percent and money type attributes are saved as decimal fields with ActiveRecord, and the dates are datetime or date fields.
It's easy to convert between formats in javascript, and you could theoretically convert them to the activerecord acceptable format onsubmit, but that's not a decent solution.
I would like to do something override the accessors in ActiveRecord so when they are set it converts them from any string to the appropriate format, but that's not the best either.
What I don't want is to have to run them through a separate processor object, which would require something like this in a controller:
def create
# params == {:product => {:price => "$144.99", :date => "Wednesday, Jan 12, 2011", :percent => "12.9%"}}
formatted_params = Product.format_params(params[:product])
# format_params == {:product => {:price => 144.99, :date => Wed, 12 Jan 2011, :percent => 12.90}}
#product = Product.new(format_params)
#product.save
# ...
end
I would like for it to be completely transparent. Where is the hook in ActiveRecord so I can do this the Rails Way?
Update
I am just doing this for now: https://gist.github.com/727494
class Product < ActiveRecord::Base
format :price, :except => /\$/
end
product = Product.new(:price => "$199.99")
product.price #=> #<BigDecimal:10b001ef8,'0.19999E3',18(18)>
You could override the setter or getter.
Overriding the setter:
class Product < ActiveRecord::Base
def price=(price)
self[:price] = price.to_s.gsub(/[^0-9\.]/, '')
end
end
Overriding the getter:
class Product < ActiveRecord::Base
def price
self[:price].to_s.gsub(/[^0-9\.]/, ''))
end
end
The difference is that the latter method still stores anything the user entered, but retrieves it formatted, while the first one, stores the formatted version.
These methods will be used when you call Product.new(...) or update_attributes, etc...
You can use a before validation hook to normalize out your params such as before_validation
class Product < ActiveRecord::Base
before_validation :format_params
.....
def format_params
self.price = price.gsub(/[^0-9\.]/, "")
....
end
Use monetize gem for parsing numbers.
Example
Monetize.parse(val).amount