# The LearningOnline Network with CAPA # The LON-CAPA Grading handler # # $Id: grades.pm,v 1.446 2007/10/08 22:29:59 banghart Exp $ # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ # package Apache::grades; use strict; use Apache::style; use Apache::lonxml; use Apache::lonnet; use Apache::loncommon; use Apache::lonhtmlcommon; use Apache::lonnavmaps; use Apache::lonhomework; use Apache::loncoursedata; use Apache::lonmsg(); use Apache::Constants qw(:common); use Apache::lonlocal; use Apache::lonenc; use String::Similarity; use LONCAPA; use POSIX qw(floor); my %perm=(); my %bubble_lines_per_response; # no. bubble lines for each response. # index is "symb.part_id" # ----- These first few routines are general use routines.---- # # --- Retrieve the parts from the metadata file.--- sub getpartlist { my ($symb) = @_; my $navmap = Apache::lonnavmaps::navmap->new(); my $res = $navmap->getBySymb($symb); my $partlist = $res->parts(); my $url = $res->src(); my @metakeys = split(/,/,&Apache::lonnet::metadata($url,'keys')); my @stores; foreach my $part (@{ $partlist }) { foreach my $key (@metakeys) { if ($key =~ m/^stores_\Q$part\E_/) { push(@stores,$key); } } } return @stores; } # --- Get the symbolic name of a problem and the url sub get_symb { my ($request,$silent) = @_; (my $url=$env{'form.url'}) =~ s-^http://($ENV{'SERVER_NAME'}|$ENV{'HTTP_HOST'})--; my $symb=($env{'form.symb'} ne '' ? $env{'form.symb'} : (&Apache::lonnet::symbread($url))); if ($symb eq '') { if (!$silent) { $request->print("Unable to handle ambiguous references:$url:."); return (); } } &Apache::lonenc::check_decrypt(\$symb); return ($symb); } #--- Format fullname, username:domain if different for display #--- Use anywhere where the student names are listed sub nameUserString { my ($type,$fullname,$uname,$udom) = @_; if ($type eq 'header') { return ' Fullname (Username)'; } else { return ' '.$fullname.' ('.$uname. ($env{'user.domain'} eq $udom ? '' : ' ('.$udom.')').')'; } } #--- Get the partlist and the response type for a given problem. --- #--- Indicate if a response type is coded handgraded or not. --- sub response_type { my ($symb) = shift; my $navmap = Apache::lonnavmaps::navmap->new(); my $res = $navmap->getBySymb($symb); my $partlist = $res->parts(); my %vPart = map { $_ => 1 } (&Apache::loncommon::get_env_multiple('form.vPart')); my (%response_types,%handgrade); foreach my $part (@{ $partlist }) { next if (%vPart && !exists($vPart{$part})); my @types = $res->responseType($part); my @ids = $res->responseIds($part); for (my $i=0; $i < scalar(@ids); $i++) { $response_types{$part}{$ids[$i]} = $types[$i]; $handgrade{$part.'_'.$ids[$i]} = &Apache::lonnet::EXT('resource.'.$part.'_'.$ids[$i]. '.handgrade',$symb); } } return ($partlist,\%handgrade,\%response_types); } sub flatten_responseType { my ($responseType) = @_; my @part_response_id = map { my $part = $_; map { [$part,$_] } sort(keys(%{ $responseType->{$part} })); } sort(keys(%$responseType)); return @part_response_id; } sub get_display_part { my ($partID,$symb)=@_; my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display',$symb); if (defined($display) and $display ne '') { $display.= " (id $partID)"; } else { $display=$partID; } return $display; } #--- Show resource title #--- and parts and response type sub showResourceInfo { my ($symb,$probTitle,$checkboxes) = @_; my $col=3; if ($checkboxes) { $col=4; } my $result = '

'.&mt('Current Resource').': '.$probTitle.'

'."\n"; $result .=''; my ($partlist,$handgrade,$responseType) = &response_type($symb); my %resptype = (); my $hdgrade='no'; my %partsseen; foreach my $partID (sort keys(%$responseType)) { foreach my $resID (sort keys(%{ $responseType->{$partID} })) { my $handgrade=$$handgrade{$partID.'_'.$resID}; my $responsetype = $responseType->{$partID}->{$resID}; $hdgrade = $handgrade if ($handgrade eq 'yes'); $result.=''; if ($checkboxes) { if (exists($partsseen{$partID})) { $result.=""; } else { $result.=""; } $partsseen{$partID}=1; } my $display_part=&get_display_part($partID,$symb); $result.=''. ''; # ''; } } $result.='
 Part: '.$display_part.' '. $resID.'Type: '.$responsetype.'
Handgrade: '.$handgrade.'
'."\n"; return $result,$responseType,$hdgrade,$partlist,$handgrade; } sub reset_caches { &reset_analyze_cache(); &reset_perm(); } { my %analyze_cache; sub reset_analyze_cache { undef(%analyze_cache); } sub get_analyze { my ($symb,$uname,$udom)=@_; my $key = "$symb\0$uname\0$udom"; return $analyze_cache{$key} if (exists($analyze_cache{$key})); my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb); $url=&Apache::lonnet::clutter($url); my $subresult=&Apache::lonnet::ssi($url, ('grade_target' => 'analyze'), ('grade_domain' => $udom), ('grade_symb' => $symb), ('grade_courseid' => $env{'request.course.id'}), ('grade_username' => $uname)); (undef,$subresult)=split(/_HASH_REF__/,$subresult,2); my %analyze=&Apache::lonnet::str2hash($subresult); return $analyze_cache{$key} = \%analyze; } sub get_order { my ($partid,$respid,$symb,$uname,$udom)=@_; my $analyze = &get_analyze($symb,$uname,$udom); return $analyze->{"$partid.$respid.shown"}; } sub get_radiobutton_correct_foil { my ($partid,$respid,$symb,$uname,$udom)=@_; my $analyze = &get_analyze($symb,$uname,$udom); foreach my $foil (@{&get_order($partid,$respid,$symb,$uname,$udom)}) { if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') { return $foil; } } } } #--- Clean response type for display #--- Currently filters option/rank/radiobutton/match/essay/Task # response types only. sub cleanRecord { my ($answer,$response,$symb,$partid,$respid,$record,$order,$version, $uname,$udom) = @_; my $grayFont = ''; if ($response =~ /^(option|rank)$/) { my %answer=&Apache::lonnet::str2hash($answer); my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); my ($toprow,$bottomrow); foreach my $foil (@$order) { if ($grading{$foil} == 1) { $toprow.=''.$answer{$foil}.' '; } else { $toprow.=''.$answer{$foil}.' '; } $bottomrow.=''.$grayFont.$foil.' '; } return '
'. ''.$toprow.''. ''. $grayFont.$bottomrow.''.'
Answer
'.$grayFont.'Option ID
'; } elsif ($response eq 'match') { my %answer=&Apache::lonnet::str2hash($answer); my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); my @items=&Apache::lonnet::str2array($record->{$version."resource.$partid.$respid.submissionitems"}); my ($toprow,$middlerow,$bottomrow); foreach my $foil (@$order) { my $item=shift(@items); if ($grading{$foil} == 1) { $toprow.=''.$item.' '; $middlerow.=''.$grayFont.$answer{$foil}.' '; } else { $toprow.=''.$item.' '; $middlerow.=''.$grayFont.$answer{$foil}.' '; } $bottomrow.=''.$grayFont.$foil.' '; } return '
'. ''.$toprow.''. ''. $middlerow.''. ''. $bottomrow.''.'
Answer
'.$grayFont.'Item ID
'.$grayFont.'Option ID
'; } elsif ($response eq 'radiobutton') { my %answer=&Apache::lonnet::str2hash($answer); my ($toprow,$bottomrow); my $correct = &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom); foreach my $foil (@$order) { if (exists($answer{$foil})) { if ($foil eq $correct) { $toprow.='true'; } else { $toprow.='true'; } } else { $toprow.='false'; } $bottomrow.=''.$grayFont.$foil.' '; } return '
'. ''.$toprow.''. ''. $grayFont.$bottomrow.''.'
Answer
'.$grayFont.'Option ID
'; } elsif ($response eq 'essay') { if (! exists ($env{'form.'.$symb})) { my (%keyhash) = &Apache::lonnet::dump('nohist_handgrade', $env{'course.'.$env{'request.course.id'}.'.domain'}, $env{'course.'.$env{'request.course.id'}.'.num'}); my $loginuser = $env{'user.name'}.':'.$env{'user.domain'}; $env{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : ''; $env{'form.kwclr'} = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red'; $env{'form.kwsize'} = $keyhash{$loginuser.'_kwsize'} ne '' ? $keyhash{$loginuser.'_kwsize'} : '0'; $env{'form.kwstyle'} = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : ''; $env{'form.'.$symb} = 1; # so that we don't have to read it from disk for multiple sub of the same prob. } $answer =~ s-\n-
-g; return '

'.&keywords_highlight($answer).'
'; } elsif ( $response eq 'organic') { my $result='Smile representation: "'.$answer.'"'; my $jme=$record->{$version."resource.$partid.$respid.molecule"}; $result.=&Apache::chemresponse::jme_img($jme,$answer,400); return $result; } elsif ( $response eq 'Task') { if ( $answer eq 'SUBMITTED') { my $files = $record->{$version."resource.$respid.$partid.bridgetask.portfiles"}; my $result = &Apache::bridgetask::file_list($files,$uname,$udom); return $result; } elsif ( grep(/^\Q$version\E.*?\.instance$/, keys(%{$record})) ) { my @matches = grep(/^\Q$version\E.*?\.instance$/, keys(%{$record})); return join('
',($version,@matches)); } else { my $result = '

' .&mt('Overall result: [_1]', $record->{$version."resource.$respid.$partid.status"}) .'

'; $result .= ''; return $result; } } elsif ( $response =~ m/(?:numerical|formula)/) { $answer = &Apache::loncommon::format_previous_attempt_value('submission', $answer); } return $answer; } #-- A couple of common js functions sub commonJSfunctions { my $request = shift; $request->print(< function radioSelection(radioButton) { var selection=null; if (radioButton.length > 1) { for (var i=0; i 1) { for (var i=0; i COMMONJSFUNCTIONS } #--- Dumps the class list with usernames,list of sections, #--- section, ids and fullnames for each user. sub getclasslist { my ($getsec,$filterlist) = @_; my @getsec; my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); if (!ref($getsec)) { if ($getsec ne '' && $getsec ne 'all') { @getsec=($getsec); } } else { @getsec=@{$getsec}; } if (grep(/^all$/,@getsec)) { undef(@getsec); } my $classlist=&Apache::loncoursedata::get_classlist(); # Bail out if we were unable to get the classlist return if (! defined($classlist)); # my %sections; my %fullnames; foreach my $student (keys(%$classlist)) { my $end = $classlist->{$student}->[&Apache::loncoursedata::CL_END()]; my $start = $classlist->{$student}->[&Apache::loncoursedata::CL_START()]; my $id = $classlist->{$student}->[&Apache::loncoursedata::CL_ID()]; my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; my $fullname = $classlist->{$student}->[&Apache::loncoursedata::CL_FULLNAME()]; my $status = $classlist->{$student}->[&Apache::loncoursedata::CL_STATUS()]; # filter students according to status selected if ($filterlist && (!($stu_status =~ /Any/))) { if (!($stu_status =~ $status)) { delete ($classlist->{$student}); next; } } $section = ($section ne '' ? $section : 'none'); if (&canview($section)) { if (!@getsec || grep(/^\Q$section\E$/,@getsec)) { $sections{$section}++; $fullnames{$student}=$fullname; } else { delete($classlist->{$student}); } } else { delete($classlist->{$student}); } } my %seen = (); my @sections = sort(keys(%sections)); return ($classlist,\@sections,\%fullnames); } sub canmodify { my ($sec)=@_; if ($perm{'mgr'}) { if (!defined($perm{'mgr_section'})) { # can modify whole class return 1; } else { if ($sec eq $perm{'mgr_section'}) { #can modify the requested section return 1; } else { # can't modify the request section return 0; } } } #can't modify return 0; } sub canview { my ($sec)=@_; if ($perm{'vgr'}) { if (!defined($perm{'vgr_section'})) { # can modify whole class return 1; } else { if ($sec eq $perm{'vgr_section'}) { #can modify the requested section return 1; } else { # can't modify the request section return 0; } } } #can't modify return 0; } #--- Retrieve the grade status of a student for all the parts sub student_gradeStatus { my ($symb,$udom,$uname,$partlist) = @_; my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); my %partstatus = (); foreach (@$partlist) { my ($status,undef) = split(/_/,$record{"resource.$_.solved"},2); $status = 'nothing' if ($status eq ''); $partstatus{$_} = $status; my $subkey = "resource.$_.submitted_by"; $partstatus{$subkey} = $record{$subkey} if ($record{$subkey} ne ''); } return %partstatus; } # hidden form and javascript that calls the form # Use by verifyscript and viewgrades # Shows a student's view of problem and submission sub jscriptNform { my ($symb) = @_; my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); my $jscript=''."\n"; $jscript.= '
'."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". '
'."\n"; return $jscript; } # Given the score (as a number [0-1] and the weight) what is the final # point value? This function will round to the nearest tenth, third, # or quarter if one of those is within the tolerance of .00001. sub compute_points { my ($score, $weight) = @_; my $tolerance = .00001; my $points = $score * $weight; # Check for nearness to 1/x. my $check_for_nearness = sub { my ($factor) = @_; my $num = ($points * $factor) + $tolerance; my $floored_num = floor($num); if ($num - $floored_num < 2 * $tolerance * $factor) { return $floored_num / $factor; } return $points; }; $points = $check_for_nearness->(10); $points = $check_for_nearness->(3); $points = $check_for_nearness->(4); return $points; } #------------------ End of general use routines -------------------- # # Find most similar essay # sub most_similar { my ($uname,$udom,$uessay,$old_essays)=@_; # ignore spaces and punctuation $uessay=~s/\W+/ /gs; # ignore empty submissions (occuring when only files are sent) unless ($uessay=~/\w+/) { return ''; } # these will be returned. Do not care if not at least 50 percent similar my $limit=0.6; my $sname=''; my $sdom=''; my $scrsid=''; my $sessay=''; # go through all essays ... foreach my $tkey (keys(%$old_essays)) { my ($tname,$tdom,$tcrsid)=map {&unescape($_)} (split(/\./,$tkey)); # ... except the same student next if (($tname eq $uname) && ($tdom eq $udom)); my $tessay=$old_essays->{$tkey}; $tessay=~s/\W+/ /gs; # String similarity gives up if not even limit my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit); # Found one if ($tsimilar>$limit) { $limit=$tsimilar; $sname=$tname; $sdom=$tdom; $scrsid=$tcrsid; $sessay=$old_essays->{$tkey}; } } if ($limit>0.6) { return ($sname,$sdom,$scrsid,$sessay,$limit); } else { return ('','','','',0); } } #------------------------------------------------------------------- #------------------------------------ Receipt Verification Routines # #--- Check whether a receipt number is valid.--- sub verifyreceipt { my $request = shift; my $courseid = $env{'request.course.id'}; my $receipt = &Apache::lonnet::recprefix($courseid).'-'. $env{'form.receipt'}; $receipt =~ s/[^\-\d]//g; my ($symb) = &get_symb($request); my $title.='

Verifying Submission Receipt '. $receipt.'

'."\n". '

Resource: '.$env{'form.probTitle'}.'



'."\n"; my ($string,$contents,$matches) = ('','',0); my (undef,undef,$fullname) = &getclasslist('all','0'); my $receiptparts=0; if ($env{"course.$courseid.receiptalg"} eq 'receipt2' || $env{"course.$courseid.receiptalg"} eq 'receipt3') { $receiptparts=1; } my $parts=['0']; if ($receiptparts) { ($parts)=&response_type($symb); } foreach (sort { if (lc($$fullname{$a}) ne lc($$fullname{$b})) { return (lc($$fullname{$a}) cmp lc($$fullname{$b})); } return $a cmp $b; } (keys(%$fullname))) { my ($uname,$udom)=split(/\:/); foreach my $part (@$parts) { if ($receipt eq &Apache::lonnet::ireceipt($uname,$udom,$courseid,$symb,$part)) { $contents.=' '."\n". ''.$$fullname{$_}.' '."\n". ' '.$uname.' '. ' '.$udom.' '; if ($receiptparts) { $contents.=' '.$part.' '; } $contents.=''."\n"; $matches++; } } } if ($matches == 0) { $string = $title.'No match found for the above receipt.'; } else { $string = &jscriptNform($symb).$title. 'The above receipt matches the following student'. ($matches <= 1 ? '.' : 's.')."\n". '
'."\n". ''."\n". ''."\n". ''."\n". ''; if ($receiptparts) { $string.=''; } $string.=''."\n".$contents. '
 Fullname  Username  Domain  Problem Part 
'."\n"; } return $string.&show_grading_menu_form($symb); } #--- This is called by a number of programs. #--- Called from the Grading Menu - View/Grade an individual student #--- Also called directly when one clicks on the subm button # on the problem page. sub listStudents { my ($request) = shift; my ($symb) = &get_symb($request); my $cdom = $env{"course.$env{'request.course.id'}.domain"}; my $cnum = $env{"course.$env{'request.course.id'}.num"}; my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; my $submitonly= $env{'form.submitonly'} eq '' ? 'all' : $env{'form.submitonly'}; my $viewgrade = $env{'form.showgrading'} eq 'yes' ? 'View/Grade/Regrade' : 'View'; $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'}; my $result='

 '.$viewgrade. ' Submissions for a Student or a Group of Students

'; my ($table,undef,$hdgrade,$partlist,$handgrade) = &showResourceInfo($symb,$env{'form.probTitle'},($env{'form.showgrading'} eq 'yes')); $request->print(< function checkSelect(checkBox) { var ctr=0; var sense=""; if (checkBox.length > 1) { for (var i=0; i LISTJAVASCRIPT &commonJSfunctions($request); $request->print($result); my $checkhdgrade = ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1 ) ? 'checked="checked"' : ''; my $checklastsub = $checkhdgrade eq '' ? 'checked="checked"' : ''; my $gradeTable='
'. "\n".$table. ' View Problem Text: '."\n". ''."\n". '
'."\n". ' View Answer: '."\n". ''."\n". '
'."\n". ' Submissions: '."\n"; if ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1) { $gradeTable.=''."\n"; } my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); my $saveStatus = $stu_status eq '' ? 'Active' : $stu_status; $env{'form.Status'} = $saveStatus; $gradeTable.=''."\n". ''."\n". ''."\n". '
'."\n". ' Grading Increments: '. &build_section_inputs(). ''."\n". '
'."\n". '
'."\n". ''."\n". ''."\n". ''."\n". ''."\n"; if (exists($env{'form.gradingMenu'}) && exists($env{'form.Status'})) { $gradeTable.=''."\n"; } else { $gradeTable.='Student Status: '. &Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,'javascript:reLoadList(this.form);').'
'; } $gradeTable.='To '.lc($viewgrade).' a submission or a group of submissions, click on the check box(es) '. 'next to the student\'s name(s). Then click on the Next button.
'."\n". ''."\n"; # checkall buttons $gradeTable.=&check_script('gradesub', 'stuinfo'); $gradeTable.='
'."\n"; $gradeTable.=&check_buttons(); $gradeTable.=''; my ($classlist, undef, $fullname) = &getclasslist($getsec,'1'); $gradeTable.='
'. ''; my $loop = 0; while ($loop < 2) { $gradeTable.=''. ''; if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'queued' && $submitonly ne 'all') { foreach (sort(@$partlist)) { my $display_part=&get_display_part((split(/_/))[0],$symb); $gradeTable.=''; } } elsif ($submitonly eq 'queued') { $gradeTable.=''; } $loop++; # $gradeTable.='' if ($loop%2 ==1); } $gradeTable.=''."\n"; my $ctr = 0; foreach my $student (sort { if (lc($$fullname{$a}) ne lc($$fullname{$b})) { return (lc($$fullname{$a}) cmp lc($$fullname{$b})); } return $a cmp $b; } (keys(%$fullname))) { my ($uname,$udom) = split(/:/,$student); my %status = (); if ($submitonly eq 'queued') { my %queue_status = &Apache::bridgetask::get_student_status($symb,$cdom,$cnum, $udom,$uname); next if (!defined($queue_status{'gradingqueue'})); $status{'gradingqueue'} = $queue_status{'gradingqueue'}; } if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'queued' && $submitonly ne 'all') { (%status) =&student_gradeStatus($symb,$udom,$uname,$partlist); my $submitted = 0; my $graded = 0; my $incorrect = 0; foreach (keys(%status)) { $submitted = 1 if ($status{$_} ne 'nothing'); $graded = 1 if ($status{$_} =~ /^ungraded/); $incorrect = 1 if ($status{$_} =~ /^incorrect/); my ($foo,$partid,$foo1) = split(/\./,$_); if ($status{'resource.'.$partid.'.submitted_by'} ne '') { $submitted = 0; my ($part)=split(/\./,$partid); $gradeTable.=''; } } next if (!$submitted && ($submitonly eq 'yes' || $submitonly eq 'incorrect' || $submitonly eq 'graded')); next if (!$graded && ($submitonly eq 'graded')); next if (!$incorrect && $submitonly eq 'incorrect'); } $ctr++; my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; if ( $perm{'vgr'} eq 'F' ) { $gradeTable.='' if ($ctr%2 ==1); $gradeTable.=''. ''."\n".''."\n"; if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { foreach (sort keys(%status)) { next if (/^resource.*?submitted_by$/); $gradeTable.=''."\n"; } } # $gradeTable.='' if ($ctr%2 ==1); $gradeTable.=''."\n" if ($ctr%2 ==0); } } if ($ctr%2 ==1) { $gradeTable.=''; if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'queued' && $submitonly ne 'all') { foreach (@$partlist) { $gradeTable.=''; } } elsif ($submitonly eq 'queued') { $gradeTable.=''; } $gradeTable.=''; } $gradeTable.='
 No.  Select '.&nameUserString('header').' Section/Group Part: '.$display_part. ' Status  '.&mt('Queue Status').' 
'.$ctr.' '. &nameUserString(undef,$$fullname{$student},$uname,$udom). ' '.$section.' '.$status{$_}.' 
     
'."\n". '
'."\n"; if ($ctr == 0) { my $num_students=(scalar(keys(%$fullname))); if ($num_students eq 0) { $gradeTable='
 There are no students currently enrolled.'; } else { my $submissions='submissions'; if ($submitonly eq 'incorrect') { $submissions = 'incorrect submissions'; } if ($submitonly eq 'graded' ) { $submissions = 'ungraded submissions'; } if ($submitonly eq 'queued' ) { $submissions = 'queued submissions'; } $gradeTable='
 '. 'No '.$submissions.' found for this resource for any students. ('.$num_students. ' students checked for '.$submissions.')
'; } } elsif ($ctr == 1) { $gradeTable =~ s/type=checkbox/type=checkbox checked/; } $gradeTable.=&show_grading_menu_form($symb); $request->print($gradeTable); return ''; } #---- Called from the listStudents routine sub check_script { my ($form, $type)=@_; my $chkallscript=''."\n"; return $chkallscript; } sub check_buttons { my $buttons.=''; $buttons.=' '; $buttons.=''; $buttons.=' '; return $buttons; } # Displays the submissions for one student or a group of students sub processGroup { my ($request) = shift; my $ctr = 0; my @stuchecked = &Apache::loncommon::get_env_multiple('form.stuinfo'); my $total = scalar(@stuchecked)-1; foreach my $student (@stuchecked) { my ($uname,$udom,$fullname) = split(/:/,$student); $env{'form.student'} = $uname; $env{'form.userdom'} = $udom; $env{'form.fullname'} = $fullname; &submission($request,$ctr,$total); $ctr++; } return ''; } #------------------------------------------------------------------------------------ # #-------------------------- Next few routines handles grading by student, essentially # handles essay response type problem/part # #--- Javascript to handle the submission page functionality --- sub sub_page_js { my $request = shift; $request->print(< function updateRadio(formname,id,weight) { var gradeBox = formname["GD_BOX"+id]; var radioButton = formname["RADVAL"+id]; var oldpts = formname["oldpts"+id].value; var pts = checkSolved(formname,id) == 'update' ? gradeBox.value : oldpts; gradeBox.value = pts; var resetbox = false; if (isNaN(pts) || pts < 0) { alert("A number equal or greater than 0 is expected. Entered value = "+pts); for (var i=0; i weight) { var resp = confirm("You entered a value ("+pts+ ") greater than the weight for the part. Accept?"); if (resp == false) { gradeBox.value = oldpts; return; } } for (var i=0; i SUBJAVASCRIPT } #--- javascript for essay type problem -- sub sub_page_kw_js { my $request = shift; my $iconpath = $request->dir_config('lonIconsURL'); &commonJSfunctions($request); my $inner_js_msg_central=< function checkInput() { opener.document.SCORE.msgsub.value = opener.checkEntities(document.msgcenter.msgsub.value); var nmsg = opener.document.SCORE.savemsgN.value; var usrctr = document.msgcenter.usrctr.value; var newval = opener.document.SCORE["newmsg"+usrctr]; newval.value = opener.checkEntities(document.msgcenter.newmsg.value); var msgchk = ""; if (document.msgcenter.subchk.checked) { msgchk = "msgsub,"; } var includemsg = 0; for (var i=1; i<=nmsg; i++) { var opnmsg = opener.document.SCORE["savemsg"+i]; var frmmsg = document.msgcenter["msg"+i]; opnmsg.value = opener.checkEntities(frmmsg.value); var showflg = opener.document.SCORE["shownOnce"+i]; showflg.value = "1"; var chkbox = document.msgcenter["msgn"+i]; if (chkbox.checked) { msgchk += "savemsg"+i+","; includemsg = 1; } } if (document.msgcenter.newmsgchk.checked) { msgchk += "newmsg"+usrctr; includemsg = 1; } imgformname = opener.document.SCORE["mailicon"+usrctr]; imgformname.src = "$iconpath/"+((includemsg) ? "mailto.gif" : "mailbkgrd.gif"); var includemsg = opener.document.SCORE["includemsg"+usrctr]; includemsg.value = msgchk; self.close() } INNERJS my $inner_js_highlight_central=< function updateChoice(flag) { opener.document.SCORE.kwclr.value = opener.radioSelection(document.hlCenter.kwdclr); opener.document.SCORE.kwsize.value = opener.radioSelection(document.hlCenter.kwdsize); opener.document.SCORE.kwstyle.value = opener.radioSelection(document.hlCenter.kwdstyle); opener.document.SCORE.refresh.value = "on"; if (opener.document.SCORE.keywords.value!=""){ opener.document.SCORE.submit(); } self.close() } INNERJS my $start_page_msg_central = &Apache::loncommon::start_page('Message Central',$inner_js_msg_central, {'js_ready' => 1, 'only_body' => 1, 'bgcolor' =>'#FFFFFF',}); my $end_page_msg_central = &Apache::loncommon::end_page({'js_ready' => 1}); my $start_page_highlight_central = &Apache::loncommon::start_page('Highlight Central', $inner_js_highlight_central, {'js_ready' => 1, 'only_body' => 1, 'bgcolor' =>'#FFFFFF',}); my $end_page_highlight_central = &Apache::loncommon::end_page({'js_ready' => 1}); my $docopen=&Apache::lonhtmlcommon::javascript_docopen(); $docopen=~s/^document\.//; $request->print(< //===================== Show list of keywords ==================== function keywords(formname) { var nret = prompt("Keywords list, separated by a space. Add/delete to list if desired.",formname.keywords.value); if (nret==null) return; formname.keywords.value = nret; if (formname.keywords.value != "") { formname.refresh.value = "on"; formname.submit(); } return; } //===================== Script to view submitted by ================== function viewSubmitter(submitter) { document.SCORE.refresh.value = "on"; document.SCORE.NCT.value = "1"; document.SCORE.unamedom0.value = submitter; document.SCORE.submit(); return; } //===================== Script to add keyword(s) ================== function getSel() { if (document.getSelection) txt = document.getSelection(); else if (document.selection) txt = document.selection.createRange().text; else return; var cleantxt = txt.replace(new RegExp('([\\f\\n\\r\\t\\v ])+', 'g')," "); if (cleantxt=="") { alert("Please select a word or group of words from document and then click this link."); return; } var nret = prompt("Add selection to keyword list? Edit if desired.",cleantxt); if (nret==null) return; document.SCORE.keywords.value = document.SCORE.keywords.value+" "+nret; if (document.SCORE.keywords.value != "") { document.SCORE.refresh.value = "on"; document.SCORE.submit(); } return; } //====================== Script for composing message ============== // preload images img1 = new Image(); img1.src = "$iconpath/mailbkgrd.gif"; img2 = new Image(); img2.src = "$iconpath/mailto.gif"; function msgCenter(msgform,usrctr,fullname) { var Nmsg = msgform.savemsgN.value; savedMsgHeader(Nmsg,usrctr,fullname); var subject = msgform.msgsub.value; var msgchk = document.SCORE["includemsg"+usrctr].value; re = /msgsub/; var shwsel = ""; if (re.test(msgchk)) { shwsel = "checked" } subject = (document.SCORE.shownSub.value == 0 ? checkEntities(subject) : subject); displaySubject(checkEntities(subject),shwsel); for (var i=1; i<=Nmsg; i++) { var testmsg = "savemsg"+i+","; re = new RegExp(testmsg,"g"); shwsel = ""; if (re.test(msgchk)) { shwsel = "checked" } var message = document.SCORE["savemsg"+i].value; message = (document.SCORE["shownOnce"+i].value == 0 ? checkEntities(message) : message); displaySavedMsg(i,message,shwsel); //I do not get it. w/o checkEntities on saved messages, //any < is already converted to <, etc. However, only once!! } newmsg = document.SCORE["newmsg"+usrctr].value; shwsel = ""; re = /newmsg/; if (re.test(msgchk)) { shwsel = "checked" } newMsg(newmsg,shwsel); msgTail(); return; } function checkEntities(strx) { if (strx.length == 0) return strx; var orgStr = ["&", "<", ">", '"']; var newStr = ["&", "<", ">", """]; var counter = 0; while (counter < 4) { strx = strReplace(strx,orgStr[counter],newStr[counter]); counter++; } return strx; } function strReplace(strx, orgStr, newStr) { return strx.split(orgStr).join(newStr); } function savedMsgHeader(Nmsg,usrctr,fullname) { var height = 70*Nmsg+250; var scrollbar = "no"; if (height > 600) { height = 600; scrollbar = "yes"; } var xpos = (screen.width-600)/2; xpos = (xpos < 0) ? '0' : xpos; var ypos = (screen.height-height)/2-30; ypos = (ypos < 0) ? '0' : ypos; pWin = window.open('', 'MessageCenter', 'resizable=yes,toolbar=no,location=no,scrollbars='+scrollbar+',screenx='+xpos+',screeny='+ypos+',width=600,height='+height); pWin.focus(); pDoc = pWin.document; pDoc.$docopen; pDoc.write('$start_page_msg_central'); pDoc.write("
"); pDoc.write(""); pDoc.write("

 Compose Message for \"+fullname+\"



"); pDoc.write("
"); pDoc.write(""); pDoc.write(""); } function displaySubject(msg,shwsel) { pDoc = pWin.document; pDoc.write(""); pDoc.write(""); pDoc.write(""); pDoc.write(""); } function displaySavedMsg(ctr,msg,shwsel) { pDoc = pWin.document; pDoc.write(""); pDoc.write(""); pDoc.write(""); pDoc.write(""); } function newMsg(newmsg,shwsel) { pDoc = pWin.document; pDoc.write(""); pDoc.write(""); pDoc.write(""); pDoc.write(""); } function msgTail() { pDoc = pWin.document; pDoc.write("
TypeIncludeMessage
Subject
"+ctr+"
New
"); pDoc.write("
 "); pDoc.write("  "); pDoc.write("

"); pDoc.write("
"); pDoc.write('$end_page_msg_central'); pDoc.close(); } //====================== Script for keyword highlight options ============== function kwhighlight() { var kwclr = document.SCORE.kwclr.value; var kwsize = document.SCORE.kwsize.value; var kwstyle = document.SCORE.kwstyle.value; var redsel = ""; var grnsel = ""; var blusel = ""; if (kwclr=="red") {var redsel="checked"}; if (kwclr=="green") {var grnsel="checked"}; if (kwclr=="blue") {var blusel="checked"}; var sznsel = ""; var sz1sel = ""; var sz2sel = ""; if (kwsize=="0") {var sznsel="checked"}; if (kwsize=="+1") {var sz1sel="checked"}; if (kwsize=="+2") {var sz2sel="checked"}; var synsel = ""; var syisel = ""; var sybsel = ""; if (kwstyle=="") {var synsel="checked"}; if (kwstyle=="") {var syisel="checked"}; if (kwstyle=="") {var sybsel="checked"}; highlightCentral(); highlightbody('red','red',redsel,'0','normal',sznsel,'','normal',synsel); highlightbody('green','green',grnsel,'+1','+1',sz1sel,'','italic',syisel); highlightbody('blue','blue',blusel,'+2','+2',sz2sel,'','bold',sybsel); highlightend(); return; } function highlightCentral() { // if (window.hwdWin) window.hwdWin.close(); var xpos = (screen.width-400)/2; xpos = (xpos < 0) ? '0' : xpos; var ypos = (screen.height-330)/2-30; ypos = (ypos < 0) ? '0' : ypos; hwdWin = window.open('', 'KeywordHighlightCentral', 'resizeable=yes,toolbar=no,location=no,scrollbars=no,width=400,height=300,screenx='+xpos+',screeny='+ypos); hwdWin.focus(); var hDoc = hwdWin.document; hDoc.$docopen; hDoc.write('$start_page_highlight_central'); hDoc.write("
"); hDoc.write("

 Keyword Highlight Options



"); hDoc.write("
"); hDoc.write(""); hDoc.write(""); } function highlightbody(clrval,clrtxt,clrsel,szval,sztxt,szsel,syval,sytxt,sysel) { var hDoc = hwdWin.document; hDoc.write(""); hDoc.write(""); hDoc.write(""); hDoc.write(""); hDoc.write(""); } function highlightend() { var hDoc = hwdWin.document; hDoc.write("
Text ColorFont SizeFont Style
"); hDoc.write(" "+clrtxt+""); hDoc.write(" "+sztxt+""); hDoc.write(" "+sytxt+"
"); hDoc.write("
 "); hDoc.write("  "); hDoc.write("

"); hDoc.write("
"); hDoc.write('$end_page_highlight_central'); hDoc.close(); } SUBJAVASCRIPT } sub get_increment { my $increment = $env{'form.increment'}; if ($increment != 1 && $increment != .5 && $increment != .25 && $increment != .1) { $increment = 1; } return $increment; } #--- displays the grading box, used in essay type problem and grading by page/sequence sub gradeBox { my ($request,$symb,$uname,$udom,$counter,$partid,$record) = @_; my $checkIcon = ''.&mt('Check Mark').
	''; my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb,$udom,$uname); my $wgtmsg = ($wgt > 0 ? '(problem weight)' : 'problem weight assigned by computer'); $wgt = ($wgt > 0 ? $wgt : '1'); my $score = ($$record{'resource.'.$partid.'.awarded'} eq '' ? '' : &compute_points($$record{'resource.'.$partid.'.awarded'},$wgt)); my $result=''."\n"; my $display_part=&get_display_part($partid,$symb); my %last_resets = &get_last_resets($symb,$env{'request.course.id'}, [$partid]); my $aggtries = $$record{'resource.'.$partid.'.tries'}; if ($last_resets{$partid}) { $aggtries = &get_num_tries($record,$last_resets{$partid},$partid); } $result.=''."\n"; $result.=''."\n"; $result.='
'. 'Part: '.$display_part.' Points: '."\n"; my $ctr = 0; my $thisweight = 0; my $increment = &get_increment(); $result.=''."\n"; # display radio buttons in a nice table 10 across while ($thisweight<=$wgt) { $result.= '\n"; $result.=(($ctr+1)%10 == 0 ? '' : ''); $thisweight += $increment; $ctr++; } $result.='
'; $result.='
 or /'.$wgt.' '.$wgtmsg. ($$record{'resource.'.$partid.'.solved'} eq 'correct_by_student' ? ' '.$checkIcon : ''). ' '."\n"; $result.=''."\n"; $result.="  \n"; $result.=''."\n". ''."\n". ''."\n". ''."\n". ''."\n"; $result.='
'."\n"; $result.=&handback_box($symb,$uname,$udom,$counter,$partid,$record); return $result; } sub handback_box { my ($symb,$uname,$udom,$counter,$partid,$record) = @_; my ($partlist,$handgrade,$responseType) = &response_type($symb); my (@respids); my @part_response_id = &flatten_responseType($responseType); foreach my $part_response_id (@part_response_id) { my ($part,$resp) = @{ $part_response_id }; if ($part eq $partid) { push(@respids,$resp); } } my $result; foreach my $respid (@respids) { my $prefix = $counter.'_'.$partid.'_'.$respid.'_'; my $files=&get_submitted_files($udom,$uname,$partid,$respid,$record); next if (!@$files); my $file_counter = 1; foreach my $file (@$files) { if ($file =~ /\/portfolio\//) { my ($file_path, $file_disp) = ($file =~ m|(.+/)(.+)$|); my ($name,$version,$ext) = &file_name_version_ext($file_disp); $file_disp = "$name.$ext"; $file = $file_path.$file_disp; $result.=&mt('Return commented version of [_1] to student.', ''.$file_disp.''); $result.=''."\n"; $result.='
'; $result.='(File will be uploaded when you click on Save & Next below.)
'; $file_counter++; } } } return $result; } sub show_problem { my ($request,$symb,$uname,$udom,$removeform,$viewon,$mode,$form) = @_; my $rendered; my %form = ((ref($form) eq 'HASH')? %{$form} : ()); &Apache::lonxml::remember_problem_counter(); if ($mode eq 'both' or $mode eq 'text') { $rendered=&Apache::loncommon::get_student_view($symb,$uname,$udom, $env{'request.course.id'}, undef,\%form); } if ($removeform) { $rendered=~s|||g; $rendered=~s|||g; $rendered=~s|(]*name\s*=\s*"?)(\w+)("?)|$1would_have_been_$2$3|g; } my $companswer; if ($mode eq 'both' or $mode eq 'answer') { &Apache::lonxml::restore_problem_counter(); $companswer= &Apache::loncommon::get_student_answers($symb,$uname,$udom, $env{'request.course.id'}, %form); } if ($removeform) { $companswer=~s|||g; $companswer=~s|||g; $companswer=~s|name="submit"|name="would_have_been_submit"|g; } my $result.='
'; $result.=''; if ($viewon) { $result.=''; } if ($mode eq 'both') { $result.='
'; if ($mode eq 'both' or $mode eq 'text') { $result.='View of the problem - '; } else { $result.='Correct answer: '; } $result.=$env{'form.fullname'}.'
'.$rendered.'
'; $result.='Correct answer:
'.$companswer; } elsif ($mode eq 'text') { $result.='
'.$rendered; } elsif ($mode eq 'answer') { $result.='
'.$companswer; } $result.='
'; $result.='

'; return $result; } sub files_exist { my ($r, $symb) = @_; my @students = &Apache::loncommon::get_env_multiple('form.stuinfo'); foreach my $student (@students) { my ($uname,$udom,$fullname) = split(/:/,$student); my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'}, $udom,$uname); my ($string,$timestamp)= &get_last_submission(\%record); foreach my $submission (@$string) { my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); my $files=&get_submitted_files($udom,$uname,$partid,$respid, \%record); return 1 if (@$files); } } return 0; } sub download_all_link { my ($r,$symb) = @_; my $all_students = join("\n", &Apache::loncommon::get_env_multiple('form.stuinfo')); my $parts = join("\n",&Apache::loncommon::get_env_multiple('form.vPart')); my $identifier = &Apache::loncommon::get_cgi_id(); &Apache::lonnet::appenv('cgi.'.$identifier.'.students' => $all_students, 'cgi.'.$identifier.'.symb' => $symb, 'cgi.'.$identifier.'.parts' => $parts,); $r->print(''. &mt('Download All Submitted Documents').''); return } sub build_section_inputs { my $section_inputs; if ($env{'form.section'} eq '') { $section_inputs .= ''."\n"; } else { my @sections = &Apache::loncommon::get_env_multiple('form.section'); foreach my $section (@sections) { $section_inputs .= ''."\n"; } } return $section_inputs; } # --------------------------- show submissions of a student, option to grade sub submission { my ($request,$counter,$total) = @_; my ($uname,$udom) = ($env{'form.student'},$env{'form.userdom'}); $udom = ($udom eq '' ? $env{'user.domain'} : $udom); #has form.userdom changed for a student? my $usec = &Apache::lonnet::getsection($udom,$uname,$env{'request.course.id'}); $env{'form.fullname'} = &Apache::loncommon::plainname($uname,$udom,'lastname') if $env{'form.fullname'} eq ''; my $symb = &get_symb($request); if ($symb eq '') { $request->print("Unable to handle ambiguous references:."); return ''; } if (!&canview($usec)) { $request->print('Unable to view requested student.('. $uname.':'.$udom.' in section '.$usec.' in course id '. $env{'request.course.id'}.')'); $request->print(&show_grading_menu_form($symb)); return; } if (!$env{'form.lastSub'}) { $env{'form.lastSub'} = 'datesub'; } if (!$env{'form.vProb'}) { $env{'form.vProb'} = 'yes'; } if (!$env{'form.vAns'}) { $env{'form.vAns'} = 'yes'; } my $last = ($env{'form.lastSub'} eq 'last' ? 'last' : ''); my $checkIcon = ''.&mt('Check Mark').
	''; my %old_essays; # header info if ($counter == 0) { &sub_page_js($request); &sub_page_kw_js($request) if ($env{'form.handgrade'} eq 'yes'); $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'}; if ($env{'form.handgrade'} eq 'yes' && &files_exist($request, $symb)) { &download_all_link($request, $symb); } $request->print('

 Submission Record

'."\n". '

 Resource: '.$env{'form.probTitle'}.'

'."\n"); if ($env{'form.handgrade'} eq 'no') { my $checkMark='

 Note: Part(s) graded correct by the computer is marked with a '. $checkIcon.' symbol.'."\n"; $request->print($checkMark); } # option to display problem, only once else it cause problems # with the form later since the problem has a form. if ($env{'form.vProb'} eq 'yes' or $env{'form.vAns'} eq 'yes') { my $mode; if ($env{'form.vProb'} eq 'yes' && $env{'form.vAns'} eq 'yes') { $mode='both'; } elsif ($env{'form.vProb'} eq 'yes') { $mode='text'; } elsif ($env{'form.vAns'} eq 'yes') { $mode='answer'; } &Apache::lonxml::clear_problem_counter(); $request->print(&show_problem($request,$symb,$uname,$udom,0,1,$mode)); } # kwclr is the only variable that is guaranteed to be non blank # if this subroutine has been called once. my %keyhash = (); if ($env{'form.kwclr'} eq '' && $env{'form.handgrade'} eq 'yes') { %keyhash = &Apache::lonnet::dump('nohist_handgrade', $env{'course.'.$env{'request.course.id'}.'.domain'}, $env{'course.'.$env{'request.course.id'}.'.num'}); my $loginuser = $env{'user.name'}.':'.$env{'user.domain'}; $env{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : ''; $env{'form.kwclr'} = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red'; $env{'form.kwsize'} = $keyhash{$loginuser.'_kwsize'} ne '' ? $keyhash{$loginuser.'_kwsize'} : '0'; $env{'form.kwstyle'} = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : ''; $env{'form.msgsub'} = $keyhash{$symb.'_subject'} ne '' ? $keyhash{$symb.'_subject'} : $env{'form.probTitle'}; $env{'form.savemsgN'} = $keyhash{$symb.'_savemsgN'} ne '' ? $keyhash{$symb.'_savemsgN'} : '0'; } my $overRideScore = $env{'form.overRideScore'} eq '' ? 'no' : $env{'form.overRideScore'}; my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); $request->print('
'."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". &build_section_inputs(). ''."\n". ''."\n". ''."\n"); if ($env{'form.handgrade'} eq 'yes') { $request->print(''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n"); foreach my $partid (&Apache::loncommon::get_env_multiple('form.vPart')) { $request->print(''."\n"); } } my ($cts,$prnmsg) = (1,''); while ($cts <= $env{'form.savemsgN'}) { $prnmsg.=''."\n". ''."\n"; $cts++; } $request->print($prnmsg); if ($env{'form.handgrade'} eq 'yes' && $env{'form.showgrading'} eq 'yes') { # # Print out the keyword options line # $request->print(<Keyword Options:  List    Paste Selection to List    Highlight Attribute

KEYWORDS # # Load the other essays for similarity check # my (undef,undef,$essayurl) = &Apache::lonnet::decode_symb($symb); my ($adom,$aname,$apath)=($essayurl=~/^($LONCAPA::domain_re)\/($LONCAPA::username_re)\/(.*)$/); $apath=&escape($apath); $apath=~s/\W/\_/gs; %old_essays=&Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname); } } # This is where output for one specific student would start my $bgcolor='#DDEEDD'; if (int($counter/2) eq $counter) { $bgcolor='#DDDDEE'; } $request->print("\n\n". '

'.$env{'form.fullname'}.'
'); if ($env{'form.vProb'} eq 'all' or $env{'form.vAns'} eq 'all') { my $mode; if ($env{'form.vProb'} eq 'all' && $env{'form.vAns'} eq 'all') { $mode='both'; } elsif ($env{'form.vProb'} eq 'all' ) { $mode='text'; } elsif ($env{'form.vAns'} eq 'all') { $mode='answer'; } &Apache::lonxml::clear_problem_counter(); $request->print(&show_problem($request,$symb,$uname,$udom,1,1,$mode)); } my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); my ($partlist,$handgrade,$responseType) = &response_type($symb); # Display student info $request->print(($counter == 0 ? '' : '
')); my $result='
'."\n". '\n"; if ($$timestamp eq '') { $lastsubonly.='
'."\n"; $result.='Fullname: '.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).'
'."\n"; $result.=''."\n"; # If any part of the problem is an essay-response (handgraded), then check for collaborators my @col_fullnames; my ($classlist,$fullname); if ($env{'form.handgrade'} eq 'yes') { ($classlist,undef,$fullname) = &getclasslist('all','0'); for (keys (%$handgrade)) { my $ncol = &Apache::lonnet::EXT('resource.'.$_. '.maxcollaborators', $symb,$udom,$uname); next if ($ncol <= 0); s/\_/\./g; next if ($record{'resource.'.$_.'.collaborators'} eq ''); my @goodcollaborators = (); my @badcollaborators = (); foreach (split(/,?\s+/,$record{'resource.'.$_.'.collaborators'})) { $_ =~ s/[\$\^\(\)]//g; next if ($_ eq ''); my ($co_name,$co_dom) = split /\@|:/,$_; $co_dom = $udom if (! defined($co_dom) || $co_dom =~ /^domain$/i); next if ($co_name eq $uname && $co_dom eq $udom); # Doing this grep allows 'fuzzy' specification my @Matches = grep /^$co_name:$co_dom$/i,keys %$classlist; if (! scalar(@Matches)) { push @badcollaborators,$_; } else { push @goodcollaborators, @Matches; } } if (scalar(@goodcollaborators) != 0) { $result.='Collaborators: '; foreach (@goodcollaborators) { my ($lastname,$givenn) = split(/,/,$$fullname{$_}); push @col_fullnames, $givenn.' '.$lastname; $result.=$$fullname{$_}.'     '; } $result.='
'."\n"; my ($part)=split(/\./,$_); $result.=''. "\n"; } if (scalar(@badcollaborators) > 0) { $result.='
'; $result.='This student has submitted '; $result.=(scalar(@badcollaborators) == 1) ? 'an invalid collaborator' : 'invalid collaborators'; $result .= ': '.join(', ',@badcollaborators); $result .= '
'; } if (scalar(@badcollaborators > $ncol)) { $result .= '
'; $result .= 'This student has submitted too many '. 'collaborators. Maximum is '.$ncol.'.'; $result .= '
'; } } } $request->print($result."\n"); # print student answer/submission # Options are (1) Handgaded submission only # (2) Last submission, includes submission that is not handgraded # (for multi-response type part) # (3) Last submission plus the parts info # (4) The whole record for this student if ($env{'form.lastSub'} =~ /^(lastonly|hdgrade)$/) { my ($string,$timestamp)= &get_last_submission(\%record); my $lastsubonly=''. ($$timestamp eq '' ? '' : 'Date Submitted: '. $$timestamp)."
'.$$string[0]; } else { my %seenparts; my @part_response_id = &flatten_responseType($responseType); foreach my $part (@part_response_id) { next if ($env{'form.lastSub'} eq 'hdgrade' && $$handgrade{$$part[0].'_'.$$part[1]} ne 'yes'); my ($partid,$respid) = @{ $part }; my $display_part=&get_display_part($partid,$symb); if ($env{"form.$uname:$udom:$partid:submitted_by"}) { if (exists($seenparts{$partid})) { next; } $seenparts{$partid}=1; my $submitby='Part: '.$display_part. ' Collaborative submission by: '. ''. $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.'
'; $request->print($submitby); next; } my $responsetype = $responseType->{$partid}->{$respid}; if (!exists($record{"resource.$partid.$respid.submission"})) { $lastsubonly.='
Part: '. $display_part.' ( ID '.$respid. ' )   '. 'Nothing submitted - no attempts

'; next; } foreach (@$string) { my ($partid,$respid) = /^resource\.([^\.]*)\.([^\.]*)\.submission/; if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; } my ($ressub,$subval) = split(/:/,$_,2); # Similarity check my $similar=''; if($env{'form.checkPlag'}){ my ($oname,$odom,$ocrsid,$oessay,$osim)= &most_similar($uname,$udom,$subval,\%old_essays); if ($osim) { $osim=int($osim*100.0); my %old_course_desc = &Apache::lonnet::coursedescription($ocrsid, {'one_time' => 1}); $similar="

". &mt('Essay is [_1]% similar to an essay by [_2] ([_3]:[_4]) in course [_5] (course id [_6]:[_7])', $osim, &Apache::loncommon::plainname($oname,$odom), $oname,$odom, $old_course_desc{'description'}, $old_course_desc{'num'}, $old_course_desc{'domain'}). '

'. &keywords_highlight($oessay). '

'; } } my $order=&get_order($partid,$respid,$symb,$uname,$udom); if ($env{'form.lastSub'} eq 'lastonly' || ($env{'form.lastSub'} eq 'hdgrade' && $$handgrade{$$part[0].'_'.$$part[1]} eq 'yes')) { my $display_part=&get_display_part($partid,$symb); $lastsubonly.='
Part: '. $display_part.' ( ID '.$respid. ' )   '; my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record); if (@$files) { $lastsubonly.='
Like all files provided by users, this file may contain virusses
'; my $file_counter = 0; foreach my $file (@$files) { $file_counter ++; &Apache::lonnet::allowuploaded('/adm/grades',$file); $lastsubonly.='
'.$file.''; } $lastsubonly.='
'; } $lastsubonly.='Submitted Answer: '. &cleanRecord($subval,$responsetype,$symb,$partid, $respid,\%record,$order); if ($similar) {$lastsubonly.="

$similar\n";} } } } } $lastsubonly.='
'."\n"; $request->print($lastsubonly); } elsif ($env{'form.lastSub'} eq 'datesub') { my (undef,$responseType,undef,$parts) = &showResourceInfo($symb); $request->print(&displaySubByDates($symb,\%record,$parts,$responseType,$checkIcon,$uname,$udom)); } elsif ($env{'form.lastSub'} =~ /^(last|all)$/) { $request->print(&Apache::loncommon::get_previous_attempt($symb,$uname,$udom, $env{'request.course.id'}, $last,'.submission', 'Apache::grades::keywords_highlight')); } $request->print(''."\n"); # return if view submission with no grading option if ($env{'form.showgrading'} eq '' || (!&canmodify($usec))) { my $toGrade.='  '."\n" if (&canmodify($usec)); $toGrade.='
'."\n"; if (($env{'form.command'} eq 'submission') || ($env{'form.command'} eq 'processGroup' && $counter == $total)) { $toGrade.=''.&show_grading_menu_form($symb); } $request->print($toGrade); return; } else { $request->print('
'."\n"); } # essay grading message center if ($env{'form.handgrade'} eq 'yes') { my ($lastname,$givenn) = split(/,/,$env{'form.fullname'}); my $msgfor = $givenn.' '.$lastname; if (scalar(@col_fullnames) > 0) { my $lastone = pop @col_fullnames; $msgfor .= ', '.(join ', ',@col_fullnames).' and '.$lastone.'.'; } $msgfor =~ s/\'/\\'/g; #' stupid emacs - no! javascript $result=''."\n". ''."\n"; $result.=' '. &mt('Compose message to student').(scalar(@col_fullnames) >= 1 ? 's' : '').')'. ''."\n". '
 ('. &mt('Message will be sent when you click on Save & Next below.').")\n"; $request->print($result); } if ($perm{'vgr'}) { $request->print('
'. &Apache::loncommon::track_student_link(&mt('View recent activity'), $uname,$udom,'check')); } if ($perm{'opa'}) { $request->print('
'. &Apache::loncommon::pprmlink(&mt('Set/Change parameters'), $uname,$udom,$symb,'check')); } my %seen = (); my @partlist; my @gradePartRespid; my @part_response_id = &flatten_responseType($responseType); foreach my $part_response_id (@part_response_id) { my ($partid,$respid) = @{ $part_response_id }; my $part_resp = join('_',@{ $part_response_id }); next if ($seen{$partid} > 0); $seen{$partid}++; next if ($$handgrade{$part_resp} ne 'yes' && $env{'form.lastSub'} eq 'hdgrade'); push @partlist,$partid; push @gradePartRespid,$partid.'.'.$respid; $request->print(&gradeBox($request,$symb,$uname,$udom,$counter,$partid,\%record)); } $result=''."\n"; $result.=''."\n" if ($counter == 0); my $ctr = 0; while ($ctr < scalar(@partlist)) { $result.=''."\n"; $ctr++; } $request->print($result.''."\n"); # Done with printing info for one student $request->print('

'); # print end of form if ($counter == $total) { my $endform='
'."\n"; $endform.='  '."\n"; my $ntstu =''."\n"; my $nsel = ($env{'form.NTSTU'} ne '' ? $env{'form.NTSTU'} : '1'); $ntstu =~ s/
'; $endform.=&show_grading_menu_form($symb); $request->print($endform); } return ''; } #--- Retrieve the last submission for all the parts sub get_last_submission { my ($returnhash)=@_; my (@string,$timestamp); if ($$returnhash{'version'}) { my %lasthash=(); my ($version); for ($version=1;$version<=$$returnhash{'version'};$version++) { foreach my $key (sort(split(/\:/, $$returnhash{$version.':keys'}))) { $lasthash{$key}=$$returnhash{$version.':'.$key}; $timestamp = scalar(localtime($$returnhash{$version.':timestamp'})); } } foreach my $key (keys(%lasthash)) { next if ($key !~ /\.submission$/); my ($partid,$foo) = split(/submission$/,$key); my $draft = $lasthash{$partid.'awarddetail'} eq 'DRAFT' ? 'Draft Copy ' : ''; push(@string, join(':', $key, $draft.$lasthash{$key})); } } if (!@string) { $string[0] = 'Nothing submitted - no attempts.'; } return (\@string,\$timestamp); } #--- High light keywords, with style choosen by user. sub keywords_highlight { my $string = shift; my $size = $env{'form.kwsize'} eq '0' ? '' : 'size='.$env{'form.kwsize'}; my $styleon = $env{'form.kwstyle'} eq '' ? '' : $env{'form.kwstyle'}; (my $styleoff = $styleon) =~ s/\$styleon$keyword$styleoff<\/font>/gi; } return $string; } #--- Called from submission routine sub processHandGrade { my ($request) = shift; my $symb = &get_symb($request); my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); my $button = $env{'form.gradeOpt'}; my $ngrade = $env{'form.NCT'}; my $ntstu = $env{'form.NTSTU'}; my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; if ($button eq 'Save & Next') { my $ctr = 0; while ($ctr < $ngrade) { my ($uname,$udom) = split(/:/,$env{'form.unamedom'.$ctr}); my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$symb,$uname,$udom,$ctr); if ($errorflag eq 'no_score') { $ctr++; next; } if ($errorflag eq 'not_allowed') { $request->print("Not allowed to modify grades for $uname:$udom"); $ctr++; next; } my $includemsg = $env{'form.includemsg'.$ctr}; my ($subject,$message,$msgstatus) = ('','',''); my $restitle = &Apache::lonnet::gettitle($symb); my ($feedurl,$showsymb) = &get_feedurl_and_symb($symb,$uname,$udom); my $messagetail; if ($includemsg =~ /savemsg|newmsg\Q$ctr\E/) { $subject = $env{'form.msgsub'} if ($includemsg =~ /msgsub/); unless ($subject=~/\w/) { $subject=&mt('Grading Feedback'); } $subject.=' ['.$restitle.']'; my (@msgnum) = split(/,/,$includemsg); foreach (@msgnum) { $message.=$env{'form.'.$_} if ($_ =~ /savemsg|newmsg/ && $_ ne ''); } $message =&Apache::lonfeedback::clear_out_html($message); if ($env{'form.withgrades'.$ctr}) { $message.="\n\nPoint".($pts > 1 ? 's':'').' awarded = '.$pts.' out of '.$wgt; $messagetail = " for $env{'form.probTitle'}"; } $msgstatus = &Apache::lonmsg::user_normal_msg($uname,$udom,$subject, $message.$messagetail, undef,$feedurl,undef, undef,undef,$showsymb, $restitle); $request->print('
'.&mt('Sending message to [_1]:[_2]',$uname,$udom).': '. $msgstatus); } if ($env{'form.collaborator'.$ctr}) { my @collabstrs=&Apache::loncommon::get_env_multiple("form.collaborator$ctr"); foreach my $collabstr (@collabstrs) { my ($part,@collaborators) = split(/:/,$collabstr); foreach my $collaborator (@collaborators) { my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$symb,$collaborator,$udom,$ctr, $env{'form.unamedom'.$ctr},$part); if ($errorflag eq 'not_allowed') { $request->print("".&mt('Not allowed to modify grades for [_1]',"$collaborator:$udom").""); next; } elsif ($message ne '') { my ($baseurl,$showsymb) = &get_feedurl_and_symb($symb,$collaborator, $udom); if ($env{'form.withgrades'.$ctr}) { $messagetail = " for $env{'form.probTitle'}"; } $msgstatus = &Apache::lonmsg::user_normal_msg($collaborator,$udom,$subject,$message.$messagetail,undef,$baseurl,undef,undef,undef,$showsymb,$restitle); } } } } $ctr++; } } if ($env{'form.handgrade'} eq 'yes') { # Keywords sorted in alphabatical order my $loginuser = $env{'user.name'}.':'.$env{'user.domain'}; my %keyhash = (); $env{'form.keywords'} =~ s/,\s{0,}|\s+/ /g; $env{'form.keywords'} =~ s/^\s+|\s+$//; my (@keywords) = sort(split(/\s+/,$env{'form.keywords'})); $env{'form.keywords'} = join(' ',@keywords); $keyhash{$symb.'_keywords'} = $env{'form.keywords'}; $keyhash{$symb.'_subject'} = $env{'form.msgsub'}; $keyhash{$loginuser.'_kwclr'} = $env{'form.kwclr'}; $keyhash{$loginuser.'_kwsize'} = $env{'form.kwsize'}; $keyhash{$loginuser.'_kwstyle'} = $env{'form.kwstyle'}; # message center - Order of message gets changed. Blank line is eliminated. # New messages are saved in env for the next student. # All messages are saved in nohist_handgrade.db my ($ctr,$idx) = (1,1); while ($ctr <= $env{'form.savemsgN'}) { if ($env{'form.savemsg'.$ctr} ne '') { $keyhash{$symb.'_savemsg'.$idx} = $env{'form.savemsg'.$ctr}; $idx++; } $ctr++; } $ctr = 0; while ($ctr < $ngrade) { if ($env{'form.newmsg'.$ctr} ne '') { $keyhash{$symb.'_savemsg'.$idx} = $env{'form.newmsg'.$ctr}; $env{'form.savemsg'.$idx} = $env{'form.newmsg'.$ctr}; $idx++; } $ctr++; } $env{'form.savemsgN'} = --$idx; $keyhash{$symb.'_savemsgN'} = $env{'form.savemsgN'}; my $putresult = &Apache::lonnet::put ('nohist_handgrade',\%keyhash,$cdom,$cnum); } # Called by Save & Refresh from Highlight Attribute Window my (undef,undef,$fullname) = &getclasslist($env{'form.section'},'1'); if ($env{'form.refresh'} eq 'on') { my ($ctr,$total) = (0,0); while ($ctr < $ngrade) { $total++ if $env{'form.unamedom'.$ctr} ne ''; $ctr++; } $env{'form.NTSTU'}=$ngrade; $ctr = 0; while ($ctr < $total) { my $processUser = $env{'form.unamedom'.$ctr}; ($env{'form.student'},$env{'form.userdom'}) = split(/:/,$processUser); $env{'form.fullname'} = $$fullname{$processUser}; &submission($request,$ctr,$total-1); $ctr++; } return ''; } # Go directly to grade student - from submission or link from chart page if ($button eq 'Grade Student') { (undef,undef,$env{'form.handgrade'},undef,undef) = &showResourceInfo($symb); my $processUser = $env{'form.unamedom'.$env{'form.studentNo'}}; ($env{'form.student'},$env{'form.userdom'}) = split(/:/,$processUser); $env{'form.fullname'} = $$fullname{$processUser}; &submission($request,0,0); return ''; } # Get the next/previous one or group of students my $firststu = $env{'form.unamedom0'}; my $laststu = $env{'form.unamedom'.($ngrade-1)}; my $ctr = 2; while ($laststu eq '') { $laststu = $env{'form.unamedom'.($ngrade-$ctr)}; $ctr++; $laststu = $firststu if ($ctr > $ngrade); } my (@parsedlist,@nextlist); my ($nextflg) = 0; foreach (sort { if (lc($$fullname{$a}) ne lc($$fullname{$b})) { return (lc($$fullname{$a}) cmp lc($$fullname{$b})); } return $a cmp $b; } (keys(%$fullname))) { if ($nextflg == 1 && $button =~ /Next$/) { push @parsedlist,$_; } $nextflg = 1 if ($_ eq $laststu); if ($button eq 'Previous') { last if ($_ eq $firststu); push @parsedlist,$_; } } $ctr = 0; @parsedlist = reverse @parsedlist if ($button eq 'Previous'); my ($partlist) = &response_type($symb); foreach my $student (@parsedlist) { my $submitonly=$env{'form.submitonly'}; my ($uname,$udom) = split(/:/,$student); if ($submitonly eq 'queued') { my %queue_status = &Apache::bridgetask::get_student_status($symb,$cdom,$cnum, $udom,$uname); next if (!defined($queue_status{'gradingqueue'})); } if ($submitonly =~ /^(yes|graded|incorrect)$/) { # my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); my %status=&student_gradeStatus($symb,$udom,$uname,$partlist); my $submitted = 0; my $ungraded = 0; my $incorrect = 0; foreach (keys(%status)) { $submitted = 1 if ($status{$_} ne 'nothing'); $ungraded = 1 if ($status{$_} =~ /^ungraded/); $incorrect = 1 if ($status{$_} =~ /^incorrect/); my ($foo,$partid,$foo1) = split(/\./,$_); if ($status{'resource.'.$partid.'.submitted_by'} ne '') { $submitted = 0; } } next if (!$submitted && ($submitonly eq 'yes' || $submitonly eq 'incorrect' || $submitonly eq 'graded')); next if (!$ungraded && ($submitonly eq 'graded')); next if (!$incorrect && $submitonly eq 'incorrect'); } push @nextlist,$student if ($ctr < $ntstu); last if ($ctr == $ntstu); $ctr++; } $ctr = 0; my $total = scalar(@nextlist)-1; foreach (sort @nextlist) { my ($uname,$udom,$submitter) = split(/:/); $env{'form.student'} = $uname; $env{'form.userdom'} = $udom; $env{'form.fullname'} = $$fullname{$_}; &submission($request,$ctr,$total); $ctr++; } if ($total < 0) { my $the_end = '

LON-CAPA User Message


'."\n"; $the_end.='Message: No more students for this section or class.

'."\n"; $the_end.='Click on the button below to return to the grading menu.

'."\n"; $the_end.=&show_grading_menu_form($symb); $request->print($the_end); } return ''; } #---- Save the score and award for each student, if changed sub saveHandGrade { my ($request,$symb,$stuname,$domain,$newflg,$submitter,$part) = @_; my @version_parts; my $usec = &Apache::lonnet::getsection($domain,$stuname, $env{'request.course.id'}); if (!&canmodify($usec)) { return('not_allowed'); } my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$domain,$stuname); my @parts_graded; my %newrecord = (); my ($pts,$wgt) = ('',''); my %aggregate = (); my $aggregateflag = 0; my @parts = split(/:/,$env{'form.partlist'.$newflg}); foreach my $new_part (@parts) { #collaborator ($submi may vary for different parts if ($submitter && $new_part ne $part) { next; } my $dropMenu = $env{'form.GD_SEL'.$newflg.'_'.$new_part}; if ($dropMenu eq 'excused') { if ($record{'resource.'.$new_part.'.solved'} ne 'excused') { $newrecord{'resource.'.$new_part.'.solved'} = 'excused'; if (exists($record{'resource.'.$new_part.'.awarded'})) { $newrecord{'resource.'.$new_part.'.awarded'} = ''; } $newrecord{'resource.'.$new_part.'.regrader'}="$env{'user.name'}:$env{'user.domain'}"; } } elsif ($dropMenu eq 'reset status' && exists($record{'resource.'.$new_part.'.solved'})) { #don't bother if no old records -> no attempts foreach my $key (keys (%record)) { if ($key=~/^resource\.\Q$new_part\E\./) { $newrecord{$key} = ''; } } $newrecord{'resource.'.$new_part.'.regrader'}= "$env{'user.name'}:$env{'user.domain'}"; my $totaltries = $record{'resource.'.$part.'.tries'}; my %last_resets = &get_last_resets($symb,$env{'request.course.id'}, [$new_part]); my $aggtries =$totaltries; if ($last_resets{$new_part}) { $aggtries = &get_num_tries(\%record,$last_resets{$new_part}, $new_part); } my $solvedstatus = $record{'resource.'.$new_part.'.solved'}; if ($aggtries > 0) { &decrement_aggs($symb,$new_part,\%aggregate,$aggtries,$totaltries,$solvedstatus); $aggregateflag = 1; } } elsif ($dropMenu eq '') { $pts = ($env{'form.GD_BOX'.$newflg.'_'.$new_part} ne '' ? $env{'form.GD_BOX'.$newflg.'_'.$new_part} : $env{'form.RADVAL'.$newflg.'_'.$new_part}); if ($pts eq '' && $env{'form.GD_SEL'.$newflg.'_'.$new_part} eq '') { next; } $wgt = $env{'form.WGT'.$newflg.'_'.$new_part} eq '' ? 1 : $env{'form.WGT'.$newflg.'_'.$new_part}; my $partial= $pts/$wgt; if ($partial eq $record{'resource.'.$new_part.'.awarded'}) { #do not update score for part if not changed. &handback_files($request,$symb,$stuname,$domain,$newflg,$new_part,\%newrecord); next; } else { push @parts_graded, $new_part; } if ($record{'resource.'.$new_part.'.awarded'} ne $partial) { $newrecord{'resource.'.$new_part.'.awarded'} = $partial; } my $reckey = 'resource.'.$new_part.'.solved'; if ($partial == 0) { if ($record{$reckey} ne 'incorrect_by_override') { $newrecord{$reckey} = 'incorrect_by_override'; } } else { if ($record{$reckey} ne 'correct_by_override') { $newrecord{$reckey} = 'correct_by_override'; } } if ($submitter && ($record{'resource.'.$new_part.'.submitted_by'} ne $submitter)) { $newrecord{'resource.'.$new_part.'.submitted_by'} = $submitter; } $newrecord{'resource.'.$new_part.'.regrader'}= "$env{'user.name'}:$env{'user.domain'}"; } # unless problem has been graded, set flag to version the submitted files unless ($record{'resource.'.$new_part.'.solved'} =~ /^correct_/ || $record{'resource.'.$new_part.'.solved'} eq 'incorrect_by_override' || $dropMenu eq 'reset status') { push (@version_parts,$new_part); } } my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; if (%newrecord) { if (@version_parts) { my @changed_keys = &version_portfiles(\%record, \@parts_graded, $env{'request.course.id'}, $symb, $domain, $stuname, \@version_parts); @newrecord{@changed_keys} = @record{@changed_keys}; foreach my $new_part (@version_parts) { &handback_files($request,$symb,$stuname,$domain,$newflg, $new_part,\%newrecord); } } &Apache::lonnet::cstore(\%newrecord,$symb, $env{'request.course.id'},$domain,$stuname); &check_and_remove_from_queue(\@parts,\%record,\%newrecord,$symb, $cdom,$cnum,$domain,$stuname); } if ($aggregateflag) { &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, $cdom,$cnum); } return ('',$pts,$wgt); } sub check_and_remove_from_queue { my ($parts,$record,$newrecord,$symb,$cdom,$cnum,$domain,$stuname) = @_; my @ungraded_parts; foreach my $part (@{$parts}) { if ( $record->{ 'resource.'.$part.'.awarded'} eq '' && $record->{ 'resource.'.$part.'.solved' } ne 'excused' && $newrecord->{'resource.'.$part.'.awarded'} eq '' && $newrecord->{'resource.'.$part.'.solved' } ne 'excused' ) { push(@ungraded_parts, $part); } } if ( !@ungraded_parts ) { &Apache::bridgetask::remove_from_queue('gradingqueue',$symb,$cdom, $cnum,$domain,$stuname); } } sub handback_files { my ($request,$symb,$stuname,$domain,$newflg,$new_part,$newrecord) = @_; my $portfolio_root = &propath($domain,$stuname).'/userfiles/portfolio'; my ($partlist,$handgrade,$responseType) = &response_type($symb); my @part_response_id = &flatten_responseType($responseType); foreach my $part_response_id (@part_response_id) { my ($part_id,$resp_id) = @{ $part_response_id }; my $part_resp = join('_',@{ $part_response_id }); if (($env{'form.'.$newflg.'_'.$part_resp.'_returndoc1'}) && ($new_part == $part_id)) { # if multiple files are uploaded names will be 'returndoc2','returndoc3' my $file_counter = 1; my $file_msg; while ($env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$file_counter}) { my $fname=$env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$file_counter.'.filename'}; my ($directory,$answer_file) = ($env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$file_counter} =~ /^(.*?)([^\/]*)$/); my ($answer_name,$answer_ver,$answer_ext) = &file_name_version_ext($answer_file); my ($portfolio_path) = ($directory =~ /^.+$stuname\/portfolio(.*)/); my @dir_list = &Apache::lonnet::dirlist($portfolio_path,$domain,$stuname,$portfolio_root); my $version = &get_next_version($answer_name, $answer_ext, \@dir_list); # fix file name my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/); my $result=&Apache::lonnet::finishuserfileupload($stuname,$domain, $newflg.'_'.$part_resp.'_returndoc'.$file_counter, $save_file_name); if ($result !~ m|^/uploaded/|) { $request->print('An error occurred ('.$result. ') while trying to upload '.$newflg.'_'.$part_resp.'_returndoc'.$file_counter.'
'); } else { # mark the file as read only my @files = ($save_file_name); my @what = ($symb,$env{'request.course.id'},'handback'); &Apache::lonnet::mark_as_readonly($domain,$stuname,\@files,\@what); if (exists($$newrecord{"resource.$new_part.$resp_id.handback"})) { $$newrecord{"resource.$new_part.$resp_id.handback"}.=','; } $$newrecord{"resource.$new_part.$resp_id.handback"} .= $save_file_name; $file_msg.= "\n".'
'.$save_file_name."
"; } $request->print("
".$fname." will be the uploaded file name"); $request->print(" ".$env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$file_counter}); $file_counter++; } my $subject = "File Handed Back by Instructor "; my $message = "A file has been returned that was originally submitted in reponse to:
"; $message .= "".&Apache::lonnet::gettitle($symb)."
"; $message .= ' The returned file(s) are named: '. $file_msg; $message .= " and can be found in your portfolio space."; my ($feedurl,$showsymb) = &get_feedurl_and_symb($symb,$domain,$stuname); my $restitle = &Apache::lonnet::gettitle($symb); my $msgstatus = &Apache::lonmsg::user_normal_msg($stuname,$domain,$subject. ' (File Returned) ['.$restitle.']',$message,undef, $feedurl,undef,undef,undef,$showsymb,$restitle); } } return; } sub get_feedurl_and_symb { my ($symb,$uname,$udom) = @_; my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); $url = &Apache::lonnet::clutter($url); my $encrypturl=&Apache::lonnet::EXT('resource.0.encrypturl', $symb,$udom,$uname); if ($encrypturl =~ /^yes$/i) { &Apache::lonenc::encrypted(\$url,1); &Apache::lonenc::encrypted(\$symb,1); } return ($url,$symb); } sub get_submitted_files { my ($udom,$uname,$partid,$respid,$record) = @_; my @files; if ($$record{"resource.$partid.$respid.portfiles"}) { my $file_url = '/uploaded/'.$udom.'/'.$uname.'/portfolio'; foreach my $file (split(',',$$record{"resource.$partid.$respid.portfiles"})) { push(@files,$file_url.$file); } } if ($$record{"resource.$partid.$respid.uploadedurl"}) { push(@files,$$record{"resource.$partid.$respid.uploadedurl"}); } return (\@files); } # ----------- Provides number of tries since last reset. sub get_num_tries { my ($record,$last_reset,$part) = @_; my $timestamp = ''; my $num_tries = 0; if ($$record{'version'}) { for (my $version=$$record{'version'};$version>=1;$version--) { if (exists($$record{$version.':resource.'.$part.'.solved'})) { $timestamp = $$record{$version.':timestamp'}; if ($timestamp > $last_reset) { $num_tries ++; } else { last; } } } } return $num_tries; } # ----------- Determine decrements required in aggregate totals sub decrement_aggs { my ($symb,$part,$aggregate,$aggtries,$totaltries,$solvedstatus) = @_; my %decrement = ( attempts => 0, users => 0, correct => 0 ); $decrement{'attempts'} = $aggtries; if ($solvedstatus =~ /^correct/) { $decrement{'correct'} = 1; } if ($aggtries == $totaltries) { $decrement{'users'} = 1; } foreach my $type (keys (%decrement)) { $$aggregate{$symb."\0".$part."\0".$type} = -$decrement{$type}; } return; } # ----------- Determine timestamps for last reset of aggregate totals for parts sub get_last_resets { my ($symb,$courseid,$partids) =@_; my %last_resets; my $cdom = $env{'course.'.$courseid.'.domain'}; my $cname = $env{'course.'.$courseid.'.num'}; my @keys; foreach my $part (@{$partids}) { push(@keys,"$symb\0$part\0resettime"); } my %results=&Apache::lonnet::get('nohist_resourcetracker',\@keys, $cdom,$cname); foreach my $part (@{$partids}) { $last_resets{$part}=$results{"$symb\0$part\0resettime"}; } return %last_resets; } # ----------- Handles creating versions for portfolio files as answers sub version_portfiles { my ($record, $parts_graded, $courseid, $symb, $domain, $stu_name, $v_flag) = @_; my $version_parts = join('|',@$v_flag); my @returned_keys; my $parts = join('|', @$parts_graded); my $portfolio_root = &propath($domain,$stu_name). '/userfiles/portfolio'; foreach my $key (keys(%$record)) { my $new_portfiles; if ($key =~ /^resource\.($version_parts)\./ && $key =~ /\.portfiles$/ ) { my @versioned_portfiles; my @portfiles = split(/\s*,\s*/,$$record{$key}); foreach my $file (@portfiles) { &Apache::lonnet::unmark_as_readonly($domain,$stu_name,[$symb,$env{'request.course.id'}],$file); my ($directory,$answer_file) =($file =~ /^(.*?)([^\/]*)$/); my ($answer_name,$answer_ver,$answer_ext) = &file_name_version_ext($answer_file); my @dir_list = &Apache::lonnet::dirlist($directory,$domain,$stu_name,$portfolio_root); my $version = &get_next_version($answer_name, $answer_ext, \@dir_list); my $new_answer = &version_selected_portfile($domain, $stu_name, $directory, $answer_file, $version); if ($new_answer ne 'problem getting file') { push(@versioned_portfiles, $directory.$new_answer); &Apache::lonnet::mark_as_readonly($domain,$stu_name, [$directory.$new_answer], [$symb,$env{'request.course.id'},'graded']); } } $$record{$key} = join(',',@versioned_portfiles); push(@returned_keys,$key); } } return (@returned_keys); } sub get_next_version { my ($answer_name, $answer_ext, $dir_list) = @_; my $version; foreach my $row (@$dir_list) { my ($file) = split(/\&/,$row,2); my ($file_name,$file_version,$file_ext) = &file_name_version_ext($file); if (($file_name eq $answer_name) && ($file_ext eq $answer_ext)) { # gets here if filename and extension match, regardless of version if ($file_version ne '') { # a versioned file is found so save it for later if ($file_version > $version) { $version = $file_version; } } } } $version ++; return($version); } sub version_selected_portfile { my ($domain,$stu_name,$directory,$file_name,$version) = @_; my ($answer_name,$answer_ver,$answer_ext) = &file_name_version_ext($file_name); my $new_answer; $env{'form.copy'} = &Apache::lonnet::getfile("/uploaded/$domain/$stu_name/portfolio$directory$file_name"); if($env{'form.copy'} eq '-1') { &Apache::lonnet::logthis('problem getting file '.$file_name); $new_answer = 'problem getting file'; } else { $new_answer = $answer_name.'.'.$version.'.'.$answer_ext; my $copy_result = &Apache::lonnet::finishuserfileupload( $stu_name,$domain,'copy', '/portfolio'.$directory.$new_answer); } return ($new_answer); } sub file_name_version_ext { my ($file)=@_; my @file_parts = split(/\./, $file); my ($name,$version,$ext); if (@file_parts > 1) { $ext=pop(@file_parts); if (@file_parts > 1 && $file_parts[-1] =~ /^\d+$/) { $version=pop(@file_parts); } $name=join('.',@file_parts); } else { $name=join('.',@file_parts); } return($name,$version,$ext); } #-------------------------------------------------------------------------------------- # #-------------------------- Next few routines handles grading by section or whole class # #--- Javascript to handle grading by section or whole class sub viewgrades_js { my ($request) = shift; $request->print(< function writePoint(partid,weight,point) { var radioButton = document.classgrade["RADVAL_"+partid]; var textbox = document.classgrade["TEXTVAL_"+partid]; if (point == "textval") { point = document.classgrade["TEXTVAL_"+partid].value; if (isNaN(point) || parseFloat(point) < 0) { alert("A number equal or greater than 0 is expected. Entered value = "+parseFloat(point)); var resetbox = false; for (var i=0; i parseFloat(weight)) { var resp = confirm("You entered a value ("+parseFloat(point)+ ") greater than the weight for the part. Accept?"); if (resp == false) { textbox.value = ""; return; } } for (var i=0; i parseFloat(weight)) { var resp = confirm("You entered a value ("+parseFloat(point)+ ") greater than the weight of the part. Accept?"); if (resp == false) { textbox.value = ""; return; } } selval[0].selected = true; } function changeOneScore(partid,user) { var selval = document.classgrade["GD_"+user+'_'+partid+"_solved"]; if (selval[1].selected || selval[2].selected) { document.classgrade["GD_"+user+'_'+partid+"_awarded"].value = ""; if (selval[2].selected) { document.classgrade["GD_"+user+'_'+partid+"_tries"].value = "0"; } } } function resetEntry(numpart) { for (ctpart=0;ctpart VIEWJAVASCRIPT } #--- show scores for a section or whole class w/ option to change/update a score sub viewgrades { my ($request) = shift; &viewgrades_js($request); my ($symb) = &get_symb($request); #need to make sure we have the correct data for later EXT calls, #thus invalidate the cache &Apache::lonnet::devalidatecourseresdata( $env{'course.'.$env{'request.course.id'}.'.num'}, $env{'course.'.$env{'request.course.id'}.'.domain'}); &Apache::lonnet::clear_EXT_cache_status(); my $result='

'.&mt('Manual Grading').'

'; $result.='

Current Resource: '.$env{'form.probTitle'}.'

'."\n"; #view individual student submission form - called using Javascript viewOneStudent $result.=&jscriptNform($symb); #beginning of class grading form my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); $result.= '
'."\n". ''."\n". ''."\n". &build_section_inputs(). ''."\n". ''."\n". ''."\n"; my $sectionClass; my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); if ($env{'form.section'} eq 'all') { $sectionClass='Class '; } elsif ($env{'form.section'} eq 'none') { $sectionClass=&mt('Students in no Section').''; } else { $sectionClass=&mt('Students in Section(s) [_1]',$section_display).''; } $result.='

'.&mt('Assign Common Grade To [_1]',$sectionClass); $result.= '
'."\n". '
'; #radio buttons/text box for assigning points for a section or class. #handles different parts of a problem my ($partlist,$handgrade,$responseType) = &response_type($symb); my %weight = (); my $ctsparts = 0; $result.=''; my %seen = (); my @part_response_id = &flatten_responseType($responseType); foreach my $part_response_id (@part_response_id) { my ($partid,$respid) = @{ $part_response_id }; my $part_resp = join('_',@{ $part_response_id }); next if $seen{$partid}; $seen{$partid}++; my $handgrade=$$handgrade{$part_resp}; my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb); $weight{$partid} = $wgt eq '' ? '1' : $wgt; $result.=''."\n"; $result.=''."\n"; my $display_part=&get_display_part($partid,$symb); $result.=''."\n"; $result.= ''. ''."\n"; $ctsparts++; } $result.='
Part: '.$display_part.'   Point: '; $result.=''; my $ctr = 0; while ($ctr<=$weight{$partid}) { # display radio buttons in a nice table 10 across $result.= '\n"; $result.=(($ctr+1)%10 == 0 ? '' : ''); $ctr++; } $result.='
'; $result.= '
or /'. $weight{$partid}.' (problem weight)
'.'
'.'
'."\n". ''; $result.=''; #table listing all the students in a section/class #header of table $result.= '

Assign Grade to Specific Students in '.$sectionClass; $result.= '
'."\n". ''. '\n"; my (@parts) = sort(&getpartlist($symb)); my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb); my @partids = (); foreach my $part (@parts) { my $display=&Apache::lonnet::metadata($url,$part.'.display'); $display =~ s|^Number of Attempts|Tries
|; # makes the column narrower if (!$display) { $display = &Apache::lonnet::metadata($url,$part.'.name'); } my ($partid) = &split_part_type($part); push(@partids, $partid); my $display_part=&get_display_part($partid,$symb); if ($display =~ /^Partial Credit Factor/) { $result.=''."\n"; next; } else { $display =~s/\[Part: \Q$partid\E\]/Part:<\/b> $display_part/; } $display =~ s|Problem Status|Grade Status
|; $result.=''."\n"; } $result.=''; my %last_resets = &get_last_resets($symb,$env{'request.course.id'},\@partids); #get info for each student #list all the students - with points and grade status my (undef,undef,$fullname) = &getclasslist($env{'form.section'},'1'); my $ctr = 0; foreach (sort { if (lc($$fullname{$a}) ne lc($$fullname{$b})) { return (lc($$fullname{$a}) cmp lc($$fullname{$b})); } return $a cmp $b; } (keys(%$fullname))) { $ctr++; $result.=&viewstudentgrade($symb,$env{'request.course.id'}, $_,$$fullname{$_},\@parts,\%weight,$ctr,\%last_resets); } $result.='
 No. '.&nameUserString('header')."Score Part: '.$display_part. '
(weight = '.$weight{$partid}.')
'.$display.'
'; $result.=''."\n"; $result.=''."\n"; if (scalar(%$fullname) eq 0) { my $colspan=3+scalar(@parts); my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); my $stu_status = join(' or ',&Apache::loncommon::get_env_multiple('form.Status')); $result=''. &mt('There are no students in section(s) [_1] with enrollment status [_2] to modify or grade', $section_display, $stu_status). ''; } $result.=&show_grading_menu_form($symb); return $result; } #--- call by previous routine to display each student sub viewstudentgrade { my ($symb,$courseid,$student,$fullname,$parts,$weight,$ctr,$last_resets) = @_; my ($uname,$udom) = split(/:/,$student); my %record=&Apache::lonnet::restore($symb,$courseid,$udom,$uname); my %aggregates = (); my $result=''. ''. "\n".$ctr.'  '. ''.$fullname.' '. '('.$uname.($env{'user.domain'} eq $udom ? '' : ':'.$udom).')'."\n"; $student=~s/:/_/; # colon doen't work in javascript for names foreach my $apart (@$parts) { my ($part,$type) = &split_part_type($apart); my $score=$record{"resource.$part.$type"}; $result.=''; my ($aggtries,$totaltries); unless (exists($aggregates{$part})) { $totaltries = $record{'resource.'.$part.'.tries'}; $aggtries = $totaltries; if ($$last_resets{$part}) { $aggtries = &get_num_tries(\%record,$$last_resets{$part}, $part); } $result.=''."\n"; $result.=''."\n"; $aggregates{$part} = 1; } if ($type eq 'awarded') { my $pts = $score eq '' ? '' : &compute_points($score,$$weight{$part}); $result.=''."\n"; $result.=''."\n"; } elsif ($type eq 'solved') { my ($status,$foo)=split(/_/,$score,2); $status = 'nothing' if ($status eq ''); $result.=''."\n"; $result.='  \n"; } else { $result.=''. "\n"; $result.=''."\n"; } } $result.=''; return $result; } #--- change scores for all the students in a section/class # record does not get update if unchanged sub editgrades { my ($request) = @_; my $symb=&get_symb($request); my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); my $title='

'.&mt('Current Grade Status').'

'; $title.='

'.&mt('Current Resource: [_1]',$env{'form.probTitle'}).'


'."\n"; $title.='

'.&mt('Section: [_1]',$section_display).'

'."\n"; my $result= '
'."\n"; $result.= ''. ''. '\n"; my %scoreptr = ( 'correct' =>'correct_by_override', 'incorrect'=>'incorrect_by_override', 'excused' =>'excused', 'ungraded' =>'ungraded_attempted', 'nothing' => '', ); my ($classlist,undef,$fullname) = &getclasslist($env{'form.section'},'0'); my (@partid); my %weight = (); my %columns = (); my ($i,$ctr,$count,$rec_update) = (0,0,0,0); my (@parts) = sort(&getpartlist($symb)); my $header; while ($ctr < $env{'form.totalparts'}) { my $partid = $env{'form.partid_'.$ctr}; push @partid,$partid; $weight{$partid} = $env{'form.weight_'.$partid}; $ctr++; } my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); foreach my $partid (@partid) { $header .= ''. ''; $columns{$partid}=2; foreach my $stores (@parts) { my ($part,$type) = &split_part_type($stores); if ($part !~ m/^\Q$partid\E/) { next;} if ($type eq 'awarded' || $type eq 'solved') { next; } my $display=&Apache::lonnet::metadata($url,$stores.'.display'); $display =~ s/\[Part: (\w)+\]//; $display =~ s/Number of Attempts/Tries/; $header .= ''. ''; $columns{$partid}+=2; } } foreach my $partid (@partid) { my $display_part=&get_display_part($partid,$symb); $result .= ''; } $result .= ''; $result .= $header; $result .= ''."\n"; my $noupdate; my ($updateCtr,$noupdateCtr) = (1,1); for ($i=0; $i<$env{'form.total'}; $i++) { my $line; my $user = $env{'form.ctr'.$i}; my ($uname,$udom)=split(/:/,$user); my %newrecord; my $updateflag = 0; $line .= ''; my $usec=$classlist->{"$uname:$udom"}[5]; if (!&canmodify($usec)) { my $numcols=scalar(@partid)*4+2; $noupdate.=$line.""; next; } my %aggregate = (); my $aggregateflag = 0; $user=~s/:/_/; # colon doen't work in javascript for names foreach (@partid) { my $old_aw = $env{'form.GD_'.$user.'_'.$_.'_awarded_s'}; my $old_part_pcr = $old_aw/($weight{$_} ne '0' ? $weight{$_}:1); my $old_part = $old_aw eq '' ? '' : $old_part_pcr; my $old_score = $scoreptr{$env{'form.GD_'.$user.'_'.$_.'_solved_s'}}; my $awarded = $env{'form.GD_'.$user.'_'.$_.'_awarded'}; my $pcr = $awarded/($weight{$_} ne '0' ? $weight{$_} : 1); my $partial = $awarded eq '' ? '' : $pcr; my $score; if ($partial eq '') { $score = $scoreptr{$env{'form.GD_'.$user.'_'.$_.'_solved_s'}}; } elsif ($partial > 0) { $score = 'correct_by_override'; } elsif ($partial == 0) { $score = 'incorrect_by_override'; } my $dropMenu = $env{'form.GD_'.$user.'_'.$_.'_solved'}; $score = 'excused' if (($dropMenu eq 'excused') && ($score ne 'excused')); $newrecord{'resource.'.$_.'.regrader'}= "$env{'user.name'}:$env{'user.domain'}"; if ($dropMenu eq 'reset status' && $old_score ne '') { # ignore if no previous attempts => nothing to reset $newrecord{'resource.'.$_.'.tries'} = ''; $newrecord{'resource.'.$_.'.solved'} = ''; $newrecord{'resource.'.$_.'.award'} = ''; $newrecord{'resource.'.$_.'.awarded'} = ''; $updateflag = 1; if ($env{'form.GD_'.$user.'_'.$_.'_aggtries'} > 0) { my $aggtries = $env{'form.GD_'.$user.'_'.$_.'_aggtries'}; my $totaltries = $env{'form.GD_'.$user.'_'.$_.'_totaltries'}; my $solvedstatus = $env{'form.GD_'.$user.'_'.$_.'_solved_s'}; &decrement_aggs($symb,$_,\%aggregate,$aggtries,$totaltries,$solvedstatus); $aggregateflag = 1; } } elsif (!($old_part eq $partial && $old_score eq $score)) { $updateflag = 1; $newrecord{'resource.'.$_.'.awarded'} = $partial if $partial ne ''; $newrecord{'resource.'.$_.'.solved'} = $score; $rec_update++; } $line .= ''. ''; my $partid=$_; foreach my $stores (@parts) { my ($part,$type) = &split_part_type($stores); if ($part !~ m/^\Q$partid\E/) { next;} if ($type eq 'awarded' || $type eq 'solved') { next; } my $old_aw = $env{'form.GD_'.$user.'_'.$part.'_'.$type.'_s'}; my $awarded = $env{'form.GD_'.$user.'_'.$part.'_'.$type}; if ($awarded ne '' && $awarded ne $old_aw) { $newrecord{'resource.'.$part.'.'.$type}= $awarded; $newrecord{'resource.'.$part.'.regrader'}="$env{'user.name'}:$env{'user.domain'}"; $updateflag=1; } $line .= ''. ''; } } $line.=''."\n"; my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; if ($updateflag) { $count++; &Apache::lonnet::cstore(\%newrecord,$symb,$env{'request.course.id'}, $udom,$uname); if (&Apache::bridgetask::in_queue('gradingqueue',$symb,$cdom, $cnum,$udom,$uname)) { # need to figure out if should be in queue. my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'}, $udom,$uname); my $all_graded = 1; my $none_graded = 1; foreach my $part (@parts) { if ( $record{'resource.'.$part.'.awarded'} eq '' ) { $all_graded = 0; } else { $none_graded = 0; } } if ($all_graded || $none_graded) { &Apache::bridgetask::remove_from_queue('gradingqueue', $symb,$cdom,$cnum, $udom,$uname); } } $result.=''.$line; $updateCtr++; } else { $noupdate.=''.$line; $noupdateCtr++; } if ($aggregateflag) { &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, $cdom,$cnum); } } if ($noupdate) { # my $numcols=(scalar(@partid)*(scalar(@parts)-1)*2)+3; my $numcols=scalar(@partid)*4+2; $result .= ''.$noupdate; } $result .= '
 No. '.&nameUserString('header')." Old Score  New Score  Old '.$display.'  New '.$display.' Part: '.$display_part. ' (Weight = '.$weight{$partid}.')
'.&nameUserString(undef,$$fullname{$user},$uname,$udom).'Not allowed to modify student
'.$old_aw.' '.$awarded. ($score eq 'excused' ? $score : '').' '.$old_aw.' '.$awarded.' 
 '.$updateCtr.' 
 '.$noupdateCtr.' 
No Changes Occurred For the Students Below
'."\n". &show_grading_menu_form ($symb); my $msg = '
Number of records updated = '.$rec_update. ' for '.$count.' student'.($count <= 1 ? '' : 's').'.
'. 'Total number of students = '.$env{'form.total'}.'
'; return $title.$msg.$result; } sub split_part_type { my ($partstr) = @_; my ($temp,@allparts)=split(/_/,$partstr); my $type=pop(@allparts); my $part=join('_',@allparts); return ($part,$type); } #------------- end of section for handling grading by section/class --------- # #---------------------------------------------------------------------------- #---------------------------------------------------------------------------- # #-------------------------- Next few routines handles grading by csv upload # #--- Javascript to handle csv upload sub csvupload_javascript_reverse_associate { my $error1=&mt('You need to specify the username or ID'); my $error2=&mt('You need to specify at least one grading field'); return(<3) { foundsomething=1; } } if (founduname==0 && foundID==0) { alert('$error1'); return; } if (foundsomething==0) { alert('$error2'); return; } vf.submit(); } function flip(vf,tf) { var nw=eval('vf.f'+tf+'.selectedIndex'); var i; //can not pick the same destination field twice for (i=0;i<=vf.nfields.value;i++) { if ((i!=tf) && (eval('vf.f'+i+'.selectedIndex')==nw)) { eval('vf.f'+i+'.selectedIndex=0;') } } } ENDPICK } sub csvuploadmap_header { my ($request,$symb,$datatoken,$distotal)= @_; my $javascript; if ($env{'form.upfile_associate'} eq 'reverse') { $javascript=&csvupload_javascript_reverse_associate(); } else { $javascript=&csvupload_javascript_forward_associate(); } my ($result) = &showResourceInfo($symb,$env{'form.probTitle'}); my $checked=(($env{'form.noFirstLine'})?' checked="checked"':''); my $ignore=&mt('Ignore First Line'); $symb = &Apache::lonenc::check_encrypt($symb); $request->print(<

Uploading Class Grades

$result

Identify fields

Total number of records found in file: $distotal
Enter as many fields as you can. The system will inform you and bring you back to this page if the data selected is insufficient to run your class.

ENDPICK return ''; } sub csvupload_fields { my ($symb) = @_; my (@parts) = &getpartlist($symb); my @fields=(['ID','Student ID'], ['username','Student Username'], ['domain','Student Domain']); my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); foreach my $part (sort(@parts)) { my @datum; my $display=&Apache::lonnet::metadata($url,$part.'.display'); my $name=$part; if (!$display) { $display = $name; } @datum=($name,$display); if ($name=~/^stores_(.*)_awarded/) { push(@fields,['stores_'.$1.'_points',"Points [Part: $1]"]); } push(@fields,\@datum); } return (@fields); } sub csvuploadmap_footer { my ($request,$i,$keyfields) =@_; $request->print(<
ENDPICK } sub checkforfile_js { my $result =< 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(); } CSVFORMJS return $result; } sub upcsvScores_form { my ($request) = shift; my ($symb)=&get_symb($request); if (!$symb) {return '';} my $result=&checkforfile_js(); $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb); my ($table) = &showResourceInfo($symb,$env{'form.probTitle'}); $result.=$table; $result.='
'."\n"; $result.=''."\n"; $result.='
'."\n"; $result.=' '.&mt('Specify a file containing the class scores for current resource'). '.
'."\n"; my $upload=&mt("Upload Scores"); my $upfile_select=&Apache::loncommon::upfile_select_html(); my $ignore=&mt('Ignore First Line'); $symb = &Apache::lonenc::check_encrypt($symb); $result.=< $upfile_select
ENDUPFORM $result.=&Apache::loncommon::help_open_topic("Course_Convert_To_CSV", &mt("How do I create a CSV file from a spreadsheet")) .'
'."\n"; $result.='


'."\n"; $result.=&show_grading_menu_form($symb); return $result; } sub csvuploadmap { my ($request)= @_; my ($symb)=&get_symb($request); if (!$symb) {return '';} my $datatoken; if (!$env{'form.datatoken'}) { $datatoken=&Apache::loncommon::upfile_store($request); } else { $datatoken=$env{'form.datatoken'}; &Apache::loncommon::load_tmp_file($request); } my @records=&Apache::loncommon::upfile_record_sep(); if ($env{'form.noFirstLine'}) { shift(@records); } &csvuploadmap_header($request,$symb,$datatoken,$#records+1); my ($i,$keyfields); if (@records) { my @fields=&csvupload_fields($symb); if ($env{'form.upfile_associate'} eq 'reverse') { &Apache::loncommon::csv_print_samples($request,\@records); $i=&Apache::loncommon::csv_print_select_table($request,\@records, \@fields); foreach (@fields) { $keyfields.=$_->[0].','; } chop($keyfields); } else { unshift(@fields,['none','']); $i=&Apache::loncommon::csv_samples_select_table($request,\@records, \@fields); foreach my $rec (@records) { my %temp = &Apache::loncommon::record_sep($rec); if (%temp) { $keyfields=join(',',sort(keys(%temp))); last; } } } } &csvuploadmap_footer($request,$i,$keyfields); $request->print(&show_grading_menu_form($symb)); return ''; } sub csvuploadoptions { my ($request)= @_; my ($symb)=&get_symb($request); my $checked=(($env{'form.noFirstLine'})?'1':'0'); my $ignore=&mt('Ignore First Line'); $request->print(<

Uploading Class Grade Options

ENDPICK my %fields=&get_fields(); if (!defined($fields{'domain'})) { my $domform = &Apache::loncommon::select_dom_form($env{'request.role.domain'},'default_domain'); $request->print("\n

Users are in domain: ".$domform."

\n"); } foreach my $key (sort(keys(%env))) { if ($key !~ /^form\.(.*)$/) { next; } my $cleankey=$1; if ($cleankey eq 'command') { next; } $request->print(''."\n"); } # FIXME do a check for any duplicated user ids... # FIXME do a check for any invalid user ids?... $request->print('

'."\n"); $request->print(&show_grading_menu_form($symb)); return ''; } sub get_fields { my %fields; my @keyfields = split(/\,/,$env{'form.keyfields'}); for (my $i=0; $i<=$env{'form.nfields'}; $i++) { if ($env{'form.upfile_associate'} eq 'reverse') { if ($env{'form.f'.$i} ne 'none') { $fields{$keyfields[$i]}=$env{'form.f'.$i}; } } else { if ($env{'form.f'.$i} ne 'none') { $fields{$env{'form.f'.$i}}=$keyfields[$i]; } } } return %fields; } sub csvuploadassign { my ($request)= @_; my ($symb)=&get_symb($request); if (!$symb) {return '';} my $error_msg = ''; &Apache::loncommon::load_tmp_file($request); my @gradedata = &Apache::loncommon::upfile_record_sep(); if ($env{'form.noFirstLine'}) { shift(@gradedata); } my %fields=&get_fields(); $request->print('

Assigning Grades

'); my $courseid=$env{'request.course.id'}; my ($classlist) = &getclasslist('all',0); my @notallowed; my @skipped; my $countdone=0; foreach my $grade (@gradedata) { my %entries=&Apache::loncommon::record_sep($grade); my $domain; if ($entries{$fields{'domain'}}) { $domain=$entries{$fields{'domain'}}; } else { $domain=$env{'form.default_domain'}; } $domain=~s/\s//g; my $username=$entries{$fields{'username'}}; $username=~s/\s//g; if (!$username) { my $id=$entries{$fields{'ID'}}; $id=~s/\s//g; my %ids=&Apache::lonnet::idget($domain,$id); $username=$ids{$id}; } if (!exists($$classlist{"$username:$domain"})) { my $id=$entries{$fields{'ID'}}; $id=~s/\s//g; if ($id) { push(@skipped,"$id:$domain"); } else { push(@skipped,"$username:$domain"); } next; } my $usec=$classlist->{"$username:$domain"}[5]; if (!&canmodify($usec)) { push(@notallowed,"$username:$domain"); next; } my %points; my %grades; foreach my $dest (keys(%fields)) { if ($dest eq 'ID' || $dest eq 'username' || $dest eq 'domain') { next; } if ($entries{$fields{$dest}} =~ /^\s*$/) { next; } if ($dest=~/stores_(.*)_points/) { my $part=$1; my $wgt =&Apache::lonnet::EXT('resource.'.$part.'.weight', $symb,$domain,$username); if ($wgt) { $entries{$fields{$dest}}=~s/\s//g; my $pcr=$entries{$fields{$dest}} / $wgt; my $award='correct_by_override'; $grades{"resource.$part.awarded"}=$pcr; $grades{"resource.$part.solved"}=$award; $points{$part}=1; } else { $error_msg = "
" . &mt("Some point values were assigned" ." for problems with a weight " ."of zero. These values were " ."ignored."); } } else { if ($dest=~/stores_(.*)_awarded/) { if ($points{$1}) {next;} } if ($dest=~/stores_(.*)_solved/) { if ($points{$1}) {next;} } my $store_key=$dest; $store_key=~s/^stores/resource/; $store_key=~s/_/\./g; $grades{$store_key}=$entries{$fields{$dest}}; } } if (! %grades) { push(@skipped,"$username:$domain no data to save"); } $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}"; # &Apache::lonnet::logthis(" storing ".(join('-',%grades))); my $result=&Apache::lonnet::cstore(\%grades,$symb, $env{'request.course.id'}, $domain,$username); if ($result eq 'ok') { $request->print('.'); } else { $request->print("

Failed to save student $username:$domain. Message when trying to save was ($result)

" ); } $request->rflush(); $countdone++; } $request->print("
Saved $countdone students\n"); if (@skipped) { $request->print('

Skipped Students

'); foreach my $student (@skipped) { $request->print("$student
\n"); } } if (@notallowed) { $request->print('

Students Not Allowed to Modify

'); foreach my $student (@notallowed) { $request->print("$student
\n"); } } $request->print("
\n"); $request->print(&show_grading_menu_form($symb)); return $error_msg; } #------------- end of section for handling csv file upload --------- # #------------------------------------------------------------------- # #-------------- Next few routines handle grading by page/sequence # #--- Select a page/sequence and a student to grade sub pickStudentPage { my ($request) = shift; $request->print(< function checkPickOne(formname) { if (radioSelection(formname.student) == null) { alert("Please select the student you wish to grade."); return; } ptr = pullDownSelection(formname.selectpage); formname.page.value = formname["page"+ptr].value; formname.title.value = formname["title"+ptr].value; formname.submit(); } LISTJAVASCRIPT &commonJSfunctions($request); my ($symb) = &get_symb($request); my $cdom = $env{"course.$env{'request.course.id'}.domain"}; my $cnum = $env{"course.$env{'request.course.id'}.num"}; my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; my $result='

 '. 'Manual Grading by Page or Sequence

'; $result.='
'."\n"; $result.=' Problems from: '."
\n"; $ctr=0; foreach (@$titles) { my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/); $result.=''."\n"; $result.=''."\n"; $ctr++; } $result.=''."\n". ''."\n"; $result.=' View Problems Text: '."\n". ''."
\n"; $result.=' Submission Details: '. ''."\n". ''."\n". ''."\n"; $result.=&build_section_inputs(); my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); $result.=''."\n". ''."\n". ''."\n". ''."
\n"; $result.=' '.&mt('Use CODE:').' '. '
'."\n"; $result.=' 
'."\n"; $request->print($result); my $studentTable.=' Select a student you wish to grade and then click on the Next button.
'. '
'. ''. ''. ''. ''. ''; my (undef,undef,$fullname) = &getclasslist($getsec,'1'); my $ptr = 1; foreach my $student (sort { if (lc($$fullname{$a}) ne lc($$fullname{$b})) { return (lc($$fullname{$a}) cmp lc($$fullname{$b})); } return $a cmp $b; } (keys(%$fullname))) { my ($uname,$udom) = split(/:/,$student); $studentTable.=($ptr%2 == 1 ? '' : ''); $studentTable.=''; $studentTable.='' : ''); $ptr++; } $studentTable.='' if ($ptr%2 == 0); $studentTable.='
 No.'.&nameUserString('header').' No.'.&nameUserString('header').'
'.$ptr.'  \n"; $studentTable.=($ptr%2 == 0 ? '
  
'."\n"; $studentTable.='
'."\n"; $studentTable.=&show_grading_menu_form($symb); $request->print($studentTable); return ''; } sub getSymbMap { my $navmap = Apache::lonnavmaps::navmap->new(); my %symbx = (); my @titles = (); my $minder = 0; # Gather every sequence that has problems. my @sequences = $navmap->retrieveResources(undef, sub { shift->is_map(); }, 1,0,1); for my $sequence ($navmap->getById('0.0'), @sequences) { if ($navmap->hasResource($sequence, sub { shift->is_problem(); }, 0) ) { my $title = $minder.'.'. &HTML::Entities::encode($sequence->compTitle(),'"\'&'); push(@titles, $title); # minder in case two titles are identical $symbx{$title} = &HTML::Entities::encode($sequence->symb(),'"\'&'); $minder++; } } return \@titles,\%symbx; } # #--- Displays a page/sequence w/wo problems, w/wo submissions sub displayPage { my ($request) = shift; my ($symb) = &get_symb($request); my $cdom = $env{"course.$env{'request.course.id'}.domain"}; my $cnum = $env{"course.$env{'request.course.id'}.num"}; my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; my $pageTitle = $env{'form.page'}; my ($classlist,undef,$fullname) = &getclasslist($getsec,'1'); my ($uname,$udom) = split(/:/,$env{'form.student'}); my $usec=$classlist->{$env{'form.student'}}[5]; #need to make sure we have the correct data for later EXT calls, #thus invalidate the cache &Apache::lonnet::devalidatecourseresdata( $env{'course.'.$env{'request.course.id'}.'.num'}, $env{'course.'.$env{'request.course.id'}.'.domain'}); &Apache::lonnet::clear_EXT_cache_status(); if (!&canview($usec)) { $request->print('Unable to view requested student.('.$env{'form.student'}.')'); $request->print(&show_grading_menu_form($symb)); return; } my $result='

 '.$env{'form.title'}.'

'; $result.='

 Student: '.&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom). '

'."\n"; if (&Apache::lonnet::validCODE($env{'form.CODE'})) { $result.='

 CODE: '.$env{'form.CODE'}.'

'."\n"; } else { delete($env{'form.CODE'}); } &sub_page_js($request); $request->print($result); my $navmap = Apache::lonnavmaps::navmap->new(); my ($mapUrl, $id, $resUrl)=&Apache::lonnet::decode_symb($env{'form.page'}); my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps if (!$map) { $request->print('Unable to view requested sequence. ('.$resUrl.')'); $request->print(&show_grading_menu_form($symb)); return; } my $iterator = $navmap->getIterator($map->map_start(), $map->map_finish()); my $studentTable='
'."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n"; if (defined($env{'form.CODE'})) { $studentTable.= ''."\n"; } my $checkIcon = ''.&mt('Check Mark').
	''; $studentTable.=' Note: Problems graded correct by the computer are marked with a '.$checkIcon. ' symbol.'."\n". ' SCANTRONFORM $r->print($result); if (&Apache::lonnet::allowed('usc',$env{'request.role.domain'}) || &Apache::lonnet::allowed('usc',$env{'request.course.id'})) { # Chunk of form to prompt for a scantron file upload. $r->print(< SCANTRONFORM } # Chunk of the form that prompts to view a scoring office file, # corrected file, skipped records in a file. $r->print(<
SCANTRONFORM $r->print(< $grading_menu_button SCANTRONFORM return } =pod =item get_scantron_config Parse and return the scantron configuration line selected as a hash of configuration file fields. Arguments: which - the name of the configuration to parse from the file. Returns: If the named configuration is not in the file, an empty hash is returned. a hash with the fields name - internal name for the this configuration setup description - text to display to operator that describes this config CODElocation - if 0 or the string 'none' - no CODE exists for this config if -1 || the string 'letter' - a CODE exists for this config and is a string of letters Unsupported value (but planned for future support) if a positive integer - The CODE exists as the first n items from the question section of the form if the string 'number' - The CODE exists for this config and is a string of numbers CODEstart - (only matter if a CODE exists) column in the line where the CODE starts CODElength - length of the CODE IDstart - column where the student ID number starts IDlength - length of the student ID info Qstart - column where the information from the bubbled 'questions' start Qlength - number of columns comprising a single bubble line from the sheet. (usually either 1 or 10) Qon - either a single character representing the character used to signal a bubble was chosen in the positional setup, or the string 'letter' if the letter of the chosen bubble is in the final, or 'number' if a number representing the chosen bubble is in the file (1->A 0->J) Qoff - the character used to represent that a bubble was left blank PaperID - if the scanning process generates a unique number for each sheet scanned the column that this ID number starts in PaperIDlength - number of columns that comprise the unique ID number for the sheet of paper FirstName - column that the first name starts in FirstNameLength - number of columns that the first name spans LastName - column that the last name starts in LastNameLength - number of columns that the last name spans =cut 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; } chomp($line); my @config=split(/:/,$line); $config{'name'}=$config[0]; $config{'description'}=$config[1]; $config{'CODElocation'}=$config[2]; $config{'CODEstart'}=$config[3]; $config{'CODElength'}=$config[4]; $config{'IDstart'}=$config[5]; $config{'IDlength'}=$config[6]; $config{'Qstart'}=$config[7]; $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; } =pod =item username_to_idmap creates a hash keyed by student id with values of the corresponding student username:domain. Arguments: $classlist - reference to the class list hash. This is a hash keyed by student name:domain whose elements are references to arrays containing various chunks of information about the student. (See loncoursedata for more info). Returns %idmap - the constructed hash =cut sub username_to_idmap { my ($classlist)= @_; my %idmap; foreach my $student (keys(%$classlist)) { $idmap{$classlist->{$student}->[&Apache::loncoursedata::CL_ID]}= $student; } return %idmap; } =pod =item scantron_fixup_scanline Process a requested correction to a scanline. Arguments: $scantron_config - hash from &get_scantron_config() $scan_data - hash of correction information (see &scantron_getfile()) $line - existing scanline $whichline - line number of the passed in scanline $field - type of change to process (either 'ID' -> correct the student ID number 'CODE' -> correct the CODE 'answer' -> fixup the submitted answers) $args - hash of additional info, - 'ID' 'newid' -> studentID to use in replacement of existing one - 'CODE' 'CODE_ignore_dup' - set to true if duplicates should be ignored. 'CODE' - is new code or 'use_unfound' if the existing unfound code should be used as is - 'answer' 'response' - new answer or 'none' if blank 'question' - the bubble line to change Returns: $line - the modified scanline Side effects: $scan_data - may be updated =cut 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 too 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 'CODE') { if ($args->{'CODE_ignore_dup'}) { &scan_data($scan_data,"$whichline.CODE_ignore_dup",'1'); } &scan_data($scan_data,"$whichline.useCODE",'1'); if ($args->{'CODE'} ne 'use_unfound') { if (length($args->{'CODE'}) > $$scantron_config{'CODElength'}) { return ($line,1,'New CODE value too large'); } if (length($args->{'CODE'}) < $$scantron_config{'CODElength'}) { $args->{'CODE'}=sprintf('%-'.$$scantron_config{'CODElength'}.'s',$args->{'CODE'}); } substr($line,$$scantron_config{'CODEstart'}-1, $$scantron_config{'CODElength'})=$args->{'CODE'}; } } 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 { if ($on eq 'letter') { my @alphabet=('A'..'Z'); $answer=$alphabet[$args->{'response'}]; } elsif ($on eq 'number') { $answer=$args->{'response'}+1; if ($answer == 10) { $answer = '0'; } } 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; } =pod =item scan_data Edit or look up an item in the scan_data hash. Arguments: $scan_data - The hash (see scantron_getfile) $key - shorthand of the key to edit (actual key is scantronfilename_key). $data - New value of the hash entry. $delete - If true, the entry is removed from the hash. Returns: The new value of the hash table field (undefined if deleted). =cut 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}; } =pod =item scantron_parse_scanline Decodes a scanline from the selected scantron file Arguments: line - The text of the scantron file line to process whichline - Line number scantron_config - Hash describing the format of the scantron lines. scan_data - Hash of extra information about the scanline (see scantron_getfile for more information) just_header - True if should not process question answers but only the stuff to the left of the answers. Returns: Hash containing the result of parsing the scanline Keys are all proceeded by the string 'scantron.' CODE - the CODE in use for this scanline useCODE - 1 if the CODE is invalid but it usage has been forced by the operator CODE_ignore_dup - 1 if the CODE is a duplicated use when unique CODEs were selected, but the usage has been forced by the operator ID - student ID PaperID - if used, the ID number printed on the sheet when the paper was scanned FirstName - first name from the sheet LastName - last name from the sheet if just_header was not true these key may also exist missingerror - a list of bubbled line numbers that had a blank bubble that is considered an error (if the operator had already okayed a blank bubble line as really being blank then that bubble line number won't appear here. doubleerror - a list of bubbled line numbers that had more than one bubble filled in and has not been corrected by the operator maxquest - the number of the last bubble line that was parsed ( starts at 1) .answer - zero or more letters representing the selected letters from the scanline for the bubble line . if blank there was either no bubble or there where multiple bubbles, (consult the keys missingerror and doubleerror if this is an error condition) =cut sub scantron_parse_scanline { my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_; my %record; my $questions=substr($line,$$scantron_config{'Qstart'}-1); # Answers my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # earlier stuff if (!($$scantron_config{'CODElocation'} eq 0 || $$scantron_config{'CODElocation'} eq 'none')) { if ($$scantron_config{'CODElocation'} < 0 || $$scantron_config{'CODElocation'} eq 'letter' || $$scantron_config{'CODElocation'} eq 'number') { $record{'scantron.CODE'}=substr($data, $$scantron_config{'CODEstart'}-1, $$scantron_config{'CODElength'}); if (&scan_data($scan_data,"$whichline.useCODE")) { $record{'scantron.useCODE'}=1; } if (&scan_data($scan_data,"$whichline.CODE_ignore_dup")) { $record{'scantron.CODE_ignore_dup'}=1; } } else { #FIXME interpret first N questions } } $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'}); if ($just_header) { return \%record; } my @alphabet=('A'..'Z'); my $questnum=0; while ($questions) { $questnum++; my $currentquest=substr($questions,0,$$scantron_config{'Qlength'}); substr($questions,0,$$scantron_config{'Qlength'})=''; if (length($currentquest) < $$scantron_config{'Qlength'}) { next; } if ($$scantron_config{'Qon'} eq 'letter') { if ($currentquest eq '?' || $currentquest eq '*') { push(@{$record{'scantron.doubleerror'}},$questnum); $record{"scantron.$questnum.answer"}=''; } elsif (!defined($currentquest) || $currentquest eq $$scantron_config{'Qoff'} || $currentquest !~ /^[A-Z]$/) { $record{"scantron.$questnum.answer"}=''; if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { push(@{$record{"scantron.missingerror"}},$questnum); } } else { $record{"scantron.$questnum.answer"}=$currentquest; } } elsif ($$scantron_config{'Qon'} eq 'number') { if ($currentquest eq '?' || $currentquest eq '*') { push(@{$record{'scantron.doubleerror'}},$questnum); $record{"scantron.$questnum.answer"}=''; } elsif (!defined($currentquest) || $currentquest eq $$scantron_config{'Qoff'} || $currentquest !~ /^\d$/) { $record{"scantron.$questnum.answer"}=''; if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { push(@{$record{"scantron.missingerror"}},$questnum); } } else { # wrap zero back to J if ($currentquest eq '0') { $record{"scantron.$questnum.answer"}= $alphabet[9]; } else { $record{"scantron.$questnum.answer"}= $alphabet[$currentquest-1]; } } } else { 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; } =pod =item scantron_add_delay Adds an error message that occurred during the grading phase to a queue of messages to be shown after grading pass is complete Arguments: $delayqueue - arrary ref of hash ref of error messages $scanline - the scanline that caused the error $errormesage - the error message $errorcode - a numeric code for the error Side Effects: updates the $delayqueue to have a new hash ref of the error =cut sub scantron_add_delay { my ($delayqueue,$scanline,$errormessage,$errorcode)=@_; push(@$delayqueue, {'line' => $scanline, 'emsg' => $errormessage, 'ecode' => $errorcode } ); } =pod =item scantron_find_student Finds the username for the current scanline Arguments: $scantron_record - hash result from scantron_parse_scanline $scan_data - hash of correction information (see &scantron_getfile() form more information) $idmap - hash from &username_to_idmap() $line - number of current scanline Returns: Either 'username:domain' or undef if unknown =cut sub scantron_find_student { 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)) { if (lc($id) eq lc($scanID)) { return $$idmap{$id}; } } return undef; } =pod =item scantron_filter Filter sub for lonnavmaps, filters out hidden resources if ignore hidden resources was selected =cut sub scantron_filter { my ($curres)=@_; if (ref($curres) && $curres->is_problem()) { # if the user has asked to not have either hidden # or 'randomout' controlled resources to be graded # don't include them if ($env{'form.scantron_options_hidden'} eq 'ignore_hidden' && $curres->randomout) { return 0; } return 1; } return 0; } =pod =item scantron_process_corrections Gets correction information out of submitted form data and corrects the scanline =cut 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,$scan_data,$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'} =~ /^(duplicate|incorrect)CODE$/) { my $resolution=$env{'form.scantron_CODE_resolution'}; my $newCODE; my %args; if ($resolution eq 'use_unfound') { $newCODE='use_unfound'; } elsif ($resolution eq 'use_found') { $newCODE=$env{'form.scantron_CODE_selectedvalue'}; } elsif ($resolution eq 'use_typed') { $newCODE=$env{'form.scantron_CODE_newvalue'}; } elsif ($resolution =~ /^use_closest_(\d+)/) { $newCODE=$env{"form.scantron_CODE_closest_$1"}; } if ($env{'form.scantron_corrections'} eq 'duplicateCODE') { $args{'CODE_ignore_dup'}=1; } $args{'CODE'}=$newCODE; ($line,$err,$errmsg)= &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,$which, 'CODE',\%args); } 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,$scan_data,$which,$line,$skip); &scantron_putfile($scanlines,$scan_data); } } =pod =item reset_skipping_status Forgets the current set of remember skipped scanlines (and thus reverts back to considering all lines in the scantron_skipped_ file) =cut sub reset_skipping_status { my ($scanlines,$scan_data)=&scantron_getfile(); &scan_data($scan_data,'remember_skipping',undef,1); &scantron_putfile(undef,$scan_data); } =pod =item start_skipping Marks a scanline to be skipped. =cut sub start_skipping { my ($scan_data,$i)=@_; my %remembered=split(':',&scan_data($scan_data,'remember_skipping')); if ($env{'form.scantron_options_redo'} =~ /^redo_/) { $remembered{$i}=2; } else { $remembered{$i}=1; } &scan_data($scan_data,'remember_skipping',join(':',%remembered)); } =pod =item should_be_skipped Checks whether a scanline should be skipped. =cut sub should_be_skipped { my ($scanlines,$scan_data,$i)=@_; if ($env{'form.scantron_options_redo'} !~ /^redo_/) { # not redoing old skips if ($scanlines->{'skipped'}[$i]) { return 1; } return 0; } my %remembered=split(':',&scan_data($scan_data,'remember_skipping')); if (exists($remembered{$i}) && $remembered{$i} != 2 ) { return 0; } return 1; } =pod =item remember_current_skipped Discovers what scanlines are in the scantron_skipped_ file and remembers them into scan_data for later use. =cut sub remember_current_skipped { my ($scanlines,$scan_data)=&scantron_getfile(); my %to_remember; for (my $i=0;$i<=$scanlines->{'count'};$i++) { if ($scanlines->{'skipped'}[$i]) { $to_remember{$i}=1; } } &scan_data($scan_data,'remember_skipping',join(':',%to_remember)); &scantron_putfile(undef,$scan_data); } =pod =item check_for_error Checks if there was an error when attempting to remove a specific scantron_.. bubble sheet data file. Prints out an error if something went wrong. =cut sub check_for_error { my ($r,$result)=@_; if ($result ne 'ok' && $result ne 'not_found' ) { $r->print("An error occurred ($result) when trying to Remove the existing corrections."); } } =pod =item scantron_warning_screen Interstitial screen to make sure the operator has selected the correct options before we start the validation phase. =cut sub scantron_warning_screen { my ($button_text)=@_; my $title=&Apache::lonnet::gettitle($env{'form.selectpage'}); my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my $CODElist; if ($scantron_config{'CODElocation'} && $scantron_config{'CODEstart'} && $scantron_config{'CODElength'}) { $CODElist=$env{'form.scantron_CODElist'}; if ($env{'form.scantron_CODElist'} eq '') { $CODElist='None'; } $CODElist= ''; } return (< Please double check the information below before clicking on '$button_text'

'. ''. ''. ''; &Apache::lonxml::clear_problem_counter(); my ($depth,$question,$prob) = (1,1,1); $iterator->next(); # skip the first BEGIN_MAP my $curRes = $iterator->next(); # for "current resource" while ($depth > 0) { if($curRes == $iterator->BEGIN_MAP) { $depth++; } if($curRes == $iterator->END_MAP) { $depth--; } if (ref($curRes) && $curRes->is_problem()) { my $parts = $curRes->parts(); my $title = $curRes->compTitle(); my $symbx = $curRes->symb(); $studentTable.=''; $studentTable.='
 Prob.  '.($env{'form.vProb'} eq 'no' ? 'Title' : 'Problem Text').'/Grade
'.$prob. (scalar(@{$parts}) == 1 ? '' : '
('.scalar(@{$parts}).' parts)').'
'; my %form = ('CODE' => $env{'form.CODE'},); if ($env{'form.vProb'} eq 'yes' ) { $studentTable.=&show_problem($request,$symbx,$uname,$udom,1, undef,'both',\%form); } else { my $companswer = &Apache::loncommon::get_student_answers($symbx,$uname,$udom,$env{'request.course.id'},%form); $companswer =~ s|||g; $companswer =~ s|||g; # while ($companswer =~ /()/s) { #\n"); # } # $companswer =~ s||
|g; $studentTable.=' '.$title.' 
 Correct answer:
'.$companswer; } my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname); if ($env{'form.lastSub'} eq 'datesub') { if ($record{'version'} eq '') { $studentTable.='
 No recorded submission for this problem
'; } else { my %responseType = (); foreach my $partid (@{$parts}) { my @responseIds =$curRes->responseIds($partid); my @responseType =$curRes->responseType($partid); my %responseIds; for (my $i=0;$i<=$#responseIds;$i++) { $responseIds{$responseIds[$i]}=$responseType[$i]; } $responseType{$partid} = \%responseIds; } $studentTable.= &displaySubByDates($symbx,\%record,$parts,\%responseType,$checkIcon,$uname,$udom); } } elsif ($env{'form.lastSub'} eq 'all') { my $last = ($env{'form.lastSub'} eq 'last' ? 'last' : ''); $studentTable.=&Apache::loncommon::get_previous_attempt($symbx,$uname,$udom, $env{'request.course.id'}, '','.submission'); } if (&canmodify($usec)) { foreach my $partid (@{$parts}) { $studentTable.=&gradeBox($request,$symbx,$uname,$udom,$question,$partid,\%record); $studentTable.=''."\n"; $question++; } $prob++; } $studentTable.=''; } $curRes = $iterator->next(); } $studentTable.='
'."\n". ''. ''."\n"; $studentTable.=&show_grading_menu_form($symb); $request->print($studentTable); return ''; } sub displaySubByDates { my ($symb,$record,$parts,$responseType,$checkIcon,$uname,$udom) = @_; my $isCODE=0; my $isTask = ($symb =~/\.task$/); if (exists($record->{'resource.CODE'})) { $isCODE=1; } my $studentTable='
'. ''. ''. ($isCODE?'':''). ''. ''; my ($version); my %mark; my %orders; $mark{'correct_by_student'} = $checkIcon; if (!exists($$record{'1:timestamp'})) { return '
 Nothing submitted - no attempts
'; } my $interaction; for ($version=1;$version<=$$record{'version'};$version++) { my $timestamp = scalar(localtime($$record{$version.':timestamp'})); if (exists($$record{$version.':resource.0.version'})) { $interaction = $$record{$version.':resource.0.version'}; } my $where = ($isTask ? "$version:resource.$interaction" : "$version:resource"); #&Apache::lonnet::logthis(" got $where"); $studentTable.=''; if ($isCODE) { $studentTable.=''; } my @versionKeys = split(/\:/,$$record{$version.':keys'}); my @displaySub = (); foreach my $partid (@{$parts}) { my @matchKey = ($isTask ? sort(grep /^resource\.\d+\.\Q$partid\E\.award$/,@versionKeys) : sort(grep /^resource\.\Q$partid\E\..*?\.submission$/,@versionKeys)); # next if ($$record{"$version:resource.$partid.solved"} eq ''); my $display_part=&get_display_part($partid,$symb); foreach my $matchKey (@matchKey) { if (exists($$record{$version.':'.$matchKey}) && $$record{$version.':'.$matchKey} ne '') { my ($responseId)= ($isTask ? ($matchKey=~ /^resource\.(.*?)\.\Q$partid\E\.award$/) : ($matchKey=~ /^resource\.\Q$partid\E\.(.*?)\.submission$/)); #&Apache::lonnet::logthis("match $matchKey $responseId (".$$record{$version.':'.$matchKey}); $displaySub[0].='Part: '.$display_part.' '; $displaySub[0].='(ID '. $responseId.') '; if ($$record{"$where.$partid.tries"} eq '') { $displaySub[0].='Trial not counted'; } else { $displaySub[0].='Trial '. $$record{"$where.$partid.tries"}; } my $responseType=($isTask ? 'Task' : $responseType->{$partid}->{$responseId}); if (!exists($orders{$partid})) { $orders{$partid}={}; } if (!exists($orders{$partid}->{$responseId})) { $orders{$partid}->{$responseId}= &get_order($partid,$responseId,$symb,$uname,$udom); } $displaySub[0].='  '. &cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom).'
'; } } if (exists($$record{"$where.$partid.checkedin"})) { $displaySub[1].='Checked in by '. $$record{"$where.$partid.checkedin"}.' into slot '. $$record{"$where.$partid.checkedin.slot"}. '
'; } if (exists $$record{"$where.$partid.award"}) { $displaySub[1].='Part: '.$display_part.'  '. lc($$record{"$where.$partid.award"}).' '. $mark{$$record{"$where.$partid.solved"}}. '
'; } if (exists $$record{"$where.$partid.regrader"}) { $displaySub[2].=$$record{"$where.$partid.regrader"}. ' ('.&mt('Part').': '.$display_part.')'; } elsif ($$record{"$version:resource.$partid.regrader"} =~ /\S/) { $displaySub[2].= $$record{"$version:resource.$partid.regrader"}. ' ('.&mt('Part').': '.$display_part.')'; } } # needed because old essay regrader has not parts info if (exists $$record{"$version:resource.regrader"}) { $displaySub[2].=$$record{"$version:resource.regrader"}; } $studentTable.=''; } $studentTable.='
Date/TimeCODESubmissionStatus 
'.$timestamp.''.$record->{$version.':resource.CODE'}.''.$displaySub[0].' '.$displaySub[1]; if ($displaySub[2]) { $studentTable.='Manually graded by '.$displaySub[2]; } $studentTable.=' 
'; return $studentTable; } sub updateGradeByPage { my ($request) = shift; my $cdom = $env{"course.$env{'request.course.id'}.domain"}; my $cnum = $env{"course.$env{'request.course.id'}.num"}; my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; my $pageTitle = $env{'form.page'}; my ($classlist,undef,$fullname) = &getclasslist($getsec,'1'); my ($uname,$udom) = split(/:/,$env{'form.student'}); my $usec=$classlist->{$env{'form.student'}}[5]; if (!&canmodify($usec)) { $request->print('Unable to modify requested student.('.$env{'form.student'}.''); $request->print(&show_grading_menu_form($env{'form.symb'})); return; } my $result='

 '.$env{'form.title'}.'

'; $result.='

 Student: '.&nameUserString(undef,$env{'form.fullname'},$uname,$udom). '

'."\n"; $request->print($result); my $navmap = Apache::lonnavmaps::navmap->new(); my ($mapUrl, $id, $resUrl) = &Apache::lonnet::decode_symb( $env{'form.page'}); my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps if (!$map) { $request->print('Unable to grade requested sequence. ('.$resUrl.')'); my ($symb)=&get_symb($request); $request->print(&show_grading_menu_form($symb)); return; } my $iterator = $navmap->getIterator($map->map_start(), $map->map_finish()); my $studentTable='
'. ''. ''. ''. ''. ''; $iterator->next(); # skip the first BEGIN_MAP my $curRes = $iterator->next(); # for "current resource" my ($depth,$question,$prob,$changeflag)= (1,1,1,0); while ($depth > 0) { if($curRes == $iterator->BEGIN_MAP) { $depth++; } if($curRes == $iterator->END_MAP) { $depth--; } if (ref($curRes) && $curRes->is_problem()) { my $parts = $curRes->parts(); my $title = $curRes->compTitle(); my $symbx = $curRes->symb(); $studentTable.=''; $studentTable.=''; my %newrecord=(); my @displayPts=(); my %aggregate = (); my $aggregateflag = 0; foreach my $partid (@{$parts}) { my $newpts = $env{'form.GD_BOX'.$question.'_'.$partid}; my $oldpts = $env{'form.oldpts'.$question.'_'.$partid}; my $wgt = $env{'form.WGT'.$question.'_'.$partid} != 0 ? $env{'form.WGT'.$question.'_'.$partid} : 1; my $partial = $newpts/$wgt; my $score; if ($partial > 0) { $score = 'correct_by_override'; } elsif ($newpts ne '') { #empty is taken as 0 $score = 'incorrect_by_override'; } my $dropMenu = $env{'form.GD_SEL'.$question.'_'.$partid}; if ($dropMenu eq 'excused') { $partial = ''; $score = 'excused'; } elsif ($dropMenu eq 'reset status' && $env{'form.solved'.$question.'_'.$partid} ne '') { #update only if previous record exists $newrecord{'resource.'.$partid.'.tries'} = 0; $newrecord{'resource.'.$partid.'.solved'} = ''; $newrecord{'resource.'.$partid.'.award'} = ''; $newrecord{'resource.'.$partid.'.awarded'} = 0; $newrecord{'resource.'.$partid.'.regrader'} = "$env{'user.name'}:$env{'user.domain'}"; $changeflag++; $newpts = ''; my $aggtries = $env{'form.aggtries'.$question.'_'.$partid}; my $totaltries = $env{'form.totaltries'.$question.'_'.$partid}; my $solvedstatus = $env{'form.solved'.$question.'_'.$partid}; if ($aggtries > 0) { &decrement_aggs($symbx,$partid,\%aggregate,$aggtries,$totaltries,$solvedstatus); $aggregateflag = 1; } } my $display_part=&get_display_part($partid,$curRes->symb()); my $oldstatus = $env{'form.solved'.$question.'_'.$partid}; $displayPts[0].=' Part: '.$display_part.' = '. (($oldstatus eq 'excused') ? 'excused' : $oldpts). ' 
'; $displayPts[1].=' Part: '.$display_part.' = '. (($score eq 'excused') ? 'excused' : $newpts). ' 
'; $question++; next if ($dropMenu eq 'reset status' || ($newpts eq $oldpts && $score ne 'excused')); $newrecord{'resource.'.$partid.'.awarded'} = $partial if $partial ne ''; $newrecord{'resource.'.$partid.'.solved'} = $score if $score ne ''; $newrecord{'resource.'.$partid.'.regrader'} = "$env{'user.name'}:$env{'user.domain'}" if (scalar(keys(%newrecord)) > 0); $changeflag++; } if (scalar(keys(%newrecord)) > 0) { my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'}, $udom,$uname); if (&Apache::lonnet::validCODE($env{'form.CODE'})) { $newrecord{'resource.CODE'} = $env{'form.CODE'}; } elsif (&Apache::lonnet::validCODE($record{'resource.CODE'})) { $newrecord{'resource.CODE'} = ''; } &Apache::lonnet::cstore(\%newrecord,$symbx,$env{'request.course.id'}, $udom,$uname); %record = &Apache::lonnet::restore($symbx, $env{'request.course.id'}, $udom,$uname); &check_and_remove_from_queue($parts,\%record,undef,$symbx, $cdom,$cnum,$udom,$uname); } if ($aggregateflag) { &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, $env{'course.'.$env{'request.course.id'}.'.domain'}, $env{'course.'.$env{'request.course.id'}.'.num'}); } $studentTable.=''. ''. ''; $prob++; } $curRes = $iterator->next(); } $studentTable.='
 Prob.  Title  Previous Score  New Score 
'.$prob. (scalar(@{$parts}) == 1 ? '' : '
('.scalar(@{$parts}).' parts)').'
 '.$title.' '.$displayPts[0].''.$displayPts[1].'
'; $studentTable.=&show_grading_menu_form($env{'form.symb'}); my $grademsg=($changeflag == 0 ? 'No score was changed or updated.' : 'The scores were changed for '. $changeflag.' problem'.($changeflag == 1 ? '.' : 's.')); $request->print($grademsg.$studentTable); return ''; } #-------- end of section for handling grading by page/sequence --------- # #------------------------------------------------------------------- #--------------------Scantron Grading----------------------------------- # #------ start of section for handling grading by page/sequence --------- =pod =head1 Bubble sheet grading routines For this documentation: 'scanline' refers to the full line of characters from the file that we are parsing that represents one entire sheet 'bubble line' refers to the data representing the line of bubbles that are on the physical bubble sheet The overall process is that a scanned in bubble sheet data is uploaded into a course. When a user wants to grade, they select a sequence/folder of resources, a file of bubble sheet info, and pick one of the predefined configurations for what each scanline looks like. Next each scanline is checked for any errors of either 'missing bubbles' (it's an error because it may have been mis-scanned because too light bubbling), 'double bubble' (each bubble line should have no more that one letter picked), invalid or duplicated CODE, invalid student ID If the CODE option is used that determines the randomization of the homework problems, either way the student ID is looked up into a username:domain. During the validation phase the instructor can choose to skip scanlines. After the validation phase, there are now 3 bubble sheet files scantron_original_filename (unmodified original file) scantron_corrected_filename (file where the corrected information has replaced the original information) scantron_skipped_filename (contains the exact text of scanlines that where skipped) Also there is a separate hash nohist_scantrondata that contains extra correction information that isn't representable in the bubble sheet file (see &scantron_getfile() for more information) After all scanlines are either valid, marked as valid or skipped, then foreach line foreach problem in the picked sequence, an ssi request is made that simulates a user submitting their selected letter(s) against the homework problem. =over 4 =cut =pod =item defaultFormData Returns html hidden inputs used to hold context/default values. Arguments: $symb - $symb of the current resource =cut sub defaultFormData { my ($symb)=@_; return ' '."\n". ''."\n". ''."\n"; } =pod =item getSequenceDropDown Return html dropdown of possible sequences to grade Arguments: $symb - $symb of the current resource =cut sub getSequenceDropDown { my ($symb)=@_; my $result=''; return $result; } =pod =item scantron_filenames Returns a list of the scantron files in the current course =cut sub scantron_filenames { 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, &propath($cdom,$cname)); my @possiblenames; foreach my $filename (sort(@files)) { ($filename)=split(/&/,$filename); if ($filename!~/^scantron_orig_/) { next ; } $filename=~s/^scantron_orig_//; push(@possiblenames,$filename); } return @possiblenames; } =pod =item scantron_uploads Returns html drop-down list of scantron files in current course. Arguments: $file2grade - filename to set as selected in the dropdown =cut sub scantron_uploads { my ($file2grade) = @_; my $result= '"; return $result; } =pod =item scantron_scantab Returns html drop down of the scantron formats in the scantronformat.tab file. =cut sub scantron_scantab { my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab'); my $result=''."\n"; return $result; } =pod =item scantron_CODElist Returns html drop down of the saved CODE lists from current course, generated from earlier printings. =cut sub scantron_CODElist { my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; my @names=&Apache::lonnet::getkeys('CODEs',$cdom,$cnum); my $namechoice=''; foreach my $name (sort {uc($a) cmp uc($b)} @names) { if ($name =~ /^error: 2 /) { next; } if ($name =~ /^type\0/) { next; } $namechoice.=''; } $namechoice=''; return $namechoice; } =pod =item scantron_CODEunique Returns the html for "Each CODE to be used once" radio. =cut sub scantron_CODEunique { my $result=' '; return $result; } =pod =item scantron_selectphase Generates the initial screen to start the bubble sheet process. Allows for - starting a grading run. - downloading existing scan data (original, corrected or skipped info) - uploading new scan data Arguments: $r - The Apache request object $file2grade - name of the file that contain the scanned data to score =cut sub scantron_selectphase { my ($r,$file2grade) = @_; my ($symb)=&get_symb($r); if (!$symb) {return '';} my $sequence_selector=&getSequenceDropDown($symb); my $default_form_data=&defaultFormData($symb); my $grading_menu_button=&show_grading_menu_form($symb); my $file_selector=&scantron_uploads($file2grade); my $format_selector=&scantron_scantab(); my $CODE_selector=&scantron_CODElist(); my $CODE_unique=&scantron_CODEunique(); my $result; # Chunk of form to prompt for a file to grade and how: $result.= <
$default_form_data
 Specify file and which Folder/Sequence to grade
Sequence to grade: $sequence_selector
Filename of scoring office file: $file_selector
Format of data file: $format_selector
Saved CODEs to validate against: $CODE_selector
Each CODE is only to be used once: $CODE_unique
Options:

 Specify a Scantron data file to upload.
SCANTRONFORM my $default_form_data=&defaultFormData(&get_symb($r,1)); my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum= $env{'course.'.$env{'request.course.id'}.'.num'}; $r->print(< 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(); }
$default_form_data File to upload:
UPLOAD $r->print(<
$default_form_data
 Download a scoring office file
Filename of scoring office file: $file_selector
List of CODES to validate against:'. $env{'form.scantron_CODElist'}.'
$CODElist
Sequence to be Graded:$title
Data File that will be used:$env{'form.scantron_selectfile'}

If this information is correct, please click on '$button_text'.

If something is incorrect, please click the 'Grading Menu' button to start over.


STUFF } =pod =item scantron_do_warning Check if the operator has picked something for all required fields. Error out if something is missing. =cut sub scantron_do_warning { my ($r)=@_; my ($symb)=&get_symb($r); if (!$symb) {return '';} my $default_form_data=&defaultFormData($symb); $r->print(&scantron_form_start().$default_form_data); if ( $env{'form.selectpage'} eq '' || $env{'form.scantron_selectfile'} eq '' || $env{'form.scantron_format'} eq '' ) { $r->print("

You have forgetten to specify some information. Please go Back and try again.

"); if ( $env{'form.selectpage'} eq '') { $r->print('

You have not selected a Sequence to grade

'); } if ( $env{'form.scantron_selectfile'} eq '') { $r->print('

You have not selected a file that contains the student\'s response data.

'); } if ( $env{'form.scantron_format'} eq '') { $r->print('

You have not selected a the format of the student\'s response data.

'); } } else { my $warning=&scantron_warning_screen('Grading: Validate Records'); $r->print(< STUFF } $r->print("
".&show_grading_menu_form($symb)); return ''; } =pod =item scantron_form_start html hidden input for remembering all selected grading options =cut sub scantron_form_start { my ($max_bubble)=@_; my $result= < SCANTRONFORM return $result; } =pod =item scantron_validate_file Dispatch routine for doing validation of a bubble sheet data file. Also processes any necessary information resets that need to occur before validation begins (ignore previous corrections, restarting the skipped records processing) =cut sub scantron_validate_file { my ($r) = @_; my ($symb)=&get_symb($r); if (!$symb) {return '';} my $default_form_data=&defaultFormData($symb); # do the detection of only doing skipped records first befroe we delete # them when doing the corrections reset if ($env{'form.scantron_options_redo'} ne 'redo_skipped_ready') { &reset_skipping_status(); } if ($env{'form.scantron_options_redo'} eq 'redo_skipped') { &remember_current_skipped(); $env{'form.scantron_options_redo'}='redo_skipped_ready'; } if ($env{'form.scantron_options_ignore'} eq 'ignore_corrections') { &check_for_error($r,&scantron_remove_file('corrected')); &check_for_error($r,&scantron_remove_file('skipped')); &check_for_error($r,&scantron_remove_scan_data()); $env{'form.scantron_options_ignore'}='done'; } if ($env{'form.scantron_corrections'}) { &scantron_process_corrections($r); } $r->print("

Gathering necessary info.

");$r->rflush(); #get the student pick code ready $r->print(&Apache::loncommon::studentbrowser_javascript()); my $max_bubble=&scantron_get_maxbubble(); my $result=&scantron_form_start($max_bubble).$default_form_data; $r->print($result); my @validate_phases=( 'sequence', 'ID', 'CODE', 'doublebubble', 'missingbubbles'); if (!$env{'form.validatepass'}) { $env{'form.validatepass'} = 0; } my $currentphase=$env{'form.validatepass'}; my $stop=0; while (!$stop && $currentphase < scalar(@validate_phases)) { $r->print("

Validating ".$validate_phases[$currentphase]."

"); $r->rflush(); my $which="scantron_validate_".$validate_phases[$currentphase]; { no strict 'refs'; ($stop,$currentphase)=&$which($r,$currentphase); } } if (!$stop) { my $warning=&scantron_warning_screen('Start Grading'); $r->print(< $warning STUFF } else { $r->print(''); $r->print(""); } if ($stop) { if ($validate_phases[$currentphase] eq 'sequence') { $r->print(''); $r->print(' this error
'); $r->print("

Or click the 'Grading Menu' button to start over.

"); } else { $r->print(''); $r->print(' using corrected info
'); $r->print(""); $r->print(" this scanline saving it for later."); } } $r->print("
".&show_grading_menu_form($symb)); return ''; } =pod =item scantron_remove_file Removes the requested bubble sheet data file, makes sure that scantron_original_ is never removed =cut sub scantron_remove_file { my ($which)=@_; my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; my $file='scantron_'; if ($which eq 'corrected' || $which eq 'skipped') { $file.=$which.'_'; } else { return 'refused'; } $file.=$env{'form.scantron_selectfile'}; return &Apache::lonnet::removeuserfile($cname,$cdom,$file); } =pod =item scantron_remove_scan_data Removes all scan_data correction for the requested bubble sheet data file. (In the case that both the are doing skipped records we need to remember the old skipped lines for the time being so that element persists for a while.) =cut sub scantron_remove_scan_data { my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; my @keys=&Apache::lonnet::getkeys('nohist_scantrondata',$cdom,$cname); my @todelete; my $filename=$env{'form.scantron_selectfile'}; foreach my $key (@keys) { if ($key=~/^\Q$filename\E_/) { if ($env{'form.scantron_options_redo'} eq 'redo_skipped_ready' && $key=~/remember_skipping/) { next; } push(@todelete,$key); } } my $result; if (@todelete) { $result=&Apache::lonnet::del('nohist_scantrondata',\@todelete,$cdom,$cname); } return $result; } =pod =item scantron_getfile Fetches the requested bubble sheet data file (all 3 versions), and the scan_data hash Arguments: None Returns: 2 hash references - first one has orig - corrected - skipped - each of which points to an array ref of the specified file broken up into individual lines count - number of scanlines - second is the scan_data hash possible keys are ($number refers to scanline numbered $number and thus the key affects only that scanline $bubline refers to the specific bubble line element and the aspects refers to that specific bubble line element) $number.user - username:domain to use $number.CODE_ignore_dup - ignore the duplicate CODE error $number.useCODE - use the CODE in the scanline as is $number.no_bubble.$bubline - it is valid that there is no bubbled in bubble at $number $bubline remember_skipping - a frozen hash containing keys of $number and values of either 1 - we are on a 'do skipped records pass' and plan on processing this line 2 - we are on a 'do skipped records pass' and this scanline has been marked to skip yet again =cut sub scantron_getfile { #FIXME really would prefer a scantron directory 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('nohist_scantrondata',$cdom,$cname); if ($tmp[0] =~ /^(error:|no_such_host)/) { @tmp=(); } my %scan_data = @tmp; return (\%scanlines,\%scan_data); } =pod =item lonnet_putfile Wrapper routine to call &Apache::lonnet::finishuserfileupload Arguments: $contents - data to store $filename - filename to store $contents into Returns: result value from &Apache::lonnet::finishuserfileupload =cut sub lonnet_putfile { my ($contents,$filename)=@_; my $docuname=$env{'course.'.$env{'request.course.id'}.'.num'}; my $docudom=$env{'course.'.$env{'request.course.id'}.'.domain'}; $env{'form.sillywaytopassafilearound'}=$contents; &Apache::lonnet::finishuserfileupload($docuname,$docudom,'sillywaytopassafilearound',$filename); } =pod =item scantron_putfile Stores the current version of the bubble sheet data files, and the scan_data hash. (Does not modify the original version only the corrected and skipped versions. Arguments: $scanlines - hash ref that looks like the first return value from &scantron_getfile() $scan_data - hash ref that looks like the second return value from &scantron_getfile() =cut sub scantron_putfile { my ($scanlines,$scan_data) = @_; #FIXME really would prefer a scantron directory my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; if ($scanlines) { 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('nohist_scantrondata',$scan_data,$cdom,$cname); } =pod =item scantron_get_line Returns the correct version of the scanline Arguments: $scanlines - hash ref that looks like the first return value from &scantron_getfile() $scan_data - hash ref that looks like the second return value from &scantron_getfile() $i - number of the requested line (starts at 0) Returns: A scanline, (either the original or the corrected one if it exists), or undef if the requested scanline should be skipped. (Either because it's an skipped scanline, or it's an unskipped scanline and we are not doing a 'do skipped scanlines' pass. =cut sub scantron_get_line { my ($scanlines,$scan_data,$i)=@_; if (&should_be_skipped($scanlines,$scan_data,$i)) { return undef; } #if ($scanlines->{'skipped'}[$i]) { return undef; } if ($scanlines->{'corrected'}[$i]) {return $scanlines->{'corrected'}[$i];} return $scanlines->{'orig'}[$i]; } =pod =item scantron_todo_count Counts the number of scanlines that need processing. Arguments: $scanlines - hash ref that looks like the first return value from &scantron_getfile() $scan_data - hash ref that looks like the second return value from &scantron_getfile() Returns: $count - number of scanlines to process =cut sub get_todo_count { my ($scanlines,$scan_data)=@_; my $count=0; for (my $i=0;$i<=$scanlines->{'count'};$i++) { my $line=&scantron_get_line($scanlines,$scan_data,$i); if ($line=~/^[\s\cz]*$/) { next; } $count++; } return $count; } =pod =item scantron_put_line Updates the 'corrected' or 'skipped' versions of the bubble sheet data file. Arguments: $scanlines - hash ref that looks like the first return value from &scantron_getfile() $scan_data - hash ref that looks like the second return value from &scantron_getfile() $i - line number to update $newline - contents of the updated scanline $skip - if true make the line for skipping and update the 'skipped' file =cut sub scantron_put_line { my ($scanlines,$scan_data,$i,$newline,$skip)=@_; if ($skip) { $scanlines->{'skipped'}[$i]=$newline; &start_skipping($scan_data,$i); return; } $scanlines->{'corrected'}[$i]=$newline; } =pod =item scantron_clear_skip Remove a line from the 'skipped' file Arguments: $scanlines - hash ref that looks like the first return value from &scantron_getfile() $scan_data - hash ref that looks like the second return value from &scantron_getfile() $i - line number to update =cut sub scantron_clear_skip { my ($scanlines,$scan_data,$i)=@_; if (exists($scanlines->{'skipped'}[$i])) { undef($scanlines->{'skipped'}[$i]); return 1; } return 0; } =pod =item scantron_filter_not_exam Filter routine used by &Apache::lonnavmaps::retrieveResources(), to filter out resources that are not marked as 'exam' mode =cut sub scantron_filter_not_exam { my ($curres)=@_; if (ref($curres) && $curres->is_problem() && !$curres->is_exam()) { # if the user has asked to not have either hidden # or 'randomout' controlled resources to be graded # don't include them if ($env{'form.scantron_options_hidden'} eq 'ignore_hidden' && $curres->randomout) { return 0; } return 1; } return 0; } =pod =item scantron_validate_sequence Validates the selected sequence, checking for resource that are not set to exam mode. =cut sub scantron_validate_sequence { my ($r,$currentphase) = @_; my $navmap=Apache::lonnavmaps::navmap->new(); my (undef,undef,$sequence)= &Apache::lonnet::decode_symb($env{'form.selectpage'}); my $map=$navmap->getResourceByUrl($sequence); $r->print(''); if ($env{'form.validate_sequence_exam'} ne 'ignore') { my @resources= $navmap->retrieveResources($map,\&scantron_filter_not_exam,1,0); if (@resources) { $r->print("

".&mt('Some resources in the sequence currently are not set to exam mode. Grading these resources currently may not work correctly.')."

"); return (1,$currentphase); } } return (0,$currentphase+1); } =pod =item scantron_validate_ID Validates all scanlines in the selected file to not have any invalid or underspecified student IDs =cut 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,$scan_data,$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,$currentphase); } elsif ($found{'usernames'}{$username}) { &scantron_get_correction($r,$i,$scan_record,\%scantron_config, $line,'duplicateID',$username); return(1,$currentphase); } #FIXME store away line we previously 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,$currentphase); } elsif (!defined($username)) { &scantron_get_correction($r,$i,$scan_record, \%scantron_config, $line,'incorrectID'); return(1,$currentphase); } $found{'usernames'}{$username}++; } else { &scantron_get_correction($r,$i,$scan_record,\%scantron_config, $line,'incorrectID'); return(1,$currentphase); } } } return (0,$currentphase+1); } =pod =item scantron_get_correction Builds the interface screen to interact with the operator to fix a specific error condition in a specific scanline Arguments: $r - Apache request object $i - number of the current scanline $scan_record - hash ref as returned from &scantron_parse_scanline() $scan_config - hash ref as returned from &get_scantron_config() $line - full contents of the current scanline $error - error condition, valid values are 'incorrectCODE', 'duplicateCODE', 'doublebubble', 'missingbubble', 'duplicateID', 'incorrectID' $arg - extra information needed For errors: - duplicateID - paper number that this studentID was seen before on - duplicateCODE - array ref of the paper numbers this CODE was seen on before - incorrectCODE - current incorrect CODE - doublebubble - array ref of the bubble lines that have double bubble errors - missingbubble - array ref of the bubble lines that have missing bubble errors =cut 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("

An error was detected ($error)"); if ( $$scan_record{'scantron.PaperID'} =~ /\S/) { $r->print(" for PaperID ". $$scan_record{'scantron.PaperID'}." \n"); } else { $r->print(" in scanline $i

".
		  $line."
\n"); } my $message="

The ID on the form is ". $$scan_record{'scantron.ID'}."
\n". "The name on the paper is ". $$scan_record{'scantron.LastName'}.",". $$scan_record{'scantron.FirstName'}."

"; $r->print(''."\n"); $r->print(''."\n"); if ($error =~ /ID$/) { if ($error eq 'incorrectID') { $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($message); $r->print("

How should I handle this?
\n"); $r->print("\n

  • "); #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(": "); $r->print("\n@". &Apache::loncommon::select_dom_form($env{'request.role.domain'},'scantron_domain')); $r->print('
  • '); } elsif ($error =~ /CODE$/) { if ($error eq 'incorrectCODE') { $r->print("

    The encoded CODE is not in the list of possible CODEs

    \n"); } elsif ($error eq 'duplicateCODE') { $r->print("

    The encoded CODE has also been used by a previous paper ".join(', ',@{$arg}).", and CODEs are supposed to be unique

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

    The CODE on the form is '". $$scan_record{'scantron.CODE'}."'
    \n"); $r->print($message); $r->print("

    How should I handle this?
    \n"); $r->print("\n
    "); my $i=0; if ($error eq 'incorrectCODE' && $$scan_record{'scantron.CODE'}=~/\S/ ) { my ($max,$closest)=&scantron_get_closely_matching_CODEs($arg,$$scan_record{'scantron.CODE'}); if ($closest > 0) { foreach my $testcode (@{$closest}) { my $checked=''; if (!$i) { $checked=' checked="checked" '; } $r->print(""); $r->print("\n
    "); $i++; } } } if ($$scan_record{'scantron.CODE'}=~/\S/ ) { my $checked; if (!$i) { $checked=' checked="checked" '; } $r->print(""); $r->print("\n
    "); } $r->print(< function change_radio(field) { var slct=document.scantronupload.scantron_CODE_resolution; var i; for (i=0;i ENDSCRIPT my $href="/adm/pickcode?". "form=".&escape("scantronupload"). "&scantron_format=".&escape($env{'form.scantron_format'}). "&scantron_CODElist=".&escape($env{'form.scantron_CODElist'}). "&curCODE=".&escape($$scan_record{'scantron.CODE'}). "&scantron_selectfile=".&escape($env{'form.scantron_selectfile'}); if ($env{'form.scantron_CODElist'} =~ /\S/) { $r->print(" Selected CODE is "); $r->print("\n
    "); } $r->print(" as the CODE."); $r->print("\n

    "); } elsif ($error eq 'doublebubble') { $r->print("

    There have been multiple bubbles scanned for a some question(s)

    \n"); $r->print(''); $r->print($message); $r->print("

    Please indicate which bubble should be used for grading

    "); 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("

    There have been no bubbles scanned for some question(s)

    \n"); $r->print($message); $r->print("

    Please indicate which bubble should be used for grading

    "); $r->print("Some questions have no scanned bubbles\n"); $r->print(''); foreach my $question (@{$arg}) { my $selected=$$scan_record{"scantron.$question.answer"}; &scantron_bubble_selector($r,$scan_config,$question); } } else { $r->print("\n
      "); } $r->print("\n
    "); } =pod =item scantron_bubble_selector Generates the html radiobuttons to correct a single bubble line possibly showing the existing the selected bubbles if known Arguments: $r - Apache request object $scan_config - hash from &get_scantron_config() $quest - number of the bubble line to make a corrector for $selected - array of letters of previously selected bubbles $lines - if present, number of bubble lines to show =cut sub scantron_bubble_selector { my ($r,$scan_config,$quest,@selected, $lines)=@_; my $max=$$scan_config{'Qlength'}; my $scmode=$$scan_config{'Qon'}; if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; } if (!defined($lines)) { $lines = 1; } my $total_lines = $lines*2; my @alphabet=('A'..'Z'); $r->print(""); for (my $l = 0; $l < $lines; $l++) { if ($l != 0) { $r->print(''); } # FIXME: This loop probably has to be considerably more clever for # multiline bubbles: User can multibubble by having bubbles in # several lines. User can skip lines legitimately etc. etc. for (my $i=0;$i<$max;$i++) { $r->print("\n".''); } if ($l == 0) { my $lspan = $total_lines * 2; # 2 table rows per bubble line. $r->print(''); } $r->print(''); # FIXME: This may have to be a bit more clever for # multiline questions (different values e.g..). for (my $i=0;$i<$max;$i++) { $r->print("\n". '"); } $r->print(''); } $r->print('
    $quest
    '); if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) ; } else { $r->print(' '); } $r->print('
    '); } =pod =item num_matches Counts the number of characters that are the same between the two arguments. Arguments: $orig - CODE from the scanline $code - CODE to match against Returns: $count - integer count of the number of same characters between the two arguments =cut sub num_matches { my ($orig,$code) = @_; my @code=split(//,$code); my @orig=split(//,$orig); my $same=0; for (my $i=0;$i{'count'};$i++) { my $line=&scantron_get_line($scanlines,$scan_data,$i); if ($line=~/^[\s\cz]*$/) { next; } my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, $scan_data); my $CODE=$$scan_record{'scantron.CODE'}; my $error=0; if (!&Apache::lonnet::validCODE($CODE)) { &scantron_get_correction($r,$i,$scan_record, \%scantron_config, $line,'incorrectCODE',\%allcodes); return(1,$currentphase); } if (%allcodes && !exists($allcodes{$CODE}) && !$$scan_record{'scantron.useCODE'}) { &scantron_get_correction($r,$i,$scan_record, \%scantron_config, $line,'incorrectCODE',\%allcodes); return(1,$currentphase); } if (exists($usedCODEs{$CODE}) && $env{'form.scantron_CODEunique'} eq 'yes' && !$$scan_record{'scantron.CODE_ignore_dup'}) { &scantron_get_correction($r,$i,$scan_record, \%scantron_config, $line,'duplicateCODE',$usedCODEs{$CODE}); return(1,$currentphase); } push (@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'}); } return (0,$currentphase+1); } =pod =item scantron_validate_doublebubble Validates all scanlines in the selected file to not have any bubble lines with multiple bubbles marked. =cut 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,$scan_data,$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); } =pod =item scantron_get_maxbubble Returns the maximum number of bubble lines that are expected to occur. Does this by walking the selected sequence rendering the resource and then checking &Apache::lonxml::get_problem_counter() for what the current value of the problem counter is. Caches the result to $env{'form.scantron_maxbubble'} =cut sub scantron_get_maxbubble { if (defined($env{'form.scantron_maxbubble'}) && $env{'form.scantron_maxbubble'}) { return $env{'form.scantron_maxbubble'}; } my $navmap=Apache::lonnavmaps::navmap->new(); my (undef,undef,$sequence)= &Apache::lonnet::decode_symb($env{'form.selectpage'}); my $map=$navmap->getResourceByUrl($sequence); my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); &Apache::lonxml::clear_problem_counter(); my $uname = $env{'form.student'}; my $udom = $env{'form.userdom'}; my $cid = $env{'request.course.id'}; my $total_lines = 0; %bubble_lines_per_response = (); foreach my $resource (@resources) { my $symb = $resource->symb(); my $result=&Apache::lonnet::ssi($resource->src(), ('symb' => $resource->symb()), ('grade_target' => 'analyze'), ('grade_courseid' => $cid), ('grade_domain' => $udom), ('grade_username' => $uname)); my (undef, $an) = split(/_HASH_REF__/,$result, 2); my %analysis = &Apache::lonnet::str2hash($an); foreach my $part_id (@{$analysis{'parts'}}) { my $bubble_lines = $analysis{"$part_id.bubble_lines"}[0]; if (!$bubble_lines) { $bubble_lines = 1; } $bubble_lines_per_response{"$symb.$part_id"} = $bubble_lines; $total_lines = $total_lines + $bubble_lines; } } &Apache::lonnet::delenv('scantron\.'); $env{'form.scantron_maxbubble'} = $total_lines; return $env{'form.scantron_maxbubble'}; } =pod =item scantron_validate_missingbubbles Validates all scanlines in the selected file to not have any bubble lines with missing bubbles that haven't been verified as missing. =cut 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=&scantron_get_maxbubble(); if (!$max_bubble) { $max_bubble=2**31; } for (my $i=0;$i<=$scanlines->{'count'};$i++) { my $line=&scantron_get_line($scanlines,$scan_data,$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); } =pod =item scantron_process_students Routine that does the actual grading of the bubble sheet information. The parsed scanline hash is added to %env Then foreach unskipped scanline it does an &Apache::lonnet::ssi() foreach resource , with the form data of 'submitted' =>'scantron' 'grade_target' =>'grade', 'grade_username'=> username of student 'grade_domain' => domain of student 'grade_courseid'=> of course 'grade_symb' => symb of resource to grade This triggers a grading pass. The problem grading code takes care of converting the bubbled letter information (now in %env) into a valid submission. =cut sub scantron_process_students { my ($r) = @_; my (undef,undef,$sequence)=&Apache::lonnet::decode_symb($env{'form.selectpage'}); my ($symb)=&get_symb($r); if (!$symb) {return '';} my $default_form_data=&defaultFormData($symb); my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); my $navmap=Apache::lonnavmaps::navmap->new(); my $map=$navmap->getResourceByUrl($sequence); my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); # $r->print("geto ".scalar(@resources)."
    "); my $result= < $default_form_data SCANTRONFORM $r->print($result); my @delayqueue; my %completedstudents; my $count=&get_todo_count($scanlines,$scan_data); my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron Status', 'Scantron Progress',$count, 'inline',undef,'scantronupload'); &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state, 'Processing first student'); my $start=&Time::HiRes::time(); my $i=-1; my ($uname,$udom,$started); while ($i<$scanlines->{'count'}) { ($uname,$udom)=('',''); $i++; my $line=&scantron_get_line($scanlines,$scan_data,$i); if ($line=~/^[\s\cz]*$/) { next; } if ($started) { &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, 'last student'); } $started=1; 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::lonxml::clear_problem_counter(); &Apache::lonnet::appenv(%$scan_record); if (&scantron_clear_skip($scanlines,$scan_data,$i)) { &scantron_putfile($scanlines,$scan_data); } my $i=0; foreach my $resource (@resources) { $i++; my %form=('submitted' =>'scantron', 'grade_target' =>'grade', 'grade_username'=>$uname, 'grade_domain' =>$udom, 'grade_courseid'=>$env{'request.course.id'}, 'grade_symb' =>$resource->symb()); if (exists($scan_record->{'scantron.CODE'}) && &Apache::lonnet::validCODE($scan_record->{'scantron.CODE'})) { $form{'CODE'}=$scan_record->{'scantron.CODE'}; } else { $form{'CODE'}=''; } my $result=&Apache::lonnet::ssi($resource->src(),%form); if ($result ne '') { &Apache::lonnet::logthis("scantron grading error -> $result"); &Apache::lonnet::logthis("scantron grading error info name $uname domain $udom course $env{'request.course.id'} url ".$resource->src()); } if (&Apache::loncommon::connection_aborted($r)) { last; } } $completedstudents{$uname}={'line'=>$line}; if (&Apache::loncommon::connection_aborted($r)) { last; } } continue { &Apache::lonxml::clear_problem_counter(); &Apache::lonnet::delenv('scantron\.'); } &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); # my $lasttime = &Time::HiRes::time()-$start; # $r->print("

    took $lasttime

    "); $r->print(""); $r->print(&show_grading_menu_form($symb)); return ''; } =pod =item scantron_upload_scantron_data Creates the screen for adding a new bubble sheet data file to a course. =cut 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', 'coursename'); my $domsel=&Apache::loncommon::select_dom_form($env{'request.role.domain'}, 'domainid'); my $default_form_data=&defaultFormData(&get_symb($r,1)); $r->print(< 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(); }
    $default_form_data
    $select_link
    Course ID:
    Course Name:
    Domain: $domsel
    File to upload:
    UPLOAD return ''; } =pod =item scantron_upload_scantron_data_save Adds a provided bubble information data file to the course if user has the correct privileges to do so. =cut sub scantron_upload_scantron_data_save { my($r)=@_; my ($symb)=&get_symb($r,1); my $doanotherupload= '
    '."\n". ''."\n". ''."\n". '
    '."\n"; if (!&Apache::lonnet::allowed('usc',$env{'form.domainid'}) && !&Apache::lonnet::allowed('usc', $env{'form.domainid'}.'_'.$env{'form.courseid'})) { $r->print("You are not allowed to upload Scantron data to the requested course.
    "); if ($symb) { $r->print(&show_grading_menu_form($symb)); } else { $r->print($doanotherupload); } return ''; } my %coursedata=&Apache::lonnet::coursedescription($env{'form.domainid'}.'_'.$env{'form.courseid'}); $r->print("Doing upload to ".$coursedata{'description'}."
    "); my $fname=$env{'form.upfile.filename'}; #FIXME #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'; } my $uploadedfile=$fname; $fname='scantron_orig_'.$fname; if (length($env{'form.upfile'}) < 2) { $r->print("Error: The file you attempted to upload, ".&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').", contained no information. Please check that you entered the correct filename."); } else { my $result=&Apache::lonnet::finishuserfileupload($env{'form.courseid'},$env{'form.domainid'},'upfile',$fname); if ($result =~ m|^/uploaded/|) { $r->print("Success: Successfully uploaded ".(length($env{'form.upfile'})-1)." bytes of data into location ".$result.""); } else { $r->print("Error: An error (".$result.") occurred when attempting to upload the file, ".&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').""); } } if ($symb) { $r->print(&scantron_selectphase($r,$uploadedfile)); } else { $r->print($doanotherupload); } return ''; } =pod =item valid_file Validates that the requested bubble data file exists in the course. =cut sub valid_file { my ($requested_file)=@_; foreach my $filename (sort(&scantron_filenames())) { if ($requested_file eq $filename) { return 1; } } return 0; } =pod =item scantron_download_scantron_data Shows a list of the three internal files (original, corrected, skipped) for a specific bubble sheet data file that exists in the course. =cut sub scantron_download_scantron_data { my ($r)=@_; my $default_form_data=&defaultFormData(&get_symb($r,1)); my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; my $file=$env{'form.scantron_selectfile'}; if (! &valid_file($file)) { $r->print(< The requested file name was invalid.

    ERROR $r->print(&show_grading_menu_form(&get_symb($r,1))); return; } my $orig='/uploaded/'.$cdom.'/'.$cname.'/scantron_orig_'.$file; my $corrected='/uploaded/'.$cdom.'/'.$cname.'/scantron_corrected_'.$file; my $skipped='/uploaded/'.$cdom.'/'.$cname.'/scantron_skipped_'.$file; &Apache::lonnet::allowuploaded('/adm/grades',$orig); &Apache::lonnet::allowuploaded('/adm/grades',$corrected); &Apache::lonnet::allowuploaded('/adm/grades',$skipped); $r->print(< Original file as uploaded by the scantron office.

    Corrections, a file of corrected records that were used in grading.

    Skipped, a file of records that were skipped.

    DOWNLOAD $r->print(&show_grading_menu_form(&get_symb($r,1))); return ''; } =pod =back =cut #-------- end of section for handling grading scantron forms ------- # #------------------------------------------------------------------- #-------------------------- Menu interface ------------------------- # #--- Show a Grading Menu button - Calls the next routine --- sub show_grading_menu_form { my ($symb)=@_; my $result.='
    '."\n". ''."\n". ''."\n". ''."\n". ''."\n". '
    '."\n"; return $result; } # -- Retrieve choices for grading form sub savedState { my %savedState = (); if ($env{'form.saveState'}) { foreach (split(/:/,$env{'form.saveState'})) { my ($key,$value) = split(/=/,$_,2); $savedState{$key} = $value; } } return \%savedState; } sub grading_menu { my ($request) = @_; my ($symb)=&get_symb($request); if (!$symb) {return '';} my $probTitle = &Apache::lonnet::gettitle($symb); my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle); # # Define menu data $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb); my ($table) = &showResourceInfo($symb,$env{'form.probTitle'}); $request->print($table); my %fields = ('symb'=>&Apache::lonenc::check_encrypt($symb), 'handgrade'=>$hdgrade, 'probTitle'=>$probTitle, 'command'=>'submit_options', 'saveState'=>"", 'gradingMenu'=>1, 'showgrading'=>"yes"); my $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); my @menu = ({ url => $url, name => &mt('Manual Grading/View Submissions'), short_description => &mt('Start the process of hand grading submissions.'), }); $fields{'command'} = 'csvform'; $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); push (@menu, { url => $url, name => &mt('Upload Scores'), short_description => &mt('Specify a file containing the class scores for current resource.')}); $fields{'command'} = 'processclicker'; $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); push (@menu, { url => $url, name => &mt('Process Clicker'), short_description => &mt('Specify a file containing the clicker information for this resource.')}); $fields{'command'} = 'scantron_selectphase'; $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); push (@menu, { url => $url, name => &mt('Grade Scantron Forms'), short_description => &mt('')}); $fields{'command'} = 'verify'; $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); push (@menu, { url => "", jscript => ' onClick="javascript:checkChoice2(document.forms.gradingMenu,\'5\',\'verify\')" ', name => &mt('Verify Receipt'), short_description => &mt('')}); $fields{'command'} = 'manage'; $url = &Apache::lonhtmlcommon::build_url('/adm/helper/resettimes.helper',\%fields); push (@menu, { url => $url, name => &mt('Manage Access Times'), short_description => &mt('')}); $fields{'command'} = 'view'; $url = &Apache::lonhtmlcommon::build_url('/adm/pickcode',\%fields); push (@menu, { url => $url, name => &mt('View Saved CODEs'), short_description => &mt('')}); # # Create the menu my $Str; # $Str .= '

    '.&mt('Please select a grading task').'

    '; $Str .= '
    '; $Str .= ''. ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n"; foreach my $menudata (@menu) { if ($menudata->{'name'} ne &mt('Verify Receipt')) { $Str .='

    {'jscript'}. ' href="'. $menudata->{'url'}.'" >'. $menudata->{'name'}."

    \n"; } else { $Str .='

    {'jscript'}. ' href="javascript:checkChoice2(document.forms.gradingMenu,\'5\',\'verify\')" >'. $menudata->{'name'}."

    \n"; $Str .= (' 'x8). ' receipt: '.&Apache::lonnet::recprefix($env{'request.course.id'}). '-'; } $Str .= ' '.(' 'x8).$menudata->{'short_description'}. "\n"; } $Str .="\n"; $Str .="
    \n"; $request->print(< function checkChoice(formname,val,cmdx) { if (val <= 2) { var cmd = radioSelection(formname.radioChoice); var cmdsave = cmd; } else { cmd = cmdx; cmdsave = 'submission'; } formname.command.value = cmd; formname.saveState.value = "saveCmd="+cmdsave+":saveSec="+pullDownSelection(formname.section)+ ":saveSub="+pullDownSelection(formname.submitonly)+":saveStatus="+pullDownSelection(formname.Status); if (val < 5) formname.submit(); if (val == 5) { if (!checkReceiptNo(formname,'notOK')) { return false;} formname.submit(); } if (val < 7) formname.submit(); } function checkChoice2(formname,val,cmdx) { if (val <= 2) { var cmd = radioSelection(formname.radioChoice); var cmdsave = cmd; } else { cmd = cmdx; cmdsave = 'submission'; } formname.command.value = cmd; if (val < 5) formname.submit(); if (val == 5) { if (!checkReceiptNo(formname,'notOK')) { return false;} formname.submit(); } if (val < 7) formname.submit(); } function checkReceiptNo(formname,nospace) { var receiptNo = formname.receipt.value; var checkOpt = false; if (nospace == "OK" && isNaN(receiptNo)) {checkOpt = true;} if (nospace == "notOK" && (isNaN(receiptNo) || receiptNo == "")) {checkOpt = true;} if (checkOpt) { alert("Please enter a receipt number given by a student in the receipt box."); formname.receipt.value = ""; formname.receipt.focus(); return false; } return true; } GRADINGMENUJS &commonJSfunctions($request); my $result='

     Manual Grading/View Submission

    '; $result.=$table; my (undef,$sections) = &getclasslist('all','0'); my $savedState = &savedState(); my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'submission' : $$savedState{'saveCmd'}); my $saveSec = ($$savedState{'saveSec'} eq '' ? 'all' : $$savedState{'saveSec'}); my $saveSub = ($$savedState{'saveSub'} eq '' ? 'all' : $$savedState{'saveSub'}); my $saveStatus = ($$savedState{'saveStatus'} eq '' ? 'Active' : $$savedState{'saveStatus'}); $result.='
    '."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n"; $result.='
    '."\n". ''."\n". '
    '."\n". ' Select a Grading/Viewing Option
    '."\n"; $result.=''; $result.=''."\n"; $result.=''; # $result.=''; $result.=''."\n"; $result.=''; $result.='
    '.&mt('Sections').'Groups'.&mt('Access Status').'
    '."\n". '    '; return $Str; } #--- Displays the submissions first page ------- sub submit_options { my ($request) = @_; my ($symb)=&get_symb($request); if (!$symb) {return '';} my $probTitle = &Apache::lonnet::gettitle($symb); $request->print(< function checkChoice(formname,val,cmdx) { if (val <= 2) { var cmd = radioSelection(formname.radioChoice); var cmdsave = cmd; } else { cmd = cmdx; cmdsave = 'submission'; } formname.command.value = cmd; formname.saveState.value = "saveCmd="+cmdsave+":saveSec="+pullDownSelection(formname.section)+ ":saveSub="+pullDownSelection(formname.submitonly)+":saveStatus="+pullDownSelection(formname.Status); if (val < 5) formname.submit(); if (val == 5) { if (!checkReceiptNo(formname,'notOK')) { return false;} formname.submit(); } if (val < 7) formname.submit(); } function checkReceiptNo(formname,nospace) { var receiptNo = formname.receipt.value; var checkOpt = false; if (nospace == "OK" && isNaN(receiptNo)) {checkOpt = true;} if (nospace == "notOK" && (isNaN(receiptNo) || receiptNo == "")) {checkOpt = true;} if (checkOpt) { alert("Please enter a receipt number given by a student in the receipt box."); formname.receipt.value = ""; formname.receipt.focus(); return false; } return true; } GRADINGMENUJS &commonJSfunctions($request); my $result='

     Manual Grading/View Submission

    '; my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle); $result.=$table; my (undef,$sections) = &getclasslist('all','0'); my $savedState = &savedState(); my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'submission' : $$savedState{'saveCmd'}); my $saveSec = ($$savedState{'saveSec'} eq '' ? 'all' : $$savedState{'saveSec'}); my $saveSub = ($$savedState{'saveSub'} eq '' ? 'all' : $$savedState{'saveSub'}); my $saveStatus = ($$savedState{'saveStatus'} eq '' ? 'Active' : $$savedState{'saveStatus'}); $result.=''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n"; $result.='
    '."\n". ''."\n". ''; #'; $result.= '
    '."\n". ' Select a Grading/Viewing Option
    '."\n"; $result.=''; $result.=''."\n"; $result.=''; $result.=''; $result.=''."\n"; $result.=''; $result.=''; $result.=''."\n"; $result.=''."\n"; $result.=''."\n"; $result.='
    '.&mt('Sections').''.&mt('Groups').''.&mt('Access Status').'
    '."\n". '    '; $result.= ''."\n"; $result.= &Apache::lonstatistics::GroupSelect('group','multiple',3); $result.=''."\n"; $result.=&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,3,undef,'mult'); $result.='
    '. '
    '. '

    '. ''. '
    '."\n"; $result.='
    '; # $result.=''; # $result.=''."\n"; # # $result.=''."\n"; # # $result.=''."\n"; # # if ((&Apache::lonnet::allowed('mgr',$env{'request.course.id'})) && ($symb)) { # $result.=''."\n"; # } # $result.=''."\n"; # $result.=''."\n"; # # $result.='
    '. # ''. # ' '.&mt('scores from file').'
    '. # ''. # ' '.&mt('clicker file').'
    '. # ' scantron forms
    '. # ''. # ' '.&mt('receipt').': '. # &Apache::lonnet::recprefix($env{'request.course.id'}). # '-'. # '
    '. # ' access times.
    '. # ' saved CODEs.
    '."\n".'
    '."\n". '
    '."\n"; return $result; } sub reset_perm { undef(%perm); } sub init_perm { &reset_perm(); foreach my $test_perm ('vgr','mgr','opa') { my $scope = $env{'request.course.id'}; if (!($perm{$test_perm}=&Apache::lonnet::allowed($test_perm,$scope))) { $scope .= '/'.$env{'request.course.sec'}; if ( $perm{$test_perm}= &Apache::lonnet::allowed($test_perm,$scope)) { $perm{$test_perm.'_section'}=$env{'request.course.sec'}; } else { delete($perm{$test_perm}); } } } } sub gather_clicker_ids { my %clicker_ids; my $classlist = &Apache::loncoursedata::get_classlist(); # Set up a couple variables. my $username_idx = &Apache::loncoursedata::CL_SNAME(); my $domain_idx = &Apache::loncoursedata::CL_SDOM(); my $status_idx = &Apache::loncoursedata::CL_STATUS(); foreach my $student (keys(%$classlist)) { if ($classlist->{$student}->[$status_idx] ne 'Active') { next; } my $username = $classlist->{$student}->[$username_idx]; my $domain = $classlist->{$student}->[$domain_idx]; my $clickers = (&Apache::lonnet::userenvironment($domain,$username,'clickers'))[1]; foreach my $id (split(/\,/,$clickers)) { $id=~s/^[\#0]+//; $id=~s/[\-\:]//g; if (exists($clicker_ids{$id})) { $clicker_ids{$id}.=','.$username.':'.$domain; } else { $clicker_ids{$id}=$username.':'.$domain; } } } return %clicker_ids; } sub gather_adv_clicker_ids { my %clicker_ids; my $cnum=$env{'course.'.$env{'request.course.id'}.'.num'}; my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; my %coursepersonnel=&Apache::lonnet::get_course_adv_roles($cdom.'/'.$cnum); foreach my $element (sort(keys(%coursepersonnel))) { foreach my $person (split(/\,/,$coursepersonnel{$element})) { my ($puname,$pudom)=split(/\:/,$person); my $clickers = (&Apache::lonnet::userenvironment($pudom,$puname,'clickers'))[1]; foreach my $id (split(/\,/,$clickers)) { $id=~s/^[\#0]+//; $id=~s/[\-\:]//g; if (exists($clicker_ids{$id})) { $clicker_ids{$id}.=','.$puname.':'.$pudom; } else { $clicker_ids{$id}=$puname.':'.$pudom; } } } } return %clicker_ids; } sub clicker_grading_parameters { return ('gradingmechanism' => 'scalar', 'upfiletype' => 'scalar', 'specificid' => 'scalar', 'pcorrect' => 'scalar', 'pincorrect' => 'scalar'); } sub process_clicker { my ($r)=@_; my ($symb)=&get_symb($r); if (!$symb) {return '';} my $result=&checkforfile_js(); $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb); my ($table) = &showResourceInfo($symb,$env{'form.probTitle'}); $result.=$table; $result.='
    '."\n"; $result.=''."\n"; $result.='
    '."\n"; $result.=' '.&mt('Specify a file containing the clicker information for this resource'). '.
    '."\n"; # Attempt to restore parameters from last session, set defaults if not present my %Saveable_Parameters=&clicker_grading_parameters(); &Apache::loncommon::restore_course_settings('grades_clicker', \%Saveable_Parameters); if (!$env{'form.pcorrect'}) { $env{'form.pcorrect'}=100; } if (!$env{'form.pincorrect'}) { $env{'form.pincorrect'}=100; } if (!$env{'form.gradingmechanism'}) { $env{'form.gradingmechanism'}='attendance'; } if (!$env{'form.upfiletype'}) { $env{'form.upfiletype'}='iclicker'; } my %checked; foreach my $gradingmechanism ('attendance','personnel','specific') { if ($env{'form.gradingmechanism'} eq $gradingmechanism) { $checked{$gradingmechanism}="checked='checked'"; } } my $upload=&mt("Upload File"); my $type=&mt("Type"); my $attendance=&mt("Award points just for participation"); my $personnel=&mt("Correctness determined from response by course personnel"); my $specific=&mt("Correctness determined from response with clicker ID(s)"); my $pcorrect=&mt("Percentage points for correct solution"); my $pincorrect=&mt("Percentage points for incorrect solution"); my $selectform=&Apache::loncommon::select_form($env{'form.upfiletype'},'upfiletype', ('iclicker' => 'i>clicker', 'interwrite' => 'interwrite PRS')); $symb = &Apache::lonenc::check_encrypt($symb); $result.=< function sanitycheck() { // Accept only integer percentages document.forms.gradesupload.pcorrect.value=Math.round(document.forms.gradesupload.pcorrect.value); document.forms.gradesupload.pincorrect.value=Math.round(document.forms.gradesupload.pincorrect.value); // Find out grading choice for (i=0; i







    ENDUPFORM $result.='
    '."\n". '


    '."\n"; $result.=&show_grading_menu_form($symb); return $result; } sub process_clicker_file { my ($r)=@_; my ($symb)=&get_symb($r); if (!$symb) {return '';} my %Saveable_Parameters=&clicker_grading_parameters(); &Apache::loncommon::store_course_settings('grades_clicker', \%Saveable_Parameters); my ($result) = &showResourceInfo($symb,$env{'form.probTitle'}); if (($env{'form.gradingmechanism'} eq 'specific') && ($env{'form.specificid'}!~/\w/)) { $result.=''.&mt('You need to specify a clicker ID for the correct answer').''; return $result.&show_grading_menu_form($symb); } my %clicker_ids=&gather_clicker_ids(); my %correct_ids; if ($env{'form.gradingmechanism'} eq 'personnel') { %correct_ids=&gather_adv_clicker_ids(); } if ($env{'form.gradingmechanism'} eq 'specific') { foreach my $correct_id (split(/[\s\,]/,$env{'form.specificid'})) {; $correct_id=~tr/a-z/A-Z/; $correct_id=~s/\s//gs; $correct_id=~s/^[\#0]+//; $correct_id=~s/[\-\:]//g; if ($correct_id) { $correct_ids{$correct_id}='specified'; } } } if ($env{'form.gradingmechanism'} eq 'attendance') { $result.=&mt('Score based on attendance only'); } else { my $number=0; $result.='

    '.&mt('Correctness determined by the following IDs').''; foreach my $id (sort(keys(%correct_ids))) { $result.='
    '.$id.' - '; if ($correct_ids{$id} eq 'specified') { $result.=&mt('specified'); } else { my ($uname,$udom)=split(/\:/,$correct_ids{$id}); $result.=&Apache::loncommon::plainname($uname,$udom); } $number++; } $result.="

    \n"; if ($number==0) { $result.=''.&mt('No IDs found to determine correct answer').''; return $result.&show_grading_menu_form($symb); } } if (length($env{'form.upfile'}) < 2) { $result.=&mt('[_1] Error: [_2] The file you attempted to upload, [_3] contained no information. Please check that you entered the correct filename.', '', '', ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').''); return $result.&show_grading_menu_form($symb); } # Were able to get all the info needed, now analyze the file $result.=&Apache::loncommon::studentbrowser_javascript(); $symb = &Apache::lonenc::check_encrypt($symb); my $heading=&mt('Scanning clicker file'); $result.=(<
    $heading
    ENDHEADER my %responses; my @questiontitles; my $errormsg=''; my $number=0; if ($env{'form.upfiletype'} eq 'iclicker') { ($errormsg,$number)=&iclicker_eval(\@questiontitles,\%responses); } if ($env{'form.upfiletype'} eq 'interwrite') { ($errormsg,$number)=&interwrite_eval(\@questiontitles,\%responses); } $result.='
    '.&mt('Found [_1] question(s)',$number).'
    '. ''. &mt('Awarding [_1] percent for corrion(s)',$number).'
    '. ''. &mt('Awarding [_1] percent for correct and [_2] percent for incorrect responses', $env{'form.pcorrect'},$env{'form.pincorrect'}). '
    '; # Remember Question Titles # FIXME: Possibly need delimiter other than ":" for (my $i=0;$i<$number;$i++) { $result.='').'" />'; } my $correct_count=0; my $student_count=0; my $unknown_count=0; # Match answers with usernames # FIXME: Possibly need delimiter other than ":" foreach my $id (keys(%responses)) { if ($correct_ids{$id}) { $result.="\n".''; $correct_count++; } elsif ($clicker_ids{$id}) { if ($clicker_ids{$id}=~/\,/) { # More than one user with the same clicker! $result.="\n
    ".&mt('Clicker registered more than once').": ".$id."
    "; $result.="\n".''. "'; $unknown_count++; } else { # Good: found one and only one user with the right clicker $result.="\n".''; $student_count++; } } else { $result.="\n
    ".&mt('Unregistered Clicker')." ".$id."
    "; $result.="\n".''. "\n".&mt("Username").":  ". "\n".&mt("Domain").": ". &Apache::loncommon::select_dom_form($env{'course.'.$env{'request.course.id'}.'.domain'},'udom'.$id).' '. &Apache::loncommon::selectstudent_link('clickeranalysis','uname'.$id,'udom'.$id); $unknown_count++; } } $result.='
    '. &mt('Found [_1] registered and [_2] unregistered clickers.',$student_count,$unknown_count); if ($env{'form.gradingmechanism'} ne 'attendance') { if ($correct_count==0) { $errormsg.="Found no correct answers answers for grading!"; } elsif ($correct_count>1) { $result.='
    '.&mt("Found [_1] entries for grading!",$correct_count).''; } } if ($number<1) { $errormsg.="Found no questions."; } if ($errormsg) { $result.='
    '.&mt($errormsg).''; } else { $result.='
    '; } $result.='
    '."\n". '


    '."\n"; return $result.&show_grading_menu_form($symb); } sub iclicker_eval { my ($questiontitles,$responses)=@_; my $number=0; my $errormsg=''; foreach my $line (split(/[\n\r]/,$env{'form.upfile'})) { my %components=&Apache::loncommon::record_sep($line); my @entries=map {$components{$_}} (sort(keys(%components))); if ($entries[0] eq 'Question') { for (my $i=3;$i<$#entries;$i+=6) { $$questiontitles[$number]=$entries[$i]; $number++; } } if ($entries[0]=~/^\#/) { my $id=$entries[0]; my @idresponses; $id=~s/^[\#0]+//; for (my $i=0;$i<$number;$i++) { my $idx=3+$i*6; push(@idresponses,$entries[$idx]); } $$responses{$id}=join(',',@idresponses); } } return ($errormsg,$number); } sub interwrite_eval { my ($questiontitles,$responses)=@_; my $number=0; my $errormsg=''; my $skipline=1; my $questionnumber=0; my %idresponses=(); foreach my $line (split(/[\n\r]/,$env{'form.upfile'})) { my %components=&Apache::loncommon::record_sep($line); my @entries=map {$components{$_}} (sort(keys(%components))); if ($entries[1] eq 'Time') { $skipline=0; next; } if ($entries[1] eq 'Response') { $skipline=1; } next if $skipline; if ($entries[0]!=$questionnumber) { $questionnumber=$entries[0]; $$questiontitles[$number]=&mt('Question [_1]',$questionnumber); $number++; } my $id=$entries[4]; $id=~s/^[\#0]+//; $id=~s/^v\d*\://i; $id=~s/[\-\:]//g; $idresponses{$id}[$number]=$entries[6]; } foreach my $id (keys %idresponses) { $$responses{$id}=join(',',@{$idresponses{$id}}); $$responses{$id}=~s/^\s*\,//; } return ($errormsg,$number); } sub assign_clicker_grades { my ($r)=@_; my ($symb)=&get_symb($r); if (!$symb) {return '';} # See which part we are saving to my ($partlist,$handgrade,$responseType) = &response_type($symb); # FIXME: This should probably look for the first handgradeable part my $part=$$partlist[0]; # Start screen output my ($result) = &showResourceInfo($symb,$env{'form.probTitle'}); my $heading=&mt('Assigning grades based on clicker file'); $result.=(<
    $heading
    ENDHEADER # Get correct result # FIXME: Possibly need delimiter other than ":" my @correct=(); my $gradingmechanism=$env{'form.gradingmechanism'}; my $number=$env{'form.number'}; if ($gradingmechanism ne 'attendance') { foreach my $key (keys(%env)) { if ($key=~/^form\.correct\:/) { my @input=split(/\,/,$env{$key}); for (my $i=0;$i<=$#input;$i++) { if (($correct[$i]) && ($input[$i]) && ($correct[$i] ne $input[$i])) { $result.='
    '. &mt('More than one correct result given for question "[_1]": [_2] versus [_3].', $env{'form.question:'.$i},$correct[$i],$input[$i]).''; } elsif ($input[$i]) { $correct[$i]=$input[$i]; } } } } for (my $i=0;$i<$number;$i++) { if (!$correct[$i]) { $result.='
    '. &mt('No correct result given for question "[_1]"!', $env{'form.question:'.$i}).''; } } $result.='
    '.&mt("Correct answer: [_1]",join(', ',map { ($_?$_:'-') } @correct)); } # Start grading my $pcorrect=$env{'form.pcorrect'}; my $pincorrect=$env{'form.pincorrect'}; my $storecount=0; foreach my $key (keys(%env)) { my $user=''; if ($key=~/^form\.student\:(.*)$/) { $user=$1; } if ($key=~/^form\.unknown\:(.*)$/) { my $id=$1; if (($env{'form.uname'.$id}) && ($env{'form.udom'.$id})) { $user=$env{'form.uname'.$id}.':'.$env{'form.udom'.$id}; } elsif ($env{'form.multi'.$id}) { $user=$env{'form.multi'.$id}; } } if ($user) { my @answer=split(/\,/,$env{$key}); my $sum=0; for (my $i=0;$i<$number;$i++) { if ($answer[$i]) { if ($gradingmechanism eq 'attendance') { $sum+=$pcorrect; } else { if ($answer[$i] eq $correct[$i]) { $sum+=$pcorrect; } else { $sum+=$pincorrect; } } } } my $ave=$sum/(100*$number); # Store my ($username,$domain)=split(/\:/,$user); my %grades=(); $grades{"resource.$part.solved"}='correct_by_override'; $grades{"resource.$part.awarded"}=$ave; $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}"; my $returncode=&Apache::lonnet::cstore(\%grades,$symb, $env{'request.course.id'}, $domain,$username); if ($returncode ne 'ok') { $result.="
    Failed to save student $username:$domain. Message when trying to save was ($returncode)"; } else { $storecount++; } } } # We are done $result.='
    '.&mt('Successfully stored grades for [_1] student(s).',$storecount). '
    '."\n". '


    '."\n"; return $result.&show_grading_menu_form($symb); } sub handler { my $request=$_[0]; &reset_caches(); if ($env{'browser.mathml'}) { &Apache::loncommon::content_type($request,'text/xml'); } else { &Apache::loncommon::content_type($request,'text/html'); } $request->send_http_header; return '' if $request->header_only; &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'}); my $symb=&get_symb($request,1); 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)); } $request->print(&Apache::loncommon::start_page('Grading')); if ($symb eq '' && $command eq '') { if ($env{'user.adv'}) { if (($env{'form.codeone'}) && ($env{'form.codetwo'}) && ($env{'form.codethree'})) { my $token=$env{'form.codeone'}.'*'.$env{'form.codetwo'}.'*'. $env{'form.codethree'}; my ($tsymb,$tuname,$tudom,$tcrsid)= &Apache::lonnet::checkin($token); if ($tsymb) { my ($map,$id,$url)=&Apache::lonnet::decode_symb($tsymb); if (&Apache::lonnet::allowed('mgr',$tcrsid)) { $request->print(&Apache::lonnet::ssi_body('/res/'.$url, ('grade_username' => $tuname, 'grade_domain' => $tudom, 'grade_courseid' => $tcrsid, 'grade_symb' => $tsymb))); } else { $request->print('

    Not authorized: '.$token.'

    '); } } else { $request->print('

    Not a valid DocID: '.$token.'

    '); } } else { $request->print(&Apache::lonxml::tokeninputfield()); } } } else { &init_perm(); if ($command eq 'submission' && $perm{'vgr'}) { ($env{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0)); } elsif ($command eq 'pickStudentPage' && $perm{'vgr'}) { &pickStudentPage($request); } elsif ($command eq 'displayPage' && $perm{'vgr'}) { &displayPage($request); } elsif ($command eq 'gradeByPage' && $perm{'mgr'}) { &updateGradeByPage($request); } elsif ($command eq 'processGroup' && $perm{'vgr'}) { &processGroup($request); } elsif ($command eq 'gradingmenu' && $perm{'vgr'}) { $request->print(&grading_menu($request)); } elsif ($command eq 'submit_options' && $perm{'vgr'}) { $request->print(&submit_options($request)); } elsif ($command eq 'viewgrades' && $perm{'vgr'}) { $request->print(&viewgrades($request)); } elsif ($command eq 'handgrade' && $perm{'mgr'}) { $request->print(&processHandGrade($request)); } elsif ($command eq 'editgrades' && $perm{'mgr'}) { $request->print(&editgrades($request)); } elsif ($command eq 'verify' && $perm{'vgr'}) { $request->print(&verifyreceipt($request)); } elsif ($command eq 'processclicker' && $perm{'mgr'}) { $request->print(&process_clicker($request)); } elsif ($command eq 'processclickerfile' && $perm{'mgr'}) { $request->print(&process_clicker_file($request)); } elsif ($command eq 'assignclickergrades' && $perm{'mgr'}) { $request->print(&assign_clicker_grades($request)); } elsif ($command eq 'csvform' && $perm{'mgr'}) { $request->print(&upcsvScores_form($request)); } elsif ($command eq 'csvupload' && $perm{'mgr'}) { $request->print(&csvupload($request)); } elsif ($command eq 'csvuploadmap' && $perm{'mgr'} ) { $request->print(&csvuploadmap($request)); } elsif ($command eq 'csvuploadoptions' && $perm{'mgr'}) { if ($env{'form.associate'} ne 'Reverse Association') { $request->print(&csvuploadoptions($request)); } else { if ( $env{'form.upfile_associate'} ne 'reverse' ) { $env{'form.upfile_associate'} = 'reverse'; } else { $env{'form.upfile_associate'} = 'forward'; } $request->print(&csvuploadmap($request)); } } elsif ($command eq 'csvuploadassign' && $perm{'mgr'} ) { $request->print(&csvuploadassign($request)); } elsif ($command eq 'scantron_selectphase' && $perm{'mgr'}) { $request->print(&scantron_selectphase($request)); } elsif ($command eq 'scantron_warning' && $perm{'mgr'}) { $request->print(&scantron_do_warning($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'})|| &Apache::lonnet::allowed('usc',$env{'request.course.id'}))) { $request->print(&scantron_upload_scantron_data($request)); } elsif ($command eq 'scantronupload_save' && (&Apache::lonnet::allowed('usc',$env{'request.role.domain'})|| &Apache::lonnet::allowed('usc',$env{'request.course.id'}))) { $request->print(&scantron_upload_scantron_data_save($request)); } elsif ($command eq 'scantron_download' && &Apache::lonnet::allowed('usc',$env{'request.course.id'})) { $request->print(&scantron_download_scantron_data($request)); } elsif ($command) { $request->print("Access Denied ($command)"); } } $request->print(&Apache::loncommon::end_page()); &reset_caches(); return ''; } 1; __END__;