Skip to content

Commit

Permalink
Implement additional context for audit record
Browse files Browse the repository at this point in the history
  • Loading branch information
amkisko committed Oct 29, 2024
1 parent 8dc7184 commit 1a16b59
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 17 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,26 @@ You can ignore the default callbacks globally unless the callback action is spec
Audited.ignored_default_callbacks = [:create, :update] # ignore callbacks create and update
```

### Context

You can attach context to each audit using an `audit_context` attribute on your model.

```ruby
user.update!(name: "Ryan", audit_context: {class_name: self.class.name, id: self.id})
user.audits.last.context # => {"class_name"=>"User", "id"=>1}
```

or using global context, it will be merged with the model context:

```ruby
Audited.context = {class_name: self.class.name, id: self.id}
user.update!(name: "Ryan")
user.audits.last.context # => {"class_name"=>"User", "id"=>1}

user.update!(name: "Brian", audit_context: {sample_key: "sample_value"})
user.audits.last.context # => {"class_name"=>"User", "id"=>2, "sample_key"=>"sample_value"}
```

### Comments

You can attach comments to each audit using an `audit_comment` attribute on your model.
Expand Down
5 changes: 5 additions & 0 deletions lib/audited.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Audited
# Wrapper around ActiveSupport::CurrentAttributes
class RequestStore < ActiveSupport::CurrentAttributes
attribute :audited_store
attribute :audit_context
end

class << self
Expand Down Expand Up @@ -34,6 +35,10 @@ def store
RequestStore.audited_store ||= {}
end

def context
RequestStore.audit_context ||= {}
end

def config
yield(self)
end
Expand Down
6 changes: 5 additions & 1 deletion lib/audited/audit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Audit < ::ActiveRecord::Base
belongs_to :user, polymorphic: true
belongs_to :associated, polymorphic: true

before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address, :set_audit_context

cattr_accessor :audited_class_names
self.audited_class_names = Set.new
Expand Down Expand Up @@ -198,5 +198,9 @@ def set_request_uuid
def set_remote_address
self.remote_address ||= ::Audited.store[:current_remote_address]
end

def set_audit_context
self.context = (::Audited.context || {}).merge(context || {})
end
end
end
10 changes: 5 additions & 5 deletions lib/audited/auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def audited(options = {})

class_attribute :audit_associated_with, instance_writer: false
class_attribute :audited_options, instance_writer: false
attr_accessor :audit_version, :audit_comment
attr_accessor :audit_version, :audit_comment, :audit_context

self.audited_options = options
normalize_audited_options
Expand Down Expand Up @@ -332,27 +332,27 @@ def audits_to(version = nil)

def audit_create
write_audit(action: "create", audited_changes: audited_attributes,
comment: audit_comment)
comment: audit_comment, context: audit_context)
end

def audit_update
unless (changes = audited_changes(exclude_readonly_attrs: true)).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
write_audit(action: "update", audited_changes: changes,
comment: audit_comment)
comment: audit_comment, context: audit_context)
end
end

def audit_touch
unless (changes = audited_changes(for_touch: true, exclude_readonly_attrs: true)).empty?
write_audit(action: "update", audited_changes: changes,
comment: audit_comment)
comment: audit_comment, context: audit_context)
end
end

def audit_destroy
unless new_record?
write_audit(action: "destroy", audited_changes: audited_attributes,
comment: audit_comment)
comment: audit_comment, context: audit_context)
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/generators/audited/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class InstallGenerator < Rails::Generators::Base

class_option :audited_changes_column_type, type: :string, default: "text", required: false
class_option :audited_user_id_column_type, type: :string, default: "integer", required: false
class_option :audited_table_name, type: :string, default: "audits", required: false

source_root File.expand_path("../templates", __FILE__)

Expand Down
12 changes: 12 additions & 0 deletions lib/generators/audited/templates/add_context_to_audits.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

<%- table_name = options[:audited_table_name].underscore.pluralize -%>
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
add_column :<%= table_name %>, :context, :jsonb
end

def self.down
remove_column :<%= table_name %>, :context
end
end
19 changes: 9 additions & 10 deletions lib/generators/audited/templates/install.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# frozen_string_literal: true

<%- table_name = options[:audited_table_name].underscore.pluralize -%>
class <%= migration_class_name %> < <%= migration_parent %>
def self.up
create_table :audits, :force => true do |t|
create_table :<%= table_name %> do |t|
t.column :auditable_id, :integer
t.column :auditable_type, :string
t.column :associated_id, :integer
Expand All @@ -12,21 +11,21 @@ def self.up
t.column :username, :string
t.column :action, :string
t.column :audited_changes, :<%= options[:audited_changes_column_type] %>
t.column :version, :integer, :default => 0
t.column :version, :integer, default: 0
t.column :comment, :string
t.column :remote_address, :string
t.column :request_uuid, :string
t.column :created_at, :datetime
end

add_index :audits, [:auditable_type, :auditable_id, :version], :name => 'auditable_index'
add_index :audits, [:associated_type, :associated_id], :name => 'associated_index'
add_index :audits, [:user_id, :user_type], :name => 'user_index'
add_index :audits, :request_uuid
add_index :audits, :created_at
add_index :<%= table_name %>, [:auditable_type, :auditable_id, :version], name: "<%= table_name %>_auditable_index"
add_index :<%= table_name %>, [:associated_type, :associated_id], name: "<%= table_name %>_associated_index"
add_index :<%= table_name %>, [:user_id, :user_type], name: "<%= table_name %>_user_index"
add_index :<%= table_name %>, :request_uuid
add_index :<%= table_name %>, :created_at
end

def self.down
drop_table :audits
drop_table :<%= table_name %>
end
end
6 changes: 6 additions & 0 deletions lib/generators/audited/upgrade_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class UpgradeGenerator < Rails::Generators::Base
include Audited::Generators::MigrationHelper
extend Audited::Generators::Migration

class_option :audited_table_name, type: :string, default: "audits", required: false

source_root File.expand_path("../templates", __FILE__)

def copy_templates
Expand Down Expand Up @@ -64,6 +66,10 @@ def migrations_to_be_applied
if indexes.any? { |i| i.columns == %w[auditable_type auditable_id] }
yield :add_version_to_auditable_index
end

unless columns.include?("context")
yield :add_context_to_audits
end
end
end
end
Expand Down
15 changes: 14 additions & 1 deletion spec/audited/auditor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ class CallbacksSpecified < ::ActiveRecord::Base
end

describe "on create" do
let(:user) { create_user status: :reliable, audit_comment: "Create" }
let(:user) { create_user status: :reliable, audit_comment: "Create", audit_context: {sample_key: "sample_value"} }

it "should change the audit count" do
expect {
Expand Down Expand Up @@ -370,6 +370,19 @@ class CallbacksSpecified < ::ActiveRecord::Base
expect(user.audits.first.comment).to eq("Create")
end

it "should store context" do
expect(user.audits.first.context).to eq({"sample_key" => "sample_value"})
end

context "with global context" do
before { Audited.context[:global_key] = "global_value" }
after { Audited.context.delete(:global_key) }

it "should merge global context" do
expect(user.audits.first.context).to eq({"sample_key" => "sample_value", "global_key" => "global_value"})
end
end

it "should not audit an attribute which is excepted if specified on create or destroy" do
on_create_destroy_except_name = Models::ActiveRecord::OnCreateDestroyExceptName.create(name: "Bart")
expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any? { |col| ["name"].include? col }).to eq(false)
Expand Down
1 change: 1 addition & 0 deletions spec/support/active_record/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
t.column :comment, :string
t.column :remote_address, :string
t.column :request_uuid, :string
t.column :context, :jsonb
t.column :created_at, :datetime
end

Expand Down

0 comments on commit 1a16b59

Please sign in to comment.