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:
- an explanation of the change is
- what url to go back to when the user decides to undo the action
- 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.