kb-acts_as_revisable 1.0.3

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 JamLab, LLC.
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,221 @@
1
+ = acts_as_revisable
2
+
3
+ http://github.com/rich/acts_as_revisable
4
+
5
+ == DESCRIPTION:
6
+
7
+ acts_as_revisable enables revision tracking, querying, reverting and branching of ActiveRecord models. It does this while providing more Rails-like API than similar plugins. This includes extensions of standard ActiveRecord methods and numerous custom callbacks for the entire AAR life-cycle.
8
+
9
+ This plugin wouldn't exist without Rick Olsen's acts_as_versioned. AAV has been a critical part of practically every Rails project I've developed. It's only through extensive usage of AAV that the concepts for AAR came about.
10
+
11
+ == FEATURES:
12
+
13
+ * Both the revisable and revision models must be explicitly defined.
14
+ Yes, this is a feature. The less magic needed the better. This allows you to build up your revision models just as you would any other.
15
+
16
+ If you absolutely need a generated revision model, you may pass ":generate_revision_class => true" to acts_as_revisable and it will generate the class at runtime for you. Think of this like scaffolding and not to be kept around for a real application.
17
+
18
+ * Numerous custom callbacks for both revisable and revision models.
19
+ * revisable models
20
+ * before_revise
21
+ * after_revise
22
+ * before_revert
23
+ * after_revert
24
+ * before_changeset
25
+ * after_changeset
26
+ * after_branch_created
27
+ * before_revise_on_destroy (when :on_destroy => :revise is set)
28
+ * after_revise_on_destroy (when :on_destroy => :revise is set)
29
+ * revision models
30
+ * before_restore
31
+ * after_restore
32
+ * both revisable and revision models
33
+ * before_branch
34
+ * after_branch
35
+ These work like any other ActiveRecord callbacks. The before_* callbacks can stop the the action. This uses the Callbacks module in ActiveSupport.
36
+ * Works with a single table.
37
+ * Provides migration generators to add the revisable columns.
38
+ * Grouping several revisable actions into a single revision (changeset).
39
+ * Monitor all or just specified columns to trigger a revision.
40
+ * Uses ActiveRecord's dirty attribute tracking.
41
+ * Several ways to find revisions including:
42
+ * revision number
43
+ * relative keywords (:first, :previous and :last)
44
+ * timestamp
45
+ * Reverting
46
+ * Branching
47
+ * Selectively disable revision tracking
48
+ * Naming revisions
49
+
50
+ == SYNOPSIS:
51
+
52
+ Given a simple model:
53
+
54
+ class Project < ActiveRecord::Base
55
+ # columns: id, name, unimportant, created_at
56
+ end
57
+
58
+ Let's make the projects table revisable:
59
+
60
+ ruby script/generate revisable_migration Project
61
+ rake db:migrate
62
+
63
+ Now Project itself:
64
+
65
+ class Project < ActiveRecord::Base
66
+ has_one :owner
67
+
68
+ acts_as_revisable do
69
+ revision_class_name "Session"
70
+ except :unimportant
71
+ end
72
+ end
73
+
74
+ Create the revision class:
75
+
76
+ class Session < ActiveRecord::Base
77
+ # we can accept the more standard hash syntax
78
+ acts_as_revision :revisable_class_name => "Project"
79
+ end
80
+
81
+ Some example usage:
82
+
83
+ @project = Project.create(:name => "Rich", :unimportant => "some text")
84
+ @project.revision_number # => 0
85
+
86
+ @project.update_attribute(:unimportant, "more text")
87
+ @project.revision_number # => 0
88
+
89
+ @project.name = "Stephen"
90
+ @project.save(:without_revision => true)
91
+ @project.name # => "Stephen"
92
+ @project.revision_number # => 0
93
+
94
+ @project.name = "Sam"
95
+ @project.save(:revision_name => "Changed name")
96
+ @project.revision_number # => 1
97
+
98
+ @project.updated_attribute(:name, "Third")
99
+ @project.revision_number # => 2
100
+
101
+ Navigating revisions:
102
+
103
+ @previous = @project.find_revision(:previous)
104
+ # or
105
+ @previous = @project.revisions.first
106
+
107
+ @previous.name # => "Sam"
108
+ @previous.current_revision.name # => "Third"
109
+ @previous.project.name # => "Third"
110
+ @previous.revision_name # => "Changed name"
111
+
112
+ @previous.previous.name # => "Rich"
113
+
114
+ # Forcing the creation of a new revision.
115
+ @project.updated_attribute("Rogelio")
116
+ @project.revision_number # => 3
117
+
118
+ @newest = @project.find_revision(:previous)
119
+ @newest.ancestors.map(&:name) # => ["Third", "Rich"]
120
+
121
+ @oldest = @project.find_revision(:first)
122
+ @oldest.descendants.map(&:name) # => ["Sam", "Third"]
123
+
124
+ Reverting:
125
+
126
+ @project.revert_to!(:previous)
127
+ @project.revision_number # => 2
128
+ @project.name # => "Rich"
129
+
130
+ @project.revert_to!(1, :without_revision => true)
131
+ @project.revision_number # => 2
132
+ @project.name # => "Sam"
133
+
134
+ Branching:
135
+
136
+ @branch = @project.branch(:name => "Bruno")
137
+ @branch.revision_number # => 0
138
+ @branch.branch_source.name # => "Sam"
139
+
140
+ Changesets:
141
+
142
+ @project.revision_number # => 2
143
+
144
+ @project.changeset! do
145
+ @project.name = "Josh"
146
+
147
+ # save would normally trigger a revision
148
+ @project.save
149
+
150
+ # update_attribute triggers a save triggering a revision (normally)
151
+ @project.updated_attribute(:name, "Chris")
152
+
153
+ # revise! normally forces a revision to be created
154
+ @project.revise!
155
+ end
156
+
157
+ # our revision number has only incremented by one
158
+ @project.revision_number # => 3
159
+
160
+ Maybe we don't want to be able to branch from revisions:
161
+
162
+ class Session < ActiveRecord::Base
163
+ # assuming we still have the other code from Session above
164
+
165
+ before_branch do
166
+ false
167
+ end
168
+ end
169
+
170
+ @project.revisions.first.branch # Raises an exception
171
+ @project.branch # works as expected
172
+
173
+ If the owner isn't set let's prevent reverting:
174
+
175
+ class Project < ActiveRecord::Base
176
+ # assuming we still have the other code from Project above
177
+
178
+ before_revert :check_owner_befor_reverting
179
+ def check_owner_befor_reverting
180
+ false unless self.owner?
181
+ end
182
+ end
183
+
184
+ == REQUIREMENTS:
185
+
186
+ This plugin requires Rails 2.3. Use version 0.9.8 of this plugin for Rails 2.1 and 2.2.
187
+
188
+ == INSTALL:
189
+
190
+ acts_as_revisable uses Rails' new ability to use gems as plugins. Installing AAR is as simple as installing a gem:
191
+
192
+ sudo gem install rich-acts_as_revisable --source=http://gems.github.com
193
+
194
+ Once the gem is installed you'll want to activate it in your Rails app by adding the following line to config/environment.rb:
195
+
196
+ config.gem "rich-acts_as_revisable", :lib => "acts_as_revisable", :source => "http://gems.github.com"
197
+
198
+ == LICENSE:
199
+
200
+ (The MIT License)
201
+
202
+ Copyright (c) 2009 Rich Cavanaugh
203
+
204
+ Permission is hereby granted, free of charge, to any person obtaining
205
+ a copy of this software and associated documentation files (the
206
+ 'Software'), to deal in the Software without restriction, including
207
+ without limitation the rights to use, copy, modify, merge, publish,
208
+ distribute, sublicense, and/or sell copies of the Software, and to
209
+ permit persons to whom the Software is furnished to do so, subject to
210
+ the following conditions:
211
+
212
+ The above copyright notice and this permission notice shall be
213
+ included in all copies or substantial portions of the Software.
214
+
215
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
216
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
217
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
218
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
219
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
220
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
221
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ require 'rake/rdoctask'
2
+ require 'rake/gempackagetask'
3
+ require 'fileutils'
4
+ require 'lib/acts_as_revisable/version'
5
+ require 'lib/acts_as_revisable/gem_spec_options'
6
+
7
+ Rake::RDocTask.new do |rdoc|
8
+ files = ['README.rdoc','LICENSE','lib/**/*.rb','doc/**/*.rdoc','spec/*.rb']
9
+ rdoc.rdoc_files.add(files)
10
+ rdoc.main = 'README.rdoc'
11
+ rdoc.title = 'acts_as_revisable RDoc'
12
+ rdoc.rdoc_dir = 'doc'
13
+ rdoc.options << '--line-numbers' << '--inline-source'
14
+ end
15
+
16
+ spec = Gem::Specification.new do |s|
17
+ WithoutScope::ActsAsRevisable::GemSpecOptions::HASH.each do |key, value|
18
+ s.send("#{key.to_s}=",value)
19
+ end
20
+ end
21
+
22
+ Rake::GemPackageTask.new(spec) do |package|
23
+ package.gem_spec = spec
24
+ end
25
+
26
+ desc "Generate the static gemspec required for github."
27
+ task :generate_gemspec do
28
+ options = WithoutScope::ActsAsRevisable::GemSpecOptions::HASH.clone
29
+ options[:name] = "acts_as_revisable"
30
+
31
+ spec = ["Gem::Specification.new do |s|"]
32
+ options.each do |key, value|
33
+ spec << " s.#{key.to_s} = #{value.inspect}"
34
+ end
35
+ spec << "end"
36
+
37
+ open("acts_as_revisable.gemspec", "w").write(spec.join("\n"))
38
+ end
39
+
40
+ desc "Install acts_as_revisable"
41
+ task :install => :repackage do
42
+ options = WithoutScope::ActsAsRevisable::GemSpecOptions::HASH.clone
43
+ sh %{sudo gem install pkg/#{options[:name]}-#{spec.version} --no-rdoc --no-ri}
44
+ end
@@ -0,0 +1,21 @@
1
+ class RevisableMigrationGenerator < Rails::Generator::NamedBase
2
+ def manifest
3
+ record do |m|
4
+ revisable_columns = [
5
+ ["revisable_original_id", "integer"],
6
+ ["revisable_branched_from_id", "integer"],
7
+ ["revisable_number", "integer", 0],
8
+ ["revisable_name", "string"],
9
+ ["revisable_type", "string"],
10
+ ["revisable_current_at", "datetime"],
11
+ ["revisable_revised_at", "datetime"],
12
+ ["revisable_deleted_at", "datetime"],
13
+ ["revisable_is_current", "boolean", 1]
14
+ ]
15
+
16
+ m.migration_template 'migration.rb', 'db/migrate',
17
+ :migration_file_name => "make_#{class_name.underscore.pluralize}_revisable",
18
+ :assigns => { :cols => revisable_columns }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ <% table_name = class_name.underscore.pluralize -%>
2
+ class Make<%= class_name.underscore.pluralize.camelize %>Revisable < ActiveRecord::Migration
3
+ def self.up
4
+ <% cols.each do |column_name,column_type,default| -%>
5
+ add_column :<%= table_name %>, :<%= column_name %>, :<%= column_type %><%= ", :default => #{default}" unless default.blank? %>
6
+ <% end -%>
7
+ end
8
+
9
+ def self.down
10
+ <% cols.each do |column_name,_| -%>
11
+ remove_column :<%= table_name %>, :<%= column_name %>
12
+ <% end -%>
13
+ end
14
+ end
@@ -0,0 +1,209 @@
1
+ module WithoutScope
2
+ module ActsAsRevisable
3
+ # This module is mixed into the revision and revisable classes.
4
+ #
5
+ # ==== Callbacks
6
+ #
7
+ # * +before_branch+ is called on the +Revisable+ or +Revision+ that is
8
+ # being branched
9
+ # * +after_branch+ is called on the +Revisable+ or +Revision+ that is
10
+ # being branched
11
+ module Common
12
+ def self.included(base) #:nodoc:
13
+ base.send(:extend, ClassMethods)
14
+
15
+ base.class_inheritable_hash :revisable_after_callback_blocks
16
+ base.revisable_after_callback_blocks = {}
17
+
18
+ base.class_inheritable_hash :revisable_current_states
19
+ base.revisable_current_states = {}
20
+
21
+ base.instance_eval do
22
+ define_callbacks :before_branch, :after_branch
23
+ has_many :branches, (revisable_options.revision_association_options || {}).merge({:class_name => base.class_name, :foreign_key => :revisable_branched_from_id})
24
+
25
+ belongs_to :branch_source, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
26
+ after_save :execute_blocks_after_save
27
+ end
28
+ end
29
+
30
+ # Executes the blocks stored in an accessor after a save.
31
+ def execute_blocks_after_save #:nodoc:
32
+ return unless revisable_after_callback_blocks[:save]
33
+ revisable_after_callback_blocks[:save].each do |block|
34
+ block.call
35
+ end
36
+ revisable_after_callback_blocks.delete(:save)
37
+ end
38
+
39
+ # Stores a block for later execution after a given callback.
40
+ # The parameter +key+ is the callback the block should be
41
+ # executed after.
42
+ def execute_after(key, &block) #:nodoc:
43
+ return unless block_given?
44
+ revisable_after_callback_blocks[key] ||= []
45
+ revisable_after_callback_blocks[key] << block
46
+ end
47
+
48
+ # Branch the +Revisable+ or +Revision+ and return the new
49
+ # +revisable+ instance. The instance has not been saved yet.
50
+ #
51
+ # ==== Callbacks
52
+ # * +before_branch+ is called on the +Revisable+ or +Revision+ that is
53
+ # being branched
54
+ # * +after_branch+ is called on the +Revisable+ or +Revision+ that is
55
+ # being branched
56
+ # * +after_branch_created+ is called on the newly created
57
+ # +Revisable+ instance.
58
+ def branch(*args, &block)
59
+ is_branching!
60
+
61
+ unless run_callbacks(:before_branch) { |r, o| r == false}
62
+ raise ActiveRecord::RecordNotSaved
63
+ end
64
+
65
+ options = args.extract_options!
66
+ options[:revisable_branched_from_id] = self.id
67
+ self.class.column_names.each do |col|
68
+ next unless self.class.revisable_should_clone_column? col
69
+ options[col.to_sym] = self[col] unless options.has_key?(col.to_sym)
70
+ end
71
+
72
+ br = self.class.revisable_class.new(options)
73
+ br.is_branching!
74
+
75
+ br.execute_after(:save) do
76
+ begin
77
+ run_callbacks(:after_branch)
78
+ br.run_callbacks(:after_branch_created)
79
+ ensure
80
+ br.is_branching!(false)
81
+ is_branching!(false)
82
+ end
83
+ end
84
+
85
+ block.call(br) if block_given?
86
+
87
+ br
88
+ end
89
+
90
+ # Same as #branch except it calls #save! on the new +Revisable+ instance.
91
+ def branch!(*args)
92
+ branch(*args) do |br|
93
+ br.save!
94
+ end
95
+ end
96
+
97
+ # Globally sets the reverting state of this record.
98
+ def is_branching!(value=true) #:nodoc:
99
+ set_revisable_state(:branching, value)
100
+ end
101
+
102
+ # Returns true if the _record_ (not just this instance
103
+ # of the record) is currently being branched.
104
+ def is_branching?
105
+ get_revisable_state(:branching)
106
+ end
107
+
108
+ # When called on a +Revision+ it returns the original id. When
109
+ # called on a +Revisable+ it returns the id.
110
+ def original_id
111
+ self[:revisable_original_id] || self[:id]
112
+ end
113
+
114
+ # Globally sets the state for a given record. This is keyed
115
+ # on the primary_key of a saved record or the object_id
116
+ # on a new instance.
117
+ def set_revisable_state(type, value) #:nodoc:
118
+ key = self.read_attribute(self.class.primary_key)
119
+ key = object_id if key.nil?
120
+ revisable_current_states[type] ||= {}
121
+ revisable_current_states[type][key] = value
122
+ revisable_current_states[type].delete(key) unless value
123
+ end
124
+
125
+ # Returns the state of the given record.
126
+ def get_revisable_state(type) #:nodoc:
127
+ key = self.read_attribute(self.class.primary_key)
128
+ revisable_current_states[type] ||= {}
129
+ revisable_current_states[type][key] || revisable_current_states[type][object_id] || false
130
+ end
131
+
132
+ # Returns true if the instance is the first revision.
133
+ def first_revision?
134
+ self.revision_number == 1
135
+ end
136
+
137
+ # Returns true if the instance is the most recent revision.
138
+ def latest_revision?
139
+ self.revision_number == self.current_revision.revision_number
140
+ end
141
+
142
+ # Returns true if the instance is the current record and not a revision.
143
+ def current_revision?
144
+ self.is_a? self.class.revisable_class
145
+ end
146
+
147
+ # Accessor for revisable_number just to make external API more pleasant.
148
+ def revision_number
149
+ self[:revisable_number] ||= 0
150
+ end
151
+
152
+ def revision_number=(value)
153
+ self[:revisable_number] = value
154
+ end
155
+
156
+ def diffs(what)
157
+ what = current_revision.find_revision(what)
158
+ returning({}) do |changes|
159
+ self.class.revisable_class.revisable_watch_columns.each do |c|
160
+ changes[c] = [self[c], what[c]] unless self[c] == what[c]
161
+ end
162
+ end
163
+ end
164
+
165
+ def deleted?
166
+ self.revisable_deleted_at.present?
167
+ end
168
+
169
+ module ClassMethods
170
+ # Returns true if the revision should clone the given column.
171
+ def revisable_should_clone_column?(col) #:nodoc:
172
+ return false if (REVISABLE_SYSTEM_COLUMNS + REVISABLE_UNREVISABLE_COLUMNS).member? col
173
+ true
174
+ end
175
+
176
+ # acts_as_revisable's override for instantiate so we can
177
+ # return the appropriate type of model based on whether
178
+ # or not the record is the current record.
179
+ def instantiate(record) #:nodoc:
180
+ is_current = columns_hash["revisable_is_current"].type_cast(
181
+ record["revisable_is_current"])
182
+
183
+ if (is_current && self == self.revisable_class) || (!is_current && self == self.revision_class)
184
+ return super(record)
185
+ end
186
+
187
+ object = if is_current
188
+ self.revisable_class
189
+ else
190
+ self.revision_class
191
+ end.allocate
192
+
193
+ object.instance_variable_set("@attributes", record)
194
+ object.instance_variable_set("@attributes_cache", Hash.new)
195
+
196
+ if object.respond_to_without_attributes?(:after_find)
197
+ object.send(:callback, :after_find)
198
+ end
199
+
200
+ if object.respond_to_without_attributes?(:after_initialize)
201
+ object.send(:callback, :after_initialize)
202
+ end
203
+
204
+ object
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,33 @@
1
+ module WithoutScope
2
+ module ActsAsRevisable
3
+ module Deletable
4
+ def self.included(base)
5
+ base.instance_eval do
6
+ define_callbacks :before_revise_on_destroy, :after_revise_on_destroy
7
+ end
8
+ end
9
+
10
+ def destroy
11
+ now = Time.zone.now
12
+
13
+ prev = self.revisions.first
14
+ self.revisable_deleted_at = now
15
+ self.revisable_is_current = false
16
+
17
+ self.revisable_current_at = if prev
18
+ prev.update_attribute(:revisable_revised_at, now)
19
+ prev.revisable_revised_at + 1.second
20
+ else
21
+ self.created_at
22
+ end
23
+
24
+ self.revisable_revised_at = self.revisable_deleted_at
25
+
26
+ return false unless run_callbacks(:before_revise_on_destroy) { |r, o| r == false}
27
+ returning(self.save(:without_revision => true)) do
28
+ run_callbacks(:after_revise_on_destroy)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end