rollbacker 1.0.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/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ coverage
2
+ rdoc
3
+ *.gem
4
+ .bundle
5
+ Gemfile.lock
6
+ pkg/*
7
+ tmp/*
8
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Near Infinity Corporation
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,16 @@
1
+ ## Rollbacker ##
2
+
3
+ Rollbacker is a manage tool for auditing changes to your ActiveRecord.
4
+ The changes of objects are added to a queue where the auditor can approve and reject those changes.
5
+
6
+ ## Installation ##
7
+
8
+ To use it with your Rails 3 project, add the following line to your Gemfile
9
+
10
+ gem 'rollbacker'
11
+
12
+ Generate the migration and create the rollbacker_changes table
13
+
14
+ rails generate rollbacker:migration
15
+ rake db:migrate
16
+
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ $:.unshift File.expand_path("../lib", __FILE__)
2
+
3
+ require 'rake'
4
+ require 'rdoc/task'
5
+ require 'rspec/core/rake_task'
6
+ require 'bundler'
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ desc 'Default: run specs'
11
+ task :default => :spec
12
+
13
+ desc "Run specs"
14
+ RSpec::Core::RakeTask.new do |t|
15
+ t.rspec_opts = %w(-fs --color)
16
+ end
17
+
18
+ desc "Run specs with RCov"
19
+ RSpec::Core::RakeTask.new(:rcov) do |t|
20
+ t.rspec_opts = %w(-fs --color)
21
+ t.rcov = true
22
+ t.rcov_opts = %w(--exclude "spec/*,gems/*")
23
+ end
24
+
25
+ desc 'Generate documentation for the gem.'
26
+ Rake::RDocTask.new(:rdoc) do |rdoc|
27
+ rdoc.rdoc_dir = 'rdoc'
28
+ rdoc.title = 'Rollbacker'
29
+ rdoc.options << '--line-numbers' << '--inline-source'
30
+ rdoc.rdoc_files.include('README.md')
31
+ rdoc.rdoc_files.include('lib/**/*.rb')
32
+ end
33
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'rollbacker'
@@ -0,0 +1,26 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module Rollbacker
5
+ module Generators
6
+ class MigrationGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ desc "Create migration for Rollbacker rollbacker_changes table"
10
+
11
+ source_root File.expand_path("../templates", __FILE__)
12
+
13
+ def self.next_migration_number(dirname)
14
+ if ActiveRecord::Base.timestamped_migrations
15
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
16
+ else
17
+ "%.3d" % (current_migration_number(dirname) + 1)
18
+ end
19
+ end
20
+
21
+ def create_migration_file
22
+ migration_template 'migration.rb', 'db/migrate/create_rollbacker_changes_table.rb'
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ class CreateRollbackerChangesTable < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :rollbacker_changes, :force => true do |t|
4
+ t.column :rollbackable_id, :integer
5
+ t.column :rollbackable_type, :string
6
+ t.column :user_id, :integer
7
+ t.column :user_type, :string
8
+ t.column :action, :string
9
+ t.column :rollbacked_changes, :text
10
+ t.column :created_at, :datetime
11
+ t.column :updated_at, :datetime
12
+ end
13
+
14
+ add_index :rollbacker_changes, [:rollbackable_id, :rollbackable_type], :name => 'rollbackable_index'
15
+ add_index :rollbacker_changes, [:user_id, :user_type], :name => 'user_index'
16
+ add_index :rollbacker_changes, :created_at
17
+ end
18
+
19
+ def self.down
20
+ drop_table :rollbacker_changes
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ module Rollbacker
2
+ module Generators
3
+ class Base < Rails::Generators::NamedBase
4
+ def self.source_root
5
+ File.expand_path(File.join(File.dirname(__FILE__), 'rollbacker', generator_name, 'templates'))
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,49 @@
1
+ require 'rollbacker/status'
2
+ require 'rollbacker/config'
3
+ require 'rollbacker/database_rollback'
4
+ require 'rollbacker/recorder'
5
+
6
+ module Rollbacker
7
+ module Base
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ def rollbacker(*args, &blk)
15
+ unless self.included_modules.include?(ActiveRecord::Transactions::ClassMethods)
16
+ include ActiveRecord::Transactions::ClassMethods
17
+ end
18
+ unless self.included_modules.include?(Rollbacker::Base::InstanceMethods)
19
+ include InstanceMethods
20
+ include Rollbacker::Status unless self.included_modules.include?(Rollbacker::Status)
21
+ has_many :rollbacker_changes, :as => :rollbackable
22
+ end
23
+
24
+ config = Rollbacker::Config.new(*args)
25
+ config.actions.each do |action|
26
+ send "around_#{action}", Rollbacker::DatabaseRollback.new(config.options)
27
+ # send :after_rollback, Rollbacker::Recorder.new(action, config.options, &blk), :on => action
28
+ end
29
+ send :after_rollback, Rollbacker::Recorder.new(config.options, &blk)
30
+ end
31
+
32
+ def rollbacker!(*args, &blk)
33
+ if args.last.kind_of?(Hash)
34
+ args.last[:fail_on_error] = true
35
+ else
36
+ args << { :fail_on_error => true }
37
+ end
38
+
39
+ rollbacker(*args, &blk)
40
+ end
41
+ end
42
+
43
+ module InstanceMethods
44
+ attr_accessor :_rollbacker_action
45
+
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ module Rollbacker
2
+ class ChangeValidator
3
+ attr_reader :action
4
+ attr_reader :options
5
+
6
+ def initialize(action, options, model)
7
+ @action = action
8
+ @options = options
9
+ @model = model
10
+ end
11
+
12
+ def valid?
13
+ case @action
14
+ when :destroy
15
+ @model.persisted?
16
+ when :update, :create
17
+ self.changes.any?
18
+ end
19
+ end
20
+
21
+ def changes(other_changes={})
22
+ chg = @model.changes.dup
23
+ chg.reverse_merge!( (other_changes || {}).with_indifferent_access )
24
+ chg.reject!{|key, value| @options[:except].include?(key) } unless @options[:except].blank?
25
+ chg.reject!{|key, value| !@options[:only].include?(key) } unless @options[:only].blank?
26
+ chg
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ module Rollbacker
2
+ class Config
3
+ attr_reader :actions
4
+ attr_reader :options
5
+
6
+ def self.valid_actions
7
+ @valid_actions ||= [:create, :update, :destroy]
8
+ end
9
+
10
+ def initialize(*args)
11
+ @options = (args.pop if args.last.kind_of?(Hash)) || {}
12
+ normalize_options(@options)
13
+
14
+ @actions = args.map(&:to_sym)
15
+ validate_actions(@actions)
16
+ end
17
+
18
+ private
19
+
20
+ def normalize_options(options)
21
+ options.each_pair { |k, v| options[k.to_sym] = options.delete(k) unless k.kind_of? Symbol }
22
+ options[:only] ||= []
23
+ options[:except] ||= []
24
+ options[:only] = Array(options[:only]).map(&:to_s)
25
+ options[:except] = Array(options[:except]).map(&:to_s)
26
+ end
27
+
28
+ def validate_actions(actions)
29
+ raise Rollbacker::Error.new "at least one action in #{Config.valid_actions.inspect} must be specified" if actions.empty?
30
+ raise Rollbacker::Error.new "#{Config.valid_actions.inspect} are the only valid actions" unless actions.all? { |a| Config.valid_actions.include?(a.to_sym) }
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ require 'rollbacker/status'
2
+ require 'rollbacker/change_validator'
3
+
4
+ module Rollbacker
5
+ class DatabaseRollback
6
+ include Status
7
+
8
+ def initialize(args)
9
+ @options = args
10
+ end
11
+
12
+ [:create, :update, :destroy].each do |action|
13
+ define_method("around_#{action}") do |model|
14
+ rollback_model_changes(model, action)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def rollback_model_changes(model, action)
21
+ model._rollbacker_action = action
22
+
23
+ if rollbacker_enabled? && ChangeValidator.new(action, @options, model).valid?
24
+ raise ActiveRecord::Rollback
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ require 'rollbacker/status'
2
+
3
+ module Rollbacker
4
+ class Recorder
5
+ include Status
6
+
7
+ # def initialize(action, options, &blk)
8
+ # @action = action
9
+ # @options = options
10
+ # @blk = blk
11
+ # end
12
+
13
+ def initialize(options, &blk)
14
+ @options = options
15
+ @blk = blk
16
+ end
17
+
18
+ def after_rollback(model)
19
+ create_or_update_changes(model)
20
+ end
21
+
22
+ private
23
+ # Should just return @action if after_rollback(:on=>:destroy) should set the correct action.. Take a look at this issue:
24
+ # https://github.com/rails/rails/issues/7640
25
+ #
26
+ # **Also remove everything about _rollbacker_action instance method.**
27
+ #
28
+ # def action
29
+ # @action
30
+ # end
31
+
32
+ def change_validator(model)
33
+ ChangeValidator.new(model._rollbacker_action, @options, model)
34
+ end
35
+
36
+ def create_or_update_changes(model)
37
+ return if rollbacker_disabled?
38
+ validator = change_validator(model)
39
+ return unless validator.valid?
40
+ user = Rollbacker::User.current_user
41
+
42
+ record = \
43
+ if model.new_record?
44
+ RollbackerChange.new(rollbackable_type: model.class.name, action: validator.action)
45
+ else
46
+ RollbackerChange.find_or_initialize_by_rollbackable_id_and_rollbackable_type_and_action(model.id, model.class.name, validator.action)
47
+ end
48
+ if model.changed?
49
+ record.rollbacked_changes = validator.changes(record.rollbacked_changes)
50
+ end
51
+
52
+ @blk.call(model, record, user, validator.action) if @blk
53
+
54
+ @options[:fail_on_error] ? record.save! : record.save
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_record'
2
+ require 'rollbacker/config'
3
+
4
+ class RollbackerChange < ActiveRecord::Base
5
+ belongs_to :rollbackable, :polymorphic => true
6
+ belongs_to :user, :polymorphic => true
7
+
8
+ before_create :set_user
9
+
10
+ serialize :rollbacked_changes
11
+
12
+ def new_attributes
13
+ (rollbacked_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
14
+ attrs[attr] = values.is_a?(Array) ? values.last : values
15
+ attrs
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def set_user
22
+ self.user = Rollbacker::User.current_user if self.user_id.nil?
23
+ end
24
+
25
+ end
@@ -0,0 +1,20 @@
1
+ module Rollbacker
2
+ module SpecHelpers
3
+ include Rollbacker::Status
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ before(:each) do
8
+ disable_rollbacker!
9
+ end
10
+
11
+ after(:each) do
12
+ enable_rollbacker!
13
+ end
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+
20
+
@@ -0,0 +1,62 @@
1
+ require 'rollbacker/user'
2
+
3
+ module Rollbacker
4
+ module Status
5
+
6
+ def rollbacker_disabled?
7
+ Thread.current[:rollbacker_disabled] == true
8
+ end
9
+
10
+ def rollbacker_enabled?
11
+ Thread.current[:rollbacker_disabled].nil? || Thread.current[:rollbacker_disabled] == false
12
+ end
13
+
14
+ def disable_rollbacker!
15
+ Thread.current[:rollbacker_disabled] = true
16
+ end
17
+
18
+ def enable_rollbacker!
19
+ Thread.current[:rollbacker_disabled] = false
20
+ end
21
+
22
+ def without_rollbacker
23
+ previously_disabled = rollbacker_disabled?
24
+
25
+ begin
26
+ disable_rollbacker!
27
+ result = yield if block_given?
28
+ ensure
29
+ enable_rollbacker! unless previously_disabled
30
+ end
31
+
32
+ result
33
+ end
34
+
35
+ def with_rollbacker
36
+ previously_disabled = rollbacker_disabled?
37
+
38
+ begin
39
+ enable_rollbacker!
40
+ result = yield if block_given?
41
+ ensure
42
+ disable_rollbacker! if previously_disabled
43
+ end
44
+
45
+ result
46
+ end
47
+
48
+ def rollbacker_as(user)
49
+ previous_user = Rollbacker::User.current_user
50
+
51
+ begin
52
+ Rollbacker::User.current_user = user
53
+ result = yield if block_given?
54
+ ensure
55
+ Rollbacker::User.current_user = previous_user
56
+ end
57
+
58
+ result
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,15 @@
1
+ module Rollbacker
2
+ module User
3
+
4
+ def current_user
5
+ Thread.current[:rollbacker_user]
6
+ end
7
+
8
+ def current_user=(user)
9
+ Thread.current[:rollbacker_user] = user
10
+ end
11
+
12
+ module_function :current_user, :current_user=
13
+
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Rollbacker
2
+ VERSION = "1.0.0"
3
+ end
data/lib/rollbacker.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'rollbacker/rollbacker_change'
2
+ require 'rollbacker/base'
3
+
4
+ module Rollbacker
5
+ class Error < StandardError; end
6
+ end
7
+
8
+ ActiveRecord::Base.send :include, Rollbacker::Base
9
+
10
+ if defined?(ActionController) and defined?(ActionController::Base)
11
+
12
+ require 'rollbacker/user'
13
+
14
+ ActionController::Base.class_eval do
15
+ before_filter do |c|
16
+ Rollbacker::User.current_user = c.send(:current_user) if c.respond_to?(:current_user)
17
+ end
18
+ end
19
+
20
+ end
21
+
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/rollbacker/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Marcos G. Zimmermann"]
6
+ gem.email = ["mgzmaster@gmail.com"]
7
+ gem.homepage = "http://github.com/marcosgz/rollbacker"
8
+ gem.name = "rollbacker"
9
+ gem.summary = %q{Rollbacker is a manage tool for auditing changes to your ActiveRecord}
10
+ gem.description = %q{Rollbacker allows you to declaratively specify what CRUD operations should be audited. The changes of objects are added to a queue where the auditor can approve and reject those changes.}
11
+ gem.version = Rollbacker::VERSION
12
+
13
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ gem.require_paths = ["lib"]
17
+
18
+ gem.add_dependency('activerecord', '>= 3.0')
19
+
20
+ gem.add_development_dependency('rspec')
21
+ gem.add_development_dependency('sqlite3-ruby')
22
+ end
data/spec/base_spec.rb ADDED
@@ -0,0 +1,127 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'rollbacker/user'
3
+ require 'rollbacker/status'
4
+
5
+ describe Rollbacker::Base do
6
+ include Rollbacker::Status
7
+
8
+ before(:each) do
9
+ @user = User.create
10
+ @original_model = Model
11
+ Rollbacker::User.current_user = @user
12
+ end
13
+
14
+ after(:each) do
15
+ reset_model
16
+ end
17
+
18
+ it 'should be created a :create change record' do
19
+ redefine_model { rollbacker!(:create) }
20
+
21
+ m = Model.new(:name => 'new')
22
+ m.should be_valid
23
+ m.should be_changed
24
+ lambda {
25
+ lambda { m.save }.should change(RollbackerChange, :count).by(1)
26
+ }.should_not change(Model, :count)
27
+
28
+ verify_change(RollbackerChange.last, m, :create, { 'name' => [nil, 'new'], 'id' => [nil, m.id] })
29
+ end
30
+
31
+ it 'should be created a :update change record' do
32
+ redefine_model { rollbacker!(:update) }
33
+ m = Model.new(:name => 'new')
34
+ without_rollbacker { m.save }
35
+
36
+ lambda {
37
+ lambda { m.update_attributes(:name => 'newer') }.should change(RollbackerChange, :count).by(1)
38
+ }.should_not change(Model, :count)
39
+
40
+ verify_change(RollbackerChange.last, m, :update, { 'name' => ['new', 'newer'] })
41
+ end
42
+
43
+
44
+ it 'should be created a :destroy change record' do
45
+ redefine_model { rollbacker!(:destroy) }
46
+ m = without_rollbacker { Model.create(:name => 'new') }
47
+
48
+ lambda {
49
+ lambda { m.destroy }.should change(RollbackerChange, :count).by(1)
50
+ }.should_not change(Model, :count)
51
+
52
+ verify_change(RollbackerChange.last, m, :destroy)
53
+ end
54
+
55
+ it 'should allow multiple actions to be specified with one rollbacker statment' do
56
+ redefine_model { rollbacker!(:update, :destroy) }
57
+ m = Model.new(:name => 'new')
58
+ lambda {
59
+ m.save
60
+ }.should_not change(RollbackerChange, :count)
61
+ m.should be_persisted
62
+ lambda {
63
+ m.update_attributes({:name => 'newer'})
64
+ }.should change(RollbackerChange, :count).by(1)
65
+ m.should be_persisted
66
+ lambda {
67
+ m.destroy
68
+ }.should change(RollbackerChange, :count).by(1)
69
+
70
+ RollbackerChange.count.should == 2
71
+ edit1 = RollbackerChange.first
72
+ edit1.action.should == 'update'
73
+ edit2 = RollbackerChange.last
74
+ edit2.action.should == 'destroy'
75
+ end
76
+
77
+ it 'should be able to turn off rollbacker for a especific field' do
78
+ redefine_model { rollbacker!(:update, :except => :name) }
79
+
80
+ m = Model.new(:name => 'name')
81
+ lambda {
82
+ m.save
83
+ }.should_not change(RollbackerChange, :count)
84
+ lambda {
85
+ m.update_attributes({:name => 'new'})
86
+ }.should_not change(RollbackerChange, :count)
87
+ lambda {
88
+ m.update_attributes({:value => 'new'})
89
+ }.should change(RollbackerChange, :count).by(1)
90
+ end
91
+
92
+ it 'should be able to to track rollbacker changes for a especific field' do
93
+ redefine_model { rollbacker!(:update, :only => :value) }
94
+
95
+ m = Model.new(:name => 'name')
96
+ lambda {
97
+ m.save
98
+ }.should_not change(RollbackerChange, :count)
99
+ lambda {
100
+ m.update_attributes({:name => 'new'})
101
+ }.should_not change(RollbackerChange, :count)
102
+ lambda {
103
+ m.update_attributes({:value => 'new'})
104
+ }.should change(RollbackerChange, :count).by(1)
105
+ end
106
+
107
+ def verify_change(change_record, model, action, changes=nil)
108
+ change_record.should_not be_nil
109
+ change_record.rollbackable_id.should == model.id
110
+ change_record.rollbackable_type.should == model.class.to_s
111
+ change_record.action.should == action.to_s
112
+ change_record.user.should == @user
113
+ change_record.rollbacked_changes.should == changes.reject{|k,v|v.map(&:nil?).all?} unless changes.nil?
114
+ end
115
+
116
+ def redefine_model(&blk)
117
+ clazz = Class.new(ActiveRecord::Base, &blk)
118
+ Object.send :remove_const, 'Model'
119
+ Object.send :const_set, 'Model', clazz
120
+ end
121
+
122
+ def reset_model
123
+ Object.send :remove_const, 'Model'
124
+ Object.send :const_set, 'Model', @original_model
125
+ end
126
+
127
+ end