rich-acts_as_revisable 0.6.0 → 0.9.8

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 CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2008 Rich Cavanaugh
1
+ Copyright (c) 2008 JamLab, LLC.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -1,6 +1,6 @@
1
1
  = acts_as_revisable
2
2
 
3
- http://github.com/rich/acts_as_revisable/tree/master
3
+ http://github.com/fatjam/acts_as_revisable/tree/master
4
4
 
5
5
  == DESCRIPTION:
6
6
 
@@ -37,7 +37,7 @@ This plugin wouldn't exist without Rick Olsen's acts_as_versioned. AAV has been
37
37
  * Uses ActiveRecord's dirty attribute tracking.
38
38
  * Several ways to find revisions including:
39
39
  * revision number
40
- * relative keywords (:previous, :last)
40
+ * relative keywords (:first, :previous and :last)
41
41
  * timestamp
42
42
  * Reverting
43
43
  * Branching
@@ -85,40 +85,75 @@ Some example usage:
85
85
 
86
86
  @project.name = "Stephen"
87
87
  @project.save(:without_revision => true)
88
- @project.name # => Stephen
88
+ @project.name # => "Stephen"
89
89
  @project.revision_number # => 0
90
90
 
91
91
  @project.name = "Sam"
92
92
  @project.save(:revision_name => "Changed name")
93
93
  @project.revision_number # => 1
94
-
94
+
95
+ @project.updated_attribute(:name, "Third")
96
+ @project.revision_number # => 2
97
+
95
98
  Navigating revisions:
96
-
99
+
97
100
  @previous = @project.find_revision(:previous)
98
101
  # or
99
102
  @previous = @project.revisions.first
100
103
 
101
- @previous.name # => Rich
102
- @previous.current_revision.name # => Sam
103
- @previous.project.name # => Sam
104
- @previous.revision_name # => Changed name
105
-
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
+
106
121
  Reverting:
107
122
 
108
123
  @project.revert_to!(:previous)
109
124
  @project.revision_number # => 2
110
- @project.name # => Rich
125
+ @project.name # => "Rich"
111
126
 
112
127
  @project.revert_to!(1, :without_revision => true)
113
128
  @project.revision_number # => 2
114
- @project.name # => Sam
129
+ @project.name # => "Sam"
115
130
 
116
- Branching
131
+ Branching:
117
132
 
118
133
  @branch = @project.branch(:name => "Bruno")
119
134
  @branch.revision_number # => 0
120
- @branch.branch_source.name # => Sam
135
+ @branch.branch_source.name # => "Sam"
121
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
+
122
157
  Associations have been cloned:
123
158
 
124
159
  @project.owner === @previous.owner # => true
@@ -146,32 +181,26 @@ If the owner isn't set let's prevent reverting:
146
181
  false unless self.owner?
147
182
  end
148
183
  end
149
-
184
+
150
185
  == REQUIREMENTS:
151
186
 
152
187
  This plugin currently depends on Edge Rails, ActiveRecord and ActiveSupport specifically, which will eventually become Rails 2.1.
153
188
 
154
189
  == INSTALL:
155
190
 
156
- acts_as_revisable uses Rails' new ability to use gems as plugins. Installing AAR is as simple as installing a gem.
157
-
158
- You may need to add GitHub as a Gem source:
159
-
160
- sudo gem sources -a http://gems.github.com/
161
-
162
- Then it can be installed as usual:
191
+ acts_as_revisable uses Rails' new ability to use gems as plugins. Installing AAR is as simple as installing a gem:
163
192
 
164
- sudo gem install rich-acts_as_revisable
193
+ sudo gem install fatjam-acts_as_revisable --source=http://gems.github.com
165
194
 
166
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:
167
196
 
168
- config.gem "rich-acts_as_revisable", :lib => "acts_as_revisable", :source => "http://gems.github.com"
197
+ config.gem "fatjam-acts_as_revisable", :lib => "acts_as_revisable", :source => "http://gems.github.com"
169
198
 
170
199
  == LICENSE:
171
200
 
172
201
  (The MIT License)
173
202
 
174
- Copyright (c) 2008
203
+ Copyright (c) 2008 JamLab, LLC.
175
204
 
176
205
  Permission is hereby granted, free of charge, to any person obtaining
177
206
  a copy of this software and associated documentation files (the
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
@@ -1,29 +1,68 @@
1
1
  module FatJam
2
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
3
11
  module Common
4
- def self.included(base)
12
+ def self.included(base) #:nodoc:
5
13
  base.send(:extend, ClassMethods)
6
-
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
+
7
21
  class << base
8
22
  alias_method_chain :instantiate, :revisable
9
23
  end
10
24
 
11
25
  base.instance_eval do
12
26
  define_callbacks :before_branch, :after_branch
13
- has_many :branches, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
27
+ has_many :branches, (revisable_options.revision_association_options || {}).merge({:class_name => base.class_name, :foreign_key => :revisable_branched_from_id})
28
+
14
29
  belongs_to :branch_source, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
15
-
16
- end
17
- base.alias_method_chain :branch_source, :open_scope
30
+ after_save :execute_blocks_after_save
31
+ disable_revisable_scope :branch_source, :branches
32
+ end
18
33
  end
19
-
20
- def branch_source_with_open_scope(*args, &block)
21
- self.class.without_model_scope do
22
- branch_source_without_open_scope(*args, &block)
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
23
40
  end
41
+ revisable_after_callback_blocks.delete(:save)
24
42
  end
25
-
26
- def branch(*args)
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
+
27
66
  unless run_callbacks(:before_branch) { |r, o| r == false}
28
67
  raise ActiveRecord::RecordNotSaved
29
68
  end
@@ -32,30 +71,138 @@ module FatJam
32
71
  options[:revisable_branched_from_id] = self.id
33
72
  self.class.column_names.each do |col|
34
73
  next unless self.class.revisable_should_clone_column? col
35
- options[col.to_sym] ||= self[col]
74
+ options[col.to_sym] = self[col] unless options.has_key?(col.to_sym)
36
75
  end
37
-
38
- returning(self.class.revisable_class.create!(options)) do |br|
39
- run_callbacks(:after_branch)
40
- br.run_callbacks(:after_branch_created)
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
41
88
  end
89
+
90
+ block.call(br) if block_given?
91
+
92
+ br
42
93
  end
43
-
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.
44
115
  def original_id
45
116
  self[:revisable_original_id] || self[:id]
46
117
  end
47
118
 
48
- module ClassMethods
49
- def revisable_should_clone_column?(col)
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:
50
194
  return false if (REVISABLE_SYSTEM_COLUMNS + REVISABLE_UNREVISABLE_COLUMNS).member? col
51
195
  true
52
196
  end
53
-
54
- def instantiate_with_revisable(record)
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:
55
202
  is_current = columns_hash["revisable_is_current"].type_cast(
56
203
  record["revisable_is_current"])
57
204
 
58
- if (is_current && self == self.revisable_class) || (is_current && self == self.revision_class)
205
+ if (is_current && self == self.revisable_class) || (!is_current && self == self.revision_class)
59
206
  return instantiate_without_revisable(record)
60
207
  end
61
208
 
@@ -0,0 +1,29 @@
1
+ module FatJam
2
+ module ActsAsRevisable
3
+ module Deletable
4
+ def self.included(base)
5
+ base.instance_eval do
6
+ alias_method_chain :destroy, :revisable
7
+ end
8
+ end
9
+
10
+ def destroy_with_revisable
11
+ now = Time.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
+ self.save(:without_revision => true)
26
+ end
27
+ end
28
+ end
29
+ end