mongoid-history icon indicating copy to clipboard operation
mongoid-history copied to clipboard

Money-rails object is converted to Hash when storing the history

Open marcocanderle opened this issue 9 years ago • 8 comments

I have a model that among other fields it uses a money-rails field (money type):

class MyModel
   include Mongoid::Document
   include Mongoid::Timestamps
   include Mongoid::History::Trackable

   field :name, type: String
   field :date, type: Date 
   field :amount, type: Money 

   track_history track_create: true
end

When I create an instance of this model, the amount field has the class Money, which is the expected behavior (and of course the date field is a Time object).

  pry(main)> m=MyModel.create(name: "A", date: Date.today, amount: 200)
  => #<MyModel _id: 5453cab06d61632044030000, name: "A", date: 2014-10-31 00:00:00 UTC, amount: {:cents=>20000.0, :currency_iso=>"USD"}, version: 1, modifier_id: nil>
 pry(main)> m.date.class
 => Date
 pry(main)> m.amount.class
 => Money

However, when I get the value back from history, the date field preserves a compatible class, but the amount field is no longer a Money field, but a BSON Document:

 pry(main)> m.history_tracks.last[:modified]
 => {"name"=>"A", "date"=>2014-10-31 00:00:00 UTC, "amount"=>{"cents"=>20000.0, "currency_iso"=>"USD"}}
 pry(main)> m.history_tracks.last[:modified]["date"].class
 => Time
 pry(main)>m.history_tracks.last[:modified]["amount"].class
 => BSON::Document

This creates complications when performing actions like comparing the current amount of the object with a previous value, stored by monogoid_history:

 pry(main)> m.amount==m.history_tracks.last[:modified]["amount"]
 => false #Wrong, should be true as no changes were made yet

However, the date field works fine:

 pry(main)> m.date==m.history_tracks.last[:modified]["date"]
 => true

So, my question is if this is a bug and the Money type is lost when the object copy is stored on the history_tracks table, or if this is a correct behavior.

If the behavior is the expected one, how would I override it so that the Money type is also "tracked" for the amount field, as it happens with the Date field?

marcocanderle avatar Oct 31 '14 17:10 marcocanderle

Well, I couldn't get any answer about this here so I'll share my solution to this Bug, in case someone else has the same issue.

Since the Money type is lost, I implemented an explicit conversion when getting the changes back, so that the amount field is converted to the correct Money field. Here's how I did it (method on the model):

def get_previous_changes
   changes=self.changes
   changes.each do |f|
      if changes.include?(f)
        if f=="amount"
         unless changes.fetch(f).first.nil?
           val=changes.fetch(f).first
           value=Money.new(val[:cents],val[:currency_iso])
         else
          value=nil
         end
       else
         value=changes.fetch(f)[0].to_s
       end
     else
       value=nil
     end
    @previous_changes.merge!(f=>value)
end

Every time I want to get the previous changes for a given object, I would use this method so that amount fields are always Money type, as it should be in the first place. I'm sure this can be fixed on this gem, but in the meantime, this option works fine.

Thanks!

marcocanderle avatar Nov 08 '14 10:11 marcocanderle

Maybe I'll leave this open, in case someone at mongoid-history can provide feedback at some point.

marcocanderle avatar Nov 08 '14 10:11 marcocanderle

I think this is a bug. I would expect this to work.

It would be helpful if you could write a spec for this.

dblock avatar Nov 10 '14 17:11 dblock

OK, I'll be happy to do that. Bu can you explain more about how exactly you want that spec? What's the usual way of doing this? Fork/write test/send pull request?

marcocanderle avatar Nov 10 '14 18:11 marcocanderle

I want a test that reproduces the problem :) I think you described it perfectly well above, so that in code.

Yes for fork/test/PR.

dblock avatar Nov 10 '14 19:11 dblock

Ok I have the test ready but when I try to run it I'm getting a weird issue that might be due to something I'm forgetting to include. The error I get is: Failure/Error: @my=MyModel.create(name: "A", date: Date.today, amount: 200.to_money) NoMethodError: undefined method `bson_type' for #<Money fractional:20000 currency:USD>

Do you know what this could be about? Or want me to send you the pull request now, even with this issue?

marcocanderle avatar Nov 12 '14 15:11 marcocanderle

You should PR what you have.

dblock avatar Nov 12 '14 18:11 dblock

1) Model with MoneyRails field should have a history entry with a valid amount field with class Money 
     Failure/Error: self.class.tracker_class.create!(history_tracker_attributes(action.to_sym).merge(version: current_version, action: action.to_s, trackable: self))

     NoMethodError:
       undefined method `bson_type' for #<Money fractional:200 currency:USD>
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/hash.rb:47:in `block in to_bson'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/hash.rb:43:in `each'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/hash.rb:43:in `to_bson'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/hash.rb:49:in `block in to_bson'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/hash.rb:43:in `each'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/hash.rb:43:in `to_bson'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/array.rb:49:in `block in to_bson'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/array.rb:46:in `each'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/array.rb:46:in `each_with_index'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/array.rb:46:in `to_bson'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/hash.rb:49:in `block in to_bson'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/hash.rb:43:in `each'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/bson-4.1.1/lib/bson/hash.rb:43:in `to_bson'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/protocol/serializers.rb:163:in `serialize'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/protocol/message.rb:209:in `block in serialize_fields'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/protocol/message.rb:197:in `each'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/protocol/message.rb:197:in `serialize_fields'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/protocol/message.rb:100:in `serialize'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/server/connection.rb:188:in `block in write'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/server/connection.rb:187:in `each'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/server/connection.rb:187:in `write'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/server/connection.rb:161:in `deliver'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/server/connection.rb:108:in `block in dispatch'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/monitoring/publishable.rb:47:in `publish_command'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/server/connection.rb:107:in `dispatch'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/operation/write/command/writable.rb:38:in `block in execute'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/server/connection_pool.rb:107:in `with_connection'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/server/context.rb:63:in `with_connection'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/operation/write/command/writable.rb:37:in `execute'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/operation/write/insert.rb:55:in `execute_write_command'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/operation/write/write_command_enabled.rb:38:in `execute'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/collection.rb:346:in `block in insert_one'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/retryable.rb:112:in `write_with_retry'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongo-2.2.6/lib/mongo/collection.rb:337:in `insert_one'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/query_cache.rb:168:in `insert_one_with_clear_cache'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/creatable.rb:79:in `insert_as_root'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/creatable.rb:27:in `block in insert'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/creatable.rb:118:in `block (2 levels) in prepare_insert'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/activesupport-4.0.13/lib/active_support/callbacks.rb:393:in `_run__1773079500231756398__create__callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/activesupport-4.0.13/lib/active_support/callbacks.rb:80:in `run_callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/interceptable.rb:138:in `run_callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/creatable.rb:117:in `block in prepare_insert'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/activesupport-4.0.13/lib/active_support/callbacks.rb:373:in `_run__1773079500231756398__save__callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/activesupport-4.0.13/lib/active_support/callbacks.rb:80:in `run_callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/interceptable.rb:138:in `run_callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/creatable.rb:116:in `prepare_insert'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/creatable.rb:23:in `insert'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/creatable.rb:180:in `block in create!'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/threaded/lifecycle.rb:161:in `_creating'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/creatable.rb:175:in `create!'
     # ./lib/mongoid/history/trackable.rb:278:in `track_history_for_action'
     # ./lib/mongoid/history/trackable.rb:240:in `track_update'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/activesupport-4.0.13/lib/active_support/callbacks.rb:387:in `_run__2981391924363940241__update__callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/activesupport-4.0.13/lib/active_support/callbacks.rb:80:in `run_callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/interceptable.rb:138:in `run_callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/updatable.rb:117:in `block in prepare_update'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/activesupport-4.0.13/lib/active_support/callbacks.rb:373:in `_run__2981391924363940241__save__callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/activesupport-4.0.13/lib/active_support/callbacks.rb:80:in `run_callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/interceptable.rb:138:in `run_callbacks'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/updatable.rb:116:in `prepare_update'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/updatable.rb:139:in `update_document'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/mongoid-5.1.3/lib/mongoid/persistable/savable.rb:25:in `save'
     # ./spec/unit/money_rails_spec.rb:23:in `block (2 levels) in <top (required)>'
     # /home/siva/.rvm/gems/ruby-2.3.0/gems/rspec-core-3.5.1/exe/rspec:4:in `<top (required)>'
     # /home/siva/.rvm/gems/ruby-2.3.0/bin/rspec:23:in `load'
     # /home/siva/.rvm/gems/ruby-2.3.0/bin/rspec:23:in `<main>'
     # /home/siva/.rvm/gems/ruby-2.3.0/bin/ruby_executable_hooks:15:in `eval'
     # /home/siva/.rvm/gems/ruby-2.3.0/bin/ruby_executable_hooks:15:in `<main>'

Trying to simulate issue but getting this error while creating an object in the test case

CustomModel.create(name: "A", date: Date.today, amount: Money.new(200))

But I am not getting error if I run same spec under money-rails project.

sivagollapalli avatar Jul 18 '16 13:07 sivagollapalli