jacqui-versioned 0.1.1

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/.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