Ruby on rails controller code, needs refactor best way to approach for more dry?

天涯浪子 提交于 2019-12-08 01:46:47

问题


I have a welcome wizzard that builds a user profile when first login.Problem is it is quite messy implemented but I have tried to refactor it several times and rewrite it but cannot comeup with something better then below.

In ideal world it would be all inside welcome_controller.rb but this have caused much headache so Now i rewrote the update method for profile_controller instead.

Any thoughts on how to improve this make it more dry and clean? Would love to recieve some good input on this and thoughts perhaps to move all update stuff to welcome controller instead?

WelcomeController:

class WelcomeController < ApplicationController

  before_filter :authenticate_user!
  before_filter :load_step

  layout "welcome"

  def sub_layout
   "center"
  end

  def edit
    # form updates post to edit since
    # profile is non existant yet

    params[:step] = "photos" unless params[:step]

    @photos   = Photo.where(:attachable_id => current_user.id)
    @profile  = Profile.where(:user_id => current_user.id).first
    @photo    = Photo.new

    if ["photos", "basics", "details", "test"].member?(params[:step])

      # force rendering the correct step
      case current_user.profile.step
        when 1
          render :template => "/profiles/edit/edit_photos", :layout => "welcome"
        when 2
          render :template => "/profiles/edit/edit_basics", :layout => "welcome"
        when 3
          render :template => "/profiles/edit/edit_details", :layout => "welcome"
        when 4
          render :template => "/profiles/edit/edit_test", :layout => "welcome"
      end

    else
      render :action => "/profiles/edit/edit_photos"
    end
  end

  def load_step

    redirect_to root_path if current_user.profile.complete

    case current_user.profile.step
      when 1
        redirect_to "/welcome" unless params[:controller] == "welcome"
      when 2
        redirect_to "/welcome/basics" unless params[:controller] == "welcome" && params[:action] == "edit" && params[:step] == "basics"
      when 3
        redirect_to "/welcome/details" unless params[:controller] == "welcome" && params[:action] == "edit" && params[:step] == "details"
      when 4
        redirect_to "/welcome/test" unless params[:controller] == "welcome" && params[:action] == "edit" && params[:step] == "test"
    end
  end

end

ProfileController:

class ProfileController < ApplicationController

...
 def update
    @profile        = Profile.find(params[:id])
    @tags           = Session.tag_counts_on(:tags)
    @profile.form   = params[:form]
    @match          = Match.where(:user_id => current_user.id).first
    authorize! :update, @profile

    respond_to do |format|

      if @profile.update_attributes(params[:profile])

        if current_user.profile.complete
          format.html { redirect_to "/profiles/#{ current_user.username }/edit/#{ @profile.form }", notice: t('notice.saved') }
        else
          case current_user.profile.step
            when 1
              current_user.profile.update_attributes(:step => 2)
              format.html { redirect_to "/welcome/basics", notice: t('notice.saved') }
            when 2
              current_user.profile.update_attributes(:step => 3)
              format.html { redirect_to "/welcome/details", notice: t('notice.saved') }
            when 3
              current_user.profile.update_attributes(:step => 4)
              format.html { redirect_to "/welcome/test", notice: t('notice.saved') }
          end
        end

      else

        if current_user.profile.complete
          format.html { render action: "/edit/edit_" + params[:profile][:form], :what => @profile.form }
        else
          case current_user.profile.step
            when 1
              current_user.profile.update_attributes(:step => 2)
              format.html { redirect_to "/welcome/basics", notice: t('notice.saved') }
            when 2
              current_user.profile.update_attributes(:step => 3)
              format.html { redirect_to "/welcome/details", notice: t('notice.saved') }
            when 3
              current_user.profile.update_attributes(:complete => 1)
              format.html { redirect_to root_path }
          end
        end
      end

    end

  end
  ...
  end

Views are in /profiles/edit/*


回答1:


Wizards are notoriously difficult to get right and I've never seen an implementation that fully satisfied me. I usually go with so called "form objects" and create a restful controller for each step.

There is an excellent (but paid) Railscast on the subject.

The gist is this: You make an object that quacks just like a regular ActiveRecord model, by using ActiveModel.

For instance:

class Welcome::BasicInformation
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming

  def persisted?
    false
  end

  def initialize(user)
    @user = user
  end

  attr_reader :user

  delegate :some_field, :some_other_field, to: :user

  validates_presence_of :some_field

  def save(params)
    user.some_field = params[:some_field]
    user.some_other_field = params[:some_other_field]
    if valid?
      user.step = 2
      user.save
    end
  end

  def photo
    @photo ||= Photo.new
  end

  def profile
    @profile ||= user.profiles.first
  end

end

You'd basically create a model like this for every step.

Then you can create controllers for each step, with a specialized ApplicationController for all the steps:

class Welcome::ApplicationController < ::ApplicationController

  layout "welcome"
  before_filter :authentice_user!

end

And for each step:

class Welcome::BasicInformationsControlller < Welcome::ApplicationController

  def new
    @step = Welcome::BasicInformation.new(current_user)
  end

  def create
    @step = Welcome::BasicInformation.new(current_user)
    if @step.save(params[:welcome_basic_information])
      redirect_to welcome_some_other_step_path, notice: "Yay"
    else
      render :new
    end
  end

end

And create a route for each step:

namespace :welcome do
  resource :basic_information, only: [:new, :create]
  resource :some_other_step,   only: [:new, :create]
end

This only leaves some automatic redirects to do, like prohibiting users from going to steps that they're not yet allowed to visit. This might not be as important now that you're using separate URLs for each step.

You can store information about which step to visit in the form objects:

class Welcome::BasicInformation
  # ...
  def allowed?
    user.profile.step == 1
  end
end

And then refactor the controllers a bit:

class Welcome::BasicInformationsController < Welcome::ApplicationController

  before_filter :allowed?

  def new
  end

  def create
    if step.save(params[:welcome_basic_information])
      redirect_to welcome_some_other_step_path, notice: "Yay"
    else
      render :new
    end
  end

  private

  def step
    @step ||= Welcome::BasicInformation.new(current_user)
  end
  helper_method :step

  def allowed?
    redirect_to previous_step_path unless step.allowed?
  end

end

This might not be shorter, but I do like how flexible it is, how focussed each step is, how you can do different validations on each step and so on. Each controller/model combination is very easy to follow and will be understandable for others.




回答2:


There are a couple of things I'd do, but first some thoughts.

  1. Sometimes you have to break restfullness a little to make code more readable. That's the case
  2. It's not a good manner to redirect between controllers as you do in here

So, what I'd do.

  1. Put all the code concerning those steps in a single controller (profile preferably) and adjust url with routing.
  2. Create a single show and single save action

If I understand properly the step that will be shown to user depends ONLY on what User#step is set on current_user. Threfore I think there's really no need to pass any url variables, you can get current/next step from current_user.

Code refactored (may be some errors, didn't test that) All in ProfileController

  def edit
    @profile  = Profile.find(current_user.id)
    @next_step = current_user.step.to_i + 1 # I imply that there's just single permissable next step
    render :template => "/profiles/edit/#{@next_step}", :layout => "welcome"
  end

  def update
    @profile = Profile.find(params[:id])
    authorize! :update, @profile

    if @profile.update_attributes(params[:profile])
      # you should pass step number in params so I get's updated by default.
      redirect_to "/welcome/basics", notice: t('notice.saved')
    else

    end
  end


来源:https://stackoverflow.com/questions/17254123/ruby-on-rails-controller-code-needs-refactor-best-way-to-approach-for-more-dry

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!