historical 0.2.1 → 0.2.2
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/.gitignore +3 -1
- data/README.markdown +121 -0
- data/Rakefile +27 -8
- data/VERSION +1 -1
- data/historical.gemspec +8 -5
- data/lib/historical.rb +42 -27
- data/lib/historical/active_record.rb +109 -29
- data/lib/historical/class_builder.rb +60 -0
- data/lib/historical/model_history.rb +39 -33
- data/lib/historical/models/attribute_diff.rb +53 -23
- data/lib/historical/models/model_version.rb +74 -6
- data/lib/historical/models/model_version/diff.rb +90 -0
- data/lib/historical/models/model_version/meta.rb +19 -0
- data/lib/historical/models/pool.rb +31 -0
- data/lib/historical/mongo_mapper_enhancements.rb +11 -0
- data/lib/historical/railtie.rb +4 -2
- data/spec/historical_spec.rb +49 -15
- data/spec/spec_helper.rb +9 -2
- metadata +10 -7
- data/README.rdoc +0 -17
- data/lib/historical/models/model_diff.rb +0 -90
@@ -0,0 +1,19 @@
|
|
1
|
+
module Historical::Models
|
2
|
+
class ModelVersion
|
3
|
+
# A meta model which stores additional data to each new version (could be used for audits).
|
4
|
+
class Meta
|
5
|
+
include MongoMapper::EmbeddedDocument
|
6
|
+
extend Historical::MongoMapperEnhancements
|
7
|
+
|
8
|
+
key :created_at, Time
|
9
|
+
|
10
|
+
# Retrieve customized class definition for a record class (e.g. TopicMeta, MessageMeta)
|
11
|
+
# @return [Class]
|
12
|
+
def self.for_class(source_class)
|
13
|
+
Historical::Models::Pool.pooled(Historical::Models::Pool.pooled_name(source_class, self)) do
|
14
|
+
Class.new(self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Historical::Models
|
2
|
+
# @private
|
3
|
+
# cached classes are stored here
|
4
|
+
module Pool
|
5
|
+
@@class_pool = {}
|
6
|
+
mattr_accessor :class_pool
|
7
|
+
|
8
|
+
# Gets a class from the pool or sets it if the class couldn't be found.
|
9
|
+
def self.pooled(name)
|
10
|
+
class_pool[name] ||= begin
|
11
|
+
yield.tap do |cls|
|
12
|
+
const_set(name, cls)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Removes all stored classes from the pool
|
18
|
+
def self.clear!
|
19
|
+
class_pool.each do |k,v|
|
20
|
+
remove_const(k)
|
21
|
+
end
|
22
|
+
|
23
|
+
self.class_pool = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Generate unique classnames within the pool.
|
27
|
+
def self.pooled_name(specialized_for, parent)
|
28
|
+
"#{specialized_for.name.demodulize}#{parent.name.demodulize}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,5 +1,12 @@
|
|
1
1
|
module Historical
|
2
|
+
# Contains some helper methods for better MongoMapper integration.
|
2
3
|
module MongoMapperEnhancements
|
4
|
+
|
5
|
+
# Simple `belongs_to` relation to an ActiveRecord model.
|
6
|
+
#
|
7
|
+
# @param name The name of the relation.
|
8
|
+
# @option options [String] :class_name (name.classify) The class name of the model (if it can't be guessed from the name)
|
9
|
+
# @option options [true,false] :polymorphic (false) Will create an additional {name}_type key in the model.
|
3
10
|
def belongs_to_active_record(name, options = {})
|
4
11
|
ar_id_field = "#{name}_id"
|
5
12
|
ar_type_field = "#{name}_type"
|
@@ -8,6 +15,7 @@ module Historical
|
|
8
15
|
class_name = options.delete(:class_name)
|
9
16
|
model_class = nil
|
10
17
|
|
18
|
+
# classname
|
11
19
|
unless polymorphic
|
12
20
|
if class_name
|
13
21
|
model_class = class_name.is_a?(Class) ? class_name.name : class_name.classify
|
@@ -16,9 +24,11 @@ module Historical
|
|
16
24
|
end
|
17
25
|
end
|
18
26
|
|
27
|
+
# define the keys
|
19
28
|
key ar_id_field, Integer, options
|
20
29
|
key ar_type_field, String, options if polymorphic
|
21
30
|
|
31
|
+
# getter
|
22
32
|
define_method name do
|
23
33
|
if id = send(ar_id_field)
|
24
34
|
if polymorphic
|
@@ -32,6 +42,7 @@ module Historical
|
|
32
42
|
end
|
33
43
|
end
|
34
44
|
|
45
|
+
# setter
|
35
46
|
define_method "#{name}=" do |val|
|
36
47
|
id = type = nil
|
37
48
|
|
data/lib/historical/railtie.rb
CHANGED
@@ -2,9 +2,11 @@ require 'historical'
|
|
2
2
|
require 'rails'
|
3
3
|
|
4
4
|
module Historical
|
5
|
+
# The railtie to be loaded by Rails.
|
5
6
|
class Railtie < Rails::Railtie
|
6
|
-
|
7
|
-
ActiveRecord::Base.send(:extend, Historical::ActiveRecord)
|
7
|
+
initializer "historical.attach_to_active_record" do
|
8
|
+
::ActiveRecord::Base.send(:extend, Historical::ActiveRecord)
|
9
|
+
Historical.boot!
|
8
10
|
end
|
9
11
|
end
|
10
12
|
end
|
data/spec/historical_spec.rb
CHANGED
@@ -10,6 +10,34 @@ describe "A historical model" do
|
|
10
10
|
is_historical
|
11
11
|
end
|
12
12
|
|
13
|
+
context "when loading model from database without history" do
|
14
|
+
before :each do
|
15
|
+
Historical.boot!
|
16
|
+
@msg = Message.find(1)
|
17
|
+
@msg.history.destroy
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should call the creation-generator" do
|
21
|
+
o = Object.new
|
22
|
+
o.stub!(:created_at=).once.with(@msg.created_at)
|
23
|
+
o.stub!(:save!).once
|
24
|
+
|
25
|
+
@msg.should_receive(:spawn_version).once.with(:create).and_return(o)
|
26
|
+
@msg.history
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should auto-generate a creation" do
|
30
|
+
@msg.history.version_index.should == 0
|
31
|
+
@msg.history.creation.should_not be_nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should have the original timestamps" do
|
35
|
+
@msg.history.creation.tap do |e|
|
36
|
+
e.created_at.should == @msg.created_at
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
13
41
|
context "when created" do
|
14
42
|
it "should persist" do
|
15
43
|
msg = Message.new(:title => "Hello")
|
@@ -41,6 +69,8 @@ describe "A historical model" do
|
|
41
69
|
version_count = lambda { @msg.history.versions.count }
|
42
70
|
|
43
71
|
lambda do
|
72
|
+
@msg.should_not be_new_record
|
73
|
+
@msg.changes.should be_empty
|
44
74
|
@msg.save!
|
45
75
|
end.should_not change(version_count, :call)
|
46
76
|
end
|
@@ -78,7 +108,7 @@ describe "A historical model" do
|
|
78
108
|
grouped[:body].old_value.should == nil
|
79
109
|
|
80
110
|
grouped[:votes].new_value.should == 42
|
81
|
-
grouped[:votes].old_value.should ==
|
111
|
+
grouped[:votes].old_value.should == 0
|
82
112
|
|
83
113
|
grouped[:read].new_value.should == true
|
84
114
|
grouped[:read].old_value.should == false
|
@@ -123,13 +153,13 @@ describe "A historical model" do
|
|
123
153
|
|
124
154
|
it "should create attribute-diffs in update-diff" do
|
125
155
|
@msg.history.tap do |h|
|
126
|
-
|
127
|
-
|
156
|
+
diff = h.updates.first.diff
|
157
|
+
diff.changes.count.should == 1
|
128
158
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
159
|
+
diff.changes.first.tap do |attr_diff|
|
160
|
+
attr_diff.attribute.should == "title"
|
161
|
+
attr_diff.old_value.should == @first_title
|
162
|
+
attr_diff.new_value.should == @new_title
|
133
163
|
end
|
134
164
|
end
|
135
165
|
end
|
@@ -232,15 +262,15 @@ describe "A historical model" do
|
|
232
262
|
it "should not break handy queries for chained restorations" do
|
233
263
|
@message = Message.create(:title => "one")
|
234
264
|
@message.update_attributes(:title => "two").should be_true
|
235
|
-
@message.history.
|
265
|
+
@message.history.version_index.should == 1
|
236
266
|
@message.version.should == 1
|
237
267
|
|
238
268
|
previous = @message.history.restore(:previous)
|
239
|
-
previous.history.
|
269
|
+
previous.history.version_index.should == 0
|
240
270
|
previous.version.should == 0
|
241
271
|
|
242
272
|
identity = previous.history.restore(:next)
|
243
|
-
identity.history.
|
273
|
+
identity.history.version_index.should == 1
|
244
274
|
identity.version.should == 1
|
245
275
|
|
246
276
|
identity.should_not be_nil
|
@@ -254,26 +284,30 @@ describe "A historical model" do
|
|
254
284
|
cattr_accessor :current_user
|
255
285
|
|
256
286
|
extend Historical::ActiveRecord
|
287
|
+
|
257
288
|
is_historical do
|
258
|
-
|
289
|
+
meta do
|
290
|
+
belongs_to_active_record :author, :required => true, :class_name => "User"
|
291
|
+
end
|
259
292
|
|
260
|
-
|
261
|
-
|
293
|
+
callback do |version|
|
294
|
+
version.meta.author = AuditedMessage.current_user
|
262
295
|
end
|
263
296
|
end
|
264
297
|
end
|
265
298
|
|
266
299
|
before :each do
|
267
300
|
AuditedMessage.current_user = nil
|
301
|
+
Historical.boot!
|
268
302
|
end
|
269
303
|
|
270
|
-
it "should create custom keys on
|
304
|
+
it "should create custom keys on ModelVersion::Diffs" do
|
271
305
|
user = User.create(:name => "Jane Doe")
|
272
306
|
AuditedMessage.current_user = user
|
273
307
|
|
274
308
|
msg = AuditedMessage.create(:title => "one")
|
275
309
|
|
276
|
-
author = msg.history.versions.first.
|
310
|
+
author = msg.history.versions.first.meta.author
|
277
311
|
author.should == user
|
278
312
|
author.id.should == user.id
|
279
313
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -6,8 +6,9 @@ require 'spec/autorun'
|
|
6
6
|
|
7
7
|
require 'rubygems'
|
8
8
|
|
9
|
-
gem "rails", "=
|
9
|
+
gem "rails", "= 3.0.0"
|
10
10
|
|
11
|
+
require 'ruby-debug'
|
11
12
|
require 'active_support'
|
12
13
|
require 'active_support/test_case'
|
13
14
|
require 'active_record'
|
@@ -24,7 +25,7 @@ ActiveRecord::Base.silence do
|
|
24
25
|
create_table :messages do |t|
|
25
26
|
t.string :title
|
26
27
|
t.text :body
|
27
|
-
t.integer :votes
|
28
|
+
t.integer :votes, :default => 0
|
28
29
|
t.datetime :published_at
|
29
30
|
t.date :stamped_on
|
30
31
|
t.decimal :donated, :precision => 10, :scale => 2
|
@@ -36,6 +37,12 @@ ActiveRecord::Base.silence do
|
|
36
37
|
t.string :name
|
37
38
|
t.timestamps
|
38
39
|
end
|
40
|
+
|
41
|
+
execute "
|
42
|
+
INSERT INTO
|
43
|
+
messages (id, title, body, created_at, updated_at)
|
44
|
+
VALUES
|
45
|
+
(1, 'existed already', 'hi', '2010-01-01 00:00:00', '2010-01-01 00:00:00')"
|
39
46
|
end
|
40
47
|
end
|
41
48
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: historical
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.2.
|
9
|
+
- 2
|
10
|
+
version: 0.2.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Marcel Jackwerth
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-08
|
18
|
+
date: 2010-10-08 00:00:00 +02:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -42,20 +42,23 @@ extensions: []
|
|
42
42
|
|
43
43
|
extra_rdoc_files:
|
44
44
|
- LICENSE
|
45
|
-
- README.
|
45
|
+
- README.markdown
|
46
46
|
files:
|
47
47
|
- .gitignore
|
48
48
|
- LICENSE
|
49
|
-
- README.
|
49
|
+
- README.markdown
|
50
50
|
- Rakefile
|
51
51
|
- VERSION
|
52
52
|
- historical.gemspec
|
53
53
|
- lib/historical.rb
|
54
54
|
- lib/historical/active_record.rb
|
55
|
+
- lib/historical/class_builder.rb
|
55
56
|
- lib/historical/model_history.rb
|
56
57
|
- lib/historical/models/attribute_diff.rb
|
57
|
-
- lib/historical/models/model_diff.rb
|
58
58
|
- lib/historical/models/model_version.rb
|
59
|
+
- lib/historical/models/model_version/diff.rb
|
60
|
+
- lib/historical/models/model_version/meta.rb
|
61
|
+
- lib/historical/models/pool.rb
|
59
62
|
- lib/historical/mongo_mapper_enhancements.rb
|
60
63
|
- lib/historical/railtie.rb
|
61
64
|
- rails/init.rb
|
data/README.rdoc
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
= historical2
|
2
|
-
|
3
|
-
Description goes here.
|
4
|
-
|
5
|
-
== Note on Patches/Pull Requests
|
6
|
-
|
7
|
-
* Fork the project.
|
8
|
-
* Make your feature addition or bug fix.
|
9
|
-
* Add tests for it. This is important so I don't break it in a
|
10
|
-
future version unintentionally.
|
11
|
-
* Commit, do not mess with rakefile, version, or history.
|
12
|
-
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
-
* Send me a pull request. Bonus points for topic branches.
|
14
|
-
|
15
|
-
== Copyright
|
16
|
-
|
17
|
-
Copyright (c) 2010 Marcel Jackwerth. See LICENSE for details.
|
@@ -1,90 +0,0 @@
|
|
1
|
-
module Historical::Models
|
2
|
-
class ModelDiff
|
3
|
-
include MongoMapper::EmbeddedDocument
|
4
|
-
extend Historical::MongoMapperEnhancements
|
5
|
-
class_inheritable_accessor :historical_callbacks
|
6
|
-
|
7
|
-
validates_associated :changes
|
8
|
-
|
9
|
-
key :_type, String
|
10
|
-
key :diff_type, String, :required => true
|
11
|
-
many :changes, :class_name => "Historical::Models::AttributeDiff"
|
12
|
-
|
13
|
-
delegate :creation?, :update?, :to => :diff_type_inquirer
|
14
|
-
|
15
|
-
def record
|
16
|
-
new_version.record
|
17
|
-
end
|
18
|
-
|
19
|
-
def new_version
|
20
|
-
@parent || _parent_document
|
21
|
-
end
|
22
|
-
|
23
|
-
def old_version
|
24
|
-
new_version.previous
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.from_versions(from, to)
|
28
|
-
return from_creation(to) if from.nil?
|
29
|
-
|
30
|
-
generate_from_version(from, 'update').tap do |d|
|
31
|
-
from.record.attribute_names.each do |attr_name|
|
32
|
-
attr = attr_name.to_sym
|
33
|
-
next if Historical::IGNORED_ATTRIBUTES.include? attr
|
34
|
-
|
35
|
-
old_value, new_value = from[attr], to[attr]
|
36
|
-
|
37
|
-
Historical::Models::AttributeDiff.specialized_for(d, attr).new.tap do |ad|
|
38
|
-
ad.attribute_type = Historical::Models::AttributeDiff.detect_attribute_type(d, attr)
|
39
|
-
ad.parent = d
|
40
|
-
ad.old_value = old_value
|
41
|
-
ad.new_value = new_value
|
42
|
-
ad.attribute = attr.to_s
|
43
|
-
d.changes << ad
|
44
|
-
end if old_value != new_value
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.from_creation(to)
|
50
|
-
generate_from_version(to)
|
51
|
-
end
|
52
|
-
|
53
|
-
protected
|
54
|
-
|
55
|
-
def diff_type_inquirer
|
56
|
-
ActiveSupport::StringInquirer.new(diff_type)
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.historical_callback(&block)
|
60
|
-
raise "no block given" unless block_given?
|
61
|
-
|
62
|
-
self.historical_callbacks ||= []
|
63
|
-
self.historical_callbacks << block
|
64
|
-
end
|
65
|
-
|
66
|
-
|
67
|
-
def self.generate_from_version(version, type = 'creation')
|
68
|
-
for_class(version.record.class).new.tap do |d|
|
69
|
-
d.diff_type = type
|
70
|
-
d.instance_variable_set :@parent, version
|
71
|
-
|
72
|
-
if cbs = d.class.historical_callbacks
|
73
|
-
cbs.each do |c|
|
74
|
-
c.call(d)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def self.for_class(source_class)
|
81
|
-
Historical::Models::Pool.pooled(Historical::Models::Pool.pooled_name(source_class, self)) do
|
82
|
-
Class.new(self).tap do |cls|
|
83
|
-
source_class.historical_customizations.each do |customization|
|
84
|
-
cls.instance_eval(&customization)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|