![]() |
![]() | ||
![]() |
![]() |
![]() |
![]() |
![]() |
Online QuiZZer Version 1.1by Brent MichalskiJan. 8, 1999
Computers were designed to make our lives easier - no really, stop laughing, they were! In this series of articles we are creating an Online QuiZZer which you can use to create tests and quizzes that are taken via the Web. The goal of this article is to give you the framework for a powerful quiz application that you can build upon. Version 1.1This is the second installation of the Online QuiZZer. In this version we make the program keep track of the answers that the user taking the quiz provides. By tracking the answers, we will be able to score the tests when the user is done. In my last article, I said that I struggled with the code. That struggle was nothing compared to the battle I had with this version! I have had nothing but problems but I know that my determination will pay off. Because of these problems, I didn't get as far as I wanted to with this version, but I promise it will be worth it. I even resorted to making a flowchart, which I hate doing! Here are some remaining items that I still want to incorporate into the final version of the Online QuiZZer:
The reason I am struggling so much with this is because I am trying to create a powerful program while keeping it all as simple as possible. Here's one example of the difficulties: The CGI.pm module allows you the ability to create multi-page forms, but I want to be able to jump from question 43 back to question 4 and have the program remember all of the answers provided up to that point. I haven't found a graceful way to do that in CGI.pm yet. My solution to this problem, at least at this point is:
The reason I split the variable and then reconstruct it each time is because I use the question number as the key to the hash. So, if a user answers question 7 after they had already answered question 7, the new values will take the place of the old answer. If I didn't do this, it would be possible to end up with many different answers to the same question. This adds complexity to the program, but I feel that once it is working properly it will be well worth the effort. The whole concept of programming is solving problems. I have been having problems with this part of the program but I know that I can make it work how I want it to. I also know that when I get it working I will feel a definite sense of accomplishment. So, on the surface, we only took a baby step in this new implementation. We went from only displaying the questions in the previous version, to storing the answers and stopping when we run out of questions in this version. I also took some of the code blocks and made them into subroutines for ease of use. I am determined to make some great strides in the next installation of the article; we will have scoring and we will be able to jump back x questions if we need to. Diving inThe program is run by calling the script directly, and passing the name of the quiz text file (ie http://webreview.com/1998/01/08/perl/quizzer.cgi?test1.txt ). It uses the text file for its questions and answers. The output is still not pretty but it is just a starting point that we will be building on.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 make it easier for me to talk about the program.
1: #!/usr/bin/perl 2: ####################### 3: # Quizzer Program # 4: # By: Brent Michalski # 5: # Version: 1.1 # 6: ####################### 7: use CGI qw(:standard); 8: print header; 9: $[ = 1; # Sets index of first array element to 1. 10: $script = "/cgi-bin/quizzer.cgi"; # The script name. 11: ($test_name,$current) = split '~', $ENV{QUERY_STRING}; 12: @ANS = ('A','B','C','D','E'); 13: $current = 1 if((($current eq "") || ($current < 1)) && ($current ne "score")); 14: $next = $current + 1; 15: $prev = $current - 1; 16: if($prev < 1){ $prev = 1; } 17: $counter = 1; 18: $question = "Q".$current; # The Question placeholder. 19: &make_current; 20: $total_questions = @question; 21: &make_saved_hash; 22: &get_answer; 23: &make_saved_variable; 24: if(($current > $total_questions) || ($current eq "score")){ &score_test; } 25: &print_question; 26: sub print_question{ 27: print<<HTML; 28: <HTML><HEAD><TITLE>Online Quizzer</TITLE></HEAD> 29: <BODY BGCOLOR="#FFFFFF"> 30: <CENTER> 31: <H2>Question $current of $total_questions questions</H2> 32: </CENTER> 33: <P><HR> 34: <FORM METHOD=POST ACTION="$script?$test_name~$next"> 35: <B> 36: $question[$current]->{QUESTION} 37: </B> 38: <BLOCKQUOTE> 39: HTML 40: foreach $val (@ANS){ 41: if($question[$current]->{$val} ne ""){ 42: print "<INPUT TYPE=CHECKBOX NAME=$question VALUE=$val> 43: $question[$current]->{$val}<BR>"; 44: } 45: } 46: print<<HTML; 47: </BLOCKQUOTE> 48: <P><HR> 49: <CENTER> 50: <INPUT TYPE=HIDDEN NAME=saved_answers VALUE="$saved_answers"> 51: <INPUT TYPE=SUBMIT VALUE="Answer And Move On" NAME=button> 52: </FORM> 53: </CENTER> 54: </BODY> 55: </HTML> 56: HTML 57: } # End of print_question subroutine. 58: sub make_record{ 59: $record = { 60: QUESTION => $QUESTION, 61: A => $A, 62: B => $B, 63: C => $C, 64: D => $D, 65: E => $E, 66: ANSWER => $ANSWER, 67: }; 68: $question[$counter] = $record; # Make the array of questions. 69: $QUESTION=$A=$B=$C=$D=$E=$ANSWER=""; # Clear variables 70: } # End of make_record subroutine. 71: sub make_current{ 72: open (QUIZ,"$test_name") || die "Couldn't open file! $!\n"; 73: while (<QUIZ>){ 74: if(/^(A|B|C|D|E)\)/){ 75: $$1 = $_; 76: } elsif (/^ANSWER:/){ 77: $ANSWER = $_; 78: } elsif (/^(\015?\012)$/){ # Mac friendly, thanks Chris! 79: &make_record; 80: $counter++; 81: } else { 82: $QUESTION .= $_; 83: $QUESTION =~ s/\015?\012/<BR>/g; 84: } # End of if..elsif..else 85: } # End of while 86: close(QUIZ); 87: } # End of make_current subroutine. 88: sub score_test{ 89: print "Score the test!<BR> Will have to wait until next article...<P>"; 90: for ($x=1;$x<=$total_questions;$x++){ 91: $this_answer = $answer{$x}; 92: if($this_answer eq ""){ 93: $this_answer = "<B>Unanswered</B>"; 94: } # End of if 95: print "Question $x: was answered with: $this_answer<BR>"; 96: if($answer{$x} eq ""){ 97: push @missed, $x; 98: } # End of if. 99: } # End of for loop. 100: print "You didn't answer question(s): " if(@missed); 101: foreach $z (@missed){ 102: print $z, " "; 103: } # End of foreach loop. 104: exit; 105: } # End of score_test subroutine. 106: sub get_answer{ 107: $q_num = "Q".$prev; 108: @current_answers = param($q_num); 109: foreach(@current_answers){ 110: $curr_answer .= $_; 111: } # End of foreach. 112: $answer{$prev} = $curr_answer; 113: } # End of get_answer. 114: sub make_saved_hash{ 115: @saved_array = split '~', param(saved_answers); 116: foreach (@saved_array){ 117: ($key, $value) = split '='; 118: $answer{$key} = $value; 119: } # End of foreach. 120: } # End of make_saved_hash. 121: sub make_saved_variable{ 122: foreach $key (keys %answer){ 123: $saved_answers .= $key . '=' . $answer{$key} . '~'; 124: } # End of foreach. 125: } # End of make_saved_variable.
Line-by-line explanationLine 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-6: A comment that identifies the program and who wrote it. Line 7: 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 8: 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 9: This is one we haven't used before. The $[ variable sets the index of the first element in the arrays. I am setting it to 1 because the questions are numbered 1,2,3..., so I thought it would be easier if we stored them with the index starting at 1. I also wanted you to know about this cool variable. Normally, $[ would default to 0. Line 10: Creates a variable which holds the path and name of this script. We use this later on in the HTML forms. Line 11: Reads in the values passed on the URL and splits them at the tilde ( ~ ). For this version of the script, I added the ability to pass the quiz/test file name in the URL along with the question number. Anything that is passed to a CGI script after the ? in the URL is sent via the QUERY_STRING environment variable. So for the 3rd question, the URL would look like something this: http://www.yourdomain.com/cgi-bin/quizzer.pl?test1.txt~3 The 3 refers to the 3rd element of the question array, hence we get the 3rd question. Remember that since we modified the $[ variable, the arrays start with element 1. Normally an array index begins at element 0, but since the questions begin with number 1, I felt that this modification reduced some of the confusion about what question number we are really on. Line 12: Sets up an array of answer values. This is used later when we loop and display the answer choices. Line 13: Sets $current to 1 if nothing was passed on the URL, OR if the value was less than 1 AND it is not equal to score. Line 14: Sets $next to one greater than $current. This allows us to get the next question. Line 15: Sets $prev to one less than $current. This allows us to get the previous question. Line 16: If $prev is less than 1, we set it to 1. Line 17: Sets a variable called $counter to 1 for us. Line 18: Creates a variable that we use in the HTML form later on; it will become the name of the checkboxes on the form. With all of the checkboxes having the same name, it causes a list of values to be sent if there are multiple answers. We read this list into an array on line 108. Line 19: Calls the make_current subroutine. Line 20: Counts the number of questions for us and stores the result in $total_questions Line 21: Calls the make_saved_hash subroutine. This subroutine creates a hash with all of the previous answers in it. Line 22: Calls the get_answer subroutine. This subroutine gets what the user answered for the previous question. When I refer to the previous question, I mean the question that was just answered by the user. Line 23: Calls the make_saved_variable subroutine. This subroutine reconstructs the hash we created on line 21 so that we can pass it back to the HTML form. Line 24: Checks to see if we need to score the test yet. Line 25: Calls the print_question subroutine to display the question. Lines 26-33: Begin the print_question subroutine. This subroutine prints out the current question. Line 34: Notice the ACTION value in the FORM tag. It is the script name, the test data file name, and then the question. All of which are separated by the tilde. Lines 35-39: More HTML for the question page. Line 40: Begins a foreach loop that iterates through the @ANS array. Line 41: An if statement to check and see if the current value is NOT blank. Lines 42-43: Prints out the HTML <INPUT TYPE=CHECKBOX> and puts the current answer on the line next to it. Lines 44-45: Close the if statement and the foreach loop. Lines 46-56: Finish off the HTML for the question page. Line 57: Closes the print_question subroutine block. Line 58: 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 59: Begins the creation the $record variable. Lines 60-66: Stores the values in the variables in their respective locations in the $record variable. Line 67: Ends the $record variable creation. Line 68: 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 69: Sets all of the variables we created above to nothing "". This gets them ready for the next record. Line 70: Ends the make_record subroutine. Line 71: Begins the make_current subroutine. Line 72: 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 73: Starts a while loop that goes through each entry in the data file. Line 74: 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: 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 75: 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 76: An elsif condition that again uses the matching operator ( // ), and looks for the word ANSWER: at the beginning of the line. Line 77: If line 76 finds a match, this line stores the value of the current line $_, in the variable called $ANSWER. Line 78: 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 AND nothing else on the line. I was able to MAYBE match on the \015 using the ? following it, which means match the preceding value zero or one times. 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 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 79: Calls the make_record subroutine. Line 80: Increments the variable $counter. Line 81: Our else condition. If none of the above are true, we execute the code inside this block. Line 82: 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 83: 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 84: Ends the if..elsif..else block. Line 85: Ends the while loop. Line 86: Closes the quiz file. Line 87: Ends the make_current subroutine. Line 88: Begins the score_test subroutine. Please note that this subroutine will be greatly enhanced in the final version of the program. Line 89: Prints out some HTML for the user. Line 90: Begins a for loop which checks to make sure all questions were answered. Line 91: Sets $this_answer to the value stored in $answer{$x}. Line 92: An if statement that checks to see if the value of $this_answer has nothing ( "" ) in it. Line 93: Sets $this_answer to Unanswered. Line 94: Closes the if statement. Line 95: Prints out what was answered for the current question for the user to see. Line 96: Begins an if loop to push the unanswered questions onto an array. Line 97: Pushes the current value onto the array @missed. Lines 98-99: Ends the if and for loops. Line 100: Prints out some text, only if any questions were unanswered. Line 101: Begins a foreach loop that will tell the user all of the questions that they didn't answer. Line 102: Prints out the current value and a space. Line 103: Ends the foreach loop. Line 104: Exits the program. This prevents the program from trying to print another question. Line 105: Ends the score_test subroutine. Line 106: Begins the get_answer subroutine. Line 107: Sets the $q_num variable to Q plus the number of the previous question. We need to set it to the previous question because when you move on, all of the values are now referring to the current question and we haven't answered that one yet. Line 108: Gets all of the values that were checked and stores them in an array called @current_answers. Line 109: Begins a foreach loop that iterates through each of the values in @current_answers. Line 110: Sets the variable $curr_answer to whatever it previously contained PLUS the current value read from the array. The .= operator means append the value on the right onto the variable on the left. It leaves whatever data was stored in the variable on the left alone. Line 111: Ends the foreach loop. Line 112: Sets the hash value at $prev to the value in $curr_answer. So if we just answered question 2 and we are now on question 3, it sets the hash value for question 2 to what we answered. Remember, we are viewing question 3 but have not sent the answer to the server yet. Line 113: Ends the get_answer subroutine. Line 114: Begins the make_saved_hash subroutine. This pulls the data from the saved values that were sent with the Web page. The reason we pull them into a hash and then put them back into a variable is because if we use the previous button (which is not here right now), it will allow us to change answers. By using a hash, we can only have one set of answers for each question. Line 115: Splits the saved_answers, which are brought in from the calling Web page, at the tilde. Line 116: Starts a foreach loop which iterates through each of the elements in @saved_array. Line 117: Now we split each element at the = and those become the key and value for the hash. Line 118: Sets $answer{$key} to $value. Line 119: Ends the foreach loop. Line 120: Ends the make_saved_hash array. Line 121: Begins the make_saved_variable subroutine. This subroutine reconstructs the hash into the $saved_answers variable. Line 122: Begins a foreach loop to iterate through each item in the %answer hash. Line 123: Appends the new values onto the $saved_answers array. Line 124: Ends the foreach loop. Line 122: Ends the make_saved_variable subroutine.
Wrapping It UpThe test file is a text file with the answers in it. Now that I think of it, we are going to have to come up with a creative way to prevent the test-takers from seeing the answers! A smart user will simply call up the test file and get an A. Boy, a programmer's work is never done :-) But that is a good thing. Anyway, back to the test file. Take a look at it and see that it is just a plain text file with a keyword, ANSWER:, thrown in so that the program knows when the current question is done. Until next time, Perl on... ;-) Source Code for Online QuiZZer Version 1.1
|
Web Techniques and Web Design and Development copyright © 1995-99 Miller Freeman, Inc. ALL RIGHTS RESERVED |