kkorach-acts_as_revisable 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,151 @@
1
+ require 'acts_as_revisable/clone_associations'
2
+
3
+ module FatJam
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.
13
+ module Revision
14
+ def self.included(base) #:nodoc:
15
+ base.send(:extend, ClassMethods)
16
+
17
+ class << base
18
+ attr_accessor :revisable_revisable_class, :revisable_cloned_associations
19
+ end
20
+
21
+ base.instance_eval do
22
+ set_table_name(revisable_class.table_name)
23
+ acts_as_scoped_model :find => {:conditions => {:revisable_is_current => false}}
24
+
25
+ CloneAssociations.clone_associations(revisable_class, self)
26
+
27
+ define_callbacks :before_restore, :after_restore
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
41
+ end
42
+ end
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:
60
+ self[:revisable_name] = val
61
+ end
62
+
63
+ # Accessor for revisable_name just to make external API more pleasant.
64
+ def revision_name #:nodoc:
65
+ self[:revisable_name]
66
+ end
67
+
68
+ # Sets some initial values for a new revision.
69
+ def revision_setup #:nodoc:
70
+ now = Time.now
71
+ prev = current_revision.revisions.first
72
+ prev.update_attribute(:revisable_revised_at, now) if prev
73
+ self[:revisable_current_at] = now + 1.second
74
+ self[:revisable_is_current] = false
75
+ self[:revisable_branched_from_id] = current_revision[:revisable_branched_from_id]
76
+ self[:revisable_type] = current_revision[:type]
77
+ self[:revisable_number] = (self.class.maximum(:revisable_number, :conditions => {:revisable_original_id => self[:revisable_original_id]}) || 0) + 1
78
+ end
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
+
104
+ module ClassMethods
105
+ # Returns the +revisable_class_name+ as configured in
106
+ # +acts_as_revisable+.
107
+ def revisable_class_name #:nodoc:
108
+ self.revisable_options.revisable_class_name || self.class_name.gsub(/Revision/, '')
109
+ end
110
+
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
115
+ end
116
+
117
+ # Returns the revision_class which in this case is simply +self+.
118
+ def revision_class #:nodoc:
119
+ self
120
+ end
121
+
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:
134
+ clone_associations = self.revisable_options.clone_associations
135
+
136
+ self.revisable_cloned_associations ||= if clone_associations.blank?
137
+ []
138
+ elsif clone_associations.eql? :all
139
+ revisable_class.reflect_on_all_associations.map(&:name)
140
+ elsif clone_associations.is_a? [].class
141
+ clone_associations
142
+ elsif clone_associations[:only]
143
+ [clone_associations[:only]].flatten
144
+ elsif clone_associations[:except]
145
+ revisable_class.reflect_on_all_associations.map(&:name) - [clone_associations[:except]].flatten
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,75 @@
1
+ module FatJam
2
+ module ActsAsScopedModel
3
+ def self.included(base)
4
+ base.send(:extend, ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ SCOPED_METHODS = %w(construct_calculation_sql construct_finder_sql update_all delete_all destroy_all).freeze
9
+
10
+ def call_method_with_static_scope(meth, args)
11
+ return send(meth, *args) unless self.scoped_model_enabled?
12
+
13
+ with_scope(self.scoped_model_static_scope) do
14
+ send(meth, *args)
15
+ end
16
+ end
17
+
18
+ SCOPED_METHODS.each do |m|
19
+ module_eval <<-EVAL
20
+ def #{m}_with_static_scope(*args)
21
+ call_method_with_static_scope(:#{m}_without_static_scope, args)
22
+ end
23
+ EVAL
24
+ end
25
+
26
+ def without_model_scope
27
+ return unless block_given?
28
+
29
+ begin
30
+ self.scoped_model_enabled = false
31
+ rv = yield
32
+ ensure
33
+ self.scoped_model_enabled = true
34
+ end
35
+
36
+ rv
37
+ end
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
+
63
+ def acts_as_scoped_model(*args)
64
+ class << self
65
+ attr_accessor :scoped_model_static_scope, :scoped_model_disable_count
66
+ SCOPED_METHODS.each do |m|
67
+ alias_method_chain m.to_sym, :static_scope
68
+ end
69
+ end
70
+ self.scoped_model_disable_count = 0
71
+ self.scoped_model_static_scope = args.extract_options!
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,66 @@
1
+ require 'acts_as_revisable/options'
2
+ require 'acts_as_revisable/acts/common'
3
+ require 'acts_as_revisable/acts/revision'
4
+ require 'acts_as_revisable/acts/revisable'
5
+ require 'acts_as_revisable/acts/deletable'
6
+
7
+ module FatJam
8
+ # define the columns used internall by AAR
9
+ REVISABLE_SYSTEM_COLUMNS = %w(revisable_original_id revisable_branched_from_id revisable_number revisable_name revisable_type revisable_current_at revisable_revised_at revisable_deleted_at revisable_is_current)
10
+
11
+ # define the ActiveRecord magic columns that should not be monitored
12
+ REVISABLE_UNREVISABLE_COLUMNS = %w(id type created_at updated_at)
13
+
14
+ module ActsAsRevisable
15
+ def self.included(base)
16
+ base.send(:extend, ClassMethods)
17
+ end
18
+
19
+ module ClassMethods
20
+
21
+ # This +acts_as+ extension provides for making a model the
22
+ # revisable model in an acts_as_revisable pair.
23
+ def acts_as_revisable(*args, &block)
24
+ revisable_shared_setup(args, block)
25
+ self.send(:include, Revisable)
26
+ self.send(:include, Deletable) if self.revisable_options.on_delete == :revise
27
+ end
28
+
29
+ # This +acts_as+ extension provides for making a model the
30
+ # revision model in an acts_as_revisable pair.
31
+ def acts_as_revision(*args, &block)
32
+ revisable_shared_setup(args, block)
33
+ self.send(:include, Revision)
34
+ end
35
+
36
+ # This +acts_as+ extension provides for making a Single Table Inheritance model the
37
+ # revisable model in an acts_as_revisable pair.
38
+ def acts_as_revisable_sti(*args, &block)
39
+ revisable_shared_setup(args, block)
40
+ self.send(:extend, Common::ClassMethods)
41
+ self.send(:extend, Revisable::ClassMethods)
42
+ end
43
+
44
+ # This +acts_as+ extension provides for making a Single Table Inheritance model the
45
+ # revision model in an acts_as_revisable pair.
46
+ def acts_as_revision_sti(*args, &block)
47
+ revisable_shared_setup(args, block)
48
+ self.send(:extend, Common::ClassMethods)
49
+ self.send(:extend, Revision::ClassMethods)
50
+ end
51
+
52
+ private
53
+ # Performs the setup needed for both kinds of acts_as_revisable
54
+ # models.
55
+ def revisable_shared_setup(args, block)
56
+ class << self
57
+ attr_accessor :revisable_options
58
+ end
59
+ options = args.extract_options!
60
+ self.revisable_options = Options.new(options, &block)
61
+
62
+ self.send(:include, Common)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,36 @@
1
+ # This module encapsulates the methods used by ActsAsRevisable
2
+ # for cloning associations from one model to another.
3
+ module FatJam
4
+ module ActsAsRevisable
5
+ module CloneAssociations
6
+ class << self
7
+ def clone_associations(from, to)
8
+ return unless from.descends_from_active_record? && to.descends_from_active_record?
9
+
10
+ to.revision_cloned_associations.each do |key|
11
+ assoc = from.reflect_on_association(key)
12
+ meth = "clone_#{assoc.macro.to_s}_association"
13
+ meth = "clone_association" unless respond_to? meth
14
+ send(meth, assoc, to)
15
+ end
16
+ end
17
+
18
+ def clone_association(association, to)
19
+ options = association.options.clone
20
+ options[:foreign_key] ||= "revisable_original_id"
21
+ to.send(association.macro, association.name, options)
22
+ end
23
+
24
+ def clone_belongs_to_association(association, to)
25
+ to.send(association.macro, association.name, association.options.clone)
26
+ end
27
+
28
+ def clone_has_many_association(association, to)
29
+ options = association.options.clone
30
+ options[:foreign_key] ||= "revisable_original_id"
31
+ to.send(association.macro, association.name, options)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ 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
@@ -0,0 +1,22 @@
1
+ module FatJam
2
+ module ActsAsRevisable
3
+ # This class provides for a flexible method of setting
4
+ # options and querying them. This is especially useful
5
+ # for giving users flexibility when using your plugin.
6
+ class Options
7
+ def initialize(*options, &block)
8
+ @options = options.extract_options!
9
+ instance_eval(&block) if block_given?
10
+ end
11
+
12
+ def method_missing(key, *args)
13
+ return (@options[key.to_s.gsub(/\?$/, '').to_sym].eql?(true)) if key.to_s.match(/\?$/)
14
+ if args.blank?
15
+ @options[key.to_sym]
16
+ else
17
+ @options[key.to_sym] = args.size == 1 ? args.first : args
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
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.
13
+ module FatJam::QuotedColumnConditions
14
+ def self.included(base)
15
+ base.send(:extend, ClassMethods)
16
+
17
+ class << base
18
+ alias_method_chain :quote_bound_value, :quoted_column
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ def quote_bound_value_with_quoted_column(value)
24
+ if value.is_a?(Symbol) && column_names.member?(value.to_s)
25
+ # code borrowed from sanitize_sql_hash_for_conditions
26
+ attr = value.to_s
27
+ table_name = quoted_table_name
28
+
29
+ return "#{table_name}.#{connection.quote_column_name(attr)}"
30
+ end
31
+
32
+ quote_bound_value_without_quoted_column(value)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ module FatJam #:nodoc:
2
+ module ActsAsRevisable
3
+ module VERSION #:nodoc:
4
+ MAJOR = 0
5
+ MINOR = 9
6
+ TINY = 7
7
+
8
+ STRING = [MAJOR, MINOR, TINY].join('.')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'activesupport' unless defined? ActiveSupport
5
+ require 'activerecord' unless defined? ActiveRecord
6
+
7
+ require 'acts_as_revisable/version.rb'
8
+ require 'acts_as_revisable/acts/scoped_model'
9
+ require 'acts_as_revisable/quoted_columns'
10
+ require 'acts_as_revisable/base'
11
+
12
+ ActiveRecord::Base.send(:include, FatJam::ActsAsScopedModel)
13
+ ActiveRecord::Base.send(:include, FatJam::QuotedColumnConditions)
14
+ ActiveRecord::Base.send(:include, FatJam::ActsAsRevisable)
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'acts_as_revisable'
@@ -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