notifiably_audited 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +8 -8
  2. data/.gitignore +17 -0
  3. data/Appraisals +11 -0
  4. data/CHANGELOG +34 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +234 -0
  8. data/Rakefile +24 -0
  9. data/audited-activerecord.gemspec +21 -0
  10. data/audited-mongo_mapper.gemspec +21 -0
  11. data/audited.gemspec +27 -0
  12. data/gemfiles/rails30.gemfile +7 -0
  13. data/gemfiles/rails31.gemfile +7 -0
  14. data/gemfiles/rails32.gemfile +7 -0
  15. data/lib/audited/audit.rb +115 -0
  16. data/lib/audited/auditor.rb +449 -0
  17. data/lib/audited/rspec_matchers.rb +173 -0
  18. data/lib/audited/sweeper.rb +51 -0
  19. data/lib/audited.rb +15 -0
  20. data/lib/notifiably_audited/version.rb +3 -0
  21. data/lib/notifiably_audited.rb +15 -0
  22. data/notifiably_audited.gemspec +23 -0
  23. data/spec/audited_spec_helpers.rb +31 -0
  24. data/spec/rails_app/config/application.rb +5 -0
  25. data/spec/rails_app/config/database.yml +24 -0
  26. data/spec/rails_app/config/environment.rb +5 -0
  27. data/spec/rails_app/config/environments/development.rb +19 -0
  28. data/spec/rails_app/config/environments/production.rb +33 -0
  29. data/spec/rails_app/config/environments/test.rb +33 -0
  30. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  31. data/spec/rails_app/config/initializers/inflections.rb +2 -0
  32. data/spec/rails_app/config/initializers/secret_token.rb +2 -0
  33. data/spec/rails_app/config/routes.rb +6 -0
  34. data/spec/spec_helper.rb +23 -0
  35. data/spec/support/active_record/models.rb +84 -0
  36. data/spec/support/active_record/schema.rb +54 -0
  37. data/spec/support/mongo_mapper/connection.rb +4 -0
  38. data/spec/support/mongo_mapper/models.rb +210 -0
  39. data/test/db/version_1.rb +17 -0
  40. data/test/db/version_2.rb +18 -0
  41. data/test/db/version_3.rb +19 -0
  42. data/test/db/version_4.rb +20 -0
  43. data/test/db/version_5.rb +18 -0
  44. data/test/install_generator_test.rb +17 -0
  45. data/test/test_helper.rb +19 -0
  46. data/test/upgrade_generator_test.rb +65 -0
  47. metadata +159 -31
  48. data/lib/audited/adapters/active_record/audit.rb +0 -69
  49. data/lib/audited/adapters/active_record.rb +0 -15
  50. data/lib/audited-activerecord.rb +0 -2
  51. data/lib/generators/audited/install_generator.rb +0 -28
  52. data/lib/generators/audited/templates/add_association_to_audits.rb +0 -11
  53. data/lib/generators/audited/templates/add_comment_to_audits.rb +0 -9
  54. data/lib/generators/audited/templates/add_remote_address_to_audits.rb +0 -10
  55. data/lib/generators/audited/templates/install.rb +0 -35
  56. data/lib/generators/audited/templates/rename_association_to_associated.rb +0 -23
  57. data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +0 -9
  58. data/lib/generators/audited/templates/rename_parent_to_association.rb +0 -11
  59. data/lib/generators/audited/upgrade_generator.rb +0 -63
@@ -0,0 +1,449 @@
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
+ # 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::Adapters::ActiveRecord::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
+ # * +protect+ - If your model uses +attr_protected+, set this to false to prevent Rails from
34
+ # raising an error. If you declare +attr_accessible+ before calling +audited+, it
35
+ # will automatically default to false. You only need to explicitly set this if you are
36
+ # calling +attr_accessible+ after.
37
+ #
38
+ # * +require_comment+ - Ensures that audit_comment is supplied before
39
+ # any create, update or destroy operation.
40
+ #
41
+ # class User < ActiveRecord::Base
42
+ # audited :protect => false
43
+ # attr_accessible :name
44
+ # end
45
+ #
46
+ def notifiably_audited(options = {})
47
+ # don't allow multiple calls
48
+ return if self.included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
49
+
50
+ class_attribute :non_audited_columns, :instance_writer => false
51
+ class_attribute :auditing_enabled, :instance_writer => false
52
+ class_attribute :audit_associated_with, :instance_writer => false
53
+
54
+ #====== beck added for notifiable ====
55
+ class_attribute :audit_alert_to, :instance_writer => false
56
+ class_attribute :audit_alert_for, :instance_writer => false
57
+ class_attribute :audit_title, :instance_writer => false
58
+ class_attribute :audit_create_comment, :instance_writer => false
59
+ class_attribute :audit_update_comment, :instance_writer => false
60
+ #=====================================
61
+
62
+ if options[:only]
63
+ except = self.column_names - options[:only].flatten.map(&:to_s)
64
+ else
65
+ except = default_ignored_attributes + Audited.ignored_attributes
66
+ except |= Array(options[:except]).collect(&:to_s) if options[:except]
67
+ end
68
+ self.non_audited_columns = except
69
+ self.audit_associated_with = options[:associated_with]
70
+
71
+ #====== beck added for notifiable ====
72
+ self.audit_alert_to = options[:alert_to] || :assigned_to
73
+ self.audit_alert_for = options[:alert_for] || nil
74
+ self.audit_title = options[:title] || :name
75
+ self.audit_create_comment = options[:create_comment] || "New <<here>> has been created"
76
+ self.audit_update_comment = options[:update_comment] || "Values of <<here>> has been updated"
77
+ #=====================================
78
+
79
+ if options[:comment_required]
80
+ validates_presence_of :audit_comment, :if => :auditing_enabled
81
+ before_destroy :require_comment
82
+ end
83
+
84
+ attr_accessor :audit_comment
85
+ unless options[:allow_mass_assignment]
86
+ attr_accessible :audit_comment
87
+ end
88
+
89
+ has_many :audits, :as => :auditable, :class_name => Audited.audit_class.name
90
+ Audited.audit_class.audited_class_names << self.to_s
91
+
92
+ after_create :audit_create if !options[:on] || (options[:on] && options[:on].include?(:create))
93
+ before_update :audit_update if !options[:on] || (options[:on] && options[:on].include?(:update))
94
+ before_destroy :audit_destroy if !options[:on] || (options[:on] && options[:on].include?(:destroy))
95
+
96
+ # Define and set an after_audit callback. This might be useful if you want
97
+ # to notify a party after the audit has been created.
98
+ define_callbacks :audit
99
+ set_callback :audit, :after, :after_audit, :if => lambda { self.respond_to?(:after_audit) }
100
+
101
+ attr_accessor :version
102
+
103
+ extend Audited::Auditor::AuditedClassMethods
104
+ include Audited::Auditor::AuditedInstanceMethods
105
+
106
+ self.auditing_enabled = true
107
+
108
+ end
109
+
110
+ def has_associated_audits
111
+ has_many :associated_audits, :as => :associated, :class_name => Audited.audit_class.name
112
+ end
113
+ end
114
+
115
+ #======== Audit Instance Methods - Means the methods on the target object =====================
116
+
117
+ #====== beck modified for notifiable ====
118
+ # @non_monitor is 1 initially, if somewhere an audit is created this is set to 0, so no future
119
+ # audit will be created.
120
+
121
+ # if polymorphic, then
122
+ # [:polymorphic,"title",
123
+ # col of self to be displayed as comment,[:commentable_type,:commentable_id]]
124
+ #----------------------------------------------
125
+ # opts format for notifiably_audited method/gem
126
+ #----------------------------------------------
127
+ # notifiably_audited alert_for: [[[:assigned_to],
128
+ # "Re-assigned",
129
+ # "This product has been reassigned",
130
+ # [:user,:email]],
131
+ # [[:color,:score],
132
+ # "Color/Score Changed",
133
+ # "Color/Score value is changed"],
134
+ # [[:product_status_id],
135
+ # "Status Changed",
136
+ # "Status of this product is changed",
137
+ # [:product_status,:name]]],
138
+ # associated_with: :product_status,
139
+ # title: :name,
140
+ # create_comment: "New <<here>> has been created",
141
+ # update_comment: "Custom: Values of <<here>> has been updated"
142
+ #
143
+ # &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
144
+ #
145
+ # notifiably_audited alert_for: [[:polymorphic,
146
+ # nil,
147
+ # :content,
148
+ # [:commentable_type,:commentable_id]]]
149
+ #
150
+ # -----------
151
+ # alert_to :
152
+ # -----------
153
+ # The column of the target_object that has the user_id to send
154
+ # the notification to.(receiver_id of the Audit record)
155
+ # -----------
156
+ # alert_for :
157
+ # -----------
158
+ # Takes array of arrays as an argument. Every array in the main array corresponds to
159
+ # one or group of columns update and how to notify.
160
+ # Following is the index wise explanation of the arrays.
161
+ # 1. Column name or Column names
162
+ # 2. Title of the notification(title column of the Audit record)
163
+ # 3. Description of the notification(audit_comment column of the Audit record)
164
+ # 4. If the column name is a foreign key of an belongs_to association, then the model_name of the
165
+ # associated model and the column of the model to be displayed in title is specified
166
+ # -----------
167
+ # title :
168
+ # -----------
169
+ # Takes 1 column name or a method name of the target object as an argument, in order to prompt the
170
+ # title of the target object wherever needed in the notification
171
+ # ---------------
172
+ # create_comment :
173
+ # ---------------
174
+ # Default audit_comment for create action
175
+ # ---------------
176
+ # update_comment :
177
+ # ---------------
178
+ # Default audit_comment for update action
179
+ #
180
+ # ***************
181
+ # :polymorphic
182
+ # ***************
183
+ # If this is passed as the first argument of the alert_for option, then it means, the current_model
184
+ # is polymorphic and the target_object is a different model to which the current model is
185
+ # polymorphic to.
186
+ #
187
+ # In the above case the 4th argument of the alert_for option should be the type and id of the
188
+ # polymorphic model.(Ex: [:commentable_type,commentable_id])
189
+ #----------------------------------------------
190
+ #=====================================
191
+
192
+ module AuditedInstanceMethods
193
+ # Temporarily turns off auditing while saving.
194
+ def save_without_auditing
195
+ without_auditing { save }
196
+ end
197
+
198
+ # Executes the block with the auditing callbacks disabled.
199
+ #
200
+ # @foo.without_auditing do
201
+ # @foo.save
202
+ # end
203
+ #
204
+ def without_auditing(&block)
205
+ self.class.without_auditing(&block)
206
+ end
207
+
208
+ # Gets an array of the revisions available
209
+ #
210
+ # user.revisions.each do |revision|
211
+ # user.name
212
+ # user.version
213
+ # end
214
+ #
215
+ def revisions(from_version = 1)
216
+ audits = self.audits.from_version(from_version)
217
+ return [] if audits.empty?
218
+ revisions = []
219
+ audits.each do |audit|
220
+ revisions << audit.revision
221
+ end
222
+ revisions
223
+ end
224
+
225
+ # Get a specific revision specified by the version number, or +:previous+
226
+ def revision(version)
227
+ revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
228
+ end
229
+
230
+ # Find the oldest revision recorded prior to the date/time provided.
231
+ def revision_at(date_or_time)
232
+ audits = self.audits.up_until(date_or_time)
233
+ revision_with Audited.audit_class.reconstruct_attributes(audits) unless audits.empty?
234
+ end
235
+
236
+ # List of attributes that are audited.
237
+ def audited_attributes
238
+ attributes.except(*non_audited_columns)
239
+ end
240
+
241
+ protected
242
+
243
+ def revision_with(attributes)
244
+ self.dup.tap do |revision|
245
+ revision.id = id
246
+ revision.send :instance_variable_set, '@attributes', self.attributes
247
+ revision.send :instance_variable_set, '@new_record', self.destroyed?
248
+ revision.send :instance_variable_set, '@persisted', !self.destroyed?
249
+ revision.send :instance_variable_set, '@readonly', false
250
+ revision.send :instance_variable_set, '@destroyed', false
251
+ revision.send :instance_variable_set, '@_destroyed', false
252
+ revision.send :instance_variable_set, '@marked_for_destruction', false
253
+ Audited.audit_class.assign_revision_attributes(revision, attributes)
254
+
255
+ # Remove any association proxies so that they will be recreated
256
+ # and reference the correct object for this revision. The only way
257
+ # to determine if an instance variable is a proxy object is to
258
+ # see if it responds to certain methods, as it forwards almost
259
+ # everything to its target.
260
+ for ivar in revision.instance_variables
261
+ proxy = revision.instance_variable_get ivar
262
+ if !proxy.nil? and proxy.respond_to? :proxy_respond_to?
263
+ revision.instance_variable_set ivar, nil
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ private
270
+
271
+ def audited_changes
272
+ changed_attributes.except(*non_audited_columns).inject({}) do |changes,(attr, old_value)|
273
+ changes[attr] = [old_value, self[attr]]
274
+ changes
275
+ end
276
+ end
277
+
278
+ def audits_to(version = nil)
279
+ if version == :previous
280
+ version = if self.version
281
+ self.version - 1
282
+ else
283
+ previous = audits.descending.offset(1).first
284
+ previous ? previous.version : 1
285
+ end
286
+ end
287
+ audits.to_version(version)
288
+ end
289
+ #====== beck modified for notifiable ====
290
+
291
+ def audit_create
292
+ set_audit_values("create")# also sets @non_monitor as 1
293
+ if !audit_alert_for.nil?
294
+ audit_alert_for.each do |f|
295
+ if f[0] == :polymorphic
296
+ polymorphic_audit(f)# also sets @non_monitor as 0
297
+ end
298
+ end
299
+ end
300
+
301
+ if @non_monitor == 1
302
+ write_audit(@audit_values)
303
+ end
304
+ end
305
+
306
+ def audit_update
307
+ unless (changes = audited_changes).empty? && audit_comment.blank?
308
+ set_audit_values("update")# also sets @non_monitor as 1
309
+
310
+ if !audit_alert_for.nil?
311
+ audit_alert_for.each do |f|
312
+ if f[0] == :polymorphic
313
+ polymorphic_audit(f)# also sets @non_monitor as 0
314
+ else
315
+ changed_eq_opts(f)# also sets @non_monitor as 0
316
+ end
317
+ end
318
+ end
319
+
320
+ if @non_monitor == 1
321
+ write_audit(@audit_values)
322
+ end
323
+
324
+ end
325
+ end
326
+
327
+ def audit_destroy
328
+ write_audit(:action => 'destroy', :audited_changes => audited_attributes,
329
+ :comment => audit_comment, :receiver_id => self.send(audit_alert_to), :checked => false)
330
+ end
331
+ #=== Following methods helps audit_create and update ======
332
+ # set_audit_values
333
+ # polymorphic_audit
334
+ # changed_eq_opts
335
+ #==========================================================
336
+
337
+ def set_audit_values(type)
338
+ @non_monitor = 1
339
+ # based on type the audit attributes are set
340
+ if (type == "create")
341
+ v_audited_changes = audited_attributes
342
+ elsif (type == "update")
343
+ v_audited_changes = changes
344
+ end
345
+ # the <<here>> part of the comment is replaced with class name
346
+ v_comment = send("audit_" + type +"_comment").gsub(/<<here>>/,self.class.name)
347
+ # fields of the audit record set initially as hash, can be over ridden
348
+ @audit_values = {action: type,
349
+ audited_changes: v_audited_changes,
350
+ comment: v_comment,
351
+ title: (self.send(audit_title) rescue self.class.name),
352
+ checked: false,
353
+ receiver_id: (self.send(audit_alert_to) rescue nil)}
354
+ end
355
+
356
+ def polymorphic_audit(opts)
357
+ # polymorphic - so the actual object is set, not the polymorphed object(product not comment)
358
+ target_object = self.send(opts[3][0]).constantize.find(self.send(opts[3][1]))
359
+ # overriding the audit values based on polymorphic opts
360
+ @audit_values[:title] = (opts[1] || @audit_values[:title]).to_s + " - #{target_object.class.name}[#{target_object.send(audit_title)}]"
361
+ @audit_values[:comment] = self.send(opts[2])[0..20].to_s + "..."
362
+ @audit_values[:receiver_id] = target_object.send(audit_alert_to)
363
+ # actual recording of audit
364
+ write_audit(@audit_values)
365
+ # setting to 0, so dont record anymore audits
366
+ @non_monitor = 0
367
+ end
368
+
369
+ def changed_eq_opts(opts)
370
+ # the cols that are going to be modified as identified by audited gem
371
+ changed = changes.keys.map &:to_sym
372
+ # the cols to be listened to, as passed in the opts
373
+ opts_changes = opts[0]
374
+ # if the sub array(to be listened) is included in the main array(audited identified cols)
375
+ if ((opts_changes - changed).size == 0)
376
+ # overriding audit values
377
+ @audit_values[:title] = opts[1]
378
+ @audit_values[:comment] = opts[2]
379
+ # if 3rd argument is present in the opts, then overriding title
380
+ if opts[3].present?
381
+ append = opts[3][0].to_s.camelize.constantize.find(self.send(opts[0][0])).send(opts[3][1])
382
+ @audit_values[:title] = @audit_values[:title] + "[#{append}]"
383
+ end
384
+ # actual recording of audit
385
+ write_audit(@audit_values)
386
+ # setting to 0, so dont record anymore audits
387
+ @non_monitor = 0
388
+ end
389
+ end
390
+ #========================================
391
+
392
+ def write_audit(attrs)
393
+ attrs[:associated] = self.send(audit_associated_with) unless audit_associated_with.nil?
394
+ self.audit_comment = nil
395
+ run_callbacks(:audit) { self.audits.create(attrs) } if auditing_enabled
396
+ end
397
+
398
+ def require_comment
399
+ if auditing_enabled && audit_comment.blank?
400
+ errors.add(:audit_comment, "Comment required before destruction")
401
+ return false
402
+ end
403
+ end
404
+
405
+ CALLBACKS.each do |attr_name|
406
+ alias_method "#{attr_name}_callback".to_sym, attr_name
407
+ end
408
+
409
+ def empty_callback #:nodoc:
410
+ end
411
+
412
+ end # InstanceMethods
413
+
414
+ module AuditedClassMethods
415
+ # Returns an array of columns that are audited. See non_audited_columns
416
+ def audited_columns
417
+ self.columns.select { |c| !non_audited_columns.include?(c.name) }
418
+ end
419
+
420
+ # Executes the block with auditing disabled.
421
+ #
422
+ # Foo.without_auditing do
423
+ # @foo.save
424
+ # end
425
+ #
426
+ def without_auditing(&block)
427
+ auditing_was_enabled = auditing_enabled
428
+ disable_auditing
429
+ block.call.tap { enable_auditing if auditing_was_enabled }
430
+ end
431
+
432
+ def disable_auditing
433
+ self.auditing_enabled = false
434
+ end
435
+
436
+ def enable_auditing
437
+ self.auditing_enabled = true
438
+ end
439
+
440
+ # All audit operations during the block are recorded as being
441
+ # made by +user+. This is not model specific, the method is a
442
+ # convenience wrapper around
443
+ # @see Audit#as_user.
444
+ def audit_as( user, &block )
445
+ Audited.audit_class.as_user( user, &block )
446
+ end
447
+ end
448
+ end
449
+ end
@@ -0,0 +1,173 @@
1
+ module Audited
2
+ module RspecMatchers
3
+ # Ensure that the model is audited.
4
+ #
5
+ # Options:
6
+ # * <tt>associated_with</tt> - tests that the audit makes use of the associated_with option
7
+ # * <tt>only</tt> - tests that the audit makes use of the only option *Overrides <tt>except</tt> option*
8
+ # * <tt>except</tt> - tests that the audit makes use of the except option
9
+ # * <tt>requires_comment</tt> - if specified, then the audit must require comments through the <tt>audit_comment</tt> attribute
10
+ # * <tt>on</tt> - tests that the audit makes use of the on option with specified parameters
11
+ #
12
+ # Example:
13
+ # it { should be_audited }
14
+ # it { should be_audited.associated_with(:user) }
15
+ # it { should be_audited.only(:field_name) }
16
+ # it { should be_audited.except(:password) }
17
+ # it { should be_audited.requires_comment }
18
+ # it { should be_audited.on(:create).associated_with(:user).except(:password) }
19
+ #
20
+ def be_audited
21
+ AuditMatcher.new
22
+ end
23
+
24
+ # Ensure that the model has associated audits
25
+ #
26
+ # Example:
27
+ # it { should have_associated_audits }
28
+ #
29
+ def have_associated_audits
30
+ AssociatedAuditMatcher.new
31
+ end
32
+
33
+ class AuditMatcher # :nodoc:
34
+ def initialize
35
+ @options = {}
36
+ end
37
+
38
+ def associated_with(model)
39
+ @options[:associated_with] = model
40
+ self
41
+ end
42
+
43
+ def only(*fields)
44
+ @options[:only] = fields.flatten
45
+ self
46
+ end
47
+
48
+ def except(*fields)
49
+ @options[:except] = fields.flatten
50
+ self
51
+ end
52
+
53
+ def requires_comment
54
+ @options[:comment_required] = true
55
+ self
56
+ end
57
+
58
+ def on(*actions)
59
+ @options[:on] = actions.flatten
60
+ self
61
+ end
62
+
63
+ def matches?(subject)
64
+ @subject = subject
65
+ auditing_enabled? &&
66
+ associated_with_model? &&
67
+ records_changes_to_specified_fields? &&
68
+ comment_required_valid?
69
+ end
70
+
71
+ def failure_message
72
+ "Expected #{@expectation}"
73
+ end
74
+
75
+ def negative_failure_message
76
+ "Did not expect #{@expectation}"
77
+ end
78
+
79
+ def description
80
+ description = "audited"
81
+ description += " associated with #{@options[:associated_with]}" if @options.key?(:associated_with)
82
+ description += " only => #{@options[:only].join ', '}" if @options.key?(:only)
83
+ description += " except => #{@options[:except].join(', ')}" if @options.key?(:except)
84
+ description += " requires audit_comment" if @options.key?(:comment_required)
85
+
86
+ description
87
+ end
88
+
89
+ protected
90
+
91
+ def expects(message)
92
+ @expectation = message
93
+ end
94
+
95
+ def auditing_enabled?
96
+ expects "#{model_class} to be audited"
97
+ model_class.respond_to?(:auditing_enabled) && model_class.auditing_enabled
98
+ end
99
+
100
+ def model_class
101
+ @subject.class
102
+ end
103
+
104
+ def associated_with_model?
105
+ expects "#{model_class} to record audits to associated model #{@options[:associated_with]}"
106
+ model_class.audit_associated_with == @options[:associated_with]
107
+ end
108
+
109
+ def records_changes_to_specified_fields?
110
+ if @options[:only] || @options[:except]
111
+ if @options[:only]
112
+ except = model_class.column_names - @options[:only].map(&:to_s)
113
+ else
114
+ except = model_class.default_ignored_attributes + Audited.ignored_attributes
115
+ except |= @options[:except].collect(&:to_s) if @options[:except]
116
+ end
117
+
118
+ expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{expect})"
119
+ model_class.non_audited_columns =~ except
120
+ else
121
+ true
122
+ end
123
+ end
124
+
125
+ def comment_required_valid?
126
+ if @options[:comment_required]
127
+ @subject.audit_comment = nil
128
+
129
+ expects "to be invalid when audit_comment is not specified"
130
+ @subject.valid? == false && @subject.errors.key?(:audit_comment)
131
+ else
132
+ true
133
+ end
134
+ end
135
+ end
136
+
137
+ class AssociatedAuditMatcher # :nodoc:
138
+ def matches?(subject)
139
+ @subject = subject
140
+
141
+ association_exists?
142
+ end
143
+
144
+ def failure_message
145
+ "Expected #{model_class} to have associated audits"
146
+ end
147
+
148
+ def negative_failure_message
149
+ "Expected #{model_class} to not have associated audits"
150
+ end
151
+
152
+ def description
153
+ "has associated audits"
154
+ end
155
+
156
+ protected
157
+
158
+ def model_class
159
+ @subject.class
160
+ end
161
+
162
+ def reflection
163
+ model_class.reflect_on_association(:associated_audits)
164
+ end
165
+
166
+ def association_exists?
167
+ (!reflection.nil?) &&
168
+ reflection.macro == :has_many &&
169
+ reflection.options[:class_name] == Audited.audit_class.name
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,51 @@
1
+ module Audited
2
+ class Sweeper < ActiveModel::Observer
3
+ observe Audited.audit_class
4
+
5
+ def before(controller)
6
+ self.controller = controller
7
+ true
8
+ end
9
+
10
+ def after(controller)
11
+ self.controller = nil
12
+ end
13
+
14
+ def before_create(audit)
15
+ audit.user ||= current_user
16
+ audit.remote_address = controller.try(:request).try(:ip)
17
+ end
18
+
19
+ def current_user
20
+ controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true)
21
+ end
22
+
23
+ def add_observer!(klass)
24
+ super
25
+ define_callback(klass)
26
+ end
27
+
28
+ def define_callback(klass)
29
+ observer = self
30
+ callback_meth = :"_notify_audited_sweeper"
31
+ klass.send(:define_method, callback_meth) do
32
+ observer.update(:before_create, self)
33
+ end
34
+ klass.send(:before_create, callback_meth)
35
+ end
36
+
37
+ def controller
38
+ ::Audited.store[:current_controller]
39
+ end
40
+
41
+ def controller=(value)
42
+ ::Audited.store[:current_controller] = value
43
+ end
44
+ end
45
+ end
46
+
47
+ if defined?(ActionController) and defined?(ActionController::Base)
48
+ ActionController::Base.class_eval do
49
+ around_filter Audited::Sweeper.instance
50
+ end
51
+ end
data/lib/audited.rb ADDED
@@ -0,0 +1,15 @@
1
+ module Audited
2
+ VERSION = '3.0.0'
3
+
4
+ class << self
5
+ attr_accessor :ignored_attributes, :current_user_method, :audit_class
6
+
7
+ def store
8
+ Thread.current[:audited_store] ||= {}
9
+ end
10
+ end
11
+
12
+ @ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
13
+
14
+ @current_user_method = :current_user
15
+ end
@@ -0,0 +1,3 @@
1
+ module NotifiablyAudited
2
+ VERSION = "0.0.1"
3
+ end