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.
- data/.gitignore +4 -0
- data/CHANGELOG +25 -0
- data/LICENSE +19 -0
- data/README +76 -0
- data/Rakefile +60 -0
- data/VERSION +1 -0
- data/acts_as_audited.gemspec +90 -0
- data/acts_as_audited_rails3.gemspec +90 -0
- data/doc/classes/Audit.html +407 -0
- data/doc/classes/CollectiveIdea/Acts/Audited/ClassMethods.html +226 -0
- data/doc/classes/CollectiveIdea/Acts/Audited/InstanceMethods.html +330 -0
- data/doc/classes/CollectiveIdea/Acts/Audited/SingletonMethods.html +254 -0
- data/doc/created.rid +1 -0
- data/doc/files/README.html +226 -0
- data/doc/files/lib/acts_as_audited/audit_rb.html +108 -0
- data/doc/files/lib/acts_as_audited/audit_sweeper_rb.html +101 -0
- data/doc/files/lib/acts_as_audited_rb.html +129 -0
- data/doc/fr_class_index.html +30 -0
- data/doc/fr_file_index.html +30 -0
- data/doc/fr_method_index.html +47 -0
- data/doc/index.html +24 -0
- data/doc/rdoc-style.css +208 -0
- data/lib/acts_as_audited/audit.rb +119 -0
- data/lib/acts_as_audited/audit_sweeper.rb +37 -0
- data/lib/acts_as_audited/base.rb +316 -0
- data/lib/acts_as_audited.rb +9 -0
- data/lib/generators/audited_migration/USAGE +7 -0
- data/lib/generators/audited_migration/audited_migration_generator.rb +24 -0
- data/lib/generators/audited_migration/templates/migration.rb +29 -0
- data/lib/generators/audited_migration_update/USAGE +7 -0
- data/lib/generators/audited_migration_update/audited_migration_update_generator.rb +24 -0
- data/lib/generators/audited_migration_update/templates/migration.rb +9 -0
- data/test/acts_as_audited_test.rb +437 -0
- data/test/audit_sweeper_test.rb +31 -0
- data/test/audit_test.rb +179 -0
- data/test/db/database.yml +21 -0
- data/test/db/schema.rb +33 -0
- data/test/test_helper.rb +75 -0
- 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>
|
data/doc/rdoc-style.css
ADDED
@@ -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
|