Creating a Multiple Choice quiz System, Part 3
Last month, we continued looking at ways to improve the user interface of our CGI quiz engine. This engine is based on the QuizQuestions.pm Perl module and allows us to create a number of different multiple-choice quizzes. Each quiz is stored in a separate ASCII text file on the server's file system, and thanks to the abstraction layer provided by the QuizQuestions object, we are able to ignore the way in which the information is stored and focus on the quiz itself.
Or can we? As we saw last month, it is relatively easy for us to create a quiz using HTML forms and a CGI program. Indeed, we created such a program without much trouble; still, as I pointed out at the end of last month's column, our job is only half done. Using an HTML form and CGI program simplifies the process of creating quiz files and reduces the potential for error. However, of someone wants to modify a quiz that he has already created, he still has to understand our file format and make sure not to disturb it. It would be nice to have a single program which allowed users to create and modify their quizzes, saving them from having to work directly with quiz files.
Such a program would be desirable for its error-free, user-friendly quiz creation. We use text files to store information with tabs or other special characters separating fields within a given record. Fewer problems will ensue if we can provide a graphical interface that restricts the type of data that our users can input. In addition, a quiz editor would empower the designers and producers on our site—who, at an increasing number of sites, are kept separate from the technical staff responsible for creating CGI programs and keeping the network up and running. While you might be an experienced system administrator who knows the difference between tabs and spaces and doesn't flinch before editing /etc/fstab or a Makefile, most content-oriented people on a site are not experienced users. If we can provide a tool that creates and modifies quizzes without the need to learn Emacs, come to us with questions or tear their hair out in frustration, then why not?
Last month, we looked at a simple program, create-quizfile.pl, which took information from an HTML form and correctly formed it into a quiz file. This month, we take the basic idea behind that quiz creator and look at how we can write a quiz editor. That is, our new program will allow us to create quizzes, just as the previous one did, but will also let us edit quizzes by modifying the text of questions and answers or deleting existing questions from the quiz file.
From the above description it seems as though our basic unit of operation is a line of text in the quiz file that will contain a question. The easiest way to handle these data is to keep track of them in an array of strings, which we call @lines. This means $lines[0] is the first non-comment, non-whitespace line in the quiz file—in other words, the first set of questions and answers. For example, to find the third answer to the fifth question, we call up $lines[4], split it across tabs using Perl's “split” operator, and read the fourth element of that array. Each line of the quiz file is of the format question answer1 answer2 answer3 answer4 correctAnswer, with each field separated from its neighbor by a tab character. Each line of the quiz file can be represented by a string containing the tabs or by a list created by splitting the string across the tab character.
Luckily, we don't have to think much about the format of the quiz file, thanks to the QuizQuestions object module we have used so far. When we create a new instance of QuizQuestions, we effectively create a new, blank quiz. To add questions to that quiz, we use the addQuestion method, which expects to receive six arguments—not surprisingly, the same arguments as appear on each line of the quiz file.
To create a brand-new quiz our program, called edit-quiz.pl, must create an HTML form containing elements into which the user can enter one or more questions and answers. To edit an old quiz file, edit-quizfile.pl has to create an instance of QuizFile corresponding to a quiz file already existing on disk. It then must read the questions from that quiz file, turning each question into a set of HTML form elements. This lets the user edit the questions and answers, delete existing questions and add new ones.
But wait a second—if edit-quizfile.pl is going to create the HTML form that the user will use for editing a quiz, which program will take the contents of this form and actually do something with it? After all, CGI programs are single operations: they receive input, perform some processing and produce output. It seems if our program produces an HTML form as output, it cannot also accept input from that form and save it to disk. The secret here is that edit-quizfile.pl can accept its own input by expecting to be invoked twice. On the first invocation, it creates the form to be used to edit quizzes, and on the second invocation, it saves the submitted data in the form of a quiz. Our program thus performs two different actions on two distinct occasions.
Why not simply write two separate programs, one to display the form and a second to process it? We certainly could have handled it in this way, but this would restrict us to a single iteration of editing. By keeping all of the quiz-editing features within a single program we can create an editing loop that allows us more than a one-shot deal. We can reuse the code for displaying the current state of the quiz file, because we always display the quiz's file state when our program is invoked. By keeping all the code within one program and simply putting the display code at the bottom of this program, everything becomes a bit easier to understand and maintain.
Before we get to work coding, let's figure out how all of this works. We invoke edit-quiz.pl in a number of different ways, using both GET (i.e., entering its URL in a browser window or clicking on a hyperlink within a page of HTML) and POST (i.e., when we click on a submit button at the bottom of an HTML form).
If edit-quiz.pl is invoked using GET without any arguments, we are asked which quiz we wish to create or edit. Entering the name of the quiz file and pressing Return submits it to the program, which then receives this argument—again using GET--in the query string.
The query string, for those of you relatively new to CGI programming, is the term for anything following the question mark in a URL. It allows the passing of simple arguments to a CGI program without having to use POST, a more complicated and sophisticated protocol for passing information. Thus, if someone invokes the program:
http://www.fictional.edu/cgi-bin/program.pl
the query string is null, because there is no argument. But if someone invokes the CGI program:
http://www.fictional.edu/cgi-bin/program.pl?foobarthe argument is foobar. If we are using CGI.pm, a Perl module for writing CGI programs (available from CPAN at http://www.perl.com/CPAN), we can theoretically retrieve the contents of the query string using the query_string method, as in:
my $query = new CGI; my $query_string = $query->query_string;For reasons that I don't completely understand, the query_string method returns the query string with a prepended keywords=, as if the query string had been submitted to our CGI program in an HTML form element named keywords. While this can sometimes come in handy, for the most part, I find it a surprising quirk in an otherwise excellent package.
If CGI.pm is going to treat the query string as a parameter named keywords, we have to retrieve its value in the same way as we would other parameters, namely:
my $query = new CGI; my $query_string = $query->param("keywords");
which might seem a bit odd at first, but you get used to it.
We determine whether our program was invoked via GET or POST using the method request_method within CGI.pm. In other words, we can do the following:
my $query = new CGI; my $request_method = $query-7gt;request_method;
At this point, the variable $request_method contains either the string GET or POST, depending on how the program was invoked. The main difference between these two invocation methods is how arguments are passed to the program: GET sends all of the variable values in the query string, while POST sends them via stdin, the file handle associated with standard input. Luckily, CGI.pm frees us from having to deal with these methods and invisibly hands us the parameters regardless of their source.
In any event, if our program is invoked with any value in the query string, we print out an HTML form containing the contents of the quiz file with that name or a blank HTML form allowing the user to create a quiz of that name. You can see that program in Listing 1.
There are several things to note in this program. First of all, we need to tell the program the maximum number of questions that each quiz can contain. We do this by setting a global variable, $MAX_QUESTIONS, at the top of the program. Either very short or very long quizzes can be allowed for by changing this variable.
Also, notice how we manage to create a page of HTML that invokes our program with an argument in the query string. We use the <ISINDEX> tag, which has been all but forgotten on the Web, mostly because it creates an ugly text box whose instructions are difficult or impossible to change and rarely relevant to the subject at hand. Nevertheless, <ISINDEX> comes in handy if you want to provide a program with a mechanism to feed a user-defined argument to itself.
In addition, we create a new instance of QuizQuestions based on the name of the quiz that we received from the user in the query string. Once the instance of QuizQuestions is created, we instruct it to load its contents from disk. Of course, if this is a new quiz, then there is nothing to load, and this is noted in the error message returned by the loadFile method. We don't care if there was an error opening the file—if the quiz file exists, the contents are displayed in the HTML form, but if it does not exist, it is treated as a new quiz.
Of course, it is not a good idea to ignore error messages altogether. But the error messages returned by the loadFile method are fairly primitive, indicating whether the file was successfully loaded. Better error messages might distinguish between an inability to find the file in question, a quiz file that exists but cannot be read and a quiz file containing errors. But for now, this is all we've got, so we will have to live with it.
We insert the current value of each HTML form element by placing the value inside a variable. One of the nice things about Perl is that uninitialized variables default to the empty string (""). This means that if we have not set a particular question or answer, things don't crash. Rather, since we get the empty string back from the variable, we can stick the variable into the form element's value attribute, thus resetting the form element's value.
We use a bit of cleverness to indicate which element of the selection list (which is used to indicate the correct answer) should be selected by default. Here is the code:
my $letter = ""; foreach $letter ("a","b","c","d") { print "<option "; print "selected " if ($letter eq $correct); print "$letter>$letter\n"; } print "</select>\n";
In this code, we simply iterate through all four possible correct answers, inserting the word selected inside of the <option> tag where it is appropriate.
As you can see, it is not difficult to create a program that displays the contents of a quiz file. If we want to create an editor, we need to write the second half of the program, namely the part that takes the submitted form contents and saves them to disk. Luckily, the way we have organized our HTML form makes this fairly trouble-free.
The part of edit-quiz.pl that handles saving information is invoked via POST, when the user clicks on the submit button at the bottom of the page. At this point, the HTML form elements are sent to edit-quiz.pl and made available using the param method from CGI.pm.
So when edit-quiz.pl is invoked using POST, we simply need to do the following:
create a new instance of QuizQuestions
iterate through all of the HTML form elements submitted
turn these form elements into new questions
save QuizQuestions
The simplest way to do this is to loop through all of the possible elements. We know how many elements might exist, thanks to the $MAX_QUESTIONS global variable. Thus, we can do something like:
my $counter = 0; foreach $counter (0 .. $MAX_QUESTIONS) { # Add question number $counter }
Adding a new question is now accomplished by using the addQuestion method from within QuizQuestions. Once we create an instance of QuizQuestions, we can add a new question by invoking the method, and passing it the question text, the possible answers, and the correct answer as arguments. Given the names of our HTML form elements are regular, we can expand the above loop as:
# Create an instance of QuizQuestions my $questions = new QuizQuestions($quizname); # Add questions to $questions my $counter = 0; foreach $counter (1 .. $MAX_QUESTIONS) { # Only handle as many questions as were filled # in, by # checking to see if the question was entered last unless ($query->param("question-$counter") ne "$counter"); # Set the question my @question = ($query->param("question-$counter"), $query->param("answer-a-$counter"), $query->param("answer-b-$counter"), $query->param("answer-c-$counter"), $query->param("answer-d-$counter"), $query->param("correct-$counter")); # Add the question to the quiz $questions->addQuestion(@question); }The above loop should look familiar if you looked through last month's column. That's because the loop is lifted from create-quiz.pl, I have changed a variable name and modified the condition on the “last” statement. This ensures that the loop will exit if the text of the quiz question is the same as the quiz number, since each quiz question is set to its number in our editor.
Now that you have seen how the heart of edit-quiz.pl works, take a look at the entire program in Listing 2. As you can see, it did not take many modifications to make this program work. That's because the bulk of the work is done by objects already created. CGI.pm takes care of reading the HTML form elements and handing them to us in a nicely packaged format, while QuizQuestions.pm takes care of loading and saving the questions from the quiz file. Now we don't have to get our hands dirty. (And neither do our users, who can now create quizzes without having to worry about formatting issues.)
I have also done a bit of reshuffling with the original segments of edit-quiz.pl, such that we always see the results of our editing work and can make additional changes to the quiz. This is somewhat better than the standard Web interface, in which saving also means quitting. Here, there isn't any real exit button, since saving brings you back to where you were.
There are a few minor problems with the current versions of these programs. In particular, it is potentially dangerous for a user to enter a pair of quotation marks ("") inside one of the text fields. A browser might have difficulties determining which quotation marks belong to the string and which set the string's boundaries. (However, Netscape 3.0 on my Linux box appears to handle this just fine, much to my surprise.)
Although we forget it most of the time, we continue to be haunted by our old friend the tab character, which still separates the fields in our quiz file. If a user were to enter a tab into one of the text fields in our quiz editor, the quiz file's format would be damaged and would cause problems. A solution to this would involve iterating through each of the HTML form elements handed to us by CGI.pm and removing any tabs. Better yet, we could replace them with spaces.
Next month, we will look at hybrid templates that you can create with HTML and Perl, thanks to the magic of the Text::Template module. By embedding programs inside of HTML files, we can include both static HTML and programming elements. This can be a great advantage when you want to produce output using a program, but don't want to lock the HTML styling inside of a program, since your site's editors and designers will eventually want to change the content and style of the program's output.
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. You can reach him at reuven@netvision.net.il.