--- loncom/homework/grades.pm 2011/12/01 00:36:59 1.596.2.12 +++ loncom/homework/grades.pm 2013/06/28 22:54:50 1.596.2.12.2.16 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.596.2.12 2011/12/01 00:36:59 raeburn Exp $ +# $Id: grades.pm,v 1.596.2.12.2.16 2013/06/28 22:54:50 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -52,6 +52,7 @@ use POSIX qw(floor); my %perm=(); +my %old_essays=(); # These variables are used to recover from ssi errors @@ -124,13 +125,16 @@ sub getpartlist { # --- 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(&mt("Unable to handle ambiguous references: [_1].",$url)); - return (); - } + my $symb=$env{'form.symb'}; + unless ($symb) { + (my $url=$env{'form.url'}) =~ s-^http://($ENV{'SERVER_NAME'}|$ENV{'HTTP_HOST'})--; + $symb = &Apache::lonnet::symbread($url); + if ($symb eq '') { + if (!$silent) { + $request->print(&mt("Unable to handle ambiguous references: [_1].",$url)); + return (); + } + } } &Apache::lonenc::check_decrypt(\$symb); return ($symb); @@ -249,7 +253,7 @@ sub showResourceInfo { $result.=''.$display_part.'' .''.''.$resID.'' .''.&mt($responsetype).'' -# .''.&mt('Handgrade: [_1]',$handgrade).'' +# .''.&mt('Handgrade: [_1]',$handgrade).'' .&Apache::loncommon::end_data_table_row(); } } @@ -260,6 +264,7 @@ sub showResourceInfo { sub reset_caches { &reset_analyze_cache(); &reset_perm(); + &reset_old_essays(); } { @@ -272,7 +277,7 @@ sub reset_caches { } sub get_analyze { - my ($symb,$uname,$udom,$no_increment,$add_to_hash,$type,$trial,$rndseed)=@_; + my ($symb,$uname,$udom,$no_increment,$add_to_hash,$type,$trial,$rndseed,$bubbles_per_row)=@_; my $key = "$symb\0$uname\0$udom"; if ($type eq 'randomizetry') { if ($trial ne '') { @@ -306,6 +311,9 @@ sub reset_caches { 'grade_courseid' => $env{'request.course.id'}, 'grade_username' => $uname, 'grade_noincrement' => $no_increment); + if ($bubbles_per_row ne '') { + $form{'bubbles_per_row'} = $bubbles_per_row; + } if ($type eq 'randomizetry') { $form{'grade_questiontype'} = $type; if ($rndseed ne '') { @@ -346,7 +354,7 @@ sub reset_caches { } sub scantron_partids_tograde { - my ($resource,$cid,$uname,$udom,$check_for_randomlist) = @_; + my ($resource,$cid,$uname,$udom,$check_for_randomlist,$bubbles_per_row) = @_; my (%analysis,@parts); if (ref($resource)) { my $symb = $resource->symb(); @@ -354,7 +362,9 @@ sub reset_caches { if ($check_for_randomlist) { $add_to_form = { 'check_parts_withrandomlist' => 1,}; } - my $analyze = &get_analyze($symb,$uname,$udom,undef,$add_to_form); + my $analyze = + &get_analyze($symb,$uname,$udom,undef,$add_to_form, + undef,undef,undef,$bubbles_per_row); if (ref($analyze) eq 'HASH') { %analysis = %{$analyze}; } @@ -738,7 +748,11 @@ sub compute_points { # sub most_similar { - my ($uname,$udom,$uessay,$old_essays)=@_; + my ($uname,$udom,$symb,$uessay)=@_; + + unless ($symb) { return ''; } + + unless (ref($old_essays{$symb}) eq 'HASH') { return ''; } # ignore spaces and punctuation @@ -755,11 +769,11 @@ sub most_similar { my $scrsid=''; my $sessay=''; # go through all essays ... - foreach my $tkey (keys(%$old_essays)) { + foreach my $tkey (keys(%{$old_essays{$symb}})) { 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}; + my $tessay=$old_essays{$symb}{$tkey}; $tessay=~s/\W+/ /gs; # String similarity gives up if not even limit my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit); @@ -769,7 +783,7 @@ sub most_similar { $sname=$tname; $sdom=$tdom; $scrsid=$tcrsid; - $sessay=$old_essays->{$tkey}; + $sessay=$old_essays{$symb}{$tkey}; } } if ($limit>0.6) { @@ -1605,17 +1619,15 @@ INNERJS 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=700,height='+height); + pWin = window.open('', 'MessageCenter', 'resizable=yes,toolbar=no,location=no,scrollbars=yes,screenx='+xpos+',screeny='+ypos+',width=700,height='+height); pWin.focus(); pDoc = pWin.document; pDoc.$docopen; @@ -1825,7 +1837,6 @@ sub gradeBox { $line.=''."\n"; - #&mt('Part:[_1]Points:[_2]or[_3]',$display_part,$radio,$line); $result .= ''.$display_part.''.$radio.''.&mt('or').''.$line.''; $result.=&Apache::loncommon::end_data_table_row(); @@ -1912,14 +1923,27 @@ sub show_problem { $companswer=~s|||g; $companswer=~s|name="submit"|name="would_have_been_submit"|g; } + my $renderheading = &mt('View of the problem'); + my $answerheading = &mt('Correct answer'); + if (($uname ne $env{'user.name'}) || ($udom ne $env{'user.domain'})) { + my $stu_fullname = $env{'form.fullname'}; + if ($stu_fullname eq '') { + $stu_fullname = &Apache::loncommon::plainname($uname,$udom,'lastname'); + } + my $forwhom = &nameUserString(undef,$stu_fullname,$uname,$udom); + if ($forwhom ne '') { + $renderheading = &mt('View of the problem for[_1]',$forwhom); + $answerheading = &mt('Correct answer for[_1]',$forwhom); + } + } $rendered= '
' - .'

'.&mt('View of the problem').'

' + .'

'.$renderheading.'

' .$rendered .'
'; $companswer= '
' - .'

'.&mt('Correct answer').'

' + .'

'.$answerheading.'

' .$companswer .'
'; my $result; @@ -1990,7 +2014,7 @@ sub submission { $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); + my ($symb) = &get_symb($request); if ($symb eq '') { $request->print("Unable to handle ambiguous references:."); return ''; } if (!&canview($usec)) { @@ -2009,7 +2033,6 @@ sub submission { '" src="'.$request->dir_config('lonIconsURL'). '/check.gif" height="16" border="0" />'; - my %old_essays; # header info if ($counter == 0) { &sub_page_js($request); @@ -2125,7 +2148,7 @@ KEYWORDS 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); + &init_old_essays($symb,$apath,$adom,$aname); } } @@ -2262,7 +2285,7 @@ KEYWORDS } if($env{'form.checkPlag'}){ my ($oname,$odom,$ocrsid,$oessay,$osim)= - &most_similar($uname,$udom,$subval,\%old_essays); + &most_similar($uname,$udom,$symb,$subval); if ($osim) { $osim=int($osim*100.0); my %old_course_desc = @@ -2594,10 +2617,187 @@ sub keywords_highlight { return $string; } +# For Tasks provide a mechanism to display previous version for one specific student + +sub show_previous_task_version { + my ($request,$symb) = @_; + if ($symb eq '') { + $request->print("Unable to handle ambiguous references."); + + return ''; + } + my ($uname,$udom) = ($env{'form.student'},$env{'form.userdom'}); + my $usec = &Apache::lonnet::getsection($udom,$uname,$env{'request.course.id'}); + if (!&canview($usec)) { + $request->print('Unable to view previous version for requested student.('. + $uname.':'.$udom.' in section '.$usec.' in course id '. + $env{'request.course.id'}.')'); + return; + } + my $mode = 'both'; + my $isTask = ($symb =~/\.task$/); + if ($isTask) { + if ($env{'form.previousversion'} =~ /^\d+$/) { + if ($env{'form.fullname'} eq '') { + $env{'form.fullname'} = + &Apache::loncommon::plainname($uname,$udom,'lastname'); + } + my $probtitle=&Apache::lonnet::gettitle($symb); + $request->print("\n\n". + '
'. + '

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

'."\n"); + &Apache::lonxml::clear_problem_counter(); + $request->print(&show_problem($request,$symb,$uname,$udom,1,1,$mode, + {'previousversion' => $env{'form.previousversion'} })); + $request->print("\n
"); + } + } + return; +} + +sub choose_task_version_form { + my ($symb,$uname,$udom,$nomenu) = @_; + my $isTask = ($symb =~/\.task$/); + my ($current,$version,$result,$js,$displayed,$rowtitle); + if ($isTask) { + my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'}, + $udom,$uname); + if (($record{'resource.0.version'} eq '') || + ($record{'resource.0.version'} < 2)) { + return ($record{'resource.0.version'}, + $record{'resource.0.version'},$result,$js); + } else { + $current = $record{'resource.0.version'}; + } + if ($env{'form.previousversion'}) { + $displayed = $env{'form.previousversion'}; + $rowtitle = &mt('Choose another version:') + } else { + $displayed = $current; + $rowtitle = &mt('Show earlier version:'); + } + $result = '
'; + my $list; + my $numversions = 0; + for (my $i=1; $i<=$record{'resource.0.version'}; $i++) { + if ($i == $current) { + if (!$env{'form.previousversion'} || $nomenu) { + next; + } else { + $list .= ''."\n"; + $numversions ++; + } + } elsif (defined($record{'resource.'.$i.'.0.status'})) { + unless ($i == $env{'form.previousversion'}) { + $numversions ++; + } + $list .= ''."\n"; + } + } + if ($numversions) { + $symb = &HTML::Entities::encode($symb,'<>"&'); + $result .= + '
'. + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_row(). + ''.$rowtitle.''. + ''. + &Apache::loncommon::end_data_table_row(); + unless ($nomenu) { + $result .= &Apache::loncommon::start_data_table_row(). + ''.&mt('Open in new window').''. + ''. + ''. + ''. + ''. + &Apache::loncommon::end_data_table_row(); + } + $result .= + &Apache::loncommon::start_data_table_row(). + ' '. + ''. + ''. + ''. + &Apache::loncommon::end_data_table_row(). + &Apache::loncommon::end_data_table(). + '
'; + $js = &previous_display_javascript($nomenu,$current); + } elsif ($displayed && $nomenu) { + $result .= ''.&mt('Close window').''; + } else { + $result .= &mt('No previous versions to show for this student'); + } + $result .= '
'; + } + return ($current,$displayed,$result,$js); +} + +sub previous_display_javascript { + my ($nomenu,$current) = @_; + my $js = <<"JSONE"; + +ENDJS + +} + #--- Called from submission routine sub processHandGrade { my ($request) = shift; - my $symb = &get_symb($request); + my ($symb) = &get_symb($request); my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); my $button = $env{'form.gradeOpt'}; my $ngrade = $env{'form.NCT'}; @@ -3002,9 +3202,11 @@ sub handback_files { &file_name_version_ext($answer_file); my ($portfolio_path) = ($directory =~ /^.+$stuname\/portfolio(.*)/); my $getpropath = 1; - my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$portfolio_path,$domain,$stuname,$getpropath); - my $version = &get_next_version($answer_name, $answer_ext, \@dir_list); - # fix file name + my ($dir_list,$listerror) = + &Apache::lonnet::dirlist($portfolio_root.$portfolio_path, + $domain,$stuname,$getpropath); + my $version = &get_next_version($answer_name,$answer_ext,$dir_list); + # fix filename my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/); my $result=&Apache::lonnet::finishuserfileupload($stuname,$domain, $newflg.'_'.$part_resp.'_returndoc'.$counter, @@ -3024,7 +3226,7 @@ sub handback_files { $file_msg.=''.$save_file_name."
"; } - $request->print('
'.&mt('[_1] will be the uploaded file name [_2]',''.$fname.'',''.$env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$counter}.'')); + $request->print('
'.&mt('[_1] will be the uploaded filename [_2]',''.$fname.'',''.$env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$counter}.'')); } } } @@ -3163,9 +3365,11 @@ sub version_portfiles { my ($directory,$answer_file) =($file =~ /^(.*?)([^\/]*)$/); my ($answer_name,$answer_ver,$answer_ext) = &file_name_version_ext($answer_file); - my $getpropath = 1; - my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$directory,$domain,$stu_name,$getpropath); - my $version = &get_next_version($answer_name, $answer_ext, \@dir_list); + my $getpropath = 1; + my ($dir_list,$listerror) = + &Apache::lonnet::dirlist($portfolio_root.$directory,$domain, + $stu_name,$getpropath); + 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); @@ -3184,21 +3388,24 @@ sub version_portfiles { 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 (ref($dir_list) eq 'ARRAY') { + 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; + # a versioned file is found so save it for later + if ($file_version > $version) { + $version = $file_version; + } } } } - } + } $version ++; return($version); } @@ -3658,7 +3865,7 @@ sub viewstudentgrade { sub editgrades { my ($request) = @_; - my $symb=&get_symb($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"; @@ -4603,8 +4810,8 @@ sub displayPage { &Apache::loncommon::start_data_table_row(). ''.$prob. (scalar(@{$parts}) == 1 ? '' - : '
('.&mt('[_1]parts)', - scalar(@{$parts}).' ') + : '
('.&mt('[_1]parts', + scalar(@{$parts}).' ').')' ). ''; $studentTable.=''; @@ -4686,6 +4893,7 @@ sub displaySubByDates { &Apache::loncommon::start_data_table_header_row(). ''.&mt('Date/Time').''. ($isCODE?''.&mt('CODE').'':''). + ($isTask?''.&mt('Version').'':''). ''.&mt('Submission').''. ''.&mt('Status').''. &Apache::loncommon::end_data_table_header_row(); @@ -4706,7 +4914,9 @@ sub displaySubByDates { if (exists($$record{$version.':resource.0.version'})) { $interaction = $$record{$version.':resource.0.version'}; } - + if ($isTask && $env{'form.previousversion'}) { + next unless ($interaction == $env{'form.previousversion'}); + } my $where = ($isTask ? "$version:resource.$interaction" : "$version:resource"); $studentTable.=&Apache::loncommon::start_data_table_row(). @@ -4714,6 +4924,9 @@ sub displaySubByDates { if ($isCODE) { $studentTable.=''.$record->{$version.':resource.CODE'}.''; } + if ($isTask) { + $studentTable.=''.$interaction.''; + } my @versionKeys = split(/\:/,$$record{$version.':keys'}); my @displaySub = (); foreach my $partid (@{$parts}) { @@ -4733,7 +4946,7 @@ sub displaySubByDates { my ($responseId)= ($isTask ? ($matchKey=~ /^resource\.(.*?)\.\Q$partid\E\.award$/) : ($matchKey=~ /^resource\.\Q$partid\E\.(.*?)\.submission$/)); - $displaySub[0].='' .' ' .'('.&mt('Response ID: [_1]',$responseId).')' @@ -5095,6 +5308,11 @@ my %subdivided_bubble_lines; # no. my %responsetype_per_response; # responsetype for each response +my %masterseq_id_responsenum; # src_id (e.g., 12.3_0.11 etc.) for each + # numbered response. Needed when randomorder + # or randompick are in use. Key is ID, value + # is response number. + # Save and restore the bubble lines array to the form env. @@ -5108,12 +5326,17 @@ sub save_bubble_lines { $env{"form.scantron.responsetype.$line"} = $responsetype_per_response{$line}; } + foreach my $resid (keys(%masterseq_id_responsenum)) { + my $line = $masterseq_id_responsenum{$resid}; + $env{"form.scantron.residpart.$line"} = $resid; + } } sub restore_bubble_lines { my $line = 0; %bubble_lines_per_response = (); + %masterseq_id_responsenum = (); while ($env{"form.scantron.bubblelines.$line"}) { my $value = $env{"form.scantron.bubblelines.$line"}; $bubble_lines_per_response{$line} = $value; @@ -5123,28 +5346,12 @@ sub restore_bubble_lines { $env{"form.scantron.sub_bubblelines.$line"}; $responsetype_per_response{$line} = $env{"form.scantron.responsetype.$line"}; + my $id = $env{"form.scantron.residpart.$line"}; + $masterseq_id_responsenum{$id} = $line; $line++; } } -# Given the parsed scanline, get the response for -# 'answer' number n: - -sub get_response_bubbles { - my ($parsed_line, $response) = @_; - - my $bubble_line = $first_bubble_line{$response-1} +1; - my $bubble_lines= $bubble_lines_per_response{$response-1}; - - my $selected = ""; - - for (my $bline = 0; $bline < $bubble_lines; $bline++) { - $selected .= $$parsed_line{"scantron.$bubble_line.answer"}.":"; - $bubble_line++; - } - return $selected; -} - =pod =item scantron_filenames @@ -5157,14 +5364,16 @@ sub scantron_filenames { my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; my $getpropath = 1; - my @files=&Apache::lonnet::dirlist('userfiles',$cdom,$cname, - $getpropath); + my ($dirlist,$listerror) = &Apache::lonnet::dirlist('userfiles',$cdom, + $cname,$getpropath); my @possiblenames; - foreach my $filename (sort(@files)) { - ($filename)=split(/&/,$filename); - if ($filename!~/^scantron_orig_/) { next ; } - $filename=~s/^scantron_orig_//; - push(@possiblenames,$filename); + if (ref($dirlist) eq 'ARRAY') { + foreach my $filename (sort(@{$dirlist})) { + ($filename)=split(/&/,$filename); + if ($filename!~/^scantron_orig_/) { next ; } + $filename=~s/^scantron_orig_//; + push(@possiblenames,$filename); + } } return @possiblenames; } @@ -5562,6 +5771,8 @@ sub scantron_selectphase { LastName - column that the last name starts in LastNameLength - number of columns that the last name spans + BubblesPerRow - number of bubbles available in each row used to + bubble an answer. (If not specified, 10 assumed). =cut @@ -5592,6 +5803,7 @@ sub get_scantron_config { $config{'FirstNamelength'}=$config[14]; $config{'LastName'}=$config[15]; $config{'LastNamelength'}=$config[16]; + $config{'BubblesPerRow'}=$config[17]; last; } return %config; @@ -5803,6 +6015,27 @@ sub digits_to_letters { (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. + randomorder - True if randomorder in use + randompick - True if randompick in use + sequence - Exam folder URL + master_seq - Ref to array containing symbs in exam folder + symb_to_resource - Ref to hash of symbs for resources in exam folder + (corresponding values are resource objects) + partids_by_symb - Ref to hash of symb -> array ref of partIDs + orderedforcode - Ref to hash of arrays. keys are CODEs and values + are refs to an array of resource objects, ordered + according to order used for CODE, when randomorder + and or randompick are in use. + respnumlookup - Ref to hash mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Sequence" (as seen by Course Coordinator). + startline - Ref to hash where key is question number (0 is first) + and value is number of first bubble line for current + student or code-based randompick and/or randomorder. + totalref - Ref of scalar used to score total number of bubble + lines needed for responses in a scan line (used when + randompick in use. + Returns: Hash containing the result of parsing the scanline @@ -5847,12 +6080,12 @@ sub digits_to_letters { =cut sub scantron_parse_scanline { - my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_; + my ($line,$whichline,$scantron_config,$scan_data,$just_header,$idmap, + $randomorder,$randompick,$sequence,$master_seq,$symb_to_resource, + $partids_by_symb,$orderedforcode,$respnumlookup,$startline,$totalref)=@_; my %record; - my $lastpos = $env{'form.scantron_maxbubble'}*$$scantron_config{'Qlength'}; - my $questions=substr($line,$$scantron_config{'Qstart'}-1,$lastpos); # Answers - my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # earlier stuff + my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # stuff before answers if (!($$scantron_config{'CODElocation'} eq 0 || $$scantron_config{'CODElocation'} eq 'none')) { if ($$scantron_config{'CODElocation'} < 0 || @@ -5888,10 +6121,29 @@ sub scantron_parse_scanline { my $questnum=0; my $ansnum =1; # Multiple 'answer lines'/question. + my $lastpos = $env{'form.scantron_maxbubble'}*$$scantron_config{'Qlength'}; + if ($randompick || $randomorder) { + my $total = &get_respnum_lookups($sequence,$scan_data,$idmap,$line,\%record, + $master_seq,$symb_to_resource, + $partids_by_symb,$orderedforcode, + $respnumlookup,$startline); + if ($total) { + $lastpos = $total*$$scantron_config{'Qlength'}; + } + if (ref($totalref)) { + $$totalref = $total; + } + } + my $questions=substr($line,$$scantron_config{'Qstart'}-1,$lastpos); # Answers chomp($questions); # Get rid of any trailing \n. $questions =~ s/\r$//; # Get rid of trailing \r too (MAC or Win uploads). while (length($questions)) { - my $answers_needed = $bubble_lines_per_response{$questnum}; + my $answers_needed; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $answers_needed = $bubble_lines_per_response{$respnumlookup->{$questnum}}; + } else { + $answers_needed = $bubble_lines_per_response{$questnum}; + } my $answer_length = ($$scantron_config{'Qlength'} * $answers_needed) || 1; $questnum++; @@ -5900,11 +6152,16 @@ sub scantron_parse_scanline { $questions = substr($questions,$answer_length); if (length($currentquest) < $answer_length) { next; } - if ($subdivided_bubble_lines{$questnum-1} =~ /,/) { + my $subdivided; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $subdivided = $subdivided_bubble_lines{$respnumlookup->{$questnum-1}}; + } else { + $subdivided = $subdivided_bubble_lines{$questnum-1}; + } + if ($subdivided =~ /,/) { my $subquestnum = 1; my $subquestions = $currentquest; - my @subanswers_needed = - split(/,/,$subdivided_bubble_lines{$questnum-1}); + my @subanswers_needed = split(/,/,$subdivided); foreach my $subans (@subanswers_needed) { my $subans_length = ($$scantron_config{'Qlength'} * $subans) || 1; @@ -5915,10 +6172,13 @@ sub scantron_parse_scanline { ($$scantron_config{'Qon'} eq 'number')) { $ansnum = &scantron_validator_lettnum($ansnum, $questnum,$quest_id,$subans,$currsubquest,$whichline, - \@alphabet,\%record,$scantron_config,$scan_data); + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); } else { $ansnum = &scantron_validator_positional($ansnum, - $questnum,$quest_id,$subans,$currsubquest,$whichline, \@alphabet,\%record,$scantron_config,$scan_data); + $questnum,$quest_id,$subans,$currsubquest,$whichline, + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); } $subquestnum ++; } @@ -5927,11 +6187,13 @@ sub scantron_parse_scanline { ($$scantron_config{'Qon'} eq 'number')) { $ansnum = &scantron_validator_lettnum($ansnum,$questnum, $quest_id,$answers_needed,$currentquest,$whichline, - \@alphabet,\%record,$scantron_config,$scan_data); + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); } else { $ansnum = &scantron_validator_positional($ansnum,$questnum, $quest_id,$answers_needed,$currentquest,$whichline, - \@alphabet,\%record,$scantron_config,$scan_data); + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); } } } @@ -5939,9 +6201,66 @@ sub scantron_parse_scanline { return \%record; } +sub get_master_seq { + my ($resources,$master_seq,$symb_to_resource) = @_; + return unless ((ref($resources) eq 'ARRAY') && (ref($master_seq) eq 'ARRAY') && + (ref($symb_to_resource) eq 'HASH')); + my $resource_error; + foreach my $resource (@{$resources}) { + my $ressymb; + if (ref($resource)) { + $ressymb = $resource->symb(); + push(@{$master_seq},$ressymb); + $symb_to_resource->{$ressymb} = $resource; + } else { + $resource_error = 1; + last; + } + } + return $resource_error; +} + +sub get_respnum_lookups { + my ($sequence,$scan_data,$idmap,$line,$record,$master_seq,$symb_to_resource, + $partids_by_symb,$orderedforcode,$respnumlookup,$startline) = @_; + return unless ((ref($record) eq 'HASH') && (ref($master_seq) eq 'ARRAY') && + (ref($symb_to_resource) eq 'HASH') && (ref($partids_by_symb) eq 'HASH') && + (ref($orderedforcode) eq 'HASH') && (ref($respnumlookup) eq 'HASH') && + (ref($startline) eq 'HASH')); + my ($user,$scancode); + if ((exists($record->{'scantron.CODE'})) && + (&Apache::lonnet::validCODE($record->{'scantron.CODE'}))) { + $scancode = $record->{'scantron.CODE'}; + } else { + $user = &scantron_find_student($record,$scan_data,$idmap,$line); + } + my @mapresources = + &users_order($user,$scancode,$sequence,$master_seq,$symb_to_resource, + $orderedforcode); + my $total = 0; + my $count = 0; + foreach my $resource (@mapresources) { + my $id = $resource->id(); + my $symb = $resource->symb(); + if (ref($partids_by_symb->{$symb}) eq 'ARRAY') { + foreach my $partid (@{$partids_by_symb->{$symb}}) { + my $respnum = $masterseq_id_responsenum{$id.'_'.$partid}; + if ($respnum ne '') { + $respnumlookup->{$count} = $respnum; + $startline->{$count} = $total; + $total += $bubble_lines_per_response{$respnum}; + $count ++; + } + } + } + } + return $total; +} + sub scantron_validator_lettnum { my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,$whichline, - $alphabet,$record,$scantron_config,$scan_data) = @_; + $alphabet,$record,$scantron_config,$scan_data,$randomorder, + $randompick,$respnumlookup) = @_; # Qon 'letter' implies for each slot in currquest we have: # ? or * for doubles, a letter in A-Z for a bubble, and @@ -5960,19 +6279,23 @@ sub scantron_validator_lettnum { $matchon = '\d'; } my $occurrences = 0; - if (($responsetype_per_response{$questnum-1} eq 'essayresponse') || - ($responsetype_per_response{$questnum-1} eq 'formularesponse') || - ($responsetype_per_response{$questnum-1} eq 'stringresponse') || - ($responsetype_per_response{$questnum-1} eq 'imageresponse') || - ($responsetype_per_response{$questnum-1} eq 'reactionresponse') || - ($responsetype_per_response{$questnum-1} eq 'organicresponse')) { + my $responsenum = $questnum-1; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$questnum-1} + } + if (($responsetype_per_response{$responsenum} eq 'essayresponse') || + ($responsetype_per_response{$responsenum} eq 'formularesponse') || + ($responsetype_per_response{$responsenum} eq 'stringresponse') || + ($responsetype_per_response{$responsenum} eq 'imageresponse') || + ($responsetype_per_response{$responsenum} eq 'reactionresponse') || + ($responsetype_per_response{$responsenum} eq 'organicresponse')) { my @singlelines = split('',$currquest); foreach my $entry (@singlelines) { $occurrences = &occurence_count($entry,$matchon); if ($occurrences > 1) { last; } - } + } } else { $occurrences = &occurence_count($currquest,$matchon); } @@ -6020,7 +6343,8 @@ sub scantron_validator_lettnum { sub scantron_validator_positional { my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest, - $whichline,$alphabet,$record,$scantron_config,$scan_data) = @_; + $whichline,$alphabet,$record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup) = @_; # Otherwise there's a positional notation; # each bubble line requires Qlength items, and there are filled in @@ -6062,12 +6386,16 @@ sub scantron_validator_positional { # record all the bubbles filled in as well as the # fact this response consists of multiple bubbles. # - if (($responsetype_per_response{$questnum-1} eq 'essayresponse') || - ($responsetype_per_response{$questnum-1} eq 'formularesponse') || - ($responsetype_per_response{$questnum-1} eq 'stringresponse') || - ($responsetype_per_response{$questnum-1} eq 'imageresponse') || - ($responsetype_per_response{$questnum-1} eq 'reactionresponse') || - ($responsetype_per_response{$questnum-1} eq 'organicresponse')) { + my $responsenum = $questnum-1; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$questnum-1} + } + if (($responsetype_per_response{$responsenum} eq 'essayresponse') || + ($responsetype_per_response{$responsenum} eq 'formularesponse') || + ($responsetype_per_response{$responsenum} eq 'stringresponse') || + ($responsetype_per_response{$responsenum} eq 'imageresponse') || + ($responsetype_per_response{$responsenum} eq 'reactionresponse') || + ($responsetype_per_response{$responsenum} eq 'organicresponse')) { my $doubleerror = 0; while (($currquest >= $$scantron_config{'Qlength'}) && (!$doubleerror)) { @@ -6376,6 +6704,12 @@ sub scantron_warning_screen { ''.&mt('List of CODES to validate against:').''. $env{'form.scantron_CODElist'}.''; } + my $lastbubblepoints; + if ($env{'form.scantron_lastbubblepoints'} ne '') { + $lastbubblepoints = + ''.&mt('Hand-graded items: points from last bubble in row').''. + $env{'form.scantron_lastbubblepoints'}.''; + } return ('

@@ -6384,11 +6718,11 @@ sub scantron_warning_screen { -'.$CODElist.' +'.$CODElist.$lastbubblepoints.'
'.&mt('Sequence to be Graded:').''.$title.'
'.&mt('Data File that will be used:').''.$env{'form.scantron_selectfile'}.'

-

'.&mt('If this information is correct, please click on \'[_1]\'.',&mt($button_text)).'

-

'.&mt('If something is incorrect, please click the \'Grading Menu\' button to start over.').'

+

'.&mt("If this information is correct, please click on '[_1]'.",&mt($button_text)).'

+

'.&mt("If something is incorrect, please click the 'Grading Menu' button to start over.").'


'); @@ -6424,8 +6758,9 @@ sub scantron_do_warning { } } else { my $warning=&scantron_warning_screen('Grading: Validate Records'); + my $bubbledbyhand=&hand_bubble_option(); $r->print(' -'.$warning.' +'.$warning.$bubbledbyhand.' '); @@ -6467,9 +6802,11 @@ SCANTRONFORM ''."\n"; $chunk .= ''."\n"; + $chunk .= + ''."\n"; $result .= $chunk; $line++; - } + } return $result; } @@ -6515,12 +6852,16 @@ sub scantron_validate_file { #get the student pick code ready $r->print(&Apache::loncommon::studentbrowser_javascript()); my $nav_error; - my $max_bubble=&scantron_get_maxbubble(\$nav_error); + my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); + my $max_bubble=&scantron_get_maxbubble(\$nav_error,\%scantron_config); if ($nav_error) { $r->print(&navmap_errormsg()); return ''; } my $result=&scantron_form_start($max_bubble).$default_form_data; + if ($env{'form.scantron_lastbubblepoints'} ne '') { + $result .= ''; + } $r->print($result); my @validate_phases=( 'sequence', @@ -6538,6 +6879,7 @@ sub scantron_validate_file { while (!$stop && $currentphase < scalar(@validate_phases)) { $r->print(&mt('Validating '.$validate_phases[$currentphase]).'
'); $r->rflush(); + my $which="scantron_validate_".$validate_phases[$currentphase]; { no strict 'refs'; @@ -6946,7 +7288,12 @@ sub scantron_validate_sequence { 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.')."

"); + $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); } } @@ -6968,7 +7315,7 @@ sub scantron_validate_ID { my ($scanlines,$scan_data)=&scantron_getfile(); my $nav_error; - &scantron_get_maxbubble(\$nav_error); # parse needs the bubble_lines.. array. + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # parse needs the bubble_lines.. array. if ($nav_error) { $r->print(&navmap_errormsg()); return(1,$currentphase); @@ -7027,7 +7374,8 @@ sub scantron_validate_ID { sub scantron_get_correction { - my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_; + my ($r,$i,$scan_record,$scan_config,$line,$error,$arg, + $randomorder,$randompick,$respnumlookup,$startline)=@_; #FIXME in the case of a duplicated ID the previous line, probably need #to show both the current line and the previous one and allow skipping #the previous one or the current one @@ -7077,7 +7425,7 @@ sub scantron_get_correction { $r->print(&Apache::loncommon::selectstudent_link('scantronupload', 'scantron_username','scantron_domain')); $r->print(": "); - $r->print("\n@". + $r->print("\n:\n". &Apache::loncommon::select_dom_form($env{'request.role.domain'},'scantron_domain')); $r->print(''); @@ -7163,7 +7511,8 @@ ENDSCRIPT # The form field scantron_questions is acutally a list of line numbers. # represented by this form so: - my $line_list = &questions_to_line_list($arg); + my $line_list = &questions_to_line_list($arg,$randomorder,$randompick, + $respnumlookup,$startline); $r->print(''); @@ -7171,7 +7520,9 @@ ENDSCRIPT $r->print("

".&mt("Please indicate which bubble should be used for grading")."

"); foreach my $question (@{$arg}) { my @linenums = &prompt_for_corrections($r,$question,$scan_config, - $scan_record, $error); + $scan_record, $error, + $randomorder,$randompick, + $respnumlookup,$startline); push(@lines_to_correct,@linenums); } $r->print(&verify_bubbles_checked(@lines_to_correct)); @@ -7185,13 +7536,16 @@ ENDSCRIPT # a list of question numbers. Therefore: # - my $line_list = &questions_to_line_list($arg); + my $line_list = &questions_to_line_list($arg,$randomorder,$randompick, + $respnumlookup,$startline); $r->print(''); foreach my $question (@{$arg}) { my @linenums = &prompt_for_corrections($r,$question,$scan_config, - $scan_record, $error); + $scan_record, $error, + $randomorder,$randompick, + $respnumlookup,$startline); push(@lines_to_correct,@linenums); } $r->print(&verify_bubbles_checked(@lines_to_correct)); @@ -7244,12 +7598,20 @@ used to fill in the scantron_questions f Arguments: questions - Reference to an array of questions. + randomorder - True if randomorder in use. + randompick - True if randompick in use. + respnumlookup - Reference to HASH mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Seqence" (as seen by Course Coordinator). + startline - Reference to hash where key is question number (0 is first) + and key is number of first bubble line for current student + or code-based randompick and/or randomorder. =cut sub questions_to_line_list { - my ($questions) = @_; + my ($questions,$randomorder,$randompick,$respnumlookup,$startline) = @_; my @lines; foreach my $item (@{$questions}) { @@ -7258,7 +7620,15 @@ sub questions_to_line_list { if ($item =~ /^(\d+)\.(\d+)$/) { $question = $1; my $subquestion = $2; - $first = $first_bubble_line{$question-1} + 1; + my $responsenum = $question-1; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1} + 1; + } + } else { + $first = $first_bubble_line{$responsenum} + 1; + } my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); my $subcount = 1; while ($subcount<$subquestion) { @@ -7314,15 +7684,26 @@ for multi and missing bubble cases). =cut sub prompt_for_corrections { - my ($r, $question, $scan_config, $scan_record, $error) = @_; + my ($r, $question, $scan_config, $scan_record, $error, $randomorder, + $randompick, $respnumlookup, $startline) = @_; my ($current_line,$lines); my @linenums; my $questionnum = $question; + my ($first,$responsenum); if ($question =~ /^(\d+)\.(\d+)$/) { $question = $1; - $current_line = $first_bubble_line{$question-1} + 1 ; my $subquestion = $2; - my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1}; + } + } else { + $responsenum = $question-1; + $first = $first_bubble_line{$responsenum} + 1; + } + $current_line = $first + 1 ; + my @subans = split(/,/,$subdivided_bubble_lines{$responsenum}); my $subcount = 1; while ($subcount<$subquestion) { $current_line += $subans[$subcount-1]; @@ -7330,25 +7711,34 @@ sub prompt_for_corrections { } $lines = $subans[$subquestion-1]; } else { - $current_line = $first_bubble_line{$question-1} + 1 ; - $lines = $bubble_lines_per_response{$question-1}; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1}; + } + } else { + $responsenum = $question-1; + $first = $first_bubble_line{$responsenum}; + } + $current_line = $first + 1; + $lines = $bubble_lines_per_response{$responsenum}; } if ($lines > 1) { $r->print(&mt('The group of bubble lines below responds to a single question.').'
'); - if (($responsetype_per_response{$question-1} eq 'essayresponse') || - ($responsetype_per_response{$question-1} eq 'formularesponse') || - ($responsetype_per_response{$question-1} eq 'stringresponse') || - ($responsetype_per_response{$question-1} eq 'imageresponse') || - ($responsetype_per_response{$question-1} eq 'reactionresponse') || - ($responsetype_per_response{$question-1} eq 'organicresponse')) { - $r->print(&mt("Although this particular question type requires handgrading, the instructions for this question in the exam directed students to leave [quant,_1,line] blank on their bubblesheets.",$lines).'

'.&mt('A non-zero score can be assigned to the student during bubblesheet grading by selecting a bubble in at least one line.').'
'.&mt('The score for this question will be a sum of the numeric values for the selected bubbles from each line, where A=1 point, B=2 points etc.').'
'.&mt("To assign a score of zero for this question, mark all lines as 'No bubble'.").'

'); + if (($responsetype_per_response{$responsenum} eq 'essayresponse') || + ($responsetype_per_response{$responsenum} eq 'formularesponse') || + ($responsetype_per_response{$responsenum} eq 'stringresponse') || + ($responsetype_per_response{$responsenum} eq 'imageresponse') || + ($responsetype_per_response{$responsenum} eq 'reactionresponse') || + ($responsetype_per_response{$responsenum} eq 'organicresponse')) { + $r->print(&mt("Although this particular question type requires handgrading, the instructions for this question in the bubblesheet exam directed students to leave [quant,_1,line] blank on their bubblesheets.",$lines).'

'.&mt('A non-zero score can be assigned to the student during bubblesheet grading by selecting a bubble in at least one line.').'
'.&mt('The score for this question will be a sum of the numeric values for the selected bubbles from each line, where A=1 point, B=2 points etc.').'
'.&mt("To assign a score of zero for this question, mark all lines as 'No bubble'.").'

'); } else { $r->print(&mt("Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")."
"); } } for (my $i =0; $i < $lines; $i++) { my $selected = $$scan_record{"scantron.$current_line.answer"}; - &scantron_bubble_selector($r,$scan_config,$current_line, + &scantron_bubble_selector($r,$scan_config,$current_line, $questionnum,$error,split('', $selected)); push(@linenums,$current_line); $current_line++; @@ -7381,7 +7771,19 @@ sub scantron_bubble_selector { my $max=$$scan_config{'Qlength'}; my $scmode=$$scan_config{'Qon'}; - if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; } + if ($scmode eq 'number' || $scmode eq 'letter') { + if (($$scan_config{'BubblesPerRow'} =~ /^\d+$/) && + ($$scan_config{'BubblesPerRow'} > 0)) { + $max=$$scan_config{'BubblesPerRow'}; + if (($scmode eq 'number') && ($max > 10)) { + $max = 10; + } elsif (($scmode eq 'letter') && $max > 26) { + $max = 26; + } + } else { + $max = 10; + } + } my @alphabet=('A'..'Z'); $r->print(&Apache::loncommon::start_data_table(). @@ -7536,7 +7938,7 @@ sub scantron_validate_CODE { my %allcodes=&get_codes(); my $nav_error; - &scantron_get_maxbubble(\$nav_error); # parse needs the lines per response array. + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # parse needs the lines per response array. if ($nav_error) { $r->print(&navmap_errormsg()); return(1,$currentphase); @@ -7590,12 +7992,43 @@ sub scantron_validate_doublebubble { #get student info my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); + my (undef,undef,$sequence)= + &Apache::lonnet::decode_symb($env{'form.selectpage'}); #get scantron line setup my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); + + my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb,%orderedforcode,%respnumlookup,%startline); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); + my $nav_error; - &scantron_get_maxbubble(\$nav_error); # parse needs the bubble line array. + if (ref($map)) { + $randomorder = $map->randomorder(); + $randompick = $map->randompick(); + if ($randomorder || $randompick) { + $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); + } + } else { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # parse needs the bubble line array. if ($nav_error) { $r->print(&navmap_errormsg()); return(1,$currentphase); @@ -7605,11 +8038,15 @@ sub scantron_validate_doublebubble { 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); + $scan_data,undef,\%idmap,$randomorder, + $randompick,$sequence,\@master_seq, + \%symb_to_resource,\%grader_partids_by_symb, + \%orderedforcode,\%respnumlookup,\%startline); if (!defined($$scan_record{'scantron.doubleerror'})) { next; } &scantron_get_correction($r,$i,$scan_record,\%scantron_config,$line, 'doublebubble', - $$scan_record{'scantron.doubleerror'}); + $$scan_record{'scantron.doubleerror'}, + $randomorder,$randompick,\%respnumlookup,\%startline); return (1,$currentphase); } return (0,$currentphase+1); @@ -7617,7 +8054,7 @@ sub scantron_validate_doublebubble { sub scantron_get_maxbubble { - my ($nav_error) = @_; + my ($nav_error,$scantron_config) = @_; if (defined($env{'form.scantron_maxbubble'}) && $env{'form.scantron_maxbubble'}) { &restore_bubble_lines(); @@ -7636,6 +8073,7 @@ sub scantron_get_maxbubble { } my $map=$navmap->getResourceByUrl($sequence); my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my $bubbles_per_row = &bubblesheet_bubbles_per_row($scantron_config); &Apache::lonxml::clear_problem_counter(); @@ -7647,11 +8085,14 @@ sub scantron_get_maxbubble { %first_bubble_line = (); %subdivided_bubble_lines = (); %responsetype_per_response = (); + %masterseq_id_responsenum = (); my $response_number = 0; my $bubble_line = 0; foreach my $resource (@resources) { - my ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname,$udom); + my $resid = $resource->id(); + my ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname, + $udom,$bubbles_per_row); if ((ref($analysis) eq 'HASH') && (ref($parts) eq 'ARRAY')) { foreach my $part_id (@{$parts}) { my $lines; @@ -7680,9 +8121,10 @@ sub scantron_get_maxbubble { if (ref($analysis->{$part_id.'.shown'}) eq 'ARRAY') { $numshown = scalar(@{$analysis->{$part_id.'.shown'}}); } - my $bubbles_per_line = 10; - my $inner_bubble_lines = int($numbub/$bubbles_per_line); - if (($numbub % $bubbles_per_line) != 0) { + my $bubbles_per_row = + &bubblesheet_bubbles_per_row($scantron_config); + my $inner_bubble_lines = int($numbub/$bubbles_per_row); + if (($numbub % $bubbles_per_row) != 0) { $inner_bubble_lines++; } for (my $i=0; $i<$numshown; $i++) { @@ -7693,12 +8135,13 @@ sub scantron_get_maxbubble { $lines = $numshown * $inner_bubble_lines; } else { $lines = $analysis->{"$part_id.bubble_lines"}; - } + } $first_bubble_line{$response_number} = $bubble_line; $bubble_lines_per_response{$response_number} = $lines; $responsetype_per_response{$response_number} = $analysis->{$part_id.'.type'}; + $masterseq_id_responsenum{$resid.'_'.$part_id} = $response_number; $response_number++; $bubble_line += $lines; @@ -7714,26 +8157,79 @@ sub scantron_get_maxbubble { return $env{'form.scantron_maxbubble'}; } +sub bubblesheet_bubbles_per_row { + my ($scantron_config) = @_; + my $bubbles_per_row; + if (ref($scantron_config) eq 'HASH') { + $bubbles_per_row = $scantron_config->{'BubblesPerRow'}; + } + if ((!$bubbles_per_row) || ($bubbles_per_row < 1)) { + $bubbles_per_row = 10; + } + return $bubbles_per_row; +} + sub scantron_validate_missingbubbles { my ($r,$currentphase) = @_; #get student info my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); + my (undef,undef,$sequence)= + &Apache::lonnet::decode_symb($env{'form.selectpage'}); #get scantron line setup my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); + + my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb,%orderedforcode,%respnumlookup,%startline); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); + + my $nav_error; + if (ref($map)) { + $randomorder = $map->randomorder(); + $randompick = $map->randompick(); + } else { + $r->print(&navmap_errormsg()); + return ''; + } my $nav_error; - my $max_bubble=&scantron_get_maxbubble(\$nav_error); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + if ($randomorder || $randompick) { + $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; + } + } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); + + my ($uname,$udom); + + my $max_bubble=&scantron_get_maxbubble(\$nav_error,\%scantron_config); if ($nav_error) { + $r->print(&navmap_errormsg()); return(1,$currentphase); } + 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); + my $scan_record = + &scantron_parse_scanline($line,$i,\%scantron_config,$scan_data,undef,\%idmap, + $randomorder,$randompick,$sequence,\@master_seq, + \%symb_to_resource,\%grader_partids_by_symb, + \%orderedforcode,\%respnumlookup,\%startline); if (!defined($$scan_record{'scantron.missingerror'})) { next; } my @to_correct; @@ -7742,28 +8238,45 @@ sub scantron_validate_missingbubbles { foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) { my $lastbubble; if ($missing =~ /^(\d+)\.(\d+)$/) { - my $question = $1; - my $subquestion = $2; - if (!defined($first_bubble_line{$question -1})) { next; } - my $first = $first_bubble_line{$question-1}; - my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); - my $subcount = 1; - while ($subcount<$subquestion) { - $first += $subans[$subcount-1]; - $subcount ++; - } - my $count = $subans[$subquestion-1]; - $lastbubble = $first + $count; + my $question = $1; + my $subquestion = $2; + my ($first,$responsenum); + if ($randomorder || $randompick) { + $responsenum = $respnumlookup{$question-1}; + $first = $startline{$question-1}; + } else { + $responsenum = $question-1; + $first = $first_bubble_line{$responsenum}; + } + if (!defined($first)) { next; } + my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); + my $subcount = 1; + while ($subcount<$subquestion) { + $first += $subans[$subcount-1]; + $subcount ++; + } + my $count = $subans[$subquestion-1]; + $lastbubble = $first + $count; } else { - if (!defined($first_bubble_line{$missing - 1})) { next; } - $lastbubble = $first_bubble_line{$missing - 1} + $bubble_lines_per_response{$missing - 1}; + my ($first,$responsenum); + if ($randomorder || $randompick) { + $responsenum = $respnumlookup{$missing-1}; + $first = $startline{$missing-1}; + } else { + $responsenum = $missing-1; + $first = $first_bubble_line{$responsenum}; + } + if (!defined($first)) { next; } + $lastbubble = $first + $bubble_lines_per_response{$responsenum}; } if ($lastbubble > $max_bubble) { next; } push(@to_correct,$missing); } if (@to_correct) { &scantron_get_correction($r,$i,$scan_record,\%scantron_config, - $line,'missingbubble',\@to_correct); + $line,'missingbubble',\@to_correct, + $randomorder,$randompick,\%respnumlookup, + \%startline); return (1,$currentphase); } @@ -7771,6 +8284,41 @@ sub scantron_validate_missingbubbles { return (0,$currentphase+1); } +sub hand_bubble_option { + my (undef, undef, $sequence) = + &Apache::lonnet::decode_symb($env{'form.selectpage'}); + return if ($sequence eq ''); + my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + return; + } + my $needs_hand_bubbles; + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + foreach my $res (@resources) { + if (ref($res)) { + if ($res->is_problem()) { + my $partlist = $res->parts(); + foreach my $part (@{ $partlist }) { + my @types = $res->responseType($part); + if (grep(/^(chem|essay|image|formula|math|string|functionplot)$/,@types)) { + $needs_hand_bubbles = 1; + last; + } + } + } + } + } + if ($needs_hand_bubbles) { + my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); + return &mt('The sequence to be graded contains response types which are handgraded.').'

'. + &mt('If you have already graded these by bubbling sheets to indicate points awarded, [_1]what point value is assigned to a filled last bubble in each row?','
'). + ' '.&mt('or').' '. + '

'; + } + return; +} sub scantron_process_students { my ($r) = @_; @@ -7783,6 +8331,7 @@ sub scantron_process_students { my $default_form_data=&defaultFormData($symb); my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); my ($scanlines,$scan_data)=&scantron_getfile(); my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); @@ -7790,36 +8339,29 @@ sub scantron_process_students { unless (ref($navmap)) { $r->print(&navmap_errormsg()); return ''; - } - my $map=$navmap->getResourceByUrl($sequence); - my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); - my (%grader_partids_by_symb,%grader_randomlists_by_symb); - &graders_resources_pass(\@resources,\%grader_partids_by_symb, - \%grader_randomlists_by_symb); - my $resource_error; - foreach my $resource (@resources) { - my $ressymb; - if (ref($resource)) { - $ressymb = $resource->symb(); - } else { - $resource_error = 1; - last; - } - my ($analysis,$parts) = - &scantron_partids_tograde($resource,$env{'request.course.id'}, - $env{'user.name'},$env{'user.domain'},1); - $grader_partids_by_symb{$ressymb} = $parts; - if (ref($analysis) eq 'HASH') { - if (ref($analysis->{'parts_withrandomlist'}) eq 'ARRAY') { - $grader_randomlists_by_symb{$ressymb} = - $analysis->{'parts_withrandomlist'}; - } - } } - if ($resource_error) { + my $map=$navmap->getResourceByUrl($sequence); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb); + if (ref($map)) { + $randomorder = $map->randomorder(); + $randompick = $map->randompick(); + } else { $r->print(&navmap_errormsg()); return ''; } + my $nav_error; + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my (%grader_partids_by_symb,%grader_randomlists_by_symb,%ordered); + if ($randomorder || $randompick) { + $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; + } + } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); my ($uname,$udom); my $result= <print('
'); @@ -7844,8 +8384,7 @@ SCANTRONFORM my $i=-1; my $started; - my $nav_error; - &scantron_get_maxbubble(\$nav_error); # Need the bubble lines array to parse. + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # Need the bubble lines array to parse. if ($nav_error) { $r->print(&navmap_errormsg()); return ''; @@ -7864,6 +8403,7 @@ SCANTRONFORM my %lettdig = &letter_to_digits(); my $numletts = scalar(keys(%lettdig)); + my %orderedforcode; while ($i<$scanlines->{'count'}) { ($uname,$udom)=('',''); @@ -7875,8 +8415,15 @@ SCANTRONFORM 'last student'); } $started=1; + my %respnumlookup = (); + my %startline = (); + my $total; my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, - $scan_data); + $scan_data,undef,\%idmap,$randomorder, + $randompick,$sequence,\@master_seq, + \%symb_to_resource,\%grader_partids_by_symb, + \%orderedforcode,\%respnumlookup,\%startline, + \$total); unless ($uname=&scantron_find_student($scan_record,$scan_data, \%idmap,$i)) { &scantron_add_delay(\@delayqueue,$line, @@ -7888,10 +8435,26 @@ SCANTRONFORM 'Student '.$uname.' has multiple sheets',2); next; } + my $usec = $classlist->{$uname}->[&Apache::loncoursedata::CL_SECTION]; + my $user = $uname.':'.$usec; ($uname,$udom)=split(/:/,$uname); + my $scancode; + if ((exists($scan_record->{'scantron.CODE'})) && + (&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'}))) { + $scancode = $scan_record->{'scantron.CODE'}; + } else { + $scancode = ''; + } + + my @mapresources = @resources; + if ($randomorder || $randompick) { + @mapresources = + &users_order($user,$scancode,$sequence,\@master_seq,\%symb_to_resource, + \%orderedforcode); + } my (%partids_by_symb,$res_error); - foreach my $resource (@resources) { + foreach my $resource (@mapresources) { my $ressymb; if (ref($resource)) { $ressymb = $resource->symb(); @@ -7902,7 +8465,8 @@ SCANTRONFORM if ((exists($grader_randomlists_by_symb{$ressymb})) || (ref($grader_partids_by_symb{$ressymb}) ne 'ARRAY')) { my ($analysis,$parts) = - &scantron_partids_tograde($resource,$env{'request.course.id'},$uname,$udom); + &scantron_partids_tograde($resource,$env{'request.course.id'}, + $uname,$udom,undef,$bubbles_per_row); $partids_by_symb{$ressymb} = $parts; } else { $partids_by_symb{$ressymb} = $grader_partids_by_symb{$ressymb}; @@ -7922,16 +8486,11 @@ SCANTRONFORM &scantron_putfile($scanlines,$scan_data); } - my $scancode; - if ((exists($scan_record->{'scantron.CODE'})) && - (&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'}))) { - $scancode = $scan_record->{'scantron.CODE'}; - } else { - $scancode = ''; - } - if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode, - \@resources,\%partids_by_symb) eq 'ssi_error') { + \@mapresources,\%partids_by_symb, + $bubbles_per_row,$randomorder,$randompick, + \%respnumlookup,\%startline) + eq 'ssi_error') { $ssi_error = 0; # So end of handler error message does not trigger. $r->print(""); &ssi_print_error($r); @@ -7940,26 +8499,43 @@ SCANTRONFORM return ''; # Why return ''? Beats me. } + if (($scancode) && ($randomorder || $randompick)) { + my $parmresult = + &Apache::lonparmset::storeparm_by_symb($symb, + '0_examcode',2,$scancode, + 'string_examcode',$uname, + $udom); + } $completedstudents{$uname}={'line'=>$line}; if ($env{'form.verifyrecord'}) { my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; + if ($randompick) { + if ($total) { + $lastpos = $total*$scantron_config{'Qlength'}; + } + } + my $studentdata = substr($line,$scantron_config{'Qstart'}-1,$lastpos); chomp($studentdata); $studentdata =~ s/\r$//; my $studentrecord = ''; my $counter = -1; - foreach my $resource (@resources) { + foreach my $resource (@mapresources) { my $ressymb = $resource->symb(); ($counter,my $recording) = &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'}, $counter,$studentdata,$partids_by_symb{$ressymb}, - \%scantron_config,\%lettdig,$numletts); + \%scantron_config,\%lettdig,$numletts,$randomorder, + $randompick,\%respnumlookup,\%startline); $studentrecord .= $recording; } if ($studentrecord ne $studentdata) { &Apache::lonxml::clear_problem_counter(); if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode, - \@resources,\%partids_by_symb) eq 'ssi_error') { + \@mapresources,\%partids_by_symb, + $bubbles_per_row,$randomorder,$randompick, + \%respnumlookup,\%startline) + eq 'ssi_error') { $ssi_error = 0; # So end of handler error message does not trigger. $r->print(""); &ssi_print_error($r); @@ -7970,12 +8546,14 @@ SCANTRONFORM } $counter = -1; $studentrecord = ''; - foreach my $resource (@resources) { + foreach my $resource (@mapresources) { my $ressymb = $resource->symb(); ($counter,my $recording) = &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'}, $counter,$studentdata,$partids_by_symb{$ressymb}, - \%scantron_config,\%lettdig,$numletts); + \%scantron_config,\%lettdig,$numletts, + $randomorder,$randompick,\%respnumlookup, + \%startline); $studentrecord .= $recording; } if ($studentrecord ne $studentdata) { @@ -8024,14 +8602,16 @@ SCANTRONFORM } sub graders_resources_pass { - my ($resources,$grader_partids_by_symb,$grader_randomlists_by_symb) = @_; + my ($resources,$grader_partids_by_symb,$grader_randomlists_by_symb, + $bubbles_per_row) = @_; if ((ref($resources) eq 'ARRAY') && (ref($grader_partids_by_symb)) && (ref($grader_randomlists_by_symb) eq 'HASH')) { foreach my $resource (@{$resources}) { my $ressymb = $resource->symb(); my ($analysis,$parts) = &scantron_partids_tograde($resource,$env{'request.course.id'}, - $env{'user.name'},$env{'user.domain'},1); + $env{'user.name'},$env{'user.domain'}, + 1,$bubbles_per_row); $grader_partids_by_symb->{$ressymb} = $parts; if (ref($analysis) eq 'HASH') { if (ref($analysis->{'parts_withrandomlist'}) eq 'ARRAY') { @@ -8044,8 +8624,67 @@ sub graders_resources_pass { return; } +=pod + +=item users_order + + Returns array of resources in current map, ordered based on either CODE, + if this is a CODEd exam, or based on student's identity if this is a + "NAMEd" exam. + + Should be used when randomorder and/or randompick applied when the + corresponding exam was printed, prior to students completing bubblesheets + for the version of the exam the student received. + +=cut + +sub users_order { + my ($user,$scancode,$mapurl,$master_seq,$symb_to_resource,$orderedforcode) = @_; + my @mapresources; + unless ((ref($master_seq) eq 'ARRAY') && (ref($symb_to_resource) eq 'HASH')) { + return @mapresources; + } + if ($scancode) { + if ((ref($orderedforcode) eq 'HASH') && (ref($orderedforcode->{$scancode}) eq 'ARRAY')) { + @mapresources = @{$orderedforcode->{$scancode}}; + } else { + $env{'form.CODE'} = $scancode; + my $actual_seq = + &Apache::lonprintout::master_seq_to_person_seq($mapurl, + $master_seq, + $user,$scancode,1); + if (ref($actual_seq) eq 'ARRAY') { + @mapresources = map { $symb_to_resource->{$_}; } @{$actual_seq}; + if (ref($orderedforcode) eq 'HASH') { + if (@mapresources > 0) { + $orderedforcode->{$scancode} = \@mapresources; + } + } + } + delete($env{'form.CODE'}); + } + } else { + my $actual_seq = + &Apache::lonprintout::master_seq_to_person_seq($mapurl, + $master_seq, + $user,undef,1); + if (ref($actual_seq) eq 'ARRAY') { + @mapresources = + map { $symb_to_resource->{$_}; } @{$actual_seq}; + } + } + return @mapresources; +} + sub grade_student_bubbles { - my ($r,$uname,$udom,$scan_record,$scancode,$resources,$parts) = @_; + my ($r,$uname,$udom,$scan_record,$scancode,$resources,$parts,$bubbles_per_row, + $randomorder,$randompick,$respnumlookup,$startline) = @_; + my $uselookup = 0; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH') && + (ref($startline) eq 'HASH')) { + $uselookup = 1; + } + if (ref($resources) eq 'ARRAY') { my $count = 0; foreach my $resource (@{$resources}) { @@ -8058,11 +8697,21 @@ sub grade_student_bubbles { 'grade_symb' => $ressymb, 'CODE' => $scancode ); + if ($bubbles_per_row ne '') { + $form{'bubbles_per_row'} = $bubbles_per_row; + } + if ($env{'form.scantron_lastbubblepoints'} ne '') { + $form{'scantron_lastbubblepoints'} = $env{'form.scantron_lastbubblepoints'}; + } if (ref($parts) eq 'HASH') { if (ref($parts->{$ressymb}) eq 'ARRAY') { foreach my $part (@{$parts->{$ressymb}}) { - $form{'scantron_questnum_start.'.$part} = - 1+$env{'form.scantron.first_bubble_line.'.$count}; + if ($uselookup) { + $form{'scantron_questnum_start.'.$part} = $startline->{$count} + 1; + } else { + $form{'scantron_questnum_start.'.$part} = + 1+$env{'form.scantron.first_bubble_line.'.$count}; + } $count++; } } @@ -8084,8 +8733,9 @@ sub scantron_upload_scantron_data { 'domainid', 'coursename',$dom); my $syllabuslink = ''.&mt('Syllabus').''. - (' 'x2).&mt('(shows course personnel)'); - my $default_form_data=&defaultFormData(&get_symb($r,1)); + (' 'x2).&mt('(shows course personnel)'); + my ($symb) = &get_symb($r,1); + my $default_form_data=&defaultFormData($symb); my $nofile_alert = &mt('Please use the browse button to select a file from your local directory.'); my $nocourseid_alert = &mt("Please use the 'Select Course' link to open a separate window where you can search for a course to which a file can be uploaded."); $r->print(' @@ -8290,17 +8940,18 @@ sub valid_file { sub scantron_download_scantron_data { my ($r)=@_; - my $default_form_data=&defaultFormData(&get_symb($r,1)); + my ($symb) = &get_symb($r,1); + my $default_form_data=&defaultFormData($symb); 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('

- '.&mt('The requested file name was invalid.').' + '.&mt('The requested filename was invalid.').'

'); - $r->print(&show_grading_menu_form(&get_symb($r,1))); + $r->print(&show_grading_menu_form($symb)); return; } my $orig='/uploaded/'.$cdom.'/'.$cname.'/scantron_orig_'.$file; @@ -8323,7 +8974,7 @@ sub scantron_download_scantron_data { '','').'

'); - $r->print(&show_grading_menu_form(&get_symb($r,1))); + $r->print(&show_grading_menu_form($symb)); return ''; } @@ -8341,6 +8992,7 @@ sub checkscantron_results { my %record; my %scantron_config = &Apache::grades::get_scantron_config($env{'form.scantron_format'}); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); my ($scanlines,$scan_data)=&Apache::grades::scantron_getfile(); my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&Apache::grades::username_to_idmap($classlist); @@ -8350,10 +9002,19 @@ sub checkscantron_results { return ''; } my $map=$navmap->getResourceByUrl($sequence); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb,%orderedforcode); + if (ref($map)) { + $randomorder=$map->randomorder(); + } my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); - my (%grader_partids_by_symb,%grader_randomlists_by_symb); - &graders_resources_pass(\@resources,\%grader_partids_by_symb, \%grader_randomlists_by_symb); - + my $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; + } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); my ($uname,$udom); my (%scandata,%lastname,%bylast); $r->print(' @@ -8362,13 +9023,10 @@ sub checkscantron_results { my @delayqueue; my %completedstudents; - my $count=&Apache::grades::get_todo_count($scanlines,$scan_data); - my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Bubblesheet/Submissions Comparison Status', - 'Progress of Bubblesheet Data/Submission Records Comparison',$count, - 'inline',undef,'checkscantron'); - my ($username,$domain,$started); - my $nav_error; - &scantron_get_maxbubble(\$nav_error); # Need the bubble lines array to parse. + my $count=&get_todo_count($scanlines,$scan_data); + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,$count); + my ($username,$domain,$started,%ordered); + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # Need the bubble lines array to parse. if ($nav_error) { $r->print(&navmap_errormsg()); return ''; @@ -8392,8 +9050,8 @@ sub checkscantron_results { my $scan_record= &Apache::grades::scantron_parse_scanline($line,$i,\%scantron_config, $scan_data); - unless ($uname=&Apache::grades::scantron_find_student($scan_record,$scan_data, - \%idmap,$i)) { + unless ($uname=&scantron_find_student($scan_record,$scan_data, + \%idmap,$i)) { &Apache::grades::scantron_add_delay(\@delayqueue,$line, 'Unable to find a student that matches',1); next; @@ -8406,26 +9064,57 @@ sub checkscantron_results { my $pid = $scan_record->{'scantron.ID'}; $lastname{$pid} = $scan_record->{'scantron.LastName'}; push(@{$bylast{$lastname{$pid}}},$pid); + my $usec = $classlist->{$uname}->[&Apache::loncoursedata::CL_SECTION]; + my $user = $uname.':'.$usec; + ($username,$domain)=split(/:/,$uname); + + my $scancode; + if ((exists($scan_record->{'scantron.CODE'})) && + (&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'}))) { + $scancode = $scan_record->{'scantron.CODE'}; + } else { + $scancode = ''; + } + + my @mapresources = @resources; my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; + my %respnumlookup=(); + my %startline=(); + if ($randomorder || $randompick) { + @mapresources = + &users_order($user,$scancode,$sequence,\@master_seq,\%symb_to_resource, + \%orderedforcode); + my $total = &get_respnum_lookups($sequence,$scan_data,\%idmap,$line, + $scan_record,\@master_seq,\%symb_to_resource, + \%grader_partids_by_symb,\%orderedforcode, + \%respnumlookup,\%startline); + if ($randompick && $total) { + $lastpos = $total*$scantron_config{'Qlength'}; + } + } $scandata{$pid} = substr($line,$scantron_config{'Qstart'}-1,$lastpos); chomp($scandata{$pid}); $scandata{$pid} =~ s/\r$//; - ($username,$domain)=split(/:/,$uname); + my $counter = -1; - foreach my $resource (@resources) { + foreach my $resource (@mapresources) { my $parts; my $ressymb = $resource->symb(); if ((exists($grader_randomlists_by_symb{$ressymb})) || (ref($grader_partids_by_symb{$ressymb}) ne 'ARRAY')) { (my $analysis,$parts) = - &scantron_partids_tograde($resource,$env{'request.course.id'},$username,$domain); + &scantron_partids_tograde($resource,$env{'request.course.id'}, + $username,$domain,undef, + $bubbles_per_row); } else { $parts = $grader_partids_by_symb{$ressymb}; } ($counter,my $recording) = &verify_scantron_grading($resource,$domain,$username,$cid,$counter, $scandata{$pid},$parts, - \%scantron_config,\%lettdig,$numletts); + \%scantron_config,\%lettdig,$numletts, + $randomorder,$randompick, + \%respnumlookup,\%startline); $record{$pid} .= $recording; } } @@ -8471,7 +9160,11 @@ sub checkscantron_results { $env{'form.scantron_maxbubble'}). '

' ); - $r->print('

'.&mt('Exact matches for [quant,_1,student].',$passed).'
'.&mt('Discrepancies detected for [quant,_1,student].',$failed).'

'); + $r->print('

' + .&mt('Exact matches for [_1][quant,_2,student][_3].','',$passed,'') + .'
' + .&mt('Discrepancies detected for [_1][quant,_2,student][_3].','',$failed,'') + .'

'); if ($passed) { $r->print(&mt('Students with exact correspondence between bubblesheet data and submissions are as follows:').'

'); $r->print(&Apache::loncommon::start_data_table()."\n". @@ -8497,7 +9190,8 @@ sub checkscantron_results { sub verify_scantron_grading { my ($resource,$domain,$username,$cid,$counter,$scandata,$partids, - $scantron_config,$lettdig,$numletts) = @_; + $scantron_config,$lettdig,$numletts,$randomorder,$randompick, + $respnumlookup,$startline) = @_; my ($record,%expected,%startpos); return ($counter,$record) if (!ref($resource)); return ($counter,$record) if (!$resource->is_problem()); @@ -8506,15 +9200,21 @@ sub verify_scantron_grading { foreach my $part_id (@{$partids}) { $counter ++; $expected{$part_id} = 0; - if ($env{"form.scantron.sub_bubblelines.$counter"}) { - my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$counter"}); + my $respnum = $counter; + if ($randomorder || $randompick) { + $respnum = $respnumlookup->{$counter}; + $startpos{$part_id} = $startline->{$counter} + 1; + } else { + $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"}; + } + if ($env{"form.scantron.sub_bubblelines.$respnum"}) { + my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$respnum"}); foreach my $item (@sub_lines) { $expected{$part_id} += $item; } } else { - $expected{$part_id} = $env{"form.scantron.bubblelines.$counter"}; + $expected{$part_id} = $env{"form.scantron.bubblelines.$respnum"}; } - $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"}; } if ($symb) { my %recorded; @@ -8610,7 +9310,7 @@ sub verify_scantron_grading { return ($counter,$record); } -sub letter_to_digits { +sub letter_to_digits { my %lettdig = ( A => 1, B => 2, @@ -8657,6 +9357,13 @@ sub savedState { return \%savedState; } +#--- Href with symb and command --- + +sub href_symb_cmd { + my ($symb,$cmd)=@_; + return '/adm/grades?symb='.&HTML::Entities::encode(&Apache::lonenc::check_encrypt($symb),'<>&"').'&command='.$cmd; +} + sub grading_menu { my ($request) = @_; my ($symb)=&get_symb($request); @@ -8964,6 +9671,21 @@ sub init_perm { } } +sub init_old_essays { + my ($symb,$apath,$adom,$aname) = @_; + if ($symb ne '') { + my %essays = &Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname); + if (keys(%essays) > 0) { + $old_essays{$symb} = \%essays; + } + } + return; +} + +sub reset_old_essays { + undef(%old_essays); +} + sub gather_clicker_ids { my %clicker_ids; @@ -9065,7 +9787,8 @@ sub process_clicker { my $pincorrect=&mt("Percentage points for incorrect solution"); my $selectform=&Apache::loncommon::select_form($env{'form.upfiletype'},'upfiletype', {'iclicker' => 'i>clicker', - 'interwrite' => 'interwrite PRS'}); + 'interwrite' => 'interwrite PRS', + 'turning' => 'Turning Technologies'}); $symb = &Apache::lonenc::check_encrypt($symb); $result.=< @@ -9238,6 +9961,9 @@ ENDHEADER if ($env{'form.upfiletype'} eq 'interwrite') { ($errormsg,$number)=&interwrite_eval(\@questiontitles,\%responses); } + if ($env{'form.upfiletype'} eq 'turning') { + ($errormsg,$number)=&turning_eval(\@questiontitles,\%responses); + } $result.='
'.&mt('Found [_1] question(s)',$number).'
'. ''. &mt('Awarding [_1] percent for correct and [_2] percent for incorrect responses', @@ -9369,6 +10095,31 @@ sub interwrite_eval { return ($errormsg,$number); } +sub turning_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>$number) { $number=$#entries; } + my $id=$entries[0]; + my @idresponses; + $id=~s/^[\#0]+//; + unless ($id) { next; } + for (my $idx=1;$idx<=$#entries;$idx++) { + $entries[$idx]=~s/\,/\;/g; + $entries[$idx]=~s/[^a-zA-Z0-9\.\*\-\+\;]+//g; + push(@idresponses,$entries[$idx]); + } + $$responses{$id}=join(',',@idresponses); + } + for (my $i=1; $i<=$number; $i++) { + $$questiontitles[$i]=&mt('Question [_1]',$i); + } + return ($errormsg,$number); +} + sub assign_clicker_grades { my ($r)=@_; my ($symb)=&get_symb($r); @@ -9510,6 +10261,19 @@ sub navmap_errormsg { ''; } +sub startpage { + my ($r,$symb,$crumbs,$onlyfolderflag,$nodisplayflag,$stuvcurrent,$stuvdisp,$nomenu,$js) = @_; + if ($nomenu) { + $r->print(&Apache::loncommon::start_page("Student's Version",$js,{'only_body' => '1'})); + } else { + $r->print(&Apache::loncommon::start_page('Grading',$js, + {'bread_crumbs' => $crumbs})); + } + unless ($nodisplayflag) { + $r->print(&Apache::lonhtmlcommon::resource_info_box($symb,$onlyfolderflag,$stuvcurrent,$stuvdisp)); + } +} + sub handler { my $request=$_[0]; &reset_caches(); @@ -9531,7 +10295,7 @@ sub handler { $ssi_error = 0; my $brcrum = [{href=>"/adm/grades",text=>"Grading"}]; my $start_page = &Apache::loncommon::start_page('Grading',undef, - {'bread_crumbs' => $brcrum}); + {'bread_crumbs' => $brcrum}); if ($symb eq '' && $command eq '') { if ($env{'user.adv'}) { &Apache::loncommon::content_type($request,'text/html'); @@ -9564,6 +10328,7 @@ sub handler { &init_perm(); if (!%perm) { $request->internal_redirect('/adm/quickgrades'); + return OK; } else { &Apache::loncommon::content_type($request,'text/html'); $request->send_http_header; @@ -9584,9 +10349,33 @@ sub handler { } &Apache::loncommon::content_type($request,'text/html'); $request->send_http_header; - $request->print($start_page); + unless ((($command eq 'submission' || $command eq 'versionsub')) && ($perm{'vgr'})) { + $request->print($start_page); + } if ($command eq 'submission' && $perm{'vgr'}) { + my ($stuvcurrent,$stuvdisp,$versionform,$js); + if (($env{'form.student'} ne '') && ($env{'form.userdom'} ne '')) { + ($stuvcurrent,$stuvdisp,$versionform,$js) = + &choose_task_version_form($symb,$env{'form.student'}, + $env{'form.userdom'}); + } + &startpage($request,$symb,[{href=>"", text=>"Student Submissions"}],undef,undef,$stuvcurrent,$stuvdisp,undef,$js); + if ($versionform) { + $request->print($versionform); + } + $request->print('
'); ($env{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0)); + } elsif ($command eq 'versionsub' && $perm{'vgr'}) { + my ($stuvcurrent,$stuvdisp,$versionform,$js) = + &choose_task_version_form($symb,$env{'form.student'}, + $env{'form.userdom'}, + $env{'form.inhibitmenu'}); + &startpage($request,$symb,[{href=>"", text=>"Previous Student Version"}],undef,undef,$stuvcurrent,$stuvdisp,$env{'form.inhibitmenu'},$js); + if ($versionform) { + $request->print($versionform); + } + $request->print('
'); + $request->print(&show_previous_task_version($request,$symb)); } elsif ($command eq 'pickStudentPage' && $perm{'vgr'}) { &pickStudentPage($request); } elsif ($command eq 'displayPage' && $perm{'vgr'}) { @@ -9757,6 +10546,16 @@ ssi_with_retries() - missingbubble - array ref of the bubble lines that have missing bubble errors + $randomorder - True if exam folder has randomorder set + $randompick - True if exam folder has randompick set + $respnumlookup - Reference to HASH mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Seqence" (as seen by Course Coordinator). + $startline - Reference to hash where key is question number (0 is first) + and value is number of first bubble line for current student + or code-based randompick and/or randomorder. + + =item scantron_get_maxbubble() : Arguments: @@ -9766,6 +10565,8 @@ ssi_with_retries() calling routine should trap the error condition and display the warning found in &navmap_errormsg(). + $scantron_config - Reference to bubblesheet format configuration hash. + 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() @@ -9775,7 +10576,7 @@ ssi_with_retries() $env{'form.scantron.bubble_lines.n'}, $env{'form.scantron.first_bubble_line.n'} and $env{"form.scantron.sub_bubblelines.n"} - which are the total number of bubble, lines, the number of bubble + which are the total number of bubble lines, the number of bubble lines for response n and number of the first bubble line for response n, and a comma separated list of numbers of bubble lines for sub-questions (for optionresponse, matchresponse, and rankresponse items), for response n.