mongoid-history 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.2@
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'easy_diff'
4
+ gem 'mongoid', '~> 2.0.0.rc.7'
5
+
6
+ group :development do
7
+ gem "rspec", "~> 2.3.0"
8
+ gem "yard", "~> 0.6.0"
9
+ gem "bundler", "~> 1.0.0"
10
+ gem "jeweler", "~> 1.5.2"
11
+ gem "rcov", ">= 0"
12
+ gem "reek", "~> 1.2.8"
13
+ gem "roodi", "~> 2.1.0"
14
+ gem "database_cleaner"
15
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Aaron Qian
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,101 @@
1
+ = mongoid-history
2
+
3
+ In frustration of Mongoid::Versioning, I created this plugin for tracking historical changes for any document, including embedded ones. It achieves this by storing all history tracks in a single collection that you define. (See Usage for more details) Embedded documents are referenced by storing an association path, which is an array of document_name and document_id fields starting from the top most parent document and down to the embedded document that should track history.
4
+
5
+ This plugin implements multi-user undo, which allows users to undo any history change in any order. Undoing a document also creates a new history track. This is great for auditing and preventing vandalism, but it is probably not suitable for use cases such as a wiki.
6
+
7
+ == Install
8
+
9
+ gem install mongoid-history
10
+
11
+ == Rails 3
12
+
13
+ In your Gemfile:
14
+
15
+ gem 'mongoid-history'
16
+
17
+ == Usage
18
+
19
+ Here is a quick example on how to use this plugin. For more details, please look at spec/integration/integration_spec.rb. It offers more detailed examples on how to use Mongoid::History.
20
+
21
+ # Create a new class for tracking all histories.
22
+ # Must include Mongoid::History::Tracker
23
+ class HistoryTracker
24
+ include Mongoid::History::Tracker
25
+ end
26
+
27
+ class Post
28
+ include Mongoid::Document
29
+ include Mongoid::Timestamps
30
+
31
+ # History tracking all Post Documents
32
+ # Note: Tracking will not work until #track_history is invoked
33
+ include Mongoid::History::Trackable
34
+
35
+ field :title
36
+ field :body
37
+ field :rating
38
+ embeds_many :comments
39
+
40
+ # Telling Mongoid::History how you want to track
41
+ track_history :on => [:title, :body], # I want to track title and body fields only. Default is :all
42
+ :modifier_field => :modifier, # Adds "referened_in :modifier" to track who made the change. Default is :modifier
43
+ :version_field => :version, # Adds "field :version, :type => Integer" to track current version. Default is :version
44
+ :track_create => false, # Do you want to track document creation? Default is false
45
+ end
46
+
47
+ class Comment
48
+ include Mongoid::Document
49
+ include Mongoid::Timestamps
50
+
51
+ # Declare that we want to track comments
52
+ include Mongoid::History::Trackable
53
+
54
+ field :title
55
+ field :body
56
+ embedded_in :post, :inverse_of => :comments
57
+
58
+ # Track title and body for all comments, scope it to post (the parent)
59
+ # Also track creation
60
+ track_history :on => [:title, :body], :scope => :post, :track_create => true
61
+ end
62
+
63
+ # The modifier can be specified as well
64
+ class User
65
+ include Mongoid::Document
66
+ include Mongoid::Timestamps
67
+
68
+ field :name
69
+ end
70
+
71
+ user = User.create(:name => "Aaron")
72
+ post = Post.create(:title => "Test", :body => "Post", :modifier => user)
73
+ comment = post.comments.create(:title => "test", :body => "comment", :modifier => user)
74
+ comment.history_tracks.count # should be 1
75
+
76
+ comment.update_attributes(:title => "Test 2")
77
+ comment.history_tracks.count # should be 2
78
+
79
+ track = comment.history_tracks.last
80
+
81
+ track.undo! # comment title should be "Test"
82
+
83
+ track.redo! # comment title should be "Test 2"
84
+
85
+
86
+
87
+ == Contributing to mongoid-history
88
+
89
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
90
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
91
+ * Fork the project
92
+ * Start a feature/bugfix branch
93
+ * Commit and push until you are happy with your contribution
94
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
95
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
96
+
97
+ == Copyright
98
+
99
+ Copyright (c) 2011 Aaron Qian. See LICENSE.txt for
100
+ further details.
101
+
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "mongoid-history"
16
+ gem.homepage = "http://github.com/aq1018/mongoid-history"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{ history tracking, auditing, undo, redo for mongoid}
19
+ gem.description = %Q{In frustration of Mongoid::Versioning, I created this plugin for tracking historical changes for any document, including embedded ones. It achieves this by storing all history tracks in a single collection that you define. (See Usage for more details) Embedded documents are referenced by storing an association path, which is an array of document_name and document_id fields starting from the top most parent document and down to the embedded document that should track history.
20
+
21
+ This plugin implements multi-user undo, which allows users to undo any history change in any order. Undoing a document also creates a new history track. This is great for auditing and preventing vandalism, but it is probably not suitable for use cases such as a wiki.}
22
+ gem.email = "aqian@attinteractive.com"
23
+ gem.authors = ["Aaron Qian"]
24
+ end
25
+ Jeweler::RubygemsDotOrgTasks.new
26
+
27
+ require 'rspec/core'
28
+ require 'rspec/core/rake_task'
29
+ RSpec::Core::RakeTask.new(:spec) do |spec|
30
+ spec.pattern = FileList['spec/**/*_spec.rb']
31
+ spec.rspec_opts = "--color --format progress"
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ spec.rcov_opts = "--exclude ~\/.rvm,spec"
38
+ end
39
+
40
+ require 'reek/rake/task'
41
+ Reek::Rake::Task.new do |t|
42
+ t.fail_on_error = true
43
+ t.verbose = false
44
+ t.source_files = 'lib/**/*.rb'
45
+ end
46
+
47
+ require 'roodi'
48
+ require 'roodi_task'
49
+ RoodiTask.new do |t|
50
+ t.verbose = false
51
+ end
52
+
53
+ task :default => :spec
54
+
55
+ require 'yard'
56
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,150 @@
1
+ module Mongoid::History
2
+ module Trackable
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def track_history(options={})
7
+ model_name = self.name.tableize.singularize.to_sym
8
+ default_options = {
9
+ :on => :all,
10
+ :except => [:created_at, :updated_at],
11
+ :modifier_field => :modifier,
12
+ :version_field => :version,
13
+ :scope => model_name,
14
+ :track_create => false
15
+ }
16
+
17
+ options = default_options.merge(options)
18
+
19
+ # normalize except fields
20
+ # manually ensure _id, id, version will not be tracked in history
21
+ options[:except] = [options[:except]] unless options[:except].is_a? Array
22
+ options[:except] << options[:version_field]
23
+ options[:except] << "#{options[:modifier_field]}_id".to_sym
24
+ options[:except] += [:_id, :id]
25
+ options[:except] = options[:except].map(&:to_s).flatten.compact.uniq
26
+ options[:except].map(&:to_s)
27
+
28
+ # normalize fields to track to either :all or an array of strings
29
+ if options[:on] != :all
30
+ options[:on] = [options[:on]] unless options[:on].is_a? Array
31
+ options[:on] = options[:on].map(&:to_s).flatten.uniq
32
+ end
33
+
34
+ field options[:version_field].to_sym, :type => Integer
35
+ referenced_in options[:modifier_field].to_sym, :class_name => Mongoid::History.modifer_class_name
36
+
37
+ include InstanceMethods
38
+ extend SingletonMethods
39
+
40
+ delegate :history_trackable_options, :to => 'self.class'
41
+
42
+ before_update :track_update
43
+ before_create :track_create if options[:track_create]
44
+
45
+
46
+ Mongoid::History.trackable_classes ||= []
47
+ Mongoid::History.trackable_classes << self
48
+ Mongoid::History.trackable_class_options ||= {}
49
+ Mongoid::History.trackable_class_options[model_name] = options
50
+ end
51
+
52
+ end
53
+
54
+ module InstanceMethods
55
+ def history_tracks
56
+ @history_tracks ||= Mongoid::History.tracker_class.where(:scope => history_trackable_options[:scope], :association_chain => triverse_association_chain)
57
+ end
58
+
59
+ private
60
+ def should_track_update?
61
+ !modified_attributes_for_update.blank?
62
+ end
63
+
64
+ def triverse_association_chain(node=self)
65
+ list = node._parent ? triverse_association_chain(node._parent) : []
66
+ list << { 'name' => node.class.name, 'id' => node.id }
67
+ list
68
+ end
69
+
70
+ def modified_attributes_for_update
71
+ @modified_attributes_for_update ||= if history_trackable_options[:on] == :all
72
+ changes
73
+ else
74
+ changes.reject do |k, v|
75
+ !history_trackable_options[:on].include?(k)
76
+ end.reject do |k, v|
77
+ history_trackable_options[:except].include?(k)
78
+ end
79
+ end
80
+ end
81
+
82
+ def modified_attributes_for_create
83
+ @modified_attributes_for_create ||= attributes.inject({}) do |h, pair|
84
+ k,v = pair
85
+ h[k] = [nil, v]
86
+ h
87
+ end.reject do |k, v|
88
+ history_trackable_options[:except].include?(k)
89
+ end
90
+ end
91
+
92
+ def history_tracker_attributes
93
+ return @history_tracker_attributes if @history_tracker_attributes
94
+
95
+ @history_tracker_attributes = {
96
+ :association_chain => triverse_association_chain,
97
+ :scope => history_trackable_options[:scope],
98
+ :modifier => send(history_trackable_options[:modifier_field])
99
+ }
100
+
101
+ original, modified = transform_changes((new_record? ? modified_attributes_for_create : modified_attributes_for_update))
102
+ @history_tracker_attributes[:original] = original
103
+ @history_tracker_attributes[:modified] = modified
104
+ @history_tracker_attributes
105
+ end
106
+
107
+ def track_update
108
+ return unless should_track_update?
109
+ current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
110
+ self.send("#{history_trackable_options[:version_field]}=", current_version)
111
+ Mongoid::History.tracker_class.create!(history_tracker_attributes.merge(:version => current_version))
112
+ clear_memoization
113
+ end
114
+
115
+ def track_create
116
+ current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
117
+ self.send("#{history_trackable_options[:version_field]}=", current_version)
118
+ Mongoid::History.tracker_class.create!(history_tracker_attributes.merge(:version => current_version))
119
+ clear_memoization
120
+ end
121
+
122
+ def clear_memoization
123
+ @history_tracker_attributes = nil
124
+ @modified_attributes_for_create = nil
125
+ @modified_attributes_for_update = nil
126
+ @history_tracks = nil
127
+ end
128
+
129
+ def transform_changes(changes)
130
+ original = {}
131
+ modified = {}
132
+ changes.each_pair do |k, v|
133
+ o, m = v
134
+ original[k] = o if o
135
+ modified[k] = m if m
136
+ end
137
+
138
+ return original.easy_diff modified
139
+ end
140
+
141
+ end
142
+
143
+ module SingletonMethods
144
+ def history_trackable_options
145
+ @history_trackable_options ||= Mongoid::History.trackable_class_options[self.name.tableize.singularize.to_sym]
146
+ end
147
+ end
148
+
149
+ end
150
+ end
@@ -0,0 +1,67 @@
1
+ module Mongoid::History
2
+ module Tracker
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include Mongoid::Document
7
+ include Mongoid::Timestamps
8
+
9
+ field :association_chain, :type => Array, :default => []
10
+ field :modified, :type => Hash
11
+ field :original, :type => Hash
12
+ field :version, :type => Integer
13
+ field :scope, :type => String
14
+ referenced_in :modifier, :class_name => Mongoid::History.modifer_class_name
15
+
16
+ Mongoid::History.tracker_class_name = self.name.tableize.singularize.to_sym
17
+ end
18
+
19
+
20
+ module ClassMethods
21
+ end
22
+
23
+ def undo!
24
+ undo_hash = affected.easy_unmerge(modified)
25
+ undo_hash.easy_merge!(original)
26
+ trackable.update_attributes!(undo_hash)
27
+ end
28
+
29
+ def redo!
30
+ redo_hash = affected.easy_unmerge(original)
31
+ redo_hash.easy_merge!(modified)
32
+ trackable.update_attributes!(redo_hash)
33
+ end
34
+
35
+ def trackable
36
+ @trackable ||= trackable_parents_and_trackable.last
37
+ end
38
+
39
+ def trackable_parents
40
+ @trackable_parents ||= trackable_parents_and_trackable[0, -1]
41
+ end
42
+
43
+ def affected
44
+ @affected ||= (modified.keys | original.keys).inject({}){ |h,k| h[k] = trackable.attributes[k]; h}
45
+ end
46
+
47
+ private
48
+ def trackable_parents_and_trackable
49
+ @trackable_parents_and_trackable ||= triverse_association_chain
50
+ end
51
+
52
+ def triverse_association_chain
53
+ chain = association_chain.dup
54
+ doc = nil
55
+ documents = []
56
+ begin
57
+ node = chain.shift
58
+ name = node['name']
59
+ col = doc.nil? ? name.classify.constantize : doc.send(name.tableize)
60
+ doc = col.where(:_id => node['id']).first
61
+ documents << doc
62
+ end while( !chain.empty? )
63
+ documents
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,12 @@
1
+ module Mongoid
2
+ module History
3
+ mattr_accessor :tracker_class_name
4
+ mattr_accessor :trackable_classes
5
+ mattr_accessor :trackable_class_options
6
+ mattr_accessor :modifer_class_name
7
+
8
+ def self.tracker_class
9
+ @tracker_class ||= tracker_class_name.to_s.classify.constantize
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/mongoid/history')
2
+ require File.expand_path(File.dirname(__FILE__) + '/mongoid/history/tracker')
3
+ require File.expand_path(File.dirname(__FILE__) + '/mongoid/history/trackable')
4
+
5
+ Mongoid::History.modifer_class_name = "User"
6
+ Mongoid::History.trackable_classes = []
7
+ Mongoid::History.trackable_class_options = {}
@@ -0,0 +1,91 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{mongoid-history}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Aaron Qian"]
12
+ s.date = %q{2011-03-04}
13
+ s.description = %q{In frustration of Mongoid::Versioning, I created this plugin for tracking historical changes for any document, including embedded ones. It achieves this by storing all history tracks in a single collection that you define. (See Usage for more details) Embedded documents are referenced by storing an association path, which is an array of document_name and document_id fields starting from the top most parent document and down to the embedded document that should track history.
14
+
15
+ This plugin implements multi-user undo, which allows users to undo any history change in any order. Undoing a document also creates a new history track. This is great for auditing and preventing vandalism, but it is probably not suitable for use cases such as a wiki.}
16
+ s.email = %q{aqian@attinteractive.com}
17
+ s.extra_rdoc_files = [
18
+ "LICENSE.txt",
19
+ "README.rdoc"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".rspec",
24
+ ".rvmrc",
25
+ "Gemfile",
26
+ "LICENSE.txt",
27
+ "README.rdoc",
28
+ "Rakefile",
29
+ "VERSION",
30
+ "lib/mongoid-history.rb",
31
+ "lib/mongoid/history.rb",
32
+ "lib/mongoid/history/trackable.rb",
33
+ "lib/mongoid/history/tracker.rb",
34
+ "mongoid-history.gemspec",
35
+ "spec/integration/integration_spec.rb",
36
+ "spec/spec_helper.rb",
37
+ "spec/trackable_spec.rb",
38
+ "spec/tracker_spec.rb"
39
+ ]
40
+ s.homepage = %q{http://github.com/aq1018/mongoid-history}
41
+ s.licenses = ["MIT"]
42
+ s.require_paths = ["lib"]
43
+ s.rubygems_version = %q{1.5.3}
44
+ s.summary = %q{history tracking, auditing, undo, redo for mongoid}
45
+ s.test_files = [
46
+ "spec/integration/integration_spec.rb",
47
+ "spec/spec_helper.rb",
48
+ "spec/trackable_spec.rb",
49
+ "spec/tracker_spec.rb"
50
+ ]
51
+
52
+ if s.respond_to? :specification_version then
53
+ s.specification_version = 3
54
+
55
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
56
+ s.add_runtime_dependency(%q<easy_diff>, [">= 0"])
57
+ s.add_runtime_dependency(%q<mongoid>, ["~> 2.0.0.rc.7"])
58
+ s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
59
+ s.add_development_dependency(%q<yard>, ["~> 0.6.0"])
60
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
61
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
62
+ s.add_development_dependency(%q<rcov>, [">= 0"])
63
+ s.add_development_dependency(%q<reek>, ["~> 1.2.8"])
64
+ s.add_development_dependency(%q<roodi>, ["~> 2.1.0"])
65
+ s.add_development_dependency(%q<database_cleaner>, [">= 0"])
66
+ else
67
+ s.add_dependency(%q<easy_diff>, [">= 0"])
68
+ s.add_dependency(%q<mongoid>, ["~> 2.0.0.rc.7"])
69
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
70
+ s.add_dependency(%q<yard>, ["~> 0.6.0"])
71
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
72
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
73
+ s.add_dependency(%q<rcov>, [">= 0"])
74
+ s.add_dependency(%q<reek>, ["~> 1.2.8"])
75
+ s.add_dependency(%q<roodi>, ["~> 2.1.0"])
76
+ s.add_dependency(%q<database_cleaner>, [">= 0"])
77
+ end
78
+ else
79
+ s.add_dependency(%q<easy_diff>, [">= 0"])
80
+ s.add_dependency(%q<mongoid>, ["~> 2.0.0.rc.7"])
81
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
82
+ s.add_dependency(%q<yard>, ["~> 0.6.0"])
83
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
84
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
85
+ s.add_dependency(%q<rcov>, [">= 0"])
86
+ s.add_dependency(%q<reek>, ["~> 1.2.8"])
87
+ s.add_dependency(%q<roodi>, ["~> 2.1.0"])
88
+ s.add_dependency(%q<database_cleaner>, [">= 0"])
89
+ end
90
+ end
91
+
@@ -0,0 +1,264 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Mongoid::History do
4
+ before :all do
5
+ class HistoryTracker
6
+ include Mongoid::History::Tracker
7
+ end
8
+
9
+ class Post
10
+ include Mongoid::Document
11
+ include Mongoid::Timestamps
12
+ include Mongoid::History::Trackable
13
+
14
+ field :title
15
+ field :body
16
+ field :rating
17
+ embeds_many :comments
18
+ track_history :on => [:title, :body]
19
+ end
20
+
21
+ class Comment
22
+ include Mongoid::Document
23
+ include Mongoid::Timestamps
24
+ include Mongoid::History::Trackable
25
+
26
+ field :title
27
+ field :body
28
+ embedded_in :post, :inverse_of => :comments
29
+ track_history :on => [:title, :body], :scope => :post, :track_create => true
30
+ end
31
+
32
+ class User
33
+ include Mongoid::Document
34
+ include Mongoid::Timestamps
35
+
36
+ field :name
37
+ end
38
+ end
39
+
40
+ before :each do
41
+ @user = User.create(:name => "Aaron")
42
+ @another_user = User.create(:name => "Another Guy")
43
+ @post = Post.create(:title => "Test", :body => "Post", :modifier => @user)
44
+ @comment = @post.comments.create(:title => "test", :body => "comment", :modifier => @user)
45
+ end
46
+
47
+ describe "track" do
48
+ describe "on creation" do
49
+ it "should have one history track in comment" do
50
+ @comment.history_tracks.count.should == 1
51
+ end
52
+
53
+ it "should assign title and body on modified" do
54
+ @comment.history_tracks.first.modified.should == {'title' => "test", 'body' => "comment"}
55
+ end
56
+
57
+ it "should not assign title and body on original" do
58
+ @comment.history_tracks.first.original.should == {}
59
+ end
60
+
61
+
62
+ it "should assign modifier" do
63
+ @comment.history_tracks.first.modifier.should == @user
64
+ end
65
+
66
+ it "should assigin version" do
67
+ @comment.history_tracks.first.version.should == 1
68
+ end
69
+
70
+ it "should assigin scope" do
71
+ @comment.history_tracks.first.scope == "Post"
72
+ end
73
+
74
+ it "should assigin association_chain" do
75
+ @comment.history_tracks.first.association_chain = [{:id => @post.id, :name => "Post"}, {:id => @comment.id, :name => "Comment"}]
76
+ end
77
+ end
78
+
79
+ describe "on update non-embedded" do
80
+ it "should create a history track if changed attributes match tracked attributes" do
81
+ lambda {
82
+ @post.update_attributes(:title => "Another Test")
83
+ }.should change(HistoryTracker, :count).by(1)
84
+ end
85
+
86
+ it "should not create a history track if changed attributes do not match tracked attributes" do
87
+ lambda {
88
+ @post.update_attributes(:rating => "untracked")
89
+ }.should change(HistoryTracker, :count).by(0)
90
+ end
91
+
92
+ it "should assign modified fields" do
93
+ @post.update_attributes(:title => "Another Test")
94
+ @post.history_tracks.first.modified.should == {
95
+ "title" => "Another Test"
96
+ }
97
+ end
98
+
99
+ it "should assign original fields" do
100
+ @post.update_attributes(:title => "Another Test")
101
+ @post.history_tracks.first.original.should == {
102
+ "title" => "Test"
103
+ }
104
+ end
105
+
106
+ it "should assign modifier" do
107
+ @post.update_attributes(:title => "Another Test")
108
+ @post.history_tracks.first.modifier.should == @user
109
+ end
110
+
111
+ it "should assigin version on history tracks" do
112
+ @post.update_attributes(:title => "Another Test")
113
+ @post.history_tracks.first.version.should == 1
114
+ end
115
+
116
+ it "should assigin version on post" do
117
+ @post.update_attributes(:title => "Another Test")
118
+ @post.version.should == 1
119
+ end
120
+
121
+ it "should assigin scope" do
122
+ @post.update_attributes(:title => "Another Test")
123
+ @post.history_tracks.first.scope == "Post"
124
+ end
125
+
126
+ it "should assigin association_chain" do
127
+ @post.update_attributes(:title => "Another Test")
128
+ @post.history_tracks.first.association_chain = [{:id => @post.id, :name => "Post"}]
129
+ end
130
+ end
131
+
132
+ describe "on update non-embedded twice" do
133
+ it "should assigin version on post" do
134
+ @post.update_attributes(:title => "Test2")
135
+ @post.update_attributes(:title => "Test3")
136
+ @post.version.should == 2
137
+ end
138
+
139
+ it "should create a history track if changed attributes match tracked attributes" do
140
+ lambda {
141
+ @post.update_attributes(:title => "Test2")
142
+ @post.update_attributes(:title => "Test3")
143
+ }.should change(HistoryTracker, :count).by(2)
144
+ end
145
+
146
+ it "should create a history track of version 2" do
147
+ @post.update_attributes(:title => "Test2")
148
+ @post.update_attributes(:title => "Test3")
149
+ @post.history_tracks.where(:version => 2).first.should_not be_nil
150
+ end
151
+
152
+ it "should assign modified fields" do
153
+ @post.update_attributes(:title => "Test2")
154
+ @post.update_attributes(:title => "Test3")
155
+ @post.history_tracks.where(:version => 2).first.modified.should == {
156
+ "title" => "Test3"
157
+ }
158
+ end
159
+
160
+ it "should assign original fields" do
161
+ @post.update_attributes(:title => "Test2")
162
+ @post.update_attributes(:title => "Test3")
163
+ @post.history_tracks.where(:version => 2).first.original.should == {
164
+ "title" => "Test2"
165
+ }
166
+ end
167
+
168
+
169
+ it "should assign modifier" do
170
+ @post.update_attributes(:title => "Another Test", :modifier => @another_user)
171
+ @post.history_tracks.first.modifier.should == @another_user
172
+ end
173
+ end
174
+
175
+ describe "on update embedded" do
176
+ it "should assigin version on comment" do
177
+ @comment.update_attributes(:title => "Test2")
178
+ @comment.version.should == 2 # first track generated on creation
179
+ end
180
+
181
+ it "should create a history track of version 2" do
182
+ @comment.update_attributes(:title => "Test2")
183
+ @comment.history_tracks.where(:version => 2).first.should_not be_nil
184
+ end
185
+
186
+ it "should assign modified fields" do
187
+ @comment.update_attributes(:title => "Test2")
188
+ @comment.history_tracks.where(:version => 2).first.modified.should == {
189
+ "title" => "Test2"
190
+ }
191
+ end
192
+
193
+ it "should assign original fields" do
194
+ @comment.update_attributes(:title => "Test2")
195
+ @comment.history_tracks.where(:version => 2).first.original.should == {
196
+ "title" => "test"
197
+ }
198
+ end
199
+
200
+ it "should assign modifier" do
201
+ @post.update_attributes(:title => "Another Test", :modifier => @another_user)
202
+ @post.history_tracks.first.modifier.should == @another_user
203
+ end
204
+ end
205
+
206
+ describe "non-embedded" do
207
+ it "should undo changes" do
208
+ @post.update_attributes(:title => "Test2")
209
+ @post.history_tracks.where(:version => 1).first.undo!
210
+ @post.reload
211
+ @post.title.should == "Test"
212
+ end
213
+
214
+ it "should create a new history track after undo" do
215
+ @post.update_attributes(:title => "Test2")
216
+ @post.history_tracks.where(:version => 1).first.undo!
217
+ @post.reload
218
+ @post.history_tracks.count.should == 2
219
+ end
220
+
221
+ it "should stay the same after undo and redo" do
222
+ @post.update_attributes(:title => "Test2")
223
+ @track = @post.history_tracks.where(:version => 1).first
224
+ @track.undo!
225
+ @track.redo!
226
+ @post2 = Post.where(:_id => @post.id).first
227
+
228
+ @post.title.should == @post2.title
229
+ end
230
+ end
231
+
232
+ describe "embedded" do
233
+ it "should undo changes" do
234
+ @comment.update_attributes(:title => "Test2")
235
+ @comment.history_tracks.where(:version => 2).first.undo!
236
+ # reloading an embedded document === KAMIKAZE
237
+ # at least for the current release of mongoid...
238
+ @post.reload
239
+ @comment = @post.comments.first
240
+ @comment.title.should == "test"
241
+ end
242
+
243
+ it "should create a new history track after undo" do
244
+ @comment.update_attributes(:title => "Test2")
245
+ @comment.history_tracks.where(:version => 2).first.undo!
246
+ @post.reload
247
+ @comment = @post.comments.first
248
+ @comment.history_tracks.count.should == 3
249
+ end
250
+
251
+ it "should stay the same after undo and redo" do
252
+ @comment.update_attributes(:title => "Test2")
253
+ @track = @comment.history_tracks.where(:version => 2).first
254
+ @track.undo!
255
+ @track.redo!
256
+ @post2 = Post.where(:_id => @post.id).first
257
+ @comment2 = @post2.comments.first
258
+
259
+ @comment.title.should == @comment2.title
260
+ end
261
+ end
262
+
263
+ end
264
+ end
@@ -0,0 +1,24 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ require 'rspec'
5
+ require 'easy_diff'
6
+ require 'mongoid'
7
+ require 'mongoid-history'
8
+ require 'database_cleaner'
9
+
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
11
+
12
+ RSpec.configure do |config|
13
+ config.before(:suite) do
14
+ DatabaseCleaner.strategy = :truncation
15
+ end
16
+
17
+ config.after(:each) do
18
+ DatabaseCleaner.clean
19
+ end
20
+ end
21
+
22
+ Mongoid.configure do |config|
23
+ config.master = Mongo::Connection.new.db("mongoid-history")
24
+ end
@@ -0,0 +1,71 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Mongoid::History::Trackable do
4
+ before :each do
5
+ class MyModel
6
+ include Mongoid::Document
7
+ include Mongoid::History::Trackable
8
+ end
9
+ end
10
+
11
+ after :each do
12
+ Mongoid::History.trackable_classes = nil
13
+ Mongoid::History.trackable_class_options = nil
14
+ end
15
+
16
+ it "should have #track_history" do
17
+ MyModel.should respond_to :track_history
18
+ end
19
+
20
+ it "should append trackable_classes ONLY when #track_history is called" do
21
+ Mongoid::History.trackable_classes.should be_blank
22
+ MyModel.track_history
23
+ Mongoid::History.trackable_classes.should == [MyModel]
24
+ end
25
+
26
+ it "should append trackable_class_options ONLY when #track_history is called" do
27
+ Mongoid::History.trackable_class_options.should be_blank
28
+ MyModel.track_history
29
+ Mongoid::History.trackable_class_options.keys.should == [:my_model]
30
+ end
31
+
32
+ describe "#track_history" do
33
+ before :each do
34
+ class MyModel
35
+ include Mongoid::Document
36
+ include Mongoid::History::Trackable
37
+ track_history
38
+ end
39
+
40
+ @expected_option = {
41
+ :on => :all,
42
+ :modifier_field => :modifier,
43
+ :version_field => :version,
44
+ :scope => :my_model,
45
+ :except => ["created_at", "updated_at", "version", "modifier_id", "_id", "id"],
46
+ :track_create => false
47
+ }
48
+ end
49
+
50
+ after :each do
51
+ Mongoid::History.trackable_classes = nil
52
+ Mongoid::History.trackable_class_options = nil
53
+ end
54
+
55
+ it "should have default options" do
56
+ Mongoid::History.trackable_class_options[:my_model].should == @expected_option
57
+ end
58
+
59
+ it "should define callback function #track_update" do
60
+ MyModel.new.private_methods.collect(&:to_sym).should include(:track_update)
61
+ end
62
+
63
+ it "should define callback function #track_create" do
64
+ MyModel.new.private_methods.collect(&:to_sym).should include(:track_create)
65
+ end
66
+
67
+ it "should define #history_trackable_options" do
68
+ MyModel.history_trackable_options.should == @expected_option
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Mongoid::History::Tracker do
4
+ before :each do
5
+ class MyTracker
6
+ include Mongoid::History::Tracker
7
+ end
8
+ end
9
+
10
+ after :each do
11
+ Mongoid::History.tracker_class_name = nil
12
+ end
13
+
14
+ it "should set tracker_class_name when included" do
15
+ Mongoid::History.tracker_class_name.should == :my_tracker
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,245 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid-history
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Aaron Qian
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-03-04 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ type: :runtime
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ name: easy_diff
33
+ version_requirements: *id001
34
+ prerelease: false
35
+ - !ruby/object:Gem::Dependency
36
+ type: :runtime
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ hash: 15424091
43
+ segments:
44
+ - 2
45
+ - 0
46
+ - 0
47
+ - rc
48
+ - 7
49
+ version: 2.0.0.rc.7
50
+ name: mongoid
51
+ version_requirements: *id002
52
+ prerelease: false
53
+ - !ruby/object:Gem::Dependency
54
+ type: :development
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 3
61
+ segments:
62
+ - 2
63
+ - 3
64
+ - 0
65
+ version: 2.3.0
66
+ name: rspec
67
+ version_requirements: *id003
68
+ prerelease: false
69
+ - !ruby/object:Gem::Dependency
70
+ type: :development
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ hash: 7
77
+ segments:
78
+ - 0
79
+ - 6
80
+ - 0
81
+ version: 0.6.0
82
+ name: yard
83
+ version_requirements: *id004
84
+ prerelease: false
85
+ - !ruby/object:Gem::Dependency
86
+ type: :development
87
+ requirement: &id005 !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ~>
91
+ - !ruby/object:Gem::Version
92
+ hash: 23
93
+ segments:
94
+ - 1
95
+ - 0
96
+ - 0
97
+ version: 1.0.0
98
+ name: bundler
99
+ version_requirements: *id005
100
+ prerelease: false
101
+ - !ruby/object:Gem::Dependency
102
+ type: :development
103
+ requirement: &id006 !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ~>
107
+ - !ruby/object:Gem::Version
108
+ hash: 7
109
+ segments:
110
+ - 1
111
+ - 5
112
+ - 2
113
+ version: 1.5.2
114
+ name: jeweler
115
+ version_requirements: *id006
116
+ prerelease: false
117
+ - !ruby/object:Gem::Dependency
118
+ type: :development
119
+ requirement: &id007 !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ hash: 3
125
+ segments:
126
+ - 0
127
+ version: "0"
128
+ name: rcov
129
+ version_requirements: *id007
130
+ prerelease: false
131
+ - !ruby/object:Gem::Dependency
132
+ type: :development
133
+ requirement: &id008 !ruby/object:Gem::Requirement
134
+ none: false
135
+ requirements:
136
+ - - ~>
137
+ - !ruby/object:Gem::Version
138
+ hash: 15
139
+ segments:
140
+ - 1
141
+ - 2
142
+ - 8
143
+ version: 1.2.8
144
+ name: reek
145
+ version_requirements: *id008
146
+ prerelease: false
147
+ - !ruby/object:Gem::Dependency
148
+ type: :development
149
+ requirement: &id009 !ruby/object:Gem::Requirement
150
+ none: false
151
+ requirements:
152
+ - - ~>
153
+ - !ruby/object:Gem::Version
154
+ hash: 11
155
+ segments:
156
+ - 2
157
+ - 1
158
+ - 0
159
+ version: 2.1.0
160
+ name: roodi
161
+ version_requirements: *id009
162
+ prerelease: false
163
+ - !ruby/object:Gem::Dependency
164
+ type: :development
165
+ requirement: &id010 !ruby/object:Gem::Requirement
166
+ none: false
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ hash: 3
171
+ segments:
172
+ - 0
173
+ version: "0"
174
+ name: database_cleaner
175
+ version_requirements: *id010
176
+ prerelease: false
177
+ description: |-
178
+ In frustration of Mongoid::Versioning, I created this plugin for tracking historical changes for any document, including embedded ones. It achieves this by storing all history tracks in a single collection that you define. (See Usage for more details) Embedded documents are referenced by storing an association path, which is an array of document_name and document_id fields starting from the top most parent document and down to the embedded document that should track history.
179
+
180
+ This plugin implements multi-user undo, which allows users to undo any history change in any order. Undoing a document also creates a new history track. This is great for auditing and preventing vandalism, but it is probably not suitable for use cases such as a wiki.
181
+ email: aqian@attinteractive.com
182
+ executables: []
183
+
184
+ extensions: []
185
+
186
+ extra_rdoc_files:
187
+ - LICENSE.txt
188
+ - README.rdoc
189
+ files:
190
+ - .document
191
+ - .rspec
192
+ - .rvmrc
193
+ - Gemfile
194
+ - LICENSE.txt
195
+ - README.rdoc
196
+ - Rakefile
197
+ - VERSION
198
+ - lib/mongoid-history.rb
199
+ - lib/mongoid/history.rb
200
+ - lib/mongoid/history/trackable.rb
201
+ - lib/mongoid/history/tracker.rb
202
+ - mongoid-history.gemspec
203
+ - spec/integration/integration_spec.rb
204
+ - spec/spec_helper.rb
205
+ - spec/trackable_spec.rb
206
+ - spec/tracker_spec.rb
207
+ has_rdoc: true
208
+ homepage: http://github.com/aq1018/mongoid-history
209
+ licenses:
210
+ - MIT
211
+ post_install_message:
212
+ rdoc_options: []
213
+
214
+ require_paths:
215
+ - lib
216
+ required_ruby_version: !ruby/object:Gem::Requirement
217
+ none: false
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ hash: 3
222
+ segments:
223
+ - 0
224
+ version: "0"
225
+ required_rubygems_version: !ruby/object:Gem::Requirement
226
+ none: false
227
+ requirements:
228
+ - - ">="
229
+ - !ruby/object:Gem::Version
230
+ hash: 3
231
+ segments:
232
+ - 0
233
+ version: "0"
234
+ requirements: []
235
+
236
+ rubyforge_project:
237
+ rubygems_version: 1.5.3
238
+ signing_key:
239
+ specification_version: 3
240
+ summary: history tracking, auditing, undo, redo for mongoid
241
+ test_files:
242
+ - spec/integration/integration_spec.rb
243
+ - spec/spec_helper.rb
244
+ - spec/trackable_spec.rb
245
+ - spec/tracker_spec.rb