This plugin is simply writing into journals all changes done on an ActiveRecord object, from its direct attributes to its subresources when they are made at the same time, and displaying them with an helper and simple partials.
Ruby
Pull request Compare This branch is 7 commits ahead of RonnieOnRails:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.
app
generators/journalization_migration
lib
public/stylesheets
shoulda_macros
tasks
test
.gitignore
CHANGELOG
LICENSE
README
Rakefile
init.rb
install.rb

README

= Journalization (v0.1.2)

* Source: http://github.com/rOnnie974/journalization
* Environment: under Rails 2.2.3 and ruby 1.8.7, not tested above

== Bugs & Feedback
Bug reports and feedback are welcome via ronnie (dot) baret (at) gmail (dot) com

== Overview
This plugin is simply writing into journals all changes done on an ActiveRecord 
object, from its direct attributes to its subresources when they are made at 
the same time, and displaying them with an helper and simple partials.

With Journalization, we assume that there are two different types of models: 
those that act on journalization, and those that are journalized. That's 
why this plugin is delivered with three modules to be implemented:
- Journalization::Models::Actor for the actor of the journalization (mainly
it is your User class) with the act_on_journalization_with method;
- Journalization::Models::Subject for the models that will be journalized;
- Journalization::Controllers::ActorSetting to link the actor in the session 
to the journalization process.

== Setup

First:

  $ script/plugin install git@github.com:rOnnie974/journalization.git

To add the journals, journal_lines and journal_identifiers tables to your 
project, run the following command:

  $ script/generate journalization_migration
  
Then migrate your project database:
  
  $ rake db:migrate

== Basic usage

Models you want to be journalized will call the <tt>journalize</tt> method,
delivered with several params.

For each call of the `save` method (after a `create` or a `update` call) on
an ActiveRecord object, a Journal will be created with as much lines 
(JournalLine) as changes according to the journalization configuration.

Each line records the property changed, its old value and its new value. Its 
journal keeps the datetime of the action with the created_at attribute.

  To journalize changes on an only one attribute:

    class Car < ActiveRecord::Base
      journalize :attributes => :name
    end
    
  Or on several attributes:
  
    class Car < ActiveRecord::Base
      belongs_to :brand
      
      journalize :attributes => [:name, :purchased_on, :brand_id]
    end
    
Journalization manage Paperclip objects since their changes are journalized
when the file name is not the same before and after a save. If not the file
name stay unchanged, the file size is compared. Then, the old and new 
file_name are recorded as old and new value in the journal_line.
    
  To journalize changes on Paperclip objects:
  
    class Car < ActiveRecord::Base
      has_attached_file :photo
      
      journalize :attachments => :photo
    end
    
  Or on several Paperclip objects:
  
    class Car < ActiveRecord::Base
      has_attached_file :photo
      has_attached_file :guide
      
      journalize :attachments => [:photo, :guide]
    end
    
  Different params types can be called together:
  
    class Car < ActiveRecord::Base
      has_attached_file :photo
      
      journalize :attributes => :name, :attachments => :photo
    end
    
Call the <tt>acts_on_journalization_with</tt> method to define a class as 
journalization actor, with a method_name as param identifying it for 
display in the view.

WARNING: If you do not perform this step, journalization will work but
journals will not have associated actors.

  In your User class (or another class with the same role):
  
    class User < ActiveRecord::Base
      acts_on_journalization_with :fullname
      
      def fullname
        "#{self.first_name} #{self.last_name}"
      end
    end
    
Call the <tt>set_journalization_actor</tt> method to link the current user
in the session with the journalization process. 

WARNING: Journalization is compatible with the acts_as_authenticated plugin
since it uses the current_user method to catch the user in the session. If
you do not work with the acts_as_authenticated plugin (or with a similar
behaviour), the set_journalization_actor_object method in the 
Journalization::Controllers::ActorSetting module has to be modified.

  In your controller (directly in ApplicationController should be better):
  
    class ApplicationController < ActionController::Base
      set_journalization_actor
    end
    
By default, <tt>set_journalization_actor</tt> only deals with create and
update controller methods. You can specify other methods by passing an array
of symbols to the call. If so, you have to add explicitely the create and
and update methods.
  
  In your controller:
  
    class ApplicationController < ActionController::Base
      set_journalization_actor
    end
    
    class CarsController < ApplicationController
      set_journalization_actor [:create, :custom_update]
    end

To display the journals :

  - add the journalization_helper and journalization.css to your controller,
  
    class CarsController < ApplicationController
      helper :journalization
    end
    
    … or directly in the ApplicationController.
    
  - call journalization.css in header, and the following helper in your views,
      
    # app/views/layouts/default.html.erb  
    <head>
      <%= stylesheet_link_tag "journalization" %>
    </head>
      
    # app/views/cars/show.html.erb
    <%= display_journals_list(@car) %>
    
== Journalizing subresources

Journalization also manage subresources in case when they are modified and
saved at the same time as its parent resource, for example with an 
after_save callback.

Let's take a short example with our Car class:

  class Car < ActiveRecord::Base
    has_one  :document
    has_many :wheels
  end
    
A journalization should be interesting at two different levels:
  (1) An evolution of the collections
  (2) A modification inside the collection
  
  Details:
    (1) With an instance of Car called @car, we would be able to know how 
      and when @car.wheel_ids were modified from [1,2,3,4] to [1,2,3,5], 
      or @car.registration_document was changed from Document ID 1 to ID 2.
    (2) We would be able to know how and when @car.wheels.first.price was
      modified from 100 to 150
      
  If you want the first case to be managed, call <tt>journalize</tt> 
  this way:
   (1)  
    class Car < ActiveRecord::Base
      has_one  :document
      has_many :wheels
      
      journalize :subresources => [{:document => :create_and_destroy,
                                    :wheels   => :create_and_destroy}]
    end
    
    @car.wheel_ids                #=> [1,2,3,4]
    @car.wheel_ids = [1,2,3,5]
    @car.wheel_ids                #=> [1,2,3,5]
    
  A journal line will be created with [1,2,3,4] as old value, [1,2,3,5] as
  new value for the wheels property.
  
  With the journalization_helper, we obtain the following result:
    
    John Doe - 2010-08-17 at 16:02
    - Wheel "Wheel #4" was removed
    - Wheel "Wheel #5" was added
    
    
  For the second case, changes are only detected on a subresource if the
  subresource is also journalizing:
   (2)  
    class Car < ActiveRecord::Base
      has_one  :document
      has_many :wheels
      
      after_save :save_wheels
      
      journalize :subresources => [{:wheels => :update}]
      
      def save_wheels
        wheels.each {|w| w.save}
      end
    end
    
    class Wheel < ActiveRecord:Base
      journalize :attributes => :price
    end
    
    @car.wheels.first.price       #=> 100
    @car.wheels.first.price = 150
    @car.save
    
  A journal line will be created for the car according to the price updated
  at the wheel level but will not show any details. However, a reference to
  will be recorded in order to find a journal at the wheel level.
  
  With the journalization_helper, we obtain the following result:
    
    John Doe - 2010-08-17 at 16:02
    - Wheel "Wheel #1" was modified

  If you want both cases to be managed on wheels but not an the document,
  call <tt>journalize</tt> this way:
  
    class Car < ActiveRecord::Base
      journalize :subresources => [ :wheels, 
                                   {:document => :create_and_destroy}
                                  ]
    end
  
  Quick summary: the subresources key expect an array of symbols (for
  subresources journalized in both cases) and/or hash (to choose explicitely
  an only one case).
  
== Journalizing belongs_to associations

Belongs_to foreign keys are considered as attributes like classic attributes
since they are also put in the table. Hence you have to call them in the
:attributes param, however journalization_helper will treat it differently.
It will display the name of the association instead of the foreign key name,
and the object identifier instead of the foreign key value (see next section
for further details about belongs_to identifiers).
  
== Journal identifiers

A last param for the journalize call is available -> :identifier_method. It 
permits to identify an object (subresource particularly) and is used in the 
journalization_helper. It can be a Symbol or a String representing an
attribute or an instance method, or a Proc to allow custom identifier
without any additional method to create.

  With the following configuration:
  
    class Car < ActiveRecord::Base
      journalize :subresources => [:wheels]
    end
    
    class Wheel < ActiveRecord:Base
      journalize :identifier_method => :reference
    end
    
    @car.wheels.first.price       #=> 100
    @car.wheels.first.price = 150
    @car.save
    
  With the journalization_helper, in the @car journal, the following 
  sentence can be displayed:
  
    John Doe - 2010-08-17 at 16:04
      - Wheel "KX-200" was modified
    
  If you do not journalize an identifier, ID is displayed by default:
  
    John Doe - 2010-08-17 at 16:05
      - Wheel "Wheel #1" was modified
    
The :identifier_method was in fact already used in the Basic usage section
of this documentation. When we did:

  class User < ActiveRecord::Base
    acts_on_journalization_with :fullname
  end
  
… User.journalize :identifier_method => :fullname was performed at the
same time so as we could identify the actor in the view:

  John Doe - 2010-08-17 at 16:06
    - Color was modified from blue to orange
    - Wheel "KX-200" was modified
    
It is also useful with belongs_to association:

  without identifier,
  
    class Car < ActiveRecord::Base
      belongs_to :brand
      
      journalize :attributes => :brand_id
    end
    
    @car.brand                  #=> Brand #1
    @car.brand = Brand.find(2)
    @car.save
  
    John Doe - 2010-08-17 at 16:07
      - Brand was modified from "Brand #1" to "Brand #2"
  
  with identifier,
  
    class Car < ActiveRecord::Base
      belongs_to :brand
      
      journalize :attributes => :brand_id
    end
    
    class Brand < ActiveRecord::Base
      journalize :identifier_method => :name
    end
    
    @car.brand                  #=> Brand #1
    @car.brand = Brand.find(2)
    @car.save
  
    John Doe - 2010-08-17 at 16:07
      - Brand was modified from "Renault" to "Dacia"
      
== ActiveRecord Tests (Shoulda::ActiveRecord::Macros)

Quick macro tests:

  class CarTest < Test::Unit::TestCase
    should_journalize :attributes   => :color, 
                      :attachments  => :photo,
                      :subresources => [ :document,
                                         {:wheels => :create_and_destroy}]
  end

  class UserTest < Test::Unit::TestCase
    should_act_on_journalization_with :fullname
  end
    
== Warnings

- To journalize a change, journalization goes by previous journals. It means
that, with the following scenario:

  (1)   Car journalizes color and @car.create(:color=>"blue", :brand_id=>1)
  (2.1) @car.color #=> "blue"
        @car.brand #=> Brand #1
  (2.2) @car.save
  (3.1) @car.color #=> "red"
  (3.1) @car.save
  (4)   The developper removes journalization on the Car class
  (5.1) @car.color #=> "green"
  (5.2) @car.save
  (6)   Car journalizes color and brand_id
  (7.1) @car.color #=> "yellow"
  (7.2) @car.save
  
  - Journals reflects the following changes:
    nil    -> "blue"
    "blue" -> "red"
    "red"  -> "yellow"
    
    We do not know when the color was "green" because car was not 
    journalized anymore at this time.
    
  - At (6), we journalize an attribute that was not journalized before. 
    It implies that at (7.2), a journal line will be created to tell 
    that brand_id was modified from nil to 1.
    
    In cases when you change the attributes to be journalized during the
    lifecycle of an object, be sure to perform a save without actor 
    (i.e. in the console) on it to journalize for the first time the 
    attributes not journalized  before. That permits, in (7.2), if
    an user wants to change the color, to puts him as actor only on the
    journal at the color change but not on the journal at the brand_id change.
    
- Since has_many & has_one changes are directly written into database, be sure
to perform a save on your ActiveRecord object to journalize them.

- JournalLine old_value and new_value method return casted values so that their
class is the same as the original journalized value.
    
== Credits and thanks
Journalization was created during the Osirails project:
- http://github.com/spidou/osirails
- http://osirails.spidou.com/wiki

Thanks to Mathieu FONTAINE (aka spidou) for the original idea and his great 
help in specifications and development.

Thanks to Julien MORILLE (aka Falc0) for his help during the plugin
development start.

Thanks to the whole Osirails project team for the support.