kkorach-acts_as_revisable 0.9.7

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