geothird_vestal_versions 1.2.3

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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +196 -0
  4. data/lib/generators/vestal_versions.rb +11 -0
  5. data/lib/generators/vestal_versions/migration/migration_generator.rb +17 -0
  6. data/lib/generators/vestal_versions/migration/templates/initializer.rb +9 -0
  7. data/lib/generators/vestal_versions/migration/templates/migration.rb +28 -0
  8. data/lib/vestal_versions.rb +126 -0
  9. data/lib/vestal_versions/changes.rb +120 -0
  10. data/lib/vestal_versions/conditions.rb +55 -0
  11. data/lib/vestal_versions/control.rb +197 -0
  12. data/lib/vestal_versions/creation.rb +91 -0
  13. data/lib/vestal_versions/deletion.rb +37 -0
  14. data/lib/vestal_versions/options.rb +41 -0
  15. data/lib/vestal_versions/reload.rb +15 -0
  16. data/lib/vestal_versions/reset.rb +22 -0
  17. data/lib/vestal_versions/reversion.rb +80 -0
  18. data/lib/vestal_versions/users.rb +53 -0
  19. data/lib/vestal_versions/version.rb +81 -0
  20. data/lib/vestal_versions/version_num.rb +3 -0
  21. data/lib/vestal_versions/version_tagging.rb +48 -0
  22. data/lib/vestal_versions/versioned.rb +27 -0
  23. data/lib/vestal_versions/versions.rb +74 -0
  24. data/spec/spec_helper.rb +20 -0
  25. data/spec/support/models.rb +19 -0
  26. data/spec/support/schema.rb +25 -0
  27. data/spec/vestal_versions/changes_spec.rb +134 -0
  28. data/spec/vestal_versions/conditions_spec.rb +103 -0
  29. data/spec/vestal_versions/control_spec.rb +120 -0
  30. data/spec/vestal_versions/creation_spec.rb +90 -0
  31. data/spec/vestal_versions/deletion_spec.rb +86 -0
  32. data/spec/vestal_versions/options_spec.rb +45 -0
  33. data/spec/vestal_versions/reload_spec.rb +18 -0
  34. data/spec/vestal_versions/reset_spec.rb +111 -0
  35. data/spec/vestal_versions/reversion_spec.rb +103 -0
  36. data/spec/vestal_versions/users_spec.rb +21 -0
  37. data/spec/vestal_versions/version_spec.rb +61 -0
  38. data/spec/vestal_versions/version_tagging_spec.rb +39 -0
  39. data/spec/vestal_versions/versioned_spec.rb +16 -0
  40. data/spec/vestal_versions/versions_spec.rb +176 -0
  41. metadata +169 -0
@@ -0,0 +1,80 @@
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
+ # Returns the current version number for the versioned object.
8
+ def version
9
+ @version ||= last_version
10
+ end
11
+
12
+ # Accepts a value corresponding to a specific version record, builds a history of changes
13
+ # between that version and the current version, and then iterates over that history updating
14
+ # the object's attributes until the it's reverted to its prior state.
15
+ #
16
+ # The single argument should adhere to one of the formats as documented in the +at+ method of
17
+ # VestalVersions::Versions.
18
+ #
19
+ # After the object is reverted to the target version, it is not saved. In order to save the
20
+ # object after the reversion, use the +revert_to!+ method.
21
+ #
22
+ # The version number of the object will reflect whatever version has been reverted to, and
23
+ # the return value of the +revert_to+ method is also the target version number.
24
+ def revert_to(value)
25
+ to_number = versions.number_at(value)
26
+
27
+ changes_between(version, to_number).each do |attribute, change|
28
+ write_attribute(attribute, change.last)
29
+ end
30
+
31
+ reset_version(to_number)
32
+ end
33
+
34
+ # Behaves similarly to the +revert_to+ method except that it automatically saves the record
35
+ # after the reversion. The return value is the success of the save.
36
+ def revert_to!(value)
37
+ revert_to(value)
38
+ reset_version if saved = save
39
+ saved
40
+ end
41
+
42
+ # Returns a boolean specifying whether the object has been reverted to a previous version or
43
+ # if the object represents the latest version in the version history.
44
+ def reverted?
45
+ version != last_version
46
+ end
47
+
48
+ private
49
+
50
+ # Mixes in the reverted_from value if it is currently within a revert
51
+ def version_attributes
52
+ attributes = super
53
+
54
+ if @reverted_from.nil?
55
+ attributes
56
+ else
57
+ attributes.merge(:reverted_from => @reverted_from)
58
+ end
59
+ end
60
+
61
+ # Returns the number of the last created version in the object's version history.
62
+ #
63
+ # If no associated versions exist, the object is considered at version 1.
64
+ def last_version
65
+ @last_version ||= versions.maximum(:number) || 1
66
+ end
67
+
68
+ # Clears the cached version number instance variables so that they can be recalculated.
69
+ # Useful after a new version is created.
70
+ def reset_version(version = nil)
71
+ if version.nil?
72
+ @last_version = nil
73
+ @reverted_from = nil
74
+ else
75
+ @reverted_from = version
76
+ end
77
+ @version = version
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,53 @@
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
+
15
+ private
16
+ # Overrides the +version_attributes+ method to include user information passed into the
17
+ # parent object, by way of a +updated_by+ attr_accessor.
18
+ def version_attributes
19
+ super.merge(:user => updated_by)
20
+ end
21
+
22
+ # Instance methods added to VestalVersions::Version to accomodate incoming user information.
23
+ module VersionMethods
24
+ extend ActiveSupport::Concern
25
+
26
+ included do
27
+ belongs_to :user, :polymorphic => true
28
+
29
+ alias_method_chain :user, :name
30
+ alias_method_chain :user=, :name
31
+ end
32
+
33
+ # Overrides the +user+ method created by the polymorphic +belongs_to+ user association. If
34
+ # the association is absent, defaults to the +user_name+ string column. This allows
35
+ # VestalVersions::Version#user to either return an ActiveRecord::Base object or a string,
36
+ # depending on what is sent to the +user_with_name=+ method.
37
+ def user_with_name
38
+ user_without_name || user_name
39
+ end
40
+
41
+ # Overrides the +user=+ method created by the polymorphic +belongs_to+ user association.
42
+ # Based on the class of the object given, either the +user+ association columns or the
43
+ # +user_name+ string column is populated.
44
+ def user_with_name=(value)
45
+ case value
46
+ when ActiveRecord::Base then self.user_without_name = value
47
+ else self.user_name = value
48
+ end
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,81 @@
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
+ attr_accessible :modifications, :number, :user
13
+
14
+ # ActiveRecord::Base#changes is an existing method, so before serializing the +changes+ column,
15
+ # the existing +changes+ method is undefined. The overridden +changes+ method pertained to
16
+ # dirty attributes, but will not affect the partial updates functionality as that's based on
17
+ # an underlying +changed_attributes+ method, not +changes+ itself.
18
+ undef_method :changes
19
+ def changes
20
+ self[:modifications]
21
+ end
22
+ serialize :modifications, Hash
23
+
24
+ # In conjunction with the included Comparable module, allows comparison of version records
25
+ # based on their corresponding version numbers, creation timestamps and IDs.
26
+ def <=>(other)
27
+ [number, created_at, id].map(&:to_i) <=> [other.number, other.created_at, other.id].map(&:to_i)
28
+ end
29
+
30
+ # Returns whether the version has a version number of 1. Useful when deciding whether to ignore
31
+ # the version during reversion, as initial versions have no serialized changes attached. Helps
32
+ # maintain backwards compatibility.
33
+ def initial?
34
+ number == 1
35
+ end
36
+
37
+ # Returns the original version number that this version was.
38
+ def original_number
39
+ if reverted_from.nil?
40
+ number
41
+ else
42
+ version = versioned.versions.at(reverted_from)
43
+ version.nil? ? 1 : version.original_number
44
+ end
45
+ end
46
+
47
+ def restore!
48
+ model = restore
49
+
50
+ if model
51
+ model.save!
52
+ destroy
53
+ end
54
+
55
+ model
56
+ end
57
+
58
+ def restore
59
+ if tag == 'deleted'
60
+ attrs = modifications
61
+
62
+ class_name = attrs['type'].blank? ? versioned_type : attrs['type']
63
+ klass = class_name.constantize
64
+ model = klass.new
65
+
66
+ attrs.each do |k, v|
67
+ begin
68
+ model.send "#{k}=", v
69
+ rescue NoMethodError
70
+ logger.warn "Attribute #{k} does not exist on #{class_name} (Version id: #{id})." rescue nil
71
+ end
72
+ end
73
+
74
+ model
75
+ else
76
+ latest_version = self.class.find(:first, :conditions => {:versioned_id => versioned_id, :versioned_type => versioned_type, :tag => 'deleted'})
77
+ latest_version.nil? ? nil : latest_version.restore
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,3 @@
1
+ module VestalVersions
2
+ VERSION = '1.2.3'
3
+ end
@@ -0,0 +1,48 @@
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
+ # Accepts a single string argument which is attached to the version record associated with
9
+ # the current version number of the parent object.
10
+ #
11
+ # Returns the given tag if successful, nil if not. Tags must be unique within the scope of
12
+ # the parent object. Tag creation will fail if non-unique.
13
+ #
14
+ # Version records corresponding to version number 1 are not typically created, but one will
15
+ # be built to house the given tag if the parent object's current version number is 1.
16
+ def tag_version(tag)
17
+ v = versions.at(version) || versions.build(:number => 1)
18
+ v.tag!(tag)
19
+ end
20
+
21
+ # Instance methods included into VestalVersions::Version to enable version tagging.
22
+ module VersionMethods
23
+ extend ActiveSupport::Concern
24
+
25
+ included do
26
+ validates_uniqueness_of :tag, :scope => [:versioned_id, :versioned_type], :if => :validate_tags?
27
+ end
28
+
29
+ # Attaches the given string to the version tag column. If the uniqueness validation fails,
30
+ # nil is returned. Otherwise, the given string is returned.
31
+ def tag!(tag)
32
+ write_attribute(:tag, tag)
33
+ save ? tag : nil
34
+ end
35
+
36
+ # Simply returns a boolean signifying whether the version instance has a tag value attached.
37
+ def tagged?
38
+ !tag.nil?
39
+ end
40
+
41
+ def validate_tags?
42
+ tagged? && tag != 'deleted'
43
+ end
44
+ end
45
+
46
+ Version.class_eval{ include VersionMethods }
47
+ end
48
+ 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 => "#{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 => "#{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 => "#{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 => ["#{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.expand_path('../support/*.rb', __FILE__)].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
+ self.table_name = 'users'
15
+ versioned :dependent => :tracking
16
+ end
17
+
18
+ class MyCustomVersion < VestalVersions::Version
19
+ end
@@ -0,0 +1,25 @@
1
+ ActiveRecord::Base.establish_connection(
2
+ :adapter => 'sqlite3',
3
+ :database => File.expand_path('../../test.db', __FILE__)
4
+ )
5
+
6
+ class CreateSchema < ActiveRecord::Migration
7
+ def self.up
8
+ create_table :users, :force => true do |t|
9
+ t.string :first_name
10
+ t.string :last_name
11
+ t.timestamps
12
+ end
13
+
14
+ create_table :versions, :force => true do |t|
15
+ t.belongs_to :versioned, :polymorphic => true
16
+ t.belongs_to :user, :polymorphic => true
17
+ t.string :user_name
18
+ t.text :modifications
19
+ t.integer :number
20
+ t.integer :reverted_from
21
+ t.string :tag
22
+ t.timestamps
23
+ end
24
+ end
25
+ end