changeling 0.0.1
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 +17 -0
- data/.rvmrc +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +139 -0
- data/Rakefile +14 -0
- data/changeling.gemspec +34 -0
- data/lib/changeling.rb +12 -0
- data/lib/changeling/models/logling.rb +80 -0
- data/lib/changeling/probeling.rb +11 -0
- data/lib/changeling/trackling.rb +13 -0
- data/lib/changeling/version.rb +3 -0
- data/spec/config/mongoid.yml +6 -0
- data/spec/fixtures/models/mongoid/blog_post.rb +10 -0
- data/spec/fixtures/models/mongoid/blog_post_no_timestamp.rb +9 -0
- data/spec/lib/changeling/models/logling_spec.rb +203 -0
- data/spec/lib/changeling/probeling_spec.rb +55 -0
- data/spec/lib/changeling/trackling_spec.rb +37 -0
- data/spec/spec_helper.rb +63 -0
- metadata +150 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Howard Huang
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# Changeling [![Build Status][travis-image]][travis-link]
|
2
|
+
|
3
|
+
[travis-image]: https://secure.travis-ci.org/hahuang65/Changeling.png?branch=master
|
4
|
+
[travis-link]: http://travis-ci.org/hahuang65/Changeling
|
5
|
+
[travis-home]: http://travis-ci.org/
|
6
|
+
|
7
|
+
A flexible and lightweight object change tracking system.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'changeling'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
```sh
|
20
|
+
$ bundle
|
21
|
+
```
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
```sh
|
26
|
+
$ gem install changeling
|
27
|
+
```
|
28
|
+
|
29
|
+
## Requirements
|
30
|
+
|
31
|
+
* Redis (Tested on 2.4.x)
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
Include the Trackling module for any class you want to keep track of:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
class Post
|
39
|
+
include Changeling::Trackling
|
40
|
+
|
41
|
+
# Model logic here...
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
That's it! Including the module will silently keep track of any changes made to objects of this class.
|
46
|
+
For example:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
@post = Post.first
|
50
|
+
@post.title
|
51
|
+
=> 'Old Title'
|
52
|
+
@post.title = 'New Title'
|
53
|
+
@post.save
|
54
|
+
```
|
55
|
+
|
56
|
+
This code will save a history that represents that the title for this post has been changed.
|
57
|
+
|
58
|
+
If you wish to see what has been logged, include the Probeling module:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
class Post
|
62
|
+
include Changeling::Trackling
|
63
|
+
include Changeling::Probeling
|
64
|
+
|
65
|
+
# Model logic here...
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
With Probeling, you can check out the changes that have been made! They're stored in the order that the changes are made.
|
70
|
+
You can access the up to the last 10 changes simply by calling
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
@post.history
|
74
|
+
```
|
75
|
+
|
76
|
+
You can access a different number of records by passing in a number to the .history method:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
# Will automatically handle if there are less than the number of histories requested.
|
80
|
+
@post.history(50)
|
81
|
+
```
|
82
|
+
|
83
|
+
Access all of an objects history:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
@post.all_history
|
87
|
+
```
|
88
|
+
|
89
|
+
Properties of Loglings (history objects):
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
log = @post.history.first
|
93
|
+
|
94
|
+
log.klass # class of the object that the Logling is tracking.
|
95
|
+
=> "posts"
|
96
|
+
|
97
|
+
log.object_id # the ID of the object that the Logling is tracking.
|
98
|
+
=> "1"
|
99
|
+
|
100
|
+
log.before # what the before state of the object was.
|
101
|
+
=> {"title" => "Old Title"}
|
102
|
+
|
103
|
+
log.after # what the after state of the object is.
|
104
|
+
=> {"title" => "New Title"}
|
105
|
+
|
106
|
+
log.modifications # what changes were made to the object that this Logling recorded. Basically a roll up of the .before and .after methods.
|
107
|
+
=> {"title" => ["Old Title", "New Title"]}
|
108
|
+
|
109
|
+
log.changed_at # what time these changes were made.
|
110
|
+
=> Sat, 08 Sep 2012 10:21:46 UTC +00:00
|
111
|
+
|
112
|
+
log.as_json # JSON representation of the changes.
|
113
|
+
=> {:modifications=>{"title" => ["Old Title", "New Title"], :changed_at => Sat, 08 Sep 2012 10:21:46 UTC +00:00}
|
114
|
+
```
|
115
|
+
|
116
|
+
## Testing
|
117
|
+
|
118
|
+
This library is tested using [Travis][travis-home], where it is tested
|
119
|
+
against the following interpreters (with corresponding ORM/ODMs) and datastores:
|
120
|
+
|
121
|
+
* MRI 1.9.2 (Mongoid 2.4.1, ActiveRecord 3.1.3)
|
122
|
+
* MRI 1.9.3 (Mongoid 3.0.3, ActiveRecord 3.2.7)
|
123
|
+
* Redis 2.4.x
|
124
|
+
|
125
|
+
## Contributing
|
126
|
+
|
127
|
+
1. Fork it
|
128
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
129
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
130
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
131
|
+
5. Create new Pull Request
|
132
|
+
|
133
|
+
## TODO
|
134
|
+
|
135
|
+
* Restore state from a Logling
|
136
|
+
* Use Mongo / SQL as a datastore rather than Redis (not sure about this one yet.)
|
137
|
+
* Sinatra app to monitor changes as they happen in real-time
|
138
|
+
* Analytics for changes
|
139
|
+
* Much more...
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new("spec") do |spec|
|
6
|
+
spec.pattern = "spec/**/*_spec.rb"
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new('spec:progress') do |spec|
|
10
|
+
spec.rspec_opts = %w(--format progress)
|
11
|
+
spec.pattern = "spec/**/*_spec.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
task :default => :spec
|
data/changeling.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/changeling/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Howard Huang"]
|
6
|
+
gem.email = ["hahuang65@gmail.com"]
|
7
|
+
gem.description = %q{A simple, yet flexible solution to tracking changes made to objects in your database.}
|
8
|
+
gem.summary = %q{Object change-logger}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "changeling"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Changeling::VERSION
|
17
|
+
|
18
|
+
# Dependencies
|
19
|
+
gem.add_dependency "redis"
|
20
|
+
|
21
|
+
# Development Dependencies
|
22
|
+
case RUBY_VERSION
|
23
|
+
when "1.9.2"
|
24
|
+
gem.add_development_dependency "mongoid", "2.4.1"
|
25
|
+
gem.add_development_dependency "activerecord", "3.1.3"
|
26
|
+
when "1.9.3"
|
27
|
+
gem.add_development_dependency "mongoid", "3.0.3"
|
28
|
+
gem.add_development_dependency "activerecord", "3.2.7"
|
29
|
+
end
|
30
|
+
gem.add_development_dependency "rake"
|
31
|
+
gem.add_development_dependency "rspec"
|
32
|
+
gem.add_development_dependency "bson_ext"
|
33
|
+
gem.add_development_dependency "database_cleaner"
|
34
|
+
end
|
data/lib/changeling.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'redis'
|
3
|
+
require "changeling/version"
|
4
|
+
|
5
|
+
module Changeling
|
6
|
+
autoload :Trackling, 'changeling/trackling'
|
7
|
+
autoload :Probeling, 'changeling/probeling'
|
8
|
+
|
9
|
+
module Models
|
10
|
+
autoload :Logling, 'changeling/models/logling'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Changeling
|
2
|
+
module Models
|
3
|
+
class Logling
|
4
|
+
attr_accessor :klass, :object_id, :modifications, :before, :after, :changed_at
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def create(object, changes)
|
8
|
+
logling = self.new(object, changes)
|
9
|
+
logling.save
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_changes(changes)
|
13
|
+
before = {}
|
14
|
+
after = {}
|
15
|
+
|
16
|
+
changes.each_pair do |attr, values|
|
17
|
+
before[attr] = values[0]
|
18
|
+
after[attr] = values[1]
|
19
|
+
end
|
20
|
+
|
21
|
+
[before, after]
|
22
|
+
end
|
23
|
+
|
24
|
+
def redis
|
25
|
+
@redis ||= Redis.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def redis_key(klass, object_id)
|
29
|
+
"changeling::#{klass}::#{object_id}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def records_for(object, length = nil)
|
33
|
+
key = self.redis_key(object.class.to_s.underscore.pluralize, object.id.to_s)
|
34
|
+
length ||= self.redis.llen(key)
|
35
|
+
|
36
|
+
results = self.redis.lrange(key, 0, length).map { |value| self.new(object, JSON.parse(value)['modifications']) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def as_json
|
41
|
+
{
|
42
|
+
:modifications => self.modifications,
|
43
|
+
:changed_at => self.changed_at
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(object, changes)
|
48
|
+
# Remove updated_at field.
|
49
|
+
changes.delete("updated_at")
|
50
|
+
|
51
|
+
self.klass = object.class.to_s.underscore.pluralize
|
52
|
+
self.object_id = object.id.to_s
|
53
|
+
self.modifications = changes
|
54
|
+
|
55
|
+
self.before, self.after = Logling.parse_changes(changes)
|
56
|
+
|
57
|
+
if object.respond_to?(:updated_at)
|
58
|
+
self.changed_at = object.updated_at
|
59
|
+
else
|
60
|
+
self.changed_at = Time.now
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def redis_key
|
65
|
+
Logling.redis_key(self.klass, self.object_id)
|
66
|
+
end
|
67
|
+
|
68
|
+
def save
|
69
|
+
key = self.redis_key
|
70
|
+
value = self.serialize
|
71
|
+
|
72
|
+
Logling.redis.lpush(key, value)
|
73
|
+
end
|
74
|
+
|
75
|
+
def serialize
|
76
|
+
self.as_json.to_json
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require (File.expand_path('../../../../spec_helper', __FILE__))
|
2
|
+
|
3
|
+
describe Changeling::Models::Logling do
|
4
|
+
before(:all) do
|
5
|
+
@klass = Changeling::Models::Logling
|
6
|
+
end
|
7
|
+
|
8
|
+
# .models is defined in spec_helper.
|
9
|
+
models.each_pair do |model, args|
|
10
|
+
puts "Testing #{model} now."
|
11
|
+
|
12
|
+
before(:each) do
|
13
|
+
@object = model.new(args[:options])
|
14
|
+
@changes = args[:changes]
|
15
|
+
|
16
|
+
@logling = @klass.new(@object, @changes)
|
17
|
+
end
|
18
|
+
|
19
|
+
context "Class Methods" do
|
20
|
+
describe ".create" do
|
21
|
+
before(:each) do
|
22
|
+
@object.stub(:changes).and_return(@changes)
|
23
|
+
|
24
|
+
@klass.should_receive(:new).with(@object, @changes).and_return(@logling)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should call new with it's parameters then save the initialized logling" do
|
28
|
+
@logling.should_receive(:save)
|
29
|
+
|
30
|
+
@klass.create(@object, @changes)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe ".new" do
|
35
|
+
before(:each) do
|
36
|
+
@before, @after = @klass.parse_changes(@changes)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should set klass as the pluralized version of the class name" do
|
40
|
+
@logling.klass.should == @object.class.to_s.underscore.pluralize
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should set object_id as the stringified object's ID" do
|
44
|
+
@logling.object_id.should == @object.id.to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should set the modifications as the incoming changes parameter" do
|
48
|
+
@logling.modifications.should == @changes
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should set before and after based on .parse_changes" do
|
52
|
+
@logling.before.should == @before
|
53
|
+
@logling.after.should == @after
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should set changed_at to the object's time of update if the object responds to the updated_at method" do
|
57
|
+
@object.should_receive(:respond_to?).with(:updated_at).and_return(true)
|
58
|
+
|
59
|
+
# Setting up a variable to prevent test flakiness from passing time.
|
60
|
+
time = Time.now
|
61
|
+
@object.stub(:updated_at).and_return(time)
|
62
|
+
|
63
|
+
# Create a new logling to trigger the initialize method
|
64
|
+
@logling = @klass.new(@object, @changes)
|
65
|
+
@logling.changed_at.should == @object.updated_at
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should set changed_at to the current time if the object doesn't respond to updated_at" do
|
69
|
+
@object.should_receive(:respond_to?).with(:updated_at).and_return(false)
|
70
|
+
|
71
|
+
# Setting up a variable to prevent test flakiness from passing time.
|
72
|
+
time = Time.now
|
73
|
+
Time.stub(:now).and_return(time)
|
74
|
+
|
75
|
+
# Create a new logling to trigger the initialize method
|
76
|
+
@logling = @klass.new(@object, @changes)
|
77
|
+
@logling.changed_at.should == time
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe ".parse_changes" do
|
82
|
+
before(:each) do
|
83
|
+
@object.save!
|
84
|
+
|
85
|
+
@before = @object.attributes.select { |attr| @changes.keys.include?(attr) }
|
86
|
+
|
87
|
+
@changes.each_pair do |k, v|
|
88
|
+
@object.send("#{k}=", v[1])
|
89
|
+
end
|
90
|
+
|
91
|
+
@after = @object.attributes.select { |attr| @changes.keys.include?(attr) }
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should correctly match the before and after states of the object" do
|
95
|
+
@klass.parse_changes(@object.changes).should == [@before, @after]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe ".redis_key" do
|
100
|
+
it "should consist of 3 parts, changeling::model_name::object_id" do
|
101
|
+
@klass.redis_key(@logling.klass, @logling.object_id).should == "changeling::#{@object.class.to_s.underscore.pluralize}::#{@object.id.to_s}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe ".records_for" do
|
106
|
+
before(:each) do
|
107
|
+
@klass.stub(:redis).and_return($redis)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should get a redis_key" do
|
111
|
+
@key = @logling.redis_key
|
112
|
+
@klass.should_receive(:redis_key).with(@logling.klass, @logling.object_id).and_return(@key)
|
113
|
+
@klass.records_for(@object)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should find the length of the Redis list" do
|
117
|
+
@key = @logling.redis_key
|
118
|
+
@klass.stub(:redis_key).and_return(@key)
|
119
|
+
$redis.should_receive(:llen).with(@key)
|
120
|
+
@klass.records_for(@object)
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should not find the length of the Redis list if length option is passed" do
|
124
|
+
@key = @logling.redis_key
|
125
|
+
@klass.stub(:redis_key).and_return(@key)
|
126
|
+
$redis.should_not_receive(:llen).with(@key)
|
127
|
+
@klass.records_for(@object, 10)
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should find all entries in the Redis list" do
|
131
|
+
@key = @logling.redis_key
|
132
|
+
@length = 100
|
133
|
+
@klass.stub(:redis_key).and_return(@key)
|
134
|
+
$redis.stub(:llen).and_return(@length)
|
135
|
+
$redis.should_receive(:lrange).with(@key, 0, @length).and_return([])
|
136
|
+
@klass.records_for(@object)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should find the specified amount of entries in the Redis list if length option is passed" do
|
140
|
+
@key = @logling.redis_key
|
141
|
+
@klass.stub(:redis_key).and_return(@key)
|
142
|
+
$redis.should_receive(:lrange).with(@key, 0, 5).and_return([])
|
143
|
+
@klass.records_for(@object, 5)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "Instance Methods" do
|
149
|
+
describe ".as_json" do
|
150
|
+
it "should include the object's modifications attribute" do
|
151
|
+
@logling.should_receive(:modifications)
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should include the object's changed_at attribute" do
|
155
|
+
@logling.should_receive(:changed_at)
|
156
|
+
end
|
157
|
+
|
158
|
+
after(:each) do
|
159
|
+
@logling.as_json
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe ".redis_key" do
|
164
|
+
it "should pass in it's klass and object_id" do
|
165
|
+
@klass.should_receive(:redis_key).with(@logling.klass, @logling.object_id)
|
166
|
+
@logling.redis_key
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe ".save" do
|
171
|
+
before(:each) do
|
172
|
+
@klass.stub(:redis).and_return($redis)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should generate a key for storing in Redis" do
|
176
|
+
@logling.should_receive(:redis_key)
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should serialize the logling" do
|
180
|
+
@logling.should_receive(:serialize)
|
181
|
+
end
|
182
|
+
|
183
|
+
it "should push the serialized object into Redis" do
|
184
|
+
@key = 1
|
185
|
+
@value = 2
|
186
|
+
@logling.stub(:redis_key).and_return(@key)
|
187
|
+
@logling.stub(:serialize).and_return(@value)
|
188
|
+
$redis.should_receive(:lpush).with(@key, @value)
|
189
|
+
end
|
190
|
+
|
191
|
+
after(:each) do
|
192
|
+
@logling.save
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe ".serialize" do
|
197
|
+
it "should JSON-ify the as_json object" do
|
198
|
+
@logling.serialize.should == @logling.as_json.to_json
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require (File.expand_path('../../../spec_helper', __FILE__))
|
2
|
+
|
3
|
+
describe Changeling::Probeling do
|
4
|
+
before(:all) do
|
5
|
+
@klass = Changeling::Models::Logling
|
6
|
+
end
|
7
|
+
|
8
|
+
models.each_pair do |model, args|
|
9
|
+
before(:each) do
|
10
|
+
@object = model.new(args[:options])
|
11
|
+
@object.save!
|
12
|
+
|
13
|
+
args[:changes].each do |field, values|
|
14
|
+
values.reverse.each do |value|
|
15
|
+
@object.send("#{field}=", value)
|
16
|
+
@object.save!
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
@object.all_history.count.should == 4
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe ".all_history" do
|
25
|
+
it "should query Logling with it's pluralized class name, and it's own ID" do
|
26
|
+
@klass.should_receive(:records_for).with(@object)
|
27
|
+
@object.all_history
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should return an array of Loglings" do
|
31
|
+
@object.all_history.map(&:class).uniq.should == [@klass]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe ".history" do
|
36
|
+
it "should query Logling with it's pluralized class name, and it's own ID, and a default number of loglings to return" do
|
37
|
+
@klass.should_receive(:records_for).with(@object, 10)
|
38
|
+
@object.history
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should take an argument that overrides the default" do
|
42
|
+
@klass.should_receive(:records_for).with(@object, 5)
|
43
|
+
@object.history(5)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should handle non-integer arguments" do
|
47
|
+
@klass.should_receive(:records_for).with(@object, 5)
|
48
|
+
@object.history("5")
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should not error out if the record count desired is more than the total number of loglings" do
|
52
|
+
@object.history(20).count.should == 4
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require (File.expand_path('../../../spec_helper', __FILE__))
|
2
|
+
|
3
|
+
describe Changeling::Trackling do
|
4
|
+
before(:all) do
|
5
|
+
@klass = Changeling::Models::Logling
|
6
|
+
end
|
7
|
+
|
8
|
+
models.each_pair do |model, args|
|
9
|
+
before(:each) do
|
10
|
+
@object = model.new(args[:options])
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "callbacks" do
|
14
|
+
before(:each) do
|
15
|
+
@changes = args[:changes]
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should not create a logling when doing the initial save of a new object" do
|
19
|
+
@klass.should_not_receive(:create)
|
20
|
+
@object.save!
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should create a logling with the changed attributes of an object when it is saved" do
|
24
|
+
# Persist object to DB so we can update it.
|
25
|
+
@object.save!
|
26
|
+
|
27
|
+
@klass.should_receive(:create).with(@object, @changes)
|
28
|
+
|
29
|
+
@changes.each_pair do |k, v|
|
30
|
+
@object.send("#{k}=", v[1])
|
31
|
+
end
|
32
|
+
|
33
|
+
@object.save!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require(File.expand_path('../../lib/changeling', __FILE__))
|
2
|
+
require 'mongoid'
|
3
|
+
require 'redis'
|
4
|
+
require 'database_cleaner'
|
5
|
+
|
6
|
+
# Fixtures
|
7
|
+
Dir[File.dirname(__FILE__) + "/fixtures/**/*.rb"].each { |file| require file }
|
8
|
+
|
9
|
+
# Pre 3.0 Mongoid doesn't have this constant defined...
|
10
|
+
if defined?(Mongoid::VERSION)
|
11
|
+
# Mongoid 3.0.3
|
12
|
+
Mongoid.load!(File.dirname(__FILE__) + "/config/mongoid.yml", :test)
|
13
|
+
else
|
14
|
+
# Mongoid 2.4.1
|
15
|
+
# Didn't use Mongoid.load! here since 2.4.1 doesn't allow passing in an environment to use.
|
16
|
+
Mongoid.database = Mongo::Connection.new('localhost','27017').db('changeling_test')
|
17
|
+
end
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
config.mock_with :rspec
|
21
|
+
|
22
|
+
config.before(:suite) do
|
23
|
+
DatabaseCleaner[:mongoid].strategy = :truncation
|
24
|
+
$redis = Redis.new(:db => 1)
|
25
|
+
end
|
26
|
+
|
27
|
+
config.before(:each) do
|
28
|
+
$redis.flushdb
|
29
|
+
DatabaseCleaner.start
|
30
|
+
end
|
31
|
+
|
32
|
+
config.after(:each) do
|
33
|
+
DatabaseCleaner.clean
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def models
|
38
|
+
@models = {
|
39
|
+
BlogPost => {
|
40
|
+
:options => {
|
41
|
+
:title => "Changeling",
|
42
|
+
:content => "Something about Changeling",
|
43
|
+
:public => false
|
44
|
+
},
|
45
|
+
:changes => {
|
46
|
+
"public" => [false, true],
|
47
|
+
"content" => ["Something about Changeling", "Content about Changeling"]
|
48
|
+
}
|
49
|
+
},
|
50
|
+
|
51
|
+
BlogPostNoTimestamp => {
|
52
|
+
:options => {
|
53
|
+
:title => "Changeling",
|
54
|
+
:content => "Something about Changeling",
|
55
|
+
:public => false
|
56
|
+
},
|
57
|
+
:changes => {
|
58
|
+
"public" => [false, true],
|
59
|
+
"content" => ["Something about Changeling", "Content about Changeling"]
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
63
|
+
end
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: changeling
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Howard Huang
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: &70287887029460 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70287887029460
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mongoid
|
27
|
+
requirement: &70287887028580 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - =
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.0.3
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70287887028580
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: activerecord
|
38
|
+
requirement: &70287887028040 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - =
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 3.2.7
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70287887028040
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &70287887027640 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70287887027640
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rspec
|
60
|
+
requirement: &70287887027080 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70287887027080
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bson_ext
|
71
|
+
requirement: &70287887026620 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70287887026620
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: database_cleaner
|
82
|
+
requirement: &70287887026120 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70287887026120
|
91
|
+
description: A simple, yet flexible solution to tracking changes made to objects in
|
92
|
+
your database.
|
93
|
+
email:
|
94
|
+
- hahuang65@gmail.com
|
95
|
+
executables: []
|
96
|
+
extensions: []
|
97
|
+
extra_rdoc_files: []
|
98
|
+
files:
|
99
|
+
- .gitignore
|
100
|
+
- .rvmrc
|
101
|
+
- .travis.yml
|
102
|
+
- Gemfile
|
103
|
+
- LICENSE
|
104
|
+
- README.md
|
105
|
+
- Rakefile
|
106
|
+
- changeling.gemspec
|
107
|
+
- lib/changeling.rb
|
108
|
+
- lib/changeling/models/logling.rb
|
109
|
+
- lib/changeling/probeling.rb
|
110
|
+
- lib/changeling/trackling.rb
|
111
|
+
- lib/changeling/version.rb
|
112
|
+
- spec/config/mongoid.yml
|
113
|
+
- spec/fixtures/models/mongoid/blog_post.rb
|
114
|
+
- spec/fixtures/models/mongoid/blog_post_no_timestamp.rb
|
115
|
+
- spec/lib/changeling/models/logling_spec.rb
|
116
|
+
- spec/lib/changeling/probeling_spec.rb
|
117
|
+
- spec/lib/changeling/trackling_spec.rb
|
118
|
+
- spec/spec_helper.rb
|
119
|
+
homepage: ''
|
120
|
+
licenses: []
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ! '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
none: false
|
133
|
+
requirements:
|
134
|
+
- - ! '>='
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
requirements: []
|
138
|
+
rubyforge_project:
|
139
|
+
rubygems_version: 1.8.15
|
140
|
+
signing_key:
|
141
|
+
specification_version: 3
|
142
|
+
summary: Object change-logger
|
143
|
+
test_files:
|
144
|
+
- spec/config/mongoid.yml
|
145
|
+
- spec/fixtures/models/mongoid/blog_post.rb
|
146
|
+
- spec/fixtures/models/mongoid/blog_post_no_timestamp.rb
|
147
|
+
- spec/lib/changeling/models/logling_spec.rb
|
148
|
+
- spec/lib/changeling/probeling_spec.rb
|
149
|
+
- spec/lib/changeling/trackling_spec.rb
|
150
|
+
- spec/spec_helper.rb
|