rich-acts_as_revisable 0.6.0 → 0.9.8

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