acts_as_versioned 0.1.2
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/CHANGELOG +11 -0
- data/MIT-LICENSE +20 -0
- data/README +19 -0
- data/RUNNING_UNIT_TESTS +41 -0
- data/lib/acts_as_versioned.rb +373 -0
- data/test/abstract_unit.rb +25 -0
- data/test/connections/native_db2/connection.rb +14 -0
- data/test/connections/native_mysql/connection.rb +14 -0
- data/test/connections/native_oci/connection.rb +15 -0
- data/test/connections/native_postgresql/connection.rb +14 -0
- data/test/connections/native_sqlite/connection.rb +34 -0
- data/test/connections/native_sqlite3/connection.rb +33 -0
- data/test/connections/native_sqlserver/connection.rb +14 -0
- data/test/connections/native_sqlserver_odbc/connection.rb +15 -0
- data/test/fixtures/activerecord_versioned.sqlite3 +0 -0
- data/test/fixtures/db_definitions/mysql.drop.sql +4 -0
- data/test/fixtures/db_definitions/mysql.sql +36 -0
- data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
- data/test/fixtures/db_definitions/postgresql.sql +34 -0
- data/test/fixtures/db_definitions/sqlite.drop.sql +4 -0
- data/test/fixtures/db_definitions/sqlite.sql +32 -0
- data/test/fixtures/locked_pages.yml +10 -0
- data/test/fixtures/locked_pages_revisions.yml +27 -0
- data/test/fixtures/migrations/1_add_versioned_tables.rb +13 -0
- data/test/fixtures/page.rb +24 -0
- data/test/fixtures/page_versions.yml +12 -0
- data/test/fixtures/pages.yml +5 -0
- data/test/migration_test.rb +29 -0
- data/test/tests.rb +2 -0
- data/test/versioned_test.rb +228 -0
- metadata +99 -0
data/CHANGELOG
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2005 Rick Olson
|
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
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
= acts_as_versioned
|
2
|
+
|
3
|
+
This library adds simple versioning to an ActiveRecord module. ActiveRecord is required.
|
4
|
+
|
5
|
+
== Download
|
6
|
+
|
7
|
+
Gem installation:
|
8
|
+
|
9
|
+
gem install acts_as_versioned --source=http://techno-weenie.net/code
|
10
|
+
|
11
|
+
Get a gzipped tar at http://techno-weenie.net/code/pkg
|
12
|
+
|
13
|
+
== Usage
|
14
|
+
|
15
|
+
RDocs are online at http://techno-weenie.net/code/doc/acts_as_versioned/. Start with the
|
16
|
+
ActiveRecord::Acts::Versioned module.
|
17
|
+
|
18
|
+
Special thanks to Dreamer on ##rubyonrails for help in early testing. His ServerSideWiki (http://serversidewiki.com)
|
19
|
+
was the first project to use acts_as_versioned <em>in the wild</em>.
|
data/RUNNING_UNIT_TESTS
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
== Creating the test database
|
2
|
+
|
3
|
+
The default name for the test databases is "activerecord_versioned". If you
|
4
|
+
want to use another database name then be sure to update the connection
|
5
|
+
adapter setups you want to test with in test/connections/<your database>/connection.rb.
|
6
|
+
When you have the database online, you can import the fixture tables with
|
7
|
+
the test/fixtures/db_definitions/*.sql files.
|
8
|
+
|
9
|
+
Make sure that you create database objects with the same user that you specified in i
|
10
|
+
connection.rb otherwise (on Postgres, at least) tests for default values will fail.
|
11
|
+
|
12
|
+
== Running with Rake
|
13
|
+
|
14
|
+
The easiest way to run the unit tests is through Rake. The default task runs
|
15
|
+
the entire test suite for all the adapters. You can also run the suite on just
|
16
|
+
one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
|
17
|
+
or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
|
18
|
+
|
19
|
+
Rake can be found at http://rake.rubyforge.org
|
20
|
+
|
21
|
+
== Running by hand
|
22
|
+
|
23
|
+
Unit tests are located in test directory. If you only want to run a single test suite,
|
24
|
+
or don't want to bother with Rake, you can do so with something like:
|
25
|
+
|
26
|
+
cd test; ruby -I "connections/native_mysql" base_test.rb
|
27
|
+
|
28
|
+
That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
|
29
|
+
and test suite name as needed.
|
30
|
+
|
31
|
+
== Faster tests
|
32
|
+
|
33
|
+
If you are using a database that supports transactions, you can set the
|
34
|
+
"AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures.
|
35
|
+
This gives a very large speed boost. With rake:
|
36
|
+
|
37
|
+
rake AR_TX_FIXTURES=yes
|
38
|
+
|
39
|
+
Or, by hand:
|
40
|
+
|
41
|
+
AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb
|
@@ -0,0 +1,373 @@
|
|
1
|
+
# Copyright (c) 2005 Rick Olson
|
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.
|
21
|
+
|
22
|
+
module ActiveRecord #: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
|
25
|
+
# versioned table ready and that your model has a version field. This works with optimisic locking if the lock_version
|
26
|
+
# column is present as well.
|
27
|
+
#
|
28
|
+
# class Page < ActiveRecord::Base
|
29
|
+
# # assumes pages_versions table
|
30
|
+
# acts_as_versioned
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# Example:
|
34
|
+
#
|
35
|
+
# page = Page.create(:title => 'hello world!')
|
36
|
+
# page.version # => 1
|
37
|
+
#
|
38
|
+
# page.title = 'hello world'
|
39
|
+
# page.save
|
40
|
+
# page.version # => 2
|
41
|
+
# page.versions.size # => 2
|
42
|
+
#
|
43
|
+
# page.revert_to(1) # using version number
|
44
|
+
# page.title # => 'hello world!'
|
45
|
+
#
|
46
|
+
# page.revert_to(page.versions.last) # using versioned instance
|
47
|
+
# page.title # => 'hello world'
|
48
|
+
#
|
49
|
+
# See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
|
50
|
+
module Versioned
|
51
|
+
def self.included(base) # :nodoc:
|
52
|
+
base.extend ClassMethods
|
53
|
+
end
|
54
|
+
|
55
|
+
module ClassMethods
|
56
|
+
# == Configuration options
|
57
|
+
#
|
58
|
+
# * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
|
59
|
+
# * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
|
60
|
+
# * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
|
61
|
+
# * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
|
62
|
+
# * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
|
63
|
+
# * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
|
64
|
+
# * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
|
65
|
+
# For finer control, pass either a Proc or modify Model#version_condition_met?
|
66
|
+
#
|
67
|
+
# acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
|
68
|
+
#
|
69
|
+
# or...
|
70
|
+
#
|
71
|
+
# class Auction
|
72
|
+
# def version_condition_met? # totally bypasses the <tt>:if</tt> option
|
73
|
+
# !expired?
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
|
78
|
+
# either a symbol or array of symbols.
|
79
|
+
#
|
80
|
+
# == Database Schema
|
81
|
+
#
|
82
|
+
# The model that you're versioning needs to have a 'version' attribute. The model is versioned
|
83
|
+
# into a table called #{model}_versions where the model name is singlular. The _versions table should
|
84
|
+
# contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
|
85
|
+
#
|
86
|
+
# A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
|
87
|
+
# then that field is reflected in the versioned model as 'versioned_type' by default.
|
88
|
+
#
|
89
|
+
# Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
|
90
|
+
# method, perfect for a migration. It will also create the version column if the main model does not already have it.
|
91
|
+
#
|
92
|
+
# class AddVersions < ActiveRecord::Migration
|
93
|
+
# def self.up
|
94
|
+
# # create_versioned_table takes the same options hash
|
95
|
+
# # that create_table does
|
96
|
+
# Post.create_versioned_table
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# def self.down
|
100
|
+
# Post.drop_versioned_table
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
def acts_as_versioned(options = {})
|
104
|
+
# don't allow multiple calls
|
105
|
+
return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
|
106
|
+
|
107
|
+
class_eval do
|
108
|
+
include ActiveRecord::Acts::Versioned::ActMethods
|
109
|
+
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
|
110
|
+
:version_column, :max_version_limit, :track_changed_attributes, :version_condition
|
111
|
+
attr_accessor :changed_attributes
|
112
|
+
end
|
113
|
+
|
114
|
+
self.versioned_class_name = options[:class_name] || "#{self.to_s.demodulize}Version"
|
115
|
+
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
|
116
|
+
self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{Inflector.underscore(Inflector.demodulize(class_name_of_active_record_descendant(self)))}_versions#{table_name_suffix}"
|
117
|
+
self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
|
118
|
+
self.version_column = options[:version_column] || 'version'
|
119
|
+
self.max_version_limit = options[:limit].to_i
|
120
|
+
self.version_condition = options[:if] || true
|
121
|
+
|
122
|
+
class_eval do
|
123
|
+
has_many :versions,
|
124
|
+
:class_name => "ActiveRecord::Acts::Versioned::#{versioned_class_name}",
|
125
|
+
:foreign_key => "#{versioned_foreign_key}",
|
126
|
+
:order => 'version'
|
127
|
+
before_save :set_new_version
|
128
|
+
after_create :save_version_on_create
|
129
|
+
after_update :save_version
|
130
|
+
after_save :clear_old_versions
|
131
|
+
after_save :clear_changed_attributes
|
132
|
+
|
133
|
+
unless options[:if_changed].nil?
|
134
|
+
self.track_changed_attributes = true
|
135
|
+
options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
|
136
|
+
options[:if_changed].each do |attr_name|
|
137
|
+
define_method("#{attr_name}=") do |value|
|
138
|
+
(self.changed_attributes ||= []) << attr_name.to_s unless self.dirty?(attr_name) or self.send(attr_name) == value
|
139
|
+
write_attribute(attr_name.to_s, value)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# create the dynamic versioned model
|
146
|
+
eval <<-EOV
|
147
|
+
class ActiveRecord::Acts::Versioned::#{versioned_class_name} < ActiveRecord::Base
|
148
|
+
set_table_name "#{versioned_table_name}"
|
149
|
+
belongs_to :#{self.to_s.demodulize.underscore}, :class_name => "#{self.to_s}"
|
150
|
+
end
|
151
|
+
EOV
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
module ActMethods
|
156
|
+
def self.included(base) # :nodoc:
|
157
|
+
base.extend ClassMethods
|
158
|
+
end
|
159
|
+
|
160
|
+
# Saves a version of the model if applicable
|
161
|
+
def save_version
|
162
|
+
save_version_on_create if save_version?
|
163
|
+
end
|
164
|
+
|
165
|
+
# Saves a version of the model in the versioned table. This is called in the after_save callback by default
|
166
|
+
def save_version_on_create
|
167
|
+
rev = self.class.versioned_class.new
|
168
|
+
self.clone_versioned_model(self, rev)
|
169
|
+
rev.version = send(self.class.version_column)
|
170
|
+
rev.send("#{self.class.versioned_foreign_key}=", self.id)
|
171
|
+
rev.save
|
172
|
+
end
|
173
|
+
|
174
|
+
# Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
|
175
|
+
# Override this method to set your own criteria for clearing old versions.
|
176
|
+
def clear_old_versions
|
177
|
+
return if self.class.max_version_limit.blank?
|
178
|
+
excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
|
179
|
+
if excess_baggage > 0
|
180
|
+
sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
|
181
|
+
self.class.versioned_class.connection.execute sql
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Finds a specific version of this model.
|
186
|
+
def find_version(version)
|
187
|
+
return version if version.is_a?(self.class.versioned_class)
|
188
|
+
return nil if version.is_a?(ActiveRecord::Base)
|
189
|
+
find_versions(:conditions => ['version = ?', version], :limit => 1).first
|
190
|
+
end
|
191
|
+
|
192
|
+
# Finds versions of this model. Takes an options hash like <tt>find</tt>
|
193
|
+
def find_versions(options = {})
|
194
|
+
versions.find(:all, {:order => 'version'}.merge(options))
|
195
|
+
end
|
196
|
+
|
197
|
+
# Reverts a model to a given version. Takes either a version number or an instance of the versioned model
|
198
|
+
def revert_to(version)
|
199
|
+
if version.is_a?(self.class.versioned_class)
|
200
|
+
return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
|
201
|
+
else
|
202
|
+
return false unless version = find_version(version)
|
203
|
+
end
|
204
|
+
self.clone_versioned_model(version, self)
|
205
|
+
self.send("#{self.class.version_column}=", version.version)
|
206
|
+
true
|
207
|
+
end
|
208
|
+
|
209
|
+
# Reverts a model to a given version and saves the model.
|
210
|
+
# Takes either a version number or an instance of the versioned model
|
211
|
+
def revert_to!(version)
|
212
|
+
revert_to(version) ? save_without_revision : false
|
213
|
+
end
|
214
|
+
|
215
|
+
# Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
|
216
|
+
def save_without_revision
|
217
|
+
old_lock_value = ActiveRecord::Base.lock_optimistically
|
218
|
+
ActiveRecord::Base.lock_optimistically = false if old_lock_value
|
219
|
+
disable_acts_as_versioned_callbacks
|
220
|
+
save_result = self.save
|
221
|
+
enable_acts_as_versioned_callbacks
|
222
|
+
ActiveRecord::Base.lock_optimistically = true if old_lock_value
|
223
|
+
save_result
|
224
|
+
end
|
225
|
+
|
226
|
+
# Returns an array of attribute keys that are versioned. See non_versioned_fields
|
227
|
+
def versioned_attributes
|
228
|
+
self.attributes.keys.select { |k| !self.class.non_versioned_fields.include?(k) }
|
229
|
+
end
|
230
|
+
|
231
|
+
# If called with no parameters, gets whether the current model is dirty and needs to be versioned.
|
232
|
+
# If called with a single parameter, gets whether the parameter is currently dirty.
|
233
|
+
def dirty?(attr_name = nil)
|
234
|
+
attr_name.nil? ?
|
235
|
+
(!self.class.track_changed_attributes or (changed_attributes and changed_attributes.length > 0)) :
|
236
|
+
(changed_attributes and changed_attributes.include?(attr_name.to_s))
|
237
|
+
end
|
238
|
+
|
239
|
+
# Clones a model. Used when saving a new version or reverting a model's version.
|
240
|
+
def clone_versioned_model(orig_model, new_model)
|
241
|
+
self.versioned_attributes.each do |key|
|
242
|
+
new_model.send("#{key}=", orig_model.attributes[key]) if orig_model.attribute_present?(key)
|
243
|
+
end
|
244
|
+
|
245
|
+
if orig_model.is_a?(self.class.versioned_class)
|
246
|
+
new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
|
247
|
+
elsif new_model.is_a?(self.class.versioned_class)
|
248
|
+
new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>dirty?</tt>.
|
253
|
+
def save_version?
|
254
|
+
version_condition_met? and dirty?
|
255
|
+
end
|
256
|
+
|
257
|
+
# Checks condition set in the :if option to check whether a revision should be created or not. Override this for
|
258
|
+
# custom version condition checking.
|
259
|
+
def version_condition_met?
|
260
|
+
case
|
261
|
+
when version_condition.is_a?(Symbol)
|
262
|
+
send(version_condition)
|
263
|
+
when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
|
264
|
+
version_condition.call(self)
|
265
|
+
else
|
266
|
+
version_condition
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
protected
|
271
|
+
# sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
|
272
|
+
def set_new_version
|
273
|
+
self.send("#{self.class.version_column}=", self.next_version) if new_record? or (!locking_enabled? and save_version?)
|
274
|
+
end
|
275
|
+
|
276
|
+
# Gets the next available version for the current record, or 1 for a new record
|
277
|
+
def next_version
|
278
|
+
return 1 if new_record?
|
279
|
+
connection.select_one("SELECT MAX(version)+1 AS next_version FROM #{self.class.versioned_table_name} WHERE #{self.class.versioned_foreign_key} = #{self.id}")['next_version'] || 1
|
280
|
+
end
|
281
|
+
|
282
|
+
# clears current dirty attributes. Called after save.
|
283
|
+
def clear_changed_attributes
|
284
|
+
self.changed_attributes = []
|
285
|
+
end
|
286
|
+
|
287
|
+
private
|
288
|
+
unless defined?(ACTS_AS_VERSIONED_CALLBACKS)
|
289
|
+
ACTS_AS_VERSIONED_CALLBACKS = [:set_new_version, :save_version_on_create, :save_version, :clear_changed_attributes]
|
290
|
+
end
|
291
|
+
|
292
|
+
ACTS_AS_VERSIONED_CALLBACKS.each do |attr_name|
|
293
|
+
alias_method "orig_#{attr_name}".to_sym, attr_name
|
294
|
+
end
|
295
|
+
|
296
|
+
def empty_callback() end #:nodoc:
|
297
|
+
|
298
|
+
def enable_acts_as_versioned_callbacks
|
299
|
+
self.class.class_eval do
|
300
|
+
ACTS_AS_VERSIONED_CALLBACKS.each do |attr_name|
|
301
|
+
alias_method attr_name, "orig_#{attr_name}".to_sym
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def disable_acts_as_versioned_callbacks
|
307
|
+
self.class.class_eval do
|
308
|
+
ACTS_AS_VERSIONED_CALLBACKS.each do |attr_name|
|
309
|
+
alias_method attr_name, :empty_callback
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
module ClassMethods
|
315
|
+
# Returns an array of columns that are versioned. See non_versioned_fields
|
316
|
+
def versioned_columns
|
317
|
+
self.columns.select { |c| !non_versioned_fields.include?(c.name) }
|
318
|
+
end
|
319
|
+
|
320
|
+
# Returns an instance of the dynamic versioned model
|
321
|
+
def versioned_class
|
322
|
+
"ActiveRecord::Acts::Versioned::#{versioned_class_name}".constantize
|
323
|
+
end
|
324
|
+
|
325
|
+
# An array of fields that are not saved in the versioned table
|
326
|
+
def non_versioned_fields
|
327
|
+
[self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
|
328
|
+
end
|
329
|
+
|
330
|
+
# Rake migration task to create the versioned table using options passed to acts_as_versioned
|
331
|
+
def create_versioned_table(create_table_options = {})
|
332
|
+
self.transaction do
|
333
|
+
# create version column in main table if it does not exist
|
334
|
+
if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
|
335
|
+
self.connection.add_column table_name, :version, :integer
|
336
|
+
end
|
337
|
+
|
338
|
+
self.connection.create_table(versioned_table_name, create_table_options) do |t|
|
339
|
+
t.column versioned_foreign_key, :integer
|
340
|
+
t.column :version, :integer
|
341
|
+
end
|
342
|
+
|
343
|
+
updated_col = nil
|
344
|
+
self.versioned_columns.each do |col|
|
345
|
+
updated_col = col if !updated_col and %(updated_at updated_on).include?(col.name)
|
346
|
+
self.connection.add_column versioned_table_name, col.name, col.type,
|
347
|
+
:limit => col.limit,
|
348
|
+
:default => col.default
|
349
|
+
end
|
350
|
+
|
351
|
+
if type_col = self.columns_hash[inheritance_column]
|
352
|
+
self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
|
353
|
+
:limit => type_col.limit,
|
354
|
+
:default => type_col.default
|
355
|
+
end
|
356
|
+
|
357
|
+
if updated_col.nil?
|
358
|
+
self.connection.add_column versioned_table_name, :updated_at, :timestamp
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Rake migration task to drop the versioned table
|
364
|
+
def drop_versioned_table
|
365
|
+
self.connection.drop_table versioned_table_name
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
ActiveRecord::Base.class_eval { include ActiveRecord::Acts::Versioned }
|
@@ -0,0 +1,25 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'active_record'
|
6
|
+
require 'active_record/fixtures'
|
7
|
+
require 'active_support/binding_of_caller'
|
8
|
+
require 'active_support/breakpoint'
|
9
|
+
require 'connection'
|
10
|
+
require 'acts_as_versioned'
|
11
|
+
|
12
|
+
class Test::Unit::TestCase #:nodoc:
|
13
|
+
def create_fixtures(*table_names)
|
14
|
+
if block_given?
|
15
|
+
Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names) { yield }
|
16
|
+
else
|
17
|
+
Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
|
23
|
+
Test::Unit::TestCase.use_instantiated_fixtures = false
|
24
|
+
Test::Unit::TestCase.use_transactional_fixtures = (ENV['AR_TX_FIXTURES'] == "yes")
|
25
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
print "Using native DB2\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
5
|
+
|
6
|
+
db1 = 'arversioned'
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(
|
9
|
+
:adapter => "db2",
|
10
|
+
:host => "localhost",
|
11
|
+
:username => "arunit",
|
12
|
+
:password => "arunit",
|
13
|
+
:database => db1
|
14
|
+
)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
print "Using native MySQL\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
5
|
+
|
6
|
+
db1 = 'activerecord_versioned'
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(
|
9
|
+
:adapter => "mysql",
|
10
|
+
:host => "localhost",
|
11
|
+
:username => "rails",
|
12
|
+
:password => "",
|
13
|
+
:database => db1
|
14
|
+
)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
print "Using OCI Oracle\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Logger.new STDOUT
|
5
|
+
ActiveRecord::Base.logger.level = Logger::WARN
|
6
|
+
|
7
|
+
db1 = 'activerecord_versioned'
|
8
|
+
|
9
|
+
ActiveRecord::Base.establish_connection(
|
10
|
+
:adapter => 'oci',
|
11
|
+
:host => '', # can use an oracle SID
|
12
|
+
:username => 'arunit',
|
13
|
+
:password => 'arunit',
|
14
|
+
:database => db1
|
15
|
+
)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
print "Using native PostgreSQL\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
5
|
+
|
6
|
+
db1 = 'activerecord_versioned'
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(
|
9
|
+
:adapter => "postgresql",
|
10
|
+
:host => nil,
|
11
|
+
:username => "postgres",
|
12
|
+
:password => "postgres",
|
13
|
+
:database => db1
|
14
|
+
)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
print "Using native SQlite\n"
|
2
|
+
require 'logger'
|
3
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
4
|
+
|
5
|
+
class SqliteError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
BASE_DIR = File.expand_path(File.dirname(__FILE__) + '/../../fixtures')
|
9
|
+
sqlite_test_db = "#{BASE_DIR}/activerecord_versioned.sqlite"
|
10
|
+
|
11
|
+
def make_connection(clazz, db_file, db_definitions_file)
|
12
|
+
unless File.exist?(db_file)
|
13
|
+
puts "SQLite database not found at #{db_file}. Rebuilding it."
|
14
|
+
sqlite_command = %Q{sqlite #{db_file} "create table a (a integer); drop table a;"}
|
15
|
+
puts "Executing '#{sqlite_command}'"
|
16
|
+
raise SqliteError.new("Seems that there is no sqlite executable available") unless system(sqlite_command)
|
17
|
+
clazz.establish_connection(
|
18
|
+
:adapter => "sqlite",
|
19
|
+
:dbfile => db_file)
|
20
|
+
script = File.read("#{BASE_DIR}/db_definitions/#{db_definitions_file}")
|
21
|
+
# SQLite-Ruby has problems with semi-colon separated commands, so split and execute one at a time
|
22
|
+
script.split(';').each do
|
23
|
+
|command|
|
24
|
+
clazz.connection.execute(command) unless command.strip.empty?
|
25
|
+
end
|
26
|
+
else
|
27
|
+
clazz.establish_connection(
|
28
|
+
:adapter => "sqlite",
|
29
|
+
:dbfile => db_file)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
make_connection(ActiveRecord::Base, sqlite_test_db, 'sqlite.sql')
|
34
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
print "Using native SQLite3\n"
|
2
|
+
require 'logger'
|
3
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
4
|
+
|
5
|
+
class SqliteError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
BASE_DIR = File.expand_path(File.dirname(__FILE__) + '/../../fixtures')
|
9
|
+
sqlite_test_db = "#{BASE_DIR}/activerecord_versioned.sqlite3"
|
10
|
+
|
11
|
+
def make_connection(clazz, db_file, db_definitions_file)
|
12
|
+
unless File.exist?(db_file)
|
13
|
+
puts "SQLite3 database not found at #{db_file}. Rebuilding it."
|
14
|
+
sqlite_command = %Q{sqlite3 #{db_file} "create table a (a integer); drop table a;"}
|
15
|
+
puts "Executing '#{sqlite_command}'"
|
16
|
+
raise SqliteError.new("Seems that there is no sqlite3 executable available") unless system(sqlite_command)
|
17
|
+
clazz.establish_connection(
|
18
|
+
:adapter => "sqlite3",
|
19
|
+
:dbfile => db_file)
|
20
|
+
script = File.read("#{BASE_DIR}/db_definitions/#{db_definitions_file}")
|
21
|
+
# SQLite-Ruby has problems with semi-colon separated commands, so split and execute one at a time
|
22
|
+
script.split(';').each do
|
23
|
+
|command|
|
24
|
+
clazz.connection.execute(command) unless command.strip.empty?
|
25
|
+
end
|
26
|
+
else
|
27
|
+
clazz.establish_connection(
|
28
|
+
:adapter => "sqlite3",
|
29
|
+
:dbfile => db_file)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
make_connection(ActiveRecord::Base, sqlite_test_db, 'sqlite.sql')
|
@@ -0,0 +1,14 @@
|
|
1
|
+
print "Using native SQLServer\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
5
|
+
|
6
|
+
db1 = 'activerecord_versioned'
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(
|
9
|
+
:adapter => "sqlserver",
|
10
|
+
:host => "localhost",
|
11
|
+
:username => "sa",
|
12
|
+
:password => "",
|
13
|
+
:database => db1
|
14
|
+
)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
print "Using native SQLServer via ODBC\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
5
|
+
|
6
|
+
dsn1 = 'activerecord_versioned'
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(
|
9
|
+
:adapter => "sqlserver",
|
10
|
+
:mode => "ODBC",
|
11
|
+
:host => "localhost",
|
12
|
+
:username => "sa",
|
13
|
+
:password => "",
|
14
|
+
:dsn => dsn1
|
15
|
+
)
|
Binary file
|
@@ -0,0 +1,36 @@
|
|
1
|
+
CREATE TABLE `pages` (
|
2
|
+
`id` int(11) NOT NULL auto_increment,
|
3
|
+
`version` int(11) default NULL,
|
4
|
+
`title` varchar(255) default NULL,
|
5
|
+
`body` TEXT NOT NULL,
|
6
|
+
`updated_on` datetime default NULL,
|
7
|
+
PRIMARY KEY (`id`)
|
8
|
+
) TYPE=InnoDB;
|
9
|
+
|
10
|
+
CREATE TABLE `page_versions` (
|
11
|
+
`id` int(11) NOT NULL auto_increment,
|
12
|
+
`page_id` int(11) default NULL,
|
13
|
+
`version` int(11) default NULL,
|
14
|
+
`title` varchar(255) default NULL,
|
15
|
+
`body` TEXT NOT NULL,
|
16
|
+
`updated_on` datetime default NULL,
|
17
|
+
PRIMARY KEY (`id`)
|
18
|
+
) TYPE=InnoDB;
|
19
|
+
|
20
|
+
CREATE TABLE `locked_pages` (
|
21
|
+
`id` int(11) NOT NULL auto_increment,
|
22
|
+
`lock_version` int(11) default NULL,
|
23
|
+
`title` varchar(255) default NULL,
|
24
|
+
`type` varchar(255) default NULL,
|
25
|
+
PRIMARY KEY (`id`)
|
26
|
+
) TYPE=InnoDB;
|
27
|
+
|
28
|
+
CREATE TABLE `locked_pages_revisions` (
|
29
|
+
`id` int(11) NOT NULL auto_increment,
|
30
|
+
`page_id` int(11) default NULL,
|
31
|
+
`version` int(11) default NULL,
|
32
|
+
`title` varchar(255) default NULL,
|
33
|
+
`version_type` varchar(255) default NULL,
|
34
|
+
`updated_at` datetime default NULL,
|
35
|
+
PRIMARY KEY (`id`)
|
36
|
+
) TYPE=InnoDB;
|
@@ -0,0 +1,34 @@
|
|
1
|
+
CREATE TABLE pages (
|
2
|
+
id SERIAL,
|
3
|
+
version INTEGER,
|
4
|
+
title VARCHAR(255),
|
5
|
+
body TEXT,
|
6
|
+
updated_on TIMESTAMP
|
7
|
+
);
|
8
|
+
SELECT setval('pages_id_seq', 100);
|
9
|
+
|
10
|
+
CREATE TABLE page_versions (
|
11
|
+
id SERIAL,
|
12
|
+
page_id INTEGER,
|
13
|
+
version INTEGER,
|
14
|
+
title VARCHAR(255),
|
15
|
+
body TEXT,
|
16
|
+
updated_on TIMESTAMP
|
17
|
+
);
|
18
|
+
|
19
|
+
CREATE TABLE locked_pages (
|
20
|
+
id SERIAL,
|
21
|
+
lock_version INTEGER,
|
22
|
+
title VARCHAR(255),
|
23
|
+
type VARCHAR(255)
|
24
|
+
);
|
25
|
+
SELECT setval('pages_id_seq', 100);
|
26
|
+
|
27
|
+
CREATE TABLE locked_pages_revisions (
|
28
|
+
id SERIAL,
|
29
|
+
page_id INTEGER,
|
30
|
+
version INTEGER,
|
31
|
+
title VARCHAR(255),
|
32
|
+
version_type VARCHAR(255),
|
33
|
+
updated_at TIMESTAMP
|
34
|
+
);
|
@@ -0,0 +1,32 @@
|
|
1
|
+
CREATE TABLE 'pages' (
|
2
|
+
'id' INTEGER NOT NULL PRIMARY KEY,
|
3
|
+
'version' INTEGER,
|
4
|
+
'title' VARCHAR(255),
|
5
|
+
'body' TEXT,
|
6
|
+
'updated_on' DATETIME DEFAULT NULL
|
7
|
+
);
|
8
|
+
|
9
|
+
CREATE TABLE 'page_versions' (
|
10
|
+
'id' INTEGER NOT NULL PRIMARY KEY,
|
11
|
+
'page_id' INTEGER NOT NULL,
|
12
|
+
'version' INTEGER NOT NULL,
|
13
|
+
'title' VARCHAR(255),
|
14
|
+
'body' TEXT DEFAULT NULL,
|
15
|
+
'updated_on' DATETIME DEFAULT NULL
|
16
|
+
);
|
17
|
+
|
18
|
+
CREATE TABLE 'locked_pages' (
|
19
|
+
'id' INTEGER NOT NULL PRIMARY KEY,
|
20
|
+
'lock_version' INTEGER NOT NULL,
|
21
|
+
'title' VARCHAR(255),
|
22
|
+
'type' VARCHAR(255)
|
23
|
+
);
|
24
|
+
|
25
|
+
CREATE TABLE 'locked_pages_revisions' (
|
26
|
+
'id' INTEGER NOT NULL PRIMARY KEY,
|
27
|
+
'page_id' INTEGER NOT NULL,
|
28
|
+
'version' INTEGER NOT NULL,
|
29
|
+
'title' VARCHAR(255),
|
30
|
+
'version_type' VARCHAR(255),
|
31
|
+
'updated_at' DATETIME DEFAULT NULL
|
32
|
+
);
|
@@ -0,0 +1,27 @@
|
|
1
|
+
welcome_1:
|
2
|
+
id: 1
|
3
|
+
page_id: 1
|
4
|
+
title: Welcome to the weblg
|
5
|
+
version: 23
|
6
|
+
version_type: LockedPage
|
7
|
+
|
8
|
+
welcome_2:
|
9
|
+
id: 2
|
10
|
+
page_id: 1
|
11
|
+
title: Welcome to the weblog
|
12
|
+
version: 24
|
13
|
+
version_type: LockedPage
|
14
|
+
|
15
|
+
thinking_1:
|
16
|
+
id: 3
|
17
|
+
page_id: 2
|
18
|
+
title: So I was thinking!!!
|
19
|
+
version: 23
|
20
|
+
version_type: SpecialLockedPage
|
21
|
+
|
22
|
+
thinking_2:
|
23
|
+
id: 4
|
24
|
+
page_id: 2
|
25
|
+
title: So I was thinking
|
26
|
+
version: 24
|
27
|
+
version_type: SpecialLockedPage
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class AddVersionedTables < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table("things") do |t|
|
4
|
+
t.column :title, :text
|
5
|
+
end
|
6
|
+
Thing.create_versioned_table
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.down
|
10
|
+
Thing.drop_versioned_table
|
11
|
+
drop_table "things" rescue nil
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Page < ActiveRecord::Base
|
2
|
+
cattr_accessor :feeling_good
|
3
|
+
@@feeling_good = true
|
4
|
+
|
5
|
+
acts_as_versioned :if => :feeling_good?
|
6
|
+
|
7
|
+
def feeling_good?
|
8
|
+
@@feeling_good == true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class LockedPage < ActiveRecord::Base
|
13
|
+
acts_as_versioned \
|
14
|
+
:inheritance_column => :version_type,
|
15
|
+
:foreign_key => :page_id,
|
16
|
+
:table_name => :locked_pages_revisions,
|
17
|
+
:class_name => 'LockedPageRevision',
|
18
|
+
:version_column => :lock_version,
|
19
|
+
:limit => 2,
|
20
|
+
:if_changed => :title
|
21
|
+
end
|
22
|
+
|
23
|
+
class SpecialLockedPage < LockedPage
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'abstract_unit'
|
2
|
+
|
3
|
+
if ActiveRecord::Base.connection.supports_migrations?
|
4
|
+
class Thing < ActiveRecord::Base
|
5
|
+
attr_accessor :version
|
6
|
+
acts_as_versioned
|
7
|
+
end
|
8
|
+
|
9
|
+
class MigrationTest < Test::Unit::TestCase
|
10
|
+
def setup
|
11
|
+
end
|
12
|
+
|
13
|
+
def teardown
|
14
|
+
ActiveRecord::Base.connection.initialize_schema_information
|
15
|
+
ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
|
16
|
+
|
17
|
+
Thing.connection.drop_table "things" rescue nil
|
18
|
+
Thing.connection.drop_table "thing_versions" rescue nil
|
19
|
+
Thing.reset_column_information
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_versioned_migration
|
23
|
+
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
|
24
|
+
ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
|
25
|
+
t = Thing.create :title => 'blah blah'
|
26
|
+
assert_equal 1, t.versions.size
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/test/tests.rb
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
require 'abstract_unit'
|
2
|
+
require 'fixtures/page'
|
3
|
+
|
4
|
+
class VersionedTest < Test::Unit::TestCase
|
5
|
+
fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions
|
6
|
+
|
7
|
+
def test_saves_versioned_copy
|
8
|
+
p = Page.create :title => 'first title', :body => 'first body'
|
9
|
+
assert !p.new_record?
|
10
|
+
assert_equal 1, p.versions.size
|
11
|
+
assert_equal 1, p.version
|
12
|
+
assert_instance_of Page.versioned_class, p.versions.first
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_rollback_with_version_number
|
16
|
+
p = pages(:welcome)
|
17
|
+
assert_equal 24, p.version
|
18
|
+
assert_equal 'Welcome to the weblog', p.title
|
19
|
+
|
20
|
+
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
|
21
|
+
assert_equal 23, p.version
|
22
|
+
assert_equal 'Welcome to the weblg', p.title
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_versioned_class_name
|
26
|
+
assert_equal 'PageVersion', Page.versioned_class_name
|
27
|
+
assert_equal 'LockedPageRevision', LockedPage.versioned_class_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_rollback_with_version_class
|
31
|
+
p = pages(:welcome)
|
32
|
+
assert_equal 24, p.version
|
33
|
+
assert_equal 'Welcome to the weblog', p.title
|
34
|
+
|
35
|
+
assert p.revert_to!(p.versions.first), "Couldn't revert to 23"
|
36
|
+
assert_equal 23, p.version
|
37
|
+
assert_equal 'Welcome to the weblg', p.title
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_rollback_fails_with_invalid_revision
|
41
|
+
p = locked_pages(:welcome)
|
42
|
+
assert !p.revert_to!(locked_pages(:thinking))
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_saves_versioned_copy_with_options
|
46
|
+
p = LockedPage.create :title => 'first title'
|
47
|
+
assert !p.new_record?
|
48
|
+
assert_equal 1, p.versions.size
|
49
|
+
assert_instance_of LockedPage.versioned_class, p.versions.first
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_rollback_with_version_number_with_options
|
53
|
+
p = locked_pages(:welcome)
|
54
|
+
assert_equal 'Welcome to the weblog', p.title
|
55
|
+
assert_equal 'LockedPage', p.versions.first.version_type
|
56
|
+
|
57
|
+
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
|
58
|
+
assert_equal 'Welcome to the weblg', p.title
|
59
|
+
assert_equal 'LockedPage', p.versions.first.version_type
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_rollback_with_version_class_with_options
|
63
|
+
p = locked_pages(:welcome)
|
64
|
+
assert_equal 'Welcome to the weblog', p.title
|
65
|
+
assert_equal 'LockedPage', p.versions.first.version_type
|
66
|
+
|
67
|
+
assert p.revert_to!(p.versions.first), "Couldn't revert to 1"
|
68
|
+
assert_equal 'Welcome to the weblg', p.title
|
69
|
+
assert_equal 'LockedPage', p.versions.first.version_type
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_saves_versioned_copy_with_sti
|
73
|
+
p = SpecialLockedPage.create :title => 'first title'
|
74
|
+
assert !p.new_record?
|
75
|
+
assert_equal 1, p.versions.size
|
76
|
+
assert_instance_of LockedPage.versioned_class, p.versions.first
|
77
|
+
assert_equal 'SpecialLockedPage', p.versions.first.version_type
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_rollback_with_version_number_with_sti
|
81
|
+
p = locked_pages(:thinking)
|
82
|
+
assert_equal 'So I was thinking', p.title
|
83
|
+
|
84
|
+
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 1"
|
85
|
+
assert_equal 'So I was thinking!!!', p.title
|
86
|
+
assert_equal 'SpecialLockedPage', p.versions.first.version_type
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_lock_version_works_with_versioning
|
90
|
+
p = locked_pages(:thinking)
|
91
|
+
p2 = LockedPage.find(p.id)
|
92
|
+
|
93
|
+
p.title = 'fresh title'
|
94
|
+
p.save
|
95
|
+
assert_equal 2, p.versions.size # limit!
|
96
|
+
|
97
|
+
assert_raises(ActiveRecord::StaleObjectError) do
|
98
|
+
p2.title = 'stale title'
|
99
|
+
p2.save
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_version_if_condition
|
104
|
+
p = Page.create :title => "title"
|
105
|
+
assert_equal 1, p.version
|
106
|
+
|
107
|
+
Page.feeling_good = false
|
108
|
+
p.save
|
109
|
+
assert_equal 1, p.version
|
110
|
+
Page.feeling_good = true
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_version_if_condition2
|
114
|
+
# set new if condition
|
115
|
+
Page.class_eval do
|
116
|
+
def new_feeling_good() title[0..0] == 'a'; end
|
117
|
+
alias_method :old_feeling_good, :feeling_good?
|
118
|
+
alias_method :feeling_good?, :new_feeling_good
|
119
|
+
end
|
120
|
+
|
121
|
+
p = Page.create :title => "title"
|
122
|
+
assert_equal 1, p.version # version does not increment
|
123
|
+
assert_equal 1, p.versions(true).size
|
124
|
+
|
125
|
+
p.update_attributes(:title => 'new title')
|
126
|
+
assert_equal 1, p.version # version does not increment
|
127
|
+
assert_equal 1, p.versions(true).size
|
128
|
+
|
129
|
+
p.update_attributes(:title => 'a title')
|
130
|
+
assert_equal 2, p.version
|
131
|
+
assert_equal 2, p.versions(true).size
|
132
|
+
|
133
|
+
# reset original if condition
|
134
|
+
Page.class_eval { alias_method :feeling_good?, :old_feeling_good }
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_version_if_condition_with_block
|
138
|
+
# set new if condition
|
139
|
+
old_condition = Page.version_condition
|
140
|
+
Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' }
|
141
|
+
|
142
|
+
p = Page.create :title => "title"
|
143
|
+
assert_equal 1, p.version # version does not increment
|
144
|
+
assert_equal 1, p.versions(true).size
|
145
|
+
|
146
|
+
p.update_attributes(:title => 'a title')
|
147
|
+
assert_equal 1, p.version # version does not increment
|
148
|
+
assert_equal 1, p.versions(true).size
|
149
|
+
|
150
|
+
p.update_attributes(:title => 'b title')
|
151
|
+
assert_equal 2, p.version
|
152
|
+
assert_equal 2, p.versions(true).size
|
153
|
+
|
154
|
+
# reset original if condition
|
155
|
+
Page.version_condition = old_condition
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_version_no_limit
|
159
|
+
p = Page.create :title => "title", :body => 'first body'
|
160
|
+
p.save
|
161
|
+
p.save
|
162
|
+
5.times do |i|
|
163
|
+
assert_page_title p, i
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_version_max_limit
|
168
|
+
p = LockedPage.create :title => "title"
|
169
|
+
p.update_attributes(:title => "title1")
|
170
|
+
p.update_attributes(:title => "title2")
|
171
|
+
5.times do |i|
|
172
|
+
assert_page_title p, i, :lock_version
|
173
|
+
assert p.versions(true).size <= 2, "locked version can only store 2 versions"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def test_track_changed_attributes_default_value
|
178
|
+
assert !Page.track_changed_attributes
|
179
|
+
assert LockedPage.track_changed_attributes
|
180
|
+
assert SpecialLockedPage.track_changed_attributes
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_version_order
|
184
|
+
assert_equal 23, pages(:welcome).versions.first.version
|
185
|
+
assert_equal 24, pages(:welcome).versions.last.version
|
186
|
+
assert_equal 23, pages(:welcome).find_versions.first.version
|
187
|
+
assert_equal 24, pages(:welcome).find_versions.last.version
|
188
|
+
end
|
189
|
+
|
190
|
+
def test_track_changed_attributes
|
191
|
+
p = LockedPage.create :title => "title"
|
192
|
+
assert_equal 1, p.lock_version
|
193
|
+
assert_equal 1, p.versions(true).size
|
194
|
+
|
195
|
+
p.title = 'title'
|
196
|
+
assert !p.save_version?
|
197
|
+
p.save
|
198
|
+
assert_equal 2, p.lock_version # still increments version because of optimistic locking
|
199
|
+
assert_equal 1, p.versions(true).size
|
200
|
+
|
201
|
+
p.title = 'updated title'
|
202
|
+
assert p.save_version?
|
203
|
+
p.save
|
204
|
+
assert_equal 3, p.lock_version
|
205
|
+
assert_equal 1, p.versions(true).size # version 1 deleted
|
206
|
+
|
207
|
+
p.title = 'updated title!'
|
208
|
+
assert p.save_version?
|
209
|
+
p.save
|
210
|
+
assert_equal 4, p.lock_version
|
211
|
+
assert_equal 2, p.versions(true).size # version 1 deleted
|
212
|
+
end
|
213
|
+
|
214
|
+
def assert_page_title(p, i, version_field = :version)
|
215
|
+
p.title = "title#{i}"
|
216
|
+
p.save
|
217
|
+
assert_equal "title#{i}", p.title
|
218
|
+
assert_equal (i+4), p.send(version_field)
|
219
|
+
end
|
220
|
+
|
221
|
+
def test_find_versions
|
222
|
+
assert_equal 2, locked_pages(:welcome).versions.size
|
223
|
+
assert_equal 1, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%weblog%']).length
|
224
|
+
assert_equal 2, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%web%']).length
|
225
|
+
assert_equal 0, locked_pages(:thinking).find_versions(:conditions => ['title LIKE ?', '%web%']).length
|
226
|
+
assert_equal 2, locked_pages(:welcome).find_versions.length
|
227
|
+
end
|
228
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.10
|
3
|
+
specification_version: 1
|
4
|
+
name: acts_as_versioned
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.2
|
7
|
+
date: 2005-09-18
|
8
|
+
summary: Simple versioning with active record models
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: technoweenie@gmail.com
|
12
|
+
homepage: http://techno-weenie.net
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire: acts_as_versioned
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
-
|
22
|
+
- ">"
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.0.0
|
25
|
+
version:
|
26
|
+
platform: ruby
|
27
|
+
authors:
|
28
|
+
- Rick Olson
|
29
|
+
files:
|
30
|
+
- lib/acts_as_versioned.rb
|
31
|
+
- test/abstract_unit.rb
|
32
|
+
- test/connections
|
33
|
+
- test/fixtures
|
34
|
+
- test/migration_test.rb
|
35
|
+
- test/tests.rb
|
36
|
+
- test/versioned_test.rb
|
37
|
+
- test/connections/native_db2
|
38
|
+
- test/connections/native_mysql
|
39
|
+
- test/connections/native_oci
|
40
|
+
- test/connections/native_postgresql
|
41
|
+
- test/connections/native_sqlite
|
42
|
+
- test/connections/native_sqlite3
|
43
|
+
- test/connections/native_sqlserver
|
44
|
+
- test/connections/native_sqlserver_odbc
|
45
|
+
- test/connections/native_db2/connection.rb
|
46
|
+
- test/connections/native_mysql/connection.rb
|
47
|
+
- test/connections/native_oci/connection.rb
|
48
|
+
- test/connections/native_postgresql/connection.rb
|
49
|
+
- test/connections/native_sqlite/connection.rb
|
50
|
+
- test/connections/native_sqlite3/connection.rb
|
51
|
+
- test/connections/native_sqlserver/connection.rb
|
52
|
+
- test/connections/native_sqlserver_odbc/connection.rb
|
53
|
+
- test/fixtures/activerecord_versioned.sqlite3
|
54
|
+
- test/fixtures/db_definitions
|
55
|
+
- test/fixtures/locked_pages.yml
|
56
|
+
- test/fixtures/locked_pages_revisions.yml
|
57
|
+
- test/fixtures/migrations
|
58
|
+
- test/fixtures/page.rb
|
59
|
+
- test/fixtures/page_versions.yml
|
60
|
+
- test/fixtures/pages.yml
|
61
|
+
- test/fixtures/db_definitions/mysql.drop.sql
|
62
|
+
- test/fixtures/db_definitions/mysql.sql
|
63
|
+
- test/fixtures/db_definitions/postgresql.drop.sql
|
64
|
+
- test/fixtures/db_definitions/postgresql.sql
|
65
|
+
- test/fixtures/db_definitions/sqlite.drop.sql
|
66
|
+
- test/fixtures/db_definitions/sqlite.sql
|
67
|
+
- test/fixtures/migrations/1_add_versioned_tables.rb
|
68
|
+
- README
|
69
|
+
- MIT-LICENSE
|
70
|
+
- CHANGELOG
|
71
|
+
- RUNNING_UNIT_TESTS
|
72
|
+
test_files:
|
73
|
+
- test/tests.rb
|
74
|
+
rdoc_options: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
executables: []
|
77
|
+
extensions: []
|
78
|
+
requirements: []
|
79
|
+
dependencies:
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: activerecord
|
82
|
+
version_requirement:
|
83
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
84
|
+
requirements:
|
85
|
+
-
|
86
|
+
- ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 1.10.1
|
89
|
+
version:
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: activesupport
|
92
|
+
version_requirement:
|
93
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
94
|
+
requirements:
|
95
|
+
-
|
96
|
+
- ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 1.1.1
|
99
|
+
version:
|