memento 0.3.7 → 0.4.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/.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
|