Skip to content

Instantly share code, notes, and snippets.

@smook1980
Created September 30, 2015 20:41
Show Gist options
  • Save smook1980/6e95f1c903167bff3d59 to your computer and use it in GitHub Desktop.
Save smook1980/6e95f1c903167bff3d59 to your computer and use it in GitHub Desktop.
A Mixin To Add Attribute Change Tracking to Models
require 'active_support/concern'
module Model
ChangeEvent = Struct.new(:event, :condition)
class ChangeTracker
def initialize
@attributes = {}
end
def track(attribute, event:, condition:)
@attributes[attribute.to_s.freeze] = ChangeEvent.new(event, condition)
end
def process_changeset(client, changes)
@attributes.keys.each do |attribute|
old, new = changes[attribute]
process(client, @attributes[attribute], old, new)
end
end
private
def process(client, change_event, old_value, new_value)
if is_valid(change_event, old_value, new_value)
# TrackingEvents.create!(...)
puts "Trigger #{change_event.event} event for model attribute change from #{old_value} to #{new_value}"
else
puts "Did not trigger #{change_event.event} event for model attribute change from #{old_value} to #{new_value}, condition returned false."
end
end
def is_valid(change_event, old_value, new_value)
change_event.condition.call(old_value, new_value)
end
end
module ChangeTracking
extend ActiveSupport::Concern
class_methods do
def change_tracking_tracker
@change_tracking_tracker ||= ChangeTracker.new
end
def track(attribute, event:, condition:)
change_tracking_tracker.track(attribute, event: event, condition: condition)
end
end
included do
around_update :trigger_for_update
def trigger_for_update
raise 'Model must respond to "client" when implement ChangeTracking!'.freeze unless self.respond_to? :client
puts "*" * 50
changeset = changes
puts "Triggering change event for attributes: #{changeset}"
# Run update SQL...
yield
# Trigger change events if need be...
self.class.change_tracking_tracker.process_changeset(client, changeset)
puts "*" * 50
end
end
end
end
@smook1980
Copy link
Author

An example of how this would be used:

ClientAcademicData.class_eval do
  include Model::ChangeTracking
  track :class_rank, event: :class_rank, condition: -> (old_value, new_value) { old_value != new_value }
end

cad = ClientAcademicData.last
cad.class_rank = 2
cad.save!

# **************************************************
# Triggering change event for attributes: {"class_rank"=>[50, 2], "mod_date"=>[Wed, 30 Sep 2015 15:37:39 CDT -05:00, Wed, 30 Sep 2015 15:38:24 CDT -05:00]}
# Trigger class_rank event for model attribute change from 50 to 2
# **************************************************

cad.class_rank = 2
cad.save!

# **************************************************
# Triggering change event for attributes: {"mod_date"=>[Wed, 30 Sep 2015 15:38:24 CDT -05:00, Wed, 30 Sep 2015 15:38:25 CDT -05:00]}
# Did not trigger class_rank event for model attribute change from  to , condition returned false.
# **************************************************

cad.class_rank = 50
cad.save!

# **************************************************
# Triggering change event for attributes: {"class_rank"=>[2, 50], "mod_date"=>[Wed, 30 Sep 2015 15:38:25 CDT -05:00, Wed, 30 Sep 2015 15:38:25 CDT -05:00]}
# Trigger class_rank event for model attribute change from 2 to 50
# **************************************************
  • You can run this from a pry session once you've copied the above into app/concerns/models/change_tracking.rb.
  • You usually would just mix this into the model class instead of reopening the class. I did that to easily spike it out / prove it works.
  • Allows you to declaratively define the field to track, the name of the event it fires and finally a proc that takes the old and new values and returns true/false to indicate if the event should fire.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment