tangledwires-audited 6.0.0

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 (34) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +37 -0
  3. data/CHANGELOG.md +539 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE +19 -0
  6. data/README.md +447 -0
  7. data/Rakefile +16 -0
  8. data/audited.gemspec +38 -0
  9. data/lib/audited/audit.rb +204 -0
  10. data/lib/audited/audit_associate.rb +8 -0
  11. data/lib/audited/auditor.rb +564 -0
  12. data/lib/audited/railtie.rb +16 -0
  13. data/lib/audited/rspec_matchers.rb +228 -0
  14. data/lib/audited/sweeper.rb +42 -0
  15. data/lib/audited/version.rb +5 -0
  16. data/lib/audited-rspec.rb +6 -0
  17. data/lib/audited.rb +60 -0
  18. data/lib/generators/audited/install_generator.rb +27 -0
  19. data/lib/generators/audited/migration.rb +25 -0
  20. data/lib/generators/audited/migration_helper.rb +11 -0
  21. data/lib/generators/audited/templates/add_association_to_audits.rb +13 -0
  22. data/lib/generators/audited/templates/add_comment_to_audits.rb +11 -0
  23. data/lib/generators/audited/templates/add_remote_address_to_audits.rb +12 -0
  24. data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +12 -0
  25. data/lib/generators/audited/templates/add_version_to_auditable_index.rb +23 -0
  26. data/lib/generators/audited/templates/create_audit_associates.rb +26 -0
  27. data/lib/generators/audited/templates/install.rb +39 -0
  28. data/lib/generators/audited/templates/rename_association_to_associated.rb +25 -0
  29. data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +11 -0
  30. data/lib/generators/audited/templates/rename_parent_to_association.rb +13 -0
  31. data/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb +22 -0
  32. data/lib/generators/audited/upgrade_generator.rb +74 -0
  33. data/shell.nix +8 -0
  34. metadata +241 -0
data/README.md ADDED
@@ -0,0 +1,447 @@
1
+ TangledWires Audited
2
+ [![Gem Version](https://img.shields.io/gem/v/tangledwires-audited.svg)](http://rubygems.org/gems/tangledwires-audited)
3
+ ![Build Status](https://github.com/TangledWiresOfficial/audited/actions/workflows/ci.yml/badge.svg)
4
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+ =======
6
+
7
+ **Audited** (previously acts_as_audited) is an ORM extension that logs all changes to your models. Audited can also record who made those changes, save comments and associate models related to the changes.
8
+
9
+
10
+ Audited currently (5.6) works with Rails 8.0, 7.2, 7.1, 7.0.
11
+
12
+ ## Supported Rubies
13
+
14
+ Audited supports and is [tested against](https://github.com/TangledWiresOfficial/audited/actions/workflows/ci.yml) the following Ruby versions:
15
+
16
+ * 2.3 (only tested on Sqlite due to testing issues with other DBs)
17
+ * 2.4
18
+ * 2.5
19
+ * 2.6
20
+ * 2.7
21
+ * 3.0
22
+ * 3.1
23
+ * 3.2
24
+ * 3.3
25
+
26
+ Audited may work just fine with a Ruby version not listed above, but we can't guarantee that it will.
27
+
28
+ ## Supported ORMs
29
+
30
+ Audited is currently ActiveRecord-only.
31
+
32
+ ## Installation
33
+
34
+ Add the gem to your Gemfile:
35
+
36
+ ```ruby
37
+ gem "tangledwires-audited"
38
+ ```
39
+
40
+ And if you're using ```require: false``` you must add initializers like this:
41
+
42
+ ```ruby
43
+ #./config/initializers/audited.rb
44
+ require "audited"
45
+
46
+ Audited::Railtie.initializers.each(&:run)
47
+ ```
48
+
49
+ Then, from your Rails app directory, create the `audits` table:
50
+
51
+ ```bash
52
+ $ rails generate audited:install
53
+ $ rake db:migrate
54
+ ```
55
+
56
+ By default, changes are stored in YAML format. If you're using PostgreSQL, then you can use `rails generate audited:install --audited-changes-column-type jsonb` (or `json` for MySQL 5.7+ and Rails 5+) to store audit changes natively with database JSON column types.
57
+
58
+ If you're using something other than integer primary keys (e.g. UUID) for your User model, then you can use `rails generate audited:install --audited-user-id-column-type uuid` to customize the `audits` table `user_id` column type.
59
+
60
+ #### Upgrading
61
+
62
+ If you're already using Audited (or acts_as_audited), your `audits` table may require additional columns. After every upgrade, please run:
63
+
64
+ ```bash
65
+ $ rails generate audited:upgrade
66
+ $ rake db:migrate
67
+ ```
68
+
69
+ Upgrading will only make changes if changes are needed.
70
+
71
+
72
+ ## Usage
73
+
74
+ Simply call `audited` on your models:
75
+
76
+ ```ruby
77
+ class User < ActiveRecord::Base
78
+ audited
79
+ end
80
+ ```
81
+
82
+ By default, whenever a user is created, updated or destroyed, a new audit is created.
83
+
84
+ ```ruby
85
+ user = User.create!(name: "Steve")
86
+ user.audits.count # => 1
87
+ user.update!(name: "Ryan")
88
+ user.audits.count # => 2
89
+ user.destroy
90
+ user.audits.count # => 3
91
+ ```
92
+
93
+ Audits contain information regarding what action was taken on the model and what changes were made.
94
+
95
+ ```ruby
96
+ user.update!(name: "Ryan")
97
+ audit = user.audits.last
98
+ audit.action # => "update"
99
+ audit.audited_changes # => {"name"=>["Steve", "Ryan"]}
100
+ ```
101
+
102
+ You can get previous versions of a record by index or date, or list all
103
+ revisions.
104
+
105
+ ```ruby
106
+ user.revisions
107
+ user.revision(1)
108
+ user.revision_at(Date.parse("2016-01-01"))
109
+ ```
110
+
111
+ ### Specifying columns
112
+
113
+ By default, a new audit is created for any attribute changes. You can, however, limit the columns to be considered.
114
+
115
+ ```ruby
116
+ class User < ActiveRecord::Base
117
+ # All fields
118
+ # audited
119
+
120
+ # Single field
121
+ # audited only: :name
122
+
123
+ # Multiple fields
124
+ # audited only: [:name, :address]
125
+
126
+ # All except certain fields
127
+ # audited except: :password
128
+ end
129
+ ```
130
+
131
+ ### Specifying callbacks
132
+
133
+ By default, a new audit is created for any Create, Update, Touch (Rails 6+) or Destroy action. You can, however, limit the actions audited.
134
+
135
+ ```ruby
136
+ class User < ActiveRecord::Base
137
+ # All fields and actions
138
+ # audited
139
+
140
+ # Single field, only audit Update and Destroy (not Create or Touch)
141
+ # audited only: :name, on: [:update, :destroy]
142
+ end
143
+ ```
144
+
145
+ You can ignore the default callbacks globally unless the callback action is specified in your model using the `:on` option. To configure default callback exclusion, put the following in an initializer file (`config/initializers/audited.rb`):
146
+
147
+ ```ruby
148
+ Audited.ignored_default_callbacks = [:create, :update] # ignore callbacks create and update
149
+ ```
150
+
151
+ ### Comments
152
+
153
+ You can attach comments to each audit using an `audit_comment` attribute on your model.
154
+
155
+ ```ruby
156
+ user.update!(name: "Ryan", audit_comment: "Changing name, just because")
157
+ user.audits.last.comment # => "Changing name, just because"
158
+ ```
159
+
160
+ You can optionally add the `:comment_required` option to your `audited` call to require comments for all audits.
161
+
162
+ ```ruby
163
+ class User < ActiveRecord::Base
164
+ audited :comment_required => true
165
+ end
166
+ ```
167
+
168
+ You can update an audit only if audit_comment is present. You can optionally add the `:update_with_comment_only` option set to `false` to your `audited` call to turn this behavior off for all audits.
169
+
170
+ ```ruby
171
+ class User < ActiveRecord::Base
172
+ audited :update_with_comment_only => false
173
+ end
174
+ ```
175
+
176
+ ### Limiting stored audits
177
+
178
+ You can limit the number of audits stored for your model. To configure limiting for all audited models, put the following in an initializer file (`config/initializers/audited.rb`):
179
+
180
+ ```ruby
181
+ Audited.max_audits = 10 # keep only 10 latest audits
182
+ ```
183
+
184
+ or customize per model:
185
+
186
+ ```ruby
187
+ class User < ActiveRecord::Base
188
+ audited max_audits: 2
189
+ end
190
+ ```
191
+
192
+ Whenever an object is updated or destroyed, extra audits are combined with newer ones and the old ones are destroyed.
193
+
194
+ ```ruby
195
+ user = User.create!(name: "Steve")
196
+ user.audits.count # => 1
197
+ user.update!(name: "Ryan")
198
+ user.audits.count # => 2
199
+ user.destroy
200
+ user.audits.count # => 2
201
+ ```
202
+
203
+ ### Current User Tracking
204
+
205
+ If you're using Audited in a Rails application, all audited changes made within a request will automatically be attributed to the current user. By default, Audited uses the `current_user` method in your controller.
206
+
207
+ ```ruby
208
+ class PostsController < ApplicationController
209
+ def create
210
+ current_user # => #<User name: "Steve">
211
+ @post = Post.create(params[:post])
212
+ @post.audits.last.user # => #<User name: "Steve">
213
+ end
214
+ end
215
+ ```
216
+
217
+ To use a method other than `current_user`, put the following in an initializer file (`config/initializers/audited.rb`):
218
+
219
+ ```ruby
220
+ Audited.current_user_method = :authenticated_user
221
+ ```
222
+
223
+ Outside of a request, Audited can still record the user with the `as_user` method:
224
+
225
+ ```ruby
226
+ Audited.audit_class.as_user(User.find(1)) do
227
+ post.update!(title: "Hello, world!")
228
+ end
229
+ post.audits.last.user # => #<User id: 1>
230
+ ```
231
+
232
+ The standard Audited install assumes your User model has an integer primary key type. If this isn't true (e.g. you're using UUID primary keys), you'll need to create a migration to update the `audits` table `user_id` column type. (See Installation above for generator flags if you'd like to regenerate the install migration.)
233
+
234
+ #### Custom Audit User
235
+
236
+ You might need to use a custom auditor from time to time. This can be done by simply passing in a string:
237
+
238
+ ```ruby
239
+ class ApplicationController < ActionController::Base
240
+ def authenticated_user
241
+ if current_user
242
+ current_user
243
+ else
244
+ 'Alexander Fleming'
245
+ end
246
+ end
247
+ end
248
+ ```
249
+
250
+ `as_user` also accepts a string, which can be useful for auditing updates made in a CLI environment:
251
+
252
+ ```rb
253
+ Audited.audit_class.as_user("console-user-#{ENV['SSH_USER']}") do
254
+ post.update_attributes!(title: "Hello, world!")
255
+ end
256
+ post.audits.last.user # => 'console-user-username'
257
+ ```
258
+
259
+ If you want to set a specific user as the auditor of the commands in a CLI environment, whether that is a string or an ActiveRecord object, you can use the following command:
260
+
261
+ ```rb
262
+ Audited.store[:audited_user] = "username"
263
+
264
+ # or
265
+
266
+ Audited.store[:audited_user] = User.find(1)
267
+ ```
268
+
269
+ ### Associated Audits
270
+
271
+ Sometimes it's useful to associate an audit with a model other than the one being changed. For instance, given the following models:
272
+
273
+ ```ruby
274
+ class User < ActiveRecord::Base
275
+ belongs_to :company
276
+ audited
277
+ end
278
+
279
+ class Company < ActiveRecord::Base
280
+ has_many :users
281
+ end
282
+ ```
283
+
284
+ Every change to a user is audited, but what if you want to grab all of the audits of users belonging to a particular company? You can add the `:associated_with` option to your `audited` call:
285
+
286
+ ```ruby
287
+ class User < ActiveRecord::Base
288
+ belongs_to :company
289
+ audited associated_with: :company
290
+ end
291
+
292
+ class Company < ActiveRecord::Base
293
+ audited
294
+ has_many :users
295
+ has_associated_audits
296
+ end
297
+ ```
298
+
299
+ Now, when an audit is created for a user, that user's company is also saved alongside the audit. This makes it much easier (and faster) to access audits indirectly related to a company.
300
+
301
+ ```ruby
302
+ company = Company.create!(name: "Collective Idea")
303
+ user = company.users.create!(name: "Steve")
304
+ user.update!(name: "Steve Richert")
305
+ user.audits.last.associated # => #<Company name: "Collective Idea">
306
+ company.associated_audits.last.auditable # => #<User name: "Steve Richert">
307
+ ```
308
+
309
+ You can access records' own audits and associated audits in one go:
310
+ ```ruby
311
+ company.own_and_associated_audits
312
+ ```
313
+
314
+ ### Conditional auditing
315
+
316
+ If you want to audit only under specific conditions, you can provide conditional options (similar to ActiveModel callbacks) that will ensure your model is only audited for these conditions.
317
+
318
+ ```ruby
319
+ class User < ActiveRecord::Base
320
+ audited if: :active?
321
+
322
+ def active?
323
+ last_login > 6.months.ago
324
+ end
325
+ end
326
+ ```
327
+
328
+ Just like in ActiveModel, you can use an inline Proc in your conditions:
329
+
330
+ ```ruby
331
+ class User < ActiveRecord::Base
332
+ audited unless: Proc.new { |u| u.ninja? }
333
+ end
334
+ ```
335
+
336
+ In the above case, the user will only be audited when `User#ninja` is `false`.
337
+
338
+ ### Disabling auditing
339
+
340
+ If you want to disable auditing temporarily doing certain tasks, there are a few
341
+ methods available.
342
+
343
+ To disable auditing on a save:
344
+
345
+ ```ruby
346
+ @user.save_without_auditing
347
+ ```
348
+
349
+ or:
350
+
351
+ ```ruby
352
+ @user.without_auditing do
353
+ @user.save
354
+ end
355
+ ```
356
+
357
+ To disable auditing on a column:
358
+
359
+ ```ruby
360
+ User.non_audited_columns = [:first_name, :last_name]
361
+ ```
362
+
363
+ To disable auditing on an entire model:
364
+
365
+ ```ruby
366
+ User.auditing_enabled = false
367
+ ```
368
+
369
+ To disable auditing on all models:
370
+
371
+ ```ruby
372
+ Audited.auditing_enabled = false
373
+ ```
374
+
375
+ If you have auditing disabled by default on your model you can enable auditing
376
+ temporarily.
377
+
378
+ ```ruby
379
+ User.auditing_enabled = false
380
+ @user.save_with_auditing
381
+ ```
382
+
383
+ or:
384
+
385
+ ```ruby
386
+ User.auditing_enabled = false
387
+ @user.with_auditing do
388
+ @user.save
389
+ end
390
+ ```
391
+
392
+ ### Encrypted attributes
393
+
394
+ If you're using ActiveRecord's encryption (available from Rails 7) to encrypt some attributes, Audited will automatically filter values of these attributes. No additional configuration is required. Changes to encrypted attributes will be logged as `[FILTERED]`.
395
+
396
+ ```ruby
397
+ class User < ActiveRecord::Base
398
+ audited
399
+ encrypts :password
400
+ end
401
+ ```
402
+
403
+ ### Custom `Audit` model
404
+
405
+ If you want to extend or modify the audit model, create a new class that
406
+ inherits from `Audited::Audit`:
407
+ ```ruby
408
+ class CustomAudit < Audited::Audit
409
+ def some_custom_behavior
410
+ "Hiya!"
411
+ end
412
+ end
413
+ ```
414
+ Then set it in an initializer:
415
+ ```ruby
416
+ # config/initializers/audited.rb
417
+
418
+ Audited.config do |config|
419
+ config.audit_class = "CustomAudit"
420
+ end
421
+ ```
422
+
423
+ ### Enum Storage
424
+
425
+ In 4.10, the default behavior for enums changed from storing the value synthesized by Rails to the value stored in the DB. You can restore the previous behavior by setting the store_synthesized_enums configuration value:
426
+
427
+ ```ruby
428
+ # config/initializers/audited.rb
429
+
430
+ Audited.store_synthesized_enums = true
431
+ ```
432
+
433
+ ## Support
434
+
435
+ You can find documentation at: https://www.rubydoc.info/gems/audited
436
+
437
+ Or join the [mailing list](http://groups.google.com/group/audited) to get help or offer suggestions.
438
+
439
+ ## Contributing
440
+
441
+ In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project. Here are a few ways _you_ can pitch in:
442
+
443
+ * Use prerelease versions of Audited.
444
+ * [Report bugs](https://github.com/TangledWiresOfficial/audited/issues).
445
+ * Fix bugs and submit [pull requests](http://github.com/TangledWiresOfficial/audited/pulls).
446
+ * Write, clarify or fix documentation.
447
+ * Refactor code.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rake/testtask"
6
+ require "appraisal"
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ Rake::TestTask.new do |t|
11
+ t.libs << "test"
12
+ t.test_files = FileList["test/**/*_test.rb"]
13
+ t.verbose = true
14
+ end
15
+
16
+ task default: [:spec, :test]
data/audited.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "audited/version"
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "tangledwires-audited"
6
+ gem.version = Audited::VERSION
7
+
8
+ gem.authors = ["TangledWires Ltd", "Brandon Keepers", "Kenneth Kalmer", "Daniel Morrison", "Brian Ryckbost", "Steve Richert", "Ryan Glover"]
9
+ gem.email = "support@tangledwires.co.uk"
10
+ gem.description = "Log all changes to your models"
11
+ gem.summary = gem.description
12
+ gem.homepage = "https://github.com/TangledWiresOfficial/audited"
13
+ gem.license = "MIT"
14
+
15
+ gem.files = `git ls-files`.split($\).reject { |f| f =~ /^(\.gemspec|\.git|\.standard|\.yard|gemfiles|test|spec)/ }
16
+
17
+ gem.required_ruby_version = ">= 2.3.0"
18
+
19
+ gem.add_dependency "activerecord", ">= 7.0", "< 8.2"
20
+ gem.add_dependency "activesupport", ">= 7.0", "< 8.2"
21
+
22
+ gem.add_development_dependency "appraisal"
23
+ gem.add_development_dependency "rails", ">= 7.0", "< 8.2"
24
+ gem.add_development_dependency "rspec-rails"
25
+ gem.add_development_dependency "standard"
26
+ gem.add_development_dependency "single_cov"
27
+
28
+ # JRuby support for the test ENV
29
+ if defined?(JRUBY_VERSION)
30
+ gem.add_development_dependency "activerecord-jdbcsqlite3-adapter", "~> 1.3"
31
+ gem.add_development_dependency "activerecord-jdbcpostgresql-adapter", "~> 1.3"
32
+ gem.add_development_dependency "activerecord-jdbcmysql-adapter", "~> 1.3"
33
+ else
34
+ gem.add_development_dependency "sqlite3", ">= 1.3.6"
35
+ gem.add_development_dependency "mysql2", ">= 0.3.20"
36
+ gem.add_development_dependency "pg", ">= 0.18", "< 2.0"
37
+ end
38
+ end
@@ -0,0 +1,204 @@
1
+ require "set"
2
+
3
+ module Audited
4
+ # Audit saves the changes to ActiveRecord models. It has the following attributes:
5
+ #
6
+ # * <tt>auditable</tt>: the ActiveRecord model that was changed
7
+ # * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model
8
+ # * <tt>action</tt>: one of create, update, or delete
9
+ # * <tt>audited_changes</tt>: a hash of all the changes
10
+ # * <tt>comment</tt>: a comment set with the audit
11
+ # * <tt>version</tt>: the version of the model
12
+ # * <tt>request_uuid</tt>: a uuid based that allows audits from the same controller request
13
+ # * <tt>created_at</tt>: Time that the change was performed
14
+ #
15
+
16
+ class YAMLIfTextColumnType
17
+ class << self
18
+ def load(obj)
19
+ if text_column?
20
+ ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
21
+ else
22
+ obj
23
+ end
24
+ end
25
+
26
+ def dump(obj)
27
+ if text_column?
28
+ ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
29
+ else
30
+ obj
31
+ end
32
+ end
33
+
34
+ def text_column?
35
+ Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
36
+ end
37
+ end
38
+ end
39
+
40
+ class Audit < ::ActiveRecord::Base
41
+ belongs_to :auditable, polymorphic: true
42
+ belongs_to :user, polymorphic: true
43
+ has_many :audit_associates
44
+
45
+ before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
46
+
47
+ cattr_accessor :audited_class_names
48
+ self.audited_class_names = Set.new
49
+
50
+ if ::ActiveRecord.version >= Gem::Version.new("7.1")
51
+ serialize :audited_changes, coder: YAMLIfTextColumnType
52
+ else
53
+ serialize :audited_changes, YAMLIfTextColumnType
54
+ end
55
+
56
+ scope :ascending, -> { reorder(version: :asc) }
57
+ scope :descending, -> { reorder(version: :desc) }
58
+ scope :creates, -> { where(action: "create") }
59
+ scope :updates, -> { where(action: "update") }
60
+ scope :destroys, -> { where(action: "destroy") }
61
+
62
+ scope :up_until, ->(date_or_time) { where("created_at <= ?", date_or_time) }
63
+ scope :from_version, ->(version) { where("version >= ?", version) }
64
+ scope :to_version, ->(version) { where("version <= ?", version) }
65
+ scope :auditable_finder, ->(auditable_id, auditable_type) { where(auditable_id: auditable_id, auditable_type: auditable_type) }
66
+ # Return all audits older than the current one.
67
+ def ancestors
68
+ self.class.ascending.auditable_finder(auditable_id, auditable_type).to_version(version)
69
+ end
70
+
71
+ def associates
72
+ audit_associates.map(&:associated)
73
+ end
74
+
75
+ # Return an instance of what the object looked like at this revision. If
76
+ # the object has been destroyed, this will be a new record.
77
+ def revision
78
+ clazz = auditable_type.constantize
79
+ (clazz.find_by_id(auditable_id) || clazz.new).tap do |m|
80
+ self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(audit_version: version))
81
+ end
82
+ end
83
+
84
+ # Returns a hash of the changed attributes with the new values
85
+ def new_attributes
86
+ (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
87
+ attrs[attr] = (action == "update") ? values.last : values
88
+ end
89
+ end
90
+
91
+ # Returns a hash of the changed attributes with the old values
92
+ def old_attributes
93
+ (audited_changes || {}).each_with_object({}.with_indifferent_access) do |(attr, values), attrs|
94
+ attrs[attr] = (action == "update") ? values.first : values
95
+ end
96
+ end
97
+
98
+ # Allows user to undo changes
99
+ def undo
100
+ case action
101
+ when "create"
102
+ # destroys a newly created record
103
+ auditable.destroy!
104
+ when "destroy"
105
+ # creates a new record with the destroyed record attributes
106
+ auditable_type.constantize.create!(audited_changes)
107
+ when "update"
108
+ # changes back attributes
109
+ auditable.update!(audited_changes.transform_values(&:first))
110
+ else
111
+ raise StandardError, "invalid action given #{action}"
112
+ end
113
+ end
114
+
115
+ # Allows user to be set to either a string or an ActiveRecord object
116
+ # @private
117
+ def user_as_string=(user)
118
+ # reset both either way
119
+ self.user_as_model = self.username = nil
120
+ user.is_a?(::ActiveRecord::Base) ?
121
+ self.user_as_model = user :
122
+ self.username = user
123
+ end
124
+ alias_method :user_as_model=, :user=
125
+ alias_method :user=, :user_as_string=
126
+
127
+ # @private
128
+ def user_as_string
129
+ user_as_model || username
130
+ end
131
+ alias_method :user_as_model, :user
132
+ alias_method :user, :user_as_string
133
+
134
+ # Returns the list of classes that are being audited
135
+ def self.audited_classes
136
+ audited_class_names.map(&:constantize)
137
+ end
138
+
139
+ # All audits made during the block called will be recorded as made
140
+ # by +user+. This method is hopefully threadsafe, making it ideal
141
+ # for background operations that require audit information.
142
+ def self.as_user(user)
143
+ last_audited_user = ::Audited.store[:audited_user]
144
+ ::Audited.store[:audited_user] = user
145
+ yield
146
+ ensure
147
+ ::Audited.store[:audited_user] = last_audited_user
148
+ end
149
+
150
+ # @private
151
+ def self.reconstruct_attributes(audits)
152
+ audits.each_with_object({}) do |audit, all|
153
+ all.merge!(audit.new_attributes)
154
+ all[:audit_version] = audit.version
155
+ end
156
+ end
157
+
158
+ # @private
159
+ def self.assign_revision_attributes(record, attributes)
160
+ attributes.each do |attr, val|
161
+ record = record.dup if record.frozen?
162
+
163
+ if record.respond_to?("#{attr}=")
164
+ record.attributes.key?(attr.to_s) ?
165
+ record[attr] = val :
166
+ record.send("#{attr}=", val)
167
+ end
168
+ end
169
+ record
170
+ end
171
+
172
+ # use created_at as timestamp cache key
173
+ def self.collection_cache_key(collection = all, *)
174
+ super(collection, :created_at)
175
+ end
176
+
177
+ private
178
+
179
+ def set_version_number
180
+ if action == "create"
181
+ self.version = 1
182
+ else
183
+ collection = (ActiveRecord::VERSION::MAJOR >= 6) ? self.class.unscoped : self.class
184
+ max = collection.auditable_finder(auditable_id, auditable_type).maximum(:version) || 0
185
+ self.version = max + 1
186
+ end
187
+ end
188
+
189
+ def set_audit_user
190
+ self.user ||= ::Audited.store[:audited_user] # from .as_user
191
+ self.user ||= ::Audited.store[:current_user].try!(:call) # from Sweeper
192
+ nil # prevent stopping callback chains
193
+ end
194
+
195
+ def set_request_uuid
196
+ self.request_uuid ||= ::Audited.store[:current_request_uuid]
197
+ self.request_uuid ||= SecureRandom.uuid
198
+ end
199
+
200
+ def set_remote_address
201
+ self.remote_address ||= ::Audited.store[:current_remote_address]
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,8 @@
1
+ module Audited
2
+ class AuditAssociate < ::ActiveRecord::Base
3
+ self.table_name_prefix = "audited_"
4
+
5
+ belongs_to :audit
6
+ belongs_to :associated, polymorphic: true
7
+ end
8
+ end