simple_audit 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +12 -0
- data/LICENSE +1 -1
- data/Rakefile +12 -9
- data/TODO +12 -3
- data/generators/simple_audit_migration/USAGE +2 -0
- data/generators/simple_audit_migration/simple_audit_migration_generator.rb +9 -0
- data/generators/simple_audit_migration/templates/migration.rb +22 -0
- data/lib/generators/simple_audit/migration/migration_generator.rb +31 -0
- data/lib/generators/simple_audit/migration/templates/active_record/migration.rb +22 -0
- data/lib/simple_audit.rb +10 -44
- data/lib/simple_audit/audit.rb +44 -0
- data/lib/simple_audit/helper.rb +35 -0
- data/lib/simple_audit/simple_audit.rb +73 -0
- data/rails/init.rb +1 -10
- data/screenshot.png +0 -0
- data/simple_audit.gemspec +28 -15
- data/test/fixtures/addresses.yml +6 -0
- data/test/fixtures/people.yml +8 -0
- data/test/test_helper.rb +74 -1
- data/test/unit/simple_audit_test.rb +66 -0
- metadata +32 -14
- data/README.markdown +0 -122
- data/lib/app/helpers/simple_audit_helper.rb +0 -33
- data/lib/app/models/audit.rb +0 -39
- data/tasks/simple_audit_tasks.rake +0 -4
- data/test/simple_audit_test.rb +0 -8
data/CHANGELOG
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Version 0.1.0 (Sept 29, 2010)
|
2
|
+
* Rails 3 compatibility
|
3
|
+
* minor API changes break backwards compatibility
|
4
|
+
* documented code
|
5
|
+
|
6
|
+
# Version 0.0.3 (June 6, 2010)
|
7
|
+
* unit tests
|
8
|
+
|
9
|
+
# Version 0.0.2 (June 3, 2010)
|
10
|
+
* added migration generator
|
11
|
+
|
12
|
+
# Version 0.0.1 (June 1, 2010)
|
data/LICENSE
CHANGED
data/Rakefile
CHANGED
@@ -27,24 +27,27 @@ end
|
|
27
27
|
#
|
28
28
|
begin
|
29
29
|
require 'jeweler'
|
30
|
-
|
30
|
+
test_files = FileList['test/**/*'].to_a
|
31
31
|
gem_files = FileList[
|
32
32
|
'[a-zA-Z]*',
|
33
33
|
'lib/**/*',
|
34
|
+
'generators/**/*',
|
34
35
|
'rails/**/*',
|
35
|
-
|
36
|
-
'test/**/*'
|
37
|
-
]
|
36
|
+
].to_a + test_files
|
38
37
|
|
39
38
|
Jeweler::Tasks.new do |gemspec|
|
40
39
|
gemspec.name = "simple_audit"
|
41
40
|
gemspec.summary = "Simple auditing solution for ActiveRecord models"
|
42
|
-
|
43
|
-
|
41
|
+
gemspec.description = <<-EOD
|
42
|
+
Provides a straightforward way for auditing changes on active record models, especially for composite entities.
|
43
|
+
Also provides helper methods for easily rendering an audit trail in Ruby on Rails views.
|
44
|
+
EOD
|
45
|
+
gemspec.email = ["gabriel.tarnovan@cubus.ro", "mihai.tarnovan@cubus.ro"]
|
44
46
|
gemspec.homepage = "http://github.com/gtarnovan/simple_audit"
|
45
|
-
gemspec.authors = ["Gabriel Tarnovan"]
|
46
|
-
gemspec.version = "0.0
|
47
|
-
gemspec.files = gem_files
|
47
|
+
gemspec.authors = ["Gabriel Tarnovan", "Mihai Tarnovan"]
|
48
|
+
gemspec.version = "0.1.0"
|
49
|
+
gemspec.files = gem_files
|
50
|
+
gemspec.test_files = test_files
|
48
51
|
|
49
52
|
gemspec.rubyforge_project = 'simple_audit'
|
50
53
|
end
|
data/TODO
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
-
|
2
|
-
*
|
3
|
-
*
|
1
|
+
# Architecture
|
2
|
+
* support for ActiveModel
|
3
|
+
* ORM agnosticism
|
4
|
+
|
5
|
+
# Funtionality
|
6
|
+
* provide some sort of UUID for audited models that identify these independently from the database provided ID.
|
7
|
+
in case of deletion and creation of a new entity with the same UUID, the audit trails of both entities could be bound.
|
8
|
+
e.g. for an invoice the UUID could be its number/series combination. when someone deletes an invoice and creates a new one
|
9
|
+
with the same UUID, the audit trail of both instances should be bound, such that the deletion event is visible during the audit (i.e. in the view helper)
|
10
|
+
|
11
|
+
# Minor
|
12
|
+
* provide a sample css file for audit trail rendering
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class SimpleAuditMigration < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :audits do |t|
|
4
|
+
t.belongs_to :auditable, :polymorphic => true
|
5
|
+
t.belongs_to :user, :polymorphic => true
|
6
|
+
|
7
|
+
t.string :username
|
8
|
+
t.string :action
|
9
|
+
t.text :change_log
|
10
|
+
t.timestamps
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
|
15
|
+
add_index :audits, [:user_id, :user_type], :name => 'user_index'
|
16
|
+
add_index :audits, :created_at
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.down
|
20
|
+
drop_table :audits
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rails/generators/migration'
|
2
|
+
|
3
|
+
module SimpleAudit
|
4
|
+
class MigrationGenerator < Rails::Generators::Base
|
5
|
+
include Rails::Generators::Migration
|
6
|
+
|
7
|
+
desc "Generates migration for Audit model"
|
8
|
+
|
9
|
+
def self.orm
|
10
|
+
Rails::Generators.options[:rails][:orm]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.source_root
|
14
|
+
File.join(File.dirname(__FILE__), 'templates', (orm.to_s unless orm.class.eql?(String)) )
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.orm_has_migration?
|
18
|
+
[:active_record].include? orm
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.next_migration_number(path)
|
22
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_migration_file
|
26
|
+
if self.class.orm_has_migration?
|
27
|
+
migration_template 'migration.rb', 'db/migrate/simple_audit_migration'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class SimpleAuditMigration < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :audits do |t|
|
4
|
+
t.belongs_to :auditable, :polymorphic => true
|
5
|
+
t.belongs_to :user, :polymorphic => true
|
6
|
+
|
7
|
+
t.string :username
|
8
|
+
t.string :action
|
9
|
+
t.text :change_log
|
10
|
+
t.timestamps
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
|
15
|
+
add_index :audits, [:user_id, :user_type], :name => 'user_index'
|
16
|
+
add_index :audits, :created_at
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.down
|
20
|
+
drop_table :audits
|
21
|
+
end
|
22
|
+
end
|
data/lib/simple_audit.rb
CHANGED
@@ -1,48 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
def self.included(base)
|
5
|
-
base.send :extend, ClassMethods
|
6
|
-
end
|
7
|
-
|
8
|
-
module ClassMethods
|
9
|
-
|
10
|
-
# create a new Audit record
|
11
|
-
# if no user is specified, try to use sentient_user's User.current
|
1
|
+
require 'active_record'
|
2
|
+
require 'action_view'
|
12
3
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
cattr_accessor :username_method
|
17
|
-
self.username_method = (options[:username_method] || :name).to_s
|
18
|
-
|
19
|
-
has_many :audits, :as => :auditable
|
20
|
-
after_create {|record| audit(record, :create)}
|
21
|
-
after_update {|record| audit(record, :update)}
|
22
|
-
|
23
|
-
|
24
|
-
end
|
25
|
-
send :include, InstanceMethods
|
26
|
-
end
|
27
|
-
|
28
|
-
def audit record, action = :update, user = nil
|
29
|
-
user ||= User.current if User.respond_to?(:current)
|
30
|
-
record.audits.create(:user => user,
|
31
|
-
:username => user.try(self.username_method),
|
32
|
-
:action => action.to_s,
|
33
|
-
:changes => record.audit_changes
|
34
|
-
)
|
35
|
-
end
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
module InstanceMethods
|
40
|
-
def audit_changes
|
41
|
-
self.attributes
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
end
|
4
|
+
require 'simple_audit/simple_audit'
|
5
|
+
require 'simple_audit/audit'
|
6
|
+
require 'simple_audit/helper'
|
46
7
|
|
8
|
+
if defined?(ActiveRecord::Base)
|
9
|
+
ActiveRecord::Base.send :include, SimpleAudit
|
47
10
|
end
|
48
11
|
|
12
|
+
if defined?(ActionView::Base)
|
13
|
+
ActionView::Base.send :include, SimpleAudit::Helper
|
14
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module SimpleAudit #:nodoc:
|
2
|
+
|
3
|
+
# Changes of the audited models will be stored here.
|
4
|
+
class Audit < ActiveRecord::Base
|
5
|
+
belongs_to :auditable, :polymorphic => true
|
6
|
+
belongs_to :user, :polymorphic => true
|
7
|
+
serialize :change_log
|
8
|
+
|
9
|
+
# Computes the differences of the change logs between two audits.
|
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
|
+
# }
|
16
|
+
def delta(other_audit)
|
17
|
+
|
18
|
+
return self.change_log if other_audit.nil?
|
19
|
+
|
20
|
+
returning({}) do |d|
|
21
|
+
|
22
|
+
# first for keys present only in this audit
|
23
|
+
(self.change_log.keys - other_audit.change_log.keys).each do |k|
|
24
|
+
d[k] = [nil, self.change_log[k]]
|
25
|
+
end
|
26
|
+
|
27
|
+
# .. then for keys present only in other audit
|
28
|
+
(other_audit.change_log.keys - self.change_log.keys).each do |k|
|
29
|
+
d[k] = [other_audit.change_log[k], nil]
|
30
|
+
end
|
31
|
+
|
32
|
+
# .. finally for keys present in both, but with different values
|
33
|
+
self.change_log.keys.each do |k|
|
34
|
+
if self.change_log[k] != other_audit.change_log[k]
|
35
|
+
d[k] = [other_audit.change_log[k], self.change_log[k]]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SimpleAudit #:nodoc:
|
2
|
+
module Helper
|
3
|
+
|
4
|
+
# Render the change log for the given audited model
|
5
|
+
|
6
|
+
def render_audits(audited_model)
|
7
|
+
return '' unless audited_model.respond_to?(:audits)
|
8
|
+
audits = (audited_model.audits || []).dup.sort{|a,b| b.created_at <=> a.created_at}
|
9
|
+
res = ''
|
10
|
+
audits.each_with_index do |audit, index|
|
11
|
+
older_audit = audits[index + 1]
|
12
|
+
res += content_tag(:div, :class => 'audit') do
|
13
|
+
content_tag(:div, audit.action, :class => "action #{audit.action}") +
|
14
|
+
content_tag(:div, audit.username, :class => "user") +
|
15
|
+
content_tag(:div, l(audit.created_at), :class => "timestamp") +
|
16
|
+
content_tag(:div, :class => 'changes') do
|
17
|
+
if older_audit.present?
|
18
|
+
audit.delta(older_audit).collect do |k, v|
|
19
|
+
"\n" +
|
20
|
+
audited_model.class.human_attribute_name(k) +
|
21
|
+
":" +
|
22
|
+
content_tag(:span, v.last, :class => 'current') +
|
23
|
+
content_tag(:span, v.first, :class => 'previous')
|
24
|
+
end
|
25
|
+
else
|
26
|
+
audit.change_log.reject{|k, v| v.blank?}.collect {|k, v| "\n#{audited_model.class.human_attribute_name(k)}: #{v}"}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
res
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# Use this macro if you want changes in your model to be saved in an audit table.
|
2
|
+
# The audits table must exist.
|
3
|
+
#
|
4
|
+
# class Booking
|
5
|
+
# simple_audit
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# See SimpleAudit::ClassMethods#simple_audit for configuration options
|
9
|
+
|
10
|
+
module SimpleAudit
|
11
|
+
def self.included(base) #:nodoc:
|
12
|
+
base.send :extend, ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
# == Configuration options
|
18
|
+
#
|
19
|
+
# * <tt>username_method => symbol</tt> - Call this method on the current user to get the name
|
20
|
+
#
|
21
|
+
# With no block, all the attributes of the audited model will be logged.
|
22
|
+
#
|
23
|
+
# class Booking
|
24
|
+
# # this is equivalent to passing no block
|
25
|
+
# simple_audit do |audited_record|
|
26
|
+
# audited_record.attributes
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# If a block is given, the data returned by the block will be saved in the audit's change log.
|
31
|
+
#
|
32
|
+
# class Booking
|
33
|
+
# has_many :housing_units
|
34
|
+
# simple_audit do |audited_record|
|
35
|
+
# {
|
36
|
+
# :some_relevant_attribute => audited_record.some_relevant_attribute,
|
37
|
+
# :human_readable_serialization_of_aggregated_models => audited_record.housing_units.collect(&:to_s),
|
38
|
+
# ...
|
39
|
+
# }
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
|
44
|
+
def simple_audit(options = {}, &block)
|
45
|
+
class_eval do
|
46
|
+
|
47
|
+
write_inheritable_attribute :username_method, (options[:username_method] || :name).to_sym
|
48
|
+
class_inheritable_reader :username_method
|
49
|
+
|
50
|
+
audit_changes_proc = block_given? ? block.to_proc : Proc.new {|record| record.attributes}
|
51
|
+
write_inheritable_attribute :audit_changes, audit_changes_proc
|
52
|
+
class_inheritable_reader :audit_changes
|
53
|
+
|
54
|
+
has_many :audits, :as => :auditable, :class_name => '::SimpleAudit::Audit'
|
55
|
+
|
56
|
+
after_create {|record| record.class.audit(record, :create)}
|
57
|
+
after_update {|record| record.class.audit(record, :update)}
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def audit(record, action = :update, user = nil) #:nodoc:
|
63
|
+
user ||= User.current if User.respond_to?(:current)
|
64
|
+
record.audits.create(:user => user,
|
65
|
+
:username => user.try(self.username_method),
|
66
|
+
:action => action.to_s,
|
67
|
+
:change_log => self.audit_changes.call(record)
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
data/rails/init.rb
CHANGED
@@ -1,11 +1,2 @@
|
|
1
|
+
# compatibility with Rails 2
|
1
2
|
require 'simple_audit'
|
2
|
-
|
3
|
-
%w{ models helpers }.each do |dir|
|
4
|
-
path = File.join(File.dirname(__FILE__), '..', 'lib', 'app', dir)
|
5
|
-
$LOAD_PATH << path
|
6
|
-
ActiveSupport::Dependencies.load_paths << path
|
7
|
-
ActiveSupport::Dependencies.load_once_paths.delete(path)
|
8
|
-
end
|
9
|
-
|
10
|
-
ActiveRecord::Base.send :include, Cubus::SimpleAudit
|
11
|
-
ActionController::Base.send :helper, :simple_audit
|
data/screenshot.png
CHANGED
Binary file
|
data/simple_audit.gemspec
CHANGED
@@ -5,41 +5,54 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{simple_audit}
|
8
|
-
s.version = "0.0
|
8
|
+
s.version = "0.1.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
-
s.authors = ["Gabriel Tarnovan"]
|
12
|
-
s.date = %q{2010-
|
13
|
-
s.
|
11
|
+
s.authors = ["Gabriel Tarnovan", "Mihai Tarnovan"]
|
12
|
+
s.date = %q{2010-09-29}
|
13
|
+
s.description = %q{ Provides a straightforward way for auditing changes on active record models, especially for composite entities.
|
14
|
+
Also provides helper methods for easily rendering an audit trail in Ruby on Rails views.
|
15
|
+
}
|
16
|
+
s.email = ["gabriel.tarnovan@cubus.ro", "mihai.tarnovan@cubus.ro"]
|
14
17
|
s.extra_rdoc_files = [
|
15
18
|
"LICENSE",
|
16
|
-
"README.markdown",
|
17
19
|
"TODO"
|
18
20
|
]
|
19
21
|
s.files = [
|
20
|
-
"
|
21
|
-
"
|
22
|
+
"CHANGELOG",
|
23
|
+
"LICENSE",
|
22
24
|
"Rakefile",
|
23
25
|
"TODO",
|
24
|
-
"
|
25
|
-
"
|
26
|
+
"generators/simple_audit_migration/USAGE",
|
27
|
+
"generators/simple_audit_migration/simple_audit_migration_generator.rb",
|
28
|
+
"generators/simple_audit_migration/templates/migration.rb",
|
29
|
+
"lib/generators/simple_audit/migration/migration_generator.rb",
|
30
|
+
"lib/generators/simple_audit/migration/templates/active_record/migration.rb",
|
26
31
|
"lib/simple_audit.rb",
|
32
|
+
"lib/simple_audit/audit.rb",
|
33
|
+
"lib/simple_audit/helper.rb",
|
34
|
+
"lib/simple_audit/simple_audit.rb",
|
27
35
|
"rails/init.rb",
|
28
36
|
"screenshot.png",
|
29
37
|
"simple_audit.gemspec",
|
30
|
-
"
|
31
|
-
"test/
|
32
|
-
"test/test_helper.rb"
|
38
|
+
"test/fixtures/addresses.yml",
|
39
|
+
"test/fixtures/people.yml",
|
40
|
+
"test/test_helper.rb",
|
41
|
+
"test/unit/simple_audit_test.rb"
|
33
42
|
]
|
34
43
|
s.homepage = %q{http://github.com/gtarnovan/simple_audit}
|
35
44
|
s.rdoc_options = ["--charset=UTF-8"]
|
36
45
|
s.require_paths = ["lib"]
|
37
46
|
s.rubyforge_project = %q{simple_audit}
|
38
|
-
s.rubygems_version = %q{1.3.
|
47
|
+
s.rubygems_version = %q{1.3.6}
|
39
48
|
s.summary = %q{Simple auditing solution for ActiveRecord models}
|
40
49
|
s.test_files = [
|
41
|
-
"test/
|
42
|
-
"test/
|
50
|
+
"test/fixtures",
|
51
|
+
"test/fixtures/addresses.yml",
|
52
|
+
"test/fixtures/people.yml",
|
53
|
+
"test/test_helper.rb",
|
54
|
+
"test/unit",
|
55
|
+
"test/unit/simple_audit_test.rb"
|
43
56
|
]
|
44
57
|
|
45
58
|
if s.respond_to? :specification_version then
|
data/test/test_helper.rb
CHANGED
@@ -1,3 +1,76 @@
|
|
1
1
|
require 'rubygems'
|
2
|
+
require 'test/unit'
|
2
3
|
require 'active_support'
|
3
|
-
require 'active_support/test_case'
|
4
|
+
require 'active_support/test_case'
|
5
|
+
require 'active_record'
|
6
|
+
require 'active_record/fixtures'
|
7
|
+
require 'action_controller'
|
8
|
+
require 'ruby-debug'
|
9
|
+
require 'ostruct'
|
10
|
+
|
11
|
+
require File.join(File.dirname(__FILE__), "..", "generators", "simple_audit_migration", "templates", "migration.rb")
|
12
|
+
require File.join(File.dirname(__FILE__), "..", "lib", "simple_audit")
|
13
|
+
|
14
|
+
ActiveRecord::Base.establish_connection({
|
15
|
+
:adapter => 'sqlite3',
|
16
|
+
:database => ':memory:'
|
17
|
+
})
|
18
|
+
|
19
|
+
ActiveRecord::Migration.suppress_messages {
|
20
|
+
ActiveRecord::Schema.define do
|
21
|
+
suppress_messages do
|
22
|
+
create_table "people", :force => true do |t|
|
23
|
+
t.column "name", :text
|
24
|
+
t.column "email", :text
|
25
|
+
end
|
26
|
+
create_table "addresses", :force => true do |t|
|
27
|
+
t.column "line_1", :text
|
28
|
+
t.column "zip", :text
|
29
|
+
t.column "type", :text
|
30
|
+
t.references :person
|
31
|
+
end
|
32
|
+
create_table "users", :force => true do |t|
|
33
|
+
t.column "name", :text
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
SimpleAuditMigration.migrate(:up)
|
38
|
+
}
|
39
|
+
|
40
|
+
class Person < ActiveRecord::Base
|
41
|
+
has_one :address
|
42
|
+
simple_audit do |record|
|
43
|
+
{
|
44
|
+
:name => record.name,
|
45
|
+
:address => { :line_1 => record.address.line_1, :zip => record.address.zip }
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Address < ActiveRecord::Base
|
51
|
+
belongs_to :person
|
52
|
+
simple_audit :username_method => :full_name
|
53
|
+
end
|
54
|
+
|
55
|
+
class HomeAddress < Address
|
56
|
+
simple_audit :username_method => :short_name
|
57
|
+
end
|
58
|
+
|
59
|
+
class User < ActiveRecord::Base
|
60
|
+
def self.current; User.first ; end
|
61
|
+
|
62
|
+
def name
|
63
|
+
"name"
|
64
|
+
end
|
65
|
+
|
66
|
+
def full_name
|
67
|
+
"full_name"
|
68
|
+
end
|
69
|
+
|
70
|
+
def short_name
|
71
|
+
"short_name"
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
User.create(:name => "some user")
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class SimpleAuditTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
include SimpleAudit
|
6
|
+
|
7
|
+
test "should audit entity creation" do
|
8
|
+
assert_difference 'Audit.count', 4 do
|
9
|
+
assert_difference 'Person.count', 2 do
|
10
|
+
Person.create(:name => "Mihai Tarnovan", :email => "mihai.tarnovan@cubus.ro", :address => Address.new(:line_1 => "M. Viteazu nr. 11 sc. C ap.32"))
|
11
|
+
Person.create(:name => "Gabriel Tarnovan", :email => "gabriel.tarnovan@cubus.ro", :address => Address.new(:line_1 => "Calea Lunga nr. 104 Sibiu 123500"))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
test "should set correct action" do
|
17
|
+
person = Person.create(:name => "Mihai Tarnovan", :email => "mihai.tarnovan@cubus.ro", :address => Address.new(:line_1 => "M. Viteazu nr. 11 sc. C ap.32"))
|
18
|
+
assert_equal Audit.last.action, "create"
|
19
|
+
person.name = "Mihai T."
|
20
|
+
person.save
|
21
|
+
assert_equal person.audits.last.action, "update"
|
22
|
+
end
|
23
|
+
|
24
|
+
test "should audit only given fields" do
|
25
|
+
person = Person.create(:name => "Mihai Tarnovan", :email => "mihai.tarnovan@cubus.ro", :address => Address.new(:line_1 => "M. Viteazu nr. 11 sc. C ap.32"))
|
26
|
+
create_audit = person.audits.last
|
27
|
+
assert_difference 'Audit.count', 1 do
|
28
|
+
person.email = "mihai.tarnovan@gmail.com"
|
29
|
+
person.save
|
30
|
+
end
|
31
|
+
update_audit = person.audits.last
|
32
|
+
assert_equal update_audit.delta(create_audit), {}
|
33
|
+
end
|
34
|
+
|
35
|
+
test "should audit associated entity changes" do
|
36
|
+
person = Person.create(:name => "Mihai Tarnovan", :email => "mihai.tarnovan@cubus.ro", :address => Address.new(:line_1 => "M. Viteazu nr. 11 sc. C ap.32", :zip => "550350"))
|
37
|
+
create_audit = person.audits.last
|
38
|
+
assert_difference 'person.audits.count', 1 do
|
39
|
+
person.address = Address.new(:line_1 => "Bdul. Victoriei nr. 51", :zip => "550150")
|
40
|
+
person.name = "Gigi Kent"
|
41
|
+
person.email = "gigi@kent.ro"
|
42
|
+
person.save
|
43
|
+
end
|
44
|
+
update_audit = person.audits.last
|
45
|
+
assert_equal update_audit.delta(create_audit), {
|
46
|
+
:name => ["Mihai Tarnovan", "Gigi Kent"],
|
47
|
+
:address => [
|
48
|
+
{ :line_1 => "M. Viteazu nr. 11 sc. C ap.32", :zip => "550350" },
|
49
|
+
{ :line_1 => "Bdul. Victoriei nr. 51", :zip => "550150" }
|
50
|
+
]
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
test "should audit all attributes by default" do
|
55
|
+
address = Address.create
|
56
|
+
assert_equal Audit.last.change_log.keys.sort, Address.column_names.sort
|
57
|
+
end
|
58
|
+
|
59
|
+
test "should use proper username method" do
|
60
|
+
address = HomeAddress.create
|
61
|
+
assert_equal User.new.short_name, address.audits.last.username
|
62
|
+
address = Address.create
|
63
|
+
assert_equal User.new.full_name, address.audits.last.username
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
metadata
CHANGED
@@ -1,42 +1,56 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple_audit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Gabriel Tarnovan
|
13
|
+
- Mihai Tarnovan
|
8
14
|
autorequire:
|
9
15
|
bindir: bin
|
10
16
|
cert_chain: []
|
11
17
|
|
12
|
-
date: 2010-
|
18
|
+
date: 2010-09-29 00:00:00 +03:00
|
13
19
|
default_executable:
|
14
20
|
dependencies: []
|
15
21
|
|
16
|
-
description:
|
17
|
-
email:
|
22
|
+
description: " Provides a straightforward way for auditing changes on active record models, especially for composite entities. \n Also provides helper methods for easily rendering an audit trail in Ruby on Rails views.\n"
|
23
|
+
email:
|
24
|
+
- gabriel.tarnovan@cubus.ro
|
25
|
+
- mihai.tarnovan@cubus.ro
|
18
26
|
executables: []
|
19
27
|
|
20
28
|
extensions: []
|
21
29
|
|
22
30
|
extra_rdoc_files:
|
23
31
|
- LICENSE
|
24
|
-
- README.markdown
|
25
32
|
- TODO
|
26
33
|
files:
|
34
|
+
- CHANGELOG
|
27
35
|
- LICENSE
|
28
|
-
- README.markdown
|
29
36
|
- Rakefile
|
30
37
|
- TODO
|
31
|
-
-
|
32
|
-
-
|
38
|
+
- generators/simple_audit_migration/USAGE
|
39
|
+
- generators/simple_audit_migration/simple_audit_migration_generator.rb
|
40
|
+
- generators/simple_audit_migration/templates/migration.rb
|
41
|
+
- lib/generators/simple_audit/migration/migration_generator.rb
|
42
|
+
- lib/generators/simple_audit/migration/templates/active_record/migration.rb
|
33
43
|
- lib/simple_audit.rb
|
44
|
+
- lib/simple_audit/audit.rb
|
45
|
+
- lib/simple_audit/helper.rb
|
46
|
+
- lib/simple_audit/simple_audit.rb
|
34
47
|
- rails/init.rb
|
35
48
|
- screenshot.png
|
36
49
|
- simple_audit.gemspec
|
37
|
-
-
|
38
|
-
- test/
|
50
|
+
- test/fixtures/addresses.yml
|
51
|
+
- test/fixtures/people.yml
|
39
52
|
- test/test_helper.rb
|
53
|
+
- test/unit/simple_audit_test.rb
|
40
54
|
has_rdoc: true
|
41
55
|
homepage: http://github.com/gtarnovan/simple_audit
|
42
56
|
licenses: []
|
@@ -50,21 +64,25 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
64
|
requirements:
|
51
65
|
- - ">="
|
52
66
|
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 0
|
53
69
|
version: "0"
|
54
|
-
version:
|
55
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
71
|
requirements:
|
57
72
|
- - ">="
|
58
73
|
- !ruby/object:Gem::Version
|
74
|
+
segments:
|
75
|
+
- 0
|
59
76
|
version: "0"
|
60
|
-
version:
|
61
77
|
requirements: []
|
62
78
|
|
63
79
|
rubyforge_project: simple_audit
|
64
|
-
rubygems_version: 1.3.
|
80
|
+
rubygems_version: 1.3.6
|
65
81
|
signing_key:
|
66
82
|
specification_version: 3
|
67
83
|
summary: Simple auditing solution for ActiveRecord models
|
68
84
|
test_files:
|
85
|
+
- test/fixtures/addresses.yml
|
86
|
+
- test/fixtures/people.yml
|
69
87
|
- test/test_helper.rb
|
70
|
-
- test/simple_audit_test.rb
|
88
|
+
- test/unit/simple_audit_test.rb
|
data/README.markdown
DELETED
@@ -1,122 +0,0 @@
|
|
1
|
-
# simple_audit
|
2
|
-
|
3
|
-
Simple auditing solution for ActiveRecord models. Provides an easy way of creating audit logs for complex model associations.
|
4
|
-
Instead of storing audits for all data aggregated by the audited model, you can specify a serializable representation of the model.
|
5
|
-
|
6
|
-
* a helper method is provided to easily display the audit log
|
7
|
-
* the Audit object provides a #delta method which computs the differences between two audits
|
8
|
-
|
9
|
-
In a nutshell:
|
10
|
-
|
11
|
-
class Booking < ActiveRecord::Base
|
12
|
-
simple_audit
|
13
|
-
|
14
|
-
# data to be audited
|
15
|
-
def audit_changes
|
16
|
-
{
|
17
|
-
:price => self.price,
|
18
|
-
:period => self.period,
|
19
|
-
...
|
20
|
-
}
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# in your view
|
25
|
-
<%= render_audits(@booking) %>
|
26
|
-
|
27
|
-
# Why ?
|
28
|
-
|
29
|
-
simple_audit is intended as a straightforward auditing solution for complex model associations.
|
30
|
-
In a normalized data structure (which is usually the case when using SQL), a core business model will aggregate data from several associated entities.
|
31
|
-
More often than not in such cases, only the core model is of interest from an auditing point of view.
|
32
|
-
|
33
|
-
So instead of auditing every entity sepparately, with simple_audit you decide for each audited model what data needs to be audited.
|
34
|
-
This data will be serialized on every model update and the available helper methods will render a clear audit trail.
|
35
|
-
|
36
|
-
s
|
37
|
-
|
38
|
-
# Installation & Configuration
|
39
|
-
|
40
|
-
## As a gem
|
41
|
-
|
42
|
-
gem install simple_audit
|
43
|
-
|
44
|
-
and require it
|
45
|
-
|
46
|
-
config.gem 'simple_audit'
|
47
|
-
|
48
|
-
## As a plugin
|
49
|
-
|
50
|
-
./script/plugin install http://github.com/gtarnovan/simple_audit
|
51
|
-
|
52
|
-
## Database
|
53
|
-
|
54
|
-
Generate and run migration
|
55
|
-
|
56
|
-
./script/generate simple_audit_migration create_audits
|
57
|
-
rake db:migrate
|
58
|
-
|
59
|
-
# Usage
|
60
|
-
|
61
|
-
Audit ActiveRecord models. Somewhere in your (backend) views show the audit logs.
|
62
|
-
|
63
|
-
# in your model
|
64
|
-
# app/models/booking.rb
|
65
|
-
|
66
|
-
class Booking < ActiveRecord::Base
|
67
|
-
simple_audit
|
68
|
-
...
|
69
|
-
end
|
70
|
-
|
71
|
-
# in your view
|
72
|
-
# app/views/bookings/booking.html.erb
|
73
|
-
|
74
|
-
...
|
75
|
-
<%= render_audits(@booking) %>
|
76
|
-
...
|
77
|
-
|
78
|
-
# Assumptions and limitations
|
79
|
-
|
80
|
-
* Your user model is called User and the current user User.current
|
81
|
-
See [sentient_user](http://github.com/bokmann/sentient_user) for more information.
|
82
|
-
|
83
|
-
* You have to write your own tests (fow now)
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
## Customize auditing
|
88
|
-
|
89
|
-
By default after each save, all model's attributes are saved in the audits table.
|
90
|
-
You can customize the data which is saved by overriding the audit_changes method. All relevant data for the audited model should be included here.
|
91
|
-
|
92
|
-
# app/models/booking.rb
|
93
|
-
|
94
|
-
class Booking < ActiveRecord::Base
|
95
|
-
simple_audit
|
96
|
-
|
97
|
-
def audit_changes
|
98
|
-
{
|
99
|
-
:state => self.state,
|
100
|
-
:price => self.price.format,
|
101
|
-
:period => self.period.to_s,
|
102
|
-
:housing_units => housing_units.collect(&:name).join('; '),
|
103
|
-
...
|
104
|
-
}
|
105
|
-
end
|
106
|
-
...
|
107
|
-
end
|
108
|
-
|
109
|
-
You can also customize the attribute of the User model which will be stored in the audit.
|
110
|
-
|
111
|
-
# default is :name
|
112
|
-
simple_audit :username_method => :email
|
113
|
-
|
114
|
-
## Rendering audit
|
115
|
-
|
116
|
-
A helper method for displaying a list of audits is provided. It will render a decorated list of the provided audits;
|
117
|
-
only the differences between revisions will be shown, thus making the audit information easily readable.
|
118
|
-
|
119
|
-

|
120
|
-
|
121
|
-
|
122
|
-
Copyright (c) 2010 [Gabriel Târnovan, Cubus Arts](http://cubus.ro "Cubus Arts"), released under the MIT license
|
@@ -1,33 +0,0 @@
|
|
1
|
-
module SimpleAuditHelper
|
2
|
-
|
3
|
-
def render_audits(audited_model)
|
4
|
-
return '' unless audited_model.respond_to?(:audits)
|
5
|
-
audits = (audited_model.audits || []).dup.sort{|a,b| b.created_at <=> a.created_at}
|
6
|
-
res = ''
|
7
|
-
audits.each_with_index do |audit, index|
|
8
|
-
older_audit = audits[index + 1]
|
9
|
-
res += content_tag(:div, :class => 'audit') do
|
10
|
-
content_tag(:div, audit.action, :class => "action #{audit.action}") +
|
11
|
-
content_tag(:div, audit.username, :class => "user") +
|
12
|
-
content_tag(:div, l(audit.created_at), :class => "timestamp") +
|
13
|
-
content_tag(:div, :class => 'changes') do
|
14
|
-
if older_audit.present?
|
15
|
-
audit.delta(older_audit).collect do |k, v|
|
16
|
-
"\n" +
|
17
|
-
audited_model.class.human_attribute_name(k) +
|
18
|
-
":" +
|
19
|
-
content_tag(:span, v.last, :class => 'current') +
|
20
|
-
content_tag(:span, v.first, :class => 'previous')
|
21
|
-
end
|
22
|
-
else
|
23
|
-
audit.changes.reject{|k, v| v.blank?}.collect {|k, v| "\n#{audited_model.class.human_attribute_name(k)}: #{v}"}
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
res
|
29
|
-
end
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
|
data/lib/app/models/audit.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
class Audit < ActiveRecord::Base
|
2
|
-
belongs_to :auditable, :polymorphic => true
|
3
|
-
belongs_to :user, :polymorphic => true
|
4
|
-
serialize :changes
|
5
|
-
|
6
|
-
#
|
7
|
-
# returns the differences of the to audits
|
8
|
-
# result is a hash containing arrays of the form [<value_in_other_audit>, <value_in_this_audit>]
|
9
|
-
# the values in this audit take precedence
|
10
|
-
#
|
11
|
-
def delta(other)
|
12
|
-
|
13
|
-
return self.changes if other.nil?
|
14
|
-
|
15
|
-
d = {}
|
16
|
-
|
17
|
-
(self.changes.keys - other.changes.keys).each do |k|
|
18
|
-
d[k] = [nil, self.changes[k]]
|
19
|
-
end
|
20
|
-
|
21
|
-
(other.changes.keys - self.changes.keys).each do |k|
|
22
|
-
d[k] = [other.changes[k], nil]
|
23
|
-
end
|
24
|
-
|
25
|
-
self.changes.keys.each do |k|
|
26
|
-
if self.changes[k] != other.changes[k]
|
27
|
-
d[k] = [other.changes[k], self.changes[k]]
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
d
|
32
|
-
|
33
|
-
end
|
34
|
-
|
35
|
-
def changes
|
36
|
-
self[:changes]
|
37
|
-
end
|
38
|
-
|
39
|
-
end
|