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.
@@ -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,5 @@
1
+ module Mongoid
2
+ module SimpleAudit
3
+ VERSION = "0.1.4"
4
+ end
5
+ end
@@ -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
@@ -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")