sequel_audited 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/env.test.sample ADDED
@@ -0,0 +1,7 @@
1
+ # NOTE: rename this file as '.env.test' and change YOURNAME or the full DB path(s) below
2
+
3
+ # Using PostgreSQL
4
+ DATABASE_URL="postgres://YOURNAME@localhost/sequel-audited-test"
5
+
6
+ # Using SQLite
7
+ # DATABASE_URL="sqlite://spec/sequel-audited-test.db"
@@ -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,11 @@
1
+
2
+ #
3
+ module Sequel
4
+
5
+ #
6
+ module Audited
7
+
8
+ VERSION = '0.2.0'
9
+
10
+ end
11
+ 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