Fingertips-revision_san 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +29 -0
- data/VERSION.yml +4 -0
- data/lib/revision_san.rb +50 -0
- data/lib/revision_san/diff.rb +101 -0
- data/test/diff_test.rb +123 -0
- data/test/revision_san_test.rb +116 -0
- data/test/test_helper.rb +91 -0
- metadata +61 -0
data/README.rdoc
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
= RevisionSan
|
2
|
+
|
3
|
+
A simple Rails plugin which creates revisions of your model and comes with an equally simple HTML differ.
|
4
|
+
|
5
|
+
== Install
|
6
|
+
|
7
|
+
* As a gem:
|
8
|
+
$ sudo gem install Fingertips-revision_san -s http://gems.github.com
|
9
|
+
|
10
|
+
* Vendor:
|
11
|
+
$ cd vendor/plugins && git clone git@github.com:Fingertips/revision_san.git
|
12
|
+
|
13
|
+
== Usage
|
14
|
+
|
15
|
+
Include the RevisionSan module into the model for which you'd like to keep
|
16
|
+
revisions.
|
17
|
+
|
18
|
+
class Artist < ActiveRecord::Base
|
19
|
+
include RevisionSan
|
20
|
+
end
|
21
|
+
|
22
|
+
And create a migration to add the columns needed by RevisionSan to your model:
|
23
|
+
|
24
|
+
add_column :artists, :revision, :default => 1
|
25
|
+
add_column :artists, :revision_parent_id, :default => nil
|
26
|
+
|
27
|
+
add_index :artists, :revision_parent_id
|
28
|
+
|
29
|
+
Copyright © 2008 Fingertips, Eloy Duran <eloy@fngtps.com>, released under the MIT license
|
data/VERSION.yml
ADDED
data/lib/revision_san.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path('../revision_san/diff', __FILE__)
|
2
|
+
|
3
|
+
module RevisionSan
|
4
|
+
def self.included(klass)
|
5
|
+
klass.class_eval do
|
6
|
+
before_update :create_new_revision
|
7
|
+
named_scope :current_revisions, { :conditions => { :revision_parent_id => nil } }
|
8
|
+
klass.extend ClassMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def find_with_current_revisions(*args)
|
14
|
+
current_revisions.find_without_current_revisions(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def count_with_current_revisions(*args)
|
18
|
+
current_revisions.count_without_current_revisions(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.extended(klass)
|
22
|
+
class << klass
|
23
|
+
alias_method_chain :find, :current_revisions
|
24
|
+
alias_method_chain :count, :current_revisions
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def revisions
|
30
|
+
self.class.find_without_current_revisions(:all, :conditions => { :revision_parent_id => id }, :order => 'id ASC') + [self]
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch_revision(revision)
|
34
|
+
revision = revision.to_i
|
35
|
+
return self if self.revision == revision
|
36
|
+
sub_query = revision_parent_id.blank? ? "revision_parent_id = #{id}" : "(id = #{revision_parent_id} OR revision_parent_id = #{revision_parent_id})"
|
37
|
+
self.class.find_without_current_revisions :first, :conditions => "#{sub_query} AND revision = #{revision}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_new_revision
|
41
|
+
if changed?
|
42
|
+
record = self.class.new(:revision_parent_id => id)
|
43
|
+
attributes.except('id', 'revision_parent_id').each do |key, value|
|
44
|
+
record.write_attribute(key, changes.has_key?(key) ? changes[key].first : value)
|
45
|
+
end
|
46
|
+
record.save(false)
|
47
|
+
self.revision += 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'diff/lcs'
|
2
|
+
|
3
|
+
module RevisionSan
|
4
|
+
# Return a RevisionSan::Diff object which compares this revision to the specified revision.
|
5
|
+
#
|
6
|
+
# artist = Artist.create(:name => 'van Gogh')
|
7
|
+
# artist.update_attribute(:name => 'Vincent van Gogh')
|
8
|
+
#
|
9
|
+
# revision_1 = artist.fetch_revision(1)
|
10
|
+
# diff = revision_1.compare_against_revision(2)
|
11
|
+
# diff.name # => '<ins>Vincent </ins>van Gogh'
|
12
|
+
def compare_against_revision(revision)
|
13
|
+
Diff.new(self, fetch_revision(revision))
|
14
|
+
end
|
15
|
+
|
16
|
+
class Diff
|
17
|
+
attr_reader :from, :to
|
18
|
+
|
19
|
+
def initialize(from, to)
|
20
|
+
@from, @to = from, to
|
21
|
+
end
|
22
|
+
|
23
|
+
def diff_for_column(column)
|
24
|
+
from_lines, to_lines = [@from.send(column), @to.send(column)].map do |res|
|
25
|
+
text = res.blank? ? '' : res.to_s
|
26
|
+
text = yield(text) if block_given?
|
27
|
+
text.scan(/\n+|.+/)
|
28
|
+
end
|
29
|
+
SimpleHTMLFormatter.diff(from_lines, to_lines)
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(method, *args, &block)
|
33
|
+
if @from.respond_to?(method) || @from.class.column_names.include?(method.to_s)
|
34
|
+
define_and_call_singleton_method(method, &block)
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def define_and_call_singleton_method(method, &block)
|
41
|
+
instance_eval %{
|
42
|
+
def #{method}(&block)
|
43
|
+
diff_for_column('#{method}', &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
#{method}(&block)
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
module SimpleHTMLFormatter
|
51
|
+
def self.diff(from_lines, to_lines)
|
52
|
+
LineDiffHTMLFormatter.new(from_lines, to_lines).output
|
53
|
+
end
|
54
|
+
|
55
|
+
class LineDiffHTMLFormatter
|
56
|
+
def output
|
57
|
+
@output.join.gsub(%r{</ins><ins>|</del><del>|<del></del>}, '')
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(from, to)
|
61
|
+
@output = []
|
62
|
+
@went_deep = false
|
63
|
+
::Diff::LCS.traverse_sequences(from, to, self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def discard_a(change)
|
67
|
+
from_words, to_words = [change.old_element, change.new_element].map { |text| text.to_s.scan(/\w+|\W|\s/) }
|
68
|
+
changes = ::Diff::LCS.diff(from_words, to_words).length
|
69
|
+
if changes > 1 && changes > (from_words.length / 5)
|
70
|
+
@output << "<del>#{change.old_element}</del>"
|
71
|
+
else
|
72
|
+
@output << WordDiffHTMLFormatter.new(from_words, to_words).output
|
73
|
+
@went_deep = true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def discard_b(change)
|
78
|
+
if @went_deep
|
79
|
+
@went_deep = false
|
80
|
+
else
|
81
|
+
@output << "<ins>#{change.new_element}</ins>"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def match(match)
|
86
|
+
@output << match.new_element
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class WordDiffHTMLFormatter < LineDiffHTMLFormatter
|
91
|
+
def output
|
92
|
+
@output.join
|
93
|
+
end
|
94
|
+
|
95
|
+
def discard_a(change)
|
96
|
+
@output << "<del>#{change.old_element}</del>"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/test/diff_test.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class DiffTestEntity
|
4
|
+
def self.column_names
|
5
|
+
["text"]
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_accessor :text
|
9
|
+
|
10
|
+
def initialize(text)
|
11
|
+
@text = text
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "RevisionSan::Diff" do
|
16
|
+
it "should return correctly formatted html" do
|
17
|
+
[
|
18
|
+
[long_text[:before], long_text[:after], long_text[:diff]],
|
19
|
+
[nil, "First words.", "<ins>First words.</ins>"],
|
20
|
+
["First words.", nil, "<del>First words.</del>"],
|
21
|
+
["No changes.", "No changes.", "No changes."],
|
22
|
+
["Bla", "Foo", "<del>Bla</del><ins>Foo</ins>"],
|
23
|
+
["Bar!", "Baz!", "<del>Bar</del><ins>Baz</ins>!"],
|
24
|
+
["Begin\n\nEnd", "Begin\n\nMiddle\n\nEnd", "Begin\n\n<ins>Middle\n\n</ins>End"],
|
25
|
+
["Multiple added.", "Multiple words are added.", "Multiple <ins>words are </ins>added."],
|
26
|
+
["Multiple words are removed.", "Multiple removed.", "Multiple <del>words are </del>removed."]
|
27
|
+
].each do |from, to, html|
|
28
|
+
diff_html(from, to).should == html
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def long_text
|
33
|
+
{
|
34
|
+
:before => %{Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Fusce gravida. Ut orci orci, molestie et, scelerisque ut, faucibus pharetra, enim. Morbi vehicula consequat nunc. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Quisque ac orci. Proin adipiscing tempor erat. Phasellus gravida tincidunt sapien. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In venenatis libero sit amet quam. Nam sapien diam, tempor placerat, feugiat quis, congue in, elit. Vivamus nec enim eget elit posuere tincidunt. Quisque scelerisque lobortis risus. Quisque cursus dolor sit amet arcu.
|
35
|
+
|
36
|
+
Suspendisse auctor. Quisque sodales dapibus pede. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras blandit tellus id libero. Morbi sed purus sed sapien ornare facilisis. Vestibulum rutrum egestas mauris. Vestibulum luctus velit vitae ante. In dictum, metus sed lacinia sagittis, leo diam elementum tortor, rutrum elementum justo tellus eget risus. Curabitur faucibus mauris eget nisi. Nam mattis nunc eget turpis. In porta. Aliquam risus ante, sodales quis, consequat vitae, fermentum ut, nisi. Etiam congue ipsum id ante aliquet dictum.},
|
37
|
+
|
38
|
+
:after => %{Landscape architecture involves the investigation and designed response to the landscape. The scope of the profession includes architectural design, site planning, environmental restoration, town or urban planning, urban design, parks and recreation planning. A practitioner in the field of landscape architecture is called a landscape architect.
|
39
|
+
|
40
|
+
Suspendisse auctor. Quisque sodales dapibus pede. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras blandit tellus id libero. Morbi sed purus sed sapien ornare facilisis. Vestibulum rutrum egestas mauris. Vestibulum luctus velit vitae ante. In dictum, metus sed lacinia sagittis, leo diam elementum tortor, rutrum elementum justo tellus eget risus. Curabitur faucibus mauris eget nisi. Nam mattis nunc eget turpis. In porta. Aliquam risus ante, sodales quis, consequat vitae, fermentum ut, nisi. Etiam congue ipsum id ante aliquet dictum.},
|
41
|
+
|
42
|
+
:diff => %{<del>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Fusce gravida. Ut orci orci, molestie et, scelerisque ut, faucibus pharetra, enim. Morbi vehicula consequat nunc. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Quisque ac orci. Proin adipiscing tempor erat. Phasellus gravida tincidunt sapien. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In venenatis libero sit amet quam. Nam sapien diam, tempor placerat, feugiat quis, congue in, elit. Vivamus nec enim eget elit posuere tincidunt. Quisque scelerisque lobortis risus. Quisque cursus dolor sit amet arcu.</del><ins>Landscape architecture involves the investigation and designed response to the landscape. The scope of the profession includes architectural design, site planning, environmental restoration, town or urban planning, urban design, parks and recreation planning. A practitioner in the field of landscape architecture is called a landscape architect.</ins>
|
43
|
+
|
44
|
+
Suspendisse auctor. Quisque sodales dapibus pede. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras blandit tellus id libero. Morbi sed purus sed sapien ornare facilisis. Vestibulum rutrum egestas mauris. Vestibulum luctus velit vitae ante. In dictum, metus sed lacinia sagittis, leo diam elementum tortor, rutrum elementum justo tellus eget risus. Curabitur faucibus mauris eget nisi. Nam mattis nunc eget turpis. In porta. Aliquam risus ante, sodales quis, consequat vitae, fermentum ut, nisi. Etiam congue ipsum id ante aliquet dictum.}
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def diff_html(from, to)
|
51
|
+
RevisionSan::Diff.new(DiffTestEntity.new(from), DiffTestEntity.new(to)).text
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Artist
|
56
|
+
def real_method
|
57
|
+
obj = Object.new
|
58
|
+
def obj.to_s
|
59
|
+
'Real method'
|
60
|
+
end
|
61
|
+
obj
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "RevisionSan, looking at diff methods" do
|
66
|
+
before do
|
67
|
+
RevisionSanTest::Initializer.setup_database
|
68
|
+
|
69
|
+
@artist_rev_2 = Artist.create(:name => 'van Gogh', :bio => 'He painted a lot.')
|
70
|
+
@artist_rev_2.update_attributes(:name => 'Vincent van Gogh', :bio => 'He occasionally drew a lot.')
|
71
|
+
@artist_rev_1 = @artist_rev_2.revisions.first
|
72
|
+
|
73
|
+
@diff = @artist_rev_1.compare_against_revision(2)
|
74
|
+
end
|
75
|
+
|
76
|
+
after do
|
77
|
+
RevisionSanTest::Initializer.teardown_database
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should take an older revision number to compare against" do
|
81
|
+
@artist_rev_2.compare_against_revision(1).should.be.instance_of RevisionSan::Diff
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should have instantiated a RevisionSan::Diff object with the correct revisions" do
|
85
|
+
@diff.from.should == @artist_rev_1
|
86
|
+
@diff.to.should == @artist_rev_2
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should lazy define accessors for requested columns" do
|
90
|
+
@diff.name
|
91
|
+
@diff.bio
|
92
|
+
|
93
|
+
@diff.should.respond_to :name
|
94
|
+
@diff.should.respond_to :bio
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should also work with real existing methods instead of a column" do
|
98
|
+
@diff.real_method
|
99
|
+
@diff.should.respond_to :real_method
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should coerce the value to a string before trying to diff them" do
|
103
|
+
@diff.real_method.should == 'Real method'
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should yield the from and to strings, if a block is given, so the user can adjust the text before diffing" do
|
107
|
+
@diff.real_method { |text| text.reverse }.should == 'dohtem laeR'
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should only define the accessors on the singleton, not the class" do
|
111
|
+
@diff.name
|
112
|
+
RevisionSan::Diff.instance_methods.should.not.include 'name'
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should still raise a NoMethodError for column names that don't exist" do
|
116
|
+
lambda { @diff.foo }.should.raise NoMethodError
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should return html with the diff for a requested column" do
|
120
|
+
@diff.name.should == "<ins>Vincent </ins>van Gogh"
|
121
|
+
@diff.bio.should == "He <del>painted</del><ins>occasionally drew</ins> a lot."
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
require 'active_support/testing/core_ext/test/unit/assertions'
|
4
|
+
|
5
|
+
describe "RevisionSan, looking at model methods" do
|
6
|
+
before do
|
7
|
+
RevisionSanTest::Initializer.setup_database
|
8
|
+
|
9
|
+
@artist = Artist.create(:name => 'van Gogh', :bio => 'He painted a lot.')
|
10
|
+
@artist.update_attributes(:name => 'Vincent van Gogh', :bio => 'He painted a lot of nice paintings.')
|
11
|
+
@artist.update_attributes(:name => 'Vincent van Gogh JR', :bio => 'Was never born.')
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
RevisionSanTest::Initializer.teardown_database
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should only return current revisions when using ::find" do
|
19
|
+
Artist.find(:all).should == [@artist]
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should return all revisions" do
|
23
|
+
revs = @artist.revisions
|
24
|
+
revs.length.should == 3
|
25
|
+
revs[0].should == Artist.find_without_current_revisions(:first, :conditions => { :revision => 1 })
|
26
|
+
revs[1].should == Artist.find_without_current_revisions(:first, :conditions => { :revision => 2 })
|
27
|
+
revs[2].should == Artist.find_without_current_revisions(:first, :conditions => { :revision => 3 })
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should return a specific revision" do
|
31
|
+
@artist.fetch_revision(1).should == @artist.revisions[0]
|
32
|
+
@artist.fetch_revision(2).should == @artist.revisions[1]
|
33
|
+
@artist.fetch_revision(3).should == @artist
|
34
|
+
|
35
|
+
@artist.fetch_revision('1').should == @artist.revisions[0]
|
36
|
+
@artist.fetch_revision('2').should == @artist.revisions[1]
|
37
|
+
@artist.fetch_revision('3').should == @artist
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should also be able to return a specific revision from an older revision" do
|
41
|
+
@artist.fetch_revision(1).fetch_revision(2).fetch_revision(3).should == @artist
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should convert a requested revision to an integer before using in the conditions" do
|
45
|
+
@artist.fetch_revision('some evil sql that will be coerced to 0').should.be nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "RevisionSan, when updating a record" do
|
50
|
+
before do
|
51
|
+
RevisionSanTest::Initializer.setup_database
|
52
|
+
|
53
|
+
@artist = Artist.create(:name => 'van Gogh', :bio => 'He painted a lot.')
|
54
|
+
@artist.update_attributes(:name => 'Vincent van Gogh', :bio => 'He painted a lot of nice paintings.')
|
55
|
+
end
|
56
|
+
|
57
|
+
after do
|
58
|
+
RevisionSanTest::Initializer.teardown_database
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should insert a new revision of a record" do
|
62
|
+
assert_difference('Artist.count_without_current_revisions', +1) do
|
63
|
+
@artist.update_attributes(:name => 'Vincent van Gogh JR', :bio => 'Was never born.')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should add the original attributes to the new revision record" do
|
68
|
+
@artist.revisions.first.name.should == 'van Gogh'
|
69
|
+
@artist.revisions.first.bio.should == 'He painted a lot.'
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should not take over the created_at value" do
|
73
|
+
created_at_before = @artist.created_at
|
74
|
+
sleep 1
|
75
|
+
@artist.update_attribute(:name, 'Gogh')
|
76
|
+
|
77
|
+
@artist.revisions.first.created_at.should.not == created_at_before
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should not create a new revision record if no attributes were changed" do
|
81
|
+
assert_no_difference('Artist.count') do
|
82
|
+
@artist.update_attributes({})
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should not create a new revision if validation fails on the original record" do
|
87
|
+
assert_no_difference('@artist.revision') do
|
88
|
+
assert_no_difference('Artist.count') do
|
89
|
+
@artist.update_attributes({ :name => '', :bio => 'Lost his name...' })
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "RevisionSan, class methods" do
|
96
|
+
before do
|
97
|
+
RevisionSanTest::Initializer.setup_database
|
98
|
+
|
99
|
+
@gogh = Artist.create(:name => 'van Gogh, 1', :bio => 'He painted a lot.')
|
100
|
+
9.times { |i| @gogh.update_attribute(:name, "van Gogh, #{i+2}") }
|
101
|
+
|
102
|
+
@picasso = Artist.create(:name => 'Picasso, 1', :bio => 'He drank a lot.')
|
103
|
+
9.times { |i| @picasso.update_attribute(:name, "Picasso, #{i+2}") }
|
104
|
+
end
|
105
|
+
|
106
|
+
after do
|
107
|
+
RevisionSanTest::Initializer.teardown_database
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should add a named_scope that only returns the latest revisions" do
|
111
|
+
revisions = Artist.current_revisions
|
112
|
+
revisions.map(&:id).should == [@gogh.id, @picasso.id]
|
113
|
+
revisions.map(&:name).should == ["van Gogh, 10", "Picasso, 10"]
|
114
|
+
revisions.map(&:revision).should == [10, 10]
|
115
|
+
end
|
116
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
module RevisionSanTest
|
2
|
+
module Initializer
|
3
|
+
VENDOR_RAILS = File.expand_path('../../../../rails', __FILE__)
|
4
|
+
OTHER_RAILS = File.expand_path('../../../rails', __FILE__)
|
5
|
+
PLUGIN_ROOT = File.expand_path('../../', __FILE__)
|
6
|
+
|
7
|
+
def self.rails_directory
|
8
|
+
if File.exist?(VENDOR_RAILS)
|
9
|
+
VENDOR_RAILS
|
10
|
+
elsif File.exist?(OTHER_RAILS)
|
11
|
+
OTHER_RAILS
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.load_dependencies
|
16
|
+
if rails_directory
|
17
|
+
$:.unshift(File.join(rails_directory, 'activesupport', 'lib'))
|
18
|
+
$:.unshift(File.join(rails_directory, 'activerecord', 'lib'))
|
19
|
+
else
|
20
|
+
require 'rubygems' rescue LoadError
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'activesupport'
|
24
|
+
require 'activerecord'
|
25
|
+
|
26
|
+
require 'rubygems' rescue LoadError
|
27
|
+
|
28
|
+
require 'test/spec'
|
29
|
+
require File.join(PLUGIN_ROOT, 'lib', 'revision_san')
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.configure_database
|
33
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
34
|
+
ActiveRecord::Migration.verbose = false
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.setup_database
|
38
|
+
ActiveRecord::Schema.define(:version => 1) do
|
39
|
+
create_table :members do |t|
|
40
|
+
t.timestamps
|
41
|
+
end
|
42
|
+
|
43
|
+
create_table :artists do |t|
|
44
|
+
t.integer :revision, :default => 1
|
45
|
+
t.integer :revision_parent_id, :default => nil
|
46
|
+
|
47
|
+
t.integer :member_id
|
48
|
+
t.string :name
|
49
|
+
t.text :bio
|
50
|
+
t.timestamps
|
51
|
+
end
|
52
|
+
|
53
|
+
create_table :category_assignments do |t|
|
54
|
+
t.integer :artist_id
|
55
|
+
t.integer :category_id
|
56
|
+
t.timestamps
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.teardown_database
|
62
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
63
|
+
ActiveRecord::Base.connection.drop_table(table)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.start
|
68
|
+
load_dependencies
|
69
|
+
configure_database
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
RevisionSanTest::Initializer.start
|
75
|
+
|
76
|
+
class Member < ActiveRecord::Base
|
77
|
+
has_one :artist
|
78
|
+
end
|
79
|
+
|
80
|
+
class Artist < ActiveRecord::Base
|
81
|
+
belongs_to :member
|
82
|
+
has_many :category_assignments
|
83
|
+
|
84
|
+
include RevisionSan
|
85
|
+
|
86
|
+
validates_presence_of :name
|
87
|
+
end
|
88
|
+
|
89
|
+
class CategoryAssignment < ActiveRecord::Base
|
90
|
+
belongs_to :artist
|
91
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: Fingertips-revision_san
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eloy Duran
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-03 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A simple Rails plugin which creates revisions of your model and comes with an equally simple HTML differ.
|
17
|
+
email: eloy.de.enige@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- README.rdoc
|
26
|
+
- VERSION.yml
|
27
|
+
- lib/revision_san
|
28
|
+
- lib/revision_san/diff.rb
|
29
|
+
- lib/revision_san.rb
|
30
|
+
- test/diff_test.rb
|
31
|
+
- test/revision_san_test.rb
|
32
|
+
- test/test_helper.rb
|
33
|
+
has_rdoc: true
|
34
|
+
homepage: http://github.com/Fingertips/revision_san
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options:
|
37
|
+
- --inline-source
|
38
|
+
- --charset=UTF-8
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.2.0
|
57
|
+
signing_key:
|
58
|
+
specification_version: 2
|
59
|
+
summary: A simple Rails plugin which creates revisions of your model and comes with an equally simple HTML differ.
|
60
|
+
test_files: []
|
61
|
+
|