The code below (server addresses Xed out for security) has been used on my 
website for years, but it does seem to misbehave on rare occasions, so I have a 
few questions on how I might improve it. I apologize in advance for my 
amateurish coding; I’m a high school teacher who cannot afford hiring help, so 
I bought a bunch of O’Reilly books and plunged in bravely. The heavy commenting 
is because I don’t often work on the code and need the reminders of why I am 
doing things!

This code serves multiple choice questions to students, stores their answer and 
other statistics, and emails me when the test is completed. It almost always 
works well, but there are exceptions which puzzle me.

First: Sometimes a test will exit early, sending me the result although the 
student did not make it through all the questions. Once this happened to the 
same student twice. I don’t think students are hacking my code because this has 
happened only to relatively unsophisticated individuals. 

Second: In one case, although I cannot verify this, the student claimed that 
she had answered a lot of questions that got lost. Perhaps I need to save 
multiple copies of their score results and delete the old ones only after a 
successful completion? Perhaps I need to save data in text files and only 
append them? General strategies for structuring and securing data are way above 
my skill level, I’m afraid. My host offers mySQL, but when I bought a book on 
it I discovered that it had a steep and scary learning curve!

Third: On one occasion my sever emailed me a student’s result twice in quick 
succession (one second apart according to Header values). One of the Received 
headers contained “by mx13.futurequest.net” and the other had “mx14” instead of 
“13”. This is not actually a problem for me like the anomaly above, but I am 
curious about it. 

And of course I welcome any and all suggestions, however small, on how to 
improve my code and even the strategies I am struggling to use, and I don’t 
mind you being blunt about it. You folks are a treasure of useful feedback and 
I am grateful for whatever you offer!

Rick




#!/usr/bin/perl

# student_poser3

use strict;
#use warnings; commented out because one of my comparisons issue a warning
use CGI; $CGI::DISABLE_UPLOADS = 1; $CGI::POST_MAX = 102_400;
use DB_File; # module for Berkeley DBM w/ DB_HASH file type
use Fcntl qw( :DEFAULT :flock );
use CGI::Carp qw ( fatalsToBrowser );
use HTML::Template;
use Net::SMTP;

my $q = new CGI; #Get parameters from the calling form
        my $course_file = $q->param( "course_file"              ); 
        my $student_id  = $q->param( "student_id"               );
        my $n                   = $q->param( "n"                                
); # 0 if from OPEN page
        my $answer              = $q->param( "submit_answer"    ); # undef if 
from OPEN page
&convert_answer; #  Converts answer to: a, b, c, d, or 0 

# Declare some global values
my $n_next; # the question that should be served next
my $fraction_complete; # fraction of test completed
my $fraction_correct; # fraction of answers correctly answer (including partial 
credit)
my @analysis; # List of student responses used in congrats subroutine

my %progress_hash;
        my $progfile = "/xxx/xxxx/xxx/data/students/$student_id/p_$course_file";
        die "There is no file called $progfile: $!\n" unless -e $progfile;
        my $db = tie %progress_hash, 'DB_File', $progfile or die "Can't tie 
progress_hash to $progfile: $!\n";
        my $fd = $db->fd(); # get a file descriptor
        open PROGFILE, "+<&=$fd" or die "Can't safely open $progfile : $!\n";
        flock ( PROGFILE, LOCK_EX ) or die "Unable to acquire exclusive lock on 
$progfile: $!\n";
        undef $db;
        
my $name_full            = $progress_hash{ name_full };
my $date_started         = $progress_hash{ date_started };
my $n_max                        = $progress_hash{ n_max };             # 
Number of questions in course
my $n_last                       = $progress_hash{ n_last };    # Last n 
answered
my $string_of_wrongs = $progress_hash{ string_of_wrongs };

&determine_n_next;      # the incoming response, if any, must match this
                                        # otherwise it is the next question to 
be served 

if ( $n ne $n_next ) {$n = 0} # If this n is not expected, ignore this 
submission

my %course_hash;
        my $coursefile = "/xxx/xxxx/xxx/data/courses/$course_file"; 
        die "There is no file called $coursefile: $!\n" unless -e $coursefile;
        my $db2 = tie %course_hash, 'DB_File', $coursefile, O_RDONLY or die 
"Can't tie course_hash to $coursefile: $!\n";
        my $fd2 = $db2->fd(); # get a file descriptor
        open COURSEFILE, "<&=$fd2" or die "Can't safely open $coursefile for 
reading: $!\n";
        flock ( COURSEFILE, LOCK_SH ) or die "Can't acquire a shared lock on 
$coursefile: $!";
        undef $db2;

if ( $n ne "0" ) {              # If there is an answer in the submission
        &score_submission;      # score and record it, 
        &determine_n_next;      # then re-determine n_next
}

if      ($n_next > $n_max) {
        &serve_congratulations; # If there are no more questions left, close up 
shop
} else {
        &serve_question;                # otherwise, serve the next question
}

untie %progress_hash; close PROGFILE;
untie %course_hash; close COURSEFILE;

exit;

sub convert_answer {
        SWITCH: {
            if ($answer eq "Choose First Option"  ) { $answer = "a"; last 
SWITCH; }
            if ($answer eq "Choose Second Option" ) { $answer = "b"; last 
SWITCH; }
            if ($answer eq "Choose Third Option"  ) { $answer = "c"; last 
SWITCH; }
            if ($answer eq "Choose Fourth Option" ) { $answer = "d"; last 
SWITCH; }
            $answer = ""; # From OPEN page there is no answer
        }
}
sub determine_n_next {
        # Capture the numeric and "h" parts of the last recorded answer
        my $num; if ($n_last =~ /(\d+)/) { $num = $1 }
        my $h;if ($n_last =~ /(h)/  ) { $h = $1 }
        
        if ( $string_of_wrongs =~ /_$n_last\_|$n_last$/ and $h ne "h") {        
# If wrong and not helped,
                $n_next = $n_last . "h"                                         
                                        # give help
        }       else {                                                          
                                                        # otherwise
                $n_next = $num + 1                                              
                                                # next question
        }
}

sub score_submission { 

        # Capture the numeric and "h" parts of this question
        my $num; if ($n =~ /(\d+)/) { $num = $1 }
        my $h; if ($n =~ /(h)/  ) { $h = $1 }
        
        &update_tally($num) unless (exists $progress_hash{tally_no_help} or 
exists $progress_hash{tally_w_help});
        
        my $correct_bool = 1; # default assumes answer was correct
        $progress_hash{$n} = $answer; # store student's answer
        $progress_hash{n_last} = $n; # store the number of question just 
answered
        $progress_hash{"time_$n"}       = &get_time;
        if ( $answer ne $course_hash{"cO_$num"} ) {     # check answer with 
numeric part of $n
                $progress_hash{string_of_wrongs} .= "_$n";
                $correct_bool = 0;
        }
        
        # Update two variables used by &determine_n_next
        $n_last = $progress_hash{n_last}; 
        $string_of_wrongs = $progress_hash{string_of_wrongs};
        
        # subroutine to tally older progress files
        &update_tally unless (exists $progress_hash{tally_no_help} or exists 
$progress_hash{tally_w_help});

        # Tally points earned
        if ( $correct_bool == 1 and $h ne "h" ) {       # If answer is right 
and no help, give credit
                $progress_hash{tally_no_help} += 1
        }
        if ( $correct_bool == 1 and $h eq "h" ) {       # If answer is right 
but with help, give half credit
                $progress_hash{tally_w_help} += .5
        }
        
        # Store statistics on progress
        $progress_hash{fraction_complete} = $num / $n_max;
        $progress_hash{fraction_correct} = ($progress_hash{tally_no_help}  + 
$progress_hash{tally_w_help}) / $num;
}

sub serve_question {

        my $percent_complete = int (100 * $progress_hash{fraction_complete});
        if ($percent_complete == 100) {$percent_complete = 99}; # Keep 100 from 
appearing before last question
        my $percent_correct = int ( 100 * $progress_hash{fraction_correct} );
        my $num; if ($n_next =~ /(\d+)/) { $num = $1 } # strip off h if there 
is one
        my $help_bool; if ($n_next =~ /(h)/  ) { $help_bool = 1 } # Serve help 
with question

        # Get params for next question
        my $course_name = $course_hash{'course_name'            };
        my $Qn                  = $course_hash{'Qn_'    . "$num"        };
        my $Oa                  = $course_hash{'Oa_'    . "$num"        };
        my $Ob                  = $course_hash{'Ob_'    . "$num"        };
        my $Oc                  = $course_hash{'Oc_'    . "$num"        };
        my $Od                  = $course_hash{'Od_'    . "$num"        };
        my $topic               = $course_hash{'topic_' . "$num"        };
        my $hp                  = $course_hash{'hp_'    . "$num"        };

        # Choose template file
        my $tmpl = new HTML::Template( filename => 
'/xxx/xxxx/xxx/www/templates/student_poser3.html' );

        # Assign template parameters
        $tmpl->param( name_full                 => $name_full                   
);
        $tmpl->param( course_name               => $course_name                 
);
        $tmpl->param( course_file               => $course_file                 
);
        $tmpl->param( percent_complete  => $percent_complete    );
        $tmpl->param( percent_correct   => $percent_correct             );
        $tmpl->param( student_id                => $student_id                  
);
        $tmpl->param( date_started              => $date_started                
);
        $tmpl->param( n                                 => $n_next              
                );
        $tmpl->param( Qn                                => $Qn                  
                );
        $tmpl->param( Oa                                => $Oa                  
                );
        $tmpl->param( Ob                                => $Ob                  
                );
        $tmpl->param( Oc                                => $Oc                  
                );
        $tmpl->param( Od                                => $Od                  
                );
        $tmpl->param( topic                     => $topic                       
        );
        $tmpl->param( hp                                => $hp                  
                );
        $tmpl->param( help_bool                 => $help_bool                   
);

        print "Content-type: text/html\n\n",
        $tmpl->output;
}

sub serve_congratulations {
        my $percent_correct_final = sprintf("%.1f", 100 * 
$progress_hash{fraction_correct}) ;
        my $course_name = $course_hash{course_name};
        my $date_ended = &get_date; $progress_hash{date_ended} = $date_ended;
        &define_analysis;
        # Produce email 
        my $from = 'liblea...@libertylearning.com';
        my $site = 'libertylearning.com';
        my $smtp_host = 'mail.libertylearning.com';
        my $to = 'liblea...@libertylearning.com';
        my $smtp = Net::SMTP->new($smtp_host, Hello => $site);
        $smtp->mail($from);
        $smtp->to($to);
        $smtp->data( );
        $smtp->datasend("From: LibLearn CGI\n");
        $smtp->datasend("To: $to\n");
        $smtp->datasend("Date: $date_ended\n");
        $smtp->datasend("Subject: $course_file $percent_correct_final 
$name_full\n");
        $smtp->datasend("\n");
        $smtp->datasend("\t$percent_correct_final\t$percent_correct_final\n");
        $smtp->datasend("$student_id\t$student_id\t$student_id\n");
        foreach (@analysis) {$smtp->datasend("$_\n")};
        $smtp->dataend( );
        $smtp->quit;
        
        # Choose template file
        my $tmpl = new HTML::Template( filename => 
'/xxx/xxxx/xxx/www/templates/student_congrats.html' );
        
        # Assign template parameters
        $tmpl->param( name_full                                 => $name_full   
                                );
        $tmpl->param( course_name                               => $course_name 
                                );
        $tmpl->param( date_started                              => 
$date_started                                );
        $tmpl->param( date_ended                                => $date_ended  
                                );
        $tmpl->param( percent_correct_final             => 
$percent_correct_final               );
        
        print "Content-type: text/html\n\n",
        $tmpl->output;
        
        chdir "/xxx/xxxx/xxx/data/students/$student_id/";
        rename "p_$course_file", "a_$course_file" or die "Can't rename to a_";
}

sub update_tally {
        my $num = shift @_; # num is the current question number (not yet 
scored)
        $num -= 1; # Knock off one to get number of questions answered already
        $string_of_wrongs = $progress_hash{string_of_wrongs} . "_"; # Append _ 
to help with matching
        my @wrongs = split /_/, $string_of_wrongs;
        shift @wrongs; # First element is blank because of a leading underscore
        my $number_correct = $num - scalar @wrongs; # answers attempted less 
wrong answers
        if ( $number_correct < 0 ) { $number_correct = 0}
        $progress_hash{tally_no_help} = $number_correct;
}

sub get_time {
        my $temp_time = time();
        if ($temp_time <100) {$temp_time = time()};     # try again if time 
function malfunctioned
        $temp_time;     
}

sub get_date {
        my ( undef, undef, undef, $day, $mon, $year, undef, undef, undef ) = 
localtime &get_time;
        $year += 1900;
        $mon += 1;
        my $formatted_date = "$mon/$day/$year";
        $formatted_date
}

sub define_analysis {
        my @n_list;     # Prepare a list of all answered questions (n's)
        foreach (keys %progress_hash) { 
                if (/^\d/) {push @n_list, $_}; # Get all the keys that begin 
with a digit (n's)
        }
        @n_list = sort {$a <=> $b || $a cmp $b} @n_list;
        
        my %timelapse; # Prepare a hash of how long it took to answer each 
question.
        my $previous_stamp = 0;
        foreach ( @n_list ) {
                $timelapse{$_} =  $progress_hash{"time_$_"} - $previous_stamp;
                $previous_stamp = $progress_hash{"time_$_"};
        }
        $timelapse{1} = "-"; # Can't tell how long it took to answer first 
question
        
        my $string_of_wrongs = $progress_hash{string_of_wrongs} . "_"; # Append 
_ to help with matching later
        #my @wrongs = split /_/, $string_of_wrongs;
        #shift @wrongs; # First element is blank because of a leading _
        
        #my %wrong_answers; # to store the answer (if wrong) of every answered 
question
        #my $err;
        #foreach (sort {$a <=> $b || $a cmp $b} keys %progress_hash) {
        #       if ( @wrongs =~/ ($progress_hash{$_}) / ) {$err = 
"$progress_hash{$_}"} else {$err = ""};
        #       $wrong_answers{$_} = $err;
        #}
        
        my $count = $progress_hash{n_last};
                if ($count =~ /(\d+)/) { $count = $1 } # strip off h if there 
is one
        my @nh_list;                            # This array will contain h's 
for EVERY question,
        foreach (1 .. $count) {         # allowing blanks for unused helps
                push @nh_list, $_;              # e.g. 1, 1h, 2, 2h, ..
                push @nh_list, $_ . "h"
        }
        
        my %wrong_answers; # to store the answer (if wrong) of every question 
including h's
        my $err;
        foreach (@nh_list) {
                if ( $string_of_wrongs =~ /_($_)_/ ) {$err = 
"$progress_hash{$_}"} else {$err = ""};
                $wrong_answers{$_} = $err;
        }
        foreach (@nh_list) {
                if ( ! defined $progress_hash{$_} )     
                                { push @analysis , "$_" }
                else    { push @analysis , "$_" . "\t" . "$wrong_answers{$_}" . 
"\t" . "$timelapse{$_}" }
        }
}
--
To unsubscribe, e-mail: beginners-unsubscr...@perl.org
For additional commands, e-mail: beginners-h...@perl.org
http://learn.perl.org/


Reply via email to