Draftsman gem: Add a draft state to your Ruby on Rails and Sinatra relational models

August 18, 2014 · Chris Peters

Draftsman is my new Ruby gem that lets you create draft versions of your database records. If you're developing a system in need of simple drafts or a publishing approval queue, then Draftsman just might be what you need.

Draftsman is a Ruby gem that lets you create draft versions of your database records. If you’re developing a system in need of simple drafts or a publishing approval queue, then Draftsman just might be what you need.

I built Draftsman to handle the draft logic in Live Editor. Because draft logic gets complicated pretty quickly, I felt that this warranted its own gem with its own set of tests. Plus this was a great opportunity to share a piece of Live Editor with the open source community!

Live Editor uses the excellent PaperTrail gem for publication revision tracking, so I built the API behind Draftsman to work much like PaperTrail.

A taste of how it works

Have a read of the README to see more examples of how to work with the Draftsman gem.

Setup

To get up and running with Draftsman, you generally have to do a few things:

  • Add draftsman to your Gemfile.
  • Add the drafts table to your database with the rails g draftsman:install command (or copy the migration file into your application if you’re using Sinatra).
  • Add draft_id, published_at, and trashed_at columns to the database tables you want to have drafts on.
  • Add has_drafts to the models you want to have drafts on.

More specific installation instructions are available at the GitHub project.

Persistence of draft data

The Draftsman API requires you to be explicit about when you want to save data as a draft. It provides these persistence methods on your models containing has_drafts:

  • draft_creation
  • draft_update
  • draft_destroy

draft_creation and draft_update work like ActiveRecord’s save and update methods: they validate data in the model, run callbacks, and return true or false if everything saved successfully.

Publishing and reverting drafts

By calling draft_creation, draft_update, and draft_destroy, you’re effectively storing a copy of the record with drafted changes in the drafts table.

You can access these drafts through a query directly on the Draftsman::Draft class or through an instance method called draft on your model objects.

# Accessing drafts through the `Draftsman::Draft` class
@drafts = Draftsman::Draft.order(:created_at)
# Accessing a draft through a model
@post = Post.find(params[:id])
@draft = @post.draft if @post.draft?

Once you have a draft record, you can call revert! on it to undo the draft or publish! to publish the changes to the main record.

Query scopes

Once you’re living in the brave new world of drafts, you’ll want to query your model data differently depending on where you are in the application.

Here is a quick listing of scopes that Draftsman adds to your model when you add has_drafts to it:

Widget.drafted # Limits to items that have drafts. Best used in an "admin" area in your application.
Widget.published # Limits to items that have been published at some point in their lifecycles. Best used in a "public" area in your application.
Widget.trashed # Limits to items that have been drafted for deletion (but not fully committed for deletion). Best used in an "admin" area in your application.
Widget.live # Limits to items that have not been drafted for deletion. Best used in an "admin" area in your application.
view raw scopes.rb hosted with ❤ by GitHub

Typically, the live scope is used in the “admin” section of your application because it limits the query to records that have not been put in the trash (or draft_destroyed, to use Draftsman terminology).

When you want to show only published data (usually in the public or non-admin area of your application), you should use the published scope to query your data.

Loading drafted data

In the admin area of your application, you’ll most likely want to load your data in a mixed state:

  • If a record has a draft, show that data
  • If a record doesn’t have a draft, show that data
  • Hide records that have been drafted for destruction (AKA put in the trash)

The way to accomplish this is through the reify method on each record’s draft. Take a look at these sample index and show actions for a posts admin area:

class Admin::PostsController < Admin::BaseController
def index
@posts = Post.live.includes(:draft).to_a
@posts.map! { |post| post.draft? ? post.draft.reify : post }
end
def show
@post = Post.live.find(params[:id])
@post = @post.draft.reify if @post.draft?
end
end

Here, reify returns a Post object with the drafted data loaded in. If any Post record is not a draft, then nothing is done.

All drafts in one spot

Live Editor, using Draftsman under the hood, allows admins to browse to a Drafts section that lists all drafts polymorphically, no matter if the drafts are of content, files, or design elements.

That way, you can review everything that’s in progress and publish or revert multiple drafts at once, no matter the type of content.

Try doing that in WordPress!

Other features of Draftsman

There is more to this gem that I won’t cover in this introduction. Some features that I didn’t mention are as follows:

  • Does not store drafts for updates that don’t change anything.
  • Allows you to specify attributes (by inclusion or exclusion) that must change for a draft to be stored.
  • publish! and revert! methods handle any dependent drafts so you don’t end up with orphaned records.
  • Allows you to get at every draft, even if the schema has since changed.
  • Automatically records who was responsible via your controller. Draftsman calls current_user by default if it exists, but you can have it call any method you like.
  • Allows you to store arbitrary model-level metadata with each draft (useful for filtering).
  • Allows you to store arbitrary controller-level information with each draft (e.g., remote IP, current account ID).
  • Stores everything in a single database table by default (generates migration for you), or you can use separate tables for separate models.
  • Supports custom draft classes so different models’ drafts can have different behavior.
  • Supports custom name for draft association.
  • Threadsafe.

For more detailed information, examples, bug reports, etc., visit the Draftsman GitHub repo. Give it a try!

About Chris Peters

With over 20 years of experience, I help plan, execute, and optimize digital experiences.

Leave a comment