jacqui-versioned 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jacqui Maher
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.rdoc ADDED
@@ -0,0 +1,51 @@
1
+ = versioned
2
+
3
+ Simple versioning for MongoMapper
4
+
5
+ = installation
6
+
7
+ The versioned gem is hosted on gemcutter.org:
8
+
9
+ * gem install versioned
10
+
11
+ = usage
12
+
13
+ class Doc
14
+ include MongoMapper::Document
15
+ include Versioned
16
+ key :title, String
17
+ end
18
+
19
+ @doc = Doc.create(:title=>"v1")
20
+ @doc.title = "v2"
21
+ @doc.save
22
+
23
+ @doc.revert
24
+
25
+ puts @doc.title
26
+ => v1
27
+
28
+ @doc.title = "v3"
29
+ @doc.save
30
+ @doc.version
31
+ => 3
32
+
33
+ @doc.retrieve_version 2
34
+ puts @doc.title
35
+ => "v2"
36
+
37
+ @doc = Doc.find(@doc.id)
38
+ @doc.title
39
+ => "v3"
40
+ @doc.version
41
+ => 3
42
+
43
+ == Note on Patches/Pull Requests
44
+
45
+ * Fork the project.
46
+ * Make your feature addition or bug fix.
47
+ * Add tests for it. This is important so I don't break it in a
48
+ future version unintentionally.
49
+ * Commit, do not mess with rakefile, version, or history.
50
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
51
+ * Send me a pull request. Bonus points for topic branches.
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |g|
9
+ g.name = 'jacqui-versioned'
10
+ g.summary = %(Versioning for MongoMapper)
11
+ g.description = %(Versioning for MongoMapper)
12
+ g.email = 'jacqui@brighter.net'
13
+ g.homepage = 'http://github.com/jacqui/versioned'
14
+ g.authors = %w(twoism toastyapps jacqui mrkurt)
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts 'Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com'
19
+ end
20
+
21
+ Rake::TestTask.new do |t|
22
+ t.libs = %w(test)
23
+ t.pattern = 'test/**/*_test.rb'
24
+ end
25
+
26
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require File.dirname(__FILE__) + 'lib/version.rb'
2
+ require File.dirname(__FILE__) + 'lib/versioned.rb'
data/lib/version.rb ADDED
@@ -0,0 +1,36 @@
1
+ class Version
2
+ include MongoMapper::Document
3
+ include Comparable
4
+
5
+ key :number, Integer
6
+ key :versioned_type, String
7
+ key :versioned_id, ObjectId
8
+ key :changes, Hash
9
+ timestamps!
10
+
11
+ belongs_to :versioned, :polymorphic => true
12
+ def changes
13
+ read_attribute(:changes)
14
+ end
15
+ alias_attribute :version, :number
16
+
17
+ def <=>(other)
18
+ number <=> other.number
19
+ end
20
+
21
+ def previous
22
+ find_related(:first, :number => {:$lt => number}, :order => 'number.desc')
23
+ end
24
+
25
+ def next
26
+ find_related(:first, :number => {:$gt => number}, :order => 'number.asc')
27
+ end
28
+
29
+ protected
30
+
31
+ def find_related(*args)
32
+ options = args.extract_options!
33
+ params = options.merge(:versioned_id => versioned_id, :versioned_type => versioned_type)
34
+ self.class.find(args.first, params)
35
+ end
36
+ end
data/lib/versioned.rb ADDED
@@ -0,0 +1,225 @@
1
+ require 'version'
2
+
3
+ module Versioned
4
+ class StaleDocumentError < MongoMapper::MongoMapperError; end
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.class_eval do
8
+ versioned
9
+ end
10
+ end
11
+
12
+ module LockingInstanceMethods
13
+ private
14
+ #new? isn't working
15
+ def is_new_document?
16
+ (read_attribute(self.version_lock_key).blank? && changes[self.version_lock_key.to_s].blank?) ||
17
+ (changes[self.version_lock_key.to_s] && changes[self.version_lock_key.to_s].first.blank?)
18
+ end
19
+ def prep_lock_version
20
+ old = read_attribute(self.version_lock_key)
21
+ if !is_new_document? || old.blank?
22
+ v = (Time.now.to_f * 1000).ceil.to_s
23
+ write_attribute self.version_lock_key, v
24
+ end
25
+
26
+ old
27
+ end
28
+
29
+ def save_to_collection(options = {})
30
+ current_version = prep_lock_version
31
+ if is_new_document?
32
+ collection.insert(to_mongo, :safe => true)
33
+ else
34
+ selector = { :_id => read_attribute(:_id), self.version_lock_key => current_version }
35
+ #can't upsert, safe must be true for this to work
36
+ result = collection.update(selector, to_mongo, :upsert => false, :safe => true)
37
+
38
+ if result.is_a?(Array) && result[0][0]['updatedExisting'] == false
39
+ write_attribute self.version_lock_key, current_version
40
+ raise StaleDocumentError.new
41
+ elsif !result.is_a?(Array)
42
+ #wtf?
43
+ write_attribute self.version_lock_key, current_version
44
+ raise "Unexpected result from mongo"
45
+ end
46
+
47
+ selector[:_id]
48
+ end
49
+ end
50
+ end
51
+ module ClassMethods
52
+ def locking!(options = {})
53
+ include(LockingInstanceMethods)
54
+ class_inheritable_accessor :version_lock_key
55
+ self.version_lock_key = options[:key] || :lock_version
56
+ key self.version_lock_key, Integer
57
+
58
+ if self.respond_to?(:version_use_key)
59
+ self.version_use_key = self.version_lock_key
60
+ (self.version_except_columns ||= []) << self.version_lock_key.to_s #don't version the lock key
61
+ end
62
+ end
63
+
64
+ def versioned(options = {})
65
+ class_inheritable_accessor :version_only_columns
66
+ self.version_only_columns = Array(options[:only]).map(&:to_s).uniq if options[:only]
67
+ class_inheritable_accessor :version_except_columns
68
+ self.version_except_columns = Array(options[:except]).map(&:to_s).uniq if options[:except]
69
+
70
+ class_inheritable_accessor :version_use_key
71
+ self.version_use_key = options[:use_key]
72
+
73
+ many :versions, :as => :versioned, :order => 'number ASC', :dependent => :delete_all do
74
+ def between(from, to)
75
+ from_number, to_number = number_at(from), number_at(to)
76
+ return [] if from_number.nil? || to_number.nil?
77
+ condition = (from_number == to_number) ? to_number : Range.new(*[from_number, to_number].sort)
78
+ if condition.is_a?(Range)
79
+ conditions = {'$gte' => condition.first, '$lte' => condition.last}
80
+ else
81
+ conditions = condition
82
+ end
83
+ find(:all,
84
+ :number => conditions,
85
+ :order => "number #{(from_number > to_number) ? 'DESC' : 'ASC'}"
86
+ )
87
+ end
88
+
89
+ def at(value)
90
+ case value
91
+ when Version then value
92
+ when Numeric then find_by_number(value.floor)
93
+ when Symbol then respond_to?(value) ? send(value) : nil
94
+ when Date, Time then last(:created_at => {'$lte' => value.to_time})
95
+ end
96
+ end
97
+
98
+ def number_at(value)
99
+ case value
100
+ when Version then value.number
101
+ when Numeric then value.floor
102
+ when Symbol, Date, Time then at(value).try(:number)
103
+ end
104
+ end
105
+ end
106
+
107
+ after_create :create_initial_version
108
+ after_update :create_initial_version, :if => :needs_initial_version?
109
+ after_update :create_version, :if => :needs_version?
110
+
111
+ include InstanceMethods
112
+ alias_method_chain :reload, :versions
113
+ end
114
+
115
+ end
116
+
117
+ module InstanceMethods
118
+ private
119
+ def versioned_columns
120
+ case
121
+ when version_only_columns then self.class.keys.keys & version_only_columns
122
+ when version_except_columns then self.class.keys.keys - version_except_columns
123
+ else self.class.keys.keys
124
+ end - %w(created_at created_on updated_at updated_on)
125
+ end
126
+
127
+ def needs_initial_version?
128
+ versions.empty?
129
+ end
130
+
131
+ def needs_version?
132
+ !(versioned_columns & changed).empty?
133
+ end
134
+
135
+ def reset_version(new_version = nil)
136
+ @last_version = nil if new_version.nil?
137
+ @version = new_version
138
+ end
139
+
140
+ def next_version_number(initial = false)
141
+ v = read_attribute self.version_use_key unless self.version_use_key.nil?
142
+ v = 1 if v.nil? && initial
143
+ v = last_version + 1 if v.nil? && !initial
144
+ v
145
+ end
146
+ def create_initial_version
147
+ versions.create(:changes => nil, :number => next_version_number(true))
148
+ end
149
+
150
+ def create_version
151
+ versions << Version.create(:changes => changes.slice(*versioned_columns), :number => next_version_number)
152
+ reset_version
153
+ end
154
+
155
+ public
156
+ def version
157
+ @version ||= last_version
158
+ end
159
+
160
+ def last_version
161
+ @last_version ||= versions.inject(1){|max, version| version.number > max ? version.number : max}
162
+ end
163
+
164
+ def reverted?
165
+ version != last_version
166
+ end
167
+
168
+ def reload_with_versions(*args)
169
+ reset_version
170
+ reload_without_versions(*args)
171
+ end
172
+
173
+ def changes_between(from, to)
174
+ from_number, to_number = versions.number_at(from), versions.number_at(to)
175
+ return {} if from_number == to_number
176
+ chain = versions.between(from_number, to_number)
177
+ return {} if chain.empty?
178
+
179
+ backward = chain.first > chain.last
180
+ backward ? chain.pop : chain.shift
181
+
182
+ chain.inject({}) do |changes, version|
183
+ version.changes.each do |attribute, change|
184
+ change.reverse! if backward
185
+ new_change = [changes.fetch(attribute, change).first, change.last]
186
+ changes.update(attribute => new_change)
187
+ end
188
+ changes
189
+ end
190
+ end
191
+
192
+ def revert
193
+ revert_to self.versions.at(self.version).previous
194
+ end
195
+
196
+ def retrieve_version n
197
+ versions.find_by_number(n).changes.each do |n,v|
198
+ self.send("#{n.to_sym}=",v.first)
199
+ end
200
+ end
201
+
202
+ def revert_to(value)
203
+ to_number = versions.number_at(value)
204
+ changes = changes_between(version, to_number)
205
+ return version if changes.empty?
206
+
207
+ changes.each do |attribute, change|
208
+ write_attribute(attribute, change.last)
209
+ end
210
+
211
+ reset_version(to_number)
212
+ end
213
+
214
+ def revert_to!(value)
215
+ revert_to(value)
216
+ reset_version if saved = save
217
+ saved
218
+ end
219
+
220
+ def latest_changes
221
+ return {} if version.nil?
222
+ versions.at(version).changes
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,58 @@
1
+ require 'test_helper'
2
+
3
+ class BetweenTest < Test::Unit::TestCase
4
+ context 'The number of versions between' do
5
+ setup do
6
+ @user = User.create(:name => 'Steve Richert')
7
+ @version = @user.version
8
+ @valid = [@version, 0, 1_000_000, :first, :last, 1.day.since(@user.created_at), @user.versions.first]
9
+ @invalid = [nil, :bogus, 'bogus', Date.parse('0001-12-25')]
10
+ end
11
+
12
+ context 'the current version and the current version' do
13
+ should 'equal one' do
14
+ assert_equal 1, @user.versions.between(@version, @version).size
15
+ end
16
+ end
17
+
18
+ context 'the current version and a valid value' do
19
+ should 'not equal zero' do
20
+ @valid.each do |valid|
21
+ assert_not_equal 0, @user.versions.between(@version, valid).size
22
+ assert_not_equal 0, @user.versions.between(valid, @version).size
23
+ end
24
+ end
25
+ end
26
+
27
+ context 'the current version and an invalid value' do
28
+ should 'equal zero' do
29
+ @invalid.each do |invalid|
30
+ assert_equal 0, @user.versions.between(@version, invalid).size
31
+ assert_equal 0, @user.versions.between(invalid, @version).size
32
+ end
33
+ end
34
+ end
35
+
36
+ context 'two invalid values' do
37
+ should 'equal zero' do
38
+ @invalid.each do |first|
39
+ @invalid.each do |second|
40
+ assert_equal 0, @user.versions.between(first, second).size
41
+ assert_equal 0, @user.versions.between(second, first).size
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'a valid value and an invalid value' do
48
+ should 'equal zero' do
49
+ @valid.each do |valid|
50
+ @invalid.each do |invalid|
51
+ assert_equal 0, @user.versions.between(valid, invalid).size
52
+ assert_equal 0, @user.versions.between(invalid, valid).size
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,28 @@
1
+ require 'test_helper'
2
+
3
+ class ChangesTest < Test::Unit::TestCase
4
+ context "A version's changes" do
5
+ setup do
6
+ @user = User.create(:name => 'Steve Richert')
7
+ end
8
+
9
+ should "initially be blank" do
10
+ assert @user.versions.first.changes.blank?
11
+ end
12
+
13
+ should 'contain all changed attributes' do
14
+ @user.name = 'Steve Jobs'
15
+ changes = @user.changes
16
+ @user.save
17
+ assert_equal changes, @user.versions.last.changes.slice(*changes.keys)
18
+ end
19
+
20
+ should 'contain no more than the changed attributes and timestamps' do
21
+ timestamps = %w(created_at created_on updated_at updated_on)
22
+ @user.name = 'Steve Jobs'
23
+ changes = @user.changes
24
+ @user.save
25
+ assert_equal changes, @user.versions.last.changes.except(*timestamps)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ class ComparableTest < Test::Unit::TestCase
4
+ context 'A comparable version' do
5
+ setup do
6
+ @version_1 = Version.new(:number => 1)
7
+ @version_2 = Version.new(:number => 2)
8
+ end
9
+
10
+ should 'equal itself' do
11
+ assert @version_1 == @version_1
12
+ assert @version_2 == @version_2
13
+ end
14
+
15
+ context 'with version number 1' do
16
+ should 'not equal a version with version number 2' do
17
+ assert @version_1 != @version_2
18
+ end
19
+
20
+ should 'be less than a version with version number 2' do
21
+ assert @version_1 < @version_2
22
+ end
23
+ end
24
+
25
+ context 'with version number 2' do
26
+ should 'not equal a version with version number 1' do
27
+ assert @version_2 != @version_1
28
+ end
29
+
30
+ should 'be greater than a version with version number 1' do
31
+ assert @version_2 > @version_1
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,79 @@
1
+ require 'test_helper'
2
+
3
+ class CreationTest < Test::Unit::TestCase
4
+ context 'The number of versions' do
5
+ setup do
6
+ @name = 'Steve Richert'
7
+ @user = User.create(:name => @name)
8
+ @count = @user.versions.count
9
+ end
10
+
11
+ should 'initially equal one' do
12
+ assert_equal 1, @count
13
+ end
14
+
15
+ should 'not increase when no changes are made in an update' do
16
+ @user.name = @name
17
+ assert_equal @count, @user.versions.count
18
+ end
19
+
20
+ should 'not increase when no changes are made before a save' do
21
+ @user.save
22
+ assert_equal @count, @user.versions.count
23
+ end
24
+
25
+ should 'not increase when reverting to the current version' do
26
+ @user.revert_to!(@user.version)
27
+ assert_equal @count, @user.versions.count
28
+ end
29
+
30
+ context 'after an update' do
31
+ setup do
32
+ @initial_count = @count
33
+ @name = 'Steve Jobs'
34
+ @user.name = @name
35
+ @user.save
36
+ @count = @user.versions.count
37
+ end
38
+
39
+ should 'increase by one' do
40
+ assert_equal @initial_count + 1, @count
41
+ end
42
+
43
+ should 'increase by one when reverted' do
44
+ @user.revert_to!(:first)
45
+ assert_equal @count + 1, @user.versions.count
46
+ end
47
+
48
+ should 'not increase until a revert is saved' do
49
+ @user.revert_to(:first)
50
+ assert_equal @count, @user.versions.count
51
+ @user.save
52
+ assert_not_equal @count, @user.versions.count
53
+ end
54
+ end
55
+
56
+ should "retrieve a specific version without reverting it" do
57
+ @user.name = "Hoge"
58
+ @user.save
59
+ version_count = @user.versions.size
60
+ @user.retrieve_version 2
61
+ assert_equal version_count, @user.versions.size
62
+ end
63
+
64
+ context 'after multiple updates' do
65
+ setup do
66
+ @initial_count = @count
67
+ @new_name = 'Steve Jobs'
68
+ @user.name = @new_name
69
+ @user.name = @name
70
+ @count = @user.versions.count
71
+ end
72
+
73
+ should 'not increase when reverting to an identical version' do
74
+ @user.revert_to!(:first)
75
+ assert_equal @count, @user.versions.count
76
+ end
77
+ end
78
+ end
79
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'versioned'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,42 @@
1
+ require 'test_helper'
2
+
3
+ class LatestChangesTest < Test::Unit::TestCase
4
+ context "A created model's last changes" do
5
+ setup do
6
+ @user = User.create(:name => 'Steve Richert')
7
+ end
8
+
9
+ should 'be blank' do
10
+ assert @user.latest_changes.blank?
11
+ end
12
+ end
13
+
14
+ context "An updated model's last changes" do
15
+ setup do
16
+ @user = User.create(:name => 'Steve Richert')
17
+ @previous_attributes = @user.attributes
18
+ @user.name = 'Steve Jobs'
19
+ @current_attributes = @user.attributes
20
+ end
21
+
22
+ should 'values of two-element arrays with unique values' do
23
+ @user.latest_changes.values.each do |value|
24
+ assert_kind_of Array, value
25
+ assert_equal 2, value.size
26
+ assert_equal value, value.uniq
27
+ end
28
+ end
29
+
30
+ should 'begin with the previous attribute values' do
31
+ changes = @user.latest_changes.inject({}){|h,(k,v)| h.update(k => v.first) }
32
+ previous = @previous_attributes.slice(*@user.latest_changes.keys)
33
+ assert_equal previous, changes
34
+ end
35
+
36
+ should 'end with the current attribute values' do
37
+ changes = @user.latest_changes.inject({}){|h,(k,v)| h.update(k => v.last) }
38
+ current = @current_attributes.slice(*@user.latest_changes.keys)
39
+ assert_equal current, changes
40
+ end
41
+ end
42
+ end
data/test/lock_test.rb ADDED
@@ -0,0 +1,91 @@
1
+ require 'test_helper'
2
+
3
+ class LockTest < Test::Unit::TestCase
4
+ context 'An unversioned model with locks' do
5
+ setup do
6
+ @user = UnversionedLockableUser.create(:name => 'Kurt')
7
+ end
8
+
9
+ should 'have a lock_version field' do
10
+ assert_not_nil @user.lock_version
11
+ end
12
+
13
+ should 'save just fine when no conflicts' do
14
+ @user.name = 'Bob'
15
+ assert @user.save
16
+ end
17
+
18
+ should 'save just fine when given the proper lock version' do
19
+ u = UnversionedLockableUser.find(@user.id)
20
+ @user.update_attributes(:name => 'Bill')
21
+
22
+ assert u.update_attributes(:name => 'Jose', :lock_version => @user.lock_version)
23
+ end
24
+
25
+ should 'not save when given the wrong version' do
26
+ assert_raise Versioned::StaleDocumentError do
27
+ @user.update_attributes(:name => 'Bob', :lock_version => 1111)
28
+ end
29
+
30
+ # lock_version should match what we passed in
31
+ assert_equal 1111, @user.lock_version
32
+ end
33
+ end
34
+ context 'A versioned model with locks' do
35
+ setup do
36
+ @user = LockableUser.create(:name => 'Kurt', :required_field => 'woo!')
37
+ end
38
+
39
+ should 'have a lock_version field' do
40
+ assert_not_nil @user.lock_version
41
+ assert_equal @user.version, @user.lock_version
42
+ end
43
+
44
+ should 'save just fine when no conflicts' do
45
+ @user.name = 'Bob'
46
+ assert @user.save
47
+ end
48
+
49
+ should 'save just fine when given the proper lock version' do
50
+ u = LockableUser.find(@user.id)
51
+ @user.update_attributes(:name => 'Bill')
52
+
53
+ assert u.update_attributes(:name => 'Jose', :lock_version => @user.lock_version)
54
+ end
55
+
56
+ should 'not save when given the wrong version' do
57
+ assert_raise Versioned::StaleDocumentError do
58
+ @user.update_attributes(:name => 'Bob', :lock_version => 1111)
59
+ end
60
+
61
+ # lock_version should match what we passed in
62
+ assert_equal 1111, @user.lock_version
63
+ end
64
+
65
+ should 'be revertable with the lock version' do
66
+ v = @user.lock_version
67
+ name = @user.name
68
+ @user.update_attributes(:name => "Schlub")
69
+
70
+ assert_not_equal v, @user.lock_version
71
+
72
+ @user.revert_to(v)
73
+
74
+ assert_equal name, @user.name
75
+ assert_not_equal v, @user.lock_version #shouldn't have reverted version
76
+ end
77
+
78
+ should 'accept a specified version on create' do
79
+ u = LockableUser.create(:name => 'Burt', :required_field => 'woo!', :lock_version => 1111)
80
+ assert_equal 1111, u.lock_version
81
+ end
82
+
83
+ should "have same lock_version when validation fails" do
84
+ @user.required_field = nil
85
+ v = @user.lock_version
86
+ result = @user.save
87
+ assert !result
88
+ assert_equal v, @user.lock_version
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,79 @@
1
+ require 'test_helper'
2
+
3
+ class RevertTest < Test::Unit::TestCase
4
+ context 'A model reversion' do
5
+ setup do
6
+ @user, @attributes, @times = User.new, {}, {}
7
+ names = ['Steve Richert', 'Stephen Richert', 'Stephen Jobs', 'Steve Jobs']
8
+ time = names.size.hours.ago
9
+ names.each do |name|
10
+ @user.update_attributes({:name => name})
11
+ @attributes[@user.version] = @user.attributes
12
+ time += 1.hour
13
+ @user.versions.last.update_attributes({:created_at => time})
14
+ @times[@user.version] = time
15
+ end
16
+ @first_version, @last_version = @attributes.keys.min, @attributes.keys.max
17
+ end
18
+
19
+ should 'do nothing for a non-existent version' do
20
+ attributes = @user.attributes
21
+ @user.revert_to!(nil)
22
+ assert_equal attributes, @user.attributes
23
+ end
24
+
25
+ should 'return the new version number' do
26
+ new_version = @user.revert_to(@first_version)
27
+ assert_equal @first_version, new_version
28
+ end
29
+
30
+ should 'change the version number when saved' do
31
+ current_version = @user.version
32
+ @user.revert_to!(@first_version)
33
+ assert_not_equal current_version, @user.version
34
+ end
35
+
36
+ should 'be able to target the first version' do
37
+ @user.revert_to(:first)
38
+ assert_equal @first_version, @user.version
39
+ end
40
+
41
+ should 'be able to target the last version' do
42
+ @user.revert_to(:last)
43
+ assert_equal @last_version, @user.version
44
+ end
45
+
46
+ should 'do nothing for a non-existent method name' do
47
+ current_version = @user.version
48
+ @user.revert_to(:bogus)
49
+ assert_equal current_version, @user.version
50
+ end
51
+
52
+ should 'be able to target a version number' do
53
+ @user.revert_to(1)
54
+ assert 1, @user.version
55
+ end
56
+
57
+ should 'be able to target a date and time' do
58
+ @times.each do |version, time|
59
+ @user.revert_to(time + 1.second)
60
+ assert_equal version, @user.version
61
+ end
62
+ end
63
+
64
+ should 'be able to target a version object' do
65
+ @user.versions.each do |version|
66
+ @user.revert_to(version)
67
+ assert_equal version.number, @user.version
68
+ end
69
+ end
70
+
71
+ should "correctly roll back the model's attributes" do
72
+ timestamps = %w(created_at created_on updated_at updated_on)
73
+ @attributes.each do |version, attributes|
74
+ @user.revert_to!(version)
75
+ assert_equal attributes.except(*timestamps), @user.attributes.except(*timestamps)
76
+ end
77
+ end
78
+ end
79
+ end
data/test/schema.rb ADDED
@@ -0,0 +1,55 @@
1
+ MongoMapper.connection = Mongo::Connection.new('127.0.0.1')
2
+ MongoMapper.database = "testing_versioned"
3
+
4
+ class User
5
+ include MongoMapper::Document
6
+ include Versioned
7
+ key :first_name, String
8
+ key :last_name, String
9
+ timestamps!
10
+
11
+ def name
12
+ [first_name, last_name].compact.join(' ')
13
+ end
14
+
15
+ def name=(names)
16
+ self[:first_name], self[:last_name] = names.split(' ', 2)
17
+ end
18
+ end
19
+
20
+ class Loser
21
+ include MongoMapper::Document
22
+ extend Versioned::ClassMethods
23
+ versioned :use_key => :revision
24
+ key :revision, Integer
25
+ key :name, String
26
+ timestamps!
27
+
28
+ before_save :set_revision
29
+
30
+ def set_revision
31
+ write_attribute :revision, (Time.now.to_f * 1000).ceil
32
+ end
33
+ end
34
+
35
+ class LockableUser
36
+ include MongoMapper::Document
37
+ include Versioned
38
+ locking!
39
+
40
+ key :name, String
41
+ key :required_field, String
42
+ validates_presence_of :required_field
43
+ end
44
+
45
+ class UnversionedLockableUser
46
+ include MongoMapper::Document
47
+ extend Versioned::ClassMethods
48
+ locking!
49
+
50
+ key :name, String
51
+ end
52
+
53
+ User.destroy_all
54
+ Loser.destroy_all
55
+ Version.destroy_all
@@ -0,0 +1,50 @@
1
+ require 'test_helper'
2
+
3
+ class SpecifiedVersionKey < Test::Unit::TestCase
4
+ context 'A model with a specified version key' do
5
+ setup do
6
+ @name = "Blah"
7
+ @loser = Loser.create(:name => @name)
8
+ @version = @loser.version
9
+ end
10
+ should 'be used for the initial version' do
11
+ assert_equal @loser.revision, @loser.version
12
+ assert_equal @loser.revision, @loser.versions.first.number
13
+ end
14
+
15
+ context 'after an update' do
16
+ setup do
17
+ @initial_version = @loser.version
18
+ @initial_count = @loser.versions.count
19
+ @initial_name = @loser.name
20
+ @name = 'Blip'
21
+ @loser.name = @name
22
+ @loser.save
23
+ @version = @loser.version
24
+ @count = @loser.versions.count
25
+ end
26
+
27
+ should 'have a different version number' do
28
+ assert_not_equal @initial_version, @loser.version
29
+ end
30
+
31
+ should 'still be using the specified key' do
32
+ assert_equal @loser.revision, @loser.version
33
+ assert_equal @loser.revision, @loser.versions.last.number
34
+ end
35
+
36
+ should 'version count should have increased by one' do
37
+ assert_equal @initial_count + 1, @count
38
+ end
39
+
40
+ should 'revert properly' do
41
+ @loser.revert
42
+ @loser.save!
43
+ assert_equal @initial_name, @loser.name
44
+ assert_equal @count + 1, @loser.versions.count
45
+ assert_not_equal @version, @loser.version
46
+ assert_not_equal @initial_version, @loser.version
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,10 @@
1
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
2
+ $: << File.dirname(__FILE__)
3
+
4
+ require 'rubygems'
5
+ require 'test/unit'
6
+ require 'shoulda'
7
+ require 'mongo_mapper'
8
+ require 'versioned'
9
+ require 'schema'
10
+ begin; require 'redgreen'; rescue LoadError; end
@@ -0,0 +1,80 @@
1
+ require "test_helper"
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/versioned')
4
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/version')
5
+
6
+ class Doc
7
+ include MongoMapper::Document
8
+ include Versioned
9
+ key :title, String
10
+ end
11
+
12
+ MongoMapper.connection.drop_database DB_NAME
13
+ MongoMapper.database = DB_NAME
14
+
15
+ class VersionTest < Test::Unit::TestCase
16
+
17
+ context "A Content Instance" do
18
+
19
+ setup do
20
+ @doc = Doc.create(:title=>"Foo")
21
+ end
22
+
23
+ should "respond to versions" do
24
+ assert @doc.respond_to?(:versions)
25
+ end
26
+
27
+ should "have before_save callback" do
28
+ assert Doc.before_update.collect(&:method).include?(:save_version)
29
+ end
30
+
31
+ context "after update" do
32
+ setup do
33
+ @doc.title = "Version 2"
34
+ @doc.save
35
+ end
36
+
37
+ should "create a version after update" do
38
+ assert_equal(1, @doc.versions.count)
39
+ end
40
+
41
+ should "have the correct version number" do
42
+ assert_equal(1, @doc.versions.first.version_number)
43
+ end
44
+
45
+ should "update the model's version key" do
46
+ assert_equal(1, @doc.version)
47
+ end
48
+
49
+ should "have :title in changes" do
50
+ assert_contains(@doc.versions.first.changed_attrs.keys, "title")
51
+ end
52
+
53
+ should "revert to last version" do
54
+ @doc.revert
55
+ assert_equal(@doc.title, "Foo")
56
+ end
57
+
58
+ should "revert by version #" do
59
+ @doc.title = "Version 3"
60
+ @doc.save
61
+ @doc.revert_to_version 2
62
+ assert_equal("Version 2", @doc.title)
63
+ end
64
+
65
+ should "retrieve a specific version without reverting it" do
66
+ @doc.title = "Version 3"
67
+ @doc.save
68
+ version_count = @doc.versions.size
69
+ @doc.retrieve_version 2
70
+ assert_equal version_count, @doc.versions.size
71
+ end
72
+ should "cleanup versions on destroy" do
73
+ @doc.destroy
74
+ assert_equal(0, Version.count)
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+ end
data/versioned.gemspec ADDED
@@ -0,0 +1,60 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{versioned}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["twoism", "toastyapps", "jacqui", "mrkurt"]
12
+ s.date = %q{2009-12-17}
13
+ s.description = %q{Versioning for MongoMapper}
14
+ s.email = %q{signalstatic@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "README.markdown"
17
+ ]
18
+ s.files = [
19
+ "README.markdown",
20
+ "Rakefile",
21
+ "VERSION",
22
+ "lib/version.rb",
23
+ "lib/versioned.rb",
24
+ "test/between_test.rb",
25
+ "test/changes_test.rb",
26
+ "test/comparable_test.rb",
27
+ "test/creation_test.rb",
28
+ "test/latest_changes_test.rb",
29
+ "test/revert_test.rb",
30
+ "test/schema.rb",
31
+ "test/test_helper.rb",
32
+ "versioned.gemspec"
33
+ ]
34
+ s.homepage = %q{http://github.com/twoism/versioned}
35
+ s.rdoc_options = ["--charset=UTF-8"]
36
+ s.require_paths = ["lib"]
37
+ s.rubygems_version = %q{1.3.5}
38
+ s.summary = %q{Versioning for MongoMapper}
39
+ s.test_files = [
40
+ "test/between_test.rb",
41
+ "test/changes_test.rb",
42
+ "test/comparable_test.rb",
43
+ "test/creation_test.rb",
44
+ "test/latest_changes_test.rb",
45
+ "test/revert_test.rb",
46
+ "test/schema.rb",
47
+ "test/test_helper.rb"
48
+ ]
49
+
50
+ if s.respond_to? :specification_version then
51
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
52
+ s.specification_version = 3
53
+
54
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
55
+ else
56
+ end
57
+ else
58
+ end
59
+ end
60
+
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jacqui-versioned
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - twoism
8
+ - toastyapps
9
+ - jacqui
10
+ - mrkurt
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+
15
+ date: 2010-01-18 00:00:00 -05:00
16
+ default_executable:
17
+ dependencies: []
18
+
19
+ description: Versioning for MongoMapper
20
+ email: jacqui@brighter.net
21
+ executables: []
22
+
23
+ extensions: []
24
+
25
+ extra_rdoc_files:
26
+ - LICENSE
27
+ - README.rdoc
28
+ files:
29
+ - .document
30
+ - .gitignore
31
+ - LICENSE
32
+ - README.rdoc
33
+ - Rakefile
34
+ - VERSION
35
+ - init.rb
36
+ - lib/version.rb
37
+ - lib/versioned.rb
38
+ - pkg/versioned-0.1.0.gem
39
+ - test/between_test.rb
40
+ - test/changes_test.rb
41
+ - test/comparable_test.rb
42
+ - test/creation_test.rb
43
+ - test/helper.rb
44
+ - test/latest_changes_test.rb
45
+ - test/lock_test.rb
46
+ - test/revert_test.rb
47
+ - test/schema.rb
48
+ - test/specified_version_key_test.rb
49
+ - test/test_helper.rb
50
+ - test/test_versioned.rb
51
+ - versioned.gemspec
52
+ has_rdoc: true
53
+ homepage: http://github.com/jacqui/versioned
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --charset=UTF-8
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ requirements: []
74
+
75
+ rubyforge_project:
76
+ rubygems_version: 1.3.5
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: Versioning for MongoMapper
80
+ test_files:
81
+ - test/between_test.rb
82
+ - test/changes_test.rb
83
+ - test/comparable_test.rb
84
+ - test/creation_test.rb
85
+ - test/helper.rb
86
+ - test/latest_changes_test.rb
87
+ - test/lock_test.rb
88
+ - test/revert_test.rb
89
+ - test/schema.rb
90
+ - test/specified_version_key_test.rb
91
+ - test/test_helper.rb
92
+ - test/test_versioned.rb