'.
- ' '."\n";
- if ($ENV{'form.handgrade'} eq 'yes') {
- $endform.=' '."\n";
- my $ntstu =''.
- '1 2 '.
- '3 5 '.
- '7 10 '."\n";
- my $nsel = ($ENV{'form.NTSTU'} ne '' ? $ENV{'form.NTSTU'} : '1');
- $ntstu =~ s/$nsel $nsel;
- $endform.=$ntstu.'student(s) ';
- } else {
- $endform.=' '."\n";
- }
- $endform.=' '."\n".
- ' ';
- $endform.='(Next and Previous do not save the scores.)'."\n"
- if ($ENV{'form.handgrade'} eq 'yes');
+ my $endform='';
- $endform.=&show_grading_menu_form($symb,$url);
+ $endform.=&show_grading_menu_form($symb);
$request->print($endform);
}
return '';
}
+sub check_collaborators {
+ my ($symb,$uname,$udom,$record,$handgrade,$counter) = @_;
+ my ($result,@col_fullnames);
+ my ($classlist,undef,$fullname) = &getclasslist('all','0');
+ foreach my $part (keys(%$handgrade)) {
+ my $ncol = &Apache::lonnet::EXT('resource.'.$part.
+ '.maxcollaborators',
+ $symb,$udom,$uname);
+ next if ($ncol <= 0);
+ $part =~ s/\_/\./g;
+ next if ($record->{'resource.'.$part.'.collaborators'} eq '');
+ my (@good_collaborators, @bad_collaborators);
+ foreach my $possible_collaborator
+ (split(/,?\s+/,$record->{'resource.'.$part.'.collaborators'})) {
+ $possible_collaborator =~ s/[\$\^\(\)]//g;
+ next if ($possible_collaborator eq '');
+ my ($co_name,$co_dom) = split(/\@|:/,$possible_collaborator);
+ $co_dom = $udom if (! defined($co_dom) || $co_dom =~ /^domain$/i);
+ next if ($co_name eq $uname && $co_dom eq $udom);
+ # Doing this grep allows 'fuzzy' specification
+ my @matches = grep(/^\Q$co_name\E:\Q$co_dom\E$/i,
+ keys(%$classlist));
+ if (! scalar(@matches)) {
+ push(@bad_collaborators, $possible_collaborator);
+ } else {
+ push(@good_collaborators, @matches);
+ }
+ }
+ if (scalar(@good_collaborators) != 0) {
+ $result.=' '.&mt('Collaborators: ');
+ foreach my $name (@good_collaborators) {
+ my ($lastname,$givenn) = split(/,/,$$fullname{$name});
+ push(@col_fullnames, $givenn.' '.$lastname);
+ $result.=$fullname->{$name}.' ';
+ }
+ $result.=' '."\n";
+ my ($part)=split(/\./,$part);
+ $result.=' '.
+ "\n";
+ }
+ if (scalar(@bad_collaborators) > 0) {
+ $result.='';
+ $result.=&mt('This student has submitted [quant,_1,invalid collaborator]: [_2]',scalar(@bad_collaborators),join(', ',@bad_collaborators));
+ $result .= '
';
+ }
+ if (scalar(@bad_collaborators > $ncol)) {
+ $result .= '';
+ $result .= &mt('This student has submitted too many '.
+ 'collaborators. Maximum is [_1].',$ncol);
+ $result .= '
';
+ }
+ }
+ return ($result,$fullname,\@col_fullnames);
+}
+
#--- Retrieve the last submission for all the parts
sub get_last_submission {
- my (%returnhash)=@_;
+ my ($returnhash)=@_;
my (@string,$timestamp);
- if ($returnhash{'version'}) {
+ if ($$returnhash{'version'}) {
my %lasthash=();
my ($version);
- for ($version=1;$version<=$returnhash{'version'};$version++) {
- foreach (sort(split(/\:/,$returnhash{$version.':keys'}))) {
- $lasthash{$_}=$returnhash{$version.':'.$_};
- $timestamp = scalar(localtime($returnhash{$version.':timestamp'}));
+ for ($version=1;$version<=$$returnhash{'version'};$version++) {
+ foreach my $key (sort(split(/\:/,
+ $$returnhash{$version.':keys'}))) {
+ $lasthash{$key}=$$returnhash{$version.':'.$key};
+ $timestamp =
+ scalar(localtime($$returnhash{$version.':timestamp'}));
}
}
- foreach ((keys %lasthash)) {
- if ($_ =~ /\.submission$/) {
- my ($partid,$foo) = split(/submission$/,$_);
- my $draft = $lasthash{$partid.'awarddetail'} eq 'DRAFT' ?
- 'Draft Copy ' : '';
- push @string, (join(':',$_,$draft.$lasthash{$_}));
- }
+ foreach my $key (keys(%lasthash)) {
+ next if ($key !~ /\.submission$/);
+
+ my ($partid,$foo) = split(/submission$/,$key);
+ my $draft = $lasthash{$partid.'awarddetail'} eq 'DRAFT' ?
+ 'Draft Copy ' : '';
+ push(@string, join(':', $key, $draft.$lasthash{$key}));
}
}
- @string = $string[0] eq '' ? 'Nothing submitted - no attempts.' : @string;
- return \@string,\$timestamp;
+ if (!@string) {
+ $string[0] =
+ 'Nothing submitted - no attempts. ';
+ }
+ return (\@string,\$timestamp);
}
#--- High light keywords, with style choosen by user.
sub keywords_highlight {
my $string = shift;
- my $size = $ENV{'form.kwsize'} eq '0' ? '' : 'size='.$ENV{'form.kwsize'};
- my $styleon = $ENV{'form.kwstyle'} eq '' ? '' : $ENV{'form.kwstyle'};
+ my $size = $env{'form.kwsize'} eq '0' ? '' : 'size='.$env{'form.kwsize'};
+ my $styleon = $env{'form.kwstyle'} eq '' ? '' : $env{'form.kwstyle'};
(my $styleoff = $styleon) =~ s/\\<\//;
- my @keylist = split(/[,\s+]/,$ENV{'form.keywords'});
- foreach (@keylist) {
- $string =~ s/\b\Q$_\E(\b|\.)/\$styleon$_$styleoff\<\/font\>/gi;
- }
- # This is not really the right place to do this, but I cannot find a
- # better one at this time. So here we go - the m in the s:::mg causes
- # ^ to match the beginning of a new line. So we replace(???) the beginning
- # of the line with to make things formatted a little better.
- $string =~ s:^: :mg;
+ my @keylist = split(/[,\s+]/,$env{'form.keywords'});
+ foreach my $keyword (@keylist) {
+ $string =~ s/\b\Q$keyword\E(\b|\.)/$styleon$keyword$styleoff<\/font>/gi;
+ }
return $string;
}
#--- Called from submission routine
sub processHandGrade {
my ($request) = shift;
- my $url = $ENV{'form.url'};
- my $symb = $ENV{'form.symb'};
- my $button = $ENV{'form.gradeOpt'};
- my $ngrade = $ENV{'form.NCT'};
- my $ntstu = $ENV{'form.NTSTU'};
+ my $symb = &get_symb($request);
+ my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
+ my $button = $env{'form.gradeOpt'};
+ my $ngrade = $env{'form.NCT'};
+ my $ntstu = $env{'form.NTSTU'};
+ my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+ my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
if ($button eq 'Save & Next') {
my $ctr = 0;
while ($ctr < $ngrade) {
- my ($uname,$udom) = split(/:/,$ENV{'form.unamedom'.$ctr});
- my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$url,$symb,$uname,$udom,$ctr);
+ my ($uname,$udom) = split(/:/,$env{'form.unamedom'.$ctr});
+ my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$symb,$uname,$udom,$ctr);
if ($errorflag eq 'no_score') {
$ctr++;
next;
}
if ($errorflag eq 'not_allowed') {
- $request->print("Not allowed to modify grades for $uname:$udom ");
+ $request->print("Not allowed to modify grades for $uname:$udom ");
$ctr++;
next;
}
- my $includemsg = $ENV{'form.includemsg'.$ctr};
+ my $includemsg = $env{'form.includemsg'.$ctr};
my ($subject,$message,$msgstatus) = ('','','');
+ my $restitle = &Apache::lonnet::gettitle($symb);
+ my ($feedurl,$showsymb) =
+ &get_feedurl_and_symb($symb,$uname,$udom);
+ my $messagetail;
if ($includemsg =~ /savemsg|newmsg\Q$ctr\E/) {
- $subject = $ENV{'form.msgsub'} if ($includemsg =~ /^msgsub/);
+ $subject = $env{'form.msgsub'} if ($includemsg =~ /msgsub/);
+ unless ($subject=~/\w/) { $subject=&mt('Grading Feedback'); }
+ $subject.=' ['.$restitle.']';
my (@msgnum) = split(/,/,$includemsg);
foreach (@msgnum) {
- $message.=$ENV{'form.'.$_} if ($_ =~ /savemsg|newmsg/ && $_ ne '');
+ $message.=$env{'form.'.$_} if ($_ =~ /savemsg|newmsg/ && $_ ne '');
}
$message =&Apache::lonfeedback::clear_out_html($message);
- $message.="\n\nPoint".($pts > 1 ? 's':'').' awarded = '.$pts.' out of '.$wgt;
- $message.=" for $ENV{'form.probTitle'} ";
- $msgstatus = &Apache::lonmsg::user_normal_msg ($uname,$udom,
- $ENV{'form.msgsub'},$message);
- }
- if ($ENV{'form.collaborator'.$ctr}) {
- my (@collaborators) = split(/:/,$ENV{'form.collaborator'.$ctr});
- foreach (@collaborators) {
- my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$url,$symb,$_,$udom,$ctr,$ENV{'form.unamedom'.$ctr});
- if ($errorflag eq 'not_allowed') {
- $request->print("Not allowed to modify grades for $_:$udom ");
- next;
- } else {
- if ($message ne '') {
- $msgstatus = &Apache::lonmsg::user_normal_msg ($_,$udom,
- $ENV{'form.msgsub'},
- $message);
+ if ($env{'form.withgrades'.$ctr}) {
+ $message.="\n\nPoint".($pts > 1 ? 's':'').' awarded = '.$pts.' out of '.$wgt;
+ $messagetail = " for $env{'form.probTitle'} ";
+ }
+ $msgstatus =
+ &Apache::lonmsg::user_normal_msg($uname,$udom,$subject,
+ $message.$messagetail,
+ undef,$feedurl,undef,
+ undef,undef,$showsymb,
+ $restitle);
+ $request->print(' '.&mt('Sending message to [_1]:[_2]',$uname,$udom).': '.
+ $msgstatus);
+ }
+ if ($env{'form.collaborator'.$ctr}) {
+ my @collabstrs=&Apache::loncommon::get_env_multiple("form.collaborator$ctr");
+ foreach my $collabstr (@collabstrs) {
+ my ($part,@collaborators) = split(/:/,$collabstr);
+ foreach my $collaborator (@collaborators) {
+ my ($errorflag,$pts,$wgt) =
+ &saveHandGrade($request,$symb,$collaborator,$udom,$ctr,
+ $env{'form.unamedom'.$ctr},$part);
+ if ($errorflag eq 'not_allowed') {
+ $request->print("".&mt('Not allowed to modify grades for [_1]',"$collaborator:$udom")." ");
+ next;
+ } elsif ($message ne '') {
+ my ($baseurl,$showsymb) =
+ &get_feedurl_and_symb($symb,$collaborator,
+ $udom);
+ if ($env{'form.withgrades'.$ctr}) {
+ $messagetail = " for $env{'form.probTitle'} ";
+ }
+ $msgstatus =
+ &Apache::lonmsg::user_normal_msg($collaborator,$udom,$subject,$message.$messagetail,undef,$baseurl,undef,undef,undef,$showsymb,$restitle);
}
}
}
@@ -1537,79 +2427,94 @@ sub processHandGrade {
}
}
- # Keywords sorted in alphabatical order
- my $loginuser = $ENV{'user.name'}.':'.$ENV{'user.domain'};
- my %keyhash = ();
- $ENV{'form.keywords'} =~ s/,\s{0,}|\s+/ /g;
- $ENV{'form.keywords'} =~ s/^\s+|\s+$//;
- my (@keywords) = sort(split(/\s+/,$ENV{'form.keywords'}));
- $ENV{'form.keywords'} = join(' ',@keywords);
- $keyhash{$symb.'_keywords'} = $ENV{'form.keywords'};
- $keyhash{$symb.'_subject'} = $ENV{'form.msgsub'};
- $keyhash{$loginuser.'_kwclr'} = $ENV{'form.kwclr'};
- $keyhash{$loginuser.'_kwsize'} = $ENV{'form.kwsize'};
- $keyhash{$loginuser.'_kwstyle'} = $ENV{'form.kwstyle'};
-
- # message center - Order of message gets changed. Blank line is eliminated.
- # New messages are saved in ENV for the next student.
- # All messages are saved in nohist_handgrade.db
- my ($ctr,$idx) = (1,1);
- while ($ctr <= $ENV{'form.savemsgN'}) {
- if ($ENV{'form.savemsg'.$ctr} ne '') {
- $keyhash{$symb.'_savemsg'.$idx} = $ENV{'form.savemsg'.$ctr};
- $idx++;
+ if ($env{'form.handgrade'} eq 'yes') {
+ # Keywords sorted in alphabatical order
+ my $loginuser = $env{'user.name'}.':'.$env{'user.domain'};
+ my %keyhash = ();
+ $env{'form.keywords'} =~ s/,\s{0,}|\s+/ /g;
+ $env{'form.keywords'} =~ s/^\s+|\s+$//;
+ my (@keywords) = sort(split(/\s+/,$env{'form.keywords'}));
+ $env{'form.keywords'} = join(' ',@keywords);
+ $keyhash{$symb.'_keywords'} = $env{'form.keywords'};
+ $keyhash{$symb.'_subject'} = $env{'form.msgsub'};
+ $keyhash{$loginuser.'_kwclr'} = $env{'form.kwclr'};
+ $keyhash{$loginuser.'_kwsize'} = $env{'form.kwsize'};
+ $keyhash{$loginuser.'_kwstyle'} = $env{'form.kwstyle'};
+
+ # message center - Order of message gets changed. Blank line is eliminated.
+ # New messages are saved in env for the next student.
+ # All messages are saved in nohist_handgrade.db
+ my ($ctr,$idx) = (1,1);
+ while ($ctr <= $env{'form.savemsgN'}) {
+ if ($env{'form.savemsg'.$ctr} ne '') {
+ $keyhash{$symb.'_savemsg'.$idx} = $env{'form.savemsg'.$ctr};
+ $idx++;
+ }
+ $ctr++;
}
- $ctr++;
- }
- $ctr = 0;
- while ($ctr < $ngrade) {
- if ($ENV{'form.newmsg'.$ctr} ne '') {
- $keyhash{$symb.'_savemsg'.$idx} = $ENV{'form.newmsg'.$ctr};
- $ENV{'form.savemsg'.$idx} = $ENV{'form.newmsg'.$ctr};
- $idx++;
+ $ctr = 0;
+ while ($ctr < $ngrade) {
+ if ($env{'form.newmsg'.$ctr} ne '') {
+ $keyhash{$symb.'_savemsg'.$idx} = $env{'form.newmsg'.$ctr};
+ $env{'form.savemsg'.$idx} = $env{'form.newmsg'.$ctr};
+ $idx++;
+ }
+ $ctr++;
}
- $ctr++;
+ $env{'form.savemsgN'} = --$idx;
+ $keyhash{$symb.'_savemsgN'} = $env{'form.savemsgN'};
+ my $putresult = &Apache::lonnet::put
+ ('nohist_handgrade',\%keyhash,$cdom,$cnum);
}
- $ENV{'form.savemsgN'} = --$idx;
- $keyhash{$symb.'_savemsgN'} = $ENV{'form.savemsgN'};
- my $putresult = &Apache::lonnet::put
- ('nohist_handgrade',\%keyhash,
- $ENV{'course.'.$ENV{'request.course.id'}.'.domain'},
- $ENV{'course.'.$ENV{'request.course.id'}.'.num'});
-
# Called by Save & Refresh from Highlight Attribute Window
- my (undef,undef,$fullname) = &getclasslist($ENV{'form.section'},'0');
- if ($ENV{'form.refresh'} eq 'on') {
+ my (undef,undef,$fullname) = &getclasslist($env{'form.section'},'1');
+ if ($env{'form.refresh'} eq 'on') {
my ($ctr,$total) = (0,0);
while ($ctr < $ngrade) {
- $total++ if $ENV{'form.unamedom'.$ctr} ne '';
+ $total++ if $env{'form.unamedom'.$ctr} ne '';
$ctr++;
}
- $ENV{'form.NTSTU'}=$ngrade;
+ $env{'form.NTSTU'}=$ngrade;
$ctr = 0;
while ($ctr < $total) {
- my $processUser = $ENV{'form.unamedom'.$ctr};
- ($ENV{'form.student'},$ENV{'form.userdom'}) = split(/:/,$processUser);
- $ENV{'form.fullname'} = $$fullname{$processUser};
+ my $processUser = $env{'form.unamedom'.$ctr};
+ ($env{'form.student'},$env{'form.userdom'}) = split(/:/,$processUser);
+ $env{'form.fullname'} = $$fullname{$processUser};
&submission($request,$ctr,$total-1);
$ctr++;
}
return '';
}
+# Go directly to grade student - from submission or link from chart page
+ if ($button eq 'Grade Student') {
+ (undef,undef,$env{'form.handgrade'},undef,undef) = &showResourceInfo($symb);
+ my $processUser = $env{'form.unamedom'.$env{'form.studentNo'}};
+ ($env{'form.student'},$env{'form.userdom'}) = split(/:/,$processUser);
+ $env{'form.fullname'} = $$fullname{$processUser};
+ &submission($request,0,0);
+ return '';
+ }
+
# Get the next/previous one or group of students
- my $firststu = $ENV{'form.unamedom0'};
- my $laststu = $ENV{'form.unamedom'.($ngrade-1)};
- $ctr = 2;
+ my $firststu = $env{'form.unamedom0'};
+ my $laststu = $env{'form.unamedom'.($ngrade-1)};
+ my $ctr = 2;
while ($laststu eq '') {
- $laststu = $ENV{'form.unamedom'.($ngrade-$ctr)};
+ $laststu = $env{'form.unamedom'.($ngrade-$ctr)};
$ctr++;
$laststu = $firststu if ($ctr > $ngrade);
}
my (@parsedlist,@nextlist);
my ($nextflg) = 0;
- foreach (sort {lc($$fullname{$a}) cmp lc($$fullname{$b}) } keys %$fullname) {
+ foreach (sort
+ {
+ if (lc($$fullname{$a}) ne lc($$fullname{$b})) {
+ return (lc($$fullname{$a}) cmp lc($$fullname{$b}));
+ }
+ return $a cmp $b;
+ } (keys(%$fullname))) {
if ($nextflg == 1 && $button =~ /Next$/) {
push @parsedlist,$_;
}
@@ -1620,21 +2525,42 @@ sub processHandGrade {
}
}
$ctr = 0;
- my ($partlist,$handgrade) = &response_type($ENV{'form.url'});
@parsedlist = reverse @parsedlist if ($button eq 'Previous');
+ my ($partlist) = &response_type($symb);
foreach my $student (@parsedlist) {
+ my $submitonly=$env{'form.submitonly'};
my ($uname,$udom) = split(/:/,$student);
- if ($ENV{'form.submitonly'} eq 'yes') {
- my (%status) = &student_gradeStatus($ENV{'form.url'},$symb,$udom,$uname,$partlist) ;
- my $statusflg = '';
+
+ if ($submitonly eq 'queued') {
+ my %queue_status =
+ &Apache::bridgetask::get_student_status($symb,$cdom,$cnum,
+ $udom,$uname);
+ next if (!defined($queue_status{'gradingqueue'}));
+ }
+
+ if ($submitonly =~ /^(yes|graded|incorrect)$/) {
+# my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname);
+ my %status=&student_gradeStatus($symb,$udom,$uname,$partlist);
+ my $submitted = 0;
+ my $ungraded = 0;
+ my $incorrect = 0;
foreach (keys(%status)) {
- $statusflg = 1 if ($status{$_} ne 'nothing');
- my ($foo,$partid,$foo1) = split(/\./);
- $statusflg = '' if ($status{'resource.'.$partid.'.submitted_by'} ne '');
+ $submitted = 1 if ($status{$_} ne 'nothing');
+ $ungraded = 1 if ($status{$_} =~ /^ungraded/);
+ $incorrect = 1 if ($status{$_} =~ /^incorrect/);
+ my ($foo,$partid,$foo1) = split(/\./,$_);
+ if ($status{'resource.'.$partid.'.submitted_by'} ne '') {
+ $submitted = 0;
+ }
}
- next if ($statusflg eq '');
+ next if (!$submitted && ($submitonly eq 'yes' ||
+ $submitonly eq 'incorrect' ||
+ $submitonly eq 'graded'));
+ next if (!$ungraded && ($submitonly eq 'graded'));
+ next if (!$incorrect && $submitonly eq 'incorrect');
}
push @nextlist,$student if ($ctr < $ntstu);
+ last if ($ctr == $ntstu);
$ctr++;
}
@@ -1643,17 +2569,17 @@ sub processHandGrade {
foreach (sort @nextlist) {
my ($uname,$udom,$submitter) = split(/:/);
- $ENV{'form.student'} = $uname;
- $ENV{'form.userdom'} = $udom;
- $ENV{'form.fullname'} = $$fullname{$_};
+ $env{'form.student'} = $uname;
+ $env{'form.userdom'} = $udom;
+ $env{'form.fullname'} = $$fullname{$_};
&submission($request,$ctr,$total);
$ctr++;
}
if ($total < 0) {
- my $the_end = 'LON-CAPA User Message '."\n";
+ my $the_end = 'LON-CAPA User Message '."\n";
$the_end.='Message: No more students for this section or class. '."\n";
$the_end.='Click on the button below to return to the grading menu. '."\n";
- $the_end.=&show_grading_menu_form ($symb,$url);
+ $the_end.=&show_grading_menu_form($symb);
$request->print($the_end);
}
return '';
@@ -1661,50 +2587,378 @@ sub processHandGrade {
#---- Save the score and award for each student, if changed
sub saveHandGrade {
- my ($request,$url,$symb,$stuname,$domain,$newflg,$submitter) = @_;
+ my ($request,$symb,$stuname,$domain,$newflg,$submitter,$part) = @_;
+ my @version_parts;
my $usec = &Apache::lonnet::getsection($domain,$stuname,
- $ENV{'request.course.id'});
+ $env{'request.course.id'});
if (!&canmodify($usec)) { return('not_allowed'); }
- my %record = &Apache::lonnet::restore($symb,$ENV{'request.course.id'},$domain,$stuname);
+ my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$domain,$stuname);
+ my @parts_graded;
my %newrecord = ();
my ($pts,$wgt) = ('','');
- foreach (split(/:/,$ENV{'form.partlist'.$newflg})) {
- if ($ENV{'form.GD_SEL'.$newflg.'_'.$_} eq 'excused') {
- if ($record{'resource.'.$_.'.solved'} ne 'excused') {
- $newrecord{'resource.'.$_.'.solved'} = 'excused';
- if (exists($record{'resource.'.$_.'.awarded'})) {
- $newrecord{'resource.'.$_.'.awarded'} = '';
+ my %aggregate = ();
+ my $aggregateflag = 0;
+ my @parts = split(/:/,$env{'form.partlist'.$newflg});
+ foreach my $new_part (@parts) {
+ #collaborator ($submi may vary for different parts
+ if ($submitter && $new_part ne $part) { next; }
+ my $dropMenu = $env{'form.GD_SEL'.$newflg.'_'.$new_part};
+ if ($dropMenu eq 'excused') {
+ if ($record{'resource.'.$new_part.'.solved'} ne 'excused') {
+ $newrecord{'resource.'.$new_part.'.solved'} = 'excused';
+ if (exists($record{'resource.'.$new_part.'.awarded'})) {
+ $newrecord{'resource.'.$new_part.'.awarded'} = '';
}
+ $newrecord{'resource.'.$new_part.'.regrader'}="$env{'user.name'}:$env{'user.domain'}";
}
- } else {
- $pts = ($ENV{'form.GD_BOX'.$newflg.'_'.$_} ne '' ?
- $ENV{'form.GD_BOX'.$newflg.'_'.$_} :
- $ENV{'form.RADVAL'.$newflg.'_'.$_});
- return 'no_score' if ($pts eq '' && $ENV{'form.GD_SEL'.$newflg.'_'.$_} eq '');
- $wgt = $ENV{'form.WGT'.$newflg.'_'.$_} eq '' ? 1 :
- $ENV{'form.WGT'.$newflg.'_'.$_};
+ } elsif ($dropMenu eq 'reset status'
+ && exists($record{'resource.'.$new_part.'.solved'})) { #don't bother if no old records -> no attempts
+ foreach my $key (keys (%record)) {
+ if ($key=~/^resource\.\Q$new_part\E\./) { $newrecord{$key} = ''; }
+ }
+ $newrecord{'resource.'.$new_part.'.regrader'}=
+ "$env{'user.name'}:$env{'user.domain'}";
+ my $totaltries = $record{'resource.'.$part.'.tries'};
+
+ my %last_resets = &get_last_resets($symb,$env{'request.course.id'},
+ [$new_part]);
+ my $aggtries =$totaltries;
+ if ($last_resets{$new_part}) {
+ $aggtries = &get_num_tries(\%record,$last_resets{$new_part},
+ $new_part);
+ }
+
+ my $solvedstatus = $record{'resource.'.$new_part.'.solved'};
+ if ($aggtries > 0) {
+ &decrement_aggs($symb,$new_part,\%aggregate,$aggtries,$totaltries,$solvedstatus);
+ $aggregateflag = 1;
+ }
+ } elsif ($dropMenu eq '') {
+ $pts = ($env{'form.GD_BOX'.$newflg.'_'.$new_part} ne '' ?
+ $env{'form.GD_BOX'.$newflg.'_'.$new_part} :
+ $env{'form.RADVAL'.$newflg.'_'.$new_part});
+ if ($pts eq '' && $env{'form.GD_SEL'.$newflg.'_'.$new_part} eq '') {
+ next;
+ }
+ $wgt = $env{'form.WGT'.$newflg.'_'.$new_part} eq '' ? 1 :
+ $env{'form.WGT'.$newflg.'_'.$new_part};
my $partial= $pts/$wgt;
- $newrecord{'resource.'.$_.'.awarded'} = $partial
- if ($record{'resource.'.$_.'.awarded'} ne $partial);
- my $reckey = 'resource.'.$_.'.solved';
+ if ($partial eq $record{'resource.'.$new_part.'.awarded'}) {
+ #do not update score for part if not changed.
+ &handback_files($request,$symb,$stuname,$domain,$newflg,$new_part,\%newrecord);
+ next;
+ } else {
+ push @parts_graded, $new_part;
+ }
+ if ($record{'resource.'.$new_part.'.awarded'} ne $partial) {
+ $newrecord{'resource.'.$new_part.'.awarded'} = $partial;
+ }
+ my $reckey = 'resource.'.$new_part.'.solved';
if ($partial == 0) {
- $newrecord{$reckey} = 'incorrect_by_override'
- if ($record{$reckey} ne 'incorrect_by_override');
+ if ($record{$reckey} ne 'incorrect_by_override') {
+ $newrecord{$reckey} = 'incorrect_by_override';
+ }
} else {
- $newrecord{$reckey} = 'correct_by_override'
- if ($record{$reckey} ne 'correct_by_override');
+ if ($record{$reckey} ne 'correct_by_override') {
+ $newrecord{$reckey} = 'correct_by_override';
+ }
+ }
+ if ($submitter &&
+ ($record{'resource.'.$new_part.'.submitted_by'} ne $submitter)) {
+ $newrecord{'resource.'.$new_part.'.submitted_by'} = $submitter;
+ }
+ $newrecord{'resource.'.$new_part.'.regrader'}=
+ "$env{'user.name'}:$env{'user.domain'}";
+ }
+ # unless problem has been graded, set flag to version the submitted files
+ unless ($record{'resource.'.$new_part.'.solved'} =~ /^correct_/ ||
+ $record{'resource.'.$new_part.'.solved'} eq 'incorrect_by_override' ||
+ $dropMenu eq 'reset status')
+ {
+ push (@version_parts,$new_part);
+ }
+ }
+ my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+ my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+
+ if (%newrecord) {
+ if (@version_parts) {
+ my @changed_keys = &version_portfiles(\%record, \@parts_graded,
+ $env{'request.course.id'}, $symb, $domain, $stuname, \@version_parts);
+ @newrecord{@changed_keys} = @record{@changed_keys};
+ foreach my $new_part (@version_parts) {
+ &handback_files($request,$symb,$stuname,$domain,$newflg,
+ $new_part,\%newrecord);
}
- $newrecord{'resource.'.$_.'.submitted_by'} = $submitter
- if ($submitter && ($record{'resource.'.$_.'.submitted_by'} ne $submitter));
- $newrecord{'resource.'.$_.'regrader'}="$ENV{'user.name'}:$ENV{'user.domain'}";
- }
+ }
+ &Apache::lonnet::cstore(\%newrecord,$symb,
+ $env{'request.course.id'},$domain,$stuname);
+ &check_and_remove_from_queue(\@parts,\%record,\%newrecord,$symb,
+ $cdom,$cnum,$domain,$stuname);
+ }
+ if ($aggregateflag) {
+ &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate,
+ $cdom,$cnum);
+ }
+ return ('',$pts,$wgt);
+}
+
+sub check_and_remove_from_queue {
+ my ($parts,$record,$newrecord,$symb,$cdom,$cnum,$domain,$stuname) = @_;
+ my @ungraded_parts;
+ foreach my $part (@{$parts}) {
+ if ( $record->{ 'resource.'.$part.'.awarded'} eq ''
+ && $record->{ 'resource.'.$part.'.solved' } ne 'excused'
+ && $newrecord->{'resource.'.$part.'.awarded'} eq ''
+ && $newrecord->{'resource.'.$part.'.solved' } ne 'excused'
+ ) {
+ push(@ungraded_parts, $part);
+ }
+ }
+ if ( !@ungraded_parts ) {
+ &Apache::bridgetask::remove_from_queue('gradingqueue',$symb,$cdom,
+ $cnum,$domain,$stuname);
+ }
+}
+
+sub handback_files {
+ my ($request,$symb,$stuname,$domain,$newflg,$new_part,$newrecord) = @_;
+ my $portfolio_root = &propath($domain,$stuname).'/userfiles/portfolio';
+ my ($partlist,$handgrade,$responseType) = &response_type($symb);
+
+ my @part_response_id = &flatten_responseType($responseType);
+ foreach my $part_response_id (@part_response_id) {
+ my ($part_id,$resp_id) = @{ $part_response_id };
+ my $part_resp = join('_',@{ $part_response_id });
+ if (($env{'form.'.$newflg.'_'.$part_resp.'_returndoc1'}) && ($new_part == $part_id)) {
+ # if multiple files are uploaded names will be 'returndoc2','returndoc3'
+ my $file_counter = 1;
+ my $file_msg;
+ while ($env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$file_counter}) {
+ my $fname=$env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$file_counter.'.filename'};
+ my ($directory,$answer_file) =
+ ($env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$file_counter} =~ /^(.*?)([^\/]*)$/);
+ my ($answer_name,$answer_ver,$answer_ext) =
+ &file_name_version_ext($answer_file);
+ my ($portfolio_path) = ($directory =~ /^.+$stuname\/portfolio(.*)/);
+ my @dir_list = &Apache::lonnet::dirlist($portfolio_path,$domain,$stuname,$portfolio_root);
+ my $version = &get_next_version($answer_name, $answer_ext, \@dir_list);
+ # fix file name
+ my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/);
+ my $result=&Apache::lonnet::finishuserfileupload($stuname,$domain,
+ $newflg.'_'.$part_resp.'_returndoc'.$file_counter,
+ $save_file_name);
+ if ($result !~ m|^/uploaded/|) {
+ $request->print('An error occurred ('.$result.
+ ') while trying to upload '.$newflg.'_'.$part_resp.'_returndoc'.$file_counter.' ');
+ } else {
+ # mark the file as read only
+ my @files = ($save_file_name);
+ my @what = ($symb,$env{'request.course.id'},'handback');
+ &Apache::lonnet::mark_as_readonly($domain,$stuname,\@files,\@what);
+ if (exists($$newrecord{"resource.$new_part.$resp_id.handback"})) {
+ $$newrecord{"resource.$new_part.$resp_id.handback"}.=',';
+ }
+ $$newrecord{"resource.$new_part.$resp_id.handback"} .= $save_file_name;
+ $file_msg.= "\n".''.$save_file_name." ";
+
+ }
+ $request->print(" ".$fname." will be the uploaded file name");
+ $request->print(" ".$env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$file_counter});
+ $file_counter++;
+ }
+ my $subject = "File Handed Back by Instructor ";
+ my $message = "A file has been returned that was originally submitted in reponse to: ";
+ $message .= "".&Apache::lonnet::gettitle($symb)." ";
+ $message .= ' The returned file(s) are named: '. $file_msg;
+ $message .= " and can be found in your portfolio space.";
+ my ($feedurl,$showsymb) =
+ &get_feedurl_and_symb($symb,$domain,$stuname);
+ my $restitle = &Apache::lonnet::gettitle($symb);
+ my $msgstatus =
+ &Apache::lonmsg::user_normal_msg($stuname,$domain,$subject.
+ ' (File Returned) ['.$restitle.']',$message,undef,
+ $feedurl,undef,undef,undef,$showsymb,$restitle);
+ }
+ }
+ return;
+}
+
+sub get_feedurl_and_symb {
+ my ($symb,$uname,$udom) = @_;
+ my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
+ $url = &Apache::lonnet::clutter($url);
+ my $encrypturl=&Apache::lonnet::EXT('resource.0.encrypturl',
+ $symb,$udom,$uname);
+ if ($encrypturl =~ /^yes$/i) {
+ &Apache::lonenc::encrypted(\$url,1);
+ &Apache::lonenc::encrypted(\$symb,1);
+ }
+ return ($url,$symb);
+}
+
+sub get_submitted_files {
+ my ($udom,$uname,$partid,$respid,$record) = @_;
+ my @files;
+ if ($$record{"resource.$partid.$respid.portfiles"}) {
+ my $file_url = '/uploaded/'.$udom.'/'.$uname.'/portfolio';
+ foreach my $file (split(',',$$record{"resource.$partid.$respid.portfiles"})) {
+ push(@files,$file_url.$file);
+ }
+ }
+ if ($$record{"resource.$partid.$respid.uploadedurl"}) {
+ push(@files,$$record{"resource.$partid.$respid.uploadedurl"});
}
+ return (\@files);
+}
- if (scalar(keys(%newrecord)) > 0) {
- &Apache::lonnet::cstore(\%newrecord,$symb,
- $ENV{'request.course.id'},$domain,$stuname);
+# ----------- Provides number of tries since last reset.
+sub get_num_tries {
+ my ($record,$last_reset,$part) = @_;
+ my $timestamp = '';
+ my $num_tries = 0;
+ if ($$record{'version'}) {
+ for (my $version=$$record{'version'};$version>=1;$version--) {
+ if (exists($$record{$version.':resource.'.$part.'.solved'})) {
+ $timestamp = $$record{$version.':timestamp'};
+ if ($timestamp > $last_reset) {
+ $num_tries ++;
+ } else {
+ last;
+ }
+ }
+ }
+ }
+ return $num_tries;
+}
+
+# ----------- Determine decrements required in aggregate totals
+sub decrement_aggs {
+ my ($symb,$part,$aggregate,$aggtries,$totaltries,$solvedstatus) = @_;
+ my %decrement = (
+ attempts => 0,
+ users => 0,
+ correct => 0
+ );
+ $decrement{'attempts'} = $aggtries;
+ if ($solvedstatus =~ /^correct/) {
+ $decrement{'correct'} = 1;
+ }
+ if ($aggtries == $totaltries) {
+ $decrement{'users'} = 1;
}
- return '',$pts,$wgt;
+ foreach my $type (keys (%decrement)) {
+ $$aggregate{$symb."\0".$part."\0".$type} = -$decrement{$type};
+ }
+ return;
+}
+
+# ----------- Determine timestamps for last reset of aggregate totals for parts
+sub get_last_resets {
+ my ($symb,$courseid,$partids) =@_;
+ my %last_resets;
+ my $cdom = $env{'course.'.$courseid.'.domain'};
+ my $cname = $env{'course.'.$courseid.'.num'};
+ my @keys;
+ foreach my $part (@{$partids}) {
+ push(@keys,"$symb\0$part\0resettime");
+ }
+ my %results=&Apache::lonnet::get('nohist_resourcetracker',\@keys,
+ $cdom,$cname);
+ foreach my $part (@{$partids}) {
+ $last_resets{$part}=$results{"$symb\0$part\0resettime"};
+ }
+ return %last_resets;
+}
+
+# ----------- Handles creating versions for portfolio files as answers
+sub version_portfiles {
+ my ($record, $parts_graded, $courseid, $symb, $domain, $stu_name, $v_flag) = @_;
+ my $version_parts = join('|',@$v_flag);
+ my @returned_keys;
+ my $parts = join('|', @$parts_graded);
+ my $portfolio_root = &propath($domain,$stu_name).
+ '/userfiles/portfolio';
+ foreach my $key (keys(%$record)) {
+ my $new_portfiles;
+ if ($key =~ /^resource\.($version_parts)\./ && $key =~ /\.portfiles$/ ) {
+ my @versioned_portfiles;
+ my @portfiles = split(/\s*,\s*/,$$record{$key});
+ foreach my $file (@portfiles) {
+ &Apache::lonnet::unmark_as_readonly($domain,$stu_name,[$symb,$env{'request.course.id'}],$file);
+ my ($directory,$answer_file) =($file =~ /^(.*?)([^\/]*)$/);
+ my ($answer_name,$answer_ver,$answer_ext) =
+ &file_name_version_ext($answer_file);
+ my @dir_list = &Apache::lonnet::dirlist($directory,$domain,$stu_name,$portfolio_root);
+ my $version = &get_next_version($answer_name, $answer_ext, \@dir_list);
+ my $new_answer = &version_selected_portfile($domain, $stu_name, $directory, $answer_file, $version);
+ if ($new_answer ne 'problem getting file') {
+ push(@versioned_portfiles, $directory.$new_answer);
+ &Apache::lonnet::mark_as_readonly($domain,$stu_name,
+ [$directory.$new_answer],
+ [$symb,$env{'request.course.id'},'graded']);
+ }
+ }
+ $$record{$key} = join(',',@versioned_portfiles);
+ push(@returned_keys,$key);
+ }
+ }
+ return (@returned_keys);
+}
+
+sub get_next_version {
+ my ($answer_name, $answer_ext, $dir_list) = @_;
+ my $version;
+ foreach my $row (@$dir_list) {
+ my ($file) = split(/\&/,$row,2);
+ my ($file_name,$file_version,$file_ext) =
+ &file_name_version_ext($file);
+ if (($file_name eq $answer_name) &&
+ ($file_ext eq $answer_ext)) {
+ # gets here if filename and extension match, regardless of version
+ if ($file_version ne '') {
+ # a versioned file is found so save it for later
+ if ($file_version > $version) {
+ $version = $file_version;
+ }
+ }
+ }
+ }
+ $version ++;
+ return($version);
+}
+
+sub version_selected_portfile {
+ my ($domain,$stu_name,$directory,$file_name,$version) = @_;
+ my ($answer_name,$answer_ver,$answer_ext) =
+ &file_name_version_ext($file_name);
+ my $new_answer;
+ $env{'form.copy'} = &Apache::lonnet::getfile("/uploaded/$domain/$stu_name/portfolio$directory$file_name");
+ if($env{'form.copy'} eq '-1') {
+ $new_answer = 'problem getting file';
+ } else {
+ $new_answer = $answer_name.'.'.$version.'.'.$answer_ext;
+ my $copy_result = &Apache::lonnet::finishuserfileupload(
+ $stu_name,$domain,'copy',
+ '/portfolio'.$directory.$new_answer);
+ }
+ return ($new_answer);
+}
+
+sub file_name_version_ext {
+ my ($file)=@_;
+ my @file_parts = split(/\./, $file);
+ my ($name,$version,$ext);
+ if (@file_parts > 1) {
+ $ext=pop(@file_parts);
+ if (@file_parts > 1 && $file_parts[-1] =~ /^\d+$/) {
+ $version=pop(@file_parts);
+ }
+ $name=join('.',@file_parts);
+ } else {
+ $name=join('.',@file_parts);
+ }
+ return($name,$version,$ext);
}
#--------------------------------------------------------------------------------------
@@ -1718,10 +2972,10 @@ sub viewgrades_js {
$request->print(<
function writePoint(partid,weight,point) {
- var radioButton = eval("document.classgrade.RADVAL_"+partid);
- var textbox = eval("document.classgrade.TEXTVAL_"+partid);
+ var radioButton = document.classgrade["RADVAL_"+partid];
+ var textbox = document.classgrade["TEXTVAL_"+partid];
if (point == "textval") {
- var point = eval("document.classgrade.TEXTVAL_"+partid+".value");
+ point = document.classgrade["TEXTVAL_"+partid].value;
if (isNaN(point) || parseFloat(point) < 0) {
alert("A number equal or greater than 0 is expected. Entered value = "+parseFloat(point));
var resetbox = false;
@@ -1752,15 +3006,14 @@ sub viewgrades_js {
}
} else {
- textbox.value = point;
+ textbox.value = parseFloat(point);
}
for (i=0;iManual Grading ';
+ my ($symb) = &get_symb($request);
+ #need to make sure we have the correct data for later EXT calls,
+ #thus invalidate the cache
+ &Apache::lonnet::devalidatecourseresdata(
+ $env{'course.'.$env{'request.course.id'}.'.num'},
+ $env{'course.'.$env{'request.course.id'}.'.domain'});
+ &Apache::lonnet::clear_EXT_cache_status();
- $result.='Problem: '.$ENV{'form.probTitle'}.' '."\n";
+ my $result=''.&mt('Manual Grading').' ';
+ $result.='Current Resource: '.$env{'form.probTitle'}.' '."\n";
#view individual student submission form - called using Javascript viewOneStudent
- $result.=&jscriptNform($url,$symb);
+ $result.=&jscriptNform($symb);
#beginning of class grading form
+ my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
$result.= ''."\n";
- $studentTable.=&show_grading_menu_form($symb,$url);
+ $studentTable.=&show_grading_menu_form($symb);
$request->print($studentTable);
return '';
}
sub getSymbMap {
- my ($request) = @_;
- my $navmap = Apache::lonnavmaps::navmap-> new($ENV{'request.course.fn'}.'.db',
- $ENV{'request.course.fn'}.'_parms.db',1, 1);
-
- my $res = $navmap->firstResource(); # temp resource to access constants
- $navmap->init();
-
- # End navmap using boilerplate
-
- my $iterator = $navmap->getIterator(undef, undef, undef, 1);
- my $depth = 1;
- $iterator->next(); # ignore first BEGIN_MAP
- my $curRes = $iterator->next();
+ my $navmap = Apache::lonnavmaps::navmap->new();
my %symbx = ();
my @titles = ();
- my $minder=0;
- while ($depth > 0) {
- if ($curRes == $iterator->BEGIN_MAP()) {$depth++;}
- if ($curRes == $iterator->END_MAP()) { $depth--; }
+ my $minder = 0;
- if (ref($curRes) && $curRes->is_map()) {
- my ($mapUrl, $id, $resUrl) = split(/___/, $curRes->symb()); # check map contains at least one problem
- my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps
-
- my $mapiterator = $navmap->getIterator($map->map_start(),
- $map->map_finish());
-
- my $mapdepth = 1;
- my $countProblems = 0;
- $mapiterator->next(); # skip the first BEGIN_MAP
- my $mapcurRes = $mapiterator->next(); # for "current resource"
- while ($mapdepth > 0) {
- if($mapcurRes == $mapiterator->BEGIN_MAP) { $mapdepth++; }
- if($mapcurRes == $mapiterator->END_MAP) { $mapdepth--; }
-
- if (ref($mapcurRes) && $mapcurRes->is_problem() && !$mapcurRes->randomout) {
- $countProblems++;
- }
- $mapcurRes = $mapiterator->next();
- }
- if ($countProblems > 0) {
- my $title = $curRes->compTitle();
- push @titles,$minder.'.'.$title; # minder, just in case two titles are identical
- $symbx{$minder.'.'.$title} = $curRes->symb();
- $minder++;
- }
- }
- $curRes = $iterator->next();
+ # Gather every sequence that has problems.
+ my @sequences = $navmap->retrieveResources(undef, sub { shift->is_map(); },
+ 1,0,1);
+ for my $sequence ($navmap->getById('0.0'), @sequences) {
+ if ($navmap->hasResource($sequence, sub { shift->is_problem(); }, 0) ) {
+ my $title = $minder.'.'.
+ &HTML::Entities::encode($sequence->compTitle(),'"\'&');
+ push(@titles, $title); # minder in case two titles are identical
+ $symbx{$title} = &HTML::Entities::encode($sequence->symb(),'"\'&');
+ $minder++;
+ }
}
-
- $navmap->untieHashes();
return \@titles,\%symbx;
}
@@ -2719,128 +4162,128 @@ sub getSymbMap {
sub displayPage {
my ($request) = shift;
- my ($symb,$url) = &get_symb_and_url($request);
- my $cdom = $ENV{"course.$ENV{'request.course.id'}.domain"};
- my $cnum = $ENV{"course.$ENV{'request.course.id'}.num"};
- my $getsec = $ENV{'form.section'} eq '' ? 'all' : $ENV{'form.section'};
- my $pageTitle = $ENV{'form.page'};
+ my ($symb) = &get_symb($request);
+ my $cdom = $env{"course.$env{'request.course.id'}.domain"};
+ my $cnum = $env{"course.$env{'request.course.id'}.num"};
+ my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'};
+ my $pageTitle = $env{'form.page'};
my ($classlist,undef,$fullname) = &getclasslist($getsec,'1');
- my ($uname,$udom) = split(/:/,$ENV{'form.student'});
- my $usec=$classlist->{$ENV{'form.student'}}[5];
+ my ($uname,$udom) = split(/:/,$env{'form.student'});
+ my $usec=$classlist->{$env{'form.student'}}[5];
+
+ #need to make sure we have the correct data for later EXT calls,
+ #thus invalidate the cache
+ &Apache::lonnet::devalidatecourseresdata(
+ $env{'course.'.$env{'request.course.id'}.'.num'},
+ $env{'course.'.$env{'request.course.id'}.'.domain'});
+ &Apache::lonnet::clear_EXT_cache_status();
+
if (!&canview($usec)) {
- $request->print('Unable to view requested student.('.$ENV{'form.student'}.') ');
- $request->print(&show_grading_menu_form($symb,$url));
+ $request->print('Unable to view requested student.('.$env{'form.student'}.') ');
+ $request->print(&show_grading_menu_form($symb));
return;
}
- my $result=' '.$ENV{'form.title'}.' ';
- $result.=' Student: '.$$fullname{$ENV{'form.student'}}.
- ' ('.$uname.($udom eq $cdom ? '':':'.$udom).') '."\n";
-
+ my $result=' '.$env{'form.title'}.' ';
+ $result.=' Student: '.&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom).
+ ' '."\n";
+ if (&Apache::lonnet::validCODE($env{'form.CODE'})) {
+ $result.=' CODE: '.$env{'form.CODE'}.' '."\n";
+ } else {
+ delete($env{'form.CODE'});
+ }
&sub_page_js($request);
$request->print($result);
- my $navmap = Apache::lonnavmaps::navmap-> new($ENV{'request.course.fn'}.'.db',
- $ENV{'request.course.fn'}.'_parms.db',1, 1);
- my ($mapUrl, $id, $resUrl) = split(/___/, $ENV{'form.page'});
+ my $navmap = Apache::lonnavmaps::navmap->new();
+ my ($mapUrl, $id, $resUrl)=&Apache::lonnet::decode_symb($env{'form.page'});
my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps
-
+ if (!$map) {
+ $request->print('Unable to view requested sequence. ('.$resUrl.') ');
+ $request->print(&show_grading_menu_form($symb));
+ return;
+ }
my $iterator = $navmap->getIterator($map->map_start(),
$map->map_finish());
my $studentTable=''."\n".
' '."\n".
- ' '."\n".
+ ' '."\n".
+ ' '."\n".
' '."\n".
- ' '."\n".
- ' '."\n".
- ' '."\n".
- ' '."\n";
-
- my $checkIcon = ' '."\n".
+ ' '."\n".
+ ' '."\n".
+ ' '."\n";
+
+ if (defined($env{'form.CODE'})) {
+ $studentTable.=
+ ' '."\n";
+ }
+ my $checkIcon = ' ';
- $studentTable.=' Note: A problem graded correct ('.$checkIcon.
- ') by the computer cannot be changed.'."\n".
+ $studentTable.=' Note: Problems graded correct by the computer are marked with a '.$checkIcon.
+ ' symbol.'."\n".
''.
''.
- ' No '.
- ' '.($ENV{'form.vProb'} eq 'no' ? 'Title' : 'Problem View').'/Grade ';
+ ' Prob. '.
+ ' '.($env{'form.vProb'} eq 'no' ? 'Title' : 'Problem Text').'/Grade ';
- my ($depth,$question) = (1,1);
+ &Apache::lonxml::clear_problem_counter();
+ my ($depth,$question,$prob) = (1,1,1);
$iterator->next(); # skip the first BEGIN_MAP
my $curRes = $iterator->next(); # for "current resource"
while ($depth > 0) {
if($curRes == $iterator->BEGIN_MAP) { $depth++; }
if($curRes == $iterator->END_MAP) { $depth--; }
- if (ref($curRes) && $curRes->is_problem() && !$curRes->randomout) {
+ if (ref($curRes) && $curRes->is_problem()) {
my $parts = $curRes->parts();
my $title = $curRes->compTitle();
my $symbx = $curRes->symb();
- $studentTable.=''.$question.
- (scalar(@{$parts}) == 1 ? '' : ' ('.scalar(@{$parts}).' parts)').' ';
+ $studentTable.=''.$prob.
+ (scalar(@{$parts}) == 1 ? '' : ' ('.scalar(@{$parts}).' parts)').' ';
$studentTable.='';
- if ($ENV{'form.vProb'} eq 'yes') {
- $studentTable.=&show_problem($request,$symbx,$uname,$udom,1);
+ my %form = ('CODE' => $env{'form.CODE'},);
+ if ($env{'form.vProb'} eq 'yes' ) {
+ $studentTable.=&show_problem($request,$symbx,$uname,$udom,1,
+ undef,'both',\%form);
} else {
- my $companswer = &Apache::loncommon::get_student_answers(
- $symbx,$uname,$udom,$ENV{'request.course.id'});
+ my $companswer = &Apache::loncommon::get_student_answers($symbx,$uname,$udom,$env{'request.course.id'},%form);
$companswer =~ s|||g;
$companswer =~ s| ||g;
-
# while ($companswer =~ /()/s) { # \n");
# }
-# $companswer =~ s///g;
- $studentTable.=' '.$title.' Correct answer: '.$companswer;
+# $companswer =~ s|||g;
+ $studentTable.=' '.$title.' Correct answer: '.$companswer;
}
- my %record = &Apache::lonnet::restore($symbx,$ENV{'request.course.id'},$udom,$uname);
+ my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname);
- if ($ENV{'form.lastSub'} eq 'datesub') {
+ if ($env{'form.lastSub'} eq 'datesub') {
if ($record{'version'} eq '') {
- $studentTable.=' No recorded submission for this problem ';
+ $studentTable.=' No recorded submission for this problem ';
} else {
- $studentTable.=''.
- ''.
- 'Date/Time '.
- 'Submission '.
- 'Status ';
- my ($version);
- for ($version=1;$version<=$record{'version'};$version++) {
- my $timestamp = scalar(localtime($record{$version.':timestamp'}));
- $studentTable.=''.$timestamp.' ';
- my @versionKeys = split(/\:/,$record{$version.':keys'});
- my @displaySub = ();
- foreach my $partid (@{$parts}) {
- my @matchKey = grep /^resource\.$partid\..*?\.submission$/,@versionKeys;
- next if ($record{"$version:resource.$partid.solved"} eq '');
-# next if ($record{"$version:resource.$partid.award"} eq 'APPROX_ANS' &&
-# $record{"$version:resource.$partid.solved"} eq '');
- $displaySub[0].=(exists $record{$version.':'.$matchKey[0]}) ?
- 'Part '.$partid.' '.
- ($record{"$version:resource.$partid.tries"} eq '' ? 'Trial not counted' :
- 'Trial '.$record{"$version:resource.$partid.tries"}).' '.
- $record{$version.':'.$matchKey[0]}.' ' : '';
- $displaySub[1].=(exists $record{"$version:resource.$partid.award"}) ?
- 'Part '.$partid.' '.
- $record{"$version:resource.$partid.award"}.'/'.
- $record{"$version:resource.$partid.solved"}.' ' : '';
- $displaySub[2].=(exists $record{"$version:resource.$partid.regrader"}) ?
- $record{"$version:resource.$partid.regrader"}.' (Part: '.$partid.')' : '';
+ my %responseType = ();
+ foreach my $partid (@{$parts}) {
+ my @responseIds =$curRes->responseIds($partid);
+ my @responseType =$curRes->responseType($partid);
+ my %responseIds;
+ for (my $i=0;$i<=$#responseIds;$i++) {
+ $responseIds{$responseIds[$i]}=$responseType[$i];
}
- $displaySub[2].=(exists $record{"$version:resource.regrader"}) ?
- $record{"$version:resource.regrader"} : '';
- $studentTable.=''.$displaySub[0].' '.$displaySub[1].
- ($displaySub[2] eq '' ? '' : 'Manually graded by '.$displaySub[2]).' ';
+ $responseType{$partid} = \%responseIds;
}
- $studentTable.='
';
+ $studentTable.= &displaySubByDates($symbx,\%record,$parts,\%responseType,$checkIcon,$uname,$udom);
+
}
- } elsif ($ENV{'form.lastSub'} eq 'all') {
- my $last = ($ENV{'form.lastSub'} eq 'last' ? 'last' : '');
+ } elsif ($env{'form.lastSub'} eq 'all') {
+ my $last = ($env{'form.lastSub'} eq 'last' ? 'last' : '');
$studentTable.=&Apache::loncommon::get_previous_attempt($symbx,$uname,$udom,
- $ENV{'request.course.id'},
+ $env{'request.course.id'},
'','.submission');
}
@@ -2850,6 +4293,7 @@ sub displayPage {
$studentTable.=' '."\n";
$question++;
}
+ $prob++;
}
$studentTable.='';
@@ -2857,125 +4301,270 @@ sub displayPage {
$curRes = $iterator->next();
}
- $navmap->untieHashes();
-
- $studentTable.='
'."\n".
- ' '.
+ $studentTable.='
'."\n".
+ ' '.
''."\n";
- $studentTable.=&show_grading_menu_form($symb,$url);
+ $studentTable.=&show_grading_menu_form($symb);
$request->print($studentTable);
return '';
}
+sub displaySubByDates {
+ my ($symb,$record,$parts,$responseType,$checkIcon,$uname,$udom) = @_;
+ my $isCODE=0;
+ my $isTask = ($symb =~/\.task$/);
+ if (exists($record->{'resource.CODE'})) { $isCODE=1; }
+ my $studentTable=&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 ' '.&mt('Nothing submitted - no attempts').' ';
+ }
+
+ my $interaction;
+ for ($version=1;$version<=$$record{'version'};$version++) {
+ 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.=&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 @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].=''.&mt('Part:').' '.$display_part.' ';
+ $displaySub[0].='('.&mt('ID').' '.
+ $responseId.') ';
+ if ($$record{"$where.$partid.tries"} eq '') {
+ $displaySub[0].=&mt('Trial not counted');
+ } else {
+ $displaySub[0].=&mt('Trial [_1]',
+ $$record{"$where.$partid.tries"});
+ }
+ my $responseType=($isTask ? 'Task'
+ : $responseType->{$partid}->{$responseId});
+ if (!exists($orders{$partid})) { $orders{$partid}={}; }
+ if (!exists($orders{$partid}->{$responseId})) {
+ $orders{$partid}->{$responseId}=
+ &get_order($partid,$responseId,$symb,$uname,$udom);
+ }
+ $displaySub[0].=' '.
+ &cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom).' ';
+ }
+ }
+ if (exists($$record{"$where.$partid.checkedin"})) {
+ $displaySub[1].='Checked in by '.
+ $$record{"$where.$partid.checkedin"}.' into slot '.
+ $$record{"$where.$partid.checkedin.slot"}.
+ ' ';
+ }
+ if (exists $$record{"$where.$partid.award"}) {
+ $displaySub[1].='Part: '.$display_part.' '.
+ lc($$record{"$where.$partid.award"}).' '.
+ $mark{$$record{"$where.$partid.solved"}}.
+ ' ';
+ }
+ if (exists $$record{"$where.$partid.regrader"}) {
+ $displaySub[2].=$$record{"$where.$partid.regrader"}.
+ ' ('.&mt('Part').': '.$display_part.')';
+ } elsif ($$record{"$version:resource.$partid.regrader"} =~ /\S/) {
+ $displaySub[2].=
+ $$record{"$version:resource.$partid.regrader"}.
+ ' ('.&mt('Part').': '.$display_part.')';
+ }
+ }
+ # needed because old essay regrader has not parts info
+ if (exists $$record{"$version:resource.regrader"}) {
+ $displaySub[2].=$$record{"$version:resource.regrader"};
+ }
+ $studentTable.=''.$displaySub[0].' '.$displaySub[1];
+ if ($displaySub[2]) {
+ $studentTable.=&mt('Manually graded by [_1]',$displaySub[2]);
+ }
+ $studentTable.=' '.
+ &Apache::loncommon::end_data_table_row();
+ }
+ $studentTable.=&Apache::loncommon::end_data_table();
+ return $studentTable;
+}
+
sub updateGradeByPage {
my ($request) = shift;
- my $cdom = $ENV{"course.$ENV{'request.course.id'}.domain"};
- my $cnum = $ENV{"course.$ENV{'request.course.id'}.num"};
- my $getsec = $ENV{'form.section'} eq '' ? 'all' : $ENV{'form.section'};
- my $pageTitle = $ENV{'form.page'};
+ my $cdom = $env{"course.$env{'request.course.id'}.domain"};
+ my $cnum = $env{"course.$env{'request.course.id'}.num"};
+ my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'};
+ my $pageTitle = $env{'form.page'};
my ($classlist,undef,$fullname) = &getclasslist($getsec,'1');
- my ($uname,$udom) = split(/:/,$ENV{'form.student'});
- my $usec=$classlist->{$ENV{'form.student'}}[5];
+ my ($uname,$udom) = split(/:/,$env{'form.student'});
+ my $usec=$classlist->{$env{'form.student'}}[5];
if (!&canmodify($usec)) {
- $request->print('Unable to modify requested student.('.$ENV{'form.student'}.' ');
- $request->print(&show_grading_menu_form($ENV{'form.symb'},$ENV{'form.url'}));
+ $request->print('Unable to modify requested student.('.$env{'form.student'}.' ');
+ $request->print(&show_grading_menu_form($env{'form.symb'}));
return;
}
- my $result=' '.$ENV{'form.title'}.' ';
- $result.=' Student: '.$$fullname{$ENV{'form.student'}}.
- ' ('.$uname.($udom eq $cdom ? '':':'.$udom).') '."\n";
+ my $result=' '.$env{'form.title'}.' ';
+ $result.=' Student: '.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).
+ ' '."\n";
$request->print($result);
- my $navmap = Apache::lonnavmaps::navmap-> new($ENV{'request.course.fn'}.'.db',
- $ENV{'request.course.fn'}.'_parms.db',1, 1);
- my ($mapUrl, $id, $resUrl) = split(/___/, $ENV{'form.page'});
+ my $navmap = Apache::lonnavmaps::navmap->new();
+ my ($mapUrl, $id, $resUrl) = &Apache::lonnet::decode_symb( $env{'form.page'});
my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps
-
+ if (!$map) {
+ $request->print('Unable to grade requested sequence. ('.$resUrl.') ');
+ my ($symb)=&get_symb($request);
+ $request->print(&show_grading_menu_form($symb));
+ return;
+ }
my $iterator = $navmap->getIterator($map->map_start(),
$map->map_finish());
my $studentTable=''.
''.
- ' No '.
+ ' Prob. '.
' Title '.
' Previous Score '.
' New Score ';
$iterator->next(); # skip the first BEGIN_MAP
my $curRes = $iterator->next(); # for "current resource"
- my ($depth,$question,$changeflag)= (1,1,0);
+ my ($depth,$question,$prob,$changeflag)= (1,1,1,0);
while ($depth > 0) {
if($curRes == $iterator->BEGIN_MAP) { $depth++; }
if($curRes == $iterator->END_MAP) { $depth--; }
- if (ref($curRes) && $curRes->is_problem() && !$curRes->randomout) {
+ if (ref($curRes) && $curRes->is_problem()) {
my $parts = $curRes->parts();
my $title = $curRes->compTitle();
my $symbx = $curRes->symb();
- $studentTable.=''.$question.
- (scalar(@{$parts}) == 1 ? '' : ' ('.scalar(@{$parts}).' parts)').' ';
+ $studentTable.=''.$prob.
+ (scalar(@{$parts}) == 1 ? '' : ' ('.scalar(@{$parts}).' parts)').' ';
$studentTable.=' '.$title.' ';
my %newrecord=();
my @displayPts=();
+ my %aggregate = ();
+ my $aggregateflag = 0;
foreach my $partid (@{$parts}) {
- my $newpts = $ENV{'form.GD_BOX'.$question.'_'.$partid};
- my $oldpts = $ENV{'form.oldpts'.$question.'_'.$partid};
+ my $newpts = $env{'form.GD_BOX'.$question.'_'.$partid};
+ my $oldpts = $env{'form.oldpts'.$question.'_'.$partid};
- my $wgt = $ENV{'form.WGT'.$question.'_'.$partid} != 0 ?
- $ENV{'form.WGT'.$question.'_'.$partid} : 1;
+ my $wgt = $env{'form.WGT'.$question.'_'.$partid} != 0 ?
+ $env{'form.WGT'.$question.'_'.$partid} : 1;
my $partial = $newpts/$wgt;
my $score;
if ($partial > 0) {
$score = 'correct_by_override';
- } elsif ($partial == 0) {
+ } elsif ($newpts ne '') { #empty is taken as 0
$score = 'incorrect_by_override';
}
- if ($ENV{'form.GD_SEL'.$question.'_'.$partid} eq 'excused') {
+ my $dropMenu = $env{'form.GD_SEL'.$question.'_'.$partid};
+ if ($dropMenu eq 'excused') {
$partial = '';
$score = 'excused';
+ } elsif ($dropMenu eq 'reset status'
+ && $env{'form.solved'.$question.'_'.$partid} ne '') { #update only if previous record exists
+ $newrecord{'resource.'.$partid.'.tries'} = 0;
+ $newrecord{'resource.'.$partid.'.solved'} = '';
+ $newrecord{'resource.'.$partid.'.award'} = '';
+ $newrecord{'resource.'.$partid.'.awarded'} = 0;
+ $newrecord{'resource.'.$partid.'.regrader'} = "$env{'user.name'}:$env{'user.domain'}";
+ $changeflag++;
+ $newpts = '';
+
+ my $aggtries = $env{'form.aggtries'.$question.'_'.$partid};
+ my $totaltries = $env{'form.totaltries'.$question.'_'.$partid};
+ my $solvedstatus = $env{'form.solved'.$question.'_'.$partid};
+ if ($aggtries > 0) {
+ &decrement_aggs($symbx,$partid,\%aggregate,$aggtries,$totaltries,$solvedstatus);
+ $aggregateflag = 1;
+ }
}
- my $oldstatus = $ENV{'form.solved'.$question.'_'.$partid};
- $displayPts[0].=' Part '.$partid.' = '.
+ my $display_part=&get_display_part($partid,$curRes->symb());
+ my $oldstatus = $env{'form.solved'.$question.'_'.$partid};
+ $displayPts[0].=' Part: '.$display_part.' = '.
(($oldstatus eq 'excused') ? 'excused' : $oldpts).
- ' ';
- $displayPts[1].=' Part '.$partid.' = '.
- ($oldstatus eq 'correct_by_student' ? $oldpts :
- (($score eq 'excused') ? 'excused' : $newpts)).
- ' ';
-
+ ' ';
+ $displayPts[1].=' Part: '.$display_part.' = '.
+ (($score eq 'excused') ? 'excused' : $newpts).
+ ' ';
$question++;
- if (($oldstatus eq 'correct_by_student') ||
- ($newpts eq $oldpts && $score eq $oldstatus))
- {
- next;
- }
+ next if ($dropMenu eq 'reset status' || ($newpts eq $oldpts && $score ne 'excused'));
+
$newrecord{'resource.'.$partid.'.awarded'} = $partial if $partial ne '';
- $newrecord{'resource.'.$partid.'.solved'} = $score;
- $newrecord{'resource.'.$partid.'.regrader'}="$ENV{'user.name'}:$ENV{'user.domain'}";
+ $newrecord{'resource.'.$partid.'.solved'} = $score if $score ne '';
+ $newrecord{'resource.'.$partid.'.regrader'} = "$env{'user.name'}:$env{'user.domain'}"
+ if (scalar(keys(%newrecord)) > 0);
$changeflag++;
}
if (scalar(keys(%newrecord)) > 0) {
- &Apache::lonnet::cstore(\%newrecord,$symbx,$ENV{'request.course.id'},
+ my %record =
+ &Apache::lonnet::restore($symbx,$env{'request.course.id'},
+ $udom,$uname);
+
+ if (&Apache::lonnet::validCODE($env{'form.CODE'})) {
+ $newrecord{'resource.CODE'} = $env{'form.CODE'};
+ } elsif (&Apache::lonnet::validCODE($record{'resource.CODE'})) {
+ $newrecord{'resource.CODE'} = '';
+ }
+ &Apache::lonnet::cstore(\%newrecord,$symbx,$env{'request.course.id'},
$udom,$uname);
+ %record = &Apache::lonnet::restore($symbx,
+ $env{'request.course.id'},
+ $udom,$uname);
+ &check_and_remove_from_queue($parts,\%record,undef,$symbx,
+ $cdom,$cnum,$udom,$uname);
}
+
+ if ($aggregateflag) {
+ &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate,
+ $env{'course.'.$env{'request.course.id'}.'.domain'},
+ $env{'course.'.$env{'request.course.id'}.'.num'});
+ }
+
$studentTable.=''.$displayPts[0].' '.
''.$displayPts[1].' '.
' ';
+ $prob++;
}
$curRes = $iterator->next();
}
- $navmap->untieHashes();
-
$studentTable.='
';
- $studentTable.=&show_grading_menu_form($ENV{'form.symb'},$ENV{'form.url'});
+ $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.'));
@@ -2992,25 +4581,94 @@ sub updateGradeByPage {
#
#------ start of section for handling grading by page/sequence ---------
+=pod
+
+=head1 Bubble sheet grading routines
+
+ For this documentation:
+
+ 'scanline' refers to the full line of characters
+ from the file that we are parsing that represents one entire sheet
+
+ 'bubble line' refers to the data
+ representing the line of bubbles that are on the physical bubble sheet
+
+
+The overall process is that a scanned in bubble sheet data is uploaded
+into a course. When a user wants to grade, they select a
+sequence/folder of resources, a file of bubble sheet info, and pick
+one of the predefined configurations for what each scanline looks
+like.
+
+Next each scanline is checked for any errors of either 'missing
+bubbles' (it's an error because it may have been mis-scanned
+because too light bubbling), 'double bubble' (each bubble line should
+have no more that one letter picked), invalid or duplicated CODE,
+invalid student ID
+
+If the CODE option is used that determines the randomization of the
+homework problems, either way the student ID is looked up into a
+username:domain.
+
+During the validation phase the instructor can choose to skip scanlines.
+
+After the validation phase, there are now 3 bubble sheet files
+
+ scantron_original_filename (unmodified original file)
+ scantron_corrected_filename (file where the corrected information has replaced the original information)
+ scantron_skipped_filename (contains the exact text of scanlines that where skipped)
+
+Also there is a separate hash nohist_scantrondata that contains extra
+correction information that isn't representable in the bubble sheet
+file (see &scantron_getfile() for more information)
+
+After all scanlines are either valid, marked as valid or skipped, then
+foreach line foreach problem in the picked sequence, an ssi request is
+made that simulates a user submitting their selected letter(s) against
+the homework problem.
+
+=over 4
+
+
+
+=item defaultFormData
+
+ Returns html hidden inputs used to hold context/default values.
+
+ Arguments:
+ $symb - $symb of the current resource
+
+=cut
+
sub defaultFormData {
- my ($symb,$url)=@_;
- return '
- '."\n".
- ' '."\n".
- ' '."\n".
- ' '."\n";
+ my ($symb)=@_;
+ return ' '."\n".
+ ' '."\n".
+ ' '."\n";
}
+
+=pod
+
+=item getSequenceDropDown
+
+ Return html dropdown of possible sequences to grade
+
+ Arguments:
+ $symb - $symb of the current resource
+
+=cut
+
sub getSequenceDropDown {
- my ($request,$symb)=@_;
+ my ($symb)=@_;
my $result=''."\n";
- my ($titles,$symbx) = &getSymbMap($request);
- my ($curpage,$type,$mapId) = ($symb =~ /(.*?\.(page|sequence))___(\d+)___/);
+ my ($titles,$symbx) = &getSymbMap();
+ my ($curpage)=&Apache::lonnet::decode_symb($symb);
my $ctr=0;
foreach (@$titles) {
my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/);
$result.=''.$showtitle.' '."\n";
$ctr++;
}
@@ -3018,23 +4676,65 @@ sub getSequenceDropDown {
return $result;
}
+
+=pod
+
+=item scantron_filenames
+
+ Returns a list of the scantron files in the current course
+
+=cut
+
+sub scantron_filenames {
+ my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
+ my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
+ my @files=&Apache::lonnet::dirlist('userfiles',$cdom,$cname,
+ &propath($cdom,$cname));
+ my @possiblenames;
+ foreach my $filename (sort(@files)) {
+ ($filename)=split(/&/,$filename);
+ if ($filename!~/^scantron_orig_/) { next ; }
+ $filename=~s/^scantron_orig_//;
+ push(@possiblenames,$filename);
+ }
+ return @possiblenames;
+}
+
+=pod
+
+=item scantron_uploads
+
+ Returns html drop-down list of scantron files in current course.
+
+ Arguments:
+ $file2grade - filename to set as selected in the dropdown
+
+=cut
+
sub scantron_uploads {
- if (!-e $Apache::lonnet::perlvar{'lonScansDir'}) { return ''};
+ my ($file2grade) = @_;
my $result= '';
- opendir(DIR,$Apache::lonnet::perlvar{'lonScansDir'});
- my @files=sort(readdir(DIR));
- foreach my $filename (@files) {
- if ($filename eq '.' or $filename eq '..') { next; }
- $result.="$filename \n";
+ $result.=" ";
+ foreach my $filename (sort(&scantron_filenames())) {
+ $result.="$filename \n";
}
- closedir(DIR);
$result.=" ";
return $result;
}
+=pod
+
+=item scantron_scantab
+
+ Returns html drop down of the scantron formats in the scantronformat.tab
+ file.
+
+=cut
+
sub scantron_scantab {
my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
my $result=''."\n";
+ $result.=' '."\n";
foreach my $line (<$fh>) {
my ($name,$descrip)=split(/:/,$line);
if ($name =~ /^\#/) { next; }
@@ -3045,60 +4745,279 @@ sub scantron_scantab {
return $result;
}
+=pod
+
+=item scantron_CODElist
+
+ Returns html drop down of the saved CODE lists from current course,
+ generated from earlier printings.
+
+=cut
+
+sub scantron_CODElist {
+ my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+ my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+ my @names=&Apache::lonnet::getkeys('CODEs',$cdom,$cnum);
+ my $namechoice=' ';
+ foreach my $name (sort {uc($a) cmp uc($b)} @names) {
+ if ($name =~ /^error: 2 /) { next; }
+ if ($name =~ /^type\0/) { next; }
+ $namechoice.=''.$name.' ';
+ }
+ $namechoice=''.$namechoice.' ';
+ return $namechoice;
+}
+
+=pod
+
+=item scantron_CODEunique
+
+ Returns the html for "Each CODE to be used once" radio.
+
+=cut
+
+sub scantron_CODEunique {
+ my $result='
+ '.&mt('Yes').'
+
+
+ '.&mt('No').'
+ ';
+ return $result;
+}
+
+=pod
+
+=item scantron_selectphase
+
+ Generates the initial screen to start the bubble sheet process.
+ Allows for - starting a grading run.
+ - downloading existing scan data (original, corrected
+ or skipped info)
+
+ - uploading new scan data
+
+ Arguments:
+ $r - The Apache request object
+ $file2grade - name of the file that contain the scanned data to score
+
+=cut
+
sub scantron_selectphase {
- my ($r) = @_;
- my ($symb,$url)=&get_symb_and_url($r);
+ my ($r,$file2grade) = @_;
+ my ($symb)=&get_symb($r);
if (!$symb) {return '';}
- my $sequence_selector=&getSequenceDropDown($r,$symb);
- my $default_form_data=&defaultFormData($symb,$url);
- my $grading_menu_button=&show_grading_menu_form($symb,$url);
- my $file_selector=&scantron_uploads();
+ my $sequence_selector=&getSequenceDropDown($symb);
+ my $default_form_data=&defaultFormData($symb);
+ my $grading_menu_button=&show_grading_menu_form($symb);
+ my $file_selector=&scantron_uploads($file2grade);
my $format_selector=&scantron_scantab();
+ my $CODE_selector=&scantron_CODElist();
+ my $CODE_unique=&scantron_CODEunique();
my $result;
+
+ # Chunk of form to prompt for a file to grade and how:
+
$result.= <
-
- $default_form_data
- ');
+ $r->print($grading_menu_button);
+ return
+}
+
+=pod
+
+=item get_scantron_config
+
+ Parse and return the scantron configuration line selected as a
+ hash of configuration file fields.
+
+ Arguments:
+ which - the name of the configuration to parse from the file.
+
+
+ Returns:
+ If the named configuration is not in the file, an empty
+ hash is returned.
+ a hash with the fields
+ name - internal name for the this configuration setup
+ description - text to display to operator that describes this config
+ CODElocation - if 0 or the string 'none'
+ - no CODE exists for this config
+ if -1 || the string 'letter'
+ - a CODE exists for this config and is
+ a string of letters
+ Unsupported value (but planned for future support)
+ if a positive integer
+ - The CODE exists as the first n items from
+ the question section of the form
+ if the string 'number'
+ - The CODE exists for this config and is
+ a string of numbers
+ CODEstart - (only matter if a CODE exists) column in the line where
+ the CODE starts
+ CODElength - length of the CODE
+ IDstart - column where the student ID number starts
+ IDlength - length of the student ID info
+ Qstart - column where the information from the bubbled
+ 'questions' start
+ Qlength - number of columns comprising a single bubble line from
+ the sheet. (usually either 1 or 10)
+ Qon - either a single character representing the character used
+ to signal a bubble was chosen in the positional setup, or
+ the string 'letter' if the letter of the chosen bubble is
+ in the final, or 'number' if a number representing the
+ chosen bubble is in the file (1->A 0->J)
+ Qoff - the character used to represent that a bubble was
+ left blank
+ PaperID - if the scanning process generates a unique number for each
+ sheet scanned the column that this ID number starts in
+ PaperIDlength - number of columns that comprise the unique ID number
+ for the sheet of paper
+ FirstName - column that the first name starts in
+ FirstNameLength - number of columns that the first name spans
+
+ LastName - column that the last name starts in
+ LastNameLength - number of columns that the last name spans
+
+=cut
sub get_scantron_config {
my ($which) = @_;
my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
my %config;
+ #FIXME probably should move to XML it has already gotten a bit much now
foreach my $line (<$fh>) {
my ($name,$descrip)=split(/:/,$line);
if ($name ne $which ) { next; }
@@ -3115,11 +5034,36 @@ sub get_scantron_config {
$config{'Qlength'}=$config[8];
$config{'Qoff'}=$config[9];
$config{'Qon'}=$config[10];
+ $config{'PaperID'}=$config[11];
+ $config{'PaperIDlength'}=$config[12];
+ $config{'FirstName'}=$config[13];
+ $config{'FirstNamelength'}=$config[14];
+ $config{'LastName'}=$config[15];
+ $config{'LastNamelength'}=$config[16];
last;
}
return %config;
}
+=pod
+
+=item username_to_idmap
+
+ creates a hash keyed by student id with values of the corresponding
+ student username:domain.
+
+ Arguments:
+
+ $classlist - reference to the class list hash. This is a hash
+ keyed by student name:domain whose elements are references
+ to arrays containing various chunks of information
+ about the student. (See loncoursedata for more info).
+
+ Returns
+ %idmap - the constructed hash
+
+=cut
+
sub username_to_idmap {
my ($classlist)= @_;
my %idmap;
@@ -3130,81 +5074,1917 @@ sub username_to_idmap {
return %idmap;
}
+=pod
+
+=item scantron_fixup_scanline
+
+ Process a requested correction to a scanline.
+
+ Arguments:
+ $scantron_config - hash from &get_scantron_config()
+ $scan_data - hash of correction information
+ (see &scantron_getfile())
+ $line - existing scanline
+ $whichline - line number of the passed in scanline
+ $field - type of change to process
+ (either
+ 'ID' -> correct the student ID number
+ 'CODE' -> correct the CODE
+ 'answer' -> fixup the submitted answers)
+
+ $args - hash of additional info,
+ - 'ID'
+ 'newid' -> studentID to use in replacement
+ of existing one
+ - 'CODE'
+ 'CODE_ignore_dup' - set to true if duplicates
+ should be ignored.
+ 'CODE' - is new code or 'use_unfound'
+ if the existing unfound code should
+ be used as is
+ - 'answer'
+ 'response' - new answer or 'none' if blank
+ 'question' - the bubble line to change
+
+ Returns:
+ $line - the modified scanline
+
+ Side effects:
+ $scan_data - may be updated
+
+=cut
+
+
+sub scantron_fixup_scanline {
+ my ($scantron_config,$scan_data,$line,$whichline,$field,$args)=@_;
+
+
+ if ($field eq 'ID') {
+ if (length($args->{'newid'}) > $$scantron_config{'IDlength'}) {
+ return ($line,1,'New value too large');
+ }
+ if (length($args->{'newid'}) < $$scantron_config{'IDlength'}) {
+ $args->{'newid'}=sprintf('%-'.$$scantron_config{'IDlength'}.'s',
+ $args->{'newid'});
+ }
+ substr($line,$$scantron_config{'IDstart'}-1,
+ $$scantron_config{'IDlength'})=$args->{'newid'};
+ if ($args->{'newid'}=~/^\s*$/) {
+ &scan_data($scan_data,"$whichline.user",
+ $args->{'username'}.':'.$args->{'domain'});
+ }
+ } elsif ($field eq 'CODE') {
+ if ($args->{'CODE_ignore_dup'}) {
+ &scan_data($scan_data,"$whichline.CODE_ignore_dup",'1');
+ }
+ &scan_data($scan_data,"$whichline.useCODE",'1');
+ if ($args->{'CODE'} ne 'use_unfound') {
+ if (length($args->{'CODE'}) > $$scantron_config{'CODElength'}) {
+ return ($line,1,'New CODE value too large');
+ }
+ if (length($args->{'CODE'}) < $$scantron_config{'CODElength'}) {
+ $args->{'CODE'}=sprintf('%-'.$$scantron_config{'CODElength'}.'s',$args->{'CODE'});
+ }
+ substr($line,$$scantron_config{'CODEstart'}-1,
+ $$scantron_config{'CODElength'})=$args->{'CODE'};
+ }
+ } elsif ($field eq 'answer') {
+ &scantron_get_maxbubble(); # Need the bubble counter info.
+ my $length=$scantron_config->{'Qlength'};
+ my $off=$scantron_config->{'Qoff'};
+ my $on=$scantron_config->{'Qon'};
+ my $answer=${off}x$length;
+ my $question_number = $args->{'question'} -1;
+ my $first_position = $first_bubble_line{$question_number};
+ my $bubble_count = $bubble_lines_per_response{$question_number};
+ my $bubbles_per_line= $$scantron_config{'Qlength'};
+ my $final_answer;
+ if ($$scantron_config{'Qon'} eq 'letter' ||
+ $$scantron_config{'Qon'} eq 'number') {
+ $bubbles_per_line = 10;
+ }
+ if (defined $args->{'response'}) {
+
+ if ($args->{'response'} eq 'none') {
+ &scan_data($scan_data,
+ "$whichline.no_bubble.".$args->{'question'},'1');
+ } else {
+ my ($bubble_line, $bubble_number) = split(/:/,$args->{'response'});
+ if ($on eq 'letter') {
+ my @alphabet=('A'..'Z');
+ $answer=$alphabet[$bubble_number];
+ } elsif ($on eq 'number') {
+ $answer=$args->$bubble_number+1;
+ if ($answer == 10) { $answer = '0'; }
+ } else {
+ substr($answer,$args->{'response'},1)=$on;
+ }
+ my $before = Dumper($scan_data);
+ &scan_data($scan_data,
+ "$whichline.no_bubble.".$args->{'question'},undef,'1');
+ my $after = Dumper($scan_data);
+ for (my $l = 0; $l < $bubble_count; $l++) {
+ if ($l eq $bubble_line) {
+ $final_answer .= $answer;
+ } else {
+ $final_answer .= ' ';
+ }
+ }
+ }
+ # $where=$length*($args->{'question'}-1)+$scantron_config->{'Qstart'};
+ #substr($line,$where-1,$length)=$answer;
+ substr($line,
+ $scantron_config->{'Qstart'}+$first_position-1,
+ $bubbles_per_line) = $final_answer;
+ }
+ }
+ return $line;
+}
+
+=pod
+
+=item scan_data
+
+ Edit or look up an item in the scan_data hash.
+
+ Arguments:
+ $scan_data - The hash (see scantron_getfile)
+ $key - shorthand of the key to edit (actual key is
+ scantronfilename_key).
+ $data - New value of the hash entry.
+ $delete - If true, the entry is removed from the hash.
+
+ Returns:
+ The new value of the hash table field (undefined if deleted).
+
+=cut
+
+
+sub scan_data {
+ my ($scan_data,$key,$value,$delete)=@_;
+ my $filename=$env{'form.scantron_selectfile'};
+ if (defined($value)) {
+ $scan_data->{$filename.'_'.$key} = $value;
+ }
+ if ($delete) { delete($scan_data->{$filename.'_'.$key}); }
+ return $scan_data->{$filename.'_'.$key};
+}
+
+=pod
+
+=item scantron_parse_scanline
+
+ Decodes a scanline from the selected scantron file
+
+ Arguments:
+ line - The text of the scantron file line to process
+ whichline - Line number
+ scantron_config - Hash describing the format of the scantron lines.
+ scan_data - Hash of extra information about the scanline
+ (see scantron_getfile for more information)
+ just_header - True if should not process question answers but only
+ the stuff to the left of the answers.
+ Returns:
+ Hash containing the result of parsing the scanline
+
+ Keys are all proceeded by the string 'scantron.'
+
+ CODE - the CODE in use for this scanline
+ useCODE - 1 if the CODE is invalid but it usage has been forced
+ by the operator
+ CODE_ignore_dup - 1 if the CODE is a duplicated use when unique
+ CODEs were selected, but the usage has been
+ forced by the operator
+ ID - student ID
+ PaperID - if used, the ID number printed on the sheet when the
+ paper was scanned
+ FirstName - first name from the sheet
+ LastName - last name from the sheet
+
+ if just_header was not true these key may also exist
+
+ missingerror - a list of bubble ranges that are considered to be answers
+ to a single question that don't have any bubbles filled in.
+ Of the form questionnumber:firstbubblenumber:count.
+ doubleerror - a list of bubble ranges that are considered to be answers
+ to a single question that have more than one bubble filled in.
+ Of the form questionnumber::firstbubblenumber:count
+
+ In the above, count is the number of bubble responses in the
+ input line needed to represent the possible answers to the question.
+ e.g. a radioresponse with 15 choices in an answer sheet with 10 choices
+ per line would have count = 2.
+
+ maxquest - the number of the last bubble line that was parsed
+
+ ( starts at 1)
+ .answer - zero or more letters representing the selected
+ letters from the scanline for the bubble line
+ .
+ if blank there was either no bubble or there where
+ multiple bubbles, (consult the keys missingerror and
+ doubleerror if this is an error condition)
+
+=cut
+
sub scantron_parse_scanline {
- my ($line,$scantron_config)=@_;
+ my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_;
+
my %record;
- my $questions=substr($line,$$scantron_config{'Qstart'}-1);
- my $data=substr($line,0,$$scantron_config{'Qstart'}-1);
- if ($$scantron_config{'CODElocation'} ne 0) {
- if ($$scantron_config{'CODElocation'} < 0) {
- $record{'scantron.CODE'}=substr($data,$$scantron_config{'CODEstart'}-1,
+ my $questions=substr($line,$$scantron_config{'Qstart'}-1); # Answers
+ my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # earlier stuff
+ if (!($$scantron_config{'CODElocation'} eq 0 ||
+ $$scantron_config{'CODElocation'} eq 'none')) {
+ if ($$scantron_config{'CODElocation'} < 0 ||
+ $$scantron_config{'CODElocation'} eq 'letter' ||
+ $$scantron_config{'CODElocation'} eq 'number') {
+ $record{'scantron.CODE'}=substr($data,
+ $$scantron_config{'CODEstart'}-1,
$$scantron_config{'CODElength'});
+ if (&scan_data($scan_data,"$whichline.useCODE")) {
+ $record{'scantron.useCODE'}=1;
+ }
+ if (&scan_data($scan_data,"$whichline.CODE_ignore_dup")) {
+ $record{'scantron.CODE_ignore_dup'}=1;
+ }
} else {
#FIXME interpret first N questions
}
}
$record{'scantron.ID'}=substr($data,$$scantron_config{'IDstart'}-1,
$$scantron_config{'IDlength'});
+ $record{'scantron.PaperID'}=
+ substr($data,$$scantron_config{'PaperID'}-1,
+ $$scantron_config{'PaperIDlength'});
+ $record{'scantron.FirstName'}=
+ substr($data,$$scantron_config{'FirstName'}-1,
+ $$scantron_config{'FirstNamelength'});
+ $record{'scantron.LastName'}=
+ substr($data,$$scantron_config{'LastName'}-1,
+ $$scantron_config{'LastNamelength'});
+ if ($just_header) { return \%record; }
+
my @alphabet=('A'..'Z');
my $questnum=0;
- while ($questions) {
+ my $ansnum =1; # Multiple 'answer lines'/question.
+
+ 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,$$scantron_config{'Qlength'});
- substr($questions,0,$$scantron_config{'Qlength'})='';
- if (length($currentquest) < $$scantron_config{'Qlength'}) { next; }
- my (@array)=split(/$$scantron_config{'Qon'}/,$currentquest);
- if (scalar(@array) gt 2) {
- #FIXME do something intelligent with double bubbles
- Apache->request->print("Wha!!! ".scalar(@array).
- '-'.$currentquest.'-'.$questnum.' ');
- }
- if (length($array[0]) eq $$scantron_config{'Qlength'}) {
- $record{"scantron.$questnum.answer"}='';
+ 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++;
+
+ }
+ if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) {
+ push(@{$record{"scantron.missingerror"}},$questnum);
+ $ansnum += $answers_needed;
+ }
+
+ } else {
+ $currentquest = &digits_to_letters($currentquest);
+ for (my $ans =0; $ans < $answers_needed; $ans++) {
+ $record{"scantron.$ansnum.answer"} = substr($currentquest, $ans, 1);
+ $ansnum++;
+ }
+ }
} else {
- $record{"scantron.$questnum.answer"}=$alphabet[length($array[0])];
+
+ # 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.
+ #
+
+ my @array=split($$scantron_config{'Qon'},$currentquest,-1);
+
+ # If the split only giveas us one element.. the full length of the
+ # answser string, no bubbles are filled in:
+
+ 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.$questnum")) {
+ push(@{$record{"scantron.missingerror"}},$questnum);
+ }
+ } elsif (scalar(@array) lt 2) {
+
+ my $location = length($array[0]);
+ my $line_num = $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++;
+ }
+ }
+ # 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;
+ }
}
}
$record{'scantron.maxquest'}=$questnum;
return \%record;
}
+=pod
+
+=item scantron_add_delay
+
+ Adds an error message that occurred during the grading phase to a
+ queue of messages to be shown after grading pass is complete
+
+ Arguments:
+ $delayqueue - arrary ref of hash ref of error messages
+ $scanline - the scanline that caused the error
+ $errormesage - the error message
+ $errorcode - a numeric code for the error
+
+ Side Effects:
+ updates the $delayqueue to have a new hash ref of the error
+
+=cut
+
sub scantron_add_delay {
+ my ($delayqueue,$scanline,$errormessage,$errorcode)=@_;
+ push(@$delayqueue,
+ {'line' => $scanline, 'emsg' => $errormessage,
+ 'ecode' => $errorcode }
+ );
}
+=pod
+
+=item scantron_find_student
+
+ Finds the username for the current scanline
+
+ Arguments:
+ $scantron_record - hash result from scantron_parse_scanline
+ $scan_data - hash of correction information
+ (see &scantron_getfile() form more information)
+ $idmap - hash from &username_to_idmap()
+ $line - number of current scanline
+
+ Returns:
+ Either 'username:domain' or undef if unknown
+
+=cut
+
sub scantron_find_student {
- my ($scantron_record,$idmap)=@_;
+ my ($scantron_record,$scan_data,$idmap,$line)=@_;
my $scanID=$$scantron_record{'scantron.ID'};
+ if ($scanID =~ /^\s*$/) {
+ return &scan_data($scan_data,"$line.user");
+ }
foreach my $id (keys(%$idmap)) {
- Apache->request->print('checking studnet -'.$id.'- againt -'.$scanID.'- ');
- if (lc($id) eq lc($scanID)) { Apache->request->print('success');return $$idmap{$id}; }
+ if (lc($id) eq lc($scanID)) {
+ return $$idmap{$id};
+ }
}
return undef;
}
+=pod
+
+=item scantron_filter
+
+ Filter sub for lonnavmaps, filters out hidden resources if ignore
+ hidden resources was selected
+
+=cut
+
sub scantron_filter {
my ($curres)=@_;
- if (ref($curres) && $curres->is_problem() && !$curres->randomout) {
+
+ if (ref($curres) && $curres->is_problem()) {
+ # if the user has asked to not have either hidden
+ # or 'randomout' controlled resources to be graded
+ # don't include them
+ if ($env{'form.scantron_options_hidden'} eq 'ignore_hidden'
+ && $curres->randomout) {
+ return 0;
+ }
+ return 1;
+ }
+ return 0;
+}
+
+=pod
+
+=item scantron_process_corrections
+
+ Gets correction information out of submitted form data and corrects
+ the scanline
+
+=cut
+
+sub scantron_process_corrections {
+ my ($r) = @_;
+ my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
+ my ($scanlines,$scan_data)=&scantron_getfile();
+ my $classlist=&Apache::loncoursedata::get_classlist();
+ my $which=$env{'form.scantron_line'};
+ my $line=&scantron_get_line($scanlines,$scan_data,$which);
+ my ($skip,$err,$errmsg);
+ if ($env{'form.scantron_skip_record'}) {
+ $skip=1;
+ } elsif ($env{'form.scantron_corrections'} =~ /^(duplicate|incorrect)ID$/) {
+ my $newstudent=$env{'form.scantron_username'}.':'.
+ $env{'form.scantron_domain'};
+ my $newid=$classlist->{$newstudent}->[&Apache::loncoursedata::CL_ID];
+ ($line,$err,$errmsg)=
+ &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,$which,
+ 'ID',{'newid'=>$newid,
+ 'username'=>$env{'form.scantron_username'},
+ 'domain'=>$env{'form.scantron_domain'}});
+ } elsif ($env{'form.scantron_corrections'} =~ /^(duplicate|incorrect)CODE$/) {
+ my $resolution=$env{'form.scantron_CODE_resolution'};
+ my $newCODE;
+ my %args;
+ if ($resolution eq 'use_unfound') {
+ $newCODE='use_unfound';
+ } elsif ($resolution eq 'use_found') {
+ $newCODE=$env{'form.scantron_CODE_selectedvalue'};
+ } elsif ($resolution eq 'use_typed') {
+ $newCODE=$env{'form.scantron_CODE_newvalue'};
+ } elsif ($resolution =~ /^use_closest_(\d+)/) {
+ $newCODE=$env{"form.scantron_CODE_closest_$1"};
+ }
+ if ($env{'form.scantron_corrections'} eq 'duplicateCODE') {
+ $args{'CODE_ignore_dup'}=1;
+ }
+ $args{'CODE'}=$newCODE;
+ ($line,$err,$errmsg)=
+ &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,$which,
+ 'CODE',\%args);
+ } elsif ($env{'form.scantron_corrections'} =~ /^(missing|double)bubble$/) {
+ foreach my $question (split(',',$env{'form.scantron_questions'})) {
+ ($line,$err,$errmsg)=
+ &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,
+ $which,'answer',
+ { 'question'=>$question,
+ 'response'=>$env{"form.scantron_correct_Q_$question"}});
+ if ($err) { last; }
+ }
+ }
+ if ($err) {
+ $r->print("Unable to accept last correction, an error occurred :$errmsg: ");
+ } else {
+ &scantron_put_line($scanlines,$scan_data,$which,$line,$skip);
+ &scantron_putfile($scanlines,$scan_data);
+ }
+}
+
+=pod
+
+=item reset_skipping_status
+
+ Forgets the current set of remember skipped scanlines (and thus
+ reverts back to considering all lines in the
+ scantron_skipped_ file)
+
+=cut
+
+sub reset_skipping_status {
+ my ($scanlines,$scan_data)=&scantron_getfile();
+ &scan_data($scan_data,'remember_skipping',undef,1);
+ &scantron_putfile(undef,$scan_data);
+}
+
+=pod
+
+=item start_skipping
+
+ Marks a scanline to be skipped.
+
+=cut
+
+sub start_skipping {
+ my ($scan_data,$i)=@_;
+ my %remembered=split(':',&scan_data($scan_data,'remember_skipping'));
+ if ($env{'form.scantron_options_redo'} =~ /^redo_/) {
+ $remembered{$i}=2;
+ } else {
+ $remembered{$i}=1;
+ }
+ &scan_data($scan_data,'remember_skipping',join(':',%remembered));
+}
+
+=pod
+
+=item should_be_skipped
+
+ Checks whether a scanline should be skipped.
+
+=cut
+
+sub should_be_skipped {
+ my ($scanlines,$scan_data,$i)=@_;
+ if ($env{'form.scantron_options_redo'} !~ /^redo_/) {
+ # not redoing old skips
+ if ($scanlines->{'skipped'}[$i]) { return 1; }
+ return 0;
+ }
+ my %remembered=split(':',&scan_data($scan_data,'remember_skipping'));
+
+ if (exists($remembered{$i}) && $remembered{$i} != 2 ) {
+ return 0;
+ }
+ return 1;
+}
+
+=pod
+
+=item remember_current_skipped
+
+ Discovers what scanlines are in the scantron_skipped_
+ file and remembers them into scan_data for later use.
+
+=cut
+
+sub remember_current_skipped {
+ my ($scanlines,$scan_data)=&scantron_getfile();
+ my %to_remember;
+ for (my $i=0;$i<=$scanlines->{'count'};$i++) {
+ if ($scanlines->{'skipped'}[$i]) {
+ $to_remember{$i}=1;
+ }
+ }
+
+ &scan_data($scan_data,'remember_skipping',join(':',%to_remember));
+ &scantron_putfile(undef,$scan_data);
+}
+
+=pod
+
+=item check_for_error
+
+ Checks if there was an error when attempting to remove a specific
+ scantron_.. bubble sheet data file. Prints out an error if
+ something went wrong.
+
+=cut
+
+sub check_for_error {
+ my ($r,$result)=@_;
+ if ($result ne 'ok' && $result ne 'not_found' ) {
+ $r->print("An error occurred ($result) when trying to Remove the existing corrections.");
+ }
+}
+
+=pod
+
+=item scantron_warning_screen
+
+ Interstitial screen to make sure the operator has selected the
+ correct options before we start the validation phase.
+
+=cut
+
+sub scantron_warning_screen {
+ my ($button_text)=@_;
+ my $title=&Apache::lonnet::gettitle($env{'form.selectpage'});
+ my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
+ my $CODElist;
+ if ($scantron_config{'CODElocation'} &&
+ $scantron_config{'CODEstart'} &&
+ $scantron_config{'CODElength'}) {
+ $CODElist=$env{'form.scantron_CODElist'};
+ if ($env{'form.scantron_CODElist'} eq '') { $CODElist='None '; }
+ $CODElist=
+ 'List of CODES to validate against: '.
+ $env{'form.scantron_CODElist'}.' ';
+ }
+ return (<
+Please double check the information
+ below before clicking on '$button_text'
+
+
+Sequence to be Graded: $title
+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.
+
+
+STUFF
+}
+
+=pod
+
+=item scantron_do_warning
+
+ Check if the operator has picked something for all required
+ fields. Error out if something is missing.
+
+=cut
+
+sub scantron_do_warning {
+ my ($r)=@_;
+ my ($symb)=&get_symb($r);
+ if (!$symb) {return '';}
+ my $default_form_data=&defaultFormData($symb);
+ $r->print(&scantron_form_start().$default_form_data);
+ if ( $env{'form.selectpage'} eq '' ||
+ $env{'form.scantron_selectfile'} eq '' ||
+ $env{'form.scantron_format'} eq '' ) {
+ $r->print("You have forgetten to specify some information. Please go Back and try again.
");
+ if ( $env{'form.selectpage'} eq '') {
+ $r->print('You have not selected a Sequence to grade
');
+ }
+ if ( $env{'form.scantron_selectfile'} eq '') {
+ $r->print('You have not selected a file that contains the student\'s response data.
');
+ }
+ if ( $env{'form.scantron_format'} eq '') {
+ $r->print('You have not selected a the format of the student\'s response data.
');
+ }
+ } else {
+ my $warning=&scantron_warning_screen('Grading: Validate Records');
+ $r->print(<
+
+STUFF
+ }
+ $r->print(" ".&show_grading_menu_form($symb));
+ return '';
+}
+
+=pod
+
+=item scantron_form_start
+
+ html hidden input for remembering all selected grading options
+
+=cut
+
+sub scantron_form_start {
+ my ($max_bubble)=@_;
+ my $result= <
+
+
+
+
+
+
+
+
+
+SCANTRONFORM
+
+ my $line = 0;
+ while (defined($env{"form.scantron.bubblelines.$line"})) {
+ my $chunk =
+ ' '."\n";
+ $chunk .=
+ ' '."\n";
+ $result .= $chunk;
+ $line++;
+ }
+ return $result;
+}
+
+=pod
+
+=item scantron_validate_file
+
+ Dispatch routine for doing validation of a bubble sheet data file.
+
+ Also processes any necessary information resets that need to
+ occur before validation begins (ignore previous corrections,
+ restarting the skipped records processing)
+
+=cut
+
+sub scantron_validate_file {
+ my ($r) = @_;
+ my ($symb)=&get_symb($r);
+ if (!$symb) {return '';}
+ my $default_form_data=&defaultFormData($symb);
+
+ # do the detection of only doing skipped records first befroe we delete
+ # them when doing the corrections reset
+ if ($env{'form.scantron_options_redo'} ne 'redo_skipped_ready') {
+ &reset_skipping_status();
+ }
+ if ($env{'form.scantron_options_redo'} eq 'redo_skipped') {
+ &remember_current_skipped();
+ $env{'form.scantron_options_redo'}='redo_skipped_ready';
+ }
+
+ if ($env{'form.scantron_options_ignore'} eq 'ignore_corrections') {
+ &check_for_error($r,&scantron_remove_file('corrected'));
+ &check_for_error($r,&scantron_remove_file('skipped'));
+ &check_for_error($r,&scantron_remove_scan_data());
+ $env{'form.scantron_options_ignore'}='done';
+ }
+
+ if ($env{'form.scantron_corrections'}) {
+ &scantron_process_corrections($r);
+ }
+ $r->print("Gathering necessary info.
");$r->rflush();
+ #get the student pick code ready
+ $r->print(&Apache::loncommon::studentbrowser_javascript());
+ my $max_bubble=&scantron_get_maxbubble();
+ my $result=&scantron_form_start($max_bubble).$default_form_data;
+ $r->print($result);
+
+ my @validate_phases=( 'sequence',
+ 'ID',
+ 'CODE',
+ 'doublebubble',
+ 'missingbubbles');
+ if (!$env{'form.validatepass'}) {
+ $env{'form.validatepass'} = 0;
+ }
+ my $currentphase=$env{'form.validatepass'};
+
+
+ my $stop=0;
+ while (!$stop && $currentphase < scalar(@validate_phases)) {
+ $r->print(" Validating ".$validate_phases[$currentphase]."
");
+ $r->rflush();
+ my $which="scantron_validate_".$validate_phases[$currentphase];
+ {
+ no strict 'refs';
+ ($stop,$currentphase)=&$which($r,$currentphase);
+ }
+ }
+ if (!$stop) {
+ my $warning=&scantron_warning_screen('Start Grading');
+ $r->print(<
+$warning
+
+
+STUFF
+
+ } else {
+ $r->print(' ');
+ $r->print(" ");
+ }
+ if ($stop) {
+ if ($validate_phases[$currentphase] eq 'sequence') {
+ $r->print(' ');
+ $r->print(' this error ');
+
+ $r->print(" Or click the 'Grading Menu' button to start over.
");
+ } else {
+ $r->print(' ');
+ $r->print(' using corrected info ');
+ $r->print(" ");
+ $r->print(" this scanline saving it for later.");
+ }
+ }
+ $r->print(" ".&show_grading_menu_form($symb));
+ return '';
+}
+
+
+=pod
+
+=item scantron_remove_file
+
+ Removes the requested bubble sheet data file, makes sure that
+ scantron_original_ is never removed
+
+
+=cut
+
+sub scantron_remove_file {
+ my ($which)=@_;
+ my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
+ my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
+ my $file='scantron_';
+ if ($which eq 'corrected' || $which eq 'skipped') {
+ $file.=$which.'_';
+ } else {
+ return 'refused';
+ }
+ $file.=$env{'form.scantron_selectfile'};
+ return &Apache::lonnet::removeuserfile($cname,$cdom,$file);
+}
+
+
+=pod
+
+=item scantron_remove_scan_data
+
+ Removes all scan_data correction for the requested bubble sheet
+ data file. (In the case that both the are doing skipped records we need
+ to remember the old skipped lines for the time being so that element
+ persists for a while.)
+
+=cut
+
+sub scantron_remove_scan_data {
+ my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
+ my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
+ my @keys=&Apache::lonnet::getkeys('nohist_scantrondata',$cdom,$cname);
+ my @todelete;
+ my $filename=$env{'form.scantron_selectfile'};
+ foreach my $key (@keys) {
+ if ($key=~/^\Q$filename\E_/) {
+ if ($env{'form.scantron_options_redo'} eq 'redo_skipped_ready' &&
+ $key=~/remember_skipping/) {
+ next;
+ }
+ push(@todelete,$key);
+ }
+ }
+ my $result;
+ if (@todelete) {
+ $result=&Apache::lonnet::del('nohist_scantrondata',\@todelete,$cdom,$cname);
+ }
+ return $result;
+}
+
+
+=pod
+
+=item scantron_getfile
+
+ Fetches the requested bubble sheet data file (all 3 versions), and
+ the scan_data hash
+
+ Arguments:
+ None
+
+ Returns:
+ 2 hash references
+
+ - first one has
+ orig -
+ corrected -
+ skipped - each of which points to an array ref of the specified
+ file broken up into individual lines
+ count - number of scanlines
+
+ - second is the scan_data hash possible keys are
+ ($number refers to scanline numbered $number and thus the key affects
+ only that scanline
+ $bubline refers to the specific bubble line element and the aspects
+ refers to that specific bubble line element)
+
+ $number.user - username:domain to use
+ $number.CODE_ignore_dup
+ - ignore the duplicate CODE error
+ $number.useCODE
+ - use the CODE in the scanline as is
+ $number.no_bubble.$bubline
+ - it is valid that there is no bubbled in bubble
+ at $number $bubline
+ remember_skipping
+ - a frozen hash containing keys of $number and values
+ of either
+ 1 - we are on a 'do skipped records pass' and plan
+ on processing this line
+ 2 - we are on a 'do skipped records pass' and this
+ scanline has been marked to skip yet again
+
+=cut
+
+sub scantron_getfile {
+ #FIXME really would prefer a scantron directory
+ my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
+ my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
+ my $lines;
+ $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.
+ 'scantron_orig_'.$env{'form.scantron_selectfile'});
+ my %scanlines;
+ $scanlines{'orig'}=[(split("\n",$lines,-1))];
+ my $temp=$scanlines{'orig'};
+ $scanlines{'count'}=$#$temp;
+
+ $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.
+ 'scantron_corrected_'.$env{'form.scantron_selectfile'});
+ if ($lines eq '-1') {
+ $scanlines{'corrected'}=[];
+ } else {
+ $scanlines{'corrected'}=[(split("\n",$lines,-1))];
+ }
+ $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.
+ 'scantron_skipped_'.$env{'form.scantron_selectfile'});
+ if ($lines eq '-1') {
+ $scanlines{'skipped'}=[];
+ } else {
+ $scanlines{'skipped'}=[(split("\n",$lines,-1))];
+ }
+ my @tmp=&Apache::lonnet::dump('nohist_scantrondata',$cdom,$cname);
+ if ($tmp[0] =~ /^(error:|no_such_host)/) { @tmp=(); }
+ my %scan_data = @tmp;
+ return (\%scanlines,\%scan_data);
+}
+
+=pod
+
+=item lonnet_putfile
+
+ Wrapper routine to call &Apache::lonnet::finishuserfileupload
+
+ Arguments:
+ $contents - data to store
+ $filename - filename to store $contents into
+
+ Returns:
+ result value from &Apache::lonnet::finishuserfileupload
+
+=cut
+
+sub lonnet_putfile {
+ my ($contents,$filename)=@_;
+ my $docuname=$env{'course.'.$env{'request.course.id'}.'.num'};
+ my $docudom=$env{'course.'.$env{'request.course.id'}.'.domain'};
+ $env{'form.sillywaytopassafilearound'}=$contents;
+ &Apache::lonnet::finishuserfileupload($docuname,$docudom,'sillywaytopassafilearound',$filename);
+
+}
+
+=pod
+
+=item scantron_putfile
+
+ Stores the current version of the bubble sheet data files, and the
+ scan_data hash. (Does not modify the original version only the
+ corrected and skipped versions.
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+
+=cut
+
+sub scantron_putfile {
+ my ($scanlines,$scan_data) = @_;
+ #FIXME really would prefer a scantron directory
+ my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
+ my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
+ if ($scanlines) {
+ my $prefix='scantron_';
+# no need to update orig, shouldn't change
+# &lonnet_putfile(join("\n",@{$scanlines->{'orig'}}),$prefix.'orig_'.
+# $env{'form.scantron_selectfile'});
+ &lonnet_putfile(join("\n",@{$scanlines->{'corrected'}}),
+ $prefix.'corrected_'.
+ $env{'form.scantron_selectfile'});
+ &lonnet_putfile(join("\n",@{$scanlines->{'skipped'}}),
+ $prefix.'skipped_'.
+ $env{'form.scantron_selectfile'});
+ }
+ &Apache::lonnet::put('nohist_scantrondata',$scan_data,$cdom,$cname);
+}
+
+=pod
+
+=item scantron_get_line
+
+ Returns the correct version of the scanline
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+ $i - number of the requested line (starts at 0)
+
+ Returns:
+ A scanline, (either the original or the corrected one if it
+ exists), or undef if the requested scanline should be
+ skipped. (Either because it's an skipped scanline, or it's an
+ unskipped scanline and we are not doing a 'do skipped scanlines'
+ pass.
+
+=cut
+
+sub scantron_get_line {
+ my ($scanlines,$scan_data,$i)=@_;
+ if (&should_be_skipped($scanlines,$scan_data,$i)) { return undef; }
+ #if ($scanlines->{'skipped'}[$i]) { return undef; }
+ if ($scanlines->{'corrected'}[$i]) {return $scanlines->{'corrected'}[$i];}
+ return $scanlines->{'orig'}[$i];
+}
+
+=pod
+
+=item scantron_todo_count
+
+ Counts the number of scanlines that need processing.
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+
+ Returns:
+ $count - number of scanlines to process
+
+=cut
+
+sub get_todo_count {
+ my ($scanlines,$scan_data)=@_;
+ my $count=0;
+ for (my $i=0;$i<=$scanlines->{'count'};$i++) {
+ my $line=&scantron_get_line($scanlines,$scan_data,$i);
+ if ($line=~/^[\s\cz]*$/) { next; }
+ $count++;
+ }
+ return $count;
+}
+
+=pod
+
+=item scantron_put_line
+
+ Updates the 'corrected' or 'skipped' versions of the bubble sheet
+ data file.
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+ $i - line number to update
+ $newline - contents of the updated scanline
+ $skip - if true make the line for skipping and update the
+ 'skipped' file
+
+=cut
+
+sub scantron_put_line {
+ my ($scanlines,$scan_data,$i,$newline,$skip)=@_;
+ if ($skip) {
+ $scanlines->{'skipped'}[$i]=$newline;
+ &start_skipping($scan_data,$i);
+ return;
+ }
+ $scanlines->{'corrected'}[$i]=$newline;
+}
+
+=pod
+
+=item scantron_clear_skip
+
+ Remove a line from the 'skipped' file
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+ $i - line number to update
+
+=cut
+
+sub scantron_clear_skip {
+ my ($scanlines,$scan_data,$i)=@_;
+ if (exists($scanlines->{'skipped'}[$i])) {
+ undef($scanlines->{'skipped'}[$i]);
+ return 1;
+ }
+ return 0;
+}
+
+=pod
+
+=item scantron_filter_not_exam
+
+ Filter routine used by &Apache::lonnavmaps::retrieveResources(), to
+ filter out resources that are not marked as 'exam' mode
+
+=cut
+
+sub scantron_filter_not_exam {
+ my ($curres)=@_;
+
+ if (ref($curres) && $curres->is_problem() && !$curres->is_exam()) {
+ # if the user has asked to not have either hidden
+ # or 'randomout' controlled resources to be graded
+ # don't include them
+ if ($env{'form.scantron_options_hidden'} eq 'ignore_hidden'
+ && $curres->randomout) {
+ return 0;
+ }
return 1;
}
return 0;
}
+=pod
+
+=item scantron_validate_sequence
+
+ Validates the selected sequence, checking for resource that are
+ not set to exam mode.
+
+=cut
+
+sub scantron_validate_sequence {
+ my ($r,$currentphase) = @_;
+
+ my $navmap=Apache::lonnavmaps::navmap->new();
+ my (undef,undef,$sequence)=
+ &Apache::lonnet::decode_symb($env{'form.selectpage'});
+
+ my $map=$navmap->getResourceByUrl($sequence);
+
+ $r->print(' ');
+ if ($env{'form.validate_sequence_exam'} ne 'ignore') {
+ my @resources=
+ $navmap->retrieveResources($map,\&scantron_filter_not_exam,1,0);
+ if (@resources) {
+ $r->print("".&mt('Some resources in the sequence currently are not set to exam mode. Grading these resources currently may not work correctly.')."
");
+ return (1,$currentphase);
+ }
+ }
+
+ return (0,$currentphase+1);
+}
+
+=pod
+
+=item scantron_validate_ID
+
+ Validates all scanlines in the selected file to not have any
+ invalid or underspecified student IDs
+
+=cut
+
+sub scantron_validate_ID {
+ my ($r,$currentphase) = @_;
+
+ #get student info
+ my $classlist=&Apache::loncoursedata::get_classlist();
+ my %idmap=&username_to_idmap($classlist);
+
+ #get scantron line setup
+ my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
+ my ($scanlines,$scan_data)=&scantron_getfile();
+
+ &scantron_get_maxbubble(); # parse needs the bubble_lines.. array.
+
+ my %found=('ids'=>{},'usernames'=>{});
+ for (my $i=0;$i<=$scanlines->{'count'};$i++) {
+ my $line=&scantron_get_line($scanlines,$scan_data,$i);
+ if ($line=~/^[\s\cz]*$/) { next; }
+ my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config,
+ $scan_data);
+ my $id=$$scan_record{'scantron.ID'};
+ my $found;
+ foreach my $checkid (keys(%idmap)) {
+ if (lc($checkid) eq lc($id)) { $found=$checkid;last; }
+ }
+ if ($found) {
+ my $username=$idmap{$found};
+ if ($found{'ids'}{$found}) {
+ &scantron_get_correction($r,$i,$scan_record,\%scantron_config,
+ $line,'duplicateID',$found);
+ return(1,$currentphase);
+ } elsif ($found{'usernames'}{$username}) {
+ &scantron_get_correction($r,$i,$scan_record,\%scantron_config,
+ $line,'duplicateID',$username);
+ return(1,$currentphase);
+ }
+ #FIXME store away line we previously saw the ID on to use above
+ $found{'ids'}{$found}++;
+ $found{'usernames'}{$username}++;
+ } else {
+ if ($id =~ /^\s*$/) {
+ my $username=&scan_data($scan_data,"$i.user");
+ if (defined($username) && $found{'usernames'}{$username}) {
+ &scantron_get_correction($r,$i,$scan_record,
+ \%scantron_config,
+ $line,'duplicateID',$username);
+ return(1,$currentphase);
+ } elsif (!defined($username)) {
+ &scantron_get_correction($r,$i,$scan_record,
+ \%scantron_config,
+ $line,'incorrectID');
+ return(1,$currentphase);
+ }
+ $found{'usernames'}{$username}++;
+ } else {
+ &scantron_get_correction($r,$i,$scan_record,\%scantron_config,
+ $line,'incorrectID');
+ return(1,$currentphase);
+ }
+ }
+ }
+
+ return (0,$currentphase+1);
+}
+
+=pod
+
+=item scantron_get_correction
+
+ Builds the interface screen to interact with the operator to fix a
+ specific error condition in a specific scanline
+
+ Arguments:
+ $r - Apache request object
+ $i - number of the current scanline
+ $scan_record - hash ref as returned from &scantron_parse_scanline()
+ $scan_config - hash ref as returned from &get_scantron_config()
+ $line - full contents of the current scanline
+ $error - error condition, valid values are
+ 'incorrectCODE', 'duplicateCODE',
+ 'doublebubble', 'missingbubble',
+ 'duplicateID', 'incorrectID'
+ $arg - extra information needed
+ For errors:
+ - duplicateID - paper number that this studentID was seen before on
+ - duplicateCODE - array ref of the paper numbers this CODE was
+ seen on before
+ - incorrectCODE - current incorrect CODE
+ - doublebubble - array ref of the bubble lines that have double
+ bubble errors
+ - missingbubble - array ref of the bubble lines that have missing
+ bubble errors
+
+=cut
+
+sub scantron_get_correction {
+ my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_;
+
+#FIXME in the case of a duplicated ID the previous line, 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");
+ } else {
+ $r->print(" in scanline $i
".
+ $line." \n");
+ }
+ my $message="The ID on the form is ".
+ $$scan_record{'scantron.ID'}." \n".
+ "The name on the paper is ".
+ $$scan_record{'scantron.LastName'}.",".
+ $$scan_record{'scantron.FirstName'}."
";
+
+ $r->print(' '."\n");
+ $r->print(' '."\n");
+ if ($error =~ /ID$/) {
+ if ($error eq 'incorrectID') {
+ $r->print("The encoded ID is not in the classlist\n");
+ } elsif ($error eq 'duplicateID') {
+ $r->print("The encoded ID has also been used by a previous paper $arg\n");
+ }
+ $r->print($message);
+ $r->print("How should I handle this? \n");
+ $r->print("\n
");
+ #FIXME it would be nice if this sent back the user ID and
+ #could do partial userID matches
+ $r->print(&Apache::loncommon::selectstudent_link('scantronupload',
+ 'scantron_username','scantron_domain'));
+ $r->print(": ");
+ $r->print("\n@".
+ &Apache::loncommon::select_dom_form($env{'request.role.domain'},'scantron_domain'));
+
+ $r->print(' ');
+ } elsif ($error =~ /CODE$/) {
+ if ($error eq 'incorrectCODE') {
+ $r->print("The encoded CODE is not in the list of possible CODEs
\n");
+ } elsif ($error eq 'duplicateCODE') {
+ $r->print("The encoded CODE has also been used by a previous paper ".join(', ',@{$arg}).", and CODEs are supposed to be unique
\n");
+ }
+ $r->print("The CODE on the form is '".
+ $$scan_record{'scantron.CODE'}."' \n");
+ $r->print($message);
+ $r->print("
How should I handle this? \n");
+ $r->print("\n ");
+ my $i=0;
+ if ($error eq 'incorrectCODE'
+ && $$scan_record{'scantron.CODE'}=~/\S/ ) {
+ my ($max,$closest)=&scantron_get_closely_matching_CODEs($arg,$$scan_record{'scantron.CODE'});
+ if ($closest > 0) {
+ foreach my $testcode (@{$closest}) {
+ my $checked='';
+ if (!$i) { $checked=' checked="checked" '; }
+ $r->print(" Use the similar CODE ".$testcode." instead. ");
+ $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. ");
+ $r->print("\n ");
+ }
+
+ $r->print(<
+function change_radio(field) {
+ var slct=document.scantronupload.scantron_CODE_resolution;
+ var i;
+ for (i=0;i
+ENDSCRIPT
+ my $href="/adm/pickcode?".
+ "form=".&escape("scantronupload").
+ "&scantron_format=".&escape($env{'form.scantron_format'}).
+ "&scantron_CODElist=".&escape($env{'form.scantron_CODElist'}).
+ "&curCODE=".&escape($$scan_record{'scantron.CODE'}).
+ "&scantron_selectfile=".&escape($env{'form.scantron_selectfile'});
+ if ($env{'form.scantron_CODElist'} =~ /\S/) {
+ $r->print(" Select a CODE from the list of all CODEs and use it. Selected CODE is ");
+ $r->print("\n ");
+ }
+ $r->print(" Use as the CODE.");
+ $r->print("\n ");
+ } elsif ($error eq 'doublebubble') {
+ $r->print("There have been multiple bubbles scanned for a some question(s)
\n");
+ $r->print(' ');
+ $r->print($message);
+ $r->print("Please indicate which bubble should be used for grading
");
+ foreach my $question (@{$arg}) {
+ my $selected = &get_response_bubbles($scan_record, $question);
+ my @select_array = split(/:/,$selected);
+ &Apache::lonnet::logthis("Questnum: $question, bubbles: $selected scanline $i");
+ &scantron_bubble_selector($r,$scan_config,$question,
+ @select_array);
+ }
+ } elsif ($error eq 'missingbubble') {
+ $r->print("There have been no bubbles scanned for some question(s)
\n");
+ $r->print($message);
+ $r->print("Please indicate which bubble should be used for grading
");
+ $r->print("Some questions have no scanned bubbles\n");
+ $r->print(' ');
+ foreach my $question (@{$arg}) {
+ my $selected = &get_response_bubbles($scan_record, $question);
+ my @select_array = split(/:/,$selected); # ought to be an array of empties.
+ &scantron_bubble_selector($r,$scan_config,$question, @select_array);
+ }
+ } else {
+ $r->print("\n");
+
+}
+
+=pod
+
+=item scantron_bubble_selector
+
+ Generates the html radiobuttons to correct a single bubble line
+ possibly showing the existing the selected bubbles if known
+
+ Arguments:
+ $r - Apache request object
+ $scan_config - hash from &get_scantron_config()
+ $quest - number of the bubble line to make a corrector for
+ @lines - array of answer lines.
+
+=cut
+
+sub scantron_bubble_selector {
+ my ($r,$scan_config,$quest,@lines)=@_;
+ my $max=$$scan_config{'Qlength'};
+
+
+ my $scmode=$$scan_config{'Qon'};
+
+ my $bubble_length = scalar(@lines);
+
+
+ if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; }
+
+ my $response = $quest-1;
+ my $lines = $bubble_lines_per_response{$response};
+
+ my $total_lines = $lines*2;
+ my @alphabet=('A'..'Z');
+
+ $r->print("');
+}
+
+=pod
+
+=item num_matches
+
+ Counts the number of characters that are the same between the two arguments.
+
+ Arguments:
+ $orig - CODE from the scanline
+ $code - CODE to match against
+
+ Returns:
+ $count - integer count of the number of same characters between the
+ two arguments
+
+=cut
+
+sub num_matches {
+ my ($orig,$code) = @_;
+ my @code=split(//,$code);
+ my @orig=split(//,$orig);
+ my $same=0;
+ for (my $i=0;$i{'count'};$i++) {
+ my $line=&scantron_get_line($scanlines,$scan_data,$i);
+ if ($line=~/^[\s\cz]*$/) { next; }
+ my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config,
+ $scan_data);
+ my $CODE=$$scan_record{'scantron.CODE'};
+ my $error=0;
+ if (!&Apache::lonnet::validCODE($CODE)) {
+ &scantron_get_correction($r,$i,$scan_record,
+ \%scantron_config,
+ $line,'incorrectCODE',\%allcodes);
+ return(1,$currentphase);
+ }
+ if (%allcodes && !exists($allcodes{$CODE})
+ && !$$scan_record{'scantron.useCODE'}) {
+ &scantron_get_correction($r,$i,$scan_record,
+ \%scantron_config,
+ $line,'incorrectCODE',\%allcodes);
+ return(1,$currentphase);
+ }
+ if (exists($usedCODEs{$CODE})
+ && $env{'form.scantron_CODEunique'} eq 'yes'
+ && !$$scan_record{'scantron.CODE_ignore_dup'}) {
+ &scantron_get_correction($r,$i,$scan_record,
+ \%scantron_config,
+ $line,'duplicateCODE',$usedCODEs{$CODE});
+ return(1,$currentphase);
+ }
+ push (@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'});
+ }
+ return (0,$currentphase+1);
+}
+
+=pod
+
+=item scantron_validate_doublebubble
+
+ Validates all scanlines in the selected file to not have any
+ bubble lines with multiple bubbles marked.
+
+=cut
+
+sub scantron_validate_doublebubble {
+ my ($r,$currentphase) = @_;
+ #get student info
+ my $classlist=&Apache::loncoursedata::get_classlist();
+ my %idmap=&username_to_idmap($classlist);
+
+ #get scantron line setup
+ my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
+ my ($scanlines,$scan_data)=&scantron_getfile();
+
+ &scantron_get_maxbubble(); # parse needs the bubble line array.
+ &Apache::lonnet::logthis("validate_double_bubble");
+
+ for (my $i=0;$i<=$scanlines->{'count'};$i++) {
+ my $line=&scantron_get_line($scanlines,$scan_data,$i);
+ if ($line=~/^[\s\cz]*$/) { next; }
+ my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config,
+ $scan_data);
+ if (!defined($$scan_record{'scantron.doubleerror'})) { next; }
+ &Apache::lonnet::logthis("Double found for line $i : $line");
+ &scantron_get_correction($r,$i,$scan_record,\%scantron_config,$line,
+ 'doublebubble',
+ $$scan_record{'scantron.doubleerror'});
+ return (1,$currentphase);
+ }
+ return (0,$currentphase+1);
+}
+
+=pod
+
+=item scantron_get_maxbubble
+
+ Returns the maximum number of bubble lines that are expected to
+ occur. Does this by walking the selected sequence rendering the
+ resource and then checking &Apache::lonxml::get_problem_counter()
+ for what the current value of the problem counter is.
+
+ Caches the 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 {
+ if (defined($env{'form.scantron_maxbubble'}) &&
+ $env{'form.scantron_maxbubble'}) {
+ &restore_bubble_lines();
+ return $env{'form.scantron_maxbubble'};
+ }
+
+ my (undef, undef, $sequence) =
+ &Apache::lonnet::decode_symb($env{'form.selectpage'});
+
+ my $navmap=Apache::lonnavmaps::navmap->new();
+ my $map=$navmap->getResourceByUrl($sequence);
+ my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
+
+ &Apache::lonxml::clear_problem_counter();
+
+ my $uname = $env{'form.student'};
+ my $udom = $env{'form.userdom'};
+ my $cid = $env{'request.course.id'};
+ my $total_lines = 0;
+ %bubble_lines_per_response = ();
+ %first_bubble_line = ();
+
+
+ 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++;
+
+ $bubble_line += $lines;
+ $total_lines += $lines;
+ }
+
+ }
+ &Apache::lonnet::delenv('scantron\.');
+
+ &save_bubble_lines();
+ $env{'form.scantron_maxbubble'} =
+ $total_lines;
+ return $env{'form.scantron_maxbubble'};
+}
+
+=pod
+
+=item scantron_validate_missingbubbles
+
+ Validates all scanlines in the selected file to not have any
+ 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
+ my $classlist=&Apache::loncoursedata::get_classlist();
+ my %idmap=&username_to_idmap($classlist);
+
+ #get scantron line setup
+ my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
+ my ($scanlines,$scan_data)=&scantron_getfile();
+ my $max_bubble=&scantron_get_maxbubble();
+ if (!$max_bubble) { $max_bubble=2**31; }
+ for (my $i=0;$i<=$scanlines->{'count'};$i++) {
+ my $line=&scantron_get_line($scanlines,$scan_data,$i);
+ if ($line=~/^[\s\cz]*$/) { next; }
+ my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config,
+ $scan_data);
+ if (!defined($$scan_record{'scantron.missingerror'})) { next; }
+ my @to_correct;
+
+ # Probably here's where the error is...
+
+ foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) {
+ if ($missing > $max_bubble) { next; }
+ push(@to_correct,$missing);
+ }
+ if (@to_correct) {
+ &scantron_get_correction($r,$i,$scan_record,\%scantron_config,
+ $line,'missingbubble',\@to_correct);
+ return (1,$currentphase);
+ }
+
+ }
+ return (0,$currentphase+1);
+}
+
+=pod
+
+=item scantron_process_students
+
+ Routine that does the actual grading of the bubble sheet information.
+
+ The parsed scanline hash is added to %env
+
+ Then foreach unskipped scanline it does an &Apache::lonnet::ssi()
+ foreach resource , with the form data of
+
+ 'submitted' =>'scantron'
+ 'grade_target' =>'grade',
+ 'grade_username'=> username of student
+ 'grade_domain' => domain of student
+ 'grade_courseid'=> of course
+ 'grade_symb' => symb of resource to grade
+
+ This triggers a grading pass. The problem grading code takes care
+ of converting the bubbled letter information (now in %env) into a
+ valid submission.
+
+=cut
+
sub scantron_process_students {
my ($r) = @_;
- my (undef,undef,$sequence)=split(/___/,$ENV{'form.selectpage'});
- my ($symb,$url)=&get_symb_and_url($r);
+ my (undef,undef,$sequence)=&Apache::lonnet::decode_symb($env{'form.selectpage'});
+ my ($symb)=&get_symb($r);
if (!$symb) {return '';}
- my $default_form_data=&defaultFormData($symb,$url);
+ my $default_form_data=&defaultFormData($symb);
- my %scantron_config=&get_scantron_config($ENV{'form.scantron_format'});
- my $scanlines=Apache::File->new($Apache::lonnet::perlvar{'lonScansDir'}."/$ENV{'form.scantron_selectfile'}");
- my @scanlines=<$scanlines>;
+ my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
+ my ($scanlines,$scan_data)=&scantron_getfile();
my $classlist=&Apache::loncoursedata::get_classlist();
my %idmap=&username_to_idmap($classlist);
- my $navmap=Apache::lonnavmaps::navmap->new($ENV{'request.course.fn'}.'.db',$ENV{'request.course.fn'}.'_parms.db',1, 1);
+ my $navmap=Apache::lonnavmaps::navmap->new();
my $map=$navmap->getResourceByUrl($sequence);
my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
- $r->print("geto ".scalar(@resources)." ");
+# $r->print("geto ".scalar(@resources)." ");
my $result= <
@@ -3213,104 +6993,278 @@ SCANTRONFORM
$r->print($result);
my @delayqueue;
- my $totalcorrect;
- my $totalincorrect;
+ my %completedstudents;
+
+ my $count=&get_todo_count($scanlines,$scan_data);
+ my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron Status',
+ 'Scantron Progress',$count,
+ 'inline',undef,'scantronupload');
+ &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,
+ 'Processing first student');
+ my $start=&Time::HiRes::time();
+ my $i=-1;
+ my ($uname,$udom,$started);
+
+ &scantron_get_maxbubble(); # Need the bubble lines array to parse.
+
+ while ($i<$scanlines->{'count'}) {
+ ($uname,$udom)=('','');
+ $i++;
+ my $line=&scantron_get_line($scanlines,$scan_data,$i);
+ if ($line=~/^[\s\cz]*$/) { next; }
+ if ($started) {
+ &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state,
+ 'last student');
+ }
+ $started=1;
+ my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config,
+ $scan_data);
+ unless ($uname=&scantron_find_student($scan_record,$scan_data,
+ \%idmap,$i)) {
+ &scantron_add_delay(\@delayqueue,$line,
+ 'Unable to find a student that matches',1);
+ next;
+ }
+ if (exists $completedstudents{$uname}) {
+ &scantron_add_delay(\@delayqueue,$line,
+ 'Student '.$uname.' has multiple sheets',2);
+ next;
+ }
+ ($uname,$udom)=split(/:/,$uname);
- my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,
- 'Scantron Status','Scantron Progress',scalar(@scanlines));
- foreach my $line (@scanlines) {
- my $studentcorrect;
- my $studentincorrect;
+ &Apache::lonxml::clear_problem_counter();
+ &Apache::lonnet::appenv(%$scan_record);
- chomp($line);
- my $scan_record=&scantron_parse_scanline($line,\%scantron_config);
- my ($uname,$udom);
- if ($uname=&scantron_find_student($scan_record,\%idmap)) {
- &scantron_add_delay(\@delayqueue,$line,
- 'Unable to find a student that matches');
- }
- $r->print('doing studnet'.$uname.' ');
- ($uname,$udom)=split(/:/,$uname);
- &Apache::lonnet::delenv('form.counter');
- &Apache::lonnet::appenv(%$scan_record);
-# &Apache::lonhomework::showhash(%ENV);
- $Apache::lonxml::debug=1;
- &Apache::lonxml::debug("line is $line");
+ if (&scantron_clear_skip($scanlines,$scan_data,$i)) {
+ &scantron_putfile($scanlines,$scan_data);
+ }
- my $i=0;
+ my $i=0;
foreach my $resource (@resources) {
$i++;
- my $result=&Apache::lonnet::ssi($resource->src(),
- ('submitted' =>'scantron',
- 'grade_target' =>'grade',
- 'grade_username'=>$uname,
- 'grade_domain' =>$udom,
- 'grade_courseid'=>$ENV{'request.course.id'},
- 'grade_symb' =>$resource->symb()));
- my %score=&Apache::lonnet::restore($resource->symb(),
- $ENV{'request.course.id'},
- $udom,$uname);
- foreach my $part ($resource->{PARTS}) {
- if ($score{'resource.'.$part.'.solved'} =~ /^correct/) {
- $studentcorrect++;
- $totalcorrect++;
- } else {
- $studentincorrect++;
- $totalincorrect++;
- }
+ 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'}='';
}
- $r->print(''.
- $resource->symb().'-'.
- $resource->src().'-'.' result is'.$result);
- &Apache::lonhomework::showhash(%score);
- # if ($i eq 3) {last;}
+ my $result=&Apache::lonnet::ssi($resource->src(),%form);
+ if ($result ne '') {
+ }
+ if (&Apache::loncommon::connection_aborted($r)) { last; }
}
- &Apache::lonnet::delenv('form.counter');
+ $completedstudents{$uname}={'line'=>$line};
+ if (&Apache::loncommon::connection_aborted($r)) { last; }
+ } continue {
+ &Apache::lonxml::clear_problem_counter();
&Apache::lonnet::delenv('scantron\.');
- &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state,
- 'last student Who got a '.$studentcorrect.' correct and '.
- $studentincorrect.' incorrect. The class has gotten '.
- $totalcorrect.' correct and '.$totalincorrect.' incorrect');
- last;
- #FIXME
- #get iterator for $sequence
- #foreach question 'submit' the students answer to the server
- # through grade target {
- # generate data to pass back that includes grade recevied
- #}
- }
- $Apache::lonxml::debug=0;
- foreach my $delay (@delayqueue) {
- #FIXME
- #print out each delayed student with interface to select how
- # to repair student provided info
- #Expected errors include
- # 1 bad/no stuid/username
- # 2 invalid bubblings
-
}
+ &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
+# my $lasttime = &Time::HiRes::time()-$start;
+# $r->print("took $lasttime
");
+
+ $r->print("");
+ $r->print(&show_grading_menu_form($symb));
+ return '';
+}
+
+=pod
+
+=item scantron_upload_scantron_data
+
+ Creates the screen for adding a new bubble sheet data file to a course.
+
+=cut
+
+sub scantron_upload_scantron_data {
+ my ($r)=@_;
+ $r->print(&Apache::loncommon::coursebrowser_javascript($env{'request.role.domain'}));
+ my $select_link=&Apache::loncommon::selectcourse_link('rules','courseid',
+ 'domainid',
+ 'coursename');
+ my $domsel=&Apache::loncommon::select_dom_form($env{'request.role.domain'},
+ 'domainid');
+ my $default_form_data=&defaultFormData(&get_symb($r,1));
+ $r->print(<
+ function checkUpload(formname) {
+ if (formname.upfile.value == "") {
+ alert("Please use the browse button to select a file from your local directory.");
+ return false;
+ }
+ formname.submit();
+ }
+
+
+
+$default_form_data
+
+
+
+
+UPLOAD
+ return '';
+}
+
+=pod
+
+=item scantron_upload_scantron_data_save
+
+ Adds a provided bubble information data file to the course if user
+ has the correct privileges to do so.
+
+=cut
+
+sub scantron_upload_scantron_data_save {
+ my($r)=@_;
+ my ($symb)=&get_symb($r,1);
+ my $doanotherupload=
+ ''."\n".
+ ' '."\n".
+ ' '."\n".
+ ' '."\n";
+ if (!&Apache::lonnet::allowed('usc',$env{'form.domainid'}) &&
+ !&Apache::lonnet::allowed('usc',
+ $env{'form.domainid'}.'_'.$env{'form.courseid'})) {
+ $r->print("You are not allowed to upload Scantron data to the requested course. ");
+ if ($symb) {
+ $r->print(&show_grading_menu_form($symb));
+ } else {
+ $r->print($doanotherupload);
+ }
+ return '';
+ }
+ my %coursedata=&Apache::lonnet::coursedescription($env{'form.domainid'}.'_'.$env{'form.courseid'});
+ $r->print("Doing upload to ".$coursedata{'description'}." ");
+ my $fname=$env{'form.upfile.filename'};
#FIXME
- # if delay queue exists 2 submits one to process delayed students one
- # to ignore delayed students, possibly saving the delay queue for later
-
- $navmap->untieHashes();
+ #copied from lonnet::userfileupload()
+ #make that function able to target a specified course
+ # Replace Windows backslashes by forward slashes
+ $fname=~s/\\/\//g;
+ # Get rid of everything but the actual filename
+ $fname=~s/^.*\/([^\/]+)$/$1/;
+ # Replace spaces by underscores
+ $fname=~s/\s+/\_/g;
+ # Replace all other weird characters by nothing
+ $fname=~s/[^\w\.\-]//g;
+ # See if there is anything left
+ unless ($fname) { return 'error: no uploaded file'; }
+ my $uploadedfile=$fname;
+ $fname='scantron_orig_'.$fname;
+ if (length($env{'form.upfile'}) < 2) {
+ $r->print("Error: The file you attempted to upload, ".&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"')." , contained no information. Please check that you entered the correct filename.");
+ } else {
+ my $result=&Apache::lonnet::finishuserfileupload($env{'form.courseid'},$env{'form.domainid'},'upfile',$fname);
+ if ($result =~ m|^/uploaded/|) {
+ $r->print("Success: Successfully uploaded ".(length($env{'form.upfile'})-1)." bytes of data into location ".$result." ");
+ } else {
+ $r->print("Error: An error (".$result.") occurred when attempting to upload the file, ".&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"')." ");
+ }
+ }
+ if ($symb) {
+ $r->print(&scantron_selectphase($r,$uploadedfile));
+ } else {
+ $r->print($doanotherupload);
+ }
+ return '';
+}
+
+=pod
+
+=item valid_file
+
+ Validates that the requested bubble data file exists in the course.
+
+=cut
+
+sub valid_file {
+ my ($requested_file)=@_;
+ foreach my $filename (sort(&scantron_filenames())) {
+ if ($requested_file eq $filename) { return 1; }
+ }
+ return 0;
}
+
+=pod
+
+=item scantron_download_scantron_data
+
+ Shows a list of the three internal files (original, corrected,
+ skipped) for a specific bubble sheet data file that exists in the
+ course.
+
+=cut
+
+sub scantron_download_scantron_data {
+ my ($r)=@_;
+ my $default_form_data=&defaultFormData(&get_symb($r,1));
+ my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
+ my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
+ my $file=$env{'form.scantron_selectfile'};
+ if (! &valid_file($file)) {
+ $r->print(<
+ The requested file name was invalid.
+
+ERROR
+ $r->print(&show_grading_menu_form(&get_symb($r,1)));
+ return;
+ }
+ my $orig='/uploaded/'.$cdom.'/'.$cname.'/scantron_orig_'.$file;
+ my $corrected='/uploaded/'.$cdom.'/'.$cname.'/scantron_corrected_'.$file;
+ my $skipped='/uploaded/'.$cdom.'/'.$cname.'/scantron_skipped_'.$file;
+ &Apache::lonnet::allowuploaded('/adm/grades',$orig);
+ &Apache::lonnet::allowuploaded('/adm/grades',$corrected);
+ &Apache::lonnet::allowuploaded('/adm/grades',$skipped);
+ $r->print(<
+ Original file as uploaded by the scantron office.
+
+
+ Corrections , a file of corrected records that were used in grading.
+
+
+ Skipped , a file of records that were skipped.
+
+DOWNLOAD
+ $r->print(&show_grading_menu_form(&get_symb($r,1)));
+ return '';
+}
+
+=pod
+
+=back
+
+=cut
+
#-------- end of section for handling grading scantron forms -------
#
#-------------------------------------------------------------------
-
#-------------------------- Menu interface -------------------------
#
#--- Show a Grading Menu button - Calls the next routine ---
sub show_grading_menu_form {
- my ($symb,$url)=@_;
- my $result.=''."\n".
- ' '."\n".
- ' '."\n".
- ' '."\n".
+ my ($symb)=@_;
+ my $result.=' '."\n".
+ ' '."\n".
+ ' '."\n".
' '."\n".
- ' '."\n".
+ ' '."\n".
' '."\n";
return $result;
}
@@ -3318,8 +7272,8 @@ sub show_grading_menu_form {
# -- Retrieve choices for grading form
sub savedState {
my %savedState = ();
- if ($ENV{'form.saveState'}) {
- foreach (split(/:/,$ENV{'form.saveState'})) {
+ if ($env{'form.saveState'}) {
+ foreach (split(/:/,$env{'form.saveState'})) {
my ($key,$value) = split(/=/,$_,2);
$savedState{$key} = $value;
}
@@ -3327,23 +7281,102 @@ sub savedState {
return \%savedState;
}
-#--- Displays the main menu page -------
-sub gradingmenu {
+sub grading_menu {
my ($request) = @_;
- my ($symb,$url)=&get_symb_and_url($request);
+ my ($symb)=&get_symb($request);
if (!$symb) {return '';}
my $probTitle = &Apache::lonnet::gettitle($symb);
+ my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle);
+ $request->print($table);
+ my %fields = ('symb'=>&Apache::lonenc::check_encrypt($symb),
+ 'handgrade'=>$hdgrade,
+ 'probTitle'=>$probTitle,
+ 'command'=>'submit_options',
+ 'saveState'=>"",
+ 'gradingMenu'=>1,
+ 'showgrading'=>"yes");
+ my $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ my @menu = ({ url => $url,
+ name => &mt('Manual Grading/View Submissions'),
+ short_description =>
+ &mt('Start the process of hand grading submissions.'),
+ });
+ $fields{'command'} = 'csvform';
+ $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ push (@menu, { url => $url,
+ name => &mt('Upload Scores'),
+ short_description =>
+ &mt('Specify a file containing the class scores for current resource.')});
+ $fields{'command'} = 'processclicker';
+ $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ push (@menu, { url => $url,
+ name => &mt('Process Clicker'),
+ short_description =>
+ &mt('Specify a file containing the clicker information for this resource.')});
+ $fields{'command'} = 'scantron_selectphase';
+ $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ push (@menu, { url => $url,
+ name => &mt('Grade/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('')});
+ #
+ # Create the menu
+ my $Str;
+ # $Str .= ''.&mt('Please select a grading task').' ';
+ $Str .= '';
+ $Str .= ' '.
+ ' '."\n".
+ ' '."\n".
+ ' '."\n".
+ ' '."\n".
+ ' '."\n".
+ ' '."\n";
+
+ foreach my $menudata (@menu) {
+ if ($menudata->{'name'} ne &mt('Verify Receipt')) {
+ $Str .=' \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";
$request->print(<
- function checkChoice(formname) {
- var cmd = formname.command;
- formname.saveState.value = "saveCmd="+radioSelection(cmd)+":saveSec="+pullDownSelection(formname.section)+
- ":saveSub="+radioSelection(formname.submitonly)+":saveStatus="+pullDownSelection(formname.Status);
- if (cmd[0].checked || cmd[1].checked || cmd[2].checked || cmd[3].checked || cmd[4].checked) formname.submit();
- if (cmd[5].checked) {
- if (!checkReceiptNo(formname,'notOK')) { return false;}
- formname.submit();
+ function checkChoice(formname,val,cmdx) {
+ if (val <= 2) {
+ var cmd = radioSelection(formname.radioChoice);
+ var cmdsave = cmd;
+ } else {
+ cmd = cmdx;
+ cmdsave = 'submission';
+ }
+ formname.command.value = cmd;
+ if (val < 5) formname.submit();
+ if (val == 5) {
+ if (!checkReceiptNo(formname,'notOK')) {
+ return false;
+ } else {
+ formname.submit();
+ }
}
}
@@ -3358,173 +7391,714 @@ sub gradingmenu {
formname.receipt.focus();
return false;
}
- formname.command[5].checked = true;
return true;
}
+
+GRADINGMENUJS
+ &commonJSfunctions($request);
+ return $Str;
+}
- function radioSelection(radioButton) {
- var selection=null;
- if (radioButton.length > 1) {
- for (var i=0; iprint(<
+ function checkChoice(formname,val,cmdx) {
+ if (val <= 2) {
+ var cmd = radioSelection(formname.radioChoice);
+ var cmdsave = cmd;
} else {
- if (radioButton.checked) return radioButton.value;
+ cmd = cmdx;
+ cmdsave = 'submission';
}
- return selection;
+ formname.command.value = cmd;
+ formname.saveState.value = "saveCmd="+cmdsave+":saveSec="+pullDownSelection(formname.section)+
+ ":saveSub="+pullDownSelection(formname.submitonly)+":saveStatus="+pullDownSelection(formname.Status);
+ if (val < 5) formname.submit();
+ if (val == 5) {
+ if (!checkReceiptNo(formname,'notOK')) { return false;}
+ formname.submit();
+ }
+ if (val < 7) formname.submit();
}
- function pullDownSelection(selectOne) {
- var selection="";
- if (selectOne.length > 1) {
- for (var i=0; i
GRADINGMENUJS
-
- my $result=' Manual Grading/View Submission '.
- ''.
- 'Problem: '.$probTitle.' '."\n";
- my ($partlist,$handgrade) = &response_type($url);
- my ($resptype,$hdgrade)=('','no');
- for (sort keys(%$handgrade)) {
- my ($responsetype,$handgrade)=split(/:/,$$handgrade{$_});
- $resptype = $responsetype;
- $hdgrade = $handgrade if ($handgrade eq 'yes');
- $result.='Part '.(split(/_/))[0].' '.
- 'Type: '.$responsetype.' '.
- 'Handgrade: '.$handgrade.' ';
- }
- $result.='
'."\n";
-
+ &commonJSfunctions($request);
+ my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle);
+ my $result;
my (undef,$sections) = &getclasslist('all','0');
my $savedState = &savedState();
- my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'pickStudentPage' : $$savedState{'saveCmd'});
+ my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'submission' : $$savedState{'saveCmd'});
my $saveSec = ($$savedState{'saveSec'} eq '' ? 'all' : $$savedState{'saveSec'});
- my $saveSub = ($$savedState{'saveSub'} eq '' ? 'yes' : $$savedState{'saveSub'});
+ my $saveSub = ($$savedState{'saveSub'} eq '' ? 'all' : $$savedState{'saveSub'});
my $saveStatus = ($$savedState{'saveStatus'} eq '' ? 'Active' : $$savedState{'saveStatus'});
$result.=''."\n".
- ' '."\n".
- ' '."\n".
- ' '."\n".
+ ' '."\n".
' '."\n".
' '."\n".
+ ' '."\n".
' '."\n".
+ ' '."\n".
' '."\n";
- $result.=''."\n".
- ''."\n".
- ' Select a Grading/Viewing Option '."\n".
- ''."\n";
-
- $result.=''.
- ''.
- ' '.
- 'Handgrade/View Submission for a student by page/sequence '."\n".
-
- ''.
- ' '.
- 'Grade by section or class '."\n".
-
- ' '.
- ($hdgrade eq 'yes' ? 'View/Grade essay response of' : 'View').
- ' an individual student '."\n".
- '--> For students who has: '.
- ' submitted'.
- ' everybody '."\n".
-
- ''.
- ' '.
- 'Upload scores from file '."\n";
-
- $result.=''.
- ' '.
- 'Grade scantron forms '."\n";
-
- if ((&Apache::lonnet::allowed('mgr',$ENV{'request.course.id'})) && ($symb)) {
- $result.=''.
- ' '.
- 'Verify a submission receipt issued by this server '.
- '--> Receipt no: '.unpack("%32C*",$Apache::lonnet::perlvar{'lonHostID'}).
- '- '.
- ' '."\n";
- }
-
- $result.=' '."\n".
- ' Select section: '."\n";
+ $result.='
+
+
+
+ '.&mt('Grade Current Resource').'
+
+
+
+ '.$table.'
+
+
+
+
+ '."\n";
if (ref($sections)) {
- foreach (sort (@$sections)) {$result.=''.$_.' '."\n";}
+ foreach my $section (sort (@$sections)) {
+ $result.=''.$section.' '."\n";
+ }
+ }
+ $result.= 'all ';
+ $result.='
+
+
+
+
+
+ '.&Apache::lonstatistics::GroupSelect('group','multiple',5).'
+
+
+
+
+
+ '.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,5,undef,'mult').'
+
+
+
+
+
+
+ '.&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').'
+
+
+
+
+ ';
+ return $result;
+}
+
+sub reset_perm {
+ undef(%perm);
+}
+
+sub init_perm {
+ &reset_perm();
+ foreach my $test_perm ('vgr','mgr','opa') {
+
+ my $scope = $env{'request.course.id'};
+ if (!($perm{$test_perm}=&Apache::lonnet::allowed($test_perm,$scope))) {
+
+ $scope .= '/'.$env{'request.course.sec'};
+ if ( $perm{$test_perm}=
+ &Apache::lonnet::allowed($test_perm,$scope)) {
+ $perm{$test_perm.'_section'}=$env{'request.course.sec'};
+ } else {
+ delete($perm{$test_perm});
+ }
+ }
}
- $result.= 'all ';
+}
- $result.='Student Status:'.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,undef);
- $result.=' (Applies to the first three options only.) '."\n";
+sub gather_clicker_ids {
+ my %clicker_ids;
- if (ref($sections)) {
- $result.=' (Section "no" implies the students were not assigned a section.) '
- if (grep /no/,@$sections);
+ my $classlist = &Apache::loncoursedata::get_classlist();
+
+ # Set up a couple variables.
+ my $username_idx = &Apache::loncoursedata::CL_SNAME();
+ my $domain_idx = &Apache::loncoursedata::CL_SDOM();
+ my $status_idx = &Apache::loncoursedata::CL_STATUS();
+
+ foreach my $student (keys(%$classlist)) {
+ if ($classlist->{$student}->[$status_idx] ne 'Active') { next; }
+ my $username = $classlist->{$student}->[$username_idx];
+ my $domain = $classlist->{$student}->[$domain_idx];
+ my $clickers =
+ (&Apache::lonnet::userenvironment($domain,$username,'clickers'))[1];
+ foreach my $id (split(/\,/,$clickers)) {
+ $id=~s/^[\#0]+//;
+ $id=~s/[\-\:]//g;
+ if (exists($clicker_ids{$id})) {
+ $clicker_ids{$id}.=','.$username.':'.$domain;
+ } else {
+ $clicker_ids{$id}=$username.':'.$domain;
+ }
+ }
}
- $result.=' ';
+ return %clicker_ids;
+}
- $result.=' '.
- ' '."\n".
- '
'."\n".
- '
'."\n".
- '
'."\n";
+sub gather_adv_clicker_ids {
+ my %clicker_ids;
+ my $cnum=$env{'course.'.$env{'request.course.id'}.'.num'};
+ my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
+ my %coursepersonnel=&Apache::lonnet::get_course_adv_roles($cdom.'/'.$cnum);
+ foreach my $element (sort(keys(%coursepersonnel))) {
+ foreach my $person (split(/\,/,$coursepersonnel{$element})) {
+ my ($puname,$pudom)=split(/\:/,$person);
+ my $clickers =
+ (&Apache::lonnet::userenvironment($pudom,$puname,'clickers'))[1];
+ foreach my $id (split(/\,/,$clickers)) {
+ $id=~s/^[\#0]+//;
+ $id=~s/[\-\:]//g;
+ if (exists($clicker_ids{$id})) {
+ $clicker_ids{$id}.=','.$puname.':'.$pudom;
+ } else {
+ $clicker_ids{$id}=$puname.':'.$pudom;
+ }
+ }
+ }
+ }
+ return %clicker_ids;
+}
+
+sub clicker_grading_parameters {
+ return ('gradingmechanism' => 'scalar',
+ 'upfiletype' => 'scalar',
+ 'specificid' => 'scalar',
+ 'pcorrect' => 'scalar',
+ 'pincorrect' => 'scalar');
+}
+
+sub process_clicker {
+ my ($r)=@_;
+ my ($symb)=&get_symb($r);
+ if (!$symb) {return '';}
+ my $result=&checkforfile_js();
+ $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb);
+ my ($table) = &showResourceInfo($symb,$env{'form.probTitle'});
+ $result.=$table;
+ $result.=''."\n";
+ $result.=''."\n".
+ '
'."\n";
+ $result.=&show_grading_menu_form($symb);
return $result;
}
+sub process_clicker_file {
+ my ($r)=@_;
+ my ($symb)=&get_symb($r);
+ if (!$symb) {return '';}
+
+ my %Saveable_Parameters=&clicker_grading_parameters();
+ &Apache::loncommon::store_course_settings('grades_clicker',
+ \%Saveable_Parameters);
+
+ my ($result) = &showResourceInfo($symb,$env{'form.probTitle'});
+ if (($env{'form.gradingmechanism'} eq 'specific') && ($env{'form.specificid'}!~/\w/)) {
+ $result.=''.&mt('You need to specify a clicker ID for the correct answer').' ';
+ return $result.&show_grading_menu_form($symb);
+ }
+ my %clicker_ids=&gather_clicker_ids();
+ my %correct_ids;
+ if ($env{'form.gradingmechanism'} eq 'personnel') {
+ %correct_ids=&gather_adv_clicker_ids();
+ }
+ if ($env{'form.gradingmechanism'} eq 'specific') {
+ foreach my $correct_id (split(/[\s\,]/,$env{'form.specificid'})) {;
+ $correct_id=~tr/a-z/A-Z/;
+ $correct_id=~s/\s//gs;
+ $correct_id=~s/^[\#0]+//;
+ $correct_id=~s/[\-\:]//g;
+ if ($correct_id) {
+ $correct_ids{$correct_id}='specified';
+ }
+ }
+ }
+ if ($env{'form.gradingmechanism'} eq 'attendance') {
+ $result.=&mt('Score based on attendance only');
+ } else {
+ my $number=0;
+ $result.=''.&mt('Correctness determined by the following IDs').' ';
+ foreach my $id (sort(keys(%correct_ids))) {
+ $result.=''.$id.' - ';
+ if ($correct_ids{$id} eq 'specified') {
+ $result.=&mt('specified');
+ } else {
+ my ($uname,$udom)=split(/\:/,$correct_ids{$id});
+ $result.=&Apache::loncommon::plainname($uname,$udom);
+ }
+ $number++;
+ }
+ $result.="
\n";
+ if ($number==0) {
+ $result.=''.&mt('No IDs found to determine correct answer').' ';
+ return $result.&show_grading_menu_form($symb);
+ }
+ }
+ if (length($env{'form.upfile'}) < 2) {
+ $result.=&mt('[_1] Error: [_2] The file you attempted to upload, [_3] contained no information. Please check that you entered the correct filename.',
+ '',
+ ' ',
+ ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').' ');
+ return $result.&show_grading_menu_form($symb);
+ }
+
+# Were able to get all the info needed, now analyze the file
+
+ $result.=&Apache::loncommon::studentbrowser_javascript();
+ $symb = &Apache::lonenc::check_encrypt($symb);
+ my $heading=&mt('Scanning clicker file');
+ $result.=(<
+
+$heading
+
+
+
+
+
+
+
+
+ENDHEADER
+ my %responses;
+ my @questiontitles;
+ my $errormsg='';
+ my $number=0;
+ if ($env{'form.upfiletype'} eq 'iclicker') {
+ ($errormsg,$number)=&iclicker_eval(\@questiontitles,\%responses);
+ }
+ if ($env{'form.upfiletype'} eq 'interwrite') {
+ ($errormsg,$number)=&interwrite_eval(\@questiontitles,\%responses);
+ }
+ $result.=' '.&mt('Found [_1] question(s)',$number).' '.
+ ' '.
+ &mt('Awarding [_1] percent for corrion(s)',$number).' '.
+ ' '.
+ &mt('Awarding [_1] percent for correct and [_2] percent for incorrect responses',
+ $env{'form.pcorrect'},$env{'form.pincorrect'}).
+ ' ';
+# Remember Question Titles
+# FIXME: Possibly need delimiter other than ":"
+ for (my $i=0;$i<$number;$i++) {
+ $result.=' ').'" />';
+ }
+ my $correct_count=0;
+ my $student_count=0;
+ my $unknown_count=0;
+# Match answers with usernames
+# FIXME: Possibly need delimiter other than ":"
+ foreach my $id (keys(%responses)) {
+ if ($correct_ids{$id}) {
+ $result.="\n".' ';
+ $correct_count++;
+ } elsif ($clicker_ids{$id}) {
+ if ($clicker_ids{$id}=~/\,/) {
+# More than one user with the same clicker!
+ $result.="\n ".&mt('Clicker registered more than once').": ".$id." ";
+ $result.="\n".' '.
+ "";
+ foreach my $reguser (sort(split(/\,/,$clicker_ids{$id}))) {
+ $result.="".&Apache::loncommon::plainname(split(/\:/,$reguser)).' ('.$reguser.') ';
+ }
+ $result.=' ';
+ $unknown_count++;
+ } else {
+# Good: found one and only one user with the right clicker
+ $result.="\n".' ';
+ $student_count++;
+ }
+ } else {
+ $result.="\n ".&mt('Unregistered Clicker')." ".$id." ";
+ $result.="\n".' '.
+ "\n".&mt("Username").": ".
+ "\n".&mt("Domain").": ".
+ &Apache::loncommon::select_dom_form($env{'course.'.$env{'request.course.id'}.'.domain'},'udom'.$id).' '.
+ &Apache::loncommon::selectstudent_link('clickeranalysis','uname'.$id,'udom'.$id);
+ $unknown_count++;
+ }
+ }
+ $result.=' '.
+ &mt('Found [_1] registered and [_2] unregistered clickers.',$student_count,$unknown_count);
+ if ($env{'form.gradingmechanism'} ne 'attendance') {
+ if ($correct_count==0) {
+ $errormsg.="Found no correct answers answers for grading!";
+ } elsif ($correct_count>1) {
+ $result.=''.&mt("Found [_1] entries for grading!",$correct_count).' ';
+ }
+ }
+ if ($number<1) {
+ $errormsg.="Found no questions.";
+ }
+ if ($errormsg) {
+ $result.=''.&mt($errormsg).' ';
+ } else {
+ $result.=' ';
+ }
+ $result.='
'."\n".
+ '
'."\n";
+ return $result.&show_grading_menu_form($symb);
+}
+
+sub iclicker_eval {
+ my ($questiontitles,$responses)=@_;
+ my $number=0;
+ my $errormsg='';
+ foreach my $line (split(/[\n\r]/,$env{'form.upfile'})) {
+ my %components=&Apache::loncommon::record_sep($line);
+ my @entries=map {$components{$_}} (sort(keys(%components)));
+ if ($entries[0] eq 'Question') {
+ for (my $i=3;$i<$#entries;$i+=6) {
+ $$questiontitles[$number]=$entries[$i];
+ $number++;
+ }
+ }
+ if ($entries[0]=~/^\#/) {
+ my $id=$entries[0];
+ my @idresponses;
+ $id=~s/^[\#0]+//;
+ for (my $i=0;$i<$number;$i++) {
+ my $idx=3+$i*6;
+ push(@idresponses,$entries[$idx]);
+ }
+ $$responses{$id}=join(',',@idresponses);
+ }
+ }
+ return ($errormsg,$number);
+}
+
+sub interwrite_eval {
+ my ($questiontitles,$responses)=@_;
+ my $number=0;
+ my $errormsg='';
+ my $skipline=1;
+ my $questionnumber=0;
+ my %idresponses=();
+ foreach my $line (split(/[\n\r]/,$env{'form.upfile'})) {
+ my %components=&Apache::loncommon::record_sep($line);
+ my @entries=map {$components{$_}} (sort(keys(%components)));
+ if ($entries[1] eq 'Time') { $skipline=0; next; }
+ if ($entries[1] eq 'Response') { $skipline=1; }
+ next if $skipline;
+ if ($entries[0]!=$questionnumber) {
+ $questionnumber=$entries[0];
+ $$questiontitles[$number]=&mt('Question [_1]',$questionnumber);
+ $number++;
+ }
+ my $id=$entries[4];
+ $id=~s/^[\#0]+//;
+ $id=~s/^v\d*\://i;
+ $id=~s/[\-\:]//g;
+ $idresponses{$id}[$number]=$entries[6];
+ }
+ foreach my $id (keys %idresponses) {
+ $$responses{$id}=join(',',@{$idresponses{$id}});
+ $$responses{$id}=~s/^\s*\,//;
+ }
+ return ($errormsg,$number);
+}
+
+sub assign_clicker_grades {
+ my ($r)=@_;
+ my ($symb)=&get_symb($r);
+ if (!$symb) {return '';}
+# See which part we are saving to
+ my ($partlist,$handgrade,$responseType) = &response_type($symb);
+# FIXME: This should probably look for the first handgradeable part
+ my $part=$$partlist[0];
+# Start screen output
+ my ($result) = &showResourceInfo($symb,$env{'form.probTitle'});
+
+ my $heading=&mt('Assigning grades based on clicker file');
+ $result.=(<
+
+$heading
+ENDHEADER
+# Get correct result
+# FIXME: Possibly need delimiter other than ":"
+ my @correct=();
+ my $gradingmechanism=$env{'form.gradingmechanism'};
+ my $number=$env{'form.number'};
+ if ($gradingmechanism ne 'attendance') {
+ foreach my $key (keys(%env)) {
+ if ($key=~/^form\.correct\:/) {
+ my @input=split(/\,/,$env{$key});
+ for (my $i=0;$i<=$#input;$i++) {
+ if (($correct[$i]) && ($input[$i]) &&
+ ($correct[$i] ne $input[$i])) {
+ $result.=''.
+ &mt('More than one correct result given for question "[_1]": [_2] versus [_3].',
+ $env{'form.question:'.$i},$correct[$i],$input[$i]).' ';
+ } elsif ($input[$i]) {
+ $correct[$i]=$input[$i];
+ }
+ }
+ }
+ }
+ for (my $i=0;$i<$number;$i++) {
+ if (!$correct[$i]) {
+ $result.=''.
+ &mt('No correct result given for question "[_1]"!',
+ $env{'form.question:'.$i}).' ';
+ }
+ }
+ $result.=' '.&mt("Correct answer: [_1]",join(', ',map { ($_?$_:'-') } @correct));
+ }
+# Start grading
+ my $pcorrect=$env{'form.pcorrect'};
+ my $pincorrect=$env{'form.pincorrect'};
+ my $storecount=0;
+ foreach my $key (keys(%env)) {
+ my $user='';
+ if ($key=~/^form\.student\:(.*)$/) {
+ $user=$1;
+ }
+ if ($key=~/^form\.unknown\:(.*)$/) {
+ my $id=$1;
+ if (($env{'form.uname'.$id}) && ($env{'form.udom'.$id})) {
+ $user=$env{'form.uname'.$id}.':'.$env{'form.udom'.$id};
+ } elsif ($env{'form.multi'.$id}) {
+ $user=$env{'form.multi'.$id};
+ }
+ }
+ if ($user) {
+ my @answer=split(/\,/,$env{$key});
+ my $sum=0;
+ for (my $i=0;$i<$number;$i++) {
+ if ($answer[$i]) {
+ if ($gradingmechanism eq 'attendance') {
+ $sum+=$pcorrect;
+ } else {
+ if ($answer[$i] eq $correct[$i]) {
+ $sum+=$pcorrect;
+ } else {
+ $sum+=$pincorrect;
+ }
+ }
+ }
+ }
+ my $ave=$sum/(100*$number);
+# Store
+ my ($username,$domain)=split(/\:/,$user);
+ my %grades=();
+ $grades{"resource.$part.solved"}='correct_by_override';
+ $grades{"resource.$part.awarded"}=$ave;
+ $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}";
+ my $returncode=&Apache::lonnet::cstore(\%grades,$symb,
+ $env{'request.course.id'},
+ $domain,$username);
+ if ($returncode ne 'ok') {
+ $result.="Failed to save student $username:$domain. Message when trying to save was ($returncode) ";
+ } else {
+ $storecount++;
+ }
+ }
+ }
+# We are done
+ $result.=' '.&mt('Successfully stored grades for [_1] student(s).',$storecount).
+ '
'."\n".
+ '
'."\n";
+ return $result.&show_grading_menu_form($symb);
+}
+
sub handler {
my $request=$_[0];
-
- undef(%perm);
- if ($ENV{'browser.mathml'}) {
- $request->content_type('text/xml');
+ &reset_caches();
+ if ($env{'browser.mathml'}) {
+ &Apache::loncommon::content_type($request,'text/xml');
} else {
- $request->content_type('text/html');
+ &Apache::loncommon::content_type($request,'text/html');
}
$request->send_http_header;
return '' if $request->header_only;
&Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'});
- my $url=$ENV{'form.url'};
- my $symb=$ENV{'form.symb'};
- my $command=$ENV{'form.command'};
- if (!$url) {
- my ($temp1,$temp2);
- ($temp1,$temp2,$ENV{'form.url'})=split(/___/,$symb);
- $url = $ENV{'form.url'};
- }
- &send_header($request);
- if ($url eq '' && $symb eq '') {
- if ($ENV{'user.adv'}) {
- if (($ENV{'form.codeone'}) && ($ENV{'form.codetwo'}) &&
- ($ENV{'form.codethree'})) {
- my $token=$ENV{'form.codeone'}.'*'.$ENV{'form.codetwo'}.'*'.
- $ENV{'form.codethree'};
+ my $symb=&get_symb($request,1);
+ my @commands=&Apache::loncommon::get_env_multiple('form.command');
+ my $command=$commands[0];
+
+ if ($#commands > 0) {
+ &Apache::lonnet::logthis("grades got multiple commands ".join(':',@commands));
+ }
+
+
+ $request->print(&Apache::loncommon::start_page('Grading'));
+ if ($symb eq '' && $command eq '') {
+ if ($env{'user.adv'}) {
+ if (($env{'form.codeone'}) && ($env{'form.codetwo'}) &&
+ ($env{'form.codethree'})) {
+ my $token=$env{'form.codeone'}.'*'.$env{'form.codetwo'}.'*'.
+ $env{'form.codethree'};
my ($tsymb,$tuname,$tudom,$tcrsid)=
&Apache::lonnet::checkin($token);
if ($tsymb) {
- my ($map,$id,$url)=split(/\_\_\_/,$tsymb);
+ my ($map,$id,$url)=&Apache::lonnet::decode_symb($tsymb);
if (&Apache::lonnet::allowed('mgr',$tcrsid)) {
$request->print(&Apache::lonnet::ssi_body('/res/'.$url,
('grade_username' => $tuname,
@@ -3542,23 +8116,9 @@ sub handler {
}
}
} else {
- if (!($perm{'vgr'}=&Apache::lonnet::allowed('vgr',$ENV{'request.course.id'}))) {
- if ($perm{'vgr'}=&Apache::lonnet::allowed('vgr',$ENV{'request.course.id'}.'/'.$ENV{'request.course.sec'})) {
- $perm{'vgr_section'}=$ENV{'request.course.sec'};
- } else {
- delete($perm{'vgr'});
- }
- }
- if (!($perm{'mgr'}=&Apache::lonnet::allowed('mgr',$ENV{'request.course.id'}))) {
- if ($perm{'mgr'}=&Apache::lonnet::allowed('mgr',$ENV{'request.course.id'}.'/'.$ENV{'request.course.sec'})) {
- $perm{'mgr_section'}=$ENV{'request.course.sec'};
- } else {
- delete($perm{'mgr'});
- }
- }
-
+ &init_perm();
if ($command eq 'submission' && $perm{'vgr'}) {
- ($ENV{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0));
+ ($env{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0));
} elsif ($command eq 'pickStudentPage' && $perm{'vgr'}) {
&pickStudentPage($request);
} elsif ($command eq 'displayPage' && $perm{'vgr'}) {
@@ -3568,7 +8128,9 @@ sub handler {
} elsif ($command eq 'processGroup' && $perm{'vgr'}) {
&processGroup($request);
} elsif ($command eq 'gradingmenu' && $perm{'vgr'}) {
- $request->print(&gradingmenu($request));
+ $request->print(&grading_menu($request));
+ } elsif ($command eq 'submit_options' && $perm{'vgr'}) {
+ $request->print(&submit_options($request));
} elsif ($command eq 'viewgrades' && $perm{'vgr'}) {
$request->print(&viewgrades($request));
} elsif ($command eq 'handgrade' && $perm{'mgr'}) {
@@ -3577,52 +8139,59 @@ sub handler {
$request->print(&editgrades($request));
} elsif ($command eq 'verify' && $perm{'vgr'}) {
$request->print(&verifyreceipt($request));
+ } elsif ($command eq 'processclicker' && $perm{'mgr'}) {
+ $request->print(&process_clicker($request));
+ } elsif ($command eq 'processclickerfile' && $perm{'mgr'}) {
+ $request->print(&process_clicker_file($request));
+ } elsif ($command eq 'assignclickergrades' && $perm{'mgr'}) {
+ $request->print(&assign_clicker_grades($request));
} elsif ($command eq 'csvform' && $perm{'mgr'}) {
$request->print(&upcsvScores_form($request));
} elsif ($command eq 'csvupload' && $perm{'mgr'}) {
$request->print(&csvupload($request));
} elsif ($command eq 'csvuploadmap' && $perm{'mgr'} ) {
$request->print(&csvuploadmap($request));
- } elsif ($command eq 'csvuploadassign' && $perm{'mgr'}) {
- if ($ENV{'form.associate'} ne 'Reverse Association') {
- $request->print(&csvuploadassign($request));
+ } elsif ($command eq 'csvuploadoptions' && $perm{'mgr'}) {
+ if ($env{'form.associate'} ne 'Reverse Association') {
+ $request->print(&csvuploadoptions($request));
} else {
- if ( $ENV{'form.upfile_associate'} ne 'reverse' ) {
- $ENV{'form.upfile_associate'} = 'reverse';
+ if ( $env{'form.upfile_associate'} ne 'reverse' ) {
+ $env{'form.upfile_associate'} = 'reverse';
} else {
- $ENV{'form.upfile_associate'} = 'forward';
+ $env{'form.upfile_associate'} = 'forward';
}
$request->print(&csvuploadmap($request));
}
+ } elsif ($command eq 'csvuploadassign' && $perm{'mgr'} ) {
+ $request->print(&csvuploadassign($request));
} elsif ($command eq 'scantron_selectphase' && $perm{'mgr'}) {
$request->print(&scantron_selectphase($request));
+ } elsif ($command eq 'scantron_warning' && $perm{'mgr'}) {
+ $request->print(&scantron_do_warning($request));
+ } elsif ($command eq 'scantron_validate' && $perm{'mgr'}) {
+ $request->print(&scantron_validate_file($request));
} elsif ($command eq 'scantron_process' && $perm{'mgr'}) {
$request->print(&scantron_process_students($request));
+ } elsif ($command eq 'scantronupload' &&
+ (&Apache::lonnet::allowed('usc',$env{'request.role.domain'})||
+ &Apache::lonnet::allowed('usc',$env{'request.course.id'}))) {
+ $request->print(&scantron_upload_scantron_data($request));
+ } elsif ($command eq 'scantronupload_save' &&
+ (&Apache::lonnet::allowed('usc',$env{'request.role.domain'})||
+ &Apache::lonnet::allowed('usc',$env{'request.course.id'}))) {
+ $request->print(&scantron_upload_scantron_data_save($request));
+ } elsif ($command eq 'scantron_download' &&
+ &Apache::lonnet::allowed('usc',$env{'request.course.id'})) {
+ $request->print(&scantron_download_scantron_data($request));
} elsif ($command) {
- $request->print("Access Denied");
+ $request->print("Access Denied ($command)");
}
}
- &send_footer($request);
+ $request->print(&Apache::loncommon::end_page());
+ &reset_caches();
return '';
}
-sub send_header {
- my ($request)= @_;
- $request->print(&Apache::lontexconvert::header());
-# $request->print("
-#");
- $request->print(&Apache::loncommon::bodytag('Grading'));
-}
-
-sub send_footer {
- my ($request)= @_;
- $request->print('');
- $request->print(&Apache::lontexconvert::footer());
-}
-
1;
__END__;