revision-san 0.2.0

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,26 @@
1
+ # Revision-San
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
+ $ gem install revision-san
8
+
9
+ ## Usage
10
+
11
+ Include the `RevisionSan` module into the model for which you'd like to keep revisions.
12
+
13
+ ```ruby
14
+ class Artist < ActiveRecord::Base
15
+ include RevisionSan
16
+ end
17
+ ```
18
+
19
+ And create a migration to add the columns needed by Revision-San to your model:
20
+
21
+ ```ruby
22
+ add_column :artists, :revision, :integer, :default => 1
23
+ add_column :artists, :revision_parent_id, :integer, :default => nil
24
+
25
+ add_index :artists, :revision_parent_id
26
+ ```
@@ -0,0 +1,49 @@
1
+ require 'revision_san/diff'
2
+
3
+ module RevisionSan
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_update :create_new_revision
8
+ scope :current_revisions, { :conditions => { :revision_parent_id => nil } }
9
+ end
10
+
11
+ module ClassMethods
12
+ def find_with_current_revisions(*args)
13
+ current_revisions.find_without_current_revisions(*args)
14
+ end
15
+
16
+ def count_with_current_revisions(*args)
17
+ current_revisions.count_without_current_revisions(*args)
18
+ end
19
+
20
+ def self.extended(klass)
21
+ class << klass
22
+ alias_method_chain :find, :current_revisions
23
+ alias_method_chain :count, :current_revisions
24
+ end
25
+ end
26
+ end
27
+
28
+ def revisions
29
+ self.class.find_without_current_revisions(:all, :conditions => { :revision_parent_id => id }, :order => 'id ASC') + [self]
30
+ end
31
+
32
+ def fetch_revision(revision)
33
+ revision = revision.to_i
34
+ return self if self.revision == revision
35
+ sub_query = revision_parent_id.blank? ? "revision_parent_id = #{id}" : "(id = #{revision_parent_id} OR revision_parent_id = #{revision_parent_id})"
36
+ self.class.find_without_current_revisions :first, :conditions => "#{sub_query} AND revision = #{revision}"
37
+ end
38
+
39
+ def create_new_revision
40
+ if changed?
41
+ record = self.class.new(:revision_parent_id => id)
42
+ attributes.except('id', 'revision_parent_id').each do |key, value|
43
+ record.send(:write_attribute, key, changes.has_key?(key) ? changes[key].first : value)
44
+ end
45
+ record.save(:validate => false)
46
+ self.revision += 1
47
+ end
48
+ end
49
+ 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
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: revision-san
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Eloy Duran
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - !ruby/object:Gem::Dependency
31
+ name: diff-lcs
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: bacon
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: sqlite3
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: A simple Rails plugin which creates revisions of your model and comes
95
+ with an equally simple HTML differ.
96
+ email: eloy.de.enige@gmail.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - README.md
102
+ - lib/revision_san.rb
103
+ - lib/revision_san/diff.rb
104
+ homepage: http://github.com/Fingertips/revision_san
105
+ licenses: []
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ! '>='
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 1.8.23
125
+ signing_key:
126
+ specification_version: 3
127
+ summary: A simple Rails plugin which creates revisions of your model and comes with
128
+ an equally simple HTML differ.
129
+ test_files: []