Question:
I need to know the records' attributes that have been called inside a block (say I need something like the following):
def my_custom_method(&block)
some_method_that_starts_tracking
block.call
some_method_that_stops_tracking
puts some_method_that_returns_called_records_attributes
do_something_about(some_method_that_returns_called_records_attributes)
end
my_custom_method { somecodethatcallsauthorofbook1andemailandfirstnameofuser43 }
# this is the `puts` output above (just as an example)
# => {
# #<Book id:1...> => [:author],
# #<User id:43...> => [:email, :first_name]
# }
code inside the block can be anything
Specifically, I meant to track any instance of a subclass of ApplicationRecord, so it can be instance of any models like Book, User, etc...
Attempts:
From my understanding, this is similar to how rspec works when a method is expected to be called. That it somehow tracks any calls of that method. So, my initial attempt is to do something like the following (which does not yet fully work):
def my_custom_method(&block)
called_records_attributes = {}
ApplicationRecord.descendants.each do |klass|
klass.class_eval do
attribute_names.each do |attribute_name|
define_method(attribute_name) do
called_records_attributes[self] ||= []
called_records_attributes[self] << attribute_name
self[attribute_name]
end
end
end
end
block.call
# the above code will work but at this point, I don't know how to clean the methods that were defined above, as the above define_methods should only be temporary
puts called_records_attributes
end
my_custom_method { Book.find_by(id: 1).title }
# => {
# #<Book id: 1...> => ['title']
# }
the .descendants above probably is not a good idea because Rails use autoload if I'm not mistaken
as already said above in the comment, I do not know how to remove these "defined_methods" that are just supposed to be only temporary for the duration of this "block".
furthermore, my code above would probably have overriden the "actual" attribute getters of the models, if ever any has been already defined, which is bad.
Background:
I am writing a gem live_record which I am adding a new feature that will allow a developer to just simply write something like
<!-- app/views/application.html.erb -->
<body>
<%= live_record_sync { #book.some_custom_method_about_book } %>
</body>
... which will render #book.some_custom_method_about_book as-is on the page, but at the same time the live_record_sync wrapper method would take note of all the attributes that have been called inside that block (i.e. inside some_custom_method_about_book the #book.title is called), and then it sets these attributes as the block's own "dependencies", in which later when that specific book's attribute has been updated, I can already also update directly the HTML page of which this attribute is a "dependency" as like specified just above. I am aware that this is not an accurate solution, but I'd like to open up my chances by experimenting on this first.
-- Rails 5
Disclaimer: I believe this is just a mediocre solution, but hopefully helps anyone with the same problem.
I tried reading rspec source code, but because I couldn't easily comprehend what is happening under the hood, and that it occurred to me that rspec's (i.e.) expect(Book.first).to receive(:title) is different from what I really want because the methods there are already specified (i.e. :title), while what I want is to track ANY methods that are attributes, so because of these two reasons I skipped reading further, and attempted my own solution, which hopefully did somehow work; see below.
Note that I am using Thread local-storage here, so this code should be thread-safe (untested yet).
# lib/my_tracker.rb
class MyTracker
Thread.current[:my_tracker_current_tracked_records] = {}
attr_accessor :tracked_records
class << self
def add_to_tracked_records(record, attribute_name)
Thread.current[:my_tracker_current_tracked_records][{model: record.class.name.to_sym, record_id: record.id}] ||= []
Thread.current[:my_tracker_current_tracked_records][{model: record.class.name.to_sym, record_id: record.id}] << attribute_name
end
end
def initialize(block)
#block = block
end
def call_block_while_tracking_records
start_tracking
#block_evaluated_value = #block.call
#tracked_records = Thread.current[:my_tracker_current_tracked_records]
stop_tracking
end
def to_s
#block_evaluated_value
end
# because I am tracking record-attributes, and you might want to track a different object / method, then you'll need to write your own `prepend` extension (look for how to use `prepend` in ruby)
module ActiveRecordExtensions
def _read_attribute(attribute_name)
if Thread.current[:my_tracker_current_tracked_records] && !Thread.current[:my_tracker_is_tracking_locked] && self.class < ApplicationRecord
# I added this "lock" to prevent infinite loop inside `add_to_tracked_records` as I am calling the record.id there, which is then calling this _read_attribute, and then loops.
Thread.current[:my_tracker_is_tracking_locked] = true
::MyTracker.add_to_tracked_records(self, attribute_name)
Thread.current[:my_tracker_is_tracking_locked] = false
end
super(attribute_name)
end
end
module Helpers
def track_records(&block)
my_tracker = MyTracker.new(block)
my_tracker.call_block_while_tracking_records
my_tracker
end
end
private
def start_tracking
Thread.current[:my_tracker_current_tracked_records] = {}
end
def stop_tracking
Thread.current[:my_tracker_current_tracked_records] = nil
end
end
ActiveSupport.on_load(:active_record) do
prepend MyTracker::ActiveRecordExtensions
end
ActiveSupport.on_load(:action_view) do
include MyTracker::Helpers
end
ActiveSupport.on_load(:action_controller) do
include MyTracker::Helpers
end
Usage Example
some_controller.rb
book = Book.find_by(id: 1)
user = User.find_by(id: 43)
my_tracker = track_records do
book.title
if user.created_at == book.created_at
puts 'same date'
end
'thisisthelastlineofthisblockandthereforewillbereturned'
end
puts my_tracker.class
# => #<MyTracker ... >
puts my_tracker.tracked_records
# => {
# {model: :Book, record_id: 1} => ['title', 'created_at'],
# {model: :User, record_id: 43} => ['created_at']
# }
puts my_tracker
# => 'thisisthelastlineofthisblockandthereforewillbereturned'
# notice that `puts my_tracker` above prints out the block itself
# this is because I defined `.to_s` above.
# I need this `.to_s` so I can immediately print the block as-is in the views.
# see example below
some_view.html.erb
<%= track_records { current_user.email } %>
P.S. Maybe it's better that I wrap this up as a gem. If you're interested, let me know
Related
I've got this method
def finalize_inquiry_process(form)
if finalize_process == true
inquiry_process.campaign_code.update(state: 'used')
document_creator_class = InquiryProcessDocumentCreatorFetcher.new(inquiry_process).call
document_creator_class.new(inquiry_process).call
end
Success(form)
end
and I want to skip in specs this part which is really trouble maker, implementation is an unnecessary waste of time (pdf generator with tons of fields)
document_creator_class = InquiryProcessDocumentCreatorFetcher.new(inquiry_process).call
document_creator_class.new(inquiry_process).call
To do so I wrote a specs:
let(:fetcher_instance) { instance_double(InquiryProcessDocumentCreatorFetcher) }
before do
allow(InquiryProcessDocumentCreatorFetcher).to receive(:new).and_return(fetcher_instance)
allow(fetcher_instance).to receive(:call).and_return(nil)
end
it 'updates state of assigned campain code' do
updated_inquiry_process = process_update.value!
expect(updated_inquiry_process.campaign_code.state).to eq('used')
end
end
InquiryProcesses::Update.call campain code updates state of assigned campain code
Failure/Error: document_creator_class.new(inquiry_process).call
NoMethodError:
undefined method `new' for nil:NilClass
Is there any chance to skip this part of code in specs?
Ok I managed it by using receive_message_chain helper. Specs should looked like this:
describe 'finalize inquiry process' do
subject(:process_update) do
described_class.new(
inquiry_process: inquiry_process,
form: loan_application_inquiry_process_update_form,
finalize_process: true,
).call
end
let!(:inquiry_process) do
create :inquiry_process, inquiry_template: loan_inquiry_template, campaign_code_uid: campaign_code.uid
end
before do
allow(InquiryProcessDocumentCreatorFetcher).to receive_message_chain(:new, :call, :new, :call)
end
it 'updates state of assigned campain code' do
updated_inquiry_process = process_update.value!
expect(updated_inquiry_process.campaign_code.state).to eq('used')
end
end
You can try your luck with dependency injection:
(It's a rough sketch, but I don't know the context of the system, not even the whole class)
def initialize(inquiry_process:, form:, finalize_process:, creator_fetcher:)
#creator_fetcher = creator_fetcher
# all the other initializetions
# maybe you can initialize creator_fetcher outside, and no need to pass inquiry_process anymore?
end
def creator_fetcher
#creator_fetcher ||= InquiryProcessDocumentCreatorFetcher.new(inquiry_process)
end
def finalize_inquiry_process(form)
if finalize_process == true
inquiry_process.campaign_code.update(state: 'used')
document_creator_class = creator_fetcher.call
document_creator_class.new(inquiry_process).call
end
Success(form)
end
and then
let(:creator_fetcher) { instance_double(InquiryProcessDocumentCreatorFetcher) }
let(:document_creator_class) { instance_double(whatever_fetcher_call_returns_or_just_unveryfying_double) }
before { allow(creator_fetcher.to_receive(:call).and_return(document_creator_class) }
subject(:process_update) do
described_class.new(
inquiry_process: inquiry_process,
form: loan_application_inquiry_process_update_form,
finalize_process: true,
).call
end
Anyway - your problems with tests show's that your code was not written with the tests, and it's a bad design.
Indirection (Dependency Injection here) might help a little to untangle the mess.
Suppose I have a class:
class Foo
def process
MyModel.where(id: [1,3,5,7]).each do |my_model|
ExternalService.dispatch(my_modal.id)
end
end
end
I want to test it:
class FooTest < ActiveSupport::TestCase
def process_test
external_service_mock = MiniTest::Mock.new
[1,3,5,7].each do |id|
external_service_mock.expect(:call, true, id)
end
ExternalService.stub(:dispatch, events_mock) do
Foo.new.process
end
external_service_mock.verify
end
end
However, #expect enforces that the following calls are made in the same order as #expect was called. That's not good for me, because I have no confidence, in what order will the results be returned by the DB.
How can I solve this problem? Is there a way to expect calls without specific order?
Try Using a Set
require 'set'
xs = Set[3,5,7,9]
#cat = Minitest::Mock.new
#cat.expect :meow?, true, [xs]
#cat.meow? 7 # => ok...
#cat.expect :meow?, true, [xs]
#cat.meow? 4 # => boom!
Alternatively, a less specific option:
Given that the value returned by the mock isn't a function of the parameter value, perhaps you can just specify a class for the parameter when setting up your mock. Here's an example of a cat that expects meow? to be called four times with an arbitrary integer.
#cat = Minitest::Mock.new
4.times { #cat.expect(:meow?, true, [Integer]) }
# Yep, I can meow thrice.
#cat.meow? 3 # => true
# Mope, I can't meow a potato number of times.
#cat.meow? "potato" # => MockExpectationError
In debugging console, while app running (using binding.pry to interrupt it), I can see that my variable Rails.configuration.hardcoded_current_user_key is set:
pry(#<TasksController>)> Rails.configuration.hardcoded_current_user_key
=> "dev"
But it doesn't appear to be defined:
pry(#<TasksController>)> defined?(Rails.configuration.hardcoded_current_user_key)
=> nil
Yet it works fine to store and test its value:
pry(#<TasksController>)> tempVar = Rails.configuration.hardcoded_current_user_key
=> "dev"
pry(#<TasksController>)> defined?(tempVar)
=> "local-variable"
What is going on?
This is because Rails config implements respond_to? but not respond_to_missing?, and defined? only recognizes respond_to_missing?:
class X
def respond_to?(name, include_all = false)
name == :another_secret || super
end
private
def method_missing(name, *args, &block)
case name
when :super_secret
'Bingo!'
when :another_secret
'Nope.'
else
super
end
end
def respond_to_missing?(name, include_all = false)
name == :super_secret || super
end
end
x = X.new
puts x.super_secret # => Bingo!
p defined?(x.super_secret) # => "method"
puts x.another_secret # => Nope.
p defined?(x.another_secret) # => nil
It's recommended to implement respond_to_missing? along with method_missing, I too wonder why Rails did it that way.
You shouldn't be using defined? on anything but the "stub" of that, or in other words, merely this:
defined?(Rails)
Anything beyond that is highly unusual to see, and I'm not even sure it's valid.
defined? is not a method, but a construct that tests if the following thing is defined as a variable, constant or method, among other things. It won't evaluate your code, it will just test it as-is. This means method calls don't happen, and as such, can't be chained.
If you want to test that something is assigned, then you should use this:
Rails.configuration.hardcoded_current_user_key.nil?
This method does not have a description on the APIdock. I know instance_exec in Ruby is similar to the this binding mechanism in JavaScript.
def interpolate(sql, record = nil)
if sql.respond_to?(:to_proc)
owner.instance_exec(record, &sql)
else
sql
end
end
Could someone briefly describe it?
First of all, the check for respond_to?(:to_proc) is necessary to make sure sql might be converted to lambda (by ampersand & to be passed to instance_exec. To simplify things, one might treat sql here as being a lambda already:
def interpolate(sql, record = nil) # assume sql is lambda
owner.instance_exec(record, &sql)
end
As by documentation on instance_exec:
Executes the given block within the context of the receiver...
That said, lambda will be executed as it was the ordinal code, placed somewhere inside instance method of the receiver.
class Owner
def initialize
#records = [:zero, :one, :two]
end
end
record_by_index = ->(idx) { #records[idx] }
Owner.new.instance_exec 1, &record_by_index #⇒ :one
The code above is [more or less] an equivalent to:
class Owner
def initialize
#records = [:zero, :one, :two]
end
def record_by_index idx
#records[idx]
end
end
Owner.new.record_by_index(1) #⇒ :one
The actual parameters of call to instance_exec will be passed to the codeblock. In the context of Owner’s instance we have an access to instance variables, private methods, etc. Hope it helps.
I am using an after_commit in my application.
I would like it to trigger only when a particular field is updated in my model. Anyone know how to do that?
Old question, but this is one method that I've found that might work with the after_commit callback (working off paukul's answer). At least, the values both persist post-commit in IRB.
after_commit :callback,
if: proc { |record|
record.previous_changes.key?(:attribute) &&
record.previous_changes[:attribute].first != record.previous_changes[:attribute].last
}
Answering this old question because it still pops up in search results
you can use the previous_changes method which returnes a hash of the format:
{ "changed_attribute" => ["old value", "new value"] }
it's what changes was until the record gets actually saved (from active_record/attribute_methods/dirty.rb):
def save(*) #:nodoc:
if status = super
#previously_changed = changes
#changed_attributes.clear
# .... whatever goes here
so in your case you can check for previous_changes.key? "your_attribute" or something like that
Old question but still pops up in search results.
As of Rails 5 attribute_changed? was deprecated. Using saved_change_to_attribute? instead of attribute_changed? is recommended.
I don't think you can do it in after_commit
The after_commit is called after the transaction is commited Rails Transactions
For example in my rails console
> record = MyModel.find(1)
=> #<MyModel id: 1, label: "test", created_at: "2011-08-19 22:57:54", updated_at: "2011-08-19 22:57:54">
> record.label = "Changing text"
=> "Changing text"
> record.label_changed?
=> true
> record.save
=> true
> record.label_changed?
=> false
Therefore you won't be able to use the :if condition on after_commit because the attribute will not be marked as changed anymore as it has been saved. You may need to track whether the field you are after is changed? in another callback before the record is saved?
This is a very old problem, but the accepted previous_changes solution just isn't robust enough. In an ActiveRecord transaction, there are many reasons why you might save a Model twice. previous_changes only reflects the result of the final save. Consider this example
class Test < ActiveRecord::Base
after_commit: :after_commit_test
def :after_commit_test
puts previous_changes.inspect
end
end
test = Test.create(number: 1, title: "1")
test = Test.find(test.id) # to initialize a fresh object
test.transaction do
test.update(number: 2)
test.update(title: "2")
end
which outputs:
{"title"=>["1", "2"], "updated_at"=>[...]}
but, what you need is:
{"title"=>["1", "2"], "number"=>[1, 2], "updated_at"=>[...]}
So, my solution is this:
module TrackSavedChanges
extend ActiveSupport::Concern
included do
# expose the details if consumer wants to do more
attr_reader :saved_changes_history, :saved_changes_unfiltered
after_initialize :reset_saved_changes
after_save :track_saved_changes
end
# on initalize, but useful for fine grain control
def reset_saved_changes
#saved_changes_unfiltered = {}
#saved_changes_history = []
end
# filter out any changes that result in the original value
def saved_changes
#saved_changes_unfiltered.reject { |k,v| v[0] == v[1] }
end
private
# on save
def track_saved_changes
# maintain an array of ActiveModel::Dirty.changes
#saved_changes_history << changes.dup
# accumulate the most recent changes
#saved_changes_history.last.each_pair { |k, v| track_saved_change k, v }
end
# v is an an array of [prev, current]
def track_saved_change(k, v)
if #saved_changes_unfiltered.key? k
#saved_changes_unfiltered[k][1] = track_saved_value v[1]
else
#saved_changes_unfiltered[k] = v.dup
end
end
# type safe dup inspred by http://stackoverflow.com/a/20955038
def track_saved_value(v)
begin
v.dup
rescue TypeError
v
end
end
end
which you can try out here: https://github.com/ccmcbeck/after-commit
It sounds like you want something like a conditional callback. If you had posted some code I could have pointed you in the right direction however I think you would want to use something like this:
after_commit :callback,
:if => Proc.new { |record| record.field_modified? }
Use gem ArTransactionChanges. previous_changes is not working for me in Rails 4.0.x
Usage:
class User < ActiveRecord::Base
include ArTransactionChanges
after_commit :print_transaction_changes
def print_transaction_changes
transaction_changed_attributes.each do |name, old_value|
puts "attribute #{name}: #{old_value.inspect} -> #{send(name).inspect}"
end
end
end