bkwld-paper_trail 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +10 -0
- data/.travis.yml +5 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +20 -0
- data/README.md +663 -0
- data/Rakefile +15 -0
- data/lib/generators/paper_trail/USAGE +2 -0
- data/lib/generators/paper_trail/install_generator.rb +20 -0
- data/lib/generators/paper_trail/templates/add_object_changes_column_to_versions.rb +9 -0
- data/lib/generators/paper_trail/templates/create_versions.rb +19 -0
- data/lib/paper_trail.rb +87 -0
- data/lib/paper_trail/config.rb +11 -0
- data/lib/paper_trail/controller.rb +76 -0
- data/lib/paper_trail/has_paper_trail.rb +228 -0
- data/lib/paper_trail/version.rb +159 -0
- data/lib/paper_trail/version_number.rb +3 -0
- data/paper_trail.gemspec +24 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/controllers/application_controller.rb +17 -0
- data/test/dummy/app/controllers/test_controller.rb +5 -0
- data/test/dummy/app/controllers/widgets_controller.rb +23 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/animal.rb +4 -0
- data/test/dummy/app/models/article.rb +12 -0
- data/test/dummy/app/models/authorship.rb +5 -0
- data/test/dummy/app/models/book.rb +5 -0
- data/test/dummy/app/models/cat.rb +2 -0
- data/test/dummy/app/models/document.rb +4 -0
- data/test/dummy/app/models/dog.rb +2 -0
- data/test/dummy/app/models/elephant.rb +3 -0
- data/test/dummy/app/models/fluxor.rb +3 -0
- data/test/dummy/app/models/foo_widget.rb +2 -0
- data/test/dummy/app/models/person.rb +5 -0
- data/test/dummy/app/models/post.rb +4 -0
- data/test/dummy/app/models/song.rb +12 -0
- data/test/dummy/app/models/widget.rb +5 -0
- data/test/dummy/app/models/wotsit.rb +4 -0
- data/test/dummy/app/versions/post_version.rb +3 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +45 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +22 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +26 -0
- data/test/dummy/config/environments/production.rb +49 -0
- data/test/dummy/config/environments/test.rb +35 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +3 -0
- data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +120 -0
- data/test/dummy/db/schema.rb +103 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/javascripts/application.js +2 -0
- data/test/dummy/public/javascripts/controls.js +965 -0
- data/test/dummy/public/javascripts/dragdrop.js +974 -0
- data/test/dummy/public/javascripts/effects.js +1123 -0
- data/test/dummy/public/javascripts/prototype.js +6001 -0
- data/test/dummy/public/javascripts/rails.js +175 -0
- data/test/dummy/public/stylesheets/.gitkeep +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/functional/controller_test.rb +71 -0
- data/test/functional/thread_safety_test.rb +26 -0
- data/test/integration/navigation_test.rb +7 -0
- data/test/paper_trail_test.rb +27 -0
- data/test/support/integration_case.rb +5 -0
- data/test/test_helper.rb +49 -0
- data/test/unit/inheritance_column_test.rb +43 -0
- data/test/unit/model_test.rb +925 -0
- metadata +236 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
class Version < ActiveRecord::Base
|
|
2
|
+
belongs_to :item, :polymorphic => true
|
|
3
|
+
validates_presence_of :event
|
|
4
|
+
serialize :columns
|
|
5
|
+
|
|
6
|
+
def self.with_item_keys(item_type, item_id)
|
|
7
|
+
scoped(:conditions => { :item_type => item_type, :item_id => item_id })
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
scope :subsequent, lambda { |version|
|
|
11
|
+
where(["#{self.primary_key} > ?", version.is_a?(self) ? version.id : version]).order("#{self.primary_key} ASC")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
scope :preceding, lambda { |version|
|
|
15
|
+
where(["#{self.primary_key} < ?", version.is_a?(self) ? version.id : version]).order("#{self.primary_key} DESC")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
scope :after, lambda { |timestamp|
|
|
19
|
+
# TODO: is this :order necessary, considering its presence on the has_many :versions association?
|
|
20
|
+
where(['created_at > ?', timestamp]).order("created_at ASC, #{self.primary_key} ASC")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Restore the item from this version.
|
|
24
|
+
#
|
|
25
|
+
# This will automatically restore all :has_one associations as they were "at the time",
|
|
26
|
+
# if they are also being versioned by PaperTrail. NOTE: this isn't always guaranteed
|
|
27
|
+
# to work so you can either change the lookback period (from the default 3 seconds) or
|
|
28
|
+
# opt out.
|
|
29
|
+
#
|
|
30
|
+
# Options:
|
|
31
|
+
# +:has_one+ set to `false` to opt out of has_one reification.
|
|
32
|
+
# set to a float to change the lookback time (check whether your db supports
|
|
33
|
+
# sub-second datetimes if you want them).
|
|
34
|
+
def reify(options = {})
|
|
35
|
+
without_identity_map do
|
|
36
|
+
options[:has_one] = 3 if options[:has_one] == true
|
|
37
|
+
options.reverse_merge! :has_one => false
|
|
38
|
+
|
|
39
|
+
unless object.nil?
|
|
40
|
+
attrs = YAML::load object
|
|
41
|
+
|
|
42
|
+
# Normally a polymorphic belongs_to relationship allows us
|
|
43
|
+
# to get the object we belong to by calling, in this case,
|
|
44
|
+
# +item+. However this returns nil if +item+ has been
|
|
45
|
+
# destroyed, and we need to be able to retrieve destroyed
|
|
46
|
+
# objects.
|
|
47
|
+
#
|
|
48
|
+
# In this situation we constantize the +item_type+ to get hold of
|
|
49
|
+
# the class...except when the stored object's attributes
|
|
50
|
+
# include a +type+ key. If this is the case, the object
|
|
51
|
+
# we belong to is using single table inheritance and the
|
|
52
|
+
# +item_type+ will be the base class, not the actual subclass.
|
|
53
|
+
# If +type+ is present but empty, the class is the base class.
|
|
54
|
+
|
|
55
|
+
if item
|
|
56
|
+
model = item
|
|
57
|
+
else
|
|
58
|
+
inheritance_column_name = item_type.constantize.inheritance_column
|
|
59
|
+
class_name = attrs[inheritance_column_name].blank? ? item_type : attrs[inheritance_column_name]
|
|
60
|
+
klass = class_name.constantize
|
|
61
|
+
model = klass.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
attrs.each do |k, v|
|
|
65
|
+
begin
|
|
66
|
+
model.send :write_attribute, k.to_sym , v
|
|
67
|
+
rescue NoMethodError
|
|
68
|
+
logger.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
model.version = self
|
|
73
|
+
|
|
74
|
+
unless options[:has_one] == false
|
|
75
|
+
reify_has_ones model, options[:has_one]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
model
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns what changed in this version of the item. Cf. `ActiveModel::Dirty#changes`.
|
|
84
|
+
# Returns nil if your `versions` table does not have an `object_changes` text column.
|
|
85
|
+
def changeset
|
|
86
|
+
if self.class.column_names.include? 'object_changes'
|
|
87
|
+
if changes = object_changes
|
|
88
|
+
HashWithIndifferentAccess[YAML::load(changes)]
|
|
89
|
+
else
|
|
90
|
+
{}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns who put the item into the state stored in this version.
|
|
96
|
+
def originator
|
|
97
|
+
previous.try :whodunnit
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns who changed the item from the state it had in this version.
|
|
101
|
+
# This is an alias for `whodunnit`.
|
|
102
|
+
def terminator
|
|
103
|
+
whodunnit
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def sibling_versions
|
|
107
|
+
self.class.with_item_keys(item_type, item_id)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def next
|
|
111
|
+
sibling_versions.subsequent(self).first
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def previous
|
|
115
|
+
sibling_versions.preceding(self).first
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def index
|
|
119
|
+
sibling_versions.select(:id).order("id ASC").map(&:id).index(self.id)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
# In Rails 3.1+, calling reify on a previous version confuses the
|
|
125
|
+
# IdentityMap, if enabled. This prevents insertion into the map.
|
|
126
|
+
def without_identity_map(&block)
|
|
127
|
+
if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
|
|
128
|
+
ActiveRecord::IdentityMap.without(&block)
|
|
129
|
+
else
|
|
130
|
+
block.call
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Restore the `model`'s has_one associations as they were when this version was
|
|
135
|
+
# superseded by the next (because that's what the user was looking at when they
|
|
136
|
+
# made the change).
|
|
137
|
+
#
|
|
138
|
+
# The `lookback` sets how many seconds before the model's change we go.
|
|
139
|
+
def reify_has_ones(model, lookback)
|
|
140
|
+
model.class.reflect_on_all_associations(:has_one).each do |assoc|
|
|
141
|
+
child = model.send assoc.name
|
|
142
|
+
if child.respond_to? :version_at
|
|
143
|
+
# N.B. we use version of the child as it was `lookback` seconds before the parent was updated.
|
|
144
|
+
# Ideally we want the version of the child as it was just before the parent was updated...
|
|
145
|
+
# but until PaperTrail knows which updates are "together" (e.g. parent and child being
|
|
146
|
+
# updated on the same form), it's impossible to tell when the overall update started;
|
|
147
|
+
# and therefore impossible to know when "just before" was.
|
|
148
|
+
if (child_as_it_was = child.version_at(created_at - lookback.seconds))
|
|
149
|
+
child_as_it_was.attributes.each do |k,v|
|
|
150
|
+
model.send(assoc.name).send :write_attribute, k.to_sym, v rescue nil
|
|
151
|
+
end
|
|
152
|
+
else
|
|
153
|
+
model.send "#{assoc.name}=", nil
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
end
|
data/paper_trail.gemspec
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
|
2
|
+
require 'paper_trail/version_number'
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |s|
|
|
5
|
+
s.name = 'bkwld-paper_trail'
|
|
6
|
+
s.version = PaperTrail::VERSION
|
|
7
|
+
s.summary = "Track changes to your models' data. Good for auditing or versioning."
|
|
8
|
+
s.description = s.summary
|
|
9
|
+
s.homepage = 'http://github.com/airblade/paper_trail'
|
|
10
|
+
s.authors = ['Andy Stewart']
|
|
11
|
+
s.email = 'boss@airbladesoftware.com'
|
|
12
|
+
|
|
13
|
+
s.files = `git ls-files`.split("\n")
|
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
16
|
+
s.require_paths = ['lib']
|
|
17
|
+
|
|
18
|
+
s.add_dependency 'rails', '~> 3'
|
|
19
|
+
|
|
20
|
+
s.add_development_dependency 'shoulda', '2.10.3'
|
|
21
|
+
s.add_development_dependency 'sqlite3-ruby', '~> 1.2'
|
|
22
|
+
s.add_development_dependency 'capybara', '>= 0.4.0'
|
|
23
|
+
s.add_development_dependency 'turn'
|
|
24
|
+
end
|
data/test/dummy/Rakefile
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
|
2
|
+
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
|
3
|
+
|
|
4
|
+
require File.expand_path('../config/application', __FILE__)
|
|
5
|
+
require 'rake'
|
|
6
|
+
|
|
7
|
+
Dummy::Application.load_tasks
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class ApplicationController < ActionController::Base
|
|
2
|
+
protect_from_forgery
|
|
3
|
+
|
|
4
|
+
def rescue_action(e)
|
|
5
|
+
raise e
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Returns id of hypothetical current user
|
|
9
|
+
def current_user
|
|
10
|
+
153
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def info_for_paper_trail
|
|
14
|
+
{:ip => request.remote_ip, :user_agent => request.user_agent}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class WidgetsController < ApplicationController
|
|
2
|
+
|
|
3
|
+
def paper_trail_enabled_for_controller
|
|
4
|
+
request.user_agent != 'Disable User-Agent'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def create
|
|
8
|
+
@widget = Widget.create params[:widget]
|
|
9
|
+
head :ok
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def update
|
|
13
|
+
@widget = Widget.find params[:id]
|
|
14
|
+
@widget.update_attributes params[:widget]
|
|
15
|
+
head :ok
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def destroy
|
|
19
|
+
@widget = Widget.find params[:id]
|
|
20
|
+
@widget.destroy
|
|
21
|
+
head :ok
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class Article < ActiveRecord::Base
|
|
2
|
+
has_paper_trail :ignore => :title,
|
|
3
|
+
:only => [:content],
|
|
4
|
+
:meta => {:answer => 42,
|
|
5
|
+
:action => :action_data_provider_method,
|
|
6
|
+
:question => Proc.new { "31 + 11 = #{31 + 11}" },
|
|
7
|
+
:article_id => Proc.new { |article| article.id } }
|
|
8
|
+
|
|
9
|
+
def action_data_provider_method
|
|
10
|
+
self.object_id.to_s
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Example from 'Overwriting default accessors' in ActiveRecord::Base.
|
|
2
|
+
class Song < ActiveRecord::Base
|
|
3
|
+
has_paper_trail
|
|
4
|
+
|
|
5
|
+
# Uses an integer of seconds to hold the length of the song
|
|
6
|
+
def length=(minutes)
|
|
7
|
+
write_attribute(:length, minutes.to_i * 60)
|
|
8
|
+
end
|
|
9
|
+
def length
|
|
10
|
+
read_attribute(:length) / 60
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require File.expand_path('../boot', __FILE__)
|
|
2
|
+
|
|
3
|
+
require "active_model/railtie"
|
|
4
|
+
require "active_record/railtie"
|
|
5
|
+
require "action_controller/railtie"
|
|
6
|
+
require "action_view/railtie"
|
|
7
|
+
require "action_mailer/railtie"
|
|
8
|
+
|
|
9
|
+
Bundler.require
|
|
10
|
+
require 'paper_trail'
|
|
11
|
+
|
|
12
|
+
module Dummy
|
|
13
|
+
class Application < Rails::Application
|
|
14
|
+
# Settings in config/environments/* take precedence over those specified here.
|
|
15
|
+
# Application configuration should go into files in config/initializers
|
|
16
|
+
# -- all .rb files in that directory are automatically loaded.
|
|
17
|
+
|
|
18
|
+
# Custom directories with classes and modules you want to be autoloadable.
|
|
19
|
+
# config.autoload_paths += %W(#{config.root}/extras)
|
|
20
|
+
|
|
21
|
+
# Only load the plugins named here, in the order given (default is alphabetical).
|
|
22
|
+
# :all can be used as a placeholder for all plugins not explicitly named.
|
|
23
|
+
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
|
|
24
|
+
|
|
25
|
+
# Activate observers that should always be running.
|
|
26
|
+
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
|
|
27
|
+
|
|
28
|
+
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
|
|
29
|
+
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
|
|
30
|
+
# config.time_zone = 'Central Time (US & Canada)'
|
|
31
|
+
|
|
32
|
+
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
|
33
|
+
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
|
34
|
+
# config.i18n.default_locale = :de
|
|
35
|
+
|
|
36
|
+
# JavaScript files you want as :defaults (application.js is always included).
|
|
37
|
+
# config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
|
|
38
|
+
|
|
39
|
+
# Configure the default encoding used in templates for Ruby 1.9.
|
|
40
|
+
config.encoding = "utf-8"
|
|
41
|
+
|
|
42
|
+
# Configure sensitive parameters which will be filtered from the log file.
|
|
43
|
+
config.filter_parameters += [:password]
|
|
44
|
+
end
|
|
45
|
+
end
|