auditable 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +7 -2
- data/Gemfile +9 -0
- data/auditable.gemspec +7 -3
- data/lib/auditable/audit.rb +3 -77
- data/lib/auditable/auditing.rb +234 -24
- data/lib/auditable/base.rb +98 -0
- data/lib/auditable/version.rb +1 -1
- data/lib/generators/auditable/migration_generator.rb +1 -1
- data/lib/generators/auditable/templates/migration.rb +2 -0
- data/lib/generators/auditable/templates/update.rb +7 -0
- data/lib/generators/auditable/update_generator.rb +27 -0
- data/spec/lib/auditable_spec.rb +11 -3
- data/spec/lib/callbacks_spec.rb +72 -0
- data/spec/lib/changed_by_spec.rb +39 -0
- data/spec/lib/snap_spec.rb +15 -0
- data/spec/lib/version_spec.rb +47 -0
- data/spec/support/models.rb +48 -1
- data/spec/support/schema.rb +5 -0
- metadata +16 -5
data/.travis.yml
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
- 1.8.7
|
4
|
-
- 1.9.2
|
5
3
|
- 1.9.3
|
4
|
+
- jruby-19mode
|
6
5
|
- ruby-head
|
6
|
+
env:
|
7
|
+
- "RAILS_VERSION=3.2.0"
|
8
|
+
- "RAILS_VERSION=4.0.0"
|
9
|
+
matrix:
|
10
|
+
allow_failures:
|
11
|
+
- rvm: ruby-head
|
7
12
|
# uncomment this line if your project needs to run something other than `rake`:
|
8
13
|
script: bundle exec rspec spec
|
data/Gemfile
CHANGED
@@ -2,3 +2,12 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in auditable.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
if rails_version = ENV["RAILS_VERSION"] || '4.0.0'
|
7
|
+
gem 'activerecord', "~> #{rails_version}"
|
8
|
+
gem 'activesupport', "~> #{rails_version}"
|
9
|
+
|
10
|
+
if rails_version == "4.0.0"
|
11
|
+
gem 'activerecord-jdbc-adapter', github: 'jruby/activerecord-jdbc-adapter', platform: :jruby
|
12
|
+
end
|
13
|
+
end
|
data/auditable.gemspec
CHANGED
@@ -18,11 +18,15 @@ Gem::Specification.new do |gem|
|
|
18
18
|
gem.add_development_dependency 'rake'
|
19
19
|
gem.add_development_dependency 'rspec', '>= 2'
|
20
20
|
gem.add_development_dependency 'watchr'
|
21
|
-
|
21
|
+
if RUBY_PLATFORM == 'java'
|
22
|
+
gem.add_development_dependency 'activerecord-jdbcsqlite3-adapter'
|
23
|
+
else
|
24
|
+
gem.add_development_dependency 'sqlite3'
|
25
|
+
end
|
22
26
|
gem.add_development_dependency 'timecop'
|
23
|
-
#
|
27
|
+
# documentation stuff
|
24
28
|
gem.add_development_dependency 'yard'
|
25
|
-
gem.add_development_dependency '
|
29
|
+
gem.add_development_dependency 'kramdown'
|
26
30
|
|
27
31
|
# debugger. only included if one sets DEBUGGER env variable
|
28
32
|
if ENV['DEBUGGER']
|
data/lib/auditable/audit.rb
CHANGED
@@ -1,81 +1,7 @@
|
|
1
|
-
|
2
|
-
class Audit < ActiveRecord::Base
|
3
|
-
belongs_to :auditable, :polymorphic => true
|
4
|
-
belongs_to :user, :polymorphic => true
|
5
|
-
serialize :modifications
|
6
|
-
|
7
|
-
attr_accessible :action, :modifications
|
8
|
-
|
9
|
-
# Diffing two audits' modifications
|
10
|
-
#
|
11
|
-
# Returns a hash containing arrays of the form
|
12
|
-
# {
|
13
|
-
# :key_1 => [<value_in_other_audit>, <value_in_this_audit>],
|
14
|
-
# :key_2 => [<value_in_other_audit>, <value_in_this_audit>],
|
15
|
-
# :other_audit_own_key => [<value_in_other_audit>, nil],
|
16
|
-
# :this_audio_own_key => [nil, <value_in_this_audit>]
|
17
|
-
# }
|
18
|
-
def diff(other_audit)
|
19
|
-
other_modifications = other_audit ? other_audit.modifications : {}
|
20
|
-
|
21
|
-
{}.tap do |d|
|
22
|
-
# find keys present only in this audit
|
23
|
-
(self.modifications.keys - other_modifications.keys).each do |k|
|
24
|
-
d[k] = [nil, self.modifications[k]] if self.modifications[k]
|
25
|
-
end
|
26
|
-
|
27
|
-
# find keys present only in other audit
|
28
|
-
(other_modifications.keys - self.modifications.keys).each do |k|
|
29
|
-
d[k] = [other_modifications[k], nil] if other_modifications[k]
|
30
|
-
end
|
1
|
+
require 'auditable/base'
|
31
2
|
|
32
|
-
|
33
|
-
|
34
|
-
if self.modifications[k] != other_modifications[k]
|
35
|
-
d[k] = [other_modifications[k], self.modifications[k]]
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
# Diff this audit with the one created immediately before it
|
42
|
-
#
|
43
|
-
# See #diff for more details
|
44
|
-
def latest_diff(options = {})
|
45
|
-
if options.present?
|
46
|
-
scoped = auditable.audits.order("id DESC")
|
47
|
-
if tag = options.delete(:tag)
|
48
|
-
scoped = scoped.where(:tag => tag)
|
49
|
-
end
|
50
|
-
if changed_by = options.delete(:changed_by)
|
51
|
-
scoped = scoped.where(:user_id => changed_by.id, :user_type => changed_by.class.name)
|
52
|
-
end
|
53
|
-
if audit_tag = options.delete(:audit_tag)
|
54
|
-
scoped = scoped.where(:tag => audit_tag)
|
55
|
-
end
|
56
|
-
diff scoped.first
|
57
|
-
else
|
58
|
-
diff_since(created_at)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
# Diff this audit with the latest audit created before the `time` variable passed
|
63
|
-
def diff_since(time)
|
64
|
-
other_audit = auditable.audits.where("created_at <= ? AND id != ?", time, id).order("id DESC").limit(1).first
|
65
|
-
diff(other_audit)
|
66
|
-
end
|
67
|
-
|
68
|
-
# Returns user object
|
69
|
-
#
|
70
|
-
# Use same method name like in update_attributes:
|
71
|
-
alias_attribute :changed_by, :user
|
72
|
-
|
73
|
-
def same_audited_content?(other_audit)
|
74
|
-
other_audit and relevant_attributes == other_audit.relevant_attributes
|
75
|
-
end
|
3
|
+
module Auditable
|
4
|
+
class Audit < Base
|
76
5
|
|
77
|
-
def relevant_attributes
|
78
|
-
attributes.slice("modifications", "tag", "action", "user").reject {|k,v| v.blank? }
|
79
|
-
end
|
80
6
|
end
|
81
7
|
end
|
data/lib/auditable/auditing.rb
CHANGED
@@ -3,16 +3,109 @@ module Auditable
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
module ClassMethods
|
6
|
-
attr_writer :audited_attributes
|
7
6
|
|
8
7
|
# Get the list of methods to track over record saves, including those inherited from parent
|
9
8
|
def audited_attributes
|
10
|
-
|
11
|
-
|
12
|
-
if superclass != ActiveRecord::Base and superclass.respond_to?(:audited_attributes)
|
13
|
-
attrs.push(*superclass.audited_attributes)
|
9
|
+
audited_cache('attributes') do |parent_class, attrs|
|
10
|
+
(attrs || []).push(*parent_class.audited_attributes)
|
14
11
|
end
|
15
|
-
|
12
|
+
end
|
13
|
+
|
14
|
+
def audited_attributes=(attributes)
|
15
|
+
set_audited_cache( 'attributes', attributes ) do |parent_class, attrs|
|
16
|
+
attrs.push(*parent_class.audited_attributes)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def audited_version
|
21
|
+
audited_cache('version')
|
22
|
+
end
|
23
|
+
|
24
|
+
def audited_version=(version)
|
25
|
+
set_audited_cache( 'version', version )
|
26
|
+
end
|
27
|
+
|
28
|
+
def audited_after_create
|
29
|
+
audited_cache('after_create')
|
30
|
+
end
|
31
|
+
|
32
|
+
def audited_after_create=(after_create)
|
33
|
+
set_audited_cache('after_create', after_create) do |parent_class, callback|
|
34
|
+
|
35
|
+
# Disable the inherited audit create callback
|
36
|
+
skip_callback(:create, :after, parent_class.audited_after_create)
|
37
|
+
|
38
|
+
callback
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def audited_after_update
|
43
|
+
audited_cache('after_update')
|
44
|
+
end
|
45
|
+
|
46
|
+
def audited_after_update=(after_update)
|
47
|
+
set_audited_cache('after_update', after_update) do |parent_class, callback|
|
48
|
+
|
49
|
+
# Disable the inherited audit create callback
|
50
|
+
skip_callback(:update, :after, parent_class.audited_after_update)
|
51
|
+
|
52
|
+
callback
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Set the configuration of Auditable. Optional block to access the parent class configuration setting.
|
57
|
+
def set_audited_cache(key,val,&blk)
|
58
|
+
|
59
|
+
if superclass != ActiveRecord::Base && superclass.respond_to?(:audited_cache)
|
60
|
+
if block_given?
|
61
|
+
begin
|
62
|
+
val = yield( superclass, val )
|
63
|
+
rescue
|
64
|
+
raise "Failed to create audit for #{self.name} accessing parent #{superclass.name} - #{$!}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# init the cache, since child classes may not declare audit
|
70
|
+
@audited_cache ||= {}.with_indifferent_access
|
71
|
+
@audited_cache[key] = val
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get the configuration of Auditable. Check the parent class for the configuration if it does not exist in the
|
75
|
+
# implementing class.
|
76
|
+
def audited_cache( key, &blk )
|
77
|
+
|
78
|
+
# init the cache, since child classes may not declare audit
|
79
|
+
@audited_cache ||= {}.with_indifferent_access
|
80
|
+
topic = @audited_cache[key]
|
81
|
+
|
82
|
+
# Check the parent for a val
|
83
|
+
if topic.nil? && superclass != ActiveRecord::Base && superclass.respond_to?(:audited_cache)
|
84
|
+
begin
|
85
|
+
if block_given?
|
86
|
+
topic = yield( superclass, topic )
|
87
|
+
else
|
88
|
+
topic = superclass.audited_cache( key )
|
89
|
+
end
|
90
|
+
rescue
|
91
|
+
raise "Failed to create audit for #{self.name} accessing parent #{superclass.name} - #{$!}"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Set cache explicitly to false if the result was nil
|
95
|
+
if topic.nil?
|
96
|
+
topic = @audited_cache[key] = false
|
97
|
+
|
98
|
+
# Coerce to symbol if a string
|
99
|
+
elsif topic.is_a? String
|
100
|
+
topic = @audited_cache[key] = topic.to_sym
|
101
|
+
|
102
|
+
# Otherwise set the cache straight up
|
103
|
+
else
|
104
|
+
@audited_cache[key] = topic
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
topic
|
16
109
|
end
|
17
110
|
|
18
111
|
# Set the list of methods to track over record saves
|
@@ -23,12 +116,50 @@ module Auditable
|
|
23
116
|
# audit :page_count, :question_ids
|
24
117
|
# end
|
25
118
|
def audit(*args)
|
119
|
+
|
26
120
|
options = args.extract_options!
|
121
|
+
|
122
|
+
# Setup callbacks
|
123
|
+
callback = options.delete(:after_create)
|
124
|
+
self.audited_after_create = callback if callback
|
125
|
+
callback = options.delete(:after_update)
|
126
|
+
self.audited_after_update = callback if callback
|
127
|
+
|
128
|
+
# setup changed_by
|
129
|
+
changed_by = options.delete(:changed_by)
|
130
|
+
|
131
|
+
if changed_by.is_a?(String) || changed_by.is_a?(Symbol) || changed_by.respond_to?(:call)
|
132
|
+
set_audited_cache('changed_by', changed_by)
|
133
|
+
|
134
|
+
# If inherited from parent's changed_by, do nothing
|
135
|
+
elsif audited_cache('changed_by')
|
136
|
+
# noop
|
137
|
+
|
138
|
+
# Otherwise create the default changed_by methods and set configuration in cache.
|
139
|
+
else
|
140
|
+
set_audited_cache('changed_by', :changed_by )
|
141
|
+
define_method(:changed_by) { @changed_by }
|
142
|
+
define_method(:changed_by=) { |change| @changed_by = change }
|
143
|
+
end
|
144
|
+
|
27
145
|
options[:class_name] ||= "Auditable::Audit"
|
28
146
|
options[:as] = :auditable
|
147
|
+
|
148
|
+
self.audited_version = options.delete(:version)
|
149
|
+
|
29
150
|
has_many :audits, options
|
30
|
-
|
31
|
-
|
151
|
+
|
152
|
+
if self.audited_after_create
|
153
|
+
after_create self.audited_after_create
|
154
|
+
else
|
155
|
+
after_create :audit_create_callback
|
156
|
+
end
|
157
|
+
|
158
|
+
if self.audited_after_update
|
159
|
+
after_update self.audited_after_update
|
160
|
+
else
|
161
|
+
after_update :audit_update_callback
|
162
|
+
end
|
32
163
|
|
33
164
|
self.audited_attributes = Array.wrap args
|
34
165
|
end
|
@@ -36,53 +167,120 @@ module Auditable
|
|
36
167
|
|
37
168
|
# INSTANCE METHODS
|
38
169
|
|
39
|
-
attr_accessor :
|
170
|
+
attr_accessor :audit_action, :audit_tag
|
171
|
+
|
172
|
+
def audit_changed_by
|
173
|
+
changed_by_call = self.class.audited_cache('changed_by')
|
174
|
+
|
175
|
+
if changed_by_call.respond_to? :call
|
176
|
+
changed_by_call.call(self)
|
177
|
+
else
|
178
|
+
self.send(changed_by_call)
|
179
|
+
end
|
180
|
+
end
|
40
181
|
|
41
182
|
# Get the latest audit record
|
42
183
|
def last_audit
|
43
|
-
|
184
|
+
# if version is enabled, use the version
|
185
|
+
if self.class.audited_version
|
186
|
+
audits.order('version DESC').first
|
187
|
+
|
188
|
+
# other pull last inserted
|
189
|
+
else
|
190
|
+
audits.last
|
191
|
+
end
|
44
192
|
end
|
45
193
|
|
46
194
|
# Mark the latest record with a tag in order to easily find and perform diff against later
|
47
195
|
# If there are no audits for this record, create a new audit with this tag
|
48
196
|
def audit_tag_with(tag)
|
49
|
-
if last_audit
|
50
|
-
|
197
|
+
if audit = last_audit
|
198
|
+
audit.update_attribute(:tag, tag)
|
199
|
+
|
200
|
+
# Force the trigger of a reload if audited_version is used. Happens automatically otherwise
|
201
|
+
audits.reload if self.class.audited_version
|
51
202
|
else
|
52
203
|
self.audit_tag = tag
|
53
204
|
snap!
|
54
205
|
end
|
55
206
|
end
|
56
207
|
|
208
|
+
# Take a snapshot of the current state of the audited record's audited attributes
|
209
|
+
def snap
|
210
|
+
serialize_attribute = lambda do |attribute|
|
211
|
+
# If a proc, do nothing, cannot be serialized
|
212
|
+
# XXX: raise warning on passing in a proc?
|
213
|
+
if attribute.is_a? Proc
|
214
|
+
# noop
|
215
|
+
|
216
|
+
# Is an ActiveRecord, serialize as hash instead of serializing the object
|
217
|
+
elsif attribute.class.ancestors.include?(ActiveRecord::Base)
|
218
|
+
attribute.serializable_hash
|
219
|
+
|
220
|
+
# If an array, such as from an association, serialize the elements in the array
|
221
|
+
elsif attribute.is_a?(Array) || attribute.is_a?(ActiveRecord::Associations::CollectionProxy)
|
222
|
+
attribute.map { |element| serialize_attribute.call(element) }
|
223
|
+
|
224
|
+
# otherwise, return val
|
225
|
+
else
|
226
|
+
attribute
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
{}.tap do |s|
|
231
|
+
self.class.audited_attributes.each do |attr|
|
232
|
+
val = self.send attr
|
233
|
+
s[attr.to_s] = serialize_attribute.call(val)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
57
238
|
# Take a snapshot of and save the current state of the audited record's audited attributes
|
58
239
|
#
|
59
240
|
# Accept values for :tag, :action and :user in the argument hash. However, these are overridden by the values set by the auditable record's virtual attributes (#audit_tag, #audit_action, #changed_by) if defined
|
60
241
|
def snap!(options = {})
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
242
|
+
data = options.merge(:modifications => self.snap)
|
243
|
+
|
244
|
+
data[:tag] = self.audit_tag if self.audit_tag
|
245
|
+
data[:action] = self.audit_action if self.audit_action
|
246
|
+
data[:changed_by] = self.audit_changed_by if self.audit_changed_by
|
66
247
|
|
67
|
-
|
248
|
+
self.save_audit( data )
|
249
|
+
end
|
250
|
+
|
251
|
+
def save_audit(data)
|
252
|
+
last_saved_audit = last_audit
|
68
253
|
|
69
254
|
# build new audit
|
70
|
-
audit = audits.build(
|
71
|
-
audit.tag = self.audit_tag if audit_tag
|
72
|
-
audit.action = self.audit_action if audit_action
|
73
|
-
audit.changed_by = self.changed_by if changed_by
|
255
|
+
audit = audits.build(data)
|
74
256
|
|
75
257
|
# only save if it's different from before
|
76
258
|
if !audit.same_audited_content?(last_saved_audit)
|
77
|
-
|
259
|
+
# If version is enabled, wrap in a transaction to get the next version number
|
260
|
+
# before saving
|
261
|
+
if self.class.audited_version
|
262
|
+
ActiveRecord::Base.transaction do
|
263
|
+
if self.class.audited_version.is_a? Symbol
|
264
|
+
audit.version = self.send( self.class.audited_version )
|
265
|
+
else
|
266
|
+
audit.version = (audits.maximum('version')||0) + 1
|
267
|
+
end
|
268
|
+
audit.save
|
269
|
+
end
|
270
|
+
|
271
|
+
# Save as usual
|
272
|
+
else
|
273
|
+
audit.save
|
274
|
+
end
|
78
275
|
else
|
79
276
|
audits.delete(audit)
|
80
277
|
end
|
278
|
+
|
81
279
|
end
|
82
280
|
|
83
281
|
# Get the latest changes by comparing the latest two audits
|
84
282
|
def audited_changes(options = {})
|
85
|
-
|
283
|
+
last_audit.try(:latest_diff, options) || {}
|
86
284
|
end
|
87
285
|
|
88
286
|
# Return last attribute's change
|
@@ -101,5 +299,17 @@ module Auditable
|
|
101
299
|
end
|
102
300
|
nil
|
103
301
|
end
|
302
|
+
|
303
|
+
protected
|
304
|
+
|
305
|
+
# Create callback
|
306
|
+
def audit_create_callback
|
307
|
+
self.snap!(:action => "create")
|
308
|
+
end
|
309
|
+
|
310
|
+
# Update callback
|
311
|
+
def audit_update_callback
|
312
|
+
self.snap!(:action => "update")
|
313
|
+
end
|
104
314
|
end
|
105
315
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
|
2
|
+
module Auditable
|
3
|
+
class Base < ActiveRecord::Base
|
4
|
+
self.abstract_class = true
|
5
|
+
belongs_to :auditable, :polymorphic => true
|
6
|
+
belongs_to :user, :polymorphic => true
|
7
|
+
serialize :modifications
|
8
|
+
|
9
|
+
if ActiveRecord::VERSION::STRING < "4.0.0"
|
10
|
+
attr_accessible :action, :modifications, :tag, :changed_by, :version
|
11
|
+
end
|
12
|
+
|
13
|
+
# Diffing two audits' modifications
|
14
|
+
#
|
15
|
+
# Returns a hash containing arrays of the form
|
16
|
+
# {
|
17
|
+
# :key_1 => [<value_in_other_audit>, <value_in_this_audit>],
|
18
|
+
# :key_2 => [<value_in_other_audit>, <value_in_this_audit>],
|
19
|
+
# :other_audit_own_key => [<value_in_other_audit>, nil],
|
20
|
+
# :this_audio_own_key => [nil, <value_in_this_audit>]
|
21
|
+
# }
|
22
|
+
def diff(other_audit)
|
23
|
+
other_modifications = other_audit ? other_audit.modifications : {}
|
24
|
+
|
25
|
+
{}.tap do |d|
|
26
|
+
# find keys present only in this audit
|
27
|
+
(self.modifications.keys - other_modifications.keys).each do |k|
|
28
|
+
d[k] = [nil, self.modifications[k]] if self.modifications[k]
|
29
|
+
end
|
30
|
+
|
31
|
+
# find keys present only in other audit
|
32
|
+
(other_modifications.keys - self.modifications.keys).each do |k|
|
33
|
+
d[k] = [other_modifications[k], nil] if other_modifications[k]
|
34
|
+
end
|
35
|
+
|
36
|
+
# find common keys and diff values
|
37
|
+
self.modifications.keys.each do |k|
|
38
|
+
if self.modifications[k] != other_modifications[k]
|
39
|
+
d[k] = [other_modifications[k], self.modifications[k]]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Diff this audit with the one created immediately before it
|
46
|
+
#
|
47
|
+
# See #diff for more details
|
48
|
+
def latest_diff(options = {})
|
49
|
+
if options.present?
|
50
|
+
scoped = auditable.class.audited_version ? auditable.audits.order("version DESC") : auditable.audits.order("id DESC")
|
51
|
+
if tag = options.delete(:tag)
|
52
|
+
scoped = scoped.where(:tag => tag)
|
53
|
+
end
|
54
|
+
if changed_by = options.delete(:changed_by)
|
55
|
+
scoped = scoped.where(:user_id => changed_by.id, :user_type => changed_by.class.name)
|
56
|
+
end
|
57
|
+
if audit_tag = options.delete(:audit_tag)
|
58
|
+
scoped = scoped.where(:tag => audit_tag)
|
59
|
+
end
|
60
|
+
diff scoped.first
|
61
|
+
else
|
62
|
+
if auditable.class.audited_version
|
63
|
+
diff_since_version(version)
|
64
|
+
else
|
65
|
+
diff_since(created_at)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Diff this audit with the latest audit created before the `time` variable passed
|
72
|
+
def diff_since(time)
|
73
|
+
other_audit = auditable.audits.where("created_at <= ? AND id != ?", time, id).order("id DESC").limit(1).first
|
74
|
+
|
75
|
+
diff(other_audit)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Diff this audit with the latest audit created before this version
|
79
|
+
def diff_since_version(version)
|
80
|
+
other_audit = auditable.audits.where("version <= ? AND id != ?", version, id).order("version DESC").limit(1).first
|
81
|
+
|
82
|
+
diff(other_audit)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns user object
|
86
|
+
#
|
87
|
+
# Use same method name like in update_attributes:
|
88
|
+
alias_attribute :changed_by, :user
|
89
|
+
|
90
|
+
def same_audited_content?(other_audit)
|
91
|
+
other_audit and relevant_attributes == other_audit.relevant_attributes
|
92
|
+
end
|
93
|
+
|
94
|
+
def relevant_attributes
|
95
|
+
attributes.slice("modifications", "tag", "user").reject {|k,v| v.blank? }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/auditable/version.rb
CHANGED
@@ -10,7 +10,7 @@ module Auditable
|
|
10
10
|
@source_root ||= File.join(File.dirname(__FILE__), 'templates')
|
11
11
|
end
|
12
12
|
|
13
|
-
# Rails expects us to override/implement this
|
13
|
+
# Rails expects us to override/implement this our self
|
14
14
|
def self.next_migration_number(dirname)
|
15
15
|
if ActiveRecord::Base.timestamped_migrations
|
16
16
|
Time.new.utc.strftime("%Y%m%d%H%M%S")
|
@@ -6,10 +6,12 @@ class CreateAudits < ActiveRecord::Migration
|
|
6
6
|
t.text :modifications
|
7
7
|
t.string :action
|
8
8
|
t.string :tag
|
9
|
+
t.integer :version
|
9
10
|
t.timestamps
|
10
11
|
end
|
11
12
|
|
12
13
|
add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
|
14
|
+
add_index :audits, [:auditable_id, :auditable_type, :version], :name => 'auditable_version_idx'
|
13
15
|
add_index :audits, [:user_id, :user_type], :name => 'user_index'
|
14
16
|
add_index :audits, :created_at
|
15
17
|
add_index :audits, :action
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module Auditable
|
5
|
+
module Generators
|
6
|
+
class UpdateGenerator < ::Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
def self.source_root
|
10
|
+
@source_root ||= File.join(File.dirname(__FILE__), 'templates')
|
11
|
+
end
|
12
|
+
|
13
|
+
# Rails expects us to override/implement this our self
|
14
|
+
def self.next_migration_number(dirname)
|
15
|
+
if ActiveRecord::Base.timestamped_migrations
|
16
|
+
Time.new.utc.strftime("%Y%m%d%H%M%S")
|
17
|
+
else
|
18
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate_files
|
23
|
+
migration_template 'update.rb', 'db/migrate/update_audits.rb'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/spec/lib/auditable_spec.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe "Model
|
4
|
-
it "should be available
|
3
|
+
describe "Model class methods" do
|
4
|
+
it "#audited_attributes should be available using audit" do
|
5
5
|
Survey.audited_attributes.should include :title
|
6
6
|
end
|
7
|
+
|
8
|
+
it "#audited_version should be available using audit" do
|
9
|
+
Survey.audited_version.should be_true
|
10
|
+
end
|
7
11
|
end
|
8
12
|
|
9
13
|
describe Auditable do
|
@@ -21,6 +25,10 @@ describe Auditable do
|
|
21
25
|
survey.audits.last.action.should == "create"
|
22
26
|
end
|
23
27
|
|
28
|
+
it "should have a version" do
|
29
|
+
survey.audits.last.version.should eql 1
|
30
|
+
end
|
31
|
+
|
24
32
|
it "should work when we have multiple audits created per second (same created_at timestamps)" do
|
25
33
|
require 'timecop'
|
26
34
|
Timecop.freeze do
|
@@ -107,7 +115,7 @@ describe Auditable do
|
|
107
115
|
end
|
108
116
|
|
109
117
|
it "should set audit_action" do
|
110
|
-
survey.update_attributes(:audit_action => "modified")
|
118
|
+
survey.update_attributes(:title => 'new title', :audit_action => "modified")
|
111
119
|
survey.audits.last.action.should == "modified"
|
112
120
|
end
|
113
121
|
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Auditable#callbacks' do
|
4
|
+
|
5
|
+
describe Plant do
|
6
|
+
let(:plant) { Plant.create :name => 'a green shrub' }
|
7
|
+
|
8
|
+
it 'should have a valid audit to start with' do
|
9
|
+
plant.name.should == 'a green shrub'
|
10
|
+
plant.audited_changes.should == {'name' => [nil, 'a green shrub']}
|
11
|
+
plant.audits.last.action.should == 'manual create'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should create a new audit using callback' do
|
15
|
+
plant.should_receive(:manually_update_audit) { plant.save_audit( {'action' => 'dig', :tag => 'tagged!', 'modifications' => { 'name' => 'over ruled!' } } ) }
|
16
|
+
plant.update_attributes :name => 'an orange shrub'
|
17
|
+
plant.audited_changes.should == {'name' => ['a green shrub', 'over ruled!']}
|
18
|
+
plant.audits.last.action.should == 'dig'
|
19
|
+
plant.audits.last.tag.should == 'tagged!'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe Tree do
|
24
|
+
let(:tree) { Tree.create :name => 'a tall pine', tastey: false }
|
25
|
+
|
26
|
+
it 'should inherit callback of the parent' do
|
27
|
+
tree.class.audited_after_create.should eql :manually_create_audit
|
28
|
+
tree.class.audited_after_update.should eql :manually_update_audit
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should have a valid audit to start with, include inherited attributes from Plant' do
|
32
|
+
tree.name.should == 'a tall pine'
|
33
|
+
tree.audits.size.should eql(1)
|
34
|
+
tree.audited_changes.should == {"plants"=>[nil, []], "name"=>[nil, "a tall pine"], "tastey"=>[nil, false]}
|
35
|
+
tree.audits.last.action.should == 'manual create'
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should create a new audit using callback' do
|
39
|
+
tree.should_receive(:manually_update_audit) { tree.save_audit( {'action' => 'dig', :tag => 'tagged!', 'modifications' => { 'name' => 'over ruled!' } } ) }
|
40
|
+
tree.update_attributes :name => 'a small oak'
|
41
|
+
tree.audited_changes.should == {"plants"=>[[], nil], "name"=>["a tall pine", "over ruled!"]}
|
42
|
+
tree.audits.last.action.should == 'dig'
|
43
|
+
tree.audits.last.tag.should == 'tagged!'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe Kale do
|
48
|
+
let(:kale) { Kale.create :name => 'a bunch of leafy kale', tastey: true }
|
49
|
+
|
50
|
+
it 'should inherit the update callback of the parent' do
|
51
|
+
kale.class.audited_after_update.should eql :manually_update_audit
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should override the create callback of the parent' do
|
55
|
+
kale.class.audited_after_create.should eql :audit_create_callback
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should have a valid audit to start with, include inherited attributes from Plant' do
|
59
|
+
kale.name.should == 'a bunch of leafy kale'
|
60
|
+
kale.audits.size.should eql(1)
|
61
|
+
kale.audited_changes.should == {'name'=>[nil, 'a bunch of leafy kale'], 'tastey'=>[nil, true]}
|
62
|
+
kale.audits.last.action.should == 'audit action'
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should create a new audit using callback' do
|
66
|
+
kale.should_receive(:manually_update_audit) { kale.save_audit( {'action' => 'dig', 'modifications' => { 'name' => 'over ruled!' } } ) }
|
67
|
+
kale.update_attributes :name => 'a small oak'
|
68
|
+
kale.audited_changes.should == {'tastey'=>[true, nil], 'name'=>['a bunch of leafy kale', 'over ruled!']}
|
69
|
+
kale.audits.last.action.should == 'dig'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Auditable#changed_By' do
|
4
|
+
|
5
|
+
describe Survey do
|
6
|
+
let(:survey) { Survey.create :title => 'Survey', changed_by: User.create( name: 'Surveyor') }
|
7
|
+
|
8
|
+
it 'should set changed_by using default' do
|
9
|
+
survey.audits.last.changed_by.name.should eql 'Surveyor'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Plant do
|
14
|
+
let(:plant) { Plant.create :name => 'an odd fungus', tastey: false }
|
15
|
+
|
16
|
+
it 'should set changed_by from symbol' do
|
17
|
+
plant.audits.last.changed_by.name.should eql 'Bob'
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
describe Tree do
|
23
|
+
let(:tree) { Tree.create :name => 'a tall pine', tastey: false }
|
24
|
+
|
25
|
+
it 'should set changed_by from symbol inherited from parent' do
|
26
|
+
tree.audits.last.changed_by.name.should eql 'Sue'
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
describe Kale do
|
32
|
+
let(:kale) { Kale.create :name => 'a bunch of leafy kale', tastey: true }
|
33
|
+
|
34
|
+
it 'should set changed_by from proc' do
|
35
|
+
kale.audits.last.changed_by.name.should eql 'bob loves a bunch of leafy kale'
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Auditing#snap" do
|
4
|
+
let(:weed) { Plant.create :name => 'a green shrub' }
|
5
|
+
let(:tree) { Tree.create :name => 'a graspy vine', plants: [weed] }
|
6
|
+
|
7
|
+
it "#snap should serialize model" do
|
8
|
+
tree.snap.should eql({
|
9
|
+
"tastey"=>nil,
|
10
|
+
"plants"=>[{"id"=>weed.id, "name"=>"a green shrub", "plant_id"=>tree.id, "tastey"=>nil}],
|
11
|
+
"name"=>"a graspy vine"
|
12
|
+
})
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Auditable::Audit#version" do
|
4
|
+
describe Document do
|
5
|
+
let(:model) { Document.create(:title => "Test") }
|
6
|
+
|
7
|
+
it "#audited_version should be set to symbol" do
|
8
|
+
Document.audited_version.should eql :latest_version
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should have a version" do
|
12
|
+
model.audits.last.version.should eql 110
|
13
|
+
end
|
14
|
+
|
15
|
+
it "it should call #latest_version on snap" do
|
16
|
+
model.should_receive(:latest_version) { 12345 }
|
17
|
+
model.update_attributes :title => 'Another Test'
|
18
|
+
model.audits.last.version.should eql 12345
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should diff by version" do
|
22
|
+
model.update_attributes :title => 'Manual Version Change'
|
23
|
+
|
24
|
+
model.audited_changes.should eql({"title"=>["Test", "Manual Version Change"]})
|
25
|
+
|
26
|
+
audit = model.audits.first
|
27
|
+
audit.version = 7000
|
28
|
+
audit.save!
|
29
|
+
|
30
|
+
model.audited_changes.should eql({"title"=>["Manual Version Change", "Test"]})
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe Kale do
|
35
|
+
let(:kale) { Kale.create :name => "a bunch of leafy kale", tastey: true }
|
36
|
+
|
37
|
+
it "should inherit version from parent" do
|
38
|
+
kale.audits.last.version.should eql 1
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should increment version on update" do
|
42
|
+
kale.update_attributes :name => 'a single leaf of kale'
|
43
|
+
kale.audits.last.version.should eql 2
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/spec/support/models.rb
CHANGED
@@ -1,17 +1,64 @@
|
|
1
1
|
class Survey < ActiveRecord::Base
|
2
2
|
attr_accessor :current_page
|
3
3
|
|
4
|
-
audit :title, :current_page, :class_name => "MyAudit"
|
4
|
+
audit :title, :current_page, :version => true, :class_name => "MyAudit"
|
5
5
|
end
|
6
6
|
|
7
7
|
class User < ActiveRecord::Base
|
8
8
|
audit :name
|
9
9
|
end
|
10
10
|
|
11
|
+
class Document < ActiveRecord::Base
|
12
|
+
self.table_name = 'surveys'
|
13
|
+
|
14
|
+
audit :title, :version => :latest_version
|
15
|
+
|
16
|
+
def latest_version
|
17
|
+
@counter= (@counter ||= 100) + 10
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
11
21
|
class MyAudit < Auditable::Audit
|
12
22
|
|
13
23
|
end
|
14
24
|
|
25
|
+
|
26
|
+
class Plant < ActiveRecord::Base
|
27
|
+
audit :name, :after_create => :manually_create_audit, :after_update => :manually_update_audit, changed_by: :lumberjack, :version => true
|
28
|
+
|
29
|
+
has_many :plants
|
30
|
+
|
31
|
+
def audit_action
|
32
|
+
'audit action'
|
33
|
+
end
|
34
|
+
|
35
|
+
def manually_create_audit
|
36
|
+
self.save_audit( {:action => 'manual create', :changed_by => self.audit_changed_by, :modifications => self.snap } )
|
37
|
+
end
|
38
|
+
|
39
|
+
def manually_update_audit
|
40
|
+
self.save_audit( {:action => 'manual update', :changed_by => self.audit_changed_by, :tag => 'tagged!', :modifications => self.snap} )
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def lumberjack
|
45
|
+
User.create( name: "Bob" )
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Tree < Plant
|
50
|
+
audit :tastey, :plants
|
51
|
+
|
52
|
+
|
53
|
+
def lumberjack
|
54
|
+
User.create( name: "Sue" )
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Kale < Plant
|
59
|
+
audit :tastey, after_create: :audit_create_callback, changed_by: Proc.new { |kale| User.create( name: "bob loves #{kale.name}") }
|
60
|
+
end
|
61
|
+
|
15
62
|
# TODO add Question class to give examples on association stuff
|
16
63
|
|
17
64
|
|
data/spec/support/schema.rb
CHANGED
@@ -12,6 +12,11 @@ class CreateTestSchema < ActiveRecord::Migration
|
|
12
12
|
create_table "users", :force => true do |t|
|
13
13
|
t.string "name"
|
14
14
|
end
|
15
|
+
create_table "plants", :force => true do |t|
|
16
|
+
t.string "name"
|
17
|
+
t.boolean "tastey"
|
18
|
+
t.integer "plant_id"
|
19
|
+
end
|
15
20
|
end
|
16
21
|
end
|
17
22
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: auditable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-10-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -108,7 +108,7 @@ dependencies:
|
|
108
108
|
- !ruby/object:Gem::Version
|
109
109
|
version: '0'
|
110
110
|
- !ruby/object:Gem::Dependency
|
111
|
-
name:
|
111
|
+
name: kramdown
|
112
112
|
requirement: !ruby/object:Gem::Requirement
|
113
113
|
none: false
|
114
114
|
requirements:
|
@@ -178,10 +178,17 @@ files:
|
|
178
178
|
- lib/auditable.rb
|
179
179
|
- lib/auditable/audit.rb
|
180
180
|
- lib/auditable/auditing.rb
|
181
|
+
- lib/auditable/base.rb
|
181
182
|
- lib/auditable/version.rb
|
182
183
|
- lib/generators/auditable/migration_generator.rb
|
183
184
|
- lib/generators/auditable/templates/migration.rb
|
185
|
+
- lib/generators/auditable/templates/update.rb
|
186
|
+
- lib/generators/auditable/update_generator.rb
|
184
187
|
- spec/lib/auditable_spec.rb
|
188
|
+
- spec/lib/callbacks_spec.rb
|
189
|
+
- spec/lib/changed_by_spec.rb
|
190
|
+
- spec/lib/snap_spec.rb
|
191
|
+
- spec/lib/version_spec.rb
|
185
192
|
- spec/spec_helper.rb
|
186
193
|
- spec/support/models.rb
|
187
194
|
- spec/support/schema.rb
|
@@ -200,7 +207,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
200
207
|
version: '0'
|
201
208
|
segments:
|
202
209
|
- 0
|
203
|
-
hash:
|
210
|
+
hash: -3344788208757919701
|
204
211
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
205
212
|
none: false
|
206
213
|
requirements:
|
@@ -209,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
209
216
|
version: '0'
|
210
217
|
segments:
|
211
218
|
- 0
|
212
|
-
hash:
|
219
|
+
hash: -3344788208757919701
|
213
220
|
requirements: []
|
214
221
|
rubyforge_project:
|
215
222
|
rubygems_version: 1.8.24
|
@@ -218,6 +225,10 @@ specification_version: 3
|
|
218
225
|
summary: A simple gem to audit attributes and methods in ActiveRecord models.
|
219
226
|
test_files:
|
220
227
|
- spec/lib/auditable_spec.rb
|
228
|
+
- spec/lib/callbacks_spec.rb
|
229
|
+
- spec/lib/changed_by_spec.rb
|
230
|
+
- spec/lib/snap_spec.rb
|
231
|
+
- spec/lib/version_spec.rb
|
221
232
|
- spec/spec_helper.rb
|
222
233
|
- spec/support/models.rb
|
223
234
|
- spec/support/schema.rb
|