mongoid-history-patched 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/.travis.yml +10 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +20 -0
- data/README.md +206 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/lib/mongoid-history.rb +10 -0
- data/lib/mongoid/history.rb +13 -0
- data/lib/mongoid/history/sweeper.rb +44 -0
- data/lib/mongoid/history/trackable.rb +381 -0
- data/lib/mongoid/history/tracker.rb +154 -0
- data/mongoid-history.gemspec +78 -0
- data/spec/integration/integration_spec.rb +553 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/trackable_spec.rb +121 -0
- data/spec/tracker_spec.rb +17 -0
- metadata +205 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
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.md
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
mongoid-history
|
2
|
+
===============
|
3
|
+
|
4
|
+
[![Build Status](https://secure.travis-ci.org/aq1018/mongoid-history.png?branch=master)](http://travis-ci.org/aq1018/mongoid-history) [![Dependency Status](https://gemnasium.com/aq1018/mongoid-history.png?travis)](https://gemnasium.com/aq1018/mongoid-history)
|
5
|
+
|
6
|
+
|
7
|
+
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. 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 plugin 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 it is probably not suitable for use cases such as a wiki.
|
10
|
+
|
11
|
+
Note
|
12
|
+
----
|
13
|
+
|
14
|
+
**Please don't use 0.1.8 and 0.2.0.**
|
15
|
+
|
16
|
+
These versions won't work in Rails because there was an error in the sweeper that causes history tracker creation to fail. Upgrade to version 0.2.1 instead as it's able to track history on `embeds_one` documents correctly.
|
17
|
+
|
18
|
+
**Refactor in progress**
|
19
|
+
|
20
|
+
If you feel brave, you can look at the `refactor` branch and get a feel of what's coming. As I stated many times before, this gem was originally hacked up in one evening, and got patched many times by various fellow users. Thus the code has become pretty unmanagable over time. The `refactor` branch tries repay this technical debt by breaking things down into smaller class and implement better tests. Stay tuned! :D
|
21
|
+
|
22
|
+
Upgrading from mongoid-history-0.1.x to >= 0.2
|
23
|
+
------------------------------------------------
|
24
|
+
|
25
|
+
If you are upgrading from 0.1.x to version 0.2.x and have existing data, run the following code **before** you start using 0.2.x. This is due to changes in `Mongoid::History::Tracker`'s `association_chain` field.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
Mongoid::History.tracker_class.all.each do |tracker|
|
29
|
+
tracker.association_chain[1..-1].each do |node|
|
30
|
+
node['name'] = node['name'].tableize
|
31
|
+
end
|
32
|
+
tracker.save!
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
Install
|
37
|
+
-------
|
38
|
+
|
39
|
+
This gem supports Ruby 1.8.7, 1.9.2, 1.9.3, JRuby, Rubinius and REE. Add it to your `Gemfile` or run `gem install mongoid-history`.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
gem 'mongoid-history'
|
43
|
+
```
|
44
|
+
|
45
|
+
Usage
|
46
|
+
-----
|
47
|
+
|
48
|
+
Here is a quick example on how to use this plugin.
|
49
|
+
|
50
|
+
**Create a history tracker**
|
51
|
+
|
52
|
+
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::History::Tracker`
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# app/models/history_tracker.rb
|
56
|
+
class HistoryTracker
|
57
|
+
include Mongoid::History::Tracker
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
**Set tracker class name**
|
62
|
+
|
63
|
+
You should 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.
|
64
|
+
|
65
|
+
The following example sets the tracker class name using a Rails initializer.
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
# config/initializers/mongoid-history.rb
|
69
|
+
# initializer for mongoid-history
|
70
|
+
# assuming HistoryTracker is your tracker class
|
71
|
+
Mongoid::History.tracker_class_name = :history_tracker
|
72
|
+
```
|
73
|
+
|
74
|
+
**Set `#current_user` method name**
|
75
|
+
|
76
|
+
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.
|
77
|
+
|
78
|
+
The following example sets the `current_user_method` using a Rails initializer
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
# config/initializers/mongoid-history.rb
|
82
|
+
# initializer for mongoid-history
|
83
|
+
# assuming you're using devise/authlogic
|
84
|
+
Mongoid::History.current_user_method = :current_user
|
85
|
+
```
|
86
|
+
|
87
|
+
When `current_user_method` is set, mongoid-history will invoke this method on each update and set its result as the instance modifier.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
# assume that current_user return #<User _id: 1>
|
91
|
+
post = Post.first
|
92
|
+
post.update_attributes(:title => 'New title')
|
93
|
+
|
94
|
+
post.history_tracks.last.modifier #=> #<User _id: 1>
|
95
|
+
```
|
96
|
+
|
97
|
+
**Create trackable classes and objects**
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class Post
|
101
|
+
include Mongoid::Document
|
102
|
+
include Mongoid::Timestamps
|
103
|
+
|
104
|
+
# history tracking all Post documents
|
105
|
+
# note: tracking will not work until #track_history is invoked
|
106
|
+
include Mongoid::History::Trackable
|
107
|
+
|
108
|
+
field :title
|
109
|
+
field :body
|
110
|
+
field :rating
|
111
|
+
embeds_many :comments
|
112
|
+
|
113
|
+
# telling Mongoid::History how you want to track changes
|
114
|
+
track_history :on => [:title, :body], # track title and body fields only, default is :all
|
115
|
+
:modifier_field => :modifier, # adds "referenced_in :modifier" to track who made the change, default is :modifier
|
116
|
+
:version_field => :version, # adds "field :version, :type => Integer" to track current version, default is :version
|
117
|
+
:track_create => false, # track document creation, default is false
|
118
|
+
:track_update => true, # track document updates, default is true
|
119
|
+
:track_destroy => false, # track document destruction, default is false
|
120
|
+
end
|
121
|
+
|
122
|
+
class Comment
|
123
|
+
include Mongoid::Document
|
124
|
+
include Mongoid::Timestamps
|
125
|
+
|
126
|
+
# declare that we want to track comments
|
127
|
+
include Mongoid::History::Trackable
|
128
|
+
|
129
|
+
field :title
|
130
|
+
field :body
|
131
|
+
embedded_in :post, :inverse_of => :comments
|
132
|
+
|
133
|
+
# track title and body for all comments, scope it to post (the parent)
|
134
|
+
# also track creation and destruction
|
135
|
+
track_history :on => [:title, :body], :scope => :post, :track_create => true, :track_destroy => true
|
136
|
+
end
|
137
|
+
|
138
|
+
# the modifier class
|
139
|
+
class User
|
140
|
+
include Mongoid::Document
|
141
|
+
include Mongoid::Timestamps
|
142
|
+
|
143
|
+
field :name
|
144
|
+
end
|
145
|
+
|
146
|
+
user = User.create(:name => "Aaron")
|
147
|
+
post = Post.create(:title => "Test", :body => "Post", :modifier => user)
|
148
|
+
comment = post.comments.create(:title => "test", :body => "comment", :modifier => user)
|
149
|
+
comment.history_tracks.count # should be 1
|
150
|
+
|
151
|
+
comment.update_attributes(:title => "Test 2")
|
152
|
+
comment.history_tracks.count # should be 2
|
153
|
+
|
154
|
+
track = comment.history_tracks.last
|
155
|
+
|
156
|
+
track.undo! user # comment title should be "Test"
|
157
|
+
|
158
|
+
track.redo! user # comment title should be "Test 2"
|
159
|
+
|
160
|
+
# undo last change
|
161
|
+
comment.undo! user
|
162
|
+
|
163
|
+
# undo versions 1 - 4
|
164
|
+
comment.undo! user, :from => 4, :to => 1
|
165
|
+
|
166
|
+
# undo last 3 versions
|
167
|
+
comment.undo! user, :last => 3
|
168
|
+
|
169
|
+
# redo versions 1 - 4
|
170
|
+
comment.redo! user, :from => 1, :to => 4
|
171
|
+
|
172
|
+
# redo last 3 versions
|
173
|
+
comment.redo! user, :last => 3
|
174
|
+
|
175
|
+
# delete post
|
176
|
+
post.destroy
|
177
|
+
|
178
|
+
# undelete post
|
179
|
+
post.undo! user
|
180
|
+
|
181
|
+
# disable tracking for comments within a block
|
182
|
+
Comment.disable_tracking do
|
183
|
+
comment.update_attributes(:title => "Test 3")
|
184
|
+
end
|
185
|
+
```
|
186
|
+
For more examples, check out [spec/integration/integration_spec.rb](https://github.com/aq1018/mongoid-history/blob/master/spec/integration/integration_spec.rb).
|
187
|
+
|
188
|
+
Contributing to mongoid-history
|
189
|
+
-------------------------------
|
190
|
+
|
191
|
+
* Check out the latest code to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
192
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
193
|
+
* Fork the project.
|
194
|
+
* Create a feature/bugfix branch.
|
195
|
+
* Commit and push until you are happy with your changes.
|
196
|
+
* Make sure to add tests.
|
197
|
+
* Update the CHANGELOG for the next release.
|
198
|
+
* Try not to mess with the Rakefile or version.
|
199
|
+
* Make a pull request.
|
200
|
+
|
201
|
+
Copyright
|
202
|
+
---------
|
203
|
+
|
204
|
+
Copyright (c) 2011-2012 Aaron Qian. MIT License.
|
205
|
+
See [LICENSE.txt](https://github.com/aq1018/mongoid-history/blob/master/LICENSE.txt) for further details.
|
206
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
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 = ["aq1018@gmail.com", "justin.mgrimes@gmail.com"]
|
23
|
+
gem.authors = ["Aaron Qian", "Justin Grimes"]
|
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
|
+
task :default => :spec
|
35
|
+
|
36
|
+
require 'yard'
|
37
|
+
YARD::Rake::YardocTask.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.3
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'easy_diff'
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/mongoid/history')
|
4
|
+
require File.expand_path(File.dirname(__FILE__) + '/mongoid/history/tracker')
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + '/mongoid/history/trackable')
|
6
|
+
require File.expand_path(File.dirname(__FILE__) + '/mongoid/history/sweeper')
|
7
|
+
|
8
|
+
Mongoid::History.modifier_class_name = "User"
|
9
|
+
Mongoid::History.trackable_class_options = {}
|
10
|
+
Mongoid::History.current_user_method ||= :current_user
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module History
|
3
|
+
mattr_accessor :tracker_class_name
|
4
|
+
mattr_accessor :trackable_class_options
|
5
|
+
mattr_accessor :modifier_class_name
|
6
|
+
mattr_accessor :current_user_method
|
7
|
+
|
8
|
+
def self.tracker_class
|
9
|
+
@tracker_class ||= tracker_class_name.to_s.classify.constantize
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Mongoid::History
|
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
|
+
def self.observed_classes
|
12
|
+
[Mongoid::History.tracker_class]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Hook to ActionController::Base#around_filter.
|
16
|
+
# Runs before a controller action is run.
|
17
|
+
# It should always return true so controller actions
|
18
|
+
# can continue.
|
19
|
+
def before(controller)
|
20
|
+
self.controller = controller
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
# Hook to ActionController::Base#around_filter.
|
25
|
+
# Runs after a controller action is run.
|
26
|
+
# Clean up so that the controller can
|
27
|
+
# be collected after this request
|
28
|
+
def after(controller)
|
29
|
+
self.controller = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def before_create(track)
|
33
|
+
modifier_field = track.trackable.history_trackable_options[:modifier_field]
|
34
|
+
modifier = track.trackable.send modifier_field
|
35
|
+
track.modifier = current_user unless modifier
|
36
|
+
end
|
37
|
+
|
38
|
+
def current_user
|
39
|
+
if controller.respond_to?(Mongoid::History.current_user_method, true)
|
40
|
+
controller.send Mongoid::History.current_user_method
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,381 @@
|
|
1
|
+
module Mongoid::History
|
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
|
+
:except_but_remember => [],
|
12
|
+
:modifier_field => :modifier,
|
13
|
+
:version_field => :version,
|
14
|
+
:scope => scope_name,
|
15
|
+
:remember_attachment => true,
|
16
|
+
:track_create => false,
|
17
|
+
:track_update => true,
|
18
|
+
:track_destroy => false,
|
19
|
+
}
|
20
|
+
|
21
|
+
options = default_options.merge(options)
|
22
|
+
|
23
|
+
# normalize except fields
|
24
|
+
# manually ensure _id, id, version will not be tracked in history
|
25
|
+
options[:except] = [options[:except]] unless options[:except].is_a? Array
|
26
|
+
options[:except] << options[:version_field]
|
27
|
+
options[:except] << "#{options[:modifier_field]}_id".to_sym
|
28
|
+
options[:except] += [:_id, :id, :updated_at]
|
29
|
+
options[:except] = options[:except].map(&:to_s).flatten.compact.uniq
|
30
|
+
options[:except].map(&:to_s)
|
31
|
+
|
32
|
+
# normalize except_but_remember fields
|
33
|
+
options[:except_but_remember] = [options[:except_but_remember]] unless options[:except_but_remember].is_a? Array
|
34
|
+
options[:except_but_remember] = options[:except_but_remember].map(&:to_s).flatten.compact.uniq
|
35
|
+
options[:except_but_remember].map(&:to_s)
|
36
|
+
|
37
|
+
# normalize fields to track to either :all or an array of strings
|
38
|
+
if options[:on] != :all
|
39
|
+
options[:on] = [options[:on]] unless options[:on].is_a? Array
|
40
|
+
options[:on] = options[:on].map(&:to_s).flatten.uniq
|
41
|
+
end
|
42
|
+
|
43
|
+
field options[:version_field].to_sym, :type => Integer
|
44
|
+
belongs_to options[:modifier_field].to_sym, :class_name => Mongoid::History.modifier_class_name
|
45
|
+
|
46
|
+
include MyInstanceMethods
|
47
|
+
extend SingletonMethods
|
48
|
+
|
49
|
+
delegate :history_trackable_options, :to => 'self.class'
|
50
|
+
delegate :track_history?, :to => 'self.class'
|
51
|
+
|
52
|
+
before_update :track_update if options[:track_update]
|
53
|
+
before_create :track_create if options[:track_create]
|
54
|
+
before_destroy :track_destroy if options[:track_destroy]
|
55
|
+
|
56
|
+
Mongoid::History.trackable_class_options ||= {}
|
57
|
+
Mongoid::History.trackable_class_options[scope_name] = options
|
58
|
+
end
|
59
|
+
|
60
|
+
def track_history?
|
61
|
+
enabled = Thread.current[track_history_flag]
|
62
|
+
enabled.nil? ? true : enabled
|
63
|
+
end
|
64
|
+
|
65
|
+
def disable_tracking(&block)
|
66
|
+
begin
|
67
|
+
Thread.current[track_history_flag] = false
|
68
|
+
yield
|
69
|
+
ensure
|
70
|
+
Thread.current[track_history_flag] = true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def track_history_flag
|
75
|
+
"mongoid_history_#{self.name.underscore}_trackable_enabled".to_sym
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module MyInstanceMethods
|
80
|
+
def history_tracks
|
81
|
+
@history_tracks ||= Mongoid::History.tracker_class.where(:scope => history_trackable_options[:scope], :association_chain => association_hash)
|
82
|
+
end
|
83
|
+
|
84
|
+
# undo :from => 1, :to => 5
|
85
|
+
# undo 4
|
86
|
+
# undo :last => 10
|
87
|
+
def undo!(modifier, options_or_version=nil)
|
88
|
+
if (!options_or_version.is_a?(Hash))
|
89
|
+
version = get_version(options_or_version.to_i)
|
90
|
+
attrs = {}
|
91
|
+
fields = history_tracks.first.modified.keys
|
92
|
+
|
93
|
+
if history_trackable_options[:on] == :all
|
94
|
+
|
95
|
+
fields.each do |k|
|
96
|
+
if !history_trackable_options[:except].include?(k)
|
97
|
+
if (!version.attributes[:modified].nil? && version.attributes[:modified].include?(k))
|
98
|
+
attrs[k] = version.attributes[:modified][k]
|
99
|
+
else
|
100
|
+
attrs[k] = version.attributes[k]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
else
|
106
|
+
|
107
|
+
fields.each do |k|
|
108
|
+
if history_trackable_options[:on].include?(k)
|
109
|
+
|
110
|
+
if (!version.attributes[:modified].nil? && version.attributes[:modified].include?(k))
|
111
|
+
attrs[k] = version.attributes[:modified][k]
|
112
|
+
else
|
113
|
+
attrs[k] = version.attributes[k]
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
if history_trackable_options[:remember_attachment]
|
122
|
+
|
123
|
+
addr = [Rails.root, '/public/', 'system/', 'store/', attachment_definitions.first.first.to_s, '/', id.to_s, '/', version.data_updated_at.to_i.to_s, '/'].join()
|
124
|
+
Dir.chdir(addr)
|
125
|
+
file_name = Dir.glob('*.*').first
|
126
|
+
|
127
|
+
attach = File.new(addr + file_name)
|
128
|
+
data = attach
|
129
|
+
|
130
|
+
attrs["data_file_name"] = file_name
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
self.update_attributes(attrs)
|
135
|
+
|
136
|
+
else
|
137
|
+
|
138
|
+
versions = get_versions_criteria(options_or_version).to_a
|
139
|
+
versions.sort!{|v1, v2| v2.version <=> v1.version}
|
140
|
+
|
141
|
+
versions.each do |v|
|
142
|
+
undo_attr = v.undo_attr(modifier)
|
143
|
+
self.attributes = v.undo_attr(modifier)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
save!
|
148
|
+
end
|
149
|
+
|
150
|
+
def redo!(modifier, options_or_version=nil)
|
151
|
+
versions = get_versions_criteria(options_or_version).to_a
|
152
|
+
versions.sort!{|v1, v2| v1.version <=> v2.version}
|
153
|
+
|
154
|
+
versions.each do |v|
|
155
|
+
redo_attr = v.redo_attr(modifier)
|
156
|
+
self.attributes = redo_attr
|
157
|
+
end
|
158
|
+
save!
|
159
|
+
end
|
160
|
+
|
161
|
+
def get_version(version)
|
162
|
+
track = history_tracks.first
|
163
|
+
|
164
|
+
track.modified.keys.each do |key|
|
165
|
+
track[key] = track.modified[key]
|
166
|
+
end
|
167
|
+
|
168
|
+
version -= 1
|
169
|
+
|
170
|
+
if (version < 0)
|
171
|
+
return nil
|
172
|
+
end
|
173
|
+
|
174
|
+
if (version >= history_tracks.count)
|
175
|
+
version = history_tracks.count-1
|
176
|
+
end
|
177
|
+
|
178
|
+
for i in 1..version
|
179
|
+
|
180
|
+
history_tracks[i].modified.keys.each do |key|
|
181
|
+
track[key] = history_tracks[i].modified[key]
|
182
|
+
end
|
183
|
+
|
184
|
+
track.modified = history_tracks[i].modified
|
185
|
+
track.original = history_tracks[i].original
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
track.version = version
|
190
|
+
track
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def get_versions_criteria(options_or_version)
|
196
|
+
if options_or_version.is_a? Hash
|
197
|
+
options = options_or_version
|
198
|
+
if options[:from] && options[:to]
|
199
|
+
lower = options[:from] >= options[:to] ? options[:to] : options[:from]
|
200
|
+
upper = options[:from] < options[:to] ? options[:to] : options[:from]
|
201
|
+
versions = history_tracks.where( :version.in => (lower .. upper).to_a )
|
202
|
+
elsif options[:last]
|
203
|
+
versions = history_tracks.limit( options[:last] )
|
204
|
+
else
|
205
|
+
raise "Invalid options, please specify (:from / :to) keys or :last key."
|
206
|
+
end
|
207
|
+
else
|
208
|
+
options_or_version = options_or_version.to_a if options_or_version.is_a?(Range)
|
209
|
+
version_field_name = history_trackable_options[:version_field]
|
210
|
+
version = options_or_version || self.attributes[version_field_name] || self.attributes[version_field_name.to_s]
|
211
|
+
version = [ version ].flatten
|
212
|
+
versions = history_tracks.where(:version.in => version)
|
213
|
+
end
|
214
|
+
versions.desc(:version)
|
215
|
+
end
|
216
|
+
|
217
|
+
def remember_only?(attributes)
|
218
|
+
|
219
|
+
return false if history_trackable_options[:except_but_remember].nil? || history_trackable_options[:except_but_remember].blank?
|
220
|
+
|
221
|
+
attributes.each do |k, v|
|
222
|
+
return false if !history_trackable_options[:except_but_remember].include?(k.to_s)
|
223
|
+
end
|
224
|
+
|
225
|
+
true
|
226
|
+
end
|
227
|
+
|
228
|
+
def fill_remember_only_attributes(for_update)
|
229
|
+
|
230
|
+
history_trackable_options[:except_but_remember].each do |key|
|
231
|
+
|
232
|
+
if (!for_update.include?(key))
|
233
|
+
for_update[key] = self[key]
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
|
240
|
+
def should_track_update?
|
241
|
+
for_update = modified_attributes_for_update
|
242
|
+
|
243
|
+
return false if (remember_only?(for_update))
|
244
|
+
|
245
|
+
track_history? && !for_update.blank?
|
246
|
+
end
|
247
|
+
|
248
|
+
def traverse_association_chain(node=self)
|
249
|
+
list = node._parent ? traverse_association_chain(node._parent) : []
|
250
|
+
list << association_hash(node)
|
251
|
+
list
|
252
|
+
end
|
253
|
+
|
254
|
+
def association_hash(node=self)
|
255
|
+
|
256
|
+
# We prefer to look up associations through the parent record because
|
257
|
+
# we're assured, through the object creation, it'll exist. Whereas we're not guarenteed
|
258
|
+
# the child to parent (embedded_in, belongs_to) relation will be defined
|
259
|
+
if node._parent
|
260
|
+
meta = _parent.relations.values.select do |relation|
|
261
|
+
relation.class_name == node.class.to_s
|
262
|
+
end.first
|
263
|
+
end
|
264
|
+
|
265
|
+
# if root node has no meta, and should use class name instead
|
266
|
+
name = meta ? meta.key.to_s : node.class.name
|
267
|
+
|
268
|
+
{ 'name' => name, 'id' => node.id}
|
269
|
+
end
|
270
|
+
|
271
|
+
def modified_attributes_for_update
|
272
|
+
@modified_attributes_for_update = changes
|
273
|
+
|
274
|
+
if history_trackable_options[:on] == :all
|
275
|
+
|
276
|
+
changes.each do |k, v|
|
277
|
+
@modified_attributes_for_update.delete(k) if history_trackable_options[:except].include?(k)
|
278
|
+
end
|
279
|
+
|
280
|
+
else
|
281
|
+
|
282
|
+
changes.each do |k, v|
|
283
|
+
@modified_attributes_for_update.delete(k) if !history_trackable_options[:on].include?(k)
|
284
|
+
end
|
285
|
+
|
286
|
+
end
|
287
|
+
|
288
|
+
@modified_attributes_for_update
|
289
|
+
end
|
290
|
+
|
291
|
+
def modified_attributes_for_create
|
292
|
+
@modified_attributes_for_create ||= attributes.inject({}) do |h, pair|
|
293
|
+
k,v = pair
|
294
|
+
h[k] = [nil, v]
|
295
|
+
h
|
296
|
+
end.reject do |k, v|
|
297
|
+
history_trackable_options[:except].include?(k)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def modified_attributes_for_destroy
|
302
|
+
@modified_attributes_for_destroy ||= attributes.inject({}) do |h, pair|
|
303
|
+
k,v = pair
|
304
|
+
h[k] = [nil, v]
|
305
|
+
h
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def history_tracker_attributes(method)
|
310
|
+
return @history_tracker_attributes if @history_tracker_attributes
|
311
|
+
|
312
|
+
@history_tracker_attributes = {
|
313
|
+
:association_chain => traverse_association_chain,
|
314
|
+
:scope => history_trackable_options[:scope],
|
315
|
+
:modifier => send(history_trackable_options[:modifier_field])
|
316
|
+
}
|
317
|
+
|
318
|
+
original, modified = transform_changes(case method
|
319
|
+
when :destroy then modified_attributes_for_destroy
|
320
|
+
when :create then modified_attributes_for_create
|
321
|
+
else modified_attributes_for_update
|
322
|
+
end)
|
323
|
+
|
324
|
+
fill_remember_only_attributes(modified)
|
325
|
+
|
326
|
+
@history_tracker_attributes[:original] = original
|
327
|
+
@history_tracker_attributes[:modified] = modified
|
328
|
+
@history_tracker_attributes
|
329
|
+
end
|
330
|
+
|
331
|
+
def track_update
|
332
|
+
return unless should_track_update?
|
333
|
+
current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
|
334
|
+
self.send("#{history_trackable_options[:version_field]}=", current_version)
|
335
|
+
Mongoid::History.tracker_class.create!(history_tracker_attributes(:update).merge(:version => current_version, :action => "update", :trackable => self))
|
336
|
+
clear_memoization
|
337
|
+
end
|
338
|
+
|
339
|
+
def track_create
|
340
|
+
return unless track_history?
|
341
|
+
current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
|
342
|
+
self.send("#{history_trackable_options[:version_field]}=", current_version)
|
343
|
+
Mongoid::History.tracker_class.create!(history_tracker_attributes(:create).merge(:version => current_version, :action => "create", :trackable => self))
|
344
|
+
clear_memoization
|
345
|
+
end
|
346
|
+
|
347
|
+
def track_destroy
|
348
|
+
return unless track_history?
|
349
|
+
current_version = (self.send(history_trackable_options[:version_field]) || 0 ) + 1
|
350
|
+
Mongoid::History.tracker_class.create!(history_tracker_attributes(:destroy).merge(:version => current_version, :action => "destroy", :trackable => self))
|
351
|
+
clear_memoization
|
352
|
+
end
|
353
|
+
|
354
|
+
def clear_memoization
|
355
|
+
@history_tracker_attributes = nil
|
356
|
+
@modified_attributes_for_create = nil
|
357
|
+
@modified_attributes_for_update = nil
|
358
|
+
@history_tracks = nil
|
359
|
+
end
|
360
|
+
|
361
|
+
def transform_changes(changes)
|
362
|
+
original = {}
|
363
|
+
modified = {}
|
364
|
+
changes.each_pair do |k, v|
|
365
|
+
o, m = v
|
366
|
+
original[k] = o unless o.nil?
|
367
|
+
modified[k] = m #unless m.nil?
|
368
|
+
end
|
369
|
+
|
370
|
+
return original.easy_diff modified
|
371
|
+
end
|
372
|
+
|
373
|
+
end
|
374
|
+
|
375
|
+
module SingletonMethods
|
376
|
+
def history_trackable_options
|
377
|
+
@history_trackable_options ||= Mongoid::History.trackable_class_options[self.collection_name.to_s.singularize.to_sym]
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|