sequel_audited 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +587 -0
- data/Rakefile +33 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/env.test.sample +7 -0
- data/lib/sequel/audited.rb +33 -0
- data/lib/sequel/audited/version.rb +11 -0
- data/lib/sequel/plugins/audited.rb +293 -0
- data/lib/tasks/sequel-audited/migrate.rake +18 -0
- data/lib/tasks/sequel-audited/templates/audited_migration.rb +23 -0
- data/sequel_audited.gemspec +49 -0
- metadata +261 -0
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
#
|
4
|
+
#
|
5
|
+
OSX = RUBY_PLATFORM.match(/darwin/)
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
Rake::TestTask.new(:spec) do |t|
|
10
|
+
t.libs << 'spec'
|
11
|
+
t.libs << 'lib'
|
12
|
+
t.test_files = FileList['spec/**/*_spec.rb']
|
13
|
+
end
|
14
|
+
|
15
|
+
task :default => :spec
|
16
|
+
|
17
|
+
desc 'Run specs with coverage'
|
18
|
+
task :coverage do
|
19
|
+
ENV['COVERAGE'] = '1'
|
20
|
+
Rake::Task['spec'].invoke
|
21
|
+
`open coverage/index.html` if OSX
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Run Rubocop report'
|
25
|
+
task :rubocop do
|
26
|
+
res = `which rubocop`
|
27
|
+
if res != ""
|
28
|
+
`rubocop -f html -o ./rubocop/report.html lib/`
|
29
|
+
`open rubocop/report.html` if OSX
|
30
|
+
else
|
31
|
+
puts "\nERROR: 'rubocop' gem is not installed or available. Please run 'gem install rubocop'."
|
32
|
+
end
|
33
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "sequel/audited"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/env.test.sample
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'sequel/audited/version'
|
3
|
+
|
4
|
+
module Sequel
|
5
|
+
|
6
|
+
#
|
7
|
+
module Audited
|
8
|
+
|
9
|
+
CREATE = 'create'
|
10
|
+
UPDATE = 'update'
|
11
|
+
DESTROY = 'destroy'
|
12
|
+
|
13
|
+
# set the name of the global method that provides the current user. Default: :current_user
|
14
|
+
@audited_current_user_method = :current_user
|
15
|
+
# enable swapping of the Audit model
|
16
|
+
@audited_model_name = :AuditLog
|
17
|
+
# toggle for enabling / disabling auditing
|
18
|
+
@audited_enabled = true
|
19
|
+
|
20
|
+
# by default ignore these columns
|
21
|
+
@audited_default_ignored_columns = [
|
22
|
+
# :id, :ref, :password, :password_hash,
|
23
|
+
:lock_version,
|
24
|
+
:created_at, :updated_at, :created_on, :updated_on
|
25
|
+
]
|
26
|
+
|
27
|
+
class << self
|
28
|
+
attr_accessor :audited_current_user_method, :audited_model_name,
|
29
|
+
:audited_enabled, :audited_default_ignored_columns
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
class AuditLog < Sequel::Model
|
2
|
+
# handle versioning of audited records
|
3
|
+
plugin :list, field: :version, scope: [:associated_type, :associated_id]
|
4
|
+
plugin :timestamps
|
5
|
+
plugin :serialization, :json, :changed
|
6
|
+
plugin :polymorphic
|
7
|
+
|
8
|
+
# TODO: see if we should add these
|
9
|
+
many_to_one :associated, polymorphic: true
|
10
|
+
many_to_one :modifier, polymorphic: true
|
11
|
+
|
12
|
+
def before_validation
|
13
|
+
# grab the current user
|
14
|
+
if u = audit_user
|
15
|
+
self.modifier = u
|
16
|
+
end
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
# private
|
22
|
+
|
23
|
+
# Obtains the `current_user` based upon the `:audited_current_user_method' value set in the
|
24
|
+
# audited model, either via defaults or via :user_method config options
|
25
|
+
#
|
26
|
+
# # NOTE! this allows overriding the default value on a per audited model
|
27
|
+
def audit_user
|
28
|
+
m = Kernel.const_get(associated_type)
|
29
|
+
m.send(m.audited_current_user_method) || send(m.audited_current_user_method)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
module Sequel
|
35
|
+
module Plugins
|
36
|
+
|
37
|
+
# Given a Post model with these fields:
|
38
|
+
# [:id, :category_id, :title, :body, :author_id, :created_at, :updated_at]
|
39
|
+
#
|
40
|
+
#
|
41
|
+
# All fields
|
42
|
+
# plugin :audited
|
43
|
+
# #=> [:category_id, :title, :body, :author_id] # NB! excluding @default_ignore_attrs
|
44
|
+
# #=> [:id, :created_at, :updated_at]
|
45
|
+
#
|
46
|
+
# Single field
|
47
|
+
# plugin :audited, only: :title
|
48
|
+
# plugin :audited, only: [:title]
|
49
|
+
# #=> [:title]
|
50
|
+
# #+> [:id, :category_id, :body, :author_id, :created_at, :updated_at] # ignored fields
|
51
|
+
#
|
52
|
+
# Multiple fields
|
53
|
+
# plugin :audited, only: [:title, :body]
|
54
|
+
# #=> [:title, :body] # tracked fields
|
55
|
+
# #=> [:id, :category_id, :author_id, :created_at, :updated_at] # ignored fields
|
56
|
+
#
|
57
|
+
#
|
58
|
+
# All fields except certain fields
|
59
|
+
# plugin :audited, except: :title
|
60
|
+
# plugin :audited, except: [:title]
|
61
|
+
# #=> [:id, :category_id, :author_id, :created_at, :updated_at] # tracked fields
|
62
|
+
# #=> [:title] # ignored fields
|
63
|
+
#
|
64
|
+
#
|
65
|
+
#
|
66
|
+
module Audited
|
67
|
+
|
68
|
+
# called when
|
69
|
+
def self.configure(model, opts = {})
|
70
|
+
model.instance_eval do
|
71
|
+
# add support for :dirty attributes tracking & JSON serializing of data
|
72
|
+
plugin(:dirty)
|
73
|
+
plugin(:json_serializer)
|
74
|
+
plugin(:polymorphic)
|
75
|
+
|
76
|
+
# set the default ignored columns or revert to defaults
|
77
|
+
set_default_ignored_columns(opts)
|
78
|
+
# sets the name of the current User method or revert to default: :current_user
|
79
|
+
# specifically for the audited model on a per model basis
|
80
|
+
set_user_method(opts)
|
81
|
+
|
82
|
+
set_reference_method(opts)
|
83
|
+
|
84
|
+
only = opts.fetch(:only, [])
|
85
|
+
except = opts.fetch(:except, [])
|
86
|
+
|
87
|
+
unless only.empty?
|
88
|
+
# we should only track the provided column
|
89
|
+
included_columns = [only].flatten
|
90
|
+
# subtract the 'only' columns from all columns to get excluded_columns
|
91
|
+
excluded_columns = columns - included_columns
|
92
|
+
else # except:
|
93
|
+
# all columns minus any excepted columns and default ignored columns
|
94
|
+
included_columns = [
|
95
|
+
[columns - [except].flatten].flatten - @audited_default_ignored_columns
|
96
|
+
].flatten.uniq
|
97
|
+
|
98
|
+
# except_columns = except.empty? ? [] : [except].flatten
|
99
|
+
excluded_columns = [columns - included_columns].flatten.uniq
|
100
|
+
# excluded_columns = [columns - [except_columns, included_columns].flatten].flatten.uniq
|
101
|
+
end
|
102
|
+
|
103
|
+
@audited_included_columns = included_columns
|
104
|
+
@audited_ignored_columns = excluded_columns
|
105
|
+
|
106
|
+
# each included model will have an associated versions
|
107
|
+
one_to_many(
|
108
|
+
:versions,
|
109
|
+
class: audit_model_name,
|
110
|
+
as: 'associated'
|
111
|
+
)
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
#
|
119
|
+
module ClassMethods
|
120
|
+
|
121
|
+
attr_accessor :audited_default_ignored_columns, :audited_current_user_method
|
122
|
+
# The holder of ignored columns
|
123
|
+
attr_reader :audited_ignored_columns
|
124
|
+
# The holder of columns that should be audited
|
125
|
+
attr_reader :audited_included_columns
|
126
|
+
|
127
|
+
attr_accessor :audited_reference_method
|
128
|
+
|
129
|
+
|
130
|
+
Plugins.inherited_instance_variables(self,
|
131
|
+
:@audited_default_ignored_columns => nil,
|
132
|
+
:@audited_current_user_method => nil,
|
133
|
+
:@audited_included_columns => nil,
|
134
|
+
:@audited_ignored_columns => nil,
|
135
|
+
:@audited_reference_method => nil
|
136
|
+
)
|
137
|
+
|
138
|
+
def non_audited_columns
|
139
|
+
columns - audited_columns
|
140
|
+
end
|
141
|
+
|
142
|
+
def audited_columns
|
143
|
+
@audited_columns ||= columns - @audited_ignored_columns
|
144
|
+
end
|
145
|
+
|
146
|
+
# def default_ignored_attrs
|
147
|
+
# # TODO: how to reference the models primary_key value??
|
148
|
+
# arr = [pk.to_s]
|
149
|
+
# # handle STI (Class Table Inheritance) models with `plugin :single_table_inheritance`
|
150
|
+
# arr << 'sti_key' if self.respond_to?(:sti_key)
|
151
|
+
# arr
|
152
|
+
# end
|
153
|
+
|
154
|
+
#
|
155
|
+
# returns true / false if any audits have been made
|
156
|
+
#
|
157
|
+
# Post.audited_versions? #=> true / false
|
158
|
+
#
|
159
|
+
def audited_versions?
|
160
|
+
audit_model.where(associated_type: name.to_s).count >= 1
|
161
|
+
end
|
162
|
+
|
163
|
+
# grab all audits for a particular model based upon filters
|
164
|
+
#
|
165
|
+
# Posts.audited_versions(:model_pk => 123)
|
166
|
+
# #=> filtered by primary_key value
|
167
|
+
#
|
168
|
+
# Posts.audited_versions(:user_id => 88)
|
169
|
+
# #=> filtered by user name
|
170
|
+
#
|
171
|
+
# Posts.audited_versions(:created_at < Date.today - 2)
|
172
|
+
# #=> filtered to last two (2) days only
|
173
|
+
#
|
174
|
+
# Posts.audited_versions(:created_at > Date.today - 7)
|
175
|
+
# #=> filtered to older than last seven (7) days
|
176
|
+
#
|
177
|
+
def audited_versions(opts = {})
|
178
|
+
audit_model.where(opts.merge(associated_type: name.to_s)).order(:version).all
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
|
185
|
+
def audit_model
|
186
|
+
const_get(audit_model_name)
|
187
|
+
end
|
188
|
+
|
189
|
+
def audit_model_name
|
190
|
+
::Sequel::Audited.audited_model_name
|
191
|
+
end
|
192
|
+
|
193
|
+
def set_default_ignored_columns(opts)
|
194
|
+
if opts[:default_ignored_columns]
|
195
|
+
@audited_default_ignored_columns = opts[:default_ignored_columns]
|
196
|
+
else
|
197
|
+
@audited_default_ignored_columns = ::Sequel::Audited.audited_default_ignored_columns
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def set_user_method(opts)
|
202
|
+
if opts[:user_method]
|
203
|
+
@audited_current_user_method = opts[:user_method]
|
204
|
+
else
|
205
|
+
@audited_current_user_method = ::Sequel::Audited.audited_current_user_method
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def set_reference_method(opts)
|
210
|
+
if opts[:reference_method]
|
211
|
+
@audited_reference_method = opts[:reference_method]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
#
|
219
|
+
module InstanceMethods
|
220
|
+
|
221
|
+
# Returns who put the post into its current state.
|
222
|
+
#
|
223
|
+
# post.blame # => 'joeblogs'
|
224
|
+
#
|
225
|
+
# post.last_audited_by # => 'joeblogs'
|
226
|
+
#
|
227
|
+
# Note! returns 'not audited' if there's no audited version (new unsaved record)
|
228
|
+
#
|
229
|
+
def blame
|
230
|
+
v = versions.last unless versions.empty?
|
231
|
+
v ? v.modifier : 'not audited'
|
232
|
+
end
|
233
|
+
alias_method :last_audited_by, :blame
|
234
|
+
|
235
|
+
# Returns who put the post into its current state.
|
236
|
+
#
|
237
|
+
# post.last_audited_at # => '2015-12-19 @ 08:24:45'
|
238
|
+
#
|
239
|
+
# post.last_audited_on # => 'joeblogs'
|
240
|
+
#
|
241
|
+
# Note! returns 'not audited' if there's no audited version (new unsaved record)
|
242
|
+
#
|
243
|
+
def last_audited_at
|
244
|
+
v = versions.last unless versions.empty?
|
245
|
+
v ? v.created_at : 'not audited'
|
246
|
+
end
|
247
|
+
alias_method :last_audited_on, :last_audited_at
|
248
|
+
|
249
|
+
private
|
250
|
+
|
251
|
+
# extract audited values only
|
252
|
+
def audited_values(event)
|
253
|
+
vals = case event
|
254
|
+
when Sequel::Audited::CREATE
|
255
|
+
self.values
|
256
|
+
when Sequel::Audited::UPDATE
|
257
|
+
(column_changes.empty? ? previous_changes : column_changes)
|
258
|
+
when Sequel::Audited::DESTROY
|
259
|
+
self.values
|
260
|
+
end
|
261
|
+
vals.except(*model.audited_default_ignored_columns)
|
262
|
+
end
|
263
|
+
|
264
|
+
def add_audited(event)
|
265
|
+
changed = audited_values(event)
|
266
|
+
unless changed.blank?
|
267
|
+
add_version(
|
268
|
+
event: event,
|
269
|
+
changed: changed
|
270
|
+
)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
### CALLBACKS ###
|
275
|
+
|
276
|
+
def after_create
|
277
|
+
super
|
278
|
+
add_audited(Sequel::Audited::CREATE)
|
279
|
+
end
|
280
|
+
|
281
|
+
def after_update
|
282
|
+
super
|
283
|
+
add_audited(Sequel::Audited::UPDATE)
|
284
|
+
end
|
285
|
+
|
286
|
+
def after_destroy
|
287
|
+
super
|
288
|
+
add_audited(Sequel::Audited::DESTROY)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
namespace :audited do
|
2
|
+
namespace :migrate do
|
3
|
+
desc 'Installs Sequel::Audited migration, but does not run it'
|
4
|
+
task :install do
|
5
|
+
num = Dir["#{Dir.pwd}/db/migrate/*.rb"].sort.last[0, 3] ||= '001'
|
6
|
+
|
7
|
+
FileUtils.cp(
|
8
|
+
"#{File.dirname(__FILE__)}/templates/audited_migration.rb",
|
9
|
+
"#{Dir.pwd}/db/migrate/#{num}_create_audited_table.rb"
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Updates existing Sequel::Audited migration files with amendments'
|
14
|
+
task :update do
|
15
|
+
puts 'TODO: no updates required yet'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
# created by sequel-audited gem
|
3
|
+
|
4
|
+
up do
|
5
|
+
create_table(:audit_logs) do
|
6
|
+
primary_key :id
|
7
|
+
String :model_type
|
8
|
+
Integer :model_pk
|
9
|
+
String :model_ref
|
10
|
+
String :event
|
11
|
+
String :changed, text: true
|
12
|
+
Integer :version, default: 0
|
13
|
+
Integer :user_id
|
14
|
+
String :username
|
15
|
+
String :user_type, default: 'User'
|
16
|
+
DateTime :created_at
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
down do
|
21
|
+
drop_table :audit_logs
|
22
|
+
end
|
23
|
+
end
|