acts_as_audited_rails3 1.1.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|