memento 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +85 -0
- data/Rakefile +33 -0
- data/TODO +3 -0
- data/VERSION.yml +5 -0
- data/generators/memento_migration/memento_migration_generator.rb +10 -0
- data/generators/memento_migration/templates/migration.rb +23 -0
- data/lib/memento.rb +59 -0
- data/lib/memento/action.rb +40 -0
- data/lib/memento/action/create.rb +37 -0
- data/lib/memento/action/destroy.rb +30 -0
- data/lib/memento/action/update.rb +55 -0
- data/lib/memento/action_controller_methods.rb +19 -0
- data/lib/memento/active_record_methods.rb +74 -0
- data/lib/memento/result.rb +36 -0
- data/lib/memento/session.rb +29 -0
- data/lib/memento/state.rb +47 -0
- data/memento.gemspec +79 -0
- data/rails/init.rb +1 -0
- data/spec/memento/action/create_spec.rb +82 -0
- data/spec/memento/action/destroy_spec.rb +43 -0
- data/spec/memento/action/update_spec.rb +213 -0
- data/spec/memento/action_controller_methods_spec.rb +55 -0
- data/spec/memento/active_record_methods_spec.rb +65 -0
- data/spec/memento/result_spec.rb +89 -0
- data/spec/memento/session_spec.rb +117 -0
- data/spec/memento/state_spec.rb +55 -0
- data/spec/memento_spec.rb +169 -0
- data/spec/spec_helper.rb +91 -0
- metadata +95 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Yolk Sebastian Munz & Julia Soergel GbR
|
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.rdoc
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
= memento
|
2
|
+
|
3
|
+
RubyGem/Plugin for undo in Rails/ActiveRecord - covers destroy, update and create actions.
|
4
|
+
|
5
|
+
== Install
|
6
|
+
|
7
|
+
memento will only work with Rails 2.3 or above.
|
8
|
+
|
9
|
+
== as a ruby-gem
|
10
|
+
|
11
|
+
Add this line to your config/environment.rb:
|
12
|
+
|
13
|
+
config.gem "yolk-memento", :lib => 'memento', :source => 'http://gems.github.com'
|
14
|
+
|
15
|
+
and run
|
16
|
+
|
17
|
+
rake gems:install
|
18
|
+
|
19
|
+
=== as rails plugin
|
20
|
+
|
21
|
+
script/plugin install git://github.com/yolk/memento.git
|
22
|
+
|
23
|
+
== Setup
|
24
|
+
|
25
|
+
memento needs two tables in your database, one to store "sessions" (sets of states) and the other to store "states" (aka snapshots of single models). To generate the necessary migration and migrate your database run:
|
26
|
+
|
27
|
+
script/generate memento_migration
|
28
|
+
rake db:migrate
|
29
|
+
|
30
|
+
memento assumes you have a user-model. Every session is owned by a user.
|
31
|
+
|
32
|
+
== Configure your models
|
33
|
+
|
34
|
+
Then you have to tell every model you want to undo actions on that it should be watched by memento:
|
35
|
+
|
36
|
+
class Person < ActiveRecord::Base
|
37
|
+
memento_changes
|
38
|
+
end
|
39
|
+
|
40
|
+
This will tell memento to create snapshots of the model when an new instance is created, an exisiting one is updated or destroyed.
|
41
|
+
|
42
|
+
If you want memento to only take snapshots on specific actions:
|
43
|
+
|
44
|
+
memento_changes :update, :destroy
|
45
|
+
|
46
|
+
This will take a snapshot only when an instance is updated or destroyed.
|
47
|
+
|
48
|
+
By default memento will ignore changes to the :updated_at and :created_at attributes. You can add further attributes to ignore with the :ignore option:
|
49
|
+
|
50
|
+
memento_changes :ignore => [:calculated_birthday, :friends_count]
|
51
|
+
|
52
|
+
This will ignore changes on the calculated_birthday and the firends_count-attributes. When memento saves a whole instance of your model before it is destroyed, those attributes will not be stored for later recovery. Only ignore attributes you can re-calculate from other data!
|
53
|
+
|
54
|
+
== Action!
|
55
|
+
|
56
|
+
When you perform any of the configured actions on your model in isolation in your controller memento will not store any changes:
|
57
|
+
|
58
|
+
Person.create!(:name => "Blah")
|
59
|
+
Memento::Session.count # => 0
|
60
|
+
|
61
|
+
You have to wrap every action block you want memento to track in your controller with the memento-method:
|
62
|
+
|
63
|
+
memento do
|
64
|
+
Person.create!(:name => "Blah")
|
65
|
+
end
|
66
|
+
Memento::Session.count # => 1
|
67
|
+
|
68
|
+
This assumes there is an method called "current_user" in your controllers. It will set the HTTP-Header 'X-Memento-Session-Id' on your response.
|
69
|
+
|
70
|
+
If you want memento to watch changes outside of your controllers (for example inside the console) you can use:
|
71
|
+
|
72
|
+
Memento.instance.memento(user) do
|
73
|
+
Person.create!(:name => "Blah")
|
74
|
+
end
|
75
|
+
|
76
|
+
Where the variable user is assumed to hold an instance of User.
|
77
|
+
|
78
|
+
== Undo!
|
79
|
+
|
80
|
+
Undoing this changes is as simple as calling #undo on an memento-session-instance.
|
81
|
+
|
82
|
+
Memento::Session.first.undo
|
83
|
+
|
84
|
+
|
85
|
+
Copyright (c) 2009-2010 Yolk Sebastian Munz & Julia Soergel GbR. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "memento"
|
8
|
+
gem.summary = %Q{Undo for Rails/ActiveRecord - covers destroy, update and create}
|
9
|
+
gem.email = "sebastian@yo.lk"
|
10
|
+
gem.homepage = "http://github.com/yolk/memento"
|
11
|
+
gem.authors = ["Yolk Sebastian Munz & Julia Soergel GbR"]
|
12
|
+
|
13
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
14
|
+
end
|
15
|
+
Jeweler::GemcutterTasks.new
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'spec/rake/spectask'
|
21
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
22
|
+
spec.libs << 'lib' << 'spec'
|
23
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
24
|
+
end
|
25
|
+
|
26
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
27
|
+
spec.libs << 'lib' << 'spec'
|
28
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
29
|
+
spec.rcov = true
|
30
|
+
end
|
31
|
+
|
32
|
+
task :default => :spec
|
33
|
+
|
data/TODO
ADDED
data/VERSION.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
class MementoMigration < ActiveRecord::Migration
|
2
|
+
|
3
|
+
def self.up
|
4
|
+
create_table :memento_sessions do |t|
|
5
|
+
t.references :user
|
6
|
+
t.timestamps
|
7
|
+
end
|
8
|
+
|
9
|
+
create_table :memento_states do |t|
|
10
|
+
t.string :action_type
|
11
|
+
t.binary :record_data, :limit => 1.megabytes
|
12
|
+
t.references :record, :polymorphic => true
|
13
|
+
t.references :session
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.down
|
19
|
+
drop_table :memento_states
|
20
|
+
drop_table :memento_sessions
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/lib/memento.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
class Memento
|
2
|
+
include Singleton
|
3
|
+
|
4
|
+
class ErrorOnRewind < StandardError;end
|
5
|
+
|
6
|
+
def memento(user)
|
7
|
+
start(user)
|
8
|
+
yield
|
9
|
+
!@session.states.count.zero? && @session rescue false
|
10
|
+
ensure
|
11
|
+
stop
|
12
|
+
end
|
13
|
+
|
14
|
+
def start(user_or_id)
|
15
|
+
user = User.find_by_id(user_or_id)
|
16
|
+
@session = user ? Memento::Session.new(:user => user) : nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def stop
|
20
|
+
@session.destroy if @session && @session.states.count.zero?
|
21
|
+
@session = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_state(action_type, record)
|
25
|
+
return unless save_session
|
26
|
+
@session.add_state(action_type, record)
|
27
|
+
end
|
28
|
+
|
29
|
+
def active?
|
30
|
+
!!(defined?(@session) && @session) && !ignore?
|
31
|
+
end
|
32
|
+
|
33
|
+
def ignore
|
34
|
+
@ignore = true
|
35
|
+
yield
|
36
|
+
ensure
|
37
|
+
@ignore = false
|
38
|
+
end
|
39
|
+
|
40
|
+
cattr_accessor :serializer
|
41
|
+
self.serializer = YAML
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def ignore?
|
46
|
+
defined?(@ignore) && @ignore
|
47
|
+
end
|
48
|
+
|
49
|
+
def save_session
|
50
|
+
active? && (!@session.changed? || @session.save)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
require 'memento/result'
|
55
|
+
require 'memento/action'
|
56
|
+
require 'memento/active_record_methods'
|
57
|
+
require 'memento/action_controller_methods'
|
58
|
+
require 'memento/state'
|
59
|
+
require 'memento/session'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Memento::Action
|
2
|
+
class Base
|
3
|
+
def initialize(state)
|
4
|
+
@state = state
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :state
|
8
|
+
|
9
|
+
def record
|
10
|
+
@state.record
|
11
|
+
end
|
12
|
+
|
13
|
+
def record_data
|
14
|
+
@state.record_data
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.inherited(child)
|
22
|
+
action_type = child.name.demodulize.underscore
|
23
|
+
write_inheritable_attribute(:action_types, action_types << action_type)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.action_types
|
27
|
+
read_inheritable_attribute(:action_types) || []
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def new_object
|
33
|
+
object = @state.record_type.constantize.new
|
34
|
+
yield(object) if block_given?
|
35
|
+
object
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Dir["#{File.dirname(__FILE__)}/action/*.rb"].each { |action| require action }
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Memento::Action::Create < Memento::Action::Base
|
2
|
+
|
3
|
+
def fetch;end
|
4
|
+
|
5
|
+
def undo
|
6
|
+
if record.nil?
|
7
|
+
build_fake_object
|
8
|
+
elsif record_was_changed?
|
9
|
+
was_changed
|
10
|
+
else
|
11
|
+
destroy_record
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def record_was_changed?
|
18
|
+
record.updated_at > record.created_at rescue false
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_fake_object
|
22
|
+
new_object do |object|
|
23
|
+
object.id = state.record_id
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def was_changed
|
28
|
+
record.errors.add(:memento_undo, ActiveSupport::StringInquirer.new("was_changed"))
|
29
|
+
record
|
30
|
+
end
|
31
|
+
|
32
|
+
def destroy_record
|
33
|
+
record.destroy
|
34
|
+
record
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Memento::Action::Destroy < Memento::Action::Base
|
2
|
+
|
3
|
+
def fetch
|
4
|
+
record.attributes_for_memento
|
5
|
+
end
|
6
|
+
|
7
|
+
def undo
|
8
|
+
rebuild_object do |object|
|
9
|
+
begin
|
10
|
+
object.save!
|
11
|
+
rescue
|
12
|
+
object.id = nil
|
13
|
+
object.save!
|
14
|
+
end
|
15
|
+
state.update_attribute(:record, object)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def rebuild_object
|
22
|
+
new_object do |object|
|
23
|
+
state.record_data.each do |attribute, value|
|
24
|
+
object.send(:"#{attribute}=", value)
|
25
|
+
end
|
26
|
+
yield(object) if block_given?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class Memento::Action::Update < Memento::Action::Base
|
2
|
+
|
3
|
+
def fetch
|
4
|
+
record.changes_for_memento
|
5
|
+
end
|
6
|
+
|
7
|
+
def fetch?
|
8
|
+
record.changes_for_memento.any?
|
9
|
+
end
|
10
|
+
|
11
|
+
def undo
|
12
|
+
if !record
|
13
|
+
was_destroyed
|
14
|
+
elsif mergable?
|
15
|
+
update_record
|
16
|
+
else
|
17
|
+
was_changed
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def update_record
|
24
|
+
returning(record) do |object|
|
25
|
+
record_data.each do |attribute, values|
|
26
|
+
object.send(:"#{attribute}=", values.first)
|
27
|
+
end
|
28
|
+
object.save!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def mergable?
|
33
|
+
record_data.all? do |attribute, values|
|
34
|
+
# ugly fix to compare times
|
35
|
+
values = values.map{|v| v.is_a?(Time) ? v.to_s(:db) : v }
|
36
|
+
current_value = record.send(:"#{attribute}")
|
37
|
+
current_value = current_value.utc.to_s(:db) if current_value.is_a?(Time)
|
38
|
+
|
39
|
+
values.include?(current_value)
|
40
|
+
end || record_data.size.zero?
|
41
|
+
end
|
42
|
+
|
43
|
+
def was_destroyed
|
44
|
+
new_object do |object|
|
45
|
+
object.errors.add(:memento_undo, ActiveSupport::StringInquirer.new("was_destroyed"))
|
46
|
+
object.id = state.record_id
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def was_changed
|
51
|
+
record.errors.add(:memento_undo, ActiveSupport::StringInquirer.new("was_changed"))
|
52
|
+
record
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Memento
|
2
|
+
module ActionControllerMethods
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def memento
|
7
|
+
block_result = nil
|
8
|
+
memento_session = Memento.instance.memento(current_user) do
|
9
|
+
block_result = yield
|
10
|
+
end
|
11
|
+
if memento_session
|
12
|
+
response.headers["X-Memento-Session-Id"] = memento_session.id.to_s
|
13
|
+
end
|
14
|
+
block_result
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ActionController::Base.send(:include, Memento::ActionControllerMethods) if defined?(ActionController::Base)
|