vestal_versions 1.0.2 → 2.0.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.
Files changed (69) hide show
  1. data/.gitignore +19 -20
  2. data/.travis.yml +22 -0
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile +10 -0
  5. data/README.rdoc +63 -36
  6. data/Rakefile +4 -43
  7. data/gemfiles/activerecord_3_0.gemfile +10 -0
  8. data/gemfiles/activerecord_3_1.gemfile +10 -0
  9. data/gemfiles/activerecord_3_2.gemfile +10 -0
  10. data/gemfiles/activerecord_4_0.gemfile +10 -0
  11. data/lib/generators/vestal_versions.rb +11 -0
  12. data/lib/generators/vestal_versions/migration/migration_generator.rb +17 -0
  13. data/{generators/vestal_versions → lib/generators/vestal_versions/migration}/templates/initializer.rb +0 -0
  14. data/{generators/vestal_versions → lib/generators/vestal_versions/migration}/templates/migration.rb +4 -3
  15. data/lib/vestal_versions.rb +39 -12
  16. data/lib/vestal_versions/changes.rb +43 -47
  17. data/lib/vestal_versions/conditions.rb +31 -43
  18. data/lib/vestal_versions/control.rb +162 -138
  19. data/lib/vestal_versions/creation.rb +67 -59
  20. data/lib/vestal_versions/deletion.rb +37 -0
  21. data/lib/vestal_versions/options.rb +6 -10
  22. data/lib/vestal_versions/reload.rb +7 -14
  23. data/lib/vestal_versions/reset.rb +15 -19
  24. data/lib/vestal_versions/reversion.rb +64 -52
  25. data/lib/vestal_versions/users.rb +36 -39
  26. data/lib/vestal_versions/version.rb +57 -2
  27. data/lib/vestal_versions/version_tagging.rb +51 -0
  28. data/lib/vestal_versions/versioned.rb +14 -17
  29. data/lib/vestal_versions/versions.rb +22 -7
  30. data/spec/spec_helper.rb +28 -0
  31. data/spec/support/models.rb +19 -0
  32. data/spec/support/schema.rb +25 -0
  33. data/spec/vestal_versions/changes_spec.rb +134 -0
  34. data/spec/vestal_versions/conditions_spec.rb +103 -0
  35. data/spec/vestal_versions/control_spec.rb +120 -0
  36. data/spec/vestal_versions/creation_spec.rb +90 -0
  37. data/spec/vestal_versions/deletion_spec.rb +86 -0
  38. data/spec/vestal_versions/options_spec.rb +45 -0
  39. data/spec/vestal_versions/reload_spec.rb +18 -0
  40. data/spec/vestal_versions/reset_spec.rb +111 -0
  41. data/spec/vestal_versions/reversion_spec.rb +103 -0
  42. data/spec/vestal_versions/users_spec.rb +21 -0
  43. data/spec/vestal_versions/version_spec.rb +61 -0
  44. data/spec/vestal_versions/version_tagging_spec.rb +39 -0
  45. data/spec/vestal_versions/versioned_spec.rb +16 -0
  46. data/spec/vestal_versions/versions_spec.rb +176 -0
  47. data/vestal_versions.gemspec +18 -100
  48. metadata +153 -102
  49. data/VERSION +0 -1
  50. data/generators/vestal_versions/vestal_versions_generator.rb +0 -10
  51. data/init.rb +0 -1
  52. data/lib/vestal_versions/configuration.rb +0 -40
  53. data/lib/vestal_versions/tagging.rb +0 -50
  54. data/test/changes_test.rb +0 -169
  55. data/test/conditions_test.rb +0 -137
  56. data/test/configuration_test.rb +0 -39
  57. data/test/control_test.rb +0 -152
  58. data/test/creation_test.rb +0 -110
  59. data/test/options_test.rb +0 -52
  60. data/test/reload_test.rb +0 -19
  61. data/test/reset_test.rb +0 -112
  62. data/test/reversion_test.rb +0 -68
  63. data/test/schema.rb +0 -43
  64. data/test/tagging_test.rb +0 -39
  65. data/test/test_helper.rb +0 -11
  66. data/test/users_test.rb +0 -25
  67. data/test/version_test.rb +0 -43
  68. data/test/versioned_test.rb +0 -18
  69. data/test/versions_test.rb +0 -172
@@ -1,17 +1,28 @@
1
+ require 'active_record'
2
+ require 'active_support/configurable'
3
+
1
4
  module VestalVersions
2
5
  # The ActiveRecord model representing versions.
3
6
  class Version < ActiveRecord::Base
4
7
  include Comparable
8
+ include ActiveSupport::Configurable
5
9
 
6
10
  # Associate polymorphically with the parent record.
7
11
  belongs_to :versioned, :polymorphic => true
8
12
 
13
+ if ActiveRecord::VERSION::MAJOR == 3
14
+ attr_accessible :modifications, :number, :user, :tag, :reverted_from
15
+ end
16
+
9
17
  # ActiveRecord::Base#changes is an existing method, so before serializing the +changes+ column,
10
- # the existing +changes+ method is undefined. The overridden +changes+ method pertained to
18
+ # the existing +changes+ method is undefined. The overridden +changes+ method pertained to
11
19
  # dirty attributes, but will not affect the partial updates functionality as that's based on
12
20
  # an underlying +changed_attributes+ method, not +changes+ itself.
13
21
  undef_method :changes
14
- serialize :changes, Hash
22
+ def changes
23
+ self[:modifications]
24
+ end
25
+ serialize :modifications, Hash
15
26
 
16
27
  # In conjunction with the included Comparable module, allows comparison of version records
17
28
  # based on their corresponding version numbers, creation timestamps and IDs.
@@ -25,5 +36,49 @@ module VestalVersions
25
36
  def initial?
26
37
  number == 1
27
38
  end
39
+
40
+ # Returns the original version number that this version was.
41
+ def original_number
42
+ if reverted_from.nil?
43
+ number
44
+ else
45
+ version = versioned.versions.at(reverted_from)
46
+ version.nil? ? 1 : version.original_number
47
+ end
48
+ end
49
+
50
+ def restore!
51
+ model = restore
52
+
53
+ if model
54
+ model.save!
55
+ destroy
56
+ end
57
+
58
+ model
59
+ end
60
+
61
+ def restore
62
+ if tag == 'deleted'
63
+ attrs = modifications
64
+
65
+ class_name = attrs['type'].blank? ? versioned_type : attrs['type']
66
+ klass = class_name.constantize
67
+ model = klass.new
68
+
69
+ attrs.each do |k, v|
70
+ begin
71
+ model.send "#{k}=", v
72
+ rescue NoMethodError
73
+ logger.warn "Attribute #{k} does not exist on #{class_name} (Version id: #{id})." rescue nil
74
+ end
75
+ end
76
+
77
+ model
78
+ else
79
+ latest_version = self.class.where(:versioned_id => versioned_id, :versioned_type => versioned_type, :tag => 'deleted').first
80
+ latest_version.nil? ? nil : latest_version.restore
81
+ end
82
+ end
28
83
  end
29
84
  end
@@ -0,0 +1,51 @@
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
+
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
+ t = v.tag!(tag)
20
+ versions.reload
21
+ t
22
+ end
23
+ end
24
+
25
+ # Instance methods included into VestalVersions::Version to enable version tagging.
26
+ module VersionMethods
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ validates_uniqueness_of :tag, :scope => [:versioned_id, :versioned_type], :if => :validate_tags?
31
+ end
32
+
33
+ # Attaches the given string to the version tag column. If the uniqueness validation fails,
34
+ # nil is returned. Otherwise, the given string is returned.
35
+ def tag!(tag)
36
+ write_attribute(:tag, tag)
37
+ save ? tag : nil
38
+ end
39
+
40
+ # Simply returns a boolean signifying whether the version instance has a tag value attached.
41
+ def tagged?
42
+ !tag.nil?
43
+ end
44
+
45
+ def validate_tags?
46
+ tagged? && tag != 'deleted'
47
+ end
48
+
49
+ Version.class_eval{ include VersionMethods }
50
+ end
51
+ end
@@ -1,30 +1,27 @@
1
1
  module VestalVersions
2
2
  # Simply adds a flag to determine whether a model class if versioned.
3
3
  module Versioned
4
- def self.extended(base) # :nodoc:
5
- base.class_eval do
6
- class << self
7
- alias_method_chain :versioned, :flag
8
- end
9
- end
10
- end
4
+ extend ActiveSupport::Concern
11
5
 
12
6
  # Overrides the +versioned+ method to first define the +versioned?+ class method before
13
7
  # deferring to the original +versioned+.
14
- def versioned_with_flag(*args)
15
- versioned_without_flag(*args)
8
+ module ClassMethods
9
+ def versioned(*args)
10
+ super(*args)
16
11
 
17
- class << self
18
- def versioned?
19
- true
12
+ class << self
13
+ def versioned?
14
+ true
15
+ end
20
16
  end
21
17
  end
22
- end
23
18
 
24
- # For all ActiveRecord::Base models that do not call the +versioned+ method, the +versioned?+
25
- # method will return false.
26
- def versioned?
27
- false
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
28
24
  end
25
+
29
26
  end
30
27
  end
@@ -1,3 +1,21 @@
1
+ # See: https://github.com/rails/rails/issues/11026
2
+ if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0 && RUBY_VERSION >= "2.0.0"
3
+ module ActiveRecord
4
+ module Associations
5
+ class AssociationProxy
6
+ def send(method, *args)
7
+ if proxy_respond_to?(method, true)
8
+ super
9
+ else
10
+ load_target
11
+ @target.send(method, *args)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
1
19
  module VestalVersions
2
20
  # An extension module for the +has_many+ association with versions.
3
21
  module Versions
@@ -13,16 +31,13 @@ module VestalVersions
13
31
  return [] if from_number.nil? || to_number.nil?
14
32
 
15
33
  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}.number #{(from_number > to_number) ? 'DESC' : 'ASC'}"
19
- )
34
+ where(:number => condition).order("#{table_name}.#{connection.quote_column_name('number')} #{(from_number > to_number) ? 'DESC' : 'ASC'}").to_a
20
35
  end
21
36
 
22
37
  # Returns all version records created before the version associated with the given value.
23
38
  def before(value)
24
39
  return [] if (number = number_at(value)).nil?
25
- all(:conditions => "#{aliased_table_name}.number < #{number}")
40
+ where("#{table_name}.#{connection.quote_column_name('number')} < #{number}").to_a
26
41
  end
27
42
 
28
43
  # Returns all version records created after the version associated with the given value.
@@ -30,7 +45,7 @@ module VestalVersions
30
45
  # This is useful for dissociating records during use of the +reset_to!+ method.
31
46
  def after(value)
32
47
  return [] if (number = number_at(value)).nil?
33
- all(:conditions => "#{aliased_table_name}.number > #{number}")
48
+ where("#{table_name}.#{connection.quote_column_name('number')} > #{number}").to_a
34
49
  end
35
50
 
36
51
  # Returns a single version associated with the given value. The following formats are valid:
@@ -49,7 +64,7 @@ module VestalVersions
49
64
  # untouched.
50
65
  def at(value)
51
66
  case value
52
- when Date, Time then last(:conditions => ["#{aliased_table_name}.created_at <= ?", value.to_time])
67
+ when Date, Time then where("#{table_name}.created_at <= ?", value.to_time).last
53
68
  when Numeric then find_by_number(value.floor)
54
69
  when String then find_by_tag(value)
55
70
  when Symbol then respond_to?(value) ? send(value) : nil
@@ -0,0 +1,28 @@
1
+ if ENV['COVERAGE']
2
+ require 'coveralls'
3
+ Coveralls.wear!
4
+ end
5
+
6
+ require 'vestal_versions'
7
+
8
+ require 'bundler'
9
+ Bundler.require(:test)
10
+
11
+ RSpec.configure do |c|
12
+ c.before(:suite) do
13
+ CreateSchema.suppress_messages{ CreateSchema.migrate(:up) }
14
+ end
15
+
16
+ c.after(:suite) do
17
+ FileUtils.rm_rf(File.expand_path('../test.db', __FILE__))
18
+ end
19
+
20
+ c.after(:each) do
21
+ VestalVersions::Version.config.clear
22
+ User.prepare_versioned_options({})
23
+ end
24
+
25
+ c.order = 'random'
26
+ end
27
+
28
+ 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
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ describe VestalVersions::Changes do
4
+ context "a version's changes" do
5
+ let(:user){ User.create(:name => 'Steve Richert') }
6
+ subject{ user.versions.last.changes }
7
+
8
+ before do
9
+ user.update_attribute(:last_name, 'Jobs')
10
+ end
11
+
12
+ it { should be_a(Hash) }
13
+ it { should_not be_empty }
14
+
15
+ it 'has string keys' do
16
+ subject.keys.each{ |key| key.should be_a(String) }
17
+ end
18
+
19
+ it 'has two-element array values' do
20
+ subject.values.each do |key|
21
+ key.should be_a(Array)
22
+ key.size.should == 2
23
+ end
24
+ end
25
+
26
+ it 'has unique-element values' do
27
+ subject.values.each{ |v| v.uniq.should == v }
28
+ end
29
+
30
+ it "equals the model's changes" do
31
+ user.first_name = 'Stephen'
32
+ model_changes = user.changes
33
+ user.save
34
+ changes = user.versions.last.changes
35
+
36
+ model_changes.should == changes
37
+ end
38
+ end
39
+
40
+ context 'a hash of changes' do
41
+ let(:changes){ {'first_name' => ['Steve', 'Stephen']} }
42
+ let(:other){ {'first_name' => ['Catie', 'Catherine']} }
43
+
44
+ it 'properly appends other changes' do
45
+ expected = {'first_name' => ['Steve', 'Catherine']}
46
+
47
+ changes.append_changes(other).should == expected
48
+
49
+ changes.append_changes!(other)
50
+ changes.should == expected
51
+ end
52
+
53
+ it 'properly prepends other changes' do
54
+ expected = {'first_name' => ['Catie', 'Stephen']}
55
+
56
+ changes.prepend_changes(other).should == expected
57
+
58
+ changes.prepend_changes!(other)
59
+ changes.should == expected
60
+ end
61
+
62
+ it 'is reversible' do
63
+ expected = {'first_name' => ['Stephen', 'Steve']}
64
+
65
+ changes.reverse_changes.should == expected
66
+
67
+ changes.reverse_changes!
68
+ changes.should == expected
69
+ end
70
+ end
71
+
72
+ context 'the changes between two versions' do
73
+ let(:name){ 'Steve Richert' }
74
+ let(:user){ User.create(:name => name) } # 1
75
+ let(:version){ user.version }
76
+
77
+ before do
78
+ user.update_attribute(:last_name, 'Jobs') # 2
79
+ user.update_attribute(:first_name, 'Stephen') # 3
80
+ user.update_attribute(:last_name, 'Richert') # 4
81
+ user.update_attribute(:name, name) # 5
82
+ end
83
+
84
+ it 'is a hash' do
85
+ 1.upto(version) do |i|
86
+ 1.upto(version) do |j|
87
+ user.changes_between(i, j).should be_a(Hash)
88
+ end
89
+ end
90
+ end
91
+
92
+ it 'has string keys' do
93
+ 1.upto(version) do |i|
94
+ 1.upto(version) do |j|
95
+ user.changes_between(i, j).keys.each{ |key| key.should be_a(String) }
96
+ end
97
+ end
98
+ end
99
+
100
+ it 'has two-element arrays with unique values' do
101
+ 1.upto(version) do |i|
102
+ 1.upto(version) do |j|
103
+ user.changes_between(i, j).values.each do |value|
104
+ value.should be_a(Array)
105
+ value.size.should == 2
106
+ value.uniq.should == value
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ it 'is empty between identical versions' do
113
+ user.changes_between(1, version).should be_empty
114
+ user.changes_between(version, 1).should be_empty
115
+ end
116
+
117
+ it 'is should reverse with direction' do
118
+ 1.upto(version) do |i|
119
+ i.upto(version) do |j|
120
+ up = user.changes_between(i, j)
121
+ down = user.changes_between(j, i)
122
+ up.should == down.reverse_changes
123
+ end
124
+ end
125
+ end
126
+
127
+ it 'is empty with invalid arguments' do
128
+ 1.upto(version) do |i|
129
+ user.changes_between(i, nil).should be_blank
130
+ user.changes_between(nil, i).should be_blank
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ describe VestalVersions::Conditions do
4
+ shared_examples_for 'a conditional option' do |option|
5
+ before do
6
+ User.class_eval do
7
+ def true; true; end
8
+ end
9
+ end
10
+
11
+ it 'is an array' do
12
+ User.vestal_versions_options[option].should be_a(Array)
13
+ User.prepare_versioned_options(option => :true)
14
+ User.vestal_versions_options[option].should be_a(Array)
15
+ end
16
+
17
+ it 'has proc values' do
18
+ User.prepare_versioned_options(option => :true)
19
+ User.vestal_versions_options[option].each{|i| i.should be_a(Proc) }
20
+ end
21
+ end
22
+
23
+ it_should_behave_like 'a conditional option', :if
24
+ it_should_behave_like 'a conditional option', :unless
25
+
26
+ context 'a new version' do
27
+ subject{ User.create(:name => 'Steve Richert') }
28
+ let(:count){ subject.versions.count }
29
+
30
+ before do
31
+ User.class_eval do
32
+ def true; true; end
33
+ def false; false; end
34
+ end
35
+ count # memoize this value
36
+ end
37
+
38
+ after do
39
+ User.prepare_versioned_options(:if => [], :unless => [])
40
+ end
41
+
42
+ context 'with :if conditions' do
43
+ context 'that pass' do
44
+ before do
45
+ User.prepare_versioned_options(:if => [:true])
46
+ subject.update_attribute(:last_name, 'Jobs')
47
+ end
48
+
49
+ its('versions.count'){ should == count + 1 }
50
+ end
51
+
52
+ context 'that fail' do
53
+ before do
54
+ User.prepare_versioned_options(:if => [:false])
55
+ subject.update_attribute(:last_name, 'Jobs')
56
+ end
57
+
58
+ its('versions.count'){ should == count }
59
+ end
60
+ end
61
+
62
+ context 'with :unless conditions' do
63
+ context 'that pass' do
64
+ before do
65
+ User.prepare_versioned_options(:unless => [:true])
66
+ subject.update_attribute(:last_name, 'Jobs')
67
+ end
68
+
69
+ its('versions.count'){ should == count }
70
+ end
71
+
72
+ context 'that fail' do
73
+ before do
74
+ User.prepare_versioned_options(:unless => [:false])
75
+ subject.update_attribute(:last_name, 'Jobs')
76
+ end
77
+
78
+ its('versions.count'){ should == count + 1 }
79
+ end
80
+ end
81
+
82
+ context 'with :if and :unless conditions' do
83
+ context 'that pass' do
84
+ before do
85
+ User.prepare_versioned_options(:if => [:true], :unless => [:true])
86
+ subject.update_attribute(:last_name, 'Jobs')
87
+ end
88
+
89
+ its('versions.count'){ should == count }
90
+ end
91
+
92
+ context 'that fail' do
93
+ before do
94
+ User.prepare_versioned_options(:if => [:false], :unless => [:false])
95
+ subject.update_attribute(:last_name, 'Jobs')
96
+ end
97
+
98
+ its('versions.count'){ should == count }
99
+ end
100
+ end
101
+
102
+ end
103
+ end