Redmine Refactor #100: Convert Issue Routes to REST Resources

For my 100th refactoring of Redmine, I decided to go big. Today I refactored Redmine’s Issue routes from standard Rails routes to actual REST resources.

Before

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.with_options :controller => 'issues' do |issues_routes|
    issues_routes.with_options :conditions => {:method => :get} do |issues_views|
      issues_views.connect 'issues', :action => 'index'
      issues_views.connect 'issues.:format', :action => 'index'
      issues_views.connect 'projects/:project_id/issues', :action => 'index'
      issues_views.connect 'projects/:project_id/issues.:format', :action => 'index'
      issues_views.connect 'projects/:project_id/issues/new', :action => 'new'
      issues_views.connect 'projects/:project_id/issues/gantt', :controller => 'gantts', :action => 'show'
      issues_views.connect 'projects/:project_id/issues/calendar', :controller => 'calendars', :action => 'show'
      issues_views.connect 'projects/:project_id/issues/:copy_from/copy', :action => 'new'
      issues_views.connect 'issues/:id', :action => 'show', :id => /\d+/
      issues_views.connect 'issues/:id.:format', :action => 'show', :id => /\d+/
      issues_views.connect 'issues/:id/edit', :action => 'edit', :id => /\d+/
    end
    issues_routes.with_options :conditions => {:method => :post} do |issues_actions|
      issues_actions.connect 'issues', :action => 'index'
      issues_actions.connect 'projects/:project_id/issues', :action => 'create'
      issues_actions.connect 'projects/:project_id/issues/gantt', :controller => 'gantts', :action => 'show'
      issues_actions.connect 'projects/:project_id/issues/calendar', :controller => 'calendars', :action => 'show'
      issues_actions.connect 'issues/:id/quoted', :controller => 'journals', :action => 'new', :id => /\d+/
      issues_actions.connect 'issues/:id/:action', :action => /edit|destroy/, :id => /\d+/
      issues_actions.connect 'issues.:format', :action => 'create', :format => /xml/
      issues_actions.connect 'issues/bulk_edit', :action => 'bulk_update'
    end
    issues_routes.with_options :conditions => {:method => :put} do |issues_actions|
      issues_actions.connect 'issues/:id/edit', :action => 'update', :id => /\d+/
      issues_actions.connect 'issues/:id.:format', :action => 'update', :id => /\d+/, :format => /xml/
    end
    issues_routes.with_options :conditions => {:method => :delete} do |issues_actions|
      issues_actions.connect 'issues/:id.:format', :action => 'destroy', :id => /\d+/, :format => /xml/
    end
    issues_routes.connect 'issues/gantt', :controller => 'gantts', :action => 'show'
    issues_routes.connect 'issues/calendar', :controller => 'calendars', :action => 'show'
    issues_routes.connect 'issues/:action'
  end
end

After

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.bulk_edit_issue 'issues/bulk_edit', :controller => 'issues', :action => 'bulk_edit', :conditions => { :method => :get }
  map.bulk_update_issue 'issues/bulk_edit', :controller => 'issues', :action => 'bulk_update', :conditions => { :method => :post }
  map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new', :id => /\d+/, :conditions => { :method => :post }
  map.connect '/issues/:id/destroy', :controller => 'issues', :action => 'destroy', :conditions => { :method => :post } # legacy
 
  map.resource :gantt, :path_prefix => '/issues', :controller => 'gantts', :only => [:show, :update]
  map.resource :gantt, :path_prefix => '/projects/:project_id/issues', :controller => 'gantts', :only => [:show, :update]
  map.resource :calendar, :path_prefix => '/issues', :controller => 'calendars', :only => [:show, :update]
  map.resource :calendar, :path_prefix => '/projects/:project_id/issues', :controller => 'calendars', :only => [:show, :update]
 
  # Following two routes conflict with the resources because #index allows POST
  map.connect '/issues', :controller => 'issues', :action => 'index', :conditions => { :method => :post }
  map.connect '/issues/create', :controller => 'issues', :action => 'index', :conditions => { :method => :post }
 
  map.resources :issues, :member => { :edit => :post }, :collection => {}
  map.resources :issues, :path_prefix => '/projects/:project_id', :collection => { :create => :post }
end

Refactoring complex routes is always difficult, especially in Redmine where the default route is still used. In this case, to handle all of the routes I had to add a few shims:

  • Named routes for bulk_edit and bulk_update. These can probably be converted over to :collection actions on the Issues resource later.
  • Named route for quoting an issue (map.quoted_issue). I think this can be removed once Journals are converted to a sub resource of Issues.
  • A Legacy route for destroying an issue.
  • Two resources for the Gantt charts, a global one and a project specific one.
  • Two resources for the Calendars, a global one and a project specific one.
  • Two routes to override the Issue resource so posting to ‘/issues’ will work for submitting an issue query. This required moving the create route to ‘/issues/create’.

You’ll also notice that there are two resources for Issues, one globally and one for the project. Once Projects are converted to REST resources, I’ll be able to nest Issues under them and use shallow routing to handle most of the paths.

I’m now finished refactoring IssuesController for now. My goal with refactoring Redmine’s IssuesController was to convert it to a REST resource. Next it’s time to start refactoring the ProjectsController, with the end goal of converting it to a REST resource also.

Reference commit