How do I get the raw pre-deserialized value of a model? - ruby-on-rails

I have an encrypted type in my model
attribute :name, :encrypted
Which is
class EncryptedType < ActiveRecord::Type::Text
And implements #serialize, #deserialize, and #changed_in_place?.
How can I get the raw value from the database before deserialization?
I want to create a rake task to encrypt values which are in the DB that existed before the fields were encrypted. So before the encryption, the name field contained Bob. After the code change with encryption, reading that value will produce an error (caught), returning an empty string. I want to read the raw value and set it like a normal attribute so it will encrypt it. After the encryption, the field will look like UD8yDrrXYEJXWrZGUGCCQpIAUCjoXCyKOsplsccnkNc=.
I want something like user.name_raw or user.raw_attributes[:name].

There's ActiveRecord::AttributeMethods::BeforeTypeCast which
provides a way to read the value of the attributes before typecasting and deserialization
and has read_attribute_before_type_cast and attributes_before_type_cast. Additionally,
it declares a method for all attributes with the *_before_type_cast suffix
So for instance:
User.last.created_at_before_type_cast # => "2017-07-29 23:31:10.862924"
User.last.created_at_before_type_cast.class # => String
User.last.created_at # => Sat, 29 Jul 2017 23:31:10 UTC +00:00
User.last.created_at.class # => ActiveSupport::TimeWithZone
User.last.attributes_before_type_cast # => all attributes before type casting and such
I imagine this would work with your custom encrypted type

As SimpleLime suggested...
namespace :encrypt do
desc "Encrypt the unencrypted values in database"
task encrypt_old_values: :environment do
User.all.each do |user|
if user.name.blank? && ! user.name_before_type_cast.blank?
User.class_variable_get(:##encrypted_fields).each do |att|
user.assign_attributes att => user.attributes_before_type_cast[att.to_s]
end
user.save validate: false
end
end

Related

Automatically parsing date/time parameters in rails

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

Reset value of an attribute to default

I would like to know how I can set a models attribute and possible associations to its default value.
user = User.find_by(name: "Martin")
user.phone = 012345
user.save!
# some time later
user.phone = # set to default
user.save!
Few options to set a default value of a column:
Set the default value in migration (preferable)
Set the default value in before_* callback
To revert to default column's value you can use ActiveRecord::ConnectionAdapters::SchemaCache#columns_hash:
user.phone = user.class.columns_hash['phone'].default
You already set default in the migration.
:default => 'your_default'
It's better to use:
User.column_defaults["phone"]
instead of:
User.columns_hash['phone'].default
since columns_hash gets the raw default value defined at database level and skips defaults set in ActiveModel. See the following example:
class Order < ApplicationRecord
enum status: %i[open closed]
attribute :deliver_at, default: -> { Date.tomorrow }
end
Order.columns_hash['status'].default # => "0" ('0' if default value was defined in the database or 'nil' otherwise)
Order.columns_hash['deliver_at'].default # => NoMethodError (undefined method `default' for nil:NilClass) if it's a virtual attribute or 'nil' if the column exists in the database
Order.column_defaults['status'] # => "open"
Order.column_defaults['deliver_at'] # => Wed, 06 May 2020

Formatting Active Record Timestamps for JSON API

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 ;)

Saving a nested hash in Ruby on Rails

I'm trying to save a nested Hash to my database and retrieve it, but nested values are lost upon retrieval.
My model looks like this:
class User
serialize :metadata, MetaData
end
The class MetaData looks like this:
class MetaData < Hash
attr_accessor :availability, :validated
end
The code I'm using to store data looks something like this (the real data is coming from a HTML form, though):
user = User.find(id)
user.metadata.validated = true
user.metadata.availability = {'Sunday' => 'Yes', 'Monday' => 'No', 'Tuesday' => 'Yes'}
user.save
When I look at the data in the database, I see the following:
--- !map:MetaData
availability: !map:ActiveSupport::HashWithIndifferentAccess
Sunday: "Yes"
Monday: "No"
Tuesday: "Yes"
validated: true
The problem occurs when I try to get the object again:
user = User.find(id)
user.metadata.validated # <- this is true
user.metadata.availability # <- this is nil
Any ideas? I'm using Rails 3.1 with Postgresql as my datastore.
If you look in the database you see "map:ActiveSupport::HashWithIndifferentAccess" for availability?
My approach would be to separate out the single instance of availablity from the hash collection structure of days available.
you mean user.metadata.validated # <- this is true ?
What DB columns are metadata and availability stored as? They need to be TEXT

Accessing a Hash in a REST POST

I have a hash in Ruby:
params={"username"=>"test"}
I want to add another associative array like:
params["user"]={"name"=>"test2"}
so params should become
params={"username"=>"test","user"=>{"name"=>"test2"}}
but when I post this params to a url, I get:
params[:user][:name] # => nil
when I dump the user data:
params[:user] # => ['name','test2']
what I want is
params[:user] # => output {'name'=>'test2'}
what am I doing wrong? thanks for help.
You're just using wrong key, you think that :user and "user" are the same, which is not.
params["user"]["name"] #=> "test2"
params["user"] #=> {"name"=>"test2"}
UPDATE from Naveed:
:user is an instance of Symbol class while "user" is instance of String
You have created a hash with keys of type string and trying to access with symbol keys. This works only with class HashWithIndifferentAccess.
If you want to achieve the same convert your hash to HashWithIndifferentAccess by using with_indifferent_access method,
> params = {"username"=>"test", "user"=>{"name"=>"test2"}}
=> {"username"=>"test", "user"=>{"name"=>"test2"}}
> params[:user][:name]
=> nil
>params = params.with_indifferent_access
> params[:user][:name]
=> "test2"
Update: request params is an instance of HashWithIndifferentAccess
The following should work:
params["user"]
params={"username"=>"test"}# params is not array nor associative array its a hash
you can add key value pair in hash by
params["key"]="value"
key and value both can be object of any class,be sure you use same object as key to access value or take a look at
HashWithIndifferentAccess
now
params["user"]={"name"=>"blah"}
params["user"] # => {"name"=>"blah"}
params["user"]["name"] # => "blah"

Resources