Redmine Refactor #115: Split Method in VersionsController

Still focusing on the VersionsController, I need to split another method before I can refactor this controller’s routing. This time #new needs to be split into #new and #create.

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
class VersionsController  [:index, :new, :close_completed]
  before_filter :find_project_from_association, :except => [:index, :new, :close_completed]
  before_filter :find_project, :only => [:index, :new, :close_completed]
  before_filter :authorize
 
  def new
    @version = @project.versions.build
    if params[:version]
      attributes = params[:version].dup
      attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
      @version.attributes = attributes
    end
    if request.post?
      if @version.save
        respond_to do |format|
          format.html do
            flash[:notice] = l(:notice_successful_create)
            redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
          end
          format.js do
            # IE doesn't support the replace_html rjs method for select box options
            render(:update) {|page| page.replace "issue_fixed_version_id",
              content_tag('select', '' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
            }
          end
        end
      else
        respond_to do |format|
          format.html
          format.js do
            render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
          end
        end
      end
    end
  end
end

After

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
39
40
41
42
43
44
45
46
47
48
49
class VersionsController  [:index, :new, :create, :close_completed]
  before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
  before_filter :find_project, :only => [:index, :new, :create, :close_completed]
  before_filter :authorize
 
  def new
    @version = @project.versions.build
    if params[:version]
      attributes = params[:version].dup
      attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
      @version.attributes = attributes
    end
  end
 
  def create
    # TODO: refactor with code above in #new
    @version = @project.versions.build
    if params[:version]
      attributes = params[:version].dup
      attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
      @version.attributes = attributes
    end
 
    if request.post?
      if @version.save
        respond_to do |format|
          format.html do
            flash[:notice] = l(:notice_successful_create)
            redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
          end
          format.js do
            # IE doesn't support the replace_html rjs method for select box options
            render(:update) {|page| page.replace "issue_fixed_version_id",
              content_tag('select', '' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
            }
          end
        end
      else
        respond_to do |format|
          format.html { render :action => 'new' }
          format.js do
            render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
          end
        end
      end
    end
  end
 
end

I’ve done similar split methods before but I want to point out two things to keep in mind.

The first change is deep in the respond_to block. In the before code, the failure state just uses format.html which by default would render the current action’s view. But when it’s refactored into #create, the current action becomes #create which doesn’t have a view. So I had to add an explicit render :action => 'new' to make sure the form is rendered with the errors. This is an edge case bug that could appear from your refactorings but it might be missed. Redmine, unfortunately, doesn’t have a test case for that so I would have missed it if I wasn’t paying attention.

The second change is more visible. When splitting #create out, I had to duplicate the six lines of setup code at the start of the method. Normally you don’t want to duplicate code at all, but in this case I am willing to take on a little technical debt now in order to move forward with my route refactoring. Ideally, you would refactor that setup code into a utility method first and then split the method.

With refactoring, you will always have trade offs. Sometimes you have to make decisions in the short term in order to have a better product in the long term. This refactoring shows one of those decisions I made.

(But, since Redmine is Open Source… I’d be happy to have a reader “fix” my refactoring and submit the patch to me. Don’t you want to say you contributed to the bug tracker that powers the Ruby language?)

Reference commit

About Eric Davis

I founded Little Stream Software where I help new entrepreneurs build a successful software business. I also created an ebook, Redmine Tips, where I show you how to become more productive using Redmine. I am also the author of Refactoring Redmine, where I go about refactoring Rails using Redmine as an example. Follow me on Twitter and Google Plus.

, , , ,

No comments yet.

Leave a Reply