audited 4.6.0 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 11243e8bfb9c50162f0f939a283270f7bf706387
4
- data.tar.gz: 9b081dea623844e1bd37b71f774490dc8596ef84
3
+ metadata.gz: 6a2d5a695157822691d7e8a980742c9dab27769c
4
+ data.tar.gz: 41262a1baa392f65fd172c6971985ca43ab53e92
5
5
  SHA512:
6
- metadata.gz: f5ca8463b13d3c2fc463749a1ff46c1522b5ed6c049dd743e72d47577c5da025f46f3ab90492fa19a88cfbf7cd38ee302e2aefced16098dcd3bd7846a6be0eb9
7
- data.tar.gz: e4e5b349f6a8db618deb7a6fd48a853eb75729ddbff3b78c431c89247ec17aaceb2e4a67dd206defbfeb5fe4e8b6a47c4af0212c5898fd777848c08625aca90e
6
+ metadata.gz: f4d0ebf2b9683b96d7954efb7e2ddab5c21e3c0cea29d9d7388334a9f9215f8729b337c850f60abf9b9084773158609d32e99ab580aec244393dc79d41846fad
7
+ data.tar.gz: 32a48ffa57d8ebd24ece8714b4c26c8c0a0af4ed7021f165a625a619c058c71749510e20417e9a15629385e72b999409b39186e050af10f5cf73ad36760b25b5
@@ -2,9 +2,10 @@ language: ruby
2
2
  cache: bundler
3
3
  rvm:
4
4
  - 2.1
5
- - 2.2.8
6
- - 2.3.5
7
- - 2.4.2
5
+ - 2.2.9
6
+ - 2.3.6
7
+ - 2.4.3
8
+ - 2.5.0
8
9
  - ruby-head
9
10
  env:
10
11
  - DB=SQLITE
@@ -12,6 +13,10 @@ env:
12
13
  - DB=MYSQL
13
14
  addons:
14
15
  postgresql: "9.4"
16
+ before_install:
17
+ # https://github.com/travis-ci/travis-ci/issues/8978
18
+ - "travis_retry gem update --system"
19
+ - "travis_retry gem install bundler"
15
20
  gemfile:
16
21
  - gemfiles/rails40.gemfile
17
22
  - gemfiles/rails41.gemfile
@@ -29,9 +34,13 @@ matrix:
29
34
  gemfile: gemfiles/rails51.gemfile
30
35
  - rvm: 2.1
31
36
  gemfile: gemfiles/rails52.gemfile
32
- - rvm: 2.4.2
37
+ - rvm: 2.4.3
33
38
  gemfile: gemfiles/rails40.gemfile
34
- - rvm: 2.4.2
39
+ - rvm: 2.4.3
40
+ gemfile: gemfiles/rails41.gemfile
41
+ - rvm: 2.5.0
42
+ gemfile: gemfiles/rails40.gemfile
43
+ - rvm: 2.5.0
35
44
  gemfile: gemfiles/rails41.gemfile
36
45
  - rvm: ruby-head
37
46
  gemfile: gemfiles/rails40.gemfile
data/Appraisals CHANGED
@@ -23,6 +23,6 @@ appraise 'rails51' do
23
23
  end
24
24
 
25
25
  appraise 'rails52' do
26
- gem 'rails', '>= 5.2.0.beta2', '< 5.3'
26
+ gem 'rails', '>= 5.2.0.rc1', '< 5.3'
27
27
  gem 'mysql2', '~> 0.4.4'
28
28
  end
@@ -18,6 +18,38 @@ Fixed
18
18
 
19
19
  - None
20
20
 
21
+ ## 4.7.0 (2018-03-14)
22
+
23
+ Breaking changes
24
+
25
+ - None
26
+
27
+ Added
28
+
29
+ - Add `inverse_of: auditable` definition to audit relation
30
+ [#413](https://github.com/collectiveidea/audited/pull/413)
31
+ - Add functionality to conditionally audit models
32
+ [#414](https://github.com/collectiveidea/audited/pull/414)
33
+ - Allow limiting number of audits stored
34
+ [#405](https://github.com/collectiveidea/audited/pull/405)
35
+
36
+ Changed
37
+
38
+ - Reduced db calls in `#revisions` method
39
+ [#402](https://github.com/collectiveidea/audited/pull/402)
40
+ [#403](https://github.com/collectiveidea/audited/pull/403)
41
+ - Update supported Ruby and Rails versions
42
+ [#404](https://github.com/collectiveidea/audited/pull/404)
43
+ [#409](https://github.com/collectiveidea/audited/pull/409)
44
+ [#415](https://github.com/collectiveidea/audited/pull/415)
45
+ [#416](https://github.com/collectiveidea/audited/pull/416)
46
+
47
+ Fixed
48
+
49
+ - Ensure that `on` and `except` options jive with `comment_required: true`
50
+ [#419](https://github.com/collectiveidea/audited/pull/419)
51
+ - Fix RSpec matchers
52
+ [#420](https://github.com/collectiveidea/audited/pull/420)
21
53
 
22
54
  ## 4.6.0 (2018-01-10)
23
55
 
data/README.md CHANGED
@@ -12,9 +12,10 @@ For Rails 3, use gem version 3.0 or see the [3.0-stable branch](https://github.c
12
12
  Audited supports and is [tested against](http://travis-ci.org/collectiveidea/audited) the following Ruby versions:
13
13
 
14
14
  * 2.1.10
15
- * 2.2.8
16
- * 2.3.5
17
- * 2.4.2
15
+ * 2.2.9
16
+ * 2.3.6
17
+ * 2.4.3
18
+ * 2.5.0
18
19
 
19
20
  Audited may work just fine with a Ruby version not listed above, but we can't guarantee that it will. If you'd like to maintain a Ruby that isn't listed, please let us know with a [pull request](https://github.com/collectiveidea/audited/pulls).
20
21
 
@@ -27,7 +28,7 @@ Audited is currently ActiveRecord-only. In a previous life, Audited worked with
27
28
  Add the gem to your Gemfile:
28
29
 
29
30
  ```ruby
30
- gem "audited", "~> 4.6"
31
+ gem "audited", "~> 4.7"
31
32
  ```
32
33
 
33
34
  Then, from your Rails app directory, create the `audits` table:
@@ -141,6 +142,33 @@ class User < ActiveRecord::Base
141
142
  end
142
143
  ```
143
144
 
145
+ ### Limiting stored audits
146
+
147
+ You can limit the number of audits stored for your model. To configure limiting for all audited models, put the following in an initializer:
148
+
149
+ ```ruby
150
+ Audited.max_audits = 10 # keep only 10 latest audits
151
+ ```
152
+
153
+ or customize per model:
154
+
155
+ ```ruby
156
+ class User < ActiveRecord::Base
157
+ audited max_audits: 2
158
+ end
159
+ ```
160
+
161
+ Whenever an object is updated or destroyed, extra audits are combined with newer ones and the old ones are destroyed.
162
+
163
+ ```ruby
164
+ user = User.create!(name: "Steve")
165
+ user.audits.count # => 1
166
+ user.update_attributes!(name: "Ryan")
167
+ user.audits.count # => 2
168
+ user.destroy
169
+ user.audits.count # => 2
170
+ ```
171
+
144
172
  ### Current User Tracking
145
173
 
146
174
  If you're using Audited in a Rails application, all audited changes made within a request will automatically be attributed to the current user. By default, Audited uses the `current_user` method in your controller.
@@ -227,6 +255,32 @@ user.audits.last.associated # => #<Company name: "Collective Idea">
227
255
  company.associated_audits.last.auditable # => #<User name: "Steve Richert">
228
256
  ```
229
257
 
258
+ ### Conditional auditing
259
+
260
+ If you want to audit only under specific conditions, you can provide conditional options (similar to ActiveModel callbacks) that will ensure your model is only audited for these conditions.
261
+
262
+ ```ruby
263
+ class User < ActiveRecord::Base
264
+ audited if: :active?
265
+
266
+ private
267
+
268
+ def active?
269
+ last_login > 6.months.ago
270
+ end
271
+ end
272
+ ```
273
+
274
+ Just like in ActiveModel, you can use an inline Proc in your conditions:
275
+
276
+ ```ruby
277
+ class User < ActiveRecord::Base
278
+ audited unless: Proc.new { |u| u.ninja? }
279
+ end
280
+ ```
281
+
282
+ In the above case, the user will only be audited when `User#ninja` is `false`.
283
+
230
284
  ### Disabling auditing
231
285
 
232
286
  If you want to disable auditing temporarily doing certain tasks, there are a few
@@ -278,30 +332,6 @@ Audited.config do |config|
278
332
  end
279
333
  ```
280
334
 
281
- ## Gotchas
282
-
283
- ### Using attr_protected with Rails 4.x
284
-
285
- If you're using the `protected_attributes` gem with Rails 4.0, 4.1 or 4.2 (the gem isn't supported in Rails 5.0 or higher), you'll have to take an extra couple of steps to get `audited` working.
286
-
287
- First be sure to add `allow_mass_assignment: true` to your `audited` call; otherwise Audited will
288
- interfere with `protected_attributes` and none of your `save` calls will work.
289
-
290
- ```ruby
291
- class User < ActiveRecord::Base
292
- audited allow_mass_assignment: true
293
- end
294
- ```
295
-
296
- Second, be sure to add `audit_ids` to the list of protected attributes to prevent data loss.
297
-
298
- ```ruby
299
- class User < ActiveRecord::Base
300
- audited allow_mass_assignment: true
301
- attr_protected :logins, :audit_ids
302
- end
303
- ```
304
-
305
335
  ## Support
306
336
 
307
337
  You can find documentation at: http://rdoc.info/github/collectiveidea/audited
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", ">= 5.2.0.beta2", "< 5.3"
5
+ gem "rails", ">= 5.2.0.rc1", "< 5.3"
6
6
  gem "mysql2", "~> 0.4.4"
7
7
 
8
8
  gemspec name: "audited", path: "../"
@@ -2,7 +2,7 @@ require 'active_record'
2
2
 
3
3
  module Audited
4
4
  class << self
5
- attr_accessor :ignored_attributes, :current_user_method
5
+ attr_accessor :ignored_attributes, :current_user_method, :max_audits
6
6
  attr_writer :audit_class
7
7
 
8
8
  def audit_class
@@ -33,6 +33,18 @@ module Audited
33
33
  #
34
34
  # * +require_comment+ - Ensures that audit_comment is supplied before
35
35
  # any create, update or destroy operation.
36
+ # * +max_audits+ - Limits the number of stored audits.
37
+ #
38
+ # * +if+ - Only audit the model when the given function returns true
39
+ # * +unless+ - Only audit the model when the given function returns false
40
+ #
41
+ # class User < ActiveRecord::Base
42
+ # audited :if => :active?
43
+ #
44
+ # def active?
45
+ # self.status == 'active'
46
+ # end
47
+ # end
36
48
  #
37
49
  def audited(options = {})
38
50
  # don't allow multiple calls
@@ -41,7 +53,7 @@ module Audited
41
53
  extend Audited::Auditor::AuditedClassMethods
42
54
  include Audited::Auditor::AuditedInstanceMethods
43
55
 
44
- class_attribute :audit_associated_with, instance_writer: false
56
+ class_attribute :audit_associated_with, instance_writer: false
45
57
  class_attribute :audited_options, instance_writer: false
46
58
  attr_accessor :version, :audit_comment
47
59
 
@@ -51,11 +63,11 @@ module Audited
51
63
  self.audit_associated_with = audited_options[:associated_with]
52
64
 
53
65
  if audited_options[:comment_required]
54
- validates_presence_of :audit_comment, if: :auditing_enabled
55
- before_destroy :require_comment
66
+ validate :presence_of_audit_comment
67
+ before_destroy :require_comment if audited_options[:on].include?(:destroy)
56
68
  end
57
69
 
58
- has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name
70
+ has_many :audits, -> { order(version: :asc) }, as: :auditable, class_name: Audited.audit_class.name, inverse_of: :auditable
59
71
  Audited.audit_class.audited_class_names << to_s
60
72
 
61
73
  after_create :audit_create if audited_options[:on].include?(:create)
@@ -101,9 +113,17 @@ module Audited
101
113
  # end
102
114
  #
103
115
  def revisions(from_version = 1)
104
- audits = self.audits.from_version(from_version)
105
- return [] if audits.empty?
106
- audits.map(&:revision)
116
+ return [] unless audits.from_version(from_version).exists?
117
+
118
+ all_audits = audits.select([:audited_changes, :version]).to_a
119
+ targeted_audits = all_audits.select { |audit| audit.version >= from_version }
120
+
121
+ previous_attributes = reconstruct_attributes(all_audits - targeted_audits)
122
+
123
+ targeted_audits.map do |audit|
124
+ previous_attributes.merge!(audit.new_attributes)
125
+ revision_with(previous_attributes.merge!(version: audit.version))
126
+ end
107
127
  end
108
128
 
109
129
  # Get a specific revision specified by the version number, or +:previous+
@@ -125,6 +145,18 @@ module Audited
125
145
  attributes.except(*non_audited_columns)
126
146
  end
127
147
 
148
+ # Combine multiple audits into one.
149
+ def combine_audits(audits_to_combine)
150
+ combine_target = audits_to_combine.last
151
+ combine_target.audited_changes = audits_to_combine.pluck(:audited_changes).reduce(&:merge)
152
+ combine_target.comment = "#{combine_target.comment}\nThis audit is the result of multiple audits being combined."
153
+
154
+ transaction do
155
+ combine_target.save!
156
+ audits_to_combine.unscope(:limit).where("version < ?", combine_target.version).delete_all
157
+ end
158
+ end
159
+
128
160
  protected
129
161
 
130
162
  def non_audited_columns
@@ -208,14 +240,41 @@ module Audited
208
240
  def write_audit(attrs)
209
241
  attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?
210
242
  self.audit_comment = nil
211
- run_callbacks(:audit) { audits.create(attrs) } if auditing_enabled
243
+
244
+ if auditing_enabled
245
+ run_callbacks(:audit) {
246
+ audit = audits.create(attrs)
247
+ combine_audits_if_needed if attrs[:action] != 'create'
248
+ audit
249
+ }
250
+ end
251
+ end
252
+
253
+ def presence_of_audit_comment
254
+ if comment_required_state?
255
+ errors.add(:audit_comment, "Comment can't be blank!") unless audit_comment.present?
256
+ end
257
+ end
258
+
259
+ def comment_required_state?
260
+ auditing_enabled &&
261
+ ((audited_options[:on].include?(:create) && self.new_record?) ||
262
+ (audited_options[:on].include?(:update) && self.persisted? && self.changed?))
263
+ end
264
+
265
+ def combine_audits_if_needed
266
+ max_audits = audited_options[:max_audits]
267
+ if max_audits && (extra_count = audits.count - max_audits) > 0
268
+ audits_to_combine = audits.limit(extra_count + 1)
269
+ combine_audits(audits_to_combine)
270
+ end
212
271
  end
213
272
 
214
273
  def require_comment
215
274
  if auditing_enabled && audit_comment.blank?
216
- errors.add(:audit_comment, "Comment required before destruction")
275
+ errors.add(:audit_comment, "Comment can't be blank!")
217
276
  return false if Rails.version.start_with?('4.')
218
- throw :abort
277
+ throw(:abort)
219
278
  end
220
279
  end
221
280
 
@@ -224,12 +283,29 @@ module Audited
224
283
  end
225
284
 
226
285
  def auditing_enabled
227
- self.class.auditing_enabled
286
+ return run_conditional_check(audited_options[:if]) &&
287
+ run_conditional_check(audited_options[:unless], matching: false) &&
288
+ self.class.auditing_enabled
289
+ end
290
+
291
+ def run_conditional_check(condition, matching: true)
292
+ return true if condition.blank?
293
+
294
+ return condition.call(self) == matching if condition.respond_to?(:call)
295
+ return send(condition) == matching if respond_to?(condition.to_sym)
296
+
297
+ true
228
298
  end
229
299
 
230
300
  def auditing_enabled=(val)
231
301
  self.class.auditing_enabled = val
232
302
  end
303
+
304
+ def reconstruct_attributes(audits)
305
+ attributes = {}
306
+ audits.each { |audit| attributes.merge!(audit.new_attributes) }
307
+ attributes
308
+ end
233
309
  end # InstanceMethods
234
310
 
235
311
  module AuditedClassMethods
@@ -240,9 +316,7 @@ module Audited
240
316
 
241
317
  # We have to calculate this here since column_names may not be available when `audited` is called
242
318
  def non_audited_columns
243
- @non_audited_columns ||= audited_options[:only].present? ?
244
- column_names - audited_options[:only] :
245
- default_ignored_attributes | audited_options[:except]
319
+ @non_audited_columns ||= calculate_non_audited_columns
246
320
  end
247
321
 
248
322
  def non_audited_columns=(columns)
@@ -288,16 +362,29 @@ module Audited
288
362
  Audited.store["#{table_name}_auditing_enabled"] = val
289
363
  end
290
364
 
291
- protected
292
365
  def default_ignored_attributes
293
- [primary_key, inheritance_column] + Audited.ignored_attributes
366
+ [primary_key, inheritance_column] | Audited.ignored_attributes
294
367
  end
295
368
 
369
+ protected
370
+
296
371
  def normalize_audited_options
297
372
  audited_options[:on] = Array.wrap(audited_options[:on])
298
373
  audited_options[:on] = [:create, :update, :destroy] if audited_options[:on].empty?
299
374
  audited_options[:only] = Array.wrap(audited_options[:only]).map(&:to_s)
300
375
  audited_options[:except] = Array.wrap(audited_options[:except]).map(&:to_s)
376
+ max_audits = audited_options[:max_audits] || Audited.max_audits
377
+ audited_options[:max_audits] = Integer(max_audits).abs if max_audits
378
+ end
379
+
380
+ def calculate_non_audited_columns
381
+ if audited_options[:only].present?
382
+ (column_names | default_ignored_attributes) - audited_options[:only]
383
+ elsif audited_options[:except].present?
384
+ default_ignored_attributes | audited_options[:except]
385
+ else
386
+ default_ignored_attributes
387
+ end
301
388
  end
302
389
  end
303
390
  end
@@ -41,12 +41,12 @@ module Audited
41
41
  end
42
42
 
43
43
  def only(*fields)
44
- @options[:only] = fields.flatten
44
+ @options[:only] = fields.flatten.map(&:to_s)
45
45
  self
46
46
  end
47
47
 
48
48
  def except(*fields)
49
- @options[:except] = fields.flatten
49
+ @options[:except] = fields.flatten.map(&:to_s)
50
50
  self
51
51
  end
52
52
 
@@ -56,16 +56,13 @@ module Audited
56
56
  end
57
57
 
58
58
  def on(*actions)
59
- @options[:on] = actions.flatten
59
+ @options[:on] = actions.flatten.map(&:to_sym)
60
60
  self
61
61
  end
62
62
 
63
63
  def matches?(subject)
64
64
  @subject = subject
65
- auditing_enabled? &&
66
- associated_with_model? &&
67
- records_changes_to_specified_fields? &&
68
- comment_required_valid?
65
+ auditing_enabled? && required_checks_for_options_satisfied?
69
66
  end
70
67
 
71
68
  def failure_message
@@ -109,31 +106,83 @@ module Audited
109
106
  end
110
107
 
111
108
  def records_changes_to_specified_fields?
112
- if @options[:only] || @options[:except]
113
- if @options[:only]
114
- except = model_class.column_names - @options[:only].map(&:to_s)
115
- else
116
- except = model_class.default_ignored_attributes + Audited.ignored_attributes
117
- except |= @options[:except].collect(&:to_s) if @options[:except]
118
- end
109
+ ignored_fields = build_ignored_fields_from_options
110
+
111
+ expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{ignored_fields})"
112
+ model_class.non_audited_columns.to_set == ignored_fields.to_set
113
+ end
114
+
115
+ def comment_required_valid?
116
+ expects "to require audit_comment before #{model_class.audited_options[:on]} when comment required"
117
+ validate_callbacks_include_presence_of_comment? && destroy_callbacks_include_comment_required?
118
+ end
119
119
 
120
- expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{except})"
121
- model_class.non_audited_columns =~ except
120
+ def only_audit_on_designated_callbacks?
121
+ {
122
+ create: [:after, :audit_create],
123
+ update: [:before, :audit_update],
124
+ destroy: [:before, :audit_destroy]
125
+ }.map do |(action, kind_callback)|
126
+ kind, callback = kind_callback
127
+ callbacks_for(action, kind: kind).include?(callback) if @options[:on].include?(action)
128
+ end.compact.all?
129
+ end
130
+
131
+ def validate_callbacks_include_presence_of_comment?
132
+ if @options[:comment_required] && audited_on_create_or_update?
133
+ callbacks_for(:validate).include?(:presence_of_audit_comment)
122
134
  else
123
135
  true
124
136
  end
125
137
  end
126
138
 
127
- def comment_required_valid?
128
- if @options[:comment_required]
129
- @subject.audit_comment = nil
139
+ def audited_on_create_or_update?
140
+ model_class.audited_options[:on].include?(:create) || model_class.audited_options[:on].include?(:update)
141
+ end
130
142
 
131
- expects "to be invalid when audit_comment is not specified"
132
- @subject.valid? == false && @subject.errors.key?(:audit_comment)
143
+ def destroy_callbacks_include_comment_required?
144
+ if @options[:comment_required] && model_class.audited_options[:on].include?(:destroy)
145
+ callbacks_for(:destroy).include?(:require_comment)
133
146
  else
134
147
  true
135
148
  end
136
149
  end
150
+
151
+ def requires_comment_before_callbacks?
152
+ [:create, :update, :destroy].map do |action|
153
+ if @options[:comment_required] && model_class.audited_options[:on].include?(action)
154
+ callbacks_for(action).include?(:require_comment)
155
+ end
156
+ end.compact.all?
157
+ end
158
+
159
+ def callbacks_for(action, kind: :before)
160
+ model_class.send("_#{action}_callbacks").select { |cb| cb.kind == kind }.map(&:filter)
161
+ end
162
+
163
+ def build_ignored_fields_from_options
164
+ default_ignored_attributes = model_class.default_ignored_attributes
165
+
166
+ if @options[:only].present?
167
+ (default_ignored_attributes | model_class.column_names) - @options[:only]
168
+ elsif @options[:except].present?
169
+ default_ignored_attributes | @options[:except]
170
+ else
171
+ default_ignored_attributes
172
+ end
173
+ end
174
+
175
+ def required_checks_for_options_satisfied?
176
+ {
177
+ only: :records_changes_to_specified_fields?,
178
+ except: :records_changes_to_specified_fields?,
179
+ comment_required: :comment_required_valid?,
180
+ associated_with: :associated_with_model?,
181
+ on: :only_audit_on_designated_callbacks?
182
+ }.map do |(option, check)|
183
+ send(check) if @options[option].present?
184
+ end.compact.all?
185
+ end
137
186
  end
138
187
 
139
188
  class AssociatedAuditMatcher # :nodoc:
@@ -1,3 +1,3 @@
1
1
  module Audited
2
- VERSION = "4.6.0"
2
+ VERSION = "4.7.0"
3
3
  end
@@ -17,6 +17,114 @@ describe Audited::Auditor do
17
17
  end
18
18
  end
19
19
 
20
+ context "should be configurable which conditions are audited" do
21
+ subject { ConditionalCompany.new.send(:auditing_enabled) }
22
+
23
+ context "when passing a method name" do
24
+ before do
25
+ class ConditionalCompany < ::ActiveRecord::Base
26
+ self.table_name = 'companies'
27
+
28
+ audited if: :public?
29
+
30
+ def public?; end
31
+ end
32
+ end
33
+
34
+ context "when conditions are true" do
35
+ before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(true) }
36
+ it { is_expected.to be_truthy }
37
+ end
38
+
39
+ context "when conditions are false" do
40
+ before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(false) }
41
+ it { is_expected.to be_falsey }
42
+ end
43
+ end
44
+
45
+ context "when passing a Proc" do
46
+ context "when conditions are true" do
47
+ before do
48
+ class InclusiveCompany < ::ActiveRecord::Base
49
+ self.table_name = 'companies'
50
+ audited if: Proc.new { true }
51
+ end
52
+ end
53
+
54
+ subject { InclusiveCompany.new.send(:auditing_enabled) }
55
+
56
+ it { is_expected.to be_truthy }
57
+ end
58
+
59
+ context "when conditions are false" do
60
+ before do
61
+ class ExclusiveCompany < ::ActiveRecord::Base
62
+ self.table_name = 'companies'
63
+ audited if: Proc.new { false }
64
+ end
65
+ end
66
+ subject { ExclusiveCompany.new.send(:auditing_enabled) }
67
+ it { is_expected.to be_falsey }
68
+ end
69
+ end
70
+ end
71
+
72
+ context "should be configurable which conditions aren't audited" do
73
+ context "when using a method name" do
74
+ before do
75
+ class ExclusionaryCompany < ::ActiveRecord::Base
76
+ self.table_name = 'companies'
77
+
78
+ audited unless: :non_profit?
79
+
80
+ def non_profit?; end
81
+ end
82
+ end
83
+
84
+ subject { ExclusionaryCompany.new.send(:auditing_enabled) }
85
+
86
+ context "when conditions are true" do
87
+ before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(true) }
88
+ it { is_expected.to be_falsey }
89
+ end
90
+
91
+ context "when conditions are false" do
92
+ before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(false) }
93
+ it { is_expected.to be_truthy }
94
+ end
95
+ end
96
+
97
+ context "when using a proc" do
98
+ context "when conditions are true" do
99
+ before do
100
+ class ExclusionaryCompany < ::ActiveRecord::Base
101
+ self.table_name = 'companies'
102
+ audited unless: Proc.new { |c| c.exclusive? }
103
+
104
+ def exclusive?
105
+ true
106
+ end
107
+ end
108
+ end
109
+
110
+ subject { ExclusionaryCompany.new.send(:auditing_enabled) }
111
+ it { is_expected.to be_falsey }
112
+ end
113
+
114
+ context "when conditions are false" do
115
+ before do
116
+ class InclusiveCompany < ::ActiveRecord::Base
117
+ self.table_name = 'companies'
118
+ audited unless: Proc.new { false }
119
+ end
120
+ end
121
+
122
+ subject { InclusiveCompany.new.send(:auditing_enabled) }
123
+ it { is_expected.to be_truthy }
124
+ end
125
+ end
126
+ end
127
+
20
128
  it "should be configurable which attributes are not audited via ignored_attributes" do
21
129
  Audited.ignored_attributes = ['delta', 'top_secret', 'created_at']
22
130
  class Secret < ::ActiveRecord::Base
@@ -36,9 +144,14 @@ describe Audited::Auditor do
36
144
  end
37
145
 
38
146
  it "should not save non-audited columns" do
39
- Models::ActiveRecord::User.non_audited_columns = (Models::ActiveRecord::User.non_audited_columns << :favourite_device)
147
+ previous = Models::ActiveRecord::User.non_audited_columns
148
+ begin
149
+ Models::ActiveRecord::User.non_audited_columns += [:favourite_device]
40
150
 
41
- expect(create_user.audits.first.audited_changes.keys.any? { |col| ['favourite_device', 'created_at', 'updated_at', 'password'].include?( col ) }).to eq(false)
151
+ expect(create_user.audits.first.audited_changes.keys.any? { |col| ['favourite_device', 'created_at', 'updated_at', 'password'].include?( col ) }).to eq(false)
152
+ ensure
153
+ Models::ActiveRecord::User.non_audited_columns = previous
154
+ end
42
155
  end
43
156
 
44
157
  it "should not save other columns than specified in 'only' option" do
@@ -84,11 +197,11 @@ describe Audited::Auditor do
84
197
  let(:migrations_path) { SPEC_ROOT.join("support/active_record/postgres") }
85
198
 
86
199
  after do
87
- ActiveRecord::Migrator.rollback([migrations_path])
200
+ run_migrations(:down, migrations_path)
88
201
  end
89
202
 
90
203
  it "should work if column type is 'json'" do
91
- ActiveRecord::Migrator.up([migrations_path], 1)
204
+ run_migrations(:up, migrations_path, 1)
92
205
  Audited::Audit.reset_column_information
93
206
  expect(Audited::Audit.columns_hash["audited_changes"].sql_type).to eq("json")
94
207
 
@@ -99,7 +212,7 @@ describe Audited::Auditor do
99
212
  end
100
213
 
101
214
  it "should work if column type is 'jsonb'" do
102
- ActiveRecord::Migrator.up([migrations_path], 2)
215
+ run_migrations(:up, migrations_path, 2)
103
216
  Audited::Audit.reset_column_information
104
217
  expect(Audited::Audit.columns_hash["audited_changes"].sql_type).to eq("jsonb")
105
218
 
@@ -329,6 +442,77 @@ describe Audited::Auditor do
329
442
  end
330
443
  end
331
444
 
445
+ describe "max_audits" do
446
+ it "should respect global setting" do
447
+ stub_global_max_audits(10) do
448
+ expect(Models::ActiveRecord::User.audited_options[:max_audits]).to eq(10)
449
+ end
450
+ end
451
+
452
+ it "should respect per model setting" do
453
+ stub_global_max_audits(10) do
454
+ expect(Models::ActiveRecord::MaxAuditsUser.audited_options[:max_audits]).to eq(5)
455
+ end
456
+ end
457
+
458
+ it "should delete old audits when keeped amount exceeded" do
459
+ stub_global_max_audits(2) do
460
+ user = create_versions(2)
461
+ user.update(name: 'John')
462
+ expect(user.audits.pluck(:version)).to eq([2, 3])
463
+ end
464
+ end
465
+
466
+ it "should not delete old audits when keeped amount not exceeded" do
467
+ stub_global_max_audits(3) do
468
+ user = create_versions(2)
469
+ user.update(name: 'John')
470
+ expect(user.audits.pluck(:version)).to eq([1, 2, 3])
471
+ end
472
+ end
473
+
474
+ it "should delete old extra audits after introducing limit" do
475
+ stub_global_max_audits(nil) do
476
+ user = Models::ActiveRecord::User.create!(name: 'Brandon', username: 'brandon')
477
+ user.update_attributes(name: 'Foobar')
478
+ user.update_attributes(name: 'Awesome', username: 'keepers')
479
+ user.update_attributes(activated: true)
480
+
481
+ Audited.max_audits = 3
482
+ Models::ActiveRecord::User.send(:normalize_audited_options)
483
+ user.update_attributes(favourite_device: 'Android Phone')
484
+ audits = user.audits
485
+
486
+ expect(audits.count).to eq(3)
487
+ expect(audits[0].audited_changes).to include({'name' => ['Foobar', 'Awesome'], 'username' => ['brandon', 'keepers']})
488
+ expect(audits[1].audited_changes).to eq({'activated' => [nil, true]})
489
+ expect(audits[2].audited_changes).to eq({'favourite_device' => [nil, 'Android Phone']})
490
+ end
491
+ end
492
+
493
+ it "should add comment line for combined audit" do
494
+ stub_global_max_audits(2) do
495
+ user = Models::ActiveRecord::User.create!(name: 'Foobar 1')
496
+ user.update(name: 'Foobar 2', audit_comment: 'First audit comment')
497
+ user.update(name: 'Foobar 3', audit_comment: 'Second audit comment')
498
+ expect(user.audits.first.comment).to match(/First audit comment.+is the result of multiple/m)
499
+ end
500
+ end
501
+
502
+ def stub_global_max_audits(max_audits)
503
+ previous_max_audits = Audited.max_audits
504
+ previous_user_audited_options = Models::ActiveRecord::User.audited_options.dup
505
+ begin
506
+ Audited.max_audits = max_audits
507
+ Models::ActiveRecord::User.send(:normalize_audited_options) # reloads audited_options
508
+ yield
509
+ ensure
510
+ Audited.max_audits = previous_max_audits
511
+ Models::ActiveRecord::User.audited_options = previous_user_audited_options
512
+ end
513
+ end
514
+ end
515
+
332
516
  describe "revisions" do
333
517
  let( :user ) { create_versions }
334
518
 
@@ -544,28 +728,44 @@ describe Audited::Auditor do
544
728
  describe "comment required" do
545
729
 
546
730
  describe "on create" do
547
- it "should not validate when audit_comment is not supplied" do
548
- expect(Models::ActiveRecord::CommentRequiredUser.new).not_to be_valid
731
+ it "should not validate when audit_comment is not supplied when initialized" do
732
+ expect(Models::ActiveRecord::CommentRequiredUser.new(name: 'Foo')).not_to be_valid
733
+ end
734
+
735
+ it "should not validate when audit_comment is not supplied trying to create" do
736
+ expect(Models::ActiveRecord::CommentRequiredUser.create(name: 'Foo')).not_to be_valid
549
737
  end
550
738
 
551
739
  it "should validate when audit_comment is supplied" do
552
- expect(Models::ActiveRecord::CommentRequiredUser.new( audit_comment: 'Create')).to be_valid
740
+ expect(Models::ActiveRecord::CommentRequiredUser.create(name: 'Foo', audit_comment: 'Create')).to be_valid
741
+ end
742
+
743
+ it "should validate when audit_comment is not supplied, and creating is not being audited" do
744
+ expect(Models::ActiveRecord::OnUpdateCommentRequiredUser.create(name: 'Foo')).to be_valid
745
+ expect(Models::ActiveRecord::OnDestroyCommentRequiredUser.create(name: 'Foo')).to be_valid
553
746
  end
554
747
 
555
748
  it "should validate when audit_comment is not supplied, and auditing is disabled" do
556
749
  Models::ActiveRecord::CommentRequiredUser.disable_auditing
557
- expect(Models::ActiveRecord::CommentRequiredUser.new).to be_valid
750
+ expect(Models::ActiveRecord::CommentRequiredUser.create(name: 'Foo')).to be_valid
558
751
  Models::ActiveRecord::CommentRequiredUser.enable_auditing
559
752
  end
560
753
  end
561
754
 
562
755
  describe "on update" do
563
756
  let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( audit_comment: 'Create' ) }
757
+ let( :on_create_user ) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
758
+ let( :on_destroy_user ) { Models::ActiveRecord::OnDestroyCommentRequiredUser.create }
564
759
 
565
760
  it "should not validate when audit_comment is not supplied" do
566
761
  expect(user.update_attributes(name: 'Test')).to eq(false)
567
762
  end
568
763
 
764
+ it "should validate when audit_comment is not supplied, and updating is not being audited" do
765
+ expect(on_create_user.update_attributes(name: 'Test')).to eq(true)
766
+ expect(on_destroy_user.update_attributes(name: 'Test')).to eq(true)
767
+ end
768
+
569
769
  it "should validate when audit_comment is supplied" do
570
770
  expect(user.update_attributes(name: 'Test', audit_comment: 'Update')).to eq(true)
571
771
  end
@@ -579,6 +779,8 @@ describe Audited::Auditor do
579
779
 
580
780
  describe "on destroy" do
581
781
  let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( audit_comment: 'Create' )}
782
+ let( :on_create_user ) { Models::ActiveRecord::OnCreateCommentRequiredUser.create!( audit_comment: 'Create' ) }
783
+ let( :on_update_user ) { Models::ActiveRecord::OnUpdateCommentRequiredUser.create }
582
784
 
583
785
  it "should not validate when audit_comment is not supplied" do
584
786
  expect(user.destroy).to eq(false)
@@ -589,6 +791,11 @@ describe Audited::Auditor do
589
791
  expect(user.destroy).to eq(user)
590
792
  end
591
793
 
794
+ it "should validate when audit_comment is not supplied, and destroying is not being audited" do
795
+ expect(on_create_user.destroy).to eq(on_create_user)
796
+ expect(on_update_user.destroy).to eq(on_update_user)
797
+ end
798
+
592
799
  it "should validate when audit_comment is not supplied, and auditing is disabled" do
593
800
  Models::ActiveRecord::CommentRequiredUser.disable_auditing
594
801
  expect(user.destroy).to eq(user)
@@ -0,0 +1,69 @@
1
+ require "spec_helper"
2
+
3
+ describe Models::ActiveRecord::UserExceptPassword do
4
+ let(:non_audited_columns) { subject.class.non_audited_columns }
5
+
6
+ it { should_not be_audited.only(non_audited_columns) }
7
+ it { should be_audited.except(:password) }
8
+ it { should_not be_audited.requires_comment }
9
+ it { should be_audited.on(:create, :update, :destroy) }
10
+ # test chaining
11
+ it { should be_audited.except(:password).on(:create, :update, :destroy) }
12
+ end
13
+
14
+ describe Models::ActiveRecord::UserOnlyPassword do
15
+ let(:audited_columns) { subject.class.audited_columns }
16
+
17
+ it { should be_audited.only(:password) }
18
+ it { should_not be_audited.except(audited_columns) }
19
+ it { should_not be_audited.requires_comment }
20
+ it { should be_audited.on(:create, :update, :destroy) }
21
+ it { should be_audited.only(:password).on(:create, :update, :destroy) }
22
+ end
23
+
24
+ describe Models::ActiveRecord::CommentRequiredUser do
25
+ let(:audited_columns) { subject.class.audited_columns }
26
+ let(:non_audited_columns) { subject.class.non_audited_columns }
27
+
28
+ it { should_not be_audited.only(non_audited_columns) }
29
+ it { should_not be_audited.except(audited_columns) }
30
+ it { should be_audited.requires_comment }
31
+ it { should be_audited.on(:create, :update, :destroy) }
32
+ it { should be_audited.requires_comment.on(:create, :update, :destroy) }
33
+ end
34
+
35
+ describe Models::ActiveRecord::OnCreateCommentRequiredUser do
36
+ let(:audited_columns) { subject.class.audited_columns }
37
+ let(:non_audited_columns) { subject.class.non_audited_columns }
38
+
39
+ it { should_not be_audited.only(non_audited_columns) }
40
+ it { should_not be_audited.except(audited_columns) }
41
+ it { should be_audited.requires_comment }
42
+ it { should be_audited.on(:create) }
43
+ it { should_not be_audited.on(:update, :destroy) }
44
+ it { should be_audited.requires_comment.on(:create) }
45
+ end
46
+
47
+ describe Models::ActiveRecord::OnUpdateCommentRequiredUser do
48
+ let(:audited_columns) { subject.class.audited_columns }
49
+ let(:non_audited_columns) { subject.class.non_audited_columns }
50
+
51
+ it { should_not be_audited.only(non_audited_columns) }
52
+ it { should_not be_audited.except(audited_columns) }
53
+ it { should be_audited.requires_comment }
54
+ it { should be_audited.on(:update) }
55
+ it { should_not be_audited.on(:create, :destroy) }
56
+ it { should be_audited.requires_comment.on(:update) }
57
+ end
58
+
59
+ describe Models::ActiveRecord::OnDestroyCommentRequiredUser do
60
+ let(:audited_columns) { subject.class.audited_columns }
61
+ let(:non_audited_columns) { subject.class.non_audited_columns }
62
+
63
+ it { should_not be_audited.only(non_audited_columns) }
64
+ it { should_not be_audited.except(audited_columns) }
65
+ it { should be_audited.requires_comment }
66
+ it { should be_audited.on(:destroy) }
67
+ it { should_not be_audited.on(:create, :update) }
68
+ it { should be_audited.requires_comment.on(:destroy) }
69
+ end
@@ -8,8 +8,8 @@ module AuditedSpecHelpers
8
8
  Models::ActiveRecord::User.new({name: 'darth', username: 'darth', password: 'noooooooo'}.merge(attrs))
9
9
  end
10
10
 
11
- def create_versions(n = 2)
12
- Models::ActiveRecord::User.create(name: 'Foobar 1').tap do |u|
11
+ def create_versions(n = 2, attrs = {})
12
+ Models::ActiveRecord::User.create(name: 'Foobar 1', **attrs).tap do |u|
13
13
  (n - 1).times do |i|
14
14
  u.update_attribute :name, "Foobar #{i + 2}"
15
15
  end
@@ -17,4 +17,16 @@ module AuditedSpecHelpers
17
17
  end
18
18
  end
19
19
 
20
+ def run_migrations(direction, migrations_paths, target_version = nil)
21
+ if rails_below?('5.2.0.rc1')
22
+ ActiveRecord::Migrator.send(direction, migrations_paths, target_version)
23
+ else
24
+ ActiveRecord::MigrationContext.new(migrations_paths).send(direction, target_version)
25
+ end
26
+ end
27
+
28
+ def rails_below?(rails_version)
29
+ Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
30
+ end
31
+
20
32
  end
@@ -7,6 +7,7 @@ end
7
7
  require 'rails_app/config/environment'
8
8
  require 'rspec/rails'
9
9
  require 'audited'
10
+ require 'audited-rspec'
10
11
  require 'audited_spec_helpers'
11
12
  require 'support/active_record/models'
12
13
 
@@ -4,7 +4,7 @@ require File.expand_path('../schema', __FILE__)
4
4
  module Models
5
5
  module ActiveRecord
6
6
  class User < ::ActiveRecord::Base
7
- audited allow_mass_assignment: true, except: :password
7
+ audited except: :password
8
8
  attribute :non_column_attr if Rails.version >= '5.1'
9
9
  attr_protected :logins if respond_to?(:attr_protected)
10
10
 
@@ -13,10 +13,15 @@ module Models
13
13
  end
14
14
  end
15
15
 
16
+ class UserExceptPassword < ::ActiveRecord::Base
17
+ self.table_name = :users
18
+ audited except: :password
19
+ end
20
+
16
21
  class UserOnlyPassword < ::ActiveRecord::Base
17
22
  self.table_name = :users
18
23
  attribute :non_column_attr if Rails.version >= '5.1'
19
- audited allow_mass_assignment: true, only: :password
24
+ audited only: :password
20
25
  end
21
26
 
22
27
  class CommentRequiredUser < ::ActiveRecord::Base
@@ -24,6 +29,21 @@ module Models
24
29
  audited comment_required: true
25
30
  end
26
31
 
32
+ class OnCreateCommentRequiredUser < ::ActiveRecord::Base
33
+ self.table_name = :users
34
+ audited comment_required: true, on: :create
35
+ end
36
+
37
+ class OnUpdateCommentRequiredUser < ::ActiveRecord::Base
38
+ self.table_name = :users
39
+ audited comment_required: true, on: :update
40
+ end
41
+
42
+ class OnDestroyCommentRequiredUser < ::ActiveRecord::Base
43
+ self.table_name = :users
44
+ audited comment_required: true, on: :destroy
45
+ end
46
+
27
47
  class AccessibleAfterDeclarationUser < ::ActiveRecord::Base
28
48
  self.table_name = :users
29
49
  audited
@@ -38,7 +58,7 @@ module Models
38
58
 
39
59
  class NoAttributeProtectionUser < ::ActiveRecord::Base
40
60
  self.table_name = :users
41
- audited allow_mass_assignment: true
61
+ audited
42
62
  end
43
63
 
44
64
  class UserWithAfterAudit < ::ActiveRecord::Base
@@ -57,6 +77,11 @@ module Models
57
77
  end
58
78
  end
59
79
 
80
+ class MaxAuditsUser < ::ActiveRecord::Base
81
+ self.table_name = :users
82
+ audited max_audits: 5
83
+ end
84
+
60
85
  class Company < ::ActiveRecord::Base
61
86
  audited
62
87
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: audited
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.6.0
4
+ version: 4.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Keepers
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2018-01-09 00:00:00.000000000 Z
16
+ date: 2018-03-14 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: activerecord
@@ -168,6 +168,7 @@ files:
168
168
  - lib/generators/audited/upgrade_generator.rb
169
169
  - spec/audited/audit_spec.rb
170
170
  - spec/audited/auditor_spec.rb
171
+ - spec/audited/rspec_matchers_spec.rb
171
172
  - spec/audited/sweeper_spec.rb
172
173
  - spec/audited_spec_helpers.rb
173
174
  - spec/rails_app/config/application.rb