set_vestal_versions 1.2.2

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.
Files changed (40) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +196 -0
  3. data/lib/generators/vestal_versions.rb +11 -0
  4. data/lib/generators/vestal_versions/migration/migration_generator.rb +17 -0
  5. data/lib/generators/vestal_versions/migration/templates/initializer.rb +9 -0
  6. data/lib/generators/vestal_versions/migration/templates/migration.rb +28 -0
  7. data/lib/vestal_versions.rb +126 -0
  8. data/lib/vestal_versions/changes.rb +122 -0
  9. data/lib/vestal_versions/conditions.rb +57 -0
  10. data/lib/vestal_versions/control.rb +200 -0
  11. data/lib/vestal_versions/creation.rb +93 -0
  12. data/lib/vestal_versions/deletion.rb +39 -0
  13. data/lib/vestal_versions/options.rb +41 -0
  14. data/lib/vestal_versions/reload.rb +17 -0
  15. data/lib/vestal_versions/reset.rb +24 -0
  16. data/lib/vestal_versions/reversion.rb +82 -0
  17. data/lib/vestal_versions/users.rb +55 -0
  18. data/lib/vestal_versions/version.rb +80 -0
  19. data/lib/vestal_versions/version_num.rb +3 -0
  20. data/lib/vestal_versions/version_tagging.rb +50 -0
  21. data/lib/vestal_versions/versioned.rb +27 -0
  22. data/lib/vestal_versions/versions.rb +74 -0
  23. data/spec/spec_helper.rb +20 -0
  24. data/spec/support/models.rb +19 -0
  25. data/spec/support/schema.rb +25 -0
  26. data/spec/vestal_versions/changes_spec.rb +134 -0
  27. data/spec/vestal_versions/conditions_spec.rb +103 -0
  28. data/spec/vestal_versions/control_spec.rb +120 -0
  29. data/spec/vestal_versions/creation_spec.rb +90 -0
  30. data/spec/vestal_versions/deletion_spec.rb +86 -0
  31. data/spec/vestal_versions/options_spec.rb +45 -0
  32. data/spec/vestal_versions/reload_spec.rb +18 -0
  33. data/spec/vestal_versions/reset_spec.rb +111 -0
  34. data/spec/vestal_versions/reversion_spec.rb +103 -0
  35. data/spec/vestal_versions/users_spec.rb +21 -0
  36. data/spec/vestal_versions/version_spec.rb +61 -0
  37. data/spec/vestal_versions/version_tagging_spec.rb +39 -0
  38. data/spec/vestal_versions/versioned_spec.rb +16 -0
  39. data/spec/vestal_versions/versions_spec.rb +176 -0
  40. metadata +165 -0
@@ -0,0 +1,24 @@
1
+ module VestalVersions
2
+ # Adds the ability to "reset" (or hard revert) a versioned ActiveRecord::Base instance.
3
+ module Reset
4
+ extend ActiveSupport::Concern
5
+
6
+ # Adds the instance methods required to reset an object to a previous version.
7
+ module InstanceMethods
8
+ # Similar to +revert_to!+, the +reset_to!+ method reverts an object to a previous version,
9
+ # only instead of creating a new record in the version history, +reset_to!+ deletes all of
10
+ # the version history that occurs after the version reverted to.
11
+ #
12
+ # The action taken on each version record after the point of reversion is determined by the
13
+ # <tt>:dependent</tt> option given to the +versioned+ method. See the +versioned+ method
14
+ # documentation for more details.
15
+ def reset_to!(value)
16
+ if saved = skip_version{ revert_to!(value) }
17
+ versions.send(:delete_records, versions.after(value))
18
+ reset_version
19
+ end
20
+ saved
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,82 @@
1
+ module VestalVersions
2
+ # Enables versioned ActiveRecord::Base instances to revert to a previously saved version.
3
+ module Reversion
4
+ extend ActiveSupport::Concern
5
+
6
+ # Provides the base instance methods required to revert a versioned instance.
7
+ module InstanceMethods
8
+ # Returns the current version number for the versioned object.
9
+ def version
10
+ @version ||= last_version
11
+ end
12
+
13
+ # Accepts a value corresponding to a specific version record, builds a history of changes
14
+ # between that version and the current version, and then iterates over that history updating
15
+ # the object's attributes until the it's reverted to its prior state.
16
+ #
17
+ # The single argument should adhere to one of the formats as documented in the +at+ method of
18
+ # VestalVersions::Versions.
19
+ #
20
+ # After the object is reverted to the target version, it is not saved. In order to save the
21
+ # object after the reversion, use the +revert_to!+ method.
22
+ #
23
+ # The version number of the object will reflect whatever version has been reverted to, and
24
+ # the return value of the +revert_to+ method is also the target version number.
25
+ def revert_to(value)
26
+ to_number = versions.number_at(value)
27
+
28
+ changes_between(version, to_number).each do |attribute, change|
29
+ write_attribute(attribute, change.last)
30
+ end
31
+
32
+ reset_version(to_number)
33
+ end
34
+
35
+ # Behaves similarly to the +revert_to+ method except that it automatically saves the record
36
+ # after the reversion. The return value is the success of the save.
37
+ def revert_to!(value)
38
+ revert_to(value)
39
+ reset_version if saved = save
40
+ saved
41
+ end
42
+
43
+ # Returns a boolean specifying whether the object has been reverted to a previous version or
44
+ # if the object represents the latest version in the version history.
45
+ def reverted?
46
+ version != last_version
47
+ end
48
+
49
+ private
50
+
51
+ # Mixes in the reverted_from value if it is currently within a revert
52
+ def version_attributes
53
+ attributes = super
54
+
55
+ if @reverted_from.nil?
56
+ attributes
57
+ else
58
+ attributes.merge(:reverted_from => @reverted_from)
59
+ end
60
+ end
61
+
62
+ # Returns the number of the last created version in the object's version history.
63
+ #
64
+ # If no associated versions exist, the object is considered at version 1.
65
+ def last_version
66
+ @last_version ||= versions.maximum(:number) || 1
67
+ end
68
+
69
+ # Clears the cached version number instance variables so that they can be recalculated.
70
+ # Useful after a new version is created.
71
+ def reset_version(version = nil)
72
+ if version.nil?
73
+ @last_version = nil
74
+ @reverted_from = nil
75
+ else
76
+ @reverted_from = version
77
+ end
78
+ @version = version
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,55 @@
1
+ module VestalVersions
2
+ # Provides a way for information to be associated with specific versions as to who was
3
+ # responsible for the associated update to the parent.
4
+ module Users
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attr_accessor :updated_by
9
+ Version.class_eval{ include VersionMethods }
10
+ end
11
+
12
+ # Methods added to versioned ActiveRecord::Base instances to enable versioning with additional
13
+ # user information.
14
+ module InstanceMethods
15
+
16
+ private
17
+ # Overrides the +version_attributes+ method to include user information passed into the
18
+ # parent object, by way of a +updated_by+ attr_accessor.
19
+ def version_attributes
20
+ super.merge(:user => updated_by)
21
+ end
22
+ end
23
+
24
+ # Instance methods added to VestalVersions::Version to accomodate incoming user information.
25
+ module VersionMethods
26
+ extend ActiveSupport::Concern
27
+
28
+ included do
29
+ belongs_to :user, :polymorphic => true
30
+
31
+ alias_method_chain :user, :name
32
+ alias_method_chain :user=, :name
33
+ end
34
+
35
+ # Overrides the +user+ method created by the polymorphic +belongs_to+ user association. If
36
+ # the association is absent, defaults to the +user_name+ string column. This allows
37
+ # VestalVersions::Version#user to either return an ActiveRecord::Base object or a string,
38
+ # depending on what is sent to the +user_with_name=+ method.
39
+ def user_with_name
40
+ user_without_name || user_name
41
+ end
42
+
43
+ # Overrides the +user=+ method created by the polymorphic +belongs_to+ user association.
44
+ # Based on the class of the object given, either the +user+ association columns or the
45
+ # +user_name+ string column is populated.
46
+ def user_with_name=(value)
47
+ case value
48
+ when ActiveRecord::Base then self.user_without_name = value
49
+ else self.user_name = value
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,80 @@
1
+ require 'active_record'
2
+ require 'active_support/configurable'
3
+
4
+ module VestalVersions
5
+ # The ActiveRecord model representing versions.
6
+ class Version < ActiveRecord::Base
7
+ include Comparable
8
+ include ActiveSupport::Configurable
9
+
10
+ # Associate polymorphically with the parent record.
11
+ belongs_to :versioned, :polymorphic => true
12
+
13
+ # ActiveRecord::Base#changes is an existing method, so before serializing the +changes+ column,
14
+ # the existing +changes+ method is undefined. The overridden +changes+ method pertained to
15
+ # dirty attributes, but will not affect the partial updates functionality as that's based on
16
+ # an underlying +changed_attributes+ method, not +changes+ itself.
17
+ undef_method :changes
18
+ def changes
19
+ self[:modifications]
20
+ end
21
+ serialize :modifications, Hash
22
+
23
+ # In conjunction with the included Comparable module, allows comparison of version records
24
+ # based on their corresponding version numbers, creation timestamps and IDs.
25
+ def <=>(other)
26
+ [number, created_at, id].map(&:to_i) <=> [other.number, other.created_at, other.id].map(&:to_i)
27
+ end
28
+
29
+ # Returns whether the version has a version number of 1. Useful when deciding whether to ignore
30
+ # the version during reversion, as initial versions have no serialized changes attached. Helps
31
+ # maintain backwards compatibility.
32
+ def initial?
33
+ number == 1
34
+ end
35
+
36
+ # Returns the original version number that this version was.
37
+ def original_number
38
+ if reverted_from.nil?
39
+ number
40
+ else
41
+ version = versioned.versions.at(reverted_from)
42
+ version.nil? ? 1 : version.original_number
43
+ end
44
+ end
45
+
46
+ def restore!
47
+ model = restore
48
+
49
+ if model
50
+ model.save!
51
+ destroy
52
+ end
53
+
54
+ model
55
+ end
56
+
57
+ def restore
58
+ if tag == 'deleted'
59
+ attrs = modifications
60
+
61
+ class_name = attrs['type'].blank? ? versioned_type : attrs['type']
62
+ klass = class_name.constantize
63
+ model = klass.new
64
+
65
+ attrs.each do |k, v|
66
+ begin
67
+ model.send "#{k}=", v
68
+ rescue NoMethodError
69
+ logger.warn "Attribute #{k} does not exist on #{class_name} (Version id: #{id})." rescue nil
70
+ end
71
+ end
72
+
73
+ model
74
+ else
75
+ latest_version = self.class.find(:first, :conditions => {:versioned_id => versioned_id, :versioned_type => versioned_type, :tag => 'deleted'})
76
+ latest_version.nil? ? nil : latest_version.restore
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ module VestalVersions
2
+ VERSION = '1.2.2'
3
+ end
@@ -0,0 +1,50 @@
1
+ module VestalVersions
2
+ # Allows specific versions to be tagged with a custom string. Useful for assigning a more
3
+ # meaningful value to a version for the purpose of reversion.
4
+ module VersionTagging
5
+ extend ActiveSupport::Concern
6
+
7
+ # Adds an instance method which allows version tagging through the parent object.
8
+ module InstanceMethods
9
+ # Accepts a single string argument which is attached to the version record associated with
10
+ # the current version number of the parent object.
11
+ #
12
+ # Returns the given tag if successful, nil if not. Tags must be unique within the scope of
13
+ # the parent object. Tag creation will fail if non-unique.
14
+ #
15
+ # Version records corresponding to version number 1 are not typically created, but one will
16
+ # be built to house the given tag if the parent object's current version number is 1.
17
+ def tag_version(tag)
18
+ v = versions.at(version) || versions.build(:number => 1)
19
+ v.tag!(tag)
20
+ end
21
+ end
22
+
23
+ # Instance methods included into VestalVersions::Version to enable version tagging.
24
+ module VersionMethods
25
+ extend ActiveSupport::Concern
26
+
27
+ included do
28
+ validates_uniqueness_of :tag, :scope => [:versioned_id, :versioned_type], :if => :validate_tags?
29
+ end
30
+
31
+ # Attaches the given string to the version tag column. If the uniqueness validation fails,
32
+ # nil is returned. Otherwise, the given string is returned.
33
+ def tag!(tag)
34
+ write_attribute(:tag, tag)
35
+ save ? tag : nil
36
+ end
37
+
38
+ # Simply returns a boolean signifying whether the version instance has a tag value attached.
39
+ def tagged?
40
+ !tag.nil?
41
+ end
42
+
43
+ def validate_tags?
44
+ tagged? && tag != 'deleted'
45
+ end
46
+ end
47
+
48
+ Version.class_eval{ include VersionMethods }
49
+ end
50
+ end
@@ -0,0 +1,27 @@
1
+ module VestalVersions
2
+ # Simply adds a flag to determine whether a model class if versioned.
3
+ module Versioned
4
+ extend ActiveSupport::Concern
5
+
6
+ # Overrides the +versioned+ method to first define the +versioned?+ class method before
7
+ # deferring to the original +versioned+.
8
+ module ClassMethods
9
+ def versioned(*args)
10
+ super(*args)
11
+
12
+ class << self
13
+ def versioned?
14
+ true
15
+ end
16
+ end
17
+ end
18
+
19
+ # For all ActiveRecord::Base models that do not call the +versioned+ method, the +versioned?+
20
+ # method will return false.
21
+ def versioned?
22
+ false
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,74 @@
1
+ module VestalVersions
2
+ # An extension module for the +has_many+ association with versions.
3
+ module Versions
4
+ # Returns all versions between (and including) the two given arguments. See documentation for
5
+ # the +at+ extension method for what arguments are valid. If either of the given arguments is
6
+ # invalid, an empty array is returned.
7
+ #
8
+ # The +between+ method preserves returns an array of version records, preserving the order
9
+ # given by the arguments. If the +from+ value represents a version before that of the +to+
10
+ # value, the array will be ordered from earliest to latest. The reverse is also true.
11
+ def between(from, to)
12
+ from_number, to_number = number_at(from), number_at(to)
13
+ return [] if from_number.nil? || to_number.nil?
14
+
15
+ condition = (from_number == to_number) ? to_number : Range.new(*[from_number, to_number].sort)
16
+ all(
17
+ :conditions => {:number => condition},
18
+ :order => "#{aliased_table_name}.#{connection.quote_column_name('number')} #{(from_number > to_number) ? 'DESC' : 'ASC'}"
19
+ )
20
+ end
21
+
22
+ # Returns all version records created before the version associated with the given value.
23
+ def before(value)
24
+ return [] if (number = number_at(value)).nil?
25
+ all(:conditions => "#{aliased_table_name}.#{connection.quote_column_name('number')} < #{number}")
26
+ end
27
+
28
+ # Returns all version records created after the version associated with the given value.
29
+ #
30
+ # This is useful for dissociating records during use of the +reset_to!+ method.
31
+ def after(value)
32
+ return [] if (number = number_at(value)).nil?
33
+ all(:conditions => "#{aliased_table_name}.#{connection.quote_column_name('number')} > #{number}")
34
+ end
35
+
36
+ # Returns a single version associated with the given value. The following formats are valid:
37
+ # * A Date or Time object: When given, +to_time+ is called on the value and the last version
38
+ # record in the history created before (or at) that time is returned.
39
+ # * A Numeric object: Typically a positive integer, these values correspond to version numbers
40
+ # and the associated version record is found by a version number equal to the given value
41
+ # rounded down to the nearest integer.
42
+ # * A String: A string value represents a version tag and the associated version is searched
43
+ # for by a matching tag value. *Note:* Be careful with string representations of numbers.
44
+ # * A Symbol: Symbols represent association class methods on the +has_many+ versions
45
+ # association. While all of the built-in association methods require arguments, additional
46
+ # extension modules can be defined using the <tt>:extend</tt> option on the +versioned+
47
+ # method. See the +versioned+ documentation for more information.
48
+ # * A Version object: If a version object is passed to the +at+ method, it is simply returned
49
+ # untouched.
50
+ def at(value)
51
+ case value
52
+ when Date, Time then last(:conditions => ["#{aliased_table_name}.created_at <= ?", value.to_time])
53
+ when Numeric then find_by_number(value.floor)
54
+ when String then find_by_tag(value)
55
+ when Symbol then respond_to?(value) ? send(value) : nil
56
+ when Version then value
57
+ end
58
+ end
59
+
60
+ # Returns the version number associated with the given value. In many cases, this involves
61
+ # simply passing the value to the +at+ method and then returning the subsequent version number.
62
+ # Hoever, for Numeric values, the version number can be returned directly and for Date/Time
63
+ # values, a default value of 1 is given to ensure that times prior to the first version
64
+ # still return a valid version number (useful for reversion).
65
+ def number_at(value)
66
+ case value
67
+ when Date, Time then (v = at(value)) ? v.number : 1
68
+ when Numeric then value.floor
69
+ when String, Symbol then (v = at(value)) ? v.number : nil
70
+ when Version then value.number
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,20 @@
1
+ require 'bundler'
2
+ Bundler.require
3
+ require 'rspec/core'
4
+
5
+ RSpec.configure do |c|
6
+ c.before(:suite) do
7
+ CreateSchema.suppress_messages{ CreateSchema.migrate(:up) }
8
+ end
9
+
10
+ c.after(:suite) do
11
+ FileUtils.rm_rf(File.expand_path('../test.db', __FILE__))
12
+ end
13
+
14
+ c.after(:each) do
15
+ VestalVersions::Version.config.clear
16
+ User.prepare_versioned_options({})
17
+ end
18
+ end
19
+
20
+ Dir[File.dirname(__FILE__) + '/support/*.rb'].each { |f| require f }
@@ -0,0 +1,19 @@
1
+ class User < ActiveRecord::Base
2
+ versioned
3
+
4
+ def name
5
+ [first_name, last_name].compact.join(' ')
6
+ end
7
+
8
+ def name= names
9
+ self[:first_name], self[:last_name] = names.split(' ', 2)
10
+ end
11
+ end
12
+
13
+ class DeletedUser < ActiveRecord::Base
14
+ set_table_name 'users'
15
+ versioned :dependent => :tracking
16
+ end
17
+
18
+ class MyCustomVersion < VestalVersions::Version
19
+ end