ChiliProject is not maintained anymore. Please be advised that there will be no more updates.

We do not recommend that you setup new ChiliProject instances and we urge all existing users to migrate their data to a maintained system, e.g. Redmine. We will provide a migration script later. In the meantime, you can use the instructions by Christian Daehn.

acts_as_journalized

Contents

Development and API documentation for using the acts_as_journalized plugin to create a version history for ActiveRecord objects.

Archetecture overview

acts_as_journalized replaces JournalDetails and acts_as_versioned in ChiliProject 2.0.0. It does this by tracking changes on the journaled object and creating an STI'd Journal when that object is saved. So by changing the name of an Issue (journaled object) from "Old subject" to "New subject", an IssueJournal is created that has shows the changes. The changes are stored in the changes field, automatically serialized to YAML.

Versions

Each Journal has a unique version field based on it's Journaled object. So Issue 100 can have a version 1 (initial journal), version 2, version 3, etc. This is automatically incremented, you don't need to do anything to update it.

Initial versions

Each Journaled object has an initial journal created when it's first created. This Journal will have the version of 1 and will store the original attributes of the Journaled object in it's changes. This means you can get a full history of a Journaled object by going through it's Journals and "walking" the changes hashes.

Changes format

The changes field is automatically serialzied to YAML but once it's loaded into Rails it's a hash of the changes:

 1>> issue.changes
 2{
 3  "attribute_name" => [
 4    "old value",
 5    "new value" 
 6  ],
 7  "relations_too_id" => [
 8    0,
 9    5
10  ]
11}
12
13>> issue.changes
14{
15  "subject" => [
16    "Old subject",
17    "New subject" 
18  ]
19}

Public API

Previously Journals were only created for Issues and used the init_journal method. While init_journal is still around, it should not be used directly anymore. It is called by the journaled class's callbacks automatically (e.g. after_save).

Instead specific setter methods should be used to set up a Journal:

Journaled#journal_user

Data type: User

Will set the creator of the Journal. Defaults to the the Journaled#updated_by user or the current user (User.current).

Journaled#journal_notes

Data type: String

Will set the notes for the journal. Defaults to an empty string.

Journaled#extra_journal_attributes

Data type: Hash

Will set extra attributes on the automatically created Journal object. This is used because the Journals are created automatically and you are unable to access them before they are created (see Acts::As::Journalized::Creation#create_journal and Acts::As::Journalized::Creation#journal_attributes for details). The extra_journal_attributes field will override any of the default attributes and let you have more control over the final Journal.

Upgrade plugin to acts_as_journalized

The standard way to provide activities in ChiliProject 2 onward has changed from acts_as_activity_provider to acts_as_journalized. This paragraph describes how to upgrade a plugin to use acts_as_journalized instead of acts_as_activity_provider. In the following, it is assumed the model providing an activity that is upgraded to acts_as_journalized is Foo.

Configure acts_as_journalized

acts_as_journalized replaces acts_as_event as well as acts_as_activity_provider but can take options for both as needed. Options prefixed with activity_ will be passed to acts_as_activity_provider, those prefixed with event_ will be passed to acts_as_event.

Assuming Foo had the following:

1acts_as_event :title => Proc.new {|o| "#{l(:label_foo)} ##{o.id}: #{o.title}"},
2              :url => Proc.new {|o| {:controller => 'foos', :action => 'show', :id => o.id}}
3acts_as_activity_provider :find_options => {:include => [:project, :author]},
4                          :author_key => :author_id

New journals are created by default with the current user as the author, i.e. you don't need the author_key parameter anymore if you had any, and you don't even need to save the author in your object if you needed it for the activity only.

acts_as_journalized will also automatically include the :project in the find_options if the journalized model has a project, so no need to put it in the activity_find_options anymore. One important thing to note: Procs in the options are passed the journal, not (as was the case before) the object, use .journaled on a journal to get the object.

All in all, this gives us the following acts_as_journalized line for the above example:

1acts_as_journalized :activity_find_options => {:include => :author},
2                    :event_title => Proc.new {|o| "#{l(:label_foo)} ##{o.journaled.id}: #{o.journaled.title}"},
3                    :event_url => Proc.new {|o| {:controller => 'foos', :action => 'show', :id => o.journaled.id}}

Create initial journals

Initial journals will have to be back-created for each existing Foo. acts_as_journalized adds a utility method recreate_initial_journal! to journalized objects to guess and create the initial journal. The migration to back-create the initial journals db/migrate/20110906203247_create_initial_foo_journals.rb could be like:

 1class CreateInitialFooJournals < ActiveRecord::Migration
 2  def self.up
 3    journaled_class = Foo
 4
 5    say_with_time("Building initial journals for #{journaled_class.class_name}") do
 6
 7      # avoid touching the journaled object on journal creation
 8      journaled_class.journal_class.class_exec {def touch_journaled_after_creation; end}
 9
10      activity_type = journaled_class.activity_provider_options.keys.first
11
12      # Create initial journals
13      journaled_class.find(:all).each do |o|
14        begin
15          o.recreate_initial_journal!
16        rescue ActiveRecord::RecordInvalid => e
17          puts "ERROR: errors creating the initial journal for #{o.class.to_s}##{o.id.to_s}:" 
18          puts "  #{e.message}" 
19        end
20      end
21    end
22  end
23
24  def self.down
25    # no-op
26    # (well, in theory we should delete the FooJournals…)
27  end
28end