yolk-memento 0.2.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.
@@ -0,0 +1,36 @@
1
+ class Memento::ResultArray < Array
2
+
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
+
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
+ @object.errors[:memento_undo]
27
+ end
28
+
29
+ def failed?
30
+ !!error
31
+ end
32
+
33
+ def success?
34
+ !failed?
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ class Memento::Session < ActiveRecord::Base
2
+ set_table_name "memento_sessions"
3
+
4
+ has_many :states, :class_name => "Memento::State", :dependent => :delete_all
5
+ belongs_to :user
6
+ validates_presence_of :user
7
+
8
+ def add_state(action_type, record)
9
+ states.store(action_type, record)
10
+ end
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
16
+ end
17
+ ensure
18
+ destroy if states.count.zero?
19
+ end
20
+
21
+ def undo!
22
+ transaction do
23
+ returning(undo) do |results|
24
+ raise Memento::ErrorOnRewind if results.failed?
25
+ end
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,71 @@
1
+ class Memento::State < ActiveRecord::Base
2
+ set_table_name "memento_states"
3
+
4
+ belongs_to :session, :class_name => "Memento::Session"
5
+ belongs_to :record, :polymorphic => true
6
+
7
+ validates_presence_of :session
8
+ validates_presence_of :record
9
+ validates_presence_of :action_type
10
+ validates_inclusion_of :action_type, :in => Memento::Action::Base.action_types, :allow_blank => true
11
+
12
+ before_create :set_record_data
13
+
14
+ def self.store(action_type, record)
15
+ self.new(:action_type => action_type.to_s, :record => record) do |state|
16
+ state.save if state.fetch?
17
+ end
18
+ end
19
+
20
+ def undo
21
+ Memento::Result.new(action.undo, self)
22
+ end
23
+
24
+ def record_data
25
+ @record_data ||= Marshal.load(super)
26
+ end
27
+
28
+ def record_data=(data)
29
+ @record_data = nil
30
+ super(Marshal.dump(data))
31
+ end
32
+
33
+ def fetch?
34
+ action.fetch?
35
+ end
36
+
37
+ def new_object
38
+ object = record_type.constantize.new
39
+ yield(object) if block_given?
40
+ object
41
+ end
42
+
43
+ def rebuild_object(*skip)
44
+ skip = skip ? skip.map(&:to_sym) : []
45
+ new_object do |object|
46
+ record_data.each do |attribute, value|
47
+ object.send(:"#{attribute}=", value) unless skip.include?(attribute.to_sym)
48
+ end
49
+ yield(object) if block_given?
50
+ end
51
+ end
52
+
53
+ def later_states_on_record_for(action_type_param)
54
+ Memento::State.all(:conditions => [
55
+ "record_id = ? AND record_type = ? AND " +
56
+ "action_type = ? AND created_at >= ? AND id != ? ",
57
+ record_id, record_type, action_type_param.to_s, created_at, id
58
+ ])
59
+ end
60
+
61
+ private
62
+
63
+ def set_record_data
64
+ self.record_data = action.fetch
65
+ end
66
+
67
+ def action
68
+ "memento/action/#{action_type}".classify.constantize.new(self)
69
+ end
70
+
71
+ end
data/memento.gemspec ADDED
@@ -0,0 +1,75 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{memento}
5
+ s.version = "0.2.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Yolk Sebastian Munz & Julia Soergel GbR"]
9
+ s.date = %q{2009-07-09}
10
+ s.email = %q{sebastian@yo.lk}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".gitignore",
17
+ "LICENSE",
18
+ "README.rdoc",
19
+ "Rakefile",
20
+ "TODO",
21
+ "VERSION.yml",
22
+ "generators/memento_migration/memento_migration_generator.rb",
23
+ "generators/memento_migration/templates/migration.rb",
24
+ "lib/memento.rb",
25
+ "lib/memento/action.rb",
26
+ "lib/memento/action/create.rb",
27
+ "lib/memento/action/destroy.rb",
28
+ "lib/memento/action/update.rb",
29
+ "lib/memento/action_controller_methods.rb",
30
+ "lib/memento/active_record_methods.rb",
31
+ "lib/memento/result.rb",
32
+ "lib/memento/session.rb",
33
+ "lib/memento/state.rb",
34
+ "memento.gemspec",
35
+ "rails/init.rb",
36
+ "spec/memento/action/create_spec.rb",
37
+ "spec/memento/action/destroy_spec.rb",
38
+ "spec/memento/action/update_spec.rb",
39
+ "spec/memento/action_controller_methods_spec.rb",
40
+ "spec/memento/active_record_methods_spec.rb",
41
+ "spec/memento/result_spec.rb",
42
+ "spec/memento/session_spec.rb",
43
+ "spec/memento/state_spec.rb",
44
+ "spec/memento_spec.rb",
45
+ "spec/spec_helper.rb"
46
+ ]
47
+ s.has_rdoc = true
48
+ s.homepage = %q{http://github.com/yolk/memento}
49
+ s.rdoc_options = ["--charset=UTF-8"]
50
+ s.require_paths = ["lib"]
51
+ s.rubygems_version = %q{1.3.2}
52
+ s.summary = %q{Undo for Rails/ActiveRecord - covers destroy, update and create}
53
+ s.test_files = [
54
+ "spec/memento/action/create_spec.rb",
55
+ "spec/memento/action/destroy_spec.rb",
56
+ "spec/memento/action/update_spec.rb",
57
+ "spec/memento/action_controller_methods_spec.rb",
58
+ "spec/memento/active_record_methods_spec.rb",
59
+ "spec/memento/result_spec.rb",
60
+ "spec/memento/session_spec.rb",
61
+ "spec/memento/state_spec.rb",
62
+ "spec/memento_spec.rb",
63
+ "spec/spec_helper.rb"
64
+ ]
65
+
66
+ if s.respond_to? :specification_version then
67
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
68
+ s.specification_version = 3
69
+
70
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
71
+ else
72
+ end
73
+ else
74
+ end
75
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'memento'
@@ -0,0 +1,96 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ describe Memento::Action::Create, "when object is created" do
4
+ before do
5
+ setup_db
6
+ setup_data
7
+ Memento.instance.start(@user)
8
+ @project = Project.create!(:name => "P1", :closed_at => 3.days.ago).reload
9
+ Memento.instance.stop
10
+ end
11
+
12
+ after do
13
+ shutdown_db
14
+ end
15
+
16
+ it "should create memento_state for ar-object with no data" do
17
+ Memento::State.count.should eql(1)
18
+ Memento::State.first.action_type.should eql("create")
19
+ Memento::State.first.record.should eql(@project) # it was destroyed, remember?
20
+ Memento::State.first.reload.record_data.should eql(nil)
21
+ end
22
+
23
+ it "should create object" do
24
+ Project.find_by_id(@project.id).should_not be_nil
25
+ Project.count.should eql(1)
26
+ end
27
+
28
+ it "should allow undoing the creation" do
29
+ Memento::Session.last.undo
30
+ Project.count.should eql(0)
31
+ end
32
+
33
+ describe "when undoing the creation" do
34
+ it "should give back undone_object" do
35
+ Memento::Session.last.undo.map{|e| e.object.class }.should eql([Project])
36
+ end
37
+
38
+ it "should not undo the creatio if object was modified" do
39
+ Project.last.update_attribute(:created_at, 1.minute.ago)
40
+ undone = Memento::Session.last.undo
41
+ Project.count.should eql(1)
42
+ undone.first.should_not be_success
43
+ undone.first.error.should be_was_changed
44
+ end
45
+
46
+ describe "when record was already destroyed" do
47
+
48
+ it "should give back fake unsaved record with id set" do
49
+ Project.last.destroy
50
+ @undone = Memento::Session.last.undo
51
+ @undone.size.should eql(1)
52
+ @undone.first.object.should be_kind_of(Project)
53
+ @undone.first.object.id.should eql(@project.id)
54
+ @undone.first.object.name.should be_nil
55
+ @undone.first.object.should be_new_record
56
+ Project.count.should eql(0)
57
+ end
58
+
59
+ it "should give back fake unsaved record with all data set when destruction was stateed" do
60
+ Memento.instance.memento(@user) { Project.last.destroy }
61
+ Memento::State.last.update_attribute(:created_at, 5.minutes.from_now)
62
+ @undone = Memento::Session.first.undo
63
+ @undone.size.should eql(1)
64
+ @undone.first.object.should be_kind_of(Project)
65
+ @undone.first.object.id.should eql(@project.id)
66
+ @undone.first.object.name.should eql(@project.name)
67
+ @undone.first.object.closed_at.should eql(@project.closed_at)
68
+ @undone.first.object.should be_new_record
69
+ Project.count.should eql(0)
70
+ end
71
+ end
72
+ end
73
+
74
+
75
+
76
+ end
77
+
78
+ describe Memento::Action::Create, "when object without timestamp is created" do
79
+ before do
80
+ setup_db
81
+ setup_data
82
+ Memento.instance.memento(@user) do
83
+ @obj = TimestamplessObject.create!(:name => "O1").reload
84
+ end
85
+ end
86
+
87
+ after do
88
+ shutdown_db
89
+ end
90
+
91
+ describe "when undoing the creation" do
92
+ it "should give back undone_object" do
93
+ Memento::Session.last.undo.map{|e| e.object.class }.should eql([TimestamplessObject])
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,43 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ describe Memento::Action::Destroy, "when object gets destroyed" do
4
+ before do
5
+ setup_db
6
+ setup_data
7
+ @project = Project.create!(:name => "P1", :closed_at => 3.days.ago).reload
8
+ Memento.instance.start(@user)
9
+ @project.destroy
10
+ end
11
+
12
+ after do
13
+ Memento.instance.stop
14
+ shutdown_db
15
+ end
16
+
17
+ it "should create memento_state for ar-object with full attributes_for_memento" do
18
+ Memento::State.count.should eql(1)
19
+ Memento::State.first.action_type.should eql("destroy")
20
+ Memento::State.first.record.should be_nil # it was destroyed, remember?
21
+ Memento::State.first.reload.record_data.should == @project.attributes_for_memento
22
+ end
23
+
24
+ it "should destroy object" do
25
+ Project.find_by_id(@project.id).should be_nil
26
+ Project.count.should be_zero
27
+ end
28
+
29
+ it "should allow undoing the destruction" do
30
+ Project.count.should be_zero
31
+ Memento::Session.last.undo
32
+ Project.count.should eql(1)
33
+ Project.first.attributes_for_memento.reject{|k, v| k.to_sym == :id }.should == (
34
+ @project.attributes_for_memento.reject{|k, v| k.to_sym == :id }
35
+ )
36
+ end
37
+
38
+ it "should give back undone_object on undoing the destruction" do
39
+ Memento::Session.last.undo.map{|e| e.object.class }.should eql([Project])
40
+ end
41
+
42
+
43
+ end
@@ -0,0 +1,209 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ describe Memento::Action::Update do
4
+ before do
5
+ setup_db
6
+ setup_data
7
+ @time1 = 3.days.ago
8
+ @time2 = 2.days.ago
9
+ @project = Project.create!(:name => "P1", :closed_at => @time1, :notes => "Bla bla").reload
10
+ @customer = Customer.create!(:name => "C1")
11
+ end
12
+
13
+ after do
14
+ shutdown_db
15
+ end
16
+
17
+ describe "when object gets updated" do
18
+
19
+ before do
20
+ Memento.instance.memento(@user) do
21
+ @project.update_attributes(:name => "P2", :closed_at => @time2, :customer => @customer, :notes => "Bla bla")
22
+ end
23
+ end
24
+
25
+ it "should create memento_state for ar-object from changes_for_memento" do
26
+ Memento::State.count.should eql(1)
27
+ Memento::State.first.action_type.should eql("update")
28
+ Memento::State.first.record.should eql(@project)
29
+ Memento::State.first.record_data.keys.sort.should eql(%w(name closed_at customer_id).sort)
30
+ Memento::State.first.record_data["name"].should eql(["P1", "P2"])
31
+ Memento::State.first.record_data["customer_id"].should eql([nil, @customer.id])
32
+ Memento::State.first.record_data["closed_at"][0].utc.to_s.should eql(@time1.utc.to_s)
33
+ Memento::State.first.record_data["closed_at"][1].utc.to_s.should eql(@time2.utc.to_s)
34
+ end
35
+
36
+ it "should update object" do
37
+ @project.reload.name.should eql("P2")
38
+ @project.customer.should eql(@customer)
39
+ @project.closed_at.to_s.should eql(@time2.to_s)
40
+ @project.should_not be_changed
41
+ Project.count.should eql(1)
42
+ end
43
+
44
+ it "should allow undoing the update" do
45
+ undone = Memento::Session.last.undo.first
46
+ undone.should be_success
47
+ undone.object.should_not be_changed
48
+ undone.object.name.should eql("P1")
49
+ undone.object.customer.should be_nil
50
+ undone.object.closed_at.to_s.should eql(@time1.to_s)
51
+ end
52
+
53
+ describe "when record was destroyed before undo" do
54
+ before do
55
+ @project.destroy
56
+ end
57
+
58
+ it "should return fake object with error" do
59
+ undone = Memento::Session.last.undo.first
60
+ undone.should_not be_success
61
+ undone.error.should be_was_destroyed
62
+ undone.object.class.should eql(Project)
63
+ undone.object.id.should eql(1)
64
+ end
65
+ end
66
+
67
+ describe "when record was changed before undo" do
68
+
69
+ describe "with mergeable unstateed changes" do
70
+ before do
71
+ @project.update_attributes({:notes => "Bla!"})
72
+ @result = Memento::Session.first.undo.first
73
+ @object = @result.object
74
+ end
75
+
76
+ it "should be success" do
77
+ @result.should be_success
78
+ end
79
+
80
+ it "should return correctly updated object" do
81
+ @object.class.should eql(Project)
82
+ @object.name.should eql("P1")
83
+ @object.customer.should be_nil
84
+ @object.closed_at.to_s.should eql(@time1.to_s)
85
+ @object.notes.should eql("Bla!")
86
+ end
87
+ end
88
+
89
+ describe "with mergeable stateed changes" do
90
+ before do
91
+ Memento.instance.memento(@user) do
92
+ @project.update_attributes({:notes => "Bla!"})
93
+ end
94
+ Memento::State.last.update_attribute(:created_at, 1.minute.from_now)
95
+ @result = Memento::Session.first.undo.first
96
+ @object = @result.object
97
+ end
98
+
99
+ it "should be success" do
100
+ @result.should be_success
101
+ end
102
+
103
+ it "should return correctly updated object" do
104
+ @object.class.should eql(Project)
105
+ @object.name.should eql("P1")
106
+ @object.customer.should be_nil
107
+ @object.closed_at.to_s.should eql(@time1.to_s)
108
+ @object.notes.should eql("Bla!")
109
+ end
110
+
111
+ describe "when second state is undone" do
112
+ before do
113
+ @result = Memento::Session.first.undo.first
114
+ @object = @result.object
115
+ end
116
+
117
+ it "should be success" do
118
+ @result.should be_success
119
+ end
120
+
121
+ it "should return correctly updated object" do
122
+ @object.class.should eql(Project)
123
+ @object.name.should eql("P1")
124
+ @object.customer.should be_nil
125
+ @object.closed_at.to_s.should eql(@time1.to_s)
126
+ @object.notes.should eql("Bla bla")
127
+ end
128
+ end
129
+ end
130
+
131
+ describe "with unmergeable unstateed changes" do
132
+ before do
133
+ @project.update_attributes({:name => "P3"})
134
+ @result = Memento::Session.last.undo.first
135
+ @object = @result.object
136
+ end
137
+
138
+ it "should fail" do
139
+ @result.should be_failed
140
+ end
141
+
142
+ it "should return not undone object" do
143
+ @object.name.should eql("P3")
144
+ @object.customer.should eql(@customer)
145
+ @object.closed_at.to_s.should eql(@time2.to_s)
146
+ @object.should_not be_changed
147
+ end
148
+ end
149
+
150
+ describe "with unmergeable stateed changes" do
151
+ before do
152
+ Memento.instance.memento(@user) do
153
+ @project.update_attributes!({:name => "P3"})
154
+ end
155
+ Memento::State.last.update_attribute(:created_at, 1.minute.from_now)
156
+ @result = Memento::Session.first.undo.first
157
+ @object = @result.object
158
+ end
159
+
160
+ it "should fail" do
161
+ @result.should be_failed
162
+ end
163
+
164
+ it "should return not undone object" do
165
+ @object.name.should eql("P3")
166
+ @object.customer.should eql(@customer)
167
+ @object.closed_at.to_s.should eql(@time2.to_s)
168
+ @object.should_not be_changed
169
+ end
170
+
171
+ describe "when second state is undone" do
172
+ before do
173
+ @result = Memento::Session.last.undo.first
174
+ @object = @result.object
175
+ end
176
+
177
+ it "should be success" do
178
+ @result.should be_success
179
+ end
180
+
181
+ it "should return correctly updated object" do
182
+ @object.class.should eql(Project)
183
+ @object.name.should eql("P2")
184
+ @object.customer.should eql(@customer)
185
+ @object.closed_at.to_s.should eql(@time2.to_s)
186
+ @object.notes.should eql("Bla bla")
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ end
193
+
194
+ describe "when object gets updated with no changes" do
195
+
196
+ before do
197
+ Memento.instance.memento(@user) do
198
+ @project.update_attributes(:name => "P1", :customer => nil, :notes => "Bla bla")
199
+ end
200
+ end
201
+
202
+ it "should not create session/state" do
203
+ Memento::Session.count.should eql(0)
204
+ Memento::State.count.should eql(0)
205
+ end
206
+
207
+ end
208
+
209
+ end