Skip to content

Instantly share code, notes, and snippets.

@ryenski
Last active December 5, 2018 12:36
Show Gist options
  • Save ryenski/50a09f09e6cc2cc3d7d102fec0b71cd6 to your computer and use it in GitHub Desktop.
Save ryenski/50a09f09e6cc2cc3d7d102fec0b71cd6 to your computer and use it in GitHub Desktop.
Audit trail activity tracker, similar to PaperTrail, but without undo.
# == Schema Information
#
# Table name: activities
#
# id :uuid not null, primary key
# user_id :uuid
# tenant_id :uuid
# trackable_type :string
# trackable_id :uuid
# action :string
# tracked_changes :json
# detail :string
# visibility :integer default("report")
# created_at :datetime not null
# updated_at :datetime not null
#
class Activity < ApplicationRecord
belongs_to :tenant
belongs_to :user # who did it
belongs_to :trackable, -> { unscope(where: :trashed_at) }, polymorphic: true # the tracked object
scope :archived, -> {where(action: 'archived')}
scope :archived_or_trashed, -> {where(action: %w(archived trashed))}
store_accessor :tracked_changes
scope :created, -> {where(action: 'create')}
after_initialize :set_defaults
def set_defaults
if self.new_record?
self.tenant_id ||= Current.tenant_id
self.user_id ||= Current.user_id
end
end
# visibility
# * report: (default) Only shows up in system reports (e.g. login activity, etc. )
# * member: Only the member can see
# * members_only: (default) Only logged in members can see
# * everyone: anyone can see, logged in or not
enum visibility: [:report, :member, :members_only, :everyone]
def summary
[action, trackable_type].join(' ')
end
alias_method :to_s, :summary
end
class ActivityCreator
def initialize(action, trackable, comment=nil)
@trackable = trackable # the affected object
@action = action # what was done
@comment = comment
@changes = collect_changes
end
def call
attrs = {
tenant_id: Current.tenant_id,
user_id: Current.user_id,
trackable: trackable,
action: action,
tracked_changes: changes
}
# trackable.run_callbacks(:track) { self.activities.create(attrs) }
unless changes.empty? && action == 'update'
Activity.create!(attrs)
end
end
attr_accessor :user, :action, :trackable, :comment, :changes
private
IGNORE_ATTRS = [
:created_at,
:password_digest,
:registration_token,
:auth_token,
:tenant_id,
:updated_at,
]
# Collect & flatten out custom field attributes so we can treat them as if they were attributes on this object.
CUSTOM_FIELD_ATTRS = CustomField.field_types.collect{|k,v| "value_#{k}"}
def collect_custom_fields(changes)
if trackable.respond_to?(:custom_field_values)
for cfv in trackable.custom_field_values
if cfv.previous_changes.any?
changes.update("#{cfv.name}" => cfv.previous_changes.detect{|k,v| CUSTOM_FIELD_ATTRS.include?(k) }.try('[]', 1))
end
end
end
return changes
end
def collect_referenced_objects(changes)
if values = changes.delete("plan_id")
from = Plan.where(id: values[0]).first.try(:to_param)
to = Plan.where(id: values[1]).first.try(:to_param)
changes["plan"] = [from, to]
end
return changes
end
def remove_empty(changes)
changes.delete_if{|k,v| v[0].blank? and v[1].blank?}
end
def collect_changes
if action == 'create'
IGNORE_ATTRS.push(:id)
end
changes = trackable.previous_changes.except(*IGNORE_ATTRS)
changes = collect_custom_fields(changes)
changes = collect_referenced_objects(changes)
changes = remove_empty(changes)
changes = changes
end
end
class Message < ApplicationRecord
# ...
include Trackable
# ...
end
module Trackable
extend ActiveSupport::Concern
included do
has_many :activities, -> {order("created_at DESC")}, as: :trackable, dependent: :nullify
after_commit :track_activity
before_destroy :track_destroy
end
def track_destroy
ActivityCreator.new('destroy', self).call
end
def track_activity
action = previous_changes.include?('id') ? 'create' : 'update'
ActivityCreator.new(action, self).call
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment