diff --git a/README.md b/README.md index 7a076c2c..b0b83f83 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/audited.rb b/lib/audited.rb index 096959d7..6360f2f7 100644 --- a/lib/audited.rb +++ b/lib/audited.rb @@ -6,6 +6,7 @@ module Audited # Wrapper around ActiveSupport::CurrentAttributes class RequestStore < ActiveSupport::CurrentAttributes attribute :audited_store + attribute :audit_context end class << self @@ -34,6 +35,10 @@ def store RequestStore.audited_store ||= {} end + def context + RequestStore.audit_context ||= {} + end + def config yield(self) end diff --git a/lib/audited/audit.rb b/lib/audited/audit.rb index 54a51f18..c0a6643e 100644 --- a/lib/audited/audit.rb +++ b/lib/audited/audit.rb @@ -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 @@ -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 diff --git a/lib/audited/auditor.rb b/lib/audited/auditor.rb index c9c867ae..5b6884db 100644 --- a/lib/audited/auditor.rb +++ b/lib/audited/auditor.rb @@ -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 @@ -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 diff --git a/lib/generators/audited/install_generator.rb b/lib/generators/audited/install_generator.rb index 8bf64182..a427ddf5 100644 --- a/lib/generators/audited/install_generator.rb +++ b/lib/generators/audited/install_generator.rb @@ -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__) diff --git a/lib/generators/audited/templates/add_context_to_audits.rb b/lib/generators/audited/templates/add_context_to_audits.rb new file mode 100644 index 00000000..fcd5b86e --- /dev/null +++ b/lib/generators/audited/templates/add_context_to_audits.rb @@ -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 diff --git a/lib/generators/audited/templates/install.rb b/lib/generators/audited/templates/install.rb index 5c6807f9..201c104e 100644 --- a/lib/generators/audited/templates/install.rb +++ b/lib/generators/audited/templates/install.rb @@ -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 @@ -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 diff --git a/lib/generators/audited/upgrade_generator.rb b/lib/generators/audited/upgrade_generator.rb index b66d082d..984a868e 100644 --- a/lib/generators/audited/upgrade_generator.rb +++ b/lib/generators/audited/upgrade_generator.rb @@ -14,11 +14,17 @@ 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 - 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 @@ -64,6 +70,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 diff --git a/spec/audited/auditor_spec.rb b/spec/audited/auditor_spec.rb index cff4044b..19d0c74c 100644 --- a/spec/audited/auditor_spec.rb +++ b/spec/audited/auditor_spec.rb @@ -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 { @@ -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) diff --git a/spec/support/active_record/schema.rb b/spec/support/active_record/schema.rb index 7145bc0c..5c770d0f 100644 --- a/spec/support/active_record/schema.rb +++ b/spec/support/active_record/schema.rb @@ -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 diff --git a/test/upgrade_generator_test.rb b/test/upgrade_generator_test.rb index 3ec3a6b8..76fe144c 100644 --- a/test/upgrade_generator_test.rb +++ b/test/upgrade_generator_test.rb @@ -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, :context, :jsonb/, 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, :context, :jsonb/, content) + end + end end