johnsbrn-acts_as_versioned 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 2
3
+ :major: 0
4
+ :minor: 5
@@ -0,0 +1,484 @@
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 optimistic locking if the lock_version
26
+ # column is present as well.
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
29
+ # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
30
+ #
31
+ # class Page < ActiveRecord::Base
32
+ # # assumes pages_versions table
33
+ # acts_as_versioned
34
+ # end
35
+ #
36
+ # Example:
37
+ #
38
+ # page = Page.create(:title => 'hello world!')
39
+ # page.version # => 1
40
+ #
41
+ # page.title = 'hello world'
42
+ # page.save
43
+ # page.version # => 2
44
+ # page.versions.size # => 2
45
+ #
46
+ # page.revert_to(1) # using version number
47
+ # page.title # => 'hello world!'
48
+ #
49
+ # page.revert_to(page.versions.last) # using versioned instance
50
+ # page.title # => 'hello world'
51
+ #
52
+ # page.versions.earliest # efficient query to find the first version
53
+ # page.versions.latest # efficient query to find the most recently created version
54
+ #
55
+ #
56
+ # Simple Queries to page between versions
57
+ #
58
+ # page.versions.before(version)
59
+ # page.versions.after(version)
60
+ #
61
+ # Access the previous/next versions from the versioned model itself
62
+ #
63
+ # version = page.versions.latest
64
+ # version.previous # go back one version
65
+ # version.next # go forward one version
66
+ #
67
+ # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
68
+ module Versioned
69
+ CALLBACKS = [:set_new_version, :save_version, :save_version?]
70
+ def self.included(base) # :nodoc:
71
+ base.extend ClassMethods
72
+ end
73
+
74
+ module ClassMethods
75
+ # == Configuration options
76
+ #
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)
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)
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.
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.
85
+ # For finer control, pass either a Proc or modify Model#version_condition_met?
86
+ #
87
+ # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
88
+ #
89
+ # or...
90
+ #
91
+ # class Auction
92
+ # def version_condition_met? # totally bypasses the <tt>:if</tt> option
93
+ # !expired?
94
+ # end
95
+ # end
96
+ #
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.
99
+ #
100
+ # * <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
101
+ # to create an anonymous mixin:
102
+ #
103
+ # class Auction
104
+ # acts_as_versioned do
105
+ # def started?
106
+ # !started_at.nil?
107
+ # end
108
+ # end
109
+ # end
110
+ #
111
+ # or...
112
+ #
113
+ # module AuctionExtension
114
+ # def started?
115
+ # !started_at.nil?
116
+ # end
117
+ # end
118
+ # class Auction
119
+ # acts_as_versioned :extend => AuctionExtension
120
+ # end
121
+ #
122
+ # Example code:
123
+ #
124
+ # @auction = Auction.find(1)
125
+ # @auction.started?
126
+ # @auction.versions.first.started?
127
+ #
128
+ # == Database Schema
129
+ #
130
+ # The model that you're versioning needs to have a 'version' attribute. The model is versioned
131
+ # into a table called #{model}_versions where the model name is singlular. The _versions table should
132
+ # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
133
+ #
134
+ # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
135
+ # then that field is reflected in the versioned model as 'versioned_type' by default.
136
+ #
137
+ # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
138
+ # method, perfect for a migration. It will also create the version column if the main model does not already have it.
139
+ #
140
+ # class AddVersions < ActiveRecord::Migration
141
+ # def self.up
142
+ # # create_versioned_table takes the same options hash
143
+ # # that create_table does
144
+ # Post.create_versioned_table
145
+ # end
146
+ #
147
+ # def self.down
148
+ # Post.drop_versioned_table
149
+ # end
150
+ # end
151
+ #
152
+ # == Changing What Fields Are Versioned
153
+ #
154
+ # By default, acts_as_versioned will version all but these fields:
155
+ #
156
+ # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
157
+ #
158
+ # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
159
+ #
160
+ # class Post < ActiveRecord::Base
161
+ # acts_as_versioned
162
+ # self.non_versioned_columns << 'comments_count'
163
+ # end
164
+ #
165
+ def acts_as_versioned(options = {}, &extension)
166
+ # don't allow multiple calls
167
+ return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
168
+
169
+ send :include, ActiveRecord::Acts::Versioned::ActMethods
170
+
171
+ cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
172
+ :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
173
+ :version_association_options, :version_if_changed
174
+
175
+ self.versioned_class_name = options[:class_name] || "Version"
176
+ self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
177
+ self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
178
+ self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
179
+ self.version_column = options[:version_column] || 'version'
180
+ self.version_sequence_name = options[:sequence_name]
181
+ self.max_version_limit = options[:limit].to_i
182
+ self.version_condition = options[:if] || true
183
+ self.non_versioned_columns = [self.primary_key, inheritance_column, self.version_column, 'lock_version', versioned_inheritance_column, 'created_at', 'created_on'] + options[:non_versioned_columns].to_a.map(&:to_s)
184
+ self.version_association_options = {
185
+ :class_name => "#{self.to_s}::#{versioned_class_name}",
186
+ :foreign_key => versioned_foreign_key,
187
+ :dependent => :delete_all
188
+ }.merge(options[:association_options] || {})
189
+
190
+ if block_given?
191
+ extension_module_name = "#{versioned_class_name}Extension"
192
+ silence_warnings do
193
+ self.const_set(extension_module_name, Module.new(&extension))
194
+ end
195
+
196
+ options[:extend] = self.const_get(extension_module_name)
197
+ end
198
+
199
+ class_eval <<-CLASS_METHODS
200
+ has_many :versions, version_association_options do
201
+ # finds earliest version of this record
202
+ def earliest
203
+ @earliest ||= find(:first, :order => '#{version_column}')
204
+ end
205
+
206
+ # find latest version of this record
207
+ def latest
208
+ @latest ||= find(:first, :order => '#{version_column} desc')
209
+ end
210
+ end
211
+ before_save :set_new_version
212
+ after_save :save_version
213
+ after_save :clear_old_versions
214
+
215
+ unless options[:if_changed].nil?
216
+ self.track_altered_attributes = true
217
+ options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
218
+ self.version_if_changed = options[:if_changed].map(&:to_s)
219
+ end
220
+
221
+ include options[:extend] if options[:extend].is_a?(Module)
222
+ CLASS_METHODS
223
+
224
+ # create the dynamic versioned model
225
+ const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
226
+ def self.reloadable? ; false ; end
227
+ # find first version before the given version
228
+ def self.before(version)
229
+ find :first, :order => 'version desc',
230
+ :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
231
+ end
232
+
233
+ # find first version after the given version.
234
+ def self.after(version)
235
+ find :first, :order => 'version',
236
+ :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
237
+ end
238
+
239
+ def previous
240
+ self.class.before(self)
241
+ end
242
+
243
+ def next
244
+ self.class.after(self)
245
+ end
246
+
247
+ def versions_count
248
+ page.version
249
+ end
250
+ end
251
+
252
+ versioned_class.cattr_accessor :original_class
253
+ versioned_class.original_class = self
254
+ versioned_class.set_table_name versioned_table_name
255
+ versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
256
+ :class_name => "::#{self.to_s}",
257
+ :foreign_key => versioned_foreign_key
258
+ versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
259
+ versioned_class.set_sequence_name version_sequence_name if version_sequence_name
260
+ end
261
+ end
262
+
263
+ module ActMethods
264
+ def self.included(base) # :nodoc:
265
+ base.extend ClassMethods
266
+ end
267
+
268
+ # Saves a version of the model in the versioned table. This is called in the after_save callback by default
269
+ def save_version
270
+ if @saving_version
271
+ @saving_version = nil
272
+ rev = self.class.versioned_class.new
273
+ clone_versioned_model(self, rev)
274
+ rev.send("#{self.class.version_column}=", send(self.class.version_column))
275
+ rev.send("#{self.class.versioned_foreign_key}=", id)
276
+ rev.save
277
+ end
278
+ end
279
+
280
+ # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
281
+ # Override this method to set your own criteria for clearing old versions.
282
+ def clear_old_versions
283
+ return if self.class.max_version_limit == 0
284
+ excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
285
+ if excess_baggage > 0
286
+ self.class.versioned_class.delete_all ["#{self.class.version_column} <= ? and #{self.class.versioned_foreign_key} = ?", excess_baggage, id]
287
+ end
288
+ end
289
+
290
+ # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
291
+ def revert_to(version)
292
+ if version.is_a?(self.class.versioned_class)
293
+ return false unless version.send(self.class.versioned_foreign_key) == id and !version.new_record?
294
+ else
295
+ return false unless version = versions.send("find_by_#{self.class.version_column}", version)
296
+ end
297
+ self.clone_versioned_model(version, self)
298
+ send("#{self.class.version_column}=", version.send(self.class.version_column))
299
+ true
300
+ end
301
+
302
+ # Reverts a model to a given version and saves the model.
303
+ # Takes either a version number or an instance of the versioned model
304
+ def revert_to!(version)
305
+ revert_to(version) ? save_without_revision : false
306
+ end
307
+
308
+ # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
309
+ def save_without_revision
310
+ save_without_revision!
311
+ true
312
+ rescue
313
+ false
314
+ end
315
+
316
+ def save_without_revision!
317
+ without_locking do
318
+ without_revision do
319
+ save!
320
+ end
321
+ end
322
+ end
323
+
324
+ def altered?
325
+ track_altered_attributes ? (version_if_changed - changed).length < version_if_changed.length : changed?
326
+ end
327
+
328
+ # Clones a model. Used when saving a new version or reverting a model's version.
329
+ def clone_versioned_model(orig_model, new_model)
330
+ self.class.versioned_columns.each do |col|
331
+ new_model.send("#{col.name}=", orig_model.send(col.name)) if orig_model.has_attribute?(col.name)
332
+ end
333
+
334
+ if orig_model.is_a?(self.class.versioned_class)
335
+ new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
336
+ elsif new_model.is_a?(self.class.versioned_class)
337
+ new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
338
+ end
339
+ end
340
+
341
+ # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
342
+ def save_version?
343
+ version_condition_met? && altered?
344
+ end
345
+
346
+ # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
347
+ # custom version condition checking.
348
+ def version_condition_met?
349
+ case
350
+ when version_condition.is_a?(Symbol)
351
+ send(version_condition)
352
+ when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
353
+ version_condition.call(self)
354
+ else
355
+ version_condition
356
+ end
357
+ end
358
+
359
+ # Executes the block with the versioning callbacks disabled.
360
+ #
361
+ # @foo.without_revision do
362
+ # @foo.save
363
+ # end
364
+ #
365
+ def without_revision(&block)
366
+ self.class.without_revision(&block)
367
+ end
368
+
369
+ # Turns off optimistic locking for the duration of the block
370
+ #
371
+ # @foo.without_locking do
372
+ # @foo.save
373
+ # end
374
+ #
375
+ def without_locking(&block)
376
+ self.class.without_locking(&block)
377
+ end
378
+
379
+ def empty_callback() end #:nodoc:
380
+
381
+ protected
382
+ # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
383
+ def set_new_version
384
+ @saving_version = new_record? || save_version?
385
+ self.send("#{self.class.version_column}=", next_version) if new_record? || (!locking_enabled? && save_version?)
386
+ end
387
+
388
+ # Gets the next available version for the current record, or 1 for a new record
389
+ def next_version
390
+ (new_record? ? 0 : versions.calculate(:max, version_column).to_i) + 1
391
+ end
392
+
393
+ module ClassMethods
394
+ # Returns an array of columns that are versioned. See non_versioned_columns
395
+ def versioned_columns
396
+ @versioned_columns ||= columns.select { |c| !non_versioned_columns.include?(c.name) }
397
+ end
398
+
399
+ # Returns an instance of the dynamic versioned model
400
+ def versioned_class
401
+ const_get versioned_class_name
402
+ end
403
+
404
+ # Rake migration task to create the versioned table using options passed to acts_as_versioned
405
+ def create_versioned_table(create_table_options = {})
406
+ # create version column in main table if it does not exist
407
+ if !self.content_columns.find { |c| [version_column.to_s, 'lock_version'].include? c.name }
408
+ self.connection.add_column table_name, version_column, :integer
409
+ self.reset_column_information
410
+ end
411
+
412
+ return if connection.tables.include?(versioned_table_name.to_s)
413
+
414
+ self.connection.create_table(versioned_table_name, create_table_options) do |t|
415
+ t.column versioned_foreign_key, :integer
416
+ t.column version_column, :integer
417
+ end
418
+
419
+ self.versioned_columns.each do |col|
420
+ self.connection.add_column versioned_table_name, col.name, col.type,
421
+ :limit => col.limit,
422
+ :default => col.default,
423
+ :scale => col.scale,
424
+ :precision => col.precision
425
+ end
426
+
427
+ if type_col = self.columns_hash[inheritance_column]
428
+ self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
429
+ :limit => type_col.limit,
430
+ :default => type_col.default,
431
+ :scale => type_col.scale,
432
+ :precision => type_col.precision
433
+ end
434
+
435
+ self.connection.add_index versioned_table_name, versioned_foreign_key
436
+ end
437
+
438
+ # Rake migration task to drop the versioned table
439
+ def drop_versioned_table
440
+ self.connection.drop_table versioned_table_name
441
+ end
442
+
443
+ # Executes the block with the versioning callbacks disabled.
444
+ #
445
+ # Foo.without_revision do
446
+ # @foo.save
447
+ # end
448
+ #
449
+ def without_revision(&block)
450
+ class_eval do
451
+ CALLBACKS.each do |attr_name|
452
+ alias_method "orig_#{attr_name}".to_sym, attr_name
453
+ alias_method attr_name, :empty_callback
454
+ end
455
+ end
456
+ block.call
457
+ ensure
458
+ class_eval do
459
+ CALLBACKS.each do |attr_name|
460
+ alias_method attr_name, "orig_#{attr_name}".to_sym
461
+ end
462
+ end
463
+ end
464
+
465
+ # Turns off optimistic locking for the duration of the block
466
+ #
467
+ # Foo.without_locking do
468
+ # @foo.save
469
+ # end
470
+ #
471
+ def without_locking(&block)
472
+ current = ActiveRecord::Base.lock_optimistically
473
+ ActiveRecord::Base.lock_optimistically = false if current
474
+ result = block.call
475
+ ActiveRecord::Base.lock_optimistically = true if current
476
+ result
477
+ end
478
+ end
479
+ end
480
+ end
481
+ end
482
+ end
483
+
484
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
@@ -0,0 +1,48 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../../../rails/activesupport/lib')
2
+ $:.unshift(File.dirname(__FILE__) + '/../../../rails/activerecord/lib')
3
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
4
+ require 'test/unit'
5
+ begin
6
+ require 'active_support'
7
+ require 'active_record'
8
+ require 'active_record/fixtures'
9
+ rescue LoadError
10
+ require 'rubygems'
11
+ retry
12
+ end
13
+
14
+ begin
15
+ require 'ruby-debug'
16
+ Debugger.start
17
+ rescue LoadError
18
+ end
19
+
20
+ require 'acts_as_versioned'
21
+
22
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
23
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
24
+ ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite3']}
25
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
26
+
27
+ load(File.dirname(__FILE__) + "/schema.rb")
28
+
29
+ # set up custom sequence on widget_versions for DBs that support sequences
30
+ if ENV['DB'] == 'postgresql'
31
+ ActiveRecord::Base.connection.execute "DROP SEQUENCE widgets_seq;" rescue nil
32
+ ActiveRecord::Base.connection.remove_column :widget_versions, :id
33
+ ActiveRecord::Base.connection.execute "CREATE SEQUENCE widgets_seq START 101;"
34
+ ActiveRecord::Base.connection.execute "ALTER TABLE widget_versions ADD COLUMN id INTEGER PRIMARY KEY DEFAULT nextval('widgets_seq');"
35
+ end
36
+
37
+ Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
38
+ $:.unshift(Test::Unit::TestCase.fixture_path)
39
+
40
+ class Test::Unit::TestCase #:nodoc:
41
+ # Turn off transactional fixtures if you're working with MyISAM tables in MySQL
42
+ self.use_transactional_fixtures = true
43
+
44
+ # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
45
+ self.use_instantiated_fixtures = false
46
+
47
+ # Add more helper methods to be used by all tests here...
48
+ end
@@ -0,0 +1,18 @@
1
+ sqlite:
2
+ :adapter: sqlite
3
+ :dbfile: acts_as_versioned_plugin.sqlite.db
4
+ sqlite3:
5
+ :adapter: sqlite3
6
+ :dbfile: acts_as_versioned_plugin.sqlite3.db
7
+ postgresql:
8
+ :adapter: postgresql
9
+ :username: postgres
10
+ :password: postgres
11
+ :database: acts_as_versioned_plugin_test
12
+ :min_messages: ERROR
13
+ mysql:
14
+ :adapter: mysql
15
+ :host: localhost
16
+ :username: rails
17
+ :password:
18
+ :database: acts_as_versioned_plugin_test
@@ -0,0 +1,6 @@
1
+ caged:
2
+ id: 1
3
+ name: caged
4
+ mly:
5
+ id: 2
6
+ name: mly
@@ -0,0 +1,3 @@
1
+ class Landmark < ActiveRecord::Base
2
+ acts_as_versioned :if_changed => [ :name, :longitude, :latitude ]
3
+ end
@@ -0,0 +1,7 @@
1
+ washington:
2
+ id: 1
3
+ landmark_id: 1
4
+ version: 1
5
+ name: Washington, D.C.
6
+ latitude: 38.895
7
+ longitude: -77.036667
@@ -0,0 +1,7 @@
1
+ washington:
2
+ id: 1
3
+ name: Washington, D.C.
4
+ latitude: 38.895
5
+ longitude: -77.036667
6
+ doesnt_trigger_version: This is not important
7
+ version: 1
@@ -0,0 +1,10 @@
1
+ welcome:
2
+ id: 1
3
+ title: Welcome to the weblog
4
+ lock_version: 24
5
+ type: LockedPage
6
+ thinking:
7
+ id: 2
8
+ title: So I was thinking
9
+ lock_version: 24
10
+ type: SpecialLockedPage
@@ -0,0 +1,27 @@
1
+ welcome_1:
2
+ id: 1
3
+ page_id: 1
4
+ title: Welcome to the weblg
5
+ lock_version: 23
6
+ version_type: LockedPage
7
+
8
+ welcome_2:
9
+ id: 2
10
+ page_id: 1
11
+ title: Welcome to the weblog
12
+ lock_version: 24
13
+ version_type: LockedPage
14
+
15
+ thinking_1:
16
+ id: 3
17
+ page_id: 2
18
+ title: So I was thinking!!!
19
+ lock_version: 23
20
+ version_type: SpecialLockedPage
21
+
22
+ thinking_2:
23
+ id: 4
24
+ page_id: 2
25
+ title: So I was thinking
26
+ lock_version: 24
27
+ version_type: SpecialLockedPage
@@ -0,0 +1,15 @@
1
+ class AddVersionedTables < ActiveRecord::Migration
2
+ def self.up
3
+ create_table("things") do |t|
4
+ t.column :title, :text
5
+ t.column :price, :decimal, :precision => 7, :scale => 2
6
+ t.column :type, :string
7
+ end
8
+ Thing.create_versioned_table
9
+ end
10
+
11
+ def self.down
12
+ Thing.drop_versioned_table
13
+ drop_table "things" rescue nil
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ class Page < ActiveRecord::Base
2
+ belongs_to :author
3
+ has_many :authors, :through => :versions, :order => 'name'
4
+ belongs_to :revisor, :class_name => 'Author'
5
+ has_many :revisors, :class_name => 'Author', :through => :versions, :order => 'name'
6
+ acts_as_versioned :if => :feeling_good? do
7
+ def self.included(base)
8
+ base.cattr_accessor :feeling_good
9
+ base.feeling_good = true
10
+ base.belongs_to :author
11
+ base.belongs_to :revisor, :class_name => 'Author'
12
+ end
13
+
14
+ def feeling_good?
15
+ @@feeling_good == true
16
+ end
17
+ end
18
+ end
19
+
20
+ module LockedPageExtension
21
+ def hello_world
22
+ 'hello_world'
23
+ end
24
+ end
25
+
26
+ class LockedPage < ActiveRecord::Base
27
+ acts_as_versioned \
28
+ :inheritance_column => :version_type,
29
+ :foreign_key => :page_id,
30
+ :table_name => :locked_pages_revisions,
31
+ :class_name => 'LockedPageRevision',
32
+ :version_column => :lock_version,
33
+ :limit => 2,
34
+ :if_changed => :title,
35
+ :extend => LockedPageExtension
36
+ end
37
+
38
+ class SpecialLockedPage < LockedPage
39
+ end
40
+
41
+ class Author < ActiveRecord::Base
42
+ has_many :pages
43
+ end
@@ -0,0 +1,16 @@
1
+ welcome_2:
2
+ id: 1
3
+ page_id: 1
4
+ title: Welcome to the weblog
5
+ body: Such a lovely day
6
+ version: 24
7
+ author_id: 1
8
+ revisor_id: 1
9
+ welcome_1:
10
+ id: 2
11
+ page_id: 1
12
+ title: Welcome to the weblg
13
+ body: Such a lovely day
14
+ version: 23
15
+ author_id: 2
16
+ revisor_id: 2
@@ -0,0 +1,8 @@
1
+ welcome:
2
+ id: 1
3
+ title: Welcome to the weblog
4
+ body: Such a lovely day
5
+ version: 24
6
+ author_id: 1
7
+ revisor_id: 1
8
+ created_on: "2008-01-01 00:00:00"
@@ -0,0 +1,6 @@
1
+ class Widget < ActiveRecord::Base
2
+ acts_as_versioned :sequence_name => 'widgets_seq', :association_options => {
3
+ :dependent => :nullify, :order => 'version desc'
4
+ }
5
+ non_versioned_columns << 'foo'
6
+ end
@@ -0,0 +1,46 @@
1
+ require File.join(File.dirname(__FILE__), '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
+ self.use_transactional_fixtures = false
11
+ def teardown
12
+ if ActiveRecord::Base.connection.respond_to?(:initialize_schema_information)
13
+ ActiveRecord::Base.connection.initialize_schema_information
14
+ ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
15
+ else
16
+ ActiveRecord::Base.connection.initialize_schema_migrations_table
17
+ ActiveRecord::Base.connection.assume_migrated_upto_version(0)
18
+ end
19
+
20
+ Thing.connection.drop_table "things" rescue nil
21
+ Thing.connection.drop_table "thing_versions" rescue nil
22
+ Thing.reset_column_information
23
+ end
24
+
25
+ def test_versioned_migration
26
+ assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
27
+ # take 'er up
28
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
29
+ t = Thing.create :title => 'blah blah', :price => 123.45, :type => 'Thing'
30
+ assert_equal 1, t.versions.size
31
+
32
+ # check that the price column has remembered its value correctly
33
+ assert_equal t.price, t.versions.first.price
34
+ assert_equal t.title, t.versions.first.title
35
+ assert_equal t[:type], t.versions.first[:type]
36
+
37
+ # make sure that the precision of the price column has been preserved
38
+ assert_equal 7, Thing::Version.columns.find{|c| c.name == "price"}.precision
39
+ assert_equal 2, Thing::Version.columns.find{|c| c.name == "price"}.scale
40
+
41
+ # now lets take 'er back down
42
+ ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
43
+ assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,82 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+ create_table :pages, :force => true do |t|
3
+ t.column :version, :integer
4
+ t.column :title, :string, :limit => 255
5
+ t.column :body, :text
6
+ t.column :created_on, :datetime
7
+ t.column :updated_on, :datetime
8
+ t.column :author_id, :integer
9
+ t.column :revisor_id, :integer
10
+ end
11
+
12
+ create_table :page_versions, :force => true do |t|
13
+ t.column :page_id, :integer
14
+ t.column :version, :integer
15
+ t.column :title, :string, :limit => 255
16
+ t.column :body, :text
17
+ t.column :created_on, :datetime
18
+ t.column :updated_on, :datetime
19
+ t.column :author_id, :integer
20
+ t.column :revisor_id, :integer
21
+ end
22
+
23
+ add_index :page_versions, [:page_id, :version], :unique => true
24
+
25
+ create_table :authors, :force => true do |t|
26
+ t.column :page_id, :integer
27
+ t.column :name, :string
28
+ end
29
+
30
+ create_table :locked_pages, :force => true do |t|
31
+ t.column :lock_version, :integer
32
+ t.column :title, :string, :limit => 255
33
+ t.column :body, :text
34
+ t.column :type, :string, :limit => 255
35
+ end
36
+
37
+ create_table :locked_pages_revisions, :force => true do |t|
38
+ t.column :page_id, :integer
39
+ t.column :lock_version, :integer
40
+ t.column :title, :string, :limit => 255
41
+ t.column :body, :text
42
+ t.column :version_type, :string, :limit => 255
43
+ t.column :updated_at, :datetime
44
+ end
45
+
46
+ add_index :locked_pages_revisions, [:page_id, :lock_version], :unique => true
47
+
48
+ create_table :widgets, :force => true do |t|
49
+ t.column :name, :string, :limit => 50
50
+ t.column :foo, :string
51
+ t.column :version, :integer
52
+ t.column :updated_at, :datetime
53
+ end
54
+
55
+ create_table :widget_versions, :force => true do |t|
56
+ t.column :widget_id, :integer
57
+ t.column :name, :string, :limit => 50
58
+ t.column :version, :integer
59
+ t.column :updated_at, :datetime
60
+ end
61
+
62
+ add_index :widget_versions, [:widget_id, :version], :unique => true
63
+
64
+ create_table :landmarks, :force => true do |t|
65
+ t.column :name, :string
66
+ t.column :latitude, :float
67
+ t.column :longitude, :float
68
+ t.column :doesnt_trigger_version,:string
69
+ t.column :version, :integer
70
+ end
71
+
72
+ create_table :landmark_versions, :force => true do |t|
73
+ t.column :landmark_id, :integer
74
+ t.column :name, :string
75
+ t.column :latitude, :float
76
+ t.column :longitude, :float
77
+ t.column :doesnt_trigger_version,:string
78
+ t.column :version, :integer
79
+ end
80
+
81
+ add_index :landmark_versions, [:landmark_id, :version], :unique => true
82
+ end
@@ -0,0 +1,352 @@
1
+ require File.join(File.dirname(__FILE__), 'abstract_unit')
2
+ require File.join(File.dirname(__FILE__), 'fixtures/page')
3
+ require File.join(File.dirname(__FILE__), 'fixtures/widget')
4
+
5
+ class VersionedTest < Test::Unit::TestCase
6
+ fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions
7
+ set_fixture_class :page_versions => Page::Version
8
+
9
+ def test_saves_versioned_copy
10
+ p = Page.create! :title => 'first title', :body => 'first body'
11
+ assert !p.new_record?
12
+ assert_equal 1, p.versions.size
13
+ assert_equal 1, p.version
14
+ assert_instance_of Page.versioned_class, p.versions.first
15
+ end
16
+
17
+ def test_version_has_unique_created_at
18
+ p = pages(:welcome)
19
+ p.title = 'update me'
20
+ p.save!
21
+ assert_not_equal p.created_on, p.versions.latest.created_on
22
+ end
23
+
24
+ def test_saves_without_revision
25
+ p = pages(:welcome)
26
+ old_versions = p.versions.count
27
+
28
+ p.save_without_revision
29
+
30
+ p.without_revision do
31
+ p.update_attributes :title => 'changed'
32
+ end
33
+
34
+ assert_equal old_versions, p.versions.count
35
+ end
36
+
37
+ def test_rollback_with_version_number
38
+ p = pages(:welcome)
39
+ assert_equal 24, p.version
40
+ assert_equal 'Welcome to the weblog', p.title
41
+
42
+ assert p.revert_to!(23), "Couldn't revert to 23"
43
+ assert_equal 23, p.version
44
+ assert_equal 'Welcome to the weblg', p.title
45
+ end
46
+
47
+ def test_versioned_class_name
48
+ assert_equal 'Version', Page.versioned_class_name
49
+ assert_equal 'LockedPageRevision', LockedPage.versioned_class_name
50
+ end
51
+
52
+ def test_versioned_class
53
+ assert_equal Page::Version, Page.versioned_class
54
+ assert_equal LockedPage::LockedPageRevision, LockedPage.versioned_class
55
+ end
56
+
57
+ def test_special_methods
58
+ assert_nothing_raised { pages(:welcome).feeling_good? }
59
+ assert_nothing_raised { pages(:welcome).versions.first.feeling_good? }
60
+ assert_nothing_raised { locked_pages(:welcome).hello_world }
61
+ assert_nothing_raised { locked_pages(:welcome).versions.first.hello_world }
62
+ end
63
+
64
+ def test_rollback_with_version_class
65
+ p = pages(:welcome)
66
+ assert_equal 24, p.version
67
+ assert_equal 'Welcome to the weblog', p.title
68
+
69
+ assert p.revert_to!(p.versions.find_by_version(23)), "Couldn't revert to 23"
70
+ assert_equal 23, p.version
71
+ assert_equal 'Welcome to the weblg', p.title
72
+ end
73
+
74
+ def test_rollback_fails_with_invalid_revision
75
+ p = locked_pages(:welcome)
76
+ assert !p.revert_to!(locked_pages(:thinking))
77
+ end
78
+
79
+ def test_saves_versioned_copy_with_options
80
+ p = LockedPage.create! :title => 'first title'
81
+ assert !p.new_record?
82
+ assert_equal 1, p.versions.size
83
+ assert_instance_of LockedPage.versioned_class, p.versions.first
84
+ end
85
+
86
+ def test_rollback_with_version_number_with_options
87
+ p = locked_pages(:welcome)
88
+ assert_equal 'Welcome to the weblog', p.title
89
+ assert_equal 'LockedPage', p.versions.first.version_type
90
+
91
+ assert p.revert_to!(p.versions.first.lock_version), "Couldn't revert to 23"
92
+ assert_equal 'Welcome to the weblg', p.title
93
+ assert_equal 'LockedPage', p.versions.first.version_type
94
+ end
95
+
96
+ def test_rollback_with_version_class_with_options
97
+ p = locked_pages(:welcome)
98
+ assert_equal 'Welcome to the weblog', p.title
99
+ assert_equal 'LockedPage', p.versions.first.version_type
100
+
101
+ assert p.revert_to!(p.versions.first), "Couldn't revert to 1"
102
+ assert_equal 'Welcome to the weblg', p.title
103
+ assert_equal 'LockedPage', p.versions.first.version_type
104
+ end
105
+
106
+ def test_saves_versioned_copy_with_sti
107
+ p = SpecialLockedPage.create! :title => 'first title'
108
+ assert !p.new_record?
109
+ assert_equal 1, p.versions.size
110
+ assert_instance_of LockedPage.versioned_class, p.versions.first
111
+ assert_equal 'SpecialLockedPage', p.versions.first.version_type
112
+ end
113
+
114
+ def test_rollback_with_version_number_with_sti
115
+ p = locked_pages(:thinking)
116
+ assert_equal 'So I was thinking', p.title
117
+
118
+ assert p.revert_to!(p.versions.first.lock_version), "Couldn't revert to 1"
119
+ assert_equal 'So I was thinking!!!', p.title
120
+ assert_equal 'SpecialLockedPage', p.versions.first.version_type
121
+ end
122
+
123
+ def test_lock_version_works_with_versioning
124
+ p = locked_pages(:thinking)
125
+ p2 = LockedPage.find(p.id)
126
+
127
+ p.title = 'fresh title'
128
+ p.save
129
+ assert_equal 2, p.versions.size # limit!
130
+
131
+ assert_raises(ActiveRecord::StaleObjectError) do
132
+ p2.title = 'stale title'
133
+ p2.save
134
+ end
135
+ end
136
+
137
+ def test_version_if_condition
138
+ p = Page.create! :title => "title"
139
+ assert_equal 1, p.version
140
+
141
+ Page.feeling_good = false
142
+ p.save
143
+ assert_equal 1, p.version
144
+ Page.feeling_good = true
145
+ end
146
+
147
+ def test_version_if_condition2
148
+ # set new if condition
149
+ Page.class_eval do
150
+ def new_feeling_good() title[0..0] == 'a'; end
151
+ alias_method :old_feeling_good, :feeling_good?
152
+ alias_method :feeling_good?, :new_feeling_good
153
+ end
154
+
155
+ p = Page.create! :title => "title"
156
+ assert_equal 1, p.version # version does not increment
157
+ assert_equal 1, p.versions.count
158
+
159
+ p.update_attributes(:title => 'new title')
160
+ assert_equal 1, p.version # version does not increment
161
+ assert_equal 1, p.versions.count
162
+
163
+ p.update_attributes(:title => 'a title')
164
+ assert_equal 2, p.version
165
+ assert_equal 2, p.versions.count
166
+
167
+ # reset original if condition
168
+ Page.class_eval { alias_method :feeling_good?, :old_feeling_good }
169
+ end
170
+
171
+ def test_version_if_condition_with_block
172
+ # set new if condition
173
+ old_condition = Page.version_condition
174
+ Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' }
175
+
176
+ p = Page.create! :title => "title"
177
+ assert_equal 1, p.version # version does not increment
178
+ assert_equal 1, p.versions.count
179
+
180
+ p.update_attributes(:title => 'a title')
181
+ assert_equal 1, p.version # version does not increment
182
+ assert_equal 1, p.versions.count
183
+
184
+ p.update_attributes(:title => 'b title')
185
+ assert_equal 2, p.version
186
+ assert_equal 2, p.versions.count
187
+
188
+ # reset original if condition
189
+ Page.version_condition = old_condition
190
+ end
191
+
192
+ def test_version_no_limit
193
+ p = Page.create! :title => "title", :body => 'first body'
194
+ p.save
195
+ p.save
196
+ 5.times do |i|
197
+ p.title = "title#{i}"
198
+ p.save
199
+ assert_equal "title#{i}", p.title
200
+ assert_equal (i+2), p.version
201
+ end
202
+ end
203
+
204
+ def test_version_max_limit
205
+ p = LockedPage.create! :title => "title"
206
+ p.update_attributes(:title => "title1")
207
+ p.update_attributes(:title => "title2")
208
+ 5.times do |i|
209
+ p.title = "title#{i}"
210
+ p.save
211
+ assert_equal "title#{i}", p.title
212
+ assert_equal (i+4), p.lock_version
213
+ assert p.versions(true).size <= 2, "locked version can only store 2 versions"
214
+ end
215
+ end
216
+
217
+ def test_track_altered_attributes_default_value
218
+ assert !Page.track_altered_attributes
219
+ assert LockedPage.track_altered_attributes
220
+ assert SpecialLockedPage.track_altered_attributes
221
+ end
222
+
223
+ def test_track_altered_attributes
224
+ p = LockedPage.create! :title => "title"
225
+ assert_equal 1, p.lock_version
226
+ assert_equal 1, p.versions(true).size
227
+
228
+ p.body = 'whoa'
229
+ assert !p.save_version?
230
+ p.save
231
+ assert_equal 2, p.lock_version # still increments version because of optimistic locking
232
+ assert_equal 1, p.versions(true).size
233
+
234
+ p.title = 'updated title'
235
+ assert p.save_version?
236
+ p.save
237
+ assert_equal 3, p.lock_version
238
+ assert_equal 1, p.versions(true).size # version 1 deleted
239
+
240
+ p.title = 'updated title!'
241
+ assert p.save_version?
242
+ p.save
243
+ assert_equal 4, p.lock_version
244
+ assert_equal 2, p.versions(true).size # version 1 deleted
245
+ end
246
+
247
+ def test_find_versions
248
+ assert_equal 1, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%weblog%']).size
249
+ end
250
+
251
+ def test_find_version
252
+ assert_equal page_versions(:welcome_1), pages(:welcome).versions.find_by_version(23)
253
+ end
254
+
255
+ def test_with_sequence
256
+ assert_equal 'widgets_seq', Widget.versioned_class.sequence_name
257
+ 3.times { Widget.create! :name => 'new widget' }
258
+ assert_equal 3, Widget.count
259
+ assert_equal 3, Widget.versioned_class.count
260
+ end
261
+
262
+ def test_has_many_through
263
+ assert_equal [authors(:caged), authors(:mly)], pages(:welcome).authors
264
+ end
265
+
266
+ def test_has_many_through_with_custom_association
267
+ assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors
268
+ end
269
+
270
+ def test_referential_integrity
271
+ pages(:welcome).destroy
272
+ assert_equal 0, Page.count
273
+ assert_equal 0, Page::Version.count
274
+ end
275
+
276
+ def test_association_options
277
+ association = Page.reflect_on_association(:versions)
278
+ options = association.options
279
+ assert_equal :delete_all, options[:dependent]
280
+
281
+ association = Widget.reflect_on_association(:versions)
282
+ options = association.options
283
+ assert_equal :nullify, options[:dependent]
284
+ assert_equal 'version desc', options[:order]
285
+ assert_equal 'widget_id', options[:foreign_key]
286
+
287
+ widget = Widget.create! :name => 'new widget'
288
+ assert_equal 1, Widget.count
289
+ assert_equal 1, Widget.versioned_class.count
290
+ widget.destroy
291
+ assert_equal 0, Widget.count
292
+ assert_equal 1, Widget.versioned_class.count
293
+ end
294
+
295
+ def test_versioned_records_should_belong_to_parent
296
+ page = pages(:welcome)
297
+ page_version = page.versions.last
298
+ assert_equal page, page_version.page
299
+ end
300
+
301
+ def test_unaltered_attributes
302
+ landmarks(:washington).attributes = landmarks(:washington).attributes.except("id")
303
+ assert !landmarks(:washington).changed?
304
+ end
305
+
306
+ def test_unchanged_string_attributes
307
+ landmarks(:washington).attributes = landmarks(:washington).attributes.except("id").inject({}) { |params, (key, value)| params.update(key => value.to_s) }
308
+ assert !landmarks(:washington).changed?
309
+ end
310
+
311
+ def test_should_find_earliest_version
312
+ assert_equal page_versions(:welcome_1), pages(:welcome).versions.earliest
313
+ end
314
+
315
+ def test_should_find_latest_version
316
+ assert_equal page_versions(:welcome_2), pages(:welcome).versions.latest
317
+ end
318
+
319
+ def test_should_find_previous_version
320
+ assert_equal page_versions(:welcome_1), page_versions(:welcome_2).previous
321
+ assert_equal page_versions(:welcome_1), pages(:welcome).versions.before(page_versions(:welcome_2))
322
+ end
323
+
324
+ def test_should_find_next_version
325
+ assert_equal page_versions(:welcome_2), page_versions(:welcome_1).next
326
+ assert_equal page_versions(:welcome_2), pages(:welcome).versions.after(page_versions(:welcome_1))
327
+ end
328
+
329
+ def test_should_find_version_count
330
+ assert_equal 2, pages(:welcome).versions.size
331
+ end
332
+
333
+ def test_if_changed_creates_version_if_a_listed_column_is_changed
334
+ landmarks(:washington).name = "Washington"
335
+ assert landmarks(:washington).changed?
336
+ assert landmarks(:washington).altered?
337
+ end
338
+
339
+ def test_if_changed_creates_version_if_all_listed_columns_are_changed
340
+ landmarks(:washington).name = "Washington"
341
+ landmarks(:washington).latitude = 1.0
342
+ landmarks(:washington).longitude = 1.0
343
+ assert landmarks(:washington).changed?
344
+ assert landmarks(:washington).altered?
345
+ end
346
+
347
+ def test_if_changed_does_not_create_new_version_if_unlisted_column_is_changed
348
+ landmarks(:washington).doesnt_trigger_version = "This should not trigger version"
349
+ assert landmarks(:washington).changed?
350
+ assert !landmarks(:washington).altered?
351
+ end
352
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: johnsbrn-acts_as_versioned
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.2
5
+ platform: ruby
6
+ authors:
7
+ - technoweenie
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-20 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: TODO
17
+ email: technoweenie@bidwell.textdrive.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - VERSION.yml
26
+ - lib/acts_as_versioned.rb
27
+ - test/abstract_unit.rb
28
+ - test/database.yml
29
+ - test/fixtures
30
+ - test/fixtures/authors.yml
31
+ - test/fixtures/landmark.rb
32
+ - test/fixtures/landmark_versions.yml
33
+ - test/fixtures/landmarks.yml
34
+ - test/fixtures/locked_pages.yml
35
+ - test/fixtures/locked_pages_revisions.yml
36
+ - test/fixtures/migrations
37
+ - test/fixtures/migrations/1_add_versioned_tables.rb
38
+ - test/fixtures/page.rb
39
+ - test/fixtures/page_versions.yml
40
+ - test/fixtures/pages.yml
41
+ - test/fixtures/widget.rb
42
+ - test/migration_test.rb
43
+ - test/schema.rb
44
+ - test/versioned_test.rb
45
+ has_rdoc: true
46
+ homepage: http://github.com/technoweenie/acts_as_versioned
47
+ post_install_message:
48
+ rdoc_options:
49
+ - --inline-source
50
+ - --charset=UTF-8
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.2.0
69
+ signing_key:
70
+ specification_version: 2
71
+ summary: TODO
72
+ test_files: []
73
+