acts_as_audited_rails3 1.1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/.gitignore +4 -0
  2. data/CHANGELOG +25 -0
  3. data/LICENSE +19 -0
  4. data/README +76 -0
  5. data/Rakefile +60 -0
  6. data/VERSION +1 -0
  7. data/acts_as_audited.gemspec +90 -0
  8. data/acts_as_audited_rails3.gemspec +90 -0
  9. data/doc/classes/Audit.html +407 -0
  10. data/doc/classes/CollectiveIdea/Acts/Audited/ClassMethods.html +226 -0
  11. data/doc/classes/CollectiveIdea/Acts/Audited/InstanceMethods.html +330 -0
  12. data/doc/classes/CollectiveIdea/Acts/Audited/SingletonMethods.html +254 -0
  13. data/doc/created.rid +1 -0
  14. data/doc/files/README.html +226 -0
  15. data/doc/files/lib/acts_as_audited/audit_rb.html +108 -0
  16. data/doc/files/lib/acts_as_audited/audit_sweeper_rb.html +101 -0
  17. data/doc/files/lib/acts_as_audited_rb.html +129 -0
  18. data/doc/fr_class_index.html +30 -0
  19. data/doc/fr_file_index.html +30 -0
  20. data/doc/fr_method_index.html +47 -0
  21. data/doc/index.html +24 -0
  22. data/doc/rdoc-style.css +208 -0
  23. data/lib/acts_as_audited/audit.rb +119 -0
  24. data/lib/acts_as_audited/audit_sweeper.rb +37 -0
  25. data/lib/acts_as_audited/base.rb +316 -0
  26. data/lib/acts_as_audited.rb +9 -0
  27. data/lib/generators/audited_migration/USAGE +7 -0
  28. data/lib/generators/audited_migration/audited_migration_generator.rb +24 -0
  29. data/lib/generators/audited_migration/templates/migration.rb +29 -0
  30. data/lib/generators/audited_migration_update/USAGE +7 -0
  31. data/lib/generators/audited_migration_update/audited_migration_update_generator.rb +24 -0
  32. data/lib/generators/audited_migration_update/templates/migration.rb +9 -0
  33. data/test/acts_as_audited_test.rb +437 -0
  34. data/test/audit_sweeper_test.rb +31 -0
  35. data/test/audit_test.rb +179 -0
  36. data/test/db/database.yml +21 -0
  37. data/test/db/schema.rb +33 -0
  38. data/test/test_helper.rb +75 -0
  39. metadata +152 -0
data/doc/index.html ADDED
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="iso-8859-1"?>
2
+ <!DOCTYPE html
3
+ PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
4
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
5
+
6
+ <!--
7
+
8
+ acts_as_audited
9
+
10
+ -->
11
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
12
+ <head>
13
+ <title>acts_as_audited</title>
14
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
15
+ </head>
16
+ <frameset rows="20%, 80%">
17
+ <frameset cols="25%,35%,45%">
18
+ <frame src="fr_file_index.html" title="Files" name="Files" />
19
+ <frame src="fr_class_index.html" name="Classes" />
20
+ <frame src="fr_method_index.html" name="Methods" />
21
+ </frameset>
22
+ <frame src="files/README.html" name="docwin" />
23
+ </frameset>
24
+ </html>
@@ -0,0 +1,208 @@
1
+
2
+ body {
3
+ font-family: Verdana,Arial,Helvetica,sans-serif;
4
+ font-size: 90%;
5
+ margin: 0;
6
+ margin-left: 40px;
7
+ padding: 0;
8
+ background: white;
9
+ }
10
+
11
+ h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; }
12
+ h1 { font-size: 150%; }
13
+ h2,h3,h4 { margin-top: 1em; }
14
+
15
+ a { background: #eef; color: #039; text-decoration: none; }
16
+ a:hover { background: #039; color: #eef; }
17
+
18
+ /* Override the base stylesheet's Anchor inside a table cell */
19
+ td > a {
20
+ background: transparent;
21
+ color: #039;
22
+ text-decoration: none;
23
+ }
24
+
25
+ /* and inside a section title */
26
+ .section-title > a {
27
+ background: transparent;
28
+ color: #eee;
29
+ text-decoration: none;
30
+ }
31
+
32
+ /* === Structural elements =================================== */
33
+
34
+ div#index {
35
+ margin: 0;
36
+ margin-left: -40px;
37
+ padding: 0;
38
+ font-size: 90%;
39
+ }
40
+
41
+
42
+ div#index a {
43
+ margin-left: 0.7em;
44
+ }
45
+
46
+ div#index .section-bar {
47
+ margin-left: 0px;
48
+ padding-left: 0.7em;
49
+ background: #ccc;
50
+ font-size: small;
51
+ }
52
+
53
+
54
+ div#classHeader, div#fileHeader {
55
+ width: auto;
56
+ color: white;
57
+ padding: 0.5em 1.5em 0.5em 1.5em;
58
+ margin: 0;
59
+ margin-left: -40px;
60
+ border-bottom: 3px solid #006;
61
+ }
62
+
63
+ div#classHeader a, div#fileHeader a {
64
+ background: inherit;
65
+ color: white;
66
+ }
67
+
68
+ div#classHeader td, div#fileHeader td {
69
+ background: inherit;
70
+ color: white;
71
+ }
72
+
73
+
74
+ div#fileHeader {
75
+ background: #057;
76
+ }
77
+
78
+ div#classHeader {
79
+ background: #048;
80
+ }
81
+
82
+
83
+ .class-name-in-header {
84
+ font-size: 180%;
85
+ font-weight: bold;
86
+ }
87
+
88
+
89
+ div#bodyContent {
90
+ padding: 0 1.5em 0 1.5em;
91
+ }
92
+
93
+ div#description {
94
+ padding: 0.5em 1.5em;
95
+ background: #efefef;
96
+ border: 1px dotted #999;
97
+ }
98
+
99
+ div#description h1,h2,h3,h4,h5,h6 {
100
+ color: #125;;
101
+ background: transparent;
102
+ }
103
+
104
+ div#validator-badges {
105
+ text-align: center;
106
+ }
107
+ div#validator-badges img { border: 0; }
108
+
109
+ div#copyright {
110
+ color: #333;
111
+ background: #efefef;
112
+ font: 0.75em sans-serif;
113
+ margin-top: 5em;
114
+ margin-bottom: 0;
115
+ padding: 0.5em 2em;
116
+ }
117
+
118
+
119
+ /* === Classes =================================== */
120
+
121
+ table.header-table {
122
+ color: white;
123
+ font-size: small;
124
+ }
125
+
126
+ .type-note {
127
+ font-size: small;
128
+ color: #DEDEDE;
129
+ }
130
+
131
+ .xxsection-bar {
132
+ background: #eee;
133
+ color: #333;
134
+ padding: 3px;
135
+ }
136
+
137
+ .section-bar {
138
+ color: #333;
139
+ border-bottom: 1px solid #999;
140
+ margin-left: -20px;
141
+ }
142
+
143
+
144
+ .section-title {
145
+ background: #79a;
146
+ color: #eee;
147
+ padding: 3px;
148
+ margin-top: 2em;
149
+ margin-left: -30px;
150
+ border: 1px solid #999;
151
+ }
152
+
153
+ .top-aligned-row { vertical-align: top }
154
+ .bottom-aligned-row { vertical-align: bottom }
155
+
156
+ /* --- Context section classes ----------------------- */
157
+
158
+ .context-row { }
159
+ .context-item-name { font-family: monospace; font-weight: bold; color: black; }
160
+ .context-item-value { font-size: small; color: #448; }
161
+ .context-item-desc { color: #333; padding-left: 2em; }
162
+
163
+ /* --- Method classes -------------------------- */
164
+ .method-detail {
165
+ background: #efefef;
166
+ padding: 0;
167
+ margin-top: 0.5em;
168
+ margin-bottom: 1em;
169
+ border: 1px dotted #ccc;
170
+ }
171
+ .method-heading {
172
+ color: black;
173
+ background: #ccc;
174
+ border-bottom: 1px solid #666;
175
+ padding: 0.2em 0.5em 0 0.5em;
176
+ }
177
+ .method-signature { color: black; background: inherit; }
178
+ .method-name { font-weight: bold; }
179
+ .method-args { font-style: italic; }
180
+ .method-description { padding: 0 0.5em 0 0.5em; }
181
+
182
+ /* --- Source code sections -------------------- */
183
+
184
+ a.source-toggle { font-size: 90%; }
185
+ div.method-source-code {
186
+ background: #262626;
187
+ color: #ffdead;
188
+ margin: 1em;
189
+ padding: 0.5em;
190
+ border: 1px dashed #999;
191
+ overflow: hidden;
192
+ }
193
+
194
+ div.method-source-code pre { color: #ffdead; overflow: hidden; }
195
+
196
+ /* --- Ruby keyword styles --------------------- */
197
+
198
+ .standalone-code { background: #221111; color: #ffdead; overflow: hidden; }
199
+
200
+ .ruby-constant { color: #7fffd4; background: transparent; }
201
+ .ruby-keyword { color: #00ffff; background: transparent; }
202
+ .ruby-ivar { color: #eedd82; background: transparent; }
203
+ .ruby-operator { color: #00ffee; background: transparent; }
204
+ .ruby-identifier { color: #ffdead; background: transparent; }
205
+ .ruby-node { color: #ffa07a; background: transparent; }
206
+ .ruby-comment { color: #b22222; font-weight: bold; background: transparent; }
207
+ .ruby-regexp { color: #ffa07a; background: transparent; }
208
+ .ruby-value { color: #7fffd4; background: transparent; }
@@ -0,0 +1,119 @@
1
+ require 'set'
2
+
3
+ # Audit saves the changes to ActiveRecord models. It has the following attributes:
4
+ #
5
+ # * <tt>auditable</tt>: the ActiveRecord model that was changed
6
+ # * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model
7
+ # * <tt>action</tt>: one of create, update, or delete
8
+ # * <tt>audit_changes</tt>: a serialized hash of all the changes
9
+ # * <tt>created_at</tt>: Time that the change was performed
10
+ #
11
+ class Audit < ActiveRecord::Base
12
+ belongs_to :auditable, :polymorphic => true
13
+ belongs_to :user, :polymorphic => true
14
+
15
+ before_create :set_version_number, :set_audit_user
16
+
17
+ serialize :audit_changes
18
+
19
+ cattr_accessor :audited_class_names
20
+ self.audited_class_names = Set.new
21
+
22
+ def self.audited_classes
23
+ self.audited_class_names.map(&:constantize)
24
+ end
25
+
26
+ # All audits made during the block called will be recorded as made
27
+ # by +user+. This method is hopefully threadsafe, making it ideal
28
+ # for background operations that require audit information.
29
+ def self.as_user(user, &block)
30
+ Thread.current[:acts_as_audited_user] = user
31
+
32
+ yield
33
+
34
+ Thread.current[:acts_as_audited_user] = nil
35
+ end
36
+
37
+ # Allows user to be set to either a string or an ActiveRecord object
38
+ def user_as_string=(user) #:nodoc:
39
+ # reset both either way
40
+ self.user_as_model = self.username = nil
41
+ user.is_a?(ActiveRecord::Base) ?
42
+ self.user_as_model = user :
43
+ self.username = user
44
+ end
45
+ alias_method :user_as_model=, :user=
46
+ alias_method :user=, :user_as_string=
47
+
48
+ def user_as_string #:nodoc:
49
+ self.user_as_model || self.username
50
+ end
51
+ alias_method :user_as_model, :user
52
+ alias_method :user, :user_as_string
53
+
54
+ def revision
55
+ clazz = auditable_type.constantize
56
+ returning clazz.find_by_id(auditable_id) || clazz.new do |m|
57
+ Audit.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge({:version => version}))
58
+ end
59
+ end
60
+
61
+ def ancestors
62
+ self.class.find(:all, :order => 'version',
63
+ :conditions => ['auditable_id = ? and auditable_type = ? and version <= ?',
64
+ auditable_id, auditable_type, version])
65
+ end
66
+
67
+ # Returns a hash of the changed attributes with the new values
68
+ def new_attributes
69
+ (audit_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
70
+ attrs[attr] = values.is_a?(Array) ? values.last : values
71
+ attrs
72
+ end
73
+ end
74
+
75
+ # Returns a hash of the changed attributes with the old values
76
+ def old_attributes
77
+ (audit_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
78
+ attrs[attr] = Array(values).first
79
+ attrs
80
+ end
81
+ end
82
+
83
+ def self.reconstruct_attributes(audits)
84
+ attributes = {}
85
+ result = audits.collect do |audit|
86
+ attributes.merge!(audit.new_attributes).merge!(:version => audit.version)
87
+ yield attributes if block_given?
88
+ end
89
+ block_given? ? result : attributes
90
+ end
91
+
92
+ def self.assign_revision_attributes(record, attributes)
93
+ attributes.each do |attr, val|
94
+ if record.respond_to?("#{attr}=")
95
+ record.attributes.has_key?(attr.to_s) ?
96
+ record[attr] = val :
97
+ record.send("#{attr}=", val)
98
+ end
99
+ end
100
+ record
101
+ end
102
+
103
+ private
104
+
105
+ def set_version_number
106
+ max = self.class.maximum(:version,
107
+ :conditions => {
108
+ :auditable_id => auditable_id,
109
+ :auditable_type => auditable_type
110
+ }) || 0
111
+ self.version = max + 1
112
+ end
113
+
114
+ def set_audit_user
115
+ self.user = Thread.current[:acts_as_audited_user] if Thread.current[:acts_as_audited_user]
116
+ nil # prevent stopping callback chains
117
+ end
118
+
119
+ end
@@ -0,0 +1,37 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module ActionController #:nodoc:
3
+ module Audited #:nodoc:
4
+ def audit(*models)
5
+ ActiveSupport::Deprecation.warn("#audit is deprecated. Declare #acts_as_audited in your models.", caller)
6
+
7
+ options = models.extract_options!
8
+
9
+ # Parse the options hash looking for classes
10
+ options.each_key do |key|
11
+ models << [key, options.delete(key)] if key.is_a?(Class)
12
+ end
13
+
14
+ models.each do |(model, model_options)|
15
+ model.send :acts_as_audited, model_options || {}
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ class AuditSweeper < ActionController::Caching::Sweeper #:nodoc:
23
+ observe Audit
24
+ def before_create(audit)
25
+ audit.user ||= current_user
26
+ end
27
+
28
+ def current_user
29
+ controller.send :current_user if controller.respond_to?(:current_user, true)
30
+ end
31
+ end
32
+
33
+ ActionController::Base.class_eval do
34
+ extend CollectiveIdea::ActionController::Audited
35
+ cache_sweeper :audit_sweeper
36
+ end
37
+ Audit.add_observer(AuditSweeper.instance)
@@ -0,0 +1,316 @@
1
+ # Copyright (c) 2006 Brandon Keepers
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module CollectiveIdea #:nodoc:
23
+ module Acts #:nodoc:
24
+ # Specify this act if you want changes to your model to be saved in an
25
+ # audit table. This assumes there is an audits table ready.
26
+ #
27
+ # class User < ActiveRecord::Base
28
+ # acts_as_audited
29
+ # end
30
+ #
31
+ # To store an audit comment set model.audit_comment to your comment before
32
+ # a create, update or destroy operation.
33
+ #
34
+ # See <tt>CollectiveIdea::Acts::Audited::ClassMethods#acts_as_audited</tt>
35
+ # for configuration options
36
+ module Audited #:nodoc:
37
+ CALLBACKS = [:audit_create, :audit_update, :audit_destroy]
38
+
39
+ def self.included(base) # :nodoc:
40
+ base.extend ClassMethods
41
+ end
42
+
43
+ module ClassMethods
44
+ # == Configuration options
45
+ #
46
+ #
47
+ # * +only+ - Only audit the given attributes
48
+ # * +except+ - Excludes fields from being saved in the audit log.
49
+ # By default, acts_as_audited will audit all but these fields:
50
+ #
51
+ # [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at']
52
+ # You can add to those by passing one or an array of fields to skip.
53
+ #
54
+ # class User < ActiveRecord::Base
55
+ # acts_as_audited :except => :password
56
+ # end
57
+ # * +protect+ - set to false to raise an error if your model uses +attr_protected+, by default it is true
58
+ #
59
+ # * +require_comment+ - Ensures that audit_comment is supplied before
60
+ # any create, update or destroy operation.
61
+ #
62
+ # class User < ActiveRecord::Base
63
+ # acts_as_audited :protect => false
64
+ # attr_accessible :name
65
+ # end
66
+ # * +full_model_enabled+ - in YAML, save the current state of the record to the audits table
67
+ # * +full_model_enabled+ - in YAML, save the current state of the record to the audits table
68
+ #
69
+ def acts_as_audited(options = {})
70
+ # don't allow multiple calls
71
+ return if self.included_modules.include?(CollectiveIdea::Acts::Audited::InstanceMethods)
72
+
73
+ class_inheritable_reader :auditing_full_model_enabled
74
+ write_inheritable_attribute :auditing_full_model_enabled, (false || options[:full_model_enabled])
75
+
76
+ options = {:protect => true}.merge(options)
77
+
78
+ class_inheritable_reader :non_audited_columns
79
+ class_inheritable_reader :auditing_enabled
80
+
81
+ if options[:only]
82
+ except = self.column_names - options[:only].flatten.map(&:to_s)
83
+ else
84
+ except = [self.primary_key, inheritance_column, 'lock_version',
85
+ 'created_at', 'updated_at', 'created_on', 'updated_on', 'created_by', 'updated_by']
86
+ except |= Array(options[:except]).collect(&:to_s) if options[:except]
87
+ end
88
+ write_inheritable_attribute :non_audited_columns, ['audits'] + (except || [])
89
+
90
+ if options[:comment_required]
91
+ validates_presence_of :audit_comment
92
+ before_destroy :require_comment
93
+ end
94
+
95
+ attr_accessor :audit_comment
96
+ unless accessible_attributes.nil? || options[:protect]
97
+ attr_accessible :audit_comment
98
+ end
99
+
100
+ has_many :audits, :as => :auditable, :order => "#{Audit.quoted_table_name}.version"
101
+ attr_protected :audit_ids if options[:protect]
102
+ Audit.audited_class_names << self.to_s
103
+
104
+ after_create :audit_create if !options[:on] || (options[:on] && options[:on].include?(:create))
105
+ before_update :audit_update if !options[:on] || (options[:on] && options[:on].include?(:update))
106
+ after_destroy :audit_destroy if !options[:on] || (options[:on] && options[:on].include?(:destroy))
107
+
108
+ attr_accessor :version
109
+
110
+ extend CollectiveIdea::Acts::Audited::SingletonMethods
111
+ include CollectiveIdea::Acts::Audited::InstanceMethods
112
+
113
+ write_inheritable_attribute :auditing_enabled, true
114
+ end
115
+ end
116
+
117
+ module InstanceMethods
118
+
119
+ # Temporarily turns off auditing while saving.
120
+ def save_without_auditing
121
+ without_auditing { save }
122
+ end
123
+
124
+ # Executes the block with the auditing callbacks disabled.
125
+ #
126
+ # @foo.without_auditing do
127
+ # @foo.save
128
+ # end
129
+ #
130
+ def without_auditing(&block)
131
+ self.class.without_auditing(&block)
132
+ end
133
+
134
+ # Gets an array of the revisions available
135
+ #
136
+ # user.revisions.each do |revision|
137
+ # user.name
138
+ # user.version
139
+ # end
140
+ #
141
+ def revisions(from_version = 1)
142
+ audits = self.audits.find(:all, :conditions => ['version >= ?', from_version])
143
+ return [] if audits.empty?
144
+ revision = self.audits.find_by_version(from_version).revision
145
+ Audit.reconstruct_attributes(audits) {|attrs| revision.revision_with(attrs) }
146
+ end
147
+
148
+ # Get a specific revision specified by the version number, or +:previous+
149
+ def revision(version)
150
+ revision_with Audit.reconstruct_attributes(audits_to(version))
151
+ end
152
+
153
+ def revision_at(date_or_time)
154
+ audits = self.audits.find(:all, :conditions => ["created_at <= ?", date_or_time])
155
+ revision_with Audit.reconstruct_attributes(audits) unless audits.empty?
156
+ end
157
+
158
+ def audited_attributes
159
+ attributes.except(*non_audited_columns)
160
+ end
161
+
162
+ protected
163
+
164
+ def revision_with(attributes)
165
+ returning self.dup do |revision|
166
+ revision.send :instance_variable_set, '@attributes', self.attributes_before_type_cast
167
+ Audit.assign_revision_attributes(revision, attributes)
168
+
169
+ # Remove any association proxies so that they will be recreated
170
+ # and reference the correct object for this revision. The only way
171
+ # to determine if an instance variable is a proxy object is to
172
+ # see if it responds to certain methods, as it forwards almost
173
+ # everything to its target.
174
+ for ivar in revision.instance_variables
175
+ proxy = revision.instance_variable_get ivar
176
+ if !proxy.nil? and proxy.respond_to? :proxy_respond_to?
177
+ revision.instance_variable_set ivar, nil
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ private
184
+
185
+ def audited_changes
186
+ changed_attributes.except(*non_audited_columns).inject({}) do |changes,(attr, old_value)|
187
+ changes[attr] = [old_value, self[attr]]
188
+ changes
189
+ end
190
+ end
191
+
192
+ def audits_to(version = nil)
193
+ if version == :previous
194
+ version = if self.version
195
+ self.version - 1
196
+ else
197
+ previous = audits.find(:first, :offset => 1,
198
+ :order => "#{Audit.quoted_table_name}.version DESC")
199
+ previous ? previous.version : 1
200
+ end
201
+ end
202
+ audits.find(:all, :conditions => ['version <= ?', version])
203
+ end
204
+
205
+ def audit_create
206
+ write_audit(:action => 'create', :audit_changes => audited_attributes,
207
+ :comment => audit_comment)
208
+ end
209
+
210
+ def audit_update
211
+ unless (changes = audited_changes).empty?
212
+ write_audit(:action => 'update', :audit_changes => changes,
213
+ :comment => audit_comment)
214
+ end
215
+ end
216
+
217
+ def audit_destroy
218
+ write_audit(:action => 'destroy', :audit_changes => audited_attributes,
219
+ :comment => audit_comment)
220
+ end
221
+
222
+ # Note- with to_yaml it's easy to get into some sort of recursion issues. If you see something like:
223
+ # yaml TypeError: wrong argument type nil (expected Data)
224
+ # check to see what other model objects are being pulled in; you may want to limit them. For example, the RequestProgram was pulling in
225
+ # request as an attribute. This caused issues. I solved this by adding not pulling in request_programs into YAML via the to_yaml_properties_with_specific method.
226
+ # In general, you want to stay away from linking objects like RequestProgram and instead use one that list programs.
227
+ def write_audit(attrs)
228
+ self.audit_comment = nil
229
+
230
+ if auditing_full_model_enabled
231
+ # Grab all the object attributes and dump them into a Map, then turn the map into YAML and store in the Audit table
232
+ self.class.reflect_on_all_associations.each {|assn| self.send assn.name.to_sym} # Load up all associations to store in the full_model serialization
233
+ ignore_properties = if non_audited_columns
234
+ non_audited_columns.map {|prop| "@#{prop.to_s}"}
235
+ else
236
+ []
237
+ end
238
+ props = (self.to_yaml_properties.map{|y| y.strip} - ignore_properties)
239
+ attributes_map = props.inject({}) do |acc, name|
240
+ name = name.gsub /@/, ''
241
+
242
+ begin
243
+ if self.respond_to? name.to_sym
244
+ val = self.send name.to_sym
245
+ unless val.blank? || (val.is_a?(Array) && val.empty?)
246
+ acc[name.to_sym] = val
247
+ end
248
+ end
249
+ rescue Exception => exception
250
+ error_msg = "Error serializing property #{name}; got exception #{exception.to_s} with backtrace #{exception.backtrace.inspect}"
251
+ p error_msg
252
+ logger.error error_msg
253
+ end
254
+ acc
255
+ end
256
+ attributes_map[:attributes] = self.attributes.except(*non_audited_columns)
257
+
258
+ attrs[:full_model] = attributes_map.to_yaml
259
+ end
260
+
261
+ self.audits.create attrs if auditing_enabled
262
+ end
263
+
264
+ def require_comment
265
+ if audit_comment.blank?
266
+ errors.add(:audit_comment, "Comment required before destruction")
267
+ return false
268
+ end
269
+ end
270
+
271
+ CALLBACKS.each do |attr_name|
272
+ alias_method "#{attr_name}_callback".to_sym, attr_name
273
+ end
274
+
275
+ def empty_callback #:nodoc:
276
+ end
277
+
278
+ end # InstanceMethods
279
+
280
+ module SingletonMethods
281
+ # Returns an array of columns that are audited. See non_audited_columns
282
+ def audited_columns
283
+ self.columns.select { |c| !non_audited_columns.include?(c.name) }
284
+ end
285
+
286
+ # Executes the block with auditing disabled.
287
+ #
288
+ # Foo.without_auditing do
289
+ # @foo.save
290
+ # end
291
+ #
292
+ def without_auditing(&block)
293
+ auditing_was_enabled = auditing_enabled
294
+ disable_auditing
295
+ returning(block.call) { enable_auditing if auditing_was_enabled }
296
+ end
297
+
298
+ def disable_auditing
299
+ write_inheritable_attribute :auditing_enabled, false
300
+ end
301
+
302
+ def enable_auditing
303
+ write_inheritable_attribute :auditing_enabled, true
304
+ end
305
+
306
+ # All audit operations during the block are recorded as being
307
+ # made by +user+. This is not model specific, the method is a
308
+ # convenience wrapper around #Audit.as_user.
309
+ def audit_as( user, &block )
310
+ Audit.as_user( user, &block )
311
+ end
312
+
313
+ end
314
+ end
315
+ end
316
+ end