Skip to content

Instantly share code, notes, and snippets.

@rob-mcgrail
Last active May 22, 2017 23:05
Show Gist options
  • Save rob-mcgrail/fa6c2371fe8cab890b7503baffb1c784 to your computer and use it in GitHub Desktop.
Save rob-mcgrail/fa6c2371fe8cab890b7503baffb1c784 to your computer and use it in GitHub Desktop.
module EntityChangeCallbacks
extend ActiveSupport::Concern
# This concern allows us to treat lists of entity
# attributes as a unit, with their own create and update
# callbacks.
#
#
# create_change_callback name: :profile,
# attributes: [
# :business_name,
# :company_name,
# :website_url
# ]
#
# The above code will:
#
# * Trigger a before/after_update_profile callback for any
# after_save-triggering update involving those attributes.
#
# If there is a profile_updated_at DateTime
# column on Entity, it will also:
#
# * Update the profile_updated_at attribute on every change to
# the listed attributes.
#
# * Provide a profile_status method, returning true if any
# of those attribtues has ever been set. (Return null if no column set).
#
# * Trigger before/after_create_profile callback first time any
# of the columns is updated.
#
#
# If all you want is the update_profile callback, there's no need to
# have the updated_at column.
#
# You can track columns in multiple overlapping groupings.
module SharedMethods
def status_method_symbol(name)
"#{name}_status".to_sym
end
def update_callback_symbol(name)
"update_#{name}".to_sym
end
def create_callback_symbol(name)
"create_#{name}".to_sym
end
def timestamp_symbol(name)
"#{name}_updated_at".to_sym
end
end
module ClassMethods
include SharedMethods
def create_change_callback(name:, attributes:)
set_tracked_attributes name, attributes
define_change_callbacks name
define_status_method name
end
def set_tracked_attributes(name, attributes)
@tracked_attributes = {} unless @tracked_attributes
@tracked_attributes[name] = attributes
end
def tracked_attributes
@tracked_attributes || {}
end
def define_status_method(name)
define_method status_method_symbol(name) do
try(timestamp_symbol(name)).present?
end
end
def define_change_callbacks(name)
define_model_callbacks update_callback_symbol(name), create_callback_symbol(name)
end
def has_timestamp_column?(name)
column_names.include? timestamp_symbol(name).to_s
end
end
included do
include SharedMethods
after_save :check_for_attribute_changes
end
def check_for_attribute_changes
self.class.tracked_attributes.each do |name, attrs|
trigger_callback_if_changed(name, attrs)
end
end
def trigger_callback_if_changed(name, attributes)
if attribues_changes_exist?(attributes)
trigger_callbacks(name)
end
end
def attribues_changes_exist?(attributes)
(attributes & changes.keys.map(&:to_sym)).any?
end
def trigger_callbacks(name)
run_callbacks update_or_create_callback(name) do
attempt_timestamp_update(name)
end
end
def update_or_create_callback(name)
if self.class.has_timestamp_column?(name)
if send(timestamp_symbol(name))
return update_callback_symbol(name)
else
return create_callback_symbol(name)
end
end
# When there's no column to track we always
# consider it an update...
update_callback_symbol(name)
end
def attempt_timestamp_update(name)
return nil unless self.class.has_timestamp_column?(name)
update_column(
timestamp_symbol(name),
Time.now
)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment