vidibus-versioning 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Andre Pankratz
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.md ADDED
@@ -0,0 +1,111 @@
1
+ # Vidibus::Versioning [![](http://travis-ci.org/vidibus/vidibus-versioning.png)](http://travis-ci.org/vidibus/vidibus-versioning) [![](http://stillmaintained.com/vidibus/vidibus-versioning.png)](http://stillmaintained.com/vidibus/vidibus-versioning)
2
+
3
+ Vidibus::Versioning provides advanced versioning for Mongoid models including support for future and editable versions.
4
+
5
+ This gem is part of [Vidibus](http://vidibus.org), an open source toolset for building distributed (video) applications.
6
+
7
+
8
+ ## Installation
9
+
10
+ Add `gem "vidibus-versioning"` to your Gemfile. Then call `bundle install` on your console.
11
+
12
+
13
+ ## Usage
14
+
15
+ To apply versioning to your model is easy. An example:
16
+
17
+ ```ruby
18
+ class Article
19
+ include Mongoid::Document
20
+ include Vidibus::Uuid::Mongoid
21
+ include Vidibus::Versioning::Mongoid # this is mandatory
22
+
23
+ field :title, :type => String
24
+ field :text, :type => String
25
+
26
+ versioned :title, :text, :editing_time => 300 # this is optional
27
+ end
28
+ ```
29
+
30
+ ### Versioned attributes
31
+
32
+ Including the versioning engine by adding `include Vidibus::Versioning::Mongoid` will set all fields of your model as
33
+ versioned ones, except those contained in `Article.unversioned_attributes`, which are `_id`, `_type`, `uuid`,
34
+ `updated_at`, `created_at`, and `version_number`.
35
+
36
+ An optional `versioned` call lets you specify the versioned attributes precisely by providing a list. For example, to
37
+ set the title as only attribute to be versioned, call `versioned :title`.
38
+
39
+
40
+ ### Combined versioning
41
+
42
+ `versioned` also takes options to tweak versioning behaviour. By calling `versioned :editing_time => 300` you set a
43
+ timespan for the version to accept changes so all changes within 300 seconds will be treated as one version.
44
+ That behaviour is especially useful if your model's UI allows changing attributes separately, like in-place editing.
45
+
46
+
47
+ ### Migrating
48
+
49
+ The basic methods for migrating a versioned object - an article in this case - are:
50
+
51
+ ```ruby
52
+ article.migrate!(32) # migrates to version 32
53
+ article.undo! # restores previous version
54
+ article.redo! # restores next version
55
+ ```
56
+
57
+
58
+ ### Version editing
59
+
60
+ There is also a method `version` that loads an exisiting version of the record or instantiates a new one:
61
+
62
+ ```ruby
63
+ article.version(3) # returns version 3 of the article
64
+ article.version(:previous) # returns the previous version of the article
65
+ article.version(:next) # returns the next version of the article
66
+ article.version(:new) # returns a new version of the article
67
+ ```
68
+
69
+ To apply a version on the current article itself, call `version` with a bang!:
70
+
71
+ ```ruby
72
+ article.version!(3) # applies version 3 to the article and returns nil
73
+ ```
74
+
75
+ It is even possible to apply versioned attributes directly by adding it to the `version` call:
76
+
77
+ ```ruby
78
+ article.version(3, :title => "Wicked!") # returns version 3 with a new title applied
79
+ ```
80
+
81
+ You may treat the article object with an applied version like the article itself. All changes will
82
+ be applied to the loaded version instead of the current instance. An example:
83
+
84
+ ```ruby
85
+ article.version!(3) # applies version 3
86
+ article.title = "Brand new" # sets a new title
87
+ article.save # saves version 3 of the article
88
+ article.reload # loads the current version of the article
89
+ ```
90
+
91
+
92
+ ### Version objects
93
+
94
+ All versions of your models are stored in a separate model: `Vidibus::Versioning::Version`. To access the
95
+ version object of an article's version, call `article.version_object`:
96
+
97
+ ```ruby
98
+ article.version(3).version_object # => #<Vidibus::Versioning::Version ... >
99
+ article.version_object # => nil
100
+ ```
101
+
102
+
103
+ ## TODO
104
+
105
+ * Handle embedded documents
106
+ * Handle related documents
107
+
108
+
109
+ ## Copyright
110
+
111
+ Copyright (c) 2011 Andre Pankratz. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require "bundler"
2
+ require "rdoc/task"
3
+ require "rspec"
4
+ require "rspec/core/rake_task"
5
+
6
+ Bundler::GemHelper.install_tasks
7
+
8
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
9
+ require "vidibus/versioning/_version"
10
+
11
+ RSpec::Core::RakeTask.new(:rcov) do |t|
12
+ t.pattern = "spec/**/*_spec.rb"
13
+ t.rcov = true
14
+ t.rcov_opts = ["--exclude", "^spec,/gems/"]
15
+ end
16
+
17
+ Rake::RDocTask.new do |rdoc|
18
+ rdoc.rdoc_dir = "rdoc"
19
+ rdoc.title = "vidibus-sysinfo #{Vidibus::Versioning::VERSION}"
20
+ rdoc.rdoc_files.include("README*")
21
+ rdoc.rdoc_files.include("lib/**/*.rb")
22
+ rdoc.options << "--charset=utf-8"
23
+ end
24
+
25
+ task :default => :rcov
@@ -0,0 +1,5 @@
1
+ module Vidibus
2
+ module Versioning
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Vidibus
2
+ module Versioning
3
+ class Error < StandardError; end
4
+ class MigrationError < Error; end
5
+ end
6
+ end
@@ -0,0 +1,281 @@
1
+ module Vidibus
2
+ module Versioning
3
+ module Mongoid
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include ::Mongoid::Timestamps
8
+ include Vidibus::Uuid::Mongoid
9
+
10
+ has_many :versions, :as => :versioned, :class_name => "Vidibus::Versioning::Version", :dependent => :destroy
11
+
12
+ field :version_number, :type => Integer, :default => 1
13
+
14
+ after_initialize :original_attributes
15
+ before_update :reset_version_cache
16
+
17
+ mattr_accessor :versioned_attributes, :unversioned_attributes, :versioning_options
18
+ self.versioned_attributes = []
19
+ self.unversioned_attributes = %w[_id _type uuid updated_at created_at version_number]
20
+ self.versioning_options = {}
21
+
22
+ # Returns the attributes that should be versioned.
23
+ # If no versioned attributes have been defined on class level,
24
+ # all attributes will be returned except the unversioned ones.
25
+ def versioned_attributes
26
+ if self.class.versioned_attributes.any?
27
+ attributes.only(self.class.versioned_attributes)
28
+ else
29
+ attributes.except(self.class.unversioned_attributes)
30
+ end
31
+ end
32
+ end
33
+
34
+ module ClassMethods
35
+
36
+ # Defines versioned attributes and options
37
+ #
38
+ # Usage:
39
+ # versioned :some, :fields, :editing_time => 300
40
+ #
41
+ def versioned(*args)
42
+ options = args.extract_options!
43
+
44
+ self.versioned_attributes = args.map {|a| a.to_s} if args.any?
45
+ self.versioning_options = options if options.any?
46
+ end
47
+ end
48
+
49
+ # Returns a copy of this object with versioned attributes applied.
50
+ #
51
+ # Valid arguments are:
52
+ # :new returns a new version of self
53
+ # :next returns the next version of self, may be new as well
54
+ # :previous returns the previous version of self
55
+ # 48 returns version 48 of self
56
+ #
57
+ def version(*args)
58
+ self.class.find(_id).tap do |copy|
59
+ copy.apply_version!(*args)
60
+ end
61
+ end
62
+
63
+ # Applies versioned attributes on this object. Returns nil.
64
+ # For valid arguments, see #version.
65
+ #
66
+ def version!(*args)
67
+ self.apply_version!(*args)
68
+ end
69
+
70
+ # Applies attributes of wanted version on self.
71
+ # Stores current attributes in a new version.
72
+ def migrate!(number = nil)
73
+ raise(MigrationError, "no version given") unless number or version_cache.wanted_version_number
74
+ version!(number) if number and number != version_cache.wanted_version_number
75
+ raise(MigrationError, "cannot migrate to current version") if version_cache.self_version
76
+
77
+ set_original_version_obj
78
+
79
+ self.attributes = version_attributes
80
+ self.version_number = version_cache.wanted_version_number
81
+ save!
82
+ end
83
+
84
+ # Calls #version!(:previous) and migrate!
85
+ def undo!
86
+ version!(:previous)
87
+ migrate!
88
+ end
89
+
90
+ # Calls #version!(:next) and migrate!
91
+ def redo!
92
+ version!(:next)
93
+ migrate!
94
+ end
95
+
96
+ # Saves the record and handles version persistence.
97
+ def save(*args)
98
+ return false if invalid?
99
+ saved = persist_version
100
+ (saved == nil) ? super(*args) : saved
101
+ end
102
+
103
+ # Raises a validation error if saving fails.
104
+ def save!(*args)
105
+ raise(::Mongoid::Errors::Validations, self) unless save(*args)
106
+ end
107
+
108
+ def delete
109
+ super if remove_version(:delete) == nil
110
+ end
111
+
112
+ def destroy
113
+ super if remove_version(:destroy) == nil
114
+ end
115
+
116
+ # Reloads this record and applies version attributes.
117
+ def reload_version(*args)
118
+ reloaded_self = self.reload
119
+ reloaded_self.version(*version_cache.version_args) if version_cache.version_args
120
+ reloaded_self
121
+ end
122
+
123
+ # Returns the currently set version object.
124
+ def version_object
125
+ version_cache.version_obj
126
+ end
127
+
128
+ # Returns true if version requested is a new one.
129
+ def new_version?
130
+ version_obj and version_obj.new_record?
131
+ end
132
+
133
+ protected
134
+
135
+ # Applies version on self. Returns nil
136
+ def apply_version!(*args)
137
+ raise ArgumentError if args.empty?
138
+ set_version_args(*args)
139
+ version_cache.self_version = (version_number == version_cache.wanted_version_number)
140
+ return if version_cache.self_version
141
+
142
+ self.attributes = version_attributes
143
+ self.version_number = version_cache.wanted_version_number
144
+ self.updated_at = version_obj.created_at if version_obj.created_at
145
+ nil
146
+ end
147
+
148
+ # Returns the originial attributes of the record.
149
+ # This method has to be called after_initialize.
150
+ def original_attributes
151
+ @original_attributes ||= versioned_attributes
152
+ end
153
+
154
+ # Returns true if versioned attributes were changed.
155
+ def versioned_attributes_changed?
156
+ versioned_attributes != original_attributes
157
+ end
158
+
159
+ # Returns original attributes with attributes of version object and wanted attributes merged in.
160
+ def version_attributes
161
+ # TODO: Setting the following line will cause DelayedJob to loop endlessly. The same should happen if an embedded document is defined as versioned_attribute!
162
+ # original_attributes.merge(version_obj.versioned_attributes).merge(version_cache.wanted_attributes.stringify_keys!)
163
+ version_obj.versioned_attributes.merge(version_cache.wanted_attributes.stringify_keys!)
164
+ end
165
+
166
+ # Sets instance variables used for versioning.
167
+ # Helper method for #version
168
+ def set_version_args(*args)
169
+ version_cache.version_args = args
170
+ version_cache.wanted_attributes = args.extract_options!
171
+ wanted_version_number = args.first
172
+
173
+ version_cache.wanted_version_number = case wanted_version_number
174
+ when :new then new_version_number
175
+ when :next then version_number + 1
176
+ when :previous then version_number - 1
177
+ else
178
+ wanted_version_number.to_i
179
+ end
180
+
181
+ version_cache.wanted_version_number = 1 if version_cache.wanted_version_number < 1
182
+ end
183
+
184
+ # Finds or builds a version object containing the record's current attributes.
185
+ def set_original_version_obj
186
+ criteria = {:number => version_number_was}
187
+ version_cache.original_version_obj = versions.where(criteria).first || versions.build(criteria)
188
+ version_cache.original_version_obj.tap do |obj|
189
+ obj.versioned_attributes = original_attributes
190
+ obj.created_at = updated_at_was if obj.new_record?
191
+ end
192
+ end
193
+
194
+ # Returns the version object:
195
+ #
196
+ # * If a version is wanted, that version will be selected or instantiated and returned.
197
+ # * If an editing time has been defined which has not yet passed nil will be returned.
198
+ # * Otherwise a new version will be instantiated.
199
+ #
200
+ def version_obj
201
+ version_cache.version_obj ||= begin
202
+ if version_cache.wanted_version_number
203
+ obj = versions.where(:number => version_cache.wanted_version_number).first
204
+ unless obj or version_cache.self_version
205
+ # versions.to_a # TODO: prefetch versions before building a new one?
206
+ obj = versions.build(
207
+ :number => version_cache.wanted_version_number,
208
+ :versioned_attributes => versioned_attributes,
209
+ :created_at => updated_at_was)
210
+ end
211
+ obj
212
+ else
213
+ editing_time = self.class.versioning_options[:editing_time]
214
+ if !editing_time or updated_at <= (Time.now-editing_time.to_i) or updated_at > Time.now # allow future versions
215
+ versions.build(:created_at => updated_at_was)
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ # Returns the next available version number.
222
+ def new_version_number
223
+ version_cache.new_version_number ||= begin
224
+ latest_version = versions.desc(:number).limit(1).first
225
+ ver = latest_version.number if latest_version
226
+ [ver.to_i, version_number].max + 1
227
+ end
228
+ end
229
+
230
+ # Caching object for gathering versioning data.
231
+ # By storing this data inside a separate object, we can
232
+ # transfer it easily to a version copy of self.
233
+ def version_cache
234
+ @version_cache ||= Struct.new(
235
+ :wanted_version_number,
236
+ :wanted_attributes,
237
+ :new_version_number,
238
+ :version_obj,
239
+ :version_args,
240
+ :original_version_obj,
241
+ :self_version
242
+ ).new
243
+ end
244
+
245
+ # Stores changes on version object.
246
+ #
247
+ # If #migrate! was called and original_version_obj is present, the object will be saved.
248
+ # If #version was called and @wanted_version is present, changes will be applied to that version.
249
+ # Otherwise a new version object will be stored with original attributes.
250
+ #
251
+ def persist_version
252
+ return if new_record?
253
+ return unless versioned_attributes_changed?
254
+ if version_cache.original_version_obj
255
+ version_cache.original_version_obj.save!
256
+ elsif version_cache.wanted_version_number
257
+ if version_obj
258
+ saved = version_obj.update_attributes(:versioned_attributes => versioned_attributes, :created_at => updated_at)
259
+ end
260
+ return saved unless version_cache.self_version
261
+ elsif version_obj
262
+ version_obj.update_attributes!(:versioned_attributes => original_attributes)
263
+ self.version_number = version_obj.number + 1
264
+ end
265
+ nil
266
+ end
267
+
268
+ # Removes version object with given method.
269
+ def remove_version(method)
270
+ return unless version_cache.wanted_version_number
271
+ version_obj.send(method)
272
+ end
273
+
274
+ # Resets instance variables used for versioning.
275
+ def reset_version_cache
276
+ @original_attributes = versioned_attributes
277
+ @version_cache = nil
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,45 @@
1
+ module Vidibus
2
+ module Versioning
3
+ class Version
4
+ include ::Mongoid::Document
5
+ include ::Mongoid::Timestamps
6
+ include Vidibus::Uuid::Mongoid
7
+
8
+ belongs_to :versioned, :polymorphic => true
9
+
10
+ field :versioned_uuid, :type => String
11
+ field :versioned_attributes, :type => Hash, :default => {}
12
+ field :number, :type => Integer, :default => nil
13
+
14
+ index :versioned_uuid
15
+ index :number
16
+
17
+ validates :versioned_uuid, :versioned_attributes, :presence => true
18
+
19
+ before_validation :set_versioned_uuid
20
+ before_create :set_number
21
+
22
+ scope :timeline, desc(:created_at)
23
+
24
+ def past?
25
+ @is_past ||= created_at && created_at < Time.now
26
+ end
27
+
28
+ def future?
29
+ @is_future ||= !created_at || created_at >= Time.now
30
+ end
31
+
32
+ protected
33
+
34
+ def set_number
35
+ return if number
36
+ previous = Version.desc(:number).limit(1).first
37
+ self.number = previous ? previous.number + 1 : 1
38
+ end
39
+
40
+ def set_versioned_uuid
41
+ self.versioned_uuid = versioned.uuid
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ require "vidibus/versioning/errors"
2
+ require "vidibus/versioning/version"
3
+ require "vidibus/versioning/mongoid"
@@ -0,0 +1,5 @@
1
+ require "mongoid"
2
+ require "vidibus-core_extensions"
3
+ require "vidibus-uuid"
4
+
5
+ require "vidibus/versioning"
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vidibus-versioning
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Andre Pankratz
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-07-14 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: mongoid
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ version: "2"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: vidibus-core_extensions
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :runtime
43
+ version_requirements: *id002
44
+ - !ruby/object:Gem::Dependency
45
+ name: vidibus-uuid
46
+ prerelease: false
47
+ requirement: &id003 !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ segments:
52
+ - 0
53
+ version: "0"
54
+ type: :runtime
55
+ version_requirements: *id003
56
+ - !ruby/object:Gem::Dependency
57
+ name: bundler
58
+ prerelease: false
59
+ requirement: &id004 !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ segments:
64
+ - 1
65
+ - 0
66
+ - 0
67
+ version: 1.0.0
68
+ type: :development
69
+ version_requirements: *id004
70
+ - !ruby/object:Gem::Dependency
71
+ name: rake
72
+ prerelease: false
73
+ requirement: &id005 !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ type: :development
81
+ version_requirements: *id005
82
+ - !ruby/object:Gem::Dependency
83
+ name: rdoc
84
+ prerelease: false
85
+ requirement: &id006 !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ type: :development
93
+ version_requirements: *id006
94
+ - !ruby/object:Gem::Dependency
95
+ name: rcov
96
+ prerelease: false
97
+ requirement: &id007 !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ type: :development
105
+ version_requirements: *id007
106
+ - !ruby/object:Gem::Dependency
107
+ name: rspec
108
+ prerelease: false
109
+ requirement: &id008 !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ~>
112
+ - !ruby/object:Gem::Version
113
+ segments:
114
+ - 2
115
+ version: "2"
116
+ type: :development
117
+ version_requirements: *id008
118
+ - !ruby/object:Gem::Dependency
119
+ name: rr
120
+ prerelease: false
121
+ requirement: &id009 !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ segments:
126
+ - 0
127
+ version: "0"
128
+ type: :development
129
+ version_requirements: *id009
130
+ description: Versioning designed for advanced usage, like scheduling versions.
131
+ email: andre@vidibus.com
132
+ executables: []
133
+
134
+ extensions: []
135
+
136
+ extra_rdoc_files: []
137
+
138
+ files:
139
+ - lib/vidibus/versioning/_version.rb
140
+ - lib/vidibus/versioning/errors.rb
141
+ - lib/vidibus/versioning/mongoid.rb
142
+ - lib/vidibus/versioning/version.rb
143
+ - lib/vidibus/versioning.rb
144
+ - lib/vidibus-versioning.rb
145
+ - LICENSE
146
+ - README.md
147
+ - Rakefile
148
+ has_rdoc: true
149
+ homepage: https://github.com/vidibus/vidibus-versioning
150
+ licenses: []
151
+
152
+ post_install_message:
153
+ rdoc_options: []
154
+
155
+ require_paths:
156
+ - lib
157
+ required_ruby_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ segments:
162
+ - 0
163
+ version: "0"
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ segments:
169
+ - 1
170
+ - 3
171
+ - 6
172
+ version: 1.3.6
173
+ requirements: []
174
+
175
+ rubyforge_project: vidibus-versioning
176
+ rubygems_version: 1.3.6
177
+ signing_key:
178
+ specification_version: 3
179
+ summary: Advanced versioning for Mongoid models
180
+ test_files: []
181
+