--- loncom/homework/grades.pm 2003/11/10 16:28:10 1.153 +++ loncom/homework/grades.pm 2003/11/21 22:59:42 1.161 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.153 2003/11/10 16:28:10 albertel Exp $ +# $Id: grades.pm,v 1.161 2003/11/21 22:59:42 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -132,6 +132,10 @@ sub response_type { my ($url,$symb) = shift; $symb=($ENV{'form.symb'} ne '' ? $ENV{'form.symb'} : (&Apache::lonnet::symbread($url))) if ($symb eq ''); my $allkeys = &Apache::lonnet::metadata($url,'keys'); + my %vPart; + foreach my $partid (&Apache::loncommon::get_env_multiple('form.vPart')) { + $vPart{$partid}=1; + } my %seen = (); my (@partlist,%handgrade,%responseType); foreach (split(/,/,&Apache::lonnet::metadata($url,'packages'))) { @@ -141,6 +145,9 @@ sub response_type { if (&Apache::loncommon::check_if_partid_hidden($partid,$symb)) { next; } + if (%vPart && !exists($vPart{$partid})) { + next; + } $responsetype =~ s/response$//; # make it compatible w/ navmaps - should move to that!! my ($value) = &Apache::lonnet::EXT('resource.'.$part.'.handgrade',$symb); $handgrade{$part} = ($value eq 'yes' ? 'yes' : 'no'); @@ -157,18 +164,31 @@ sub response_type { #--- Show resource title #--- and parts and response type sub showResourceInfo { - my ($url,$probTitle) = @_; + my ($url,$probTitle,$checkboxes) = @_; + my $col=3; + if ($checkboxes) { $col=4; } my $result ='<table border="0">'. - '<tr><td colspan=3><font size=+1><b>Current Resource: </b>'.$probTitle.'</font></td></tr>'."\n"; + '<tr><td colspan="'.$col.'"><font size="+1"><b>Current Resource: </b>'. + $probTitle.'</font></td></tr>'."\n"; my ($partlist,$handgrade,$responseType) = &response_type($url); my %resptype = (); my $hdgrade='no'; + my %partsseen; for my $part_resID (sort keys(%$handgrade)) { my $handgrade=$$handgrade{$part_resID}; my ($partID,$resID) = split(/_/,$part_resID); my $responsetype = $responseType->{$partID}->{$resID}; $hdgrade = $handgrade if ($handgrade eq 'yes'); - $result.='<tr><td><b>Part </b>'.$partID.' <font color="#999999">'. + $result.='<tr>'; + if ($checkboxes) { + if (exists($partsseen{$partID})) { + $result.="<td> </td>"; + } else { + $result.="<td><input type='checkbox' name='vPart' value='$partID' checked='on' /></td>"; + } + $partsseen{$partID}=1; + } + $result.='<td><b>Part </b>'.$partID.' <font color="#999999">'. $resID.'</font></td>'. '<td><b>Type: </b>'.$responsetype.'</td></tr>'; # '<td><b>Handgrade: </b>'.$handgrade.'</td></tr>'; @@ -550,8 +570,7 @@ sub listStudents { my $result='<h3><font color="#339933"> '.$viewgrade. ' Submissions for a Student or a Group of Students</font></h3>'; - my ($table,undef,$hdgrade,$partlist,$handgrade) = &showResourceInfo($url,$ENV{'form.probTitle'}); - $result.=$table; + my ($table,undef,$hdgrade,$partlist,$handgrade) = &showResourceInfo($url,$ENV{'form.probTitle'},($ENV{'form.showgrading'} eq 'yes')); $request->print(<<LISTJAVASCRIPT); <script type="text/javascript" language="javascript"> @@ -591,7 +610,8 @@ LISTJAVASCRIPT my $checkhdgrade = ($ENV{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1 ) ? 'checked' : ''; my $checklastsub = $checkhdgrade eq '' ? 'checked' : ''; - my $gradeTable='<form action="/adm/grades" method="post" name="gradesub">'."\n". + my $gradeTable='<form action="/adm/grades" method="post" name="gradesub">'. + "\n".$table. ' <b>View Problem Text: </b><input type="radio" name="vProb" value="no" checked="on" /> no '."\n". '<input type="radio" name="vProb" value="yes" /> one student '."\n". '<input type="radio" name="vProb" value="all" /> all students <br />'."\n". @@ -671,8 +691,11 @@ LISTJAVASCRIPT $status{'resource.'.$partid.'.submitted_by'}.'" />'; } } - next if (!$submitted && ($submitonly eq 'yes' || $submitonly eq 'graded')); - next if (!$graded && $submitonly eq 'graded'); + next if (!$submitted && ($submitonly eq 'yes' || + $submitonly eq 'incorrect' || + $submitonly eq 'graded')); + next if (!$graded && ($submitonly eq 'graded' || + $submitonly eq 'incorrect')); } $ctr++; @@ -729,8 +752,7 @@ LISTJAVASCRIPT sub processGroup { my ($request) = shift; my $ctr = 0; - my @stuchecked = (ref($ENV{'form.stuinfo'}) ? @{$ENV{'form.stuinfo'}} - : ($ENV{'form.stuinfo'})); + my @stuchecked = &Apache::loncommon::get_env_multiple('form.stuinfo'); my $total = scalar(@stuchecked)-1; foreach (@stuchecked) { @@ -1435,6 +1457,9 @@ sub submission { '<input type="hidden" name="msgsub" value="'.$ENV{'form.msgsub'}.'" />'."\n". '<input type="hidden" name="shownSub" value="0" />'."\n". '<input type="hidden" name="savemsgN" value="'.$ENV{'form.savemsgN'}.'" />'."\n"); + foreach my $partid (&Apache::loncommon::get_env_multiple('form.vPart')) { + $request->print('<input type="hidden" name="vPart" value="'.$partid.'" />'."\n"); + } } my ($cts,$prnmsg) = (1,''); @@ -1806,12 +1831,7 @@ sub processHandGrade { $ENV{'form.msgsub'},$message); } if ($ENV{'form.collaborator'.$ctr}) { - my @collabstrs; - if (ref($ENV{'form.collaborator'.$ctr}) eq 'ARRAY') { - @collabstrs=@{$ENV{'form.collaborator'.$ctr}}; - } else { - @collabstrs=$ENV{'form.collaborator'.$ctr}; - } + my @collabstrs=&Apache::loncommon::get_env_multiple("form.collaborator$ctr"); foreach my $collabstr (@collabstrs) { my ($part,@collaborators) = split(/:/,$collabstr); foreach (@collaborators) { @@ -1933,7 +1953,7 @@ sub processHandGrade { foreach my $student (@parsedlist) { my $submitonly=$ENV{'form.submitonly'}; my ($uname,$udom) = split(/:/,$student); - if ($submitonly =~ /^(yes|graded)$/) { + if ($submitonly =~ /^(yes|graded|incorrect)$/) { # my %record = &Apache::lonnet::restore($symb,$ENV{'request.course.id'},$udom,$uname); my %status=&student_gradeStatus($url,$symb,$udom,$uname,$partlist); my $submitted = 0; @@ -1946,8 +1966,11 @@ sub processHandGrade { $submitted = 0; } } - next if (!$submitted && ($submitonly eq 'yes' || $submitonly eq 'graded')); - next if (!$graded && $submitonly eq 'graded'); + next if (!$submitted && ($submitonly eq 'yes' || + $submitonly eq 'incorrect' || + $submitonly eq 'graded')); + next if (!$graded && ($submitonly eq 'graded' || + $submitonly eq 'incorrect')); } push @nextlist,$student if ($ctr < $ntstu); last if ($ctr == $ntstu); @@ -2817,7 +2840,9 @@ sub csvuploadassign { foreach my $grade (@gradedata) { my %entries=&Apache::loncommon::record_sep($grade); my $username=$entries{$fields{'username'}}; + $username=~s/\s//g; my $domain=$entries{$fields{'domain'}}; + $domain=~s/\s//g; if (!exists($$classlist{"$username:$domain"})) { push(@skipped,"$username:$domain"); next; @@ -3351,13 +3376,16 @@ sub getSequenceDropDown { sub scantron_uploads { if (!-e $Apache::lonnet::perlvar{'lonScansDir'}) { return ''}; my $result= '<select name="scantron_selectfile">'; - opendir(DIR,$Apache::lonnet::perlvar{'lonScansDir'}); - my @files=sort(readdir(DIR)); + my $cdom=$ENV{'course.'.$ENV{'request.course.id'}.'.domain'}; + my $cname=$ENV{'course.'.$ENV{'request.course.id'}.'.num'}; + my @files=&Apache::lonnet::dirlist('userfiles',$cdom,$cname, + &Apache::locommon::propath($cdom,$cname)); foreach my $filename (@files) { - if ($filename eq '.' or $filename eq '..') { next; } + ($filename)=split(/&/,$filename); + if ($filename!~/^scantron_orig_/) { next ; } + $filename=~s/^scantron_orig_//; $result.="<option>$filename</option>\n"; } - closedir(DIR); $result.="</select>"; return $result; } @@ -3385,6 +3413,8 @@ sub scantron_selectphase { my $file_selector=&scantron_uploads(); my $format_selector=&scantron_scantab(); my $result; + #FIXME allow instructor to be able to download the scantron file + # and to upload it, $result.= <<SCANTRONFORM; <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantro_process"> <input type="hidden" name="command" value="scantron_validate" /> @@ -3413,6 +3443,13 @@ sub scantron_selectphase { Format of data file: $format_selector </td> </tr> + <tr bgcolor="#ffffe6"> + <td> +<!-- FIXME this is lazy, a single parse of the set should let me know what this is --> + Last line to expect an answer on: + <input type="text" name="scantron_maxbubble" /> + </td> + </tr> </table> </td> </tr> @@ -3429,6 +3466,7 @@ sub get_scantron_config { my ($which) = @_; my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab'); my %config; + #FIXME probably should move to XML it has already gotten a bit much now foreach my $line (<$fh>) { my ($name,$descrip)=split(/:/,$line); if ($name ne $which ) { next; } @@ -3445,6 +3483,12 @@ sub get_scantron_config { $config{'Qlength'}=$config[8]; $config{'Qoff'}=$config[9]; $config{'Qon'}=$config[10]; + $config{'PaperID'}=$config[11]; + $config{'PaperIDlength'}=$config[12]; + $config{'FirstName'}=$config[13]; + $config{'FirstNamelength'}=$config[14]; + $config{'LastName'}=$config[15]; + $config{'LastNamelength'}=$config[16]; last; } return %config; @@ -3460,8 +3504,53 @@ sub username_to_idmap { return %idmap; } +sub scantron_fixup_scanline { + my ($scantron_config,$scan_data,$line,$whichline,$field,$args)=@_; + if ($field eq 'ID') { + if (length($args->{'newid'}) > $$scantron_config{'IDlength'}) { + return ($line,1,'New value to large'); + } + if (length($args->{'newid'}) < $$scantron_config{'IDlength'}) { + $args->{'newid'}=sprintf('%-'.$$scantron_config{'IDlength'}.'s', + $args->{'newid'}); + } + substr($line,$$scantron_config{'IDstart'}-1, + $$scantron_config{'IDlength'})=$args->{'newid'}; + if ($args->{'newid'}=~/^\s*$/) { + &scan_data($scan_data,"$whichline.user", + $args->{'username'}.':'.$args->{'domain'}); + } + } elsif ($field eq 'answer') { + my $length=$scantron_config->{'Qlength'}; + my $off=$scantron_config->{'Qoff'}; + my $on=$scantron_config->{'Qon'}; + my $answer=${off}x$length; + if ($args->{'response'} eq 'none') { + &scan_data($scan_data, + "$whichline.no_bubble.".$args->{'question'},'1'); + } else { + substr($answer,$args->{'response'},1)=$on; + &scan_data($scan_data, + "$whichline.no_bubble.".$args->{'question'},undef,'1'); + } + my $where=$length*($args->{'question'}-1)+$scantron_config->{'Qstart'}; + substr($line,$where-1,$length)=$answer; + } + return $line; +} + +sub scan_data { + my ($scan_data,$key,$value,$delete)=@_; + my $filename=$ENV{'form.scantron_selectfile'}; + if (defined($value)) { + $scan_data->{$filename.'_'.$key} = $value; + } + if ($delete) { delete($scan_data->{$filename.'_'.$key}); } + return $scan_data->{$filename.'_'.$key}; +} + sub scantron_parse_scanline { - my ($line,$scantron_config)=@_; + my ($line,$whichline,$scantron_config,$scan_data)=@_; my %record; my $questions=substr($line,$$scantron_config{'Qstart'}-1); my $data=substr($line,0,$$scantron_config{'Qstart'}-1); @@ -3475,6 +3564,15 @@ sub scantron_parse_scanline { } $record{'scantron.ID'}=substr($data,$$scantron_config{'IDstart'}-1, $$scantron_config{'IDlength'}); + $record{'scantron.PaperID'}= + substr($data,$$scantron_config{'PaperID'}-1, + $$scantron_config{'PaperIDlength'}); + $record{'scantron.FirstName'}= + substr($data,$$scantron_config{'FirstName'}-1, + $$scantron_config{'FirstNamelength'}); + $record{'scantron.LastName'}= + substr($data,$$scantron_config{'LastName'}-1, + $$scantron_config{'LastNamelength'}); my @alphabet=('A'..'Z'); my $questnum=0; while ($questions) { @@ -3482,17 +3580,25 @@ sub scantron_parse_scanline { my $currentquest=substr($questions,0,$$scantron_config{'Qlength'}); substr($questions,0,$$scantron_config{'Qlength'})=''; if (length($currentquest) < $$scantron_config{'Qlength'}) { next; } - my (@array)=split(/$$scantron_config{'Qon'}/,$currentquest); - if (scalar(@array) gt 2) { - #FIXME do something intelligent with double bubbles - Apache->request->print("<br ><b>Wha!!!</b> <pre>".scalar(@array). - '-'.$currentquest.'-'.$questnum.'</pre><br />'); - } + my @array=split($$scantron_config{'Qon'},$currentquest,-1); if (length($array[0]) eq $$scantron_config{'Qlength'}) { $record{"scantron.$questnum.answer"}=''; + if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { + push(@{$record{"scantron.missingerror"}},$questnum); + } } else { $record{"scantron.$questnum.answer"}=$alphabet[length($array[0])]; } + if (scalar(@array) gt 2) { + push(@{$record{'scantron.doubleerror'}},$questnum); + my @ans=@array; + my $i=length($ans[0]);shift(@ans); + while ($#ans) { + $i+=length($ans[0])+1; + $record{"scantron.$questnum.answer"}.=$alphabet[$i]; + shift(@ans); + } + } } $record{'scantron.maxquest'}=$questnum; return \%record; @@ -3500,7 +3606,6 @@ sub scantron_parse_scanline { sub scantron_add_delay { my ($delayqueue,$scanline,$errormessage,$errorcode)=@_; - Apache->request->print('add_delay_error '.$_[2] ); push(@$delayqueue, {'line' => $scanline, 'emsg' => $errormessage, 'ecode' => $errorcode } @@ -3508,14 +3613,15 @@ sub scantron_add_delay { } sub scantron_find_student { - my ($scantron_record,$idmap)=@_; + my ($scantron_record,$scan_data,$idmap,$line)=@_; my $scanID=$$scantron_record{'scantron.ID'}; + if ($scanID =~ /^\s*$/) { + return &scan_data($scan_data,"$line.user"); + } foreach my $id (keys(%$idmap)) { - #Apache->request->print('<pre>checking studnet -'.$id.'- againt -'.$scanID.'- </pre>'); - if (lc($id) eq lc($scanID)) { - #Apache->request->print('success'); - return $$idmap{$id}; - } + if (lc($id) eq lc($scanID)) { + return $$idmap{$id}; + } } return undef; } @@ -3528,16 +3634,393 @@ sub scantron_filter { return 0; } -#FIXME I think I am doing this in the wrong order, I think it would be -#better to make a several passes analyzing all of the lines in the -#file for common errors wrong/invalid PID/username duplicated -#PID/username, missing bubbles, double bubbles, missing/invalid CODE -#and then get the instructor to fix all of these errors, then grade -#the corrected one, I'll still need to catch error conditions, but -#maybe most will taken care even before we start +sub scantron_process_corrections { + my ($r) = @_; + my %scantron_config=&get_scantron_config($ENV{'form.scantron_format'}); + my ($scanlines,$scan_data)=&scantron_getfile(); + my $classlist=&Apache::loncoursedata::get_classlist(); + my $which=$ENV{'form.scantron_line'}; + my $line=&scantron_get_line($scanlines,$which); + my ($skip,$err,$errmsg); + if ($ENV{'form.scantron_skip_record'}) { + $skip=1; + } elsif ($ENV{'form.scantron_corrections'} =~ /^(duplicate|incorrect)ID$/) { + my $newstudent=$ENV{'form.scantron_username'}.':'. + $ENV{'form.scantron_domain'}; + my $newid=$classlist->{$newstudent}->[&Apache::loncoursedata::CL_ID]; + ($line,$err,$errmsg)= + &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,$which, + 'ID',{'newid'=>$newid, + 'username'=>$ENV{'form.scantron_username'}, + 'domain'=>$ENV{'form.scantron_domain'}}); + } elsif ($ENV{'form.scantron_corrections'} =~ /^(missing|double)bubble$/) { + foreach my $question (split(',',$ENV{'form.scantron_questions'})) { + ($line,$err,$errmsg)= + &scantron_fixup_scanline(\%scantron_config,$scan_data,$line, + $which,'answer', + { 'question'=>$question, + 'response'=>$ENV{"form.scantron_correct_Q_$question"}}); + if ($err) { last; } + } + } + if ($err) { + $r->print("Unable to accept last correction, an error occurred :$errmsg:"); + } else { + &scantron_put_line($scanlines,$which,$line,$skip); + &scantron_putfile($scanlines,$scan_data); + } +} + sub scantron_validate_file { my ($r) = @_; + my ($symb,$url)=&get_symb_and_url($r); + if (!$symb) {return '';} + my $default_form_data=&defaultFormData($symb,$url); + + if ($ENV{'form.scantron_corrections'}) { + &scantron_process_corrections($r); + } + #get the student pick code ready + $r->print(&Apache::loncommon::studentbrowser_javascript()); + my $result= <<SCANTRONFORM; +<form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantronupload"> + <input type="hidden" name="selectpage" value="$ENV{'form.selectpage'}" /> + <input type="hidden" name="scantron_format" value="$ENV{'form.scantron_format'}" /> + <input type="hidden" name="scantron_selectfile" value="$ENV{'form.scantron_selectfile'}" /> + <input type="hidden" name="scantron_maxbubble" value="$ENV{'form.scantron_maxbubble'}" /> + $default_form_data +SCANTRONFORM + $r->print($result); + + my @validate_phases=( 'ID', + 'CODE', + 'doublebubble', + 'missingbubbles'); + if (!$ENV{'form.validatepass'}) { + $ENV{'form.valiadatepass'} = 0; + } + my $currentphase=$ENV{'form.valiadatepass'}; + + if ($ENV{'form.scantron_selectfile'}=~m-^/-) { + #first pass copy file to classdir + + } + my $stop=0; + while (!$stop && $currentphase < scalar(@validate_phases)) { + $r->print("<p> Validating ".$validate_phases[$currentphase]."</p>"); + $r->rflush(); + my $which="scantron_validate_".$validate_phases[$currentphase]; + { + no strict 'refs'; + ($stop,$currentphase)=&$which($r,$currentphase); + } + } + if (!$stop) { + $r->print("Validation process complete.<br />"); + $r->print('<input type="submit" name="submit" value="Start Grading" />'); + $r->print('<input type="hidden" name="command" value="scantron_process" />'); + } else { + $r->print('<input type="hidden" name="command" value="scantron_validate" />'); + $r->print("<input type='hidden' name='validatepass' value='".$currentphase."' />"); + } + if ($stop) { + $r->print('<input type="submit" name="submit" value="Continue ->" />'); + $r->print(' using corrected info <br />'); + $r->print("<input type='submit' value='Skip' name='scantron_skip_record' />"); + $r->print(" this scanline saving it for later."); + } + $r->print(" </form><br />".&show_grading_menu_form($symb,$url). + "</body></html>"); + return ''; +} + +sub scantron_getfile { + #FIXME really would prefer a scantron directory but tokenwrapper + # doesn't allow access to subdirs of userfiles + my $cname=$ENV{'course.'.$ENV{'request.course.id'}.'.num'}; + my $cdom=$ENV{'course.'.$ENV{'request.course.id'}.'.domain'}; + my $lines; + $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'. + 'scantron_orig_'.$ENV{'form.scantron_selectfile'}); + my %scanlines; + $scanlines{'orig'}=[(split("\n",$lines,-1))]; + my $temp=$scanlines{'orig'}; + $scanlines{'count'}=$#$temp; + + $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'. + 'scantron_corrected_'.$ENV{'form.scantron_selectfile'}); + if ($lines eq '-1') { + $scanlines{'corrected'}=[]; + } else { + $scanlines{'corrected'}=[(split("\n",$lines,-1))]; + } + $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'. + 'scantron_skipped_'.$ENV{'form.scantron_selectfile'}); + if ($lines eq '-1') { + $scanlines{'skipped'}=[]; + } else { + $scanlines{'skipped'}=[(split("\n",$lines,-1))]; + } + my @tmp=&Apache::lonnet::dump('scantrondata',$cdom,$cname); + if ($tmp[0] =~ /^(error:|no_such_host)/) { @tmp=(); } + my %scan_data = @tmp; + return (\%scanlines,\%scan_data); +} + +sub lonnet_putfile { + my ($contents,$filename)=@_; + my $docuname=$ENV{'course.'.$ENV{'request.course.id'}.'.num'}; + my $docudom=$ENV{'course.'.$ENV{'request.course.id'}.'.domain'}; + my $docuhome=$ENV{'course.'.$ENV{'request.course.id'}.'.home'}; + $ENV{'form.sillywaytopassafilearound'}=$contents; + &Apache::lonnet::finishuserfileupload($docuname,$docudom,$docuhome,'sillywaytopassafilearound',$filename); + +} + +sub scantron_putfile { + my ($scanlines,$scan_data) = @_; + #FIXME really would prefer a scantron directory but tokenwrapper + # doesn't allow access to subdirs of userfiles + my $cname=$ENV{'course.'.$ENV{'request.course.id'}.'.num'}; + my $cdom=$ENV{'course.'.$ENV{'request.course.id'}.'.domain'}; + my $prefix='scantron_'; +# no need to update orig, shouldn't change +# &lonnet_putfile(join("\n",@{$scanlines->{'orig'}}),$prefix.'orig_'. +# $ENV{'form.scantron_selectfile'}); + &lonnet_putfile(join("\n",@{$scanlines->{'corrected'}}), + $prefix.'corrected_'. + $ENV{'form.scantron_selectfile'}); + &lonnet_putfile(join("\n",@{$scanlines->{'skipped'}}), + $prefix.'skipped_'. + $ENV{'form.scantron_selectfile'}); + &Apache::lonnet::put('scantrondata',$scan_data,$cdom,$cname); +} + +sub scantron_get_line { + my ($scanlines,$i)=@_; + if ($scanlines->{'skipped'}[$i]) {return undef;} + if ($scanlines->{'corrected'}[$i]) {return $scanlines->{'corrected'}[$i];} + return $scanlines->{'orig'}[$i]; +} + +sub scantron_put_line { + my ($scanlines,$i,$newline,$skip)=@_; + if ($skip) { + $scanlines->{'skipped'}[$i]=$newline; + return; + } + $scanlines->{'corrected'}[$i]=$newline; +} + +sub scantron_validate_ID { + my ($r,$currentphase) = @_; + + #get student info + my $classlist=&Apache::loncoursedata::get_classlist(); + my %idmap=&username_to_idmap($classlist); + + #get scantron line setup + my %scantron_config=&get_scantron_config($ENV{'form.scantron_format'}); + my ($scanlines,$scan_data)=&scantron_getfile(); + + my %found=('ids'=>{},'usernames'=>{}); + for (my $i=0;$i<=$scanlines->{'count'};$i++) { + my $line=&scantron_get_line($scanlines,$i); + if ($line=~/^[\s\cz]*$/) { next; } + my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + my $id=$$scan_record{'scantron.ID'}; + my $found; + foreach my $checkid (keys(%idmap)) { + if (lc($checkid) eq lc($id)) { $found=$checkid;last; } + } + if ($found) { + my $username=$idmap{$found}; + if ($found{'ids'}{$found}) { + &scantron_get_correction($r,$i,$scan_record,\%scantron_config, + $line,'duplicateID',$found); + return(1); + } elsif ($found{'usernames'}{$username}) { + &scantron_get_correction($r,$i,$scan_record,\%scantron_config, + $line,'duplicateID',$username); + return(1); + } + #FIXME store away line we prviously saw the ID on to use above + $found{'ids'}{$found}++; + $found{'usernames'}{$username}++; + } else { + if ($id =~ /^\s*$/) { + my $username=&scan_data($scan_data,"$i.user"); + if (defined($username) && $found{'usernames'}{$username}) { + &scantron_get_correction($r,$i,$scan_record, + \%scantron_config, + $line,'duplicateID',$username); + return(1); + } elsif (!defined($username)) { + &scantron_get_correction($r,$i,$scan_record, + \%scantron_config, + $line,'incorrectID'); + return(1); + } + $found{'usernames'}{$username}++; + } else { + &scantron_get_correction($r,$i,$scan_record,\%scantron_config, + $line,'incorrectID'); + return(1); + } + } + } + + return (0,$currentphase+1); +} + +sub scantron_get_correction { + my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_; + +#FIXME in the case of a duplicated ID the previous line, probaly need +#to show both the current line and the previous one and allow skipping +#the previous one or the current one + + $r->print("<p><b>An error was detected ($error)</b>"); + if ( defined($$scan_record{'scantron.PaperID'}) ) { + $r->print(" for PaperID <tt>". + $$scan_record{'scantron.PaperID'}."</tt> \n"); + } else { + $r->print(" in scanline $i <pre>". + $line."</pre> \n"); + } + $r->print('<input type="hidden" name="scantron_corrections" value="'.$error.'" />'."\n"); + $r->print('<input type="hidden" name="scantron_line" value="'.$i.'" />'."\n"); + if ($error =~ /ID$/) { + if ($error eq 'unknownID') { + $r->print("The encoded ID is not in the classlist</p>\n"); + } elsif ($error eq 'duplicateID') { + $r->print("The encoded ID has also been used by a previous paper $arg</p>\n"); + } + $r->print("<p>The ID on the form is <tt>". + $$scan_record{'scantron.ID'}."</tt><br />\n"); + $r->print("The name on the paper is ". + $$scan_record{'scantron.LastName'}.",". + $$scan_record{'scantron.FirstName'}."</p>"); + $r->print("<p>How should I handle this? <br /> \n"); + $r->print("\n<ul><li> "); + #FIXME it would be nice if this sent back the user ID and + #could do partial userID matches + $r->print(&Apache::loncommon::selectstudent_link('scantronupload', + 'scantron_username','scantron_domain')); + $r->print(": <input type='text' name='scantron_username' value='' />"); + $r->print("\n@". + &Apache::loncommon::select_dom_form(undef,'scantron_domain')); + + $r->print('</li>'); + } elsif ($error eq 'doublebubble') { +#FIXME Need to print out who this is along with the paper info + $r->print("<p>There have been multiple bubbles scanned for a some question(s)</p>\n"); + $r->print('<input type="hidden" name="scantron_questions" value="'. + join(',',@{$arg}).'" />'); + $r->print("<p>Please indicate which bubble should be used for grading</p>"); + foreach my $question (@{$arg}) { + my $selected=$$scan_record{"scantron.$question.answer"}; + &scantron_bubble_selector($r,$scan_config,$question,split('',$selected)); + } + } elsif ($error eq 'missingbubble') { + $r->print("<p>There have been <b>no</b> bubbles scanned for some question(s)</p>\n"); + $r->print("<p>Please indicate which bubble should be used for grading</p>"); + $r->print("Some questions have no scanned bubbles\n"); + $r->print('<input type="hidden" name="scantron_questions" value="'. + join(',',@{$arg}).'" />'); + foreach my $question (@{$arg}) { + my $selected=$$scan_record{"scantron.$question.answer"}; + &scantron_bubble_selector($r,$scan_config,$question); + } + } else { + $r->print("\n<ul>"); + } + $r->print("\n</li></ul>"); + +} + +sub scantron_bubble_selector { + my ($r,$scan_config,$quest,@selected)=@_; + my $max=$$scan_config{'Qlength'}; + my @alphabet=('A'..'Z'); + $r->print("<table border='1'><tr><td rowspan='2'>$quest</td>"); + for (my $i=0;$i<$max+1;$i++) { + $r->print('<td align="center">'); + if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) } + else { $r->print(' '); } + $r->print('</td>'); + } + $r->print('<td></td></tr><tr>'); + for (my $i=0;$i<$max;$i++) { + $r->print('<td><input type="radio" name="scantron_correct_Q_'.$quest. + '" value="'.$i.'" />'.$alphabet[$i]."</td>"); + } + $r->print('<td><input type="radio" name="scantron_correct_Q_'.$quest. + '" value="none" /> No bubble </td>'); + $r->print('</tr></table>'); +} + +sub scantron_validate_CODE { + my ($r,$currentphase) = @_; + #FIXME doesn't do anything yet + return (0,$currentphase+1); +} + +sub scantron_validate_doublebubble { + my ($r,$currentphase) = @_; + #get student info + my $classlist=&Apache::loncoursedata::get_classlist(); + my %idmap=&username_to_idmap($classlist); + + #get scantron line setup + my %scantron_config=&get_scantron_config($ENV{'form.scantron_format'}); + my ($scanlines,$scan_data)=&scantron_getfile(); + for (my $i=0;$i<=$scanlines->{'count'};$i++) { + my $line=&scantron_get_line($scanlines,$i); + if ($line=~/^[\s\cz]*$/) { next; } + my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + if (!defined($$scan_record{'scantron.doubleerror'})) { next; } + &scantron_get_correction($r,$i,$scan_record,\%scantron_config,$line, + 'doublebubble', + $$scan_record{'scantron.doubleerror'}); + return (1,$currentphase); + } + return (0,$currentphase+1); +} + +sub scantron_validate_missingbubbles { + my ($r,$currentphase) = @_; + #get student info + my $classlist=&Apache::loncoursedata::get_classlist(); + my %idmap=&username_to_idmap($classlist); + + #get scantron line setup + my %scantron_config=&get_scantron_config($ENV{'form.scantron_format'}); + my ($scanlines,$scan_data)=&scantron_getfile(); + my $max_bubble=$ENV{'form.scantron_maxbubble'}; + if (!$max_bubble) { $max_bubble=2**31; } + for (my $i=0;$i<=$scanlines->{'count'};$i++) { + my $line=&scantron_get_line($scanlines,$i); + if ($line=~/^[\s\cz]*$/) { next; } + my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + if (!defined($$scan_record{'scantron.missingerror'})) { next; } + my @to_correct; + foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) { + if ($missing > $max_bubble) { next; } + push(@to_correct,$missing); + } + if (@to_correct) { + &scantron_get_correction($r,$i,$scan_record,\%scantron_config, + $line,'missingbubble',\@to_correct); + return (1,$currentphase); + } + + } + return (0,$currentphase+1); } sub scantron_process_students { @@ -3548,8 +4031,7 @@ sub scantron_process_students { my $default_form_data=&defaultFormData($symb,$url); my %scantron_config=&get_scantron_config($ENV{'form.scantron_format'}); - my $scanlines=Apache::File->new($Apache::lonnet::perlvar{'lonScansDir'}."/$ENV{'form.scantron_selectfile'}"); - my @scanlines=<$scanlines>; + my ($scanlines,$scan_data)=&scantron_getfile(); my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); my $navmap=Apache::lonnavmaps::navmap->new(); @@ -3566,36 +4048,36 @@ SCANTRONFORM my @delayqueue; my %completedstudents; - my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r, - 'Scantron Status','Scantron Progress',scalar(@scanlines)); + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron Status', + 'Scantron Progress',$scanlines->{'count'}); &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state, 'Processing first student'); my $start=&Time::HiRes::time(); - foreach my $line (@scanlines) { - $r->print('<pre>line is'.$line.'</pre>'); - - chomp($line); - my $scan_record=&scantron_parse_scanline($line,\%scantron_config); - my ($uname,$udom); - unless ($uname=&scantron_find_student($scan_record,\%idmap)) { - &scantron_add_delay(\@delayqueue,$line, - 'Unable to find a student that matches',1); - next; - } - if (exists $completedstudents{$uname}) { - &scantron_add_delay(\@delayqueue,$line, - 'Student '.$uname.' has multiple sheets',2); - next; - } - $r->print('<pre>doing studnet'.$uname.'</pre>'); - ($uname,$udom)=split(/:/,$uname); - &Apache::lonnet::delenv('form.counter'); - &Apache::lonnet::appenv(%$scan_record); -# &Apache::lonhomework::showhash(%ENV); -# $Apache::lonxml::debug=1; -# &Apache::lonxml::debug("line is $line"); + my $i=-1; + my ($uname,$udom); + while ($i<$scanlines->{'count'}) { + ($uname,$udom)=('',''); + $i++; + my $line=&scantron_get_line($scanlines,$i); + if ($line=~/^[\s\cz]*$/) { next; } + my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + unless ($uname=&scantron_find_student($scan_record,$scan_data, + \%idmap,$i)) { + &scantron_add_delay(\@delayqueue,$line, + 'Unable to find a student that matches',1); + next; + } + if (exists $completedstudents{$uname}) { + &scantron_add_delay(\@delayqueue,$line, + 'Student '.$uname.' has multiple sheets',2); + next; + } + ($uname,$udom)=split(/:/,$uname); + &Apache::lonnet::delenv('form.counter'); + &Apache::lonnet::appenv(%$scan_record); - my $i=0; + my $i=0; foreach my $resource (@resources) { $i++; my $result=&Apache::lonnet::ssi($resource->src(), @@ -3605,23 +4087,6 @@ SCANTRONFORM 'grade_domain' =>$udom, 'grade_courseid'=>$ENV{'request.course.id'}, 'grade_symb' =>$resource->symb())); -# my %score=&Apache::lonnet::restore($resource->symb(), -# $ENV{'request.course.id'}, -# $udom,$uname); -# foreach my $part ($resource->{PARTS}) { -# if ($score{'resource.'.$part.'.solved'} =~ /^correct/) { -# $studentcorrect++; -# $totalcorrect++; -# } else { -# $studentincorrect++; -# $totalincorrect++; -# } -# } -# $r->print('<pre>'. -# $resource->symb().'-'. -# $resource->src().'-'.'</pre>result is'.$result); -# &Apache::lonhomework::showhash(%score); - # if ($i eq 3) {last;} } $completedstudents{$uname}={'line'=>$line}; } continue { @@ -3629,34 +4094,76 @@ SCANTRONFORM &Apache::lonnet::delenv('scantron\.'); &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, 'last student'); - #last; - #FIXME - #get iterator for $sequence - #foreach question 'submit' the students answer to the server - # through grade target { - # generate data to pass back that includes grade recevied - #} } &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); my $lasttime = &Time::HiRes::time()-$start; $r->print("<p>took $lasttime</p>"); - #$Apache::lonxml::debug=0; - foreach my $delay (@delayqueue) { - #FIXME - #print out each delayed student with interface to select how - # to repair student provided info - #Expected errors include - # 1 bad/no stuid/username - # 2 invalid bubblings - + $navmap->untieHashes(); + $r->print("<p>Done</p>"); + $r->print(&show_grading_menu_form($symb,$url)); + return ''; +} + +sub scantron_upload_scantron_data { + my ($r)=@_; + $r->print(&Apache::loncommon::coursebrowser_javascript($ENV{'request.role.domain'})); + my $select_link=&Apache::loncommon::selectcourse_link('rules','courseid', + 'domainid'); + my $domsel=&Apache::loncommon::select_dom_form($ENV{'request.role.domain'}, + 'domainid'); + $r->print(<<UPLOAD); +<script type="text/javascript" language="javascript"> + function checkUpload(formname) { + if (formname.upfile.value == "") { + alert("Please use the browse button to select a file from your local directory."); + return false; + } + formname.submit(); } +</script> + +<form enctype='multipart/form-data' action='/adm/grades' name='rules' method='post'> +Course: <input name='courseid' type='text' /> +Domain: $domsel $select_link +<br /> +<input name='command' value='scantronupload_save' type='hidden' /> +File to upload:<input type="file" name="upfile" size="50" /> +<br /> +<input type="button" onClick="javascript:checkUpload(this.form);" value="Upload Scantron Data" /> +</form> +UPLOAD + return ''; +} + +sub scantron_upload_scantron_data_save { + my($r)=@_; + $r->print("Doing upload to ".$ENV{'form.courseid'}); + my $home=&Apache::lonnet::homeserver($ENV{'form.courseid'}, + $ENV{'form.domainid'}); + my $fname=$ENV{'form.upfile.filename'}; #FIXME - # if delay queue exists 2 submits one to process delayed students one - # to ignore delayed students, possibly saving the delay queue for later - - $navmap->untieHashes(); + #copied from lonnet::userfileupload() + #make that function able to target a specified course + # Replace Windows backslashes by forward slashes + $fname=~s/\\/\//g; + # Get rid of everything but the actual filename + $fname=~s/^.*\/([^\/]+)$/$1/; + # Replace spaces by underscores + $fname=~s/\s+/\_/g; + # Replace all other weird characters by nothing + $fname=~s/[^\w\.\-]//g; + # See if there is anything left + unless ($fname) { return 'error: no uploaded file'; } + $fname='scantron_orig_'.$fname; + &Apache::lonnet::logthis("fname is $fname"); + $r->print(&Apache::lonnet::finishuserfileupload($ENV{'form.courseid'}, + $ENV{'form.domainid'}, + $home,'upfile',$fname)); + return ''; } + + #-------- end of section for handling grading scantron forms ------- # #------------------------------------------------------------------- @@ -3761,16 +4268,17 @@ GRADINGMENUJS $result.='<tr bgcolor="#ffffe6" valign="top"><td>'."\n". ' Select Section: <select name="section">'."\n"; if (ref($sections)) { - foreach (sort (@$sections)) {$result.='<option value="'.$_.'" '. - ($saveSec eq $_ ? 'selected="on"' : '').'>'.$_.'</option>'."\n";} + foreach (sort (@$sections)) { + $result.='<option value="'.$_.'" '. + ($saveSec eq $_ ? 'selected="on"':'').'>'.$_.'</option>'."\n"; + } } $result.= '<option value="all" '.($saveSec eq 'all' ? 'selected="on"' : ''). '>all</select> '; $result.='Student Status:</b>'.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,undef); - if (ref($sections)) { - $result.=' (Section "no" implies the students were not assigned a section.)<br />' - if (grep /no/,@$sections); + if (ref($sections) && (grep /no/,@$sections)) { + $result.=' (Section "no" implies the students were not assigned a section.)<br />'; } $result.='</td></tr>'; @@ -3782,6 +4290,8 @@ GRADINGMENUJS ($saveSub eq 'yes' ? 'selected="on"' : '').'>with submissions</option>'. '<option value="graded" '. ($saveSub eq 'graded' ? 'selected="on"' : '').'>with ungraded submissions</option>'. + '<option value="incorrect" '. + ($saveSub eq 'incorrect' ? 'selected="on"' : '').'>with incorrect submissions</option>'. '<option value="all" '. ($saveSub eq 'all' ? 'selected="on"' : '').'>with any status</option></select></td></tr>'."\n"; @@ -3838,14 +4348,18 @@ sub handler { &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'}); my $url=$ENV{'form.url'}; my $symb=$ENV{'form.symb'}; - my $command=$ENV{'form.command'}; + my @commands=&Apache::loncommon::get_env_multiple('form.command'); + my $command=$commands[0]; + if ($#commands > 0) { + &Apache::lonnet::logthis("grades got multiple commands ".join(':',@commands)); + } if (!$url) { my ($temp1,$temp2); ($temp1,$temp2,$ENV{'form.url'})=&Apache::lonnet::decode_symb($symb); $url = $ENV{'form.url'}; } &send_header($request); - if ($url eq '' && $symb eq '') { + if ($url eq '' && $symb eq '' && $command eq '') { if ($ENV{'user.adv'}) { if (($ENV{'form.codeone'}) && ($ENV{'form.codetwo'}) && ($ENV{'form.codethree'})) { @@ -3886,7 +4400,6 @@ sub handler { delete($perm{'mgr'}); } } - if ($command eq 'submission' && $perm{'vgr'}) { ($ENV{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0)); } elsif ($command eq 'pickStudentPage' && $perm{'vgr'}) { @@ -3926,12 +4439,21 @@ sub handler { } } elsif ($command eq 'scantron_selectphase' && $perm{'mgr'}) { $request->print(&scantron_selectphase($request)); + } elsif ($command eq 'scantron_validate' && $perm{'mgr'}) { + $request->print(&scantron_validate_file($request)); } elsif ($command eq 'scantron_validate' && $perm{'mgr'}) { $request->print(&scantron_validate_file($request)); } elsif ($command eq 'scantron_process' && $perm{'mgr'}) { $request->print(&scantron_process_students($request)); + } elsif ($command eq 'scantronupload' && + &Apache::lonnet::allowed('usc',$ENV{'request.role.domain'})) { + $request->print(&scantron_upload_scantron_data($request)); + + } elsif ($command eq 'scantronupload_save' && + &Apache::lonnet::allowed('usc',$ENV{'request.role.domain'})) { + $request->print(&scantron_upload_scantron_data_save($request)); } elsif ($command) { - $request->print("Access Denied"); + $request->print("Access Denied ($command)"); } } &send_footer($request); @@ -3947,11 +4469,7 @@ sub send_header { #remotewindow.close(); #</script>"); $request->print(&Apache::loncommon::bodytag('Grading')); - foreach my $key (sort(keys(%ENV))) { - if ($key =~ /^form\./) { - Apache->request->print("$key => $ENV{$key} <br />"); - } - } + $request->rflush(); } sub send_footer {