memento 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
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
@@ -0,0 +1,3 @@
1
+ * [DONE] scrip/generate memento_migration
2
+ * configs for current_user & User-Object
3
+ * [DONE] readme/documentation
data/VERSION.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ :build:
3
+ :patch: 0
4
+ :major: 0
5
+ :minor: 3
@@ -0,0 +1,10 @@
1
+ class MementoMigrationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template("migration.rb", 'db/migrate',
5
+ :assigns => { },
6
+ :migration_file_name => "memento_migration"
7
+ )
8
+ end
9
+ end
10
+ end
@@ -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)