");
- pDoc.write("");
- pDoc.write("Type<\\/b><\\/td> Include<\\/b><\\/td> Message<\\/td><\\/tr>");
+ pDoc.write('');
+ pDoc.write('
-
+
ENDPICK
}
sub checkforfile_js {
+ my $alertmsg = &mt('Please use the browse button to select a file from your local directory.');
my $result =<
function checkUpload(formname) {
if (formname.upfile.value == "") {
- alert("Please use the browse button to select a file from your local directory.");
+ alert("$alertmsg");
return false;
}
formname.submit();
@@ -3729,9 +4082,9 @@ sub upcsvScores_form {
$result.=$table;
$result.=''."\n";
$result.=''."\n";
- $result.=' '.&mt('Specify a file containing the class scores for current resource').
- '. '."\n";
- $result.=''."\n";
+ $result.=' '.&mt('Specify a file containing the class scores for current resource.').
+ ' '."\n";
+ $result.=''."\n";
my $upload=&mt("Upload Scores");
my $upfile_select=&Apache::loncommon::upfile_select_html();
my $ignore=&mt('Ignore First Line');
@@ -3743,7 +4096,7 @@ sub upcsvScores_form {
$upfile_select
-
+
$ignore
ENDUPFORM
@@ -3773,8 +4126,12 @@ sub csvuploadmap {
&csvuploadmap_header($request,$symb,$datatoken,$#records+1);
my ($i,$keyfields);
if (@records) {
- my @fields=&csvupload_fields($symb);
-
+ my $fieldserror;
+ my @fields=&csvupload_fields($symb,\$fieldserror);
+ if ($fieldserror) {
+ $request->print(&navmap_errormsg());
+ return;
+ }
if ($env{'form.upfile_associate'} eq 'reverse') {
&Apache::loncommon::csv_print_samples($request,\@records);
$i=&Apache::loncommon::csv_print_select_table($request,\@records,
@@ -3875,6 +4232,7 @@ sub csvuploadassign {
my ($classlist) = &getclasslist('all',0);
my @notallowed;
my @skipped;
+ my @warnings;
my $countdone=0;
foreach my $grade (@gradedata) {
my %entries=&Apache::loncommon::record_sep($grade);
@@ -3923,6 +4281,9 @@ sub csvuploadassign {
my $pcr=$entries{$fields{$dest}} / $wgt;
my $award=($pcr == 0) ? 'incorrect_by_override'
: 'correct_by_override';
+ if ($pcr>1) {
+ push(@warnings,&mt("[_1]: point value larger than weight","$username:$domain"));
+ }
$grades{"resource.$part.awarded"}=$pcr;
$grades{"resource.$part.solved"}=$award;
$points{$part}=1;
@@ -3942,32 +4303,41 @@ sub csvuploadassign {
$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'}";
- my $result=&Apache::lonnet::cstore(\%grades,$symb,
+ if (! %grades) {
+ push(@skipped,&mt("[_1]: no data to save","$username:$domain"));
+ } else {
+ $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}";
+ 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++;
+ if ($result eq 'ok') {
+ $request->print('.');
+# Remove from grading queue
+ &Apache::bridgetask::remove_from_queue('gradingqueue',$symb,
+ $env{'course.'.$env{'request.course.id'}.'.domain'},
+ $env{'course.'.$env{'request.course.id'}.'.num'},
+ $domain,$username);
+ } else {
+ $request->print("".
+ &mt("Failed to save data for student [_1]. Message when trying to save was: [_2]",
+ "$username:$domain",$result)."
");
+ }
+ $request->rflush();
+ $countdone++;
+ }
+ }
+ $request->print(' '.&Apache::lonhtmlcommon::confirm_success(&mt("Saved scores for [quant,_1,student]",$countdone),$countdone==0));
+ if (@warnings) {
+ $request->print(' '.&Apache::lonhtmlcommon::confirm_success(&mt('Warnings generated for the following saved scores:'),1).' ');
+ $request->print(join(', ',@warnings));
}
- $request->print(" Saved $countdone students\n");
if (@skipped) {
- $request->print('
Skipped Students ');
- foreach my $student (@skipped) { $request->print("$student \n"); }
+ $request->print(' '.&Apache::lonhtmlcommon::confirm_success(&mt('No scores stored for the following username(s):'),1).' ');
+ $request->print(join(', ',@skipped));
}
if (@notallowed) {
- $request->print('Students Not Allowed to Modify
');
- foreach my $student (@notallowed) { $request->print("$student \n"); }
+ $request->print(' '.&Apache::lonhtmlcommon::confirm_success(&mt('Modification of scores not allowed for the following username(s):'),1).' ');
+ $request->print(join(', ',@notallowed));
}
$request->print(" \n");
$request->print(&show_grading_menu_form($symb));
@@ -3983,12 +4353,13 @@ sub csvuploadassign {
sub pickStudentPage {
my ($request) = shift;
+ my $alertmsg = &mt('Please select the student you wish to grade.');
$request->print(<
function checkPickOne(formname) {
if (radioSelection(formname.student) == null) {
- alert("Please select the student you wish to grade.");
+ alert("$alertmsg");
return;
}
ptr = pullDownSelection(formname.selectpage);
@@ -4006,23 +4377,30 @@ LISTJAVASCRIPT
my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'};
my $result=' '.
- 'Manual Grading by Page or Sequence ';
+ &mt('Manual Grading by Page or Sequence').'';
$result.=''."\n";
- $result.=' Problems from: '."\n";
- my ($titles,$symbx) = &getSymbMap();
+ my $map_error;
+ my ($titles,$symbx) = &getSymbMap($map_error);
+ if ($map_error) {
+ $request->print(&navmap_errormsg());
+ return;
+ }
my ($curpage) =&Apache::lonnet::decode_symb($symb);
# my ($curpage,$mapId) =&Apache::lonnet::decode_symb($symb);
# my $type=($curpage =~ /\.(page|sequence)/);
+ my $select = ''."\n";
my $ctr=0;
foreach (@$titles) {
my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/);
- $result.=''.$showtitle.' '."\n";
$ctr++;
}
- $result.= ' '." \n";
+ $select.= ' ';
+ $result.=' '.&mt('Problems from').': '.$select." \n";
+
$ctr=0;
foreach (@$titles) {
my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/);
@@ -4033,13 +4411,16 @@ LISTJAVASCRIPT
$result.=' '."\n".
' '."\n";
- $result.=' View Problems Text: no '."\n".
- ' yes '." \n";
-
- $result.=' Submission Details: '.
- ' none '."\n".
- ' by dates and submissions '."\n".
- ' all details '."\n";
+ my $options =
+ ' '.&mt('no').' '."\n".
+ ' '.&mt('yes').' '." \n";
+ $result.=' '.&mt('View Problem Text').': '.$options;
+
+ $options =
+ ' '.&mt('none').' '."\n".
+ ' '.&mt('by dates and submissions').' '."\n".
+ ' '.&mt('all details').' '."\n";
+ $result.=' '.&mt('Submissions').': '.$options;
$result.=&build_section_inputs();
my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
@@ -4048,21 +4429,21 @@ LISTJAVASCRIPT
' '."\n".
' '." \n";
- $result.=' '.&mt('Use CODE:').' '.
- ' '."\n";
+ $result.=' '.&mt('Use CODE').': '."\n";
$result.=' " /> '."\n";
+ 'onclick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" /> '."\n";
$request->print($result);
- my $studentTable.=' Select a student you wish to grade and then click on the Next button. '.
- ''.
- ''.
- ' No. '.
- ''.&nameUserString('header').' '.
- ' No. '.
- ''.&nameUserString('header').' ';
+ my $studentTable.=' '.&mt('Select a student you wish to grade and then click on the Next button.').' '.
+ &Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_header_row().
+ ' '.&mt('No.').' '.
+ ''.&nameUserString('header').' '.
+ ' '.&mt('No.').' '.
+ ''.&nameUserString('header').' '.
+ &Apache::loncommon::end_data_table_header_row();
my (undef,undef,$fullname) = &getclasslist($getsec,'1');
my $ptr = 1;
@@ -4074,17 +4455,23 @@ LISTJAVASCRIPT
return $a cmp $b;
} (keys(%$fullname))) {
my ($uname,$udom) = split(/:/,$student);
- $studentTable.=($ptr%2 == 1 ? '' : '');
+ $studentTable.=($ptr%2==1 ? &Apache::loncommon::start_data_table_row()
+ : '');
$studentTable.=''.$ptr.' ';
$studentTable.=' '
.&nameUserString(undef,$$fullname{$student},$uname,$udom)." \n";
- $studentTable.=($ptr%2 == 0 ? ' ' : '');
+ $studentTable.=
+ ($ptr%2 == 0 ? ''.&Apache::loncommon::end_data_table_row()
+ : '');
$ptr++;
}
- $studentTable.=' ' if ($ptr%2 == 0);
- $studentTable.='
'."\n";
+ if ($ptr%2 == 0) {
+ $studentTable.=' '.
+ &Apache::loncommon::end_data_table_row();
+ }
+ $studentTable.=&Apache::loncommon::end_data_table()."\n";
$studentTable.=' " />'."\n";
+ 'onclick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" />'."\n";
$studentTable.=&show_grading_menu_form($symb);
$request->print($studentTable);
@@ -4093,8 +4480,14 @@ LISTJAVASCRIPT
}
sub getSymbMap {
+ my ($map_error) = @_;
my $navmap = Apache::lonnavmaps::navmap->new();
-
+ unless (ref($navmap)) {
+ if (ref($map_error)) {
+ $$map_error = 'navmap';
+ }
+ return;
+ }
my %symbx = ();
my @titles = ();
my $minder = 0;
@@ -4136,15 +4529,16 @@ sub displayPage {
&Apache::lonnet::clear_EXT_cache_status();
if (!&canview($usec)) {
- $request->print('Unable to view requested student.('.$env{'form.student'}.') ');
+ $request->print(''.&mt('Unable to view requested student. ([_1])',$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).
+ $result.=' '.&mt('Student: [_1]',&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom)).
' '."\n";
- if (&Apache::lonnet::validCODE($env{'form.CODE'})) {
- $result.=' CODE: '.$env{'form.CODE'}.' '."\n";
+ $env{'form.CODE'} = uc($env{'form.CODE'});
+ if (&Apache::lonnet::validCODE(uc($env{'form.CODE'}))) {
+ $result.=' '.&mt('CODE: [_1]',$env{'form.CODE'}).' '."\n";
} else {
delete($env{'form.CODE'});
}
@@ -4152,10 +4546,15 @@ sub displayPage {
$request->print($result);
my $navmap = Apache::lonnavmaps::navmap->new();
+ unless (ref($navmap)) {
+ $request->print(&navmap_errormsg());
+ $request->print(&show_grading_menu_form($symb));
+ return;
+ }
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(''.&mt('Unable to view requested sequence. ([_1])',$resUrl).' ');
$request->print(&show_grading_menu_form($symb));
return;
}
@@ -4177,15 +4576,16 @@ sub displayPage {
' '."\n";
}
my $checkIcon = ' ';
+ '" src="'.&Apache::loncommon::lonhttpdurl($request->dir_config('lonIconsURL').'/check.gif').'" height="16" border="0" />';
- $studentTable.=' Note: Problems graded correct by the computer are marked with a '.$checkIcon.
- ' symbol.'."\n".
- ' '.
- ''.
- ' Prob. '.
- ' '.($env{'form.vProb'} eq 'no' ? 'Title' : 'Problem Text').'/Grade ';
+ $studentTable.=' '.
+ &mt('Problems graded correct by the computer are marked with a [_1] symbol.',$checkIcon).
+ ' '."\n".
+ &Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_header_row().
+ ' Prob. '.
+ ' '.($env{'form.vProb'} eq 'no' ? &mt('Title') : &mt('Problem Text')).'/'.&mt('Grade').' '.
+ &Apache::loncommon::end_data_table_header_row();
&Apache::lonxml::clear_problem_counter();
my ($depth,$question,$prob) = (1,1,1);
@@ -4199,8 +4599,14 @@ sub displayPage {
my $parts = $curRes->parts();
my $title = $curRes->compTitle();
my $symbx = $curRes->symb();
- $studentTable.=''.$prob.
- (scalar(@{$parts}) == 1 ? '' : ' ('.scalar(@{$parts}).' parts)').' ';
+ $studentTable.=
+ &Apache::loncommon::start_data_table_row().
+ ''.$prob.
+ (scalar(@{$parts}) == 1 ? ''
+ : ' ('.&mt('[_1]parts)',
+ scalar(@{$parts}).' ')
+ ).
+ ' ';
$studentTable.='';
my %form = ('CODE' => $env{'form.CODE'},);
if ($env{'form.vProb'} eq 'yes' ) {
@@ -4215,14 +4621,14 @@ sub displayPage {
# $request->print('match='.$1." \n");
# }
# $companswer =~ s|||g;
- $studentTable.=' '.$title.' Correct answer: '.$companswer;
+ $studentTable.=' '.$title.' '.&mt('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 ';
+ $studentTable.=' '.&mt('No recorded submission for this problem.').' ';
} else {
my %responseType = ();
foreach my $partid (@{$parts}) {
@@ -4245,11 +4651,13 @@ sub displayPage {
}
if (&canmodify($usec)) {
+ $studentTable.=&gradeBox_start();
foreach my $partid (@{$parts}) {
$studentTable.=&gradeBox($request,$symbx,$uname,$udom,$question,$partid,\%record);
$studentTable.=' '."\n";
$question++;
}
+ $studentTable.=&gradeBox_end();
$prob++;
}
$studentTable.='';
@@ -4258,10 +4666,11 @@ sub displayPage {
$curRes = $iterator->next();
}
- $studentTable.='
'."\n".
- ' '.
- ''."\n";
+ $studentTable.=
+ '
'."\n".
+ ' '.
+ ''."\n";
$studentTable.=&show_grading_menu_form($symb);
$request->print($studentTable);
@@ -4273,76 +4682,104 @@ sub displaySubByDates {
my $isCODE=0;
my $isTask = ($symb =~/\.task$/);
if (exists($record->{'resource.CODE'})) { $isCODE=1; }
- my $studentTable=''.
- ''.
- 'Date/Time '.
- ($isCODE?'CODE ':'').
- 'Submission '.
- 'Status ';
+ my $studentTable=&Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_header_row().
+ ''.&mt('Date/Time').' '.
+ ($isCODE?''.&mt('CODE').' ':'').
+ ''.&mt('Submission').' '.
+ ''.&mt('Status').' '.
+ &Apache::loncommon::end_data_table_header_row();
my ($version);
my %mark;
my %orders;
$mark{'correct_by_student'} = $checkIcon;
if (!exists($$record{'1:timestamp'})) {
- return ' Nothing submitted - no attempts ';
+ return ' '.&mt('Nothing submitted - no attempts.').' ';
}
my $interaction;
+ my $no_increment = 1;
+ my %lastrndseed;
for ($version=1;$version<=$$record{'version'};$version++) {
- my $timestamp = scalar(localtime($$record{$version.':timestamp'}));
+ my $timestamp =
+ &Apache::lonlocal::locallocaltime($$record{$version.':timestamp'});
if (exists($$record{$version.':resource.0.version'})) {
$interaction = $$record{$version.':resource.0.version'};
}
my $where = ($isTask ? "$version:resource.$interaction"
: "$version:resource");
- $studentTable.=''.$timestamp.' ';
+ $studentTable.=&Apache::loncommon::start_data_table_row().
+ ''.$timestamp.' ';
if ($isCODE) {
$studentTable.=''.$record->{$version.':resource.CODE'}.' ';
}
my @versionKeys = split(/\:/,$$record{$version.':keys'});
my @displaySub = ();
foreach my $partid (@{$parts}) {
+ my ($hidden,$type);
+ $type = $$record{$version.':resource.'.$partid.'.type'};
+ if (($type eq 'anonsurvey') || ($type eq 'anonsurveycred')) {
+ $hidden = 1;
+ }
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$/));
- $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'
+ $displaySub[0].=''.&mt('Part: [_1]',$display_part).' '
+ .' '
+ .'('.&mt('Response ID: [_1]',$responseId).')'
+ .' '
+ .' ';
+ if ($hidden) {
+ $displaySub[0].= &mt('Anonymous Survey').' ';
+ } else {
+ my ($trial,$rndseed,$newvariation);
+ if ($type eq 'randomizetry') {
+ $trial = $$record{"$where.$partid.tries"};
+ $rndseed = $$record{"$where.$partid.rndseed"};
+ }
+ if ($$record{"$where.$partid.tries"} eq '') {
+ $displaySub[0].=&mt('Trial not counted');
+ } else {
+ $displaySub[0].=&mt('Trial: [_1]',
+ $$record{"$where.$partid.tries"});
+ if ($rndseed || $lastrndseed{$partid}) {
+ if ($rndseed ne $lastrndseed{$partid}) {
+ $newvariation = ' ('.&mt('New variation this try').')';
+ }
+ }
+ }
+ 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($orders{$partid})) { $orders{$partid}={}; }
+ if ((!exists($orders{$partid}->{$responseId})) || ($trial)) {
+ $orders{$partid}->{$responseId}=
+ &get_order($partid,$responseId,$symb,$uname,$udom,
+ $no_increment,$type,$trial,$rndseed);
+ }
+ $displaySub[0].=''.$newvariation.''; # /nobreak
+ $displaySub[0].=' '.
+ &cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom,$type,$trial,$rndseed).' ';
+ }
}
}
if (exists($$record{"$where.$partid.checkedin"})) {
- $displaySub[1].='Checked in by '.
- $$record{"$where.$partid.checkedin"}.' into slot '.
- $$record{"$where.$partid.checkedin.slot"}.
- ' ';
+ $displaySub[1].=&mt('Checked in by [_1] into slot [_2]',
+ $$record{"$where.$partid.checkedin"},
+ $$record{"$where.$partid.checkedin.slot"}).
+ ' ';
}
if (exists $$record{"$where.$partid.award"}) {
- $displaySub[1].='Part: '.$display_part.' '.
+ $displaySub[1].=''.&mt('Part:').' '.$display_part.' '.
lc($$record{"$where.$partid.award"}).' '.
$mark{$$record{"$where.$partid.solved"}}.
' ';
@@ -4362,12 +4799,12 @@ sub displaySubByDates {
}
$studentTable.=''.$displaySub[0].' '.$displaySub[1];
if ($displaySub[2]) {
- $studentTable.='Manually graded by '.$displaySub[2];
+ $studentTable.=&mt('Manually graded by [_1]',$displaySub[2]);
}
- $studentTable.=' ';
-
+ $studentTable.=' '.
+ &Apache::loncommon::end_data_table_row();
}
- $studentTable.='
';
+ $studentTable.=&Apache::loncommon::end_data_table();
return $studentTable;
}
@@ -4382,21 +4819,26 @@ sub updateGradeByPage {
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(''.&mt('Unable to modify requested student ([_1])',$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).
+ $result.=' '.&mt('Student: ').&nameUserString(undef,$env{'form.fullname'},$uname,$udom).
' '."\n";
$request->print($result);
+
my $navmap = Apache::lonnavmaps::navmap->new();
+ unless (ref($navmap)) {
+ $request->print(&navmap_errormsg());
+ return;
+ }
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.') ');
+ $request->print(''.&mt('Unable to grade requested sequence ([_1]).',$resUrl).' ');
my ($symb)=&get_symb($request);
$request->print(&show_grading_menu_form($symb));
return;
@@ -4404,12 +4846,14 @@ sub updateGradeByPage {
my $iterator = $navmap->getIterator($map->map_start(),
$map->map_finish());
- my $studentTable=' '.
- ''.
- ' Prob. '.
- ' Title '.
- ' Previous Score '.
- ' New Score ';
+ my $studentTable=
+ &Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_header_row().
+ ' '.&mt('Prob.').' '.
+ ' '.&mt('Title').' '.
+ ' '.&mt('Previous Score').' '.
+ ' '.&mt('New Score').' '.
+ &Apache::loncommon::end_data_table_header_row();
$iterator->next(); # skip the first BEGIN_MAP
my $curRes = $iterator->next(); # for "current resource"
@@ -4422,8 +4866,12 @@ sub updateGradeByPage {
my $parts = $curRes->parts();
my $title = $curRes->compTitle();
my $symbx = $curRes->symb();
- $studentTable.=''.$prob.
- (scalar(@{$parts}) == 1 ? '' : ' ('.scalar(@{$parts}).' parts)').' ';
+ $studentTable.=
+ &Apache::loncommon::start_data_table_row().
+ ''.$prob.
+ (scalar(@{$parts}) == 1 ? ''
+ : ' ('.&mt('[quant,_1,part]',scalar(@{$parts}))
+ .')').' ';
$studentTable.=' '.$title.' ';
my %newrecord=();
@@ -4467,10 +4915,10 @@ sub updateGradeByPage {
}
my $display_part=&get_display_part($partid,$curRes->symb());
my $oldstatus = $env{'form.solved'.$question.'_'.$partid};
- $displayPts[0].=' Part: '.$display_part.' = '.
+ $displayPts[0].=' '.&mt('Part').': '.$display_part.' = '.
(($oldstatus eq 'excused') ? 'excused' : $oldpts).
' ';
- $displayPts[1].=' Part: '.$display_part.' = '.
+ $displayPts[1].=' '.&mt('Part').': '.$display_part.' = '.
(($score eq 'excused') ? 'excused' : $newpts).
' ';
$question++;
@@ -4510,18 +4958,18 @@ sub updateGradeByPage {
$studentTable.=''.$displayPts[0].' '.
''.$displayPts[1].' '.
- ' ';
+ &Apache::loncommon::end_data_table_row();
$prob++;
}
$curRes = $iterator->next();
}
- $studentTable.='
';
+ $studentTable.=&Apache::loncommon::end_data_table();
$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.'));
+ my $grademsg=($changeflag == 0 ? &mt('No score was changed or updated.') :
+ &mt('The scores were changed for [quant,_1,problem].',
+ $changeflag));
$request->print($grademsg.$studentTable);
return '';
@@ -4531,7 +4979,7 @@ sub updateGradeByPage {
#
#-------------------------------------------------------------------
-#--------------------Scantron Grading-----------------------------------
+#-------------------- Bubblesheet (Scantron) Grading -------------------
#
#------ start of section for handling grading by page/sequence ---------
@@ -4558,10 +5006,10 @@ Next each scanline is checked for any er
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
+invalid student/employee 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
+homework problems, either way the student/employee ID is looked up into a
username:domain.
During the validation phase the instructor can choose to skip scanlines.
@@ -4609,14 +5057,19 @@ sub defaultFormData {
Return html dropdown of possible sequences to grade
Arguments:
- $symb - $symb of the current resource
+ $symb - $symb of the current resource
+ $map_error - ref to scalar which will container error if
+ $navmap object is unavailable in &getSymbMap().
=cut
sub getSequenceDropDown {
- my ($symb)=@_;
+ my ($symb,$map_error)=@_;
my $result=''."\n";
- my ($titles,$symbx) = &getSymbMap();
+ my ($titles,$symbx) = &getSymbMap($map_error);
+ if (ref($map_error)) {
+ return if ($$map_error);
+ }
my ($curpage)=&Apache::lonnet::decode_symb($symb);
my $ctr=0;
foreach (@$titles) {
@@ -4630,6 +5083,67 @@ sub getSequenceDropDown {
return $result;
}
+my %bubble_lines_per_response; # no. bubble lines for each response.
+ # key is zero-based index - 0, 1, 2 ...
+
+my %first_bubble_line; # First bubble line no. for each bubble.
+
+my %subdivided_bubble_lines; # no. bubble lines for optionresponse,
+ # matchresponse or rankresponse, where
+ # an individual response can have multiple
+ # lines
+
+my %responsetype_per_response; # responsetype for each response
+
+# Save and restore the bubble lines array to the form env.
+
+
+sub save_bubble_lines {
+ foreach my $line (keys(%bubble_lines_per_response)) {
+ $env{"form.scantron.bubblelines.$line"} = $bubble_lines_per_response{$line};
+ $env{"form.scantron.first_bubble_line.$line"} =
+ $first_bubble_line{$line};
+ $env{"form.scantron.sub_bubblelines.$line"} =
+ $subdivided_bubble_lines{$line};
+ $env{"form.scantron.responsetype.$line"} =
+ $responsetype_per_response{$line};
+ }
+}
+
+
+sub restore_bubble_lines {
+ my $line = 0;
+ %bubble_lines_per_response = ();
+ while ($env{"form.scantron.bubblelines.$line"}) {
+ my $value = $env{"form.scantron.bubblelines.$line"};
+ $bubble_lines_per_response{$line} = $value;
+ $first_bubble_line{$line} =
+ $env{"form.scantron.first_bubble_line.$line"};
+ $subdivided_bubble_lines{$line} =
+ $env{"form.scantron.sub_bubblelines.$line"};
+ $responsetype_per_response{$line} =
+ $env{"form.scantron.responsetype.$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
@@ -4642,8 +5156,9 @@ sub getSequenceDropDown {
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,
- &propath($cdom,$cname));
+ $getpropath);
my @possiblenames;
foreach my $filename (sort(@files)) {
($filename)=split(/&/,$filename);
@@ -4686,19 +5201,76 @@ sub scantron_uploads {
=cut
sub scantron_scantab {
- my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
my $result=''."\n";
$result.=' '."\n";
- foreach my $line (<$fh>) {
- my ($name,$descrip)=split(/:/,$line);
- if ($name =~ /^\#/) { next; }
- $result.=''.$descrip.' '."\n";
+ my @lines = &get_scantronformat_file();
+ if (@lines > 0) {
+ foreach my $line (@lines) {
+ next if (($line =~ /^\#/) || ($line eq ''));
+ my ($name,$descrip)=split(/:/,$line);
+ $result.=''.$descrip.' '."\n";
+ }
}
$result.=' '."\n";
-
return $result;
}
+=pod
+
+=item get_scantronformat_file
+
+ Returns an array containing lines from the scantron format file for
+ the domain of the course.
+
+ If a url for a custom.tab file is listed in domain's configuration.db,
+ lines are from this file.
+
+ Otherwise, if a default.tab has been published in RES space by the
+ domainconfig user, lines are from this file.
+
+ Otherwise, fall back to getting lines from the legacy file on the
+ local server: /home/httpd/lonTabs/default_scantronformat.tab
+
+=cut
+
+sub get_scantronformat_file {
+ my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'};
+ my %domconfig = &Apache::lonnet::get_dom('configuration',['scantron'],$cdom);
+ my $gottab = 0;
+ my @lines;
+ if (ref($domconfig{'scantron'}) eq 'HASH') {
+ if ($domconfig{'scantron'}{'scantronformat'} ne '') {
+ my $formatfile = &Apache::lonnet::getfile($Apache::lonnet::perlvar{'lonDocRoot'}.$domconfig{'scantron'}{'scantronformat'});
+ if ($formatfile ne '-1') {
+ @lines = split("\n",$formatfile,-1);
+ $gottab = 1;
+ }
+ }
+ }
+ if (!$gottab) {
+ my $confname = $cdom.'-domainconfig';
+ my $default = $Apache::lonnet::perlvar{'lonDocRoot'}.'/res/'.$cdom.'/'.$confname.'/default.tab';
+ my $formatfile = &Apache::lonnet::getfile($default);
+ if ($formatfile ne '-1') {
+ @lines = split("\n",$formatfile,-1);
+ $gottab = 1;
+ }
+ }
+ if (!$gottab) {
+ my @domains = &Apache::lonnet::current_machine_domains();
+ if (grep(/^\Q$cdom\E$/,@domains)) {
+ my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
+ @lines = <$fh>;
+ close($fh);
+ } else {
+ my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/default_scantronformat.tab');
+ @lines = <$fh>;
+ close($fh);
+ }
+ }
+ return @lines;
+}
+
=pod
=item scantron_CODElist
@@ -4731,11 +5303,11 @@ sub scantron_CODElist {
=cut
sub scantron_CODEunique {
- my $result='
+ my $result='
'.&mt('Yes').'
-
+
'.&mt('No').'
';
@@ -4763,7 +5335,12 @@ sub scantron_selectphase {
my ($r,$file2grade) = @_;
my ($symb)=&get_symb($r);
if (!$symb) {return '';}
- my $sequence_selector=&getSequenceDropDown($symb);
+ my $map_error;
+ my $sequence_selector=&getSequenceDropDown($symb,\$map_error);
+ if ($map_error) {
+ $r->print(' '.&navmap_errormsg().' ');
+ return;
+ }
my $default_form_data=&defaultFormData($symb);
my $grading_menu_button=&show_grading_menu_form($symb);
my $file_selector=&scantron_uploads($file2grade);
@@ -4772,142 +5349,163 @@ sub scantron_selectphase {
my $CODE_unique=&scantron_CODEunique();
my $result;
- # Chunk of form to prompt for a file to grade and how:
-
- $result.= <
-
-
-
-
- $default_form_data
-
-
-
-
-SCANTRONFORM
-
- $r->print($result);
+ $ssi_error = 0;
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.
+ # Chunk of form to prompt for a scantron file upload.
- $r->print(<
-
-
-
-
-SCANTRONFORM
+ '.&Apache::loncommon::end_data_table_row().'
+ '.&Apache::loncommon::end_data_table().'
+');
}
+ # Chunk of form to prompt for a file to grade and how:
+
+ $result.= '
+
+
+
+ '.$default_form_data.'
+ '.&Apache::loncommon::start_data_table('LC_scantron_action').'
+ '.&Apache::loncommon::start_data_table_header_row().'
+
+ '.&mt('Specify file and which Folder/Sequence to grade').'
+
+ '.&Apache::loncommon::end_data_table_header_row().'
+ '.&Apache::loncommon::start_data_table_row().'
+ '.&mt('Sequence to grade:').' '.$sequence_selector.'
+ '.&Apache::loncommon::end_data_table_row().'
+ '.&Apache::loncommon::start_data_table_row().'
+ '.&mt('Filename of bubblesheet data file:').' '.$file_selector.'
+ '.&Apache::loncommon::end_data_table_row().'
+ '.&Apache::loncommon::start_data_table_row().'
+ '.&mt('Format of bubblesheet data file:').' '.$format_selector.'
+ '.&Apache::loncommon::end_data_table_row().'
+ '.&Apache::loncommon::start_data_table_row().'
+ '.&mt('Saved CODEs to validate against:').' '.$CODE_selector.'
+ '.&Apache::loncommon::end_data_table_row().'
+ '.&Apache::loncommon::start_data_table_row().'
+ '.&mt('Each CODE is only to be used once:').' '.$CODE_unique.'
+ '.&Apache::loncommon::end_data_table_row().'
+ '.&Apache::loncommon::start_data_table_row().'
+ '.&mt('Options:').'
+
+ '.&mt('Do only previously skipped records').'
+ '.&mt('Remove all existing corrections').'
+ '.&mt('Skip hidden resources when grading').'
+
+ '.&Apache::loncommon::end_data_table_row().'
+ '.&Apache::loncommon::start_data_table_row().'
+
+
+
+ '.&Apache::loncommon::end_data_table_row().'
+ '.&Apache::loncommon::end_data_table().'
+
+';
+
+ $r->print($result);
+
# Chunk of the form that prompts to view a scoring office file,
# corrected file, skipped records in a file.
- $r->print(<
-
-
- $default_form_data
-
-
-
-
-
-SCANTRONFORM
+ $r->print('
+
+
+ '.$default_form_data.'
+
+ '.&Apache::loncommon::start_data_table('LC_scantron_action').'
+ '.&Apache::loncommon::start_data_table_header_row().'
+
+ '.&mt('Download a scoring office file').'
+
+ '.&Apache::loncommon::end_data_table_header_row().'
+ '.&Apache::loncommon::start_data_table_row().'
+ '.&mt('Filename of scoring office file: [_1]',$file_selector).'
+
+
+ '.&Apache::loncommon::end_data_table_row().'
+ '.&Apache::loncommon::end_data_table().'
+
+
+');
- $r->print(' ');
&Apache::lonpickcode::code_list($r,2);
- $r->print('
');
+
+ $r->print(''.
+ $default_form_data."\n".
+ &Apache::loncommon::start_data_table('LC_scantron_action')."\n".
+ &Apache::loncommon::start_data_table_header_row()."\n".
+ '
+ '.&mt('Review bubblesheet data and submissions for a previously graded folder/sequence')."\n".
+ ' '."\n".
+ &Apache::loncommon::end_data_table_header_row()."\n".
+ &Apache::loncommon::start_data_table_row()."\n".
+ ' '.&mt('Graded folder/sequence:').' '."\n".
+ ' '.$sequence_selector.' '.
+ &Apache::loncommon::end_data_table_row()."\n".
+ &Apache::loncommon::start_data_table_row()."\n".
+ ' '.&mt('Filename of scoring office file:').' '."\n".
+ ' '.$file_selector.' '."\n".
+ &Apache::loncommon::end_data_table_row()."\n".
+ &Apache::loncommon::start_data_table_row()."\n".
+ ' '.&mt('Format of data file:').' '."\n".
+ ' '.$format_selector.' '."\n".
+ &Apache::loncommon::end_data_table_row()."\n".
+ &Apache::loncommon::start_data_table_row()."\n".
+ ' '.&mt('Options').' '."\n".
+ ' '.&mt('Skip hidden resources').' '.
+ &Apache::loncommon::end_data_table_row()."\n".
+ &Apache::loncommon::start_data_table_row()."\n".
+ ''."\n".
+ ' '."\n".
+ ' '."\n".
+ ' '."\n".
+ &Apache::loncommon::end_data_table_row()."\n".
+ &Apache::loncommon::end_data_table()."\n".
+ ' ');
$r->print($grading_menu_button);
- return
+ return;
}
=pod
@@ -4942,8 +5540,8 @@ SCANTRONFORM
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
+ IDstart - column where the student/employee ID starts
+ IDlength - length of the student/employee ID info
Qstart - column where the information from the bubbled
'questions' start
Qlength - number of columns comprising a single bubble line from
@@ -4969,10 +5567,10 @@ SCANTRONFORM
sub get_scantron_config {
my ($which) = @_;
- my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
+ my @lines = &get_scantronformat_file();
my %config;
#FIXME probably should move to XML it has already gotten a bit much now
- foreach my $line (<$fh>) {
+ foreach my $line (@lines) {
my ($name,$descrip)=split(/:/,$line);
if ($name ne $which ) { next; }
chomp($line);
@@ -4985,7 +5583,7 @@ sub get_scantron_config {
$config{'IDstart'}=$config[5];
$config{'IDlength'}=$config[6];
$config{'Qstart'}=$config[7];
- $config{'Qlength'}=$config[8];
+ $config{'Qlength'}=$config[8];
$config{'Qoff'}=$config[9];
$config{'Qon'}=$config[10];
$config{'PaperID'}=$config[11];
@@ -5003,7 +5601,7 @@ sub get_scantron_config {
=item username_to_idmap
- creates a hash keyed by student id with values of the corresponding
+ creates a hash keyed by student/employee ID with values of the corresponding
student username:domain.
Arguments:
@@ -5042,7 +5640,7 @@ sub username_to_idmap {
$whichline - line number of the passed in scanline
$field - type of change to process
(either
- 'ID' -> correct the student ID number
+ 'ID' -> correct the student/employee ID
'CODE' -> correct the CODE
'answer' -> fixup the submitted answers)
@@ -5059,6 +5657,8 @@ sub username_to_idmap {
- 'answer'
'response' - new answer or 'none' if blank
'question' - the bubble line to change
+ 'questionnum' - the question identifier,
+ may include subquestion.
Returns:
$line - the modified scanline
@@ -5071,7 +5671,6 @@ sub username_to_idmap {
sub scantron_fixup_scanline {
my ($scantron_config,$scan_data,$line,$whichline,$field,$args)=@_;
-
if ($field eq 'ID') {
if (length($args->{'newid'}) > $$scantron_config{'IDlength'}) {
return ($line,1,'New value too large');
@@ -5108,7 +5707,7 @@ sub scantron_fixup_scanline {
my $answer=${off}x$length;
if ($args->{'response'} eq 'none') {
&scan_data($scan_data,
- "$whichline.no_bubble.".$args->{'question'},'1');
+ "$whichline.no_bubble.".$args->{'questionnum'},'1');
} else {
if ($on eq 'letter') {
my @alphabet=('A'..'Z');
@@ -5120,7 +5719,7 @@ sub scantron_fixup_scanline {
substr($answer,$args->{'response'},1)=$on;
}
&scan_data($scan_data,
- "$whichline.no_bubble.".$args->{'question'},undef,'1');
+ "$whichline.no_bubble.".$args->{'questionnum'},undef,'1');
}
my $where=$length*($args->{'question'}-1)+$scantron_config->{'Qstart'};
substr($line,$where-1,$length)=$answer;
@@ -5157,6 +5756,39 @@ sub scan_data {
return $scan_data->{$filename.'_'.$key};
}
+# ----- These first few routines are general use routines.----
+
+# Return the number of occurences of a pattern in a string.
+
+sub occurence_count {
+ my ($string, $pattern) = @_;
+
+ my @matches = ($string =~ /$pattern/g);
+
+ return scalar(@matches);
+}
+
+
+# Take a string known to have digits and convert all the
+# digits into letters in the range J,A..I.
+
+sub digits_to_letters {
+ my ($input) = @_;
+
+ my @alphabet = ('J', 'A'..'I');
+
+ my @input = split(//, $input);
+ my $output ='';
+ for (my $i = 0; $i < scalar(@input); $i++) {
+ if ($input[$i] =~ /\d/) {
+ $output .= $alphabet[$input[$i]];
+ } else {
+ $output .= $input[$i];
+ }
+ }
+ return $output;
+}
+
=pod
=item scantron_parse_scanline
@@ -5182,7 +5814,7 @@ sub scan_data {
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
+ ID - student/employee ID
PaperID - if used, the ID number printed on the sheet when the
paper was scanned
FirstName - first name from the sheet
@@ -5216,8 +5848,10 @@ sub scan_data {
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 $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
if (!($$scantron_config{'CODElocation'} eq 0 ||
$$scantron_config{'CODElocation'} eq 'none')) {
@@ -5254,166 +5888,222 @@ sub scantron_parse_scanline {
my $questnum=0;
my $ansnum =1; # Multiple 'answer lines'/question.
- while ($questions) {
+ 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 $answer_length = $$scantron_config{'Qlength'} * $answers_needed;
-
-
-
- $questnum++;
- my $currentquest = substr($questions,0,$answer_length);
- $questions = substr($questions,0,$answer_length)='';
- if (length($currentquest) < $answer_length) { next; }
-
- # Qon letter implies for each slot in currentquest we have:
- # ? or * for doubles a letter in A-Z for a bubble and
- # about anything else (esp. a value of Qoff for missing
- # bubbles.
-
-
- if ($$scantron_config{'Qon'} eq 'letter') {
-
- if ($currentquest =~ /\?/
- || $currentquest =~ /\*/
- || (&occurence_count($currentquest, "[A-Z]") > 1)) {
- push(@{$record{'scantron.doubleerror'}},$questnum);
- for (my $ans = 0; $ans < $answers_needed; $ans++) {
- my $bubble = substr($currentquest, $ans, 1);
- if ($bubble =~ /[A-Z]/ ) {
- $record{"scantron.$ansnum.answer"} = $bubble;
- } else {
- $record{"scantron.$ansnum.answer"}='';
- }
- $ansnum++;
- }
-
- } elsif (!defined($currentquest)
- || (&occurence_count($currentquest, $$scantron_config{'Qoff'}) == length($currentquest))
- || (&occurence_count($currentquest, "[A-Z]") == 0)) {
- for (my $ans = 0; $ans < $answers_needed; $ans++ ) {
- $record{"scantron.$ansnum.answer"}='';
- $ansnum++;
-
- }
- if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) {
- push(@{$record{"scantron.missingerror"}},$questnum);
- $ansnum += $answers_needed;
- }
-
- } else {
- for (my $ans = 0; $ans < $answers_needed; $ans++) {
- $record{"scantron.$ansnum.answer"} = substr($currentquest, $ans, 1);
- $ansnum++;
- }
- }
-
- # Qon 'number' implies each slot gives a digit that indexes the
- # the bubbles filled or Qoff or a non number for unbubbled lines.
- # and *? for double bubbles on a line.
- # these answers are also stored as letters.
-
- } elsif ($$scantron_config{'Qon'} eq 'number') {
- if ($currentquest =~ /\?/
- || $currentquest =~ /\*/
- || (&occurence_count($currentquest, '\d') > 1)) {
- push(@{$record{'scantron.doubleerror'}},$questnum);
- for (my $ans = 0; $ans < $answers_needed; $ans++) {
- my $bubble = substr($currentquest, $ans, 1);
- if ($bubble =~ /\d/) {
- $record{"scantron.$ansnum.answer"} = $alphabet[$bubble];
- } else {
- $record{"scantron.$ansnum.answer"}=' ';
- }
- $ansnum++;
- }
-
- } elsif (!defined($currentquest)
- || (&occurence_count($currentquest,$$scantron_config{'Qoff'}) == length($currentquest))
- || (&occurence_count($currentquest, '\d') == 0)) {
- for (my $ans = 0; $ans < $answers_needed; $ans++ ) {
- $record{"scantron.$ansnum.answer"}='';
- $ansnum++;
+ my $answer_length = ($$scantron_config{'Qlength'} * $answers_needed)
+ || 1;
+ $questnum++;
+ my $quest_id = $questnum;
+ my $currentquest = substr($questions,0,$answer_length);
+ $questions = substr($questions,$answer_length);
+ if (length($currentquest) < $answer_length) { next; }
+
+ if ($subdivided_bubble_lines{$questnum-1} =~ /,/) {
+ my $subquestnum = 1;
+ my $subquestions = $currentquest;
+ my @subanswers_needed =
+ split(/,/,$subdivided_bubble_lines{$questnum-1});
+ foreach my $subans (@subanswers_needed) {
+ my $subans_length =
+ ($$scantron_config{'Qlength'} * $subans) || 1;
+ my $currsubquest = substr($subquestions,0,$subans_length);
+ $subquestions = substr($subquestions,$subans_length);
+ $quest_id = "$questnum.$subquestnum";
+ if (($$scantron_config{'Qon'} eq 'letter') ||
+ ($$scantron_config{'Qon'} eq 'number')) {
+ $ansnum = &scantron_validator_lettnum($ansnum,
+ $questnum,$quest_id,$subans,$currsubquest,$whichline,
+ \@alphabet,\%record,$scantron_config,$scan_data);
+ } else {
+ $ansnum = &scantron_validator_positional($ansnum,
+ $questnum,$quest_id,$subans,$currsubquest,$whichline, \@alphabet,\%record,$scantron_config,$scan_data);
+ }
+ $subquestnum ++;
+ }
+ } else {
+ if (($$scantron_config{'Qon'} eq 'letter') ||
+ ($$scantron_config{'Qon'} eq 'number')) {
+ $ansnum = &scantron_validator_lettnum($ansnum,$questnum,
+ $quest_id,$answers_needed,$currentquest,$whichline,
+ \@alphabet,\%record,$scantron_config,$scan_data);
+ } else {
+ $ansnum = &scantron_validator_positional($ansnum,$questnum,
+ $quest_id,$answers_needed,$currentquest,$whichline,
+ \@alphabet,\%record,$scantron_config,$scan_data);
+ }
+ }
+ }
+ $record{'scantron.maxquest'}=$questnum;
+ return \%record;
+}
- }
- if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) {
- push(@{$record{"scantron.missingerror"}},$questnum);
- $ansnum += $answers_needed;
- }
+sub scantron_validator_lettnum {
+ my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,$whichline,
+ $alphabet,$record,$scantron_config,$scan_data) = @_;
+
+ # Qon 'letter' implies for each slot in currquest we have:
+ # ? or * for doubles, a letter in A-Z for a bubble, and
+ # about anything else (esp. a value of Qoff) for missing
+ # bubbles.
+ #
+ # Qon 'number' implies each slot gives a digit that indexes the
+ # bubbles filled, or Qoff, or a non-number for unbubbled lines,
+ # and * or ? for double bubbles on a single line.
+ #
- } else {
- $currentquest = &digits_to_letters($currentquest);
- for (my $ans =0; $ans < $answers_needed; $ans++) {
- $record{"scantron.$ansnum.answer"} = substr($currentquest, $ans, 1);
- $ansnum++;
- }
- }
- } else {
+ my $matchon;
+ if ($$scantron_config{'Qon'} eq 'letter') {
+ $matchon = '[A-Z]';
+ } elsif ($$scantron_config{'Qon'} eq 'number') {
+ $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 @singlelines = split('',$currquest);
+ foreach my $entry (@singlelines) {
+ $occurrences = &occurence_count($entry,$matchon);
+ if ($occurrences > 1) {
+ last;
+ }
+ }
+ } else {
+ $occurrences = &occurence_count($currquest,$matchon);
+ }
+ if (($currquest =~ /\?/ || $currquest =~ /\*/) || ($occurrences > 1)) {
+ push(@{$record->{'scantron.doubleerror'}},$quest_id);
+ for (my $ans=0; $ans<$answers_needed; $ans++) {
+ my $bubble = substr($currquest,$ans,1);
+ if ($bubble =~ /$matchon/ ) {
+ if ($$scantron_config{'Qon'} eq 'number') {
+ if ($bubble == 0) {
+ $bubble = 10;
+ }
+ $record->{"scantron.$ansnum.answer"} =
+ $alphabet->[$bubble-1];
+ } else {
+ $record->{"scantron.$ansnum.answer"} = $bubble;
+ }
+ } else {
+ $record->{"scantron.$ansnum.answer"}='';
+ }
+ $ansnum++;
+ }
+ } elsif (!defined($currquest)
+ || (&occurence_count($currquest, $$scantron_config{'Qoff'}) == length($currquest))
+ || (&occurence_count($currquest,$matchon) == 0)) {
+ for (my $ans=0; $ans<$answers_needed; $ans++ ) {
+ $record->{"scantron.$ansnum.answer"}='';
+ $ansnum++;
+ }
+ if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) {
+ push(@{$record->{'scantron.missingerror'}},$quest_id);
+ }
+ } else {
+ if ($$scantron_config{'Qon'} eq 'number') {
+ $currquest = &digits_to_letters($currquest);
+ }
+ for (my $ans=0; $ans<$answers_needed; $ans++) {
+ my $bubble = substr($currquest,$ans,1);
+ $record->{"scantron.$ansnum.answer"} = $bubble;
+ $ansnum++;
+ }
+ }
+ return $ansnum;
+}
- # Otherwise there's a positional notation;
- # each bubble line requires Qlength items, and there are filled in
- # bubbles for each case where there 'Qon' characters.
- #
+sub scantron_validator_positional {
+ my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,
+ $whichline,$alphabet,$record,$scantron_config,$scan_data) = @_;
- my @array=split($$scantron_config{'Qon'},$currentquest,-1);
+ # Otherwise there's a positional notation;
+ # each bubble line requires Qlength items, and there are filled in
+ # bubbles for each case where there 'Qon' characters.
+ #
- # If the split only giveas us one element.. the full length of the
- # answser string, no bubbles are filled in:
+ my @array=split($$scantron_config{'Qon'},$currquest,-1);
- if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) {
- for (my $ans = 0; $ans < $answers_needed; $ans++ ) {
- $record{"scantron.$ansnum.answer"}='';
- $ansnum++;
+ # If the split only gives us one element.. the full length of the
+ # answer string, no bubbles are filled in:
- }
- if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) {
- push(@{$record{"scantron.missingerror"}},$questnum);
- }
- } elsif (scalar(@array) lt 2) {
+ if ($answers_needed eq '') {
+ return;
+ }
- my $location = length($array[0]);
- my $line_num = $location / $$scantron_config{'Qlength'};
- my $bubble = $alphabet[$location % $$scantron_config{'Qlength'}];
+ if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) {
+ for (my $ans=0; $ans<$answers_needed; $ans++ ) {
+ $record->{"scantron.$ansnum.answer"}='';
+ $ansnum++;
+ }
+ if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) {
+ push(@{$record->{"scantron.missingerror"}},$quest_id);
+ }
+ } elsif (scalar(@array) == 2) {
+ my $location = length($array[0]);
+ my $line_num = int($location / $$scantron_config{'Qlength'});
+ my $bubble = $alphabet->[$location % $$scantron_config{'Qlength'}];
+ for (my $ans=0; $ans<$answers_needed; $ans++) {
+ if ($ans eq $line_num) {
+ $record->{"scantron.$ansnum.answer"} = $bubble;
+ } else {
+ $record->{"scantron.$ansnum.answer"} = ' ';
+ }
+ $ansnum++;
+ }
+ } else {
+ # If there's more than one instance of a bubble character
+ # That's a double bubble; with positional notation we can
+ # 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 $doubleerror = 0;
+ while (($currquest >= $$scantron_config{'Qlength'}) &&
+ (!$doubleerror)) {
+ my $currline = substr($currquest,0,$$scantron_config{'Qlength'});
+ $currquest = substr($currquest,$$scantron_config{'Qlength'});
+ my @currarray = split($$scantron_config{'Qon'},$currline,-1);
+ if (length(@currarray) > 2) {
+ $doubleerror = 1;
+ }
+ }
+ if ($doubleerror) {
+ push(@{$record->{'scantron.doubleerror'}},$quest_id);
+ }
+ } else {
+ push(@{$record->{'scantron.doubleerror'}},$quest_id);
+ }
+ my $item = $ansnum;
+ for (my $ans=0; $ans<$answers_needed; $ans++) {
+ $record->{"scantron.$item.answer"} = '';
+ $item ++;
+ }
- for (my $ans = 0; $ans < $answers_needed; $ans++) {
- if ($ans eq $line_num) {
- $record{"scantron.$ansnum.answer"} = $bubble;
- } else {
- $record{"scantron.$ansnum.answer"} = ' ';
- }
- $ansnum++;
- }
- }
- # If there's more than one instance of a bubble character
- # That's a double bubble; with positional notation we can
- # record all the bubbles filled in as well as the
- # fact this response consists of multiple bubbles.
- #
- else {
- push(@{$record{'scantron.doubleerror'}},$questnum);
-
- my $first_answer = $ansnum;
- for (my $ans =0; $ans < $answers_needed; $ans++) {
- my $item = $first_answer+$ans;
- $record{"scantron.$item.answer"} = '';
- }
-
- my @ans=@array;
- my $i=0;
- my $increment = 0;
- while ($#ans) {
- $i+=length($ans[0]) + $increment;
- my $line = int($i/$$scantron_config{'Qlength'} + $first_answer);
- my $bubble = $i%$$scantron_config{'Qlength'};
- $record{"scantron.$line.answer"}.=$alphabet[$bubble];
- shift(@ans);
- $increment = 1;
- }
- $ansnum += $answers_needed;
- }
- }
+ my @ans=@array;
+ my $i=0;
+ my $increment = 0;
+ while ($#ans) {
+ $i+=length($ans[0]) + $increment;
+ my $line = int($i/$$scantron_config{'Qlength'} + $ansnum);
+ my $bubble = $i%$$scantron_config{'Qlength'};
+ $record->{"scantron.$line.answer"}.=$alphabet->[$bubble];
+ shift(@ans);
+ $increment = 1;
+ }
+ $ansnum += $answers_needed;
}
- $record{'scantron.maxquest'}=$questnum;
- return \%record;
+ return $ansnum;
}
=pod
@@ -5553,7 +6243,8 @@ sub scantron_process_corrections {
&scantron_fixup_scanline(\%scantron_config,$scan_data,$line,
$which,'answer',
{ 'question'=>$question,
- 'response'=>$env{"form.scantron_correct_Q_$question"}});
+ 'response'=>$env{"form.scantron_correct_Q_$question"},
+ 'questionnum'=>$env{"form.scantron_questionnum_Q_$question"}});
if ($err) { last; }
}
}
@@ -5658,7 +6349,7 @@ sub remember_current_skipped {
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.");
+ $r->print(&mt("An error occurred ([_1]) when trying to remove the existing corrections.",$result));
}
}
@@ -5682,25 +6373,25 @@ sub scantron_warning_screen {
$CODElist=$env{'form.scantron_CODElist'};
if ($env{'form.scantron_CODElist'} eq '') { $CODElist='None '; }
$CODElist=
- 'List of CODES to validate against: '.
+ ''.&mt('List of CODES to validate against:').' '.
$env{'form.scantron_CODElist'}.' ';
}
- return (<
-Please double check the information
- below before clicking on '$button_text'
+
+'.&mt('Please double check the information below before clicking on \'[_1]\'',&mt($button_text)).'
-Sequence to be Graded: $title
-Data File that will be used: $env{'form.scantron_selectfile'}
-$CODElist
+'.&mt('Sequence to be Graded:').' '.$title.'
+'.&mt('Data File that will be used:').' '.$env{'form.scantron_selectfile'}.'
+'.$CODElist.'
- If this information is correct, please click on '$button_text'.
- 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.').'
-STUFF
+');
}
=pod
@@ -5721,23 +6412,23 @@ sub scantron_do_warning {
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.
");
+ $r->print("".&mt('You have forgotten 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
');
+ $r->print(''.&mt('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.
');
+ $r->print(''.&mt("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.
');
+ $r->print(''.&mt(:You have not selected the format of the student's response data.").'
');
}
} else {
my $warning=&scantron_warning_screen('Grading: Validate Records');
- $r->print(<
+ $r->print('
+'.$warning.'
+
-STUFF
+');
}
$r->print(" ".&show_grading_menu_form($symb));
return '';
@@ -5772,6 +6463,10 @@ SCANTRONFORM
' '."\n";
$chunk .=
' '."\n";
+ $chunk .=
+ ' '."\n";
+ $chunk .=
+ ' '."\n";
$result .= $chunk;
$line++;
}
@@ -5816,10 +6511,15 @@ sub scantron_validate_file {
if ($env{'form.scantron_corrections'}) {
&scantron_process_corrections($r);
}
- $r->print("Gathering necessary info.
");$r->rflush();
+ $r->print(''.&mt('Gathering necessary information.').'
');$r->rflush();
#get the student pick code ready
$r->print(&Apache::loncommon::studentbrowser_javascript());
- my $max_bubble=&scantron_get_maxbubble();
+ my $nav_error;
+ my $max_bubble=&scantron_get_maxbubble(\$nav_error);
+ if ($nav_error) {
+ $r->print(&navmap_errormsg());
+ return '';
+ }
my $result=&scantron_form_start($max_bubble).$default_form_data;
$r->print($result);
@@ -5836,7 +6536,7 @@ sub scantron_validate_file {
my $stop=0;
while (!$stop && $currentphase < scalar(@validate_phases)) {
- $r->print(" Validating ".$validate_phases[$currentphase]."
");
+ $r->print(&mt('Validating '.$validate_phases[$currentphase]).' ');
$r->rflush();
my $which="scantron_validate_".$validate_phases[$currentphase];
{
@@ -5846,28 +6546,37 @@ sub scantron_validate_file {
}
if (!$stop) {
my $warning=&scantron_warning_screen('Start Grading');
- $r->print(<
-$warning
-
-
-STUFF
-
+ $r->print(&mt('Validation process complete.').' '.
+ $warning.
+ &mt('Perform verification for each student after storage of submissions?').
+ ' '.
+ ' '.&mt('Yes').' '.
+ (' 'x3).''.
+ ' '.&mt('No').
+ ' '.
+ &mt('Grading will take longer if you use verification.').' '.
+ &mt("Alternatively, the 'Review bubblesheet data' utility (see grading menu) can be used for all students after grading is complete.").' '.
+ ' '.
+ ' '."\n");
} else {
$r->print(' ');
$r->print(" ");
}
if ($stop) {
if ($validate_phases[$currentphase] eq 'sequence') {
- $r->print(' ');
- $r->print(' this error ');
+ $r->print(' ');
+ $r->print(' '.&mt('this error').' ');
- $r->print(" Or click the 'Grading Menu' button to start over.
");
+ $r->print(" ".&mt("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.");
+ if ($validate_phases[$currentphase] eq 'doublebubble' || $validate_phases[$currentphase] eq 'missingbubbles') {
+ $r->print(' ');
+ } else {
+ $r->print(' ');
+ }
+ $r->print(' '.&mt('using corrected info').' ');
+ $r->print(" ");
+ $r->print(" ".&mt("this scanline saving it for later."));
}
}
$r->print(" ".&show_grading_menu_form($symb));
@@ -5928,7 +6637,10 @@ sub scantron_remove_scan_data {
}
my $result;
if (@todelete) {
- $result=&Apache::lonnet::del('nohist_scantrondata',\@todelete,$cdom,$cname);
+ $result = &Apache::lonnet::del('nohist_scantrondata',
+ \@todelete,$cdom,$cname);
+ } else {
+ $result = 'ok';
}
return $result;
}
@@ -6219,6 +6931,10 @@ sub scantron_validate_sequence {
my ($r,$currentphase) = @_;
my $navmap=Apache::lonnavmaps::navmap->new();
+ unless (ref($navmap)) {
+ $r->print(&navmap_errormsg());
+ return (1,$currentphase);
+ }
my (undef,undef,$sequence)=
&Apache::lonnet::decode_symb($env{'form.selectpage'});
@@ -6238,14 +6954,7 @@ sub scantron_validate_sequence {
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) = @_;
@@ -6257,8 +6966,13 @@ sub scantron_validate_ID {
#get scantron line setup
my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
my ($scanlines,$scan_data)=&scantron_getfile();
-
- &scantron_get_maxbubble(); # parse needs the bubble_lines.. array.
+
+ my $nav_error;
+ &scantron_get_maxbubble(\$nav_error); # parse needs the bubble_lines.. array.
+ if ($nav_error) {
+ $r->print(&navmap_errormsg());
+ return(1,$currentphase);
+ }
my %found=('ids'=>{},'usernames'=>{});
for (my $i=0;$i<=$scanlines->{'count'};$i++) {
@@ -6311,67 +7025,43 @@ sub scantron_validate_ID {
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, probably 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");
+ $r->print("
".&mt("An error was detected ($error) ".
+ " for PaperID [_1] ",
+ $$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("".&mt("An error was detected ($error) ".
+ " in scanline [_1]
[_2] ",
+ $i,$line)." \n");
+ }
+ my $message="".&mt("The ID on the form is [_1] ".
+ "The name on the paper is [_2],[_3]",
+ $$scan_record{'scantron.ID'},
+ $$scan_record{'scantron.LastName'},
+ $$scan_record{'scantron.FirstName'})."
";
$r->print(' '."\n");
$r->print(' '."\n");
+ # Array populated for doublebubble or
+ my @lines_to_correct; # missingbubble errors to build javascript
+ # to validate radio button checking
+
if ($error =~ /ID$/) {
if ($error eq 'incorrectID') {
- $r->print("The encoded ID is not in the classlist\n");
+ $r->print("".&mt("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("".&mt("The encoded ID has also been used by a previous paper [_1]",$arg)."
\n");
}
$r->print($message);
- $r->print("How should I handle this? \n");
+ $r->print("
".&mt("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
@@ -6384,14 +7074,14 @@ sub scantron_get_correction {
$r->print(' ');
} elsif ($error =~ /CODE$/) {
if ($error eq 'incorrectCODE') {
- $r->print("The encoded CODE is not in the list of possible CODEs
\n");
+ $r->print("".&mt("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("".&mt("The encoded CODE has also been used by a previous paper [_1], and CODEs are supposed to be unique.",join(', ',@{$arg}))."
\n");
}
- $r->print("The CODE on the form is '".
- $$scan_record{'scantron.CODE'}."' \n");
+ $r->print("
".&mt("The CODE on the form is '[_1]' ",
+ $$scan_record{'scantron.CODE'})." \n");
$r->print($message);
- $r->print("
How should I handle this? \n");
+ $r->print("
".&mt("How should I handle this?")." \n");
$r->print("\n ");
my $i=0;
if ($error eq 'incorrectCODE'
@@ -6400,16 +7090,27 @@ sub scantron_get_correction {
if ($closest > 0) {
foreach my $testcode (@{$closest}) {
my $checked='';
- if (!$i) { $checked=' checked="checked" '; }
- $r->print(" Use the similar CODE ".$testcode." instead. ");
+ if (!$i) { $checked=' checked="checked"'; }
+ $r->print("
+
+
+ ".&mt("Use the similar CODE [_1] instead.",
+ "".$testcode." ")."
+
+ ");
$r->print("\n ");
$i++;
}
}
}
if ($$scan_record{'scantron.CODE'}=~/\S/ ) {
- my $checked; if (!$i) { $checked=' checked="checked" '; }
- $r->print(" Use the CODE ".$$scan_record{'scantron.CODE'}." that is was on the paper, ignoring the error. ");
+ my $checked; if (!$i) { $checked=' checked="checked"'; }
+ $r->print("
+
+
+ ".&mt("Use the CODE [_1] that is was on the paper, ignoring the error.",
+ "".$$scan_record{'scantron.CODE'}." ")."
+ ");
$r->print("\n ");
}
@@ -6431,115 +7132,274 @@ ENDSCRIPT
"&curCODE=".&escape($$scan_record{'scantron.CODE'}).
"&scantron_selectfile=".&escape($env{'form.scantron_selectfile'});
if ($env{'form.scantron_CODElist'} =~ /\S/) {
- $r->print(" Select a CODE from the list of all CODEs and use it. Selected CODE is ");
+ $r->print("
+
+
+ ".&mt("[_1]Select[_2] a CODE from the list of all CODEs and use it.",
+ ""," ")."
+
+ ".&mt("Selected CODE is [_1]",' '));
$r->print("\n ");
}
- $r->print(" Use as the CODE.");
+ $r->print("
+
+
+ ".&mt("Use [_1] 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("".&mt("There have been multiple bubbles scanned for some question(s)")."
\n");
+
+ # 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);
+
$r->print(' ');
+ $line_list.'" />');
$r->print($message);
- $r->print("Please indicate which bubble should be used for grading
");
+ $r->print("".&mt("Please indicate which bubble should be used for grading")."
");
foreach my $question (@{$arg}) {
- my $selected = &get_response_bubbles($scan_record, $question);
- my @select_array = split(/:/,$selected);
- &scantron_bubble_selector($r,$scan_config,$question,
- @select_array);
+ my @linenums = &prompt_for_corrections($r,$question,$scan_config,
+ $scan_record, $error);
+ push(@lines_to_correct,@linenums);
}
+ $r->print(&verify_bubbles_checked(@lines_to_correct));
} elsif ($error eq 'missingbubble') {
- $r->print("There have been no bubbles scanned for some question(s)
\n");
+ $r->print("".&mt("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("".&mt("Please indicate which bubble should be used for grading.")."
");
+ $r->print(&mt("Some questions have no scanned bubbles.")."\n");
+
+ # The form field scantron_questions is actually a list of line numbers not
+ # a list of question numbers. Therefore:
+ #
+
+ my $line_list = &questions_to_line_list($arg);
+
$r->print(' ');
+ $line_list.'" />');
foreach my $question (@{$arg}) {
- my $selected = &get_response_bubbles($scan_record, $question);
- &scantron_bubble_selector($r,$scan_config,$question);
+ my @linenums = &prompt_for_corrections($r,$question,$scan_config,
+ $scan_record, $error);
+ push(@lines_to_correct,@linenums);
}
+ $r->print(&verify_bubbles_checked(@lines_to_correct));
} else {
$r->print("\n");
+}
+sub verify_bubbles_checked {
+ my (@ansnums) = @_;
+ my $ansnumstr = join('","',@ansnums);
+ my $warning = &mt("A bubble or 'No bubble' selection has not been made for one or more lines.");
+ my $output = (<
+function verify_bubble_radio(form) {
+ var ansnumArray = new Array ("$ansnumstr");
+ var need_bubble_count = 0;
+ for (var i=0; i 1) {
+ var bubble_picked = 0;
+ for (var j=0; j
+ENDSCRIPT
+ return $output;
}
=pod
-=item scantron_bubble_selector
-
- Generates the html radiobuttons to correct a single bubble line
- possibly showing the existing the selected bubbles if known
+=item questions_to_line_list
- Arguments:
- $r - Apache request object
- $scan_config - hash from &get_scantron_config()
- $quest - number of the bubble line to make a corrector for
- $lines - array of answer lines.
+Converts a list of questions into a string of comma separated
+line numbers in the answer sheet used by the questions. This is
+used to fill in the scantron_questions form field.
+
+ Arguments:
+ questions - Reference to an array of questions.
=cut
-sub scantron_bubble_selector {
- my ($r,$scan_config,$quest,@lines)=@_;
- my $max=$$scan_config{'Qlength'};
+sub questions_to_line_list {
+ my ($questions) = @_;
+ my @lines;
+
+ foreach my $item (@{$questions}) {
+ my $question = $item;
+ my ($first,$count,$last);
+ if ($item =~ /^(\d+)\.(\d+)$/) {
+ $question = $1;
+ my $subquestion = $2;
+ $first = $first_bubble_line{$question-1} + 1;
+ my @subans = split(/,/,$subdivided_bubble_lines{$question-1});
+ my $subcount = 1;
+ while ($subcount<$subquestion) {
+ $first += $subans[$subcount-1];
+ $subcount ++;
+ }
+ $count = $subans[$subquestion-1];
+ } else {
+ $first = $first_bubble_line{$question-1} + 1;
+ $count = $bubble_lines_per_response{$question-1};
+ }
+ $last = $first+$count-1;
+ push(@lines, ($first..$last));
+ }
+ return join(',', @lines);
+}
- my $scmode=$$scan_config{'Qon'};
+=pod
- my $bubble_length = scalar(@lines);
+=item prompt_for_corrections
+Prompts for a potentially multiline correction to the
+user's bubbling (factors out common code from scantron_get_correction
+for multi and missing bubble cases).
- if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; }
+ Arguments:
+ $r - Apache request object.
+ $question - The question number to prompt for.
+ $scan_config - The scantron file configuration hash.
+ $scan_record - Reference to the hash that has the the parsed scanlines.
+ $error - Type of error
+
+ Implicit inputs:
+ %bubble_lines_per_response - Starting line numbers for each question.
+ Numbered from 0 (but question numbers are from
+ 1.
+ %first_bubble_line - Starting bubble line for each question.
+ %subdivided_bubble_lines - optionresponse, matchresponse and rankresponse
+ type problems render as separate sub-questions,
+ in exam mode. This hash contains a
+ comma-separated list of the lines per
+ sub-question.
+ %responsetype_per_response - essayresponse, formularesponse,
+ stringresponse, imageresponse, reactionresponse,
+ and organicresponse type problem parts can have
+ multiple lines per response if the weight
+ assigned exceeds 10. In this case, only
+ one bubble per line is permitted, but more
+ than one line might contain bubbles, e.g.
+ bubbling of: line 1 - J, line 2 - J,
+ line 3 - B would assign 22 points.
- my $response = $quest-1;
- my $lines = $bubble_lines_per_response{$response};
+=cut
- my $total_lines = $lines*2;
- my @alphabet=('A'..'Z');
- $r->print("$quest ");
+sub prompt_for_corrections {
+ my ($r, $question, $scan_config, $scan_record, $error) = @_;
+ my ($current_line,$lines);
+ my @linenums;
+ my $questionnum = $question;
+ 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});
+ my $subcount = 1;
+ while ($subcount<$subquestion) {
+ $current_line += $subans[$subcount-1];
+ $subcount ++;
+ }
+ $lines = $subans[$subquestion-1];
+ } else {
+ $current_line = $first_bubble_line{$question-1} + 1 ;
+ $lines = $bubble_lines_per_response{$question-1};
+ }
+ 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'.").' ');
+ } 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,
+ $questionnum,$error,split('', $selected));
+ push(@linenums,$current_line);
+ $current_line++;
+ }
+ if ($lines > 1) {
+ $r->print(" ");
+ }
+ return @linenums;
+}
- for (my $l = 0; $l < $lines; $l++) {
- if ($l != 0) {
- $r->print('');
- }
- my @selected = split(//,$lines[$l]);
- for (my $i=0;$i<$max;$i++) {
- $r->print("\n".'');
- if ($selected[0] eq $alphabet[$i]) {
- $r->print('X');
- shift(@selected) ;
- } else {
- $r->print(' ');
- }
- $r->print(' ');
-
- }
+=pod
- if ($l == 0) {
- my $lspan = $total_lines * 2; # 2 table rows per bubble line.
+=item scantron_bubble_selector
+
+ Generates the html radiobuttons to correct a single bubble line
+ possibly showing the existing the selected bubbles if known
- $r->print(' No bubble ');
-
- }
+ Arguments:
+ $r - Apache request object
+ $scan_config - hash from &get_scantron_config()
+ $line - Number of the line being displayed.
+ $questionnum - Question number (may include subquestion)
+ $error - Type of error.
+ @selected - Array of bubbles picked on this line.
- $r->print(' ');
+=cut
- # FIXME: This may have to be a bit more clever for
- # multiline questions (different values e.g..).
+sub scantron_bubble_selector {
+ my ($r,$scan_config,$line,$questionnum,$error,@selected)=@_;
+ my $max=$$scan_config{'Qlength'};
- for (my $i=0;$i<$max;$i++) {
- $r->print("\n".
- ' '.$alphabet[$i]." ");
- }
- $r->print(' ');
+ my $scmode=$$scan_config{'Qon'};
+ if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; }
-
- }
- $r->print('
');
+ my @alphabet=('A'..'Z');
+ $r->print(&Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_row());
+ $r->print('');
+ for (my $i=0;$i<$max+1;$i++) {
+ $r->print("\n".'');
+ if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) }
+ else { $r->print(' '); }
+ $r->print(' ');
+ }
+ $r->print(&Apache::loncommon::end_data_table_row().
+ &Apache::loncommon::start_data_table_row());
+ for (my $i=0;$i<$max;$i++) {
+ $r->print("\n".
+ ' '.$alphabet[$i]." ");
+ }
+ my $nobub_checked = ' ';
+ if ($error eq 'missingbubble') {
+ $nobub_checked = ' checked = "checked" ';
+ }
+ $r->print("\n".' '.&mt('No bubble').
+ ' '."\n".' ');
+ $r->print(&Apache::loncommon::end_data_table_row().
+ &Apache::loncommon::end_data_table());
}
=pod
@@ -6665,7 +7525,12 @@ sub scantron_validate_CODE {
my %allcodes=&get_codes();
- &scantron_get_maxbubble(); # parse needs the lines per response array.
+ my $nav_error;
+ &scantron_get_maxbubble(\$nav_error); # parse needs the lines per response array.
+ if ($nav_error) {
+ $r->print(&navmap_errormsg());
+ return(1,$currentphase);
+ }
my ($scanlines,$scan_data)=&scantron_getfile();
for (my $i=0;$i<=$scanlines->{'count'};$i++) {
@@ -6696,7 +7561,7 @@ sub scantron_validate_CODE {
$line,'duplicateCODE',$usedCODEs{$CODE});
return(1,$currentphase);
}
- push (@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'});
+ push(@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'});
}
return (0,$currentphase+1);
}
@@ -6719,8 +7584,12 @@ sub scantron_validate_doublebubble {
#get scantron line setup
my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
my ($scanlines,$scan_data)=&scantron_getfile();
-
- &scantron_get_maxbubble(); # parse needs the bubble line array.
+ my $nav_error;
+ &scantron_get_maxbubble(\$nav_error); # parse needs the bubble line array.
+ if ($nav_error) {
+ $r->print(&navmap_errormsg());
+ return(1,$currentphase);
+ }
for (my $i=0;$i<=$scanlines->{'count'};$i++) {
my $line=&scantron_get_line($scanlines,$scan_data,$i);
@@ -6736,24 +7605,9 @@ sub scantron_validate_doublebubble {
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 results to $env{'form.scantron_maxbubble'},
- $env{'form.scantron.bubble_lines.n'} and
- $env{'form.scantron.first_bubble_line.n'}
- which are the total number of bubble, lines, the number of bubble
- lines for reponse n and number of the first bubble line for response n.
-
-=cut
-sub scantron_get_maxbubble {
+sub scantron_get_maxbubble {
+ my ($nav_error) = @_;
if (defined($env{'form.scantron_maxbubble'}) &&
$env{'form.scantron_maxbubble'}) {
&restore_bubble_lines();
@@ -6764,55 +7618,85 @@ sub scantron_get_maxbubble {
&Apache::lonnet::decode_symb($env{'form.selectpage'});
my $navmap=Apache::lonnavmaps::navmap->new();
+ unless (ref($navmap)) {
+ if (ref($nav_error)) {
+ $$nav_error = 1;
+ }
+ return;
+ }
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 $uname = $env{'user.name'};
+ my $udom = $env{'user.domain'};
my $cid = $env{'request.course.id'};
my $total_lines = 0;
%bubble_lines_per_response = ();
%first_bubble_line = ();
+ %subdivided_bubble_lines = ();
+ %responsetype_per_response = ();
-
my $response_number = 0;
my $bubble_line = 0;
foreach my $resource (@resources) {
- my $symb = $resource->symb();
- &Apache::lonxml::clear_bubble_lines_for_part();
- 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 $lines = $analysis{"$part_id.bubble_lines"};;
-
- # TODO - make this a persistent hash not an array.
-
-
- $first_bubble_line{$response_number} = $bubble_line;
- $bubble_lines_per_response{$response_number} = $lines;
- $response_number++;
+ my ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname,$udom);
+ if ((ref($analysis) eq 'HASH') && (ref($parts) eq 'ARRAY')) {
+ foreach my $part_id (@{$parts}) {
+ my $lines;
+
+ # TODO - make this a persistent hash not an array.
+
+ # optionresponse, matchresponse and rankresponse type items
+ # render as separate sub-questions in exam mode.
+ if (($analysis->{$part_id.'.type'} eq 'optionresponse') ||
+ ($analysis->{$part_id.'.type'} eq 'matchresponse') ||
+ ($analysis->{$part_id.'.type'} eq 'rankresponse')) {
+ my ($numbub,$numshown);
+ if ($analysis->{$part_id.'.type'} eq 'optionresponse') {
+ if (ref($analysis->{$part_id.'.options'}) eq 'ARRAY') {
+ $numbub = scalar(@{$analysis->{$part_id.'.options'}});
+ }
+ } elsif ($analysis->{$part_id.'.type'} eq 'matchresponse') {
+ if (ref($analysis->{$part_id.'.items'}) eq 'ARRAY') {
+ $numbub = scalar(@{$analysis->{$part_id.'.items'}});
+ }
+ } elsif ($analysis->{$part_id.'.type'} eq 'rankresponse') {
+ if (ref($analysis->{$part_id.'.foils'}) eq 'ARRAY') {
+ $numbub = scalar(@{$analysis->{$part_id.'.foils'}});
+ }
+ }
+ 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) {
+ $inner_bubble_lines++;
+ }
+ for (my $i=0; $i<$numshown; $i++) {
+ $subdivided_bubble_lines{$response_number} .=
+ $inner_bubble_lines.',';
+ }
+ $subdivided_bubble_lines{$response_number} =~ s/,$//;
+ $lines = $numshown * $inner_bubble_lines;
+ } else {
+ $lines = $analysis->{"$part_id.bubble_lines"};
+ }
- $bubble_line += $lines;
- $total_lines += $lines;
- }
+ $first_bubble_line{$response_number} = $bubble_line;
+ $bubble_lines_per_response{$response_number} = $lines;
+ $responsetype_per_response{$response_number} =
+ $analysis->{$part_id.'.type'};
+ $response_number++;
+ $bubble_line += $lines;
+ $total_lines += $lines;
+ }
+ }
}
- &Apache::lonnet::delenv('scantron\.');
+ &Apache::lonnet::delenv('scantron.');
&save_bubble_lines();
$env{'form.scantron_maxbubble'} =
@@ -6820,16 +7704,6 @@ sub scantron_get_maxbubble {
return $env{'form.scantron_maxbubble'};
}
-=pod
-
-=item scantron_validate_missingbubbles
-
- Validates all scanlines in the selected file to not have any
- answers that don't have bubbles that have not been verified
- to be bubble free.
-
-=cut
-
sub scantron_validate_missingbubbles {
my ($r,$currentphase) = @_;
#get student info
@@ -6839,7 +7713,11 @@ sub scantron_validate_missingbubbles {
#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();
+ my $nav_error;
+ my $max_bubble=&scantron_get_maxbubble(\$nav_error);
+ if ($nav_error) {
+ 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);
@@ -6848,8 +7726,29 @@ sub scantron_validate_missingbubbles {
$scan_data);
if (!defined($$scan_record{'scantron.missingerror'})) { next; }
my @to_correct;
+
+ # Probably here's where the error is...
+
foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) {
- if ($missing > $max_bubble) { next; }
+ 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;
+ } else {
+ if (!defined($first_bubble_line{$missing - 1})) { next; }
+ $lastbubble = $first_bubble_line{$missing - 1} + $bubble_lines_per_response{$missing - 1};
+ }
+ if ($lastbubble > $max_bubble) { next; }
push(@to_correct,$missing);
}
if (@to_correct) {
@@ -6862,35 +7761,15 @@ sub scantron_validate_missingbubbles {
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 '';}
+ if (!$symb) {
+ return '';
+ }
my $default_form_data=&defaultFormData($symb);
my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
@@ -6898,9 +7777,41 @@ sub scantron_process_students {
my $classlist=&Apache::loncoursedata::get_classlist();
my %idmap=&username_to_idmap($classlist);
my $navmap=Apache::lonnavmaps::navmap->new();
+ unless (ref($navmap)) {
+ $r->print(&navmap_errormsg());
+ return '';
+ }
my $map=$navmap->getResourceByUrl($sequence);
my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
-# $r->print("geto ".scalar(@resources)." ");
+ 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) {
+ $r->print(&navmap_errormsg());
+ return '';
+ }
+
+ my ($uname,$udom);
my $result= <
@@ -6909,19 +7820,40 @@ SCANTRONFORM
$r->print($result);
my @delayqueue;
- my %completedstudents;
+ my (%completedstudents,%scandata);
+ my $lock=&Apache::lonnet::set_lock(&mt('Grading bubblesheet exam'));
my $count=&get_todo_count($scanlines,$scan_data);
- my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron Status',
- 'Scantron Progress',$count,
+ my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Bubblesheet Status',
+ 'Bubblesheet Progress',$count,
'inline',undef,'scantronupload');
&Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,
'Processing first student');
+ $r->print(' ');
my $start=&Time::HiRes::time();
my $i=-1;
- my ($uname,$udom,$started);
+ my $started;
+
+ my $nav_error;
+ &scantron_get_maxbubble(\$nav_error); # Need the bubble lines array to parse.
+ if ($nav_error) {
+ $r->print(&navmap_errormsg());
+ return '';
+ }
- &scantron_get_maxbubble(); # Need the bubble lines array to parse.
+ # If an ssi failed in scantron_get_maxbubble, put an error message out to
+ # the user and return.
+
+ if ($ssi_error) {
+ $r->print("");
+ &ssi_print_error($r);
+ $r->print(&show_grading_menu_form($symb));
+ &Apache::lonnet::remove_lock($lock);
+ return ''; # Dunno why the other returns return '' rather than just returning.
+ }
+
+ my %lettdig = &letter_to_digits();
+ my $numletts = scalar(keys(%lettdig));
while ($i<$scanlines->{'count'}) {
($uname,$udom)=('','');
@@ -6948,41 +7880,131 @@ SCANTRONFORM
}
($uname,$udom)=split(/:/,$uname);
+ my (%partids_by_symb,$res_error);
+ foreach my $resource (@resources) {
+ my $ressymb;
+ if (ref($resource)) {
+ $ressymb = $resource->symb();
+ } else {
+ $res_error = 1;
+ last;
+ }
+ 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);
+ $partids_by_symb{$ressymb} = $parts;
+ } else {
+ $partids_by_symb{$ressymb} = $grader_partids_by_symb{$ressymb};
+ }
+ }
+
+ if ($res_error) {
+ &scantron_add_delay(\@delayqueue,$line,
+ 'An error occurred while grading student '.$uname,2);
+ next;
+ }
+
&Apache::lonxml::clear_problem_counter();
- &Apache::lonnet::appenv(%$scan_record);
+ &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 '') {
- }
- if (&Apache::loncommon::connection_aborted($r)) { last; }
- }
+ 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') {
+ $ssi_error = 0; # So end of handler error message does not trigger.
+ $r->print("");
+ &ssi_print_error($r);
+ $r->print(&show_grading_menu_form($symb));
+ &Apache::lonnet::remove_lock($lock);
+ return ''; # Why return ''? Beats me.
+ }
+
$completedstudents{$uname}={'line'=>$line};
- if (&Apache::loncommon::connection_aborted($r)) { last; }
+ if ($env{'form.verifyrecord'}) {
+ my $lastpos = $env{'form.scantron_maxbubble'}*$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) {
+ 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);
+ $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') {
+ $ssi_error = 0; # So end of handler error message does not trigger.
+ $r->print("");
+ &ssi_print_error($r);
+ $r->print(&show_grading_menu_form($symb));
+ &Apache::lonnet::remove_lock($lock);
+ delete($completedstudents{$uname});
+ return '';
+ }
+ $counter = -1;
+ $studentrecord = '';
+ foreach my $resource (@resources) {
+ 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);
+ $studentrecord .= $recording;
+ }
+ if ($studentrecord ne $studentdata) {
+ $r->print('');
+ if ($scancode eq '') {
+ $r->print(&mt('Mismatch grading bubble sheet for user: [_1] with ID: [_2].',
+ $uname.':'.$udom,$scan_record->{'scantron.ID'}));
+ } else {
+ $r->print(&mt('Mismatch grading bubble sheet for user: [_1] with ID: [_2] and CODE: [_3].',
+ $uname.':'.$udom,$scan_record->{'scantron.ID'},$scancode));
+ }
+ $r->print(' '.&Apache::loncommon::start_data_table()."\n".
+ &Apache::loncommon::start_data_table_header_row()."\n".
+ '
'.&mt('Source').' '.&mt('Bubbled responses').' '.
+ &Apache::loncommon::end_data_table_header_row()."\n".
+ &Apache::loncommon::start_data_table_row().
+ ''.&mt('Bubble Sheet').' '.
+ ''.$studentdata.' '.
+ &Apache::loncommon::end_data_table_row().
+ &Apache::loncommon::start_data_table_row().
+ 'Stored submissions '.
+ ''.$studentrecord.' '."\n".
+ &Apache::loncommon::end_data_table_row().
+ &Apache::loncommon::end_data_table().'');
+ } else {
+ $r->print(''.
+ &mt('A second grading pass was needed for user: [_1] with ID: [_2], because a mismatch was seen on the first pass.',$uname.':'.$udom,$scan_record->{'scantron.ID'}).' '.
+ &mt("As a consequence, this user's submission history records two tries.").
+ ' ');
+ }
+ }
+ }
+ if (&Apache::loncommon::connection_aborted($r)) { last; }
} continue {
&Apache::lonxml::clear_problem_counter();
- &Apache::lonnet::delenv('scantron\.');
+ &Apache::lonnet::delenv('scantron.');
}
&Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
+ &Apache::lonnet::remove_lock($lock);
# my $lasttime = &Time::HiRes::time()-$start;
# $r->print("took $lasttime
");
@@ -6991,58 +8013,127 @@ SCANTRONFORM
return '';
}
-=pod
-
-=item scantron_upload_scantron_data
-
- Creates the screen for adding a new bubble sheet data file to a course.
+sub graders_resources_pass {
+ my ($resources,$grader_partids_by_symb,$grader_randomlists_by_symb) = @_;
+ 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);
+ $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'};
+ }
+ }
+ }
+ }
+ return;
+}
-=cut
+sub grade_student_bubbles {
+ my ($r,$uname,$udom,$scan_record,$scancode,$resources,$parts) = @_;
+ if (ref($resources) eq 'ARRAY') {
+ my $count = 0;
+ foreach my $resource (@{$resources}) {
+ my $ressymb = $resource->symb();
+ my %form = ('submitted' => 'scantron',
+ 'grade_target' => 'grade',
+ 'grade_username' => $uname,
+ 'grade_domain' => $udom,
+ 'grade_courseid' => $env{'request.course.id'},
+ 'grade_symb' => $ressymb,
+ 'CODE' => $scancode
+ );
+ 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};
+ $count++;
+ }
+ }
+ }
+ my $result=&ssi_with_retries($resource->src(),$ssi_retries,%form);
+ return 'ssi_error' if ($ssi_error);
+ last if (&Apache::loncommon::connection_aborted($r));
+ }
+ }
+ return;
+}
sub scantron_upload_scantron_data {
my ($r)=@_;
- $r->print(&Apache::loncommon::coursebrowser_javascript($env{'request.role.domain'}));
+ my $dom = $env{'request.role.domain'};
+ my $domdesc = &Apache::lonnet::domain($dom,'description');
+ $r->print(&Apache::loncommon::coursebrowser_javascript($dom));
my $select_link=&Apache::loncommon::selectcourse_link('rules','courseid',
'domainid',
- 'coursename');
- my $domsel=&Apache::loncommon::select_dom_form($env{'request.role.domain'},
- 'domainid');
+ 'coursename',$dom);
+ my $syllabuslink = ''.&mt('Syllabus').' '.
+ (' 'x2).&mt('(shows course personnel)');
my $default_form_data=&defaultFormData(&get_symb($r,1));
- $r->print(<print('
-
-$default_form_data
-
-
-
+'.&mt('Send bubblesheet data to a course').'
+
+
+'.$default_form_data.
+ &Apache::lonhtmlcommon::start_pick_box().
+ &Apache::lonhtmlcommon::row_title(&mt('Course ID')).
+ ' '.$select_link.
+ &Apache::lonhtmlcommon::row_closure().
+ &Apache::lonhtmlcommon::row_title(&mt('Course Name')).
+ ' '.$syllabuslink.
+ &Apache::lonhtmlcommon::row_closure().
+ &Apache::lonhtmlcommon::row_title(&mt('Domain')).
+ ' '.$domdesc.
+ &Apache::lonhtmlcommon::row_closure().
+ &Apache::lonhtmlcommon::row_title(&mt('File to upload')).
+ ' '.
+ &Apache::lonhtmlcommon::row_closure(1).
+ &Apache::lonhtmlcommon::end_pick_box().'
+
+
+
-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)=@_;
@@ -7050,12 +8141,12 @@ sub scantron_upload_scantron_data_save {
my $doanotherupload=
''."\n".
' '."\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. ");
+ $r->print(&mt("You are not allowed to upload bubblesheet data to the requested course.")." ");
if ($symb) {
$r->print(&show_grading_menu_form($symb));
} else {
@@ -7064,31 +8155,25 @@ sub scantron_upload_scantron_data_save {
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;
+ my $uploadedfile;
+ $r->print(''.&mt("Uploading file to [_1]",$coursedata{'description'}).' ');
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.");
+ $r->print(&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'},'<>&"').' '));
} 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." ");
+ my $result =
+ &Apache::lonnet::userfileupload('upfile','','scantron','','','',
+ $env{'form.courseid'},$env{'form.domainid'});
+ if ($result =~ m{^/uploaded/}) {
+ $r->print(&mt('[_1]Success:[_2] Successfully uploaded [_3] bytes of data into location: [_4]',
+ '',' ',(length($env{'form.upfile'})-1),
+ ''.$result.' '));
+ ($uploadedfile) = ($result =~ m{/([^/]+)$});
+ $r->print(&validate_uploaded_scantron_file($env{'form.domainid'},
+ $env{'form.courseid'},$uploadedfile));
} else {
- $r->print("Error: An error (".$result.") occurred when attempting to upload the file, ".&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"')." ");
+ $r->print(&mt('[_1]Error:[_2] An error ([_3]) occurred when attempting to upload the file, [_4]',
+ '',' ',$result,
+ ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').' '));
}
}
if ($symb) {
@@ -7099,13 +8184,91 @@ sub scantron_upload_scantron_data_save {
return '';
}
-=pod
-
-=item valid_file
-
- Validates that the requested bubble data file exists in the course.
-
-=cut
+sub validate_uploaded_scantron_file {
+ my ($cdom,$cname,$fname) = @_;
+ my $scanlines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.$fname);
+ my @lines;
+ if ($scanlines ne '-1') {
+ @lines=split("\n",$scanlines,-1);
+ }
+ my $output;
+ if (@lines) {
+ my (%counts,$max_match_format);
+ my ($max_match_count,$max_match_pct) = (0,0);
+ my $classlist = &Apache::loncoursedata::get_classlist($cdom,$cname);
+ my %idmap = &username_to_idmap($classlist);
+ foreach my $key (keys(%idmap)) {
+ my $lckey = lc($key);
+ $idmap{$lckey} = $idmap{$key};
+ }
+ my %unique_formats;
+ my @formatlines = &get_scantronformat_file();
+ foreach my $line (@formatlines) {
+ chomp($line);
+ my @config = split(/:/,$line);
+ my $idstart = $config[5];
+ my $idlength = $config[6];
+ if (($idstart ne '') && ($idlength > 0)) {
+ if (ref($unique_formats{$idstart.':'.$idlength}) eq 'ARRAY') {
+ push(@{$unique_formats{$idstart.':'.$idlength}},$config[0].':'.$config[1]);
+ } else {
+ $unique_formats{$idstart.':'.$idlength} = [$config[0].':'.$config[1]];
+ }
+ }
+ }
+ foreach my $key (keys(%unique_formats)) {
+ my ($idstart,$idlength) = split(':',$key);
+ %{$counts{$key}} = (
+ 'found' => 0,
+ 'total' => 0,
+ );
+ foreach my $line (@lines) {
+ next if ($line =~ /^#/);
+ next if ($line =~ /^[\s\cz]*$/);
+ my $id = substr($line,$idstart-1,$idlength);
+ $id = lc($id);
+ if (exists($idmap{$id})) {
+ $counts{$key}{'found'} ++;
+ }
+ $counts{$key}{'total'} ++;
+ }
+ if ($counts{$key}{'total'}) {
+ my $percent_match = (100*$counts{$key}{'found'})/($counts{$key}{'total'});
+ if (($max_match_format eq '') || ($percent_match > $max_match_pct)) {
+ $max_match_pct = $percent_match;
+ $max_match_format = $key;
+ $max_match_count = $counts{$key}{'total'};
+ }
+ }
+ }
+ if (ref($unique_formats{$max_match_format}) eq 'ARRAY') {
+ my $format_descs;
+ my $numwithformat = @{$unique_formats{$max_match_format}};
+ for (my $i=0; $i<$numwithformat; $i++) {
+ my ($name,$desc) = split(':',$unique_formats{$max_match_format}[$i]);
+ if ($i<$numwithformat-2) {
+ $format_descs .= '"'.$desc.' ", ';
+ } elsif ($i==$numwithformat-2) {
+ $format_descs .= '"'.$desc.' " '.&mt('and').' ';
+ } elsif ($i==$numwithformat-1) {
+ $format_descs .= '"'.$desc.' "';
+ }
+ }
+ my $showpct = sprintf("%.0f",$max_match_pct).'%';
+ $output .= ' '.&mt('Comparison of student IDs in the uploaded file with the course roster found matches for [_1] of the [_2] entries in the file (for the format defined for [_3]).',''.$showpct.' ',''.$max_match_count.' ',$format_descs).
+ ' '.&mt('A low percentage of matches results from one of the following:').''.
+ ''.&mt('The file was uploaded to the wrong course').' '.
+ ''.&mt('The data are not in the format expected for the domain: [_1]',
+ ''.$cdom.' ').' '.
+ ''.&mt('Students did not bubble their IDs, or mis-bubbled them').' '.
+ ''.&mt('The course roster is not up to date').' '.
+ ' ';
+ }
+ } else {
+ $output = ''.&mt('Uploaded file contained no data').' ';
+ }
+ return $output;
+}
sub valid_file {
my ($requested_file)=@_;
@@ -7115,16 +8278,6 @@ sub valid_file {
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));
@@ -7132,11 +8285,11 @@ sub scantron_download_scantron_data {
my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
my $file=$env{'form.scantron_selectfile'};
if (! &valid_file($file)) {
- $r->print(<print('
- The requested file name was invalid.
+ '.&mt('The requested file name was invalid.').'
-ERROR
+');
$r->print(&show_grading_menu_form(&get_symb($r,1)));
return;
}
@@ -7146,26 +8299,323 @@ ERROR
&Apache::lonnet::allowuploaded('/adm/grades',$orig);
&Apache::lonnet::allowuploaded('/adm/grades',$corrected);
&Apache::lonnet::allowuploaded('/adm/grades',$skipped);
- $r->print(<print('
- Original file as uploaded by the scantron office.
+ '.&mt('[_1]Original[_2] file as uploaded by the scantron office.',
+ '',' ').'
- Corrections , a file of corrected records that were used in grading.
+ '.&mt('[_1]Corrections[_2], a file of corrected records that were used in grading.',
+ '',' ').'
- Skipped , a file of records that were skipped.
+ '.&mt('[_1]Skipped[_2], a file of records that were skipped.',
+ '',' ').'
-DOWNLOAD
+');
$r->print(&show_grading_menu_form(&get_symb($r,1)));
return '';
}
-=pod
+sub checkscantron_results {
+ my ($r) = @_;
+ my ($symb)=&get_symb($r);
+ if (!$symb) {return '';}
+ my $grading_menu_button=&show_grading_menu_form($symb);
+ my $cid = $env{'request.course.id'};
+ my %lettdig = &letter_to_digits();
+ my $numletts = scalar(keys(%lettdig));
+ my $cnum = $env{'course.'.$cid.'.num'};
+ my $cdom = $env{'course.'.$cid.'.domain'};
+ my (undef, undef, $sequence) = &Apache::lonnet::decode_symb($env{'form.selectpage'});
+ my %record;
+ my %scantron_config =
+ &Apache::grades::get_scantron_config($env{'form.scantron_format'});
+ my ($scanlines,$scan_data)=&Apache::grades::scantron_getfile();
+ my $classlist=&Apache::loncoursedata::get_classlist();
+ my %idmap=&Apache::grades::username_to_idmap($classlist);
+ my $navmap=Apache::lonnavmaps::navmap->new();
+ 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);
-=back
+ my ($uname,$udom);
+ my (%scandata,%lastname,%bylast);
+ $r->print('
+'."\n");
+
+ 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.
+ if ($nav_error) {
+ $r->print(&navmap_errormsg());
+ return '';
+ }
+
+ &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,
+ 'Processing first student');
+ my $start=&Time::HiRes::time();
+ my $i=-1;
+
+ while ($i<$scanlines->{'count'}) {
+ ($username,$domain,$uname)=('','','');
+ $i++;
+ my $line=&Apache::grades::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=
+ &Apache::grades::scantron_parse_scanline($line,$i,\%scantron_config,
+ $scan_data);
+ unless ($uname=&Apache::grades::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;
+ }
+ if (exists $completedstudents{$uname}) {
+ &Apache::grades::scantron_add_delay(\@delayqueue,$line,
+ 'Student '.$uname.' has multiple sheets',2);
+ next;
+ }
+ my $pid = $scan_record->{'scantron.ID'};
+ $lastname{$pid} = $scan_record->{'scantron.LastName'};
+ push(@{$bylast{$lastname{$pid}}},$pid);
+ my $lastpos = $env{'form.scantron_maxbubble'}*$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) {
+ 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);
+ } 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);
+ $record{$pid} .= $recording;
+ }
+ }
+ &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
+ $r->print(' ');
+ my ($okstudents,$badstudents,$numstudents,$passed,$failed);
+ $passed = 0;
+ $failed = 0;
+ $numstudents = 0;
+ foreach my $last (sort(keys(%bylast))) {
+ if (ref($bylast{$last}) eq 'ARRAY') {
+ foreach my $pid (sort(@{$bylast{$last}})) {
+ my $showscandata = $scandata{$pid};
+ my $showrecord = $record{$pid};
+ $showscandata =~ s/\s/ /g;
+ $showrecord =~ s/\s/ /g;
+ if ($scandata{$pid} eq $record{$pid}) {
+ my $css_class = ($passed % 2)?'LC_odd_row':'LC_even_row';
+ $okstudents .= ''.
+''.&mt('Bubblesheet').' '.$showscandata.' '.$last.' '.$pid.' '."\n".
+' '."\n".
+''."\n".
+'Submissions '.$showrecord.' '."\n";
+ $passed ++;
+ } else {
+ my $css_class = ($failed % 2)?'LC_odd_row':'LC_even_row';
+ $badstudents .= ''.&mt('Bubblesheet').' '.$scandata{$pid}.' '.$last.' '.$pid.' '."\n".
+' '."\n".
+''."\n".
+'Submissions '.$record{$pid}.' '."\n".
+' '."\n";
+ $failed ++;
+ }
+ $numstudents ++;
+ }
+ }
+ }
+ $r->print(''.
+ &mt('Comparison of bubblesheet data (including corrections) with corresponding submission records (most recent submission) for [_1][quant,_2,student][_3] ([_4] bubblesheet lines/student).',
+ '',
+ $numstudents,
+ ' ',
+ $env{'form.scantron_maxbubble'}).
+ '
'
+ );
+ $r->print(''.&mt('Exact matches for [quant,_1,student] .',$passed).' '.&mt('Discrepancies detected for [quant,_1,student] .',$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".
+ &Apache::loncommon::start_data_table_header_row()."\n".
+ ''.&mt('Source').' '.&mt('Bubble records').' '.&mt('Name').' '.&mt('ID').' '.
+ &Apache::loncommon::end_data_table_header_row()."\n".
+ $okstudents."\n".
+ &Apache::loncommon::end_data_table().' ');
+ }
+ if ($failed) {
+ $r->print(&mt('Students with differences between bubblesheet data and submissions are as follows:').' ');
+ $r->print(&Apache::loncommon::start_data_table()."\n".
+ &Apache::loncommon::start_data_table_header_row()."\n".
+ ''.&mt('Source').' '.&mt('Bubble records').' '.&mt('Name').' '.&mt('ID').' '.
+ &Apache::loncommon::end_data_table_header_row()."\n".
+ $badstudents."\n".
+ &Apache::loncommon::end_data_table()).' '.
+ &mt('Differences can occur if submissions were modified using manual grading after a bubblesheet grading pass.').' '.&mt('If unexpected discrepancies were detected, it is recommended that you inspect the original bubblesheets.');
+ }
+ $r->print(' '.$grading_menu_button);
+ return;
+}
+
+sub verify_scantron_grading {
+ my ($resource,$domain,$username,$cid,$counter,$scandata,$partids,
+ $scantron_config,$lettdig,$numletts) = @_;
+ my ($record,%expected,%startpos);
+ return ($counter,$record) if (!ref($resource));
+ return ($counter,$record) if (!$resource->is_problem());
+ my $symb = $resource->symb();
+ return ($counter,$record) if (ref($partids) ne 'ARRAY');
+ 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"});
+ foreach my $item (@sub_lines) {
+ $expected{$part_id} += $item;
+ }
+ } else {
+ $expected{$part_id} = $env{"form.scantron.bubblelines.$counter"};
+ }
+ $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"};
+ }
+ if ($symb) {
+ my %recorded;
+ my (%returnhash) = &Apache::lonnet::restore($symb,$cid,$domain,$username);
+ 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};
+ }
+ }
+ foreach my $key (keys(%lasthash)) {
+ if ($key =~ /\.scantron$/) {
+ my $value = &unescape($lasthash{$key});
+ my ($part_id) = ($key =~ /^resource\.(.+)\.scantron$/);
+ if ($value eq '') {
+ for (my $i=0; $i<$expected{$part_id}; $i++) {
+ for (my $j=0; $j<$scantron_config->{'length'}; $j++) {
+ $recorded{$part_id} .= $scantron_config->{'Qoff'};
+ }
+ }
+ } else {
+ my @tocheck;
+ my @items = split(//,$value);
+ if (($scantron_config->{'Qon'} eq 'letter') ||
+ ($scantron_config->{'Qon'} eq 'number')) {
+ if (@items < $expected{$part_id}) {
+ my $fragment = substr($scandata,$startpos{$part_id},$expected{$part_id});
+ my @singles = split(//,$fragment);
+ foreach my $pos (@singles) {
+ if ($pos eq ' ') {
+ push(@tocheck,$pos);
+ } else {
+ my $next = shift(@items);
+ push(@tocheck,$next);
+ }
+ }
+ } else {
+ @tocheck = @items;
+ }
+ foreach my $letter (@tocheck) {
+ if ($scantron_config->{'Qon'} eq 'letter') {
+ if ($letter !~ /^[A-J]$/) {
+ $letter = $scantron_config->{'Qoff'};
+ }
+ $recorded{$part_id} .= $letter;
+ } elsif ($scantron_config->{'Qon'} eq 'number') {
+ my $digit;
+ if ($letter !~ /^[A-J]$/) {
+ $digit = $scantron_config->{'Qoff'};
+ } else {
+ $digit = $lettdig->{$letter};
+ }
+ $recorded{$part_id} .= $digit;
+ }
+ }
+ } else {
+ @tocheck = @items;
+ for (my $i=0; $i<$expected{$part_id}; $i++) {
+ my $curr_sub = shift(@tocheck);
+ my $digit;
+ if ($curr_sub =~ /^[A-J]$/) {
+ $digit = $lettdig->{$curr_sub}-1;
+ }
+ if ($curr_sub eq 'J') {
+ $digit += scalar($numletts);
+ }
+ for (my $j=0; $j<$scantron_config->{'Qlength'}; $j++) {
+ if ($j == $digit) {
+ $recorded{$part_id} .= $scantron_config->{'Qon'};
+ } else {
+ $recorded{$part_id} .= $scantron_config->{'Qoff'};
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ foreach my $part_id (@{$partids}) {
+ if ($recorded{$part_id} eq '') {
+ for (my $i=0; $i<$expected{$part_id}; $i++) {
+ for (my $j=0; $j<$scantron_config->{'Qlength'}; $j++) {
+ $recorded{$part_id} .= $scantron_config->{'Qoff'};
+ }
+ }
+ }
+ $record .= $recorded{$part_id};
+ }
+ }
+ return ($counter,$record);
+}
+
+sub letter_to_digits {
+ my %lettdig = (
+ A => 1,
+ B => 2,
+ C => 3,
+ D => 4,
+ E => 5,
+ F => 6,
+ G => 7,
+ H => 8,
+ I => 9,
+ J => 0,
+ );
+ return %lettdig;
+}
-=cut
#-------- end of section for handling grading scantron forms -------
#
@@ -7180,7 +8630,7 @@ sub show_grading_menu_form {
' '."\n".
' '."\n".
' '."\n".
- ' '."\n".
+ ' '."\n".
''."\n";
return $result;
}
@@ -7212,36 +8662,49 @@ sub grading_menu {
'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.'),
- });
+
+ my $url1 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+
$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.')});
+ my $url2 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+
$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.')});
+ my $url3 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+
$fields{'command'} = 'scantron_selectphase';
- $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
- push (@menu, { url => $url,
- name => &mt('Grade/Manage Scantron Forms'),
- short_description =>
- &mt('')});
- $fields{'command'} = 'verify';
- $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
- push (@menu, { url => "",
- name => &mt('Verify Receipt'),
- short_description =>
- &mt('')});
+ my $url4 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+
+ my @menu = ({ categorytitle=>'Course Grading',
+ items =>[
+ { linktext => 'Manual Grading/View Submissions',
+ url => $url1,
+ permission => 'F',
+ icon => 'edit-find-replace.png',
+ linktitle => 'Start the process of hand grading submissions.'
+ },
+ { linktext => 'Upload Scores',
+ url => $url2,
+ permission => 'F',
+ icon => 'uploadscores.png',
+ linktitle => 'Specify a file containing the class scores for current resource.'
+ },
+ { linktext => 'Process Clicker',
+ url => $url3,
+ permission => 'F',
+ icon => 'addClickerInfoFile.png',
+ linktitle => 'Specify a file containing the clicker information for this resource.'
+ },
+ { linktext => 'Grade/Manage/Review Bubblesheets',
+ url => $url4,
+ permission => 'F',
+ icon => 'stat.png',
+ linktitle => 'Grade bubblesheet exams, upload/download bubblesheet data files, and review previously graded bubblesheet exams.'
+ }
+ ]
+ });
+
+ #$fields{'command'} = 'verify';
+ #$url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
#
# Create the menu
my $Str;
@@ -7250,32 +8713,21 @@ sub grading_menu {
$Str .= ' '.
' '."\n".
' '."\n".
- ' '."\n".
+ ' '."\n".
' '."\n".
' '."\n".
' '."\n";
- foreach my $menudata (@menu) {
- if ($menudata->{'name'} ne &mt('Verify Receipt')) {
- $Str .=' \n";
- } else {
- $Str .=' {'jscript'}.
- ' onClick="javascript:checkChoice(document.forms.gradingMenu,\'5\',\'verify\')" '.
- ' /> ';
- $Str .= (' 'x8).
- ' receipt: '.&Apache::lonnet::recprefix($env{'request.course.id'}).
- '- ';
- }
- $Str .= ' '.(' 'x8).$menudata->{'short_description'}.
- "\n";
- }
- $Str .="\n";
+ $Str .= Apache::lonhtmlcommon::generate_menu(@menu);
+ #$menudata->{'jscript'}
+ $Str .=' '.
+ &Apache::lonnet::recprefix($env{'request.course.id'}).
+ '- ';
+
$Str .="\n";
+ my $receiptalert = &mt("Please enter a receipt number given by a student in the receipt box.");
$request->print(<
function checkChoice(formname,val,cmdx) {
@@ -7303,7 +8755,7 @@ sub grading_menu {
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.");
+ alert("$receiptalert");
formname.receipt.value = "";
formname.receipt.focus();
return false;
@@ -7324,6 +8776,7 @@ sub submit_options {
if (!$symb) {return '';}
my $probTitle = &Apache::lonnet::gettitle($symb);
+ my $receiptalert = &mt("Please enter a receipt number given by a student in the receipt box.");
$request->print(<
function checkChoice(formname,val,cmdx) {
@@ -7351,7 +8804,7 @@ sub submit_options {
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.");
+ alert("$receiptalert");
formname.receipt.value = "";
formname.receipt.focus();
return false;
@@ -7361,9 +8814,8 @@ sub submit_options {
GRADINGMENUJS
&commonJSfunctions($request);
- my $result=' Manual Grading/View Submission ';
my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle);
- $result.=$table;
+ my $result;
my (undef,$sections) = &getclasslist('all','0');
my $savedState = &savedState();
my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'submission' : $$savedState{'saveCmd'});
@@ -7371,6 +8823,15 @@ GRADINGMENUJS
my $saveSub = ($$savedState{'saveSub'} eq '' ? 'all' : $$savedState{'saveSub'});
my $saveStatus = ($$savedState{'saveStatus'} eq '' ? 'Active' : $$savedState{'saveStatus'});
+ # Preselect sections
+ my $selsec="";
+ if (ref($sections)) {
+ foreach my $section (sort(@$sections)) {
+ $selsec.=''.$section.' '."\n";
+ }
+ }
+
$result.=''."\n".
' '."\n".
' '."\n".
@@ -7380,102 +8841,94 @@ GRADINGMENUJS
' '."\n".
' '."\n";
- $result.=''."\n".
- ''."\n".
- ' Select a Grading/Viewing Option '."\n".
- ''."\n";
-
- $result.=''."\n";
-
- $result.=' '; #';
-
-# $result.=''."\n".' ';
- $result.= '
'."\n".
- '
'."\n";
+ $result.='
+
+
+
+
+ '.&mt('Groups').'
+
+ '.&Apache::lonstatistics::GroupSelect('group','multiple',5).'
+
+
+
+
+ '.&mt('Access Status').'
+
+ '.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,5,undef,'mult').'
+
+
+
+
+ '.&mt('Submission Status').'
+
+
+ '.&mt('with submissions').'
+ '.&mt('in grading queue').'
+ '.&mt('with ungraded submissions').'
+ '.&mt('with incorrect submissions').'
+ '.&mt('with any status').'
+
+
+
+
+
+
+
+
+
+
+ '.&mt('Grade Complete Folder for One Student').'
+
+
+ ';
+ $result .= &show_grading_menu_form($symb);
return $result;
}
@@ -7572,9 +9025,9 @@ sub process_clicker {
$result.=$table;
$result.=''."\n";
$result.=''."\n";
- $result.=' '.&mt('Specify a file containing the clicker information for this resource').
- '. '."\n";
- $result.=''."\n";
+ $result.=' '.&mt('Specify a file containing the clicker information for this resource.').
+ ' '."\n";
+ $result.=''."\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',
@@ -7585,9 +9038,9 @@ sub process_clicker {
if (!$env{'form.upfiletype'}) { $env{'form.upfiletype'}='iclicker'; }
my %checked;
- foreach my $gradingmechanism ('attendance','personnel','specific') {
+ foreach my $gradingmechanism ('attendance','personnel','specific','given') {
if ($env{'form.gradingmechanism'} eq $gradingmechanism) {
- $checked{$gradingmechanism}="checked='checked'";
+ $checked{$gradingmechanism}=' checked="checked"';
}
}
@@ -7596,11 +9049,13 @@ sub process_clicker {
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 $given=&mt("Correctness determined from given list of answers").' '.
+ '('.&mt("Provide comma-separated list. Use '*' for any answer correct, '-' for skip").') ';
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'));
+ {'iclicker' => 'i>clicker',
+ 'interwrite' => 'interwrite PRS'});
$symb = &Apache::lonenc::check_encrypt($symb);
$result.=<
@@ -7649,14 +9104,17 @@ function sanitycheck() {
$type: $selectform
- $attendance
- $personnel
- $specific
+ $attendance
+ $personnel
+ $specific
+ $given
+
+
-$pcorrect:
-$pincorrect:
-
+$pcorrect:
+$pincorrect:
+
ENDUPFORM
$result.='
'."\n".
@@ -7679,6 +9137,19 @@ sub process_clicker_file {
$result.=''.&mt('You need to specify a clicker ID for the correct answer').' ';
return $result.&show_grading_menu_form($symb);
}
+ if (($env{'form.gradingmechanism'} eq 'given') && ($env{'form.givenanswer'}!~/\S/)) {
+ $result.=''.&mt('You need to specify the correct answer').' ';
+ return $result.&show_grading_menu_form($symb);
+ }
+ my $foundgiven=0;
+ if ($env{'form.gradingmechanism'} eq 'given') {
+ $env{'form.givenanswer'}=~s/^\s*//gs;
+ $env{'form.givenanswer'}=~s/\s*$//gs;
+ $env{'form.givenanswer'}=~s/[^a-zA-Z0-9\.\*\-\+]+/\,/g;
+ $env{'form.givenanswer'}=uc($env{'form.givenanswer'});
+ my @answers=split(/\,/,$env{'form.givenanswer'});
+ $foundgiven=$#answers+1;
+ }
my %clicker_ids=&gather_clicker_ids();
my %correct_ids;
if ($env{'form.gradingmechanism'} eq 'personnel') {
@@ -7697,6 +9168,8 @@ sub process_clicker_file {
}
if ($env{'form.gradingmechanism'} eq 'attendance') {
$result.=&mt('Score based on attendance only');
+ } elsif ($env{'form.gradingmechanism'} eq 'given') {
+ $result.=&mt('Score based on [_1] ([_2] answers)',''.$env{'form.givenanswer'}.' ',$foundgiven);
} else {
my $number=0;
$result.=''.&mt('Correctness determined by the following IDs').' ';
@@ -7732,7 +9205,7 @@ sub process_clicker_file {
$result.=(<
-$heading
+$heading
@@ -7742,6 +9215,9 @@ sub process_clicker_file {
ENDHEADER
+ if ($env{'form.gradingmechanism'} eq 'given') {
+ $result.=' ';
+ }
my %responses;
my @questiontitles;
my $errormsg='';
@@ -7754,11 +9230,13 @@ ENDHEADER
}
$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'}).
' ';
+ if (($env{'form.gradingmechanism'} eq 'given') && ($number!=$foundgiven)) {
+ $result.=''.&mt('Number of given answers does not agree with number of questions in file.').' ';
+ return $result.&show_grading_menu_form($symb);
+ }
# Remember Question Titles
# FIXME: Possibly need delimiter other than ":"
for (my $i=0;$i<$number;$i++) {
@@ -7796,13 +9274,13 @@ ENDHEADER
"\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);
+ &Apache::loncommon::selectstudent_link('clickeranalysis','uname'.$id,'udom'.$id,0,$id);
$unknown_count++;
}
}
$result.=' '.
&mt('Found [_1] registered and [_2] unregistered clickers.',$student_count,$unknown_count);
- if ($env{'form.gradingmechanism'} ne 'attendance') {
+ if (($env{'form.gradingmechanism'} ne 'attendance') && ($env{'form.gradingmechanism'} ne 'given')) {
if ($correct_count==0) {
$errormsg.="Found no correct answers answers for grading!";
} elsif ($correct_count>1) {
@@ -7841,6 +9319,7 @@ sub iclicker_eval {
$id=~s/^[\#0]+//;
for (my $i=0;$i<$number;$i++) {
my $idx=3+$i*6;
+ $entries[$idx]=~s/[^a-zA-Z0-9\.\*\-\+]+//g;
push(@idresponses,$entries[$idx]);
}
$$responses{$id}=join(',',@idresponses);
@@ -7873,7 +9352,7 @@ sub interwrite_eval {
$id=~s/[\-\:]//g;
$idresponses{$id}[$number]=$entries[6];
}
- foreach my $id (keys %idresponses) {
+ foreach my $id (keys(%idresponses)) {
$$responses{$id}=join(',',@{$idresponses{$id}});
$$responses{$id}=~s/^\s*\,//;
}
@@ -7885,18 +9364,22 @@ sub assign_clicker_grades {
my ($symb)=&get_symb($r);
if (!$symb) {return '';}
# See which part we are saving to
- my ($partlist,$handgrade,$responseType) = &response_type($symb);
+ my $res_error;
+ my ($partlist,$handgrade,$responseType) = &response_type($symb,\$res_error);
+ if ($res_error) {
+ return &navmap_errormsg();
+ }
# 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 ($result) = &showResourceInfo($symb,$env{'form.probTitle'}).' ';
+
+ $result .= &Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_header_row().
+ ''.&mt('Assigning grades based on clicker file').' '.
+ &Apache::loncommon::end_data_table_header_row().
+ &Apache::loncommon::start_data_table_row().'';
- my $heading=&mt('Assigning grades based on clicker file');
- $result.=(<
-
-$heading
-ENDHEADER
# Get correct result
# FIXME: Possibly need delimiter other than ":"
my @correct=();
@@ -7912,25 +9395,26 @@ ENDHEADER
$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]) {
+ } elsif (($input[$i]) || ($input[$i] eq '0')) {
$correct[$i]=$input[$i];
}
}
}
}
for (my $i=0;$i<$number;$i++) {
- if (!$correct[$i]) {
+ if ((!$correct[$i]) && ($correct[$i] ne '0')) {
$result.=''.
&mt('No correct result given for question "[_1]"!',
$env{'form.question:'.$i}).' ';
}
}
- $result.=' '.&mt("Correct answer: [_1]",join(', ',map { ($_?$_:'-') } @correct));
+ $result.=' '.&mt("Correct answer: [_1]",join(', ',map { ((($_) || ($_ eq '0'))?$_:'-') } @correct));
}
# Start grading
my $pcorrect=$env{'form.pcorrect'};
my $pincorrect=$env{'form.pincorrect'};
my $storecount=0;
+ my %users=();
foreach my $key (keys(%env)) {
my $user='';
if ($key=~/^form\.student\:(.*)$/) {
@@ -7944,23 +9428,46 @@ ENDHEADER
$user=$env{'form.multi'.$id};
}
}
- if ($user) {
+ if ($user) {
+ if ($users{$user}) {
+ $result.=''.
+ &mt("More than one entry found for [_1] !",$user).
+ ' ';
+ }
+ $users{$user}=1;
my @answer=split(/\,/,$env{$key});
my $sum=0;
+ my $realnumber=$number;
for (my $i=0;$i<$number;$i++) {
- if ($answer[$i]) {
+ if ($correct[$i] eq '-') {
+ $realnumber--;
+ } elsif ($answer[$i]) {
if ($gradingmechanism eq 'attendance') {
$sum+=$pcorrect;
+ } elsif ($correct[$i] eq '*') {
+ $sum+=$pcorrect;
} else {
- if ($answer[$i] eq $correct[$i]) {
- $sum+=$pcorrect;
- } else {
- $sum+=$pincorrect;
+# We actually grade if correct or not
+ my $increment=$pincorrect;
+# Special case: numerical answer "0"
+ if ($correct[$i] eq '0') {
+ if ($answer[$i]=~/^[0\.]+$/) {
+ $increment=$pcorrect;
+ }
+# General numerical answer, both evaluate to something non-zero
+ } elsif ((1.0*$correct[$i]!=0) && (1.0*$answer[$i]!=0)) {
+ if (1.0*$correct[$i]==1.0*$answer[$i]) {
+ $increment=$pcorrect;
+ }
+# Must be just alphanumeric
+ } elsif ($answer[$i] eq $correct[$i]) {
+ $increment=$pcorrect;
}
+ $sum+=$increment;
}
}
}
- my $ave=$sum/(100*$number);
+ my $ave=$sum/(100*$realnumber);
# Store
my ($username,$domain)=split(/\:/,$user);
my %grades=();
@@ -7978,23 +9485,30 @@ ENDHEADER
}
}
# We are done
- $result.=' '.&mt('Successfully stored grades for [_1] student(s).',$storecount).
- '
'."\n".
- '
'."\n";
+ $result.=' '.&mt('Successfully stored grades for [quant,_1,student].',$storecount).
+ ' '.
+ &Apache::loncommon::end_data_table_row().
+ &Apache::loncommon::end_data_table()." \n";
return $result.&show_grading_menu_form($symb);
}
+sub navmap_errormsg {
+ return ''.
+ &mt('An error occurred retrieving information about resources in the course.').'
'.
+ &mt('It is recommended that you [_1]re-initialize the course[_2] and then return to this grading page.','
',' ').
+ '
';
+}
+
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');
+ if ($request->header_only) {
+ &Apache::loncommon::content_type($request,'text/html');
+ $request->send_http_header;
+ return OK;
}
- $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];
@@ -8003,10 +9517,15 @@ sub handler {
&Apache::lonnet::logthis("grades got multiple commands ".join(':',@commands));
}
-
- $request->print(&Apache::loncommon::start_page('Grading'));
+ $ssi_error = 0;
+ my $brcrum = [{href=>"/adm/grades",text=>"Grading"}];
+ my $start_page = &Apache::loncommon::start_page('Grading',undef,
+ {'bread_crumbs' => $brcrum}));
if ($symb eq '' && $command eq '') {
if ($env{'user.adv'}) {
+ &Apache::loncommon::content_type($request,'text/html');
+ $request->send_http_header;
+ $request->print($start_page);
if (($env{'form.codeone'}) && ($env{'form.codetwo'}) &&
($env{'form.codethree'})) {
my $token=$env{'form.codeone'}.'*'.$env{'form.codetwo'}.'*'.
@@ -8016,7 +9535,7 @@ sub handler {
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,
+ $request->print(&ssi_with_retries('/res/'.$url, $ssi_retries,
('grade_username' => $tuname,
'grade_domain' => $tudom,
'grade_courseid' => $tcrsid,
@@ -8030,9 +9549,28 @@ sub handler {
} else {
$request->print(&Apache::lonxml::tokeninputfield());
}
- }
+ } elsif ($env{'request.course.id'}) {
+ &init_perm();
+ if (!%perm) {
+ $request->internal_redirect('/adm/quickgrades');
+ } else {
+ &Apache::loncommon::content_type($request,'text/html');
+ $request->send_http_header;
+ $request->print($start_page);
+ }
+ }
} else {
- &init_perm();
+ &init_perm();
+ if (!$env{'request.course.id'}) {
+ # Not in a course.
+ $env{'user.error.msg'}="/adm/grades::vgr:0:0:Cannot display grades page outside course context";
+ return HTTP_NOT_ACCEPTABLE;
+ } elsif (!%perm) {
+ $request->internal_redirect('/adm/quickgrades');
+ }
+ &Apache::loncommon::content_type($request,'text/html');
+ $request->send_http_header;
+ $request->print($start_page);
if ($command eq 'submission' && $perm{'vgr'}) {
($env{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0));
} elsif ($command eq 'pickStudentPage' && $perm{'vgr'}) {
@@ -8099,15 +9637,191 @@ sub handler {
} elsif ($command eq 'scantron_download' &&
&Apache::lonnet::allowed('usc',$env{'request.course.id'})) {
$request->print(&scantron_download_scantron_data($request));
+ } elsif ($command eq 'checksubmissions' && $perm{'vgr'}) {
+ $request->print(&checkscantron_results($request));
} elsif ($command) {
- $request->print("Access Denied ($command)");
+ $request->print(''.&mt('Access Denied ([_1])',$command).'
');
}
}
+ if ($ssi_error) {
+ &ssi_print_error($request);
+ }
$request->print(&Apache::loncommon::end_page());
&reset_caches();
- return '';
+ return OK;
}
1;
__END__;
+
+
+=head1 NAME
+
+Apache::grades
+
+=head1 SYNOPSIS
+
+Handles the viewing of grades.
+
+This is part of the LearningOnline Network with CAPA project
+described at http://www.lon-capa.org.
+
+=head1 OVERVIEW
+
+Do an ssi with retries:
+While I'd love to factor out this with the vesrion in lonprintout,
+that would either require a data coupling between modules, which I refuse to perpetuate (there's quite enough of that already), or would require the invention of another infrastructure
+I'm not quite ready to invent (e.g. an ssi_with_retry object).
+
+At least the logic that drives this has been pulled out into loncommon.
+
+
+
+ssi_with_retries - Does the server side include of a resource.
+ if the ssi call returns an error we'll retry it up to
+ the number of times requested by the caller.
+ If we still have a proble, no text is appended to the
+ output and we set some global variables.
+ to indicate to the caller an SSI error occurred.
+ All of this is supposed to deal with the issues described
+ in LonCAPA BZ 5631 see:
+ http://bugs.lon-capa.org/show_bug.cgi?id=5631
+ by informing the user that this happened.
+
+Parameters:
+ resource - The resource to include. This is passed directly, without
+ interpretation to lonnet::ssi.
+ form - The form hash parameters that guide the interpretation of the resource
+
+ retries - Number of retries allowed before giving up completely.
+Returns:
+ On success, returns the rendered resource identified by the resource parameter.
+Side Effects:
+ The following global variables can be set:
+ ssi_error - If an unrecoverable error occurred this becomes true.
+ It is up to the caller to initialize this to false
+ if desired.
+ ssi_error_resource - If an unrecoverable error occurred, this is the value
+ of the resource that could not be rendered by the ssi
+ call.
+ ssi_error_message - The error string fetched from the ssi response
+ in the event of an error.
+
+
+=head1 HANDLER SUBROUTINE
+
+ssi_with_retries()
+
+=head1 SUBROUTINES
+
+=over
+
+=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
+
+=item scantron_get_maxbubble() :
+
+ Arguments:
+ $nav_error - Reference to scalar which is a flag to indicate a
+ failure to retrieve a navmap object.
+ if $nav_error is set to 1 by scantron_get_maxbubble(), the
+ calling routine should trap the error condition and display the warning
+ found in &navmap_errormsg().
+
+ 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 results to $env{'form.scantron_maxbubble'},
+ $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
+ 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.
+
+
+=item scantron_validate_missingbubbles() :
+
+ Validates all scanlines in the selected file to not have any
+ answers that don't have bubbles that have not been verified
+ to be bubble free.
+
+=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.
+
+=item scantron_upload_scantron_data() :
+
+ Creates the screen for adding a new bubble sheet data file to a course.
+
+=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.
+
+=item valid_file() :
+
+ Validates that the requested bubble data file exists in the course.
+
+=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.
+
+=item scantron_validate_ID() :
+
+ Validates all scanlines in the selected file to not have any
+ invalid or underspecified student/employee IDs
+
+=item navmap_errormsg() :
+
+ Returns HTML mark-up inside a
with a link to re-initialize the course.
+ Should be called whenever the request to instantiate a navmap object fails.
+
+=back
+
+=cut