notifiably_audited 0.0.6 → 0.0.7
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.
- checksums.yaml +8 -8
- data/.gitignore +17 -0
- data/Appraisals +11 -0
- data/CHANGELOG +34 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +234 -0
- data/Rakefile +24 -0
- data/audited-activerecord.gemspec +21 -0
- data/audited-mongo_mapper.gemspec +21 -0
- data/audited.gemspec +27 -0
- data/gemfiles/rails30.gemfile +7 -0
- data/gemfiles/rails31.gemfile +7 -0
- data/gemfiles/rails32.gemfile +7 -0
- data/lib/audited/audit.rb +115 -0
- data/lib/audited/auditor.rb +449 -0
- data/lib/audited/rspec_matchers.rb +173 -0
- data/lib/audited/sweeper.rb +51 -0
- data/lib/audited.rb +15 -0
- data/lib/notifiably_audited/version.rb +3 -0
- data/lib/notifiably_audited.rb +15 -0
- data/notifiably_audited.gemspec +23 -0
- data/spec/audited_spec_helpers.rb +31 -0
- data/spec/rails_app/config/application.rb +5 -0
- data/spec/rails_app/config/database.yml +24 -0
- data/spec/rails_app/config/environment.rb +5 -0
- data/spec/rails_app/config/environments/development.rb +19 -0
- data/spec/rails_app/config/environments/production.rb +33 -0
- data/spec/rails_app/config/environments/test.rb +33 -0
- data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails_app/config/initializers/inflections.rb +2 -0
- data/spec/rails_app/config/initializers/secret_token.rb +2 -0
- data/spec/rails_app/config/routes.rb +6 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/active_record/models.rb +84 -0
- data/spec/support/active_record/schema.rb +54 -0
- data/spec/support/mongo_mapper/connection.rb +4 -0
- data/spec/support/mongo_mapper/models.rb +210 -0
- data/test/db/version_1.rb +17 -0
- data/test/db/version_2.rb +18 -0
- data/test/db/version_3.rb +19 -0
- data/test/db/version_4.rb +20 -0
- data/test/db/version_5.rb +18 -0
- data/test/install_generator_test.rb +17 -0
- data/test/test_helper.rb +19 -0
- data/test/upgrade_generator_test.rb +65 -0
- metadata +159 -31
- data/lib/audited/adapters/active_record/audit.rb +0 -69
- data/lib/audited/adapters/active_record.rb +0 -15
- data/lib/audited-activerecord.rb +0 -2
- data/lib/generators/audited/install_generator.rb +0 -28
- data/lib/generators/audited/templates/add_association_to_audits.rb +0 -11
- data/lib/generators/audited/templates/add_comment_to_audits.rb +0 -9
- data/lib/generators/audited/templates/add_remote_address_to_audits.rb +0 -10
- data/lib/generators/audited/templates/install.rb +0 -35
- data/lib/generators/audited/templates/rename_association_to_associated.rb +0 -23
- data/lib/generators/audited/templates/rename_changes_to_audited_changes.rb +0 -9
- data/lib/generators/audited/templates/rename_parent_to_association.rb +0 -11
- 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
|