mongoid_simple_audit 0.1.4
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.
- checksums.yaml +15 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +22 -0
- data/.travis.yml +24 -0
- data/Appraisals +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +151 -0
- data/Rakefile +12 -0
- data/gemfiles/rails_3.gemfile +7 -0
- data/gemfiles/rails_3.gemfile.lock +137 -0
- data/gemfiles/rails_4.gemfile +7 -0
- data/gemfiles/rails_4.gemfile.lock +128 -0
- data/lib/mongoid/simple_audit/audit.rb +27 -0
- data/lib/mongoid/simple_audit/helper.rb +39 -0
- data/lib/mongoid/simple_audit/modification.rb +57 -0
- data/lib/mongoid/simple_audit/railtie.rb +11 -0
- data/lib/mongoid/simple_audit/simple_audit.rb +113 -0
- data/lib/mongoid/simple_audit/tasks.rb +1 -0
- data/lib/mongoid/simple_audit/version.rb +5 -0
- data/lib/mongoid_simple_audit.rb +17 -0
- data/lib/tasks/mongoid_simple_audit_migration.rake +57 -0
- data/mongoid_simple_audit.gemspec +30 -0
- data/spec/factories.rb +25 -0
- data/spec/helpers/simple_audit_helper_spec.rb +28 -0
- data/spec/lib/tasks/mongoid_simple_audit_rake_spec.rb +28 -0
- data/spec/models/simple_mongo_audit_spec.rb +64 -0
- data/spec/spec_helper.rb +125 -0
- metadata +218 -0
@@ -0,0 +1,113 @@
|
|
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 Mongoid
|
11
|
+
|
12
|
+
module SimpleAudit
|
13
|
+
module Model
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
def audit
|
17
|
+
Audit.find_by_auditable( self )
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
# == Configuration options
|
24
|
+
#
|
25
|
+
# * <tt>username_method => symbol</tt> - Call this method on the current user to get the name
|
26
|
+
#
|
27
|
+
# With no block, all the attributes and <tt>belongs_to</tt> associations (id and to_s) of the audited model will be logged.
|
28
|
+
#
|
29
|
+
# class Booking
|
30
|
+
# # this is equivalent to passing no block
|
31
|
+
# simple_audit do |audited_record|
|
32
|
+
# audited_record.attributes
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# If a block is given, the data returned by the block will be saved in the audit's change log.
|
37
|
+
#
|
38
|
+
# class Booking
|
39
|
+
# has_many :housing_units
|
40
|
+
# simple_audit do |audited_record|
|
41
|
+
# {
|
42
|
+
# :some_relevant_attribute => audited_record.some_relevant_attribute,
|
43
|
+
# :human_readable_serialization_of_aggregated_models => audited_record.housing_units.collect(&:to_s),
|
44
|
+
# ...
|
45
|
+
# }
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
|
50
|
+
def simple_audit(options = {}, &block)
|
51
|
+
class_eval do
|
52
|
+
|
53
|
+
class_attribute :username_method
|
54
|
+
class_attribute :audit_changes
|
55
|
+
class_attribute :audit_changes_only
|
56
|
+
|
57
|
+
self.username_method = (options[:username_method] || :name).to_sym
|
58
|
+
self.audit_changes_only = options[:audit_changes_only] === true
|
59
|
+
|
60
|
+
attributes_and_associations = proc do |record|
|
61
|
+
changes = record.attributes
|
62
|
+
record.class.reflect_on_all_associations(:belongs_to).each do |assoc|
|
63
|
+
changes[assoc.name] = record.send(assoc.name).to_s
|
64
|
+
end
|
65
|
+
changes
|
66
|
+
end
|
67
|
+
audit_changes_proc = block_given? ? block.to_proc : attributes_and_associations
|
68
|
+
|
69
|
+
self.audit_changes = audit_changes_proc
|
70
|
+
after_create {|record| record.class.audit(record, :create, nil)}
|
71
|
+
after_update {|record| record.class.audit(record, :update, nil)}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def audit(record, action = :update, user = nil) #:nodoc:
|
76
|
+
|
77
|
+
record_audit = SimpleAudit::Audit.find_or_create_by_auditable( record )
|
78
|
+
|
79
|
+
# check current user for typical Authentication systems
|
80
|
+
user = nil
|
81
|
+
if defined?(User) and User.respond_to?(:current)
|
82
|
+
user = User.current
|
83
|
+
elsif defined?(Cms::User) and Cms::User.respond_to?(:current)
|
84
|
+
user = Cms::User.current
|
85
|
+
end
|
86
|
+
|
87
|
+
current_change_log = self.audit_changes.call(record)
|
88
|
+
|
89
|
+
# do only log if anything changed
|
90
|
+
record_changed = true
|
91
|
+
if audit_changes_only and last_change_log = record.audit.modifications.last
|
92
|
+
record_changed = last_change_log.symbolized_change_log != current_change_log
|
93
|
+
end
|
94
|
+
|
95
|
+
if record_changed
|
96
|
+
if user.present?
|
97
|
+
record_audit.modifications.create(
|
98
|
+
:user => {id: user.id, type: user.class.name},
|
99
|
+
:username => user.try(self.username_method),
|
100
|
+
:action => action.to_s,
|
101
|
+
:change_log => current_change_log)
|
102
|
+
else
|
103
|
+
record_audit.modifications.create(
|
104
|
+
:action => action.to_s,
|
105
|
+
:change_log => current_change_log)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Dir[File.join(File.dirname(__FILE__),'../../tasks/*.rake')].each { |f| load f }
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'mongoid/simple_audit/simple_audit'
|
2
|
+
require 'mongoid/simple_audit/audit'
|
3
|
+
require 'mongoid/simple_audit/modification'
|
4
|
+
require 'mongoid/simple_audit/helper'
|
5
|
+
require File.dirname(__FILE__) + '/mongoid/simple_audit/railtie' if defined?(Rails::Railtie)
|
6
|
+
|
7
|
+
if defined?(ActiveRecord::Base)
|
8
|
+
ActiveRecord::Base.send :include, Mongoid::SimpleAudit::Model
|
9
|
+
end
|
10
|
+
|
11
|
+
if defined?(Mongoid::Document)
|
12
|
+
Mongoid::Document.send :include, Mongoid::SimpleAudit::Model
|
13
|
+
end
|
14
|
+
|
15
|
+
if defined?(ActionView::Base)
|
16
|
+
ActionView::Base.send :include, Mongoid::SimpleAudit::Helper
|
17
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# rake mongoid_simple_audit:migrates_from_mysql_to_mongoid
|
2
|
+
|
3
|
+
namespace :mongoid_simple_audit do
|
4
|
+
|
5
|
+
desc "Migrates all datasets from simple audit ActiveRecord Gem into MongoDB"
|
6
|
+
task :migrates_from_mysql_to_mongoid => :environment do
|
7
|
+
|
8
|
+
# create ActiveRecord Version of Audit
|
9
|
+
ArAudit = Class.new(ActiveRecord::Base) do
|
10
|
+
belongs_to :auditable, :polymorphic => true
|
11
|
+
belongs_to :user, :polymorphic => true
|
12
|
+
serialize :change_log
|
13
|
+
self.table_name = 'audits'
|
14
|
+
end
|
15
|
+
|
16
|
+
total_count = ArAudit.count
|
17
|
+
new_audit = nil
|
18
|
+
current_count = 0
|
19
|
+
batch_size = 1000
|
20
|
+
offset = 0
|
21
|
+
puts "starting to migrate #{total_count} records."
|
22
|
+
|
23
|
+
loop do
|
24
|
+
break if offset > total_count
|
25
|
+
|
26
|
+
ArAudit.offset( offset ).limit( batch_size ).order( [:auditable_type, :auditable_id] ).each do |audit|
|
27
|
+
|
28
|
+
if new_audit.nil? || new_audit.auditable_id != audit.auditable_id || new_audit.auditable_type != audit.auditable_type
|
29
|
+
new_audit = Mongoid::SimpleAudit::Audit.find_or_create_by( {auditable_type: audit.auditable_type, auditable_id: audit.auditable_id} )
|
30
|
+
end
|
31
|
+
|
32
|
+
current_count += 1
|
33
|
+
if (current_count % batch_size == 0) || (total_count < batch_size)
|
34
|
+
puts "migrated #{current_count}. #{total_count-current_count} remaining"
|
35
|
+
end
|
36
|
+
|
37
|
+
begin
|
38
|
+
ds = {
|
39
|
+
action: audit.action,
|
40
|
+
created_at: audit.created_at,
|
41
|
+
change_log: audit.change_log
|
42
|
+
}
|
43
|
+
if audit.user_id.present?
|
44
|
+
ds.merge!({
|
45
|
+
user: {id: audit.user_id, type: audit.user_type},
|
46
|
+
username: audit.username
|
47
|
+
})
|
48
|
+
end
|
49
|
+
new_audit.modifications.create( ds )
|
50
|
+
rescue Exception => e
|
51
|
+
puts "could not copy audit with ID: #{audit.id} because: #{e.message}"
|
52
|
+
end
|
53
|
+
end # <- END: Find Audits
|
54
|
+
offset += batch_size
|
55
|
+
end # <- END: batch-loop
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mongoid/simple_audit/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "mongoid_simple_audit"
|
8
|
+
spec.version = Mongoid::SimpleAudit::VERSION
|
9
|
+
spec.authors = ["Felix Abele"]
|
10
|
+
spec.email = ["felix.abele@googlemail.com"]
|
11
|
+
spec.summary = %q{Simple auditing solution for ActiveRecord models}
|
12
|
+
spec.description = %q{Provides a straightforward way for auditing changes on active record models, especially for composite entities. Also provides helper methods for easily rendering an audit trail in Ruby on Rails views.}
|
13
|
+
spec.homepage = "https://github.com/felixabele/mongoid_simple_audit.git"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "mongoid"
|
21
|
+
spec.add_development_dependency "appraisal"
|
22
|
+
spec.add_development_dependency "activerecord"
|
23
|
+
spec.add_development_dependency "factory_girl_rails"
|
24
|
+
spec.add_development_dependency "sqlite3"
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rspec"
|
28
|
+
spec.add_development_dependency "rspec-rails"
|
29
|
+
spec.add_development_dependency "coveralls", '~> 0.7.1'
|
30
|
+
end
|
data/spec/factories.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
|
3
|
+
factory :address do
|
4
|
+
line_1 "John"
|
5
|
+
zip "12047"
|
6
|
+
end
|
7
|
+
|
8
|
+
factory :person do
|
9
|
+
name "Felix"
|
10
|
+
email "felix.abele@gmail.com"
|
11
|
+
address
|
12
|
+
end
|
13
|
+
|
14
|
+
factory :user do
|
15
|
+
name "Some user"
|
16
|
+
end
|
17
|
+
|
18
|
+
factory :ar_audit, class: ArAudit do
|
19
|
+
association :auditable, :factory => :person
|
20
|
+
association :user, :factory => :user
|
21
|
+
action "some-action"
|
22
|
+
change_log( {'name' => 'Joe'} )
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'action_view'
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
include ActionView::Helpers::TagHelper
|
6
|
+
include ActionView::Helpers::TranslationHelper
|
7
|
+
include ActionView::Helpers::OutputSafetyHelper
|
8
|
+
include ActionView::Context
|
9
|
+
|
10
|
+
|
11
|
+
module Mongoid
|
12
|
+
|
13
|
+
describe SimpleAudit::Helper do
|
14
|
+
|
15
|
+
include SimpleAudit::Helper
|
16
|
+
|
17
|
+
it "should create an audit trail" do
|
18
|
+
person = create(:person)
|
19
|
+
expect( render_audits(person) ).to match person.name
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should create mark differences between two change logs" do
|
23
|
+
person = create(:person)
|
24
|
+
person.update_attribute :name, 'Franz'
|
25
|
+
expect( render_audits(person) ).to match("<span class=\"current\">Franz</span><span class=\"previous\">Felix</span>")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# spec/lib/tasks/mongoid_simple_audit_rake_spec.rb
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'rake'
|
4
|
+
|
5
|
+
describe "mongoid_simple_audit:migrates_from_mysql_to_mongoid" do
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
task_name = 'mongoid_simple_audit:migrates_from_mysql_to_mongoid'
|
9
|
+
task_path = "../lib/tasks/mongoid_simple_audit_migration"
|
10
|
+
rake = Rake::Application.new
|
11
|
+
Rake.application = rake
|
12
|
+
Rake.application.rake_require( task_path )
|
13
|
+
Rake::Task.define_task(:environment)
|
14
|
+
@subject = rake[task_name]
|
15
|
+
end
|
16
|
+
|
17
|
+
it "task should copy datasets from ActiveRecord to MongoDB" do
|
18
|
+
person1 = create(:person)
|
19
|
+
person2 = create(:person)
|
20
|
+
2.times{ create :ar_audit, auditable: person1 }
|
21
|
+
4.times{ create :ar_audit, auditable: person2 }
|
22
|
+
@subject.invoke
|
23
|
+
expect( Mongoid::SimpleAudit::Audit.by_auditable(ArAudit.first.auditable).where('modifications.action' => 'some-action') ).to exist
|
24
|
+
expect( Mongoid::SimpleAudit::Audit.find_by_auditable( person1 ).modifications.count ).to be 3
|
25
|
+
expect( Mongoid::SimpleAudit::Audit.find_by_auditable( person2 ).modifications.count ).to be 5
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# spec/models/simple_mongo_audit_spec.rb
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
module Mongoid
|
5
|
+
|
6
|
+
describe SimpleAudit::Audit do
|
7
|
+
|
8
|
+
it 'should create new Audit' do
|
9
|
+
expect( SimpleAudit::Audit.new( auditable_type: 'SomeModel', auditable_id: 1).save ).to be true
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should create valid fixtures' do
|
13
|
+
create :address
|
14
|
+
expect( Address.first ).not_to be_nil
|
15
|
+
|
16
|
+
create :person
|
17
|
+
expect( Person.first ).not_to be_nil
|
18
|
+
expect( Person.first.address ).not_to be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should create a new audit record with initial modification record' do
|
22
|
+
audit = SimpleAudit::Audit.find_or_create_by_auditable( create(:person) )
|
23
|
+
expect( audit ).not_to be_nil
|
24
|
+
expect( audit.modifications ).not_to be_empty
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should set correct action" do
|
28
|
+
address = create :address
|
29
|
+
expect( address.audit.modifications.last.action ).to eql 'create'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should audit only given fields" do
|
33
|
+
person = create :person
|
34
|
+
person.update_attributes email: 'new@email.com', name: 'new name'
|
35
|
+
expect( person.audit.modifications.last.change_log ).not_to have_key( 'email' )
|
36
|
+
expect( person.audit.modifications.last.change_log ).to have_key( 'name' )
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should audit associated entity changes" do
|
40
|
+
person = create :person
|
41
|
+
expect( person.audit.modifications.last.change_log['address']['line_1'] ).to be_present
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should not audit if no changes where made" do
|
45
|
+
person = create :person
|
46
|
+
person.update_attributes name: person.name
|
47
|
+
expect( person.audit.modifications.count ).to eql 1
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should use proper username method" do
|
51
|
+
address = HomeAddress.create
|
52
|
+
expect( User.new.short_name ).to eql address.audit.modifications.last.username
|
53
|
+
|
54
|
+
address = Address.create
|
55
|
+
expect( User.new.full_name ).to eql address.audit.modifications.last.username
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should audit Mongoid models' do
|
59
|
+
address = MongoidAddress.create(name: 'Hans', street: 'Sesamstr. 18')
|
60
|
+
expect( address.audit.modifications ).not_to be_empty
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'coveralls'
|
2
|
+
Coveralls.wear!
|
3
|
+
$:.unshift(File.join(File.dirname(__FILE__),'..','lib'))
|
4
|
+
|
5
|
+
require 'mongoid'
|
6
|
+
require 'rubygems'
|
7
|
+
require 'active_record'
|
8
|
+
require 'factory_girl_rails'
|
9
|
+
require 'bundler'
|
10
|
+
require 'mongoid_simple_audit'
|
11
|
+
|
12
|
+
def database_id
|
13
|
+
ENV['CI'] ? "mongoid_simple_audit_#{Process.pid}" : 'mongoid_simple_audit_test'
|
14
|
+
end
|
15
|
+
|
16
|
+
Mongoid.configure do |config|
|
17
|
+
config.connect_to database_id
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
RSpec.configure do |c|
|
22
|
+
|
23
|
+
c.color = true
|
24
|
+
c.tty = true
|
25
|
+
c.formatter = :documentation
|
26
|
+
c.include FactoryGirl::Syntax::Methods
|
27
|
+
|
28
|
+
c.before(:each) do |example|
|
29
|
+
unless example.metadata[:keep_datasets]
|
30
|
+
Mongoid.purge!
|
31
|
+
Mongoid::IdentityMap.clear if defined?(Mongoid::IdentityMap)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
c.after(:suite) do
|
36
|
+
Mongoid::Threaded.sessions[:default].drop if ENV['CI']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
ActiveRecord::Base.establish_connection({
|
41
|
+
:adapter => 'sqlite3',
|
42
|
+
:database => ':memory:'
|
43
|
+
})
|
44
|
+
|
45
|
+
ActiveRecord::Migration.suppress_messages {
|
46
|
+
ActiveRecord::Schema.define do
|
47
|
+
suppress_messages do
|
48
|
+
create_table "people", :force => true do |t|
|
49
|
+
t.column "name", :text
|
50
|
+
t.column "email", :text
|
51
|
+
end
|
52
|
+
create_table "addresses", :force => true do |t|
|
53
|
+
t.column "line_1", :text
|
54
|
+
t.column "zip", :text
|
55
|
+
t.column "type", :text
|
56
|
+
t.references :person
|
57
|
+
end
|
58
|
+
create_table "users", :force => true do |t|
|
59
|
+
t.column "name", :text
|
60
|
+
end
|
61
|
+
|
62
|
+
# in order to test migration from ActiveRecord
|
63
|
+
create_table "audits", :force => true do |t|
|
64
|
+
t.belongs_to :auditable, :polymorphic => true
|
65
|
+
t.belongs_to :user, :polymorphic => true
|
66
|
+
t.string :username
|
67
|
+
t.string :action
|
68
|
+
t.text :change_log
|
69
|
+
t.timestamps
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
}
|
74
|
+
|
75
|
+
class Address < ActiveRecord::Base
|
76
|
+
belongs_to :person
|
77
|
+
simple_audit( username_method: :full_name )
|
78
|
+
end
|
79
|
+
|
80
|
+
class MongoidAddress
|
81
|
+
include Mongoid::Document
|
82
|
+
include Mongoid::Attributes::Dynamic if Mongoid::VERSION.to_i > 3
|
83
|
+
simple_audit
|
84
|
+
end
|
85
|
+
|
86
|
+
class Person < ActiveRecord::Base
|
87
|
+
has_one :address
|
88
|
+
simple_audit( audit_changes_only: true ) do |record|
|
89
|
+
{
|
90
|
+
name: record.name,
|
91
|
+
address: { line_1: record.address.line_1, zip: record.address.zip }
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class HomeAddress < Address
|
97
|
+
simple_audit :username_method => :short_name
|
98
|
+
end
|
99
|
+
|
100
|
+
class ArAudit < ActiveRecord::Base
|
101
|
+
belongs_to :auditable, :polymorphic => true
|
102
|
+
belongs_to :user, :polymorphic => true
|
103
|
+
serialize :change_log
|
104
|
+
self.table_name = "audits"
|
105
|
+
end
|
106
|
+
|
107
|
+
class User < ActiveRecord::Base
|
108
|
+
def self.current; User.first ; end
|
109
|
+
|
110
|
+
def name
|
111
|
+
"name"
|
112
|
+
end
|
113
|
+
|
114
|
+
def full_name
|
115
|
+
"full_name"
|
116
|
+
end
|
117
|
+
|
118
|
+
def short_name
|
119
|
+
"short_name"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
require 'factories'
|
124
|
+
|
125
|
+
User.create(:name => "some user")
|