1
\$\begingroup\$

I have created a quiz function in my rails application. Since I want to track responses and make it a server-side application I created a table to record the responses of the questions people answer.

create_table "responses", force: :cascade do |t|
    t.bigint "question_id"
    t.bigint "answer_id"
    t.bigint "user_id"
    t.uuid "uuid", default: -> { "uuid_generate_v4()" }
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["answer_id"], name: "index_responses_on_answer_id"
    t.index ["question_id"], name: "index_responses_on_question_id"
    t.index ["user_id"], name: "index_responses_on_user_id"
  end

Each question can have 4 possible answers association with them. I want to create and check the response directly after the user clicks on it. To do this I created this bit of logic here.

<h1><%= @question.prompt %></h1>
<% @question.answers.each do |a| %>
    <%= form_for @response, url: send_wager_response_path, remote: true do |f| %>
        <%= f.hidden_field :user_id, value: current_user.id %>
        <%= f.hidden_field :question_id, value: @question.id %>
        <%= f.hidden_field :answer_id, value: a.id %>
        <%= f.submit "#{a.answer.titleize}", class: 'btn btn-block btn-lg btn-primary' %>
    <% end %>   
<% end %>

The problem I have with this is that I am creating 4 different forms on the question page and passing the current user id, the answer id they select, and the question id. This seems a bit not-so DRY. Is there a way to improve on this concept?

I am currently looking into a action-cable based approach since this is a head-to-head application but this is a good place to start.

\$\endgroup\$
0
\$\begingroup\$

Why do you submit the current user id in the form? I assume the user is logged in by the statement current_user.id in the form. If you're placing the current user id into the form, you allow the user to change the value (assuming you use the submitted user id in your handler). I know the field is hidden, but everything that an client submits can be manipulated. The only thing to do is open up the developer tools and alter the HTML source. Instead of submitting the id through the form I recommend simply using current_user in the handler and don't submit the user id at all.

You mention you're looking into Action Cable at the moment. However Action Cable is based on the web-socket technology. This could be used, but the main reason they where invented is to keep the connection between the server and client open so the server can send data to the client without the client making a request first. This is mostly done when something changed on the server that has impact on the current view of the client. The term for this would be push notifications or messages.

Since the view doesn't have to change when something changed on the server there is no need to make things more complicated. Instead you want to update the responses when user selects an answer (user initiated). The solution is to update the user response on change (using JavaScript). Rails has a build in system that does this documented in the Ruby on Rails Guides - Working with JavaScript in Rails: 3.2 Customize remote elements.

Let me provide you with an (untested) example:

First we ensure that the correct routes are present so we can update the response in a reasonable fashion.

# config/routes.rb
# ...
resources :questions, only: [] do
  member do
    post 'answer', to: 'responses#answer'
  end
end
# ...

This should create the route /questions/:id/answer that will be handled in ResponsesController#answer. This route should create a helper named answer_question that accepts an question or question id as its first argument.

The controller might look something like this:

# app/controllers/responses_controller.rb
# ...
def answer
  question = Question.find(params[:id])
  answer   = question.answers.find(params[:answer_id])
  response = question.responses.find_or_initialize_by(user: current_user)

  if response.update(answer: answer)
    head :ok
  else
    render 'answer_error'
    # renders /app/views/responses/answer_error.js.erb
    # Here you can write some JS to update the DOM and sets an error
    # message somewhere.
  end
end
# ...

And now for the final part of the puzzle we need the view.

<% # I don't know which view this is, but it replaces the one that OP provided. %>
<h1><%= @question.prompt %></h1>
<% @question.answers.each do |question| %>
  <%= form_for @response, url: answer_question_path(@question), method: :post, remote: true do |f| %>
    <%= f.hidden_field :answer_id, value: answer.id %>
    <%= f.submit a.answer.titleize, class: 'btn btn-block btn-lg btn-primary' %>
  <% end %>
<% end %>

By having an nested route you eliminate the need to submit the question id. And the user id can be left out for reasons mentioned before.

From the tone in your post I assume you also want to drop those forms. This is possible by using the "Customize remote elements" that I linked above. This would leave you with a view that might look something like this.

<h1><%= @question.prompt %></h1>
<% @question.answers.each do |answer| %>
  <%= radio_button_tag "question_#{question.id}_answer", answer.id, false, data: {
    remote: true,
    method: :post,
    url:    answer_question_path(@question),
    params: { answer_id: answer.id }
  } %>
<% end %>

The above would simply generate radio buttons (using radio_button_tag) that are linked to each other (per question). On change a request will be send to the specified URL. This eliminates the need for a form altogether. Keep in mind that no labels are currently present for the radio buttons, you'll have to add them yourself.

Of course you could also use other input HTML input elements to achieve the same result. If you want to stick to the button approach I recommend taking a look at button_tag in combination with the technique shown above. Or button_to which is an helper that generates the wrapping form for you.

As I said, this is an untested example. Might any errors come up let me know in the comments.

\$\endgroup\$
  • \$\begingroup\$ button_tag was causing issues with params. If you are building a similar function, I recommend using button_to so you don't run into issues with data params. \$\endgroup\$ – Trenton Tyler Dec 29 '18 at 17:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.