auditable 0.1.5 → 0.1.6
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.
- 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
|