--- loncom/homework/grades.pm 2003/08/17 18:36:46 1.130.2.1 +++ loncom/homework/grades.pm 2003/09/29 20:58:50 1.130.2.1.2.5 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.130.2.1 2003/08/17 18:36:46 albertel Exp $ +# $Id: grades.pm,v 1.130.2.1.2.5 2003/09/29 20:58:50 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -2800,7 +2800,7 @@ sub getSymbMap { my ($request) = @_; my $navmap = Apache::lonnavmaps::navmap-> new($ENV{'request.course.fn'}.'.db', $ENV{'request.course.fn'}.'_parms.db'); - $navmap->init(); +# $navmap->init(); my %symbx = (); my @titles = (); @@ -3146,6 +3146,8 @@ sub getSequenceDropDown { } sub scantron_uploads { + #FIXME need to support scantron files put in another location, + # maybe the course directory? a scantron dir in the course directory? if (!-e $Apache::lonnet::perlvar{'lonScansDir'}) { return ''}; my $result= ' + $default_form_data @@ -3210,11 +3214,18 @@ sub scantron_selectphase { Format of data file: $format_selector + + +
+ + Last line to expect an answer on: + +
- + $grading_menu_button SCANTRONFORM @@ -3226,6 +3237,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; } @@ -3242,6 +3254,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; @@ -3257,8 +3275,50 @@ sub username_to_idmap { return %idmap; } +sub scantron_fixup_scanline { + my ($scantron_config,$scan_data,$line,$field,$newvalue,$arg) = @_; + if ($field eq 'ID') { + if ($newvalue > $$scantron_config{'IDlength'}) { + return ($line,1,'New value to large'); + } + if ($newvalue < $$scantron_config{'IDlength'}) { + $newvalue=sprintf('%-'.$$scantron_config{'IDlength'}.'s', + $newvalue); + } + substr($line,$$scantron_config{'IDstart'}-1, + $$scantron_config{'IDlength'})=$newvalue; + } 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 ($arg ne 'none') { + substr($answer,$arg,1)=$on; + &scan_data($scan_data,"no_bubble.$newvalue",undef,'1'); + } else { + &scan_data($scan_data,"no_bubble.$newvalue",'1'); + } + my $where=$length*($newvalue-1)+$scantron_config->{'Qstart'}; + Apache->request->print("where $where arg $arg "); + Apache->request->print('b:
'.$line.'
'); + substr($line,$where-1,$length)=$answer; + Apache->request->print('a:
'.$line.'
'); + } + 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,$scantron_config,$scan_data)=@_; my %record; my $questions=substr($line,$$scantron_config{'Qstart'}-1); my $data=substr($line,0,$$scantron_config{'Qstart'}-1); @@ -3272,6 +3332,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) { @@ -3279,31 +3348,48 @@ 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("
Wha!!!
".scalar(@array).
-				   '-'.$currentquest.'-'.$questnum.'

'); - } + 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,"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; } sub scantron_add_delay { + my ($delayqueue,$scanline,$errormessage,$errorcode)=@_; + Apache->request->print('add_delay_error '.$_[2] ); + push(@$delayqueue, + {'line' => $scanline, 'emsg' => $errormessage, + 'ecode' => $errorcode } + ); } sub scantron_find_student { my ($scantron_record,$idmap)=@_; my $scanID=$$scantron_record{'scantron.ID'}; foreach my $id (keys(%$idmap)) { - Apache->request->print('
checking studnet -'.$id.'- againt -'.$scanID.'- 
'); - if (lc($id) eq lc($scanID)) { Apache->request->print('success');return $$idmap{$id}; } + #Apache->request->print('
checking studnet -'.$id.'- againt -'.$scanID.'- 
'); + if (lc($id) eq lc($scanID)) { + #Apache->request->print('success'); + return $$idmap{$id}; + } } return undef; } @@ -3316,6 +3402,363 @@ 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,'ID', + $newid); + } 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, + 'answer',$question, + $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= < + + + + + + $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)) { + my $which="scantron_validate_".$validate_phases[$currentphase]; + { + no strict 'refs'; + ($stop,$currentphase)=&$which($r,$currentphase); + } + } + $r->print(""); + return ''; +} + +sub scantron_getfile { + #my $scanlines=Apache::File->new($Apache::lonnet::perlvar{'lonScansDir'}."/$ENV{'form.scantron_selectfile'}"); + #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'}); + if ($lines eq '-1') { + #FIXME need to actually replicate file to course space + } + my %scanlines; + $scanlines{'orig'}=[split("\n",$lines)]; + 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)]; + } + $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)]; + } + 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) { next; } + my $scan_record=&scantron_parse_scanline($line,\%scantron_config,$scan_data); + my $id=$$scan_record{'scantron.ID'}; + $r->print("

Checking ID ".$$scan_record{'scantron.ID'}. + " on paper ID ".$$scan_record{'scantron.PaperID'}."

\n"); + my $found; + foreach my $checkid (keys(%idmap)) { + if (lc($checkid) eq lc($id)) { + if ($checkid ne $id) { + $r->print("

Using $checkid for encoded $id

\n"); + } + $found=$checkid;last; + } + } + if ($found) { + if ($found{'ids'}{$found}) { + #FIXME store away line we prviously saw the ID on + &scantron_get_correction($r,$i,$scan_record,\%scantron_config, + $line,'duplicateID',$found); + return(1); + } else { + $found{'ids'}{$found}++; + } + } 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("

This scantron record has an error ($error). "); + if ( defined($$scan_record{'scantron.PaperID'}) ) { + $r->print("The current PaperID is ". + $$scan_record{'scantron.PaperID'}." \n"); + } else { + $r->print("The current scanline is

".
+		  $line."
\n"); + } + $r->print(''."\n"); + $r->print(''."\n"); + if ($error =~ /ID$/) { + if ($error eq 'unknownID') { + $r->print("The encoded ID is not in the classlist

\n"); + } elsif ($error eq 'duplicateID') { + $r->print("The encoded ID has also been used by a previous paper $arg

\n"); + } + $r->print("

Original ID is ".$$scan_record{'scantron.ID'}. + "
\n"); + $r->print("Name on paper is ".$$scan_record{'scantron.LastName'}.",". + $$scan_record{'scantron.FirstName'}."

"); + $r->print("

Please correct
\n"); + $r->print("\n