audited 4.3.0 → 4.6.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.
Potentially problematic release.
This version of audited might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.travis.yml +19 -2
- data/Appraisals +8 -2
- data/CHANGELOG.md +260 -0
- data/README.md +32 -12
- data/gemfiles/rails40.gemfile +1 -1
- data/gemfiles/rails41.gemfile +1 -1
- data/gemfiles/rails42.gemfile +1 -1
- data/gemfiles/rails50.gemfile +1 -2
- data/gemfiles/rails51.gemfile +7 -0
- data/gemfiles/rails52.gemfile +8 -0
- data/lib/audited.rb +6 -4
- data/lib/audited/audit.rb +57 -8
- data/lib/audited/auditor.rb +54 -59
- data/lib/audited/rspec_matchers.rb +2 -2
- data/lib/audited/sweeper.rb +18 -29
- data/lib/audited/version.rb +1 -1
- data/lib/generators/audited/install_generator.rb +5 -0
- data/lib/generators/audited/migration_helper.rb +9 -0
- data/lib/generators/audited/templates/add_association_to_audits.rb +1 -1
- data/lib/generators/audited/templates/add_comment_to_audits.rb +1 -1
- data/lib/generators/audited/templates/add_remote_address_to_audits.rb +1 -1
- data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +1 -1
- data/lib/generators/audited/templates/install.rb +5 -5
- data/lib/generators/audited/templates/rename_association_to_associated.rb +1 -1
- data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +1 -1
- data/lib/generators/audited/templates/rename_parent_to_association.rb +1 -1
- data/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb +20 -0
- data/lib/generators/audited/upgrade_generator.rb +7 -0
- data/spec/audited/audit_spec.rb +71 -2
- data/spec/audited/auditor_spec.rb +69 -3
- data/spec/audited/sweeper_spec.rb +24 -6
- data/spec/audited_spec_helpers.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/support/active_record/models.rb +2 -1
- data/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb +12 -0
- data/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb +12 -0
- data/spec/support/active_record/schema.rb +1 -0
- data/test/install_generator_test.rb +49 -3
- data/test/upgrade_generator_test.rb +16 -1
- metadata +14 -22
- data/CHANGELOG +0 -34
data/gemfiles/rails50.gemfile
CHANGED
data/lib/audited.rb
CHANGED
@@ -1,19 +1,21 @@
|
|
1
|
-
require 'rails/observers/active_model/active_model'
|
2
1
|
require 'active_record'
|
3
2
|
|
4
3
|
module Audited
|
5
4
|
class << self
|
6
5
|
attr_accessor :ignored_attributes, :current_user_method
|
6
|
+
attr_writer :audit_class
|
7
7
|
|
8
|
-
# Deprecate audit_class accessors in preperation of their removal
|
9
8
|
def audit_class
|
10
|
-
|
9
|
+
@audit_class ||= Audit
|
11
10
|
end
|
12
|
-
deprecate audit_class: "Audited.audit_class is now always Audited::Audit. This method will be removed."
|
13
11
|
|
14
12
|
def store
|
15
13
|
Thread.current[:audited_store] ||= {}
|
16
14
|
end
|
15
|
+
|
16
|
+
def config
|
17
|
+
yield(self)
|
18
|
+
end
|
17
19
|
end
|
18
20
|
|
19
21
|
@ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
|
data/lib/audited/audit.rb
CHANGED
@@ -6,25 +6,44 @@ module Audited
|
|
6
6
|
# * <tt>auditable</tt>: the ActiveRecord model that was changed
|
7
7
|
# * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model
|
8
8
|
# * <tt>action</tt>: one of create, update, or delete
|
9
|
-
# * <tt>audited_changes</tt>: a
|
9
|
+
# * <tt>audited_changes</tt>: a hash of all the changes
|
10
10
|
# * <tt>comment</tt>: a comment set with the audit
|
11
11
|
# * <tt>version</tt>: the version of the model
|
12
12
|
# * <tt>request_uuid</tt>: a uuid based that allows audits from the same controller request
|
13
13
|
# * <tt>created_at</tt>: Time that the change was performed
|
14
14
|
#
|
15
|
-
class Audit < ::ActiveRecord::Base
|
16
|
-
include ActiveModel::Observing
|
17
15
|
|
16
|
+
class YAMLIfTextColumnType
|
17
|
+
class << self
|
18
|
+
def load(obj)
|
19
|
+
if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
|
20
|
+
ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
|
21
|
+
else
|
22
|
+
obj
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def dump(obj)
|
27
|
+
if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
|
28
|
+
ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
|
29
|
+
else
|
30
|
+
obj
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Audit < ::ActiveRecord::Base
|
18
37
|
belongs_to :auditable, polymorphic: true
|
19
38
|
belongs_to :user, polymorphic: true
|
20
39
|
belongs_to :associated, polymorphic: true
|
21
40
|
|
22
|
-
before_create :set_version_number, :set_audit_user, :set_request_uuid
|
41
|
+
before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
|
23
42
|
|
24
43
|
cattr_accessor :audited_class_names
|
25
44
|
self.audited_class_names = Set.new
|
26
45
|
|
27
|
-
serialize :audited_changes
|
46
|
+
serialize :audited_changes, YAMLIfTextColumnType
|
28
47
|
|
29
48
|
scope :ascending, ->{ reorder(version: :asc) }
|
30
49
|
scope :descending, ->{ reorder(version: :desc)}
|
@@ -67,6 +86,25 @@ module Audited
|
|
67
86
|
end
|
68
87
|
end
|
69
88
|
|
89
|
+
# Allows user to undo changes
|
90
|
+
def undo
|
91
|
+
model = self.auditable_type.constantize
|
92
|
+
if action == 'create'
|
93
|
+
# destroys a newly created record
|
94
|
+
model.find(auditable_id).destroy!
|
95
|
+
elsif action == 'destroy'
|
96
|
+
# creates a new record with the destroyed record attributes
|
97
|
+
model.create(audited_changes)
|
98
|
+
else
|
99
|
+
# changes back attributes
|
100
|
+
audited_object = model.find(auditable_id)
|
101
|
+
self.audited_changes.each do |k, v|
|
102
|
+
audited_object[k] = v[0]
|
103
|
+
end
|
104
|
+
audited_object.save
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
70
108
|
# Allows user to be set to either a string or an ActiveRecord object
|
71
109
|
# @private
|
72
110
|
def user_as_string=(user)
|
@@ -95,10 +133,10 @@ module Audited
|
|
95
133
|
# by +user+. This method is hopefully threadsafe, making it ideal
|
96
134
|
# for background operations that require audit information.
|
97
135
|
def self.as_user(user, &block)
|
98
|
-
|
136
|
+
::Audited.store[:audited_user] = user
|
99
137
|
yield
|
100
138
|
ensure
|
101
|
-
|
139
|
+
::Audited.store[:audited_user] = nil
|
102
140
|
end
|
103
141
|
|
104
142
|
# @private
|
@@ -125,6 +163,11 @@ module Audited
|
|
125
163
|
record
|
126
164
|
end
|
127
165
|
|
166
|
+
# use created_at as timestamp cache key
|
167
|
+
def self.collection_cache_key(collection = all, timestamp_column = :created_at)
|
168
|
+
super(collection, :created_at)
|
169
|
+
end
|
170
|
+
|
128
171
|
private
|
129
172
|
|
130
173
|
def set_version_number
|
@@ -133,12 +176,18 @@ module Audited
|
|
133
176
|
end
|
134
177
|
|
135
178
|
def set_audit_user
|
136
|
-
self.user
|
179
|
+
self.user ||= ::Audited.store[:audited_user] # from .as_user
|
180
|
+
self.user ||= ::Audited.store[:current_user].try!(:call) # from Sweeper
|
137
181
|
nil # prevent stopping callback chains
|
138
182
|
end
|
139
183
|
|
140
184
|
def set_request_uuid
|
185
|
+
self.request_uuid ||= ::Audited.store[:current_request_uuid]
|
141
186
|
self.request_uuid ||= SecureRandom.uuid
|
142
187
|
end
|
188
|
+
|
189
|
+
def set_remote_address
|
190
|
+
self.remote_address ||= ::Audited.store[:current_remote_address]
|
191
|
+
end
|
143
192
|
end
|
144
193
|
end
|
data/lib/audited/auditor.rb
CHANGED
@@ -38,26 +38,29 @@ module Audited
|
|
38
38
|
# don't allow multiple calls
|
39
39
|
return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
|
40
40
|
|
41
|
+
extend Audited::Auditor::AuditedClassMethods
|
42
|
+
include Audited::Auditor::AuditedInstanceMethods
|
43
|
+
|
41
44
|
class_attribute :audit_associated_with, instance_writer: false
|
42
45
|
class_attribute :audited_options, instance_writer: false
|
46
|
+
attr_accessor :version, :audit_comment
|
43
47
|
|
44
48
|
self.audited_options = options
|
45
|
-
|
49
|
+
normalize_audited_options
|
46
50
|
|
47
|
-
|
51
|
+
self.audit_associated_with = audited_options[:associated_with]
|
52
|
+
|
53
|
+
if audited_options[:comment_required]
|
48
54
|
validates_presence_of :audit_comment, if: :auditing_enabled
|
49
55
|
before_destroy :require_comment
|
50
56
|
end
|
51
57
|
|
52
|
-
|
53
|
-
|
54
|
-
has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audit.name
|
55
|
-
Audit.audited_class_names << to_s
|
58
|
+
has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name
|
59
|
+
Audited.audit_class.audited_class_names << to_s
|
56
60
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
before_destroy :audit_destroy if on.empty? || on.include?(:destroy)
|
61
|
+
after_create :audit_create if audited_options[:on].include?(:create)
|
62
|
+
before_update :audit_update if audited_options[:on].include?(:update)
|
63
|
+
before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
|
61
64
|
|
62
65
|
# Define and set after_audit and around_audit callbacks. This might be useful if you want
|
63
66
|
# to notify a party after the audit has been created or if you want to access the newly-created
|
@@ -66,20 +69,11 @@ module Audited
|
|
66
69
|
set_callback :audit, :after, :after_audit, if: lambda { respond_to?(:after_audit, true) }
|
67
70
|
set_callback :audit, :around, :around_audit, if: lambda { respond_to?(:around_audit, true) }
|
68
71
|
|
69
|
-
|
70
|
-
|
71
|
-
extend Audited::Auditor::AuditedClassMethods
|
72
|
-
include Audited::Auditor::AuditedInstanceMethods
|
73
|
-
|
74
|
-
self.auditing_enabled = true
|
72
|
+
enable_auditing
|
75
73
|
end
|
76
74
|
|
77
75
|
def has_associated_audits
|
78
|
-
has_many :associated_audits, as: :associated, class_name:
|
79
|
-
end
|
80
|
-
|
81
|
-
def default_ignored_attributes
|
82
|
-
[primary_key, inheritance_column]
|
76
|
+
has_many :associated_audits, as: :associated, class_name: Audited.audit_class.name
|
83
77
|
end
|
84
78
|
end
|
85
79
|
|
@@ -109,22 +103,21 @@ module Audited
|
|
109
103
|
def revisions(from_version = 1)
|
110
104
|
audits = self.audits.from_version(from_version)
|
111
105
|
return [] if audits.empty?
|
112
|
-
|
113
|
-
audits.each do |audit|
|
114
|
-
revisions << audit.revision
|
115
|
-
end
|
116
|
-
revisions
|
106
|
+
audits.map(&:revision)
|
117
107
|
end
|
118
108
|
|
119
109
|
# Get a specific revision specified by the version number, or +:previous+
|
110
|
+
# Returns nil for versions greater than revisions count
|
120
111
|
def revision(version)
|
121
|
-
|
112
|
+
if version == :previous || self.audits.last.version >= version
|
113
|
+
revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
|
114
|
+
end
|
122
115
|
end
|
123
116
|
|
124
117
|
# Find the oldest revision recorded prior to the date/time provided.
|
125
118
|
def revision_at(date_or_time)
|
126
119
|
audits = self.audits.up_until(date_or_time)
|
127
|
-
revision_with
|
120
|
+
revision_with Audited.audit_class.reconstruct_attributes(audits) unless audits.empty?
|
128
121
|
end
|
129
122
|
|
130
123
|
# List of attributes that are audited.
|
@@ -132,16 +125,16 @@ module Audited
|
|
132
125
|
attributes.except(*non_audited_columns)
|
133
126
|
end
|
134
127
|
|
135
|
-
def non_audited_columns
|
136
|
-
self.class.non_audited_columns
|
137
|
-
end
|
138
|
-
|
139
128
|
protected
|
140
129
|
|
141
130
|
def non_audited_columns
|
142
131
|
self.class.non_audited_columns
|
143
132
|
end
|
144
133
|
|
134
|
+
def audited_columns
|
135
|
+
self.class.audited_columns
|
136
|
+
end
|
137
|
+
|
145
138
|
def revision_with(attributes)
|
146
139
|
dup.tap do |revision|
|
147
140
|
revision.id = id
|
@@ -152,7 +145,7 @@ module Audited
|
|
152
145
|
revision.send :instance_variable_set, '@destroyed', false
|
153
146
|
revision.send :instance_variable_set, '@_destroyed', false
|
154
147
|
revision.send :instance_variable_set, '@marked_for_destruction', false
|
155
|
-
|
148
|
+
Audited.audit_class.assign_revision_attributes(revision, attributes)
|
156
149
|
|
157
150
|
# Remove any association proxies so that they will be recreated
|
158
151
|
# and reference the correct object for this revision. The only way
|
@@ -175,17 +168,11 @@ module Audited
|
|
175
168
|
private
|
176
169
|
|
177
170
|
def audited_changes
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
changed_attributes.except(*non_audited_columns)
|
184
|
-
end
|
185
|
-
|
186
|
-
collection.inject({}) do |changes, (attr, old_value)|
|
187
|
-
changes[attr] = [old_value, self[attr]]
|
188
|
-
changes
|
171
|
+
all_changes = respond_to?(:changes_to_save) ? changes_to_save : changes
|
172
|
+
if audited_options[:only].present?
|
173
|
+
all_changes.slice(*audited_columns)
|
174
|
+
else
|
175
|
+
all_changes.except(*non_audited_columns)
|
189
176
|
end
|
190
177
|
end
|
191
178
|
|
@@ -236,9 +223,6 @@ module Audited
|
|
236
223
|
alias_method "#{attr_name}_callback".to_sym, attr_name
|
237
224
|
end
|
238
225
|
|
239
|
-
def empty_callback #:nodoc:
|
240
|
-
end
|
241
|
-
|
242
226
|
def auditing_enabled
|
243
227
|
self.class.auditing_enabled
|
244
228
|
end
|
@@ -251,20 +235,19 @@ module Audited
|
|
251
235
|
module AuditedClassMethods
|
252
236
|
# Returns an array of columns that are audited. See non_audited_columns
|
253
237
|
def audited_columns
|
254
|
-
|
238
|
+
@audited_columns ||= column_names - non_audited_columns
|
255
239
|
end
|
256
240
|
|
241
|
+
# We have to calculate this here since column_names may not be available when `audited` is called
|
257
242
|
def non_audited_columns
|
258
|
-
@non_audited_columns ||=
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
except
|
267
|
-
end
|
243
|
+
@non_audited_columns ||= audited_options[:only].present? ?
|
244
|
+
column_names - audited_options[:only] :
|
245
|
+
default_ignored_attributes | audited_options[:except]
|
246
|
+
end
|
247
|
+
|
248
|
+
def non_audited_columns=(columns)
|
249
|
+
@audited_columns = nil # reset cached audited columns on assignment
|
250
|
+
@non_audited_columns = columns.map(&:to_s)
|
268
251
|
end
|
269
252
|
|
270
253
|
# Executes the block with auditing disabled.
|
@@ -294,7 +277,7 @@ module Audited
|
|
294
277
|
# convenience wrapper around
|
295
278
|
# @see Audit#as_user.
|
296
279
|
def audit_as(user, &block)
|
297
|
-
|
280
|
+
Audited.audit_class.as_user(user, &block)
|
298
281
|
end
|
299
282
|
|
300
283
|
def auditing_enabled
|
@@ -304,6 +287,18 @@ module Audited
|
|
304
287
|
def auditing_enabled=(val)
|
305
288
|
Audited.store["#{table_name}_auditing_enabled"] = val
|
306
289
|
end
|
290
|
+
|
291
|
+
protected
|
292
|
+
def default_ignored_attributes
|
293
|
+
[primary_key, inheritance_column] + Audited.ignored_attributes
|
294
|
+
end
|
295
|
+
|
296
|
+
def normalize_audited_options
|
297
|
+
audited_options[:on] = Array.wrap(audited_options[:on])
|
298
|
+
audited_options[:on] = [:create, :update, :destroy] if audited_options[:on].empty?
|
299
|
+
audited_options[:only] = Array.wrap(audited_options[:only]).map(&:to_s)
|
300
|
+
audited_options[:except] = Array.wrap(audited_options[:except]).map(&:to_s)
|
301
|
+
end
|
307
302
|
end
|
308
303
|
end
|
309
304
|
end
|
@@ -117,7 +117,7 @@ module Audited
|
|
117
117
|
except |= @options[:except].collect(&:to_s) if @options[:except]
|
118
118
|
end
|
119
119
|
|
120
|
-
expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{
|
120
|
+
expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{except})"
|
121
121
|
model_class.non_audited_columns =~ except
|
122
122
|
else
|
123
123
|
true
|
@@ -170,7 +170,7 @@ module Audited
|
|
170
170
|
def association_exists?
|
171
171
|
!reflection.nil? &&
|
172
172
|
reflection.macro == :has_many &&
|
173
|
-
reflection.options[:class_name] ==
|
173
|
+
reflection.options[:class_name] == Audited.audit_class.name
|
174
174
|
end
|
175
175
|
end
|
176
176
|
end
|
data/lib/audited/sweeper.rb
CHANGED
@@ -1,60 +1,49 @@
|
|
1
|
-
require "rails/observers/activerecord/active_record"
|
2
|
-
require "rails/observers/action_controller/caching"
|
3
|
-
|
4
1
|
module Audited
|
5
|
-
class Sweeper
|
6
|
-
|
2
|
+
class Sweeper
|
3
|
+
STORED_DATA = {
|
4
|
+
current_remote_address: :remote_ip,
|
5
|
+
current_request_uuid: :request_uuid,
|
6
|
+
current_user: :current_user
|
7
|
+
}
|
8
|
+
|
9
|
+
delegate :store, to: ::Audited
|
7
10
|
|
8
11
|
def around(controller)
|
9
12
|
self.controller = controller
|
13
|
+
STORED_DATA.each { |k,m| store[k] = send(m) }
|
10
14
|
yield
|
11
15
|
ensure
|
12
16
|
self.controller = nil
|
17
|
+
STORED_DATA.keys.each { |k| store.delete(k) }
|
13
18
|
end
|
14
19
|
|
15
|
-
def
|
16
|
-
|
17
|
-
audit.remote_address = controller.try(:request).try(:remote_ip)
|
18
|
-
audit.request_uuid = request_uuid if request_uuid
|
20
|
+
def current_user
|
21
|
+
lambda { controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true) }
|
19
22
|
end
|
20
23
|
|
21
|
-
def
|
22
|
-
controller.
|
24
|
+
def remote_ip
|
25
|
+
controller.try(:request).try(:remote_ip)
|
23
26
|
end
|
24
27
|
|
25
28
|
def request_uuid
|
26
29
|
controller.try(:request).try(:uuid)
|
27
30
|
end
|
28
31
|
|
29
|
-
def add_observer!(klass)
|
30
|
-
super
|
31
|
-
define_callback(klass)
|
32
|
-
end
|
33
|
-
|
34
|
-
def define_callback(klass)
|
35
|
-
observer = self
|
36
|
-
callback_meth = :_notify_audited_sweeper
|
37
|
-
klass.send(:define_method, callback_meth) do
|
38
|
-
observer.update(:before_create, self)
|
39
|
-
end
|
40
|
-
klass.send(:before_create, callback_meth)
|
41
|
-
end
|
42
|
-
|
43
32
|
def controller
|
44
|
-
|
33
|
+
store[:current_controller]
|
45
34
|
end
|
46
35
|
|
47
36
|
def controller=(value)
|
48
|
-
|
37
|
+
store[:current_controller] = value
|
49
38
|
end
|
50
39
|
end
|
51
40
|
end
|
52
41
|
|
53
42
|
ActiveSupport.on_load(:action_controller) do
|
54
43
|
if defined?(ActionController::Base)
|
55
|
-
ActionController::Base.around_action Audited::Sweeper.
|
44
|
+
ActionController::Base.around_action Audited::Sweeper.new
|
56
45
|
end
|
57
46
|
if defined?(ActionController::API)
|
58
|
-
ActionController::API.around_action Audited::Sweeper.
|
47
|
+
ActionController::API.around_action Audited::Sweeper.new
|
59
48
|
end
|
60
49
|
end
|