sequel_audited 0.2.0

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/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