after_commit for an attribute - ruby-on-rails

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

Related

Rails Postgresql JSONB column check if key value was changed?

Given I have data_config as jsonb column
I want to check the value of specific key if was changed before saving.
Test Data:
data_config: {
"status"=>true,
"text"=>"sample"
}
Rails Model:
attr_json_config(default_container_attribute: :data_config)
attr_json :status, :boolean, default: true
before_save :check_config
def check_config
if self.status_changed? # => want to have something like this to check if value changed?
# ... do something
end
end
This works in my case, but I believe there's a better way.
status_val = self.attr_json_changes.changes_to_save.try(:[], "status")
# above returns [true, false] (old & new value)
if status_val.try(:first) != status_val.try(:last)
...do something
end
Why don't you compare the params with the current value?
Because you give very little information, I'll assume you want to change other key in jsonb if the status changed. In this case, you should make your record immutable before the updated value is ready to commit.
So in your controller, it should be like this:
class MyController < ApplicationController
def update
if params[:data_config][:status] != #model.['data_config']['status'] # check if value change
# ... do something
new_params = do_something
end
if #model.update(new_params)
# render success
else
# render failed
end
end
end
I hope this approach can help you.

How to track objects "called" inside a block?

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

How to format values before saving to database in rails 3

I have a User model with Profit field. Profit field is a DECIMAL (11,0) type. I have a masked input on the form which allows user to input something like $1,000. I want to format that value and remove everything except numbers from it so i will have 1000 saved. Here is what i have so far:
class User < ActiveRecord::Base
before_save :format_values
private
def format_values
self.profit.to_s.delete!('^0-9') unless self.profit.nil?
end
end
But it keeps saving 0 in database. Looks like it is converting it to decimal before my formatting function.
Try this:
def profit=(new_profit)
self[:profit] = new_profit.gsub(/[^0-9]/, '')
end
First of all, this:
def format_values
self.profit.to_s.delete!('^0-9') unless self.profit.nil?
end
is pretty much the same as this:
def format_values
return if(self.profit.nil?)
p = self.profit
s = p.to_s
s.delete!('^0-9')
end
So there's no reason to expect your format_values method to have any effect whatsoever on self.profit.
You could of course change format_values to assign the processed string to self.profit but that won't help because your cleansing logic is in the wrong place and it will be executed after '$1,000' has been turned into a zero.
When you assign a value to a property, ActiveRecord will apply some type conversions along the way. What happens when you try to convert '$1,000' to a number? You get zero of course. You can watch this happening if you play around in the console:
> a = M.find(id)
> puts a.some_number
11
> a.some_number = 'pancakes'
=> "pancakes"
> puts a.some_number
0
> a.some_number = '$1,000'
=> "1,000"
> puts a.some_number
0
> a.some_number = '1000'
=> "1000"
> puts a.some_number
1000
So, your data cleanup has to take place before the data goes into the model instance because as soon as AR gets its hands on the value, your '$1,000' will become 0 and all is lost. I'd put the logic in the controller, the controller's job is to mediate between the outside world and the models and data formatting and mangling certainly counts as mediation. So you could have something like this in your controller:
def some_controller
fix_numbers_in(:profit)
# assign from params as usual...
end
private
def fix_numbers_in(*which)
which.select { |p| params.has_key?(p) }.each do |p|
params[p] = params[p].gsub(/\D/, '') # Or whatever works for you
end
end
Then everything would be clean before ActiveRecord gets its grubby little hands on your data and makes a mess of things.
You could do similar things by overriding the profit= method in your model but that's really not the model's job.
class User < ActiveRecord::Base
before_save :format_values
private
def format_values
self.profit = profit.to_s.gsub(/\D/,'') if profit
end
end
def format_values
self.profit.to_d!
end
I recommend you to write custom setter for this particular instance variable #profit:
class User
attr_accessor :profit
def profit= value
#profit = value.gsub(/\D/,'')
end
end
u = User.new
u.profit = "$1,000"
p u.profit # => "1000"
I would suggest using the rails helper of number with precision. Below is some code.
Generic Example:
number_with_precision(111.2345, :precision => 1, :significant => true) # => 100
Rails code Example:
def profit=(new_profit)
number_with_precision(self[:profit], :precision => 1, :significant => true)
end

Papertrail and Carrierwave

I have a model that use both: Carrierwave for store photos, and PaperTrail for versioning.
I also configured Carrierwave for store diferent files when updates (That's because I want to version the photos) with config.remove_previously_stored_files_after_update = false
The problem is that PaperTrail try to store the whole Ruby Object from the photo (CarrierWave Uploader) instead of simply a string (that would be its url)
(version table, column object)
---
first_name: Foo
last_name: Bar
photo: !ruby/object:PhotoUploader
model: !ruby/object:Bla
attributes:
id: 2
first_name: Foo1
segundo_nombre: 'Bar1'
........
How can I fix this to store a simple string in the photo version?
You can override item_before_change on your versioned model so you don't call the uploader accesor directly and use write_attribute instead. Alternatively, since you might want to do that for several models, you can monkey-patch the method directly, like this:
module PaperTrail
module Model
module InstanceMethods
private
def item_before_change
previous = self.dup
# `dup` clears timestamps so we add them back.
all_timestamp_attributes.each do |column|
previous[column] = send(column) if respond_to?(column) && !send(column).nil?
end
previous.tap do |prev|
prev.id = id
changed_attributes.each do |attr, before|
if defined?(CarrierWave::Uploader::Base) && before.is_a?(CarrierWave::Uploader::Base)
prev.send(:write_attribute, attr, before.url && File.basename(before.url))
else
prev[attr] = before
end
end
end
end
end
end
end
Not sure if it's the best solution, but it seems to work.
Adding #beardedd's comment as an answer because I think this is a better way to handle the problem.
Name your database columns something like picture_filename and then in your model mount the uploader using:
class User < ActiveRecord::Base
has_paper_trail
mount_uploader :picture, PictureUploader, mount_on: :picture_filename
end
You still use the user.picture.url attribute to access your model but PaperTrail will store revisions under picture_filename.
Here is a bit updated version of monkeypatch from #rabusmar, I use it for rails 4.2.0 and paper_trail 4.0.0.beta2, in /config/initializers/paper_trail.rb.
The second method override is required if you use optional object_changes column for versions. It works in a bit strange way for carrierwave + fog if you override filename in uploader, old value will be from cloud and new one from local filename, but in my case it's ok.
Also I have not checked if it works correctly when you restore old version.
module PaperTrail
module Model
module InstanceMethods
private
# override to keep only basename for carrierwave attributes in object hash
def item_before_change
previous = self.dup
# `dup` clears timestamps so we add them back.
all_timestamp_attributes.each do |column|
if self.class.column_names.include?(column.to_s) and not send("#{column}_was").nil?
previous[column] = send("#{column}_was")
end
end
enums = previous.respond_to?(:defined_enums) ? previous.defined_enums : {}
previous.tap do |prev|
prev.id = id # `dup` clears the `id` so we add that back
changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each do |attr, before|
if defined?(CarrierWave::Uploader::Base) && before.is_a?(CarrierWave::Uploader::Base)
prev.send(:write_attribute, attr, before.url && File.basename(before.url))
else
before = enums[attr][before] if enums[attr]
prev[attr] = before
end
end
end
end
# override to keep only basename for carrierwave attributes in object_changes hash
def changes_for_paper_trail
_changes = changes.delete_if { |k,v| !notably_changed.include?(k) }
if PaperTrail.serialized_attributes?
self.class.serialize_attribute_changes(_changes)
end
if defined?(CarrierWave::Uploader::Base)
Hash[
_changes.to_hash.map do |k, values|
[k, values.map { |value| value.is_a?(CarrierWave::Uploader::Base) ? value.url && File.basename(value.url) : value }]
end
]
else
_changes.to_hash
end
end
end
end
end
This is what actually functions for me, put this on config/initializers/paper_trail/.rb
module PaperTrail
module Reifier
class << self
def reify_attributes(model, version, attrs)
enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
attrs.each do |k, v|
is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k)
if model.send("#{k}").is_a?(CarrierWave::Uploader::Base)
if v.present?
model.send("remote_#{k}_url=", v["#{k}"][:url])
model.send("#{k}").recreate_versions!
else
model.send("remove_#{k}!")
end
else
if model.has_attribute?(k) && !is_enum_without_type_caster
model[k.to_sym] = v
elsif model.respond_to?("#{k}=")
model.send("#{k}=", v)
elsif version.logger
version.logger.warn(
"Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
)
end
end
end
end
end
end
end
This overrides the reify method to work on S3 + heroku
For uploaders to keep old files from updated or deleted records do this in the uploader
configure do |config|
config.remove_previously_stored_files_after_update = false
end
def remove!
true
end
Then make up some routine to clear old files from time to time, good luck
I want to add to the previous answers the following:
It can happen that you upload different files with the same name, and this may overwrite your previous file, so you won't be able to restore the old one.
You may use a timestamp in file names or create random and unique filenames for all versioned files.
Update
This doesn't seem to work in all edge cases for me, when assigning more than a single file to the same object within a single request request.
I'm using this right now:
def filename
[#cache_id, original_filename].join('-') if original_filename.present?
end
This seems to work, as the #cache_id is generated for each and every upload again (which isn't the case as it seems for the ideas provided in the links above).
#Sjors Provoost
We also need to override pt_recordable_object method in PaperTrail::Model::InstanceMethods module
def pt_recordable_object
attr = attributes_before_change
object_attrs = object_attrs_for_paper_trail(attr)
hash = Hash[
object_attrs.to_hash.map do |k, value|
[k, value.is_a?(CarrierWave::Uploader::Base) ? value.url && File.basename(value.url) : value ]
end
]
if self.class.paper_trail_version_class.object_col_is_json?
hash
else
PaperTrail.serializer.dump(hash)
end
end

Manually set updated_at in Rails

I'm migrating my old blog posts into my new Rails blog, and I want their updated_at attribute to match the corresponding value on my old blog (not the date they were migrated into my new Rails blog).
How can I do this? When I set updated_at manually it gets overridden by the before_save callback.
Note: This question is only valid for Rails < 3.2.11. Newer versions of Rails allow you to manually set timestamps without them being overwritten.
If it's a one time thing you can turn record_timestamps on or off.
ActiveRecord::Base.record_timestamps = false
#set timestamps manually
ActiveRecord::Base.record_timestamps = true
When I ran into this issue with my app, I searched around for a bit and this seemed like it made the most sense to me. It's an initializer that I can call where I need to:
module ActiveRecord
class Base
def update_record_without_timestamping
class << self
def record_timestamps; false; end
end
save!
class << self
def record_timestamps; super ; end
end
end
end
end
As of recent versions of Rails (3.2.11 as per iGELs comment) you can set the updated_at property in code and the change will be honoured when saving.
I assume rails is keeping track of 'dirty' properties that have been manually changed and not overwriting on save.
> note = Note.last
Note Load (1.4ms) SELECT "notes".* FROM "notes" ORDER BY "notes"."id" DESC LIMIT 1
=> #<Note id: 39, content: "A wee note", created_at: "2015-06-09 11:06:01", updated_at: "2015-06-09 11:06:01">
> note.updated_at = 2.years.ago
=> Sun, 07 Jul 2013 21:20:47 UTC +00:00
> note.save
(0.4ms) BEGIN
(0.8ms) UPDATE "notes" SET "updated_at" = '2013-07-07 21:20:47.972990' WHERE "notes"."id" = 39
(0.8ms) COMMIT
=> true
> note
=> #<Note id: 39, content: "A wee note", created_at: "2015-06-09 11:06:01", updated_at: "2013-07-07 21:20:47">
So short answer, workarounds are not needed any longer in recent versions of rails.
I see two ways to accomplish this easily:
touch (Rails >=5)
In Rails 5 you can use the touch method and give a named parameter time like described in the documentation of touch
foo.touch(time: old_timestamp)
update_column (Rails >=4)
If you want it in Rails 4 and lower or want to avoid all callbacks you could use one of the update_column or update_columns methods which bypass all safe or touch callbacks and validations
foo.update_column(updated_at, old_timestamp)
or
foo.update_columns(updated_at: old_timestamp)
I took Andy's answer and modified it to accept blocks:
module ActiveRecord
class Base
def without_timestamping
class << self
def record_timestamps; false; end
end
yield
class << self
remove_method :record_timestamps
end
end
end
end
This is riffing off of Andy Gaskell's answer:
class ActiveRecord::Base
class_inheritable_writer :record_timestamps
def do_without_changing_timestamps
self.class.record_timestamps = false
yield
ensure
self.class.record_timestamps = true
end
end
The solution is to temporarily set ActiveRecord::Base.record_timestamps to false:
ActiveRecord::Base.record_timestamps = false
# Make whatever changes you want to the timestamps here
ActiveRecord::Base.record_timestamps = true
If you want a somewhat more robust solution, you may want to try something like what mrm suggested:
module ActiveRecord
class Base
def self.without_timestamping
timestamping = self.record_timestamps
begin
self.record_timestamps = false
yield
ensure
self.record_timestamps = timestamping
end
end
end
end
Then you can easily make changes to models without their timestamps being automatically updated:
ActiveRecord::Base.without_timestamping do
foo = Foo.first
bar = Bar.first
foo.updated_at = 1.month.ago
bar.updated_at = foo.updated_at + 1.week
foo.save!
bar.save!
end
Or, if you only want to update records from a specific class without timestamping:
module ActiveRecord
class Base
# Don't delete Rail's ActiveRecord::Base#inherited method
self.singleton_class.send(:alias_method, :__inherited__, :inherited)
def self.inherited(subclass)
__inherited__
# Adding class methods to `subclass`
class << subclass
def without_timestamping
# Temporarily override record_timestamps for this class
class << self
def record_timestamps; false; end
end
yield
ensure
class << self
remove_method :record_timestamps
end
end
end
end
end
end
E.g:
Foo.without_timestamping do
foo = Foo.first
bar = Bar.new(foo: foo)
foo.updated_at = 1.month.ago
foo.save! # Timestamps not automatically updated
bar.save! # Timestamps updated normally
end
Or you could use an approach similar to what Venkat D. suggested, which works on a per-instance basis:
module ActiveRecord
class Base
def without_timestamping
class << self
def record_timestamps; false; end
end
yield
ensure
class << self
remove_method :record_timestamps
end
end
end
end
E.g:
foo = Foo.first
foo.without_timestamping do
foo2 = Foo.new(parent: foo)
foo.updated_at = 1.month.ago
foo.save! # Timestamps not automatically updated
foo2.save! # Timestamps updated normally
end

Resources