Implementing Resumable Sessions

only for RuBoard - do not distribute or recompile

One benefit provided by sessions is that they enable you to implement activities that can be suspended for a while and then resumed later when the user is ready to continue. This is a common thing to do with long-running games that users may play over the course of several days. Resumable sessions are also useful for presenting an activity in discrete segments: The user completes the activity one piece at a time, but can break off at any point and continue later. If you re asking users to complete a lengthy survey, for example, you can make the experience more palatable by not requiring that the entire thing be completed in a single sitting. The user who is interrupted or needs to do something else while answering questions can suspend the survey and then return to it later at a more convenient time. (Of course, some users may forget to complete their form. You might want to solicit an email address at the beginning of the process and store it along with a timestamp in the session record so that you can send a reminder message to users who seem not to have made any progress on their surveys for a while.)

In this section, we ll implement an application survey.pl that presents a survey conducted over several pages. For simplicity, we ll present only three pages and ask only one question per page, where each page contains a multiple-choice question and two buttons, Continue and Finish Later. The user selects Continue to submit the answer for the current question and continue on to the next page. Finish Later suspends the survey; if the user selects this button, the script presents a page containing instructions that indicate how to resume the survey later.

survey.pl uses a session record to track the progress of the survey. As the user submits answers to questions, the application adds them to the session record. When the user completes the survey, the script prints a thank-you message, moves the answers from the session record to a survey table designed to record completed surveys, destroys the session, and tells the browser to delete the session cookie.

To represent the survey questions, survey.pl uses the following array, where each question corresponds to a hash with three elements. In each hash, the question element is the question to present to the user, answers lists the possible alternatives the user can choose from, and name indicates the column in the database table where we ll store the results when the user has finished:

 my @question =  (     {         question => "Which of these foods do you like best?",          answers => ["hamburgers", "hot dogs", "bratwurst"],          name => "food"      },      {         question => "Do you put ketchup on your %food%?",          answers => ["yes", "no", "sometimes", "I prefer tabasco sauce"],          name => "ketchup"      },      {         question => "How many %food% do you usually eat at one sitting?",          answers => ["1", "2", "3", "more than 3!"],          name => "quantity"      }  ); 

The survey table used to hold completed surveys looks like this:

 CREATE TABLE survey  (     id          INT NOT NULL AUTO_INCREMENT PRIMARY KEY,      food        CHAR(25),      ketchup     CHAR(25),      quantity    CHAR(25)  ) 

For this application, the questions are coded right into the script, and the layout of the survey table is tightly tied to the questions. For greater flexibility, you d probably want to use the database to store information about the questions to be asked, generate the question forms on the basis of table lookups, and store completed survey responses using a more general table structure.

Our application uses a simple mechanism for tailoring questions according to the responses to previous questions. When a question contains a sequence of the form %xxx %, survey.pl looks for another question with a name value of xxx , and then replaces %xxx% with the answer to that question. Thus, when the script asks the second and third questions, it replaces the instances of %food% in those questions with the user s answer to the first question. If the user answers bratwurst to the first question, for example, the second question becomes Do you like ketchup on your bratwurst?

As usual, our script begins by checking for a session-identifier cookie and opens the corresponding session. The code is similar to that used for earlier applications; there s no need to show it here. Then we initialize the session if it s a new one:

 if (!defined ($sess_ref->attr ("index")))  {     $sess_ref->attr ("index", 0);      $sess_ref->attr ("answers", { });   # hash to hold answers      $sess_ref->attr ("suspended", 0);  } 

The session record has several elements. index indicates how many questions the user has answered so far; the script uses this value to figure out which question to present on any given page. answers is a reference to a hash that holds the answers that the user has submitted. suspended is something of a frill. We ll set it to a non-zero value if the user selects the Finish Later button, to indicate that the session is being suspended. When the user resumes the survey, we ll notice the non-zero value and add a little Welcome Back message to the page:

 if ($sess_ref->attr ("suspended"))  {     $page .= p ("Welcome Back.");      $sess_ref->attr ("suspended", 0);  } 

The dispatch logic checks which question is current and performs the appropriate action. For a session that is just beginning (or that has been resumed after having been suspended), we just display the current question. If the user submitted an answer, we record it and advance to the next question (or finish up if the user has completed the survey). If the user wants to finish the survey later, we mark the session suspended and display some instructions indicating how to resume the survey later:

 my $choice = lc (param ("choice")); # get choice, lowercased  my $cur_q = $question[$sess_ref->attr ("index")];   # info for current question  if ($choice eq "")              # present question  {     $page .= display_question ($sess_ref, $cur_q);      $sess_ref->close ();  }  elsif ($choice eq "submit")     # record answer, present next question  {     $page .= process_answer ($sess_ref, $cur_q);      if ($sess_ref->attr ("index") < @question)  # any more questions?      {         $cur_q = $question[$sess_ref->attr ("index")];          $page .= display_question ($sess_ref, $cur_q);          $sess_ref->close ();      }      else                                        # no more questions      {         # store responses in database, destroy session, create cookie          # that tells browser to delete the one it's storing, and thank          # the user          save_answers ($sess_ref);          $sess_ref->delete ();          $cookie = cookie (-name => "sess_id",                              -value => $sess_ref->session_id (),                              -path => url (-absolute => 1),                              -expires => "-1d");     # "expire yesterday"          $page .= p ("Hey, thanks for participating.");      }  }  elsif ($choice eq "finish later")       # suspend session  {     $sess_ref->attr ("suspended", 1);   # mark session as suspended      $sess_ref->close ();      $page .= p ("To be continued.")     # give user instructions for resuming              . p ("You can resume by selecting\n"                      . a ({-href => url ()}, "this link")                      . ", or by visiting the following URL: ")              . p (url ());  }  else  { error ("Logic error, unknown choice: $choice");  } 

The display_question() function generates the pages that present each question to the user. The most noteworthy thing here is that the answers to previous questions are consulted as necessary to resolve any references to them that appear in the question string:

 sub display_question  { my ($sess_ref, $question) = @_;  my ($answers, $str, $page);      # Get question to ask, then substitute in previous answers      # if sequences of the form %xxx% are found, where xxx is      # the name associated with the answer.      $answers = $sess_ref->{answers};      $str = $question->{question};      $str =~ s/%([^%]+)%/$answers->{$1}/ while $str =~ /%[^%]+%/;      $page .= start_form (-action => url ())          . p (($sess_ref->attr ("index") + 1) . ". $str")    # question          . radio_group (-name => "answer",                   # possible answers                          -values => $question->{answers},                          -default => "[NO DEFAULT]",                          -linebreak => 1)          . br () . br ()          . submit (-name => "choice", -value => "Submit")          . " "          . submit (-name => "choice", -value => "Finish Later")          . end_form ();      return ($page);  } 

Each time the user submits an answer by selecting the Continue button, process_answer() checks the form contents. If the user didn t actually select one of the alternatives, we issue a reminder and leave the index counter unchanged so that display_question() will present the same question again. Otherwise, we add the new answer to the session record and increment the counter to go to the next question:

 sub process_answer  { my ($sess_ref, $question) = @_;  my $answer = WebDB::trim (param ("answer"));  my $page;      if ($answer eq "") # user didn't select any answer; display reminder      {         $page .= p ("Please answer the question.");      }      else      {         # Add new answer to those that already have been recorded; each          # answer is keyed to the name of the "survey" table column in          # which it will be stored at survey completion time. Then          # increment question index so next question gets displayed.          my $answers = $sess_ref->attr ("answers");          $answers->{$question->{name}} = $answer;          $sess_ref->attr ("answers", $answers);          # go to next question          $sess_ref->attr ("index", $sess_ref->attr ("index") + 1);      }      return ($page);  } 

Finally, after the user has completed the survey, we call save_answers() to extract the responses from the session record and store them in the survey table:

 sub save_answers  { my $sess_ref = shift;  my $answers = $sess_ref->attr ("answers");  # the user's answers  my ($dbh, $stmt, @placeholder);      # the keys of the answers hash are the column names in the "survey"      # table in which to store the answers      foreach my $key (keys (%{$answers}))      {         $stmt .= "," if $$stmt;     # put commas between assignments          $stmt .= "$key = ?";        # add column name, placeholder          push (@placeholder, $answers->{$key});  # save placeholder value      }      $stmt = "INSERT INTO survey SET $stmt"; # complete the statement      $dbh = WebDB::connect ();      $dbh->do ($stmt, undef, @placeholder);      $dbh->disconnect ();  } 

Suggested Modifications

Implement a Start Over button that forgets all the previous answers and begins again with the first question.

Implement a Go Back button that enables the user to back up and change the answers to previous questions.

only for RuBoard - do not distribute or recompile


MySQL and Perl for the Web
MySQL and Perl for the Web
ISBN: 0735710546
EAN: 2147483647
Year: 2005
Pages: 77
Authors: Paul DuBois

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net