##// END OF EJS Templates
acts_as_versioned not compatible with ActiveRecord 4.2.1 (#19957)....
Jean-Philippe Lang -
r13912:ea34967e65b2
parent child
Show More
@@ -1,568 +1,568
1 # Copyright (c) 2005 Rick Olson
1 # Copyright (c) 2005 Rick Olson
2 #
2 #
3 # Permission is hereby granted, free of charge, to any person obtaining
3 # Permission is hereby granted, free of charge, to any person obtaining
4 # a copy of this software and associated documentation files (the
4 # a copy of this software and associated documentation files (the
5 # "Software"), to deal in the Software without restriction, including
5 # "Software"), to deal in the Software without restriction, including
6 # without limitation the rights to use, copy, modify, merge, publish,
6 # without limitation the rights to use, copy, modify, merge, publish,
7 # distribute, sublicense, and/or sell copies of the Software, and to
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
8 # permit persons to whom the Software is furnished to do so, subject to
9 # the following conditions:
9 # the following conditions:
10 #
10 #
11 # The above copyright notice and this permission notice shall be
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Software.
12 # included in all copies or substantial portions of the Software.
13 #
13 #
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
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
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
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.
20 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
22 module ActiveRecord #:nodoc:
22 module ActiveRecord #:nodoc:
23 module Acts #:nodoc:
23 module Acts #:nodoc:
24 # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
24 # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
25 # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
25 # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
26 # column is present as well.
26 # column is present as well.
27 #
27 #
28 # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
28 # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
29 # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
29 # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
30 #
30 #
31 # class Page < ActiveRecord::Base
31 # class Page < ActiveRecord::Base
32 # # assumes pages_versions table
32 # # assumes pages_versions table
33 # acts_as_versioned
33 # acts_as_versioned
34 # end
34 # end
35 #
35 #
36 # Example:
36 # Example:
37 #
37 #
38 # page = Page.create(:title => 'hello world!')
38 # page = Page.create(:title => 'hello world!')
39 # page.version # => 1
39 # page.version # => 1
40 #
40 #
41 # page.title = 'hello world'
41 # page.title = 'hello world'
42 # page.save
42 # page.save
43 # page.version # => 2
43 # page.version # => 2
44 # page.versions.size # => 2
44 # page.versions.size # => 2
45 #
45 #
46 # page.revert_to(1) # using version number
46 # page.revert_to(1) # using version number
47 # page.title # => 'hello world!'
47 # page.title # => 'hello world!'
48 #
48 #
49 # page.revert_to(page.versions.last) # using versioned instance
49 # page.revert_to(page.versions.last) # using versioned instance
50 # page.title # => 'hello world'
50 # page.title # => 'hello world'
51 #
51 #
52 # page.versions.earliest # efficient query to find the first version
52 # page.versions.earliest # efficient query to find the first version
53 # page.versions.latest # efficient query to find the most recently created version
53 # page.versions.latest # efficient query to find the most recently created version
54 #
54 #
55 #
55 #
56 # Simple Queries to page between versions
56 # Simple Queries to page between versions
57 #
57 #
58 # page.versions.before(version)
58 # page.versions.before(version)
59 # page.versions.after(version)
59 # page.versions.after(version)
60 #
60 #
61 # Access the previous/next versions from the versioned model itself
61 # Access the previous/next versions from the versioned model itself
62 #
62 #
63 # version = page.versions.latest
63 # version = page.versions.latest
64 # version.previous # go back one version
64 # version.previous # go back one version
65 # version.next # go forward one version
65 # version.next # go forward one version
66 #
66 #
67 # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
67 # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
68 module Versioned
68 module Versioned
69 CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
69 CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
70 def self.included(base) # :nodoc:
70 def self.included(base) # :nodoc:
71 base.extend ClassMethods
71 base.extend ClassMethods
72 end
72 end
73
73
74 module ClassMethods
74 module ClassMethods
75 # == Configuration options
75 # == Configuration options
76 #
76 #
77 # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
77 # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
78 # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
78 # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
79 # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
79 # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
80 # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
80 # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
81 # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
81 # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
82 # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
82 # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
83 # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
83 # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
84 # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
84 # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
85 # For finer control, pass either a Proc or modify Model#version_condition_met?
85 # For finer control, pass either a Proc or modify Model#version_condition_met?
86 #
86 #
87 # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
87 # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
88 #
88 #
89 # or...
89 # or...
90 #
90 #
91 # class Auction
91 # class Auction
92 # def version_condition_met? # totally bypasses the <tt>:if</tt> option
92 # def version_condition_met? # totally bypasses the <tt>:if</tt> option
93 # !expired?
93 # !expired?
94 # end
94 # end
95 # end
95 # end
96 #
96 #
97 # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
97 # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
98 # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
98 # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
99 # Use this instead if you want to write your own attribute setters (and ignore if_changed):
99 # Use this instead if you want to write your own attribute setters (and ignore if_changed):
100 #
100 #
101 # def name=(new_name)
101 # def name=(new_name)
102 # write_changed_attribute :name, new_name
102 # write_changed_attribute :name, new_name
103 # end
103 # end
104 #
104 #
105 # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
105 # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
106 # to create an anonymous mixin:
106 # to create an anonymous mixin:
107 #
107 #
108 # class Auction
108 # class Auction
109 # acts_as_versioned do
109 # acts_as_versioned do
110 # def started?
110 # def started?
111 # !started_at.nil?
111 # !started_at.nil?
112 # end
112 # end
113 # end
113 # end
114 # end
114 # end
115 #
115 #
116 # or...
116 # or...
117 #
117 #
118 # module AuctionExtension
118 # module AuctionExtension
119 # def started?
119 # def started?
120 # !started_at.nil?
120 # !started_at.nil?
121 # end
121 # end
122 # end
122 # end
123 # class Auction
123 # class Auction
124 # acts_as_versioned :extend => AuctionExtension
124 # acts_as_versioned :extend => AuctionExtension
125 # end
125 # end
126 #
126 #
127 # Example code:
127 # Example code:
128 #
128 #
129 # @auction = Auction.find(1)
129 # @auction = Auction.find(1)
130 # @auction.started?
130 # @auction.started?
131 # @auction.versions.first.started?
131 # @auction.versions.first.started?
132 #
132 #
133 # == Database Schema
133 # == Database Schema
134 #
134 #
135 # The model that you're versioning needs to have a 'version' attribute. The model is versioned
135 # The model that you're versioning needs to have a 'version' attribute. The model is versioned
136 # into a table called #{model}_versions where the model name is singlular. The _versions table should
136 # into a table called #{model}_versions where the model name is singlular. The _versions table should
137 # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
137 # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
138 #
138 #
139 # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
139 # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
140 # then that field is reflected in the versioned model as 'versioned_type' by default.
140 # then that field is reflected in the versioned model as 'versioned_type' by default.
141 #
141 #
142 # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
142 # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
143 # method, perfect for a migration. It will also create the version column if the main model does not already have it.
143 # method, perfect for a migration. It will also create the version column if the main model does not already have it.
144 #
144 #
145 # class AddVersions < ActiveRecord::Migration
145 # class AddVersions < ActiveRecord::Migration
146 # def self.up
146 # def self.up
147 # # create_versioned_table takes the same options hash
147 # # create_versioned_table takes the same options hash
148 # # that create_table does
148 # # that create_table does
149 # Post.create_versioned_table
149 # Post.create_versioned_table
150 # end
150 # end
151 #
151 #
152 # def self.down
152 # def self.down
153 # Post.drop_versioned_table
153 # Post.drop_versioned_table
154 # end
154 # end
155 # end
155 # end
156 #
156 #
157 # == Changing What Fields Are Versioned
157 # == Changing What Fields Are Versioned
158 #
158 #
159 # By default, acts_as_versioned will version all but these fields:
159 # By default, acts_as_versioned will version all but these fields:
160 #
160 #
161 # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
161 # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
162 #
162 #
163 # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
163 # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
164 #
164 #
165 # class Post < ActiveRecord::Base
165 # class Post < ActiveRecord::Base
166 # acts_as_versioned
166 # acts_as_versioned
167 # self.non_versioned_columns << 'comments_count'
167 # self.non_versioned_columns << 'comments_count'
168 # end
168 # end
169 #
169 #
170 def acts_as_versioned(options = {}, &extension)
170 def acts_as_versioned(options = {}, &extension)
171 # don't allow multiple calls
171 # don't allow multiple calls
172 return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
172 return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
173
173
174 send :include, ActiveRecord::Acts::Versioned::ActMethods
174 send :include, ActiveRecord::Acts::Versioned::ActMethods
175
175
176 cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
176 cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
177 :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
177 :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
178 :version_association_options
178 :version_association_options
179
179
180 # legacy
180 # legacy
181 alias_method :non_versioned_fields, :non_versioned_columns
181 alias_method :non_versioned_fields, :non_versioned_columns
182 alias_method :non_versioned_fields=, :non_versioned_columns=
182 alias_method :non_versioned_fields=, :non_versioned_columns=
183
183
184 class << self
184 class << self
185 alias_method :non_versioned_fields, :non_versioned_columns
185 alias_method :non_versioned_fields, :non_versioned_columns
186 alias_method :non_versioned_fields=, :non_versioned_columns=
186 alias_method :non_versioned_fields=, :non_versioned_columns=
187 end
187 end
188
188
189 send :attr_accessor, :altered_attributes
189 send :attr_accessor, :altered_attributes
190
190
191 self.versioned_class_name = options[:class_name] || "Version"
191 self.versioned_class_name = options[:class_name] || "Version"
192 self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
192 self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
193 self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
193 self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
194 self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
194 self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
195 self.version_column = options[:version_column] || 'version'
195 self.version_column = options[:version_column] || 'version'
196 self.version_sequence_name = options[:sequence_name]
196 self.version_sequence_name = options[:sequence_name]
197 self.max_version_limit = options[:limit].to_i
197 self.max_version_limit = options[:limit].to_i
198 self.version_condition = options[:if] || true
198 self.version_condition = options[:if] || true
199 self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
199 self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
200 self.version_association_options = {
200 self.version_association_options = {
201 :class_name => "#{self.to_s}::#{versioned_class_name}",
201 :class_name => "#{self.to_s}::#{versioned_class_name}",
202 :foreign_key => versioned_foreign_key,
202 :foreign_key => versioned_foreign_key,
203 :dependent => :delete_all
203 :dependent => :delete_all
204 }.merge(options[:association_options] || {})
204 }.merge(options[:association_options] || {})
205
205
206 if block_given?
206 if block_given?
207 extension_module_name = "#{versioned_class_name}Extension"
207 extension_module_name = "#{versioned_class_name}Extension"
208 silence_warnings do
208 silence_warnings do
209 self.const_set(extension_module_name, Module.new(&extension))
209 self.const_set(extension_module_name, Module.new(&extension))
210 end
210 end
211
211
212 options[:extend] = self.const_get(extension_module_name)
212 options[:extend] = self.const_get(extension_module_name)
213 end
213 end
214
214
215 class_eval do
215 class_eval do
216 has_many :versions, version_association_options do
216 has_many :versions, version_association_options do
217 # finds earliest version of this record
217 # finds earliest version of this record
218 def earliest
218 def earliest
219 @earliest ||= order('version').first
219 @earliest ||= order('version').first
220 end
220 end
221
221
222 # find latest version of this record
222 # find latest version of this record
223 def latest
223 def latest
224 @latest ||= order('version desc').first
224 @latest ||= order('version desc').first
225 end
225 end
226 end
226 end
227 before_save :set_new_version
227 before_save :set_new_version
228 after_create :save_version_on_create
228 after_create :save_version_on_create
229 after_update :save_version
229 after_update :save_version
230 after_save :clear_old_versions
230 after_save :clear_old_versions
231 after_save :clear_altered_attributes
231 after_save :clear_altered_attributes
232
232
233 unless options[:if_changed].nil?
233 unless options[:if_changed].nil?
234 self.track_altered_attributes = true
234 self.track_altered_attributes = true
235 options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
235 options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
236 options[:if_changed].each do |attr_name|
236 options[:if_changed].each do |attr_name|
237 define_method("#{attr_name}=") do |value|
237 define_method("#{attr_name}=") do |value|
238 write_changed_attribute attr_name, value
238 write_changed_attribute attr_name, value
239 end
239 end
240 end
240 end
241 end
241 end
242
242
243 include options[:extend] if options[:extend].is_a?(Module)
243 include options[:extend] if options[:extend].is_a?(Module)
244 end
244 end
245
245
246 # create the dynamic versioned model
246 # create the dynamic versioned model
247 const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
247 const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
248 def self.reloadable? ; false ; end
248 def self.reloadable? ; false ; end
249 # find first version before the given version
249 # find first version before the given version
250 def self.before(version)
250 def self.before(version)
251 order('version desc').
251 order('version desc').
252 where("#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version).
252 where("#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version).
253 first
253 first
254 end
254 end
255
255
256 # find first version after the given version.
256 # find first version after the given version.
257 def self.after(version)
257 def self.after(version)
258 order('version').
258 order('version').
259 where("#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version).
259 where("#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version).
260 first
260 first
261 end
261 end
262
262
263 def previous
263 def previous
264 self.class.before(self)
264 self.class.before(self)
265 end
265 end
266
266
267 def next
267 def next
268 self.class.after(self)
268 self.class.after(self)
269 end
269 end
270
270
271 def versions_count
271 def versions_count
272 page.version
272 page.version
273 end
273 end
274 end
274 end
275
275
276 versioned_class.cattr_accessor :original_class
276 versioned_class.cattr_accessor :original_class
277 versioned_class.original_class = self
277 versioned_class.original_class = self
278 versioned_class.table_name = versioned_table_name
278 versioned_class.table_name = versioned_table_name
279 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
279 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
280 :class_name => "::#{self.to_s}",
280 :class_name => "::#{self.to_s}",
281 :foreign_key => versioned_foreign_key
281 :foreign_key => versioned_foreign_key
282 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
282 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
283 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
283 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
284 end
284 end
285 end
285 end
286
286
287 module ActMethods
287 module ActMethods
288 def self.included(base) # :nodoc:
288 def self.included(base) # :nodoc:
289 base.extend ClassMethods
289 base.extend ClassMethods
290 end
290 end
291
291
292 # Finds a specific version of this record
292 # Finds a specific version of this record
293 def find_version(version = nil)
293 def find_version(version = nil)
294 self.class.find_version(id, version)
294 self.class.find_version(id, version)
295 end
295 end
296
296
297 # Saves a version of the model if applicable
297 # Saves a version of the model if applicable
298 def save_version
298 def save_version
299 save_version_on_create if save_version?
299 save_version_on_create if save_version?
300 end
300 end
301
301
302 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
302 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
303 def save_version_on_create
303 def save_version_on_create
304 rev = self.class.versioned_class.new
304 rev = self.class.versioned_class.new
305 self.clone_versioned_model(self, rev)
305 self.clone_versioned_model(self, rev)
306 rev.version = send(self.class.version_column)
306 rev.version = send(self.class.version_column)
307 rev.send("#{self.class.versioned_foreign_key}=", self.id)
307 rev.send("#{self.class.versioned_foreign_key}=", self.id)
308 rev.save
308 rev.save
309 end
309 end
310
310
311 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
311 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
312 # Override this method to set your own criteria for clearing old versions.
312 # Override this method to set your own criteria for clearing old versions.
313 def clear_old_versions
313 def clear_old_versions
314 return if self.class.max_version_limit == 0
314 return if self.class.max_version_limit == 0
315 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
315 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
316 if excess_baggage > 0
316 if excess_baggage > 0
317 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
317 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
318 self.class.versioned_class.connection.execute sql
318 self.class.versioned_class.connection.execute sql
319 end
319 end
320 end
320 end
321
321
322 def versions_count
322 def versions_count
323 version
323 version
324 end
324 end
325
325
326 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
326 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
327 def revert_to(version)
327 def revert_to(version)
328 if version.is_a?(self.class.versioned_class)
328 if version.is_a?(self.class.versioned_class)
329 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
329 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
330 else
330 else
331 return false unless version = versions.find_by_version(version)
331 return false unless version = versions.find_by_version(version)
332 end
332 end
333 self.clone_versioned_model(version, self)
333 self.clone_versioned_model(version, self)
334 self.send("#{self.class.version_column}=", version.version)
334 self.send("#{self.class.version_column}=", version.version)
335 true
335 true
336 end
336 end
337
337
338 # Reverts a model to a given version and saves the model.
338 # Reverts a model to a given version and saves the model.
339 # Takes either a version number or an instance of the versioned model
339 # Takes either a version number or an instance of the versioned model
340 def revert_to!(version)
340 def revert_to!(version)
341 revert_to(version) ? save_without_revision : false
341 revert_to(version) ? save_without_revision : false
342 end
342 end
343
343
344 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
344 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
345 def save_without_revision
345 def save_without_revision
346 save_without_revision!
346 save_without_revision!
347 true
347 true
348 rescue
348 rescue
349 false
349 false
350 end
350 end
351
351
352 def save_without_revision!
352 def save_without_revision!
353 without_locking do
353 without_locking do
354 without_revision do
354 without_revision do
355 save!
355 save!
356 end
356 end
357 end
357 end
358 end
358 end
359
359
360 # Returns an array of attribute keys that are versioned. See non_versioned_columns
360 # Returns an array of attribute keys that are versioned. See non_versioned_columns
361 def versioned_attributes
361 def versioned_attributes
362 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
362 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
363 end
363 end
364
364
365 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
365 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
366 # If called with a single parameter, gets whether the parameter has changed.
366 # If called with a single parameter, gets whether the parameter has changed.
367 def changed?(attr_name = nil)
367 def changed?(attr_name = nil)
368 attr_name.nil? ?
368 attr_name.nil? ?
369 (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
369 (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
370 (altered_attributes && altered_attributes.include?(attr_name.to_s))
370 (altered_attributes && altered_attributes.include?(attr_name.to_s))
371 end
371 end
372
372
373 # keep old dirty? method
373 # keep old dirty? method
374 alias_method :dirty?, :changed?
374 alias_method :dirty?, :changed?
375
375
376 # Clones a model. Used when saving a new version or reverting a model's version.
376 # Clones a model. Used when saving a new version or reverting a model's version.
377 def clone_versioned_model(orig_model, new_model)
377 def clone_versioned_model(orig_model, new_model)
378 self.versioned_attributes.each do |key|
378 self.versioned_attributes.each do |key|
379 new_model.send("#{key}=", orig_model.send(key)) if orig_model.respond_to?(key)
379 new_model.send("#{key}=", orig_model.send(key)) if orig_model.respond_to?(key)
380 end
380 end
381
381
382 if self.class.columns_hash.include?(self.class.inheritance_column)
382 if self.class.columns_hash.include?(self.class.inheritance_column)
383 if orig_model.is_a?(self.class.versioned_class)
383 if orig_model.is_a?(self.class.versioned_class)
384 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
384 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
385 elsif new_model.is_a?(self.class.versioned_class)
385 elsif new_model.is_a?(self.class.versioned_class)
386 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
386 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
387 end
387 end
388 end
388 end
389 end
389 end
390
390
391 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
391 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
392 def save_version?
392 def save_version?
393 version_condition_met? && changed?
393 version_condition_met? && changed?
394 end
394 end
395
395
396 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
396 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
397 # custom version condition checking.
397 # custom version condition checking.
398 def version_condition_met?
398 def version_condition_met?
399 case
399 case
400 when version_condition.is_a?(Symbol)
400 when version_condition.is_a?(Symbol)
401 send(version_condition)
401 send(version_condition)
402 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
402 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
403 version_condition.call(self)
403 version_condition.call(self)
404 else
404 else
405 version_condition
405 version_condition
406 end
406 end
407 end
407 end
408
408
409 # Executes the block with the versioning callbacks disabled.
409 # Executes the block with the versioning callbacks disabled.
410 #
410 #
411 # @foo.without_revision do
411 # @foo.without_revision do
412 # @foo.save
412 # @foo.save
413 # end
413 # end
414 #
414 #
415 def without_revision(&block)
415 def without_revision(&block)
416 self.class.without_revision(&block)
416 self.class.without_revision(&block)
417 end
417 end
418
418
419 # Turns off optimistic locking for the duration of the block
419 # Turns off optimistic locking for the duration of the block
420 #
420 #
421 # @foo.without_locking do
421 # @foo.without_locking do
422 # @foo.save
422 # @foo.save
423 # end
423 # end
424 #
424 #
425 def without_locking(&block)
425 def without_locking(&block)
426 self.class.without_locking(&block)
426 self.class.without_locking(&block)
427 end
427 end
428
428
429 def empty_callback() end #:nodoc:
429 def empty_callback() end #:nodoc:
430
430
431 protected
431 protected
432 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
432 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
433 def set_new_version
433 def set_new_version
434 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
434 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
435 end
435 end
436
436
437 # Gets the next available version for the current record, or 1 for a new record
437 # Gets the next available version for the current record, or 1 for a new record
438 def next_version
438 def next_version
439 return 1 if new_record?
439 return 1 if new_record?
440 (versions.maximum('version') || 0) + 1
440 (versions.maximum('version') || 0) + 1
441 end
441 end
442
442
443 # clears current changed attributes. Called after save.
443 # clears current changed attributes. Called after save.
444 def clear_altered_attributes
444 def clear_altered_attributes
445 self.altered_attributes = []
445 self.altered_attributes = []
446 end
446 end
447
447
448 def write_changed_attribute(attr_name, attr_value)
448 def write_changed_attribute(attr_name, attr_value)
449 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
449 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
450 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
450 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast_from_database(attr_value)
451 (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
451 (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
452 write_attribute(attr_name, attr_value_for_db)
452 write_attribute(attr_name, attr_value_for_db)
453 end
453 end
454
454
455 module ClassMethods
455 module ClassMethods
456 # Finds a specific version of a specific row of this model
456 # Finds a specific version of a specific row of this model
457 def find_version(id, version = nil)
457 def find_version(id, version = nil)
458 return find(id) unless version
458 return find(id) unless version
459
459
460 conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
460 conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
461 options = { :conditions => conditions, :limit => 1 }
461 options = { :conditions => conditions, :limit => 1 }
462
462
463 if result = find_versions(id, options).first
463 if result = find_versions(id, options).first
464 result
464 result
465 else
465 else
466 raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
466 raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
467 end
467 end
468 end
468 end
469
469
470 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
470 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
471 def find_versions(id, options = {})
471 def find_versions(id, options = {})
472 versioned_class.all({
472 versioned_class.all({
473 :conditions => ["#{versioned_foreign_key} = ?", id],
473 :conditions => ["#{versioned_foreign_key} = ?", id],
474 :order => 'version' }.merge(options))
474 :order => 'version' }.merge(options))
475 end
475 end
476
476
477 # Returns an array of columns that are versioned. See non_versioned_columns
477 # Returns an array of columns that are versioned. See non_versioned_columns
478 def versioned_columns
478 def versioned_columns
479 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
479 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
480 end
480 end
481
481
482 # Returns an instance of the dynamic versioned model
482 # Returns an instance of the dynamic versioned model
483 def versioned_class
483 def versioned_class
484 const_get versioned_class_name
484 const_get versioned_class_name
485 end
485 end
486
486
487 # Rake migration task to create the versioned table using options passed to acts_as_versioned
487 # Rake migration task to create the versioned table using options passed to acts_as_versioned
488 def create_versioned_table(create_table_options = {})
488 def create_versioned_table(create_table_options = {})
489 # create version column in main table if it does not exist
489 # create version column in main table if it does not exist
490 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
490 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
491 self.connection.add_column table_name, :version, :integer
491 self.connection.add_column table_name, :version, :integer
492 end
492 end
493
493
494 self.connection.create_table(versioned_table_name, create_table_options) do |t|
494 self.connection.create_table(versioned_table_name, create_table_options) do |t|
495 t.column versioned_foreign_key, :integer
495 t.column versioned_foreign_key, :integer
496 t.column :version, :integer
496 t.column :version, :integer
497 end
497 end
498
498
499 updated_col = nil
499 updated_col = nil
500 self.versioned_columns.each do |col|
500 self.versioned_columns.each do |col|
501 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
501 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
502 self.connection.add_column versioned_table_name, col.name, col.type,
502 self.connection.add_column versioned_table_name, col.name, col.type,
503 :limit => col.limit,
503 :limit => col.limit,
504 :default => col.default,
504 :default => col.default,
505 :scale => col.scale,
505 :scale => col.scale,
506 :precision => col.precision
506 :precision => col.precision
507 end
507 end
508
508
509 if type_col = self.columns_hash[inheritance_column]
509 if type_col = self.columns_hash[inheritance_column]
510 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
510 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
511 :limit => type_col.limit,
511 :limit => type_col.limit,
512 :default => type_col.default,
512 :default => type_col.default,
513 :scale => type_col.scale,
513 :scale => type_col.scale,
514 :precision => type_col.precision
514 :precision => type_col.precision
515 end
515 end
516
516
517 if updated_col.nil?
517 if updated_col.nil?
518 self.connection.add_column versioned_table_name, :updated_at, :timestamp
518 self.connection.add_column versioned_table_name, :updated_at, :timestamp
519 end
519 end
520 end
520 end
521
521
522 # Rake migration task to drop the versioned table
522 # Rake migration task to drop the versioned table
523 def drop_versioned_table
523 def drop_versioned_table
524 self.connection.drop_table versioned_table_name
524 self.connection.drop_table versioned_table_name
525 end
525 end
526
526
527 # Executes the block with the versioning callbacks disabled.
527 # Executes the block with the versioning callbacks disabled.
528 #
528 #
529 # Foo.without_revision do
529 # Foo.without_revision do
530 # @foo.save
530 # @foo.save
531 # end
531 # end
532 #
532 #
533 def without_revision(&block)
533 def without_revision(&block)
534 class_eval do
534 class_eval do
535 CALLBACKS.each do |attr_name|
535 CALLBACKS.each do |attr_name|
536 alias_method "orig_#{attr_name}".to_sym, attr_name
536 alias_method "orig_#{attr_name}".to_sym, attr_name
537 alias_method attr_name, :empty_callback
537 alias_method attr_name, :empty_callback
538 end
538 end
539 end
539 end
540 block.call
540 block.call
541 ensure
541 ensure
542 class_eval do
542 class_eval do
543 CALLBACKS.each do |attr_name|
543 CALLBACKS.each do |attr_name|
544 alias_method attr_name, "orig_#{attr_name}".to_sym
544 alias_method attr_name, "orig_#{attr_name}".to_sym
545 end
545 end
546 end
546 end
547 end
547 end
548
548
549 # Turns off optimistic locking for the duration of the block
549 # Turns off optimistic locking for the duration of the block
550 #
550 #
551 # Foo.without_locking do
551 # Foo.without_locking do
552 # @foo.save
552 # @foo.save
553 # end
553 # end
554 #
554 #
555 def without_locking(&block)
555 def without_locking(&block)
556 current = ActiveRecord::Base.lock_optimistically
556 current = ActiveRecord::Base.lock_optimistically
557 ActiveRecord::Base.lock_optimistically = false if current
557 ActiveRecord::Base.lock_optimistically = false if current
558 result = block.call
558 result = block.call
559 ActiveRecord::Base.lock_optimistically = true if current
559 ActiveRecord::Base.lock_optimistically = true if current
560 result
560 result
561 end
561 end
562 end
562 end
563 end
563 end
564 end
564 end
565 end
565 end
566 end
566 end
567
567
568 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned No newline at end of file
568 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
General Comments 0
You need to be logged in to leave comments. Login now