Creating a Multiple Choice Quiz System, Part 2

 in
Designing our CGI quiz to be more robust and to include error checking.

Two months ago, we began to write a simple multiple-choice quiz engine in Perl. Virtually all of that column covered the nuts and bolts of the engine—creating the QuizQuestions object and the programs using that object to create a simple multiple-choice quiz, as well as to check its answers.

The end result was two CGI programs. The first, askquestion.pl, creates an instance of QuizQuestions and uses it to select a random question, which is then turned into an HTML form that is sent to the user's browser.

The other program, checkanswer.pl, accepts the submission of this form from the user, and then checks that the user chose the correct answer.

Even more important than the QuizQuestions object is the “quiz file”, an ASCII text file containing three different types of items:

  • Comments beginning with a hash character (#). Comments are ignored by the quiz engine. Therefore, questions must not begin with #, but we can use # inside a question or answer without having to fear that the end of the question will be chopped off.

  • Whitespace, such as spaces, tab characters and carriage returns that are also ignored. We allow for whitespace because users will undoubtedly separate items in the quiz file with blank lines, for example, and we need not require them to comment out the lines.

  • Question records containing the questions and answers for the quiz. Each record contains the text of a question, followed by each of the four possible answers, and then by an A, B, C or D, indicating the correct answer. The fields in each record (question, answer 1, answer 2, answer 3, answer 4 and the correct answer) are separated by tab characters, and so, neither questions nor answers can contain tabs.

A sample quiz file that tests users on their knowledge of the GNU Emacs text editor is shown in Listing 1. While this may not be obvious on paper, it is important to remember that the fields within each line are separated by tab characters, not by spaces.

One of the main flaws with the original quiz system was that it depended on the ability of users to create quiz files that conformed to these standards. Moreover, the QuizQuestions object didn't check for errors in format when reading the quiz file.

This month we take a look at how we can make the quiz system a bit more robust, while staying within the confines of the CGI standard.

Checking for Errors

First, we will modify the definition of QuizQuestions so that it checks for errors while loading the quiz file. What sorts of errors could we have it check for? One simple test ensures that each non-commented, non-whitespace line contains exactly six fields (one question, four answers and one answer key). Lines having a different number of fields will be flagged as errors.

Listing 2

The original version of QuizQuestions.pm is shown in Listing 2. To make sure that the quiz file is correct, we have to modify methods that read from the quiz file—which in this particular case, means the new method, the constructor for QuizQuestions. We can create a new instance of QuizQuestions with the following line:

my $quiz = new QuizQuestions("emacs");

Before we decide how to check for errors in the quiz file, we should think about how errors should be reported. If a method within QuizQuestions.pm discovers an error in the quiz file, should the method produce an HTML response for the user to see? Should it fail, calling die and indicating the error in the HTTP server's error log? Should it do both?

I suggest that QuizQuestions.pm should not use either of these options, since both violate the abstraction that we have created. QuizQuestions is an object for manipulating questions within a quiz file easily, and does not “know” whether it is being used from within a CGI program. Methods within QuizQuestions should report errors, when they occur, to the calling program rather than directly to the user.

If we were using a language such as Java that includes an extensive exception-handling mechanism, this would be a perfect time to use it; we don't want the calling routine to receive a return value that could be misinterpreted as a legitimate value for $quiz. At the same time, we do want to return information about any errors that have occurred.

Perl's exception handling isn't as extensive as that of Java. Luckily, though, Perl does permit assigning various types of data to the same operator. In this case, if the file contains no errors, new returns a new instance of QuizQuestions. If there are errors in the file, new returns a string that consists of the line containing the error. It could simply return 0 in such cases; however, since we have the flexibility to return any scalar value, it is better to return a value that encodes more information.

Now that we have determined that error messages will be sent back to the calling method, let's think about how to determine which lines in the quiz file contain errors. Fortunately, this is a simple problem to solve, since each non-comment, non-blank line of a quiz file should contain exactly six tab-separated fields. Thus, if a line is not a comment, is not an empty line and does not contain six fields, it must be an error and should generate an error value.

Here is the loop in the existing version of new inside the QuizQuestions object that loads the quiz file from disk:

# Loop through the question file while (<QUESTIONS>)
{
   next if /^#/;      # Ignore comment lines
   next unless /\w/;  # Ignore whitespace lines
   chomp;
   # Add this question to the list.
   $questions[$counter++] = $_;
}

To check for errors, we simply break each line into its constituent fields using the split operator and count the number of list elements. If that number is not six, then we have a syntax error to be reported by returning the offending string to the calling routine. Here is a modified version of the above loop that implements this strategy:

# Loop through the question fil,e
while (<QUESTIONS>)
 {
    next if /^#/;      # Ignore comment lines
    next unless /\w/;  # Ignore whitespace lines
    chomp;
    # Split the line across tabs
    my @list = split(/  /);
    # Check to make sure that there are six fields
    if ($#list != 5)
    {
        # Return the line containing the error
        return $_;
    }
    else
    {
        # Add this question to the list
        $questions[$counter++] = $_;
    }
}
This code is the same as the original while loop with only one difference. Before adding the current line, $_, to @questions (an array containing questions and answers from the quiz file), we split it at each tab, creating a list with one element per field in the quiz file. If the list contains six elements, then this line of the quiz file is acceptable, and we continue with the original version of new--adding the current line to @questions, incrementing $counter, and moving on to the next line of the file.

If the list does not contain six fields, the line obviously contains an error. By the time we perform this test, we have already eliminated the possibility that the current line could be a comment or solely contain whitespace.

But wait a second—the caller is expecting to receive an object of type QuizQuestions in return. Because the QuizQuestions object can return many different kinds of scalar data, we have to make sure that the caller can determine whether the method invocation was a success (i.e., an object was returned) or a failure (i.e., a string was returned).

In this case, we use Perl's ref operator to find out if a scalar is a reference to an object and what kind of object it is. Invoking ref on a non-object scalar returns an empty string, which makes such testing easy. So, in the above version of new, we can create an instance of QuizQuestions with this code:

my $questions = new QuizQuestions("emacs");
&log_and_die($questions) unless (ref($questions)
         eq "

The second line checks to see if $questions is an instance of QuizQuestions. If not, we call &log_and_die, a routine (included in in Listing 5) that provides nicer logging of errors than a simple call to die.

While this code works, it makes for a poorly designed object. After all, why write the constructor so that the caller has to test the type of the object it returned? A better solution is to make new a minimalist creation method, and put the quizfile-loading mechanism into another method, called loadFile. This new method could then return either 0 indicating no error or a string containing the offending line.

With such methods in place, we write:

my $questions = new QuizQuestions("emacs");
my $error = $questions->loadFile;
&log_and_die($error) if $error;

This code creates an instance of QuizQuestions using the new operator, which does only the bare essentials. We load quiz file with the loadFile method. The loadFile method returns either 0, indicating that the file was loaded successfully, or a text string containing the line that caused a problem.

Since we modified loadFile to deal with errors, I have replaced the original uses of die which are inappropriate in a low-level object, (as mentioned earlier), with calls to return.

Rewritten versions of new and loadFile are shown in Listing 3.

______________________

Webinar
One Click, Universal Protection: Implementing Centralized Security Policies on Linux Systems

As Linux continues to play an ever increasing role in corporate data centers and institutions, ensuring the integrity and protection of these systems must be a priority. With 60% of the world's websites and an increasing share of organization's mission-critical workloads running on Linux, failing to stop malware and other advanced threats on Linux can increasingly impact an organization's reputation and bottom line.

Learn More

Sponsored by Bit9

Webinar
Linux Backup and Recovery Webinar

Most companies incorporate backup procedures for critical data, which can be restored quickly if a loss occurs. However, fewer companies are prepared for catastrophic system failures, in which they lose all data, the entire operating system, applications, settings, patches and more, reducing their system(s) to “bare metal.” After all, before data can be restored to a system, there must be a system to restore it to.

In this one hour webinar, learn how to enhance your existing backup strategies for better disaster recovery preparedness using Storix System Backup Administrator (SBAdmin), a highly flexible bare-metal recovery solution for UNIX and Linux systems.

Learn More

Sponsored by Storix