PaperTrail Gem Tutorial
Introduction
In this tutorial I am going to show you how to revert and restore records using the PaperTrail Gem.
Reverting Old Versions
Restoring Deleted Versions
Step 1: Setup
Generate Base Application
First we need to create a base application on which to build. Open up a terminal window and run the following commands.
Install and Configure PaperTrail
Next we need to install the paper_trail Gem. Open up your application’s Gemfile
and add the following.
Next, run the following commands from your application’s root per the installation instructions.
Next, add has_paper_trail
to the Article model.
Add Seed Data
In order to have something to work with, we’ll want to add some seed data to our application.
faker is a library for generating fake data such as names, addresses, and phone numbers.
Add the faker Gem to your application’s Gemfile
and run bundle install
.
Next, add the following to db/seeds.rb
.
Finally, run rails db:seed
.
Update Root Path
Now we just need to update our routes
so that the root_path
displays our data.
Open up config/routes.rb
and add the following:
Finally, run rails s
in your application’s root directory and navigate to http://localhost:3000/
. You should see something similar to the following:
Step 2: Display Previous Versions
Now that we have a basic application with seed data, we can start to carve out our versioning system. The first step is to create a partial to be shared across layouts.
Create a Partial
First create a new partial by running touch app/views/articles/_article.html.erb
in your application’s root. Then, add the following:
Then, replace everything within <tbody></tbody>
with <%= render @articles %>
in app/views/articles/index.html.erb
.
Create a Versions Action
Next, open app/controllers/articles_controller.rb
and add the following:
- We add
:versions
to thebefore_action :set_article
so that ourversions
action has access to the@article
stored in the privateset_article
method. - The
.versions
method is provided by paper_trail, and returns all versions of a given record.
Create a Versions Route
Next, open up config/routes.rb
and add the following:
- We use a member route in order to organize our route based on the associated Article.
Create a Versions View
Next, we’ll need to create a corresponding view to display all Article versions. In the root of your application, run cp app/views/articles/index.html.erb app/views/articles/versions.html.erb
Next, open app/views/articles/versions.html.erb
and add the following:
- We update the title.
- We add
<%= render partial: "article", collection: @articles %>
which will render theapp/views/articles/_article.html.erb
partial, and use the@articles
instance variable from theversions
action.
Refactor Article Partial
If you navigate to the new route, such as http://localhost:3000/articles/1/versions
, you will see the following error:
This is because the versions
action is returning PaperTrail::Version
instances, not Article
instances.
Load Correct Version Data
In order to fix the error add the following to app/views/articles/_article.html.erb
.
PaperTrail does not waste space storing a version of the object as it currently stands
- We wrap our partial in a conditional that checks to see if the current version is simply the creation of the record by checking the event value.
- Note that we we also run
article.try(:event)
incase the record being passed into the partial is not aPaperTrail::Version
instance.
- Note that we we also run
- We conditionally load the
title
andbody
using a call to reify. Reify simply deserializes the value stored in theobject
column on aPaperTrail::Version
instance. This value is a serialized version of the Article.- Note that we we also run
article.try
incase the record being passed into the partial is not aPaperTrail::Version
instance.
- Note that we we also run
This seems to have fixed the problem. However, if you visit the versions
path for an article
you will see any version
where the action was destroy
. For example, if you visit http://localhost:3000/articles/4/versions
you should see the following:
Open up app/views/articles/_article.html.erb
and make the following edit:
- We add
article.event == "destroy"
to hide any version where theevent
wasdestroy
.
Now if you visit http://localhost:3000/articles/4/versions
you’ll no longer see that deleted
version.
Add Versions Link to Partial
Finally, let’s add a link to the versions page for each Article. Open up app/views/articles/_article.html.erb
and make the following edit:
- We wrap our links in a conditional so that they only appear when viewed on the
index
view.
If you navigate to http://localhost:3000/articles/1/versions
you should see the following:
Step 3: Preview Previous Versions
Now that we have a page which lists all previous versions, we’ll want to add a page to preview that version.
Create a Version Action
Open up app/controllers/articles_controller.rb
and add the following:
- We create a private
set_version
method that finds a particular version of an Article. - The
PaperTrail::Version
instance can be queried just like any other record.- Here, we’re looking for an instance of
PaperTrail::Version
where theitem_id
is the same as theid
of current@article
, and theid
is set fromparams[:version_id]
.
- Here, we’re looking for an instance of
Create a Version Route
Next, open up config/routes.rb
and add the following:
Create a Version View
Finally, create a new view by running cp app/views/articles/show.html.erb app/views/articles/version.html.erb
in the applications root.
Open up app/views/articles/version.html.erb
and make the following edits:
- We call
reify
in order to deserialize the value stored in the@version.object
column. - We add a back button for improved user experience.
Refactor Article Partial
Now that we have a view to render a preview, we can add a link allowing a user to view that specific version.
Open up app/views/articles/_article.html.erb
and add the following:
- Note the we conditionally load this link to only appear on the
versions
view.
If you navigate to http://localhost:3000/articles/1/versions
you should see the following:
If you navigate to a specific version, like http://localhost:3000/articles/1/version/2
, you should see a preview of that version.
Step 4: Revert Previous Versions
Now that we can view previous versions, we need the ability to revert back to them.
Create Revert Action
Open up app/controllers/articles_controller.rb
and add the following:
- We add
:revert
tobefore_action :set_article
andbefore_action :set_version
in order to have access to the specific@article
and@version
. - We call
reify
on the@version
in order to deserialize the data stored in theobject
column.
Create Revert Route
Next we need to create a route to correspond with this action. Open up config/routes.rb
and add the following:
- Note that we issue a
post
request, since we’re writing to the database.
Refactor Version Partial
Now that we have an action and corresponding route, we need the ability to revert via a link. Open up app/views/articles/version.html.erb
and add the following:
- Note that we add
method: :post
to issue apost
request.
If you navigate to http://localhost:3000/articles/1/version/2
for should now be able to revert to older versions.
Step 5: Display Deleted Versions
Now let’s add the ability to view deleted versions.
Create Deleted Action
Open up app/controllers/articles_controller.rb
and add the following:
- Here we simply query for all instances of
PaperTrail::Version
where theitem_type
is Article and theevent
was destroy. Simply put, this finds all versions of deleted Articles. - We
order
the query bycreated_at: :desc
, in order to view the most recently deleted Articles first.
Create Deleted Route
Next we need to add a corresponding route for our deleted
action. Open up config/routes.rb
and add the following:
- We use a collection route in order for the path to be
articles/deleted
.
Create Deleted View
Now we need a view to display deleted Articles. In the root of your application run cp app/views/articles/versions.html.erb app/views/articles/deleted.html.erb
Now, open up app/views/articles/deleted.html.erb
and add the following:
- Note that all we change is the page tilte.
Refactor Deleted Action
If you navigate to http://localhost:3000/articles/deleted
you should see the following:
What’s happening is that we’re seeing ALL versions of deleted Articles. For example, an Article can be destroyed, and then restored, and then destroyed again many times. This means that there are can be multiple instances of versions where the event
is set to destroy
for a single Article.
To account for this, we need to refactor our deleted
action. Open up app/controllers/articles_controller.rb
and update the following:
Understanding the Query
Admittedly this refactor is a little cryptic, so let’s go through it in smaller pieces.
Our end goal is to get the latest deleted version of an Article, and ignore older deleted versions of the same Article.
Open up the rails console and by running rails c -s
PaperTrail::Version.where(item_type: “Article”, event: “destroy”)
- Here we simply query for all instances of
PaperTrail::Version
where theitem_type
is Article and theevent
was destroy. Simply put, this finds all versions of deleted Articles.
.filter { |v| v.reify.versions.last.event == “destroy” }
- Building off of that query, we can call filter to only return Articles where the last version was destroy. This is important because we don’t need to see all previous version of the Article when the
event
was destroy. - This goes back to the idea that an Article can be destroyed, and then restored multiple times. If an Article was destroyed, and then restored, we no longer need to see the version of the Article when is was destroyed.
.map(&:reify)
- By addng a call to map, we can return an array of
Article
instances, and notPaperTrail::Version
instances. - This will allow us to have access to columns on the
Article
class.
.uniq(&:id)
- Finally, we can add a call to uniq to eliminate duplicates from the array of
Article
instances based on theid
column. - It’s important that we called
.order(created_at: :desc)
on the query. Otherwise the new array ofArticle
instances would be older deleted versions.
Now if you navigate to http://localhost:3000/articles/deleted
, you should see a filtered list.
Step 6: Restore Deleted Versions
Now that the heavy lifting is done, we can easily restore a deleted Article.
Create Restore Action
Open up app/controllers/articles_controller.rb
and add the following:
- We make sure to see if the latest version was triggered by a
destroy
event
. This is important because we don’t want to restore a version that is actually no longer destroyed.
Create Restore Route
Next, we need to add a corresponding route for our restore
action. Open up config/routes.rb
and add the following:
- Note that we issue a
post
request, since we’re writing to the database.
Refactor Article Partial
Now that we have an action and corresponding route, we need the ability to restore via a link. Open up app/views/articles/version.html.erb
and add the following:
- Note the we conditionally load this link to only appear on the
deleted
view.
If you navigate to http://localhost:3000/articles/deleted
you should be able to restore a deleted Article.
Conclusion and Next Steps
The PaperTrail Gem is powerful in it’s simplicity. By storing all events and their associated record in a separate versions
table, one can create an auditing version as simple or as complex as needed. This tutorial just explored common patterns. Don’t feel you need to follow them precisely.
As always, make sure to thoroughly test your application. You can view this application’s controller and integration tests for inspiration.