simple_audit 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,127 @@
1
+ # simple_audit
2
+
3
+ Simple auditing solution for ActiveRecord models. Provides an easy way of creating audit logs for complex model associations.
4
+ Instead of storing audits for all data aggregated by the audited model, you can specify a serializable representation of the model.
5
+
6
+ * a helper method is provided to easily display the audit log
7
+ * the Audit object provides a #delta method which computes the differences between two audits
8
+
9
+ In a nutshell:
10
+
11
+ class Booking < ActiveRecord::Base
12
+ simple_audit do |record|
13
+ # data to be audited
14
+ {
15
+ :price => record.price,
16
+ :period => record.period,
17
+ ...
18
+ }
19
+ end
20
+ end
21
+
22
+ # in your view
23
+ <%= render_audits(@booking) %>
24
+
25
+ # Why ?
26
+
27
+ simple_audit is intended as a straightforward auditing solution for complex model associations.
28
+ In a normalized data structure (which is usually the case when using SQL), a core business model will aggregate data from several associated entities.
29
+ More often than not in such cases, only the core model is of interest from an auditing point of view.
30
+
31
+ So instead of auditing every entity separately, with simple_audit you decide for each audited model what data needs to be audited.
32
+ This data will be serialized on every model update and the available helper methods will render a clear audit trail.
33
+
34
+ ![Screenshot of helper result](http://github.com/gtarnovan/simple_audit/raw/master/screenshot.png)
35
+
36
+ # Installation & Configuration
37
+
38
+ ## Rails 2.3.x
39
+
40
+ ### Gem
41
+
42
+ # in your config/environment.rb
43
+ config.gem 'simple_audit'
44
+
45
+ # then install if you don't already have it
46
+ rake gems:install
47
+
48
+ ### Plugin
49
+
50
+ ./script/plugin install http://github.com/gtarnovan/simple_audit
51
+
52
+ ### Post installation
53
+ # generate & run migration
54
+ ./script/generate simple_audit_migration
55
+ rake db:migrate
56
+
57
+ ## Rails 3
58
+ # in your Gemfile
59
+ gem 'simple_audit'
60
+
61
+ # then install
62
+ bundle install
63
+
64
+ ### Post installation
65
+ # generate & run migration
66
+ rails generate simple_audit:migration
67
+ rake db:migrate
68
+
69
+ # Usage
70
+
71
+ Audit ActiveRecord models. Somewhere in your (backend) views show the audit logs.
72
+
73
+ # in your model
74
+ # app/models/booking.rb
75
+
76
+ class Booking < ActiveRecord::Base
77
+ simple_audit
78
+ ...
79
+ end
80
+
81
+ # in your view
82
+ # app/views/bookings/booking.html.erb
83
+
84
+ ...
85
+ <%= render_audits(@booking) %>
86
+ ...
87
+
88
+ # Assumptions and limitations
89
+
90
+ * Your user model is called User and the current user User.current
91
+ See [sentient_user](http://github.com/bokmann/sentient_user) for more information.
92
+
93
+
94
+ ## Customize auditing
95
+
96
+ By default after each save, all model's attributes are saved in the audits table.
97
+ You can customize the data which is saved by supplying a block which will return all relevant data for the audited model.
98
+
99
+ # app/models/booking.rb
100
+
101
+ class Booking < ActiveRecord::Base
102
+ simple_audit do |record|
103
+ {
104
+ :state => record.state,
105
+ :price => record.price.format,
106
+ :period => record.period.to_s,
107
+ :housing_units => record.housing_units.collect(&:name).join('; '),
108
+ ...
109
+ }
110
+ end
111
+ ...
112
+ end
113
+
114
+ You can also customize the attribute of the User model which will be stored in the audit.
115
+
116
+ # default is :name
117
+ simple_audit :username_method => :email
118
+
119
+ ## Rendering audit trail
120
+
121
+ A helper method for displaying a list of audits is provided. It will render a decorated list of the provided audits;
122
+ only the differences between revisions will be shown, thus making the audit information easily readable.
123
+
124
+ ![Screenshot of helper result](http://github.com/gtarnovan/simple_audit/raw/master/screenshot.png)
125
+
126
+
127
+ Copyright (c) 2010 [Gabriel Târnovan, Cubus Arts](http://cubus.ro "Cubus Arts"), released under the MIT license
data/Rakefile CHANGED
@@ -45,7 +45,7 @@ begin
45
45
  gemspec.email = ["gabriel.tarnovan@cubus.ro", "mihai.tarnovan@cubus.ro"]
46
46
  gemspec.homepage = "http://github.com/gtarnovan/simple_audit"
47
47
  gemspec.authors = ["Gabriel Tarnovan", "Mihai Tarnovan"]
48
- gemspec.version = "0.1.0"
48
+ gemspec.version = "0.1.1"
49
49
  gemspec.files = gem_files
50
50
  gemspec.test_files = test_files
51
51
 
data/TODO CHANGED
@@ -2,11 +2,16 @@
2
2
  * support for ActiveModel
3
3
  * ORM agnosticism
4
4
 
5
- # Funtionality
5
+ # Functionality
6
6
  * provide some sort of UUID for audited models that identify these independently from the database provided ID.
7
7
  in case of deletion and creation of a new entity with the same UUID, the audit trails of both entities could be bound.
8
8
  e.g. for an invoice the UUID could be its number/series combination. when someone deletes an invoice and creates a new one
9
9
  with the same UUID, the audit trail of both instances should be bound, such that the deletion event is visible during the audit (i.e. in the view helper)
10
10
 
11
+ * recent activity page
12
+
13
+ # Testing
14
+ * test helper
15
+
11
16
  # Minor
12
17
  * provide a sample css file for audit trail rendering
data/lib/simple_audit.rb CHANGED
@@ -6,7 +6,7 @@ require 'simple_audit/audit'
6
6
  require 'simple_audit/helper'
7
7
 
8
8
  if defined?(ActiveRecord::Base)
9
- ActiveRecord::Base.send :include, SimpleAudit
9
+ ActiveRecord::Base.send :include, SimpleAudit::Model
10
10
  end
11
11
 
12
12
  if defined?(ActionView::Base)
@@ -17,7 +17,7 @@ module SimpleAudit #:nodoc:
17
17
 
18
18
  return self.change_log if other_audit.nil?
19
19
 
20
- returning({}) do |d|
20
+ {}.tap do |d|
21
21
 
22
22
  # first for keys present only in this audit
23
23
  (self.change_log.keys - other_audit.change_log.keys).each do |k|
@@ -14,7 +14,7 @@ module SimpleAudit #:nodoc:
14
14
  content_tag(:div, audit.username, :class => "user") +
15
15
  content_tag(:div, l(audit.created_at), :class => "timestamp") +
16
16
  content_tag(:div, :class => 'changes') do
17
- if older_audit.present?
17
+ changes = if older_audit.present?
18
18
  audit.delta(older_audit).collect do |k, v|
19
19
  "\n" +
20
20
  audited_model.class.human_attribute_name(k) +
@@ -24,7 +24,8 @@ module SimpleAudit #:nodoc:
24
24
  end
25
25
  else
26
26
  audit.change_log.reject{|k, v| v.blank?}.collect {|k, v| "\n#{audited_model.class.human_attribute_name(k)}: #{v}"}
27
- end
27
+ end
28
+ changes.join
28
29
  end
29
30
  end
30
31
  end
@@ -7,67 +7,69 @@
7
7
  #
8
8
  # See SimpleAudit::ClassMethods#simple_audit for configuration options
9
9
 
10
- module SimpleAudit
11
- def self.included(base) #:nodoc:
12
- base.send :extend, ClassMethods
13
- end
10
+ module SimpleAudit
11
+
12
+ module Model
13
+ def self.included(base) #:nodoc:
14
+ base.send :extend, ClassMethods
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ # == Configuration options
20
+ #
21
+ # * <tt>username_method => symbol</tt> - Call this method on the current user to get the name
22
+ #
23
+ # With no block, all the attributes of the audited model will be logged.
24
+ #
25
+ # class Booking
26
+ # # this is equivalent to passing no block
27
+ # simple_audit do |audited_record|
28
+ # audited_record.attributes
29
+ # end
30
+ # end
31
+ #
32
+ # If a block is given, the data returned by the block will be saved in the audit's change log.
33
+ #
34
+ # class Booking
35
+ # has_many :housing_units
36
+ # simple_audit do |audited_record|
37
+ # {
38
+ # :some_relevant_attribute => audited_record.some_relevant_attribute,
39
+ # :human_readable_serialization_of_aggregated_models => audited_record.housing_units.collect(&:to_s),
40
+ # ...
41
+ # }
42
+ # end
43
+ # end
44
+ #
45
+
46
+ def simple_audit(options = {}, &block)
47
+ class_eval do
14
48
 
15
- module ClassMethods
16
-
17
- # == Configuration options
18
- #
19
- # * <tt>username_method => symbol</tt> - Call this method on the current user to get the name
20
- #
21
- # With no block, all the attributes of the audited model will be logged.
22
- #
23
- # class Booking
24
- # # this is equivalent to passing no block
25
- # simple_audit do |audited_record|
26
- # audited_record.attributes
27
- # end
28
- # end
29
- #
30
- # If a block is given, the data returned by the block will be saved in the audit's change log.
31
- #
32
- # class Booking
33
- # has_many :housing_units
34
- # simple_audit do |audited_record|
35
- # {
36
- # :some_relevant_attribute => audited_record.some_relevant_attribute,
37
- # :human_readable_serialization_of_aggregated_models => audited_record.housing_units.collect(&:to_s),
38
- # ...
39
- # }
40
- # end
41
- # end
42
- #
49
+ write_inheritable_attribute :username_method, (options[:username_method] || :name).to_sym
50
+ class_inheritable_reader :username_method
43
51
 
44
- def simple_audit(options = {}, &block)
45
- class_eval do
46
-
47
- write_inheritable_attribute :username_method, (options[:username_method] || :name).to_sym
48
- class_inheritable_reader :username_method
49
-
50
- audit_changes_proc = block_given? ? block.to_proc : Proc.new {|record| record.attributes}
51
- write_inheritable_attribute :audit_changes, audit_changes_proc
52
- class_inheritable_reader :audit_changes
52
+ audit_changes_proc = block_given? ? block.to_proc : Proc.new {|record| record.attributes}
53
+ write_inheritable_attribute :audit_changes, audit_changes_proc
54
+ class_inheritable_reader :audit_changes
53
55
 
54
- has_many :audits, :as => :auditable, :class_name => '::SimpleAudit::Audit'
55
-
56
- after_create {|record| record.class.audit(record, :create)}
57
- after_update {|record| record.class.audit(record, :update)}
58
-
56
+ has_many :audits, :as => :auditable, :class_name => '::SimpleAudit::Audit'
57
+
58
+ after_create {|record| record.class.audit(record, :create)}
59
+ after_update {|record| record.class.audit(record, :update)}
60
+
61
+ end
62
+ end
63
+
64
+ def audit(record, action = :update, user = nil) #:nodoc:
65
+ user ||= User.current if User.respond_to?(:current)
66
+ record.audits.create(:user => user,
67
+ :username => user.try(self.username_method),
68
+ :action => action.to_s,
69
+ :change_log => self.audit_changes.call(record)
70
+ )
59
71
  end
60
72
  end
61
73
 
62
- def audit(record, action = :update, user = nil) #:nodoc:
63
- user ||= User.current if User.respond_to?(:current)
64
- record.audits.create(:user => user,
65
- :username => user.try(self.username_method),
66
- :action => action.to_s,
67
- :change_log => self.audit_changes.call(record)
68
- )
69
- end
70
-
71
74
  end
72
-
73
75
  end
@@ -0,0 +1,112 @@
1
+ # Auditing in Rails
2
+
3
+ Auditing is a broad term, but when talking about web applications it usually refers to keeping track of who did what, and when. Auditing changes in models is a common requirement for web applications. For Rails there are several solutions which provide this functionality. We'll be looking at two of the best solutions available - acts_as_audit and paper_trail - and introduce simple_audit, a straightforward approach focusing on human readable audit trails.
4
+
5
+ ## Implementing Auditing with Rails
6
+
7
+ Implementing an auditing solution for ActiveRecord models is pretty straightforward due to the many interception points ActiveRecord provides in the lifecycle of a record with callbacks, observers and cache sweepers. Every change to a record can thus easily be tracked and stored in the database.
8
+
9
+ <pre>
10
+ class AuditObserver < ActiveRecord::Observer
11
+ observe :person, :account
12
+
13
+ def after_update(record)
14
+ AuditTrail.new(record, "UPDATED")
15
+ end
16
+ end
17
+ </pre>
18
+
19
+ As you can see, the problem with using observers is that the user who performed the update is not accessible. Sweepers don't have this problem, as they can access the session through the controller, but they will only track changes originating in controller actions. To work around this problem when employing observers/callbacks, many solutions use Thread.local to store the currently logged user, making the data globally available.
20
+
21
+ There are two main approaches for storing the audited records' changes:
22
+
23
+ * one additional audit table for each audited model; audit tables will have the same structure as the audited tables
24
+ * a single audit table for all audited models; tracked changes are stored in a serialized form
25
+
26
+ Using the first approach allows one to issue database queries directly on the audited attributes of a model; this can not be done efficiently with a serialized form used by the second approach.
27
+
28
+ The second approach is used more widely due its flexibility:
29
+
30
+ * any number of models can be audited
31
+ * changes to the attributes of the audited models don't require any updates to the audits table or to the already audited changes
32
+ * database backups / exports can be managed easier
33
+ * smaller footprint - only the changed attributes can be stored, not the entire record
34
+
35
+ Versioning solutions are very similar to auditing solutions; core functionality is basically the same. This is why versioning systems are sometimes also employed for the auditing system of an application or vice versa. See [vestal_versions](http://github.com/laserlemon/vestal_versions) for a solid versioning solution.
36
+
37
+ When relying on ActiveRecord's hooks in the lifecycle of a record to perform auditing, one should keep in mind that some operations will not trigger callbacks and will have to be audited separately. Such operations include direct queries to the database, methods that don't instantiate the records like ActiveRecord#delete_all, #update_all, updating counter caches on associations, etc.
38
+
39
+ ## Available Auditing Solutions for Rails
40
+
41
+ [acts_as_audit](http://github.com/collectiveidea/acts_as_audited) is an established solution for auditing in Rails, having been around since Rails 1.2. It stores each change to a model's attributes in an audit table. The initial value is also stored, making each audit entry self-contained. One can specify which attributes should be audited; the _current_user_ method of the controller is used to get the acting user. A list of versions is available for each audited records.
42
+
43
+ [paper_trail](http://github.com/airblade/paper_trail) is a good solution for both auditing and versioning your models. Besides the functionality provided by acts_as_audited, it allows you to store arbitrary controller-level information (e.g. remote IP, user agent, etc) and also arbitrary model-level metadata with each version. Metadata fields must have their respective columns in the audits table; this allows audits to be quickly filtered using SQL rather then deserializing all data and then filtering it.
44
+
45
+ ## Keeping it simple: simple_audit
46
+
47
+ [simple_audit](http://github.com/gtarnovan/simple_audit) is an auditing solution for Rails primarily intended to be used when the human readable representation of the changes is the main reason for implementing the audit. It handles everything from storing record changes to displaying a human readable audit trail.
48
+
49
+ Many applications need auditing just to show to a privileged user details about actions performed on the database records. Versioning, storing all changed attributes - especially database internal data like record ids - are not as important as displaying a human readable activity feed in a form that is easy to interpret.
50
+
51
+ simple_audit encourages the developer to generate a readable representation of the relevant attributes of a model. This is especially helpful when tracking changes on the parent of a complex aggregation or on a composite record (see [aggregation vs composition](http://en.wikipedia.org/wiki/Object_composition#Aggregation) ).
52
+ Tracking changes on all records in the database is required in certain situations, but it gets complicated if one has to rebuild an aggregation of several audited records just to display a readable audit log.
53
+
54
+ Let's look at an example:
55
+
56
+ <pre>
57
+ class Person < ActiveRecord::Base
58
+ belongs_to :booking
59
+ has_one :address
60
+ end
61
+
62
+ class Room < ActiveRecord::Base
63
+ has_many :room_bookings
64
+ has_many :bookings, :through => :room_bookings
65
+ end
66
+
67
+ class RoomBooking < ActiveRecord::Base
68
+ belongs_to :room
69
+ belongs_to :booking
70
+ end
71
+
72
+ class Booking < ActiveRecord::Base
73
+ has_one :person
74
+ has_many :room_bookings
75
+ has_many :rooms, :through => :room_bookings
76
+
77
+ simple_audit do |booking|
78
+ # this is a human readable representation of the relevant attributes
79
+ {
80
+ :person => "#{booking.person.full_name} #{booking.address.to_s}",
81
+ :housing_units => booking.rooms.collect(&:name).join('; '),
82
+ :arrival => booking.arrival,
83
+ ...
84
+ # other relevant data
85
+ }
86
+ end
87
+
88
+ end
89
+ </pre>
90
+
91
+ The audit trail generated by the supplied view helper will look like this:
92
+
93
+ ![Screenshot of helper result](http://github.com/gtarnovan/simple_audit/raw/master/screenshot.png)
94
+
95
+ In this context, changes made to booking records are central to the application domain. RoomBooking and Person records are relevant only in the context of a booking, therefore auditing them separately would make little sense and would require recomposing data from several audit entries just to show a single change in a booking.
96
+
97
+ simple_audit works on Rails 2.3.x and Rails 3.
98
+
99
+ ### Future developments of simple_audit
100
+
101
+ Support for other ORMs is planned to be implemented until end of november. This will include support for DataMapper and MongoDB.
102
+
103
+ Another planned development is auditing controller actions. Not every action results in a change to the database, but even such actions are sometimes worth tracking (e.g. login & logout, searches, sending email, etc).
104
+ <br />
105
+ Tracking database updates and controller actions would cover most auditing needs for web applications.
106
+
107
+ Integration of [Aquarium](http://aquarium.rubyforge.org/) AOP library to enable auditing of any relevant method call will also be investigated.
108
+
109
+
110
+
111
+
112
+
data/simple_audit.gemspec CHANGED
@@ -5,22 +5,24 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{simple_audit}
8
- s.version = "0.1.0"
8
+ s.version = "0.1.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Gabriel Tarnovan", "Mihai Tarnovan"]
12
- s.date = %q{2010-09-29}
12
+ s.date = %q{2010-11-09}
13
13
  s.description = %q{ Provides a straightforward way for auditing changes on active record models, especially for composite entities.
14
14
  Also provides helper methods for easily rendering an audit trail in Ruby on Rails views.
15
15
  }
16
16
  s.email = ["gabriel.tarnovan@cubus.ro", "mihai.tarnovan@cubus.ro"]
17
17
  s.extra_rdoc_files = [
18
18
  "LICENSE",
19
+ "README.markdown",
19
20
  "TODO"
20
21
  ]
21
22
  s.files = [
22
23
  "CHANGELOG",
23
24
  "LICENSE",
25
+ "README.markdown",
24
26
  "Rakefile",
25
27
  "TODO",
26
28
  "generators/simple_audit_migration/USAGE",
@@ -32,6 +34,7 @@ Gem::Specification.new do |s|
32
34
  "lib/simple_audit/audit.rb",
33
35
  "lib/simple_audit/helper.rb",
34
36
  "lib/simple_audit/simple_audit.rb",
37
+ "rails-magazine-article.markdown",
35
38
  "rails/init.rb",
36
39
  "screenshot.png",
37
40
  "simple_audit.gemspec",
@@ -44,7 +47,7 @@ Gem::Specification.new do |s|
44
47
  s.rdoc_options = ["--charset=UTF-8"]
45
48
  s.require_paths = ["lib"]
46
49
  s.rubyforge_project = %q{simple_audit}
47
- s.rubygems_version = %q{1.3.6}
50
+ s.rubygems_version = %q{1.3.7}
48
51
  s.summary = %q{Simple auditing solution for ActiveRecord models}
49
52
  s.test_files = [
50
53
  "test/fixtures",
@@ -59,7 +62,7 @@ Gem::Specification.new do |s|
59
62
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
60
63
  s.specification_version = 3
61
64
 
62
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
65
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
63
66
  else
64
67
  end
65
68
  else
data/test/test_helper.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require 'rubygems'
2
2
  require 'test/unit'
3
- require 'active_support'
4
- require 'active_support/test_case'
3
+ require 'active_support/all'
5
4
  require 'active_record'
6
5
  require 'active_record/fixtures'
7
6
  require 'action_controller'
@@ -62,5 +62,5 @@ class SimpleAuditTest < ActiveSupport::TestCase
62
62
  address = Address.create
63
63
  assert_equal User.new.full_name, address.audits.last.username
64
64
  end
65
-
65
+
66
66
  end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_audit
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 25
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
8
  - 1
8
- - 0
9
- version: 0.1.0
9
+ - 1
10
+ version: 0.1.1
10
11
  platform: ruby
11
12
  authors:
12
13
  - Gabriel Tarnovan
@@ -15,7 +16,7 @@ autorequire:
15
16
  bindir: bin
16
17
  cert_chain: []
17
18
 
18
- date: 2010-09-29 00:00:00 +03:00
19
+ date: 2010-11-09 00:00:00 +01:00
19
20
  default_executable:
20
21
  dependencies: []
21
22
 
@@ -29,10 +30,12 @@ extensions: []
29
30
 
30
31
  extra_rdoc_files:
31
32
  - LICENSE
33
+ - README.markdown
32
34
  - TODO
33
35
  files:
34
36
  - CHANGELOG
35
37
  - LICENSE
38
+ - README.markdown
36
39
  - Rakefile
37
40
  - TODO
38
41
  - generators/simple_audit_migration/USAGE
@@ -44,6 +47,7 @@ files:
44
47
  - lib/simple_audit/audit.rb
45
48
  - lib/simple_audit/helper.rb
46
49
  - lib/simple_audit/simple_audit.rb
50
+ - rails-magazine-article.markdown
47
51
  - rails/init.rb
48
52
  - screenshot.png
49
53
  - simple_audit.gemspec
@@ -61,23 +65,27 @@ rdoc_options:
61
65
  require_paths:
62
66
  - lib
63
67
  required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
64
69
  requirements:
65
70
  - - ">="
66
71
  - !ruby/object:Gem::Version
72
+ hash: 3
67
73
  segments:
68
74
  - 0
69
75
  version: "0"
70
76
  required_rubygems_version: !ruby/object:Gem::Requirement
77
+ none: false
71
78
  requirements:
72
79
  - - ">="
73
80
  - !ruby/object:Gem::Version
81
+ hash: 3
74
82
  segments:
75
83
  - 0
76
84
  version: "0"
77
85
  requirements: []
78
86
 
79
87
  rubyforge_project: simple_audit
80
- rubygems_version: 1.3.6
88
+ rubygems_version: 1.3.7
81
89
  signing_key:
82
90
  specification_version: 3
83
91
  summary: Simple auditing solution for ActiveRecord models