simple_audit 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
![Screenshot of helper result](http://github.com/gtarnovan/simple_audit/raw/master/screenshot.png)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
|
-
![Screenshot of helper result](http://github.com/gtarnovan/simple_audit/raw/master/screenshot.png)
|
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
|