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

只谈情不闲聊 提交于 2019-12-06 06:52:31

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.

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