vidibus-versioning 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+