rich-acts_as_revisable 0.6.0 → 0.9.8

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.
@@ -2,10 +2,22 @@ require 'acts_as_revisable/clone_associations'
2
2
 
3
3
  module FatJam
4
4
  module ActsAsRevisable
5
+ # This module is mixed into the revision classes.
6
+ #
7
+ # ==== Callbacks
8
+ #
9
+ # * +before_restore+ is called on the revision class before it is
10
+ # restored as the current record.
11
+ # * +after_restore+ is called on the revision class after it is
12
+ # restored as the current record.
5
13
  module Revision
6
- def self.included(base)
14
+ def self.included(base) #:nodoc:
7
15
  base.send(:extend, ClassMethods)
8
-
16
+
17
+ class << base
18
+ attr_accessor :revisable_revisable_class, :revisable_cloned_associations
19
+ end
20
+
9
21
  base.instance_eval do
10
22
  set_table_name(revisable_class.table_name)
11
23
  acts_as_scoped_model :find => {:conditions => {:revisable_is_current => false}}
@@ -13,27 +25,48 @@ module FatJam
13
25
  CloneAssociations.clone_associations(revisable_class, self)
14
26
 
15
27
  define_callbacks :before_restore, :after_restore
16
-
17
- belongs_to :current_revision, :class_name => revisable_class_name, :foreign_key => :revisable_original_id
18
- belongs_to revisable_class_name.downcase.to_sym, :class_name => revisable_class_name, :foreign_key => :revisable_original_id
19
-
20
28
  before_create :revision_setup
29
+ after_create :grab_my_branches
30
+
31
+ [:current_revision, revisable_association_name.to_sym].each do |a|
32
+ belongs_to a, :class_name => revisable_class_name, :foreign_key => :revisable_original_id
33
+ end
34
+
35
+ [[:ancestors, "<"], [:descendants, ">"]].each do |a|
36
+ # Jumping through hoops here to try and make sure the
37
+ # :finder_sql is cross-database compatible. :finder_sql
38
+ # in a plugin is evil but, I see no other option.
39
+ has_many a.first, :class_name => revision_class_name, :finder_sql => "select * from #{quoted_table_name} where #{quote_bound_value(:revisable_original_id)} = \#{revisable_original_id} and #{quote_bound_value(:revisable_number)} #{a.last} \#{revisable_number} and #{quote_bound_value(:revisable_is_current)} = #{quote_value(false)} order by #{quote_bound_value(:revisable_number)} #{(a.last.eql?("<") ? "DESC" : "ASC")}"
40
+ end
21
41
  end
22
42
  end
23
-
24
- def revision_name=(val)
43
+
44
+ def find_revision(*args)
45
+ current_revision.find_revision(*args)
46
+ end
47
+
48
+ # Return the revision prior to this one.
49
+ def previous_revision
50
+ self.class.find(:first, :conditions => {:revisable_original_id => revisable_original_id, :revisable_number => revisable_number - 1})
51
+ end
52
+
53
+ # Return the revision after this one.
54
+ def next_revision
55
+ self.class.find(:first, :conditions => {:revisable_original_id => revisable_original_id, :revisable_number => revisable_number + 1})
56
+ end
57
+
58
+ # Setter for revisable_name just to make external API more pleasant.
59
+ def revision_name=(val) #:nodoc:
25
60
  self[:revisable_name] = val
26
61
  end
27
62
 
28
- def revision_name
63
+ # Accessor for revisable_name just to make external API more pleasant.
64
+ def revision_name #:nodoc:
29
65
  self[:revisable_name]
30
66
  end
31
-
32
- def revision_number
33
- self[:revisable_number]
34
- end
35
67
 
36
- def revision_setup
68
+ # Sets some initial values for a new revision.
69
+ def revision_setup #:nodoc:
37
70
  now = Time.now
38
71
  prev = current_revision.revisions.first
39
72
  prev.update_attribute(:revisable_revised_at, now) if prev
@@ -43,24 +76,64 @@ module FatJam
43
76
  self[:revisable_type] = current_revision[:type]
44
77
  self[:revisable_number] = (self.class.maximum(:revisable_number, :conditions => {:revisable_original_id => self[:revisable_original_id]}) || 0) + 1
45
78
  end
46
-
79
+
80
+ def grab_my_branches
81
+ self.class.revisable_class.update_all(["revisable_branched_from_id = ?", self[:id]], ["revisable_branched_from_id = ?", self[:revisable_original_id]])
82
+ end
83
+
84
+ def from_revisable
85
+ current_revision.for_revision
86
+ end
87
+
88
+ def reverting_from
89
+ from_revisable[:reverting_from]
90
+ end
91
+
92
+ def reverting_from=(val)
93
+ from_revisable[:reverting_from] = val
94
+ end
95
+
96
+ def reverting_to
97
+ from_revisable[:reverting_to]
98
+ end
99
+
100
+ def reverting_to=(val)
101
+ from_revisable[:reverting_to] = val
102
+ end
103
+
47
104
  module ClassMethods
48
- def revisable_class_name
105
+ # Returns the +revisable_class_name+ as configured in
106
+ # +acts_as_revisable+.
107
+ def revisable_class_name #:nodoc:
49
108
  self.revisable_options.revisable_class_name || self.class_name.gsub(/Revision/, '')
50
109
  end
51
110
 
52
- def revisable_class
53
- @revisable_class ||= revisable_class_name.constantize
111
+ # Returns the actual +Revisable+ class based on the
112
+ # #revisable_class_name.
113
+ def revisable_class #:nodoc:
114
+ self.revisable_revisable_class ||= revisable_class_name.constantize
54
115
  end
55
116
 
56
- def revision_class
117
+ # Returns the revision_class which in this case is simply +self+.
118
+ def revision_class #:nodoc:
57
119
  self
58
120
  end
59
121
 
60
- def revision_cloned_associations
122
+ def revision_class_name #:nodoc:
123
+ self.name
124
+ end
125
+
126
+ # Returns the name of the association acts_as_revision
127
+ # creates.
128
+ def revisable_association_name #:nodoc:
129
+ revisable_class_name.downcase
130
+ end
131
+
132
+ # Returns an array of the associations that should be cloned.
133
+ def revision_cloned_associations #:nodoc:
61
134
  clone_associations = self.revisable_options.clone_associations
62
135
 
63
- @aa_revisable_cloned_associations ||= if clone_associations.blank?
136
+ self.revisable_cloned_associations ||= if clone_associations.blank?
64
137
  []
65
138
  elsif clone_associations.eql? :all
66
139
  revisable_class.reflect_on_all_associations.map(&:name)
@@ -8,7 +8,7 @@ module FatJam
8
8
  SCOPED_METHODS = %w(construct_calculation_sql construct_finder_sql update_all delete_all destroy_all).freeze
9
9
 
10
10
  def call_method_with_static_scope(meth, args)
11
- return send(meth, *args) unless self.scoped_model_enabled
11
+ return send(meth, *args) unless self.scoped_model_enabled?
12
12
 
13
13
  with_scope(self.scoped_model_static_scope) do
14
14
  send(meth, *args)
@@ -22,7 +22,7 @@ module FatJam
22
22
  end
23
23
  EVAL
24
24
  end
25
-
25
+
26
26
  def without_model_scope
27
27
  return unless block_given?
28
28
 
@@ -36,14 +36,38 @@ module FatJam
36
36
  rv
37
37
  end
38
38
 
39
+ def disable_model_scope!
40
+ self.scoped_model_disable_count += 1
41
+ end
42
+
43
+ def enable_model_scope!
44
+ self.scoped_model_disable_count -= 1
45
+ end
46
+
47
+ def scoped_model_enabled?
48
+ self.scoped_model_disable_count == 0
49
+ end
50
+
51
+ def scoped_model_enabled
52
+ self.scoped_model_enabled?
53
+ end
54
+
55
+ def scoped_model_enabled=(value)
56
+ if value == false
57
+ disable_model_scope!
58
+ else
59
+ enable_model_scope!
60
+ end
61
+ end
62
+
39
63
  def acts_as_scoped_model(*args)
40
64
  class << self
41
- attr_accessor :scoped_model_static_scope, :scoped_model_enabled
65
+ attr_accessor :scoped_model_static_scope, :scoped_model_disable_count
42
66
  SCOPED_METHODS.each do |m|
43
67
  alias_method_chain m.to_sym, :static_scope
44
68
  end
45
69
  end
46
- self.scoped_model_enabled = true
70
+ self.scoped_model_disable_count = 0
47
71
  self.scoped_model_static_scope = args.extract_options!
48
72
  end
49
73
  end
@@ -2,6 +2,7 @@ require 'acts_as_revisable/options'
2
2
  require 'acts_as_revisable/acts/common'
3
3
  require 'acts_as_revisable/acts/revision'
4
4
  require 'acts_as_revisable/acts/revisable'
5
+ require 'acts_as_revisable/acts/deletable'
5
6
 
6
7
  module FatJam
7
8
  # define the columns used internall by AAR
@@ -22,25 +23,27 @@ module FatJam
22
23
  def acts_as_revisable(*args, &block)
23
24
  revisable_shared_setup(args, block)
24
25
  self.send(:include, Revisable)
26
+ self.send(:include, Deletable) if self.revisable_options.on_delete == :revise
25
27
  end
26
28
 
27
29
  # This +acts_as+ extension provides for making a model the
28
30
  # revision model in an acts_as_revisable pair.
29
31
  def acts_as_revision(*args, &block)
30
32
  revisable_shared_setup(args, block)
31
- self.send(:include, Revision)
33
+ self.send(:include, Revision)
32
34
  end
33
35
 
34
36
  private
35
37
  # Performs the setup needed for both kinds of acts_as_revisable
36
38
  # models.
37
39
  def revisable_shared_setup(args, block)
38
- self.send(:include, Common)
39
40
  class << self
40
41
  attr_accessor :revisable_options
41
42
  end
42
43
  options = args.extract_options!
43
- @revisable_options = Options.new(options, &block)
44
+ self.revisable_options = Options.new(options, &block)
45
+
46
+ self.send(:include, Common)
44
47
  end
45
48
  end
46
49
  end
@@ -1,3 +1,5 @@
1
+ # This module encapsulates the methods used by ActsAsRevisable
2
+ # for cloning associations from one model to another.
1
3
  module FatJam
2
4
  module ActsAsRevisable
3
5
  module CloneAssociations
@@ -22,6 +24,12 @@ module FatJam
22
24
  def clone_belongs_to_association(association, to)
23
25
  to.send(association.macro, association.name, association.options.clone)
24
26
  end
27
+
28
+ def clone_has_many_association(association, to)
29
+ options = association.options.clone
30
+ options[:association_foreign_key] ||= "revisable_original_id"
31
+ to.send(association.macro, association.name, options)
32
+ end
25
33
  end
26
34
  end
27
35
  end
@@ -0,0 +1,18 @@
1
+ module FatJam #:nodoc:
2
+ module ActsAsRevisable
3
+ class GemSpecOptions
4
+ HASH = {
5
+ :name => "fatjam-acts_as_revisable",
6
+ :version => FatJam::ActsAsRevisable::VERSION::STRING,
7
+ :summary => "acts_as_revisable enables revision tracking, querying, reverting and branching of ActiveRecord models. Inspired by acts_as_versioned.",
8
+ :email => "cavanaugh@fatjam.com",
9
+ :homepage => "http://github.com/fatjam/acts_as_revisable/tree/master",
10
+ :has_rdoc => true,
11
+ :authors => ["Rich Cavanaugh of JamLab, LLC.", "Stephen Caudill of JamLab, LLC."],
12
+ :files => %w( LICENSE README.rdoc Rakefile ) + Dir["{spec,lib,generators,rails}/**/*"],
13
+ :rdoc_options => ["--main", "README.rdoc"],
14
+ :extra_rdoc_files => ["README.rdoc", "LICENSE"]
15
+ }
16
+ end
17
+ end
18
+ end
@@ -1,7 +1,19 @@
1
+ # This module is more about the pretty than anything else. This allows
2
+ # you to use symbols for column names in a conditions hash.
3
+ #
4
+ # User.find(:all, :conditions => ["? = ?", :name, "sam"])
5
+ #
6
+ # Would generate:
7
+ #
8
+ # select * from users where "users"."name" = 'sam'
9
+ #
10
+ # This is consistent with Rails and Ruby where symbols are used to
11
+ # represent methods. Only a symbol matching a column name will
12
+ # trigger this beavior.
1
13
  module FatJam::QuotedColumnConditions
2
14
  def self.included(base)
3
15
  base.send(:extend, ClassMethods)
4
-
16
+
5
17
  class << base
6
18
  alias_method_chain :quote_bound_value, :quoted_column
7
19
  end
@@ -12,14 +24,7 @@ module FatJam::QuotedColumnConditions
12
24
  if value.is_a?(Symbol) && column_names.member?(value.to_s)
13
25
  # code borrowed from sanitize_sql_hash_for_conditions
14
26
  attr = value.to_s
15
-
16
- # Extract table name from qualified attribute names.
17
- if attr.include?('.')
18
- table_name, attr = attr.split('.', 2)
19
- table_name = connection.quote_table_name(table_name)
20
- else
21
- table_name = quoted_table_name
22
- end
27
+ table_name = quoted_table_name
23
28
 
24
29
  return "#{table_name}.#{connection.quote_column_name(attr)}"
25
30
  end
@@ -2,8 +2,8 @@ module FatJam #:nodoc:
2
2
  module ActsAsRevisable
3
3
  module VERSION #:nodoc:
4
4
  MAJOR = 0
5
- MINOR = 6
6
- TINY = 0
5
+ MINOR = 9
6
+ TINY = 8
7
7
 
8
8
  STRING = [MAJOR, MINOR, TINY].join('.')
9
9
  end
@@ -0,0 +1,22 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe FatJam::ActsAsRevisable do
4
+ after(:each) do
5
+ cleanup_db
6
+ end
7
+
8
+ before(:each) do
9
+ @project = Project.create(:name => "Rich", :notes => "this plugin's author")
10
+ @project.update_attribute(:name, "one")
11
+ @project.update_attribute(:name, "two")
12
+ @project.update_attribute(:name, "three")
13
+ end
14
+
15
+ it "should have a pretty named association" do
16
+ lambda { @project.sessions }.should_not raise_error
17
+ end
18
+
19
+ it "should return all the revisions" do
20
+ @project.revisions.size.should == 3
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ class Project
4
+ validates_presence_of :name
5
+ end
6
+
7
+ describe FatJam::ActsAsRevisable, "with branching" do
8
+ after(:each) do
9
+ cleanup_db
10
+ end
11
+
12
+ before(:each) do
13
+ @project = Project.create(:name => "Rich", :notes => "a note")
14
+ @project.update_attribute(:name, "Sam")
15
+ end
16
+
17
+ it "should allow for branch creation" do
18
+ @project.should == @project.branch.branch_source
19
+ end
20
+
21
+ it "should branch without saving" do
22
+ @project.branch.should be_new_record
23
+ end
24
+
25
+ it "should branch and save" do
26
+ @project.branch!.should_not be_new_record
27
+ end
28
+
29
+ it "should not raise an error for a valid branch" do
30
+ lambda { @project.branch!(:name => "A New User") }.should_not raise_error
31
+ end
32
+
33
+ it "should raise an error for invalid records" do
34
+ lambda { @project.branch!(:name => nil) }.should raise_error
35
+ end
36
+
37
+ it "should not save an invalid record" do
38
+ @branch = @project.branch(:name => nil)
39
+ @branch.save.should be_false
40
+ @branch.should be_new_record
41
+ end
42
+ end
data/spec/find_spec.rb ADDED
@@ -0,0 +1,38 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe FatJam::ActsAsRevisable do
4
+ after(:each) do
5
+ cleanup_db
6
+ end
7
+
8
+ describe "with a single revision" do
9
+ before(:each) do
10
+ @project1 = Project.create(:name => "Rich", :notes => "a note")
11
+ @project1.update_attribute(:name, "Sam")
12
+ end
13
+
14
+ it "should just find the current revision by default" do
15
+ Project.find(:first).name.should == "Sam"
16
+ end
17
+
18
+ it "should accept the :with_revisions options" do
19
+ lambda { Project.find(:all, :with_revisions => true) }.should_not raise_error
20
+ end
21
+
22
+ it "should provide find_with_revisions" do
23
+ lambda { Project.find_with_revisions(:all) }.should_not raise_error
24
+ end
25
+
26
+ it "should find current and revisions with the :with_revisions option" do
27
+ Project.find(:all, :with_revisions => true).size.should == 2
28
+ end
29
+
30
+ it "should find current and revisions with the find_with_revisions method" do
31
+ Project.find_with_revisions(:all).size.should == 2
32
+ end
33
+
34
+ it "should find revisions with conditions" do
35
+ Project.find_with_revisions(:all, :conditions => {:name => "Rich"}).should == [@project1.find_revision(:previous)]
36
+ end
37
+ end
38
+ end
@@ -1,43 +1,34 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper.rb'
2
2
 
3
- class Project < ActiveRecord::Base
4
- acts_as_revisable do
5
- revision_class_name "Session"
6
- except :unimportant
7
- end
8
- end
9
-
10
- class Session < ActiveRecord::Base
11
- acts_as_revision do
12
- revisable_class_name "Project"
13
- end
14
- end
15
-
16
- describe FatJam::ActsAsRevisable do
17
- before(:all) do
18
- setup_db
19
- end
20
-
3
+ describe FatJam::ActsAsRevisable do
21
4
  after(:each) do
22
5
  cleanup_db
23
6
  end
24
-
25
- after(:all) do
26
- teardown_db
27
- end
28
-
7
+
29
8
  before(:each) do
30
9
  @project = Project.create(:name => "Rich", :notes => "this plugin's author")
31
10
  end
32
11
 
33
12
  describe "without revisions" do
34
13
  it "should have a revision_number of zero" do
35
- @project.revision_number.should == 0
14
+ @project.revision_number.should be_zero
36
15
  end
37
16
 
38
- it "should have no revisions" do
17
+ it "should be the current revision" do
18
+ @project.revisable_is_current.should be_true
19
+ end
20
+
21
+ it "should respond to current_revision? positively" do
22
+ @project.current_revision?.should be_true
23
+ end
24
+
25
+ it "should not have any revisions in the generic association" do
39
26
  @project.revisions.should be_empty
40
27
  end
28
+
29
+ it "should not have any revisions in the pretty named association" do
30
+ @project.sessions.should be_empty
31
+ end
41
32
  end
42
33
 
43
34
  describe "with revisions" do
@@ -49,10 +40,14 @@ describe FatJam::ActsAsRevisable do
49
40
  @project.revision_number.should == 1
50
41
  end
51
42
 
52
- it "should have a single revision" do
43
+ it "should have a single revision in the generic association" do
53
44
  @project.revisions.size.should == 1
54
45
  end
55
46
 
47
+ it "should have a single revision in the pretty named association" do
48
+ @project.sessions.size.should == 1
49
+ end
50
+
56
51
  it "should return an instance of the revision class" do
57
52
  @project.revisions.first.should be_an_instance_of(Session)
58
53
  end
@@ -0,0 +1,19 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe "the quoted_columns extension" do
4
+ after(:each) do
5
+ cleanup_db
6
+ end
7
+
8
+ it "should quote symbols matching column names as columns" do
9
+ Project.send(:quote_bound_value, :name).should == %q{"projects"."name"}
10
+ end
11
+
12
+ it "should not quote symbols that don't match column names" do
13
+ Project.send(:quote_bound_value, :whatever).should == "'#{:whatever.to_yaml}'"
14
+ end
15
+
16
+ it "should not quote strings any differently" do
17
+ Project.send(:quote_bound_value, "what").should == Project.send(:quote_bound_value_with_quoted_column, "what")
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe FatJam::ActsAsRevisable, "with reverting" do
4
+ after(:each) do
5
+ cleanup_db
6
+ end
7
+
8
+ before(:each) do
9
+ @project = Project.create(:name => "Rich", :notes => "a note")
10
+ @project.update_attribute(:name, "Sam")
11
+ end
12
+
13
+ it "should let you revert to previous versions" do
14
+ @project.revert_to!(:first)
15
+ @project.name.should == "Rich"
16
+ end
17
+
18
+ it "should accept the :without_revision hash option" do
19
+ lambda { @project.revert_to!(:first, :without_revision => true) }.should_not raise_error
20
+ @project.name.should == "Rich"
21
+ end
22
+
23
+ it "should support the revert_to_without_revision method" do
24
+ lambda { @project.revert_to_without_revision(:first).save }.should_not raise_error
25
+ @project.name.should == "Rich"
26
+ end
27
+
28
+ it "should support the revert_to_without_revision! method" do
29
+ lambda { @project.revert_to_without_revision!(:first) }.should_not raise_error
30
+ @project.name.should == "Rich"
31
+ end
32
+
33
+ it "should let you revert to previous versions without a new revision" do
34
+ @project.revert_to!(:first, :without_revision => true)
35
+ @project.revisions.size.should == 1
36
+ end
37
+
38
+ it "should support the revert_to method" do
39
+ lambda{ @project.revert_to(:first) }.should_not raise_error
40
+ @project.should be_changed
41
+ end
42
+ end
data/spec/spec_helper.rb CHANGED
@@ -19,6 +19,15 @@ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memo
19
19
 
20
20
  def setup_db
21
21
  ActiveRecord::Schema.define(:version => 1) do
22
+ create_table :people do |t|
23
+ t.string :name, :revisable_name, :revisable_type
24
+ t.text :notes
25
+ t.boolean :revisable_is_current
26
+ t.integer :revisable_original_id, :revisable_branched_from_id, :revisable_number, :project_id
27
+ t.datetime :revisable_current_at, :revisable_revised_at, :revisable_deleted_at
28
+ t.timestamps
29
+ end
30
+
22
31
  create_table :projects do |t|
23
32
  t.string :name, :unimportant, :revisable_name, :revisable_type
24
33
  t.text :notes
@@ -30,14 +39,41 @@ def setup_db
30
39
  end
31
40
  end
32
41
 
33
- def teardown_db
34
- ActiveRecord::Base.connection.tables.each do |table|
35
- ActiveRecord::Base.connection.drop_table(table)
36
- end
37
- end
42
+ setup_db
38
43
 
39
44
  def cleanup_db
40
45
  ActiveRecord::Base.connection.tables.each do |table|
41
46
  ActiveRecord::Base.connection.execute("delete from #{table}")
42
47
  end
48
+ end
49
+
50
+ class Person < ActiveRecord::Base
51
+ belongs_to :project
52
+
53
+ acts_as_revisable do
54
+ revision_class_name "OldPerson"
55
+ end
56
+ end
57
+
58
+ class OldPerson < ActiveRecord::Base
59
+ acts_as_revision do
60
+ revisable_class_name "Person"
61
+ clone_associations :all
62
+ end
63
+ end
64
+
65
+ class Project < ActiveRecord::Base
66
+ has_many :people
67
+
68
+ acts_as_revisable do
69
+ revision_class_name "Session"
70
+ except :unimportant
71
+ end
72
+ end
73
+
74
+ class Session < ActiveRecord::Base
75
+ acts_as_revision do
76
+ revisable_class_name "Project"
77
+ clone_associations :all
78
+ end
43
79
  end