audited 4.2.0 → 4.4.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.
- checksums.yaml +4 -4
- data/.travis.yml +21 -9
- data/Appraisals +11 -3
- data/CHANGELOG.md +190 -0
- data/Gemfile +1 -1
- data/README.md +76 -45
- data/Rakefile +3 -18
- data/gemfiles/rails40.gemfile +1 -1
- data/gemfiles/rails41.gemfile +1 -1
- data/gemfiles/rails42.gemfile +1 -1
- data/gemfiles/rails50.gemfile +7 -0
- data/gemfiles/rails51.gemfile +7 -0
- data/lib/audited/audit.rb +126 -56
- data/lib/audited/auditor.rb +87 -40
- data/lib/audited/rspec_matchers.rb +5 -1
- data/lib/audited/sweeper.rb +24 -44
- data/lib/audited/version.rb +1 -1
- data/lib/audited-rspec.rb +4 -0
- data/lib/audited.rb +16 -2
- data/lib/generators/audited/install_generator.rb +24 -0
- data/lib/generators/audited/migration.rb +15 -0
- data/lib/generators/audited/migration_helper.rb +9 -0
- data/lib/generators/audited/templates/add_association_to_audits.rb +11 -0
- data/lib/generators/audited/templates/add_comment_to_audits.rb +9 -0
- data/lib/generators/audited/templates/add_remote_address_to_audits.rb +10 -0
- data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +10 -0
- data/lib/generators/audited/templates/install.rb +30 -0
- data/lib/generators/audited/templates/rename_association_to_associated.rb +23 -0
- data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +9 -0
- data/lib/generators/audited/templates/rename_parent_to_association.rb +11 -0
- data/lib/generators/audited/upgrade_generator.rb +59 -0
- data/spec/audited/audit_spec.rb +246 -0
- data/spec/audited/auditor_spec.rb +648 -0
- data/spec/audited/sweeper_spec.rb +108 -0
- data/spec/audited_spec_helpers.rb +6 -22
- data/spec/rails_app/config/environments/test.rb +7 -4
- data/spec/rails_app/config/initializers/secret_token.rb +1 -1
- data/spec/rails_app/config/routes.rb +1 -4
- data/spec/spec_helper.rb +7 -9
- data/spec/support/active_record/models.rb +24 -13
- 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 +37 -12
- data/test/db/version_1.rb +4 -4
- data/test/db/version_2.rb +4 -4
- data/test/db/version_3.rb +4 -4
- data/test/db/version_4.rb +4 -4
- data/test/db/version_5.rb +2 -2
- data/test/db/version_6.rb +2 -2
- data/test/install_generator_test.rb +31 -3
- data/test/upgrade_generator_test.rb +25 -10
- metadata +54 -84
- data/CHANGELOG +0 -34
- data/lib/audited/active_record/version.rb +0 -5
- data/lib/audited/mongo_mapper/version.rb +0 -5
- data/spec/support/mongo_mapper/connection.rb +0 -4
- data/spec/support/mongo_mapper/models.rb +0 -214
data/lib/audited/audit.rb
CHANGED
|
@@ -1,60 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
module Audit
|
|
3
|
-
def self.included(klass)
|
|
4
|
-
klass.extend(ClassMethods)
|
|
5
|
-
klass.setup_audit
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
module ClassMethods
|
|
9
|
-
def setup_audit
|
|
10
|
-
belongs_to :auditable, :polymorphic => true
|
|
11
|
-
belongs_to :user, :polymorphic => true
|
|
12
|
-
belongs_to :associated, :polymorphic => true
|
|
1
|
+
require 'set'
|
|
13
2
|
|
|
14
|
-
|
|
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
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
class YAMLIfTextColumnType
|
|
17
|
+
class << self
|
|
18
|
+
def load(obj)
|
|
19
|
+
if Audited.audit_class.columns_hash["audited_changes"].sql_type == "text"
|
|
20
|
+
ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
|
|
21
|
+
else
|
|
22
|
+
obj
|
|
23
|
+
end
|
|
18
24
|
end
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
def dump(obj)
|
|
27
|
+
if Audited.audit_class.columns_hash["audited_changes"].sql_type == "text"
|
|
28
|
+
ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
|
|
29
|
+
else
|
|
30
|
+
obj
|
|
31
|
+
end
|
|
23
32
|
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Thread.current[:audited_user] = user
|
|
30
|
-
yield
|
|
31
|
-
ensure
|
|
32
|
-
Thread.current[:audited_user] = nil
|
|
33
|
-
end
|
|
36
|
+
class Audit < ::ActiveRecord::Base
|
|
37
|
+
belongs_to :auditable, polymorphic: true
|
|
38
|
+
belongs_to :user, polymorphic: true
|
|
39
|
+
belongs_to :associated, polymorphic: true
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
def reconstruct_attributes(audits)
|
|
37
|
-
attributes = {}
|
|
38
|
-
result = audits.collect do |audit|
|
|
39
|
-
attributes.merge!(audit.new_attributes).merge!(:version => audit.version)
|
|
40
|
-
yield attributes if block_given?
|
|
41
|
-
end
|
|
42
|
-
block_given? ? result : attributes
|
|
43
|
-
end
|
|
41
|
+
before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
attributes.each do |attr, val|
|
|
48
|
-
record = record.dup if record.frozen?
|
|
43
|
+
cattr_accessor :audited_class_names
|
|
44
|
+
self.audited_class_names = Set.new
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
serialize :audited_changes, YAMLIfTextColumnType
|
|
47
|
+
|
|
48
|
+
scope :ascending, ->{ reorder(version: :asc) }
|
|
49
|
+
scope :descending, ->{ reorder(version: :desc)}
|
|
50
|
+
scope :creates, ->{ where(action: 'create')}
|
|
51
|
+
scope :updates, ->{ where(action: 'update')}
|
|
52
|
+
scope :destroys, ->{ where(action: 'destroy')}
|
|
53
|
+
|
|
54
|
+
scope :up_until, ->(date_or_time){ where("created_at <= ?", date_or_time) }
|
|
55
|
+
scope :from_version, ->(version){ where('version >= ?', version) }
|
|
56
|
+
scope :to_version, ->(version){ where('version <= ?', version) }
|
|
57
|
+
scope :auditable_finder, ->(auditable_id, auditable_type){ where(auditable_id: auditable_id, auditable_type: auditable_type)}
|
|
58
|
+
# Return all audits older than the current one.
|
|
59
|
+
def ancestors
|
|
60
|
+
self.class.ascending.auditable_finder(auditable_id, auditable_type).to_version(version)
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
# Return an instance of what the object looked like at this revision. If
|
|
@@ -62,13 +65,13 @@ module Audited
|
|
|
62
65
|
def revision
|
|
63
66
|
clazz = auditable_type.constantize
|
|
64
67
|
(clazz.find_by_id(auditable_id) || clazz.new).tap do |m|
|
|
65
|
-
self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(
|
|
68
|
+
self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(version: version))
|
|
66
69
|
end
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
# Returns a hash of the changed attributes with the new values
|
|
70
73
|
def new_attributes
|
|
71
|
-
(audited_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
|
|
74
|
+
(audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
|
|
72
75
|
attrs[attr] = values.is_a?(Array) ? values.last : values
|
|
73
76
|
attrs
|
|
74
77
|
end
|
|
@@ -76,29 +79,96 @@ module Audited
|
|
|
76
79
|
|
|
77
80
|
# Returns a hash of the changed attributes with the old values
|
|
78
81
|
def old_attributes
|
|
79
|
-
(audited_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
|
|
82
|
+
(audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
|
|
80
83
|
attrs[attr] = Array(values).first
|
|
81
84
|
|
|
82
85
|
attrs
|
|
83
86
|
end
|
|
84
87
|
end
|
|
85
88
|
|
|
89
|
+
# Allows user to be set to either a string or an ActiveRecord object
|
|
90
|
+
# @private
|
|
91
|
+
def user_as_string=(user)
|
|
92
|
+
# reset both either way
|
|
93
|
+
self.user_as_model = self.username = nil
|
|
94
|
+
user.is_a?(::ActiveRecord::Base) ?
|
|
95
|
+
self.user_as_model = user :
|
|
96
|
+
self.username = user
|
|
97
|
+
end
|
|
98
|
+
alias_method :user_as_model=, :user=
|
|
99
|
+
alias_method :user=, :user_as_string=
|
|
100
|
+
|
|
101
|
+
# @private
|
|
102
|
+
def user_as_string
|
|
103
|
+
user_as_model || username
|
|
104
|
+
end
|
|
105
|
+
alias_method :user_as_model, :user
|
|
106
|
+
alias_method :user, :user_as_string
|
|
107
|
+
|
|
108
|
+
# Returns the list of classes that are being audited
|
|
109
|
+
def self.audited_classes
|
|
110
|
+
audited_class_names.map(&:constantize)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# All audits made during the block called will be recorded as made
|
|
114
|
+
# by +user+. This method is hopefully threadsafe, making it ideal
|
|
115
|
+
# for background operations that require audit information.
|
|
116
|
+
def self.as_user(user, &block)
|
|
117
|
+
::Audited.store[:audited_user] = user
|
|
118
|
+
yield
|
|
119
|
+
ensure
|
|
120
|
+
::Audited.store[:audited_user] = nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @private
|
|
124
|
+
def self.reconstruct_attributes(audits)
|
|
125
|
+
attributes = {}
|
|
126
|
+
result = audits.collect do |audit|
|
|
127
|
+
attributes.merge!(audit.new_attributes)[:version] = audit.version
|
|
128
|
+
yield attributes if block_given?
|
|
129
|
+
end
|
|
130
|
+
block_given? ? result : attributes
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @private
|
|
134
|
+
def self.assign_revision_attributes(record, attributes)
|
|
135
|
+
attributes.each do |attr, val|
|
|
136
|
+
record = record.dup if record.frozen?
|
|
137
|
+
|
|
138
|
+
if record.respond_to?("#{attr}=")
|
|
139
|
+
record.attributes.key?(attr.to_s) ?
|
|
140
|
+
record[attr] = val :
|
|
141
|
+
record.send("#{attr}=", val)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
record
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# use created_at as timestamp cache key
|
|
148
|
+
def self.collection_cache_key(collection = all, timestamp_column = :created_at)
|
|
149
|
+
super(collection, :created_at)
|
|
150
|
+
end
|
|
151
|
+
|
|
86
152
|
private
|
|
153
|
+
|
|
87
154
|
def set_version_number
|
|
88
|
-
max = self.class.
|
|
89
|
-
:auditable_id => auditable_id,
|
|
90
|
-
:auditable_type => auditable_type
|
|
91
|
-
).order(:version.desc).first.try(:version) || 0
|
|
155
|
+
max = self.class.auditable_finder(auditable_id, auditable_type).descending.first.try(:version) || 0
|
|
92
156
|
self.version = max + 1
|
|
93
157
|
end
|
|
94
158
|
|
|
95
159
|
def set_audit_user
|
|
96
|
-
self.user
|
|
160
|
+
self.user ||= ::Audited.store[:audited_user] # from .as_user
|
|
161
|
+
self.user ||= ::Audited.store[:current_user] # from Sweeper
|
|
97
162
|
nil # prevent stopping callback chains
|
|
98
163
|
end
|
|
99
164
|
|
|
100
165
|
def set_request_uuid
|
|
166
|
+
self.request_uuid ||= ::Audited.store[:current_request_uuid]
|
|
101
167
|
self.request_uuid ||= SecureRandom.uuid
|
|
102
168
|
end
|
|
169
|
+
|
|
170
|
+
def set_remote_address
|
|
171
|
+
self.remote_address ||= ::Audited.store[:current_remote_address]
|
|
172
|
+
end
|
|
103
173
|
end
|
|
104
174
|
end
|
data/lib/audited/auditor.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Audited
|
|
|
9
9
|
# To store an audit comment set model.audit_comment to your comment before
|
|
10
10
|
# a create, update or destroy operation.
|
|
11
11
|
#
|
|
12
|
-
# See <tt>Audited::
|
|
12
|
+
# See <tt>Audited::Auditor::ClassMethods#audited</tt>
|
|
13
13
|
# for configuration options
|
|
14
14
|
module Auditor #:nodoc:
|
|
15
15
|
extend ActiveSupport::Concern
|
|
@@ -28,7 +28,7 @@ module Audited
|
|
|
28
28
|
# You can add to those by passing one or an array of fields to skip.
|
|
29
29
|
#
|
|
30
30
|
# class User < ActiveRecord::Base
|
|
31
|
-
# audited :
|
|
31
|
+
# audited except: :password
|
|
32
32
|
# end
|
|
33
33
|
#
|
|
34
34
|
# * +require_comment+ - Ensures that audit_comment is supplied before
|
|
@@ -36,41 +36,35 @@ module Audited
|
|
|
36
36
|
#
|
|
37
37
|
def audited(options = {})
|
|
38
38
|
# don't allow multiple calls
|
|
39
|
-
return if
|
|
39
|
+
return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
|
|
40
40
|
|
|
41
|
-
class_attribute :
|
|
42
|
-
class_attribute :
|
|
43
|
-
class_attribute :audit_associated_with, :instance_writer => false
|
|
41
|
+
class_attribute :audit_associated_with, instance_writer: false
|
|
42
|
+
class_attribute :audited_options, instance_writer: false
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
except = self.column_names - options[:only].flatten.map(&:to_s)
|
|
47
|
-
else
|
|
48
|
-
except = default_ignored_attributes + Audited.ignored_attributes
|
|
49
|
-
except |= Array(options[:except]).collect(&:to_s) if options[:except]
|
|
50
|
-
end
|
|
51
|
-
self.non_audited_columns = except
|
|
44
|
+
self.audited_options = options
|
|
52
45
|
self.audit_associated_with = options[:associated_with]
|
|
53
46
|
|
|
54
47
|
if options[:comment_required]
|
|
55
|
-
validates_presence_of :audit_comment, :
|
|
48
|
+
validates_presence_of :audit_comment, if: :auditing_enabled
|
|
56
49
|
before_destroy :require_comment
|
|
57
50
|
end
|
|
58
51
|
|
|
59
52
|
attr_accessor :audit_comment
|
|
60
53
|
|
|
61
|
-
has_many :audits, :
|
|
62
|
-
Audited.audit_class.audited_class_names <<
|
|
54
|
+
has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name
|
|
55
|
+
Audited.audit_class.audited_class_names << to_s
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
on = Array(options[:on])
|
|
58
|
+
after_create :audit_create if on.empty? || on.include?(:create)
|
|
59
|
+
before_update :audit_update if on.empty? || on.include?(:update)
|
|
60
|
+
before_destroy :audit_destroy if on.empty? || on.include?(:destroy)
|
|
67
61
|
|
|
68
62
|
# Define and set after_audit and around_audit callbacks. This might be useful if you want
|
|
69
63
|
# to notify a party after the audit has been created or if you want to access the newly-created
|
|
70
64
|
# audit.
|
|
71
65
|
define_callbacks :audit
|
|
72
|
-
set_callback :audit, :after, :after_audit, :
|
|
73
|
-
set_callback :audit, :around, :around_audit, :
|
|
66
|
+
set_callback :audit, :after, :after_audit, if: lambda { respond_to?(:after_audit, true) }
|
|
67
|
+
set_callback :audit, :around, :around_audit, if: lambda { respond_to?(:around_audit, true) }
|
|
74
68
|
|
|
75
69
|
attr_accessor :version
|
|
76
70
|
|
|
@@ -81,7 +75,11 @@ module Audited
|
|
|
81
75
|
end
|
|
82
76
|
|
|
83
77
|
def has_associated_audits
|
|
84
|
-
has_many :associated_audits, :
|
|
78
|
+
has_many :associated_audits, as: :associated, class_name: Audited.audit_class.name
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def default_ignored_attributes
|
|
82
|
+
[primary_key, inheritance_column]
|
|
85
83
|
end
|
|
86
84
|
end
|
|
87
85
|
|
|
@@ -134,14 +132,22 @@ module Audited
|
|
|
134
132
|
attributes.except(*non_audited_columns)
|
|
135
133
|
end
|
|
136
134
|
|
|
135
|
+
def non_audited_columns
|
|
136
|
+
self.class.non_audited_columns
|
|
137
|
+
end
|
|
138
|
+
|
|
137
139
|
protected
|
|
138
140
|
|
|
141
|
+
def non_audited_columns
|
|
142
|
+
self.class.non_audited_columns
|
|
143
|
+
end
|
|
144
|
+
|
|
139
145
|
def revision_with(attributes)
|
|
140
|
-
|
|
146
|
+
dup.tap do |revision|
|
|
141
147
|
revision.id = id
|
|
142
148
|
revision.send :instance_variable_set, '@attributes', self.attributes if rails_below?('4.2.0')
|
|
143
|
-
revision.send :instance_variable_set, '@new_record',
|
|
144
|
-
revision.send :instance_variable_set, '@persisted', !
|
|
149
|
+
revision.send :instance_variable_set, '@new_record', destroyed?
|
|
150
|
+
revision.send :instance_variable_set, '@persisted', !destroyed?
|
|
145
151
|
revision.send :instance_variable_set, '@readonly', false
|
|
146
152
|
revision.send :instance_variable_set, '@destroyed', false
|
|
147
153
|
revision.send :instance_variable_set, '@_destroyed', false
|
|
@@ -153,9 +159,9 @@ module Audited
|
|
|
153
159
|
# to determine if an instance variable is a proxy object is to
|
|
154
160
|
# see if it responds to certain methods, as it forwards almost
|
|
155
161
|
# everything to its target.
|
|
156
|
-
|
|
162
|
+
revision.instance_variables.each do |ivar|
|
|
157
163
|
proxy = revision.instance_variable_get ivar
|
|
158
|
-
if !proxy.nil?
|
|
164
|
+
if !proxy.nil? && proxy.respond_to?(:proxy_respond_to?)
|
|
159
165
|
revision.instance_variable_set ivar, nil
|
|
160
166
|
end
|
|
161
167
|
end
|
|
@@ -169,7 +175,15 @@ module Audited
|
|
|
169
175
|
private
|
|
170
176
|
|
|
171
177
|
def audited_changes
|
|
172
|
-
|
|
178
|
+
collection =
|
|
179
|
+
if audited_options[:only]
|
|
180
|
+
audited_columns = self.class.audited_columns.map(&:name)
|
|
181
|
+
changed_attributes.slice(*audited_columns)
|
|
182
|
+
else
|
|
183
|
+
changed_attributes.except(*non_audited_columns)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
collection.inject({}) do |changes, (attr, old_value)|
|
|
173
187
|
changes[attr] = [old_value, self[attr]]
|
|
174
188
|
changes
|
|
175
189
|
end
|
|
@@ -188,32 +202,33 @@ module Audited
|
|
|
188
202
|
end
|
|
189
203
|
|
|
190
204
|
def audit_create
|
|
191
|
-
write_audit(:
|
|
192
|
-
:
|
|
205
|
+
write_audit(action: 'create', audited_changes: audited_attributes,
|
|
206
|
+
comment: audit_comment)
|
|
193
207
|
end
|
|
194
208
|
|
|
195
209
|
def audit_update
|
|
196
210
|
unless (changes = audited_changes).empty? && audit_comment.blank?
|
|
197
|
-
write_audit(:
|
|
198
|
-
:
|
|
211
|
+
write_audit(action: 'update', audited_changes: changes,
|
|
212
|
+
comment: audit_comment)
|
|
199
213
|
end
|
|
200
214
|
end
|
|
201
215
|
|
|
202
216
|
def audit_destroy
|
|
203
|
-
write_audit(:
|
|
204
|
-
:
|
|
217
|
+
write_audit(action: 'destroy', audited_changes: audited_attributes,
|
|
218
|
+
comment: audit_comment) unless new_record?
|
|
205
219
|
end
|
|
206
220
|
|
|
207
221
|
def write_audit(attrs)
|
|
208
|
-
attrs[:associated] =
|
|
222
|
+
attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
|
|
209
223
|
self.audit_comment = nil
|
|
210
|
-
run_callbacks(:audit) {
|
|
224
|
+
run_callbacks(:audit) { audits.create(attrs) } if auditing_enabled
|
|
211
225
|
end
|
|
212
226
|
|
|
213
227
|
def require_comment
|
|
214
228
|
if auditing_enabled && audit_comment.blank?
|
|
215
229
|
errors.add(:audit_comment, "Comment required before destruction")
|
|
216
|
-
return false
|
|
230
|
+
return false if Rails.version.start_with?('4.')
|
|
231
|
+
throw :abort
|
|
217
232
|
end
|
|
218
233
|
end
|
|
219
234
|
|
|
@@ -224,12 +239,36 @@ module Audited
|
|
|
224
239
|
def empty_callback #:nodoc:
|
|
225
240
|
end
|
|
226
241
|
|
|
242
|
+
def auditing_enabled
|
|
243
|
+
self.class.auditing_enabled
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def auditing_enabled=(val)
|
|
247
|
+
self.class.auditing_enabled = val
|
|
248
|
+
end
|
|
227
249
|
end # InstanceMethods
|
|
228
250
|
|
|
229
251
|
module AuditedClassMethods
|
|
230
252
|
# Returns an array of columns that are audited. See non_audited_columns
|
|
231
253
|
def audited_columns
|
|
232
|
-
|
|
254
|
+
columns.select {|c| !non_audited_columns.include?(c.name) }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def non_audited_columns
|
|
258
|
+
@non_audited_columns ||= begin
|
|
259
|
+
options = audited_options
|
|
260
|
+
if options[:only]
|
|
261
|
+
except = column_names - Array.wrap(options[:only]).flatten.map(&:to_s)
|
|
262
|
+
else
|
|
263
|
+
except = default_ignored_attributes + Audited.ignored_attributes
|
|
264
|
+
except |= Array(options[:except]).collect(&:to_s) if options[:except]
|
|
265
|
+
end
|
|
266
|
+
except
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def non_audited_columns=(columns)
|
|
271
|
+
@non_audited_columns = columns
|
|
233
272
|
end
|
|
234
273
|
|
|
235
274
|
# Executes the block with auditing disabled.
|
|
@@ -258,8 +297,16 @@ module Audited
|
|
|
258
297
|
# made by +user+. This is not model specific, the method is a
|
|
259
298
|
# convenience wrapper around
|
|
260
299
|
# @see Audit#as_user.
|
|
261
|
-
def audit_as(
|
|
262
|
-
Audited.audit_class.as_user(
|
|
300
|
+
def audit_as(user, &block)
|
|
301
|
+
Audited.audit_class.as_user(user, &block)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def auditing_enabled
|
|
305
|
+
Audited.store.fetch("#{table_name}_auditing_enabled", true)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def auditing_enabled=(val)
|
|
309
|
+
Audited.store["#{table_name}_auditing_enabled"] = val
|
|
263
310
|
end
|
|
264
311
|
end
|
|
265
312
|
end
|
|
@@ -76,6 +76,8 @@ module Audited
|
|
|
76
76
|
"Did not expect #{@expectation}"
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
alias_method :failure_message_when_negated, :negative_failure_message
|
|
80
|
+
|
|
79
81
|
def description
|
|
80
82
|
description = "audited"
|
|
81
83
|
description += " associated with #{@options[:associated_with]}" if @options.key?(:associated_with)
|
|
@@ -149,6 +151,8 @@ module Audited
|
|
|
149
151
|
"Expected #{model_class} to not have associated audits"
|
|
150
152
|
end
|
|
151
153
|
|
|
154
|
+
alias_method :failure_message_when_negated, :negative_failure_message
|
|
155
|
+
|
|
152
156
|
def description
|
|
153
157
|
"has associated audits"
|
|
154
158
|
end
|
|
@@ -164,7 +168,7 @@ module Audited
|
|
|
164
168
|
end
|
|
165
169
|
|
|
166
170
|
def association_exists?
|
|
167
|
-
|
|
171
|
+
!reflection.nil? &&
|
|
168
172
|
reflection.macro == :has_many &&
|
|
169
173
|
reflection.options[:class_name] == Audited.audit_class.name
|
|
170
174
|
end
|
data/lib/audited/sweeper.rb
CHANGED
|
@@ -1,69 +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
|
+
}
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
def before(controller)
|
|
10
|
-
self.controller = controller
|
|
11
|
-
true
|
|
12
|
-
end
|
|
9
|
+
delegate :store, to: ::Audited
|
|
13
10
|
|
|
14
|
-
def
|
|
11
|
+
def around(controller)
|
|
12
|
+
self.controller = controller
|
|
13
|
+
STORED_DATA.each { |k,m| store[k] = send(m) }
|
|
14
|
+
yield
|
|
15
|
+
ensure
|
|
15
16
|
self.controller = nil
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def before_create(audit)
|
|
19
|
-
audit.user ||= current_user
|
|
20
|
-
audit.remote_address = controller.try(:request).try(:remote_ip)
|
|
21
|
-
audit.request_uuid = request_uuid if request_uuid
|
|
17
|
+
STORED_DATA.keys.each { |k| store.delete(k) }
|
|
22
18
|
end
|
|
23
19
|
|
|
24
20
|
def current_user
|
|
25
21
|
controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true)
|
|
26
22
|
end
|
|
27
23
|
|
|
28
|
-
def
|
|
29
|
-
controller.try(:request).try(:
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def add_observer!(klass)
|
|
33
|
-
super
|
|
34
|
-
define_callback(klass)
|
|
24
|
+
def remote_ip
|
|
25
|
+
controller.try(:request).try(:remote_ip)
|
|
35
26
|
end
|
|
36
27
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
callback_meth = :"_notify_audited_sweeper"
|
|
40
|
-
klass.send(:define_method, callback_meth) do
|
|
41
|
-
observer.update(:before_create, self)
|
|
42
|
-
end
|
|
43
|
-
klass.send(:before_create, callback_meth)
|
|
28
|
+
def request_uuid
|
|
29
|
+
controller.try(:request).try(:uuid)
|
|
44
30
|
end
|
|
45
31
|
|
|
46
32
|
def controller
|
|
47
|
-
|
|
33
|
+
store[:current_controller]
|
|
48
34
|
end
|
|
49
35
|
|
|
50
36
|
def controller=(value)
|
|
51
|
-
|
|
37
|
+
store[:current_controller] = value
|
|
52
38
|
end
|
|
53
39
|
end
|
|
54
40
|
end
|
|
55
41
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# around_filter collision
|
|
60
|
-
sweeper_class = Class.new(Audited::Sweeper) do
|
|
61
|
-
def self.name
|
|
62
|
-
"#{Audited.audit_class}::Sweeper"
|
|
63
|
-
end
|
|
42
|
+
ActiveSupport.on_load(:action_controller) do
|
|
43
|
+
if defined?(ActionController::Base)
|
|
44
|
+
ActionController::Base.around_action Audited::Sweeper.new
|
|
64
45
|
end
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
around_filter sweeper_class.instance
|
|
46
|
+
if defined?(ActionController::API)
|
|
47
|
+
ActionController::API.around_action Audited::Sweeper.new
|
|
68
48
|
end
|
|
69
49
|
end
|
data/lib/audited/version.rb
CHANGED
data/lib/audited.rb
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
|
-
require '
|
|
2
|
-
|
|
1
|
+
require 'active_record'
|
|
3
2
|
|
|
4
3
|
module Audited
|
|
5
4
|
class << self
|
|
6
5
|
attr_accessor :ignored_attributes, :current_user_method, :audit_class
|
|
7
6
|
|
|
7
|
+
def audit_class
|
|
8
|
+
@audit_class || Audit
|
|
9
|
+
end
|
|
10
|
+
|
|
8
11
|
def store
|
|
9
12
|
Thread.current[:audited_store] ||= {}
|
|
10
13
|
end
|
|
14
|
+
|
|
15
|
+
def config
|
|
16
|
+
yield(self)
|
|
17
|
+
end
|
|
11
18
|
end
|
|
12
19
|
|
|
13
20
|
@ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
|
|
14
21
|
|
|
15
22
|
@current_user_method = :current_user
|
|
16
23
|
end
|
|
24
|
+
|
|
25
|
+
require 'audited/auditor'
|
|
26
|
+
require 'audited/audit'
|
|
27
|
+
|
|
28
|
+
::ActiveRecord::Base.send :include, Audited::Auditor
|
|
29
|
+
|
|
30
|
+
require 'audited/sweeper'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'rails/generators'
|
|
2
|
+
require 'rails/generators/migration'
|
|
3
|
+
require 'active_record'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
|
+
require 'generators/audited/migration'
|
|
6
|
+
require 'generators/audited/migration_helper'
|
|
7
|
+
|
|
8
|
+
module Audited
|
|
9
|
+
module Generators
|
|
10
|
+
class InstallGenerator < Rails::Generators::Base
|
|
11
|
+
include Rails::Generators::Migration
|
|
12
|
+
include Audited::Generators::MigrationHelper
|
|
13
|
+
extend Audited::Generators::Migration
|
|
14
|
+
|
|
15
|
+
class_option :audited_changes_column_type, type: :string, default: "text", required: false
|
|
16
|
+
|
|
17
|
+
source_root File.expand_path("../templates", __FILE__)
|
|
18
|
+
|
|
19
|
+
def copy_migration
|
|
20
|
+
migration_template 'install.rb', 'db/migrate/install_audited.rb'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Audited
|
|
2
|
+
module Generators
|
|
3
|
+
module Migration
|
|
4
|
+
# Implement the required interface for Rails::Generators::Migration.
|
|
5
|
+
def next_migration_number(dirname) #:nodoc:
|
|
6
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
7
|
+
if ::ActiveRecord::Base.timestamped_migrations
|
|
8
|
+
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
|
9
|
+
else
|
|
10
|
+
"%.3d" % next_migration_number
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|