kkorach-acts_as_revisable 0.9.7

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.
@@ -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