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.
Files changed (78) hide show
  1. data/.gitignore +10 -0
  2. data/.travis.yml +5 -0
  3. data/Gemfile +2 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +663 -0
  6. data/Rakefile +15 -0
  7. data/lib/generators/paper_trail/USAGE +2 -0
  8. data/lib/generators/paper_trail/install_generator.rb +20 -0
  9. data/lib/generators/paper_trail/templates/add_object_changes_column_to_versions.rb +9 -0
  10. data/lib/generators/paper_trail/templates/create_versions.rb +19 -0
  11. data/lib/paper_trail.rb +87 -0
  12. data/lib/paper_trail/config.rb +11 -0
  13. data/lib/paper_trail/controller.rb +76 -0
  14. data/lib/paper_trail/has_paper_trail.rb +228 -0
  15. data/lib/paper_trail/version.rb +159 -0
  16. data/lib/paper_trail/version_number.rb +3 -0
  17. data/paper_trail.gemspec +24 -0
  18. data/test/dummy/Rakefile +7 -0
  19. data/test/dummy/app/controllers/application_controller.rb +17 -0
  20. data/test/dummy/app/controllers/test_controller.rb +5 -0
  21. data/test/dummy/app/controllers/widgets_controller.rb +23 -0
  22. data/test/dummy/app/helpers/application_helper.rb +2 -0
  23. data/test/dummy/app/models/animal.rb +4 -0
  24. data/test/dummy/app/models/article.rb +12 -0
  25. data/test/dummy/app/models/authorship.rb +5 -0
  26. data/test/dummy/app/models/book.rb +5 -0
  27. data/test/dummy/app/models/cat.rb +2 -0
  28. data/test/dummy/app/models/document.rb +4 -0
  29. data/test/dummy/app/models/dog.rb +2 -0
  30. data/test/dummy/app/models/elephant.rb +3 -0
  31. data/test/dummy/app/models/fluxor.rb +3 -0
  32. data/test/dummy/app/models/foo_widget.rb +2 -0
  33. data/test/dummy/app/models/person.rb +5 -0
  34. data/test/dummy/app/models/post.rb +4 -0
  35. data/test/dummy/app/models/song.rb +12 -0
  36. data/test/dummy/app/models/widget.rb +5 -0
  37. data/test/dummy/app/models/wotsit.rb +4 -0
  38. data/test/dummy/app/versions/post_version.rb +3 -0
  39. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  40. data/test/dummy/config.ru +4 -0
  41. data/test/dummy/config/application.rb +45 -0
  42. data/test/dummy/config/boot.rb +10 -0
  43. data/test/dummy/config/database.yml +22 -0
  44. data/test/dummy/config/environment.rb +5 -0
  45. data/test/dummy/config/environments/development.rb +26 -0
  46. data/test/dummy/config/environments/production.rb +49 -0
  47. data/test/dummy/config/environments/test.rb +35 -0
  48. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/test/dummy/config/initializers/inflections.rb +10 -0
  50. data/test/dummy/config/initializers/mime_types.rb +5 -0
  51. data/test/dummy/config/initializers/secret_token.rb +7 -0
  52. data/test/dummy/config/initializers/session_store.rb +8 -0
  53. data/test/dummy/config/locales/en.yml +5 -0
  54. data/test/dummy/config/routes.rb +3 -0
  55. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +120 -0
  56. data/test/dummy/db/schema.rb +103 -0
  57. data/test/dummy/db/test.sqlite3 +0 -0
  58. data/test/dummy/public/404.html +26 -0
  59. data/test/dummy/public/422.html +26 -0
  60. data/test/dummy/public/500.html +26 -0
  61. data/test/dummy/public/favicon.ico +0 -0
  62. data/test/dummy/public/javascripts/application.js +2 -0
  63. data/test/dummy/public/javascripts/controls.js +965 -0
  64. data/test/dummy/public/javascripts/dragdrop.js +974 -0
  65. data/test/dummy/public/javascripts/effects.js +1123 -0
  66. data/test/dummy/public/javascripts/prototype.js +6001 -0
  67. data/test/dummy/public/javascripts/rails.js +175 -0
  68. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  69. data/test/dummy/script/rails +6 -0
  70. data/test/functional/controller_test.rb +71 -0
  71. data/test/functional/thread_safety_test.rb +26 -0
  72. data/test/integration/navigation_test.rb +7 -0
  73. data/test/paper_trail_test.rb +27 -0
  74. data/test/support/integration_case.rb +5 -0
  75. data/test/test_helper.rb +49 -0
  76. data/test/unit/inheritance_column_test.rb +43 -0
  77. data/test/unit/model_test.rb +925 -0
  78. metadata +236 -0
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+
6
+ desc 'Test the paper_trail plugin.'
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'lib'
9
+ t.libs << 'test'
10
+ t.pattern = 'test/**/*_test.rb'
11
+ t.verbose = false
12
+ end
13
+
14
+ desc 'Default: run unit tests.'
15
+ task :default => :test
@@ -0,0 +1,2 @@
1
+ Description:
2
+ Generates (but does not run) a migration to add a versions table.
@@ -0,0 +1,20 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+ require 'rails/generators/active_record/migration'
4
+
5
+ module PaperTrail
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+ extend ActiveRecord::Generators::Migration
9
+
10
+ source_root File.expand_path('../templates', __FILE__)
11
+ class_option :with_changes, :type => :boolean, :default => false, :desc => "Store changeset (diff) with each version"
12
+
13
+ desc 'Generates (but does not run) a migration to add a versions table.'
14
+
15
+ def create_migration_file
16
+ migration_template 'create_versions.rb', 'db/migrate/create_versions.rb'
17
+ migration_template 'add_object_changes_column_to_versions.rb', 'db/migrate/add_object_changes_column_to_versions.rb' if options.with_changes?
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ class AddObjectChangesColumnToVersions < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :versions, :object_changes, :text
4
+ end
5
+
6
+ def self.down
7
+ remove_column :versions, :object_changes
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ class CreateVersions < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :versions do |t|
4
+ t.string :item_type, :null => false
5
+ t.integer :item_id, :null => false
6
+ t.string :event, :null => false
7
+ t.string :whodunnit
8
+ t.text :object
9
+ t.text :columns
10
+ t.datetime :created_at
11
+ end
12
+ add_index :versions, [:item_type, :item_id]
13
+ end
14
+
15
+ def self.down
16
+ remove_index :versions, [:item_type, :item_id]
17
+ drop_table :versions
18
+ end
19
+ end
@@ -0,0 +1,87 @@
1
+ require 'singleton'
2
+ require 'yaml'
3
+
4
+ require 'paper_trail/config'
5
+ require 'paper_trail/controller'
6
+ require 'paper_trail/has_paper_trail'
7
+ require 'paper_trail/version'
8
+
9
+ # PaperTrail's module methods can be called in both models and controllers.
10
+ module PaperTrail
11
+
12
+ # Switches PaperTrail on or off.
13
+ def self.enabled=(value)
14
+ PaperTrail.config.enabled = value
15
+ end
16
+
17
+ # Returns `true` if PaperTrail is on, `false` otherwise.
18
+ # PaperTrail is enabled by default.
19
+ def self.enabled?
20
+ !!PaperTrail.config.enabled
21
+ end
22
+
23
+ # Returns `true` if PaperTrail is enabled for the request, `false` otherwise.
24
+ #
25
+ # See `PaperTrail::Controller#paper_trail_enabled_for_controller`.
26
+ def self.enabled_for_controller?
27
+ !!paper_trail_store[:request_enabled_for_controller]
28
+ end
29
+
30
+ # Sets whether PaperTrail is enabled or disabled for the current request.
31
+ def self.enabled_for_controller=(value)
32
+ paper_trail_store[:request_enabled_for_controller] = value
33
+ end
34
+
35
+ # Returns who is reponsible for any changes that occur.
36
+ def self.whodunnit
37
+ paper_trail_store[:whodunnit]
38
+ end
39
+
40
+ # Sets who is responsible for any changes that occur.
41
+ # You would normally use this in a migration or on the console,
42
+ # when working with models directly. In a controller it is set
43
+ # automatically to the `current_user`.
44
+ def self.whodunnit=(value)
45
+ paper_trail_store[:whodunnit] = value
46
+ end
47
+
48
+ # Returns any information from the controller that you want
49
+ # PaperTrail to store.
50
+ #
51
+ # See `PaperTrail::Controller#info_for_paper_trail`.
52
+ def self.controller_info
53
+ paper_trail_store[:controller_info]
54
+ end
55
+
56
+ # Sets any information from the controller that you want PaperTrail
57
+ # to store. By default this is set automatically by a before filter.
58
+ def self.controller_info=(value)
59
+ paper_trail_store[:controller_info] = value
60
+ end
61
+
62
+
63
+ private
64
+
65
+ # Thread-safe hash to hold PaperTrail's data.
66
+ # Initializing with needed default values.
67
+ def self.paper_trail_store
68
+ Thread.current[:paper_trail] ||= {
69
+ :request_enabled_for_controller => true
70
+ }
71
+ end
72
+
73
+ # Returns PaperTrail's configuration object.
74
+ def self.config
75
+ @@config ||= PaperTrail::Config.instance
76
+ end
77
+
78
+ end
79
+
80
+
81
+ ActiveSupport.on_load(:active_record) do
82
+ include PaperTrail::Model
83
+ end
84
+
85
+ ActiveSupport.on_load(:action_controller) do
86
+ include PaperTrail::Controller
87
+ end
@@ -0,0 +1,11 @@
1
+ module PaperTrail
2
+ class Config
3
+ include Singleton
4
+ attr_accessor :enabled
5
+
6
+ def initialize
7
+ # Indicates whether PaperTrail is on or off.
8
+ @enabled = true
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,76 @@
1
+ module PaperTrail
2
+ module Controller
3
+
4
+ def self.included(base)
5
+ base.before_filter :set_paper_trail_whodunnit
6
+ base.before_filter :set_paper_trail_controller_info
7
+ base.before_filter :set_paper_trail_enabled_for_controller
8
+ end
9
+
10
+ protected
11
+
12
+ # Returns the user who is responsible for any changes that occur.
13
+ # By default this calls `current_user` and returns the result.
14
+ #
15
+ # Override this method in your controller to call a different
16
+ # method, e.g. `current_person`, or anything you like.
17
+ def user_for_paper_trail
18
+ current_user rescue nil
19
+ end
20
+
21
+ # Returns any information about the controller or request that you
22
+ # want PaperTrail to store alongside any changes that occur. By
23
+ # default this returns an empty hash.
24
+ #
25
+ # Override this method in your controller to return a hash of any
26
+ # information you need. The hash's keys must correspond to columns
27
+ # in your `versions` table, so don't forget to add any new columns
28
+ # you need.
29
+ #
30
+ # For example:
31
+ #
32
+ # {:ip => request.remote_ip, :user_agent => request.user_agent}
33
+ #
34
+ # The columns `ip` and `user_agent` must exist in your `versions` # table.
35
+ #
36
+ # Use the `:meta` option to `PaperTrail::Model::ClassMethods.has_paper_trail`
37
+ # to store any extra model-level data you need.
38
+ def info_for_paper_trail
39
+ {}
40
+ end
41
+
42
+ # Returns `true` (default) or `false` depending on whether PaperTrail should
43
+ # be active for the current request.
44
+ #
45
+ # Override this method in your controller to specify when PaperTrail should
46
+ # be off.
47
+ def paper_trail_enabled_for_controller
48
+ true
49
+ end
50
+
51
+ private
52
+
53
+ # Tells PaperTrail whether versions should be saved in the current request.
54
+ def set_paper_trail_enabled_for_controller
55
+ ::PaperTrail.enabled_for_controller = paper_trail_enabled_for_controller
56
+ end
57
+
58
+ # Tells PaperTrail who is responsible for any changes that occur.
59
+ def set_paper_trail_whodunnit
60
+ ::PaperTrail.whodunnit = user_for_paper_trail
61
+ end
62
+
63
+ # DEPRECATED: please use `set_paper_trail_whodunnit` instead.
64
+ def set_whodunnit
65
+ logger.warn '[PaperTrail]: the `set_whodunnit` controller method has been deprecated. Please rename to `set_paper_trail_whodunnit`.'
66
+ set_paper_trail_whodunnit
67
+ end
68
+
69
+ # Tells PaperTrail any information from the controller you want
70
+ # to store alongside any changes that occur.
71
+ def set_paper_trail_controller_info
72
+ ::PaperTrail.controller_info = info_for_paper_trail
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,228 @@
1
+ module PaperTrail
2
+ module Model
3
+
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ end
7
+
8
+
9
+ module ClassMethods
10
+ # Declare this in your model to track every create, update, and destroy. Each version of
11
+ # the model is available in the `versions` association.
12
+ #
13
+ # Options:
14
+ # :on the events to track (optional; defaults to all of them). Set to an array of
15
+ # `:create`, `:update`, `:destroy` as desired.
16
+ # :class_name the name of a custom Version class. This class should inherit from Version.
17
+ # :ignore an array of attributes for which a new `Version` will not be created if only they change.
18
+ # :only inverse of `ignore` - a new `Version` will be created only for these attributes if supplied
19
+ # :meta a hash of extra data to store. You must add a column to the `versions` table for each key.
20
+ # Values are objects or procs (which are called with `self`, i.e. the model with the paper
21
+ # trail). See `PaperTrail::Controller.info_for_paper_trail` for how to store data from
22
+ # the controller.
23
+ # :versions the name to use for the versions association. Default is `:versions`.
24
+ def has_paper_trail(options = {})
25
+ # Lazily include the instance methods so we don't clutter up
26
+ # any more ActiveRecord models than we have to.
27
+ send :include, InstanceMethods
28
+
29
+ # The version this instance was reified from.
30
+ attr_accessor :version
31
+
32
+ class_attribute :version_class_name
33
+ self.version_class_name = options[:class_name] || 'Version'
34
+
35
+ class_attribute :ignore
36
+ self.ignore = ([options[:ignore]].flatten.compact || []).map &:to_s
37
+
38
+ class_attribute :only
39
+ self.only = ([options[:only]].flatten.compact || []).map &:to_s
40
+
41
+ class_attribute :meta
42
+ self.meta = options[:meta] || {}
43
+
44
+ class_attribute :track_columns
45
+ self.track_columns = options[:track_columns] || true
46
+
47
+ class_attribute :ignored_columns
48
+ self.ignored_columns = options[:ignored_columns] || ['id', 'updated_at', 'created_at']
49
+
50
+ class_attribute :paper_trail_enabled_for_model
51
+ self.paper_trail_enabled_for_model = true
52
+
53
+ class_attribute :versions_association_name
54
+ self.versions_association_name = options[:versions] || :versions
55
+
56
+ has_many self.versions_association_name,
57
+ :class_name => version_class_name,
58
+ :as => :item,
59
+ :order => "created_at ASC, #{self.version_class_name.constantize.primary_key} ASC"
60
+
61
+ after_create :record_create if !options[:on] || options[:on].include?(:create)
62
+ before_update :record_update if !options[:on] || options[:on].include?(:update)
63
+ after_destroy :record_destroy if !options[:on] || options[:on].include?(:destroy)
64
+ end
65
+
66
+ # Switches PaperTrail off for this class.
67
+ def paper_trail_off
68
+ self.paper_trail_enabled_for_model = false
69
+ end
70
+
71
+ # Switches PaperTrail on for this class.
72
+ def paper_trail_on
73
+ self.paper_trail_enabled_for_model = true
74
+ end
75
+ end
76
+
77
+ # Wrap the following methods in a module so we can include them only in the
78
+ # ActiveRecord models that declare `has_paper_trail`.
79
+ module InstanceMethods
80
+ # Returns true if this instance is the current, live one;
81
+ # returns false if this instance came from a previous version.
82
+ def live?
83
+ version.nil?
84
+ end
85
+
86
+ # Returns who put the object into its current state.
87
+ def originator
88
+ version_class.with_item_keys(self.class.name, id).last.try :whodunnit
89
+ end
90
+
91
+ # Returns the object (not a Version) as it was at the given timestamp.
92
+ def version_at(timestamp, reify_options={})
93
+ # Because a version stores how its object looked *before* the change,
94
+ # we need to look for the first version created *after* the timestamp.
95
+ version = send(self.class.versions_association_name).after(timestamp).first
96
+ version ? version.reify(reify_options) : self
97
+ end
98
+
99
+ # Returns the object (not a Version) as it was most recently.
100
+ def previous_version
101
+ preceding_version = version ? version.previous : send(self.class.versions_association_name).last
102
+ preceding_version.try :reify
103
+ end
104
+
105
+ # Returns the object (not a Version) as it became next.
106
+ def next_version
107
+ # NOTE: if self (the item) was not reified from a version, i.e. it is the
108
+ # "live" item, we return nil. Perhaps we should return self instead?
109
+ subsequent_version = version ? version.next : nil
110
+ subsequent_version.reify if subsequent_version
111
+ end
112
+
113
+ # Executes the given method or block without creating a new version.
114
+ def without_versioning(method = nil)
115
+ paper_trail_was_enabled = self.paper_trail_enabled_for_model
116
+ self.class.paper_trail_off
117
+ method ? method.to_proc.call(self) : yield
118
+ ensure
119
+ self.class.paper_trail_on if paper_trail_was_enabled
120
+ end
121
+
122
+ private
123
+
124
+ def version_class
125
+ version_class_name.constantize
126
+ end
127
+
128
+ def record_create
129
+ if switched_on?
130
+ send(self.class.versions_association_name).create merge_metadata(:event => 'create', :whodunnit => PaperTrail.whodunnit)
131
+ end
132
+ end
133
+
134
+ def record_update
135
+ if switched_on? && changed_notably?
136
+ data = {
137
+ :event => 'update',
138
+ :object => object_to_string(item_before_change),
139
+ :whodunnit => PaperTrail.whodunnit
140
+ }
141
+
142
+ if version_class.column_names.include? 'object_changes'
143
+ # The double negative (reject, !include?) preserves the hash structure of self.changes.
144
+ data[:object_changes] = self.changes.reject do |key, value|
145
+ !notably_changed.include?(key)
146
+ end.to_yaml
147
+ end
148
+ send(self.class.versions_association_name).build merge_metadata(data)
149
+ end
150
+ end
151
+
152
+ def record_destroy
153
+ if switched_on? and not new_record?
154
+ version_class.create merge_metadata(:item_id => self.id,
155
+ :item_type => self.class.base_class.name,
156
+ :event => 'destroy',
157
+ :object => object_to_string(item_before_change),
158
+ :whodunnit => PaperTrail.whodunnit)
159
+ end
160
+ send(self.class.versions_association_name).send :load_target
161
+ end
162
+
163
+ def merge_metadata(data)
164
+ # First we merge the model-level metadata in `meta`.
165
+ meta.each do |k,v|
166
+ data[k] =
167
+ if v.respond_to?(:call)
168
+ v.call(self)
169
+ elsif v.is_a?(Symbol) && respond_to?(v)
170
+ send(v)
171
+ else
172
+ v
173
+ end
174
+ end
175
+ # Second we merge any extra data from the controller (if available).
176
+ data.merge(PaperTrail.controller_info || {})
177
+
178
+ column_changes = {}
179
+ change_info = changes.dup
180
+ change_info.reject! { |column_name, change_values|
181
+ self.class.ignored_columns.include?(column_name)
182
+ }
183
+
184
+ change_info.each do |column_name, change_values|
185
+ column_changes[column_name.to_sym] = {
186
+ :after => change_values[1],
187
+ :before => change_values[0]
188
+ }
189
+ end
190
+
191
+ data.merge(:columns => column_changes)
192
+ end
193
+
194
+ def item_before_change
195
+ previous = self.dup
196
+ # `dup` clears timestamps so we add them back.
197
+ all_timestamp_attributes.each do |column|
198
+ previous[column] = send(column) if respond_to?(column) && !send(column).nil?
199
+ end
200
+ previous.tap do |prev|
201
+ prev.id = id
202
+ changed_attributes.each { |attr, before| prev[attr] = before }
203
+ end
204
+ end
205
+
206
+ def object_to_string(object)
207
+ object.attributes.to_yaml
208
+ end
209
+
210
+ def changed_notably?
211
+ notably_changed.any?
212
+ end
213
+
214
+ def notably_changed
215
+ self.class.only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & self.class.only)
216
+ end
217
+
218
+ def changed_and_not_ignored
219
+ changed - self.class.ignore
220
+ end
221
+
222
+ def switched_on?
223
+ PaperTrail.enabled? && PaperTrail.enabled_for_controller? && self.class.paper_trail_enabled_for_model
224
+ end
225
+ end
226
+
227
+ end
228
+ end