├── 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 [![Build Status](https://secure.travis-ci.org/collectiveidea/audited.png)](http://travis-ci.org/collectiveidea/audited) [![Dependency Status](https://gemnasium.com/collectiveidea/audited.png)](https://gemnasium.com/collectiveidea/audited)[![Code Climate](https://codeclimate.com/github/collectiveidea/audited.png)](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 | --------------------------------------------------------------------------------