Tracking Information with Sessions

Problem

You want to maintain state across several web pages of an application without using a database.

Solution

Use Rails's built-in sessions to maintain state across multiple pages of a web application, such as the state of an online quiz.

Create an online quiz that consists of a sequence of questions, one per page. As a user proceeds through the quiz, her score is added to the total. The last screen of the quiz displays the results as the number correct out of the total number of questions.

Create a Quiz Controller that includes a data structure to store the questions, optional answers, and correct answers for each question. The controller contains methods for displaying each question, checking answers, displaying the results, and starting over.

app/controllers/quiz_controller.rb:

class QuizController < ApplicationController
 @@quiz = [
 { :question => "What's the square root of 9?",
 :options => ['2','3','4'],
 :answer => "3" },
 { :question => "What's the square root of 4?",
 :options => ['16','2','8'],
 :answer => '16' },
 { :question => "How many feet in a mile?",
 :options => ['90','130','5,280','23,890'],
 :answer => '5,280' },
 { :question => "What's the total area of irrigated land in Nepal?",
 :options => ['742 sq km','11,350 sq km','5,000 sq km',
 'none of the above'],
 :answer => '11,350 sq km' },
 ]
 def index
 if session[:count].nil?
 session[:count] = 0
 end
 @step = @@quiz[session[:count]]
 end
 def check
 session[:correct] ||= 0
 if params[:answer] == @@quiz[session[:count]][:answer]
 session[:correct] += 1
 end
 session[:count] += 1
 @step = @@quiz[session[:count]]
 if @step.nil?
 redirect_to :action => "results" 
 else
 redirect_to :action => "index" 
 end
 end
 def results
 @correct = session[:correct]
 @possible = @@quiz.length
 end
 def start_over
 reset_session
 redirect_to :action => "index" 
 end end

Create a template to display each question along with its optional answers:

app/views/quiz/index.rhtml:

<h1>Quiz</h1>
<p><%= @step[:question] %></p>
<% form_tag :action => "check" do %>
 <% for answer in @step[:options] %>
 <%= radio_button_tag("answer", answer, checked = false) %>
 <%= answer %>;
 <% end %>
 <%= submit_tag "Answer" %>
<% end %> 

At the end of the quiz, the following view displays the total score along with a link prompting to try again:

app/views/quiz/results.rhtml:

<h1>Quiz</h1>
<p><strong>Results:</strong>
 You got <%= @correct %> out of <%= @possible %>!</p> 
<%= link_to "Try again?", :action => "start_over" %>

Discussion

The Web is stateless, which means that each request from a browser carries all the information that the server needs to make the request. The server never says, "Oh, yes, I remember that your current score is 4 out of 5." Being stateless makes it much easier to write web servers but harder to write complex applications, which often need to remember what went before: they need to remember which questions you've answered, what items you've put in your shopping cart, and so on.

This problem is solved by the use of sessions. A session stores a unique key as a cookie in the user's browser. The browser presents the session key to the server, which can use the key to look up any state that it has stored as part of the session. The Web interaction is stateless: the HTTP request includes all the information needed to complete the request. But that information contains information the server can use to look up information about previous requests.

In the case of the quiz, the controller checks the answers to each question and maintains a running total, storing it in the session hash with the :correct key. Another key in the session hash is used to keep track of the current question. This number is used to access questions in the @@quiz class variable, which stores each question, its possible answers, and the correct answer in an array. Each question element consists of a hash containing all the information needed to display that question in the view.

The index view displays a form for each question and submits the user's input to the check action of the controller. Using session[:count], the check action verifies the answer and increments session[:correct] if it's correct. Either way, the question count is incremented, and the next question is rendered.

When the question count fails to retrieve an elementor questionfrom the @@quiz array, the quiz is over, and the results view is rendered. The total correct is pulled from the session hash and displayed with the total number of questions, which is determined from the length of the quiz array.

A quiz such as this lends itself reasonably well to the convenience of session storage. Be aware that sessions are considered somewhat volatile and potentially insecure, and are usually not used to store critical or sensitive information. For that type of data, a traditional database approach makes more sense.

shows the four steps of the session-driven online quiz.

Figure 4-3. An online quiz saving state with sessions

Rails session support is on by default. As the solution demonstrates, you can access the session hash as if it's just another instance variable. If your application doesn't need session support, you can turn it off for a controller by using the :disabled option of Action Controller's session method in the controller's definition. The call to disable session support for a controller may also include or exclude specific actions within a controller by passing a list of actions to session's :only or :except options. The following disables session support for the display action of the News Controller:

class NewsController < ActionController::Base
 session :off, :only => "display" 
end

To turn session support off for your entire application, pass :off to the session method within your ApplicationController definition:

class ApplicationController < ActionController::Base
 session :off end

See Also