audited-hp 4.3.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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +32 -0
  4. data/.yardopts +3 -0
  5. data/Appraisals +22 -0
  6. data/CHANGELOG +153 -0
  7. data/Gemfile +3 -0
  8. data/LICENSE +19 -0
  9. data/README.md +299 -0
  10. data/Rakefile +18 -0
  11. data/gemfiles/rails40.gemfile +9 -0
  12. data/gemfiles/rails41.gemfile +8 -0
  13. data/gemfiles/rails42.gemfile +8 -0
  14. data/gemfiles/rails50.gemfile +8 -0
  15. data/lib/audited-rspec.rb +4 -0
  16. data/lib/audited.rb +29 -0
  17. data/lib/audited/audit.rb +149 -0
  18. data/lib/audited/auditor.rb +309 -0
  19. data/lib/audited/rspec_matchers.rb +177 -0
  20. data/lib/audited/sweeper.rb +60 -0
  21. data/lib/audited/version.rb +3 -0
  22. data/lib/generators/audited/install_generator.rb +20 -0
  23. data/lib/generators/audited/migration.rb +15 -0
  24. data/lib/generators/audited/templates/add_association_to_audits.rb +11 -0
  25. data/lib/generators/audited/templates/add_comment_to_audits.rb +9 -0
  26. data/lib/generators/audited/templates/add_remote_address_to_audits.rb +10 -0
  27. data/lib/generators/audited/templates/add_request_uuid_to_audits.rb +10 -0
  28. data/lib/generators/audited/templates/install.rb +30 -0
  29. data/lib/generators/audited/templates/rename_association_to_associated.rb +23 -0
  30. data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +9 -0
  31. data/lib/generators/audited/templates/rename_parent_to_association.rb +11 -0
  32. data/lib/generators/audited/upgrade_generator.rb +57 -0
  33. data/spec/audited/audit_spec.rb +199 -0
  34. data/spec/audited/auditor_spec.rb +607 -0
  35. data/spec/audited/sweeper_spec.rb +106 -0
  36. data/spec/audited_spec_helpers.rb +20 -0
  37. data/spec/rails_app/config/application.rb +8 -0
  38. data/spec/rails_app/config/database.yml +24 -0
  39. data/spec/rails_app/config/environment.rb +5 -0
  40. data/spec/rails_app/config/environments/development.rb +21 -0
  41. data/spec/rails_app/config/environments/production.rb +35 -0
  42. data/spec/rails_app/config/environments/test.rb +47 -0
  43. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  44. data/spec/rails_app/config/initializers/inflections.rb +2 -0
  45. data/spec/rails_app/config/initializers/secret_token.rb +3 -0
  46. data/spec/rails_app/config/routes.rb +3 -0
  47. data/spec/spec_helper.rb +21 -0
  48. data/spec/support/active_record/models.rb +99 -0
  49. data/spec/support/active_record/schema.rb +81 -0
  50. data/test/db/version_1.rb +17 -0
  51. data/test/db/version_2.rb +18 -0
  52. data/test/db/version_3.rb +19 -0
  53. data/test/db/version_4.rb +20 -0
  54. data/test/db/version_5.rb +18 -0
  55. data/test/db/version_6.rb +17 -0
  56. data/test/install_generator_test.rb +17 -0
  57. data/test/test_helper.rb +19 -0
  58. data/test/upgrade_generator_test.rb +77 -0
  59. metadata +229 -0
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'bundler/gem_helper'
4
+ require 'rspec/core/rake_task'
5
+ require 'rake/testtask'
6
+ require 'appraisal'
7
+
8
+ Bundler::GemHelper.install_tasks(name: 'audited')
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ Rake::TestTask.new do |t|
13
+ t.libs << "test"
14
+ t.test_files = FileList['test/**/*_test.rb']
15
+ t.verbose = true
16
+ end
17
+
18
+ task default: [:spec, :test]
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 4.0.0"
6
+ gem "protected_attributes"
7
+ gem "test-unit"
8
+
9
+ 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", "~> 4.1.0"
6
+ gem "protected_attributes"
7
+
8
+ 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", "~> 4.2.0"
6
+ gem "protected_attributes"
7
+
8
+ 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.0.0"
6
+ gem "rails-observers", :github => "rails/rails-observers", :branch => "master"
7
+
8
+ gemspec :name => "audited", :path => "../"
@@ -0,0 +1,4 @@
1
+ require 'audited/rspec_matchers'
2
+ module RSpec::Matchers
3
+ include Audited::RspecMatchers
4
+ end
data/lib/audited.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'rails/observers/active_model/active_model'
2
+ require 'active_record'
3
+
4
+ module Audited
5
+ class << self
6
+ attr_accessor :ignored_attributes, :current_user_method
7
+
8
+ # Deprecate audit_class accessors in preperation of their removal
9
+ def audit_class
10
+ Audited::Audit
11
+ end
12
+ deprecate audit_class: "Audited.audit_class is now always Audited::Audit. This method will be removed."
13
+
14
+ def store
15
+ Thread.current[:audited_store] ||= {}
16
+ end
17
+ end
18
+
19
+ @ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
20
+
21
+ @current_user_method = :current_user
22
+ end
23
+
24
+ require 'audited/auditor'
25
+ require 'audited/audit'
26
+
27
+ ::ActiveRecord::Base.send :include, Audited::Auditor
28
+
29
+ require 'audited/sweeper'
@@ -0,0 +1,149 @@
1
+ require 'set'
2
+
3
+ module Audited
4
+ # Audit saves the changes to ActiveRecord models. It has the following attributes:
5
+ #
6
+ # * <tt>auditable</tt>: the ActiveRecord model that was changed
7
+ # * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model
8
+ # * <tt>action</tt>: one of create, update, or delete
9
+ # * <tt>audited_changes</tt>: a serialized hash of all the changes
10
+ # * <tt>comment</tt>: a comment set with the audit
11
+ # * <tt>version</tt>: the version of the model
12
+ # * <tt>request_uuid</tt>: a uuid based that allows audits from the same controller request
13
+ # * <tt>created_at</tt>: Time that the change was performed
14
+ #
15
+ class Audit < ::ActiveRecord::Base
16
+ include ActiveModel::Observing
17
+
18
+ belongs_to :auditable, polymorphic: true
19
+ belongs_to :user, polymorphic: true
20
+ belongs_to :associated, polymorphic: true
21
+
22
+ before_create :set_version_number, :set_audit_user, :set_request_uuid
23
+
24
+ cattr_accessor :audited_class_names
25
+ self.audited_class_names = Set.new
26
+
27
+ serialize :audited_changes
28
+
29
+ scope :ascending, ->{ reorder(version: :asc) }
30
+ scope :descending, ->{ reorder(version: :desc)}
31
+ scope :creates, ->{ where(action: 'create')}
32
+ scope :updates, ->{ where(action: 'update')}
33
+ scope :destroys, ->{ where(action: 'destroy')}
34
+
35
+ scope :up_until, ->(date_or_time){ where("created_at <= ?", date_or_time) }
36
+ scope :from_version, ->(version){ where('version >= ?', version) }
37
+ scope :to_version, ->(version){ where('version <= ?', version) }
38
+ scope :auditable_finder, ->(auditable_id, auditable_type){ where(auditable_id: auditable_id, auditable_type: auditable_type)}
39
+ # Return all audits older than the current one.
40
+ def ancestors
41
+ self.class.ascending.auditable_finder(auditable_id, auditable_type).to_version(version)
42
+ end
43
+
44
+ # Return an instance of what the object looked like at this revision. If
45
+ # the object has been destroyed, this will be a new record.
46
+ def revision
47
+ clazz = auditable_type.constantize
48
+ (clazz.find_by_id(auditable_id) || clazz.new).tap do |m|
49
+ self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge(version: version))
50
+ end
51
+ end
52
+
53
+ # Returns a hash of the changed attributes with the new values
54
+ def new_attributes
55
+ (audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
56
+ attrs[attr] = values.is_a?(Array) ? values.last : values
57
+ attrs
58
+ end
59
+ end
60
+
61
+ # Returns a hash of the changed attributes with the old values
62
+ def old_attributes
63
+ (audited_changes || {}).inject({}.with_indifferent_access) do |attrs, (attr, values)|
64
+ attrs[attr] = Array(values).first
65
+
66
+ attrs
67
+ end
68
+ end
69
+
70
+ # Allows user to be set to either a string or an ActiveRecord object
71
+ # @private
72
+ def user_as_string=(user)
73
+ # reset both either way
74
+ self.user_as_model = self.username = nil
75
+ user.is_a?(::ActiveRecord::Base) ?
76
+ self.user_as_model = user :
77
+ self.username = user
78
+ end
79
+ alias_method :user_as_model=, :user=
80
+ alias_method :user=, :user_as_string=
81
+
82
+ # @private
83
+ def user_as_string
84
+ user_as_model || username
85
+ end
86
+ alias_method :user_as_model, :user
87
+ alias_method :user, :user_as_string
88
+
89
+ # Returns the list of classes that are being audited
90
+ def self.audited_classes
91
+ audited_class_names.map(&:constantize)
92
+ end
93
+
94
+ # All audits made during the block called will be recorded as made
95
+ # by +user+. This method is hopefully threadsafe, making it ideal
96
+ # for background operations that require audit information.
97
+ def self.as_user(user, &block)
98
+ Thread.current[:audited_user] = user
99
+ yield
100
+ ensure
101
+ Thread.current[:audited_user] = nil
102
+ end
103
+
104
+ # @private
105
+ def self.reconstruct_attributes(audits)
106
+ attributes = {}
107
+ result = audits.collect do |audit|
108
+ attributes.merge!(audit.new_attributes)[:version] = audit.version
109
+ yield attributes if block_given?
110
+ end
111
+ block_given? ? result : attributes
112
+ end
113
+
114
+ # @private
115
+ def self.assign_revision_attributes(record, attributes)
116
+ attributes.each do |attr, val|
117
+ record = record.dup if record.frozen?
118
+
119
+ if record.respond_to?("#{attr}=")
120
+ record.attributes.key?(attr.to_s) ?
121
+ record[attr] = val :
122
+ record.send("#{attr}=", val)
123
+ end
124
+ end
125
+ record
126
+ end
127
+
128
+ # use created_at as timestamp cache key
129
+ def self.collection_cache_key(collection = all, timestamp_column = :created_at)
130
+ super(collection, :created_at)
131
+ end
132
+
133
+ private
134
+
135
+ def set_version_number
136
+ max = self.class.auditable_finder(auditable_id, auditable_type).descending.first.try(:version) || 0
137
+ self.version = max + 1
138
+ end
139
+
140
+ def set_audit_user
141
+ self.user = Thread.current[:audited_user] if Thread.current[:audited_user]
142
+ nil # prevent stopping callback chains
143
+ end
144
+
145
+ def set_request_uuid
146
+ self.request_uuid ||= SecureRandom.uuid
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,309 @@
1
+ module Audited
2
+ # Specify this act if you want changes to your model to be saved in an
3
+ # audit table. This assumes there is an audits table ready.
4
+ #
5
+ # class User < ActiveRecord::Base
6
+ # acts_at_audited
7
+ # end
8
+ #
9
+ # To store an audit comment set model.audit_comment to your comment before
10
+ # a create, update or destroy operation.
11
+ #
12
+ # See <tt>Audited::Auditor::ClassMethods#audited</tt>
13
+ # for configuration options
14
+ module Auditor #:nodoc:
15
+ extend ActiveSupport::Concern
16
+
17
+ CALLBACKS = [:audit_create, :audit_update, :audit_destroy]
18
+
19
+ module ClassMethods
20
+ # == Configuration options
21
+ #
22
+ #
23
+ # * +only+ - Only audit the given attributes
24
+ # * +except+ - Excludes fields from being saved in the audit log.
25
+ # By default, Audited will audit all but these fields:
26
+ #
27
+ # [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at']
28
+ # You can add to those by passing one or an array of fields to skip.
29
+ #
30
+ # class User < ActiveRecord::Base
31
+ # audited except: :password
32
+ # end
33
+ #
34
+ # * +require_comment+ - Ensures that audit_comment is supplied before
35
+ # any create, update or destroy operation.
36
+ #
37
+ def acts_as_audited(options = {})
38
+ # don't allow multiple calls
39
+ return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
40
+
41
+ class_attribute :audit_associated_with, instance_writer: false
42
+ class_attribute :audited_options, instance_writer: false
43
+
44
+ self.audited_options = options
45
+ self.audit_associated_with = options[:associated_with]
46
+
47
+ if options[:comment_required]
48
+ validates_presence_of :audit_comment, if: :auditing_enabled
49
+ before_destroy :require_comment
50
+ end
51
+
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
56
+
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
+
62
+ # Define and set after_audit and around_audit callbacks. This might be useful if you want
63
+ # to notify a party after the audit has been created or if you want to access the newly-created
64
+ # audit.
65
+ define_callbacks :audit
66
+ set_callback :audit, :after, :after_audit, if: lambda { respond_to?(:after_audit, true) }
67
+ set_callback :audit, :around, :around_audit, if: lambda { respond_to?(:around_audit, true) }
68
+
69
+ attr_accessor :version
70
+
71
+ extend Audited::Auditor::AuditedClassMethods
72
+ include Audited::Auditor::AuditedInstanceMethods
73
+
74
+ self.auditing_enabled = true
75
+ end
76
+
77
+ 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]
83
+ end
84
+ end
85
+
86
+ module AuditedInstanceMethods
87
+ # Temporarily turns off auditing while saving.
88
+ def save_without_auditing
89
+ without_auditing { save }
90
+ end
91
+
92
+ # Executes the block with the auditing callbacks disabled.
93
+ #
94
+ # @foo.without_auditing do
95
+ # @foo.save
96
+ # end
97
+ #
98
+ def without_auditing(&block)
99
+ self.class.without_auditing(&block)
100
+ end
101
+
102
+ # Gets an array of the revisions available
103
+ #
104
+ # user.revisions.each do |revision|
105
+ # user.name
106
+ # user.version
107
+ # end
108
+ #
109
+ def revisions(from_version = 1)
110
+ audits = self.audits.from_version(from_version)
111
+ return [] if audits.empty?
112
+ revisions = []
113
+ audits.each do |audit|
114
+ revisions << audit.revision
115
+ end
116
+ revisions
117
+ end
118
+
119
+ # Get a specific revision specified by the version number, or +:previous+
120
+ def revision(version)
121
+ revision_with Audit.reconstruct_attributes(audits_to(version))
122
+ end
123
+
124
+ # Find the oldest revision recorded prior to the date/time provided.
125
+ def revision_at(date_or_time)
126
+ audits = self.audits.up_until(date_or_time)
127
+ revision_with Audit.reconstruct_attributes(audits) unless audits.empty?
128
+ end
129
+
130
+ # List of attributes that are audited.
131
+ def audited_attributes
132
+ attributes.except(*non_audited_columns)
133
+ end
134
+
135
+ def non_audited_columns
136
+ self.class.non_audited_columns
137
+ end
138
+
139
+ protected
140
+
141
+ def non_audited_columns
142
+ self.class.non_audited_columns
143
+ end
144
+
145
+ def revision_with(attributes)
146
+ dup.tap do |revision|
147
+ revision.id = id
148
+ revision.send :instance_variable_set, '@attributes', self.attributes if rails_below?('4.2.0')
149
+ revision.send :instance_variable_set, '@new_record', destroyed?
150
+ revision.send :instance_variable_set, '@persisted', !destroyed?
151
+ revision.send :instance_variable_set, '@readonly', false
152
+ revision.send :instance_variable_set, '@destroyed', false
153
+ revision.send :instance_variable_set, '@_destroyed', false
154
+ revision.send :instance_variable_set, '@marked_for_destruction', false
155
+ Audit.assign_revision_attributes(revision, attributes)
156
+
157
+ # Remove any association proxies so that they will be recreated
158
+ # and reference the correct object for this revision. The only way
159
+ # to determine if an instance variable is a proxy object is to
160
+ # see if it responds to certain methods, as it forwards almost
161
+ # everything to its target.
162
+ revision.instance_variables.each do |ivar|
163
+ proxy = revision.instance_variable_get ivar
164
+ if !proxy.nil? && proxy.respond_to?(:proxy_respond_to?)
165
+ revision.instance_variable_set ivar, nil
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ def rails_below?(rails_version)
172
+ Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
173
+ end
174
+
175
+ private
176
+
177
+ 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
189
+ end
190
+ end
191
+
192
+ def audits_to(version = nil)
193
+ if version == :previous
194
+ version = if self.version
195
+ self.version - 1
196
+ else
197
+ previous = audits.descending.offset(1).first
198
+ previous ? previous.version : 1
199
+ end
200
+ end
201
+ audits.to_version(version)
202
+ end
203
+
204
+ def audit_create
205
+ write_audit(action: 'create', audited_changes: audited_attributes,
206
+ comment: audit_comment)
207
+ end
208
+
209
+ def audit_update
210
+ unless (changes = audited_changes).empty? && audit_comment.blank?
211
+ write_audit(action: 'update', audited_changes: changes,
212
+ comment: audit_comment)
213
+ end
214
+ end
215
+
216
+ def audit_destroy
217
+ write_audit(action: 'destroy', audited_changes: audited_attributes,
218
+ comment: audit_comment) unless new_record?
219
+ end
220
+
221
+ def write_audit(attrs)
222
+ attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
223
+ self.audit_comment = nil
224
+ run_callbacks(:audit) { audits.create(attrs) } if auditing_enabled
225
+ end
226
+
227
+ def require_comment
228
+ if auditing_enabled && audit_comment.blank?
229
+ errors.add(:audit_comment, "Comment required before destruction")
230
+ return false if Rails.version.start_with?('4.')
231
+ throw :abort
232
+ end
233
+ end
234
+
235
+ CALLBACKS.each do |attr_name|
236
+ alias_method "#{attr_name}_callback".to_sym, attr_name
237
+ end
238
+
239
+ def empty_callback #:nodoc:
240
+ end
241
+
242
+ def auditing_enabled
243
+ self.class.auditing_enabled
244
+ end
245
+
246
+ def auditing_enabled=(val)
247
+ self.class.auditing_enabled = val
248
+ end
249
+ end # InstanceMethods
250
+
251
+ module AuditedClassMethods
252
+ # Returns an array of columns that are audited. See non_audited_columns
253
+ def audited_columns
254
+ columns.select {|c| !non_audited_columns.include?(c.name) }
255
+ end
256
+
257
+ 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
268
+ end
269
+
270
+ # Executes the block with auditing disabled.
271
+ #
272
+ # Foo.without_auditing do
273
+ # @foo.save
274
+ # end
275
+ #
276
+ def without_auditing
277
+ auditing_was_enabled = auditing_enabled
278
+ disable_auditing
279
+ yield
280
+ ensure
281
+ enable_auditing if auditing_was_enabled
282
+ end
283
+
284
+ def disable_auditing
285
+ self.auditing_enabled = false
286
+ end
287
+
288
+ def enable_auditing
289
+ self.auditing_enabled = true
290
+ end
291
+
292
+ # All audit operations during the block are recorded as being
293
+ # made by +user+. This is not model specific, the method is a
294
+ # convenience wrapper around
295
+ # @see Audit#as_user.
296
+ def audit_as(user, &block)
297
+ Audit.as_user(user, &block)
298
+ end
299
+
300
+ def auditing_enabled
301
+ Audited.store.fetch("#{table_name}_auditing_enabled", true)
302
+ end
303
+
304
+ def auditing_enabled=(val)
305
+ Audited.store["#{table_name}_auditing_enabled"] = val
306
+ end
307
+ end
308
+ end
309
+ end