No commit activity in last 3 years
No release in over 3 years
With Controlled Versioning, you can make any model versionable. This will record suggested changes to the model, which can then be approved or declined according to your application's design.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

~> 4.0
 Project Readme

RubberStamp Gem Version Build Status Code Climate

  • Approve and decline user revisions
  • Keep meticulous records of all changes to your data
  • Harness the awesome power of a bureaucrat

RubberStamp is inspired by the excellent PaperTrail, though with more emphasis on controlling incoming data than simply recording it. This makes RubberStamp ideal for crowd-sourced websites, as contributions and revisions can be reviewed before publication.

Compatibility

At the moment, RubberStamp only supports Rails 4.

Installation

  1. Add RubberStamp to your gemfile: gem 'rubber_stamp'
  2. Run bundler: bundle install
  3. Run this in your app folder: rake rubber_stamp:install:migrations
  4. Run your migrations: rake db:migrate
  5. Add acts_as_versionable to the models you want to have controlled versioning

Options

By default, RubberStamp will track all attributes except a model's id, created_at, and updated_at. If you want to specify a set of attributes to track, you can do so by passing the versionable_attributes argument to acts_as_versionable:

acts_as_versionable versionable_attributes: [:title, :author]

Conversely, you can exclude specific attributes (and track everything else):

acts_as_versionable nonversionable_attributes: [:publication_date]

Finally, if a model is nested within a parent model using accepts_nested_attributes_for, you can bundle its revisions with its parent's version by using nested_within. Pass in the parent's association name as the sole argument:

belongs_to :publisher, class_name: "DigitalPublisher"
acts_as_versionable nested_within: :publisher

Tracking Users and Notes

RubberStamp can keep track of both who contributed a revision and any notes they choose to leave about the revision. In both cases, these attributes must be passed in while saving the model.

With users, the simplest approach is to merge the current user into the strong_params hash:

def my_model_params
  params.require(:novel).permit(:title, :author).merge(user: current_user)
end

Similarly, if you want the user to leave notes, you should create a textarea for the notes in your form, then permit them in the strong_params hash:

def my_model_params
  params.require(:novel).permit(:title, :author, :notes)
end

And that's all you need to track contributor data and notes.

Usage

Create versionable models with the methods new_with_version and create_with_version:

def create
  @novel = Novel.new_with_version(novel_params)
  @novel.save
end

or:

def create
  @novel = Novel.create_with_version(novel_params)
end

When you do so, RubberStamp will automatically clone its attributes to create an initial version. You can view this version by calling:

@novel.initial_version

Versions start out as pending, and can be set to accepted and declined. To accept a version:

@novel.initial_version.accept

To decline a version:

@novel.initial_version.decline

When updating a versionable model, you'll need to use submit_revision instead of update_attributes:

def update
  @novel = Novel.find(params[id])
  @novel.submit_revision(novel_params)
end

When using submit_revision, your original @novel will not be altered. Instead, RubberStamp creates a new version with the submitted revisions. You can then review this submission:

@novel.versions.last

And either decline it:

@novel.versions.last.decline

Or accept it:

@novel.versions.last.accept

If you accept it, then RubberStamp will update the original with your revisions:

@novel.title # => "fragments from Work in Progress"
@novel.submit_revision(title: "Finnegans Wake")
@novel.title # => "fragments from Work in Progress"
@novel.versions.pending.last.accept
@novel.title # => "Finnegans Wake"

Examining Changes

Before accepting or declining a revision, you'll need to examine it to ensure that the changes are up to your guidelines. You can do so with the version.revisions method:

@novel.submit_revision(title: "Finnegans Wake")
version = @novel.versions.last
changed_attributes = version.revisions.attributes
changed_attributes.first.name # => "title"
changed_attributes.first.old_value # => "fragments from Work in Progress"
changed_attributes.first.new_value # => "Finnegans Wake"

Here we see that revisions returns an array of changed attributes. Each of these attributes can be queried for their name, old_value, and new_value. You could use this array to create a table of changes:

<% version.revisions.attributes.each do |attr| %>
  <th><%= attr.name %></th>
  <td><%= attr.old_value %></td>
  <td><%= attr.new_value %></td>
<% end %>

In addition to an array of attributes, revisions can return an array of nested resources that have been altered:

@novel.submit_revision(characters_attributes: [{ id: 7, name: "Shaun" },
  { name: "Shem" },
  { id: 8, _destroy: true }])
version = @novel.versions.last
changed_children = version.revisions.children

changed_children[0].attributes.first.name # => "name"
changed_children[0].attributes.first.old_value # => "Stanislaus"
changed_children[0].attributes.first.new_value # => "Shaun"

changed_children[1].attributes.first.name # => "name"
changed_children[1].attributes.first.old_value # => nil
changed_children[1].attributes.first.new_value # => "Shem"
changed_children[1].new? # => true

changed_children[2].marked_for_removal? # => true

In this example, three children are being edited. The first has had his name changed, the second is a new addition, and the third is being removed from the family. If this revision is accepted, then the final child will be deleted from the database, while the second is added to it.

You'll also notice that each child contains an attributes hash, just like its parent. They also contain a children hash, allowing you to explore deeply nested associations. If you wanted, you could create a recursive table that would represent all changes (on every level) that a version suggests.

Finally, there are two convenience booleans. The first allows you to find out if a child is new?. The second checks if a child is marked_for_removal?.

Scopes

The Version model supports three scopes corresponding to the three states a version can be in. They are:

@novel.versions.pending
@novel.versions.accepted
@novel.versions.declined

Version Callbacks

Aside from setting internal metadata, RubberStamp does nothing when a version is declined--or when an initial version is accepted. The only time it does something is when a revision is accepted, at which point it updates the versionable.

Most likely, you'll want extra handling in these situations. Perhaps you want to award user reputation points for submitting an acceptable resource. Perhaps you want a resource to be hidden until it has been accepted. Perhaps you want to destroy resources that are declined. You can do all of these things by passing a block to accept or decline. This block will receive the version in question as its sole parameter.

For instance:

@resource.accept { |version| version.versionable.make_visible if version.initial? }

Or:

@resource.decline do |version|
  version.user.deduct_points
  version.versionable.destroy if version.initial?
end

Text Diffs

WARNING: This feature is still unstable.

By default, RubberStamp records changes to the attribute as a whole, though sometimes this is ineffective. For instance, imagine you have a large block of text. Rather than overwriting the whole thing, it makes more sense to record fine grain diffs, such as deleting the substring from index 76 to 94 and inserting the substring 'new text' at index 89. In addition to being more effecient, it allows contributors to concurrently edit the document without necessarily overriding eachother's edits. To gain this fine-grain control over an attribute, declare it in a diff_attribute_names array:

acts_as_versionable diff_attribute_names: [:my_text, :my_string]

You can then access the diffs off of the attribute's version_attribute. For instance:

@version.version_attributes.find_by(name: "my_text").diff_attributes

Each diff_attribute will represent a fine-grain change to the attribute. The point at which the substring starts is at diff_attribute.index. Its text can be found at diff_attribute.text. While substrings that are marked for deletion will return true for diff_attribute.deletion.

Issues

RubberStamp is still very young, and both its functionality and its documentation are bound to be lacking. If you're having trouble with something, feel free to open an issue.

Contributing

Please! Feel free to send us a pull request. Test new features via Rspec, and try to keep your code simple and readable. Ideally, we'll keep that Code Climate in the green.

Contributors

Timothy Baron

Alternatives

If you don't need the ability to accept or decline content, I'd recommend going with PaperTrail, instead. It has a ton of features, as well as support for non-Rails frameworks.

License

Released under the MIT license.

Important Trivia

"Stamping has become a very popular home based craft, and there are a number of forums, some with many thousands of members. Craft stampers tend to be associated with other paper crafts, such as card making and scrapbooking."

--Wikipedia