--- loncom/homework/grades.pm 2023/02/12 21:01:30 1.792 +++ loncom/homework/grades.pm 2024/12/09 02:29:37 1.797 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.792 2023/02/12 21:01:30 raeburn Exp $ +# $Id: grades.pm,v 1.797 2024/12/09 02:29:37 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); @@ -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,611 @@ sub verifyreceipt { return $string; } +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 $sigmethod = 'HMAC-SHA1'; + my $type = 'linkprot'; + my $clientip = &Apache::lonnet::get_requestor_ip(); + my $lonhost = $Apache::lonnet::perlvar{'lonHostID'}; + my $ip = &Apache::lonnet::get_host_ip($lonhost); + 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,$type,$ltinum,$keynum,$id, + $url,$scoretype,$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' => $ip, + 'host' => $lonhost, + 'protector' => $linkprotector, + 'deeplink' => $linkuri, + 'scope' => $scope, + 'url' => $url, + 'id' => $id, + 'clientip' => $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,'',$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' => $type, + 'pbscope' => $pbscope, + 'pbmap' => $pbmap, + 'pbsymb' => $pbsymb, + 'format' => $scoretype, + 'scope' => $scope, + 'clientip' => $clientip, + 'linkprot' => $linkprotector, + '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"; +} + #--- 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 +1697,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 '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[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; - $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)); @@ -2857,6 +3341,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); @@ -2953,9 +3617,14 @@ sub get_last_submission { $prevsolved{$partid} = $solved{$partid}; } } - $timestamp = - &Apache::lonlocal::locallocaltime($$returnhash{$version.':timestamp'}); } +# +# 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'}); @@ -3838,8 +4507,8 @@ sub version_portfiles { $$record{$key} = join(',',@versioned_portfiles); push(@returned_keys,$key); } - } - return (@returned_keys); + } + return (@returned_keys); } #-------------------------------------------------------------------------------------- @@ -10545,7 +11214,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 { @@ -10654,7 +11324,20 @@ sub grading_menu { ] }); - + 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 .= '
    '; @@ -11555,6 +12238,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(); @@ -11803,6 +12557,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).'

    ');