blank.gif
webreview.com - Cross-Training for Web Teams
Search for: 
Jump to:
blank.gif
blank.gif

 
 

A Songline PACE Production



An Online Quizzer

by Brent Michalski
Dec. 18, 1998
 
 

Quizzes and tests are ideal material to web-ify. Grading tests is boring, monotonous, error-prone and time consuming. Wouldn't it be nice to have the computer do it for you?

This article begins a series in which we will build an online quiz program. I'm not sure yet whether this will be a 2-part or 3-part series. The reason I'm still unsure is because I want to hear from you as to what features this online quiz program should have. I can't guarantee that we can put all of the requested features in, but I'd love to transform this into an easy-to-use, powerful, online quiz program.
 
View the demo


View the demo of this week's program.

I must admit that I struggled with this code for quite some time. I wanted to make creating the quiz files as easy as possible, but in doing so, I made it hard to read them into the quiz program. Well, it was hard at first, until I figured out how I wanted to do it. I probably tried five different ways to read the quizzes into the program before I settled on the one I am using.

At the 1998 Perl Conference, Tom Christiansen had some of the best "words of wisdom" I've heard about programming. He said "when programming, write the program and then throw it away and write it again!" You may think that this is counterproductive but I've actually found it very helpful when I do it. When you were in school, did you always turn in the first draft of your English assignments? Probably not. You probably rewrote them several times until you got them fine-tuned.

Programming is really just writing with a little logic thrown in. So, by re-writing and re-thinking the way I read in the quiz files, I was able to come up with a way that kept the quiz files themselves simple while also keeping the code which reads them fairly simple and quite short.

The code and article this week are the framework for the next articles. This week we'll go over the code that reads and generates the quizzes. In the next article of the series, we'll cover the code that actually keeps track of the answers and scores the quizzes when they are done.

Diving in 

The program is run by calling the script directly. The output is not very pretty this week but it is really just a starting point that we will be building on. It uses a text file for its questions and answers.

I have numbered the lines of code, the line numbers are not part of the program. You can also view the program without the line numbers. The line numbers simply make it easier for me to talk about the program.

1: #!/usr/bin/perl
2: #######################
3: # Online Quiz Program #
4: # By: Brent Michalski #
5: #######################

6: use CGI qw(:standard);
7: print header;

8: $script   = "quizzer.pl";  # The script name.
9: $q_number = $ENV{QUERY_STRING};
10: @ANS      = ('A','B','C','D','E');
11: $current  = $next = $q_number + 1;
12: $prev     = $q_number - 1;
13: $counter  = 0;

14: open (QUIZ,"test1.txt") ||
        die "Couldn't open file! $!\n";
15:  while (<QUIZ>){
16:   if(/^(A|B|C|D|E)\)/){
17:     $$1 = $_;
18:    } elsif (/^ANSWER:/){
19:     $ANSWER = $_;
20:    } elsif (/^(\015?\012)$/){
        # Mac friendly, thanks Chris!
21:     &make_record;
22:     $counter++;
23:    } else {
24:     $QUESTION .= $_;
25:     $QUESTION =~ s/\015?\012/<BR>/g;
26:   } # End of if..elsif..else
27: }  # End of while
28: close(QUIZ);

29: sub make_record{
30:   $record = {
31:     QUESTION => $QUESTION,
32:     A        => $A,
33:     B        => $B,
34:     C        => $C,
35:     D        => $D,
36:     E        => $E,
37:     ANSWER   => $ANSWER,
38:   };

39:   $question[$counter] = $record;
40:   $QUESTION=$A=$B=$C=$D=$E=$ANSWER="";
41: } # End of make_record subroutine.

42: $q_count = @question;

43: print<<HTML;
44:  <HTML><HEAD><TITLE>Online Quizzer</TITLE></HEAD>
45:   <BODY BGCOLOR="#FFFFFF">
46:    <CENTER>
47:     <H2>Question $current of $q_count questions</H2>
48:    </CENTER>
49:    <P><HR>
50:    <FORM METHOD=POST ACTION="$script?$next">
51:     <B>
52:      $question[$q_number]->{QUESTION}
53:     </B>
54:     <BLOCKQUOTE>
55: HTML

56: foreach $val (@ANS){
57:   if($question[$q_number]->{$val} ne ""){
58:     print "<INPUT TYPE=CHECKBOX
        VALUE=$val>$question[$q_number]->{$val}<BR>";
59:   }
60: }

61: print<<HTML;
62:     </BLOCKQUOTE>
63:    <P><HR>
64:    <CENTER>
65:     <INPUT TYPE=SUBMIT VALUE="Answer And Move On">
66:    </FORM>

67:    <FORM ACTION="$script?$prev" METHOD=POST>
68:     <INPUT TYPE=SUBMIT VALUE="Previous Question">
69:    </FORM>
70:   </CENTER>
71:  </BODY>
72: </HTML>
73: HTML

Line-by-line explanation 

Line 1: Tells the program where to find Perl on the web server. This line will vary depending on where Perl is installed on your server so you need to make any necessary changes. On a UNIX server, this line is required. If you are running this program on an NT server, this line is not required but won't hurt anything if included. 

Lines 2-5: A comment that identifies the program and who wrote it. 

Line 6: Loads the CGI.pm module into the program and imports the standard function definitions. By using the standard definitions, you don't have to use the $cgi->function conventions.

Line 7: Prints the standard header for CGI scripts. The header tells the web server what kind of data it is sending. This line is equivalent to the following line:

print "Content-type: text/html\n\n";

Line 8: This is the relative path to the program. This is used later on in the <FORM ACTION="...> HTML tags.

Line 9: Reads in the value passed on the URL. Anything that is passed after the ? on the URL is sent via the QUERY_STRING environment variable. So for the 3rd question, the URL would look like something this:

http://www.domain.com/cgi-bin/quizzer.pl?2

The 2 refers to the 3rd element of the question array, (0 being thr first) hence we get the 3rd question.

Line 10: Sets up an array of answer values. This is used later when we loop and display the answer choices.

Line 11: Sets $current and $next to the one greater than $q_number. I could have just used one value since it is not modified by the program but I wanted to keep the variable names easier to understand.

Line 12: Sets $prev to one less than $q_number. This allows us to get the previous question.

Line 13: Simply sets a variable called $counter to 0 for us.

Line 14: Opens the quiz data file and names the filehandle QUIZ. We also check to see if the file opened successfully. If it did not, we end the program with die and display an error message.

Line 15: Starts a while loop that goes through each entry in the data-file.

Line 16: Checks to see if the current value begins with an A, B, C, D, or E followed by a ). If it did, that means that the current value is a possible answer so we store it in a variable. This is a tricky line so here is a breakdown of what this line does.

Here is what we are testing in the if statement: 
/^(A|B|C|D|E)\)/

The forward slashes (//) are the matching operator. So what we are doing is matching whatever is between them.

The ^ means match at the beginning of the string. This eliminates the possibility of a false match in the middle of the line somewhere.

The parentheses ()around the A|B|C|D|E are capturing parentheses. In a regular expression this means that we store the value that was matched in the variable called $1. If there were more sets of parentheses, they'd be stored in $2, $3..., but we only have one set here so we are only worried about the variable $1.

The A|B|C|D|E means we are looking for A or B or C or D or E. Whatever was matched is then stored in the variable $1.

Finally we have \). This will match a right parentheses.

So we are really looking for is A) or B) ... E) at the beginning of the string. If a match is found, we store the entire line in a new variable we create on the next line. 

Line 17: We now create a new variable out of the value in $1 which we captured above. So, if the line began with B), then $1 will contain a value of B and when we say: $$1 = $_; we are effectively saying: $B = $_; because the value stored in $1 is extrapolated from the variable. Remember that $_ stores whatever the current line contains.

Line 18: An elsif condition that again uses the matching operator // (forward slashes), and looks for the word ANSWER: at the beginning of the line.

Line 19: If line 18 finds a match, this line stores the value of the current line $_, in the variable called $ANSWER.

Line 20: This line looks for a line containing only a carriage return, or carriage return/line feed combination. 

Remember how ^ meant at the beginning of the line in a regular expression? Well, the $ means at the end of the line

So, we are saying look for \015 maybe followed immediately by \012 for sure and nothing else on the line. 

I said maybe on the \015 because the ? following it means the value immediately preceding it can be in the string zero or one times for a match. So the \015 doesn't have to be there, but the \012 must be.

I put a comment on this line because Chris Nandor, of MacPerl fame, has corrected me in the past. By using \015?\012 we make this line portable from Unix to NT to Mac's without any further modifications. I could have been lazy and just said \015\012 but some flavors of Unix won't recognize this and the Mac won't either.

Line 21: Calls the make_record subroutine.

Line 22: Increments the variable $counter.

Line 23: The else condition. If none of the above are true, we execute the code inside this block.

Line 24: Appends the value stored in $_ to the value stored in $QUESTION. We append the values onto $QUESTION here because a question might have embedded carriage returns.

Line 25: Looks at the variable $QUESTION and replaces any embedded carriage returns with the HTML tag <BR>. This causes the question to display properly for us.

Line 26: Ends the if..elsif..else block.

Line 27: Ends the while loop.

Line 28: Closes the quiz file.

Line 29: Begins the make_record subroutine. The goal of this subroutine is to construct a record which contains the question, possible answers, and the real answer. Then we can store this record in an array for later use.

Line 30: Begins the creation the $record variable.

Lines 31-37: Stores the values in the variables in their respective locations in the $record variable.

Line 38: Ends the $record variable creation.

Line 39: Adds the current $record onto the array called @question. Since I am referring to an actual location in @question, I use a $ instead of a @. @ refers to the entire array while $ refers to a specific location.

Line 40: Sets all of the variables we created above to nothing "". This gets them ready for the next record.

Line 41: Ends the make_record subroutine.

Line 42: Stores the number of elements in @question in the variable $q_count.

Line 43: Begins a here document which is the HTML that we use to display the current question. If we didn't pass a question number via the URL, the first question gets displayed.

Lines 44-51: HTML for the question page.

Line 52: Displays the current question. Since we stored records in the @question array, we use the arrow operator -> to reference the data inside of the array element.

Lines 53-54: More HTML for the question page.

Line 55: Ends the here document.

Line 56: Begins a foreach loop that goes through each element in the array @ANS (from line 10). Each time through the loop, the current value is stored in the variable $val.

Line 57: An if statement that checks to see if the current value of the record contains data. If it does not, we don't display anything. If there is data, then we create an HTML checkbox and display the question on the page.

If the current value of $val was C and the current value of $q_number was 3, the if statement would be saying:

if($question[3]->C ne "") ...continue

Line 58: Prints the line of HTML that corresponds to this question.

Line 59: Ends the if block.

Line 60: Ends the foreach loop.

Lines 61-73: A here document that finishes off the HTML code for the page that we display. 

Wrapping it up 

Well, that wasn't too painful. I think that hardest part was line 16.

There are a few notes about the test data file that need to be mentioned. 

  • Each question group begins with the question on a line by itself.
  • This is followed by the answer choices which need an A), B), ...E) in front of them.
  • These are followed by the correct answer, which begins with ANSWER:
  • There must be a single line of white-space between question groups.
  • There must be a single line of white-space after the last question group.
I figured that these weren't too many restrictions to put on the creation of the test files. It keeps them very readable so creating them should be easy.

Ok, that is it for this week. I was serious when I said that I wanted you to give me suggestions about features you'd like to see in the program!

Send your QuiZZer suggestion to me at: perlguy@technologist.com and please put QuiZZer Suggestion on the subject line.

I won't possibly be able to put in all of the suggestions, but I'll put in as many useful ones as I can without making the program too big. 

I can't wait until the next article!

Perl on... ;-)


Source Code for an Online Quizzer
View and download the source code for this week's script.
Next: Online QuiZZer Version 1.1

Web Review copyright © 1995-99 Songline Studios, Inc.
Web Techniques and Web Design and Development copyright © 1995-99 Miller Freeman, Inc.
ALL RIGHTS RESERVED