return to first page linux journal archive
keywordscontents

WWWsmith

At the Forge

Creating a Multiple Choice Quiz System, Part 2

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

by Reuven M. Lerner

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:

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(/\t/);

    # 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.

Creating the Quiz File

So far, we have dealt with ways in which the QuizQuestions object can handle syntax errors within the quiz file. But many syntax errors are created simply by mistake or by users unfamiliar with the defined file format.

One solution is to provide users with tools for creating quiz files with fewer errors. Given the amount of time we spend writing CGI programs and HTML forms, it makes sense to create a short program that takes the contents of an HTML form and saves it to disk.

An example of one such form is shown in Listing 4. Upon submission, the form's contents are handed to create-quizfile.pl, which then creates a properly formatted quiz file.

In order to implement this feature, we need to add two new methods to QuizQuestions. One, addQuestion, takes a six-element list and adds it to questions, the instance variable containing fields from the quiz file. The second method, saveFile, does the opposite of loadFile, taking the current questions and saving them.

Here is one possible implementation of addQuestion:

sub addQuestion
{
    # Get ourselves
    my $self = shift;

    # Get our arguments
    my ($question, $a1, $a2, $a3, $a4, 
		    $correct) = @_;

    # Turn our arguments into a string
    my $new_question = join("\t", @_);

    # Get our instance variable
    my @questions = @{$self->{"questions"}};

    # Add the new question
    push (@questions, $new_question);

    # Reset the instance variable
    $self->{"questions"} = \@questions;

    # Return successfully (= 0)
    return 0;
}
This version of addQuestion is fairly simple, if not very robust. For instance, it doesn't check to make sure the correct answer is one of A, B, C or D. But it does let us add new questions to the QuizQuestions object. Notice that addQuestion both retrieves and sets values for the instance variable questions.

If we were interested in extending our quiz on Emacs, we could use addQuestion in the following way:

my $error = $questions->loadFile;
&log_and_die($error) if $error;
$questions->addQuestion(
"What term describes the cursor's current location?",
  "mark", "point", "cursor", "mouse", "B");
Immediately after executing this code, $questions contains one more question. However, this question is lost upon the program's exit, because we have not yet saved the new question to the quiz file. In order to save the questions to a quiz file, define saveFile like this:

sub saveFile
{
    # Get ourselves
    my $self = shift;

    # Open the questions file for writing
    open (QUESTIONS, ">$questionDir" . 
		    $self->{"quizname"}) ||
        return "Could not open " . 
		    $self->{"quizname"} . " for writing";

    # Loop through the questions
    my @questions = @{$self->{"questions"}};

    my $question;
    for each $question (@questions)
    {
        print QUESTIONS $question, "\n";
    }
    close(QUESTIONS);
    return 0;
}
This code iterates through the questions, and writes them to the quiz file. Since we are writing all of the questions to disk rather than appending them, we use the > when opening the file, thereby overwriting any data that existed previously.

Since saveFile saves only the contents of the questions instance variable, it effectively obliterates comments and white space in the file. Of course, anyone creating the quiz file using a program is unlikely to look at the comments. Nonetheless, a more refined version of saveFile and the QuizQuestions object might let users add comments and white space to the file, as well as questions. (Obviously, the HTML form would also have to allow for this.)

Our version of saveFile uses the same system for reporting errors as loadFile--by returning a string, while the lack of an error is indicated by returning 0. This lets us use the following code:

$error = $questions->saveFile;
&log_and_die($error) if $error;
Now that you have seen the skeleton for create-quizfile.pl, you should have a good understanding of the program shown in Listing 5. This version of create-quizfile.pl is fairly straightforward. It checks to see if the user entered a question; if there is text for a question, it takes the remaining parameters from the HTML form submitted.

Now is a good time to remember that CGI programs that write user-defined strings to your file system are potentially dangerous, and thus must be placed in locations that are restricted to authorized users, either by using your HTTP server's built-in protection or by placing such programs behind a firewall. No matter how unlikely this may seem, a user may eventually discover that you have a program named create-quizfile.pl, and create quizzes on your system, possibly overwriting your creations.

This month, we made our quiz engine friendlier for non-programmers by checking the integrity of the quiz file and by allowing users to create quiz files using HTML forms. What happens when users want to edit quiz files? For now, they are stuck modifying the file on disk, which again opens Pandora's box of potential syntax problems. While we can discover these problems with our simple error-checking code, it might be a good idea to create a program that can edit quiz files as well as create them. Next month, we will modify create-quizfile.pl to do just that, making our quiz system easier for everyone to handle.

Reuven M. Lerner is an Internet and Web consultant living in Haifa, Israel, who has been using the Web since early 1993. In his spare time, he cooks, reads and volunteers with educational projects in his community. He can be reached via e-mail at reuven@netvision.net.il.