audited 4.3.0 → 4.6.0

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.

Potentially problematic release.


This version of audited might be problematic. Click here for more details.

Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +19 -2
  3. data/Appraisals +8 -2
  4. data/CHANGELOG.md +260 -0
  5. data/README.md +32 -12
  6. data/gemfiles/rails40.gemfile +1 -1
  7. data/gemfiles/rails41.gemfile +1 -1
  8. data/gemfiles/rails42.gemfile +1 -1
  9. data/gemfiles/rails50.gemfile +1 -2
  10. data/gemfiles/rails51.gemfile +7 -0
  11. data/gemfiles/rails52.gemfile +8 -0
  12. data/lib/audited.rb +6 -4
  13. data/lib/audited/audit.rb +57 -8
  14. data/lib/audited/auditor.rb +54 -59
  15. data/lib/audited/rspec_matchers.rb +2 -2
  16. data/lib/audited/sweeper.rb +18 -29
  17. data/lib/audited/version.rb +1 -1
  18. data/lib/generators/audited/install_generator.rb +5 -0
  19. data/lib/generators/audited/migration_helper.rb +9 -0
  20. data/lib/generators/audited/templates/add_association_to_audits.rb +1 -1
  21. data/lib/generators/audited/templates/add_comment_to_audits.rb +1 -1
  22. data/lib/generators/audited/templates/add_remote_address_to_audits.rb +1 -1
  23. data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +1 -1
  24. data/lib/generators/audited/templates/install.rb +5 -5
  25. data/lib/generators/audited/templates/rename_association_to_associated.rb +1 -1
  26. data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +1 -1
  27. data/lib/generators/audited/templates/rename_parent_to_association.rb +1 -1
  28. data/lib/generators/audited/templates/revert_polymorphic_indexes_order.rb +20 -0
  29. data/lib/generators/audited/upgrade_generator.rb +7 -0
  30. data/spec/audited/audit_spec.rb +71 -2
  31. data/spec/audited/auditor_spec.rb +69 -3
  32. data/spec/audited/sweeper_spec.rb +24 -6
  33. data/spec/audited_spec_helpers.rb +1 -1
  34. data/spec/spec_helper.rb +1 -1
  35. data/spec/support/active_record/models.rb +2 -1
  36. data/spec/support/active_record/postgres/1_change_audited_changes_type_to_json.rb +12 -0
  37. data/spec/support/active_record/postgres/2_change_audited_changes_type_to_jsonb.rb +12 -0
  38. data/spec/support/active_record/schema.rb +1 -0
  39. data/test/install_generator_test.rb +49 -3
  40. data/test/upgrade_generator_test.rb +16 -1
  41. metadata +14 -22
  42. data/CHANGELOG +0 -34
@@ -3,6 +3,5 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rails", "~> 5.0.0"
6
- gem "rails-observers", :github => "rails/rails-observers", :branch => "master"
7
6
 
8
- gemspec :name => "audited", :path => "../"
7
+ gemspec name: "audited", path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 5.1.4"
6
+
7
+ gemspec name: "audited", path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", ">= 5.2.0.beta2", "< 5.3"
6
+ gem "mysql2", "~> 0.4.4"
7
+
8
+ gemspec name: "audited", path: "../"
@@ -1,19 +1,21 @@
1
- require 'rails/observers/active_model/active_model'
2
1
  require 'active_record'
3
2
 
4
3
  module Audited
5
4
  class << self
6
5
  attr_accessor :ignored_attributes, :current_user_method
6
+ attr_writer :audit_class
7
7
 
8
- # Deprecate audit_class accessors in preperation of their removal
9
8
  def audit_class
10
- Audited::Audit
9
+ @audit_class ||= Audit
11
10
  end
12
- deprecate audit_class: "Audited.audit_class is now always Audited::Audit. This method will be removed."
13
11
 
14
12
  def store
15
13
  Thread.current[:audited_store] ||= {}
16
14
  end
15
+
16
+ def config
17
+ yield(self)
18
+ end
17
19
  end
18
20
 
19
21
  @ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
@@ -6,25 +6,44 @@ module Audited
6
6
  # * <tt>auditable</tt>: the ActiveRecord model that was changed
7
7
  # * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model
8
8
  # * <tt>action</tt>: one of create, update, or delete
9
- # * <tt>audited_changes</tt>: a serialized hash of all the changes
9
+ # * <tt>audited_changes</tt>: a hash of all the changes
10
10
  # * <tt>comment</tt>: a comment set with the audit
11
11
  # * <tt>version</tt>: the version of the model
12
12
  # * <tt>request_uuid</tt>: a uuid based that allows audits from the same controller request
13
13
  # * <tt>created_at</tt>: Time that the change was performed
14
14
  #
15
- class Audit < ::ActiveRecord::Base
16
- include ActiveModel::Observing
17
15
 
16
+ class YAMLIfTextColumnType
17
+ class << self
18
+ def load(obj)
19
+ if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
20
+ ActiveRecord::Coders::YAMLColumn.new(Object).load(obj)
21
+ else
22
+ obj
23
+ end
24
+ end
25
+
26
+ def dump(obj)
27
+ if Audited.audit_class.columns_hash["audited_changes"].type.to_s == "text"
28
+ ActiveRecord::Coders::YAMLColumn.new(Object).dump(obj)
29
+ else
30
+ obj
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ class Audit < ::ActiveRecord::Base
18
37
  belongs_to :auditable, polymorphic: true
19
38
  belongs_to :user, polymorphic: true
20
39
  belongs_to :associated, polymorphic: true
21
40
 
22
- before_create :set_version_number, :set_audit_user, :set_request_uuid
41
+ before_create :set_version_number, :set_audit_user, :set_request_uuid, :set_remote_address
23
42
 
24
43
  cattr_accessor :audited_class_names
25
44
  self.audited_class_names = Set.new
26
45
 
27
- serialize :audited_changes
46
+ serialize :audited_changes, YAMLIfTextColumnType
28
47
 
29
48
  scope :ascending, ->{ reorder(version: :asc) }
30
49
  scope :descending, ->{ reorder(version: :desc)}
@@ -67,6 +86,25 @@ module Audited
67
86
  end
68
87
  end
69
88
 
89
+ # Allows user to undo changes
90
+ def undo
91
+ model = self.auditable_type.constantize
92
+ if action == 'create'
93
+ # destroys a newly created record
94
+ model.find(auditable_id).destroy!
95
+ elsif action == 'destroy'
96
+ # creates a new record with the destroyed record attributes
97
+ model.create(audited_changes)
98
+ else
99
+ # changes back attributes
100
+ audited_object = model.find(auditable_id)
101
+ self.audited_changes.each do |k, v|
102
+ audited_object[k] = v[0]
103
+ end
104
+ audited_object.save
105
+ end
106
+ end
107
+
70
108
  # Allows user to be set to either a string or an ActiveRecord object
71
109
  # @private
72
110
  def user_as_string=(user)
@@ -95,10 +133,10 @@ module Audited
95
133
  # by +user+. This method is hopefully threadsafe, making it ideal
96
134
  # for background operations that require audit information.
97
135
  def self.as_user(user, &block)
98
- Thread.current[:audited_user] = user
136
+ ::Audited.store[:audited_user] = user
99
137
  yield
100
138
  ensure
101
- Thread.current[:audited_user] = nil
139
+ ::Audited.store[:audited_user] = nil
102
140
  end
103
141
 
104
142
  # @private
@@ -125,6 +163,11 @@ module Audited
125
163
  record
126
164
  end
127
165
 
166
+ # use created_at as timestamp cache key
167
+ def self.collection_cache_key(collection = all, timestamp_column = :created_at)
168
+ super(collection, :created_at)
169
+ end
170
+
128
171
  private
129
172
 
130
173
  def set_version_number
@@ -133,12 +176,18 @@ module Audited
133
176
  end
134
177
 
135
178
  def set_audit_user
136
- self.user = Thread.current[:audited_user] if Thread.current[:audited_user]
179
+ self.user ||= ::Audited.store[:audited_user] # from .as_user
180
+ self.user ||= ::Audited.store[:current_user].try!(:call) # from Sweeper
137
181
  nil # prevent stopping callback chains
138
182
  end
139
183
 
140
184
  def set_request_uuid
185
+ self.request_uuid ||= ::Audited.store[:current_request_uuid]
141
186
  self.request_uuid ||= SecureRandom.uuid
142
187
  end
188
+
189
+ def set_remote_address
190
+ self.remote_address ||= ::Audited.store[:current_remote_address]
191
+ end
143
192
  end
144
193
  end
@@ -38,26 +38,29 @@ module Audited
38
38
  # don't allow multiple calls
39
39
  return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
40
40
 
41
+ extend Audited::Auditor::AuditedClassMethods
42
+ include Audited::Auditor::AuditedInstanceMethods
43
+
41
44
  class_attribute :audit_associated_with, instance_writer: false
42
45
  class_attribute :audited_options, instance_writer: false
46
+ attr_accessor :version, :audit_comment
43
47
 
44
48
  self.audited_options = options
45
- self.audit_associated_with = options[:associated_with]
49
+ normalize_audited_options
46
50
 
47
- if options[:comment_required]
51
+ self.audit_associated_with = audited_options[:associated_with]
52
+
53
+ if audited_options[:comment_required]
48
54
  validates_presence_of :audit_comment, if: :auditing_enabled
49
55
  before_destroy :require_comment
50
56
  end
51
57
 
52
- attr_accessor :audit_comment
53
-
54
- has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audit.name
55
- Audit.audited_class_names << to_s
58
+ has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name
59
+ Audited.audit_class.audited_class_names << to_s
56
60
 
57
- on = Array(options[:on])
58
- after_create :audit_create if on.empty? || on.include?(:create)
59
- before_update :audit_update if on.empty? || on.include?(:update)
60
- before_destroy :audit_destroy if on.empty? || on.include?(:destroy)
61
+ after_create :audit_create if audited_options[:on].include?(:create)
62
+ before_update :audit_update if audited_options[:on].include?(:update)
63
+ before_destroy :audit_destroy if audited_options[:on].include?(:destroy)
61
64
 
62
65
  # Define and set after_audit and around_audit callbacks. This might be useful if you want
63
66
  # to notify a party after the audit has been created or if you want to access the newly-created
@@ -66,20 +69,11 @@ module Audited
66
69
  set_callback :audit, :after, :after_audit, if: lambda { respond_to?(:after_audit, true) }
67
70
  set_callback :audit, :around, :around_audit, if: lambda { respond_to?(:around_audit, true) }
68
71
 
69
- attr_accessor :version
70
-
71
- extend Audited::Auditor::AuditedClassMethods
72
- include Audited::Auditor::AuditedInstanceMethods
73
-
74
- self.auditing_enabled = true
72
+ enable_auditing
75
73
  end
76
74
 
77
75
  def has_associated_audits
78
- has_many :associated_audits, as: :associated, class_name: Audit.name
79
- end
80
-
81
- def default_ignored_attributes
82
- [primary_key, inheritance_column]
76
+ has_many :associated_audits, as: :associated, class_name: Audited.audit_class.name
83
77
  end
84
78
  end
85
79
 
@@ -109,22 +103,21 @@ module Audited
109
103
  def revisions(from_version = 1)
110
104
  audits = self.audits.from_version(from_version)
111
105
  return [] if audits.empty?
112
- revisions = []
113
- audits.each do |audit|
114
- revisions << audit.revision
115
- end
116
- revisions
106
+ audits.map(&:revision)
117
107
  end
118
108
 
119
109
  # Get a specific revision specified by the version number, or +:previous+
110
+ # Returns nil for versions greater than revisions count
120
111
  def revision(version)
121
- revision_with Audit.reconstruct_attributes(audits_to(version))
112
+ if version == :previous || self.audits.last.version >= version
113
+ revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
114
+ end
122
115
  end
123
116
 
124
117
  # Find the oldest revision recorded prior to the date/time provided.
125
118
  def revision_at(date_or_time)
126
119
  audits = self.audits.up_until(date_or_time)
127
- revision_with Audit.reconstruct_attributes(audits) unless audits.empty?
120
+ revision_with Audited.audit_class.reconstruct_attributes(audits) unless audits.empty?
128
121
  end
129
122
 
130
123
  # List of attributes that are audited.
@@ -132,16 +125,16 @@ module Audited
132
125
  attributes.except(*non_audited_columns)
133
126
  end
134
127
 
135
- def non_audited_columns
136
- self.class.non_audited_columns
137
- end
138
-
139
128
  protected
140
129
 
141
130
  def non_audited_columns
142
131
  self.class.non_audited_columns
143
132
  end
144
133
 
134
+ def audited_columns
135
+ self.class.audited_columns
136
+ end
137
+
145
138
  def revision_with(attributes)
146
139
  dup.tap do |revision|
147
140
  revision.id = id
@@ -152,7 +145,7 @@ module Audited
152
145
  revision.send :instance_variable_set, '@destroyed', false
153
146
  revision.send :instance_variable_set, '@_destroyed', false
154
147
  revision.send :instance_variable_set, '@marked_for_destruction', false
155
- Audit.assign_revision_attributes(revision, attributes)
148
+ Audited.audit_class.assign_revision_attributes(revision, attributes)
156
149
 
157
150
  # Remove any association proxies so that they will be recreated
158
151
  # and reference the correct object for this revision. The only way
@@ -175,17 +168,11 @@ module Audited
175
168
  private
176
169
 
177
170
  def audited_changes
178
- collection =
179
- if audited_options[:only]
180
- audited_columns = self.class.audited_columns.map(&:name)
181
- changed_attributes.slice(*audited_columns)
182
- else
183
- changed_attributes.except(*non_audited_columns)
184
- end
185
-
186
- collection.inject({}) do |changes, (attr, old_value)|
187
- changes[attr] = [old_value, self[attr]]
188
- changes
171
+ all_changes = respond_to?(:changes_to_save) ? changes_to_save : changes
172
+ if audited_options[:only].present?
173
+ all_changes.slice(*audited_columns)
174
+ else
175
+ all_changes.except(*non_audited_columns)
189
176
  end
190
177
  end
191
178
 
@@ -236,9 +223,6 @@ module Audited
236
223
  alias_method "#{attr_name}_callback".to_sym, attr_name
237
224
  end
238
225
 
239
- def empty_callback #:nodoc:
240
- end
241
-
242
226
  def auditing_enabled
243
227
  self.class.auditing_enabled
244
228
  end
@@ -251,20 +235,19 @@ module Audited
251
235
  module AuditedClassMethods
252
236
  # Returns an array of columns that are audited. See non_audited_columns
253
237
  def audited_columns
254
- columns.select {|c| !non_audited_columns.include?(c.name) }
238
+ @audited_columns ||= column_names - non_audited_columns
255
239
  end
256
240
 
241
+ # We have to calculate this here since column_names may not be available when `audited` is called
257
242
  def non_audited_columns
258
- @non_audited_columns ||= begin
259
- options = audited_options
260
- if options[:only]
261
- except = column_names - Array.wrap(options[:only]).flatten.map(&:to_s)
262
- else
263
- except = default_ignored_attributes + Audited.ignored_attributes
264
- except |= Array(options[:except]).collect(&:to_s) if options[:except]
265
- end
266
- except
267
- end
243
+ @non_audited_columns ||= audited_options[:only].present? ?
244
+ column_names - audited_options[:only] :
245
+ default_ignored_attributes | audited_options[:except]
246
+ end
247
+
248
+ def non_audited_columns=(columns)
249
+ @audited_columns = nil # reset cached audited columns on assignment
250
+ @non_audited_columns = columns.map(&:to_s)
268
251
  end
269
252
 
270
253
  # Executes the block with auditing disabled.
@@ -294,7 +277,7 @@ module Audited
294
277
  # convenience wrapper around
295
278
  # @see Audit#as_user.
296
279
  def audit_as(user, &block)
297
- Audit.as_user(user, &block)
280
+ Audited.audit_class.as_user(user, &block)
298
281
  end
299
282
 
300
283
  def auditing_enabled
@@ -304,6 +287,18 @@ module Audited
304
287
  def auditing_enabled=(val)
305
288
  Audited.store["#{table_name}_auditing_enabled"] = val
306
289
  end
290
+
291
+ protected
292
+ def default_ignored_attributes
293
+ [primary_key, inheritance_column] + Audited.ignored_attributes
294
+ end
295
+
296
+ def normalize_audited_options
297
+ audited_options[:on] = Array.wrap(audited_options[:on])
298
+ audited_options[:on] = [:create, :update, :destroy] if audited_options[:on].empty?
299
+ audited_options[:only] = Array.wrap(audited_options[:only]).map(&:to_s)
300
+ audited_options[:except] = Array.wrap(audited_options[:except]).map(&:to_s)
301
+ end
307
302
  end
308
303
  end
309
304
  end
@@ -117,7 +117,7 @@ module Audited
117
117
  except |= @options[:except].collect(&:to_s) if @options[:except]
118
118
  end
119
119
 
120
- expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{expect})"
120
+ expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{except})"
121
121
  model_class.non_audited_columns =~ except
122
122
  else
123
123
  true
@@ -170,7 +170,7 @@ module Audited
170
170
  def association_exists?
171
171
  !reflection.nil? &&
172
172
  reflection.macro == :has_many &&
173
- reflection.options[:class_name] == Audit.name
173
+ reflection.options[:class_name] == Audited.audit_class.name
174
174
  end
175
175
  end
176
176
  end
@@ -1,60 +1,49 @@
1
- require "rails/observers/activerecord/active_record"
2
- require "rails/observers/action_controller/caching"
3
-
4
1
  module Audited
5
- class Sweeper < ActionController::Caching::Sweeper
6
- observe Audited::Audit
2
+ class Sweeper
3
+ STORED_DATA = {
4
+ current_remote_address: :remote_ip,
5
+ current_request_uuid: :request_uuid,
6
+ current_user: :current_user
7
+ }
8
+
9
+ delegate :store, to: ::Audited
7
10
 
8
11
  def around(controller)
9
12
  self.controller = controller
13
+ STORED_DATA.each { |k,m| store[k] = send(m) }
10
14
  yield
11
15
  ensure
12
16
  self.controller = nil
17
+ STORED_DATA.keys.each { |k| store.delete(k) }
13
18
  end
14
19
 
15
- def before_create(audit)
16
- audit.user ||= current_user
17
- audit.remote_address = controller.try(:request).try(:remote_ip)
18
- audit.request_uuid = request_uuid if request_uuid
20
+ def current_user
21
+ lambda { controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true) }
19
22
  end
20
23
 
21
- def current_user
22
- controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true)
24
+ def remote_ip
25
+ controller.try(:request).try(:remote_ip)
23
26
  end
24
27
 
25
28
  def request_uuid
26
29
  controller.try(:request).try(:uuid)
27
30
  end
28
31
 
29
- def add_observer!(klass)
30
- super
31
- define_callback(klass)
32
- end
33
-
34
- def define_callback(klass)
35
- observer = self
36
- callback_meth = :_notify_audited_sweeper
37
- klass.send(:define_method, callback_meth) do
38
- observer.update(:before_create, self)
39
- end
40
- klass.send(:before_create, callback_meth)
41
- end
42
-
43
32
  def controller
44
- ::Audited.store[:current_controller]
33
+ store[:current_controller]
45
34
  end
46
35
 
47
36
  def controller=(value)
48
- ::Audited.store[:current_controller] = value
37
+ store[:current_controller] = value
49
38
  end
50
39
  end
51
40
  end
52
41
 
53
42
  ActiveSupport.on_load(:action_controller) do
54
43
  if defined?(ActionController::Base)
55
- ActionController::Base.around_action Audited::Sweeper.instance
44
+ ActionController::Base.around_action Audited::Sweeper.new
56
45
  end
57
46
  if defined?(ActionController::API)
58
- ActionController::API.around_action Audited::Sweeper.instance
47
+ ActionController::API.around_action Audited::Sweeper.new
59
48
  end
60
49
  end