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.
@@ -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