audited 4.5.0 → 4.10.0

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 (40) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -1
  3. data/.rubocop.yml +25 -0
  4. data/.travis.yml +37 -16
  5. data/Appraisals +33 -11
  6. data/CHANGELOG.md +151 -0
  7. data/README.md +125 -39
  8. data/gemfiles/rails42.gemfile +4 -1
  9. data/gemfiles/rails50.gemfile +4 -1
  10. data/gemfiles/rails51.gemfile +5 -2
  11. data/gemfiles/rails52.gemfile +10 -0
  12. data/gemfiles/rails60.gemfile +10 -0
  13. data/gemfiles/rails61.gemfile +10 -0
  14. data/lib/audited.rb +4 -2
  15. data/lib/audited/audit.rb +39 -14
  16. data/lib/audited/auditor.rb +223 -72
  17. data/lib/audited/rspec_matchers.rb +70 -21
  18. data/lib/audited/version.rb +1 -1
  19. data/lib/generators/audited/templates/add_version_to_auditable_index.rb +21 -0
  20. data/lib/generators/audited/templates/install.rb +2 -2
  21. data/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb +20 -0
  22. data/lib/generators/audited/upgrade_generator.rb +9 -0
  23. data/spec/audited/audit_spec.rb +93 -4
  24. data/spec/audited/auditor_spec.rb +473 -57
  25. data/spec/audited/rspec_matchers_spec.rb +69 -0
  26. data/spec/audited/sweeper_spec.rb +15 -6
  27. data/spec/audited_spec_helpers.rb +16 -2
  28. data/spec/rails_app/app/assets/config/manifest.js +1 -0
  29. data/spec/rails_app/app/controllers/application_controller.rb +2 -0
  30. data/spec/rails_app/config/application.rb +5 -0
  31. data/spec/rails_app/config/database.yml +1 -0
  32. data/spec/spec_helper.rb +4 -1
  33. data/spec/support/active_record/models.rb +51 -4
  34. data/spec/support/active_record/schema.rb +4 -2
  35. data/test/db/version_6.rb +2 -0
  36. data/test/test_helper.rb +1 -2
  37. data/test/upgrade_generator_test.rb +10 -0
  38. metadata +62 -22
  39. data/gemfiles/rails40.gemfile +0 -9
  40. data/gemfiles/rails41.gemfile +0 -8
@@ -4,5 +4,8 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "rails", "~> 4.2.0"
6
6
  gem "protected_attributes"
7
+ gem "mysql2", ">= 0.3.13", "< 0.6.0"
8
+ gem "pg", "~> 0.15"
9
+ gem "sqlite3", "~> 1.3.6"
7
10
 
8
- gemspec :name => "audited", :path => "../"
11
+ gemspec name: "audited", path: "../"
@@ -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
- gemspec :name => "audited", :path => "../"
10
+ gemspec name: "audited", path: "../"
@@ -2,6 +2,9 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", ">= 5.1.0.rc1", "< 5.2"
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
- gemspec :name => "audited", :path => "../"
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", ">= 5.2.0", "< 5.3"
6
+ gem "mysql2", ">= 0.4.4", "< 0.6.0"
7
+ gem "pg", ">= 0.18", "< 2.0"
8
+ gem "sqlite3", "~> 1.3.6"
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.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.rb CHANGED
@@ -2,10 +2,11 @@ require 'active_record'
2
2
 
3
3
  module Audited
4
4
  class << self
5
- attr_accessor :ignored_attributes, :current_user_method, :audit_class
5
+ attr_accessor :ignored_attributes, :current_user_method, :max_audits, :auditing_enabled
6
+ attr_writer :audit_class
6
7
 
7
8
  def audit_class
8
- @audit_class || Audit
9
+ @audit_class ||= Audit
9
10
  end
10
11
 
11
12
  def store
@@ -20,6 +21,7 @@ module Audited
20
21
  @ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
21
22
 
22
23
  @current_user_method = :current_user
24
+ @auditing_enabled = true
23
25
  end
24
26
 
25
27
  require 'audited/auditor'
data/lib/audited/audit.rb CHANGED
@@ -16,7 +16,7 @@ module Audited
16
16
  class YAMLIfTextColumnType
17
17
  class << self
18
18
  def load(obj)
19
- if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
19
+ if text_column?
20
20
  ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
21
21
  else
22
22
  obj
@@ -24,12 +24,16 @@ module Audited
24
24
  end
25
25
 
26
26
  def dump(obj)
27
- if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
27
+ if text_column?
28
28
  ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
29
29
  else
30
30
  obj
31
31
  end
32
32
  end
33
+
34
+ def text_column?
35
+ Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
36
+ end
33
37
  end
34
38
  end
35
39
 
@@ -65,7 +69,7 @@ module Audited
65
69
  def revision
66
70
  clazz = auditable_type.constantize
67
71
  (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))
72
+ self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(audit_version: version))
69
73
  end
70
74
  end
71
75
 
@@ -86,6 +90,23 @@ module Audited
86
90
  end
87
91
  end
88
92
 
93
+ # Allows user to undo changes
94
+ def undo
95
+ case action
96
+ when 'create'
97
+ # destroys a newly created record
98
+ auditable.destroy!
99
+ when 'destroy'
100
+ # creates a new record with the destroyed record attributes
101
+ auditable_type.constantize.create!(audited_changes)
102
+ when 'update'
103
+ # changes back attributes
104
+ auditable.update!(audited_changes.transform_values(&:first))
105
+ else
106
+ raise StandardError, "invalid action given #{action}"
107
+ end
108
+ end
109
+
89
110
  # Allows user to be set to either a string or an ActiveRecord object
90
111
  # @private
91
112
  def user_as_string=(user)
@@ -113,21 +134,20 @@ module Audited
113
134
  # All audits made during the block called will be recorded as made
114
135
  # by +user+. This method is hopefully threadsafe, making it ideal
115
136
  # for background operations that require audit information.
116
- def self.as_user(user, &block)
137
+ def self.as_user(user)
138
+ last_audited_user = ::Audited.store[:audited_user]
117
139
  ::Audited.store[:audited_user] = user
118
140
  yield
119
141
  ensure
120
- ::Audited.store[:audited_user] = nil
142
+ ::Audited.store[:audited_user] = last_audited_user
121
143
  end
122
144
 
123
145
  # @private
124
146
  def self.reconstruct_attributes(audits)
125
- attributes = {}
126
- result = audits.collect do |audit|
127
- attributes.merge!(audit.new_attributes)[:version] = audit.version
128
- yield attributes if block_given?
129
- end
130
- block_given? ? result : attributes
147
+ audits.each_with_object({}) do |audit, all|
148
+ all.merge!(audit.new_attributes)
149
+ all[:audit_version] = audit.version
150
+ end
131
151
  end
132
152
 
133
153
  # @private
@@ -145,15 +165,20 @@ module Audited
145
165
  end
146
166
 
147
167
  # use created_at as timestamp cache key
148
- def self.collection_cache_key(collection = all, timestamp_column = :created_at)
168
+ def self.collection_cache_key(collection = all, *)
149
169
  super(collection, :created_at)
150
170
  end
151
171
 
152
172
  private
153
173
 
154
174
  def set_version_number
155
- max = self.class.auditable_finder(auditable_id, auditable_type).descending.first.try(:version) || 0
156
- self.version = max + 1
175
+ if action == 'create'
176
+ self.version = 1
177
+ else
178
+ collection = Rails::VERSION::MAJOR == 6 ? self.class.unscoped : self.class
179
+ max = collection.auditable_finder(auditable_id, auditable_type).maximum(:version) || 0
180
+ self.version = max + 1
181
+ end
157
182
  end
158
183
 
159
184
  def set_audit_user
@@ -33,31 +33,56 @@ module Audited
33
33
  #
34
34
  # * +require_comment+ - Ensures that audit_comment is supplied before
35
35
  # any create, update or destroy operation.
36
+ # * +max_audits+ - Limits the number of stored audits.
37
+
38
+ # * +redacted+ - Changes to these fields will be logged, but the values
39
+ # will not. This is useful, for example, if you wish to audit when a
40
+ # password is changed, without saving the actual password in the log.
41
+ # To store values as something other than '[REDACTED]', pass an argument
42
+ # to the redaction_value option.
43
+ #
44
+ # class User < ActiveRecord::Base
45
+ # audited redacted: :password, redaction_value: SecureRandom.uuid
46
+ # end
47
+ #
48
+ # * +if+ - Only audit the model when the given function returns true
49
+ # * +unless+ - Only audit the model when the given function returns false
50
+ #
51
+ # class User < ActiveRecord::Base
52
+ # audited :if => :active?
53
+ #
54
+ # def active?
55
+ # self.status == 'active'
56
+ # end
57
+ # end
36
58
  #
37
59
  def audited(options = {})
38
60
  # don't allow multiple calls
39
61
  return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
40
62
 
41
- class_attribute :audit_associated_with, instance_writer: false
63
+ extend Audited::Auditor::AuditedClassMethods
64
+ include Audited::Auditor::AuditedInstanceMethods
65
+
66
+ class_attribute :audit_associated_with, instance_writer: false
42
67
  class_attribute :audited_options, instance_writer: false
68
+ attr_accessor :audit_version, :audit_comment
43
69
 
44
70
  self.audited_options = options
45
- self.audit_associated_with = options[:associated_with]
71
+ normalize_audited_options
46
72
 
47
- if options[:comment_required]
48
- validates_presence_of :audit_comment, if: :auditing_enabled
49
- before_destroy :require_comment
50
- end
73
+ self.audit_associated_with = audited_options[:associated_with]
51
74
 
52
- attr_accessor :audit_comment
75
+ if audited_options[:comment_required]
76
+ validate :presence_of_audit_comment
77
+ before_destroy :require_comment if audited_options[:on].include?(:destroy)
78
+ end
53
79
 
54
- has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name
80
+ has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
55
81
  Audited.audit_class.audited_class_names << to_s
56
82
 
57
- on = Array(options[:on])
58
- after_create :audit_create if on.empty? || on.include?(:create)
59
- before_update :audit_update if on.empty? || on.include?(:update)
60
- before_destroy :audit_destroy if on.empty? || on.include?(:destroy)
83
+ after_create :audit_create if audited_options[:on].include?(:create)
84
+ before_update :audit_update if audited_options[:on].include?(:update)
85
+ before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
61
86
 
62
87
  # Define and set after_audit and around_audit callbacks. This might be useful if you want
63
88
  # to notify a party after the audit has been created or if you want to access the newly-created
@@ -66,24 +91,17 @@ module Audited
66
91
  set_callback :audit, :after, :after_audit, if: lambda { respond_to?(:after_audit, true) }
67
92
  set_callback :audit, :around, :around_audit, if: lambda { respond_to?(:around_audit, true) }
68
93
 
69
- attr_accessor :version
70
-
71
- extend Audited::Auditor::AuditedClassMethods
72
- include Audited::Auditor::AuditedInstanceMethods
73
-
74
- self.auditing_enabled = true
94
+ enable_auditing
75
95
  end
76
96
 
77
97
  def has_associated_audits
78
98
  has_many :associated_audits, as: :associated, class_name: Audited.audit_class.name
79
99
  end
80
-
81
- def default_ignored_attributes
82
- [primary_key, inheritance_column]
83
- end
84
100
  end
85
101
 
86
102
  module AuditedInstanceMethods
103
+ REDACTED = '[REDACTED]'
104
+
87
105
  # Temporarily turns off auditing while saving.
88
106
  def save_without_auditing
89
107
  without_auditing { save }
@@ -99,6 +117,21 @@ module Audited
99
117
  self.class.without_auditing(&block)
100
118
  end
101
119
 
120
+ # Temporarily turns on auditing while saving.
121
+ def save_with_auditing
122
+ with_auditing { save }
123
+ end
124
+
125
+ # Executes the block with the auditing callbacks enabled.
126
+ #
127
+ # @foo.with_auditing do
128
+ # @foo.save
129
+ # end
130
+ #
131
+ def with_auditing(&block)
132
+ self.class.with_auditing(&block)
133
+ end
134
+
102
135
  # Gets an array of the revisions available
103
136
  #
104
137
  # user.revisions.each do |revision|
@@ -107,18 +140,25 @@ module Audited
107
140
  # end
108
141
  #
109
142
  def revisions(from_version = 1)
110
- audits = self.audits.from_version(from_version)
111
- return [] if audits.empty?
112
- revisions = []
113
- audits.each do |audit|
114
- revisions << audit.revision
143
+ return [] unless audits.from_version(from_version).exists?
144
+
145
+ all_audits = audits.select([:audited_changes, :version]).to_a
146
+ targeted_audits = all_audits.select { |audit| audit.version >= from_version }
147
+
148
+ previous_attributes = reconstruct_attributes(all_audits - targeted_audits)
149
+
150
+ targeted_audits.map do |audit|
151
+ previous_attributes.merge!(audit.new_attributes)
152
+ revision_with(previous_attributes.merge!(version: audit.version))
115
153
  end
116
- revisions
117
154
  end
118
155
 
119
156
  # Get a specific revision specified by the version number, or +:previous+
157
+ # Returns nil for versions greater than revisions count
120
158
  def revision(version)
121
- revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
159
+ if version == :previous || self.audits.last.version >= version
160
+ revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
161
+ end
122
162
  end
123
163
 
124
164
  # Find the oldest revision recorded prior to the date/time provided.
@@ -129,23 +169,35 @@ module Audited
129
169
 
130
170
  # List of attributes that are audited.
131
171
  def audited_attributes
132
- attributes.except(*non_audited_columns.map(&:to_s))
172
+ audited_attributes = attributes.except(*self.class.non_audited_columns)
173
+ normalize_enum_changes(audited_attributes)
133
174
  end
134
175
 
135
- def non_audited_columns
136
- self.class.non_audited_columns
176
+ # Returns a list combined of record audits and associated audits.
177
+ def own_and_associated_audits
178
+ Audited.audit_class.unscoped
179
+ .where('(auditable_type = :type AND auditable_id = :id) OR (associated_type = :type AND associated_id = :id)',
180
+ type: self.class.name, id: id)
181
+ .order(created_at: :desc)
137
182
  end
138
183
 
139
- protected
184
+ # Combine multiple audits into one.
185
+ def combine_audits(audits_to_combine)
186
+ combine_target = audits_to_combine.last
187
+ combine_target.audited_changes = audits_to_combine.pluck(:audited_changes).reduce(&:merge)
188
+ combine_target.comment = "#{combine_target.comment}\nThis audit is the result of multiple audits being combined."
140
189
 
141
- def non_audited_columns
142
- self.class.non_audited_columns
190
+ transaction do
191
+ combine_target.save!
192
+ audits_to_combine.unscope(:limit).where("version < ?", combine_target.version).delete_all
193
+ end
143
194
  end
144
195
 
196
+ protected
197
+
145
198
  def revision_with(attributes)
146
199
  dup.tap do |revision|
147
200
  revision.id = id
148
- revision.send :instance_variable_set, '@attributes', self.attributes if rails_below?('4.2.0')
149
201
  revision.send :instance_variable_set, '@new_record', destroyed?
150
202
  revision.send :instance_variable_set, '@persisted', !destroyed?
151
203
  revision.send :instance_variable_set, '@readonly', false
@@ -168,31 +220,62 @@ module Audited
168
220
  end
169
221
  end
170
222
 
171
- def rails_below?(rails_version)
172
- Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
173
- end
174
-
175
223
  private
176
224
 
177
225
  def audited_changes
178
- collection =
179
- if audited_options[:only]
180
- audited_columns = self.class.audited_columns.map(&:name)
181
- changed_attributes.slice(*audited_columns)
226
+ all_changes = respond_to?(:changes_to_save) ? changes_to_save : changes
227
+ filtered_changes = \
228
+ if audited_options[:only].present?
229
+ all_changes.slice(*self.class.audited_columns)
182
230
  else
183
- changed_attributes.except(*non_audited_columns)
231
+ all_changes.except(*self.class.non_audited_columns)
184
232
  end
185
233
 
186
- collection.inject({}) do |changes, (attr, old_value)|
187
- changes[attr] = [old_value, self[attr]]
188
- changes
234
+ filtered_changes = redact_values(filtered_changes)
235
+ filtered_changes = normalize_enum_changes(filtered_changes)
236
+ filtered_changes.to_hash
237
+ end
238
+
239
+ def normalize_enum_changes(changes)
240
+ self.class.defined_enums.each do |name, values|
241
+ if changes.has_key?(name)
242
+ changes[name] = \
243
+ if changes[name].is_a?(Array)
244
+ changes[name].map { |v| values[v] }
245
+ elsif rails_below?('5.0')
246
+ changes[name]
247
+ else
248
+ values[changes[name]]
249
+ end
250
+ end
189
251
  end
252
+ changes
253
+ end
254
+
255
+ def redact_values(filtered_changes)
256
+ [audited_options[:redacted]].flatten.compact.each do |option|
257
+ changes = filtered_changes[option.to_s]
258
+ new_value = audited_options[:redaction_value] || REDACTED
259
+ if changes.is_a? Array
260
+ values = changes.map { new_value }
261
+ else
262
+ values = new_value
263
+ end
264
+ hash = Hash[option.to_s, values]
265
+ filtered_changes.merge!(hash)
266
+ end
267
+
268
+ filtered_changes
269
+ end
270
+
271
+ def rails_below?(rails_version)
272
+ Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
190
273
  end
191
274
 
192
275
  def audits_to(version = nil)
193
276
  if version == :previous
194
- version = if self.version
195
- self.version - 1
277
+ version = if self.audit_version
278
+ self.audit_version - 1
196
279
  else
197
280
  previous = audits.descending.offset(1).first
198
281
  previous ? previous.version : 1
@@ -207,7 +290,7 @@ module Audited
207
290
  end
208
291
 
209
292
  def audit_update
210
- unless (changes = audited_changes).empty? && audit_comment.blank?
293
+ unless (changes = audited_changes).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
211
294
  write_audit(action: 'update', audited_changes: changes,
212
295
  comment: audit_comment)
213
296
  end
@@ -221,14 +304,41 @@ module Audited
221
304
  def write_audit(attrs)
222
305
  attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
223
306
  self.audit_comment = nil
224
- run_callbacks(:audit) { audits.create(attrs) } if auditing_enabled
307
+
308
+ if auditing_enabled
309
+ run_callbacks(:audit) {
310
+ audit = audits.create(attrs)
311
+ combine_audits_if_needed if attrs[:action] != 'create'
312
+ audit
313
+ }
314
+ end
315
+ end
316
+
317
+ def presence_of_audit_comment
318
+ if comment_required_state?
319
+ errors.add(:audit_comment, "Comment can't be blank!") unless audit_comment.present?
320
+ end
321
+ end
322
+
323
+ def comment_required_state?
324
+ auditing_enabled &&
325
+ ((audited_options[:on].include?(:create) && self.new_record?) ||
326
+ (audited_options[:on].include?(:update) && self.persisted? && self.changed?))
327
+ end
328
+
329
+ def combine_audits_if_needed
330
+ max_audits = audited_options[:max_audits]
331
+ if max_audits && (extra_count = audits.count - max_audits) > 0
332
+ audits_to_combine = audits.limit(extra_count + 1)
333
+ combine_audits(audits_to_combine)
334
+ end
225
335
  end
226
336
 
227
337
  def require_comment
228
338
  if auditing_enabled && audit_comment.blank?
229
- errors.add(:audit_comment, "Comment required before destruction")
339
+ errors.add(:audit_comment, "Comment can't be blank!")
230
340
  return false if Rails.version.start_with?('4.')
231
- throw :abort
341
+ throw(:abort)
232
342
  end
233
343
  end
234
344
 
@@ -236,39 +346,41 @@ module Audited
236
346
  alias_method "#{attr_name}_callback".to_sym, attr_name
237
347
  end
238
348
 
239
- def empty_callback #:nodoc:
349
+ def auditing_enabled
350
+ return run_conditional_check(audited_options[:if]) &&
351
+ run_conditional_check(audited_options[:unless], matching: false) &&
352
+ self.class.auditing_enabled
240
353
  end
241
354
 
242
- def auditing_enabled
243
- self.class.auditing_enabled
355
+ def run_conditional_check(condition, matching: true)
356
+ return true if condition.blank?
357
+ return condition.call(self) == matching if condition.respond_to?(:call)
358
+ return send(condition) == matching if respond_to?(condition.to_sym, true)
359
+
360
+ true
244
361
  end
245
362
 
246
- def auditing_enabled=(val)
247
- self.class.auditing_enabled = val
363
+ def reconstruct_attributes(audits)
364
+ attributes = {}
365
+ audits.each { |audit| attributes.merge!(audit.new_attributes) }
366
+ attributes
248
367
  end
249
368
  end # InstanceMethods
250
369
 
251
370
  module AuditedClassMethods
252
371
  # Returns an array of columns that are audited. See non_audited_columns
253
372
  def audited_columns
254
- columns.reject { |c| non_audited_columns.map(&:to_s).include?(c.name) }
373
+ @audited_columns ||= column_names - non_audited_columns
255
374
  end
256
375
 
376
+ # We have to calculate this here since column_names may not be available when `audited` is called
257
377
  def non_audited_columns
258
- @non_audited_columns ||= begin
259
- options = audited_options
260
- if options[:only]
261
- except = column_names - Array.wrap(options[:only]).flatten.map(&:to_s)
262
- else
263
- except = default_ignored_attributes + Audited.ignored_attributes
264
- except |= Array(options[:except]).collect(&:to_s) if options[:except]
265
- end
266
- except
267
- end
378
+ @non_audited_columns ||= calculate_non_audited_columns
268
379
  end
269
380
 
270
381
  def non_audited_columns=(columns)
271
- @non_audited_columns = columns
382
+ @audited_columns = nil # reset cached audited columns on assignment
383
+ @non_audited_columns = columns.map(&:to_s)
272
384
  end
273
385
 
274
386
  # Executes the block with auditing disabled.
@@ -285,6 +397,20 @@ module Audited
285
397
  enable_auditing if auditing_was_enabled
286
398
  end
287
399
 
400
+ # Executes the block with auditing enabled.
401
+ #
402
+ # Foo.with_auditing do
403
+ # @foo.save
404
+ # end
405
+ #
406
+ def with_auditing
407
+ auditing_was_enabled = auditing_enabled
408
+ enable_auditing
409
+ yield
410
+ ensure
411
+ disable_auditing unless auditing_was_enabled
412
+ end
413
+
288
414
  def disable_auditing
289
415
  self.auditing_enabled = false
290
416
  end
@@ -302,12 +428,37 @@ module Audited
302
428
  end
303
429
 
304
430
  def auditing_enabled
305
- Audited.store.fetch("#{table_name}_auditing_enabled", true)
431
+ Audited.store.fetch("#{table_name}_auditing_enabled", true) && Audited.auditing_enabled
306
432
  end
307
433
 
308
434
  def auditing_enabled=(val)
309
435
  Audited.store["#{table_name}_auditing_enabled"] = val
310
436
  end
437
+
438
+ def default_ignored_attributes
439
+ [primary_key, inheritance_column] | Audited.ignored_attributes
440
+ end
441
+
442
+ protected
443
+
444
+ def normalize_audited_options
445
+ audited_options[:on] = Array.wrap(audited_options[:on])
446
+ audited_options[:on] = [:create, :update, :destroy] if audited_options[:on].empty?
447
+ audited_options[:only] = Array.wrap(audited_options[:only]).map(&:to_s)
448
+ audited_options[:except] = Array.wrap(audited_options[:except]).map(&:to_s)
449
+ max_audits = audited_options[:max_audits] || Audited.max_audits
450
+ audited_options[:max_audits] = Integer(max_audits).abs if max_audits
451
+ end
452
+
453
+ def calculate_non_audited_columns
454
+ if audited_options[:only].present?
455
+ (column_names | default_ignored_attributes) - audited_options[:only]
456
+ elsif audited_options[:except].present?
457
+ default_ignored_attributes | audited_options[:except]
458
+ else
459
+ default_ignored_attributes
460
+ end
461
+ end
311
462
  end
312
463
  end
313
464
  end