Imagine the situation, You open your website’s admin page to do some clean up, find some old data that hasn’t been viewed by anyone for ages, and delete it. Deletion succeeds and everything is okay, but after a second “No!!! That data contained VERY IMPORTANT INFORMATION that could possibly change the world!”, you realize. But, it’s gone and the world remains unchanged (well, there is still a chance for recovery if you have a backup of the Database).
Can we prevent this situation in our Rails application? Yes, for sure we can. Well, in this article, we are going to talk about how to implement a “History” page and an “undo” button (as well as “redo”) with the help of the paper_trail
gem.
We are going to build a simple blog that allows to add, update, and destroy posts. And would be able to undo every action related to the posts (for example, undoing an unintentional delete). We will also provide a “History” page displaying a list of actions (and some other info) that users performed while working with posts.
Create Rails Project
Run command below to create a new Rails application:
1
|
|
Add Gems
We will add a gems to our Gemfile. paper_trail, it will help us to create both the “History” page and an “undo” button:
1
|
|
The blog app has one controller (apart from the ApplicationController), PostsController, that will be used to manage our posts. Initially, it has seven default methods: index, new, create, destroy, edit and update, along with a number of views related to them. I will not go into the details on how to create these methods and views, because they are really basic (you can create them using rails g scaffold Posts).
The blog contains a model: Post. The posts table have the following columns: id, title, body, created_at, updated_at.
And routes file will looks like:
1 2 |
|
At this point, we are ready to setup paper_trail. First of all, create a special table to store the versions (it’s a cool feature of paper_trail – if we want to clear old versions later, we would need only to access one table).
Run command below to generate and migrate paper_trail:
1 2 |
|
Create and apply the required migration. Add the following line to your Post model (app/models/post.rb):
1 2 3 |
|
Well! Now, all changes to the posts table will be audited automatically. How cool is that?
You can also specify which changes should not be tracked. For example, if posts had a view_count column in and we did not want to track changes to it, the modification looks like in (app/models/post.rb):
1
|
|
Some fields can be skipped, they will neither be tracked nor included in the serialized version of the object (app/models/post.rb):
1
|
|
Or can specified as well (app/models/post.rb):
1
|
|
Or provide conditions on whether to track the event:
1 2 |
|
Display the Log
Now, you probably want to display the audited versions, It’s easy, just, add the following route:
1 2 3 |
|
To the history page for Posts. If your app has many models that are being audited, it’s a good idea to create a separate VersionsController and place all versions-related methods there. However, in our case, only the Post model is being audited, so let’s stick with one controller.
Add a history method to the controller (app/controllers/posts_controller.rb):
1 2 3 |
|
Note that we have to use PaperTrail::Version, not just Version. This line of code extracts all the recorded events from the versions table that we have created earlier and sorts them by the creation date. In a real app, paginating these events by using will_paginate or kaminari gem is advisable.
Rendering the view history (app/views/posts/history.html.erb):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Here is the data being displayed:
Data | Description |
---|---|
version.created_at | When this event took place. |
version.id | ID of this event. |
version.item_type | Model name for the event. In our case, it’s Post. |
version.item_id | ID of the resource (Post) that was changed. |
version.event | The action applied to the resource (create, update, destroy).version.object, Full dump of the resource that was changed. |
There are some things that could be improved. For example, which fields were changed (especially for the update action)? Well, that is very easy to implement.
Run command below to create and apply migration:
1 2 |
|
This can also be done when setting up paper_trail. You just need to provide an appropriate option to the generator below:
1
|
|
No further action is required, paper_trail will automatically diff the versions.
Now, we can add a new div with version.changeset block to our view (app/views/posts/history.html.erb):
1 2 3 4 5 6 |
|
It will display attribute values before and after the event (if an attribute remained unchanged it will not be displayed).
Track User-Specific Information
Okay, now we know when of our precious blog post deletion. But, we don’t know who, High time to fix this issue.
Let’s track an IP address of the user responsible for the action. Of course, an IP address can be forged, but the main point here is to explain how to store metadata alongside with the event’s data. Go on and create a new migration below:
1 2 |
|
paper_trail will not store anything in the ip column, by default, so we need to help it out a bit. Add this method to the ApplicationController (app/controllers/application_controller.rb):
1 2 3 |
|
paper_trail will use this method to fetch some additional info and store it as metadata. If you are using Rails 3 or protected_attributes with Rails 4, you will also need to create an initializer (initializers/paper_trail.rb):
1 2 3 4 5 |
|
Metadata can also be provided in the model (app/models/post.rb) like this (presuming we have a timestamp column):
1
|
|
The last thing to do is add a line into view (app/views/posts/history.html.erb):
1 2 3 |
|
Undo an Action
Let’s move on, allowing the user to undo their actions. Create undo method in posts controller (app/controllers/posts_controller.rb) that will undo the requested action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
The above code, find a version by id and then check if there are previous versions available for the resource using the reify method, this method will return nil if the resource was just created in the current version (obviously, if the resource was just created it does not have any previous versions.) Either rollback to the previous version using @post_version.reify.save or destroy the newly created resource using @post_version.item.destroy (the @post_version.item returns the actual resource).
And routes file will looks like:
1 2 3 4 |
|
Then put this undo link into the flash message, so make sure to render it somewhere in your layout (app/views/layouts/application.html.erb):
1 2 3 4 5 6 7 8 |
|
Then create a private method in the PostsController that generates an undo link:
1 2 3 4 5 |
|
We cannot use link_to inside the controller, so the reference to view_context points to the actual view. @post.versions fetches all the versions for the Post resource and @post.versions.last gets the latest one. This method can be used like this in PostsController (app/controllers/posts_controller.rb):
1 2 3 4 5 6 7 8 9 10 11 |
|
Make sure to add it to the create and destroy methods as well.
Okay, I have undone an action… but now I want to redo it. Here, we should introduce a redo link that reverts the undo. There are only few modifications needed.
Then create another private method in PostsController (app/controllers/posts_controller.rb):
1 2 3 4 5 6 7 |
|
This method is very similar to make_undo_link. The main difference is the params[:redo] which is true of false. Based on this parameter, change the text of the link – the URL actually remains unchanged. This is because redoing basically means reverting to the previous version, which is absolutely the same as the undo action.
Then alter the flash message inside the undo method in PostsController:
1 2 3 4 5 |
|
That’s it, users can undo and redo their actions as many times and they want, every time being recorded by paper_trail.
The only one problem is that the versions table can become fat very quickly. This should probably be handled with some background process to remove old entries. The job would use something like:
1
|
|
You also can limit the number of created versions per object by adding this line into an initializer:
1
|
|
So far so good, That’s it!!! See ya!!! :)