acts_as_audited 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ acts_as_audited_plugin.sqlite3.db
2
+ test/debug.log
3
+ coverage/
4
+ pkg
data/CHANGELOG ADDED
@@ -0,0 +1,25 @@
1
+ acts_as_audited ChangeLog
2
+ -------------------------------------------------------------------------------
3
+ * 2009-01-27 - Store old and new values for updates, and store all attributes on destroy.
4
+ Refactored revisioning methods to work as expected
5
+ * 2008-10-10 - changed to make it work in development mode
6
+ * 2008-04-19 - refactored to make compatible with dirty tracking in edge rails
7
+ and to stop storing both old and new values in a single audit
8
+ * 2008-04-18 - Fix NoMethodError when trying to access the :previous revision
9
+ on a model that doesn't have previous revisions [Alex Soto]
10
+ * 2008-03-21 - added #changed_attributes to get access to the changes before a
11
+ save [Chris Parker]
12
+ * 2007-12-16 - Added #revision_at for retrieving a revision from a specific
13
+ time [Jacob Atzen]
14
+ * 2007-12-16 - Fix error when getting revision from audit with no changes
15
+ [Geoffrey Wiseman]
16
+ * 2007-12-16 - Remove dependency on acts_as_list
17
+ * 2007-06-17 - Added support getting previous revisions
18
+ * 2006-11-17 - Replaced use of singleton User.current_user with cache sweeper
19
+ implementation for auditing the user that made the change
20
+ * 2006-11-17 - added migration generator
21
+ * 2006-08-14 - incorporated changes from Michael Schuerig to write_attribute
22
+ that saves the new value after every change and not just the
23
+ first, and performs proper type-casting before doing comparisons
24
+ * 2006-08-14 - The "changes" are now saved as a serialized hash
25
+ * 2006-07-21 - initial version
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright © 2008 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.
data/README ADDED
@@ -0,0 +1,84 @@
1
+ = acts_as_audited
2
+
3
+ acts_as_audited is an ActiveRecord extension that logs all changes to your models in an audits table.
4
+
5
+ The purpose of this fork is to store both the previous values and the changed value, making each audit record selfcontained.
6
+
7
+ == Installation
8
+
9
+ * acts_as_audited can be installed as a gem:
10
+
11
+ # config/environment.rb
12
+ config.gem 'collectiveidea-acts_as_audited', :lib => false,
13
+ :source => 'http://gems.github.com'
14
+
15
+ or a plugin:
16
+
17
+ script/plugin install git://github.com/collectiveidea/acts_as_audited.git
18
+
19
+ * Generate the migration
20
+ script/generate audited_migration add_audits_table
21
+ rake db:migrate
22
+
23
+ == Usage
24
+
25
+ If you're using acts_as_audited within Rails, you can simply declare which models should be audited. acts_as_audited can also automatically record the user that made the change if your controller has a <tt>current_user</tt> method.
26
+
27
+ class ApplicationController < ActionController::Base
28
+ audit User, List, Item => {:except => :password}
29
+ protected
30
+ def current_user
31
+ @user ||= User.find(session[:user])
32
+ end
33
+ end
34
+
35
+ To get auditing outside of Rails you can explicitly declare <tt>acts_as_audited</tt> on your models:
36
+
37
+ class User < ActiveRecord::Base
38
+ acts_as_audited :except => [:password, :mistress]
39
+ end
40
+
41
+ To record a user in the audits when the sweepers are not available, you can use <tt>as_user</tt>:
42
+
43
+ Audit.as_user( user ) do
44
+ # Perform changes on audited models
45
+ end
46
+
47
+ See http://opensoul.org/2006/07/21/acts_as_audited for more information.
48
+
49
+ == Caveats
50
+
51
+ If your model declares +attr_accessible+ after +acts_as_audited+, you need to set +:protect+ to false. acts_as_audited uses +attr_protected+ internally to prevent malicious users from unassociating your audits, and Rails does not allow both +attr_protected+ and +attr_accessible+. It will default to false if +attr_accessible+ is called before +acts_as_audited+, but needs to be explicitly set if it is called after.
52
+
53
+ class User < ActiveRecord::Base
54
+ acts_as_audited :protect => false
55
+ attr_accessible :name
56
+ end
57
+
58
+ === ActiveScaffold
59
+
60
+ Many users have also reported problems with acts_as_audited and ActiveScaffold, which appears to be caused by a limitation in ActiveScaffold not supporting polymorphic associations. To get it to work with ActiveScaffold:
61
+
62
+ class ApplicationController < ActionController::Base
63
+ audit MyModel, :only => [:create, :update, :destroy]
64
+ end
65
+
66
+ == Compatability
67
+
68
+ acts_as_audited works with Rails 2.1 or later.
69
+
70
+ == Contributing
71
+
72
+ Contributions are always welcome. Checkout the latest code on GitHub:
73
+ http://github.com/collectiveidea/acts_as_audited
74
+
75
+ Please include tests with your patches. There are a few gems required to run the tests:
76
+ $ gem install multi_rails
77
+ $ gem install thoughtbot-shoulda jnunemaker-matchy --source http://gems.github.com
78
+
79
+ Make sure the tests pass against all versions of Rails since 2.1:
80
+
81
+ $ rake test:multi_rails:all
82
+
83
+ Please report bugs or feature suggestions on GitHub:
84
+ http://github.com/collectiveidea/acts_as_audited/issues
data/Rakefile ADDED
@@ -0,0 +1,57 @@
1
+ require 'rake'
2
+ require 'load_multi_rails_rake_tasks'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+
6
+ desc 'Default: run tests.'
7
+ task :default => :test
8
+
9
+ begin
10
+ require 'jeweler'
11
+ Jeweler::Tasks.new do |gem|
12
+ gem.name = "acts_as_audited"
13
+ gem.summary = %Q{ActiveRecord extension that logs all changes to your models in an audits table}
14
+ gem.email = "brandon@opensoul.org"
15
+ gem.homepage = "http://github.com/collectiveidea/acts_as_audited"
16
+ gem.authors = ["Brandon Keepers"]
17
+ gem.add_dependency 'activerecord', '>=2.1'
18
+ gem.add_development_dependency "thoughtbot-shoulda"
19
+ gem.add_development_dependency "jnunemaker-matchy"
20
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
21
+ end
22
+ # Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
25
+ end
26
+
27
+ desc 'Test the acts_as_audited plugin'
28
+ Rake::TestTask.new(:test) do |t|
29
+ t.libs << 'lib'
30
+ t.pattern = 'test/**/*_test.rb'
31
+ t.verbose = true
32
+ end
33
+
34
+ task :test => :check_dependencies
35
+
36
+ begin
37
+ require 'rcov/rcovtask'
38
+ Rcov::RcovTask.new do |test|
39
+ test.libs << 'test'
40
+ test.pattern = 'test/**/*_test.rb'
41
+ test.verbose = true
42
+ test.rcov_opts = %w(--exclude test,/usr/lib/ruby,/Library/Ruby,$HOME/.gem --sort coverage)
43
+ end
44
+ rescue LoadError
45
+ task :rcov do
46
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
47
+ end
48
+ end
49
+
50
+ desc 'Generate documentation for the acts_as_audited plugin.'
51
+ Rake::RDocTask.new(:rdoc) do |rdoc|
52
+ rdoc.rdoc_dir = 'doc'
53
+ rdoc.title = 'acts_as_audited'
54
+ rdoc.options << '--line-numbers' << '--inline-source'
55
+ rdoc.rdoc_files.include('README')
56
+ rdoc.rdoc_files.include('lib/**/*.rb')
57
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.1
@@ -0,0 +1,72 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{acts_as_audited}
8
+ s.version = "1.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brandon Keepers"]
12
+ s.date = %q{2009-09-28}
13
+ s.email = %q{brandon@opensoul.org}
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "CHANGELOG",
21
+ "LICENSE",
22
+ "README",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "acts_as_audited.gemspec",
26
+ "generators/audited_migration/USAGE",
27
+ "generators/audited_migration/audited_migration_generator.rb",
28
+ "generators/audited_migration/templates/migration.rb",
29
+ "init.rb",
30
+ "lib/acts_as_audited.rb",
31
+ "lib/acts_as_audited/audit.rb",
32
+ "lib/acts_as_audited/audit_sweeper.rb",
33
+ "rails/init.rb",
34
+ "test/acts_as_audited_test.rb",
35
+ "test/audit_sweeper_test.rb",
36
+ "test/audit_test.rb",
37
+ "test/db/database.yml",
38
+ "test/db/schema.rb",
39
+ "test/test_helper.rb"
40
+ ]
41
+ s.homepage = %q{http://github.com/collectiveidea/acts_as_audited}
42
+ s.rdoc_options = ["--charset=UTF-8"]
43
+ s.require_paths = ["lib"]
44
+ s.rubygems_version = %q{1.3.5}
45
+ s.summary = %q{ActiveRecord extension that logs all changes to your models in an audits table}
46
+ s.test_files = [
47
+ "test/acts_as_audited_test.rb",
48
+ "test/audit_sweeper_test.rb",
49
+ "test/audit_test.rb",
50
+ "test/db/schema.rb",
51
+ "test/test_helper.rb"
52
+ ]
53
+
54
+ if s.respond_to? :specification_version then
55
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
56
+ s.specification_version = 3
57
+
58
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
59
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.1"])
60
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
61
+ s.add_development_dependency(%q<jnunemaker-matchy>, [">= 0"])
62
+ else
63
+ s.add_dependency(%q<activerecord>, [">= 2.1"])
64
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
65
+ s.add_dependency(%q<jnunemaker-matchy>, [">= 0"])
66
+ end
67
+ else
68
+ s.add_dependency(%q<activerecord>, [">= 2.1"])
69
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
70
+ s.add_dependency(%q<jnunemaker-matchy>, [">= 0"])
71
+ end
72
+ end
@@ -0,0 +1,7 @@
1
+ Description:
2
+ The audited migration generator creates a migration to add the audits table.
3
+
4
+ Example:
5
+ ./script/generate audited_migration add_audits_table
6
+
7
+ This will create a migration in db/migrate/. Run "rake db:migrate" to update your database.
@@ -0,0 +1,7 @@
1
+ class AuditedMigrationGenerator < Rails::Generator::NamedBase
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ class <%= 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 :user_id, :integer
7
+ t.column :user_type, :string
8
+ t.column :username, :string
9
+ t.column :action, :string
10
+ t.column :changes, :text
11
+ t.column :version, :integer, :default => 0
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
+ def self.down
21
+ drop_table :audits
22
+ end
23
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), 'rails', 'init')
@@ -0,0 +1,122 @@
1
+ require 'set'
2
+
3
+ # Audit saves the changes to ActiveRecord models. It has the following attributes:
4
+ #
5
+ # * <tt>auditable</tt>: the ActiveRecord model that was changed
6
+ # * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model
7
+ # * <tt>action</tt>: one of create, update, or delete
8
+ # * <tt>changes</tt>: a serialized hash of all the changes
9
+ # * <tt>created_at</tt>: Time that the change was performed
10
+ #
11
+ class Audit < ActiveRecord::Base
12
+ belongs_to :auditable, :polymorphic => true
13
+ belongs_to :user, :polymorphic => true
14
+
15
+ before_create :set_version_number, :set_audit_user
16
+
17
+ serialize :changes
18
+
19
+ cattr_accessor :audited_class_names
20
+ self.audited_class_names = Set.new
21
+
22
+ def self.audited_classes
23
+ self.audited_class_names.map(&:constantize)
24
+ end
25
+
26
+ cattr_accessor :audit_as_user
27
+ self.audit_as_user = nil
28
+
29
+ # All audits made during the block called will be recorded as made
30
+ # by +user+. This method is hopefully threadsafe, making it ideal
31
+ # for background operations that require audit information.
32
+ def self.as_user(user, &block)
33
+ Thread.current[:acts_as_audited_user] = user
34
+
35
+ yield
36
+
37
+ Thread.current[:acts_as_audited_user] = nil
38
+ end
39
+
40
+ # Allows user to be set to either a string or an ActiveRecord object
41
+ def user_as_string=(user) #:nodoc:
42
+ # reset both either way
43
+ self.user_as_model = self.username = nil
44
+ user.is_a?(ActiveRecord::Base) ?
45
+ self.user_as_model = user :
46
+ self.username = user
47
+ end
48
+ alias_method :user_as_model=, :user=
49
+ alias_method :user=, :user_as_string=
50
+
51
+ def user_as_string #:nodoc:
52
+ self.user_as_model || self.username
53
+ end
54
+ alias_method :user_as_model, :user
55
+ alias_method :user, :user_as_string
56
+
57
+ def revision
58
+ clazz = auditable_type.constantize
59
+ returning clazz.find_by_id(auditable_id) || clazz.new do |m|
60
+ Audit.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge({:version => version}))
61
+ end
62
+ end
63
+
64
+ def ancestors
65
+ self.class.find(:all, :order => 'version',
66
+ :conditions => ['auditable_id = ? and auditable_type = ? and version <= ?',
67
+ auditable_id, auditable_type, version])
68
+ end
69
+
70
+ # Returns a hash of the changed attributes with the new values
71
+ def new_attributes
72
+ (changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
73
+ attrs[attr] = Array(values).last
74
+ attrs
75
+ end
76
+ end
77
+
78
+ # Returns a hash of the changed attributes with the old values
79
+ def old_attributes
80
+ (changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
81
+ attrs[attr] = Array(values).first
82
+ attrs
83
+ end
84
+ end
85
+
86
+ def self.reconstruct_attributes(audits)
87
+ attributes = {}
88
+ result = audits.collect do |audit|
89
+ attributes.merge!(audit.new_attributes).merge!(:version => audit.version)
90
+ yield attributes if block_given?
91
+ end
92
+ block_given? ? result : attributes
93
+ end
94
+
95
+ def self.assign_revision_attributes(record, attributes)
96
+ attributes.each do |attr, val|
97
+ if record.respond_to?("#{attr}=")
98
+ record.attributes.has_key?(attr.to_s) ?
99
+ record[attr] = val :
100
+ record.send("#{attr}=", val)
101
+ end
102
+ end
103
+ record
104
+ end
105
+
106
+ private
107
+
108
+ def set_version_number
109
+ max = self.class.maximum(:version,
110
+ :conditions => {
111
+ :auditable_id => auditable_id,
112
+ :auditable_type => auditable_type
113
+ }) || 0
114
+ self.version = max + 1
115
+ end
116
+
117
+ def set_audit_user
118
+ self.user = Thread.current[:acts_as_audited_user] if Thread.current[:acts_as_audited_user]
119
+ nil # prevent stopping callback chains
120
+ end
121
+
122
+ end
@@ -0,0 +1,79 @@
1
+
2
+ module CollectiveIdea #:nodoc:
3
+ module ActionController #:nodoc:
4
+ module Audited #:nodoc:
5
+
6
+ def self.included(base) # :nodoc:
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ # Declare models that should be audited in your controller
12
+ #
13
+ # class ApplicationController < ActionController::Base
14
+ # audit User, Widget
15
+ # end
16
+ #
17
+ # You can optionally pass options for each model to be audited:
18
+ #
19
+ # audit User, Widget, Task => { :except => :position }
20
+ #
21
+ # NOTE: Models which do not have options must be listed first in the
22
+ # call to <tt>audit</tt>.
23
+ #
24
+ # See <tt>CollectiveIdea::Acts::Audited::ClassMethods#acts_as_audited</tt>
25
+ # for configuration options
26
+ #
27
+ # You can also specify an options hash which will be passed on to
28
+ # Rails' cache_sweeper call:
29
+ #
30
+ # audit User, :only => [:create, :edit, :destroy]
31
+ #
32
+ def audit(*models)
33
+ options = models.extract_options!
34
+
35
+ # Parse the options hash looking for classes
36
+ options.each_key do |key|
37
+ models << [key, options.delete(key)] if key.is_a?(Class)
38
+ end
39
+
40
+ models.each do |(model, model_options)|
41
+ model.send :acts_as_audited, model_options || {}
42
+
43
+ # disable ActiveRecord callbacks, which are replaced by the AuditSweeper
44
+ model.send :disable_auditing_callbacks
45
+
46
+ # prevent observer from being registered multiple times
47
+ model.delete_observer(AuditSweeper.instance)
48
+ model.add_observer(AuditSweeper.instance)
49
+ end
50
+
51
+ class_eval do
52
+ cache_sweeper :audit_sweeper, options
53
+ end
54
+ end
55
+ end
56
+
57
+ end
58
+ end
59
+ end
60
+
61
+ class AuditSweeper < ActionController::Caching::Sweeper #:nodoc:
62
+
63
+ def after_create(record)
64
+ record.send(:audit_create, current_user)
65
+ end
66
+
67
+ def after_destroy(record)
68
+ record.send(:audit_destroy, current_user)
69
+ end
70
+
71
+ def before_update(record)
72
+ record.send(:audit_update, current_user)
73
+ end
74
+
75
+ def current_user
76
+ controller.send :current_user if controller.respond_to?(:current_user, true)
77
+ end
78
+
79
+ end