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.
- data/README.md +26 -0
- data/lib/revision_san.rb +49 -0
- data/lib/revision_san/diff.rb +101 -0
- metadata +129 -0
data/README.md
ADDED
@@ -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
|
+
```
|
data/lib/revision_san.rb
ADDED
@@ -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: []
|