rich-acts_as_revisable 0.6.0 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
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