auditable 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +152 -0
- data/Rakefile +13 -0
- data/auditable.gemspec +28 -0
- data/generators/auditable/migration_generator.rb +27 -0
- data/generators/auditable/templates/migration.rb +12 -0
- data/lib/auditable.rb +8 -0
- data/lib/auditable/audit.rb +66 -0
- data/lib/auditable/auditing.rb +71 -0
- data/lib/auditable/version.rb +3 -0
- data/spec/lib/auditable_spec.rb +119 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/models.rb +24 -0
- data/spec/support/schema.rb +10 -0
- data/specs.watchr +61 -0
- metadata +164 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm --create use 1.9.3@auditable
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Harley Trung
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# Auditable
|
2
|
+
|
3
|
+
There are a lot of gems under https://www.ruby-toolbox.com/categories/Active_Record_Versioning and https://www.ruby-toolbox.com/categories/Active_Record_User_Stamping but there are various issues with them (as of this writing):
|
4
|
+
|
5
|
+
* Almost all of them are outdated and not working with Rails 3.2.2. Some of the popular ones such as `papertrail` and `vestal_versions` have many issues and pull requests but haven't been addressed or merged. Based on the large number of forks of these popular gems, people seem to be OK with their own gem tweaks. Some tweaks are good, but some are too hackish (to deal with recent Rails changes) based on the commits that I have read. I have tried some of the more popular ones but they don't work reliably for something simple I'm trying to achieve.
|
6
|
+
* Many of these gems have evolved overtime and become rather (very) cumbersome.
|
7
|
+
* Most or all of them don't seem to support beyond the database columns, i.e. not working (or working well) with virtual methods or associations. `papertrail` supports `has_one` but the author said it's not easy to go further than that.
|
8
|
+
* I need something simple and lightweight.
|
9
|
+
|
10
|
+
A lot of the gems in the above category are great. I'm just aiming to create a dead simple gem that lets you easily diff the changes on a model's attributes or methods. Yes, methods, not just attributes. Here's the key difference in my approach:
|
11
|
+
|
12
|
+
If you want to track changes to complicated stuff such as associated records, just define a method that returns some representation of the associated records and let `auditable` keeps track of the changes in those values over time.
|
13
|
+
|
14
|
+
Basically:
|
15
|
+
|
16
|
+
* I don't want the default to track all my columns. Only the attributes or methods I specify please.
|
17
|
+
* I don't want to deal with association mess. Use methods instead.
|
18
|
+
* I care about tracking the values of certain virtual attributes or methods, not just database columns
|
19
|
+
* I want something simple, similar to [ActiveRecord::Dirty#changes](http://ar.rubyonrails.org/classes/ActiveRecord/Dirty.html#M000291) but persistent across saves. See `Auditable::Auditing#audited_changes` below.
|
20
|
+
|
21
|
+
See examples under Usage section.
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
Add this line to your application's Gemfile:
|
26
|
+
|
27
|
+
gem 'auditable'
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
$ gem install auditable
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
First, add the necessary `audits` table to your database with:
|
40
|
+
|
41
|
+
rails generate auditable:migration
|
42
|
+
rake db:migrate
|
43
|
+
|
44
|
+
Then, provide a list of methods you'd like to audit to the `audit` method in your model.
|
45
|
+
|
46
|
+
class Survey
|
47
|
+
has_many :questions
|
48
|
+
attr_accessor :current_page
|
49
|
+
|
50
|
+
audit :title, :current_page, :question_ids
|
51
|
+
end
|
52
|
+
|
53
|
+
## Demo
|
54
|
+
|
55
|
+
I'm going to demo with the test models from the test suite. You probably want to use 'rails console' and test with the model that you want to audit.
|
56
|
+
|
57
|
+
For more details, I suggest you check out the test examples in the `spec` folder itself.
|
58
|
+
|
59
|
+
$ bundle console
|
60
|
+
>> require(File.expand_path "../spec/spec_helper", __FILE__)
|
61
|
+
=> true
|
62
|
+
|
63
|
+
>> s = Survey.create :title => "demo"
|
64
|
+
=> #<Survey id: 1, title: "demo">
|
65
|
+
|
66
|
+
>> Survey.audited_attributes
|
67
|
+
=> [:title, :current_page]
|
68
|
+
|
69
|
+
>> s.audited_changes
|
70
|
+
=> {"title"=>[nil, "demo"]}
|
71
|
+
|
72
|
+
>> s.update_attributes(:title => "new title", :current_page => 2)
|
73
|
+
=> true
|
74
|
+
|
75
|
+
>> s.audited_changes
|
76
|
+
=> {"title"=>["demo", "new title"], "current_page"=>[nil, 2]}
|
77
|
+
|
78
|
+
>> s.update_attributes(:current_page => 3, :action => "modified", :changed_by => User.create(:name => "someone"))
|
79
|
+
=> true
|
80
|
+
|
81
|
+
>> s.audited_changes
|
82
|
+
=> {"current_page"=>[2, 3]}
|
83
|
+
|
84
|
+
>> s.audits.last
|
85
|
+
=> #<Auditable::Audit id: 3, auditable_id: 1, auditable_type: "Survey", user_id: 1, user_type: "User", modifications: {"title"=>"new title", "current_page"=>3}, action: "modified", created_at: ...>
|
86
|
+
|
87
|
+
>> s.tag_with(:tag => "something memorable")
|
88
|
+
# we just tagged the latest audit, now then do make changes with s
|
89
|
+
# ...
|
90
|
+
# assuming you've made some changes to s
|
91
|
+
|
92
|
+
>> s.audited_changes(:tag => "something memorable")
|
93
|
+
# return the changes against the tagged version above
|
94
|
+
# note s.audited_changes still diff against the second latest audit
|
95
|
+
# you can also pass in other filters, such as s.audited_changes(:changed_by => some_user, :audit_action => "modified")
|
96
|
+
# note that it always uses the latest audit to diff against an earlier audit matching the arguments to audited_changes
|
97
|
+
|
98
|
+
## How it works
|
99
|
+
### Audit Model
|
100
|
+
|
101
|
+
As seen above, I intend to have a migration file like this for the Audit model:
|
102
|
+
|
103
|
+
class CreateAudits < ActiveRecord::Migration
|
104
|
+
def change
|
105
|
+
create_table :audits do |t|
|
106
|
+
t.belongs_to :auditable, :polymorphic => true
|
107
|
+
t.belongs_to :user, :polymorphic => true
|
108
|
+
t.text :modifications
|
109
|
+
t.string :action
|
110
|
+
t.timestamps
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
It guessable from the above that `audits.modifications` will just be a serialized representation of keys and values of the audited attributes.
|
116
|
+
|
117
|
+
### Who changed it and what was that action?
|
118
|
+
|
119
|
+
If you want to store the user who made the changed to the record, just assigned it to the record's `changed_by` attribute, like so:
|
120
|
+
|
121
|
+
# note you have to define `attr_accessor :changed_by` yourself
|
122
|
+
>> @survey.update_attributes(:changed_by => current_user, # and other attributes you want to set)
|
123
|
+
# then @surveys.audits.last.user will be set to current_user
|
124
|
+
# also works when you set changed_by and call save later, of course
|
125
|
+
|
126
|
+
`action` will just be `create` or `update` depending on the operation on the record, but you can also override it with another virtual attribute, call it `change_action`
|
127
|
+
|
128
|
+
>> @survey.changed_action = "add page"
|
129
|
+
>> @survey.update_attribute :page_count, 2
|
130
|
+
|
131
|
+
**Don't store a new row in `audits` table if the `modifications` column is the same as the one immediately before it. This makes it easier to review change**
|
132
|
+
|
133
|
+
That's all I can do for this README Driven approach. Back soon.
|
134
|
+
|
135
|
+
## TODO
|
136
|
+
|
137
|
+
* improve api (still clumsy) -- come up with better syntax
|
138
|
+
* get some suggestions and feedback
|
139
|
+
* update README
|
140
|
+
|
141
|
+
e.g. right now, changes are serialized into `audits.modifications` column, but what if we what to do multiple sets of audits at each save. I'm thinking of supporting syntax like this:
|
142
|
+
|
143
|
+
# store snapshots of certain methods to audits.trivial_changes column (that you can easily add yourself)
|
144
|
+
audit :modifications => [:method_1, :method_2], :trivial_changes => [:method_3, :method_4, :method_5]
|
145
|
+
|
146
|
+
## Contributing
|
147
|
+
|
148
|
+
1. Fork it
|
149
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
150
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
151
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
152
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:rspec) do |spec|
|
6
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
7
|
+
spec.rspec_opts = ['-cfs --backtrace']
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Default: run specs"
|
11
|
+
task :default => :rspec
|
12
|
+
|
13
|
+
|
data/auditable.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/auditable/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Harley Trung"]
|
6
|
+
gem.email = ["harley@socialsci.com"]
|
7
|
+
gem.description = %q{A simple gem that audit models' attributes or methods by taking snapshots and diff them for you. Starting from scratch to work with Rails 3.2.2 onwards}
|
8
|
+
gem.summary = %q{A simple gem to audit attributes and methods in ActiveRecord models.}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "auditable"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Auditable::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency 'rake'
|
19
|
+
gem.add_development_dependency 'rspec'
|
20
|
+
gem.add_development_dependency 'watchr'
|
21
|
+
gem.add_development_dependency 'sqlite3'
|
22
|
+
# documetation stuff
|
23
|
+
gem.add_development_dependency 'yard'
|
24
|
+
gem.add_development_dependency 'rdiscount'
|
25
|
+
|
26
|
+
gem.add_runtime_dependency 'activesupport'
|
27
|
+
gem.add_runtime_dependency 'activerecord'
|
28
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module Auditable
|
5
|
+
module Generators
|
6
|
+
class MigrationGenerator < ::Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
def self.source_root
|
10
|
+
@source_root ||= File.join(File.dirname(__FILE__), 'templates')
|
11
|
+
end
|
12
|
+
|
13
|
+
# Rails expects us to override/implement this ourself
|
14
|
+
def self.next_migration_number(dirname)
|
15
|
+
if ActiveRecord::Base.timestamped_migrations
|
16
|
+
Time.new.utc.strftime("%Y%m%d%H%M%S")
|
17
|
+
else
|
18
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate_files
|
23
|
+
migration_template 'migration.rb', 'db/migrate/create_audits.rb'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateAudits < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :audits do |t|
|
4
|
+
t.belongs_to :auditable, :polymorphic => true
|
5
|
+
t.belongs_to :user, :polymorphic => true
|
6
|
+
t.text :modifications
|
7
|
+
t.string :action
|
8
|
+
t.string :tag
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/auditable.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module Auditable
|
2
|
+
class Audit < ActiveRecord::Base
|
3
|
+
belongs_to :auditable, :polymorphic => true
|
4
|
+
belongs_to :user, :polymorphic => true
|
5
|
+
serialize :modifications
|
6
|
+
|
7
|
+
# Diffing two audits' modifications
|
8
|
+
#
|
9
|
+
# Returns a hash containing arrays of the form
|
10
|
+
# {
|
11
|
+
# :key_1 => [<value_in_other_audit>, <value_in_this_audit>],
|
12
|
+
# :key_2 => [<value_in_other_audit>, <value_in_this_audit>],
|
13
|
+
# :other_audit_own_key => [<value_in_other_audit>, nil],
|
14
|
+
# :this_audio_own_key => [nil, <value_in_this_audit>]
|
15
|
+
# }
|
16
|
+
def diff(other_audit)
|
17
|
+
other_modifications = other_audit ? other_audit.modifications : {}
|
18
|
+
|
19
|
+
{}.tap do |d|
|
20
|
+
# find keys present only in this audit
|
21
|
+
(self.modifications.keys - other_modifications.keys).each do |k|
|
22
|
+
d[k] = [nil, self.modifications[k]] if self.modifications[k]
|
23
|
+
end
|
24
|
+
|
25
|
+
# find keys present only in other audit
|
26
|
+
(other_modifications.keys - self.modifications.keys).each do |k|
|
27
|
+
d[k] = [other_modifications[k], nil] if other_modifications[k]
|
28
|
+
end
|
29
|
+
|
30
|
+
# find common keys and diff values
|
31
|
+
self.modifications.keys.each do |k|
|
32
|
+
if self.modifications[k] != other_modifications[k]
|
33
|
+
d[k] = [other_modifications[k], self.modifications[k]]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Diff this audit with the one created immediately before it
|
40
|
+
#
|
41
|
+
# See #diff for more details
|
42
|
+
def latest_diff(options = {})
|
43
|
+
if options.present?
|
44
|
+
scoped = auditable.audits.order("created_at DESC")
|
45
|
+
if tag = options.delete(:tag)
|
46
|
+
scoped = scoped.where(:tag => tag)
|
47
|
+
end
|
48
|
+
if changed_by = options.delete(:changed_by)
|
49
|
+
scoped = scoped.where(:user_id => changed_by.id, :user_type => changed_by.class.name)
|
50
|
+
end
|
51
|
+
if audit_tag = options.delete(:audit_tag)
|
52
|
+
scoped = scoped.where(:tag => audit_tag)
|
53
|
+
end
|
54
|
+
diff scoped.first
|
55
|
+
else
|
56
|
+
diff_since(created_at)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Diff this audit with the latest audit created before the `time` variable passed
|
61
|
+
def diff_since(time)
|
62
|
+
other_audit = self.class.where("created_at < ?", time).order("created_at DESC").limit(1).first
|
63
|
+
diff(other_audit)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
#require 'active_support/concern'
|
2
|
+
|
3
|
+
module Auditable
|
4
|
+
module Auditing
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
attr_writer :audited_attributes
|
9
|
+
|
10
|
+
# Get the list of methods to track over record saves, including those inherited from parent
|
11
|
+
def audited_attributes
|
12
|
+
attrs = @audited_attributes || []
|
13
|
+
# handle STI case: include parent's audited_attributes
|
14
|
+
if superclass != ActiveRecord::Base and superclass.respond_to?(:audited_attributes)
|
15
|
+
attrs.push(*superclass.audited_attributes)
|
16
|
+
end
|
17
|
+
attrs
|
18
|
+
end
|
19
|
+
|
20
|
+
# Set the list of methods to track over record saves
|
21
|
+
#
|
22
|
+
# Example:
|
23
|
+
#
|
24
|
+
# class Survey < ActiveRecord::Base
|
25
|
+
# audit :page_count, :question_ids
|
26
|
+
# end
|
27
|
+
def audit(*options)
|
28
|
+
has_many :audits, :class_name => "Auditable::Audit", :as => :auditable
|
29
|
+
after_create {|record| record.snap!("create")}
|
30
|
+
after_update {|record| record.snap!("update")}
|
31
|
+
|
32
|
+
self.audited_attributes = Array.wrap options
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# INSTANCE METHODS
|
37
|
+
|
38
|
+
# Get the latest audit record
|
39
|
+
def last_audit
|
40
|
+
audits.last
|
41
|
+
end
|
42
|
+
|
43
|
+
# Mark the latest record in order to easily find and perform diff against later
|
44
|
+
def tag_with(tag)
|
45
|
+
last_audit.update_attribute(:tag, tag)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Take a snapshot of and save the current state of the audited record's audited attributes
|
49
|
+
def snap!(action_default = "update")
|
50
|
+
snap = {}.tap do |s|
|
51
|
+
self.class.audited_attributes.each do |attr|
|
52
|
+
s[attr.to_s] = self.send attr
|
53
|
+
end
|
54
|
+
end
|
55
|
+
audits.create! do |audit|
|
56
|
+
audit.modifications = snap
|
57
|
+
audit.tag = self.audit_tag if self.respond_to?(:audit_tag)
|
58
|
+
audit.action = (self.respond_to?(:audit_action) && self.audit_action) || action_default
|
59
|
+
audit.user = self.changed_by if self.respond_to?(:changed_by)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get the latest changes by comparing the latest two audits
|
64
|
+
def audited_changes(options = {})
|
65
|
+
audits.last.latest_diff(options)
|
66
|
+
end
|
67
|
+
|
68
|
+
#def self.included(base)
|
69
|
+
#end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Model.audited_attributes" do
|
4
|
+
it "should be available to models using audit" do
|
5
|
+
Survey.audited_attributes.should include :title
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Auditable do
|
10
|
+
let(:survey) { Survey.create :title => "test survey" }
|
11
|
+
let(:user) { User.create(:name => "test user") }
|
12
|
+
let(:another_user) { User.create(:name => "another user") }
|
13
|
+
|
14
|
+
it "should have a valid audit to start with" do
|
15
|
+
survey.title.should == "test survey"
|
16
|
+
survey.audited_changes.should == {"title" => [nil, "test survey"]}
|
17
|
+
survey.audits.last.action.should == "create"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should behave similar to ActiveRecord::Dirty#changes" do
|
21
|
+
# first, let's see how ActiveRecord::Dirty#changes behave
|
22
|
+
survey.title = "new title"
|
23
|
+
survey.changes.should == {"title" => ["test survey", "new title"]}
|
24
|
+
survey.save!
|
25
|
+
survey.changes.should == {}
|
26
|
+
# .audited_changes to the rescue:
|
27
|
+
survey.audited_changes.should == {"title" => ["test survey", "new title"]}
|
28
|
+
survey.audits.last.action.should == "update"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should handle virtual attributes" do
|
32
|
+
survey.current_page = 1
|
33
|
+
# ActiveRecord::Dirty#changes is not meant to track virtual attributes
|
34
|
+
survey.changes.should == {}
|
35
|
+
survey.save!
|
36
|
+
# but we do because we were told to track it
|
37
|
+
survey.audited_changes.should == {"current_page" => [nil, 1]}
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should handle multiple keys" do
|
41
|
+
survey.current_page = 1
|
42
|
+
survey.title = "new title"
|
43
|
+
survey.save
|
44
|
+
survey.audited_changes.should == {
|
45
|
+
"title" => ["test survey", "new title"],
|
46
|
+
"current_page" => [nil, 1]
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
context "setting additional attributes" do
|
51
|
+
it "should set changed_by" do
|
52
|
+
survey.update_attributes(:title => "another title", :changed_by => user)
|
53
|
+
survey.audits.last.user.should == user
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should set audit_action" do
|
57
|
+
survey.update_attributes(:audit_action => "modified")
|
58
|
+
survey.audits.last.action.should == "modified"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should set audit_tag" do
|
62
|
+
survey.update_attributes(:audit_tag => "some tag")
|
63
|
+
survey.audits.last.tag.should == "some tag"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe ".last_audit" do
|
68
|
+
it "should be the same as audits.last" do
|
69
|
+
survey.audits.last.should == survey.last_audit
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "tagging" do
|
74
|
+
describe ".tag_with" do
|
75
|
+
it "should tag the latest audit" do
|
76
|
+
survey.audits.last.tag.should_not == "hey"
|
77
|
+
survey.tag_with("hey")
|
78
|
+
survey.audits.last.tag.should == "hey"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe ".audited_changes" do
|
83
|
+
context "using :tag and :changed_by arguments" do
|
84
|
+
before do
|
85
|
+
survey.tag_with("original")
|
86
|
+
survey.update_attributes :title => "new title 1", :changed_by => user
|
87
|
+
survey.tag_with("locked")
|
88
|
+
survey.update_attributes :title => "new title 2", :changed_by => another_user
|
89
|
+
survey.tag_with("locked")
|
90
|
+
survey.update_attributes :title => "new title 3", :changed_by => user
|
91
|
+
survey.update_attributes :title => "new title 4", :changed_by => another_user
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should diff with latest audit matching a tag" do
|
95
|
+
survey.audited_changes(:tag => "original").should == {"title" => ["test survey", "new title 4"]}
|
96
|
+
survey.audited_changes(:tag => "locked").should == {"title" => ["new title 2", "new title 4"]}
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should diff with latest audit matching user" do
|
100
|
+
survey.audited_changes(:changed_by => user).should == {"title" => ["new title 3", "new title 4"]}
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should diff under the same user and tag" do
|
104
|
+
survey.audited_changes(:tag => "locked", :changed_by => user).should == {"title" => ["new title 1", "new title 4"]}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "distinguishing between audited records" do
|
111
|
+
let(:another_survey) { Survey.create :title => "another survey" }
|
112
|
+
it "should audit changes pertaining to each record only" do
|
113
|
+
survey.update_attributes :title => "new title 1"
|
114
|
+
another_survey.update_attributes :title => "another title 1"
|
115
|
+
survey.audited_changes.should == {"title" => ["test survey", "new title 1"]}
|
116
|
+
another_survey.audited_changes.should == {"title" => ["another survey", "another title 1"]}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
class Survey < ActiveRecord::Base
|
2
|
+
attr_accessor :current_page
|
3
|
+
attr_accessor :changed_by, :audit_action, :audit_tag
|
4
|
+
|
5
|
+
audit :title, :current_page
|
6
|
+
end
|
7
|
+
|
8
|
+
class User < ActiveRecord::Base
|
9
|
+
end
|
10
|
+
|
11
|
+
# TODO add Question class to give examples on association stuff
|
12
|
+
|
13
|
+
# prepare test data
|
14
|
+
class CreateTestSchema < ActiveRecord::Migration
|
15
|
+
def change
|
16
|
+
create_table "surveys", :force => true do |t|
|
17
|
+
t.string "title"
|
18
|
+
end
|
19
|
+
create_table "users", :force => true do |t|
|
20
|
+
t.string "name"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
ActiveRecord::Base.establish_connection({
|
2
|
+
:adapter => 'sqlite3',
|
3
|
+
:database => ':memory:'
|
4
|
+
})
|
5
|
+
|
6
|
+
CreateTestSchema.migrate(:up)
|
7
|
+
|
8
|
+
# run gem's required migration
|
9
|
+
require(File.expand_path("../../../generators/auditable/templates/migration.rb", __FILE__))
|
10
|
+
CreateAudits.migrate(:up)
|
data/specs.watchr
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# Run me with:
|
2
|
+
#
|
3
|
+
# $ watchr specs.watchr
|
4
|
+
|
5
|
+
# --------------------------------------------------
|
6
|
+
# Convenience Methods
|
7
|
+
# --------------------------------------------------
|
8
|
+
def all_spec_files
|
9
|
+
Dir['spec/**/*_spec.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_spec_matching(thing_to_match)
|
13
|
+
matches = all_spec_files.grep(/#{thing_to_match}/i)
|
14
|
+
if matches.empty?
|
15
|
+
puts "Sorry, thanks for playing, but there were no matches for #{thing_to_match}"
|
16
|
+
else
|
17
|
+
run matches.join(' ')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def run(files_to_run)
|
22
|
+
puts("Running: #{files_to_run}")
|
23
|
+
system("clear;rspec #{files_to_run}")
|
24
|
+
no_int_for_you
|
25
|
+
end
|
26
|
+
|
27
|
+
def run_all_specs
|
28
|
+
run(all_spec_files.join(' '))
|
29
|
+
end
|
30
|
+
|
31
|
+
# --------------------------------------------------
|
32
|
+
# Watchr Rules
|
33
|
+
# --------------------------------------------------
|
34
|
+
watch('^spec/(.*)_spec\.rb') { |m| run_spec_matching(m[1]) }
|
35
|
+
watch('^lib/(.*)\.rb') { |m| run_spec_matching(m[1]) }
|
36
|
+
watch('^lib/(.*)/.*\.rb') { |m| run_spec_matching(m[1]) }
|
37
|
+
watch('^spec/spec_helper\.rb') { run_all_specs }
|
38
|
+
watch('^spec/support/.*\.rb') { run_all_specs }
|
39
|
+
|
40
|
+
# --------------------------------------------------
|
41
|
+
# Signal Handling
|
42
|
+
# --------------------------------------------------
|
43
|
+
|
44
|
+
def no_int_for_you
|
45
|
+
@sent_an_int = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
Signal.trap 'INT' do
|
49
|
+
if @sent_an_int then
|
50
|
+
puts " A second INT? Ok, I get the message. Shutting down now."
|
51
|
+
exit
|
52
|
+
else
|
53
|
+
puts " Did you just send me an INT? Ugh. I'll quit for real if you do it again."
|
54
|
+
@sent_an_int = true
|
55
|
+
Kernel.sleep 1.5
|
56
|
+
run_all_specs
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# vim:ft=ruby
|
61
|
+
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: auditable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Harley Trung
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: &2157976740 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2157976740
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
requirement: &2157976240 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2157976240
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: watchr
|
38
|
+
requirement: &2157975760 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2157975760
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: sqlite3
|
49
|
+
requirement: &2157975300 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2157975300
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: yard
|
60
|
+
requirement: &2157974760 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2157974760
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rdiscount
|
71
|
+
requirement: &2157974220 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *2157974220
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: activesupport
|
82
|
+
requirement: &2157973780 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :runtime
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *2157973780
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: activerecord
|
93
|
+
requirement: &2157973340 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :runtime
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *2157973340
|
102
|
+
description: A simple gem that audit models' attributes or methods by taking snapshots
|
103
|
+
and diff them for you. Starting from scratch to work with Rails 3.2.2 onwards
|
104
|
+
email:
|
105
|
+
- harley@socialsci.com
|
106
|
+
executables: []
|
107
|
+
extensions: []
|
108
|
+
extra_rdoc_files: []
|
109
|
+
files:
|
110
|
+
- .gitignore
|
111
|
+
- .rspec
|
112
|
+
- .rvmrc
|
113
|
+
- Gemfile
|
114
|
+
- LICENSE
|
115
|
+
- README.md
|
116
|
+
- Rakefile
|
117
|
+
- auditable.gemspec
|
118
|
+
- generators/auditable/migration_generator.rb
|
119
|
+
- generators/auditable/templates/migration.rb
|
120
|
+
- lib/auditable.rb
|
121
|
+
- lib/auditable/audit.rb
|
122
|
+
- lib/auditable/auditing.rb
|
123
|
+
- lib/auditable/version.rb
|
124
|
+
- spec/lib/auditable_spec.rb
|
125
|
+
- spec/spec_helper.rb
|
126
|
+
- spec/support/models.rb
|
127
|
+
- spec/support/schema.rb
|
128
|
+
- specs.watchr
|
129
|
+
homepage: ''
|
130
|
+
licenses: []
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options: []
|
133
|
+
require_paths:
|
134
|
+
- lib
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
136
|
+
none: false
|
137
|
+
requirements:
|
138
|
+
- - ! '>='
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
segments:
|
142
|
+
- 0
|
143
|
+
hash: -1046713668439752865
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
segments:
|
151
|
+
- 0
|
152
|
+
hash: -1046713668439752865
|
153
|
+
requirements: []
|
154
|
+
rubyforge_project:
|
155
|
+
rubygems_version: 1.8.17
|
156
|
+
signing_key:
|
157
|
+
specification_version: 3
|
158
|
+
summary: A simple gem to audit attributes and methods in ActiveRecord models.
|
159
|
+
test_files:
|
160
|
+
- spec/lib/auditable_spec.rb
|
161
|
+
- spec/spec_helper.rb
|
162
|
+
- spec/support/models.rb
|
163
|
+
- spec/support/schema.rb
|
164
|
+
has_rdoc:
|