auditable 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
- gem.add_development_dependency 'sqlite3'
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
- # documetation stuff
27
+ # documentation stuff
24
28
  gem.add_development_dependency 'yard'
25
- gem.add_development_dependency 'rdiscount'
29
+ gem.add_development_dependency 'kramdown'
26
30
 
27
31
  # debugger. only included if one sets DEBUGGER env variable
28
32
  if ENV['DEBUGGER']
@@ -1,81 +1,7 @@
1
- module Auditable
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
- # find common keys and diff values
33
- self.modifications.keys.each do |k|
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
@@ -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
- attrs = @audited_attributes || []
11
- # handle STI case: include parent's audited_attributes
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
- attrs
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
- after_create {|record| record.snap!(:action => "create")}
31
- after_update {|record| record.snap!(:action => "update")}
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 :changed_by, :audit_action, :audit_tag
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
- audits.last
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
- last_audit.update_attribute(:tag, tag)
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
- snap = {}.tap do |s|
62
- self.class.audited_attributes.each do |attr|
63
- s[attr.to_s] = self.send attr
64
- end
65
- end
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
- last_saved_audit = audits.last
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(options.merge :modifications => snap)
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
- audit.save
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
- audits.last.try(:latest_diff, options) || {}
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
@@ -1,3 +1,3 @@
1
1
  module Auditable
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.6"
3
3
  end
@@ -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 ourself
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,7 @@
1
+ class UpdateAudits < ActiveRecord::Migration
2
+ def change
3
+ add_column :audits, :version, :integer
4
+
5
+ add_index :audits, [:auditable_id, :auditable_type, :version], :name => 'auditable_version_idx'
6
+ end
7
+ end
@@ -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
@@ -1,9 +1,13 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe "Model.audited_attributes" do
4
- it "should be available to models using audit" do
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
@@ -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
 
@@ -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.5
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-06-11 00:00:00.000000000 Z
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: rdiscount
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: 2404637449666859294
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: 2404637449666859294
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