velocity_audited 5.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +115 -0
  3. data/.gitignore +17 -0
  4. data/.standard.yml +5 -0
  5. data/.yardopts +3 -0
  6. data/Appraisals +44 -0
  7. data/CHANGELOG.md +419 -0
  8. data/Gemfile +3 -0
  9. data/LICENSE +19 -0
  10. data/README.md +433 -0
  11. data/Rakefile +18 -0
  12. data/gemfiles/rails50.gemfile +10 -0
  13. data/gemfiles/rails51.gemfile +10 -0
  14. data/gemfiles/rails52.gemfile +10 -0
  15. data/gemfiles/rails60.gemfile +10 -0
  16. data/gemfiles/rails61.gemfile +10 -0
  17. data/gemfiles/rails70.gemfile +10 -0
  18. data/lib/audited/audit.rb +198 -0
  19. data/lib/audited/auditor.rb +476 -0
  20. data/lib/audited/railtie.rb +16 -0
  21. data/lib/audited/rspec_matchers.rb +228 -0
  22. data/lib/audited/sweeper.rb +67 -0
  23. data/lib/audited/version.rb +5 -0
  24. data/lib/audited-rspec.rb +6 -0
  25. data/lib/audited.rb +49 -0
  26. data/lib/generators/audited/install_generator.rb +27 -0
  27. data/lib/generators/audited/migration.rb +17 -0
  28. data/lib/generators/audited/migration_helper.rb +11 -0
  29. data/lib/generators/audited/templates/add_association_to_audits.rb +13 -0
  30. data/lib/generators/audited/templates/add_comment_to_audits.rb +11 -0
  31. data/lib/generators/audited/templates/add_remote_address_to_audits.rb +12 -0
  32. data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +12 -0
  33. data/lib/generators/audited/templates/add_version_to_auditable_index.rb +23 -0
  34. data/lib/generators/audited/templates/install.rb +32 -0
  35. data/lib/generators/audited/templates/rename_association_to_associated.rb +25 -0
  36. data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +11 -0
  37. data/lib/generators/audited/templates/rename_parent_to_association.rb +13 -0
  38. data/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb +22 -0
  39. data/lib/generators/audited/upgrade_generator.rb +70 -0
  40. data/lib/velocity_audited.rb +5 -0
  41. data/spec/audited/audit_spec.rb +357 -0
  42. data/spec/audited/auditor_spec.rb +1097 -0
  43. data/spec/audited/rspec_matchers_spec.rb +69 -0
  44. data/spec/audited/sweeper_spec.rb +133 -0
  45. data/spec/audited_spec.rb +18 -0
  46. data/spec/audited_spec_helpers.rb +32 -0
  47. data/spec/rails_app/app/assets/config/manifest.js +2 -0
  48. data/spec/rails_app/config/application.rb +13 -0
  49. data/spec/rails_app/config/database.yml +26 -0
  50. data/spec/rails_app/config/environment.rb +5 -0
  51. data/spec/rails_app/config/environments/test.rb +47 -0
  52. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  53. data/spec/rails_app/config/initializers/inflections.rb +2 -0
  54. data/spec/rails_app/config/initializers/secret_token.rb +3 -0
  55. data/spec/rails_app/config/routes.rb +3 -0
  56. data/spec/spec_helper.rb +24 -0
  57. data/spec/support/active_record/models.rb +151 -0
  58. data/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb +11 -0
  59. data/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb +11 -0
  60. data/spec/support/active_record/schema.rb +90 -0
  61. data/test/db/version_1.rb +17 -0
  62. data/test/db/version_2.rb +18 -0
  63. data/test/db/version_3.rb +18 -0
  64. data/test/db/version_4.rb +19 -0
  65. data/test/db/version_5.rb +17 -0
  66. data/test/db/version_6.rb +19 -0
  67. data/test/install_generator_test.rb +62 -0
  68. data/test/test_helper.rb +18 -0
  69. data/test/upgrade_generator_test.rb +97 -0
  70. metadata +260 -0
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Audited
6
+ # Audit saves the changes to ActiveRecord models. It has the following attributes:
7
+ #
8
+ # * <tt>auditable</tt>: the ActiveRecord model that was changed
9
+ # * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model
10
+ # * <tt>action</tt>: one of create, update, or delete
11
+ # * <tt>audited_changes</tt>: a hash of all the changes
12
+ # * <tt>comment</tt>: a comment set with the audit
13
+ # * <tt>version</tt>: the version of the model
14
+ # * <tt>request_uuid</tt>: a uuid based that allows audits from the same controller request
15
+ # * <tt>created_at</tt>: Time that the change was performed
16
+ #
17
+
18
+ class YAMLIfTextColumnType
19
+ class << self
20
+ def load(obj)
21
+ if text_column?
22
+ ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
23
+ else
24
+ obj
25
+ end
26
+ end
27
+
28
+ def dump(obj)
29
+ if text_column?
30
+ ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
31
+ else
32
+ obj
33
+ end
34
+ end
35
+
36
+ def text_column?
37
+ Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
38
+ end
39
+ end
40
+ end
41
+
42
+ class Audit < ::ActiveRecord::Base
43
+ belongs_to :auditable, polymorphic: true
44
+ belongs_to :user, polymorphic: true
45
+ belongs_to :associated, polymorphic: true
46
+
47
+ before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
48
+
49
+ cattr_accessor :audited_class_names
50
+ self.audited_class_names = Set.new
51
+
52
+ serialize :audited_changes, YAMLIfTextColumnType
53
+
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") }
59
+
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) }
64
+ # Return all audits older than the current one.
65
+ def ancestors
66
+ self.class.ascending.auditable_finder(auditable_id, auditable_type).to_version(version)
67
+ end
68
+
69
+ # Return an instance of what the object looked like at this revision. If
70
+ # the object has been destroyed, this will be a new record.
71
+ def revision
72
+ clazz = auditable_type.constantize
73
+ (clazz.find_by_id(auditable_id) || clazz.new).tap do |m|
74
+ self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(audit_version: version))
75
+ end
76
+ end
77
+
78
+ # Returns a hash of the changed attributes with the new values
79
+ def new_attributes
80
+ (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
81
+ attrs[attr] = (action == "update" ? values.last : values)
82
+ end
83
+ end
84
+
85
+ # Returns a hash of the changed attributes with the old values
86
+ def old_attributes
87
+ (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
88
+ attrs[attr] = (action == "update" ? values.first : values)
89
+ end
90
+ end
91
+
92
+ # Allows user to undo changes
93
+ def undo
94
+ case action
95
+ when "create"
96
+ # destroys a newly created record
97
+ auditable.destroy!
98
+ when "destroy"
99
+ # creates a new record with the destroyed record attributes
100
+ auditable_type.constantize.create!(audited_changes)
101
+ when "update"
102
+ # changes back attributes
103
+ auditable.update!(audited_changes.transform_values(&:first))
104
+ else
105
+ raise StandardError, "invalid action given #{action}"
106
+ end
107
+ end
108
+
109
+ # Allows user to be set to either a string or an ActiveRecord object
110
+ # @private
111
+ def user_as_string=(user)
112
+ # reset both either way
113
+ self.user_as_model = self.username = nil
114
+ user.is_a?(::ActiveRecord::Base) ?
115
+ self.user_as_model = user :
116
+ self.username = user
117
+ end
118
+ alias_method :user_as_model=, :user=
119
+ alias_method :user=, :user_as_string=
120
+
121
+ # @private
122
+ def user_as_string
123
+ user_as_model || username
124
+ end
125
+ alias_method :user_as_model, :user
126
+ alias_method :user, :user_as_string
127
+
128
+ # Returns the list of classes that are being audited
129
+ def self.audited_classes
130
+ audited_class_names.map(&:constantize)
131
+ end
132
+
133
+ # All audits made during the block called will be recorded as made
134
+ # by +user+. This method is hopefully threadsafe, making it ideal
135
+ # for background operations that require audit information.
136
+ def self.as_user(user)
137
+ last_audited_user = ::Audited.store[:audited_user]
138
+ ::Audited.store[:audited_user] = user
139
+ yield
140
+ ensure
141
+ ::Audited.store[:audited_user] = last_audited_user
142
+ end
143
+
144
+ # @private
145
+ def self.reconstruct_attributes(audits)
146
+ audits.each_with_object({}) do |audit, all|
147
+ all.merge!(audit.new_attributes)
148
+ all[:audit_version] = audit.version
149
+ end
150
+ end
151
+
152
+ # @private
153
+ def self.assign_revision_attributes(record, attributes)
154
+ attributes.each do |attr, val|
155
+ record = record.dup if record.frozen?
156
+
157
+ if record.respond_to?("#{attr}=")
158
+ record.attributes.key?(attr.to_s) ?
159
+ record[attr] = val :
160
+ record.send("#{attr}=", val)
161
+ end
162
+ end
163
+ record
164
+ end
165
+
166
+ # use created_at as timestamp cache key
167
+ def self.collection_cache_key(collection = all, *)
168
+ super(collection, :created_at)
169
+ end
170
+
171
+ private
172
+
173
+ def set_version_number
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
181
+ end
182
+
183
+ def set_audit_user
184
+ self.user ||= ::Audited.store[:audited_user] # from .as_user
185
+ self.user ||= ::Audited.store[:current_user].try!(:call) # from Sweeper
186
+ nil # prevent stopping callback chains
187
+ end
188
+
189
+ def set_request_uuid
190
+ self.request_uuid ||= ::Audited.store[:current_request_uuid]
191
+ self.request_uuid ||= SecureRandom.uuid
192
+ end
193
+
194
+ def set_remote_address
195
+ self.remote_address ||= ::Audited.store[:current_remote_address]
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,476 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audited
4
+ # Specify this act if you want changes to your model to be saved in an
5
+ # audit table. This assumes there is an audits table ready.
6
+ #
7
+ # class User < ActiveRecord::Base
8
+ # audited
9
+ # end
10
+ #
11
+ # To store an audit comment set model.audit_comment to your comment before
12
+ # a create, update or destroy operation.
13
+ #
14
+ # See <tt>Audited::Auditor::ClassMethods#audited</tt>
15
+ # for configuration options
16
+ module Auditor #:nodoc:
17
+ extend ActiveSupport::Concern
18
+
19
+ CALLBACKS = [:audit_create, :audit_update, :audit_destroy]
20
+
21
+ module ClassMethods
22
+ # == Configuration options
23
+ #
24
+ #
25
+ # * +only+ - Only audit the given attributes
26
+ # * +except+ - Excludes fields from being saved in the audit log.
27
+ # By default, Audited will audit all but these fields:
28
+ #
29
+ # [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at']
30
+ # You can add to those by passing one or an array of fields to skip.
31
+ #
32
+ # class User < ActiveRecord::Base
33
+ # audited except: :password
34
+ # end
35
+ #
36
+ # * +require_comment+ - Ensures that audit_comment is supplied before
37
+ # any create, update or destroy operation.
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
49
+ #
50
+ # * +if+ - Only audit the model when the given function returns true
51
+ # * +unless+ - Only audit the model when the given function returns false
52
+ #
53
+ # class User < ActiveRecord::Base
54
+ # audited :if => :active?
55
+ #
56
+ # def active?
57
+ # self.status == 'active'
58
+ # end
59
+ # end
60
+ #
61
+ def audited(options = {})
62
+ # don't allow multiple calls
63
+ return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
64
+
65
+ extend Audited::Auditor::AuditedClassMethods
66
+ include Audited::Auditor::AuditedInstanceMethods
67
+
68
+ class_attribute :audit_associated_with, instance_writer: false
69
+ class_attribute :audited_options, instance_writer: false
70
+ attr_accessor :audit_version, :audit_comment
71
+
72
+ self.audited_options = options
73
+ normalize_audited_options
74
+
75
+ self.audit_associated_with = audited_options[:associated_with]
76
+
77
+ if audited_options[:comment_required]
78
+ validate :presence_of_audit_comment
79
+ before_destroy :require_comment if audited_options[:on].include?(:destroy)
80
+ end
81
+
82
+ has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
83
+ Audited.audit_class.audited_class_names << to_s
84
+
85
+ after_create :audit_create if audited_options[:on].include?(:create)
86
+ before_update :audit_update if audited_options[:on].include?(:update)
87
+ before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
88
+
89
+ # Define and set after_audit and around_audit callbacks. This might be useful if you want
90
+ # to notify a party after the audit has been created or if you want to access the newly-created
91
+ # audit.
92
+ define_callbacks :audit
93
+ set_callback :audit, :after, :after_audit, if: lambda { respond_to?(:after_audit, true) }
94
+ set_callback :audit, :around, :around_audit, if: lambda { respond_to?(:around_audit, true) }
95
+
96
+ enable_auditing
97
+ end
98
+
99
+ def has_associated_audits
100
+ has_many :associated_audits, as: :associated, class_name: Audited.audit_class.name
101
+ end
102
+ end
103
+
104
+ module AuditedInstanceMethods
105
+ REDACTED = "[REDACTED]"
106
+
107
+ # Temporarily turns off auditing while saving.
108
+ def save_without_auditing
109
+ without_auditing { save }
110
+ end
111
+
112
+ # Executes the block with the auditing callbacks disabled.
113
+ #
114
+ # @foo.without_auditing do
115
+ # @foo.save
116
+ # end
117
+ #
118
+ def without_auditing(&block)
119
+ self.class.without_auditing(&block)
120
+ end
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
+
137
+ # Gets an array of the revisions available
138
+ #
139
+ # user.revisions.each do |revision|
140
+ # user.name
141
+ # user.version
142
+ # end
143
+ #
144
+ def revisions(from_version = 1)
145
+ return [] unless audits.from_version(from_version).exists?
146
+
147
+ all_audits = audits.select([:audited_changes, :version, :action]).to_a
148
+ targeted_audits = all_audits.select { |audit| audit.version >= from_version }
149
+
150
+ previous_attributes = reconstruct_attributes(all_audits - targeted_audits)
151
+
152
+ targeted_audits.map do |audit|
153
+ previous_attributes.merge!(audit.new_attributes)
154
+ revision_with(previous_attributes.merge!(version: audit.version))
155
+ end
156
+ end
157
+
158
+ # Get a specific revision specified by the version number, or +:previous+
159
+ # Returns nil for versions greater than revisions count
160
+ def revision(version)
161
+ if version == :previous || audits.last.version >= version
162
+ revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
163
+ end
164
+ end
165
+
166
+ # Find the oldest revision recorded prior to the date/time provided.
167
+ def revision_at(date_or_time)
168
+ audits = self.audits.up_until(date_or_time)
169
+ revision_with Audited.audit_class.reconstruct_attributes(audits) unless audits.empty?
170
+ end
171
+
172
+ # List of attributes that are audited.
173
+ def audited_attributes
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)
184
+ end
185
+
186
+ # Combine multiple audits into one.
187
+ def combine_audits(audits_to_combine)
188
+ combine_target = audits_to_combine.last
189
+ combine_target.audited_changes = audits_to_combine.pluck(:audited_changes).reduce(&:merge)
190
+ combine_target.comment = "#{combine_target.comment}\nThis audit is the result of multiple audits being combined."
191
+
192
+ transaction do
193
+ combine_target.save!
194
+ audits_to_combine.unscope(:limit).where("version < ?", combine_target.version).delete_all
195
+ end
196
+ end
197
+
198
+ protected
199
+
200
+ def revision_with(attributes)
201
+ dup.tap do |revision|
202
+ revision.id = id
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
209
+ Audited.audit_class.assign_revision_attributes(revision, attributes)
210
+
211
+ # Remove any association proxies so that they will be recreated
212
+ # and reference the correct object for this revision. The only way
213
+ # to determine if an instance variable is a proxy object is to
214
+ # see if it responds to certain methods, as it forwards almost
215
+ # everything to its target.
216
+ revision.instance_variables.each do |ivar|
217
+ proxy = revision.instance_variable_get ivar
218
+ if !proxy.nil? && proxy.respond_to?(:proxy_respond_to?)
219
+ revision.instance_variable_set ivar, nil
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ private
226
+
227
+ def audited_changes
228
+ all_changes = respond_to?(:changes_to_save) ? changes_to_save : changes
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)
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)
277
+ end
278
+
279
+ def audits_to(version = nil)
280
+ if version == :previous
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
287
+ end
288
+ audits.to_version(version)
289
+ end
290
+
291
+ def audit_create
292
+ write_audit(action: "create", audited_changes: audited_attributes,
293
+ comment: audit_comment)
294
+ end
295
+
296
+ def audit_update
297
+ unless (changes = audited_changes).empty? && (audit_comment.blank? || audited_options[:update_with_comment_only] == false)
298
+ write_audit(action: "update", audited_changes: changes,
299
+ comment: audit_comment)
300
+ end
301
+ end
302
+
303
+ def audit_destroy
304
+ unless new_record?
305
+ write_audit(action: "destroy", audited_changes: audited_attributes,
306
+ comment: audit_comment)
307
+ end
308
+ end
309
+
310
+ def write_audit(attrs)
311
+ self.audit_comment = nil
312
+ attrs[:db_audit] = true
313
+ attrs = add_metadata(attrs)
314
+ if auditing_enabled
315
+ attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
316
+ logger.audit(attrs.to_json)
317
+ end
318
+ end
319
+
320
+ def add_metadata(attrs)
321
+ attrs[:client] = ::Audited.store[:client]
322
+ attrs[:origin] = ::Audited.store[:origin]
323
+ attrs[:user_agent] = ::Audited.store[:user_agent]
324
+ attrs[:ip] = ::Audited.store[:ip]
325
+ attrs[:uid] = ::Audited.store[:uid]
326
+ attrs
327
+ end
328
+
329
+ def presence_of_audit_comment
330
+ if comment_required_state?
331
+ errors.add(:audit_comment, :blank) unless audit_comment.present?
332
+ end
333
+ end
334
+
335
+ def comment_required_state?
336
+ auditing_enabled &&
337
+ audited_changes.present? &&
338
+ ((audited_options[:on].include?(:create) && new_record?) ||
339
+ (audited_options[:on].include?(:update) && persisted? && changed?))
340
+ end
341
+
342
+ def combine_audits_if_needed
343
+ max_audits = audited_options[:max_audits]
344
+ if max_audits && (extra_count = audits.count - max_audits) > 0
345
+ audits_to_combine = audits.limit(extra_count + 1)
346
+ combine_audits(audits_to_combine)
347
+ end
348
+ end
349
+
350
+ def require_comment
351
+ if auditing_enabled && audit_comment.blank?
352
+ errors.add(:audit_comment, :blank)
353
+ throw(:abort)
354
+ end
355
+ end
356
+
357
+ CALLBACKS.each do |attr_name|
358
+ alias_method "#{attr_name}_callback".to_sym, attr_name
359
+ end
360
+
361
+ def auditing_enabled
362
+ run_conditional_check(audited_options[:if]) &&
363
+ run_conditional_check(audited_options[:unless], matching: false) &&
364
+ self.class.auditing_enabled
365
+ end
366
+
367
+ def run_conditional_check(condition, matching: true)
368
+ return true if condition.blank?
369
+ return condition.call(self) == matching if condition.respond_to?(:call)
370
+ return send(condition) == matching if respond_to?(condition.to_sym, true)
371
+
372
+ true
373
+ end
374
+
375
+ def reconstruct_attributes(audits)
376
+ attributes = {}
377
+ audits.each { |audit| attributes.merge!(audit.new_attributes) }
378
+ attributes
379
+ end
380
+ end
381
+
382
+ module AuditedClassMethods
383
+ # Returns an array of columns that are audited. See non_audited_columns
384
+ def audited_columns
385
+ @audited_columns ||= column_names - non_audited_columns
386
+ end
387
+
388
+ # We have to calculate this here since column_names may not be available when `audited` is called
389
+ def non_audited_columns
390
+ @non_audited_columns ||= calculate_non_audited_columns
391
+ end
392
+
393
+ def non_audited_columns=(columns)
394
+ @audited_columns = nil # reset cached audited columns on assignment
395
+ @non_audited_columns = columns.map(&:to_s)
396
+ end
397
+
398
+ # Executes the block with auditing disabled.
399
+ #
400
+ # Foo.without_auditing do
401
+ # @foo.save
402
+ # end
403
+ #
404
+ def without_auditing
405
+ auditing_was_enabled = auditing_enabled
406
+ disable_auditing
407
+ yield
408
+ ensure
409
+ enable_auditing if auditing_was_enabled
410
+ end
411
+
412
+ # Executes the block with auditing enabled.
413
+ #
414
+ # Foo.with_auditing do
415
+ # @foo.save
416
+ # end
417
+ #
418
+ def with_auditing
419
+ auditing_was_enabled = auditing_enabled
420
+ enable_auditing
421
+ yield
422
+ ensure
423
+ disable_auditing unless auditing_was_enabled
424
+ end
425
+
426
+ def disable_auditing
427
+ self.auditing_enabled = false
428
+ end
429
+
430
+ def enable_auditing
431
+ self.auditing_enabled = true
432
+ end
433
+
434
+ # All audit operations during the block are recorded as being
435
+ # made by +user+. This is not model specific, the method is a
436
+ # convenience wrapper around
437
+ # @see Audit#as_user.
438
+ def audit_as(user, &block)
439
+ Audited.audit_class.as_user(user, &block)
440
+ end
441
+
442
+ def auditing_enabled
443
+ Audited.store.fetch("#{table_name}_auditing_enabled", true) && Audited.auditing_enabled
444
+ end
445
+
446
+ def auditing_enabled=(val)
447
+ Audited.store["#{table_name}_auditing_enabled"] = val
448
+ end
449
+
450
+ def default_ignored_attributes
451
+ [primary_key, inheritance_column] | Audited.ignored_attributes
452
+ end
453
+
454
+ protected
455
+
456
+ def normalize_audited_options
457
+ audited_options[:on] = Array.wrap(audited_options[:on])
458
+ audited_options[:on] = [:create, :update, :destroy] if audited_options[:on].empty?
459
+ audited_options[:only] = Array.wrap(audited_options[:only]).map(&:to_s)
460
+ audited_options[:except] = Array.wrap(audited_options[:except]).map(&:to_s)
461
+ max_audits = audited_options[:max_audits] || Audited.max_audits
462
+ audited_options[:max_audits] = Integer(max_audits).abs if max_audits
463
+ end
464
+
465
+ def calculate_non_audited_columns
466
+ if audited_options[:only].present?
467
+ (column_names | default_ignored_attributes) - audited_options[:only]
468
+ elsif audited_options[:except].present?
469
+ default_ignored_attributes | audited_options[:except]
470
+ else
471
+ default_ignored_attributes
472
+ end
473
+ end
474
+ end
475
+ end
476
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audited
4
+ class Railtie < Rails::Railtie
5
+ initializer "audited.sweeper" do
6
+ ActiveSupport.on_load(:action_controller) do
7
+ if defined?(ActionController::Base)
8
+ ActionController::Base.around_action Audited::Sweeper.new
9
+ end
10
+ if defined?(ActionController::API)
11
+ ActionController::API.around_action Audited::Sweeper.new
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end