audited 4.7.1 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of audited might be problematic. Click here for more details.

Files changed (67) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -1
  3. data/.standard.yml +5 -0
  4. data/.travis.yml +35 -26
  5. data/Appraisals +27 -18
  6. data/CHANGELOG.md +97 -4
  7. data/Gemfile +1 -1
  8. data/README.md +88 -19
  9. data/Rakefile +6 -6
  10. data/gemfiles/rails50.gemfile +3 -0
  11. data/gemfiles/rails51.gemfile +3 -0
  12. data/gemfiles/rails52.gemfile +3 -1
  13. data/gemfiles/rails60.gemfile +10 -0
  14. data/gemfiles/rails61.gemfile +10 -0
  15. data/lib/audited-rspec.rb +3 -1
  16. data/lib/audited.rb +26 -8
  17. data/lib/audited/audit.rb +48 -43
  18. data/lib/audited/auditor.rb +137 -57
  19. data/lib/audited/railtie.rb +16 -0
  20. data/lib/audited/rspec_matchers.rb +5 -3
  21. data/lib/audited/sweeper.rb +3 -10
  22. data/lib/audited/version.rb +3 -1
  23. data/lib/generators/audited/install_generator.rb +9 -7
  24. data/lib/generators/audited/migration.rb +2 -0
  25. data/lib/generators/audited/migration_helper.rb +3 -1
  26. data/lib/generators/audited/templates/add_association_to_audits.rb +2 -0
  27. data/lib/generators/audited/templates/add_comment_to_audits.rb +2 -0
  28. data/lib/generators/audited/templates/add_remote_address_to_audits.rb +2 -0
  29. data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +2 -0
  30. data/lib/generators/audited/templates/add_version_to_auditable_index.rb +23 -0
  31. data/lib/generators/audited/templates/install.rb +3 -1
  32. data/lib/generators/audited/templates/rename_association_to_associated.rb +2 -0
  33. data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +2 -0
  34. data/lib/generators/audited/templates/rename_parent_to_association.rb +2 -0
  35. data/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb +2 -0
  36. data/lib/generators/audited/upgrade_generator.rb +20 -14
  37. data/spec/audited/audit_spec.rb +151 -62
  38. data/spec/audited/auditor_spec.rb +456 -239
  39. data/spec/audited/sweeper_spec.rb +29 -20
  40. data/spec/audited_spec.rb +18 -0
  41. data/spec/audited_spec_helpers.rb +7 -7
  42. data/spec/rails_app/app/assets/config/manifest.js +2 -0
  43. data/spec/rails_app/config/application.rb +7 -2
  44. data/spec/rails_app/config/database.yml +1 -0
  45. data/spec/rails_app/config/environment.rb +1 -1
  46. data/spec/rails_app/config/environments/test.rb +5 -5
  47. data/spec/rails_app/config/initializers/secret_token.rb +2 -2
  48. data/spec/spec_helper.rb +15 -13
  49. data/spec/support/active_record/models.rb +37 -11
  50. data/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb +1 -2
  51. data/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb +1 -2
  52. data/spec/support/active_record/schema.rb +28 -20
  53. data/test/db/version_1.rb +2 -2
  54. data/test/db/version_2.rb +2 -2
  55. data/test/db/version_3.rb +2 -3
  56. data/test/db/version_4.rb +2 -3
  57. data/test/db/version_5.rb +0 -1
  58. data/test/db/version_6.rb +2 -0
  59. data/test/install_generator_test.rb +18 -19
  60. data/test/test_helper.rb +6 -7
  61. data/test/upgrade_generator_test.rb +22 -17
  62. metadata +64 -29
  63. data/gemfiles/rails40.gemfile +0 -9
  64. data/gemfiles/rails41.gemfile +0 -8
  65. data/gemfiles/rails42.gemfile +0 -8
  66. data/spec/rails_app/config/environments/development.rb +0 -21
  67. data/spec/rails_app/config/environments/production.rb +0 -35
data/Rakefile CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env rake
2
2
 
3
- require 'bundler/gem_helper'
4
- require 'rspec/core/rake_task'
5
- require 'rake/testtask'
6
- require 'appraisal'
3
+ require "bundler/gem_helper"
4
+ require "rspec/core/rake_task"
5
+ require "rake/testtask"
6
+ require "appraisal"
7
7
 
8
- Bundler::GemHelper.install_tasks(name: 'audited')
8
+ Bundler::GemHelper.install_tasks(name: "audited")
9
9
 
10
10
  RSpec::Core::RakeTask.new(:spec)
11
11
 
12
12
  Rake::TestTask.new do |t|
13
13
  t.libs << "test"
14
- t.test_files = FileList['test/**/*_test.rb']
14
+ t.test_files = FileList["test/**/*_test.rb"]
15
15
  t.verbose = true
16
16
  end
17
17
 
@@ -3,5 +3,8 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rails", "~> 5.0.0"
6
+ gem "mysql2", ">= 0.3.18", "< 0.6.0"
7
+ gem "pg", ">= 0.18", "< 2.0"
8
+ gem "sqlite3", "~> 1.3.6"
6
9
 
7
10
  gemspec name: "audited", path: "../"
@@ -3,5 +3,8 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rails", "~> 5.1.4"
6
+ gem "mysql2", ">= 0.3.18", "< 0.6.0"
7
+ gem "pg", ">= 0.18", "< 2.0"
8
+ gem "sqlite3", "~> 1.3.6"
6
9
 
7
10
  gemspec name: "audited", path: "../"
@@ -3,6 +3,8 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rails", ">= 5.2.0", "< 5.3"
6
- gem "mysql2", "~> 0.4.4"
6
+ gem "mysql2", ">= 0.4.4", "< 0.6.0"
7
+ gem "pg", ">= 0.18", "< 2.0"
8
+ gem "sqlite3", "~> 1.3.6"
7
9
 
8
10
  gemspec name: "audited", path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", ">= 6.0.0", "< 6.1"
6
+ gem "mysql2", ">= 0.4.4"
7
+ gem "pg", ">= 0.18", "< 2.0"
8
+ gem "sqlite3", "~> 1.4"
9
+
10
+ gemspec name: "audited", path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", ">= 6.1.0", "< 6.2"
6
+ gem "mysql2", ">= 0.4.4"
7
+ gem "pg", ">= 1.1", "< 2.0"
8
+ gem "sqlite3", "~> 1.4"
9
+
10
+ gemspec name: "audited", path: "../"
data/lib/audited-rspec.rb CHANGED
@@ -1,4 +1,6 @@
1
- require 'audited/rspec_matchers'
1
+ # frozen_string_literal: true
2
+
3
+ require "audited/rspec_matchers"
2
4
  module RSpec::Matchers
3
5
  include Audited::RspecMatchers
4
6
  end
data/lib/audited.rb CHANGED
@@ -1,8 +1,15 @@
1
- require 'active_record'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
2
4
 
3
5
  module Audited
4
6
  class << self
5
- attr_accessor :ignored_attributes, :current_user_method, :max_audits
7
+ attr_accessor \
8
+ :auditing_enabled,
9
+ :current_user_method,
10
+ :ignored_attributes,
11
+ :max_audits,
12
+ :store_synthesized_enums
6
13
  attr_writer :audit_class
7
14
 
8
15
  def audit_class
@@ -10,7 +17,13 @@ module Audited
10
17
  end
11
18
 
12
19
  def store
13
- Thread.current[:audited_store] ||= {}
20
+ current_store_value = Thread.current.thread_variable_get(:audited_store)
21
+
22
+ if current_store_value.nil?
23
+ Thread.current.thread_variable_set(:audited_store, {})
24
+ else
25
+ current_store_value
26
+ end
14
27
  end
15
28
 
16
29
  def config
@@ -18,14 +31,19 @@ module Audited
18
31
  end
19
32
  end
20
33
 
21
- @ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
34
+ @ignored_attributes = %w[lock_version created_at updated_at created_on updated_on]
22
35
 
23
36
  @current_user_method = :current_user
37
+ @auditing_enabled = true
38
+ @store_synthesized_enums = false
24
39
  end
25
40
 
26
- require 'audited/auditor'
27
- require 'audited/audit'
41
+ require "audited/auditor"
42
+ require "audited/audit"
28
43
 
29
- ::ActiveRecord::Base.send :include, Audited::Auditor
44
+ ActiveSupport.on_load :active_record do
45
+ include Audited::Auditor
46
+ end
30
47
 
31
- require 'audited/sweeper'
48
+ require "audited/sweeper"
49
+ require "audited/railtie"
data/lib/audited/audit.rb CHANGED
@@ -1,4 +1,6 @@
1
- require 'set'
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
2
4
 
3
5
  module Audited
4
6
  # Audit saves the changes to ActiveRecord models. It has the following attributes:
@@ -16,7 +18,7 @@ module Audited
16
18
  class YAMLIfTextColumnType
17
19
  class << self
18
20
  def load(obj)
19
- if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
21
+ if text_column?
20
22
  ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
21
23
  else
22
24
  obj
@@ -24,18 +26,22 @@ module Audited
24
26
  end
25
27
 
26
28
  def dump(obj)
27
- if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
29
+ if text_column?
28
30
  ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
29
31
  else
30
32
  obj
31
33
  end
32
34
  end
35
+
36
+ def text_column?
37
+ Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
38
+ end
33
39
  end
34
40
  end
35
41
 
36
42
  class Audit < ::ActiveRecord::Base
37
- belongs_to :auditable, polymorphic: true
38
- belongs_to :user, polymorphic: true
43
+ belongs_to :auditable, polymorphic: true
44
+ belongs_to :user, polymorphic: true
39
45
  belongs_to :associated, polymorphic: true
40
46
 
41
47
  before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
@@ -45,16 +51,16 @@ module Audited
45
51
 
46
52
  serialize :audited_changes, YAMLIfTextColumnType
47
53
 
48
- scope :ascending, ->{ reorder(version: :asc) }
49
- scope :descending, ->{ reorder(version: :desc)}
50
- scope :creates, ->{ where(action: 'create')}
51
- scope :updates, ->{ where(action: 'update')}
52
- scope :destroys, ->{ where(action: 'destroy')}
54
+ scope :ascending, -> { reorder(version: :asc) }
55
+ scope :descending, -> { reorder(version: :desc) }
56
+ scope :creates, -> { where(action: "create") }
57
+ scope :updates, -> { where(action: "update") }
58
+ scope :destroys, -> { where(action: "destroy") }
53
59
 
54
- scope :up_until, ->(date_or_time){ where("created_at <= ?", date_or_time) }
55
- scope :from_version, ->(version){ where('version >= ?', version) }
56
- scope :to_version, ->(version){ where('version <= ?', version) }
57
- scope :auditable_finder, ->(auditable_id, auditable_type){ where(auditable_id: auditable_id, auditable_type: auditable_type)}
60
+ scope :up_until, ->(date_or_time) { where("created_at <= ?", date_or_time) }
61
+ scope :from_version, ->(version) { where("version >= ?", version) }
62
+ scope :to_version, ->(version) { where("version <= ?", version) }
63
+ scope :auditable_finder, ->(auditable_id, auditable_type) { where(auditable_id: auditable_id, auditable_type: auditable_type) }
58
64
  # Return all audits older than the current one.
59
65
  def ancestors
60
66
  self.class.ascending.auditable_finder(auditable_id, auditable_type).to_version(version)
@@ -65,43 +71,38 @@ module Audited
65
71
  def revision
66
72
  clazz = auditable_type.constantize
67
73
  (clazz.find_by_id(auditable_id) || clazz.new).tap do |m|
68
- self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(version: version))
74
+ self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(audit_version: version))
69
75
  end
70
76
  end
71
77
 
72
78
  # Returns a hash of the changed attributes with the new values
73
79
  def new_attributes
74
- (audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
75
- attrs[attr] = values.is_a?(Array) ? values.last : values
76
- attrs
80
+ (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
81
+ attrs[attr] = (action == "update" ? values.last : values)
77
82
  end
78
83
  end
79
84
 
80
85
  # Returns a hash of the changed attributes with the old values
81
86
  def old_attributes
82
- (audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
83
- attrs[attr] = Array(values).first
84
-
85
- attrs
87
+ (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
88
+ attrs[attr] = (action == "update" ? values.first : values)
86
89
  end
87
90
  end
88
91
 
89
92
  # Allows user to undo changes
90
93
  def undo
91
- model = self.auditable_type.constantize
92
- if action == 'create'
94
+ case action
95
+ when "create"
93
96
  # destroys a newly created record
94
- model.find(auditable_id).destroy!
95
- elsif action == 'destroy'
97
+ auditable.destroy!
98
+ when "destroy"
96
99
  # creates a new record with the destroyed record attributes
97
- model.create(audited_changes)
98
- else
100
+ auditable_type.constantize.create!(audited_changes)
101
+ when "update"
99
102
  # changes back attributes
100
- audited_object = model.find(auditable_id)
101
- self.audited_changes.each do |k, v|
102
- audited_object[k] = v[0]
103
- end
104
- audited_object.save
103
+ auditable.update!(audited_changes.transform_values(&:first))
104
+ else
105
+ raise StandardError, "invalid action given #{action}"
105
106
  end
106
107
  end
107
108
 
@@ -132,21 +133,20 @@ module Audited
132
133
  # All audits made during the block called will be recorded as made
133
134
  # by +user+. This method is hopefully threadsafe, making it ideal
134
135
  # for background operations that require audit information.
135
- def self.as_user(user, &block)
136
+ def self.as_user(user)
137
+ last_audited_user = ::Audited.store[:audited_user]
136
138
  ::Audited.store[:audited_user] = user
137
139
  yield
138
140
  ensure
139
- ::Audited.store[:audited_user] = nil
141
+ ::Audited.store[:audited_user] = last_audited_user
140
142
  end
141
143
 
142
144
  # @private
143
145
  def self.reconstruct_attributes(audits)
144
- attributes = {}
145
- result = audits.collect do |audit|
146
- attributes.merge!(audit.new_attributes)[:version] = audit.version
147
- yield attributes if block_given?
146
+ audits.each_with_object({}) do |audit, all|
147
+ all.merge!(audit.new_attributes)
148
+ all[:audit_version] = audit.version
148
149
  end
149
- block_given? ? result : attributes
150
150
  end
151
151
 
152
152
  # @private
@@ -164,15 +164,20 @@ module Audited
164
164
  end
165
165
 
166
166
  # use created_at as timestamp cache key
167
- def self.collection_cache_key(collection = all, timestamp_column = :created_at)
167
+ def self.collection_cache_key(collection = all, *)
168
168
  super(collection, :created_at)
169
169
  end
170
170
 
171
171
  private
172
172
 
173
173
  def set_version_number
174
- max = self.class.auditable_finder(auditable_id, auditable_type).descending.first.try(:version) || 0
175
- self.version = max + 1
174
+ if action == "create"
175
+ self.version = 1
176
+ else
177
+ collection = Rails::VERSION::MAJOR >= 6 ? self.class.unscoped : self.class
178
+ max = collection.auditable_finder(auditable_id, auditable_type).maximum(:version) || 0
179
+ self.version = max + 1
180
+ end
176
181
  end
177
182
 
178
183
  def set_audit_user
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Audited
2
4
  # Specify this act if you want changes to your model to be saved in an
3
5
  # audit table. This assumes there is an audits table ready.
@@ -34,6 +36,16 @@ module Audited
34
36
  # * +require_comment+ - Ensures that audit_comment is supplied before
35
37
  # any create, update or destroy operation.
36
38
  # * +max_audits+ - Limits the number of stored audits.
39
+
40
+ # * +redacted+ - Changes to these fields will be logged, but the values
41
+ # will not. This is useful, for example, if you wish to audit when a
42
+ # password is changed, without saving the actual password in the log.
43
+ # To store values as something other than '[REDACTED]', pass an argument
44
+ # to the redaction_value option.
45
+ #
46
+ # class User < ActiveRecord::Base
47
+ # audited redacted: :password, redaction_value: SecureRandom.uuid
48
+ # end
37
49
  #
38
50
  # * +if+ - Only audit the model when the given function returns true
39
51
  # * +unless+ - Only audit the model when the given function returns false
@@ -54,8 +66,8 @@ module Audited
54
66
  include Audited::Auditor::AuditedInstanceMethods
55
67
 
56
68
  class_attribute :audit_associated_with, instance_writer: false
57
- class_attribute :audited_options, instance_writer: false
58
- attr_accessor :version, :audit_comment
69
+ class_attribute :audited_options, instance_writer: false
70
+ attr_accessor :audit_version, :audit_comment
59
71
 
60
72
  self.audited_options = options
61
73
  normalize_audited_options
@@ -70,8 +82,8 @@ module Audited
70
82
  has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
71
83
  Audited.audit_class.audited_class_names << to_s
72
84
 
73
- after_create :audit_create if audited_options[:on].include?(:create)
74
- before_update :audit_update if audited_options[:on].include?(:update)
85
+ after_create :audit_create if audited_options[:on].include?(:create)
86
+ before_update :audit_update if audited_options[:on].include?(:update)
75
87
  before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
76
88
 
77
89
  # Define and set after_audit and around_audit callbacks. This might be useful if you want
@@ -90,6 +102,8 @@ module Audited
90
102
  end
91
103
 
92
104
  module AuditedInstanceMethods
105
+ REDACTED = "[REDACTED]"
106
+
93
107
  # Temporarily turns off auditing while saving.
94
108
  def save_without_auditing
95
109
  without_auditing { save }
@@ -105,6 +119,21 @@ module Audited
105
119
  self.class.without_auditing(&block)
106
120
  end
107
121
 
122
+ # Temporarily turns on auditing while saving.
123
+ def save_with_auditing
124
+ with_auditing { save }
125
+ end
126
+
127
+ # Executes the block with the auditing callbacks enabled.
128
+ #
129
+ # @foo.with_auditing do
130
+ # @foo.save
131
+ # end
132
+ #
133
+ def with_auditing(&block)
134
+ self.class.with_auditing(&block)
135
+ end
136
+
108
137
  # Gets an array of the revisions available
109
138
  #
110
139
  # user.revisions.each do |revision|
@@ -115,7 +144,7 @@ module Audited
115
144
  def revisions(from_version = 1)
116
145
  return [] unless audits.from_version(from_version).exists?
117
146
 
118
- all_audits = audits.select([:audited_changes, :version]).to_a
147
+ all_audits = audits.select([:audited_changes, :version, :action]).to_a
119
148
  targeted_audits = all_audits.select { |audit| audit.version >= from_version }
120
149
 
121
150
  previous_attributes = reconstruct_attributes(all_audits - targeted_audits)
@@ -129,7 +158,7 @@ module Audited
129
158
  # Get a specific revision specified by the version number, or +:previous+
130
159
  # Returns nil for versions greater than revisions count
131
160
  def revision(version)
132
- if version == :previous || self.audits.last.version >= version
161
+ if version == :previous || audits.last.version >= version
133
162
  revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
134
163
  end
135
164
  end
@@ -142,7 +171,16 @@ module Audited
142
171
 
143
172
  # List of attributes that are audited.
144
173
  def audited_attributes
145
- attributes.except(*non_audited_columns)
174
+ audited_attributes = attributes.except(*self.class.non_audited_columns)
175
+ normalize_enum_changes(audited_attributes)
176
+ end
177
+
178
+ # Returns a list combined of record audits and associated audits.
179
+ def own_and_associated_audits
180
+ Audited.audit_class.unscoped
181
+ .where("(auditable_type = :type AND auditable_id = :id) OR (associated_type = :type AND associated_id = :id)",
182
+ type: self.class.base_class.name, id: id)
183
+ .order(created_at: :desc)
146
184
  end
147
185
 
148
186
  # Combine multiple audits into one.
@@ -159,24 +197,15 @@ module Audited
159
197
 
160
198
  protected
161
199
 
162
- def non_audited_columns
163
- self.class.non_audited_columns
164
- end
165
-
166
- def audited_columns
167
- self.class.audited_columns
168
- end
169
-
170
200
  def revision_with(attributes)
171
201
  dup.tap do |revision|
172
202
  revision.id = id
173
- revision.send :instance_variable_set, '@attributes', self.attributes if rails_below?('4.2.0')
174
- revision.send :instance_variable_set, '@new_record', destroyed?
175
- revision.send :instance_variable_set, '@persisted', !destroyed?
176
- revision.send :instance_variable_set, '@readonly', false
177
- revision.send :instance_variable_set, '@destroyed', false
178
- revision.send :instance_variable_set, '@_destroyed', false
179
- revision.send :instance_variable_set, '@marked_for_destruction', false
203
+ revision.send :instance_variable_set, "@new_record", destroyed?
204
+ revision.send :instance_variable_set, "@persisted", !destroyed?
205
+ revision.send :instance_variable_set, "@readonly", false
206
+ revision.send :instance_variable_set, "@destroyed", false
207
+ revision.send :instance_variable_set, "@_destroyed", false
208
+ revision.send :instance_variable_set, "@marked_for_destruction", false
180
209
  Audited.audit_class.assign_revision_attributes(revision, attributes)
181
210
 
182
211
  # Remove any association proxies so that they will be recreated
@@ -193,58 +222,100 @@ module Audited
193
222
  end
194
223
  end
195
224
 
196
- def rails_below?(rails_version)
197
- Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
198
- end
199
-
200
225
  private
201
226
 
202
227
  def audited_changes
203
228
  all_changes = respond_to?(:changes_to_save) ? changes_to_save : changes
204
- if audited_options[:only].present?
205
- all_changes.slice(*audited_columns)
206
- else
207
- all_changes.except(*non_audited_columns)
229
+ filtered_changes = \
230
+ if audited_options[:only].present?
231
+ all_changes.slice(*self.class.audited_columns)
232
+ else
233
+ all_changes.except(*self.class.non_audited_columns)
234
+ end
235
+
236
+ filtered_changes = redact_values(filtered_changes)
237
+ filtered_changes = normalize_enum_changes(filtered_changes)
238
+ filtered_changes.to_hash
239
+ end
240
+
241
+ def normalize_enum_changes(changes)
242
+ return changes if Audited.store_synthesized_enums
243
+
244
+ self.class.defined_enums.each do |name, values|
245
+ if changes.has_key?(name)
246
+ changes[name] = \
247
+ if changes[name].is_a?(Array)
248
+ changes[name].map { |v| values[v] }
249
+ elsif rails_below?("5.0")
250
+ changes[name]
251
+ else
252
+ values[changes[name]]
253
+ end
254
+ end
255
+ end
256
+ changes
257
+ end
258
+
259
+ def redact_values(filtered_changes)
260
+ [audited_options[:redacted]].flatten.compact.each do |option|
261
+ changes = filtered_changes[option.to_s]
262
+ new_value = audited_options[:redaction_value] || REDACTED
263
+ values = if changes.is_a? Array
264
+ changes.map { new_value }
265
+ else
266
+ new_value
267
+ end
268
+ hash = {option.to_s => values}
269
+ filtered_changes.merge!(hash)
208
270
  end
271
+
272
+ filtered_changes
273
+ end
274
+
275
+ def rails_below?(rails_version)
276
+ Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
209
277
  end
210
278
 
211
279
  def audits_to(version = nil)
212
280
  if version == :previous
213
- version = if self.version
214
- self.version - 1
215
- else
216
- previous = audits.descending.offset(1).first
217
- previous ? previous.version : 1
218
- end
281
+ version = if audit_version
282
+ audit_version - 1
283
+ else
284
+ previous = audits.descending.offset(1).first
285
+ previous ? previous.version : 1
286
+ end
219
287
  end
220
288
  audits.to_version(version)
221
289
  end
222
290
 
223
291
  def audit_create
224
- write_audit(action: 'create', audited_changes: audited_attributes,
292
+ write_audit(action: "create", audited_changes: audited_attributes,
225
293
  comment: audit_comment)
226
294
  end
227
295
 
228
296
  def audit_update
229
- unless (changes = audited_changes).empty? && audit_comment.blank?
230
- write_audit(action: 'update', audited_changes: changes,
297
+ unless (changes = audited_changes).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
298
+ write_audit(action: "update", audited_changes: changes,
231
299
  comment: audit_comment)
232
300
  end
233
301
  end
234
302
 
235
303
  def audit_destroy
236
- write_audit(action: 'destroy', audited_changes: audited_attributes,
237
- comment: audit_comment) unless new_record?
304
+ unless new_record?
305
+ write_audit(action: "destroy", audited_changes: audited_attributes,
306
+ comment: audit_comment)
307
+ end
238
308
  end
239
309
 
240
310
  def write_audit(attrs)
241
- attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
242
311
  self.audit_comment = nil
243
312
 
244
313
  if auditing_enabled
314
+ attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
315
+
245
316
  run_callbacks(:audit) {
246
317
  audit = audits.create(attrs)
247
- combine_audits_if_needed if attrs[:action] != 'create'
318
+ combine_audits_if_needed if attrs[:action] != "create"
248
319
  audit
249
320
  }
250
321
  end
@@ -252,14 +323,15 @@ module Audited
252
323
 
253
324
  def presence_of_audit_comment
254
325
  if comment_required_state?
255
- errors.add(:audit_comment, "Comment can't be blank!") unless audit_comment.present?
326
+ errors.add(:audit_comment, :blank) unless audit_comment.present?
256
327
  end
257
328
  end
258
329
 
259
330
  def comment_required_state?
260
331
  auditing_enabled &&
261
- ((audited_options[:on].include?(:create) && self.new_record?) ||
262
- (audited_options[:on].include?(:update) && self.persisted? && self.changed?))
332
+ audited_changes.present? &&
333
+ ((audited_options[:on].include?(:create) && new_record?) ||
334
+ (audited_options[:on].include?(:update) && persisted? && changed?))
263
335
  end
264
336
 
265
337
  def combine_audits_if_needed
@@ -272,8 +344,7 @@ module Audited
272
344
 
273
345
  def require_comment
274
346
  if auditing_enabled && audit_comment.blank?
275
- errors.add(:audit_comment, "Comment can't be blank!")
276
- return false if Rails.version.start_with?('4.')
347
+ errors.add(:audit_comment, :blank)
277
348
  throw(:abort)
278
349
  end
279
350
  end
@@ -283,30 +354,25 @@ module Audited
283
354
  end
284
355
 
285
356
  def auditing_enabled
286
- return run_conditional_check(audited_options[:if]) &&
357
+ run_conditional_check(audited_options[:if]) &&
287
358
  run_conditional_check(audited_options[:unless], matching: false) &&
288
359
  self.class.auditing_enabled
289
360
  end
290
361
 
291
362
  def run_conditional_check(condition, matching: true)
292
363
  return true if condition.blank?
293
-
294
364
  return condition.call(self) == matching if condition.respond_to?(:call)
295
- return send(condition) == matching if respond_to?(condition.to_sym)
365
+ return send(condition) == matching if respond_to?(condition.to_sym, true)
296
366
 
297
367
  true
298
368
  end
299
369
 
300
- def auditing_enabled=(val)
301
- self.class.auditing_enabled = val
302
- end
303
-
304
370
  def reconstruct_attributes(audits)
305
371
  attributes = {}
306
372
  audits.each { |audit| attributes.merge!(audit.new_attributes) }
307
373
  attributes
308
374
  end
309
- end # InstanceMethods
375
+ end
310
376
 
311
377
  module AuditedClassMethods
312
378
  # Returns an array of columns that are audited. See non_audited_columns
@@ -338,6 +404,20 @@ module Audited
338
404
  enable_auditing if auditing_was_enabled
339
405
  end
340
406
 
407
+ # Executes the block with auditing enabled.
408
+ #
409
+ # Foo.with_auditing do
410
+ # @foo.save
411
+ # end
412
+ #
413
+ def with_auditing
414
+ auditing_was_enabled = auditing_enabled
415
+ enable_auditing
416
+ yield
417
+ ensure
418
+ disable_auditing unless auditing_was_enabled
419
+ end
420
+
341
421
  def disable_auditing
342
422
  self.auditing_enabled = false
343
423
  end
@@ -355,7 +435,7 @@ module Audited
355
435
  end
356
436
 
357
437
  def auditing_enabled
358
- Audited.store.fetch("#{table_name}_auditing_enabled", true)
438
+ Audited.store.fetch("#{table_name}_auditing_enabled", true) && Audited.auditing_enabled
359
439
  end
360
440
 
361
441
  def auditing_enabled=(val)