I’ve been using Rick Olson’s acts_as_versioned Rails plugin on a project and recently ran into some thngs that I thought might be worth sharing.
In general, acts_as_versioned behaves well and integration has gone smoothly. The object that we wanted to version is called SurveyContent. We created a database table called survey_content_versions with the same columns and data types as survey_contents, plus id (key) and survey_content_id (foreign key to the versioned object). After adding :acts_as_versioned to the class definition for SurveyContent, every save of a SurveyContent object that has changes results in a new survey_content_version entry in the database.
Making acts_as_versioned play nicely with acts_as_state_machine
acts_as_versioned provides methods for things like .revert_to, .latest, and .previous, which is really nice. But we ran into trouble on a few fronts, particularly since SurveyContent also uses acts_as_state_machine. Users can request changes to their associated survey content, but these changes are subject to administrative approval, so we basically care about two versions of a SurveyContent instance: the approved version, and the latest version. Since we have distinct states defined for acts_as_state_machine, we’re used to calling foo.approved? to see if an object is in the “approved” state. Unfortunately, these convenience methods are not available on our survey_content_version object.
We ended up adding the methods to our version by creating anonymous mix-ins in SurveyContent:
class SurveyContent < ActiveRecord::Base
...
acts_as_versioned do
def approved?
self.status == "approved"
end
...
end
No real magic here– the documentation for the plugin provided the tip– but hopefully this will be of use to someone else.
Cloning a version
There was another situation in which we needed to act on an actual instance of a different version of SurveyContent, while keeping the existing instance intact. In that case, we opted to clone:
def approved_version
if @approved_version.nil? && self.survey_content
_last_approved = self.survey_content.versions.reverse.detect
{|v| v.status == "approved"}
unless _last_approved.nil?
@approved_survey_content = SurveyContent.new
_temp = _last_approved.attributes
_temp.delete('survey_content_id')
@approved_survey_content.attributes = _temp
end
end
@approved_survey_content
end
Maybe a little ham-fisted, but it got the job done. Note that we had to drop the survey_content_id from the attributes, as SurveyContent has no such field.
acts_as_versioned and serialized attributes
The last problem we ran into had to do with the fact that one of the attributes on our versioned object was serialized. Out of the box, acts_as_versioned ended up converting it to a string. Luckily, some Google mining later, we found the solution.
First, we added some code to acts_as_versioned.rb:
def acts_as_versioned(options = {}, &extension)
...
# Preserve serialized attributes
self.serialized_attributes().each do |key, value|
versioned_class.serialize key, value
end
end
end
module ActMethods
...
Then, we added another method to our anonymous mix-in on SurveyContent:
class SurveyContent < ActiveRecord::Base
...
acts_as_versioned do
def after_initialize
self.data ||= Hash.new
end
...
def approved?
(Note that in the snippet above, data is the name of our attribute.)
One last problem with serialization that we ran into was that acts_as_versioned was not detecting changes when we did a merge operation on our serialized attribute. To ensure that a save took place, we had to call will_change!:
def survey_content_date=( survey_content_params )
self.survey_content.data_will_change!
...
Again, substitute the name of your object’s parameter for data.
Go version something!
If you’re looking for the plugin, be sure to download acts_as_versioned from github and not Rick’s site– he’s moved from SVN to Git, and github has the latest version.
No Comments »