Permalink
Browse files

First commit.

  • Loading branch information...
0 parents commit e9a8648c22ef38a742eebd5910955b28cb2db3e4 @airblade airblade committed May 27, 2009
@@ -0,0 +1,20 @@
+Copyright (c) 2009 [name of plugin creator]
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
152 README.md
@@ -0,0 +1,152 @@
+# PaperTrail
+
+Track changes to your models' data. Good for auditing or versioning.
+
+
+## Features
+
+* Stores every create, update and destroy.
+* Does not store updates which don't change anything.
+* Allows you to get at every version, including the original, even once destroyed.
+* Allows you to get at every version even if the schema has since changed.
+* Automatically records who was responsible if your controller has a `current_user` method.
+* Allows you to set who is responsible at model-level (useful for migrations).
+* Can be turned off/on (useful for migrations).
+* No configuration necessary.
+* Stores everything in a single database table (generates migration for you).
+* Thoroughly tested.
+
+
+## Rails Version
+
+Known to work on Rails 2.3. Probably works on Rails 2.2 and 2.1.
+
+
+## Basic Usage
+
+PaperTrail is simple to use. Just add 15 characters to a model to get a paper trail of every
+`create`, `update`, and `destroy`.
+
+ class Widget < ActiveRecord::Base
+ has_paper_trail
+ end
+
+This gives you a `versions` method which returns the paper trail of changes to your model.
+
+ >> widget = Widget.find 42
+ >> widget.versions # [<Version>, <Version>, ...]
+
+Once you have a version, you can find out what happened:
+
+ >> v = widget.versions.last
+ >> v.event # 'update' (or 'create' or 'destroy')
+ >> v.whodunnit # '153' (if the update was via a controller and
+ # the controller has a current_user method,
+ # here returning the id of the current user)
+ >> v.created_at # when the update occurred
+ >> widget = v.reify # the widget as it was before the update;
+ # would be nil for a create event
+
+PaperTrail stores the pre-change version of the model, unlike some other auditing/versioning
+plugins, so you can retrieve the original version. This is useful when you start keeping a
+paper trail for models that already have records in the database.
+
+ >> widget = Widget.find 153
+ >> widget.name # 'Doobly'
+ >> widget.versions # []
+ >> widget.update_attributes :name => 'Wotsit'
+ >> widget.versions.first.reify.name # 'Doobly'
+ >> widget.versions.first.event # 'update'
+
+This also means that PaperTrail does not waste space storing a version of the object as it
+currently stands. The `versions` method lets you get at previous versions only; after all,
+you already know what the object currently looks like.
+
+Here's a helpful table showing what PaperTrail stores:
+
+<table>
+ <tr>
+ <th>Event</th>
+ <th>Model Before</th>
+ <th>Model After</th>
+ </tr>
+ <tr>
+ <td>create</td>
+ <td>nil</td>
+ <td>widget</td>
+ </tr>
+ <tr>
+ <td>update</td>
+ <td>widget</td>
+ <td>widget'</td>
+ <tr>
+ <td>destroy</td>
+ <td>widget</td>
+ <td>nil</td>
+ </tr>
+</table>
+
+PaperTrail stores the Before column. Most other auditing/versioning plugins store the After
+column.
+
+
+## Finding Out Who Was Responsible For A Change
+
+If your `ApplicationController` has a `current_user` method, PaperTrail will store the value it
+returns in the `version`'s `whodunnit` column. Note that this column is a string so you will have
+to convert it to an integer if it's an id and you want to look up the user later on:
+
+ >> last_change = Widget.versions.last
+ >> user_who_made_the_change = User.find last_change.whodunnit.to_i
+
+In a migration or in `script/console` you can set who is responsible like this:
+
+ >> PaperTrail.whodunnit = 'Andy Stewart'
+ >> widget.update_attributes :name => 'Wibble'
+ >> widget.versions.last.whodunnit # Andy Stewart
+
+
+## Turning PaperTrail Off/On
+
+Sometimes you don't want to store changes. Perhaps you are only interested in changes made
+by your users and don't need to store changes you make yourself in, say, a migration.
+
+If you are about change some widgets and you don't want a paper trail of your changes, you can
+turn PaperTrail off like this:
+
+ >> Widget.paper_trail_off
+
+And on again like this:
+
+ >> Widget.paper_trail_on
+
+
+## Installation
+
+1. Install PaperTrail either as a gem or as a plugin:
+
+ config.gem 'airblade-paper_trail', :lib => 'paper_trail', :source => 'http://gems.github.com'
+
+ script/plugin install git://github.com/airblade/paper_trail.git
+
+2. Generate a migration which wll add a `versions` table to your database.
+
+ script/generate paper_trail
+
+3. Run the migration.
+
+ rake db:migrate
+
+4. Add `has_paper_trail` to the models you want to track.
+
+
+## Inspirations
+
+* [Simply Versioned](http://github.com/github/simply_versioned)
+* [Acts As Audited](http://github.com/collectiveidea/acts_as_audited)
+
+
+## Intellectual Property
+
+Copyright (c) 2009 Andy Stewart (boss@airbladesoftware.com).
+Released under the MIT licence.
@@ -0,0 +1,23 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the paper_trail plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.libs << 'test'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the paper_trail plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'PaperTrail'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
@@ -0,0 +1,2 @@
+Description:
+ Generates (but does not run) a migration to add a versions table.
@@ -0,0 +1,9 @@
+class PaperTrailGenerator < Rails::Generator::Base
+
+ def manifest
+ record do |m|
+ m.migration_template 'create_versions.rb', 'db/migrate', :migration_file_name => 'create_versions'
+ end
+ end
+
+end
@@ -0,0 +1,16 @@
+class CreateVersions < ActiveRecord::Migration
+ def self.up
+ create_table :versions do |t|
+ t.string :item_type, :null => false
+ t.integer :item_id, :null => false
+ t.string :event, :null => false
+ t.string :whodunnit
+ t.text :object
+ t.datetime :created_at
+ end
+ end
+
+ def self.down
+ drop_table :versions
+ end
+end
@@ -0,0 +1 @@
+# Include hook code here
@@ -0,0 +1 @@
+# Install hook code here
@@ -0,0 +1,31 @@
+require 'yaml'
+require 'paper_trail/has_paper_trail'
+require 'paper_trail/version'
+
+module PaperTrail
+ VERSION = '1.0.0'
+
+ @@whodunnit = nil
+
+ def self.included(base)
+ base.before_filter :set_whodunnit
+ end
+
+ def self.whodunnit
+ @@whodunnit.respond_to?(:call) ? @@whodunnit.call : @@whodunnit
+ end
+
+ def self.whodunnit=(value)
+ @@whodunnit = value
+ end
+
+ private
+
+ def set_whodunnit
+ @@whodunnit = lambda {
+ self.respond_to?(:current_user) ? self.current_user : nil
+ }
+ end
+end
+
+ActionController::Base.send :include, PaperTrail
@@ -0,0 +1,70 @@
+module PaperTrail
+
+ def self.included(base)
+ base.send :extend, ClassMethods
+ end
+
+
+ module ClassMethods
+ def has_paper_trail
+ send :include, InstanceMethods
+
+ cattr_accessor :paper_trail_active
+ self.paper_trail_active = true
+
+ has_many :versions, :as => :item, :order => 'created_at ASC, id ASC'
+
+ after_create :record_create
+ before_update :record_update
+ after_destroy :record_destroy
+ end
+
+ def paper_trail_off
+ self.paper_trail_active = false
+ end
+
+ def paper_trail_on
+ self.paper_trail_active = true
+ end
+ end
+
+
+ module InstanceMethods
+ def record_create
+ versions.create(:event => 'create',
+ :whodunnit => PaperTrail.whodunnit) if self.class.paper_trail_active
+ end
+
+ def record_update
+ if changed? and self.class.paper_trail_active
+ versions.build :event => 'update',
+ :object => object_to_string(previous_version),
+ :whodunnit => PaperTrail.whodunnit
+ end
+ end
+
+ def record_destroy
+ versions.create(:event => 'destroy',
+ :object => object_to_string(previous_version),
+ :whodunnit => PaperTrail.whodunnit) if self.class.paper_trail_active
+ end
+
+ private
+
+ def previous_version
+ previous = self.clone
+ previous.id = id
+ changes.each do |attr, ary|
+ previous.send "#{attr}=", ary.first
+ end
+ previous
+ end
+
+ def object_to_string(object)
+ object.attributes.to_yaml
+ end
+ end
+
+end
+
+ActiveRecord::Base.send :include, PaperTrail
@@ -0,0 +1,20 @@
+class Version < ActiveRecord::Base
+ belongs_to :item, :polymorphic => true
+ validates_presence_of :event
+
+ def reify
+ unless object.nil?
+ # Using +item_type.constantize+ rather than +item.class+
+ # allows us to retrieve destroyed objects.
+ model = item_type.constantize.new
+ YAML::load(object).each do |k, v|
+ begin
+ model.send "#{k}=", v
+ rescue NoMethodError
+ RAILS_DEFAULT_LOGGER.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
+ end
+ end
+ model
+ end
+ end
+end
@@ -0,0 +1 @@
+require 'paper_trail'
@@ -0,0 +1,16 @@
+require 'rubygems'
+require 'rake'
+
+begin
+ require 'jeweler'
+ Jeweler::Tasks.new do |gemspec|
+ gemspec.name = 'paper_trail'
+ gemspec.summary = "Track changes to your models' data. Good for auditing or versioning."
+ gemspec.email = 'boss@airbladesoftware.com'
+ gemspec.homepage = 'http://github.com/airblade/paper_trail'
+ gemspec.authors = ['Andy Stewart']
+ end
+rescue LoadError
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
+end
+
@@ -0,0 +1,22 @@
+sqlite:
+ :adapter: sqlite
+ :dbfile: vendor/plugins/paper_trail/test/paper_trail_plugin.sqlite.db
+
+sqlite3:
+ :adapter: sqlite3
+ :dbfile: vendor/plugins/paper_trail/test/paper_trail_plugin.sqlite3.db
+
+postgresql:
+ :adapter: postgresql
+ :username: postgres
+ :password: postgres
+ :database: paper_trail_plugin_test
+ :min_messages: ERROR
+
+mysql:
+ :adapter: mysql
+ :host: localhost
+ :username: andy
+ :password:
+ :database: paper_trail_plugin_test
+ :socket: /tmp/mysql.sock
Oops, something went wrong.

0 comments on commit e9a8648

Please sign in to comment.