--- loncom/homework/grades.pm 2020/11/10 17:17:54 1.779 +++ loncom/homework/grades.pm 2024/12/09 21:39:48 1.799 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.779 2020/11/10 17:17:54 raeburn Exp $ +# $Id: grades.pm,v 1.799 2024/12/09 21:39:48 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -47,10 +47,12 @@ use Apache::lonstathelpers; use Apache::lonquickgrades; use Apache::bridgetask(); use Apache::lontexconvert(); +use Apache::loncourserespicker; use String::Similarity; use HTML::Parser(); use File::MMagic; use LONCAPA; +use LONCAPA::ltiutils(); use POSIX qw(floor); @@ -65,7 +67,7 @@ my $ssi_retries = 5; my $ssi_error; my $ssi_error_resource; my $ssi_error_message; - +my $registered_cleanup; sub ssi_with_retries { my ($resource, $retries, %form) = @_; @@ -636,7 +638,7 @@ COMMONJSFUNCTIONS #--- Dumps the class list with usernames,list of sections, #--- section, ids and fullnames for each user. sub getclasslist { - my ($getsec,$filterbyaccstatus,$getgroup,$symb,$submitonly,$filterbysubmstatus) = @_; + my ($getsec,$filterbyaccstatus,$getgroup,$symb,$submitonly,$filterbysubmstatus,$filterbypbid,$possibles) = @_; my @getsec; my @getgroup; my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); @@ -664,12 +666,16 @@ sub getclasslist { # my %sections; my %fullnames; + my %passback; my ($cdom,$cnum,$partlist); if (($filterbysubmstatus) && ($submitonly ne 'all') && ($symb ne '')) { $cdom = $env{"course.$env{'request.course.id'}.domain"}; $cnum = $env{"course.$env{'request.course.id'}.num"}; my $res_error; ($partlist) = &response_type($symb,\$res_error); + } elsif ($filterbypbid) { + $cdom = $env{"course.$env{'request.course.id'}.domain"}; + $cnum = $env{"course.$env{'request.course.id'}.num"}; } foreach my $student (keys(%$classlist)) { my $end = @@ -756,6 +762,27 @@ sub getclasslist { } } } + if ($filterbypbid) { + if (ref($possibles) eq 'HASH') { + unless (exists($possibles->{$student})) { + delete($classlist->{$student}); + next; + } + } + my $udom = + $classlist->{$student}->[&Apache::loncoursedata::CL_SDOM()]; + my $uname = + $classlist->{$student}->[&Apache::loncoursedata::CL_SNAME()]; + if (($udom ne '') && ($uname ne '')) { + my %pbinfo = &Apache::lonnet::get('nohist_'.$cdom.'_'.$cnum.'_linkprot_pb',[$filterbypbid],$udom,$uname); + if (ref($pbinfo{$filterbypbid}) eq 'ARRAY') { + $passback{$student} = $pbinfo{$filterbypbid}; + } else { + delete($classlist->{$student}); + next; + } + } + } $section = ($section ne '' ? $section : 'none'); if (&canview($section)) { if (!@getsec || grep(/^\Q$section\E$/,@getsec)) { @@ -771,7 +798,7 @@ sub getclasslist { } } my @sections = sort(keys(%sections)); - return ($classlist,\@sections,\%fullnames); + return ($classlist,\@sections,\%fullnames,\%passback); } sub canmodify { @@ -1034,6 +1061,878 @@ sub verifyreceipt { return $string; } +#------------------------------------------------------------------- + +#------------------------------------------- Grade Passback Routines +# + +sub initialpassback { + my ($request,$symb) = @_; + my $cdom = $env{"course.$env{'request.course.id'}.domain"}; + my $cnum = $env{"course.$env{'request.course.id'}.num"}; + my $crstype = &Apache::loncommon::course_type(); + my %passback = &Apache::lonnet::dump('nohist_linkprot_passback',$cdom,$cnum); + my $readonly; + unless ($perm{'mgr'}) { + $readonly = 1; + } + my $formname = 'initialpassback'; + my $navmap = Apache::lonnavmaps::navmap->new(); + my $output; + if (!defined($navmap)) { + if ($crstype eq 'Community') { + $output = &mt('Unable to retrieve information about community contents'); + } else { + $output = &mt('Unable to retrieve information about course contents'); + } + return '

'.$output.'

'; + } + return &Apache::loncourserespicker::create_picker($navmap,'passback',$formname,$crstype,undef, + undef,undef,undef,undef,undef,undef, + \%passback,$readonly); +} + +sub passback_filters { + my ($request,$symb) = @_; + my $cdom = $env{"course.$env{'request.course.id'}.domain"}; + my $cnum = $env{"course.$env{'request.course.id'}.num"}; + my $crstype = &Apache::loncommon::course_type(); + my ($launcher,$appname,$setter,$linkuri,$linkprotector,$scope,$chosen); + if ($env{'form.passback'} ne '') { + $chosen = &unescape($env{'form.passback'}); + ($linkuri,$linkprotector,$scope) = split("\0",$chosen); + ($launcher,$appname,$setter) = &get_passback_launcher($cdom,$cnum,$chosen); + } + my $result; + if ($launcher ne '') { + $result = &launcher_info_box($launcher,$appname,$setter,$linkuri,$scope). + '


'.&mt('Set criteria to use to list students for possible passback of scores, then push Next [_1]', + '→'). + '

'; + } + $result .= '
'."\n". + ''."\n". + ''."\n"; + my ($submittext,$newcommand); + if ($launcher ne '') { + $submittext = &mt('Next').' →'; + $newcommand = 'passbacknames'; + $result .= &selectfield(0)."\n"; + } else { + $submittext = '← '.&mt('Previous'); + $newcommand = 'initialpassback'; + if ($env{'form.passback'}) { + $result .= ''.&mt('Invalid launcher').''."\n"; + } else { + $result .= ''.&mt('No launcher selected').''."\n"; + } + } + $result .= ''."\n". + '
'."\n". + ''."\n". + '
'."\n". + '
'."\n"; + return $result; +} + +sub names_for_passback { + my ($request,$symb) = @_; + my $cdom = $env{"course.$env{'request.course.id'}.domain"}; + my $cnum = $env{"course.$env{'request.course.id'}.num"}; + my $crstype = &Apache::loncommon::course_type(); + my ($launcher,$appname,$setter,$linkuri,$linkprotector,$scope,$chosen); + if ($env{'form.passback'} ne '') { + $chosen = &unescape($env{'form.passback'}); + ($linkuri,$linkprotector,$scope) = split("\0",$chosen); + ($launcher,$appname,$setter) = &get_passback_launcher($cdom,$cnum,$chosen); + } + my ($result,$ctr,$newcommand,$submittext); + if ($launcher ne '') { + $result = &launcher_info_box($launcher,$appname,$setter,$linkuri,$scope); + } + $ctr = 0; + my @statuses = &Apache::loncommon::get_env_multiple('form.Status'); + my $stu_status = join(':',@statuses); + $result .= '
'."\n". + ''."\n"; + if ($launcher ne '') { + $result .= ''."\n". + ''."\n"; + my ($sections,$groups,$group_display,$disabled) = §ions_and_groups(); + my $section_display = join(' ',@{$sections}); + my $status_display; + if ((grep(/^Any$/,@statuses)) || + (@statuses == 3)) { + $status_display = &mt('Any'); + } else { + $status_display = join(' '.&mt('or').' ',map { &mt($_); } @statuses); + } + $result .= '

'.&mt('Student(s) with stored passback credentials for [_1], and also satisfy:', + ''.$linkuri.''). + '

'; + my ($classlist,undef,$fullname) = &getclasslist($sections,'1',$groups,'','','',$chosen); + if (keys(%$fullname)) { + $newcommand = 'passbackscores'; + $result .= &build_section_inputs(). + &checkselect_js('passbackusers'). + '


'. + &mt("To send scores, check box(es) next to the student's name(s), then push 'Send Scores'."). + '

'. + &check_script('passbackusers', 'stuinfo')."\n". + '
'."\n". + &check_buttons()."\n". + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(); + my $loop = 0; + while ($loop < 2) { + $result .= ''.&mt('No.').''.&mt('Select').''. + ''.&nameUserString('header').' '.&mt('Section/Group').''; + $loop++; + } + $result .= &Apache::loncommon::end_data_table_header_row()."\n"; + foreach my $student (sort + { + if (lc($$fullname{$a}) ne lc($$fullname{$b})) { + return (lc($$fullname{$a}) cmp lc($$fullname{$b})); + } + return $a cmp $b; + } + (keys(%$fullname))) { + $ctr++; + my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; + my $group = $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()]; + my $udom = $classlist->{$student}->[&Apache::loncoursedata::CL_SDOM()]; + my $uname = $classlist->{$student}->[&Apache::loncoursedata::CL_SNAME()]; + if ( $perm{'vgr'} eq 'F' ) { + if ($ctr%2 ==1) { + $result.= &Apache::loncommon::start_data_table_row(); + } + $result .= ''.$ctr.' '. + ''."\n".''. + &nameUserString(undef,$$fullname{$student},$uname,$udom). + ' '.$section.($group ne '' ?'/'.$group:'').''."\n"; + + if ($ctr%2 ==0) { + $result .= &Apache::loncommon::end_data_table_row()."\n"; + } + } + } + if ($ctr%2 ==1) { + $result .= &Apache::loncommon::end_data_table_row(); + } + $result .= &Apache::loncommon::end_data_table()."\n"; + if ($ctr) { + $result .= ''."\n"; + } + } else { + $submittext = '← '.&mt('Previous'); + $newcommand = 'passback'; + $result .= ''.&mt('No students match the selection criteria').'

'; + } + } else { + $newcommand = 'initialpassback'; + $submittext = &mt('Start over'); + if ($env{'form.passback'}) { + $result .= ''.&mt('Invalid launcher').''."\n"; + } else { + $result .= ''.&mt('No launcher selected').''."\n"; + } + } + $result .= ''."\n"; + if (!$ctr) { + $result .= '
'."\n". + ''."\n". + '
'."\n"; + } + $result .= ''."\n"; + return $result; +} + +sub do_passback { + my ($request,$symb) = @_; + my $cdom = $env{"course.$env{'request.course.id'}.domain"}; + my $cnum = $env{"course.$env{'request.course.id'}.num"}; + my $crstype = &Apache::loncommon::course_type(); + my ($launcher,$appname,$setter,$linkuri,$linkprotector,$scope,$chosen); + if ($env{'form.passback'} ne '') { + $chosen = &unescape($env{'form.passback'}); + ($linkuri,$linkprotector,$scope) = split("\0",$chosen); + ($launcher,$appname,$setter) = &get_passback_launcher($cdom,$cnum,$chosen); + } + if ($launcher ne '') { + $request->print(&launcher_info_box($launcher,$appname,$setter,$linkuri,$scope)); + } + my $error; + if ($perm{'mgr'}) { + if ($launcher ne '') { + my @poss_students = &Apache::loncommon::get_env_multiple('form.stuinfo'); + if (@poss_students) { + my %possibles; + foreach my $item (@poss_students) { + my ($stuname,$studom) = split(/:/,$item,3); + $possibles{$stuname.':'.$studom} = 1; + } + my ($sections,$groups,$group_display,$disabled) = §ions_and_groups(); + my ($classlist,undef,$fullname,$pbinfo) = + &getclasslist($sections,'1',$groups,'','','',$chosen,\%possibles); + if ((ref($classlist) eq 'HASH') && (ref($pbinfo) eq 'HASH')) { + my %passback = %{$pbinfo}; + my (%tosend,%remotenotok,%scorenotok,%zeroposs,%nopbinfo); + foreach my $possible (keys(%possibles)) { + if ((exists($classlist->{$possible})) && + (exists($passback{$possible})) && (ref($passback{$possible}) eq 'ARRAY')) { + $tosend{$possible} = 1; + } + } + if (keys(%tosend)) { + my ($lti_in_use,$crsdef); + my ($ltinum,$ltitype) = ($linkprotector =~ /^(\d+)(c|d)$/); + if ($ltitype eq 'c') { + my %crslti = &Apache::lonnet::get_course_lti($cnum,$cdom,'provider'); + $lti_in_use = $crslti{$ltinum}; + $crsdef = 1; + } else { + my %domlti = &Apache::lonnet::get_domain_lti($cdom,'linkprot'); + $lti_in_use = $domlti{$ltinum}; + } + if (ref($lti_in_use) eq 'HASH') { + my $msgformat = $lti_in_use->{'passbackformat'}; + my $keynum = $lti_in_use->{'cipher'}; + my $scoretype = 'decimal'; + if ($lti_in_use->{'scoreformat'} =~ /^(decimal|ratio|percentage)$/) { + $scoretype = $1; + } + my $pbsymb = &Apache::loncommon::symb_from_tinyurl($linkuri,$cnum,$cdom); + my $pbmap; + if ($pbsymb =~ /\.(page|sequence)$/) { + $pbmap = &Apache::lonnet::deversion((&Apache::lonnet::decode_symb($pbsymb))[2]); + } else { + $pbmap = &Apache::lonnet::deversion((&Apache::lonnet::decode_symb($pbsymb))[0]); + } + $pbmap = &Apache::lonnet::clutter($pbmap); + my $pbscope; + if ($scope eq 'res') { + $pbscope = 'resource'; + } elsif ($scope eq 'map') { + $pbscope = 'nonrec'; + } elsif ($scope eq 'rec') { + $pbscope = 'map'; + } + my %pb = &common_passback_info(); + my $numstudents = scalar(keys(%tosend)); + my %prog_state = &Apache::lonhtmlcommon::Create_PrgWin($request,$numstudents); + my $outcome = &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(); + my $loop = 0; + while ($loop < 2) { + $outcome .= ''.&mt('No.').''. + ''.&nameUserString('header').' '.&mt('Section/Group').''. + ''.&mt('Score').''; + $loop++; + } + $outcome .= &Apache::loncommon::end_data_table_header_row()."\n"; + my $ctr=0; + foreach my $student (sort + { + if (lc($$fullname{$a}) ne lc($$fullname{$b})) { + return (lc($$fullname{$a}) cmp lc($$fullname{$b})); + } + return $a cmp $b; + } (keys(%$fullname))) { + next unless ($tosend{$student}); + my ($uname,$udom) = split(/:/,$student); + &Apache::lonhtmlcommon::Increment_PrgWin($request,\%prog_state,'last student'); + my ($uname,$udom) = split(/:/,$student); + my $uhome = &Apache::lonnet::homeserver($uname,$udom), + my $id = $passback{$student}[0], + my $url = $passback{$student}[1], + my ($total,$possible,$usec); + if (ref($classlist->{$student}) eq 'ARRAY') { + $usec = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION]; + } + if ($pbscope eq 'resource') { + $total = 0; + $possible = 0; + my $navmap = Apache::lonnavmaps::navmap->new($uname,$udom); + if (ref($navmap)) { + my $res = $navmap->getBySymb($pbsymb); + if (ref($res)) { + my $partlist = $res->parts(); + if (ref($partlist) eq 'ARRAY') { + my %record = &Apache::lonnet::restore($pbsymb,$env{'request.course.id'},$udom,$uname); + foreach my $part (@{$partlist}) { + next if ($record{"resource.$part.solved"} =~/^excused/); + my $weight = &Apache::lonnet::EXT("resource.$part.weight",$pbsymb,$udom,$uname,$usec); + $possible += $weight; + if (($record{'version'}) && (exists($record{"resource.$part.awarded"}))) { + my $awarded = $record{"resource.$part.awarded"}; + if ($awarded) { + $total += $weight * $awarded; + } + } + } + } + } + } + } elsif (($pbscope eq 'map') || ($pbscope eq 'nonrec')) { + ($total,$possible) = + &Apache::lonhomework::get_lti_score($uname,$udom,$pbmap,$pbscope); + } + if (($id ne '') && ($url ne '') && ($possible)) { + my ($sent,$score,$code,$result) = + &LONCAPA::ltiutils::send_grade($cdom,$cnum,$crsdef,$pb{'type'},$ltinum,$keynum,$id, + $url,$scoretype,$pb{'sigmethod'},$msgformat,$total,$possible); + my $no_passback; + if ($sent) { + if ($code == 200) { + delete($tosend{$student}); + my $namespace = $cdom.'_'.$cnum.'_lp_passback'; + my $store = { + 'score' => $score, + 'ip' => $pb{'ip'}, + 'host' => $pb{'lonhost'}, + 'protector' => $linkprotector, + 'deeplink' => $linkuri, + 'scope' => $scope, + 'url' => $url, + 'id' => $id, + 'clientip' => $pb{'clientip'}, + 'whodoneit' => $env{'user.name'}.':'.$env{'user.domain'}, + }; + my $value=''; + foreach my $key (keys(%{$store})) { + $value.=&escape($key).'='.&Apache::lonnet::freeze_escape($store->{$key}).'&'; + } + $value=~s/\&$//; + &Apache::lonnet::courselog(&escape($linkuri).':'.$uname.':'.$udom.':EXPORT:'.$value); + &Apache::lonnet::cstore({'score' => $score},$chosen,$namespace,$udom,$uname,'',$pb{'ip'},1); + $ctr++; + if ($ctr%2 ==1) { + $outcome .= &Apache::loncommon::start_data_table_row(); + } + my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; + my $group = $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()]; + $outcome .= ''.$ctr.' '. + ''.&nameUserString(undef,$$fullname{$student},$uname,$udom). + ' '.$section.($group ne '' ?'/'.$group:'').''. + ''.$score.''."\n"; + if ($ctr%2 ==0) { + $outcome .= &Apache::loncommon::end_data_table_row()."\n"; + } + } else { + $remotenotok{$student} = 1; + $no_passback = "Passback response for ".$linkprotector." was $code ($result)"; + &Apache::lonnet::logthis($no_passback." for $uname:$udom in ${cdom}_${cnum}"); + } + } else { + $scorenotok{$student} = 1; + $no_passback = "Passback of grades not sent for ".$linkprotector; + &Apache::lonnet::logthis($no_passback." for $uname:$udom in ${cdom}_${cnum}"); + } + if ($no_passback) { + &Apache::lonnet::log($udom,$uname,$uhome,$no_passback." score: $score; total: $total; possible: $possible"); + my $ltigrade = { + 'ltinum' => $ltinum, + 'lti' => $lti_in_use, + 'crsdef' => $crsdef, + 'cid' => $cdom.'_'.$cnum, + 'uname' => $uname, + 'udom' => $udom, + 'uhome' => $uhome, + 'pbid' => $id, + 'pburl' => $url, + 'pbtype' => $pb{'type'}, + 'pbscope' => $pbscope, + 'pbmap' => $pbmap, + 'pbsymb' => $pbsymb, + 'format' => $scoretype, + 'scope' => $scope, + 'clientip' => $pb{'clientip'}, + 'linkprot' => $linkprotector.':'.$linkuri, + 'total' => $total, + 'possible' => $possible, + 'score' => $score, + }; + &Apache::lonnet::put('linkprot_passback_pending',$ltigrade,$cdom,$cnum); + } + } else { + if (($id ne '') && ($url ne '')) { + $zeroposs{$student} = 1; + } else { + $nopbinfo{$student} = 1; + } + } + } + &Apache::lonhtmlcommon::Close_PrgWin($request,\%prog_state); + if ($ctr%2 ==1) { + $outcome .= &Apache::loncommon::end_data_table_row(); + } + $outcome .= &Apache::loncommon::end_data_table(); + if ($ctr) { + $request->print('


'.&mt('Scores sent to launcher CMS').'

'. + '

'.$outcome.'

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

'.&mt('No scores sent to launcher CMS').'

'); + } + if (keys(%tosend)) { + $request->print('

'.&mt('No scores sent for following')); + my ($zeros,$nopbcreds,$noconfirm,$noscore); + foreach my $student (sort + { + if (lc($$fullname{$a}) ne lc($$fullname{$b})) { + return (lc($$fullname{$a}) cmp lc($$fullname{$b})); + } + return $a cmp $b; + } (keys(%$fullname))) { + next unless ($tosend{$student}); + my ($uname,$udom) = split(/:/,$student); + my $line = '

  • '.&nameUserString(undef,$$fullname{$student},$uname,$udom).'
  • '."\n"; + if ($zeroposs{$student}) { + $zeros .= $line; + } elsif ($nopbinfo{$student}) { + $nopbcreds .= $line; + } elsif ($remotenotok{$student}) { + $noconfirm .= $line; + } elsif ($scorenotok{$student}) { + $noscore .= $line; + } + } + if ($zeros) { + $request->print('
    '.&mt('Total points possible was 0').':'. + '
    '); + } + if ($nopbcreds) { + $request->print('
    '.&mt('Missing unique identifier and/or passback location').':'. + '
    '); + } + if ($noconfirm) { + $request->print('
    '.&mt('Score receipt not confirmed by receiving CMS').':'. + '
    '); + } + if ($noscore) { + $request->print('
    '.&mt('Score computation or transmission failed').':'. + '
    '); + } + $request->print('

    '); + } + } else { + $error = &mt('Settings for deep-link launch target unavailable, so no scores were sent'); + } + } else { + $error = &mt('No available students for whom scores can be sent.'); + } + } else { + $error = &mt('Classlist could not be retrieved so no scores were sent.'); + } + } else { + $error = &mt('No students selected to receive scores so none were sent.'); + } + } else { + if ($env{'form.passback'}) { + $error = &mt('Deep-link launch target was invalid so no scores were sent.'); + } else { + $error = &mt('Deep-link launch target was missing so no scores were sent.'); + } + } + } else { + $error = &mt('You do not have permission to manage grades, so no scores were sent'); + } + if ($error) { + $request->print('

    '.$error.'

    '); + } + return; +} + +sub get_passback_launcher { + my ($cdom,$cnum,$chosen) = @_; + my ($linkuri,$linkprotector,$scope) = split("\0",$chosen); + my ($ltinum,$ltitype) = ($linkprotector =~ /^(\d+)(c|d)$/); + my ($appname,$setter); + if ($ltitype eq 'c') { + my %lti = &Apache::lonnet::get_course_lti($cnum,$cdom,'provider'); + if (ref($lti{$ltinum}) eq 'HASH') { + $appname = $lti{$ltinum}{'name'}; + if ($appname) { + $setter = ' (defined in course)'; + } + } + } elsif ($ltitype eq 'd') { + my %lti = &Apache::lonnet::get_domain_lti($cdom,'linkprot'); + if (ref($lti{$ltinum}) eq 'HASH') { + $appname = $lti{$ltinum}{'name'}; + if ($appname) { + $setter = ' (defined in domain)'; + } + } + } + if ($linkuri =~ m{^\Q/tiny/$cdom/\E(\w+)$}) { + my $key = $1; + my $tinyurl; + my ($result,$cached)=&Apache::lonnet::is_cached_new('tiny',$cdom."\0".$key); + if (defined($cached)) { + $tinyurl = $result; + } else { + my $configuname = &Apache::lonnet::get_domainconfiguser($cdom); + my %currtiny = &Apache::lonnet::get('tiny',[$key],$cdom,$configuname); + if ($currtiny{$key} ne '') { + $tinyurl = $currtiny{$key}; + &Apache::lonnet::do_cache_new('tiny',$cdom."\0".$key,$currtiny{$key},600); + } + } + if ($tinyurl) { + my ($crsnum,$launchsymb) = split(/\&/,$tinyurl); + if ($crsnum eq $cnum) { + my %passback = &Apache::lonnet::get('nohist_linkprot_passback',[$launchsymb],$cdom,$cnum); + if (ref($passback{$launchsymb}) eq 'HASH') { + if (exists($passback{$launchsymb}{$chosen})) { + return ($launchsymb,$appname,$setter); + } + } + } + } + } + return (); +} + +sub sections_and_groups { + my (@sections,@groups,$group_display); + @groups = &Apache::loncommon::get_env_multiple('form.group'); + if (grep(/^all$/,@groups)) { + @groups = ('all'); + $group_display = 'all'; + } elsif (grep(/^none$/,@groups)) { + @groups = ('none'); + $group_display = 'none'; + } elsif (@groups > 0) { + $group_display = join(', ',@groups); + } + if ($env{'request.course.sec'} ne '') { + @sections = ($env{'request.course.sec'}); + } else { + @sections = &Apache::loncommon::get_env_multiple('form.section'); + } + my $disabled = ' disabled="disabled"'; + if ($perm{'mgr'}) { + if (grep(/^all$/,@sections)) { + undef($disabled); + } else { + foreach my $sec (@sections) { + if (&canmodify($sec)) { + undef($disabled); + last; + } + } + } + } + if (grep(/^all$/,@sections)) { + @sections = ('all'); + } + return(\@sections,\@groups,$group_display,$disabled); +} + +sub launcher_info_box { + my ($launcher,$appname,$setter,$linkuri,$scope) = @_; + my $shownscope; + if ($scope eq 'res') { + $shownscope = &mt('Resource'); + } elsif ($scope eq 'map') { + $shownscope = &mt('Folder'); + } elsif ($scope eq 'rec') { + $shownscope = &mt('Folder + sub-folders'); + } + return '

    '. + &Apache::lonhtmlcommon::start_pick_box(). + &Apache::lonhtmlcommon::row_title(&mt('Launch Item Title')). + &Apache::lonnet::gettitle($launcher). + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('Deep-link')). + $linkuri. + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('Launcher')). + $appname.' '.$setter. + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('Score Type')). + $shownscope. + &Apache::lonhtmlcommon::row_closure(1). + &Apache::lonhtmlcommon::end_pick_box().'

    '."\n"; +} + +sub passbacks_for_symb { + my ($cdom,$cnum,$symb) = @_; + my %passback = &Apache::lonnet::dump('nohist_linkprot_passback',$cdom,$cnum); + my %needpb; + if (keys(%passback)) { + my $checkpb = 1; + if (exists($passback{$symb})) { + if (keys(%passback) == 1) { + undef($checkpb); + } + if (ref($passback{$symb}) eq 'HASH') { + foreach my $launcher (keys(%{$passback{$symb}})) { + $needpb{$launcher} = 1; + } + } + } + if ($checkpb) { + my ($map,$id,$url) = &Apache::lonnet::decode_symb($symb); + my $navmap = Apache::lonnavmaps::navmap->new(); + if (ref($navmap)) { + my $mapres = $navmap->getResourceByUrl($map); + if (ref($mapres)) { + my $mapsymb = $mapres->symb(); + if (exists($passback{$mapsymb})) { + if (keys(%passback) == 1) { + undef($checkpb); + } + if (ref($passback{$mapsymb}) eq 'HASH') { + foreach my $launcher (keys(%{$passback{$mapsymb}})) { + $needpb{$launcher} = 1; + } + } + } + my %posspb; + if ($checkpb) { + my @recurseup = $navmap->recurseup_maps($map,1); + if (@recurseup) { + map { $posspb{$_} = 1; } @recurseup; + } + } + foreach my $key (keys(%passback)) { + if (exists($posspb{$key})) { + if (ref($passback{$key}) eq 'HASH') { + foreach my $launcher (keys(%{$passback{$key}})) { + my ($linkuri,$linkprotector,$scope) = split("\0",$launcher); + next unless ($scope eq 'rec'); + $needpb{$launcher} = 1; + } + } + } + } + } + } + } + } + return %needpb; +} + +sub process_passbacks { + my ($context,$symbs,$cdom,$cnum,$udom,$uname,$weights,$awardeds,$excuseds,$needpb, + $skip_passback,$pbsave,$pbids) = @_; + if ((ref($needpb) eq 'HASH') && (ref($skip_passback) eq 'HASH') && (ref($pbsave) eq 'HASH')) { + my (%weight,%awarded,%excused); + if ((ref($symbs) eq 'ARRAY') && (ref($weights) eq 'HASH') && (ref($awardeds) eq 'HASH') && + (ref($excuseds) eq 'HASH')) { + %weight = %{$weights}; + %awarded = %{$awardeds}; + %excused = %{$excuseds}; + } + my $uhome = &Apache::lonnet::homeserver($uname,$udom); + my @launchers = keys(%{$needpb}); + my %pbinfo; + if (ref($pbids) eq 'HASH') { + %pbinfo = %{$pbids}; + } else { + %pbinfo = &Apache::lonnet::get('nohist_'.$cdom.'_'.$cnum.'_linkprot_pb',\@launchers,$udom,$uname); + } + my %pbc = &common_passback_info(); + foreach my $launcher (@launchers) { + if (ref($pbinfo{$launcher}) eq 'ARRAY') { + my $pbid = $pbinfo{$launcher}[0]; + my $pburl = $pbinfo{$launcher}[1]; + my (%total_by_symb,%possible_by_symb); + if (($pbid ne '') && ($pburl ne '')) { + next if ($skip_passback->{$launcher}); + my %pb = %pbc; + if ((exists($pbsave->{$launcher})) && + (ref($pbsave->{$launcher}) eq 'HASH')) { + foreach my $item ('lti_in_use','crsdef','ltinum','keynum','scoretype','msgformat', + 'symb','map','pbscope','linkuri','linkprotector','scope') { + $pb{$item} = $pbsave->{$launcher}{$item}; + } + } else { + my $ltitype; + ($pb{'linkuri'},$pb{'linkprotector'},$pb{'scope'}) = split("\0",$launcher); + ($pb{'ltinum'},$ltitype) = ($pb{'linkprotector'} =~ /^(\d+)(c|d)$/); + if ($ltitype eq 'c') { + my %crslti = &Apache::lonnet::get_course_lti($cnum,$cdom,'provider'); + $pb{'lti_in_use'} = $crslti{$pb{'ltinum'}}; + $pb{'crsdef'} = 1; + } else { + my %domlti = &Apache::lonnet::get_domain_lti($cdom,'linkprot'); + $pb{'lti_in_use'} = $domlti{$pb{'ltinum'}}; + } + if (ref($pb{'lti_in_use'}) eq 'HASH') { + $pb{'msgformat'} = $pb{'lti_in_use'}->{'passbackformat'}; + $pb{'keynum'} = $pb{'lti_in_use'}->{'cipher'}; + $pb{'scoretype'} = 'decimal'; + if ($pb{'lti_in_use'}->{'scoreformat'} =~ /^(decimal|ratio|percentage)$/) { + $pb{'scoretype'} = $1; + } + $pb{'symb'} = &Apache::loncommon::symb_from_tinyurl($pb{'linkuri'},$cnum,$cdom); + if ($pb{'symb'} =~ /\.(page|sequence)$/) { + $pb{'map'} = &Apache::lonnet::deversion((&Apache::lonnet::decode_symb($pb{'symb'}))[2]); + } else { + $pb{'map'} = &Apache::lonnet::deversion((&Apache::lonnet::decode_symb($pb{'symb'}))[0]); + } + $pb{'map'} = &Apache::lonnet::clutter($pb{'map'}); + if ($pb{'scope'} eq 'res') { + $pb{'pbscope'} = 'resource'; + } elsif ($pb{'scope'} eq 'map') { + $pb{'pbscope'} = 'nonrec'; + } elsif ($pb{'scope'} eq 'rec') { + $pb{'pbscope'} = 'map'; + } + foreach my $item ('lti_in_use','crsdef','ltinum','keynum','scoretype','msgformat', + 'symb','map','pbscope','linkuri','linkprotector','scope') { + $pbsave->{$launcher}{$item} = $pb{$item}; + } + } else { + $skip_passback->{$launcher} = 1; + } + } + if (ref($symbs) eq 'ARRAY') { + foreach my $symb (@{$symbs}) { + if ((ref($weight{$symb}) eq 'HASH') && (ref($awarded{$symb}) eq 'HASH') && + (ref($excused{$symb}) eq 'HASH')) { + foreach my $part (keys(%{$weight{$symb}})) { + if ($excused{$symb}{$part}) { + next; + } + my $partweight = $weight{$symb}{$part} eq '' ? 1 : + $weight{$symb}{$part}; + if ($awarded{$symb}{$part}) { + $total_by_symb{$symb} += $partweight * $awarded{$symb}{$part}; + } + $possible_by_symb{$symb} += $partweight; + } + } + } + } + if ($context eq 'updatebypage') { + my $ltigrade = { + 'ltinum' => $pb{'ltinum'}, + 'lti' => $pb{'lti_in_use'}, + 'crsdef' => $pb{'crsdef'}, + 'cid' => $cdom.'_'.$cnum, + 'uname' => $uname, + 'udom' => $udom, + 'uhome' => $uhome, + 'pbid' => $pbid, + 'pburl' => $pburl, + 'pbtype' => $pb{'type'}, + 'pbscope' => $pb{'pbscope'}, + 'pbmap' => $pb{'map'}, + 'pbsymb' => $pb{'symb'}, + 'format' => $pb{'scoretype'}, + 'scope' => $pb{'scope'}, + 'clientip' => $pb{'clientip'}, + 'linkprot' => $pb{'linkprotector'}.':'.$pb{'linkuri'}, + 'total_s' => \%total_by_symb, + 'possible_s' => \%possible_by_symb, + }; + push(@Apache::lonhomework::ltipassback,$ltigrade); + next; + } + my ($total,$possible); + if ($pb{'pbscope'} eq 'resource') { + $total = $total_by_symb{$pb{'symb'}}; + $possible = $possible_by_symb{$pb{'symb'}}; + } elsif (($pb{'pbscope'} eq 'map') || ($pb{'pbscope'} eq 'nonrec')) { + ($total,$possible) = + &Apache::lonhomework::get_lti_score($uname,$udom,$pb{'map'},$pb{'pbscope'}, + \%total_by_symb,\%possible_by_symb); + } + if (!$possible) { + $total = 0; + $possible = 1; + } + my ($sent,$score,$code,$result) = + &LONCAPA::ltiutils::send_grade($cdom,$cnum,$pb{'crsdef'},$pb{'type'},$pb{'ltinum'}, + $pb{'keynum'},$pbid,$pburl,$pb{'scoretype'},$pb{'sigmethod'}, + $pb{'msgformat'},$total,$possible); + my $no_passback; + if ($sent) { + if ($code == 200) { + my $namespace = $cdom.'_'.$cnum.'_lp_passback'; + my $store = { + 'score' => $score, + 'ip' => $pb{'ip'}, + 'host' => $pb{'lonhost'}, + 'protector' => $pb{'linkprotector'}, + 'deeplink' => $pb{'linkuri'}, + 'scope' => $pb{'scope'}, + 'url' => $pburl, + 'id' => $pbid, + 'clientip' => $pb{'clientip'}, + 'whodoneit' => $env{'user.name'}.':'.$env{'user.domain'}, + }; + my $value=''; + foreach my $key (keys(%{$store})) { + $value.=&escape($key).'='.&Apache::lonnet::freeze_escape($store->{$key}).'&'; + } + $value=~s/\&$//; + &Apache::lonnet::courselog(&escape($pb{'linkuri'}).':'.$uname.':'.$udom.':EXPORT:'.$value); + &Apache::lonnet::cstore({'score' => $score},$launcher,$namespace,$udom,$uname,'',$pb{'ip'},1); + } else { + $no_passback = 1; + } + } else { + $no_passback = 1; + } + if ($no_passback) { + &Apache::lonnet::log($udom,$uname,$uhome,$no_passback." score: $score; total: $total; possible: $possible"); + my $ltigrade = { + 'ltinum' => $pb{'ltinum'}, + 'lti' => $pb{'lti_in_use'}, + 'crsdef' => $pb{'crsdef'}, + 'cid' => $cdom.'_'.$cnum, + 'uname' => $uname, + 'udom' => $udom, + 'uhome' => $uhome, + 'pbid' => $pbid, + 'pburl' => $pburl, + 'pbtype' => $pb{'type'}, + 'pbscope' => $pb{'pbscope'}, + 'pbmap' => $pb{'map'}, + 'pbsymb' => $pb{'symb'}, + 'format' => $pb{'scoretype'}, + 'scope' => $pb{'scope'}, + 'clientip' => $pb{'clientip'}, + 'linkprot' => $pb{'linkprotector'}.':'.$pb{'linkuri'}, + 'total' => $total, + 'possible' => $possible, + 'score' => $score, + }; + &Apache::lonnet::put('linkprot_passback_pending',$ltigrade,$cdom,$cnum); + } + } + } + } + } + return; +} + +sub common_passback_info { + my %pbc = ( + sigmethod => 'HMAC-SHA1', + type => 'linkprot', + clientip => &Apache::lonnet::get_requestor_ip(), + lonhost => $Apache::lonnet::perlvar{'lonHostID'}, + ip => &Apache::lonnet::get_host_ip($Apache::lonnet::perlvar{'lonHostID'}), + ); + return %pbc; +} + #--- This is called by a number of programs. #--- Called from the Grading Menu - View/Grade an individual student #--- Also called directly when one clicks on the subm button @@ -1065,34 +1964,8 @@ sub listStudents { } } - my %js_lt = &Apache::lonlocal::texthash ( - 'multiple' => 'Please select a student or group of students before clicking on the Next button.', - 'single' => 'Please select the student before clicking on the Next button.', - ); - &js_escape(\%js_lt); + $request->print(&checkselect_js()); $request->print(&Apache::lonhtmlcommon::scripttag(< 1) { - for (var i=0; i' .'' .&Apache::lonhtmlcommon::row_closure(); - $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Grading Increments')) + $gradeTable .= + &Apache::lonhtmlcommon::row_title(&mt('Grading Increments')) .''; + } $gradeTable .= &build_section_inputs(). ''."\n". ''."\n". ''."\n"; if (exists($env{'form.Status'})) { - $gradeTable .= ''."\n"; + $gradeTable .= ''."\n"; } else { $gradeTable .= &Apache::lonhtmlcommon::row_closure() .&Apache::lonhtmlcommon::row_title(&mt('Student Status')) @@ -1353,7 +2253,55 @@ LISTJAVASCRIPT return ''; } -#---- Called from the listStudents routine +#---- Called from the listStudents and the names_for_passback routines. + +sub checkselect_js { + my ($formname) = @_; + if ($formname eq '') { + $formname = 'gradesub'; + } + my %js_lt; + if ($formname eq 'passbackusers') { + %js_lt = &Apache::lonlocal::texthash ( + 'multiple' => 'Please select a student or group of students before pushing the Save Scores button.', + 'single' => 'Please select the student before pushing the Save Scores button.', + ); + } else { + %js_lt = &Apache::lonlocal::texthash ( + 'multiple' => 'Please select a student or group of students before clicking on the Next button.', + 'single' => 'Please select the student before clicking on the Next button.', + ); + } + &js_escape(\%js_lt); + return &Apache::lonhtmlcommon::scripttag(< 1) { + for (var i=0; i'; - } elsif ($is_tool) { - $lastsubonly = - '
    ' - .''.&mt('Date Grade Passed Back:').' '.$$timestamp."
    \n"; - } else { - $lastsubonly = - '
    ' - .''.&mt('Date Submitted:').' '.$$timestamp."\n"; - - my %seenparts; - my @part_response_id = &flatten_responseType($responseType); - foreach my $part (@part_response_id) { - my ($partid,$respid) = @{ $part }; - my $display_part=&get_display_part($partid,$symb); - if ($env{"form.$uname:$udom:$partid:submitted_by"}) { - if (exists($seenparts{$partid})) { next; } - $seenparts{$partid}=1; - $request->print( - ''.&mt('Part: [_1]',$display_part).''. - ' '.&mt('Collaborative submission by: [_1]', - ''. - $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.''). - '
    '); - next; - } - my $responsetype = $responseType->{$partid}->{$respid}; - if (!exists($record{"resource.$partid.$respid.submission"})) { - $lastsubonly.="\n".'
    '. - ''.&mt('Part: [_1]',$display_part).''. - ' '. - '('.&mt('Response ID: [_1]',$respid).')'. - '   '. - ''.&mt('Nothing submitted - no attempts.').'

    '; - next; - } - foreach my $submission (@$string) { - my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); - if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; } - my ($ressub,$hide,$draft,$subval) = split(/:/,$submission,4); - # Similarity check - my $similar=''; - my ($type,$trial,$rndseed); - if ($hide eq 'rand') { - $type = 'randomizetry'; - $trial = $record{"resource.$partid.tries"}; - $rndseed = $record{"resource.$partid.rndseed"}; - } - if ($env{'form.checkPlag'}) { - my ($oname,$odom,$ocrsid,$oessay,$osim)= - &most_similar($uname,$udom,$symb,$subval); - if ($osim) { - $osim=int($osim*100.0); - if ($hide eq 'anon') { - $similar='
    '.&mt("Essay was found to be similar to another essay submitted for this assignment.").'
    '. - &mt('As the current submission is for an anonymous survey, no other details are available.').'

    '; - } else { - $similar='
    '; - if ($essayurl eq 'lib/templates/simpleproblem.problem') { - $similar .= '

    '. - &mt('Essay is [_1]% similar to an essay by [_2]', - $osim, - &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')'). - '

    '; - } else { - my %old_course_desc; - if ($ocrsid ne '') { - if (ref($coursedesc_by_cid{$ocrsid}) eq 'HASH') { - %old_course_desc = %{$coursedesc_by_cid{$ocrsid}}; - } else { - my $args; - if ($ocrsid ne $env{'request.course.id'}) { - $args = {'one_time' => 1}; - } - %old_course_desc = - &Apache::lonnet::coursedescription($ocrsid,$args); - $coursedesc_by_cid{$ocrsid} = \%old_course_desc; - } - $similar .= - '

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

    '; - } else { - $similar .= - '

    '. - &mt('Essay is [_1]% similar to an essay by [_2] in an unknown course', - $osim, - &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')'). - '

    '; - } - } - $similar .= '
    '. - &keywords_highlight($oessay). - '

    '; - } - } - } - my $order=&get_order($partid,$respid,$symb,$uname,$udom, - undef,$type,$trial,$rndseed); - if (($env{'form.lastSub'} eq 'lastonly') || - ($env{'form.lastSub'} eq 'datesub') || - ($env{'form.lastSub'} =~ /^(last|all)$/)) { - my $display_part=&get_display_part($partid,$symb); - $lastsubonly.='
    '. - ''.&mt('Part: [_1]',$display_part).''. - ' '. - '('.&mt('Response ID: [_1]',$respid).')'. - '   '; - my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record); - if (@$files) { - if ($hide eq 'anon') { - $lastsubonly.='
    '.&mt('[quant,_1,file] uploaded to this anonymous survey',scalar(@{$files})); - } else { - $lastsubonly.='

    '.''.&mt('Submitted Files:').'' - .'
    '; - if(@$files == 1) { - $lastsubonly .= &mt('Like all files provided by users, this file may contain viruses!'); - } else { - $lastsubonly .= &mt('Like all files provided by users, these files may contain viruses!'); - } - $lastsubonly .= ''; - foreach my $file (@$files) { - &Apache::lonnet::allowuploaded('/adm/grades',$file); - $lastsubonly.='
    '.$file.''; - } - } - $lastsubonly.='
    '; - } - if ($hide eq 'anon') { - $lastsubonly.='
    '.&mt('Anonymous Survey').''; - } else { - $lastsubonly.='
    '.&mt('Submitted Answer:').' '; - if ($draft) { - $lastsubonly.= ' '.&mt('Draft Copy').''; - } - $subval = - &cleanRecord($subval,$responsetype,$symb,$partid, - $respid,\%record,$order,undef,$uname,$udom,$type,$trial,$rndseed); - if ($responsetype eq 'essay') { - $subval =~ s{\n}{
    }g; - } - $lastsubonly.=$subval."\n"; - } - if ($similar) {$lastsubonly.="

    $similar\n";} - $lastsubonly.='
    '; - } - } - } - $lastsubonly.='
    '."\n"; # End: LC_grade_submissions_body - } + my ($lastsubonly,$partinfo) = + &show_last_submission($uname,$udom,$symb,$essayurl,$responseType,$env{'form.lastSub'}, + $is_tool,$fullname,\%record,\%coursedesc_by_cid); + $request->print($partinfo); $request->print($lastsubonly); + if ($env{'form.lastSub'} eq 'datesub') { my ($parts,$handgrade,$responseType) = &response_type($symb,\$res_error); $request->print(&displaySubByDates($symb,\%record,$parts,$responseType,$checkIcon,$uname,$udom)); @@ -2817,6 +3608,186 @@ sub submission { return ''; } +sub show_last_submission { + my ($uname,$udom,$symb,$essayurl,$responseType,$viewtype,$is_tool,$fullname, + $record,$coursedesc_by_cid) = @_; + my ($string,$timestamp,$lastgradetime,$lastsubmittime) = + &get_last_submission($record,$is_tool); + + my ($lastsubonly,$partinfo); + if ($timestamp eq '') { + $lastsubonly.='
    '.$string->[0].'
    '; + } elsif ($is_tool) { + $lastsubonly = + '
    ' + .''.&mt('Date Grade Passed Back:').' '.$timestamp."
    \n"; + } else { + my ($shownsubmdate,$showngradedate); + if ($lastsubmittime && $lastgradetime) { + $shownsubmdate = &Apache::lonlocal::locallocaltime($lastsubmittime); + if ($lastgradetime > $lastsubmittime) { + $showngradedate = &Apache::lonlocal::locallocaltime($lastgradetime); + } + } else { + $shownsubmdate = $timestamp; + } + $lastsubonly = + '
    ' + .''.&mt('Date Submitted:').' '.$shownsubmdate."\n"; + if ($showngradedate) { + $lastsubonly .= '
    '.&mt('Date Graded:').' '.$showngradedate."\n"; + } + + my %seenparts; + my @part_response_id = &flatten_responseType($responseType); + foreach my $part (@part_response_id) { + my ($partid,$respid) = @{ $part }; + my $display_part=&get_display_part($partid,$symb); + if ($env{"form.$uname:$udom:$partid:submitted_by"}) { + if (exists($seenparts{$partid})) { next; } + $seenparts{$partid}=1; + $partinfo .= + ''.&mt('Part: [_1]',$display_part).''. + ' '.&mt('Collaborative submission by: [_1]', + ''. + $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.''). + '
    '; + next; + } + my $responsetype = $responseType->{$partid}->{$respid}; + if (!exists($record->{"resource.$partid.$respid.submission"})) { + $lastsubonly.="\n".'
    '. + ''.&mt('Part: [_1]',$display_part).''. + ' '. + '('.&mt('Response ID: [_1]',$respid).')'. + '   '. + ''.&mt('Nothing submitted - no attempts.').'

    '; + next; + } + foreach my $submission (@$string) { + my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); + if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; } + my ($ressub,$hide,$draft,$subval) = split(/:/,$submission,4); + # Similarity check + my $similar=''; + my ($type,$trial,$rndseed); + if ($hide eq 'rand') { + $type = 'randomizetry'; + $trial = $record->{"resource.$partid.tries"}; + $rndseed = $record->{"resource.$partid.rndseed"}; + } + if ($env{'form.checkPlag'}) { + my ($oname,$odom,$ocrsid,$oessay,$osim)= + &most_similar($uname,$udom,$symb,$subval); + if ($osim) { + $osim=int($osim*100.0); + if ($hide eq 'anon') { + $similar='
    '.&mt("Essay was found to be similar to another essay submitted for this assignment.").'
    '. + &mt('As the current submission is for an anonymous survey, no other details are available.').'

    '; + } else { + $similar='
    '; + if ($essayurl eq 'lib/templates/simpleproblem.problem') { + $similar .= '

    '. + &mt('Essay is [_1]% similar to an essay by [_2]', + $osim, + &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')'). + '

    '; + } else { + my %old_course_desc; + if ($ocrsid ne '') { + if (ref($coursedesc_by_cid->{$ocrsid}) eq 'HASH') { + %old_course_desc = %{$coursedesc_by_cid->{$ocrsid}}; + } else { + my $args; + if ($ocrsid ne $env{'request.course.id'}) { + $args = {'one_time' => 1}; + } + %old_course_desc = + &Apache::lonnet::coursedescription($ocrsid,$args); + $coursedesc_by_cid->{$ocrsid} = \%old_course_desc; + } + $similar .= + '

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

    '; + } else { + $similar .= + '

    '. + &mt('Essay is [_1]% similar to an essay by [_2] in an unknown course', + $osim, + &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')'). + '

    '; + } + } + $similar .= '
    '. + &keywords_highlight($oessay). + '

    '; + } + } + } + my $order=&get_order($partid,$respid,$symb,$uname,$udom, + undef,$type,$trial,$rndseed); + if (($viewtype eq 'lastonly') || + ($viewtype eq 'datesub') || + ($viewtype =~ /^(last|all)$/)) { + my $display_part=&get_display_part($partid,$symb); + $lastsubonly.='
    '. + ''.&mt('Part: [_1]',$display_part).''. + ' '. + '('.&mt('Response ID: [_1]',$respid).')'. + '   '; + my $files=&get_submitted_files($udom,$uname,$partid,$respid,$record); + if (@$files) { + if ($hide eq 'anon') { + $lastsubonly.='
    '.&mt('[quant,_1,file] uploaded to this anonymous survey',scalar(@{$files})); + } else { + $lastsubonly.='

    '.''.&mt('Submitted Files:').'' + .'
    '; + if(@$files == 1) { + $lastsubonly .= &mt('Like all files provided by users, this file may contain viruses!'); + } else { + $lastsubonly .= &mt('Like all files provided by users, these files may contain viruses!'); + } + $lastsubonly .= ''; + foreach my $file (@$files) { + &Apache::lonnet::allowuploaded('/adm/grades',$file); + $lastsubonly.='
    '.$file.''; + } + } + $lastsubonly.='
    '; + } + if ($hide eq 'anon') { + $lastsubonly.='
    '.&mt('Anonymous Survey').''; + } else { + $lastsubonly.='
    '.&mt('Submitted Answer:').' '; + if ($draft) { + $lastsubonly.= ' '.&mt('Draft Copy').''; + } + $subval = + &cleanRecord($subval,$responsetype,$symb,$partid, + $respid,$record,$order,undef,$uname,$udom,$type,$trial,$rndseed); + if ($responsetype eq 'essay') { + $subval =~ s{\n}{
    }g; + } + $lastsubonly.=$subval."\n"; + } + if ($similar) {$lastsubonly.="

    $similar\n";} + $lastsubonly.='
    '; + } + } + } + $lastsubonly.='
    '."\n"; # End: LC_grade_submissions_body + } + return ($lastsubonly,$partinfo); +} + sub check_collaborators { my ($symb,$uname,$udom,$record,$handgrade,$counter) = @_; my ($result,@col_fullnames); @@ -2876,18 +3847,51 @@ sub check_collaborators { #--- Retrieve the last submission for all the parts sub get_last_submission { my ($returnhash,$is_tool)=@_; - my (@string,$timestamp,%lasthidden); + my (@string,$timestamp,$lastgradetime,$lastsubmittime); if ($$returnhash{'version'}) { my %lasthash=(); - my ($version); + my %prevsolved=(); + my %solved=(); + my $version; for ($version=1;$version<=$$returnhash{'version'};$version++) { + my %handgraded = (); foreach my $key (sort(split(/\:/, $$returnhash{$version.':keys'}))) { $lasthash{$key}=$$returnhash{$version.':'.$key}; - $timestamp = - &Apache::lonlocal::locallocaltime($$returnhash{$version.':timestamp'}); + if ($key =~ /\.([^.]+)\.regrader$/) { + $handgraded{$1} = 1; + } elsif ($key =~ /\.portfiles$/) { + if (($$returnhash{$version.':'.$key} ne '') && + ($$returnhash{$version.':'.$key} !~ /\.\d+\.\w+$/)) { + $lastsubmittime = $$returnhash{$version.':timestamp'}; + } + } elsif ($key =~ /\.submission$/) { + if ($$returnhash{$version.':'.$key} ne '') { + $lastsubmittime = $$returnhash{$version.':timestamp'}; + } + } elsif ($key =~ /\.([^.]+)\.solved$/) { + $prevsolved{$1} = $solved{$1}; + $solved{$1} = $lasthash{$key}; + } + } + foreach my $partid (keys(%handgraded)) { + if (($prevsolved{$partid} eq 'ungraded_attempted') && + (($solved{$partid} eq 'incorrect_by_override') || + ($solved{$partid} eq 'correct_by_override'))) { + $lastgradetime = $$returnhash{$version.':timestamp'}; + } + if ($solved{$partid} ne '') { + $prevsolved{$partid} = $solved{$partid}; + } } } +# +# Timestamp is for last transaction for this resource, which does not +# necessarily correspond to the time of last submission for problem (or part). +# + if ($lasthash{'timestamp'} ne '') { + $timestamp = &Apache::lonlocal::locallocaltime($lasthash{'timestamp'}); + } my (%typeparts,%randombytry); my $showsurv = &Apache::lonnet::allowed('vas',$env{'request.course.id'}); @@ -2950,7 +3954,7 @@ sub get_last_submission { $string[0] = ''.$msg.''; } - return (\@string,\$timestamp); + return (\@string,$timestamp,$lastgradetime,$lastsubmittime); } #--- High light keywords, with style choosen by user. @@ -3157,13 +4161,33 @@ sub processHandGrade { my $ntstu = $env{'form.NTSTU'}; my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my ($res_error,%queueable); + my ($partlist,$handgrade,$responseType,$numresp,$numessay) = &response_type($symb,\$res_error); + if ($res_error) { + $request->print(&navmap_errormsg()); + return; + } else { + foreach my $part (@{$partlist}) { + if (ref($responseType->{$part}) eq 'HASH') { + foreach my $id (keys(%{$responseType->{$part}})) { + if (($responseType->{$part}->{$id} eq 'essay') || + (lc($handgrade->{$part.'_'.$id}) eq 'yes')) { + $queueable{$part} = 1; + last; + } + } + } + } + } if ($button eq 'Save & Next') { + my %needpb = &passbacks_for_symb($cdom,$cnum,$symb); + my (%skip_passback,%pbsave,%pbcollab); my $ctr = 0; while ($ctr < $ngrade) { my ($uname,$udom) = split(/:/,$env{'form.unamedom'.$ctr}); my ($errorflag,$pts,$wgt,$numhidden) = - &saveHandGrade($request,$symb,$uname,$udom,$ctr); + &saveHandGrade($request,$symb,$uname,$udom,$ctr,undef,undef,\%queueable,\%needpb,\%skip_passback,\%pbsave); if ($errorflag eq 'no_score') { $ctr++; next; @@ -3218,11 +4242,13 @@ sub processHandGrade { foreach my $collaborator (@collaborators) { my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$symb,$collaborator,$udom,$ctr, - $env{'form.unamedom'.$ctr},$part); + $env{'form.unamedom'.$ctr},$part,\%queueable,\%needpb,\%skip_passback,%pbsave); if ($errorflag eq 'not_allowed') { $request->print("".&mt('Not allowed to modify grades for [_1]',"$collaborator:$udom").""); next; - } elsif ($message ne '') { + } else { + $pbcollab{$collaborator}{$part} = [$pts,$wgt]; + if ($message ne '') { my ($baseurl,$showsymb) = &get_feedurl_and_symb($symb,$collaborator, $udom); @@ -3238,13 +4264,9 @@ sub processHandGrade { } $ctr++; } - } - - my $res_error; - my ($partlist,$handgrade,$responseType,$numresp,$numessay) = &response_type($symb,\$res_error); - if ($res_error) { - $request->print(&navmap_errormsg()); - return; + if ((keys(%pbcollab)) && (keys(%needpb))) { + # FIXME passback scores for collaborators + } } my %keyhash = (); @@ -3398,7 +4420,7 @@ sub processHandGrade { #---- Save the score and award for each student, if changed sub saveHandGrade { - my ($request,$symb,$stuname,$domain,$newflg,$submitter,$part) = @_; + my ($request,$symb,$stuname,$domain,$newflg,$submitter,$part,$queueable,$needpb,$skip_passback,$pbsave) = @_; my @version_parts; my $usec = &Apache::lonnet::getsection($domain,$stuname, $env{'request.course.id'}); @@ -3409,23 +4431,36 @@ sub saveHandGrade { my ($pts,$wgt,$totchg) = ('','',0); my %aggregate = (); my $aggregateflag = 0; + my $sendupdate; if ($env{'form.HIDE'.$newflg}) { my ($version,$parts) = split(/:/,$env{'form.HIDE'.$newflg},2); my $numchgs = &makehidden($version,$parts,\%record,$symb,$domain,$stuname,1); $totchg += $numchgs; + if ($numchgs) { + $sendupdate = 1; + } } + my (%weights,%awardeds,%excuseds); 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 ($env{'form.WGT'.$newflg.'_'.$new_part} eq '') { + $weights{$symb}{$new_part} = 1; + } else { + $weights{$symb}{$new_part} = $env{'form.WGT'.$newflg.'_'.$new_part}; + } if ($dropMenu eq 'excused') { + $excuseds{$symb}{$new_part} = 1; + $awardeds{$symb}{$new_part} = ''; 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'}"; + $sendupdate = 1; } } elsif ($dropMenu eq 'reset status' && exists($record{'resource.'.$new_part.'.solved'})) { #don't bother if no old records -> no attempts @@ -3449,6 +4484,9 @@ sub saveHandGrade { &decrement_aggs($symb,$new_part,\%aggregate,$aggtries,$totaltries,$solvedstatus); $aggregateflag = 1; } + $sendupdate = 1; + $excuseds{$symb}{$new_part} = ''; + $awardeds{$symb}{$new_part} = ''; } elsif ($dropMenu eq '') { $pts = ($env{'form.GD_BOX'.$newflg.'_'.$new_part} ne '' ? $env{'form.GD_BOX'.$newflg.'_'.$new_part} : @@ -3459,12 +4497,15 @@ sub saveHandGrade { $wgt = $env{'form.WGT'.$newflg.'_'.$new_part} eq '' ? 1 : $env{'form.WGT'.$newflg.'_'.$new_part}; my $partial= $pts/$wgt; + $awardeds{$symb}{$new_part} = $partial; + $excuseds{$symb}{$new_part} = ''; 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); + $sendupdate = 1; } if ($record{'resource.'.$new_part.'.awarded'} ne $partial) { $newrecord{'resource.'.$new_part.'.awarded'} = $partial; @@ -3510,12 +4551,19 @@ sub saveHandGrade { &Apache::lonnet::cstore(\%newrecord,$symb, $env{'request.course.id'},$domain,$stuname); &check_and_remove_from_queue(\@parts,\%record,\%newrecord,$symb, - $cdom,$cnum,$domain,$stuname); + $cdom,$cnum,$domain,$stuname,$queueable); } if ($aggregateflag) { &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, $cdom,$cnum); } + if (($sendupdate) && (!$submitter)) { + if ((ref($needpb) eq 'HASH') && + (keys(%{$needpb}))) { + &process_passbacks('handgrade',[$symb],$cdom,$cnum,$domain,$stuname,\%weights, + \%awardeds,\%excuseds,$needpb,$skip_passback,$pbsave); + } + } return ('',$pts,$wgt,$totchg); } @@ -3550,7 +4598,7 @@ sub makehidden { } sub check_and_remove_from_queue { - my ($parts,$record,$newrecord,$symb,$cdom,$cnum,$domain,$stuname) = @_; + my ($parts,$record,$newrecord,$symb,$cdom,$cnum,$domain,$stuname,$queueable) = @_; my @ungraded_parts; foreach my $part (@{$parts}) { if ( $record->{ 'resource.'.$part.'.awarded'} eq '' @@ -3558,7 +4606,9 @@ sub check_and_remove_from_queue { && $newrecord->{'resource.'.$part.'.awarded'} eq '' && $newrecord->{'resource.'.$part.'.solved' } ne 'excused' ) { - push(@ungraded_parts, $part); + if ($queueable->{$part}) { + push(@ungraded_parts, $part); + } } } if ( !@ungraded_parts ) { @@ -3757,8 +4807,8 @@ sub version_portfiles { $$record{$key} = join(',',@versioned_portfiles); push(@returned_keys,$key); } - } - return (@returned_keys); + } + return (@returned_keys); } #-------------------------------------------------------------------------------------- @@ -3977,7 +5027,26 @@ sub viewgrades { } my ($common_header,$specific_header,@sections,$section_display); - @sections = &Apache::loncommon::get_env_multiple('form.section'); + if ($env{'request.course.sec'} ne '') { + @sections = ($env{'request.course.sec'}); + } else { + @sections = &Apache::loncommon::get_env_multiple('form.section'); + } + +# Check if Save button should be usable + my $disabled = ' disabled="disabled"'; + if ($perm{'mgr'}) { + if (grep(/^all$/,@sections)) { + undef($disabled); + } else { + foreach my $sec (@sections) { + if (&canmodify($sec)) { + undef($disabled); + last; + } + } + } + } if (grep(/^all$/,@sections)) { @sections = ('all'); if ($group_display) { @@ -4169,7 +5238,7 @@ sub viewgrades { } $result.=&Apache::loncommon::end_data_table(); $result.=''."\n"; - $result.=''."\n"; if ($ctr == 0) { my $stu_status = join(' or ',&Apache::loncommon::get_env_multiple('form.Status')); @@ -4382,6 +5451,10 @@ sub editgrades { ); my ($classlist,undef,$fullname) = &getclasslist($env{'form.section'},'0'); + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my %needpb = &passbacks_for_symb($cdom,$cnum,$symb); + my (@partid); my %weight = (); my %columns = (); @@ -4432,6 +5505,7 @@ sub editgrades { &Apache::loncommon::end_data_table_header_row(); my @noupdate; my ($updateCtr,$noupdateCtr) = (1,1); + my ($got_types,%queueable,%pbsave,%skip_passback); for ($i=0; $i<$env{'form.total'}; $i++) { my $user = $env{'form.ctr'.$i}; my ($uname,$udom)=split(/:/,$user); @@ -4450,6 +5524,7 @@ sub editgrades { my %aggregate = (); my $aggregateflag = 0; $user=~s/:/_/; # colon doen't work in javascript for names + my (%weights,%awardeds,%excuseds); foreach (@partid) { my $old_aw = $env{'form.GD_'.$user.'_'.$_.'_awarded_s'}; my $old_part_pcr = $old_aw/($weight{$_} ne '0' ? $weight{$_}:1); @@ -4458,6 +5533,7 @@ sub editgrades { my $awarded = $env{'form.GD_'.$user.'_'.$_.'_awarded'}; my $pcr = $awarded/($weight{$_} ne '0' ? $weight{$_} : 1); my $partial = $awarded eq '' ? '' : $pcr; + $awardeds{$symb}{$_} = $partial; my $score; if ($partial eq '') { $score = $scoreptr{$env{'form.GD_'.$user.'_'.$_.'_solved_s'}}; @@ -4498,6 +5574,11 @@ sub editgrades { my $partid=$_; + if ($score eq 'excused') { + $excuseds{$symb}{$partid} = 1; + } else { + $excuseds{$symb}{$partid} = ''; + } foreach my $stores (@parts) { my ($part,$type) = &split_part_type($stores); if ($part !~ m/^\Q$partid\E/) { next;} @@ -4515,9 +5596,6 @@ sub editgrades { } $line.="\n"; - my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; - my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; - if ($updateflag) { $count++; &Apache::lonnet::cstore(\%newrecord,$symb,$env{'request.course.id'}, @@ -4531,14 +5609,33 @@ sub editgrades { $udom,$uname); my $all_graded = 1; my $none_graded = 1; + unless ($got_types) { + my $error; + my ($plist,$handgrd,$resptype) = &response_type($symb,\$error); + unless ($error) { + foreach my $part (@parts) { + if (ref($resptype->{$part}) eq 'HASH') { + foreach my $id (keys(%{$resptype->{$part}})) { + if (($resptype->{$part}->{$id} eq 'essay') || + (lc($handgrd->{$part.'_'.$id}) eq 'yes')) { + $queueable{$part} = 1; + last; + } + } + } + } + } + $got_types = 1; + } foreach my $part (@parts) { - if ( $record{'resource.'.$part.'.awarded'} eq '' ) { - $all_graded = 0; - } else { - $none_graded = 0; + if ($queueable{$part}) { + if ( $record{'resource.'.$part.'.awarded'} eq '' ) { + $all_graded = 0; + } else { + $none_graded = 0; + } } - } - + } if ($all_graded || $none_graded) { &Apache::bridgetask::remove_from_queue('gradingqueue', $symb,$cdom,$cnum, @@ -4550,6 +5647,11 @@ sub editgrades { ' '.$updateCtr.' '.$line. &Apache::loncommon::end_data_table_row(); $updateCtr++; + if (keys(%needpb)) { + $weights{$symb} = \%weight; + &process_passbacks('editgrades',[$symb],$cdom,$cnum,$udom,$uname,\%weights, + \%awardeds,\%excuseds,\%needpb,\%skip_passback,\%pbsave); + } } else { push(@noupdate, ' '.$noupdateCtr.' '.$line); @@ -4915,11 +6017,33 @@ sub csvuploadassign { my @gradedata = &Apache::loncommon::upfile_record_sep(); my %fields=&get_fields(); my $courseid=$env{'request.course.id'}; + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; my ($classlist) = &getclasslist('all',0); my @notallowed; my @skipped; my @warnings; my $countdone=0; + my @parts; + my %needpb = &passbacks_for_symb($cdom,$cnum,$symb); + my $passback; + if (keys(%needpb)) { + $passback = 1; + my $navmap = Apache::lonnavmaps::navmap->new(); + if (ref($navmap)) { + my $res = $navmap->getBySymb($symb); + if (ref($res)) { + my $partlist = $res->parts(); + if (ref($partlist) eq 'ARRAY') { + @parts = sort(@{$partlist}); + } + } + } else { + return &navmap_errormsg(); + } + } + my (%skip_passback,%pbsave,%weights,%awardeds,%excuseds); + foreach my $grade (@gradedata) { my %entries=&Apache::loncommon::record_sep($grade); my $domain; @@ -4994,9 +6118,14 @@ sub csvuploadassign { my $part=$1; my $wgt =&Apache::lonnet::EXT('resource.'.$part.'.weight', $symb,$domain,$username); + $weights{$symb}{$part} = $wgt; if ($wgt) { $entries{$fields{$dest}}=~s/\s//g; my $pcr=$entries{$fields{$dest}} / $wgt; + if ($passback) { + $awardeds{$symb}{$part} = $pcr; + $excuseds{$symb}{$part} = ''; + } my $award=($pcr == 0) ? 'incorrect_by_override' : 'correct_by_override'; if ($pcr>1) { @@ -5016,6 +6145,22 @@ sub csvuploadassign { if ($dest=~/stores_(.*)_awarded/) { if ($points{$1}) {next;} } if ($dest=~/stores_(.*)_solved/) { if ($points{$1}) {next;} } my $store_key=$dest; + if ($passback) { + if ($store_key=~/stores_(.*)_(awarded|solved)/) { + my ($part,$key) = ($1,$2); + unless ((ref($weights{$symb}) eq 'HASH') && (exists($weights{$symb}{$part}))) { + $weights{$symb}{$part} = &Apache::lonnet::EXT('resource.'.$part.'.weight', + $symb,$domain,$username); + } + if ($key eq 'awarded') { + $awardeds{$symb}{$part} = $entries{$fields{$dest}}; + } elsif ($key eq 'solved') { + if ($entries{$fields{$dest}} =~ /^excused/) { + $excuseds{$symb}{$part} = 1; + } + } + } + } $store_key=~s/^stores/resource/; $store_key=~s/_/\./g; $grades{$store_key}=$entries{$fields{$dest}}; @@ -5032,11 +6177,32 @@ sub csvuploadassign { # Successfully stored $request->print('.'); # Remove from grading queue - &Apache::bridgetask::remove_from_queue('gradingqueue',$symb, - $env{'course.'.$env{'request.course.id'}.'.domain'}, - $env{'course.'.$env{'request.course.id'}.'.num'}, + &Apache::bridgetask::remove_from_queue('gradingqueue',$symb,$cdom,$cnum, $domain,$username); $countdone++; + if ($passback) { + my @parts_in_upload; + if (ref($weights{$symb}) eq 'HASH') { + @parts_in_upload = sort(keys(%{$weights{$symb}})); + } + my @diffs = &Apache::loncommon::compare_arrays(\@parts_in_upload,\@parts); + if (@diffs > 0) { + my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$domain,$username); + foreach my $part (@parts) { + next if (grep(/^\Q$part\E$/,@parts_in_upload)); + $weights{$symb}{$part} = &Apache::lonnet::EXT('resource.'.$part.'.weight', + $symb,$domain,$username); + if ($record{"resource.$part.solved"} =~/^excused/) { + $excuseds{$symb}{$part} = 1; + } else { + $excuseds{$symb}{$part} = ''; + } + $awardeds{$symb}{$part} = $record{"resource.$part.awarded"}; + } + } + &process_passbacks('csvupload',[$symb],$cdom,$cnum,$domain,$username,\%weights, + \%awardeds,\%excuseds,\%needpb,\%skip_passback,\%pbsave); + } } else { $request->print("

    ". &mt("Failed to save data for student [_1]. Message when trying to save was: [_2]", @@ -5409,10 +6575,14 @@ sub displayPage { } $curRes = $iterator->next(); } + my $disabled; + unless (&canmodify($usec)) { + $disabled = ' disabled="disabled"'; + } $studentTable.= ''."\n". - ''. ''."\n"; $request->print($studentTable); @@ -5628,6 +6798,7 @@ sub updateGradeByPage { $iterator->next(); # skip the first BEGIN_MAP my $curRes = $iterator->next(); # for "current resource" my ($depth,$question,$prob,$changeflag,$hideflag)= (1,1,1,0,0); + my (@updates,%weights,%excuseds,%awardeds,@symbs_in_map); while ($depth > 0) { if($curRes == $iterator->BEGIN_MAP) { $depth++; } if($curRes == $iterator->END_MAP) { $depth--; } @@ -5636,6 +6807,7 @@ sub updateGradeByPage { my $parts = $curRes->parts(); my $title = $curRes->compTitle(); my $symbx = $curRes->symb(); + push(@symbs_in_map,$symbx); $studentTable.= &Apache::loncommon::start_data_table_row(). ''.$prob. @@ -5648,18 +6820,37 @@ sub updateGradeByPage { my @displayPts=(); my %aggregate = (); my $aggregateflag = 0; + my %queueable; if ($env{'form.HIDE'.$prob}) { my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname); my ($version,$parts) = split(/:/,$env{'form.HIDE'.$prob},2); my $numchgs = &makehidden($version,$parts,\%record,$symbx,$udom,$uname,1); + if ($numchgs) { + push(@updates,$symbx); + } $hideflag += $numchgs; } foreach my $partid (@{$parts}) { my $newpts = $env{'form.GD_BOX'.$question.'_'.$partid}; my $oldpts = $env{'form.oldpts'.$question.'_'.$partid}; - + my @types = $curRes->responseType($partid); + if (grep(/^essay$/,@types)) { + $queueable{$partid} = 1; + } else { + my @ids = $curRes->responseIds($partid); + for (my $i=0; $i < scalar(@ids); $i++) { + my $hndgrd = &Apache::lonnet::EXT('resource.'.$partid.'_'.$ids[$i]. + '.handgrade',$symb); + if (lc($hndgrd) eq 'yes') { + $queueable{$partid} = 1; + last; + } + } + } my $wgt = $env{'form.WGT'.$question.'_'.$partid} != 0 ? $env{'form.WGT'.$question.'_'.$partid} : 1; + $weights{$symbx}{$partid} = $wgt; + $excuseds{$symbx}{$partid} = ''; my $partial = $newpts/$wgt; my $score; if ($partial > 0) { @@ -5671,6 +6862,7 @@ sub updateGradeByPage { if ($dropMenu eq 'excused') { $partial = ''; $score = 'excused'; + $excuseds{$symbx}{$partid} = 1; } elsif ($dropMenu eq 'reset status' && $env{'form.solved'.$question.'_'.$partid} ne '') { #update only if previous record exists $newrecord{'resource.'.$partid.'.tries'} = 0; @@ -5698,6 +6890,11 @@ sub updateGradeByPage { (($score eq 'excused') ? 'excused' : $newpts). ' 
    '; $question++; + if (($newpts eq '') || ($partial eq '')) { + $awardeds{$symbx}{$partid} = 0; + } else { + $awardeds{$symbx}{$partid} = $partial; + } next if ($dropMenu eq 'reset status' || ($newpts eq $oldpts && $score ne 'excused')); $newrecord{'resource.'.$partid.'.awarded'} = $partial if $partial ne ''; @@ -5723,7 +6920,7 @@ sub updateGradeByPage { $env{'request.course.id'}, $udom,$uname); &check_and_remove_from_queue($parts,\%record,undef,$symbx, - $cdom,$cnum,$udom,$uname); + $cdom,$cnum,$udom,$uname,\%queueable); } if ($aggregateflag) { @@ -5737,6 +6934,9 @@ sub updateGradeByPage { &Apache::loncommon::end_data_table_row(); $prob++; + if ($changeflag) { + push(@updates,$symbx); + } } $curRes = $iterator->next(); } @@ -5750,6 +6950,76 @@ sub updateGradeByPage { $hideflag).'
    '); $request->print($hidemsg.$grademsg.$studentTable); + if (@updates) { + undef(@Apache::lonhomework::ltipassback); + my (@allsymbs,$mapsymb,@recurseup,%parentmapsymbs,%possmappb,%possrespb); + @allsymbs = @updates; + if (ref($map)) { + $mapsymb = $map->symb(); + push(@allsymbs,$mapsymb); + @recurseup = $navmap->recurseup_maps($map->src,1); + } + if (@recurseup) { + push(@allsymbs,@recurseup); + map { $parentmapsymbs{$_} = 1; } @recurseup; + } + my %passback = &Apache::lonnet::get('nohist_linkprot_passback',\@allsymbs,$cdom,$cnum); + my (%uniqsymbs,$use_symbs_in_map); + if (keys(%passback)) { + foreach my $possible (keys(%passback)) { + if (ref($passback{$possible}) eq 'HASH') { + if ($possible eq $mapsymb) { + foreach my $launcher (keys(%{$passback{$possible}})) { + $possmappb{$launcher} = 1; + } + $use_symbs_in_map = 1; + } elsif (exists($parentmapsymbs{$possible})) { + foreach my $launcher (keys(%{$passback{$possible}})) { + my ($linkuri,$linkprotector,$scope) = split(/\0/,$launcher); + if ($scope eq 'rec') { + $possmappb{$launcher} = 1; + $use_symbs_in_map = 1; + } + } + } elsif (grep(/^\Q$possible$\E$/,@updates)) { + foreach my $launcher (keys(%{$passback{$possible}})) { + $possrespb{$launcher} = 1; + } + $uniqsymbs{$possible} = 1; + } + } + } + } + if ($use_symbs_in_map) { + map { $uniqsymbs{$_} = 1; } @symbs_in_map; + } + my @posslaunchers; + if (keys(%possmappb)) { + push(@posslaunchers,keys(%possmappb)); + } + if (keys(%possrespb)) { + push(@posslaunchers,keys(%possrespb)); + } + if (@posslaunchers) { + my (%pbsave,%skip_passback,%needpb); + my %pbids = &Apache::lonnet::get('nohist_'.$cdom.'_'.$cnum.'_linkprot_pb',\@posslaunchers,$udom,$uname); + foreach my $key (keys(%pbids)) { + if (ref($pbids{$key}) eq 'ARRAY') { + $needpb{$key} = 1; + } + } + my @symbs = keys(%uniqsymbs); + &process_passbacks('updatebypage',\@symbs,$cdom,$cnum,$udom,$uname,\%weights, + \%awardeds,\%excuseds,\%needpb,\%skip_passback,\%pbsave,\%pbids); + if (@Apache::lonhomework::ltipassback) { + unless ($registered_cleanup) { + my $handlers = $request->get_handlers('PerlCleanupHandler'); + $request->set_handlers('PerlCleanupHandler' => + [\&Apache::lonhomework::do_ltipassback,@{$handlers}]); + } + } + } + } return ''; } @@ -6635,9 +7905,12 @@ sub scantron_parse_scanline { } sub get_master_seq { - my ($resources,$master_seq,$symb_to_resource) = @_; + my ($resources,$master_seq,$symb_to_resource,$need_symb_in_map,$symb_for_examcode) = @_; return unless ((ref($resources) eq 'ARRAY') && (ref($master_seq) eq 'ARRAY') && (ref($symb_to_resource) eq 'HASH')); + if ($need_symb_in_map) { + return unless (ref($symb_for_examcode) eq 'HASH'); + } my $resource_error; foreach my $resource (@{$resources}) { my $ressymb; @@ -6645,6 +7918,14 @@ sub get_master_seq { $ressymb = $resource->symb(); push(@{$master_seq},$ressymb); $symb_to_resource->{$ressymb} = $resource; + if ($need_symb_in_map) { + unless ($resource->is_map()) { + my $map=(&Apache::lonnet::decode_symb($ressymb))[0]; + unless (exists($symb_for_examcode->{$map})) { + $symb_for_examcode->{$map} = $ressymb; + } + } + } } else { $resource_error = 1; last; @@ -8656,6 +9937,17 @@ sub scantron_validate_doublebubble { if (ref($map)) { $randomorder = $map->randomorder(); $randompick = $map->randompick(); + unless ($randomorder || $randompick) { + foreach my $res ($navmap->retrieveResources($map,sub { $_[0]->is_map() },1,0,1)) { + if ($res->randomorder()) { + $randomorder = 1; + } + if ($res->randompick()) { + $randompick = 1; + } + last if ($randomorder || $randompick); + } + } if ($randomorder || $randompick) { $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); if ($nav_error) { @@ -8839,6 +10131,17 @@ sub scantron_validate_missingbubbles { if (ref($map)) { $randomorder = $map->randomorder(); $randompick = $map->randompick(); + unless ($randomorder || $randompick) { + foreach my $res ($navmap->retrieveResources($map,sub { $_[0]->is_map() },1,0,1)) { + if ($res->randomorder()) { + $randomorder = 1; + } + if ($res->randompick()) { + $randompick = 1; + } + last if ($randomorder || $randompick); + } + } if ($randomorder || $randompick) { $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); if ($nav_error) { @@ -8980,10 +10283,21 @@ sub scantron_process_students { } my $map=$navmap->getResourceByUrl($sequence); my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, - %grader_randomlists_by_symb); + %grader_randomlists_by_symb,%symb_for_examcode); if (ref($map)) { $randomorder = $map->randomorder(); $randompick = $map->randompick(); + unless ($randomorder || $randompick) { + foreach my $res ($navmap->retrieveResources($map,sub { $_[0]->is_map() },1,0,1)) { + if ($res->randomorder()) { + $randomorder = 1; + } + if ($res->randompick()) { + $randompick = 1; + } + last if ($randomorder || $randompick); + } + } } else { $r->print(&navmap_errormsg()); return ''; @@ -8991,7 +10305,7 @@ sub scantron_process_students { my $nav_error; my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); if ($randomorder || $randompick) { - $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource,1,\%symb_for_examcode); if ($nav_error) { $r->print(&navmap_errormsg()); return ''; @@ -9146,11 +10460,16 @@ SCANTRONFORM } if (($scancode) && ($randomorder || $randompick)) { - my $parmresult = - &Apache::lonparmset::storeparm_by_symb($symb, - '0_examcode',2,$scancode, - 'string_examcode',$uname, - $udom); + foreach my $key (keys(%symb_for_examcode)) { + my $symb_in_map = $symb_for_examcode{$key}; + if ($symb_in_map ne '') { + my $parmresult = + &Apache::lonparmset::storeparm_by_symb($symb_in_map, + '0_examcode',2,$scancode, + 'string_examcode',$uname, + $udom); + } + } } $completedstudents{$uname}={'line'=>$line}; if ($env{'form.verifyrecord'}) { @@ -9465,7 +10784,7 @@ END my @lines = &Apache::lonnet::get_scantronformat_file(); my $count = 0; foreach my $line (@lines) { - next if ($line =~ /^#/); + next if (($line =~ /^\#/) || ($line eq '')); $singleline = $line; $count ++; } @@ -9659,7 +10978,7 @@ sub validate_uploaded_scantron_file { my %unique_formats; my @formatlines = &Apache::lonnet::get_scantronformat_file(); foreach my $line (@formatlines) { - chomp($line); + next if (($line =~ /^\#/) || ($line eq '')); my @config = split(/:/,$line); my $idstart = $config[5]; my $idlength = $config[6]; @@ -10029,6 +11348,17 @@ sub checkscantron_results { if (ref($map)) { $randomorder=$map->randomorder(); $randompick=$map->randompick(); + unless ($randomorder || $randompick) { + foreach my $res ($navmap->retrieveResources($map,sub { $_[0]->is_map() },1,0,1)) { + if ($res->randomorder()) { + $randomorder = 1; + } + if ($res->randompick()) { + $randompick = 1; + } + last if ($randomorder || $randompick); + } + } } my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); my $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); @@ -10347,7 +11677,8 @@ sub verify_scantron_grading { sub href_symb_cmd { my ($symb,$cmd)=@_; - return '/adm/grades?symb='.&HTML::Entities::encode(&Apache::lonenc::check_encrypt($symb),'<>&"').'&command='.$cmd; + return '/adm/grades?symb='.&HTML::Entities::encode(&Apache::lonenc::check_encrypt($symb),'<>&"').'&command='. + &HTML::Entities::encode($cmd,'<>&"'); } sub grading_menu { @@ -10382,37 +11713,47 @@ sub grading_menu { $fields{'command'} = 'initialverifyreceipt'; my $url5 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); - + + my %permissions; + if ($perm{'mgr'}) { + $permissions{'either'} = 'F'; + $permissions{'mgr'} = 'F'; + } + if ($perm{'vgr'}) { + $permissions{'either'} = 'F'; + $permissions{'vgr'} = 'F'; + } + my @menu = ({ categorytitle=>'Hand Grading', items =>[ { linktext => 'Select individual students to grade', url => $url1a, - permission => 'F', + permission => $permissions{'either'}, icon => 'grade_students.png', linktitle => 'Grade current resource for a selection of students.' }, { linktext => 'Grade ungraded submissions', url => $url1b, - permission => 'F', + permission => $permissions{'either'}, icon => 'ungrade_sub.png', linktitle => 'Grade all submissions that have not been graded yet.' }, { linktext => 'Grading table', url => $url1c, - permission => 'F', + permission => $permissions{'either'}, icon => 'grading_table.png', linktitle => 'Grade current resource for all students.' }, { linktext => 'Grade page/folder for one student', url => $url1d, - permission => 'F', + permission => $permissions{'either'}, icon => 'grade_PageFolder.png', linktitle => 'Grade all resources in current page/sequence/folder for one student.' }, { linktext => 'Download submissions', url => $url1e, - permission => 'F', + permission => $permissions{'either'}, icon => 'download_sub.png', linktitle => 'Download all students submissions.' }]}, @@ -10421,32 +11762,45 @@ sub grading_menu { { linktext => 'Upload Scores', url => $url2, - permission => 'F', + permission => $permissions{'mgr'}, icon => 'uploadscores.png', linktitle => 'Specify a file containing the class scores for current resource.' }, { linktext => 'Process Clicker', url => $url3, - permission => 'F', + permission => $permissions{'mgr'}, icon => 'addClickerInfoFile.png', linktitle => 'Specify a file containing the clicker information for this resource.' }, { linktext => 'Grade/Manage/Review Bubblesheets', url => $url4, - permission => 'F', + permission => $permissions{'mgr'}, icon => 'bubblesheet.png', linktitle => 'Grade bubblesheet exams, upload/download bubblesheet data files, and review previously graded bubblesheet exams.' }, { linktext => 'Verify Receipt Number', url => $url5, - permission => 'F', + permission => $permissions{'either'}, icon => 'receipt_number.png', linktitle => 'Verify a system-generated receipt number for correct problem solution.' } ] }); - + my $cdom = $env{"course.$env{'request.course.id'}.domain"}; + my $cnum = $env{"course.$env{'request.course.id'}.num"}; + my %passback = &Apache::lonnet::dump('nohist_linkprot_passback',$cdom,$cnum); + if (keys(%passback)) { + $fields{'command'} = 'initialpassback'; + my $url6 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + push (@{$menu[1]{items}}, + { linktext => 'Passback of Scores', + url => $url6, + permission => $permissions{'either'}, + icon => 'passback.png', + linktitle => 'Passback scores to launcher CMS for resources accessed via LTI-mediated deep-linking', + }); + } # Create the menu my $Str; $Str .= '

    '; @@ -10574,6 +11928,14 @@ sub selectfield { (&substatus_options, 'select_form_order' => ['yes','queued','graded','incorrect','all']); } + + # + # PrepareClasslist() needs to be called to avoid getting a sections list + # for a different course from the @Sections global in lonstatistics.pm, + # populated by an earlier request. + # + &Apache::lonstatistics::PrepareClasslist(); + my $result='
    @@ -11286,6 +12648,7 @@ sub assign_clicker_grades { $result.="
    Failed to save student $username:$domain. Message when trying to save was ($returncode)"; } else { $storecount++; + #FIXME Do passback for $user if required } } } @@ -11317,7 +12680,9 @@ sub startpage { $args{'only_body'} = 1; $r->print(&Apache::loncommon::start_page("Student's Version",$head_extra,\%args)); } else { - unshift(@$crumbs,{href=>&href_symb_cmd($symb,'gradingmenu'),text=>"Grading"}); + if ($env{'request.course.id'}) { + unshift(@$crumbs,{href=>&href_symb_cmd($symb,'gradingmenu'),text=>"Grading"}); + } $args{'bread_crumbs'} = $crumbs; $r->print(&Apache::loncommon::start_page('Grading',$head_extra,\%args)); if ($env{'request.course.id'}) { @@ -11337,6 +12702,77 @@ sub select_problem { $r->print(''); } +#----- display problem, answer, and submissions for a single student (no grading) + +sub view_as_user { + my ($symb,$vuname,$vudom,$hasperm) = @_; + my $plainname = &Apache::loncommon::plainname($vuname,$vudom,'lastname'); + my $displayname = &nameUserString('',$plainname,$vuname,$vudom); + my $output = &Apache::loncommon::get_student_view($symb,$vuname,$vudom, + $env{'request.course.id'}, + undef,{'disable_submit' => 1}). + "\n\n". + '
    '. + '

    '.$displayname.'

    '. + "\n". + &Apache::loncommon::track_student_link('View recent activity', + $vuname,$vudom,'check').' '. + "\n"; + if (&Apache::lonnet::allowed('opa',$env{'request.course.id'}) || + (($env{'request.course.sec'} ne '') && + &Apache::lonnet::allowed('opa',$env{'request.course.id'}.'/'.$env{'request.course.sec'}))) { + $output .= &Apache::loncommon::pprmlink(&mt('Set/Change parameters'), + $vuname,$vudom,$symb,'check'); + } + $output .= "\n"; + my $companswer = &Apache::loncommon::get_student_answers($symb,$vuname,$vudom, + $env{'request.course.id'}); + $companswer=~s|||g; + $companswer=~s|||g; + $companswer=~s|name="submit"|name="would_have_been_submit"|g; + $output .= '
    '. + '

    '.&mt('Correct answer for[_1]',$displayname).'

    '. + $companswer. + '
    '."\n"; + my $is_tool = ($symb =~ /ext\.tool$/); + my ($essayurl,%coursedesc_by_cid); + (undef,undef,$essayurl) = &Apache::lonnet::decode_symb($symb); + my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$vudom,$vuname); + my $res_error; + my ($partlist,$handgrade,$responseType,$numresp,$numessay) = + &response_type($symb,\$res_error); + my $fullname; + my $collabinfo; + if ($numessay) { + unless ($hasperm) { + &init_perm(); + } + ($collabinfo,$fullname)= + &check_collaborators($symb,$vuname,$vudom,\%record,$handgrade,0); + unless ($hasperm) { + &reset_perm(); + } + } + my $checkIcon = ''.&mt('Check Mark').
+                    ''; + my ($lastsubonly,$partinfo) = + &show_last_submission($vuname,$vudom,$symb,$essayurl,$responseType,'datesub', + '',$fullname,\%record,\%coursedesc_by_cid); + $output .= '
    '. + '

    '.&mt('Submissions').'

    '."\n".$collabinfo."\n"; + if (($numresp > $numessay) & !$is_tool) { + $output .='

    '. + &mt('Part(s) graded correct by the computer is marked with a [_1] symbol.',$checkIcon). + "

    \n"; + } + $output .= $partinfo; + $output .= $lastsubonly; + $output .= &displaySubByDates($symb,\%record,$partlist,$responseType,$checkIcon,$vuname,$vudom); + $output .= '
    '."\n"; + return $output; +} + sub handler { my $request=$_[0]; &reset_caches(); @@ -11585,6 +13021,44 @@ sub handler { undef,undef,undef,undef,undef,undef,undef,1); $request->print('
    '); &submit_download_link($request,$symb); + } elsif ($command eq 'initialpassback') { + &startpage($request,$symb,[{href=>'', text=>'Choose Launcher'}],undef,1); + $request->print(&initialpassback($request,$symb)); + } elsif ($command eq 'passback') { + &startpage($request,$symb, + [{href=>&href_symb_cmd($symb,'initialpassback'), text=>'Choose Launcher'}, + {href=>'', text=>'Types of User'}],undef,1); + $request->print(&passback_filters($request,$symb)); + } elsif ($command eq 'passbacknames') { + my $chosen; + if ($env{'form.passback'} ne '') { + if ($env{'form.passback'} eq &unescape($env{'form.passback'})) { + $env{'form.passback'} = &escape($env{'form.passback'} ); + } + $chosen = &HTML::Entities::encode($env{'form.passback'},'<>"&'); + } + &startpage($request,$symb, + [{href=>&href_symb_cmd($symb,'initialpassback'), text=>'Choose Launcher'}, + {href=>&href_symb_cmd($symb,'passback').'&passback='.$chosen, text=>'Types of User'}, + {href=>'', text=>'Select Users'}],undef,1); + $request->print(&names_for_passback($request,$symb)); + } elsif ($command eq 'passbackscores') { + my ($chosen,$stu_status); + if ($env{'form.passback'} ne '') { + if ($env{'form.passback'} eq &unescape($env{'form.passback'})) { + $env{'form.passback'} = &escape($env{'form.passback'} ); + } + $chosen = &HTML::Entities::encode($env{'form.passback'},'<>"&'); + } + if ($env{'form.Status'}) { + $stu_status = &HTML::Entities::encode($env{'form.Status'}); + } + &startpage($request,$symb, + [{href=>&href_symb_cmd($symb,'initialpassback'), text=>'Choose Launcher'}, + {href=>&href_symb_cmd($symb,'passback').'&passback='.$chosen, text=>'Types of User'}, + {href=>&href_symb_cmd($symb,'passbacknames').'&Status='.$stu_status.'&passback='.$chosen, text=>'Select Users'}, + {href=>'', text=>'Execute Passback'}],undef,1); + $request->print(&do_passback($request,$symb)); } elsif ($command) { &startpage($request,$symb,[{href=>'', text=>'Access denied'}]); $request->print('

    '.&mt('Access Denied ([_1])',$command).'

    '); @@ -11763,8 +13237,8 @@ Side Effects: None. - missingbubble - array ref of the bubble lines that have missing bubble errors - $randomorder - True if exam folder has randomorder set - $randompick - True if exam folder has randompick set + $randomorder - True if exam folder (or a sub-folder) has randomorder set + $randompick - True if exam folder (or a sub-folder) has randompick set $respnumlookup - Reference to HASH mapping question numbers in bubble lines for current line to question number used for same question in "Master Seqence" (as seen by Course Coordinator).