Search code examples
ruby-on-railsstrong-parameterswicked-gem

Rails Wicked multi step form - param is missing or the value is empty vs unpermitted parameters


I am following this tutorial to create a multi-step form using the Wicked gem. Each User has one Answer (model to store answers to a set of questions), and I have a QuestionnaireController that steps through the questions that the user will answer and have stored in Answer. So far I have just two steps, each with one question (boolean columns on the answers table). When I step through and answer each question everything works perfectly, however when I submit without a value I get:

param is missing or the value is empty: answer

My code...

answer.rb:

class Answer < ApplicationRecord
  belongs_to :user

  cattr_accessor :questionnaire_steps do
    %w(step_one step_two)
  end

  attr_accessor :current_step #stores the current questionnaire step

  validates :user_id, presence: true

  with_options if: -> { required_for_step?(:step_one) } do |step|
    step.validates :question_one, inclusion: [true, false]
  end
  with_options if: -> { required_for_step?(:step_two) } do |step|
    step.validates :question_two, inclusion: [true, false]
  end

  def required_for_step?(step)
    # All fields are required if no questionnaire step is present
    return true if current_step.nil?
    # All fields from previous steps are required if the
    # step parameter appears before or we are on the current step
    return true if self.questionnaire_steps.index(step.to_s) <= self.questionnaire_steps.index(current_step)
  end
end

answers_controller.rb:

class AnswersController < ApplicationController
  before_action :setup

  def setup
    @user = current_user
  end

  def show
    @answer = @user.answer
  end

  def new
    @answer = Answer.new
    @answer = @user.build_answer
    @answer.save(validate: false)
  end

  def create
    @answer = @user.answer
    redirect_to questionnaire_path(@answer, Answer.questionnaire_steps.first)
  end
end

questionnaire_controller.rb:

class QuestionnaireController < ApplicationController
  include Wicked::Wizard

  before_action :setup

  steps *Answer.questionnaire_steps

  def setup
    @user = current_user
  end

  def show
    @answer = @user.answer
    render_wizard
  end

  def update
    @answer = @user.answer
    @answer.update_attributes(answer_params(step))
    render_wizard @answer
  end

  private

  def answer_params(step)
    permitted_attributes = case step
      when "step_one"
        [:question_one]
      when "step_two"
        [:question_two]
    end

    params.require(:answer).permit(permitted_attributes).merge(current_step: step)
  end
end

step_one.html.erb:

<div id="questionnaire" class="container-fluid well inline-headers">
  <%= form_for @answer, method: :put, url: wizard_path do |f| %>
  <div class="row">
    <% if f.object.errors.any? %>
      <div id="error_explanation" class="alert alert-dismissible alert-danger" >
        <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
        <ul>
          <% f.object.errors.each do |attribute, message| %>
            <%= content_tag :li, f.object.errors.full_message(attribute, message), :id => "error_#{attribute}" %>
          <% end %>
        <ul>
      </div>
    <% end %>
  </div>
  <div class="row">
    <div class="col-md-8 form-group">
      <p>Are you any good at this?</p>
    </div>
    <div class="col-md-4 form-group">
      <%= label_tag(:question_one, "Yes") %>
      <%= f.radio_button :question_one, true %>
      <%= label_tag(:question_one, "No") %>
      <%= f.radio_button :question_one, false %>
    </div>
  </div>
  <div class="row">
    <div class="col-md-4 col-md-offset-8 form-group">
      <p><%= f.submit "Save & Continue", class: "btn btn-warning" %></p>
    </div>
  </div>
  <% end %>
</div>

routes.rb (admittedly I tried to get cute with these to mask the IDs, feel free to shame me into doing this the right way):

resources :answers, only: [:show, :new, :create]
  get 'user/questionnaire', to: 'answers#show'
  get 'user/questionnaire/new', to: 'answers#new'
  post 'user/questionnaire/new', to: 'answers#create'

resources :questionnaire

When I select an option and submit, it saves to the database and redirects to the next step, however if I submit without selecting an option I get the param is missing error when I expected to get validation errors. From the server log:

Processing by QuestionnaireController#update as HTML Parameters: {"utf8"=>"���", "authenticity_token"=>"J45sG/YIqTaBECgBqNLr9Ys3rJewDK78d/cKHWFHYEIzAaOiTXhgH6evPZKl5LhnSAMnQnA94CXonqriyj0fCQ==", "commit"=>"Save & Continue", "id"=>"step_one"}

I have tried a number of potential fixes, including removing require(:answer) from params.require(:answer).permit(permitted_attributes).merge(current_step: step), which gets me the validation error I was expecting but then doesn't allow saving a valid value. In that scenario I still get the validation error with the following in the server log:

Started PUT "/questionnaire/step_one" for ::1 at 2016-12-08 23:26:14 +0545 Processing by QuestionnaireController#update as HTML
Parameters: {"utf8"=>"���", "authenticity_token"=>"RWAHfu0BE82ik6gTChh3nmtuyOUa5j0qiz4H03ejIkRR78jHVnHa5IQsvYAHLiQMqFpDMNrXc/MUV6cs3NldDw==", "answer"=>{"question_one"=>"true"}, "commit"=>"Save & Continue", "id"=>"step_one"} ...SQL... Unpermitted parameters: utf8, _method, authenticity_token, answer, commit, id

Many thanks in advance for helping out a rookie.

UPDATE: I modified my routes to match the tutorial (nested) thinking this might have something to do with it (i.e. maybe not getting the answer ID) but I'm still getting the param is missing error.

resources :answers, only: [:show, :new, :create] do
    resources :questionnaire, only: [:show, :update], controller: 'questionnaire'
  end
  get 'user/questionnaire', to: 'answers#show'
  get 'user/questionnaire/new', to: 'answers#new'
  post 'user/questionnaire/new', to: 'answers#create'

I also tried adding a hidden_field_tag before the submit, like so:

<%= hidden_field_tag(:answer, @answer) %>

But then I get:

undefined method `permit' for #

Probably because the hidden_field_tag passes @answer.attributes as a string when the strong parameter declaration expects a hash. From the server:

Started PUT "/answers/46/questionnaire/step_one" for ::1 at 2016-12-12 13:25:19 +0545 Processing by QuestionnaireController#update as HTML Parameters: {"utf8"=>"���", "authenticity_token"=>"wlLpKoMvqgyfwB5gyP/qubR4ZpZa6jm9KHeMuiwJTc6IOJZi9PgiSegvF+OzlN09ctEuxuxiAuRs+/B2InRGhA==", "answer"=>"{\"id\"=>46, \"user_id\"=>4, \"question_one\"=>nil, \"question_two\"=>nil, \"percentage_complete\"=>#, \"completed\"=>false, \"created_at\"=>Mon, 12 Dec 2016 02:12:31 EST -05:00, \"updated_at\"=>Mon, 12 Dec 2016 02:12:31 EST -05:00}", "commit"=>"Save & Continue", "answer_id"=>"46", "id"=>"step_one"}

Please help!


Solution

  • I changed params.require to params.fetch and that works (validations are triggered appropriately and I can go through the steps with valid values successfully as well).