├── lib
├── audited
│ ├── version.rb
│ ├── mongo_mapper
│ │ └── version.rb
│ ├── active_record
│ │ └── version.rb
│ ├── adapters
│ │ ├── active_record.rb
│ │ ├── mongo_mapper.rb
│ │ ├── mongo_mapper
│ │ │ ├── audited_changes.rb
│ │ │ └── audit.rb
│ │ └── active_record
│ │ │ └── audit.rb
│ ├── sweeper.rb
│ ├── audit.rb
│ ├── rspec_matchers.rb
│ └── auditor.rb
├── audited-mongo_mapper.rb
├── audited-activerecord.rb
├── audited-rspec.rb
├── generators
│ └── audited
│ │ ├── templates
│ │ ├── add_comment_to_audits.rb
│ │ ├── add_remote_address_to_audits.rb
│ │ ├── rename_changes_to_audited_changes.rb
│ │ ├── add_request_uuid_to_audits.rb
│ │ ├── add_association_to_audits.rb
│ │ ├── rename_parent_to_association.rb
│ │ ├── rename_association_to_associated.rb
│ │ └── install.rb
│ │ ├── install_generator.rb
│ │ └── upgrade_generator.rb
└── audited.rb
├── Gemfile
├── .yardopts
├── spec
├── rails_app
│ └── config
│ │ ├── initializers
│ │ ├── inflections.rb
│ │ ├── secret_token.rb
│ │ └── backtrace_silencers.rb
│ │ ├── environment.rb
│ │ ├── application.rb
│ │ ├── routes.rb
│ │ ├── database.yml
│ │ └── environments
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
├── support
│ ├── mongo_mapper
│ │ ├── connection.rb
│ │ └── models.rb
│ └── active_record
│ │ ├── schema.rb
│ │ └── models.rb
├── audited
│ └── adapters
│ │ ├── active_record
│ │ ├── active_record_spec_helper.rb
│ │ ├── sweeper_spec.rb
│ │ ├── audit_spec.rb
│ │ └── auditor_spec.rb
│ │ └── mongo_mapper
│ │ ├── mongo_mapper_spec_helper.rb
│ │ ├── sweeper_spec.rb
│ │ ├── audit_spec.rb
│ │ └── auditor_spec.rb
├── spec_helper.rb
└── audited_spec_helpers.rb
├── gemfiles
├── rails41.gemfile
├── rails42.gemfile
└── rails40.gemfile
├── .gitignore
├── Appraisals
├── test
├── test_helper.rb
├── install_generator_test.rb
├── db
│ ├── version_6.rb
│ ├── version_5.rb
│ ├── version_1.rb
│ ├── version_2.rb
│ ├── version_3.rb
│ └── version_4.rb
└── upgrade_generator_test.rb
├── .travis.yml
├── Rakefile
├── audited-mongo_mapper.gemspec
├── audited-activerecord.gemspec
├── LICENSE
├── audited.gemspec
├── CHANGELOG
└── README.md
/lib/audited/version.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | VERSION = "4.2.0"
3 | end
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec :name => 'audited'
4 |
--------------------------------------------------------------------------------
/.yardopts:
--------------------------------------------------------------------------------
1 | --no-private
2 | --title acts_as_audited
3 | --exclude lib/generators
4 |
--------------------------------------------------------------------------------
/lib/audited-mongo_mapper.rb:
--------------------------------------------------------------------------------
1 | require 'audited'
2 | require 'audited/adapters/mongo_mapper'
3 |
--------------------------------------------------------------------------------
/lib/audited-activerecord.rb:
--------------------------------------------------------------------------------
1 | require 'audited'
2 | require 'audited/adapters/active_record'
3 |
--------------------------------------------------------------------------------
/spec/rails_app/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | ActiveSupport::Inflector.inflections do |inflect|
2 | end
3 |
--------------------------------------------------------------------------------
/lib/audited/mongo_mapper/version.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | module MongoMapper
3 | VERSION = "4.2.0"
4 | end
5 | end
--------------------------------------------------------------------------------
/lib/audited/active_record/version.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | module ActiveRecord
3 | VERSION = "4.2.0"
4 | end
5 | end
--------------------------------------------------------------------------------
/lib/audited-rspec.rb:
--------------------------------------------------------------------------------
1 | require 'audited/rspec_matchers'
2 | module RSpec::Matchers
3 | include Audited::RspecMatchers
4 | end
5 |
--------------------------------------------------------------------------------
/spec/support/mongo_mapper/connection.rb:
--------------------------------------------------------------------------------
1 | require 'mongo_mapper'
2 |
3 | MongoMapper.connection = Mongo::Connection.new
4 | MongoMapper.database = 'audited_test'
5 |
--------------------------------------------------------------------------------
/spec/rails_app/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | RailsApp::Application.initialize!
6 |
--------------------------------------------------------------------------------
/gemfiles/rails41.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 4.1.0"
6 | gem "rails-observers"
7 |
8 | gemspec :name => "audited", :path => "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails42.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 4.2.0"
6 | gem "rails-observers"
7 |
8 | gemspec :name => "audited", :path => "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails40.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 4.0.0"
6 | gem "rails-observers"
7 | gem "test-unit"
8 |
9 | gemspec :name => "audited", :path => "../"
10 |
--------------------------------------------------------------------------------
/spec/rails_app/config/application.rb:
--------------------------------------------------------------------------------
1 | require 'rails/all'
2 |
3 | module RailsApp
4 | class Application < Rails::Application
5 | config.root = File.expand_path('../../', __FILE__)
6 | config.i18n.enforce_available_locales = true
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.ipr
3 | *.iws
4 | *.log
5 | *.swp
6 | .bundle
7 | .rakeTasks
8 | .ruby-gemset
9 | .ruby-version
10 | .rvmrc
11 | .yardoc
12 | coverage/
13 | doc/
14 | Gemfile.lock
15 | gemfiles/*.lock
16 | pkg
17 | tmp/*
18 | audited_test.sqlite3.db
19 |
--------------------------------------------------------------------------------
/spec/audited/adapters/active_record/active_record_spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'support/active_record/schema'
3 | require 'audited/adapters/active_record'
4 | require 'support/active_record/models'
5 | load "audited/sweeper.rb" # force to reload sweeper
6 |
--------------------------------------------------------------------------------
/spec/audited/adapters/mongo_mapper/mongo_mapper_spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'support/mongo_mapper/connection'
3 | require 'audited/adapters/mongo_mapper'
4 | require 'support/mongo_mapper/models'
5 | load "audited/sweeper.rb" # force to reload sweeper
6 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_comment_to_audits.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < ActiveRecord::Migration
2 | def self.up
3 | add_column :audits, :comment, :string
4 | end
5 |
6 | def self.down
7 | remove_column :audits, :comment
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_remote_address_to_audits.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < ActiveRecord::Migration
2 | def self.up
3 | add_column :audits, :remote_address, :string
4 | end
5 |
6 | def self.down
7 | remove_column :audits, :remote_address
8 | end
9 | end
10 |
11 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/rename_changes_to_audited_changes.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < ActiveRecord::Migration
2 | def self.up
3 | rename_column :audits, :changes, :audited_changes
4 | end
5 |
6 | def self.down
7 | rename_column :audits, :audited_changes, :changes
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise 'rails40' do
2 | gem 'rails', '~> 4.0.0'
3 | gem 'rails-observers'
4 | gem 'test-unit'
5 | end
6 |
7 | appraise 'rails41' do
8 | gem 'rails', '~> 4.1.0'
9 | gem 'rails-observers'
10 | end
11 |
12 | appraise 'rails42' do
13 | gem 'rails', '~> 4.2.0'
14 | gem 'rails-observers'
15 | end
16 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_request_uuid_to_audits.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < ActiveRecord::Migration
2 | def self.up
3 | add_column :audits, :request_uuid, :string
4 | add_index :audits, :request_uuid
5 | end
6 |
7 | def self.down
8 | remove_column :audits, :request_uuid
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/rails_app/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | Rails.application.config.secret_token = 'ea942c41850d502f2c8283e26bdc57829f471bb18224ddff0a192c4f32cdf6cb5aa0d82b3a7a7adbeb640c4b06f3aa1cd5f098162d8240f669b39d6b49680571'
2 | Rails.application.config.session_store :cookie_store, :key => "_my_app"
3 | Rails.application.config.secret_key_base = 'secret value'
4 |
--------------------------------------------------------------------------------
/spec/rails_app/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 |
3 | # This is a legacy wild controller route that's not recommended for RESTful applications.
4 | # Note: This route will make all actions in every controller accessible via GET requests.
5 | match ':controller(/:action(/:id(.:format)))', via: [:get, :post, :put, :delete]
6 | end
7 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/add_association_to_audits.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < ActiveRecord::Migration
2 | def self.up
3 | add_column :audits, :association_id, :integer
4 | add_column :audits, :association_type, :string
5 | end
6 |
7 | def self.down
8 | remove_column :audits, :association_type
9 | remove_column :audits, :association_id
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/audited.rb:
--------------------------------------------------------------------------------
1 | require 'rails/observers/active_model/active_model'
2 |
3 |
4 | module Audited
5 | class << self
6 | attr_accessor :ignored_attributes, :current_user_method, :audit_class
7 |
8 | def store
9 | Thread.current[:audited_store] ||= {}
10 | end
11 | end
12 |
13 | @ignored_attributes = %w(lock_version created_at updated_at created_on updated_on)
14 |
15 | @current_user_method = :current_user
16 | end
17 |
--------------------------------------------------------------------------------
/lib/audited/adapters/active_record.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 | require 'audited/auditor'
3 | require 'audited/adapters/active_record/audit'
4 |
5 | module Audited::Auditor::ClassMethods
6 | def default_ignored_attributes
7 | [self.primary_key, inheritance_column]
8 | end
9 | end
10 |
11 | ::ActiveRecord::Base.send :include, Audited::Auditor
12 |
13 | Audited.audit_class = Audited::Adapters::ActiveRecord::Audit
14 |
15 | require 'audited/sweeper'
16 |
--------------------------------------------------------------------------------
/spec/rails_app/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/rename_parent_to_association.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < ActiveRecord::Migration
2 | def self.up
3 | rename_column :audits, :auditable_parent_id, :association_id
4 | rename_column :audits, :auditable_parent_type, :association_type
5 | end
6 |
7 | def self.down
8 | rename_column :audits, :association_type, :auditable_parent_type
9 | rename_column :audits, :association_id, :auditable_parent_id
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] = 'test'
2 |
3 | $:.unshift File.dirname(__FILE__)
4 |
5 | require File.expand_path('../../spec/rails_app/config/environment', __FILE__)
6 | require 'rails/test_help'
7 |
8 | require 'audited'
9 |
10 | class ActiveSupport::TestCase
11 |
12 | setup do
13 | ActiveRecord::Migration.verbose = false
14 | end
15 |
16 | def load_schema( version )
17 | load File.dirname(__FILE__) + "/db/version_#{version}.rb"
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/audited/adapters/mongo_mapper.rb:
--------------------------------------------------------------------------------
1 | require 'mongo_mapper'
2 | require 'audited/auditor'
3 | require 'audited/adapters/mongo_mapper/audited_changes'
4 | require 'audited/adapters/mongo_mapper/audit'
5 |
6 | module Audited::Auditor::ClassMethods
7 | def default_ignored_attributes
8 | ['id', '_id']
9 | end
10 | end
11 |
12 | ::MongoMapper::Document.plugin Audited::Auditor
13 |
14 | Audited.audit_class = Audited::Adapters::MongoMapper::Audit
15 |
16 | require 'audited/sweeper'
17 |
--------------------------------------------------------------------------------
/spec/rails_app/config/database.yml:
--------------------------------------------------------------------------------
1 | sqlite3mem: &SQLITE3MEM
2 | adapter: sqlite3
3 | database: ":memory:"
4 |
5 | sqlite3: &SQLITE
6 | adapter: sqlite3
7 | database: audited_test.sqlite3.db
8 |
9 | postgresql: &POSTGRES
10 | adapter: postgresql
11 | username: postgres
12 | password:
13 | database: audited_test
14 | min_messages: ERROR
15 |
16 | mysql: &MYSQL
17 | adapter: mysql2
18 | host: localhost
19 | username: root
20 | password:
21 | database: audited_test
22 |
23 | test:
24 | <<: *<%= ENV['DB'] || 'SQLITE3MEM' %>
25 |
--------------------------------------------------------------------------------
/test/install_generator_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | require 'generators/audited/install_generator'
4 |
5 | class InstallGeneratorTest < Rails::Generators::TestCase
6 | destination File.expand_path('../../tmp', __FILE__)
7 | setup :prepare_destination
8 | tests Audited::Generators::InstallGenerator
9 |
10 | test "should generate a migration" do
11 | run_generator %w(install)
12 |
13 | assert_migration "db/migrate/install_audited.rb" do |content|
14 | assert_match /class InstallAudited/, content
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/db/version_6.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, :force => true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :audited_changes, :text
10 | t.column :version, :integer, :default => 0
11 | t.column :comment, :string
12 | t.column :created_at, :datetime
13 | t.column :remote_address, :string
14 | t.column :associated_id, :integer
15 | t.column :associated_type, :string
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/db/version_5.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, :force => true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :audited_changes, :text
10 | t.column :version, :integer, :default => 0
11 | t.column :comment, :string
12 | t.column :created_at, :datetime
13 | t.column :remote_address, :string
14 | t.column :association_id, :integer
15 | t.column :association_type, :string
16 | end
17 | end
18 |
19 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | services: mongodb
3 | rvm:
4 | - 2.0
5 | - 2.1
6 | - 2.2
7 | - jruby-head
8 | env:
9 | - DB=SQLITE
10 | - DB=POSTGRES
11 | - DB=MYSQL
12 | before_script:
13 | - mysql -e 'create database audited_test;'
14 | - psql -c 'create database audited_test;' -U postgres
15 | gemfile:
16 | - gemfiles/rails40.gemfile
17 | - gemfiles/rails41.gemfile
18 | - gemfiles/rails42.gemfile
19 | matrix:
20 | allow_failures:
21 | - rvm: jruby-head
22 | branches:
23 | only:
24 | - master
25 | sudo: false
26 | notifications:
27 | webhooks:
28 | urls:
29 | - http://buildlight.collectiveidea.com/
30 | on_start: true
31 |
--------------------------------------------------------------------------------
/test/db/version_1.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, :force => true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :changes, :text
10 | t.column :version, :integer, :default => 0
11 | t.column :created_at, :datetime
12 | end
13 |
14 | add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
15 | add_index :audits, [:user_id, :user_type], :name => 'user_index'
16 | add_index :audits, :created_at
17 | end
18 |
--------------------------------------------------------------------------------
/test/db/version_2.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, :force => true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :changes, :text
10 | t.column :version, :integer, :default => 0
11 | t.column :comment, :string
12 | t.column :created_at, :datetime
13 | end
14 |
15 | add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
16 | add_index :audits, [:user_id, :user_type], :name => 'user_index'
17 | add_index :audits, :created_at
18 | end
19 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] = 'test'
2 |
3 | require 'protected_attributes'
4 | require 'rails_app/config/environment'
5 | require 'rspec/rails'
6 | require 'audited'
7 | require 'audited_spec_helpers'
8 |
9 | SPEC_ROOT = Pathname.new(File.expand_path('../', __FILE__))
10 |
11 | Dir[SPEC_ROOT.join('support/*.rb')].each{|f| require f }
12 |
13 | RSpec.configure do |config|
14 | config.include AuditedSpecHelpers
15 |
16 | config.before(:each, :adapter => :active_record) do
17 | Audited.audit_class = Audited::Adapters::ActiveRecord::Audit
18 | end
19 |
20 | config.before(:each, :adapter => :mongo_mapper) do
21 | Audited.audit_class = Audited::Adapters::MongoMapper::Audit
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/db/version_3.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, :force => true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :audited_changes, :text
10 | t.column :version, :integer, :default => 0
11 | t.column :comment, :string
12 | t.column :created_at, :datetime
13 | end
14 |
15 | add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
16 | add_index :audits, [:user_id, :user_type], :name => 'user_index'
17 | add_index :audits, :created_at
18 | end
19 |
20 |
--------------------------------------------------------------------------------
/test/db/version_4.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | create_table :audits, :force => true do |t|
3 | t.column :auditable_id, :integer
4 | t.column :auditable_type, :string
5 | t.column :user_id, :integer
6 | t.column :user_type, :string
7 | t.column :username, :string
8 | t.column :action, :string
9 | t.column :audited_changes, :text
10 | t.column :version, :integer, :default => 0
11 | t.column :comment, :string
12 | t.column :created_at, :datetime
13 | t.column :remote_address, :string
14 | end
15 |
16 | add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
17 | add_index :audits, [:user_id, :user_type], :name => 'user_index'
18 | add_index :audits, :created_at
19 | end
20 |
21 |
--------------------------------------------------------------------------------
/lib/audited/adapters/mongo_mapper/audited_changes.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | module Adapters
3 | module MongoMapper
4 | class AuditedChanges < ::Hash
5 | def self.from_mongo(changes)
6 | changes.is_a?(Hash) ? new.replace(changes) : changes
7 | end
8 |
9 | def self.to_mongo(changes)
10 | if changes.is_a?(Hash)
11 | changes.inject({}) do |memo, (key, value)|
12 | memo[key] = if value.is_a?(Array)
13 | value.map{|v| v.class.to_mongo(v) }
14 | else
15 | value
16 | end
17 | memo
18 | end
19 | else
20 | changes
21 | end
22 | end
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/rails_app/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | RailsApp::Application.configure do
2 | # Settings specified here will take precedence over those in config/environment.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the webserver when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | # config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_view.debug_rjs = true
15 | config.action_controller.perform_caching = false
16 |
17 | # Don't care if the mailer can't send
18 | config.action_mailer.raise_delivery_errors = false
19 |
20 | config.eager_load = false
21 | end
22 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 |
3 | require 'bundler/gem_helper'
4 | require 'rspec/core/rake_task'
5 | require 'rake/testtask'
6 | require 'appraisal'
7 |
8 | Bundler::GemHelper.install_tasks(:name => 'audited')
9 | Bundler::GemHelper.install_tasks(:name => 'audited-activerecord')
10 | Bundler::GemHelper.install_tasks(:name => 'audited-mongo_mapper')
11 |
12 | ADAPTERS = %w(active_record mongo_mapper)
13 |
14 | ADAPTERS.each do |adapter|
15 | desc "Run RSpec code examples for #{adapter} adapter"
16 | RSpec::Core::RakeTask.new(adapter) do |t|
17 | t.pattern = "spec/audited/adapters/#{adapter}/**/*_spec.rb"
18 | end
19 | end
20 |
21 | task :spec do
22 | ADAPTERS.each do |adapter|
23 | Rake::Task[adapter].invoke
24 | end
25 | end
26 |
27 | Rake::TestTask.new do |t|
28 | t.libs << "test"
29 | t.test_files = FileList['test/**/*_test.rb']
30 | t.verbose = true
31 | end
32 |
33 | task :default => [:spec, :test]
34 |
--------------------------------------------------------------------------------
/audited-mongo_mapper.gemspec:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | $:.push File.expand_path("../lib", __FILE__)
4 | require "audited/version"
5 | require "audited/mongo_mapper/version"
6 |
7 | Gem::Specification.new do |gem|
8 | gem.name = 'audited-mongo_mapper'
9 | gem.version = Audited::MongoMapper::VERSION
10 |
11 | gem.authors = ['Brandon Keepers', 'Kenneth Kalmer', 'Daniel Morrison', 'Brian Ryckbost', 'Steve Richert', 'Ryan Glover']
12 | gem.email = 'info@collectiveidea.com'
13 | gem.description = 'Log all changes to your MongoMapper models'
14 | gem.summary = gem.description
15 | gem.homepage = 'https://github.com/collectiveidea/audited'
16 | gem.license = 'MIT'
17 |
18 | gem.add_dependency 'audited', Audited::VERSION
19 | gem.add_dependency 'mongo_mapper', '~> 0.12.0'
20 |
21 | gem.files = `git ls-files lib`.split($\).grep(/mongo_mapper/)
22 | gem.files << 'LICENSE'
23 | gem.require_paths = ['lib']
24 | end
25 |
26 |
--------------------------------------------------------------------------------
/audited-activerecord.gemspec:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | $:.push File.expand_path("../lib", __FILE__)
4 | require "audited/version"
5 | require "audited/active_record/version"
6 |
7 | Gem::Specification.new do |gem|
8 | gem.name = 'audited-activerecord'
9 | gem.version = Audited::ActiveRecord::VERSION
10 |
11 | gem.authors = ['Brandon Keepers', 'Kenneth Kalmer', 'Daniel Morrison', 'Brian Ryckbost', 'Steve Richert', 'Ryan Glover']
12 | gem.email = 'info@collectiveidea.com'
13 | gem.description = 'Log all changes to your ActiveRecord models'
14 | gem.summary = gem.description
15 | gem.homepage = 'https://github.com/collectiveidea/audited'
16 | gem.license = 'MIT'
17 |
18 | gem.add_dependency 'audited', Audited::VERSION
19 | gem.add_dependency 'activerecord', '~> 4.0'
20 |
21 | gem.files = `git ls-files lib`.split($\).grep(/(active_?record|generators)/)
22 | gem.files << 'LICENSE'
23 | gem.require_paths = ['lib']
24 | end
25 |
26 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/rename_association_to_associated.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < ActiveRecord::Migration
2 | def self.up
3 | if index_exists? :audits, [:association_id, :association_type], :name => 'association_index'
4 | remove_index :audits, :name => 'association_index'
5 | end
6 |
7 | rename_column :audits, :association_id, :associated_id
8 | rename_column :audits, :association_type, :associated_type
9 |
10 | add_index :audits, [:associated_id, :associated_type], :name => 'associated_index'
11 | end
12 |
13 | def self.down
14 | if index_exists? :audits, [:associated_id, :associated_type], :name => 'associated_index'
15 | remove_index :audits, :name => 'associated_index'
16 | end
17 |
18 | rename_column :audits, :associated_type, :association_type
19 | rename_column :audits, :associated_id, :association_id
20 |
21 | add_index :audits, [:association_id, :association_type], :name => 'association_index'
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/generators/audited/install_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators'
2 | require 'rails/generators/migration'
3 | require 'active_record'
4 | require 'rails/generators/active_record'
5 |
6 | module Audited
7 | module Generators
8 | class InstallGenerator < Rails::Generators::Base
9 | include Rails::Generators::Migration
10 |
11 | source_root File.expand_path("../templates", __FILE__)
12 |
13 | # Implement the required interface for Rails::Generators::Migration.
14 | def self.next_migration_number(dirname) #:nodoc:
15 | next_migration_number = current_migration_number(dirname) + 1
16 | if ::ActiveRecord::Base.timestamped_migrations
17 | [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
18 | else
19 | "%.3d" % next_migration_number
20 | end
21 | end
22 |
23 | def copy_migration
24 | migration_template 'install.rb', 'db/migrate/install_audited.rb'
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2010 Brandon Keepers - Collective Idea
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/lib/generators/audited/templates/install.rb:
--------------------------------------------------------------------------------
1 | class <%= migration_class_name %> < ActiveRecord::Migration
2 | def self.up
3 | create_table :audits, :force => true do |t|
4 | t.column :auditable_id, :integer
5 | t.column :auditable_type, :string
6 | t.column :associated_id, :integer
7 | t.column :associated_type, :string
8 | t.column :user_id, :integer
9 | t.column :user_type, :string
10 | t.column :username, :string
11 | t.column :action, :string
12 | t.column :audited_changes, :text
13 | t.column :version, :integer, :default => 0
14 | t.column :comment, :string
15 | t.column :remote_address, :string
16 | t.column :request_uuid, :string
17 | t.column :created_at, :datetime
18 | end
19 |
20 | add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
21 | add_index :audits, [:associated_id, :associated_type], :name => 'associated_index'
22 | add_index :audits, [:user_id, :user_type], :name => 'user_index'
23 | add_index :audits, :request_uuid
24 | add_index :audits, :created_at
25 | end
26 |
27 | def self.down
28 | drop_table :audits
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/audited_spec_helpers.rb:
--------------------------------------------------------------------------------
1 | module AuditedSpecHelpers
2 |
3 | def create_user(use_mongo = false, attrs = {})
4 | klass = use_mongo ? Models::MongoMapper::User : Models::ActiveRecord::User
5 | klass.create({:name => 'Brandon', :username => 'brandon', :password => 'password'}.merge(attrs))
6 | end
7 |
8 | def build_user(use_mongo = false, attrs = {})
9 | klass = use_mongo ? Models::MongoMapper::User : Models::ActiveRecord::User
10 | klass.new({:name => 'darth', :username => 'darth', :password => 'noooooooo'}.merge(attrs))
11 | end
12 |
13 | def create_versions(n = 2, use_mongo = false)
14 | klass = use_mongo ? Models::MongoMapper::User : Models::ActiveRecord::User
15 |
16 | klass.create(:name => 'Foobar 1').tap do |u|
17 | (n - 1).times do |i|
18 | u.update_attribute :name, "Foobar #{i + 2}"
19 | end
20 | u.reload
21 | end
22 | end
23 |
24 | def create_active_record_user(attrs = {})
25 | create_user(false, attrs)
26 | end
27 |
28 | def create_mongo_user(attrs = {})
29 | create_user(true, attrs)
30 | end
31 |
32 | def create_mongo_versions(n = 2)
33 | create_versions(n, true)
34 | end
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/spec/rails_app/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | RailsApp::Application.configure do
2 | # Settings specified here will take precedence over those in config/environment.rb
3 |
4 | # The production environment is meant for finished, "live" apps.
5 | # Code is not reloaded between requests
6 | config.cache_classes = true
7 |
8 | # Full error reports are disabled and caching is turned on
9 | config.consider_all_requests_local = false
10 | config.action_controller.perform_caching = true
11 |
12 | # See everything in the log (default is :info)
13 | # config.log_level = :debug
14 |
15 | # Use a different logger for distributed setups
16 | # config.logger = SyslogLogger.new
17 |
18 | # Use a different cache store in production
19 | # config.cache_store = :mem_cache_store
20 |
21 | # Disable Rails's static asset server
22 | # In production, Apache or nginx will already do this
23 | config.serve_static_assets = false
24 |
25 | # Enable serving of images, stylesheets, and javascripts from an asset server
26 | # config.action_controller.asset_host = "http://assets.example.com"
27 |
28 | # Disable delivery errors, bad email addresses will be ignored
29 | # config.action_mailer.raise_delivery_errors = false
30 |
31 | # Enable threaded mode
32 | # config.threadsafe!
33 |
34 | config.eager_load = true
35 | end
36 |
--------------------------------------------------------------------------------
/audited.gemspec:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "audited/version"
4 |
5 | Gem::Specification.new do |gem|
6 | gem.name = 'audited'
7 | gem.version = Audited::VERSION
8 | gem.platform = Gem::Platform::RUBY
9 |
10 | gem.authors = ['Brandon Keepers', 'Kenneth Kalmer', 'Daniel Morrison', 'Brian Ryckbost', 'Steve Richert', 'Ryan Glover']
11 | gem.email = 'info@collectiveidea.com'
12 | gem.description = 'Log all changes to your models'
13 | gem.summary = gem.description
14 | gem.homepage = 'https://github.com/collectiveidea/audited'
15 | gem.license = 'MIT'
16 |
17 | gem.files = `git ls-files`.split($\).reject{|f| f =~ /(\.gemspec|lib\/audited\-|adapters|generators)/ }
18 | gem.test_files = gem.files.grep(/^spec\//)
19 | gem.require_paths = ['lib']
20 |
21 | gem.add_dependency 'rails-observers', '~> 0.1.2'
22 |
23 | gem.add_development_dependency "protected_attributes"
24 | gem.add_development_dependency 'appraisal', '~> 1.0.0'
25 | gem.add_development_dependency 'mongo_mapper', '~> 0.13.0.beta2'
26 | gem.add_development_dependency 'rails', '~> 4.2.0'
27 | gem.add_development_dependency 'rspec-rails', '~> 3.0'
28 |
29 | # JRuby support for the test ENV
30 | unless defined?(JRUBY_VERSION)
31 | gem.add_development_dependency 'sqlite3', '~> 1.2'
32 | gem.add_development_dependency 'mysql2', '~> 0.3'
33 | gem.add_development_dependency 'pg', '~> 0.17'
34 | gem.add_development_dependency 'bson_ext', '~> 1.6'
35 | else
36 | gem.add_development_dependency 'activerecord-jdbcsqlite3-adapter', '~> 1.3'
37 | gem.add_development_dependency 'activerecord-jdbcpostgresql-adapter', '~> 1.3'
38 | gem.add_development_dependency 'activerecord-jdbcmysql-adapter', '~> 1.3'
39 | gem.add_development_dependency 'bson', '~> 1.6'
40 | end
41 | end
42 |
43 |
--------------------------------------------------------------------------------
/spec/support/active_record/schema.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 | require 'logger'
3 |
4 | ActiveRecord::Base.establish_connection
5 | ActiveRecord::Base.logger = Logger.new(SPEC_ROOT.join('debug.log'))
6 | ActiveRecord::Migration.verbose = false
7 |
8 | ActiveRecord::Schema.define do
9 | create_table :users, :force => true do |t|
10 | t.column :name, :string
11 | t.column :username, :string
12 | t.column :password, :string
13 | t.column :activated, :boolean
14 | t.column :suspended_at, :datetime
15 | t.column :logins, :integer, :default => 0
16 | t.column :created_at, :datetime
17 | t.column :updated_at, :datetime
18 | end
19 |
20 | create_table :companies, :force => true do |t|
21 | t.column :name, :string
22 | t.column :owner_id, :integer
23 | end
24 |
25 | create_table :authors, :force => true do |t|
26 | t.column :name, :string
27 | end
28 |
29 | create_table :books, :force => true do |t|
30 | t.column :authord_id, :integer
31 | t.column :title, :string
32 | end
33 |
34 | create_table :audits, :force => true do |t|
35 | t.column :auditable_id, :integer
36 | t.column :auditable_type, :string
37 | t.column :associated_id, :integer
38 | t.column :associated_type, :string
39 | t.column :user_id, :integer
40 | t.column :user_type, :string
41 | t.column :username, :string
42 | t.column :action, :string
43 | t.column :audited_changes, :text
44 | t.column :version, :integer, :default => 0
45 | t.column :comment, :string
46 | t.column :remote_address, :string
47 | t.column :request_uuid, :string
48 | t.column :created_at, :datetime
49 | end
50 |
51 | add_index :audits, [:auditable_id, :auditable_type], :name => 'auditable_index'
52 | add_index :audits, [:associated_id, :associated_type], :name => 'associated_index'
53 | add_index :audits, [:user_id, :user_type], :name => 'user_index'
54 | add_index :audits, :request_uuid
55 | add_index :audits, :created_at
56 | end
57 |
--------------------------------------------------------------------------------
/lib/audited/sweeper.rb:
--------------------------------------------------------------------------------
1 | require "rails/observers/activerecord/active_record"
2 | require "rails/observers/action_controller/caching"
3 |
4 | module Audited
5 | class Sweeper < ActionController::Caching::Sweeper
6 | observe Audited.audit_class
7 |
8 | attr_accessor :controller
9 | def before(controller)
10 | self.controller = controller
11 | true
12 | end
13 |
14 | def after(controller)
15 | self.controller = nil
16 | end
17 |
18 | def before_create(audit)
19 | audit.user ||= current_user
20 | audit.remote_address = controller.try(:request).try(:remote_ip)
21 | audit.request_uuid = request_uuid if request_uuid
22 | end
23 |
24 | def current_user
25 | controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true)
26 | end
27 |
28 | def request_uuid
29 | controller.try(:request).try(:uuid)
30 | end
31 |
32 | def add_observer!(klass)
33 | super
34 | define_callback(klass)
35 | end
36 |
37 | def define_callback(klass)
38 | observer = self
39 | callback_meth = :"_notify_audited_sweeper"
40 | klass.send(:define_method, callback_meth) do
41 | observer.update(:before_create, self)
42 | end
43 | klass.send(:before_create, callback_meth)
44 | end
45 |
46 | def controller
47 | ::Audited.store[:current_controller]
48 | end
49 |
50 | def controller=(value)
51 | ::Audited.store[:current_controller] = value
52 | end
53 | end
54 | end
55 |
56 | if defined?(ActionController) and defined?(ActionController::Base)
57 | # Create dynamic subclass of Audited::Sweeper otherwise rspec will
58 | # fail with both ActiveRecord and MongoMapper tests as there will be
59 | # around_filter collision
60 | sweeper_class = Class.new(Audited::Sweeper) do
61 | def self.name
62 | "#{Audited.audit_class}::Sweeper"
63 | end
64 | end
65 |
66 | ActionController::Base.class_eval do
67 | around_filter sweeper_class.instance
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/spec/rails_app/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | RailsApp::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static asset server for tests with Cache-Control for performance.
16 | config.serve_static_assets = true
17 | # Configure static file server for tests with Cache-Control for performance.
18 | config.serve_static_files = true
19 | config.static_cache_control = 'public, max-age=3600'
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | # config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | # config.action_controller.allow_forgery_protection = false
30 |
31 | # Tell Action Mailer not to deliver emails to the real world.
32 | # The :test delivery method accumulates sent emails in the
33 | # ActionMailer::Base.deliveries array.
34 | config.action_mailer.delivery_method = :test
35 |
36 | # Randomize the order test cases are executed.
37 | config.active_support.test_order = :random
38 |
39 | # Print deprecation notices to the stderr.
40 | config.active_support.deprecation = :stderr
41 |
42 | # Raises error for missing translations
43 | # config.action_view.raise_on_missing_translations = true
44 | end
45 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | Audited ChangeLog
2 | -------------------------------------------------------------------------------
3 | * 2012-04-10 - Add Audit scopes for creates, updates and destroys [chriswfx]
4 | * 2011-10-25 - Made ignored_attributes configurable [senny]
5 | * 2011-09-09 - Rails 3.x support
6 | Support for associated audits
7 | Support for remote IP address storage
8 | Plenty of bug fixes and refactoring
9 | [kennethkalmer, ineu, PatrickMa, jrozner, dwarburton, bsiggelkow, dgm]
10 | * 2009-01-27 - Store old and new values for updates, and store all attributes on destroy.
11 | Refactored revisioning methods to work as expected
12 | * 2008-10-10 - changed to make it work in development mode
13 | * 2008-09-24 - Add ability to record parent record of the record being audited
14 | [Kenneth Kalmer]
15 | * 2008-04-19 - refactored to make compatible with dirty tracking in edge rails
16 | and to stop storing both old and new values in a single audit
17 | * 2008-04-18 - Fix NoMethodError when trying to access the :previous revision
18 | on a model that doesn't have previous revisions [Alex Soto]
19 | * 2008-03-21 - added #changed_attributes to get access to the changes before a
20 | save [Chris Parker]
21 | * 2007-12-16 - Added #revision_at for retrieving a revision from a specific
22 | time [Jacob Atzen]
23 | * 2007-12-16 - Fix error when getting revision from audit with no changes
24 | [Geoffrey Wiseman]
25 | * 2007-12-16 - Remove dependency on acts_as_list
26 | * 2007-06-17 - Added support getting previous revisions
27 | * 2006-11-17 - Replaced use of singleton User.current_user with cache sweeper
28 | implementation for auditing the user that made the change
29 | * 2006-11-17 - added migration generator
30 | * 2006-08-14 - incorporated changes from Michael Schuerig to write_attribute
31 | that saves the new value after every change and not just the
32 | first, and performs proper type-casting before doing comparisons
33 | * 2006-08-14 - The "changes" are now saved as a serialized hash
34 | * 2006-07-21 - initial version
35 |
--------------------------------------------------------------------------------
/lib/generators/audited/upgrade_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators'
2 | require 'rails/generators/migration'
3 | require 'active_record'
4 | require 'rails/generators/active_record'
5 |
6 | module Audited
7 | module Generators
8 | class UpgradeGenerator < Rails::Generators::Base
9 | include Rails::Generators::Migration
10 |
11 | source_root File.expand_path("../templates", __FILE__)
12 |
13 | # Implement the required interface for Rails::Generators::Migration.
14 | def self.next_migration_number(dirname) #:nodoc:
15 | next_migration_number = current_migration_number(dirname) + 1
16 | if ::ActiveRecord::Base.timestamped_migrations
17 | [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
18 | else
19 | "%.3d" % next_migration_number
20 | end
21 | end
22 |
23 | def copy_templates
24 | migrations_to_be_applied do |m|
25 | migration_template "#{m}.rb", "db/migrate/#{m}.rb"
26 | end
27 | end
28 |
29 | private
30 |
31 | def migrations_to_be_applied
32 | Audited::Adapters::ActiveRecord::Audit.reset_column_information
33 | columns = Audited::Adapters::ActiveRecord::Audit.columns.map(&:name)
34 |
35 | unless columns.include?( 'comment' )
36 | yield :add_comment_to_audits
37 | end
38 |
39 | if columns.include?( 'changes' )
40 | yield :rename_changes_to_audited_changes
41 | end
42 |
43 | unless columns.include?( 'remote_address' )
44 | yield :add_remote_address_to_audits
45 | end
46 |
47 | unless columns.include?( 'request_uuid' )
48 | yield :add_request_uuid_to_audits
49 | end
50 |
51 | unless columns.include?( 'association_id' )
52 | if columns.include?('auditable_parent_id')
53 | yield :rename_parent_to_association
54 | else
55 | unless columns.include?( 'associated_id' )
56 | yield :add_association_to_audits
57 | end
58 | end
59 | end
60 |
61 | if columns.include?( 'association_id' )
62 | yield :rename_association_to_associated
63 | end
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/test/upgrade_generator_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | require 'audited/adapters/active_record'
4 | require 'generators/audited/upgrade_generator'
5 |
6 | class UpgradeGeneratorTest < Rails::Generators::TestCase
7 | destination File.expand_path('../../tmp', __FILE__)
8 | setup :prepare_destination
9 | tests Audited::Generators::UpgradeGenerator
10 |
11 | test "should add 'comment' to audits table" do
12 | load_schema 1
13 |
14 | run_generator %w(upgrade)
15 |
16 | assert_migration "db/migrate/add_comment_to_audits.rb" do |content|
17 | assert_match /add_column :audits, :comment, :string/, content
18 | end
19 |
20 | assert_migration "db/migrate/rename_changes_to_audited_changes.rb"
21 | end
22 |
23 | test "should rename 'changes' to 'audited_changes'" do
24 | load_schema 2
25 |
26 | run_generator %w(upgrade)
27 |
28 | assert_no_migration "db/migrate/add_comment_to_audits.rb"
29 |
30 | assert_migration "db/migrate/rename_changes_to_audited_changes.rb" do |content|
31 | assert_match /rename_column :audits, :changes, :audited_changes/, content
32 | end
33 | end
34 |
35 | test "should add a 'remote_address' to audits table" do
36 | load_schema 3
37 |
38 | run_generator %w(upgrade)
39 |
40 | assert_migration "db/migrate/add_remote_address_to_audits.rb" do |content|
41 | assert_match /add_column :audits, :remote_address, :string/, content
42 | end
43 | end
44 |
45 | test "should add 'association_id' and 'association_type' to audits table" do
46 | load_schema 4
47 |
48 | run_generator %w(upgrade)
49 |
50 | assert_migration "db/migrate/add_association_to_audits.rb" do |content|
51 | assert_match /add_column :audits, :association_id, :integer/, content
52 | assert_match /add_column :audits, :association_type, :string/, content
53 | end
54 | end
55 |
56 | test "should rename 'association_id' to 'associated_id' and 'association_type' to 'associated_type'" do
57 | load_schema 5
58 |
59 | run_generator %w(upgrade)
60 |
61 | assert_migration "db/migrate/rename_association_to_associated.rb" do |content|
62 | assert_match /rename_column :audits, :association_id, :associated_id/, content
63 | assert_match /rename_column :audits, :association_type, :associated_type/, content
64 | end
65 | end
66 |
67 | test "should add 'request_uuid' to audits table" do
68 | load_schema 6
69 |
70 | run_generator %w(upgrade)
71 |
72 | assert_migration "db/migrate/add_request_uuid_to_audits.rb" do |content|
73 | assert_match /add_column :audits, :request_uuid, :string/, content
74 | assert_match /add_index :audits, :request_uuid/, content
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/spec/support/active_record/models.rb:
--------------------------------------------------------------------------------
1 | require 'cgi'
2 | require File.expand_path('../schema', __FILE__)
3 |
4 | module Models
5 | module ActiveRecord
6 | class User < ::ActiveRecord::Base
7 | audited :allow_mass_assignment => true, :except => :password
8 |
9 | attr_protected :logins
10 |
11 | def name=(val)
12 | write_attribute(:name, CGI.escapeHTML(val))
13 | end
14 | end
15 |
16 | class CommentRequiredUser < ::ActiveRecord::Base
17 | self.table_name = :users
18 | audited :comment_required => true
19 | end
20 |
21 | class AccessibleAfterDeclarationUser < ::ActiveRecord::Base
22 | self.table_name = :users
23 | audited
24 | attr_accessible :name, :username, :password
25 | end
26 |
27 | class AccessibleBeforeDeclarationUser < ::ActiveRecord::Base
28 | self.table_name = :users
29 | attr_accessible :name, :username, :password # declare attr_accessible before calling aaa
30 | audited
31 | end
32 |
33 | class NoAttributeProtectionUser < ::ActiveRecord::Base
34 | self.table_name = :users
35 | audited :allow_mass_assignment => true
36 | end
37 |
38 | class UserWithAfterAudit < ::ActiveRecord::Base
39 | self.table_name = :users
40 | audited
41 | attr_accessor :bogus_attr, :around_attr
42 |
43 | def after_audit
44 | self.bogus_attr = "do something"
45 | end
46 |
47 | def around_audit
48 | self.around_attr = yield
49 | end
50 | end
51 |
52 | class Company < ::ActiveRecord::Base
53 | audited
54 | end
55 |
56 | class Owner < ::ActiveRecord::Base
57 | self.table_name = 'users'
58 | has_associated_audits
59 | has_many :companies, class_name: "OwnedCompany", dependent: :destroy
60 | end
61 |
62 | class OwnedCompany < ::ActiveRecord::Base
63 | self.table_name = 'companies'
64 | belongs_to :owner, :class_name => "Owner"
65 | attr_accessible :name, :owner # declare attr_accessible before calling aaa
66 | audited :associated_with => :owner
67 | end
68 |
69 | class OnUpdateDestroy < ::ActiveRecord::Base
70 | self.table_name = 'companies'
71 | audited :on => [:update, :destroy]
72 | end
73 |
74 | class OnCreateDestroy < ::ActiveRecord::Base
75 | self.table_name = 'companies'
76 | audited :on => [:create, :destroy]
77 | end
78 |
79 | class OnCreateDestroyExceptName < ::ActiveRecord::Base
80 | self.table_name = 'companies'
81 | audited :except => :name, :on => [:create, :destroy]
82 | end
83 |
84 | class OnCreateUpdate < ::ActiveRecord::Base
85 | self.table_name = 'companies'
86 | audited :on => [:create, :update]
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/lib/audited/adapters/active_record/audit.rb:
--------------------------------------------------------------------------------
1 | require 'set'
2 | require 'audited/audit'
3 |
4 | module Audited
5 | module Adapters
6 | module ActiveRecord
7 | # Audit saves the changes to ActiveRecord models. It has the following attributes:
8 | #
9 | # * auditable: the ActiveRecord model that was changed
10 | # * user: the user that performed the change; a string or an ActiveRecord model
11 | # * action: one of create, update, or delete
12 | # * audited_changes: a serialized hash of all the changes
13 | # * comment: a comment set with the audit
14 | # * version: the version of the model
15 | # * request_uuid: a uuid based that allows audits from the same controller request
16 | # * created_at: Time that the change was performed
17 | #
18 | class Audit < ::ActiveRecord::Base
19 | include Audited::Audit
20 | include ActiveModel::Observing
21 |
22 | serialize :audited_changes
23 |
24 | default_scope ->{ order(:version)}
25 | scope :descending, ->{ reorder("version DESC")}
26 | scope :creates, ->{ where({:action => 'create'})}
27 | scope :updates, ->{ where({:action => 'update'})}
28 | scope :destroys, ->{ where({:action => 'destroy'})}
29 |
30 | scope :up_until, ->(date_or_time){where("created_at <= ?", date_or_time) }
31 | scope :from_version, ->(version){where(['version >= ?', version]) }
32 | scope :to_version, ->(version){where(['version <= ?', version]) }
33 | scope :auditable_finder, ->(auditable_id, auditable_type){where(auditable_id: auditable_id, auditable_type: auditable_type)}
34 | # Return all audits older than the current one.
35 | def ancestors
36 | self.class.where(['auditable_id = ? and auditable_type = ? and version <= ?',
37 | auditable_id, auditable_type, version])
38 | end
39 |
40 | # Allows user to be set to either a string or an ActiveRecord object
41 | # @private
42 | def user_as_string=(user)
43 | # reset both either way
44 | self.user_as_model = self.username = nil
45 | user.is_a?(::ActiveRecord::Base) ?
46 | self.user_as_model = user :
47 | self.username = user
48 | end
49 | alias_method :user_as_model=, :user=
50 | alias_method :user=, :user_as_string=
51 |
52 | # @private
53 | def user_as_string
54 | self.user_as_model || self.username
55 | end
56 | alias_method :user_as_model, :user
57 | alias_method :user, :user_as_string
58 |
59 | private
60 | def set_version_number
61 | max = self.class.auditable_finder(auditable_id, auditable_type).maximum(:version) || 0
62 | self.version = max + 1
63 | end
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/spec/audited/adapters/mongo_mapper/sweeper_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../mongo_mapper_spec_helper', __FILE__)
2 |
3 | class MongoAuditsController < ActionController::Base
4 | def audit
5 | @company = Models::MongoMapper::Company.create
6 | render :nothing => true
7 | end
8 |
9 | def update_user
10 | current_user.update_attributes( :password => 'foo')
11 | render :nothing => true
12 | end
13 |
14 | private
15 |
16 | attr_accessor :current_user
17 | attr_accessor :custom_user
18 | end
19 |
20 | describe MongoAuditsController, :adapter => :mongo_mapper do
21 | include RSpec::Rails::ControllerExampleGroup
22 |
23 | before(:each) do
24 | Audited.current_user_method = :current_user
25 | end
26 |
27 | let( :user ) { create_mongo_user }
28 |
29 | describe "POST audit" do
30 |
31 | it "should audit user" do
32 | controller.send(:current_user=, user)
33 |
34 | expect {
35 | post :audit
36 | }.to change( Audited.audit_class, :count )
37 |
38 | expect(assigns(:company).audits.last.user).to eq(user)
39 | end
40 |
41 | it "should support custom users for sweepers" do
42 | controller.send(:custom_user=, user)
43 | Audited.current_user_method = :custom_user
44 |
45 | expect {
46 | post :audit
47 | }.to change( Audited.audit_class, :count )
48 |
49 | expect(assigns(:company).audits.last.user).to eq(user)
50 | end
51 |
52 | it "should record the remote address responsible for the change" do
53 | request.env['REMOTE_ADDR'] = "1.2.3.4"
54 | controller.send(:current_user=, user)
55 |
56 | post :audit
57 |
58 | expect(assigns(:company).audits.last.remote_address).to eq('1.2.3.4')
59 | end
60 |
61 | it "should record a UUID for the web request responsible for the change" do
62 | allow_any_instance_of(ActionDispatch::Request).to receive(:uuid).and_return("abc123")
63 | controller.send(:current_user=, user)
64 |
65 | post :audit
66 |
67 | expect(assigns(:company).audits.last.request_uuid).to eq("abc123")
68 | end
69 |
70 | end
71 |
72 | describe "POST update_user" do
73 |
74 | it "should not save blank audits" do
75 | controller.send(:current_user=, user)
76 |
77 | expect {
78 | post :update_user
79 | }.to_not change( Audited.audit_class, :count )
80 | end
81 |
82 | end
83 | end
84 |
85 |
86 | describe Audited::Sweeper, :adapter => :mongo_mapper do
87 |
88 | it "should be thread-safe" do
89 | t1 = Thread.new do
90 | sleep 0.5
91 | Audited::Sweeper.instance.controller = 'thread1 controller instance'
92 | expect(Audited::Sweeper.instance.controller).to eq('thread1 controller instance')
93 | end
94 |
95 | t2 = Thread.new do
96 | Audited::Sweeper.instance.controller = 'thread2 controller instance'
97 | sleep 1
98 | expect(Audited::Sweeper.instance.controller).to eq('thread2 controller instance')
99 | end
100 |
101 | t1.join; t2.join
102 |
103 | expect(Audited::Sweeper.instance.controller).to be_nil
104 | end
105 |
106 | end
107 |
--------------------------------------------------------------------------------
/spec/audited/adapters/active_record/sweeper_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../active_record_spec_helper', __FILE__)
2 |
3 | class AuditsController < ActionController::Base
4 | def audit
5 | @company = Models::ActiveRecord::Company.create
6 | render :nothing => true
7 | end
8 |
9 | def update_user
10 | current_user.update_attributes( :password => 'foo')
11 | render :nothing => true
12 | end
13 |
14 | private
15 |
16 | attr_accessor :current_user
17 | attr_accessor :custom_user
18 | end
19 |
20 | describe AuditsController, :adapter => :active_record do
21 | include RSpec::Rails::ControllerExampleGroup
22 | render_views
23 |
24 | before(:each) do
25 | Audited.current_user_method = :current_user
26 | end
27 |
28 | let( :user ) { create_user }
29 |
30 | describe "POST audit" do
31 |
32 | it "should audit user" do
33 | controller.send(:current_user=, user)
34 | expect {
35 | post :audit
36 | }.to change( Audited.audit_class, :count )
37 |
38 | expect(assigns(:company).audits.last.user).to eq(user)
39 | end
40 |
41 | it "should support custom users for sweepers" do
42 | controller.send(:custom_user=, user)
43 | Audited.current_user_method = :custom_user
44 |
45 | expect {
46 | post :audit
47 | }.to change( Audited.audit_class, :count )
48 |
49 | expect(assigns(:company).audits.last.user).to eq(user)
50 | end
51 |
52 | it "should record the remote address responsible for the change" do
53 | request.env['REMOTE_ADDR'] = "1.2.3.4"
54 | controller.send(:current_user=, user)
55 |
56 | post :audit
57 |
58 | expect(assigns(:company).audits.last.remote_address).to eq('1.2.3.4')
59 | end
60 |
61 | it "should record a UUID for the web request responsible for the change" do
62 | allow_any_instance_of(ActionDispatch::Request).to receive(:uuid).and_return("abc123")
63 | controller.send(:current_user=, user)
64 |
65 | post :audit
66 |
67 | expect(assigns(:company).audits.last.request_uuid).to eq("abc123")
68 | end
69 |
70 | end
71 |
72 | describe "POST update_user" do
73 |
74 | it "should not save blank audits" do
75 | controller.send(:current_user=, user)
76 |
77 | expect {
78 | post :update_user
79 | }.to_not change( Audited.audit_class, :count )
80 | end
81 |
82 | end
83 | end
84 |
85 |
86 | describe Audited::Sweeper, :adapter => :active_record do
87 |
88 | it "should be thread-safe" do
89 | t1 = Thread.new do
90 | sleep 0.5
91 | Audited::Sweeper.instance.controller = 'thread1 controller instance'
92 | expect(Audited::Sweeper.instance.controller).to eq('thread1 controller instance')
93 | end
94 |
95 | t2 = Thread.new do
96 | Audited::Sweeper.instance.controller = 'thread2 controller instance'
97 | sleep 1
98 | expect(Audited::Sweeper.instance.controller).to eq('thread2 controller instance')
99 | end
100 |
101 | t1.join; t2.join
102 |
103 | expect(Audited::Sweeper.instance.controller).to be_nil
104 | end
105 |
106 | end
107 |
--------------------------------------------------------------------------------
/lib/audited/audit.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | module Audit
3 | def self.included(klass)
4 | klass.extend(ClassMethods)
5 | klass.setup_audit
6 | end
7 |
8 | module ClassMethods
9 | def setup_audit
10 | belongs_to :auditable, :polymorphic => true
11 | belongs_to :user, :polymorphic => true
12 | belongs_to :associated, :polymorphic => true
13 |
14 | before_create :set_version_number, :set_audit_user, :set_request_uuid
15 |
16 | cattr_accessor :audited_class_names
17 | self.audited_class_names = Set.new
18 | end
19 |
20 | # Returns the list of classes that are being audited
21 | def audited_classes
22 | audited_class_names.map(&:constantize)
23 | end
24 |
25 | # All audits made during the block called will be recorded as made
26 | # by +user+. This method is hopefully threadsafe, making it ideal
27 | # for background operations that require audit information.
28 | def as_user(user, &block)
29 | Thread.current[:audited_user] = user
30 | yield
31 | ensure
32 | Thread.current[:audited_user] = nil
33 | end
34 |
35 | # @private
36 | def reconstruct_attributes(audits)
37 | attributes = {}
38 | result = audits.collect do |audit|
39 | attributes.merge!(audit.new_attributes).merge!(:version => audit.version)
40 | yield attributes if block_given?
41 | end
42 | block_given? ? result : attributes
43 | end
44 |
45 | # @private
46 | def assign_revision_attributes(record, attributes)
47 | attributes.each do |attr, val|
48 | record = record.dup if record.frozen?
49 |
50 | if record.respond_to?("#{attr}=")
51 | record.attributes.has_key?(attr.to_s) ?
52 | record[attr] = val :
53 | record.send("#{attr}=", val)
54 | end
55 | end
56 | record
57 | end
58 | end
59 |
60 | # Return an instance of what the object looked like at this revision. If
61 | # the object has been destroyed, this will be a new record.
62 | def revision
63 | clazz = auditable_type.constantize
64 | (clazz.find_by_id(auditable_id) || clazz.new).tap do |m|
65 | self.class.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge({ :version => version }))
66 | end
67 | end
68 |
69 | # Returns a hash of the changed attributes with the new values
70 | def new_attributes
71 | (audited_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
72 | attrs[attr] = values.is_a?(Array) ? values.last : values
73 | attrs
74 | end
75 | end
76 |
77 | # Returns a hash of the changed attributes with the old values
78 | def old_attributes
79 | (audited_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
80 | attrs[attr] = Array(values).first
81 |
82 | attrs
83 | end
84 | end
85 |
86 | private
87 | def set_version_number
88 | max = self.class.where(
89 | :auditable_id => auditable_id,
90 | :auditable_type => auditable_type
91 | ).order(:version.desc).first.try(:version) || 0
92 | self.version = max + 1
93 | end
94 |
95 | def set_audit_user
96 | self.user = Thread.current[:audited_user] if Thread.current[:audited_user]
97 | nil # prevent stopping callback chains
98 | end
99 |
100 | def set_request_uuid
101 | self.request_uuid ||= SecureRandom.uuid
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/lib/audited/adapters/mongo_mapper/audit.rb:
--------------------------------------------------------------------------------
1 | require 'set'
2 | require 'audited/audit'
3 |
4 | module Audited
5 | module Adapters
6 | module MongoMapper
7 | # Audit saves the changes to ActiveRecord models. It has the following attributes:
8 | #
9 | # * auditable: the ActiveRecord model that was changed
10 | # * user: the user that performed the change; a string or an ActiveRecord model
11 | # * action: one of create, update, or delete
12 | # * audited_changes: a serialized hash of all the changes
13 | # * comment: a comment set with the audit
14 | # * created_at: Time that the change was performed
15 | #
16 | class Audit
17 | include ::MongoMapper::Document
18 | include ActiveModel::Observing
19 |
20 | key :auditable_id, ObjectId
21 | key :auditable_type, String
22 | key :associated_id, ObjectId
23 | key :associated_type, String
24 | key :user_id, ObjectId
25 | key :user_type, String
26 | key :username, String
27 | key :action, String
28 | key :audited_changes, AuditedChanges
29 | key :version, Integer, :default => 0
30 | key :comment, String
31 | key :remote_address, String
32 | key :request_uuid, String
33 | key :created_at, Time
34 |
35 | include Audited::Audit
36 |
37 | before_create :set_created_at
38 |
39 | def self.acending; sort(:version.asc); end
40 | def self.ascending; sort(:version.asc); end
41 | def self.descending; sort(:version.desc); end
42 | def self.creates; where(:action => 'create'); end
43 | def self.updates; where(:action => 'update'); end
44 | def self.destroys; where(:action => 'destroy'); end
45 |
46 | def self.up_until(date_or_time); where(:created_at.lte => date_or_time); end
47 | def self.from_version(version); where(:version.gte => version); end
48 | def self.to_version(version); where(:version.lte => version); end
49 |
50 | class << self
51 |
52 | # @private
53 | def sanitize_for_time_with_zone(value)
54 | case value
55 | when Hash
56 | value.inject({}){|h,(k,v)| h[k] = sanitize_for_time_with_zone(v); h }
57 | when Array
58 | value.map{|v| sanitize_for_time_with_zone(v) }
59 | when ActiveSupport::TimeWithZone
60 | value.utc
61 | else
62 | value
63 | end
64 | end
65 | end
66 |
67 | def audited_changes=(value)
68 | self[:audited_changes] = self.class.sanitize_for_time_with_zone(value)
69 | end
70 |
71 | # Allows user to be set to either a string or an ActiveRecord object
72 | # @private
73 | def user_as_string=(user)
74 | # reset both either way
75 | self.user_as_model = self.username = nil
76 | user.is_a?(::MongoMapper::Document) ?
77 | self.user_as_model = user :
78 | self.username = user
79 | end
80 | alias_method :user_as_model=, :user=
81 | alias_method :user=, :user_as_string=
82 |
83 | # @private
84 | def user_as_string
85 | self.user_as_model || self.username
86 | end
87 | alias_method :user_as_model, :user
88 | alias_method :user, :user_as_string
89 |
90 | # Return all audits older than the current one.
91 | def ancestors
92 | self.class.where(:auditable_id => auditable_id, :auditable_type => auditable_type, :version.lte => version)
93 | end
94 |
95 | def set_created_at
96 | self[:created_at] = Time.now.utc if !persisted? && !created_at?
97 | end
98 | end
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/lib/audited/rspec_matchers.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | module RspecMatchers
3 | # Ensure that the model is audited.
4 | #
5 | # Options:
6 | # * associated_with - tests that the audit makes use of the associated_with option
7 | # * only - tests that the audit makes use of the only option *Overrides except option*
8 | # * except - tests that the audit makes use of the except option
9 | # * requires_comment - if specified, then the audit must require comments through the audit_comment attribute
10 | # * on - tests that the audit makes use of the on option with specified parameters
11 | #
12 | # Example:
13 | # it { should be_audited }
14 | # it { should be_audited.associated_with(:user) }
15 | # it { should be_audited.only(:field_name) }
16 | # it { should be_audited.except(:password) }
17 | # it { should be_audited.requires_comment }
18 | # it { should be_audited.on(:create).associated_with(:user).except(:password) }
19 | #
20 | def be_audited
21 | AuditMatcher.new
22 | end
23 |
24 | # Ensure that the model has associated audits
25 | #
26 | # Example:
27 | # it { should have_associated_audits }
28 | #
29 | def have_associated_audits
30 | AssociatedAuditMatcher.new
31 | end
32 |
33 | class AuditMatcher # :nodoc:
34 | def initialize
35 | @options = {}
36 | end
37 |
38 | def associated_with(model)
39 | @options[:associated_with] = model
40 | self
41 | end
42 |
43 | def only(*fields)
44 | @options[:only] = fields.flatten
45 | self
46 | end
47 |
48 | def except(*fields)
49 | @options[:except] = fields.flatten
50 | self
51 | end
52 |
53 | def requires_comment
54 | @options[:comment_required] = true
55 | self
56 | end
57 |
58 | def on(*actions)
59 | @options[:on] = actions.flatten
60 | self
61 | end
62 |
63 | def matches?(subject)
64 | @subject = subject
65 | auditing_enabled? &&
66 | associated_with_model? &&
67 | records_changes_to_specified_fields? &&
68 | comment_required_valid?
69 | end
70 |
71 | def failure_message
72 | "Expected #{@expectation}"
73 | end
74 |
75 | def negative_failure_message
76 | "Did not expect #{@expectation}"
77 | end
78 |
79 | def description
80 | description = "audited"
81 | description += " associated with #{@options[:associated_with]}" if @options.key?(:associated_with)
82 | description += " only => #{@options[:only].join ', '}" if @options.key?(:only)
83 | description += " except => #{@options[:except].join(', ')}" if @options.key?(:except)
84 | description += " requires audit_comment" if @options.key?(:comment_required)
85 |
86 | description
87 | end
88 |
89 | protected
90 |
91 | def expects(message)
92 | @expectation = message
93 | end
94 |
95 | def auditing_enabled?
96 | expects "#{model_class} to be audited"
97 | model_class.respond_to?(:auditing_enabled) && model_class.auditing_enabled
98 | end
99 |
100 | def model_class
101 | @subject.class
102 | end
103 |
104 | def associated_with_model?
105 | expects "#{model_class} to record audits to associated model #{@options[:associated_with]}"
106 | model_class.audit_associated_with == @options[:associated_with]
107 | end
108 |
109 | def records_changes_to_specified_fields?
110 | if @options[:only] || @options[:except]
111 | if @options[:only]
112 | except = model_class.column_names - @options[:only].map(&:to_s)
113 | else
114 | except = model_class.default_ignored_attributes + Audited.ignored_attributes
115 | except |= @options[:except].collect(&:to_s) if @options[:except]
116 | end
117 |
118 | expects "non audited columns (#{model_class.non_audited_columns.inspect}) to match (#{expect})"
119 | model_class.non_audited_columns =~ except
120 | else
121 | true
122 | end
123 | end
124 |
125 | def comment_required_valid?
126 | if @options[:comment_required]
127 | @subject.audit_comment = nil
128 |
129 | expects "to be invalid when audit_comment is not specified"
130 | @subject.valid? == false && @subject.errors.key?(:audit_comment)
131 | else
132 | true
133 | end
134 | end
135 | end
136 |
137 | class AssociatedAuditMatcher # :nodoc:
138 | def matches?(subject)
139 | @subject = subject
140 |
141 | association_exists?
142 | end
143 |
144 | def failure_message
145 | "Expected #{model_class} to have associated audits"
146 | end
147 |
148 | def negative_failure_message
149 | "Expected #{model_class} to not have associated audits"
150 | end
151 |
152 | def description
153 | "has associated audits"
154 | end
155 |
156 | protected
157 |
158 | def model_class
159 | @subject.class
160 | end
161 |
162 | def reflection
163 | model_class.reflect_on_association(:associated_audits)
164 | end
165 |
166 | def association_exists?
167 | (!reflection.nil?) &&
168 | reflection.macro == :has_many &&
169 | reflection.options[:class_name] == Audited.audit_class.name
170 | end
171 | end
172 | end
173 | end
174 |
--------------------------------------------------------------------------------
/spec/support/mongo_mapper/models.rb:
--------------------------------------------------------------------------------
1 | require 'cgi'
2 | require 'mongo_mapper'
3 | require File.expand_path('../connection', __FILE__)
4 |
5 | module Models
6 | module MongoMapper
7 | class User
8 | include ::MongoMapper::Document
9 |
10 | key :name, String
11 | key :username, String
12 | key :password, String
13 | key :activated, Boolean
14 | key :suspended_at, Time
15 | key :logins, Integer, :default => 0
16 | timestamps!
17 |
18 | audited :allow_mass_assignment => true, :except => :password
19 |
20 | attr_protected :logins
21 |
22 | def name=(val)
23 | write_attribute(:name, CGI.escapeHTML(val))
24 | end
25 | end
26 |
27 | class CommentRequiredUser
28 | include ::MongoMapper::Document
29 |
30 | key :name, String
31 | key :username, String
32 | key :password, String
33 | key :activated, Boolean
34 | key :suspended_at, Time
35 | key :logins, Integer, :default => 0
36 | timestamps!
37 |
38 | audited :comment_required => true
39 | end
40 |
41 | class AccessibleAfterDeclarationUser
42 | include ::MongoMapper::Document
43 |
44 | key :name, String
45 | key :username, String
46 | key :password, String
47 | key :activated, Boolean
48 | key :suspended_at, Time
49 | key :logins, Integer, :default => 0
50 | timestamps!
51 |
52 | audited
53 | attr_accessible :name, :username, :password
54 | end
55 |
56 | class AccessibleBeforeDeclarationUser
57 | include ::MongoMapper::Document
58 |
59 | key :name, String
60 | key :username, String
61 | key :password, String
62 | key :activated, Boolean
63 | key :suspended_at, Time
64 | key :logins, Integer, :default => 0
65 | timestamps!
66 |
67 | attr_accessible :name, :username, :password # declare attr_accessible before calling aaa
68 | audited
69 | end
70 |
71 | class NoAttributeProtectionUser
72 | include ::MongoMapper::Document
73 |
74 | key :name, String
75 | key :username, String
76 | key :password, String
77 | key :activated, Boolean
78 | key :suspended_at, Time
79 | key :logins, Integer, :default => 0
80 | timestamps!
81 |
82 | audited :allow_mass_assignment => true
83 | end
84 |
85 | class UserWithAfterAudit
86 | include ::MongoMapper::Document
87 |
88 | key :name, String
89 | key :username, String
90 | key :password, String
91 | key :activated, Boolean
92 | key :suspended_at, Time
93 | key :logins, Integer, :default => 0
94 | timestamps!
95 |
96 | audited
97 | attr_accessor :bogus_attr, :around_attr
98 |
99 | def after_audit
100 | self.bogus_attr = "do something"
101 | end
102 |
103 | def around_audit
104 | self.around_attr = yield
105 | end
106 | end
107 |
108 | class Company
109 | include ::MongoMapper::Document
110 |
111 | key :name, String
112 | key :owner_id, ObjectId
113 |
114 | audited
115 | end
116 |
117 | class Owner
118 | include ::MongoMapper::Document
119 |
120 | key :name, String
121 | key :username, String
122 | key :password, String
123 | key :activated, Boolean
124 | key :suspended_at, Time
125 | key :logins, Integer, :default => 0
126 | timestamps!
127 |
128 | has_associated_audits
129 | end
130 |
131 | class OwnedCompany
132 | include ::MongoMapper::Document
133 |
134 | key :name, String
135 | key :owner_id, ObjectId
136 |
137 | belongs_to :owner, :class_name => "Owner"
138 | attr_accessible :name, :owner # declare attr_accessible before calling aaa
139 | audited :associated_with => :owner
140 | end
141 |
142 | class OnUpdateDestroy
143 | include ::MongoMapper::Document
144 |
145 | key :name, String
146 | key :owner_id, ObjectId
147 |
148 | audited :on => [:update, :destroy]
149 | end
150 |
151 | class OnCreateDestroy
152 | include ::MongoMapper::Document
153 |
154 | key :name, String
155 | key :owner_id, ObjectId
156 |
157 | audited :on => [:create, :destroy]
158 | end
159 |
160 | class OnCreateDestroyExceptName
161 | include ::MongoMapper::Document
162 |
163 | key :name, String
164 | key :owner_id, ObjectId
165 |
166 | audited :except => :name, :on => [:create, :destroy]
167 | end
168 |
169 | class OnCreateUpdate
170 | include ::MongoMapper::Document
171 |
172 | key :name, String
173 | key :owner_id, ObjectId
174 |
175 | audited :on => [:create, :update]
176 | end
177 |
178 | class RichObjectUser
179 | include ::MongoMapper::Document
180 |
181 | class Name
182 | attr_accessor :first_name, :last_name
183 |
184 | def self.from_mongo(value)
185 | case value
186 | when String then new(*value.split)
187 | when self then value
188 | end
189 | end
190 |
191 | def self.to_mongo(value)
192 | case value
193 | when String then value
194 | when self then value.to_s
195 | end
196 | end
197 |
198 | def initialize(first_name, last_name)
199 | self.first_name, self.last_name = first_name, last_name
200 | end
201 |
202 | def to_s
203 | [first_name, last_name].compact.join(' ')
204 | end
205 | end
206 |
207 | key :name, Name
208 |
209 | attr_accessible :name
210 |
211 | audited
212 | end
213 | end
214 | end
215 |
--------------------------------------------------------------------------------
/spec/audited/adapters/mongo_mapper/audit_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../mongo_mapper_spec_helper', __FILE__)
2 |
3 | describe Audited::Adapters::MongoMapper::Audit, :adapter => :mongo_mapper do
4 | let(:user) { Models::MongoMapper::User.new :name => 'Testing' }
5 |
6 | it "sets created_at timestamp when audit is created" do
7 | subject.save!
8 | expect(subject.created_at).to be_a Time
9 | end
10 |
11 | describe "user=" do
12 |
13 | it "should be able to set the user to a model object" do
14 | subject.user = user
15 | expect(subject.user).to eq(user)
16 | end
17 |
18 | it "should be able to set the user to nil" do
19 | subject.user_id = 1
20 | subject.user_type = 'Models::MongoMapper::User'
21 | subject.username = 'joe'
22 |
23 | subject.user = nil
24 |
25 | expect(subject.user).to be_nil
26 | expect(subject.user_id).to be_nil
27 | expect(subject.user_type).to be_nil
28 | expect(subject.username).to be_nil
29 | end
30 |
31 | it "should be able to set the user to a string" do
32 | subject.user = 'test'
33 | expect(subject.user).to eq('test')
34 | end
35 |
36 | it "should clear model when setting to a string" do
37 | subject.user = user
38 | subject.user = 'testing'
39 | expect(subject.user_id).to be_nil
40 | expect(subject.user_type).to be_nil
41 | end
42 |
43 | it "should clear the username when setting to a model" do
44 | subject.username = 'test'
45 | subject.user = user
46 | expect(subject.username).to be_nil
47 | end
48 |
49 | end
50 |
51 | describe "revision" do
52 |
53 | it "should recreate attributes" do
54 | user = Models::MongoMapper::User.create :name => "1"
55 | 5.times { |i| user.update_attribute :name, (i + 2).to_s }
56 |
57 | user.audits.each do |audit|
58 | expect(audit.revision.name).to eq(audit.version.to_s)
59 | end
60 | end
61 |
62 | it "should set protected attributes" do
63 | u = Models::MongoMapper::User.create(:name => 'Brandon')
64 | u.update_attribute :logins, 1
65 | u.update_attribute :logins, 2
66 |
67 | expect(u.audits[2].revision.logins).to eq(2)
68 | expect(u.audits[1].revision.logins).to eq(1)
69 | expect(u.audits[0].revision.logins).to eq(0)
70 | end
71 |
72 | it "should bypass attribute assignment wrappers" do
73 | u = Models::MongoMapper::User.create(:name => '')
74 | expect(u.audits.first.revision.name).to eq('<Joe>')
75 | end
76 |
77 | it "should work for deleted records" do
78 | user = Models::MongoMapper::User.create :name => "1"
79 | user.destroy
80 | revision = user.audits.last.revision
81 | expect(revision.name).to eq(user.name)
82 | expect(revision).to be_a_new_record
83 | end
84 |
85 | end
86 |
87 | it "should set the version number on create" do
88 | user = Models::MongoMapper::User.create! :name => 'Set Version Number'
89 | expect(user.audits.first.version).to eq(1)
90 | user.update_attribute :name, "Set to 2"
91 | audits = user.audits.reload.all
92 | expect(audits.first.version).to eq(1)
93 | expect(audits.last.version).to eq(2)
94 | user.destroy
95 | expect(Audited.audit_class.where(:auditable_type => 'Models::MongoMapper::User', :auditable_id => user.id).all.last.version).to eq(3)
96 | end
97 |
98 | it "should set the request uuid on create" do
99 | user = Models::MongoMapper::User.create! :name => 'Set Request UUID'
100 | audits = user.audits.reload.all
101 | expect(audits.first.request_uuid).not_to be_blank
102 | end
103 |
104 | describe "reconstruct_attributes" do
105 |
106 | it "should work with the old way of storing just the new value" do
107 | audits = Audited.audit_class.reconstruct_attributes([Audited.audit_class.new(:audited_changes => {'attribute' => 'value'})])
108 | expect(audits['attribute']).to eq('value')
109 | end
110 |
111 | end
112 |
113 | describe "audited_classes" do
114 | class Models::MongoMapper::CustomUser
115 | include ::MongoMapper::Document
116 | end
117 | class Models::MongoMapper::CustomUserSubclass < Models::MongoMapper::CustomUser
118 | audited
119 | end
120 |
121 | it "should include audited classes" do
122 | expect(Audited.audit_class.audited_classes).to include(Models::MongoMapper::User)
123 | end
124 |
125 | it "should include subclasses" do
126 | expect(Audited.audit_class.audited_classes).to include(Models::MongoMapper::CustomUserSubclass)
127 | end
128 | end
129 |
130 | describe "new_attributes" do
131 |
132 | it "should return a hash of the new values" do
133 | new_attributes = Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).new_attributes
134 | expect(new_attributes).to eq({'a' => 2, 'b' => 4})
135 | end
136 |
137 | end
138 |
139 | describe "old_attributes" do
140 |
141 | it "should return a hash of the old values" do
142 | old_attributes = Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).old_attributes
143 | expect(old_attributes).to eq({'a' => 1, 'b' => 3})
144 | end
145 |
146 | end
147 |
148 | describe "as_user" do
149 |
150 | it "should record user objects" do
151 | Audited.audit_class.as_user(user) do
152 | company = Models::MongoMapper::Company.create :name => 'The auditors'
153 | company.name = 'The Auditors, Inc'
154 | company.save
155 |
156 | company.audits.each do |audit|
157 | expect(audit.user).to eq(user)
158 | end
159 | end
160 | end
161 |
162 | it "should record usernames" do
163 | Audited.audit_class.as_user(user.name) do
164 | company = Models::MongoMapper::Company.create :name => 'The auditors'
165 | company.name = 'The Auditors, Inc'
166 | company.save
167 |
168 | company.audits.each do |audit|
169 | expect(audit.username).to eq(user.name)
170 | end
171 | end
172 | end
173 |
174 | it "should be thread safe" do
175 | t1 = Thread.new do
176 | Audited.audit_class.as_user(user) do
177 | sleep 1
178 | expect(Models::MongoMapper::Company.create(:name => 'The Auditors, Inc').audits.first.user).to eq(user)
179 | end
180 | end
181 |
182 | t2 = Thread.new do
183 | Audited.audit_class.as_user(user.name) do
184 | expect(Models::MongoMapper::Company.create(:name => 'The Competing Auditors, LLC').audits.first.username).to eq(user.name)
185 | sleep 0.5
186 | end
187 | end
188 |
189 | t1.join
190 | t2.join
191 | end
192 |
193 | it "should return the value from the yield block" do
194 | result = Audited.audit_class.as_user('foo') do
195 | 42
196 | end
197 | expect(result).to eq(42)
198 | end
199 |
200 | it "should reset audited_user when the yield block raises an exception" do
201 | expect {
202 | Audited.audit_class.as_user('foo') do
203 | raise StandardError
204 | end
205 | }.to raise_exception
206 | expect(Thread.current[:audited_user]).to be_nil
207 | end
208 |
209 | end
210 |
211 | end
212 |
--------------------------------------------------------------------------------
/spec/audited/adapters/active_record/audit_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../active_record_spec_helper', __FILE__)
2 |
3 | describe Audited::Adapters::ActiveRecord::Audit, :adapter => :active_record do
4 | let(:user) { Models::ActiveRecord::User.new :name => 'Testing' }
5 |
6 | describe "user=" do
7 |
8 | it "should be able to set the user to a model object" do
9 | subject.user = user
10 | expect(subject.user).to eq(user)
11 | end
12 |
13 | it "should be able to set the user to nil" do
14 | subject.user_id = 1
15 | subject.user_type = 'Models::ActiveRecord::User'
16 | subject.username = 'joe'
17 |
18 | subject.user = nil
19 |
20 | expect(subject.user).to be_nil
21 | expect(subject.user_id).to be_nil
22 | expect(subject.user_type).to be_nil
23 | expect(subject.username).to be_nil
24 | end
25 |
26 | it "should be able to set the user to a string" do
27 | subject.user = 'test'
28 | expect(subject.user).to eq('test')
29 | end
30 |
31 | it "should clear model when setting to a string" do
32 | subject.user = user
33 | subject.user = 'testing'
34 | expect(subject.user_id).to be_nil
35 | expect(subject.user_type).to be_nil
36 | end
37 |
38 | it "should clear the username when setting to a model" do
39 | subject.username = 'test'
40 | subject.user = user
41 | expect(subject.username).to be_nil
42 | end
43 |
44 | end
45 |
46 | describe "revision" do
47 |
48 | it "should recreate attributes" do
49 | user = Models::ActiveRecord::User.create :name => "1"
50 | 5.times { |i| user.update_attribute :name, (i + 2).to_s }
51 |
52 | user.audits.each do |audit|
53 | expect(audit.revision.name).to eq(audit.version.to_s)
54 | end
55 | end
56 |
57 | it "should set protected attributes" do
58 | u = Models::ActiveRecord::User.create(:name => 'Brandon')
59 | u.update_attribute :logins, 1
60 | u.update_attribute :logins, 2
61 |
62 | expect(u.audits[2].revision.logins).to eq(2)
63 | expect(u.audits[1].revision.logins).to eq(1)
64 | expect(u.audits[0].revision.logins).to eq(0)
65 | end
66 |
67 | it "should bypass attribute assignment wrappers" do
68 | u = Models::ActiveRecord::User.create(:name => '')
69 | expect(u.audits.first.revision.name).to eq('<Joe>')
70 | end
71 |
72 | it "should work for deleted records" do
73 | user = Models::ActiveRecord::User.create :name => "1"
74 | user.destroy
75 | revision = user.audits.last.revision
76 | expect(revision.name).to eq(user.name)
77 | expect(revision).to be_a_new_record
78 | end
79 |
80 | end
81 |
82 | it "should set the version number on create" do
83 | user = Models::ActiveRecord::User.create! :name => 'Set Version Number'
84 | expect(user.audits.first.version).to eq(1)
85 | user.update_attribute :name, "Set to 2"
86 | expect(user.audits(true).first.version).to eq(1)
87 | expect(user.audits(true).last.version).to eq(2)
88 | user.destroy
89 | expect(Audited.audit_class.where(:auditable_type => 'Models::ActiveRecord::User', :auditable_id => user.id).last.version).to eq(3)
90 | end
91 |
92 | it "should set the request uuid on create" do
93 | user = Models::ActiveRecord::User.create! :name => 'Set Request UUID'
94 | expect(user.audits(true).first.request_uuid).not_to be_blank
95 | end
96 |
97 | describe "reconstruct_attributes" do
98 |
99 | it "should work with the old way of storing just the new value" do
100 | audits = Audited.audit_class.reconstruct_attributes([Audited.audit_class.new(:audited_changes => {'attribute' => 'value'})])
101 | expect(audits['attribute']).to eq('value')
102 | end
103 |
104 | end
105 |
106 | describe "audited_classes" do
107 | class Models::ActiveRecord::CustomUser < ::ActiveRecord::Base
108 | end
109 | class Models::ActiveRecord::CustomUserSubclass < Models::ActiveRecord::CustomUser
110 | audited
111 | end
112 |
113 | it "should include audited classes" do
114 | expect(Audited.audit_class.audited_classes).to include(Models::ActiveRecord::User)
115 | end
116 |
117 | it "should include subclasses" do
118 | expect(Audited.audit_class.audited_classes).to include(Models::ActiveRecord::CustomUserSubclass)
119 | end
120 | end
121 |
122 | describe "new_attributes" do
123 |
124 | it "should return a hash of the new values" do
125 | new_attributes = Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).new_attributes
126 | expect(new_attributes).to eq({'a' => 2, 'b' => 4})
127 | end
128 |
129 | end
130 |
131 | describe "old_attributes" do
132 |
133 | it "should return a hash of the old values" do
134 | old_attributes = Audited.audit_class.new(:audited_changes => {:a => [1, 2], :b => [3, 4]}).old_attributes
135 | expect(old_attributes).to eq({'a' => 1, 'b' => 3})
136 | end
137 |
138 | end
139 |
140 | describe "as_user" do
141 |
142 | it "should record user objects" do
143 | Audited.audit_class.as_user(user) do
144 | company = Models::ActiveRecord::Company.create :name => 'The auditors'
145 | company.name = 'The Auditors, Inc'
146 | company.save
147 |
148 | company.audits.each do |audit|
149 | expect(audit.user).to eq(user)
150 | end
151 | end
152 | end
153 |
154 | it "should record usernames" do
155 | Audited.audit_class.as_user(user.name) do
156 | company = Models::ActiveRecord::Company.create :name => 'The auditors'
157 | company.name = 'The Auditors, Inc'
158 | company.save
159 |
160 | company.audits.each do |audit|
161 | expect(audit.username).to eq(user.name)
162 | end
163 | end
164 | end
165 |
166 | it "should be thread safe" do
167 | begin
168 | expect(user.save).to eq(true)
169 |
170 | t1 = Thread.new do
171 | Audited.audit_class.as_user(user) do
172 | sleep 1
173 | expect(Models::ActiveRecord::Company.create(:name => 'The Auditors, Inc').audits.first.user).to eq(user)
174 | end
175 | end
176 |
177 | t2 = Thread.new do
178 | Audited.audit_class.as_user(user.name) do
179 | expect(Models::ActiveRecord::Company.create(:name => 'The Competing Auditors, LLC').audits.first.username).to eq(user.name)
180 | sleep 0.5
181 | end
182 | end
183 |
184 | t1.join
185 | t2.join
186 | rescue ActiveRecord::StatementInvalid
187 | STDERR.puts "Thread safety tests cannot be run with SQLite"
188 | end
189 | end
190 |
191 | it "should return the value from the yield block" do
192 | result = Audited.audit_class.as_user('foo') do
193 | 42
194 | end
195 | expect(result).to eq(42)
196 | end
197 |
198 | it "should reset audited_user when the yield block raises an exception" do
199 | expect {
200 | Audited.audit_class.as_user('foo') do
201 | raise StandardError
202 | end
203 | }.to raise_exception
204 | expect(Thread.current[:audited_user]).to be_nil
205 | end
206 |
207 | end
208 |
209 | end
210 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Audited [](http://travis-ci.org/collectiveidea/audited) [](https://gemnasium.com/collectiveidea/audited)[](https://codeclimate.com/github/collectiveidea/audited)
2 | =======
3 |
4 | **Audited** (previously acts_as_audited) is an ORM extension that logs all changes to your models. Audited also allows you to record who made those changes, save comments and associate models related to the changes.
5 |
6 | Audited currently (4.x) works with Rails 4.2. For Rails 3, use gem version 3.0 or see the [3.0-stable branch](https://github.com/collectiveidea/audited/tree/3.0-stable).
7 |
8 | ## Supported Rubies
9 |
10 | Audited supports and is [tested against](http://travis-ci.org/collectiveidea/audited) the following Ruby versions:
11 |
12 | * 2.0.0
13 | * 2.1.5
14 | * 2.2.0
15 |
16 | Audited may work just fine with a Ruby version not listed above, but we can't guarantee that it will. If you'd like to maintain a Ruby that isn't listed, please let us know with a [pull request](https://github.com/collectiveidea/audited/pulls).
17 |
18 | ## Supported ORMs
19 |
20 | In a previous life, Audited was ActiveRecord-only. Audited will now audit models for the following backends:
21 |
22 | * ActiveRecord
23 | * MongoMapper
24 |
25 | ## Installation
26 |
27 | The installation process depends on what ORM your app is using.
28 |
29 | ### ActiveRecord
30 |
31 | Add the appropriate gem to your Gemfile:
32 |
33 | ```ruby
34 | gem "audited-activerecord", "~> 4.0"
35 | ```
36 |
37 | Then, from your Rails app directory, create the `audits` table:
38 |
39 | ```bash
40 | $ rails generate audited:install
41 | $ rake db:migrate
42 | ```
43 |
44 | #### Upgrading
45 |
46 | If you're already using Audited (or acts_as_audited), your `audits` table may require additional columns. After every upgrade, please run:
47 |
48 | ```bash
49 | $ rails generate audited:upgrade
50 | $ rake db:migrate
51 | ```
52 |
53 | Upgrading will only make changes if changes are needed.
54 |
55 | ### MongoMapper
56 |
57 | ```ruby
58 | gem "audited-mongo_mapper", "~> 4.0"
59 | ```
60 |
61 | ## Usage
62 |
63 | Simply call `audited` on your models:
64 |
65 | ```ruby
66 | class User < ActiveRecord::Base
67 | audited
68 | end
69 | ```
70 |
71 | By default, whenever a user is created, updated or destroyed, a new audit is created.
72 |
73 | ```ruby
74 | user = User.create!(:name => "Steve")
75 | user.audits.count # => 1
76 | user.update_attributes!(:name => "Ryan")
77 | user.audits.count # => 2
78 | user.destroy
79 | user.audits.count # => 3
80 | ```
81 |
82 | Audits contain information regarding what action was taken on the model and what changes were made.
83 |
84 | ```ruby
85 | user.update_attributes!(:name => "Ryan")
86 | audit = user.audits.last
87 | audit.action # => "update"
88 | audit.audited_changes # => {"name"=>["Steve", "Ryan"]}
89 | ```
90 |
91 | ### Specifying columns
92 |
93 | By default, a new audit is created for any attribute changes. You can, however, limit the columns to be considered.
94 |
95 | ```ruby
96 | class User < ActiveRecord::Base
97 | # All fields
98 | # audited
99 |
100 | # Single field
101 | # audited only: :name
102 |
103 | # Multiple fields
104 | # audited only: [:name, :address]
105 |
106 | # All except certain fields
107 | # audited except: :password
108 | end
109 | ```
110 |
111 | ### Specifying callbacks
112 |
113 | By default, a new audit is created for any Create, Update or Destroy action. You can, however, limit the actions audited.
114 |
115 | ```ruby
116 | class User < ActiveRecord::Base
117 | # All fields and actions
118 | # audited
119 |
120 | # Single field, only audit Update and Destroy (not Create)
121 | # audited only: :name, on: [:update, :destroy]
122 | end
123 | ```
124 |
125 | ### Comments
126 |
127 | You can attach comments to each audit using an `audit_comment` attribute on your model.
128 |
129 | ```ruby
130 | user.update_attributes!(:name => "Ryan", :audit_comment => "Changing name, just because")
131 | user.audits.last.comment # => "Changing name, just because"
132 | ```
133 |
134 | You can optionally add the `:comment_required` option to your `audited` call to require comments for all audits.
135 |
136 | ```ruby
137 | class User < ActiveRecord::Base
138 | audited :comment_required => true
139 | end
140 | ```
141 |
142 | ### Current User Tracking
143 |
144 | If you're using Audited in a Rails application, all audited changes made within a request will automatically be attributed to the current user. By default, Audited uses the `current_user` method in your controller.
145 |
146 | ```
147 | class PostsController < ApplicationController
148 | def create
149 | current_user # => #
150 | @post = Post.create(params[:post])
151 | @post.audits.last.user # => #
152 | end
153 | end
154 | ```
155 |
156 | To use a method other than `current_user`, put the following in an initializer:
157 |
158 | ```ruby
159 | Audited.current_user_method = :authenticated_user
160 | ```
161 |
162 | Outside of a request, Audited can still record the user with the `as_user` method:
163 |
164 | ```ruby
165 | Audited.audit_class.as_user(User.find(1)) do
166 | post.update_attribute!(:title => "Hello, world!")
167 | end
168 | post.audits.last.user # => #
169 | ```
170 |
171 | ### Associated Audits
172 |
173 | Sometimes it's useful to associate an audit with a model other than the one being changed. For instance, given the following models:
174 |
175 | ```ruby
176 | class User < ActiveRecord::Base
177 | belongs_to :company
178 | audited
179 | end
180 |
181 | class Company < ActiveRecord::Base
182 | has_many :users
183 | end
184 | ```
185 |
186 | Every change to a user is audited, but what if you want to grab all of the audits of users belonging to a particular company? You can add the `:associated_with` option to your `audited` call:
187 |
188 | ```ruby
189 | class User < ActiveRecord::Base
190 | belongs_to :company
191 | audited :associated_with => :company
192 | end
193 |
194 | class Company < ActiveRecord::Base
195 | has_many :users
196 | has_associated_audits
197 | end
198 | ```
199 |
200 | Now, when a audit is created for a user, that user's company is also saved alongside the audit. This makes it much easier (and faster) to access audits indirectly related to a company.
201 |
202 | ```ruby
203 | company = Company.create!(:name => "Collective Idea")
204 | user = company.users.create!(:name => "Steve")
205 | user.update_attribute!(:name => "Steve Richert")
206 | user.audits.last.associated # => #
207 | company.associated_audits.last.auditable # => #
208 | ```
209 |
210 | ### Disabling auditing
211 |
212 | If you want to disable auditing temporarily doing certain tasks, there are a few
213 | methods available.
214 |
215 | To disable auditing on a save:
216 |
217 | ```ruby
218 | @user.save_without_auditing
219 | ```
220 |
221 | or:
222 |
223 | ```ruby
224 | @user.without_auditing do
225 | @user.save
226 | end
227 | ```
228 |
229 | To disable auditing on a column:
230 |
231 | ```ruby
232 | User.non_audited_columns = [:first_name, :last_name]
233 | ```
234 |
235 | To disable auditing on an entire model:
236 |
237 | ```ruby
238 | User.auditing_enabled = false
239 | ```
240 |
241 | ## Gotchas
242 |
243 | ### Using attr_protected or strong_parameters
244 |
245 | Audited assumes you are using `attr_accessible`. If you're using
246 | `attr_protected` or `strong_parameters`, you'll have to take an extra step or
247 | two.
248 |
249 |
250 | If you're using `strong_parameters` with Rails 3.x, be sure to add `:allow_mass_assignment => true` to your `audited` call; otherwise Audited will
251 | interfere with `strong_parameters` and none of your `save` calls will work.
252 |
253 | ```ruby
254 | class User < ActiveRecord::Base
255 | audited :allow_mass_assignment => true
256 | end
257 | ```
258 |
259 | If using `attr_protected`, add `:allow_mass_assignment => true`, and also be sure to add `audit_ids` to the list of protected attributes to prevent data loss.
260 |
261 | ```ruby
262 | class User < ActiveRecord::Base
263 | audited :allow_mass_assignment => true
264 | attr_protected :logins, :audit_ids
265 | end
266 | ```
267 |
268 | ### MongoMapper Embedded Documents
269 |
270 | Currently, Audited does not track changes on embedded documents. Audited works by tracking a model's [dirty changes](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html) but changes to embedded documents don't appear in dirty tracking.
271 |
272 | ## Support
273 |
274 | You can find documentation at: http://rdoc.info/github/collectiveidea/audited
275 |
276 | Or join the [mailing list](http://groups.google.com/group/audited) to get help or offer suggestions.
277 |
278 | ## Contributing
279 |
280 | In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project. Here are a few ways _you_ can pitch in:
281 |
282 | * Use prerelease versions of Audited.
283 | * [Report bugs](https://github.com/collectiveidea/audited/issues).
284 | * Fix bugs and submit [pull requests](http://github.com/collectiveidea/audited/pulls).
285 | * Write, clarify or fix documentation.
286 | * Refactor code.
287 |
--------------------------------------------------------------------------------
/lib/audited/auditor.rb:
--------------------------------------------------------------------------------
1 | module Audited
2 | # Specify this act if you want changes to your model to be saved in an
3 | # audit table. This assumes there is an audits table ready.
4 | #
5 | # class User < ActiveRecord::Base
6 | # audited
7 | # end
8 | #
9 | # To store an audit comment set model.audit_comment to your comment before
10 | # a create, update or destroy operation.
11 | #
12 | # See Audited::Adapters::ActiveRecord::Auditor::ClassMethods#audited
13 | # for configuration options
14 | module Auditor #:nodoc:
15 | extend ActiveSupport::Concern
16 |
17 | CALLBACKS = [:audit_create, :audit_update, :audit_destroy]
18 |
19 | module ClassMethods
20 | # == Configuration options
21 | #
22 | #
23 | # * +only+ - Only audit the given attributes
24 | # * +except+ - Excludes fields from being saved in the audit log.
25 | # By default, Audited will audit all but these fields:
26 | #
27 | # [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at']
28 | # You can add to those by passing one or an array of fields to skip.
29 | #
30 | # class User < ActiveRecord::Base
31 | # audited :except => :password
32 | # end
33 | #
34 | # * +require_comment+ - Ensures that audit_comment is supplied before
35 | # any create, update or destroy operation.
36 | #
37 | def audited(options = {})
38 | # don't allow multiple calls
39 | return if self.included_modules.include?(Audited::Auditor::AuditedInstanceMethods)
40 |
41 | class_attribute :non_audited_columns, :instance_writer => false
42 | class_attribute :auditing_enabled, :instance_writer => false
43 | class_attribute :audit_associated_with, :instance_writer => false
44 |
45 | if options[:only]
46 | except = self.column_names - options[:only].flatten.map(&:to_s)
47 | else
48 | except = default_ignored_attributes + Audited.ignored_attributes
49 | except |= Array(options[:except]).collect(&:to_s) if options[:except]
50 | end
51 | self.non_audited_columns = except
52 | self.audit_associated_with = options[:associated_with]
53 |
54 | if options[:comment_required]
55 | validates_presence_of :audit_comment, :if => :auditing_enabled
56 | before_destroy :require_comment
57 | end
58 |
59 | attr_accessor :audit_comment
60 |
61 | has_many :audits, :as => :auditable, :class_name => Audited.audit_class.name
62 | Audited.audit_class.audited_class_names << self.to_s
63 |
64 | after_create :audit_create if !options[:on] || (options[:on] && options[:on].include?(:create))
65 | before_update :audit_update if !options[:on] || (options[:on] && options[:on].include?(:update))
66 | before_destroy :audit_destroy if !options[:on] || (options[:on] && options[:on].include?(:destroy))
67 |
68 | # Define and set after_audit and around_audit callbacks. This might be useful if you want
69 | # to notify a party after the audit has been created or if you want to access the newly-created
70 | # audit.
71 | define_callbacks :audit
72 | set_callback :audit, :after, :after_audit, :if => lambda { self.respond_to?(:after_audit) }
73 | set_callback :audit, :around, :around_audit, :if => lambda { self.respond_to?(:around_audit) }
74 |
75 | attr_accessor :version
76 |
77 | extend Audited::Auditor::AuditedClassMethods
78 | include Audited::Auditor::AuditedInstanceMethods
79 |
80 | self.auditing_enabled = true
81 | end
82 |
83 | def has_associated_audits
84 | has_many :associated_audits, :as => :associated, :class_name => Audited.audit_class.name
85 | end
86 | end
87 |
88 | module AuditedInstanceMethods
89 | # Temporarily turns off auditing while saving.
90 | def save_without_auditing
91 | without_auditing { save }
92 | end
93 |
94 | # Executes the block with the auditing callbacks disabled.
95 | #
96 | # @foo.without_auditing do
97 | # @foo.save
98 | # end
99 | #
100 | def without_auditing(&block)
101 | self.class.without_auditing(&block)
102 | end
103 |
104 | # Gets an array of the revisions available
105 | #
106 | # user.revisions.each do |revision|
107 | # user.name
108 | # user.version
109 | # end
110 | #
111 | def revisions(from_version = 1)
112 | audits = self.audits.from_version(from_version)
113 | return [] if audits.empty?
114 | revisions = []
115 | audits.each do |audit|
116 | revisions << audit.revision
117 | end
118 | revisions
119 | end
120 |
121 | # Get a specific revision specified by the version number, or +:previous+
122 | def revision(version)
123 | revision_with Audited.audit_class.reconstruct_attributes(audits_to(version))
124 | end
125 |
126 | # Find the oldest revision recorded prior to the date/time provided.
127 | def revision_at(date_or_time)
128 | audits = self.audits.up_until(date_or_time)
129 | revision_with Audited.audit_class.reconstruct_attributes(audits) unless audits.empty?
130 | end
131 |
132 | # List of attributes that are audited.
133 | def audited_attributes
134 | attributes.except(*non_audited_columns)
135 | end
136 |
137 | protected
138 |
139 | def revision_with(attributes)
140 | self.dup.tap do |revision|
141 | revision.id = id
142 | revision.send :instance_variable_set, '@attributes', self.attributes if rails_below?('4.2.0')
143 | revision.send :instance_variable_set, '@new_record', self.destroyed?
144 | revision.send :instance_variable_set, '@persisted', !self.destroyed?
145 | revision.send :instance_variable_set, '@readonly', false
146 | revision.send :instance_variable_set, '@destroyed', false
147 | revision.send :instance_variable_set, '@_destroyed', false
148 | revision.send :instance_variable_set, '@marked_for_destruction', false
149 | Audited.audit_class.assign_revision_attributes(revision, attributes)
150 |
151 | # Remove any association proxies so that they will be recreated
152 | # and reference the correct object for this revision. The only way
153 | # to determine if an instance variable is a proxy object is to
154 | # see if it responds to certain methods, as it forwards almost
155 | # everything to its target.
156 | for ivar in revision.instance_variables
157 | proxy = revision.instance_variable_get ivar
158 | if !proxy.nil? and proxy.respond_to? :proxy_respond_to?
159 | revision.instance_variable_set ivar, nil
160 | end
161 | end
162 | end
163 | end
164 |
165 | def rails_below?(rails_version)
166 | Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
167 | end
168 |
169 | private
170 |
171 | def audited_changes
172 | changed_attributes.except(*non_audited_columns).inject({}) do |changes,(attr, old_value)|
173 | changes[attr] = [old_value, self[attr]]
174 | changes
175 | end
176 | end
177 |
178 | def audits_to(version = nil)
179 | if version == :previous
180 | version = if self.version
181 | self.version - 1
182 | else
183 | previous = audits.descending.offset(1).first
184 | previous ? previous.version : 1
185 | end
186 | end
187 | audits.to_version(version)
188 | end
189 |
190 | def audit_create
191 | write_audit(:action => 'create', :audited_changes => audited_attributes,
192 | :comment => audit_comment)
193 | end
194 |
195 | def audit_update
196 | unless (changes = audited_changes).empty? && audit_comment.blank?
197 | write_audit(:action => 'update', :audited_changes => changes,
198 | :comment => audit_comment)
199 | end
200 | end
201 |
202 | def audit_destroy
203 | write_audit(:action => 'destroy', :audited_changes => audited_attributes,
204 | :comment => audit_comment) unless self.new_record?
205 | end
206 |
207 | def write_audit(attrs)
208 | attrs[:associated] = self.send(audit_associated_with) unless audit_associated_with.nil?
209 | self.audit_comment = nil
210 | run_callbacks(:audit) { self.audits.create(attrs) } if auditing_enabled
211 | end
212 |
213 | def require_comment
214 | if auditing_enabled && audit_comment.blank?
215 | errors.add(:audit_comment, "Comment required before destruction")
216 | return false
217 | end
218 | end
219 |
220 | CALLBACKS.each do |attr_name|
221 | alias_method "#{attr_name}_callback".to_sym, attr_name
222 | end
223 |
224 | def empty_callback #:nodoc:
225 | end
226 |
227 | end # InstanceMethods
228 |
229 | module AuditedClassMethods
230 | # Returns an array of columns that are audited. See non_audited_columns
231 | def audited_columns
232 | self.columns.select { |c| !non_audited_columns.include?(c.name) }
233 | end
234 |
235 | # Executes the block with auditing disabled.
236 | #
237 | # Foo.without_auditing do
238 | # @foo.save
239 | # end
240 | #
241 | def without_auditing
242 | auditing_was_enabled = auditing_enabled
243 | disable_auditing
244 | yield
245 | ensure
246 | enable_auditing if auditing_was_enabled
247 | end
248 |
249 | def disable_auditing
250 | self.auditing_enabled = false
251 | end
252 |
253 | def enable_auditing
254 | self.auditing_enabled = true
255 | end
256 |
257 | # All audit operations during the block are recorded as being
258 | # made by +user+. This is not model specific, the method is a
259 | # convenience wrapper around
260 | # @see Audit#as_user.
261 | def audit_as( user, &block )
262 | Audited.audit_class.as_user( user, &block )
263 | end
264 | end
265 | end
266 | end
267 |
--------------------------------------------------------------------------------
/spec/audited/adapters/active_record/auditor_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../active_record_spec_helper', __FILE__)
2 |
3 | describe Audited::Auditor, :adapter => :active_record do
4 |
5 | describe "configuration" do
6 | it "should include instance methods" do
7 | expect(Models::ActiveRecord::User.new).to be_a_kind_of( Audited::Auditor::AuditedInstanceMethods)
8 | end
9 |
10 | it "should include class methods" do
11 | expect(Models::ActiveRecord::User).to be_a_kind_of( Audited::Auditor::AuditedClassMethods )
12 | end
13 |
14 | ['created_at', 'updated_at', 'created_on', 'updated_on', 'lock_version', 'id', 'password'].each do |column|
15 | it "should not audit #{column}" do
16 | expect(Models::ActiveRecord::User.non_audited_columns).to include(column)
17 | end
18 | end
19 |
20 | it "should be configurable which attributes are not audited" do
21 | Audited.ignored_attributes = ['delta', 'top_secret', 'created_at']
22 | class Secret < ::ActiveRecord::Base
23 | audited
24 | end
25 |
26 | expect(Secret.non_audited_columns).to include('delta', 'top_secret', 'created_at')
27 | end
28 |
29 | it "should not save non-audited columns" do
30 | expect(create_active_record_user.audits.first.audited_changes.keys.any? { |col| ['created_at', 'updated_at', 'password'].include?( col ) }).to eq(false)
31 | end
32 | end
33 |
34 | describe :new do
35 | it "should allow mass assignment of all unprotected attributes" do
36 | yesterday = 1.day.ago
37 |
38 | u = Models::ActiveRecord::NoAttributeProtectionUser.new(:name => 'name',
39 | :username => 'username',
40 | :password => 'password',
41 | :activated => true,
42 | :suspended_at => yesterday,
43 | :logins => 2)
44 |
45 | expect(u.name).to eq('name')
46 | expect(u.username).to eq('username')
47 | expect(u.password).to eq('password')
48 | expect(u.activated).to eq(true)
49 | expect(u.suspended_at).to eq(yesterday)
50 | expect(u.logins).to eq(2)
51 | end
52 | end
53 |
54 | describe "on create" do
55 | let( :user ) { create_active_record_user :audit_comment => "Create" }
56 |
57 | it "should change the audit count" do
58 | expect {
59 | user
60 | }.to change( Audited.audit_class, :count ).by(1)
61 | end
62 |
63 | it "should create associated audit" do
64 | expect(user.audits.count).to eq(1)
65 | end
66 |
67 | it "should set the action to create" do
68 | expect(user.audits.first.action).to eq('create')
69 | expect(Audited.audit_class.creates.reorder(:id).last).to eq(user.audits.first)
70 | expect(user.audits.creates.count).to eq(1)
71 | expect(user.audits.updates.count).to eq(0)
72 | expect(user.audits.destroys.count).to eq(0)
73 | end
74 |
75 | it "should store all the audited attributes" do
76 | expect(user.audits.first.audited_changes).to eq(user.audited_attributes)
77 | end
78 |
79 | it "should store comment" do
80 | expect(user.audits.first.comment).to eq('Create')
81 | end
82 |
83 | it "should not audit an attribute which is excepted if specified on create or destroy" do
84 | on_create_destroy_except_name = Models::ActiveRecord::OnCreateDestroyExceptName.create(:name => 'Bart')
85 | expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any?{|col| ['name'].include? col}).to eq(false)
86 | end
87 |
88 | it "should not save an audit if only specified on update/destroy" do
89 | expect {
90 | Models::ActiveRecord::OnUpdateDestroy.create!( :name => 'Bart' )
91 | }.to_not change( Audited.audit_class, :count )
92 | end
93 | end
94 |
95 | describe "on update" do
96 | before do
97 | @user = create_active_record_user( :name => 'Brandon', :audit_comment => 'Update' )
98 | end
99 |
100 | it "should save an audit" do
101 | expect {
102 | @user.update_attribute(:name, "Someone")
103 | }.to change( Audited.audit_class, :count ).by(1)
104 | expect {
105 | @user.update_attribute(:name, "Someone else")
106 | }.to change( Audited.audit_class, :count ).by(1)
107 | end
108 |
109 | it "should set the action to 'update'" do
110 | @user.update_attributes :name => 'Changed'
111 | expect(@user.audits.last.action).to eq('update')
112 | expect(Audited.audit_class.updates.reorder(:id).last).to eq(@user.audits.last)
113 | expect(@user.audits.updates.last).to eq(@user.audits.last)
114 | end
115 |
116 | it "should store the changed attributes" do
117 | @user.update_attributes :name => 'Changed'
118 | expect(@user.audits.last.audited_changes).to eq({ 'name' => ['Brandon', 'Changed'] })
119 | end
120 |
121 | it "should store audit comment" do
122 | expect(@user.audits.last.comment).to eq('Update')
123 | end
124 |
125 | it "should not save an audit if only specified on create/destroy" do
126 | on_create_destroy = Models::ActiveRecord::OnCreateDestroy.create( :name => 'Bart' )
127 | expect {
128 | on_create_destroy.update_attributes :name => 'Changed'
129 | }.to_not change( Audited.audit_class, :count )
130 | end
131 |
132 | it "should not save an audit if the value doesn't change after type casting" do
133 | @user.update_attributes! :logins => 0, :activated => true
134 | expect { @user.update_attribute :logins, '0' }.to_not change( Audited.audit_class, :count )
135 | expect { @user.update_attribute :activated, 1 }.to_not change( Audited.audit_class, :count )
136 | expect { @user.update_attribute :activated, '1' }.to_not change( Audited.audit_class, :count )
137 | end
138 |
139 | describe "with no dirty changes" do
140 | it "does not create an audit if the record is not changed" do
141 | expect {
142 | @user.save!
143 | }.to_not change( Audited.audit_class, :count )
144 | end
145 |
146 | it "creates an audit when an audit comment is present" do
147 | expect {
148 | @user.audit_comment = "Comment"
149 | @user.save!
150 | }.to change( Audited.audit_class, :count )
151 | end
152 | end
153 | end
154 |
155 | describe "on destroy" do
156 | before do
157 | @user = create_active_record_user
158 | end
159 |
160 | it "should save an audit" do
161 | expect {
162 | @user.destroy
163 | }.to change( Audited.audit_class, :count )
164 |
165 | expect(@user.audits.size).to eq(2)
166 | end
167 |
168 | it "should set the action to 'destroy'" do
169 | @user.destroy
170 |
171 | expect(@user.audits.last.action).to eq('destroy')
172 | expect(Audited.audit_class.destroys.reorder(:id).last).to eq(@user.audits.last)
173 | expect(@user.audits.destroys.last).to eq(@user.audits.last)
174 | end
175 |
176 | it "should store all of the audited attributes" do
177 | @user.destroy
178 |
179 | expect(@user.audits.last.audited_changes).to eq(@user.audited_attributes)
180 | end
181 |
182 | it "should be able to reconstruct a destroyed record without history" do
183 | @user.audits.delete_all
184 | @user.destroy
185 |
186 | revision = @user.audits.first.revision
187 | expect(revision.name).to eq(@user.name)
188 | end
189 |
190 | it "should not save an audit if only specified on create/update" do
191 | on_create_update = Models::ActiveRecord::OnCreateUpdate.create!( :name => 'Bart' )
192 |
193 | expect {
194 | on_create_update.destroy
195 | }.to_not change( Audited.audit_class, :count )
196 | end
197 |
198 | it "should audit dependent destructions" do
199 | owner = Models::ActiveRecord::Owner.create!
200 | company = owner.companies.create!
201 |
202 | expect {
203 | owner.destroy
204 | }.to change( Audited.audit_class, :count )
205 |
206 | expect(company.audits.map { |a| a.action }).to eq(['create', 'destroy'])
207 | end
208 | end
209 |
210 | describe "on destroy with unsaved object" do
211 | let(:user) { Models::ActiveRecord::User.new }
212 |
213 | it "should not audit on 'destroy'" do
214 | expect {
215 | user.destroy
216 | }.to_not raise_error
217 |
218 | expect( user.audits ).to be_empty
219 | end
220 | end
221 |
222 | describe "associated with" do
223 | let(:owner) { Models::ActiveRecord::Owner.create(:name => 'Models::ActiveRecord::Owner') }
224 | let(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(:name => 'The auditors', :owner => owner) }
225 |
226 | it "should record the associated object on create" do
227 | expect(owned_company.audits.first.associated).to eq(owner)
228 | end
229 |
230 | it "should store the associated object on update" do
231 | owned_company.update_attribute(:name, 'The Auditors')
232 | expect(owned_company.audits.last.associated).to eq(owner)
233 | end
234 |
235 | it "should store the associated object on destroy" do
236 | owned_company.destroy
237 | expect(owned_company.audits.last.associated).to eq(owner)
238 | end
239 | end
240 |
241 | describe "has associated audits" do
242 | let!(:owner) { Models::ActiveRecord::Owner.create!(:name => 'Models::ActiveRecord::Owner') }
243 | let!(:owned_company) { Models::ActiveRecord::OwnedCompany.create!(:name => 'The auditors', :owner => owner) }
244 |
245 | it "should list the associated audits" do
246 | expect(owner.associated_audits.length).to eq(1)
247 | expect(owner.associated_audits.first.auditable).to eq(owned_company)
248 | end
249 | end
250 |
251 | describe "revisions" do
252 | let( :user ) { create_versions }
253 |
254 | it "should return an Array of Users" do
255 | expect(user.revisions).to be_a_kind_of( Array )
256 | user.revisions.each { |version| expect(version).to be_a_kind_of Models::ActiveRecord::User }
257 | end
258 |
259 | it "should have one revision for a new record" do
260 | expect(create_active_record_user.revisions.size).to eq(1)
261 | end
262 |
263 | it "should have one revision for each audit" do
264 | expect(user.audits.size).to eql( user.revisions.size )
265 | end
266 |
267 | it "should set the attributes for each revision" do
268 | u = Models::ActiveRecord::User.create(:name => 'Brandon', :username => 'brandon')
269 | u.update_attributes :name => 'Foobar'
270 | u.update_attributes :name => 'Awesome', :username => 'keepers'
271 |
272 | expect(u.revisions.size).to eql(3)
273 |
274 | expect(u.revisions[0].name).to eql('Brandon')
275 | expect(u.revisions[0].username).to eql('brandon')
276 |
277 | expect(u.revisions[1].name).to eql('Foobar')
278 | expect(u.revisions[1].username).to eql('brandon')
279 |
280 | expect(u.revisions[2].name).to eql('Awesome')
281 | expect(u.revisions[2].username).to eql('keepers')
282 | end
283 |
284 | it "access to only recent revisions" do
285 | u = Models::ActiveRecord::User.create(:name => 'Brandon', :username => 'brandon')
286 | u.update_attributes :name => 'Foobar'
287 | u.update_attributes :name => 'Awesome', :username => 'keepers'
288 |
289 | expect(u.revisions(2).size).to eq(2)
290 |
291 | expect(u.revisions(2)[0].name).to eq('Foobar')
292 | expect(u.revisions(2)[0].username).to eq('brandon')
293 |
294 | expect(u.revisions(2)[1].name).to eq('Awesome')
295 | expect(u.revisions(2)[1].username).to eq('keepers')
296 | end
297 |
298 | it "should be empty if no audits exist" do
299 | user.audits.delete_all
300 | expect(user.revisions).to be_empty
301 | end
302 |
303 | it "should ignore attributes that have been deleted" do
304 | user.audits.last.update_attributes :audited_changes => {:old_attribute => 'old value'}
305 | expect { user.revisions }.to_not raise_error
306 | end
307 | end
308 |
309 | describe "revisions" do
310 | let( :user ) { create_versions(5) }
311 |
312 | it "should maintain identity" do
313 | expect(user.revision(1)).to eq(user)
314 | end
315 |
316 | it "should find the given revision" do
317 | revision = user.revision(3)
318 | expect(revision).to be_a_kind_of( Models::ActiveRecord::User )
319 | expect(revision.version).to eq(3)
320 | expect(revision.name).to eq('Foobar 3')
321 | end
322 |
323 | it "should find the previous revision with :previous" do
324 | revision = user.revision(:previous)
325 | expect(revision.version).to eq(4)
326 | #expect(revision).to eq(user.revision(4))
327 | expect(revision.attributes).to eq(user.revision(4).attributes)
328 | end
329 |
330 | it "should be able to get the previous revision repeatedly" do
331 | previous = user.revision(:previous)
332 | expect(previous.version).to eq(4)
333 | expect(previous.revision(:previous).version).to eq(3)
334 | end
335 |
336 | it "should be able to set protected attributes" do
337 | u = Models::ActiveRecord::User.create(:name => 'Brandon')
338 | u.update_attribute :logins, 1
339 | u.update_attribute :logins, 2
340 |
341 | expect(u.revision(3).logins).to eq(2)
342 | expect(u.revision(2).logins).to eq(1)
343 | expect(u.revision(1).logins).to eq(0)
344 | end
345 |
346 | it "should set attributes directly" do
347 | u = Models::ActiveRecord::User.create(:name => '')
348 | expect(u.revision(1).name).to eq('<Joe>')
349 | end
350 |
351 | it "should set the attributes for each revision" do
352 | u = Models::ActiveRecord::User.create(:name => 'Brandon', :username => 'brandon')
353 | u.update_attributes :name => 'Foobar'
354 | u.update_attributes :name => 'Awesome', :username => 'keepers'
355 |
356 | expect(u.revision(3).name).to eq('Awesome')
357 | expect(u.revision(3).username).to eq('keepers')
358 |
359 | expect(u.revision(2).name).to eq('Foobar')
360 | expect(u.revision(2).username).to eq('brandon')
361 |
362 | expect(u.revision(1).name).to eq('Brandon')
363 | expect(u.revision(1).username).to eq('brandon')
364 | end
365 |
366 | it "should be able to get time for first revision" do
367 | suspended_at = Time.zone.now
368 | u = Models::ActiveRecord::User.create(:suspended_at => suspended_at)
369 | expect(u.revision(1).suspended_at.to_s).to eq(suspended_at.to_s)
370 | end
371 |
372 | it "should not raise an error when no previous audits exist" do
373 | user.audits.destroy_all
374 | expect { user.revision(:previous) }.to_not raise_error
375 | end
376 |
377 | it "should mark revision's attributes as changed" do
378 | expect(user.revision(1).name_changed?).to eq(true)
379 | end
380 |
381 | it "should record new audit when saving revision" do
382 | expect {
383 | user.revision(1).save!
384 | }.to change( user.audits, :count ).by(1)
385 | end
386 |
387 | it "should re-insert destroyed records" do
388 | user.destroy
389 | expect {
390 | user.revision(1).save!
391 | }.to change( Models::ActiveRecord::User, :count ).by(1)
392 | end
393 | end
394 |
395 | describe "revision_at" do
396 | let( :user ) { create_active_record_user }
397 |
398 | it "should find the latest revision before the given time" do
399 | audit = user.audits.first
400 | audit.created_at = 1.hour.ago
401 | audit.save!
402 | user.update_attributes :name => 'updated'
403 | expect(user.revision_at( 2.minutes.ago ).version).to eq(1)
404 | end
405 |
406 | it "should be nil if given a time before audits" do
407 | expect(user.revision_at( 1.week.ago )).to be_nil
408 | end
409 | end
410 |
411 | describe "without auditing" do
412 | it "should not save an audit when calling #save_without_auditing" do
413 | expect {
414 | u = Models::ActiveRecord::User.new(:name => 'Brandon')
415 | expect(u.save_without_auditing).to eq(true)
416 | }.to_not change( Audited.audit_class, :count )
417 | end
418 |
419 | it "should not save an audit inside of the #without_auditing block" do
420 | expect {
421 | Models::ActiveRecord::User.without_auditing { Models::ActiveRecord::User.create!( :name => 'Brandon' ) }
422 | }.to_not change( Audited.audit_class, :count )
423 | end
424 |
425 | it "should reset auditing status even it raises an exception" do
426 | Models::ActiveRecord::User.without_auditing { raise } rescue nil
427 | expect(Models::ActiveRecord::User.auditing_enabled).to eq(true)
428 | end
429 | end
430 |
431 | describe "comment required" do
432 |
433 | describe "on create" do
434 | it "should not validate when audit_comment is not supplied" do
435 | expect(Models::ActiveRecord::CommentRequiredUser.new).not_to be_valid
436 | end
437 |
438 | it "should validate when audit_comment is supplied" do
439 | expect(Models::ActiveRecord::CommentRequiredUser.new( :audit_comment => 'Create')).to be_valid
440 | end
441 |
442 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
443 | Models::ActiveRecord::CommentRequiredUser.disable_auditing
444 | expect(Models::ActiveRecord::CommentRequiredUser.new).to be_valid
445 | Models::ActiveRecord::CommentRequiredUser.enable_auditing
446 | end
447 | end
448 |
449 | describe "on update" do
450 | let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( :audit_comment => 'Create' ) }
451 |
452 | it "should not validate when audit_comment is not supplied" do
453 | expect(user.update_attributes(:name => 'Test')).to eq(false)
454 | end
455 |
456 | it "should validate when audit_comment is supplied" do
457 | expect(user.update_attributes(:name => 'Test', :audit_comment => 'Update')).to eq(true)
458 | end
459 |
460 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
461 | Models::ActiveRecord::CommentRequiredUser.disable_auditing
462 | expect(user.update_attributes(:name => 'Test')).to eq(true)
463 | Models::ActiveRecord::CommentRequiredUser.enable_auditing
464 | end
465 | end
466 |
467 | describe "on destroy" do
468 | let( :user ) { Models::ActiveRecord::CommentRequiredUser.create!( :audit_comment => 'Create' )}
469 |
470 | it "should not validate when audit_comment is not supplied" do
471 | expect(user.destroy).to eq(false)
472 | end
473 |
474 | it "should validate when audit_comment is supplied" do
475 | user.audit_comment = "Destroy"
476 | expect(user.destroy).to eq(user)
477 | end
478 |
479 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
480 | Models::ActiveRecord::CommentRequiredUser.disable_auditing
481 | expect(user.destroy).to eq(user)
482 | Models::ActiveRecord::CommentRequiredUser.enable_auditing
483 | end
484 | end
485 |
486 | end
487 |
488 | describe "attr_protected and attr_accessible" do
489 |
490 | it "should not raise error when attr_accessible is set and protected is false" do
491 | expect {
492 | Models::ActiveRecord::AccessibleAfterDeclarationUser.new(:name => 'No fail!')
493 | }.to_not raise_error
494 | end
495 |
496 | it "should not rause an error when attr_accessible is declared before audited" do
497 | expect {
498 | Models::ActiveRecord::AccessibleAfterDeclarationUser.new(:name => 'No fail!')
499 | }.to_not raise_error
500 | end
501 | end
502 |
503 | describe "audit_as" do
504 | let( :user ) { Models::ActiveRecord::User.create :name => 'Testing' }
505 |
506 | it "should record user objects" do
507 | Models::ActiveRecord::Company.audit_as( user ) do
508 | company = Models::ActiveRecord::Company.create :name => 'The auditors'
509 | company.update_attributes :name => 'The Auditors'
510 |
511 | company.audits.each do |audit|
512 | expect(audit.user).to eq(user)
513 | end
514 | end
515 | end
516 |
517 | it "should record usernames" do
518 | Models::ActiveRecord::Company.audit_as( user.name ) do
519 | company = Models::ActiveRecord::Company.create :name => 'The auditors'
520 | company.update_attributes :name => 'The Auditors'
521 |
522 | company.audits.each do |audit|
523 | expect(audit.user).to eq(user.name)
524 | end
525 | end
526 | end
527 | end
528 |
529 | describe "after_audit" do
530 | let( :user ) { user = Models::ActiveRecord::UserWithAfterAudit.new }
531 |
532 | it "should invoke after_audit callback on create" do
533 | expect(user.bogus_attr).to be_nil
534 | expect(user.save).to eq(true)
535 | expect(user.bogus_attr).to eq("do something")
536 | end
537 | end
538 |
539 | describe "around_audit" do
540 | let( :user ) { user = Models::ActiveRecord::UserWithAfterAudit.new }
541 |
542 | it "should invoke around_audit callback on create" do
543 | expect(user.around_attr).to be_nil
544 | expect(user.save).to eq(true)
545 | expect(user.around_attr).to eq(user.audits.last)
546 | end
547 | end
548 | end
549 |
--------------------------------------------------------------------------------
/spec/audited/adapters/mongo_mapper/auditor_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../mongo_mapper_spec_helper', __FILE__)
2 |
3 | describe Audited::Auditor, :adapter => :mongo_mapper do
4 |
5 | describe "configuration" do
6 | it "should include instance methods" do
7 | expect(Models::MongoMapper::User.new).to be_a_kind_of(Audited::Auditor::AuditedInstanceMethods)
8 | end
9 |
10 | it "should include class methods" do
11 | expect(Models::MongoMapper::User).to be_a_kind_of( Audited::Auditor::AuditedClassMethods )
12 | end
13 |
14 | ['created_at', 'updated_at', 'created_on', 'updated_on', 'lock_version', 'id', '_id', 'password'].each do |column|
15 | it "should not audit #{column}" do
16 | expect(Models::MongoMapper::User.non_audited_columns).to include(column)
17 | end
18 | end
19 |
20 | it "should be configurable which attributes are not audited" do
21 | Audited.ignored_attributes = ['delta', 'top_secret', 'created_at']
22 | class Secret
23 | include MongoMapper::Document
24 | audited
25 | end
26 |
27 | expect(Secret.non_audited_columns).to include('delta', 'top_secret', 'created_at')
28 | end
29 |
30 | it "should not save non-audited columns" do
31 | expect(create_mongo_user.audits.first.audited_changes.keys.any? { |col| ['created_at', 'updated_at', 'password'].include?( col ) }).to eq(false)
32 | end
33 | end
34 |
35 | describe :new do
36 | it "should allow mass assignment of all unprotected attributes" do
37 | yesterday = 1.day.ago.utc
38 |
39 | u = Models::MongoMapper::NoAttributeProtectionUser.new(:name => 'name',
40 | :username => 'username',
41 | :password => 'password',
42 | :activated => true,
43 | :suspended_at => yesterday,
44 | :logins => 2)
45 |
46 | expect(u.name).to eq('name')
47 | expect(u.username).to eq('username')
48 | expect(u.password).to eq('password')
49 | expect(u.activated).to eq(true)
50 | expect(u.suspended_at.to_i).to eq(yesterday.to_i)
51 | expect(u.logins).to eq(2)
52 | end
53 | end
54 |
55 | describe "on create" do
56 | let( :user ) { create_mongo_user :audit_comment => "Create" }
57 |
58 | it "should change the audit count" do
59 | expect {
60 | user
61 | }.to change( Audited.audit_class, :count ).by(1)
62 | end
63 |
64 | it "should create associated audit" do
65 | expect(user.audits.count).to eq(1)
66 | end
67 |
68 | it "should set the action to create" do
69 | expect(user.audits.first.action).to eq('create')
70 | expect(Audited.audit_class.creates.sort(:id.asc).last).to eq(user.audits.first)
71 | expect(user.audits.creates.count).to eq(1)
72 | expect(user.audits.updates.count).to eq(0)
73 | expect(user.audits.destroys.count).to eq(0)
74 | end
75 |
76 | it "should store all the audited attributes" do
77 | expect(user.audits.first.audited_changes).to eq(user.audited_attributes)
78 | end
79 |
80 | it "should store comment" do
81 | expect(user.audits.first.comment).to eq('Create')
82 | end
83 |
84 | it "should not audit an attribute which is excepted if specified on create or destroy" do
85 | on_create_destroy_except_name = Models::MongoMapper::OnCreateDestroyExceptName.create(:name => 'Bart')
86 | expect(on_create_destroy_except_name.audits.first.audited_changes.keys.any?{|col| ['name'].include? col}).to eq(false)
87 | end
88 |
89 | it "should not save an audit if only specified on update/destroy" do
90 | expect {
91 | Models::MongoMapper::OnUpdateDestroy.create!( :name => 'Bart' )
92 | }.to_not change( Audited.audit_class, :count )
93 | end
94 | end
95 |
96 | describe "on update" do
97 | before do
98 | @user = create_mongo_user( :name => 'Brandon', :audit_comment => 'Update' )
99 | end
100 |
101 | it "should save an audit" do
102 | expect {
103 | @user.update_attribute(:name, "Someone")
104 | }.to change( Audited.audit_class, :count ).by(1)
105 | expect {
106 | @user.update_attribute(:name, "Someone else")
107 | }.to change( Audited.audit_class, :count ).by(1)
108 | end
109 |
110 | it "should set the action to 'update'" do
111 | @user.update_attributes :name => 'Changed'
112 | expect(@user.audits.all.last.action).to eq('update')
113 | expect(Audited.audit_class.updates.sort(:id.asc).last).to eq(@user.audits.all.last)
114 | expect(@user.audits.updates.last).to eq(@user.audits.all.last)
115 | end
116 |
117 | it "should store the changed attributes" do
118 | now = Time.at(Time.now.to_i).utc
119 | @user.update_attributes :name => 'Changed', :suspended_at => now
120 | expect(@user.audits.all.last.audited_changes).to eq({ 'name' => ['Brandon', 'Changed'], 'suspended_at' => [nil, now] })
121 | end
122 |
123 | it "should store audit comment" do
124 | expect(@user.audits.all.last.comment).to eq('Update')
125 | end
126 |
127 | it "should not save an audit if only specified on create/destroy" do
128 | on_create_destroy = Models::MongoMapper::OnCreateDestroy.create( :name => 'Bart' )
129 | expect {
130 | on_create_destroy.update_attributes :name => 'Changed'
131 | }.to_not change( Audited.audit_class, :count )
132 | end
133 |
134 | it "should not save an audit if the value doesn't change after type casting" do
135 | @user.update_attributes! :logins => 0, :activated => true
136 | expect { @user.update_attribute :logins, '0' }.to_not change( Audited.audit_class, :count )
137 | expect { @user.update_attribute :activated, 1 }.to_not change( Audited.audit_class, :count )
138 | expect { @user.update_attribute :activated, '1' }.to_not change( Audited.audit_class, :count )
139 | end
140 |
141 | it "saves audits of rich objects" do
142 | user = Models::MongoMapper::RichObjectUser.create!(:name => 'Bart Simpson')
143 |
144 | expect {
145 | user.update_attribute(:name, 'O.J. Simpson')
146 | }.to_not raise_error
147 |
148 | change = user.audits.all.last.audited_changes['name']
149 | expect(change).to be_all{|c| c.is_a?(String) }
150 | expect(change[0]).to eq('Bart Simpson')
151 | expect(change[1]).to eq('O.J. Simpson')
152 | end
153 |
154 | describe "with no dirty changes" do
155 | it "does not create an audit if the record is not changed" do
156 | expect {
157 | @user.save!
158 | }.to_not change( Audited.audit_class, :count )
159 | end
160 |
161 | it "creates an audit when an audit comment is present" do
162 | expect {
163 | @user.audit_comment = "Comment"
164 | @user.save!
165 | }.to change( Audited.audit_class, :count )
166 | end
167 | end
168 | end
169 |
170 | describe "on destroy" do
171 | before do
172 | @user = create_mongo_user
173 | end
174 |
175 | it "should save an audit" do
176 | expect {
177 | @user.destroy
178 | }.to change( Audited.audit_class, :count )
179 |
180 | expect(@user.audits.size).to eq(2)
181 | end
182 |
183 | it "should set the action to 'destroy'" do
184 | @user.destroy
185 |
186 | expect(@user.audits.all.last.action).to eq('destroy')
187 | expect(Audited.audit_class.destroys.sort(:id.asc).last).to eq(@user.audits.all.last)
188 | expect(@user.audits.destroys.last).to eq(@user.audits.all.last)
189 | end
190 |
191 | it "should store all of the audited attributes" do
192 | @user.destroy
193 |
194 | expect(@user.audits.all.last.audited_changes).to eq(@user.audited_attributes)
195 | end
196 |
197 | it "should be able to reconstruct a destroyed record without history" do
198 | @user.audits.delete_all
199 | @user.destroy
200 |
201 | revision = @user.audits.first.revision
202 | expect(revision.name).to eq(@user.name)
203 | end
204 |
205 | it "should not save an audit if only specified on create/update" do
206 | on_create_update = Models::MongoMapper::OnCreateUpdate.create!( :name => 'Bart' )
207 |
208 | expect {
209 | on_create_update.destroy
210 | }.to_not change( Audited.audit_class, :count )
211 | end
212 | end
213 |
214 | describe "on destroy with unsaved object" do
215 | let(:user) { Models::MongoMapper::User.new }
216 |
217 | it "should not audit on 'destroy'" do
218 | expect {
219 | user.destroy
220 | }.to_not raise_error
221 |
222 | expect( user.audits ).to be_empty
223 | end
224 | end
225 |
226 | describe "associated with" do
227 | let(:owner) { Models::MongoMapper::Owner.create(:name => 'Models::MongoMapper::Owner') }
228 | let(:owned_company) { Models::MongoMapper::OwnedCompany.create!(:name => 'The auditors', :owner => owner) }
229 |
230 | it "should record the associated object on create" do
231 | expect(owned_company.audits.first.associated).to eq(owner)
232 | end
233 |
234 | it "should store the associated object on update" do
235 | owned_company.update_attribute(:name, 'The Auditors')
236 | expect(owned_company.audits.all.last.associated).to eq(owner)
237 | end
238 |
239 | it "should store the associated object on destroy" do
240 | owned_company.destroy
241 | expect(owned_company.audits.all.last.associated).to eq(owner)
242 | end
243 | end
244 |
245 | describe "has associated audits" do
246 | let!(:owner) { Models::MongoMapper::Owner.create!(:name => 'Models::MongoMapper::Owner') }
247 | let!(:owned_company) { Models::MongoMapper::OwnedCompany.create!(:name => 'The auditors', :owner => owner) }
248 |
249 | it "should list the associated audits" do
250 | expect(owner.associated_audits.length).to eq(1)
251 | expect(owner.associated_audits.first.auditable).to eq(owned_company)
252 | end
253 | end
254 |
255 | describe "revisions" do
256 | let( :user ) { create_mongo_versions }
257 |
258 | it "should return an Array of Users" do
259 | expect(user.revisions).to be_a_kind_of( Array )
260 | user.revisions.each { |version| expect(version).to be_a_kind_of(Models::MongoMapper::User) }
261 | end
262 |
263 | it "should have one revision for a new record" do
264 | expect(create_mongo_user.revisions.size).to eq(1)
265 | end
266 |
267 | it "should have one revision for each audit" do
268 | expect(user.audits.size).to eql( user.revisions.size )
269 | end
270 |
271 | it "should set the attributes for each revision" do
272 | u = Models::MongoMapper::User.create(:name => 'Brandon', :username => 'brandon')
273 | u.update_attributes :name => 'Foobar'
274 | u.update_attributes :name => 'Awesome', :username => 'keepers'
275 |
276 | expect(u.revisions.size).to eq(3)
277 |
278 | expect(u.revisions[0].name).to eq('Brandon')
279 | expect(u.revisions[0].username).to eq('brandon')
280 |
281 | expect(u.revisions[1].name).to eq('Foobar')
282 | expect(u.revisions[1].username).to eq('brandon')
283 |
284 | expect(u.revisions[2].name).to eq('Awesome')
285 | expect(u.revisions[2].username).to eq('keepers')
286 | end
287 |
288 | it "access to only recent revisions" do
289 | u = Models::MongoMapper::User.create(:name => 'Brandon', :username => 'brandon')
290 | u.update_attributes :name => 'Foobar'
291 | u.update_attributes :name => 'Awesome', :username => 'keepers'
292 |
293 | expect(u.revisions(2).size).to eq(2)
294 |
295 | expect(u.revisions(2)[0].name).to eq('Foobar')
296 | expect(u.revisions(2)[0].username).to eq('brandon')
297 |
298 | expect(u.revisions(2)[1].name).to eq('Awesome')
299 | expect(u.revisions(2)[1].username).to eq('keepers')
300 | end
301 |
302 | it "should be empty if no audits exist" do
303 | user.audits.delete_all
304 | expect(user.revisions).to be_empty
305 | end
306 |
307 | it "should ignore attributes that have been deleted" do
308 | user.audits.all.last.update_attributes :audited_changes => {:old_attribute => 'old value'}
309 | expect { user.revisions }.to_not raise_error
310 | end
311 | end
312 |
313 | describe "revisions" do
314 | let( :user ) { create_mongo_versions(5) }
315 |
316 | it "should maintain identity" do
317 | expect(user.revision(1)).to eq(user)
318 | end
319 |
320 | it "should find the given revision" do
321 | revision = user.revision(3)
322 | expect(revision).to be_a_kind_of( Models::MongoMapper::User )
323 | expect(revision.version).to eq(3)
324 | expect(revision.name).to eq('Foobar 3')
325 | end
326 |
327 | it "should find the previous revision with :previous" do
328 | revision = user.revision(:previous)
329 | expect(revision.version).to eq(4)
330 | #expect(revision).to eq(user.revision(4))
331 | expect(revision.attributes).to eq(user.revision(4).attributes)
332 | end
333 |
334 | it "should be able to get the previous revision repeatedly" do
335 | previous = user.revision(:previous)
336 | expect(previous.version).to eq(4)
337 | expect(previous.revision(:previous).version).to eq(3)
338 | end
339 |
340 | it "should be able to set protected attributes" do
341 | u = Models::MongoMapper::User.create(:name => 'Brandon')
342 | u.update_attribute :logins, 1
343 | u.update_attribute :logins, 2
344 |
345 | expect(u.revision(3).logins).to eq(2)
346 | expect(u.revision(2).logins).to eq(1)
347 | expect(u.revision(1).logins).to eq(0)
348 | end
349 |
350 | it "should set attributes directly" do
351 | u = Models::MongoMapper::User.create(:name => '')
352 | expect(u.revision(1).name).to eq('<Joe>')
353 | end
354 |
355 | it "should set the attributes for each revision" do
356 | u = Models::MongoMapper::User.create(:name => 'Brandon', :username => 'brandon')
357 | u.update_attributes :name => 'Foobar'
358 | u.update_attributes :name => 'Awesome', :username => 'keepers'
359 |
360 | expect(u.revision(3).name).to eq('Awesome')
361 | expect(u.revision(3).username).to eq('keepers')
362 |
363 | expect(u.revision(2).name).to eq('Foobar')
364 | expect(u.revision(2).username).to eq('brandon')
365 |
366 | expect(u.revision(1).name).to eq('Brandon')
367 | expect(u.revision(1).username).to eq('brandon')
368 | end
369 |
370 | it "should be able to get time for first revision" do
371 | suspended_at = Time.now.utc
372 | u = Models::MongoMapper::User.create(:suspended_at => suspended_at)
373 | expect(u.revision(1).suspended_at.to_i).to eq(suspended_at.to_i)
374 | end
375 |
376 | it "should not raise an error when no previous audits exist" do
377 | user.audits.destroy_all
378 | expect { user.revision(:previous) }.to_not raise_error
379 | end
380 |
381 | it "should mark revision's attributes as changed" do
382 | expect(user.revision(1).name_changed?).to eq(true)
383 | end
384 |
385 | it "should record new audit when saving revision" do
386 | user.destroy
387 | expect {
388 | user.revision(1).save!
389 | }.to change( user.audits, :count ).by(1)
390 | end
391 |
392 | it "should re-insert destroyed records" do
393 | user.destroy
394 | expect {
395 | user.revision(1).save!
396 | }.to change( Models::MongoMapper::User, :count ).by(1)
397 | end
398 | end
399 |
400 | describe "revision_at" do
401 | let( :user ) { create_mongo_user }
402 |
403 | it "should find the latest revision before the given time" do
404 | audit = user.audits.first
405 | audit.created_at = 1.hour.ago
406 | audit.save!
407 | user.update_attributes :name => 'updated'
408 | expect(user.revision_at( 2.minutes.ago ).version).to eq(1)
409 | end
410 |
411 | it "should be nil if given a time before audits" do
412 | expect(user.revision_at( 1.week.ago )).to be_nil
413 | end
414 | end
415 |
416 | describe "without auditing" do
417 | it "should not save an audit when calling #save_without_auditing" do
418 | expect {
419 | u = Models::MongoMapper::User.new(:name => 'Brandon')
420 | expect(u.save_without_auditing).to eq(true)
421 | }.to_not change( Audited.audit_class, :count )
422 | end
423 |
424 | it "should not save an audit inside of the #without_auditing block" do
425 | expect {
426 | Models::MongoMapper::User.without_auditing { Models::MongoMapper::User.create!( :name => 'Brandon' ) }
427 | }.to_not change( Audited.audit_class, :count )
428 | end
429 |
430 | it "should reset auditing status even it raises an exception" do
431 | Models::MongoMapper::User.without_auditing { raise } rescue nil
432 | expect(Models::MongoMapper::User.auditing_enabled).to eq(true)
433 | end
434 | end
435 |
436 | describe "comment required" do
437 |
438 | describe "on create" do
439 | it "should not validate when audit_comment is not supplied" do
440 | expect(Models::MongoMapper::CommentRequiredUser.new).not_to be_valid
441 | end
442 |
443 | it "should validate when audit_comment is supplied" do
444 | expect(Models::MongoMapper::CommentRequiredUser.new( :audit_comment => 'Create')).to be_valid
445 | end
446 |
447 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
448 | Models::MongoMapper::CommentRequiredUser.disable_auditing
449 | expect(Models::MongoMapper::CommentRequiredUser.new).to be_valid
450 | Models::MongoMapper::CommentRequiredUser.enable_auditing
451 | end
452 | end
453 |
454 | describe "on update" do
455 | let( :user ) { Models::MongoMapper::CommentRequiredUser.create!( :audit_comment => 'Create' ) }
456 |
457 | it "should not validate when audit_comment is not supplied" do
458 | expect(user.update_attributes(:name => 'Test')).to eq(false)
459 | end
460 |
461 | it "should validate when audit_comment is supplied" do
462 | expect(user.update_attributes(:name => 'Test', :audit_comment => 'Update')).to eq(true)
463 | end
464 |
465 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
466 | Models::MongoMapper::CommentRequiredUser.disable_auditing
467 | expect(user.update_attributes(:name => 'Test')).to eq(true)
468 | Models::MongoMapper::CommentRequiredUser.enable_auditing
469 | end
470 | end
471 |
472 | describe "on destroy" do
473 | let( :user ) { Models::MongoMapper::CommentRequiredUser.create!( :audit_comment => 'Create' )}
474 |
475 | it "should not validate when audit_comment is not supplied" do
476 | expect(user.destroy).to eq(false)
477 | end
478 |
479 | it "should validate when audit_comment is supplied" do
480 | user.audit_comment = "Destroy"
481 | user.destroy
482 | expect(user).to be_destroyed
483 | end
484 |
485 | it "should validate when audit_comment is not supplied, and auditing is disabled" do
486 | Models::MongoMapper::CommentRequiredUser.disable_auditing
487 | user.destroy
488 | expect(user).to be_destroyed
489 | Models::MongoMapper::CommentRequiredUser.enable_auditing
490 | end
491 | end
492 |
493 | end
494 |
495 | describe "attr_protected and attr_accessible" do
496 |
497 | it "should not raise error when attr_accessible is set and protected is false" do
498 | expect {
499 | Models::MongoMapper::AccessibleAfterDeclarationUser.new(:name => 'No fail!')
500 | }.to_not raise_error
501 | end
502 |
503 | it "should not rause an error when attr_accessible is declared before audited" do
504 | expect {
505 | Models::MongoMapper::AccessibleAfterDeclarationUser.new(:name => 'No fail!')
506 | }.to_not raise_error
507 | end
508 | end
509 |
510 | describe "audit_as" do
511 | let( :user ) { Models::MongoMapper::User.create :name => 'Testing' }
512 |
513 | it "should record user objects" do
514 | Models::MongoMapper::Company.audit_as( user ) do
515 | company = Models::MongoMapper::Company.create :name => 'The auditors'
516 | company.update_attributes :name => 'The Auditors'
517 |
518 | company.audits.each do |audit|
519 | expect(audit.user).to eq(user)
520 | end
521 | end
522 | end
523 |
524 | it "should record usernames" do
525 | Models::MongoMapper::Company.audit_as( user.name ) do
526 | company = Models::MongoMapper::Company.create :name => 'The auditors'
527 | company.update_attributes :name => 'The Auditors'
528 |
529 | company.audits.each do |audit|
530 | expect(audit.user).to eq(user.name)
531 | end
532 | end
533 | end
534 | end
535 |
536 | describe "after_audit" do
537 | let( :user ) { user = Models::MongoMapper::UserWithAfterAudit.new }
538 |
539 | it "should invoke after_audit callback on create" do
540 | expect(user.bogus_attr).to be_nil
541 | expect(user.save).to eq(true)
542 | expect(user.bogus_attr).to eq("do something")
543 | end
544 | end
545 |
546 | describe "around_audit" do
547 | let( :user ) { user = Models::MongoMapper::UserWithAfterAudit.new }
548 |
549 | it "should invoke around_audit callback on create" do
550 | expect(user.around_attr).to be_nil
551 | expect(user.save).to eq(true)
552 | expect(user.around_attr).to eq(user.audits.last)
553 | end
554 | end
555 | end
556 |
--------------------------------------------------------------------------------