Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.audited_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.audited_context # => {"class_name"=>"User", "id"=>1}

user.update!(name: "Brian", audit_context: {sample_key: "sample_value"})
user.audits.last.audited_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
11 changes: 11 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,16 @@ def store
RequestStore.audited_store ||= {}
end

def context
RequestStore.audit_context ||= {}
end

def context=(value)
raise "context must be a hash" unless value.is_a?(Hash)

RequestStore.audit_context = value
end

def config
yield(self)
end
Expand Down
47 changes: 28 additions & 19 deletions lib/audited/audit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,48 @@ module Audited
#

class YAMLIfTextColumnType
class << self
def load(obj)
if text_column?
ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
else
obj
end
end
def initialize(audit_class, column_name)
@audit_class = audit_class
@column_name = column_name
end

def dump(obj)
if text_column?
ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
else
obj
end
def load(obj)
if text_column?
ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
else
obj
end
end

def text_column?
Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
def dump(obj)
if text_column?
ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
else
obj
end
end

def text_column?
@audit_class.columns_hash[@column_name].type.to_s == "text"
end
end

class Audit < ::ActiveRecord::Base
belongs_to :auditable, polymorphic: true
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_audited_context

cattr_accessor :audited_class_names
self.audited_class_names = Set.new

if ::ActiveRecord.version >= Gem::Version.new("7.1")
serialize :audited_changes, coder: YAMLIfTextColumnType
serialize :audited_changes, coder: YAMLIfTextColumnType.new(self, "audited_changes")
serialize :audited_context, coder: YAMLIfTextColumnType.new(self, "audited_context")
else
serialize :audited_changes, YAMLIfTextColumnType
serialize :audited_changes, YAMLIfTextColumnType.new(self, "audited_changes")
serialize :audited_context, YAMLIfTextColumnType.new(self, "audited_context")
end

scope :ascending, -> { reorder(version: :asc) }
Expand Down Expand Up @@ -198,5 +203,9 @@ def set_request_uuid
def set_remote_address
self.remote_address ||= ::Audited.store[:current_remote_address]
end

def set_audited_context
self.audited_context = (::Audited.context || {}).merge(audited_context || {})
end
end
end
13 changes: 7 additions & 6 deletions lib/audited/auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def set_audit(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

set_audited_options(options)

Expand Down Expand Up @@ -204,7 +204,8 @@ def own_and_associated_audits
# Combine multiple audits into one.
def combine_audits(audits_to_combine)
combine_target = audits_to_combine.last
combine_target.audited_changes = audits_to_combine.pluck(:audited_changes).reduce(&:merge)
combine_target.audited_changes = audits_to_combine.select(:audited_changes).reduce({}) { |changes, audit| changes.merge!(audit.audited_changes || {}) }
combine_target.audited_context = audits_to_combine.select(:audited_context).reduce({}) { |context, audit| context.merge!(audit.audited_context || {}) }
combine_target.comment = "#{combine_target.comment}\nThis audit is the result of multiple audits being combined."

transaction do
Expand Down Expand Up @@ -348,27 +349,27 @@ def audits_to(version = nil)

def audit_create
write_audit(action: "create", audited_changes: audited_attributes,
comment: audit_comment)
comment: audit_comment, audited_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, audited_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, audited_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, audited_context: audit_context)
end
end

Expand Down
2 changes: 2 additions & 0 deletions lib/generators/audited/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ 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
class_option :audited_context_column_type, type: :string, default: "text", 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 %>, :audited_context, :<%= options[:audited_context_column_type] %>
end

def self.down
remove_column :<%= table_name %>, :audited_context
end
end
20 changes: 10 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,22 @@ 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 :audited_context, :<%= options[:audited_context_column_type] %>
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
15 changes: 13 additions & 2 deletions lib/generators/audited/upgrade_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ class UpgradeGenerator < Rails::Generators::Base
include Audited::Generators::MigrationHelper
extend Audited::Generators::Migration

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

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

def copy_templates
migrations_to_be_applied do |m|
migration_template "#{m}.rb", "db/migrate/#{m}.rb"
migrations_to_be_applied do |template_name|
name = "db/migrate/#{template_name}.rb"
if options[:audited_table_name] != "audits"
name = name.gsub("_to_audits", "_to_#{options[:audited_table_name]}")
end
migration_template "#{template_name}.rb", name
end
end

Expand Down Expand Up @@ -64,6 +71,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
2 changes: 1 addition & 1 deletion spec/audited/audit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUse
end

it "does not unserialize from binary columns" do
allow(Audited::YAMLIfTextColumnType).to receive(:text_column?).and_return(false)
allow_any_instance_of(Audited::YAMLIfTextColumnType).to receive(:text_column?).and_return(false)
audit.audited_changes = {foo: "bar"}
expect(audit.audited_changes).to eq "{:foo=>\"bar\"}"
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.audited_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.audited_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 :audited_context, :text
t.column :created_at, :datetime
end

Expand Down
20 changes: 20 additions & 0 deletions test/upgrade_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,24 @@ class UpgradeGeneratorTest < Rails::Generators::TestCase
assert_includes(content, "class AddCommentToAudits < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]\n")
end
end

test "generate migration with context column change" do
load_schema 6

run_generator %w[upgrade]

assert_migration "db/migrate/add_context_to_audits.rb" do |content|
assert_match(/add_column :audits, :audited_context, :text/, content)
end
end

test "generate migration with context column change for custom table name" do
load_schema 6

run_generator %w[upgrade --audited_table_name=custom_audits]

assert_migration "db/migrate/add_context_to_custom_audits.rb" do |content|
assert_match(/add_column :custom_audits, :audited_context, :text/, content)
end
end
end