mongoid-audit 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ services: mongodb
3
+ rvm:
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mongoid-audit.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Gleb Tv
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,191 @@
1
+ Mongoid::Audit
2
+ ==============
3
+
4
+ [![Build Status](https://secure.travis-ci.org/rs-pro/mongoid-audit.png?branch=master)](http://travis-ci.org/rs-pro/mongoid-audit)
5
+ [![Dependency Status](https://gemnasium.com/rs-pro/mongoid-audit.png)](https://gemnasium.com/rs-pro/mongoid-audit)
6
+
7
+ Mongoid-audit tracks historical changes for any document, including embedded ones. It achieves this by storing all history tracks in a single collection that you define. 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.
8
+
9
+ This gem also 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 is probably not suitable for use cases such as a wiki.
10
+
11
+ Installation
12
+ ____________
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'mongoid-audit'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install mongoid-audit
25
+
26
+ Usage
27
+ -----
28
+
29
+ **Create a history tracker**
30
+
31
+ Create a new class to track histories. All histories are stored in this tracker. The name of the class can be anything you like. The only requirement is that it includes `Mongoid::Audit::Tracker`
32
+
33
+ ```ruby
34
+ # app/models/history_tracker.rb
35
+ class HistoryTracker
36
+ include Mongoid::Audit::Tracker
37
+ end
38
+ ```
39
+
40
+ **Set tracker class name**
41
+
42
+ Manually set the tracker class name to make sure your tracker can be found and loaded properly. You can skip this step if you manually require your tracker before using any trackables.
43
+
44
+ The following example sets the tracker class name using a Rails initializer.
45
+
46
+ ```ruby
47
+ # config/initializers/mongoid-audit.rb
48
+ # initializer for mongoid-audit
49
+ # assuming HistoryTracker is your tracker class
50
+ Mongoid::Audit.tracker_class_name = :history_tracker
51
+ ```
52
+
53
+ **Set `#current_user` method name**
54
+
55
+ You can set the name of the method that returns currently logged in user if you don't want to set `modifier` explicitly on every update.
56
+
57
+ The following example sets the `current_user_method` using a Rails initializer
58
+
59
+ ```ruby
60
+ # config/initializers/mongoid-audit.rb
61
+ # initializer for mongoid-audit
62
+ # assuming you're using devise/authlogic
63
+ Mongoid::Audit.current_user_method = :current_user
64
+ ```
65
+
66
+ **IMPORTANT**
67
+ for this to work in development environment, add
68
+ ```ruby
69
+ require_dependency 'history_tracker.rb' if Rails.env == "development"
70
+ ```
71
+ to the initializer so controller filter would be installed
72
+
73
+
74
+ When `current_user_method` is set, mongoid-audit will invoke this method on each update and set its result as the instance modifier.
75
+
76
+ ```ruby
77
+ # assume that current_user return #<User _id: 1>
78
+ post = Post.first
79
+ post.update_attributes(:title => 'New title')
80
+
81
+ post.history_tracks.last.modifier #=> #<User _id: 1>
82
+ ```
83
+
84
+ **Create trackable classes and objects**
85
+
86
+ ```ruby
87
+ class Post
88
+ include Mongoid::Document
89
+ include Mongoid::Timestamps
90
+
91
+ # history tracking all Post documents
92
+ # note: tracking will not work until #track_history is invoked
93
+ include Mongoid::Audit::Trackable
94
+
95
+ field :title
96
+ field :body
97
+ field :rating
98
+ embeds_many :comments
99
+
100
+ # telling Mongoid::Audit how you want to track changes
101
+ track_history :on => [:title, :body], # track title and body fields only, default is :all
102
+ :modifier_field => :modifier, # adds "referenced_in :modifier" to track who made the change, default is :modifier
103
+ :version_field => :version, # adds "field :version, :type => Integer" to track current version, default is :version
104
+ :track_create => false, # track document creation, default is false
105
+ :track_update => true, # track document updates, default is true
106
+ :track_destroy => false, # track document destruction, default is false
107
+ end
108
+
109
+ class Comment
110
+ include Mongoid::Document
111
+ include Mongoid::Timestamps
112
+
113
+ # declare that we want to track comments
114
+ include Mongoid::Audit::Trackable
115
+
116
+ field :title
117
+ field :body
118
+ embedded_in :post, :inverse_of => :comments
119
+
120
+ # track title and body for all comments, scope it to post (the parent)
121
+ # also track creation and destruction
122
+ track_history :on => [:title, :body], :scope => :post, :track_create => true, :track_destroy => true
123
+ end
124
+
125
+ # the modifier class
126
+ class User
127
+ include Mongoid::Document
128
+ include Mongoid::Timestamps
129
+
130
+ field :name
131
+ end
132
+
133
+ user = User.create(:name => "Aaron")
134
+ post = Post.create(:title => "Test", :body => "Post", :modifier => user)
135
+ comment = post.comments.create(:title => "test", :body => "comment", :modifier => user)
136
+ comment.history_tracks.count # should be 1
137
+
138
+ comment.update_attributes(:title => "Test 2")
139
+ comment.history_tracks.count # should be 2
140
+
141
+ track = comment.history_tracks.last
142
+
143
+ track.undo! user # comment title should be "Test"
144
+
145
+ track.redo! user # comment title should be "Test 2"
146
+
147
+ # undo last change
148
+ comment.undo! user
149
+
150
+ # undo versions 1 - 4
151
+ comment.undo! user, :from => 4, :to => 1
152
+
153
+ # undo last 3 versions
154
+ comment.undo! user, :last => 3
155
+
156
+ # redo versions 1 - 4
157
+ comment.redo! user, :from => 1, :to => 4
158
+
159
+ # redo last 3 versions
160
+ comment.redo! user, :last => 3
161
+
162
+ # delete post
163
+ post.destroy
164
+
165
+ # undelete post
166
+ post.undo! user
167
+
168
+ # disable tracking for comments within a block
169
+ Comment.disable_tracking do
170
+ comment.update_attributes(:title => "Test 3")
171
+ end
172
+ ```
173
+ For more examples, check out [spec/integration/integration_spec.rb](https://github.com/aq1018/mongoid-history/blob/master/spec/integration/integration_spec.rb).
174
+
175
+ ## Credits
176
+
177
+ This gem is loosely based on https://github.com/aq1018/mongoid-history
178
+
179
+ The original gem didn't work correctly for us, when not setting modifier manually, so we rewrote it a bit.
180
+ Seems to work fully now, including manually setting modifier
181
+
182
+ Mongoid-history Copyright (c) 2011-2012 Aaron Qian. MIT License. See [LICENSE.txt](https://github.com/aq1018/mongoid-history/blob/master/LICENSE.txt) for further details.
183
+ Mongoid-audit Copyright (c) 2013 http://rocketscience.pro MIT License.
184
+
185
+ ## Contributing
186
+
187
+ 1. Fork it
188
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
189
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
190
+ 4. Push to the branch (`git push origin my-new-feature`)
191
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,8 @@
1
+ test:
2
+ sessions:
3
+ default:
4
+ database: mongoid-history
5
+ hosts:
6
+ - localhost:27017
7
+ options:
8
+ consistency: :strong
@@ -0,0 +1,24 @@
1
+ require 'mongoid-audit/version'
2
+ require 'easy_diff'
3
+
4
+ module Mongoid
5
+ module Audit
6
+ mattr_accessor :tracker_class_name
7
+ mattr_accessor :trackable_class_options
8
+ mattr_accessor :modifier_class_name
9
+ mattr_accessor :current_user_method
10
+
11
+ def self.tracker_class
12
+ @tracker_class ||= tracker_class_name.to_s.classify.constantize
13
+ end
14
+ end
15
+ end
16
+
17
+ require 'mongoid-audit/tracker'
18
+ require 'mongoid-audit/trackable'
19
+ require 'mongoid-audit/sweeper'
20
+
21
+ Mongoid::Audit.modifier_class_name = "User"
22
+ Mongoid::Audit.trackable_class_options = {}
23
+ Mongoid::Audit.current_user_method ||= :current_user
24
+
@@ -0,0 +1,40 @@
1
+ module Mongoid::Audit
2
+ class Sweeper < Mongoid::Observer
3
+ def controller
4
+ Thread.current[:mongoid_history_sweeper_controller]
5
+ end
6
+
7
+ def controller=(value)
8
+ Thread.current[:mongoid_history_sweeper_controller] = value
9
+ end
10
+
11
+ # Hook to ActionController::Base#around_filter.
12
+ # Runs before a controller action is run.
13
+ # It should always return true so controller actions
14
+ # can continue.
15
+ def before(controller)
16
+ self.controller = controller
17
+ true
18
+ end
19
+
20
+ # Hook to ActionController::Base#around_filter.
21
+ # Runs after a controller action is run.
22
+ # Clean up so that the controller can
23
+ # be collected after this request
24
+ def after(controller)
25
+ self.controller = nil
26
+ end
27
+
28
+ def before_create(track)
29
+ track.modifier = current_user if track.modifier.nil?
30
+ end
31
+
32
+ def current_user
33
+ if controller.respond_to?(Mongoid::Audit.current_user_method, true)
34
+ controller.send Mongoid::Audit.current_user_method
35
+ else
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,262 @@
1
+ module Mongoid::Audit
2
+ module Trackable
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def track_history(options={})
7
+ scope_name = self.collection_name.to_s.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 => scope_name,
14
+ :track_create => false,
15
+ :track_update => true,
16
+ :track_destroy => false,
17
+ }
18
+
19
+ options = default_options.merge(options)
20
+
21
+ # normalize except fields
22
+ # manually ensure _id, id, version will not be tracked in history
23
+ options[:except] = [options[:except]] unless options[:except].is_a? Array
24
+ options[:except] << options[:version_field]
25
+ options[:except] << "#{options[:modifier_field]}_id".to_sym
26
+ options[:except] += [:_id, :id]
27
+ options[:except] = options[:except].map(&:to_s).flatten.compact.uniq
28
+ options[:except].map(&:to_s)
29
+
30
+ # normalize fields to track to either :all or an array of strings
31
+ if options[:on] != :all
32
+ options[:on] = [options[:on]] unless options[:on].is_a? Array
33
+ options[:on] = options[:on].map(&:to_s).flatten.uniq
34
+ end
35
+
36
+ field options[:version_field].to_sym, :type => Integer
37
+ belongs_to options[:modifier_field].to_sym, :class_name => Mongoid::Audit.modifier_class_name
38
+
39
+ include MyInstanceMethods
40
+ extend SingletonMethods
41
+
42
+ delegate :history_trackable_options, :to => 'self.class'
43
+ delegate :track_history?, :to => 'self.class'
44
+
45
+ before_update :track_update if options[:track_update]
46
+ before_create :track_create if options[:track_create]
47
+ before_destroy :track_destroy if options[:track_destroy]
48
+
49
+ Mongoid::Audit.trackable_class_options ||= {}
50
+ Mongoid::Audit.trackable_class_options[scope_name] = options
51
+ end
52
+
53
+ def track_history?
54
+ enabled = Thread.current[track_history_flag]
55
+ enabled.nil? ? true : enabled
56
+ end
57
+
58
+ def disable_tracking(&block)
59
+ begin
60
+ Thread.current[track_history_flag] = false
61
+ yield
62
+ ensure
63
+ Thread.current[track_history_flag] = true
64
+ end
65
+ end
66
+
67
+ def track_history_flag
68
+ "mongoid_history_#{self.name.underscore}_trackable_enabled".to_sym
69
+ end
70
+ end
71
+
72
+ module MyInstanceMethods
73
+ def history_tracks
74
+ @history_tracks ||= Mongoid::Audit.tracker_class.where(:scope => history_trackable_options[:scope], :association_chain => association_hash)
75
+ end
76
+
77
+ # undo :from => 1, :to => 5
78
+ # undo 4
79
+ # undo :last => 10
80
+ def undo!(modifier, options_or_version=nil)
81
+ versions = get_versions_criteria(options_or_version).to_a
82
+ versions.sort!{|v1, v2| v2.version <=> v1.version}
83
+
84
+ versions.each do |v|
85
+ undo_attr = v.undo_attr(modifier)
86
+ self.attributes = v.undo_attr(modifier)
87
+ end
88
+ save!
89
+ end
90
+
91
+ def redo!(modifier, options_or_version=nil)
92
+ versions = get_versions_criteria(options_or_version).to_a
93
+ versions.sort!{|v1, v2| v1.version <=> v2.version}
94
+
95
+ versions.each do |v|
96
+ redo_attr = v.redo_attr(modifier)
97
+ self.attributes = redo_attr
98
+ end
99
+ save!
100
+ end
101
+
102
+ private
103
+ def get_versions_criteria(options_or_version)
104
+ if options_or_version.is_a? Hash
105
+ options = options_or_version
106
+ if options[:from] && options[:to]
107
+ lower = options[:from] >= options[:to] ? options[:to] : options[:from]
108
+ upper = options[:from] < options[:to] ? options[:to] : options[:from]
109
+ versions = history_tracks.where( :version.in => (lower .. upper).to_a )
110
+ elsif options[:last]
111
+ versions = history_tracks.limit( options[:last] )
112
+ else
113
+ raise "Invalid options, please specify (:from / :to) keys or :last key."
114
+ end
115
+ else
116
+ options_or_version = options_or_version.to_a if options_or_version.is_a?(Range)
117
+ version_field_name = history_trackable_options[:version_field]
118
+ version = options_or_version || self.attributes[version_field_name] || self.attributes[version_field_name.to_s]
119
+ version = [ version ].flatten
120
+ versions = history_tracks.where(:version.in => version)
121
+ end
122
+ versions.desc(:version)
123
+ end
124
+
125
+ def should_track_update?
126
+ track_history? && !modified_attributes_for_update.blank?
127
+ end
128
+
129
+ def traverse_association_chain(node=self)
130
+ list = node._parent ? traverse_association_chain(node._parent) : []
131
+ list << association_hash(node)
132
+ list
133
+ end
134
+
135
+ def association_hash(node=self)
136
+
137
+ # We prefer to look up associations through the parent record because
138
+ # we're assured, through the object creation, it'll exist. Whereas we're not guarenteed
139
+ # the child to parent (embedded_in, belongs_to) relation will be defined
140
+ if node._parent
141
+ meta = _parent.relations.values.select do |relation|
142
+ relation.class_name == node.class.to_s
143
+ end.first
144
+ end
145
+
146
+ # if root node has no meta, and should use class name instead
147
+ name = meta ? meta.key.to_s : node.class.name
148
+
149
+ { 'name' => name, 'id' => node.id}
150
+ end
151
+
152
+ def modified_attributes_for_update
153
+ @modified_attributes_for_update ||= if history_trackable_options[:on] == :all
154
+ changes.reject do |k, v|
155
+ history_trackable_options[:except].include?(k)
156
+ end
157
+ else
158
+ changes.reject do |k, v|
159
+ !history_trackable_options[:on].include?(k)
160
+ end
161
+
162
+ end
163
+ end
164
+
165
+ def modified_attributes_for_create
166
+ @modified_attributes_for_create ||= attributes.inject({}) do |h, pair|
167
+ k,v = pair
168
+ h[k] = [nil, v]
169
+ h
170
+ end.reject do |k, v|
171
+ history_trackable_options[:except].include?(k)
172
+ end
173
+ end
174
+
175
+ def modified_attributes_for_destroy
176
+ @modified_attributes_for_destroy ||= attributes.inject({}) do |h, pair|
177
+ k,v = pair
178
+ h[k] = [nil, v]
179
+ h
180
+ end
181
+ end
182
+
183
+ def history_tracker_attributes(method)
184
+ return @history_tracker_attributes if @history_tracker_attributes
185
+
186
+ @history_tracker_attributes = {
187
+ :association_chain => traverse_association_chain,
188
+ :scope => history_trackable_options[:scope],
189
+ :modifier => send("#{history_trackable_options[:modifier_field].to_s}_id_changed?".to_sym) ? send(history_trackable_options[:modifier_field]) : nil,
190
+ }
191
+
192
+ original, modified = transform_changes(case method
193
+ when :destroy then modified_attributes_for_destroy
194
+ when :create then modified_attributes_for_create
195
+ else modified_attributes_for_update
196
+ end)
197
+
198
+ @history_tracker_attributes[:original] = original
199
+ @history_tracker_attributes[:modified] = modified
200
+ @history_tracker_attributes
201
+ end
202
+
203
+ def track_update
204
+ return unless should_track_update?
205
+ current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
206
+ self.send("#{history_trackable_options[:version_field]}=", current_version)
207
+
208
+ track = Mongoid::Audit.tracker_class.create!(history_tracker_attributes(:update).merge(:version => current_version, :action => "update", :trackable => self))
209
+ self.send("#{history_trackable_options[:modifier_field]}=", track.modifier)
210
+
211
+ clear_memoization
212
+ end
213
+
214
+ def track_create
215
+ return unless track_history?
216
+ current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
217
+ self.send("#{history_trackable_options[:version_field]}=", current_version)
218
+
219
+ track = Mongoid::Audit.tracker_class.create!(history_tracker_attributes(:create).merge(:version => current_version, :action => "create", :trackable => self))
220
+ self.send("#{history_trackable_options[:modifier_field]}=", track.modifier)
221
+
222
+ clear_memoization
223
+ end
224
+
225
+ def track_destroy
226
+ return unless track_history?
227
+ current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
228
+
229
+ track = Mongoid::Audit.tracker_class.create!(history_tracker_attributes(:destroy).merge(:version => current_version, :action => "destroy", :trackable => self))
230
+ self.send("#{history_trackable_options[:modifier_field]}=", track.modifier)
231
+
232
+ clear_memoization
233
+ end
234
+
235
+ def clear_memoization
236
+ @history_tracker_attributes = nil
237
+ @modified_attributes_for_create = nil
238
+ @modified_attributes_for_update = nil
239
+ @history_tracks = nil
240
+ end
241
+
242
+ def transform_changes(changes)
243
+ original = {}
244
+ modified = {}
245
+ changes.each_pair do |k, v|
246
+ o, m = v
247
+ original[k] = o unless o.nil?
248
+ modified[k] = m unless m.nil?
249
+ end
250
+
251
+ original.easy_diff modified
252
+ end
253
+
254
+ end
255
+
256
+ module SingletonMethods
257
+ def history_trackable_options
258
+ @history_trackable_options ||= Mongoid::Audit.trackable_class_options[self.collection_name.to_s.singularize.to_sym]
259
+ end
260
+ end
261
+ end
262
+ end