Rails Undo Redo

What is RUR (Rails Undo Redo)

RUR (Rails-Undo-Redo) is a rails plugin (and soon a gem as well) to make it so easy to implement multi-level undo redo in your Rails application that you better start thinking of better excuses not to implement Undo/Redo (no, seriously, it is going to be a lot easier).

Try the demo!

To get a feel on how undo/redo can help make your app a lot more difficult for users to make mistake, test drive the RUR demo app

The full source code for that demo app is available here: rur_demo. Or via git:

git clone git://gitorious.org/rur_demo/mainline.git

Installing RUR

Using git-rails (this way, you’ll be able to stay up to date, assuming you use git):

git-rails install git://gitorious.org/rur/mainline.git rur

Using scrip/plugin:

script/plugin install http://svn.nanorails.com/plugins/rur

Then copy the migration from vendor/plugins/rur/migrations to db/migrations (renumber as needed), then run:

rake db:migrate

Using RUR

Define the undoable models

Once you have installed the plugin, for each model that you’d like to have its changes undone, just add act_as_undoable>

class Project < ActiveRecord::Base
  belongs_to :user
  has_many :tasks

  acts_as_undoable
end

Define the undoable controllers:

Add the undo/redo logic to all your controllers:

class TasksController < ApplicationController
  ...

  undoable_methods
  ...
end

Track changes

For all controller methods that make a change (and that are directly called by the user), you need to specify 3 things:

  1. an explanation of the change is
  2. what url to go back to when the user decides to undo the action
  3. what url to go back to when the user decides to redo the action

For this, you enclose the code that will change the records within a “change block”.

For example, here’s a create method:

def create
  @task = @project.tasks.new(params[:task])

  respond_to do |format|
    change("create task #{@task.title}", project_tasks_path(@project), project_tasks_path(@project)) do
      if @task.save
        flash[:notice] = 'Task was successfully created.'
        format.html { redirect_to(project_task_path(@project, @task)) }
        format.xml  { render :xml => @task, :status => :created, :location => @task }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @task.errors, :status => :unprocessable_entity }
      end
    end
  end
end

or for update:

def create
  @task = @project.tasks.new(params[:task])

  respond_to do |format|
    change("create task #{@task.title}", project_tasks_path(@project), project_tasks_path(@project)) do
      if @task.save
        flash[:notice] = 'Task was successfully created.'
        format.html { redirect_to(project_task_path(@project, @task)) }
        format.xml  { render :xml => @task, :status => :created, :location => @task }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @task.errors, :status => :unprocessable_entity }
      end
    end
  end
end

or for destroy:

def destroy
  @task = @project.tasks.find(params[:id])
  change("delete task #{@task.title}", project_task_path(@project, @task), project_tasks_path(@project)) do
    @task.destroy
  end

  respond_to do |format|
    format.html { redirect_to(project_tasks_url(@project)) }
    format.xml  { head :ok }
  end
end

Or even a non REST method:

def move_to
  @task = @project.tasks.find(params[:id])
  @new_project = Project.find(params[:task][:new_project_id])

  unless (@new_project == @project)
    change("move task #{@task.title} to #{@new_project.title}", project_task_path(@project, @task), project_task_path(@new_project, @task)) do
      @task.project.tasks.delete @task
      @new_project.tasks << @task
    end
  end
  flash[:notice] = "Task was successfully reassigned to #{@new_project.title}"
  redirect_to(project_task_path(@new_project, @task))
end

Any change within a change block to a model with an “act_as_undoable” attribute will be recorded and can then later on be undone and redone by calling undo or redo.

Let the user undo and redo

The last piece of the puzzle is to add the following to your views (the layout is probably a good place for it):

<% if undo_redo_links != "" %>
<p><%= undo_redo_links %></p>
<% end %>

Example

For a real life example, check out the rur_demo source code.

How does it work?

Coming up shortly… :)

Contributing

The ruby forge RUR project

Clone away the Git repository: git://gitorious.org/rur/mainline.git

git clone git://gitorious.org/rur/mainline.git

Or, better yet, create your own branch on gitorious.