velocity_audited 5.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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