memento 0.3.7 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rbenv-version +1 -1
- data/CHANGES.md +10 -1
- data/README.rdoc +9 -9
- data/generators/memento_migration/templates/migration.rb +4 -4
- data/lib/memento.rb +72 -54
- data/lib/memento/action.rb +7 -7
- data/lib/memento/action/create.rb +35 -33
- data/lib/memento/action/destroy.rb +26 -25
- data/lib/memento/action/update.rb +49 -48
- data/lib/memento/action_controller_methods.rb +3 -3
- data/lib/memento/active_record_methods.rb +21 -21
- data/lib/memento/result.rb +35 -34
- data/lib/memento/session.rb +25 -24
- data/lib/memento/state.rb +45 -44
- data/lib/memento/version.rb +2 -2
- data/memento.gemspec +2 -2
- data/spec/memento/action/create_spec.rb +14 -14
- data/spec/memento/action/destroy_spec.rb +9 -9
- data/spec/memento/action/update_spec.rb +34 -34
- data/spec/memento/action_controller_methods_spec.rb +9 -9
- data/spec/memento/active_record_methods_spec.rb +17 -17
- data/spec/memento/result_spec.rb +12 -12
- data/spec/memento/session_spec.rb +21 -21
- data/spec/memento/state_spec.rb +12 -12
- data/spec/memento_spec.rb +109 -68
- data/spec/spec_helper.rb +9 -9
- metadata +2 -2
@@ -1,9 +1,9 @@
|
|
1
|
-
|
1
|
+
module Memento
|
2
2
|
module ActionControllerMethods
|
3
|
-
|
3
|
+
|
4
4
|
def memento
|
5
5
|
block_result = nil
|
6
|
-
memento_session = Memento
|
6
|
+
memento_session = Memento(current_user) do
|
7
7
|
block_result = yield
|
8
8
|
end
|
9
9
|
if memento_session
|
@@ -1,82 +1,82 @@
|
|
1
1
|
require 'active_support/core_ext/class/attribute'
|
2
2
|
|
3
3
|
module Memento::ActiveRecordMethods
|
4
|
-
|
4
|
+
|
5
5
|
IGNORE_ATTRIBUTES = [:updated_at, :created_at]
|
6
|
-
|
6
|
+
|
7
7
|
def self.included(base)
|
8
8
|
base.class_attribute(:memento_options_store, :instance_reader => false, :instance_writer => false)
|
9
9
|
base.memento_options_store = {}
|
10
10
|
base.extend ClassMethods
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
module ClassMethods
|
14
|
-
|
14
|
+
|
15
15
|
def memento_changes(*action_types)
|
16
16
|
if defined?(@memento_initialized) && @memento_initialized
|
17
17
|
raise "Memento initialized twice. Use memento_changes only once per model"
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
@memento_initialized = true
|
21
|
-
|
21
|
+
|
22
22
|
include InstanceMethods
|
23
|
-
|
23
|
+
|
24
24
|
self.memento_options = action_types.last.is_a?(Hash) ? action_types.pop : {}
|
25
|
-
|
25
|
+
|
26
26
|
action_types = Memento::Action::Base.action_types if action_types.empty?
|
27
27
|
action_types.map!(&:to_s).uniq!
|
28
28
|
unless (invalid = action_types - Memento::Action::Base.action_types).empty?
|
29
29
|
raise ArgumentError.new("Invalid memento_changes: #{invalid.to_sentence}; allowed are only #{Memento::Action::Base.action_types.to_sentence}")
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
action_types.each do |action_type|
|
33
33
|
send :"after_#{action_type}", :"record_#{action_type}"
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
has_many :memento_states, :class_name => "Memento::State", :as => :record
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
def memento_options
|
40
40
|
self.memento_options_store ||= {}
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
def memento_options=(options)
|
44
44
|
options.symbolize_keys!
|
45
45
|
options[:ignore] = [options[:ignore]].flatten.map(&:to_sym) if options[:ignore]
|
46
46
|
self.memento_options_store = memento_options_store.merge(options)
|
47
47
|
end
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
50
|
module InstanceMethods
|
51
|
-
|
51
|
+
|
52
52
|
def attributes_for_memento
|
53
53
|
filter_attributes_for_memento(attributes)
|
54
54
|
end
|
55
|
-
|
55
|
+
|
56
56
|
def changes_for_memento
|
57
57
|
filter_attributes_for_memento(changes)
|
58
58
|
end
|
59
|
-
|
59
|
+
|
60
60
|
def filter_attributes_for_memento(hash)
|
61
|
-
hash.delete_if do |key, value|
|
61
|
+
hash.delete_if do |key, value|
|
62
62
|
ignore_attributes_for_memento.include?(key.to_sym)
|
63
63
|
end
|
64
64
|
end
|
65
65
|
private :filter_attributes_for_memento
|
66
|
-
|
66
|
+
|
67
67
|
def ignore_attributes_for_memento
|
68
68
|
Memento::ActiveRecordMethods::IGNORE_ATTRIBUTES + (self.class.memento_options[:ignore] || [])
|
69
69
|
end
|
70
70
|
private :ignore_attributes_for_memento
|
71
|
-
|
71
|
+
|
72
72
|
Memento::Action::Base.action_types.each do |action_type|
|
73
73
|
define_method :"record_#{action_type}" do
|
74
|
-
Memento.
|
74
|
+
Memento.add_state(action_type, self)
|
75
75
|
end
|
76
76
|
private :"record_#{action_type}"
|
77
77
|
end
|
78
78
|
end
|
79
|
-
|
79
|
+
|
80
80
|
end
|
81
81
|
|
82
82
|
ActiveRecord::Base.send(:include, Memento::ActiveRecordMethods) if defined?(ActiveRecord::Base)
|
data/lib/memento/result.rb
CHANGED
@@ -1,37 +1,38 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
class Memento::Result
|
18
|
-
|
19
|
-
attr_reader :object, :state
|
20
|
-
|
21
|
-
def initialize(object, state)
|
22
|
-
@object, @state = object, state
|
23
|
-
end
|
24
|
-
|
25
|
-
def error
|
26
|
-
error = @object.errors[:memento_undo]
|
27
|
-
error.present? ? error : nil
|
28
|
-
end
|
29
|
-
|
30
|
-
def failed?
|
31
|
-
!!error
|
1
|
+
module Memento
|
2
|
+
class ResultArray < Array
|
3
|
+
def errors
|
4
|
+
self.find_all{ |result| result.failed? }
|
5
|
+
end
|
6
|
+
|
7
|
+
def failed?
|
8
|
+
self.any?{ |result| result.failed? }
|
9
|
+
end
|
10
|
+
|
11
|
+
def success?
|
12
|
+
!failed?
|
13
|
+
end
|
14
|
+
|
32
15
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
16
|
+
|
17
|
+
class Result
|
18
|
+
|
19
|
+
attr_reader :object, :state
|
20
|
+
|
21
|
+
def initialize(object, state)
|
22
|
+
@object, @state = object, state
|
23
|
+
end
|
24
|
+
|
25
|
+
def error
|
26
|
+
error = @object.errors[:memento_undo]
|
27
|
+
error.present? ? error : nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def failed?
|
31
|
+
!!error
|
32
|
+
end
|
33
|
+
|
34
|
+
def success?
|
35
|
+
!failed?
|
36
|
+
end
|
36
37
|
end
|
37
38
|
end
|
data/lib/memento/session.rb
CHANGED
@@ -1,29 +1,30 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
def undo
|
13
|
-
states.map(&:undo).inject(Memento::ResultArray.new) do |results, result|
|
14
|
-
result.state.destroy if result.success?
|
15
|
-
results << result
|
1
|
+
module Memento
|
2
|
+
class Session < ActiveRecord::Base
|
3
|
+
self.table_name = "memento_sessions"
|
4
|
+
|
5
|
+
has_many :states, :class_name => "Memento::State", :dependent => :delete_all, :order => "id DESC"
|
6
|
+
belongs_to :user
|
7
|
+
validates_presence_of :user
|
8
|
+
|
9
|
+
def add_state(action_type, record)
|
10
|
+
states.store(action_type, record)
|
16
11
|
end
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
12
|
+
|
13
|
+
def undo
|
14
|
+
states.map(&:undo).inject(Memento::ResultArray.new) do |results, result|
|
15
|
+
result.state.destroy if result.success?
|
16
|
+
results << result
|
17
|
+
end
|
18
|
+
ensure
|
19
|
+
destroy if states.count.zero?
|
20
|
+
end
|
21
|
+
|
22
|
+
def undo!
|
23
|
+
transaction do
|
24
|
+
undo.tap do |results|
|
25
|
+
raise Memento::ErrorOnRewind if results.failed?
|
26
|
+
end
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
28
|
-
|
29
30
|
end
|
data/lib/memento/state.rb
CHANGED
@@ -1,47 +1,48 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
self.
|
16
|
-
|
1
|
+
module Memento
|
2
|
+
class State < ActiveRecord::Base
|
3
|
+
self.table_name = "memento_states"
|
4
|
+
|
5
|
+
belongs_to :session, :class_name => "Memento::Session"
|
6
|
+
belongs_to :record, :polymorphic => true
|
7
|
+
|
8
|
+
validates_presence_of :session
|
9
|
+
validates_presence_of :record
|
10
|
+
validates_presence_of :action_type
|
11
|
+
validates_inclusion_of :action_type, :in => Memento::Action::Base.action_types, :allow_blank => true
|
12
|
+
|
13
|
+
before_create :set_record_data
|
14
|
+
|
15
|
+
def self.store(action_type, record)
|
16
|
+
self.new(:action_type => action_type.to_s, :record => record) do |state|
|
17
|
+
state.save if state.fetch?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def undo
|
22
|
+
Memento::Result.new(action.undo, self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def record_data
|
26
|
+
@record_data ||= Memento.serializer.load(read_attribute(:record_data))
|
27
|
+
end
|
28
|
+
|
29
|
+
def record_data=(data)
|
30
|
+
@record_data = nil
|
31
|
+
write_attribute(:record_data, data.is_a?(String) ? data : Memento.serializer.dump(data))
|
32
|
+
end
|
33
|
+
|
34
|
+
def fetch?
|
35
|
+
action.fetch?
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def set_record_data
|
41
|
+
self.record_data = action.fetch
|
42
|
+
end
|
43
|
+
|
44
|
+
def action
|
45
|
+
"memento/action/#{action_type}".classify.constantize.new(self)
|
17
46
|
end
|
18
47
|
end
|
19
|
-
|
20
|
-
def undo
|
21
|
-
Memento::Result.new(action.undo, self)
|
22
|
-
end
|
23
|
-
|
24
|
-
def record_data
|
25
|
-
@record_data ||= Memento.serializer.load(read_attribute(:record_data))
|
26
|
-
end
|
27
|
-
|
28
|
-
def record_data=(data)
|
29
|
-
@record_data = nil
|
30
|
-
write_attribute(:record_data, data.is_a?(String) ? data : Memento.serializer.dump(data))
|
31
|
-
end
|
32
|
-
|
33
|
-
def fetch?
|
34
|
-
action.fetch?
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def set_record_data
|
40
|
-
self.record_data = action.fetch
|
41
|
-
end
|
42
|
-
|
43
|
-
def action
|
44
|
-
"memento/action/#{action_type}".classify.constantize.new(self)
|
45
|
-
end
|
46
|
-
|
47
48
|
end
|
data/lib/memento/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = "0.
|
1
|
+
module Memento
|
2
|
+
VERSION = "0.4.0"
|
3
3
|
end
|
data/memento.gemspec
CHANGED
@@ -11,12 +11,12 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.homepage = %q{http://github.com/yolk/memento}
|
12
12
|
s.summary = %q{Undo for Rails/ActiveRecord - covers destroy, update and create}
|
13
13
|
s.description = %q{Undo for Rails/ActiveRecord - covers destroy, update and create}
|
14
|
-
|
14
|
+
|
15
15
|
s.files = `git ls-files`.split("\n")
|
16
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
18
|
s.require_paths = ["lib"]
|
19
|
-
|
19
|
+
|
20
20
|
s.add_dependency 'activerecord', '~> 3.2.5'
|
21
21
|
s.add_dependency 'actionpack', '~> 3.2.5'
|
22
22
|
|
@@ -4,32 +4,32 @@ describe Memento::Action::Create, "when object is created" do
|
|
4
4
|
before do
|
5
5
|
setup_db
|
6
6
|
setup_data
|
7
|
-
Memento.
|
7
|
+
Memento.start(@user)
|
8
8
|
@project = Project.create!(:name => "P1", :closed_at => 3.days.ago).reload
|
9
|
-
Memento.
|
9
|
+
Memento.stop
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
after do
|
13
13
|
shutdown_db
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
it "should create memento_state for ar-object with no data" do
|
17
17
|
Memento::State.count.should eql(1)
|
18
18
|
Memento::State.first.action_type.should eql("create")
|
19
19
|
Memento::State.first.record.should eql(@project) # it was destroyed, remember?
|
20
20
|
Memento::State.first.reload.record_data.should eql(nil)
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
it "should create object" do
|
24
24
|
Project.find_by_id(@project.id).should_not be_nil
|
25
25
|
Project.count.should eql(1)
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
it "should allow undoing the creation" do
|
29
29
|
Memento::Session.last.undo
|
30
30
|
Project.count.should eql(0)
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
describe "when undoing the creation" do
|
34
34
|
it "should give back undone_object" do
|
35
35
|
Memento::Session.last.undo.map{|e| e.object.class }.should eql([Project])
|
@@ -42,7 +42,7 @@ describe Memento::Action::Create, "when object is created" do
|
|
42
42
|
undone.first.should_not be_success
|
43
43
|
undone.first.error.first.should be_was_changed
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
describe "when record was already destroyed" do
|
47
47
|
it "should give back fake unsaved record with id set" do
|
48
48
|
Project.last.destroy
|
@@ -56,24 +56,24 @@ describe Memento::Action::Create, "when object is created" do
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
59
|
-
|
60
|
-
|
61
|
-
|
59
|
+
|
60
|
+
|
61
|
+
|
62
62
|
end
|
63
63
|
|
64
64
|
describe Memento::Action::Create, "when object without timestamp is created" do
|
65
65
|
before do
|
66
66
|
setup_db
|
67
67
|
setup_data
|
68
|
-
Memento
|
68
|
+
Memento(@user) do
|
69
69
|
@obj = TimestamplessObject.create!(:name => "O1").reload
|
70
70
|
end
|
71
71
|
end
|
72
|
-
|
72
|
+
|
73
73
|
after do
|
74
74
|
shutdown_db
|
75
75
|
end
|
76
|
-
|
76
|
+
|
77
77
|
describe "when undoing the creation" do
|
78
78
|
it "should give back undone_object" do
|
79
79
|
Memento::Session.last.undo.map{|e| e.object.class }.should eql([TimestamplessObject])
|
@@ -5,27 +5,27 @@ describe Memento::Action::Destroy, "when object gets destroyed" do
|
|
5
5
|
setup_db
|
6
6
|
setup_data
|
7
7
|
@project = Project.create!(:name => "P1", :closed_at => 3.days.ago).reload
|
8
|
-
Memento.
|
8
|
+
Memento.start(@user)
|
9
9
|
@project.destroy
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
after do
|
13
|
-
Memento.
|
13
|
+
Memento.stop
|
14
14
|
shutdown_db
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
it "should create memento_state for ar-object with full attributes_for_memento" do
|
18
18
|
Memento::State.count.should eql(1)
|
19
19
|
Memento::State.first.action_type.should eql("destroy")
|
20
20
|
Memento::State.first.record.should be_nil # it was destroyed, remember?
|
21
21
|
Memento::State.first.reload.record_data.should == @project.attributes_for_memento
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
it "should destroy object" do
|
25
25
|
Project.find_by_id(@project.id).should be_nil
|
26
26
|
Project.count.should be_zero
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
it "should allow undoing the destruction" do
|
30
30
|
Project.count.should be_zero
|
31
31
|
Memento::Session.last.undo
|
@@ -34,10 +34,10 @@ describe Memento::Action::Destroy, "when object gets destroyed" do
|
|
34
34
|
@project.attributes_for_memento.reject{|k, v| k.to_sym == :id }
|
35
35
|
)
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
it "should give back undone_object on undoing the destruction" do
|
39
39
|
Memento::Session.last.undo.map{|e| e.object.class }.should eql([Project])
|
40
40
|
end
|
41
|
-
|
42
|
-
|
41
|
+
|
42
|
+
|
43
43
|
end
|